diff --git a/base/commandLine.cpp b/base/commandLine.cpp new file mode 100644 index 00000000000..3828e1da44f --- /dev/null +++ b/base/commandLine.cpp @@ -0,0 +1,518 @@ +/* Residual - Virtual machine to run LucasArts' 3D adventure games + * + * Residual is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the AUTHORS + * 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. + * + * $URL$ + * $Id$ + * + */ + +#include "engines/metaengine.h" +#include "base/commandLine.h" +#include "base/plugins.h" +#include "base/version.h" + +#include "common/config-manager.h" +#include "common/system.h" +#include "common/fs.h" + +namespace Base { + +#ifndef DISABLE_COMMAND_LINE + +static const char USAGE_STRING[] = + "%s: %s\n" + "Usage: %s [OPTIONS]... [GAME]\n" + "\n" + "Try '%s --help' for more options.\n" +; + +// DONT FIXME: DO NOT ORDER ALPHABETICALLY, THIS IS ORDERED BY IMPORTANCE/CATEGORY! :) +#if defined(PALMOS_MODE) || defined(__SYMBIAN32__) || defined(__GP32__) +static const char HELP_STRING[] = "NoUsageString"; // save more data segment space +#else +static const char HELP_STRING[] = + "Residual - Virtual machine to run 3D adventure games\n" + "Usage: %s [OPTIONS]... [GAME]\n" + " -v, --version Display Residual version information and exit\n" + " -h, --help Display a brief help text and exit\n" + " -z, --list-games Display list of supported games and exit\n" + " -t, --list-targets Display list of configured targets and exit\n" + + "\n" + " -c, --config=CONFIG Use alternate configuration file\n" + " -p, --path=PATH Path to where the game is installed\n" + " -f, --fullscreen Force full-screen mode\n" + " -q, --language=LANG Select language (en,de,fr,it,pt,es,jp,zh,kr,se,gb,\n" + " hb,ru,cz)\n" + " -m, --music-volume=NUM Set the music volume, 0-127 (default: 127)\n" + " -s, --sfx-volume=NUM Set the sfx volume, 0-127 (default: 127)\n" + " -r, --speech-volume=NUM Set the speech volume, 0-127 (default: 127)\n" + " --speech-mode=NUM Set the mode of speech 1-Text only, 2-Voice Only, 3-Voice and Text\n" + " --text-speed=NUM Set talk speed for games (default: 7)\n" + " --soft-renderer=BOOL Set the turn on/off software 3D renderer: TRUE/FALSE\n" + " -d, --debuglevel=NUM Set debug verbosity level\n" + " --debugflags=FLAGS Enables engine specific debug flags\n" + " --savepath=PATH Path to where savegames are stored\n" + " --extrapath=PATH Extra path to additional game data\n" + " --output-rate=RATE Select output sample rate in Hz (e.g. 22050)\n" + " --game-devel-mode=BOOL Set the turn on/off game engine development mode: TRUE/FALSE\n" + " --joystick[=NUM] Enable joystick input (default: 0 = first joystick)\n" + " --gamma=FLOAT Set the gamma correction\n" + " --show-fps=BOOL Set the turn on/off display FPS info: TRUE/FALSE\n" + " --engine-speed=NUM Set engine speed (default: 30)\n" + " -b, --boot-param=NUM Pass number to the boot script (boot param)\n" + "\n" +; +#endif + +static const char *s_appName = "residual"; + +static void usage(const char *s, ...) GCC_PRINTF(1, 2); + +static void usage(const char *s, ...) { + char buf[STRINGBUFLEN]; + va_list va; + + va_start(va, s); + vsnprintf(buf, STRINGBUFLEN, s, va); + va_end(va); + +#if !(defined(__GP32__) || defined (__SYMBIAN32__)) + printf(USAGE_STRING, s_appName, buf, s_appName, s_appName); +#endif + exit(1); +} + +#endif // DISABLE_COMMAND_LINE + + +void registerDefaults() { + ConfMan.registerDefault("platform", Common::kPlatformPC); + ConfMan.registerDefault("language", "en"); + ConfMan.registerDefault("autosave_period", 5 * 60); // By default, trigger autosave every 5 minutes + + ConfMan.registerDefault("music_volume", 127); + ConfMan.registerDefault("sfx_volume", 127); + ConfMan.registerDefault("voice_volume", 127); + ConfMan.registerDefault("speech_mode", "3"); + + ConfMan.registerDefault("boot_param", ""); + ConfMan.registerDefault("text_speed", "70"); + + ConfMan.registerDefault("path", "."); + + ConfMan.registerDefault("soft_renderer", "TRUE"); + ConfMan.registerDefault("fullscreen", "FALSE"); + ConfMan.registerDefault("gamma", "1.0"); + ConfMan.registerDefault("show_fps", "FALSE"); + ConfMan.registerDefault("engine_speed", "30"); + ConfMan.registerDefault("game_devel_mode", ""); + + ConfMan.registerDefault("joystick", "1"); + + ConfMan.registerDefault("disable_sdl_parachute", false); + + ConfMan.registerDefault("record_mode", "none"); + ConfMan.registerDefault("record_file_name", "record.bin"); + ConfMan.registerDefault("record_temp_file_name", "record.tmp"); + ConfMan.registerDefault("record_time_file_name", "record.time"); +} + +// +// Various macros used by the command line parser. +// + +#ifndef DISABLE_COMMAND_LINE + +// Use this for options which have an *optional* value +#define DO_OPTION_OPT(shortCmd, longCmd, defaultVal) \ + if (isLongCmd ? (!strcmp(s+2, longCmd) || !memcmp(s+2, longCmd"=", sizeof(longCmd"=") - 1)) : (tolower(s[1]) == shortCmd)) { \ + s += 2; \ + if (isLongCmd) { \ + s += sizeof(longCmd) - 1; \ + if (*s == '=') \ + s++; \ + } \ + const char *option = s; \ + if (*s == '\0' && !isLongCmd) { option = s2; i++; } \ + if (!option || *option == '\0') option = defaultVal; \ + if (option) settings[longCmd] = option; + +// Use this for options which have a required (string) value +#define DO_OPTION(shortCmd, longCmd) \ + DO_OPTION_OPT(shortCmd, longCmd, 0) \ + if (!option) usage("Option '%s' requires an argument", argv[isLongCmd ? i : i-1]); + +// Use this for options which have a required integer value +#define DO_OPTION_INT(shortCmd, longCmd) \ + DO_OPTION(shortCmd, longCmd) \ + char *endptr = 0; \ + int intValue; intValue = (int)strtol(option, &endptr, 0); \ + if (endptr == NULL || *endptr != 0) usage("--%s: Invalid number '%s'", longCmd, option); + +// Use this for boolean options; this distinguishes between "-x" and "-X", +// resp. between "--some-option" and "--no-some-option". +#define DO_OPTION_BOOL(shortCmd, longCmd) \ + if (isLongCmd ? (!strcmp(s+2, longCmd) || !strcmp(s+2, "no-"longCmd)) : (tolower(s[1]) == shortCmd)) { \ + bool boolValue = (islower(s[1]) != 0); \ + s += 2; \ + if (isLongCmd) { \ + boolValue = !strcmp(s, longCmd); \ + s += boolValue ? (sizeof(longCmd) - 1) : (sizeof("no-"longCmd) - 1); \ + } \ + if (*s != '\0') goto unknownOption; \ + const char *option = boolValue ? "true" : "false"; \ + settings[longCmd] = option; + +// Use this for options which never have a value, i.e. for 'commands', like "--help". +#define DO_COMMAND(shortCmd, longCmd) \ + if (isLongCmd ? (!strcmp(s+2, longCmd)) : (tolower(s[1]) == shortCmd)) { \ + s += 2; \ + if (isLongCmd) \ + s += sizeof(longCmd) - 1; \ + if (*s != '\0') goto unknownOption; \ + return longCmd; + + +#define DO_LONG_OPTION_OPT(longCmd, d) DO_OPTION_OPT(0, longCmd, d) +#define DO_LONG_OPTION(longCmd) DO_OPTION(0, longCmd) +#define DO_LONG_OPTION_INT(longCmd) DO_OPTION_INT(0, longCmd) +#define DO_LONG_OPTION_BOOL(longCmd) DO_OPTION_BOOL(0, longCmd) +#define DO_LONG_COMMAND(longCmd) DO_COMMAND(0, longCmd) + +// End an option handler +#define END_OPTION \ + continue; \ + } + + +Common::String parseCommandLine(Common::StringMap &settings, int argc, const char * const *argv) { + const char *s, *s2; + + // argv[0] contains the name of the executable. + if (argv && argv[0]) { + s = strrchr(argv[0], '/'); + s_appName = s ? (s+1) : argv[0]; + } + + // We store all command line settings into a string map. + + // Iterate over all command line arguments and parse them into our string map. + for (int i = 1; i < argc; ++i) { + s = argv[i]; + s2 = (i < argc-1) ? argv[i+1] : 0; + + if (s[0] != '-') { + // The argument doesn't start with a dash, so it's not an option. + // Hence it must be the target name. We currently enforce that + // this always comes last. + if (i != argc - 1) + usage("Stray argument '%s'", s); + + // We defer checking whether this is a valid target to a later point. + return s; + } else { + + bool isLongCmd = (s[0] == '-' && s[1] == '-'); + + DO_COMMAND('h', "help") + END_OPTION + + DO_COMMAND('v', "version") + END_OPTION + + DO_COMMAND('t', "list-targets") + END_OPTION + + DO_COMMAND('z', "list-games") + END_OPTION + + DO_OPTION('c', "config") + END_OPTION + + DO_OPTION_INT('b', "boot-param") + END_OPTION + + DO_OPTION_OPT('d', "debuglevel", "0") + END_OPTION + + DO_LONG_OPTION("debugflags") + END_OPTION + + DO_LONG_OPTION_INT("output-rate") + END_OPTION + + DO_OPTION_BOOL('f', "fullscreen") + END_OPTION + + DO_OPTION_INT('m', "music-volume") + END_OPTION + + DO_OPTION('s', "sfx-volume") + END_OPTION + + DO_OPTION('r', "voice-volume") + END_OPTION + + DO_OPTION('q', "language") + if (Common::parseLanguage(option) == Common::UNK_LANG) + usage("Unrecognized language '%s'", option); + END_OPTION + + DO_LONG_OPTION_OPT("joystick", "0") + settings["joystick_num"] = option; + settings.erase("joystick"); + END_OPTION + + DO_LONG_OPTION("show-fps") + END_OPTION + + DO_LONG_OPTION("soft-renderer") + END_OPTION + + DO_LONG_OPTION("engine-speed") + END_OPTION + + DO_LONG_OPTION("manny-state") + END_OPTION + + DO_LONG_OPTION("movement") + END_OPTION + + DO_LONG_OPTION("transcript") + END_OPTION + + DO_LONG_OPTION("game-devel-mode") + END_OPTION + + DO_LONG_OPTION("gamma") + END_OPTION + + DO_OPTION('p', "path") + Common::FSNode path(option); + if (!path.exists()) { + usage("Non-existent game path '%s'", option); + } else if (!path.isReadable()) { + usage("Non-readable game path '%s'", option); + } + END_OPTION + + DO_LONG_OPTION_BOOL("disable-sdl-parachute") + END_OPTION + + DO_LONG_OPTION("savepath") + Common::FSNode path(option); + if (!path.exists()) { + usage("Non-existent savegames path '%s'", option); + } else if (!path.isWritable()) { + usage("Non-writable savegames path '%s'", option); + } + END_OPTION + + DO_LONG_OPTION("extrapath") + Common::FSNode path(option); + if (!path.exists()) { + usage("Non-existent extra path '%s'", option); + } else if (!path.isReadable()) { + usage("Non-readable extra path '%s'", option); + } + END_OPTION + + DO_LONG_OPTION("text-speed") + END_OPTION + + DO_LONG_OPTION("speech-mode") + END_OPTION + + DO_LONG_OPTION("record-mode") + END_OPTION + + DO_LONG_OPTION("record-file-name") + END_OPTION + + DO_LONG_OPTION("record-temp-file-name") + END_OPTION + + DO_LONG_OPTION("record-time-file-name") + END_OPTION + +#ifdef IPHONE + // This is automatically set when launched from the Springboard. + DO_LONG_OPTION_OPT("launchedFromSB", 0) + END_OPTION +#endif + +unknownOption: + // If we get till here, the option is unhandled and hence unknown. + usage("Unrecognized option '%s'", argv[i]); + } + } + + return Common::String(); +} + +/** List all supported game IDs, i.e. all games which any loaded plugin supports. */ +static void listGames() { + printf("Game ID Full Title \n" + "-------------------- ------------------------------------------------------\n"); + + const EnginePlugin::List &plugins = EngineMan.getPlugins(); + EnginePlugin::List::const_iterator iter = plugins.begin(); + for (iter = plugins.begin(); iter != plugins.end(); ++iter) { + GameList list = (**iter)->getSupportedGames(); + for (GameList::iterator v = list.begin(); v != list.end(); ++v) { + printf("%-20s %s\n", v->gameid().c_str(), v->description().c_str()); + } + } +} + +/** List all targets which are configured in the config file. */ +static void listTargets() { + printf("Target Description \n" + "-------------------- ------------------------------------------------------\n"); + + using namespace Common; + const ConfigManager::DomainMap &domains = ConfMan.getGameDomains(); + ConfigManager::DomainMap::const_iterator iter; + for (iter = domains.begin(); iter != domains.end(); ++iter) { + Common::String name(iter->_key); + Common::String description(iter->_value.get("description")); + + if (description.empty()) { + // FIXME: At this point, we should check for a "gameid" override + // to find the proper desc. In fact, the platform probably should + // be taken into account, too. + Common::String gameid(name); + GameDescriptor g = EngineMan.findGame(gameid); + if (g.description().size() > 0) + description = g.description(); + } + + printf("%-20s %s\n", name.c_str(), description.c_str()); + + } +} + + +#else // DISABLE_COMMAND_LINE + + +Common::String parseCommandLine(Common::StringMap &settings, int argc, const char * const *argv) { + return Common::String(); +} + + +#endif // DISABLE_COMMAND_LINE + + +bool processSettings(Common::String &command, Common::StringMap &settings) { + +#ifndef DISABLE_COMMAND_LINE + + // Handle commands passed via the command line (like --list-targets and + // --list-games). This must be done after the config file and the plugins + // have been loaded. + if (command == "list-targets") { + listTargets(); + return false; + } else if (command == "list-games") { + listGames(); + return false; + } else if (command == "version") { + printf("%s\n", gResidualFullVersion); + printf("Features compiled in: %s\n", gResidualFeatures); + return false; + } else if (command == "help") { + printf(HELP_STRING, s_appName); + return false; + } + +#endif // DISABLE_COMMAND_LINE + + + // If a target was specified, check whether there is either a game + // domain (i.e. a target) matching this argument, or alternatively + // whether there is a gameid matching that name. + if (!command.empty()) { + GameDescriptor gd = EngineMan.findGame(command); + if (ConfMan.hasGameDomain(command) || !gd.gameid().empty()) { + bool idCameFromCommandLine = false; + + // WORKAROUND: Fix for bug #1719463: "DETECTOR: Launching + // undefined target adds launcher entry" + // + // We designate gameids which come strictly from command line + // so AdvancedDetector will not save config file with invalid + // gameid in case target autoupgrade was performed + if (!ConfMan.hasGameDomain(command)) { + idCameFromCommandLine = true; + } + + ConfMan.setActiveDomain(command); + + if (idCameFromCommandLine) + ConfMan.set("id_came_from_command_line", "1"); + + } else { +#ifndef DISABLE_COMMAND_LINE + usage("Unrecognized game target '%s'", command.c_str()); +#endif // DISABLE_COMMAND_LINE + } + } + + + // The user can override the savepath with the SCUMMVM_SAVEPATH + // environment variable. This is weaker than a --savepath on the + // command line, but overrides the default savepath, hence it is + // handled here, just before the command line gets parsed. +#if !defined(MACOS_CARBON) && !defined(_WIN32_WCE) && !defined(PALMOS_MODE) && !defined(__GP32__) + if (!settings.contains("savepath")) { + const char *dir = getenv("RESIDUAL_SAVEPATH"); + if (dir && *dir && strlen(dir) < MAXPATHLEN) { + Common::FSNode saveDir(dir); + if (!saveDir.exists()) { + warning("Non-existent RESIDUAL_SAVEPATH save path. It will be ignored."); + } else if (!saveDir.isWritable()) { + warning("Non-writable RESIDUAL_SAVEPATH save path. It will be ignored."); + } else { + settings["savepath"] = dir; + } + } + } +#endif + + // Finally, store the command line settings into the config manager. + for (Common::StringMap::const_iterator x = settings.begin(); x != settings.end(); ++x) { + Common::String key(x->_key); + Common::String value(x->_value); + + // Replace any "-" in the key by "_" (e.g. change "save-slot" to "save_slot"). + for (Common::String::iterator c = key.begin(); c != key.end(); ++c) + if (*c == '-') + *c = '_'; + + // Store it into ConfMan. + ConfMan.set(key, value, Common::ConfigManager::kTransientDomain); + } + + return true; +} + +} // End of namespace Base diff --git a/base/commandLine.h b/base/commandLine.h new file mode 100644 index 00000000000..b50d9d47efd --- /dev/null +++ b/base/commandLine.h @@ -0,0 +1,40 @@ +/* Residual - Virtual machine to run LucasArts' 3D adventure games + * + * Residual is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the AUTHORS + * 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. + * + * $URL$ + * $Id$ + * + */ + +#ifndef BASE_OPTIONS_H +#define BASE_OPTIONS_H + +#include "common/str.h" +#include "common/config-manager.h" + +namespace Base { + +void registerDefaults(); +Common::String parseCommandLine(Common::StringMap &settings, int argc, const char * const *argv); +bool processSettings(Common::String &command, Common::StringMap &settings); + +} // End of namespace Base + +#endif diff --git a/base/internal_version.h b/base/internal_version.h new file mode 100644 index 00000000000..65ea4a696a8 --- /dev/null +++ b/base/internal_version.h @@ -0,0 +1 @@ +#define RESIDUAL_VERSION "0.0.6svn" diff --git a/base/internal_version.h.in b/base/internal_version.h.in new file mode 100644 index 00000000000..7abe155cc0f --- /dev/null +++ b/base/internal_version.h.in @@ -0,0 +1 @@ +#define RESIDUAL_VERSION "@VERSION@" diff --git a/base/main.cpp b/base/main.cpp new file mode 100644 index 00000000000..192e2589c0d --- /dev/null +++ b/base/main.cpp @@ -0,0 +1,275 @@ +/* Residual - Virtual machine to run LucasArts' 3D adventure games + * + * Residual is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the AUTHORS + * 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. + * + * $URL$ + * $Id$ + * + */ + +/*! \mainpage %Residual Source Reference + * + * These pages contains a cross referenced documentation for the %Residual source code, + * generated with Doxygen (http://www.doxygen.org) directly from the source. + * Currently not much is actually properly documented, but at least you can get an overview + * of almost all the classes, methods and variables, and how they interact. + */ + +#include "engines/engine.h" +#include "engines/metaengine.h" +#include "base/commandLine.h" +#include "base/plugins.h" +#include "base/version.h" + +#include "common/archive.h" +#include "common/config-manager.h" +#include "common/debug.h" +#include "common/events.h" +#include "common/file.h" +#include "common/fs.h" +#include "common/system.h" + + +static const EnginePlugin *detectPlugin() { + const EnginePlugin *plugin = 0; + + // Make sure the gameid is set in the config manager, and that it is lowercase. + Common::String gameid(ConfMan.getActiveDomainName()); + assert(!gameid.empty()); + if (ConfMan.hasKey("gameid")) + gameid = ConfMan.get("gameid"); + gameid.toLowercase(); + ConfMan.set("gameid", gameid); + + // Query the plugins and find one that will handle the specified gameid + printf("User picked target '%s' (gameid '%s')...\n", ConfMan.getActiveDomainName().c_str(), gameid.c_str()); + printf(" Looking for a plugin supporting this gameid... "); + GameDescriptor game = EngineMan.findGame(gameid, &plugin); + + if (plugin == 0) { + printf("failed\n"); + warning("%s is an invalid gameid. Use the --list-games option to list supported gameid", gameid.c_str()); + return 0; + } else { + printf("%s\n", plugin->getName()); + } + + // FIXME: Do we really need this one? + printf(" Starting '%s'\n", game.description().c_str()); + + return plugin; +} + +// TODO: specify the possible return values here +static Common::Error runGame(const EnginePlugin *plugin, OSystem &system, const Common::String &edebuglevels) { + // Determine the game data path, for validation and error messages + Common::FSNode dir(ConfMan.get("path")); + Common::Error err = Common::kNoError; + Engine *engine = 0; + + // Verify that the game path refers to an actual directory + if (!(dir.exists() && dir.isDirectory())) + err = Common::kInvalidPathError; + + // Create the game engine + if (err == Common::kNoError) + err = (*plugin)->createInstance(&system, &engine); + + // Check for errors + if (!engine || err != Common::kNoError) { + const char *errMsg = 0; + switch (err) { + case Common::kInvalidPathError: + errMsg = "Invalid game path"; + break; + case Common::kNoGameDataFoundError: + errMsg = "Unable to locate game data"; + break; + default: + errMsg = "Unknown error"; + } + + warning("%s failed to instantiate engine: %s (target '%s', path '%s')", + plugin->getName(), + errMsg, + ConfMan.getActiveDomainName().c_str(), + dir.getPath().c_str() + ); + return err; + } + + // Set the window caption to the game name + Common::String caption(ConfMan.get("description")); + + Common::String desc = EngineMan.findGame(ConfMan.get("gameid")).description(); + if (caption.empty() && !desc.empty()) + caption = desc; + if (caption.empty()) + caption = ConfMan.getActiveDomainName(); // Use the domain (=target) name + if (!caption.empty()) { + system.setWindowCaption(caption.c_str()); + } + + // + // Setup various paths in the SearchManager + // + + // Add the game path to the directory search list + SearchMan.addDirectory(dir.getPath(), dir, 0, 4); + + // Add extrapath (if any) to the directory search list + if (ConfMan.hasKey("extrapath")) { + dir = Common::FSNode(ConfMan.get("extrapath")); + SearchMan.addDirectory(dir.getPath(), dir); + } + + // If a second extrapath is specified on the app domain level, add that as well. + if (ConfMan.hasKey("extrapath", Common::ConfigManager::kApplicationDomain)) { + dir = Common::FSNode(ConfMan.get("extrapath", Common::ConfigManager::kApplicationDomain)); + SearchMan.addDirectory(dir.getPath(), dir); + } + + // On creation the engine should have set up all debug levels so we can use + // the command line arugments here + Common::StringTokenizer tokenizer(edebuglevels, " ,"); + while (!tokenizer.empty()) { + Common::String token = tokenizer.nextToken(); +// if (!enableDebugChannel(token)) +// warning("Engine does not support debug level '%s'", token.c_str()); + } + + // Inform backend that the engine is about to be run + system.engineInit(); + + // Run the engine + Common::Error result = engine->run(); + + // Inform backend that the engine finished + system.engineDone(); + + // Free up memory + delete engine; + + // Reset the file/directory mappings + SearchMan.clear(); + + // Return result (== 0 means no error) + return result; +} + +extern "C" int residual_main(int argc, const char * const argv[]) { + Common::String specialDebug; + Common::String command; + + // Verify that the backend has been initialized (i.e. g_system has been set). + assert(g_system); + OSystem &system = *g_system; + + // Register config manager defaults + Base::registerDefaults(); + + // Parse the command line + Common::StringMap settings; + command = Base::parseCommandLine(settings, argc, argv); + + // Load the config file (possibly overriden via command line): + if (settings.contains("config")) { + ConfMan.loadConfigFile(settings["config"]); + settings.erase("config"); + } else { + ConfMan.loadDefaultConfigFile(); + } + + // Update the config file + ConfMan.set("versioninfo", gResidualVersion, Common::ConfigManager::kApplicationDomain); + + + // Load and setup the debuglevel and the debug flags. We do this at the + // soonest possible moment to ensure debug output starts early on, if + // requested. + if (settings.contains("debuglevel")) { + gDebugLevel = (enDebugLevels)strtol(settings["debuglevel"].c_str(), 0, 10); + printf("Debuglevel (from command line): %d\n", gDebugLevel); + settings.erase("debuglevel"); // This option should not be passed to ConfMan. + } else if (ConfMan.hasKey("debuglevel")) + gDebugLevel = (enDebugLevels)ConfMan.getInt("debuglevel"); + + if (settings.contains("debugflags")) { + specialDebug = settings["debugflags"]; + settings.erase("debugflags"); + } + + // Load the plugins. + PluginManager::instance().loadPlugins(); + + // Process the remaining command line settings. Must be done after the + // config file and the plugins have been loaded. + if (!Base::processSettings(command, settings)) + return 0; + + // Init the backend. Must take place after all config data (including + // the command line params) was read. + system.initBackend(); + + // Init the event manager. As the virtual keyboard is loaded here, it must + // take place after the backend is initiated and the screen has been setup + system.getEventManager()->init(); + + // Try to find a plugin which feels responsible for the specified game. + const EnginePlugin *plugin = detectPlugin(); + if (plugin) { + // Unload all plugins not needed for this game, + // to save memory + PluginManager::instance().unloadPluginsExcept(PLUGIN_TYPE_ENGINE, plugin); + + // Try to run the game + Common::Error result = runGame(plugin, system, specialDebug); + + // Did an error occur ? + if (result != Common::kNoError) { + // TODO: Show an informative error dialog if starting the selected game failed. + } + + // Quit unless an error occurred + if (result == 0) + goto exit; + + // Discard any command line options. It's unlikely that the user + // wanted to apply them to *all* games ever launched. + ConfMan.getDomain(Common::ConfigManager::kTransientDomain)->clear(); + + // Clear the active config domain + ConfMan.setActiveDomain(""); + + // PluginManager::instance().unloadPlugins(); + PluginManager::instance().loadPlugins(); + } else { + // A dialog would be nicer, but we don't have any + // screen to draw on yet. + warning("Could not find any engine capable of running the selected game"); + } + +exit: + PluginManager::instance().unloadPlugins(); + PluginManager::destroy(); + Common::ConfigManager::destroy(); + Common::SearchManager::destroy(); + + return 0; +} diff --git a/base/main.h b/base/main.h new file mode 100644 index 00000000000..d709e09aeb6 --- /dev/null +++ b/base/main.h @@ -0,0 +1,33 @@ +/* Residual - Virtual machine to run LucasArts' 3D adventure games + * + * Residual is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the AUTHORS + * 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. + * + * $URL$ + * $Id$ + * + */ + +#ifndef BASE_MAIN_H +#define BASE_MAIN_H + +#include "common/sys.h" + +extern "C" int residual_main(int argc, const char *const argv[]); + +#endif diff --git a/base/module.mk b/base/module.mk new file mode 100644 index 00000000000..f12a710920b --- /dev/null +++ b/base/module.mk @@ -0,0 +1,10 @@ +MODULE := base + +MODULE_OBJS := \ + main.o \ + commandLine.o \ + plugins.o \ + version.o + +# Include common rules +include $(srcdir)/rules.mk diff --git a/base/plugins.cpp b/base/plugins.cpp new file mode 100644 index 00000000000..b391e67686b --- /dev/null +++ b/base/plugins.cpp @@ -0,0 +1,299 @@ +/* Residual - Virtual machine to run LucasArts' 3D adventure games + * + * Residual is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the AUTHORS + * 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. + * + * $URL$ + * $Id$ + * + */ + +#include "base/plugins.h" + +#include "common/func.h" +#include "common/debug.h" + +#ifdef DYNAMIC_MODULES +#include "common/config-manager.h" +#include "common/fs.h" +#endif + +// Plugin versioning + +int pluginTypeVersions[PLUGIN_TYPE_MAX] = { + PLUGIN_TYPE_ENGINE_VERSION, +}; + + +// Abstract plugins + +PluginType Plugin::getType() const { + return _type; +} + +const char *Plugin::getName() const { + return _pluginObject->getName(); +} + +class StaticPlugin : public Plugin { +public: + StaticPlugin(PluginObject *pluginobject, PluginType type) { + assert(pluginobject); + assert(type < PLUGIN_TYPE_MAX); + _pluginObject = pluginobject; + _type = type; + } + + ~StaticPlugin() { + delete _pluginObject; + } + + virtual bool loadPlugin() { return true; } + virtual void unloadPlugin() {} +}; + +class StaticPluginProvider : public PluginProvider { +public: + StaticPluginProvider() { + } + + ~StaticPluginProvider() { + } + + virtual PluginList getPlugins() { + PluginList pl; + + #define LINK_PLUGIN(ID) \ + extern PluginType g_##ID##_type; \ + extern PluginObject *g_##ID##_getObject(); \ + pl.push_back(new StaticPlugin(g_##ID##_getObject(), g_##ID##_type)); + + // "Loader" for the static plugins. + // Iterate over all registered (static) plugins and load them. + + // Engine plugins + #if PLUGIN_ENABLED_STATIC(GRIM) + LINK_PLUGIN(GRIM) + #endif + + return pl; + } +}; + +#ifdef DYNAMIC_MODULES + +PluginList FilePluginProvider::getPlugins() { + PluginList pl; + + // Prepare the list of directories to search + Common::FSList pluginDirs; + + // Add the default directories + pluginDirs.push_back(Common::FSNode(".")); + pluginDirs.push_back(Common::FSNode("plugins")); + + // Add the provider's custom directories + addCustomDirectories(pluginDirs); + + // Add the user specified directory + Common::String pluginsPath(ConfMan.get("pluginspath")); + if (!pluginsPath.empty()) + pluginDirs.push_back(Common::FSNode(pluginsPath)); + + Common::FSList::const_iterator dir; + for (dir = pluginDirs.begin(); dir != pluginDirs.end(); dir++) { + // Load all plugins. + // Scan for all plugins in this directory + Common::FSList files; + if (!dir->getChildren(files, Common::FSNode::kListFilesOnly)) { + debug(1, "Couldn't open plugin directory '%s'", dir->getPath().c_str()); + continue; + } else { + debug(1, "Reading plugins from plugin directory '%s'", dir->getPath().c_str()); + } + + for (Common::FSList::const_iterator i = files.begin(); i != files.end(); ++i) { + if (isPluginFilename(*i)) { + pl.push_back(createPlugin(*i)); + } + } + } + + return pl; +} + +bool FilePluginProvider::isPluginFilename(const Common::FSNode &node) const { + Common::String filename = node.getName(); + +#ifdef PLUGIN_PREFIX + // Check the plugin prefix + if (!filename.hasPrefix(PLUGIN_PREFIX)) + return false; +#endif + +#ifdef PLUGIN_SUFFIX + // Check the plugin suffix + if (!filename.hasSuffix(PLUGIN_SUFFIX)) + return false; +#endif + + return true; +} + +void FilePluginProvider::addCustomDirectories(Common::FSList &dirs) const { +#ifdef PLUGIN_DIRECTORY + dirs.push_back(Common::FSNode(PLUGIN_DIRECTORY)); +#endif +} + +#endif // DYNAMIC_MODULES + +#pragma mark - + +DECLARE_SINGLETON(PluginManager); + +PluginManager::PluginManager() { + // Always add the static plugin provider. + addPluginProvider(new StaticPluginProvider()); +} + +PluginManager::~PluginManager() { + // Explicitly unload all loaded plugins + unloadPlugins(); + + // Delete the plugin providers + for (ProviderList::iterator pp = _providers.begin(); + pp != _providers.end(); + ++pp) { + delete *pp; + } +} + +void PluginManager::addPluginProvider(PluginProvider *pp) { + _providers.push_back(pp); +} + +void PluginManager::loadPlugins() { + for (ProviderList::iterator pp = _providers.begin(); + pp != _providers.end(); + ++pp) { + PluginList pl((*pp)->getPlugins()); + Common::for_each(pl.begin(), pl.end(), Common::bind1st(Common::mem_fun(&PluginManager::tryLoadPlugin), this)); + } + +} + +void PluginManager::unloadPlugins() { + for (int i = 0; i < PLUGIN_TYPE_MAX; i++) + unloadPluginsExcept((PluginType)i, NULL); +} + +void PluginManager::unloadPluginsExcept(PluginType type, const Plugin *plugin) { + Plugin *found = NULL; + for (PluginList::iterator p = _plugins[type].begin(); p != _plugins[type].end(); ++p) { + if (*p == plugin) { + found = *p; + } else { + (*p)->unloadPlugin(); + delete *p; + } + } + _plugins[type].clear(); + if (found != NULL) { + _plugins[type].push_back(found); + } +} + +bool PluginManager::tryLoadPlugin(Plugin *plugin) { + assert(plugin); + // Try to load the plugin + if (plugin->loadPlugin()) { + // The plugin is valid, see if it provides the same module as an + // already loaded one and should replace it. + bool found = false; + + PluginList::iterator pl = _plugins[plugin->getType()].begin(); + while (!found && pl != _plugins[plugin->getType()].end()) { + if (!strcmp(plugin->getName(), (*pl)->getName())) { + // Found a duplicated module. Replace the old one. + found = true; + delete *pl; + *pl = plugin; + debug(1, "Replaced the duplicated plugin: '%s'", plugin->getName()); + } + pl++; + } + + if (!found) { + // If it provides a new module, just add it to the list of known plugins. + _plugins[plugin->getType()].push_back(plugin); + } + + return true; + } else { + // Failed to load the plugin + delete plugin; + return false; + } +} + + +// Engine plugins + +#include "engines/metaengine.h" + +DECLARE_SINGLETON(EngineManager); + +GameDescriptor EngineManager::findGame(const Common::String &gameName, const EnginePlugin **plugin) const { + // Find the GameDescriptor for this target + const EnginePlugin::List &plugins = getPlugins(); + GameDescriptor result; + + if (plugin) + *plugin = 0; + + EnginePlugin::List::const_iterator iter = plugins.begin(); + for (iter = plugins.begin(); iter != plugins.end(); ++iter) { + result = (**iter)->findGame(gameName.c_str()); + if (!result.gameid().empty()) { + if (plugin) + *plugin = *iter; + break; + } + } + return result; +} + +GameList EngineManager::detectGames(const Common::FSList &fslist) const { + GameList candidates; + + const EnginePlugin::List &plugins = getPlugins(); + + // Iterate over all known games and for each check if it might be + // the game in the presented directory. + EnginePlugin::List::const_iterator iter; + for (iter = plugins.begin(); iter != plugins.end(); ++iter) { + candidates.push_back((**iter)->detectGames(fslist)); + } + + return candidates; +} + +const EnginePlugin::List &EngineManager::getPlugins() const { + return (const EnginePlugin::List &)PluginManager::instance().getPlugins(PLUGIN_TYPE_ENGINE); +} diff --git a/base/plugins.h b/base/plugins.h new file mode 100644 index 00000000000..855c70521a8 --- /dev/null +++ b/base/plugins.h @@ -0,0 +1,292 @@ +/* Residual - Virtual machine to run LucasArts' 3D adventure games + * + * Residual is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the AUTHORS + * 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. + * + * $URL$ + * $Id$ + * + */ + +#ifndef BASE_PLUGINS_H +#define BASE_PLUGINS_H + +#include "common/error.h" +#include "common/singleton.h" +#include "common/util.h" + +namespace Common { + class FSList; + class FSNode; +} + + +/** + * @page pagePlugins An overview of the ScummVM plugin system + * This is a brief overview of how plugins (dynamically loadable code modules) + * work in ScummVM. We will explain how to write plugins, how they work internally, + * and sketch how porters can add support for them in their ports. + * + * \section secPluginImpl Implementing a plugin + * TODO + * + * \section secPluginUse Using plugins + * TODO + * + * \section secPluginInternals How plugins work internally + * TODO + * + * \section secPluginBackend How to add support for dynamic plugins to a port + * TODO + */ + + + +// Plugin versioning + +/** Global Plugin API version */ +#define PLUGIN_VERSION 1 + +enum PluginType { + PLUGIN_TYPE_ENGINE = 0, + + PLUGIN_TYPE_MAX +}; + +// TODO: Make the engine API version depend on ScummVM's version +// because of the backlinking (posibly from the SVN revision) +#define PLUGIN_TYPE_ENGINE_VERSION 1 + +extern int pluginTypeVersions[PLUGIN_TYPE_MAX]; + + +// Plugin linking + +#define STATIC_PLUGIN 1 +#define DYNAMIC_PLUGIN 2 + +#define PLUGIN_ENABLED_STATIC(ID) \ + (ENABLE_##ID && !PLUGIN_ENABLED_DYNAMIC(ID)) + +#define PLUGIN_ENABLED_DYNAMIC(ID) \ + (ENABLE_##ID && (ENABLE_##ID == DYNAMIC_PLUGIN) && DYNAMIC_MODULES) + +/** + * REGISTER_PLUGIN_STATIC is a convenience macro which is used to declare + * the plugin interface for static plugins. Code (such as game engines) + * which needs to implement a static plugin can simply invoke this macro + * with a plugin ID, plugin type and PluginObject subclass, and the correct + * wrapper code will be inserted. + * + * @see REGISTER_PLUGIN_DYNAMIC + */ +#define REGISTER_PLUGIN_STATIC(ID,TYPE,PLUGINCLASS) \ + PluginType g_##ID##_type = TYPE; \ + PluginObject *g_##ID##_getObject() { \ + return new PLUGINCLASS(); \ + } \ + void dummyFuncToAllowTrailingSemicolon() + +#ifdef DYNAMIC_MODULES + +/** + * REGISTER_PLUGIN_DYNAMIC is a convenience macro which is used to declare + * the plugin interface for dynamically loadable plugins. Code (such as game engines) + * which needs to implement a dynamic plugin can simply invoke this macro + * with a plugin ID, plugin type and PluginObject subclass, and the correct + * wrapper code will be inserted. + * + * @see REGISTER_PLUGIN_STATIC + */ +#define REGISTER_PLUGIN_DYNAMIC(ID,TYPE,PLUGINCLASS) \ + extern "C" { \ + PLUGIN_EXPORT int32 PLUGIN_getVersion() { return PLUGIN_VERSION; } \ + PLUGIN_EXPORT int32 PLUGIN_getType() { return TYPE; } \ + PLUGIN_EXPORT int32 PLUGIN_getTypeVersion() { return TYPE##_VERSION; } \ + PLUGIN_EXPORT PluginObject *PLUGIN_getObject() { \ + return new PLUGINCLASS(); \ + } \ + } \ + void dummyFuncToAllowTrailingSemicolon() + +#endif // DYNAMIC_MODULES + + +// Abstract plugins + +/** + * Abstract base class for the plugin objects which handle plugins + * instantiation. Subclasses for this may be used for engine plugins + * and other types of plugins. + * + * FIXME: This class needs better documentation, esp. how it differs from class Plugin + */ +class PluginObject { +public: + virtual ~PluginObject() {} + + /** Returns the name of the plugin. */ + virtual const char *getName() const = 0; +}; + +/** + * Abstract base class for the plugin system. + * Subclasses for this can be used to wrap both static and dynamic + * plugins. + * + * FIXME: This class needs better documentation, esp. how it differs from class PluginObject + */ +class Plugin { +protected: + PluginObject *_pluginObject; + PluginType _type; + +public: + Plugin() : _pluginObject(0) {} + virtual ~Plugin() { + //if (isLoaded()) + //unloadPlugin(); + } + +// virtual bool isLoaded() const = 0; // TODO + virtual bool loadPlugin() = 0; // TODO: Rename to load() ? + virtual void unloadPlugin() = 0; // TODO: Rename to unload() ? + + PluginType getType() const; + const char *getName() const; +}; + +/** List of Plugin instances. */ +typedef Common::Array PluginList; + +/** + * Convenience template to make it easier defining normal Plugin + * subclasses. Namely, the PluginSubclass will manage PluginObjects + * of a type specified via the PO_t template parameter. + */ +template +class PluginSubclass : public Plugin { +public: + PO_t *operator->() const { + return (PO_t *)_pluginObject; + } + + typedef Common::Array List; +}; + +/** + * Abstract base class for Plugin factories. Subclasses of this + * are responsible for creating plugin objects, e.g. by loading + * loadable modules from storage media; by creating "fake" plugins + * from static code; or whatever other means. + */ +class PluginProvider { +public: + virtual ~PluginProvider() {} + + /** + * Return a list of Plugin objects. The caller is responsible for actually + * loading/unloading them (by invoking the appropriate Plugin methods). + * Furthermore, the caller is responsible for deleting these objects + * eventually. + * + * @return a list of Plugin instances + */ + virtual PluginList getPlugins() = 0; +}; + +#ifdef DYNAMIC_MODULES + +/** + * Abstract base class for Plugin factories which load binary code from files. + * Subclasses only have to implement the createPlugin() method, and optionally + * can overload the other protected methods to achieve custom behavior. + */ +class FilePluginProvider : public PluginProvider { +public: + /** + * Return a list of Plugin objects loaded via createPlugin from disk. + * For this, a list of directories is searched for plugin objects: + * The current dir and its "plugins" subdirectory (if present), a list + * of custom search dirs (see addCustomDirectories) and finally the + * directory specified via the "pluginspath" config variable (if any). + * + * @return a list of Plugin instances + */ + virtual PluginList getPlugins(); + +protected: + /** + * Create a Plugin instance from a loadable code module with the specified name. + * Subclasses of FilePluginProvider have to at least overload this method. + * If the file is not found, or does not contain loadable code, 0 is returned instead. + * + * @param node the FSNode of the loadable code module + * @return a pointer to a Plugin instance, or 0 if an error occurred. + */ + virtual Plugin *createPlugin(const Common::FSNode &node) const = 0; + + /** + * Check if the supplied file corresponds to a loadable plugin file in + * the current platform. Usually, this will just check the file name. + * + * @param node the FSNode of the file to check + * @return true if the filename corresponds to a plugin, false otherwise + */ + virtual bool isPluginFilename(const Common::FSNode &node) const; + + /** + * Optionally add to the list of directories to be searched for + * plugins by getPlugins(). + * + * @param dirs the reference to the list of directories to be used when + * searching for plugins. + */ + virtual void addCustomDirectories(Common::FSList &dirs) const; +}; + +#endif // DYNAMIC_MODULES + +/** + * Singleton class which manages all plugins, including loading them, + * managing all Plugin class instances, and unloading them. + */ +class PluginManager : public Common::Singleton { + typedef Common::Array ProviderList; +private: + PluginList _plugins[PLUGIN_TYPE_MAX]; + ProviderList _providers; + + bool tryLoadPlugin(Plugin *plugin); + + friend class Common::Singleton; + PluginManager(); + +public: + ~PluginManager(); + + void addPluginProvider(PluginProvider *pp); + + void loadPlugins(); + void unloadPlugins(); + void unloadPluginsExcept(PluginType type, const Plugin *plugin); + + const PluginList &getPlugins(PluginType t) { return _plugins[t]; } +}; + +#endif diff --git a/base/version.cpp b/base/version.cpp new file mode 100644 index 00000000000..9e3ea91630d --- /dev/null +++ b/base/version.cpp @@ -0,0 +1,49 @@ +/* Residual - Virtual machine to run LucasArts' 3D adventure games + * + * Residual is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the AUTHORS + * file distributed with this source distribution. + + * This library 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 + * Lesser General Public License for more details. + + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + * + * $URL$ + * $Id$ + * + */ + +#include "base/internal_version.h" +#include "base/version.h" + +const char *gResidualVersion = RESIDUAL_VERSION; +const char *gResidualBuildDate = __DATE__ " " __TIME__; +const char *gResidualVersionDate = RESIDUAL_VERSION " (" __DATE__ " " __TIME__ ")"; +const char *gResidualFullVersion = "Residual " RESIDUAL_VERSION " (" __DATE__ " " __TIME__ ")"; +const char *gResidualFeatures = "" +#ifdef USE_TREMOR + "Tremor " +#else +#ifdef USE_VORBIS + "Vorbis " +#endif +#endif + +#ifdef USE_FLAC + "FLAC " +#endif + +#ifdef USE_MAD + "MP3 " +#endif + +#ifdef USE_ZLIB + "zLib " +#endif + ; + diff --git a/base/version.h b/base/version.h new file mode 100644 index 00000000000..40f20604531 --- /dev/null +++ b/base/version.h @@ -0,0 +1,29 @@ +/* Residual - Virtual machine to run LucasArts' 3D adventure games + * + * Residual is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the AUTHORS + * file distributed with this source distribution. + + * This library 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 + * Lesser General Public License for more details. + + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + * + * $URL$ + * $Id$ + */ + +#ifndef BASE_VERSION_H +#define BASE_VERSION_H + +extern const char *gResidualVersion; // e.g. "0.0.6" +extern const char *gResidualBuildDate; // e.g. "2008-06-15" +extern const char *gResidualVersionDate; // e.g. "0.0.6 (2008-06-15)" +extern const char *gResidualFullVersion; // e.g. "Residual 0.0.6 (2008-06-15)" +extern const char *gResidualFeatures; // e.g. "ALSA MPEG2 zLib" + +#endif diff --git a/common/md5.cpp b/common/md5.cpp new file mode 100644 index 00000000000..05339747fdc --- /dev/null +++ b/common/md5.cpp @@ -0,0 +1,431 @@ +/* Residual - Virtual machine to run LucasArts' 3D adventure games + * + * Residual is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the AUTHORS + * 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. + * + * $URL$ + * $Id$ + * + */ + +/* + * RFC 1321 compliant MD5 implementation, + * by Christophe Devine + * this program is licensed under the GPL. + */ + +#include "common/file.h" +#include "common/fs.h" +#include "common/md5.h" +#include "common/util.h" +#include "common/endian.h" + +namespace Common { + +typedef struct { + uint32 total[2]; + uint32 state[4]; + uint8 buffer[64]; +} md5_context; + +void md5_starts(md5_context *ctx); +void md5_update(md5_context *ctx, const uint8 *input, uint32 length); +void md5_finish(md5_context *ctx, uint8 digest[16]); + + +#define GET_UINT32(n, b, i) (n) = READ_LE_UINT32(b + i) +#define PUT_UINT32(n, b, i) WRITE_LE_UINT32(b + i, n) + +void md5_starts(md5_context *ctx) { + ctx->total[0] = 0; + ctx->total[1] = 0; + + ctx->state[0] = 0x67452301; + ctx->state[1] = 0xEFCDAB89; + ctx->state[2] = 0x98BADCFE; + ctx->state[3] = 0x10325476; +} + +static void md5_process(md5_context *ctx, const uint8 data[64]) { + uint32 X[16], A, B, C, D; + + GET_UINT32(X[0], data, 0); + GET_UINT32(X[1], data, 4); + GET_UINT32(X[2], data, 8); + GET_UINT32(X[3], data, 12); + GET_UINT32(X[4], data, 16); + GET_UINT32(X[5], data, 20); + GET_UINT32(X[6], data, 24); + GET_UINT32(X[7], data, 28); + GET_UINT32(X[8], data, 32); + GET_UINT32(X[9], data, 36); + GET_UINT32(X[10], data, 40); + GET_UINT32(X[11], data, 44); + GET_UINT32(X[12], data, 48); + GET_UINT32(X[13], data, 52); + GET_UINT32(X[14], data, 56); + GET_UINT32(X[15], data, 60); + +#define S(x, n) ((x << n) | ((x & 0xFFFFFFFF) >> (32 - n))) + +#define P(a, b, c, d, k, s, t) \ +{ \ + a += F(b,c,d) + X[k] + t; a = S(a,s) + b; \ +} + + A = ctx->state[0]; + B = ctx->state[1]; + C = ctx->state[2]; + D = ctx->state[3]; + +#define F(x, y, z) (z ^ (x & (y ^ z))) + + P(A, B, C, D, 0, 7, 0xD76AA478); + P(D, A, B, C, 1, 12, 0xE8C7B756); + P(C, D, A, B, 2, 17, 0x242070DB); + P(B, C, D, A, 3, 22, 0xC1BDCEEE); + P(A, B, C, D, 4, 7, 0xF57C0FAF); + P(D, A, B, C, 5, 12, 0x4787C62A); + P(C, D, A, B, 6, 17, 0xA8304613); + P(B, C, D, A, 7, 22, 0xFD469501); + P(A, B, C, D, 8, 7, 0x698098D8); + P(D, A, B, C, 9, 12, 0x8B44F7AF); + P(C, D, A, B, 10, 17, 0xFFFF5BB1); + P(B, C, D, A, 11, 22, 0x895CD7BE); + P(A, B, C, D, 12, 7, 0x6B901122); + P(D, A, B, C, 13, 12, 0xFD987193); + P(C, D, A, B, 14, 17, 0xA679438E); + P(B, C, D, A, 15, 22, 0x49B40821); + +#undef F + +#define F(x, y, z) (y ^ (z & (x ^ y))) + + P(A, B, C, D, 1, 5, 0xF61E2562); + P(D, A, B, C, 6, 9, 0xC040B340); + P(C, D, A, B, 11, 14, 0x265E5A51); + P(B, C, D, A, 0, 20, 0xE9B6C7AA); + P(A, B, C, D, 5, 5, 0xD62F105D); + P(D, A, B, C, 10, 9, 0x02441453); + P(C, D, A, B, 15, 14, 0xD8A1E681); + P(B, C, D, A, 4, 20, 0xE7D3FBC8); + P(A, B, C, D, 9, 5, 0x21E1CDE6); + P(D, A, B, C, 14, 9, 0xC33707D6); + P(C, D, A, B, 3, 14, 0xF4D50D87); + P(B, C, D, A, 8, 20, 0x455A14ED); + P(A, B, C, D, 13, 5, 0xA9E3E905); + P(D, A, B, C, 2, 9, 0xFCEFA3F8); + P(C, D, A, B, 7, 14, 0x676F02D9); + P(B, C, D, A, 12, 20, 0x8D2A4C8A); + +#undef F + +#define F(x, y, z) (x ^ y ^ z) + + P(A, B, C, D, 5, 4, 0xFFFA3942); + P(D, A, B, C, 8, 11, 0x8771F681); + P(C, D, A, B, 11, 16, 0x6D9D6122); + P(B, C, D, A, 14, 23, 0xFDE5380C); + P(A, B, C, D, 1, 4, 0xA4BEEA44); + P(D, A, B, C, 4, 11, 0x4BDECFA9); + P(C, D, A, B, 7, 16, 0xF6BB4B60); + P(B, C, D, A, 10, 23, 0xBEBFBC70); + P(A, B, C, D, 13, 4, 0x289B7EC6); + P(D, A, B, C, 0, 11, 0xEAA127FA); + P(C, D, A, B, 3, 16, 0xD4EF3085); + P(B, C, D, A, 6, 23, 0x04881D05); + P(A, B, C, D, 9, 4, 0xD9D4D039); + P(D, A, B, C, 12, 11, 0xE6DB99E5); + P(C, D, A, B, 15, 16, 0x1FA27CF8); + P(B, C, D, A, 2, 23, 0xC4AC5665); + +#undef F + +#define F(x, y, z) (y ^ (x | ~z)) + + P(A, B, C, D, 0, 6, 0xF4292244); + P(D, A, B, C, 7, 10, 0x432AFF97); + P(C, D, A, B, 14, 15, 0xAB9423A7); + P(B, C, D, A, 5, 21, 0xFC93A039); + P(A, B, C, D, 12, 6, 0x655B59C3); + P(D, A, B, C, 3, 10, 0x8F0CCC92); + P(C, D, A, B, 10, 15, 0xFFEFF47D); + P(B, C, D, A, 1, 21, 0x85845DD1); + P(A, B, C, D, 8, 6, 0x6FA87E4F); + P(D, A, B, C, 15, 10, 0xFE2CE6E0); + P(C, D, A, B, 6, 15, 0xA3014314); + P(B, C, D, A, 13, 21, 0x4E0811A1); + P(A, B, C, D, 4, 6, 0xF7537E82); + P(D, A, B, C, 11, 10, 0xBD3AF235); + P(C, D, A, B, 2, 15, 0x2AD7D2BB); + P(B, C, D, A, 9, 21, 0xEB86D391); + +#undef F + + ctx->state[0] += A; + ctx->state[1] += B; + ctx->state[2] += C; + ctx->state[3] += D; +} + +void md5_update(md5_context *ctx, const uint8 *input, uint32 length) { + uint32 left, fill; + + if (!length) + return; + + left = ctx->total[0] & 0x3F; + fill = 64 - left; + + ctx->total[0] += length; + ctx->total[0] &= 0xFFFFFFFF; + + if (ctx->total[0] < length) + ctx->total[1]++; + + if (left && length >= fill) { + memcpy((void *)(ctx->buffer + left), (const void *)input, fill); + md5_process(ctx, ctx->buffer); + length -= fill; + input += fill; + left = 0; + } + + while (length >= 64) { + md5_process(ctx, input); + length -= 64; + input += 64; + } + + if (length) { + memcpy((void *)(ctx->buffer + left), (const void *)input, length); + } +} + +static const uint8 md5_padding[64] = { + 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +}; + +void md5_finish(md5_context *ctx, uint8 digest[16]) { + uint32 last, padn; + uint32 high, low; + uint8 msglen[8]; + + high = (ctx->total[0] >> 29) | (ctx->total[1] << 3); + low = (ctx->total[0] << 3); + + PUT_UINT32(low, msglen, 0); + PUT_UINT32(high, msglen, 4); + + last = ctx->total[0] & 0x3F; + padn = (last < 56) ? (56 - last) : (120 - last); + + md5_update(ctx, md5_padding, padn); + md5_update(ctx, msglen, 8); + + PUT_UINT32(ctx->state[0], digest, 0); + PUT_UINT32(ctx->state[1], digest, 4); + PUT_UINT32(ctx->state[2], digest, 8); + PUT_UINT32(ctx->state[3], digest, 12); +} + +bool md5_file(const FSNode &file, uint8 digest[16], uint32 length) { + if (!file.exists()) { + warning("md5_file: using an inexistent FSNode"); + return false; + } else if (!file.isReadable()) { + warning("md5_file: using an unreadable FSNode"); + return false; + } else if (file.isDirectory()) { + warning("md5_file: using a directory FSNode"); + return false; + } + + ReadStream *stream = file.createReadStream(); + if (!stream) { + warning("md5_file: failed to open '%s'", file.getPath().c_str()); + return false; + } + + bool result = md5_file(*stream, digest, length); + delete stream; + return result; +} + +bool md5_file(const char *name, uint8 digest[16], uint32 length) { + File f; + + f.open(name); + if (!f.isOpen()) { + warning("md5_file couldn't open '%s'", name); + return false; + } + + return md5_file(f, digest, length); +} + + +bool md5_file(ReadStream &stream, uint8 digest[16], uint32 length) { + +#ifdef DISABLE_MD5 + memset(digest, 0, 16); +#else + md5_context ctx; + int i; + unsigned char buf[1000]; + bool restricted = (length != 0); + uint32 readlen; + + if (!restricted || sizeof(buf) <= length) + readlen = sizeof(buf); + else + readlen = length; + + md5_starts(&ctx); + + while ((i = stream.read(buf, readlen)) > 0) { + md5_update(&ctx, buf, i); + + length -= i; + if (restricted && length == 0) + break; + + if (restricted && sizeof(buf) > length) + readlen = length; + } + + md5_finish(&ctx, digest); +#endif + return true; +} + +bool md5_file_string(const FSNode &file, char *md5str, uint32 length) { + uint8 digest[16]; + if (!md5_file(file, digest, length)) + return false; + + for (int i = 0; i < 16; i++) { + snprintf(md5str + i*2, 3, "%02x", (int)digest[i]); + } + + return true; +} + +bool md5_file_string(const char *name, char *md5str, uint32 length) { + uint8 digest[16]; + if (!md5_file(name, digest, length)) + return false; + + for (int i = 0; i < 16; i++) { + snprintf(md5str + i*2, 3, "%02x", (int)digest[i]); + } + + return true; +} + +bool md5_file_string(ReadStream &stream, char *md5str, uint32 length) { + uint8 digest[16]; + if (!md5_file(stream, digest, length)) + return false; + + for (int i = 0; i < 16; i++) { + snprintf(md5str + i*2, 3, "%02x", (int)digest[i]); + } + + return true; +} + +} // End of namespace Common + +#ifdef TEST + +#include +#include + +/* + * those are the standard RFC 1321 test vectors + */ + +static const char *msg[] = { + "", + "a", + "abc", + "message digest", + "abcdefghijklmnopqrstuvwxyz", + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", + "12345678901234567890123456789012345678901234567890123456789012" \ + "345678901234567890" +}; + +static const char *val[] = { + "d41d8cd98f00b204e9800998ecf8427e", + "0cc175b9c0f1b6a831c399e269772661", + "900150983cd24fb0d6963f7d28e17f72", + "f96b697d7cb7938d525a2f31aaf161d0", + "c3fcd3d76192e4007dfb496cca67e13b", + "d174ab98d277d9f5a5611c2c9f419d9f", + "57edf4a22be3c955ac49da2e2107b67a" +}; + +int main(int argc, char *argv[]) { + int i, j; + char output[33]; + md5_context ctx; + unsigned char md5sum[16]; + + if (argc < 2) { + printf("\n MD5 Validation Tests:\n\n"); + + for (i = 0; i < 7; i++) { + printf(" Test %d ", i + 1); + + md5_starts(&ctx); + md5_update(&ctx, (const uint8 *)msg[i], strlen(msg[i])); + md5_finish(&ctx, md5sum); + + for (j = 0; j < 16; j++) { + sprintf(output + j * 2, "%02x", md5sum[j]); + } + + if (memcmp(output, val[i], 32)) { + printf("failed!\n"); + return 1; + } + + printf("passed.\n"); + } + + printf("\n"); + } else { + for (i = 1; i < argc; i++) { + md5_file(argv[i], md5sum); + for (j = 0; j < 16; j++) { + printf("%02x", md5sum[j]); + } + + printf(" %s\n", argv[i]); + } + } + + return 0; +} + +#endif diff --git a/common/md5.h b/common/md5.h new file mode 100644 index 00000000000..e26f46172aa --- /dev/null +++ b/common/md5.h @@ -0,0 +1,51 @@ +/* Residual - Virtual machine to run LucasArts' 3D adventure games + * + * Residual is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the AUTHORS + * 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. + * + * $URL$ + * $Id$ + * + */ + +#ifndef COMMON_MD5_H +#define COMMON_MD5_H + +#include "common/sys.h" + +namespace Common { + +class FSNode; +class ReadStream; + +bool md5_file(const char *name, uint8 digest[16], uint32 length = 0); +bool md5_file(const FSNode &file, uint8 digest[16], uint32 length = 0); +bool md5_file(ReadStream &stream, uint8 digest[16], uint32 length = 0); + +// The following two methods work similar to the above two, but +// instead of computing the binary MD5 digest, they produce +// a human readable lowercase hexstring representing the digest. +// The md5str parameter must point to a buffer of 32+1 chars. +bool md5_file_string(const char *name, char *md5str, uint32 length = 0); +bool md5_file_string(const FSNode &file, char *md5str, uint32 length = 0); +bool md5_file_string(ReadStream &stream, char *md5str, uint32 length = 0); + + +} // End of namespace Common + +#endif diff --git a/common/module.mk b/common/module.mk index 5df8574398c..c01dc8e5f49 100644 --- a/common/module.mk +++ b/common/module.mk @@ -10,6 +10,7 @@ MODULE_OBJS := \ hashmap.o \ libz.o \ memorypool.o \ + md5.o \ mutex.o \ str.o \ stream.o \ diff --git a/engines/advancedDetector.cpp b/engines/advancedDetector.cpp new file mode 100644 index 00000000000..9f2d06e60cd --- /dev/null +++ b/engines/advancedDetector.cpp @@ -0,0 +1,538 @@ +/* Residual - Virtual machine to run LucasArts' 3D adventure games + * + * Residual is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the AUTHORS + * 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. + * + * $URL$ + * $Id$ + * + */ + +#include "base/plugins.h" + +#include "common/debug.h" +#include "common/util.h" +#include "common/hash-str.h" +#include "common/file.h" +#include "common/md5.h" +#include "common/config-manager.h" + +#include "engines/advancedDetector.h" + +/** + * A list of pointers to ADGameDescription structs (or subclasses thereof). + */ +typedef Common::Array ADGameDescList; + + +/** + * Detect games in specified directory. + * Parameters language and platform are used to pass on values + * specified by the user. I.e. this is used to restrict search scope. + * + * @param fslist FSList to scan or NULL for scanning all specified + * default directories. + * @param params a ADParams struct containing various parameters + * @param language restrict results to specified language only + * @param platform restrict results to specified platform only + * @return list of ADGameDescription (or subclass) pointers corresponding to matched games + */ +static ADGameDescList detectGame(const Common::FSList &fslist, const ADParams ¶ms, Common::Language language, Common::Platform platform, const Common::String extra); + + +/** + * Returns list of targets supported by the engine. + * Distinguishes engines with single ID + */ +static GameList gameIDList(const ADParams ¶ms) { + if (params.singleid != NULL) { + GameList gl; + + const PlainGameDescriptor *g = params.list; + while (g->gameid) { + if (0 == strcasecmp(params.singleid, g->gameid)) { + gl.push_back(GameDescriptor(g->gameid, g->description)); + + return gl; + } + g++; + } + error("Engine %s doesn't have its singleid specified in ids list", params.singleid); + } + + return GameList(params.list); +} + +static void upgradeTargetIfNecessary(const ADParams ¶ms) { + if (params.obsoleteList == 0) + return; + + Common::String gameid = ConfMan.get("gameid"); + + for (const ADObsoleteGameID *o = params.obsoleteList; o->from; ++o) { + if (gameid.equalsIgnoreCase(o->from)) { + gameid = o->to; + ConfMan.set("gameid", gameid); + + if (o->platform != Common::kPlatformUnknown) + ConfMan.set("platform", Common::getPlatformCode(o->platform)); + + warning("Target upgraded from %s to %s", o->from, o->to); + + // WORKAROUND: Fix for bug #1719463: "DETECTOR: Launching + // undefined target adds launcher entry" + if (ConfMan.hasKey("id_came_from_command_line")) { + warning("Target came from command line. Skipping save"); + } else { + ConfMan.flushToDisk(); + } + break; + } + } +} + +namespace AdvancedDetector { + +GameDescriptor findGameID( + const char *gameid, + const PlainGameDescriptor *list, + const ADObsoleteGameID *obsoleteList + ) { + // First search the list of supported game IDs for a match. + const PlainGameDescriptor *g = findPlainGameDescriptor(gameid, list); + if (g) + return GameDescriptor(*g); + + // If we didn't find the gameid in the main list, check if it + // is an obsolete game id. + if (obsoleteList != 0) { + const ADObsoleteGameID *o = obsoleteList; + while (o->from) { + if (0 == strcasecmp(gameid, o->from)) { + g = findPlainGameDescriptor(o->to, list); + if (g && g->description) + return GameDescriptor(gameid, "Obsolete game ID (" + Common::String(g->description) + ")"); + else + return GameDescriptor(gameid, "Obsolete game ID"); + } + o++; + } + } + + // No match found + return GameDescriptor(); +} + +} // End of namespace AdvancedDetector + +static GameDescriptor toGameDescriptor(const ADGameDescription &g, const PlainGameDescriptor *sg) { + const char *title = 0; + + while (sg->gameid) { + if (!strcasecmp(g.gameid, sg->gameid)) + title = sg->description; + sg++; + } + + GameDescriptor gd(g.gameid, title, g.language, g.platform); + gd.updateDesc(g.extra); + return gd; +} + +/** + * 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 Common::String &id, const ADGameDescription *desc) { + Common::String res(id); + + if (desc->flags & ADGF_DEMO) { + res = res + "-demo"; + } + + if (desc->flags & ADGF_CD) { + res = res + "-cd"; + } + + if (desc->platform != Common::kPlatformPC && desc->platform != Common::kPlatformUnknown) { + 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; +} + +static void updateGameDescriptor(GameDescriptor &desc, const ADGameDescription *realDesc, const ADParams ¶ms) { + if (params.singleid != NULL) { + desc["preferredtarget"] = desc["gameid"]; + desc["gameid"] = params.singleid; + } + + if (!(params.flags & kADFlagDontAugmentPreferredTarget)) { + if (!desc.contains("preferredtarget")) + desc["preferredtarget"] = desc["gameid"]; + + desc["preferredtarget"] = generatePreferredTarget(desc["preferredtarget"], realDesc); + } + + if (params.flags & kADFlagUseExtraAsHint) + desc["extra"] = realDesc->extra; +} + +GameList AdvancedMetaEngine::detectGames(const Common::FSList &fslist) const { + ADGameDescList matches = detectGame(fslist, params, Common::UNK_LANG, Common::kPlatformUnknown, ""); + GameList detectedGames; + + // Use fallback detector if there were no matches by other means + if (matches.empty()) { + const ADGameDescription *fallbackDesc = fallbackDetect(fslist); + if (fallbackDesc != 0) { + GameDescriptor desc(toGameDescriptor(*fallbackDesc, params.list)); + updateGameDescriptor(desc, fallbackDesc, params); + detectedGames.push_back(desc); + } + } else for (uint i = 0; i < matches.size(); i++) { // Otherwise use the found matches + GameDescriptor desc(toGameDescriptor(*matches[i], params.list)); + updateGameDescriptor(desc, matches[i], params); + detectedGames.push_back(desc); + } + + return detectedGames; +} + +Common::Error AdvancedMetaEngine::createInstance(OSystem *syst, Engine **engine) const { + assert(engine); + upgradeTargetIfNecessary(params); + + 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 (params.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; + } + + ADGameDescList matches = detectGame(files, params, language, platform, extra); + + if (params.singleid == NULL) { + 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(files); + if (agdDesc != 0) { + // Seems we found a fallback match. But first perform a basic + // sanity check: the gameid must match. + if (params.singleid == NULL && agdDesc->gameid != gameid) + agdDesc = 0; + } + } + + if (agdDesc == 0) { + return Common::kNoGameDataFoundError; + } + + debug(2, "Running %s", toGameDescriptor(*agdDesc, params.list).description().c_str()); + if (!createInstance(syst, engine, agdDesc)) { + return Common::kNoGameDataFoundError; + } + return Common::kNoError; +} + +struct SizeMD5 { + int size; + char md5[32+1]; +}; + +typedef Common::HashMap SizeMD5Map; +typedef Common::HashMap FileMap; + +static void reportUnknown(const Common::FSNode &path, const SizeMD5Map &filesSizeMD5) { + // TODO: This message should be cleaned up / made more specific. + // For example, we should specify at least which engine triggered this. + // + // Might also be helpful to display the full path (for when this is used + // from the mass detector). + printf("The game in '%s' seems to be unknown.\n", path.getPath().c_str()); + printf("Please, report the following data to the ScummVM team along with name\n"); + printf("of the game you tried to add and its version/language/etc.:\n"); + + for (SizeMD5Map::const_iterator file = filesSizeMD5.begin(); file != filesSizeMD5.end(); ++file) + printf(" \"%s\", \"%s\", %d\n", file->_key.c_str(), file->_value.md5, file->_value.size); + + printf("\n"); +} + +static ADGameDescList detectGameFilebased(const FileMap &allFiles, const ADParams ¶ms); + +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. + for (Common::FSList::const_iterator file = fslist.begin(); file != fslist.end(); ++file) { + if (file->isDirectory()) + continue; + + Common::String tstr = file->getName(); + + // Strip any trailing dot + if (tstr.lastChar() == '.') + tstr.deleteLastChar(); + + allFiles[tstr] = *file; // Record the presence of this file + } + + // 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; + if (allFiles.contains(fname) && !filesSizeMD5.contains(fname)) { + debug(3, "+ %s", fname.c_str()); + + SizeMD5 tmp; + if (!md5_file_string(allFiles[fname], tmp.md5, params.md5Bytes)) + tmp.md5[0] = 0; + + debug(3, "> '%s': '%s'", fname.c_str(), tmp.md5); + + Common::File testFile; + if (testFile.open(allFiles[fname])) + tmp.size = (int32)testFile.size(); + else + tmp.size = -1; + + 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) || + (platform != Common::kPlatformUnknown && g->platform != Common::kPlatformUnknown && g->platform != platform)) { + continue; + } + + if ((params.flags & kADFlagUseExtraAsHint) && !extra.empty() && g->extra != extra) + continue; + + bool allFilesPresent = true; + + // 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 && 0 != strcmp(fileDesc->md5, filesSizeMD5[tstr].md5)) { + debug(3, "MD5 Mismatch. Skipping (%s) (%s)", fileDesc->md5, filesSizeMD5[tstr].md5); + 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()); + } + + // We found at least one entry with all required files present. + // That means that we got new variant of the game. + // + // Wihtout this check we would have errorneous 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); + + // Count the number of matching files. Then, only keep those + // entries which match a maximal amount of files. + int curFilesMatched = 0; + for (fileDesc = g->filesDescriptions; fileDesc->fileName; fileDesc++) + curFilesMatched++; + + if (curFilesMatched > maxFilesMatched) { + debug(2, " ... new best match, removing all previous candidates"); + maxFilesMatched = curFilesMatched; + + for (uint j = 0; j < matched.size();) { + if (matched[j]->flags & ADGF_KEEPMATCH) + ++j; + else + matched.remove_at(j); + } + 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; +} + +/** + * Check for each ADFileBasedFallback record whether all files listed + * in it are present. If multiple pass this test, we pick the one with + * the maximal number of matching files. In case of a tie, the entry + * coming first in the list is chosen. + */ +static ADGameDescList detectGameFilebased(const FileMap &allFiles, const ADParams ¶ms) { + const ADFileBasedFallback *ptr; + const char* const* filenames; + + int maxNumMatchedFiles = 0; + const ADGameDescription *matchedDesc = 0; + + for (ptr = params.fileBasedFallback; ptr->desc; ++ptr) { + const ADGameDescription *agdesc = (const ADGameDescription *)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) { + matchedDesc = agdesc; + maxNumMatchedFiles = numMatchedFiles; + + debug(4, "and overriden"); + } + } + } + + ADGameDescList matched; + + if (matchedDesc) { // We got a match + matched.push_back(matchedDesc); + if (params.flags & kADFlagPrintWarningOnFileBasedFallback) { + printf("Your game version has been detected using filename matching as a\n"); + printf("variant of %s.\n", matchedDesc->gameid); + printf("If this is an original and unmodified version, please report any\n"); + printf("information previously printed by ScummVM to the team.\n"); + } + } + + return matched; +} + +GameList AdvancedMetaEngine::getSupportedGames() const { + return gameIDList(params); +} +GameDescriptor AdvancedMetaEngine::findGame(const char *gameid) const { + return AdvancedDetector::findGameID(gameid, params.list, params.obsoleteList); +} diff --git a/engines/advancedDetector.h b/engines/advancedDetector.h new file mode 100644 index 00000000000..9c5b835ffde --- /dev/null +++ b/engines/advancedDetector.h @@ -0,0 +1,218 @@ +/* Residual - Virtual machine to run LucasArts' 3D adventure games + * + * Residual is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the AUTHORS + * 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. + * + * $URL$ + * $Id$ + * + */ +#ifndef ENGINES_ADVANCED_DETECTOR_H +#define ENGINES_ADVANCED_DETECTOR_H + +#include "common/fs.h" +#include "common/error.h" + +#include "engines/metaengine.h" + + +struct ADGameFileDescription { + const char *fileName; + uint16 fileType; // Optional. Not used during detection, only by engines. + const char *md5; // Optional. May be NULL. + int32 fileSize; // Optional. Set to -1 to ignore. +}; + +#define AD_ENTRY1(f, x) {{ f, 0, x, -1}, {NULL, 0, NULL, 0}} +#define AD_ENTRY1s(f, x, s) {{ f, 0, x, s}, {NULL, 0, NULL, 0}} + +enum ADGameFlags { + ADGF_NO_FLAGS = 0, + ADGF_KEEPMATCH = (1 << 27), // this entry is kept even when there are matched + // entries with more files + ADGF_DROPLANGUAGE = (1 << 28), // don't add language to gameid + ADGF_CD = (1 << 29), // add "-cd" to gameid + ADGF_DEMO = (1 << 30) // add "-demo" to gameid +}; + +struct ADGameDescription { + const char *gameid; + const char *extra; + ADGameFileDescription filesDescriptions[14]; + Common::Language language; + Common::Platform platform; + + /** + * A bitmask of extra flags. The top 8 bits are reserved for generic flags + * defined in the ADGameFlags. This leaves 24 flags to be used by client + * code. + */ + uint32 flags; +}; + +/** + * End marker for a table of ADGameDescription structs. Use this to + * terminate a list to be passed to the AdvancedDetector API. + */ +#define AD_TABLE_END_MARKER \ + { NULL, NULL, { { NULL, 0, NULL, 0 } }, Common::UNK_LANG, Common::kPlatformUnknown, ADGF_NO_FLAGS } + + +struct ADObsoleteGameID { + const char *from; + const char *to; + Common::Platform platform; +}; + +struct ADFileBasedFallback { + /** + * Pointer to an ADGameDescription or subclass thereof which will get + * returned if there's a detection match. + */ + const void *desc; + + /** + * A zero-terminated list of filenames used for matching. All files in + * the list must be present to get a detection match. + */ + const char *filenames[10]; +}; + + +enum ADFlags { + /** + * Generate/augment preferred target with information on the language (if + * not equal to english) and platform (if not equal to PC). + */ + kADFlagDontAugmentPreferredTarget = (1 << 0), + kADFlagPrintWarningOnFileBasedFallback = (1 << 1), + kADFlagUseExtraAsHint = (1 << 2) +}; + +/** + * A structure containing all parameters for the AdvancedDetector. + * Typically, an engine will have a single instance of this which is + * used by its AdvancedMetaEngine subclass as a parameter to the + * primary AdvancedMetaEngine constructor. + */ +struct ADParams { + /** + * Pointer to an array of objects which are either ADGameDescription + * or superset structures (i.e. start with an ADGameDescription member. + * The list is terminated by an entry with a gameid equal to 0 + * (see AD_TABLE_END_MARKER). + */ + const byte *descs; + + /** + * The size of a single entry of the above descs array. Always + * must be >= sizeof(ADGameDescription). + */ + uint descItemSize; + + /** + * The number of bytes to compute MD5 sum for. The AdvancedDetector + * is primarily based on computing and matching MD5 checksums of files. + * Since doing that for large files can be slow, it can be restricted + * to a subset of all files. + * Typically this will be set to something between 5 and 50 kilobyte, + * but arbitrary non-zero values are possible. + */ + uint md5Bytes; + + /** + * A list of all gameids (and their corresponding descriptions) supported + * by this engine. + */ + const PlainGameDescriptor *list; + + /** + * Structure for autoupgrading obsolete targets (optional). + * + * @todo Properly explain this. + */ + const ADObsoleteGameID *obsoleteList; + + /** + * Name of single gameid (optional). + * + * @todo Properly explain this -- what does it do? + */ + const char *singleid; + + /** + * List of files for file-based fallback detection (optional). + * This is used if the regular MD5 based detection failed to + * detect anything. + * As usual this list is terminated by an all-zero entry. + * + * @todo Properly explain this + */ + const ADFileBasedFallback *fileBasedFallback; + + /** + * A bitmask of flags which can be used to configure the behavior + * of the AdvancedDetector. Refer to ADFlags for a list of flags + * that can be ORed together and passed here. + */ + uint32 flags; +}; + + +namespace AdvancedDetector { + +/** + * Scan through the game descriptors specified in params and search for + * 'gameid' in there. If a match is found, returns a GameDescriptor + * with gameid and description set. + */ +GameDescriptor findGameID( + const char *gameid, + const PlainGameDescriptor *list, + const ADObsoleteGameID *obsoleteList = 0 + ); + +} // End of namespace AdvancedDetector + +/** + * A MetaEngine implementation based around the advanced detector code. + */ +class AdvancedMetaEngine : public MetaEngine { + const ADParams ¶ms; +public: + AdvancedMetaEngine(const ADParams &dp) : params(dp) {} + + virtual GameList getSupportedGames() const; + virtual GameDescriptor findGame(const char *gameid) const; + virtual GameList detectGames(const Common::FSList &fslist) const; + virtual Common::Error createInstance(OSystem *syst, Engine **engine) const; + + // To be provided by subclasses + virtual bool createInstance(OSystem *syst, Engine **engine, const ADGameDescription *desc) const = 0; + + /** + * An (optional) generic fallback detect function which is invoked + * if both the regular MD5 based detection as well as the file + * based fallback failed to detect anything. + */ + virtual const ADGameDescription *fallbackDetect(const Common::FSList &fslist) const { + return 0; + } +}; + +#endif diff --git a/engines/engine.cpp b/engines/engine.cpp new file mode 100644 index 00000000000..627a9606548 --- /dev/null +++ b/engines/engine.cpp @@ -0,0 +1,146 @@ +/* Residual - Virtual machine to run LucasArts' 3D adventure games + * + * Residual is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the AUTHORS + * 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. + * + * $URL$ + * $Id$ + */ + +#if defined(WIN32) +#include +#include +// winnt.h defines ARRAYSIZE, but we want our own one... +#undef ARRAYSIZE +#endif + +#include "engines/engine.h" +#include "common/config-manager.h" +#include "common/events.h" +#include "common/file.h" +#include "common/timer.h" +#include "common/savefile.h" +#include "common/system.h" +#include "mixer/mixer.h" +#include "engines/metaengine.h" + +#ifdef _WIN32_WCE +extern bool isSmartphone(void); +#endif + +Engine::Engine(OSystem *syst) + : _system(syst), + _mixer(_system->getMixer()), + _timer(_system->getTimerManager()), + _eventMan(_system->getEventManager()), + _saveFileMan(_system->getSavefileManager()), + _targetName(ConfMan.getActiveDomainName()), + _gameDataDir(ConfMan.get("path")), + _pauseLevel(0) { + + // FIXME: Get rid of the following again. It is only here temporarily. + // We really should never run with a non-working Mixer, so ought to handle + // this at a much earlier stage. If we *really* want to support systems + // without a working mixer, then we need more work. E.g. we could modify the + // Mixer to immediately drop any streams passed to it. This way, at least + // we don't crash because heaps of (sound) memory get allocated but never + // freed. Of course, there still would be problems with many games... + if (!_mixer->isReady()) + warning("Sound initialization failed. This may cause severe problems in some games."); +} + +Engine::~Engine() { + _mixer->stopAll(); +} + +bool Engine::shouldPerformAutoSave(int lastSaveTime) { + const int diff = _system->getMillis() - lastSaveTime; + const int autosavePeriod = ConfMan.getInt("autosave_period"); + return autosavePeriod != 0 && diff > autosavePeriod * 1000; +} + +void Engine::pauseEngine(bool pause) { + assert((pause && _pauseLevel >= 0) || (!pause && _pauseLevel)); + + if (pause) + _pauseLevel++; + else + _pauseLevel--; + + if (_pauseLevel == 1) { + pauseEngineIntern(true); + } else if (_pauseLevel == 0) { + pauseEngineIntern(false); + } +} + +void Engine::pauseEngineIntern(bool pause) { + // By default, just (un)pause all digital sounds + _mixer->pauseAll(pause); +} + +void Engine::syncSoundSettings() { + + // Sync the engine with the config manager + int soundVolumeMusic = ConfMan.getInt("music_volume"); + int soundVolumeSFX = ConfMan.getInt("sfx_volume"); + int soundVolumeSpeech = ConfMan.getInt("speech_volume"); + + _mixer->setVolumeForSoundType(Audio::Mixer::kMusicSoundType, soundVolumeMusic); + _mixer->setVolumeForSoundType(Audio::Mixer::kSFXSoundType, soundVolumeSFX); + _mixer->setVolumeForSoundType(Audio::Mixer::kSpeechSoundType, soundVolumeSpeech); +} + +Common::Error Engine::loadGameState(int slot) { + // Do nothing by default + return Common::kNoError; +} + +bool Engine::canLoadGameStateCurrently() { + // Do not allow loading by default + return false; +} + +Common::Error Engine::saveGameState(int slot, const char *desc) { + // Do nothing by default + return Common::kNoError; +} + +bool Engine::canSaveGameStateCurrently() { + // Do not allow saving by default + return false; +} + +void Engine::quitGame() { + Common::Event event; + + event.type = Common::EVENT_QUIT; + g_system->getEventManager()->pushEvent(event); +} + +/* +EnginePlugin *Engine::getMetaEnginePlugin() const { + + const EnginePlugin *plugin = 0; + Common::String gameid = ConfMan.get("gameid"); + gameid.toLowercase(); + EngineMan.findGame(gameid, &plugin); + return plugin; +} + +*/ diff --git a/engines/engine.h b/engines/engine.h new file mode 100644 index 00000000000..ff374cd894a --- /dev/null +++ b/engines/engine.h @@ -0,0 +1,211 @@ +/* Residual - Virtual machine to run LucasArts' 3D adventure games + * + * Residual is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the AUTHORS + * 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. + * + * $URL$ + * $Id$ + */ + +#ifndef ENGINES_ENGINE_H +#define ENGINES_ENGINE_H + +#include "common/sys.h" +#include "common/error.h" +#include "common/fs.h" +#include "common/str.h" + +class OSystem; + +namespace Audio { + class Mixer; +} +namespace Common { + class EventManager; + class SaveFileManager; + class TimerManager; +} + +class Engine { +public: + OSystem *_system; + Audio::Mixer *_mixer; + +protected: + Common::TimerManager *_timer; + Common::EventManager *_eventMan; + Common::SaveFileManager *_saveFileMan; + + const Common::String _targetName; // target name for saves + + const Common::FSNode _gameDataDir; // FIXME: Get rid of this + +private: + /** + * The pause level, 0 means 'running', a positive value indicates + * how often the engine has been paused (and hence how often it has + * to be un-paused before it resumes running). This makes it possible + * to nest code which pauses the engine. + */ + int _pauseLevel; + +public: + + + /** + * A feature in this context means an ability of the engine which can be + * either available or not. + * @see Engine::hasFeature() + */ + enum EngineFeature { + /** + * Enables the subtitle speed and toggle items in the Options section + * of the global main menu. + */ + kSupportsSubtitleOptions, + + /** + * 'Return to launcher' feature is supported, i.e., EVENT_RTL is handled + * either directly, or indirectly (that is, the engine calls and honors + * the result of the Engine::shouldQuit() method appropriately). + */ + kSupportsRTL, + + /** + * Loading savestates during runtime is supported, that is, this engine + * implements loadGameState() and canLoadGameStateCurrently(). + * If this feature is supported, then the corresponding MetaEngine *must* + * support the kSupportsListSaves feature. + */ + kSupportsLoadingDuringRuntime, + + /** + * Loading savestates during runtime is supported, that is, this engine + * implements saveGameState() and canSaveGameStateCurrently(). + * If this feature is supported, then the corresponding MetaEngine *must* + * support the kSupportsListSaves feature. + */ + kSupportsSavingDuringRuntime + }; + + + + /** @name Overloadable methods + * + * All Engine subclasses should consider overloading some or all of the following methods. + */ + //@{ + + Engine(OSystem *syst); + virtual ~Engine(); + + /** + * Init the engine and start its main loop. + * @return returns kNoError on success, else an error code. + */ + virtual Common::Error run() = 0; + + /** + * Determine whether the engine supports the specified feature. + */ + virtual bool hasFeature(EngineFeature f) const { return false; } + +// virtual EnginePlugin *getMetaEnginePlugin() const; + + /** + * Notify the engine that the sound settings in the config manager may have + * changed and that it hence should adjust any internal volume etc. values + * accordingly. + * @todo find a better name for this + */ + virtual void syncSoundSettings(); + + /** + * Load a game state. + * @param slot the slot from which a savestate should be loaded + * @return returns kNoError on success, else an error code. + */ + virtual Common::Error loadGameState(int slot); + + /** + * Indicates whether a game state can be loaded. + */ + virtual bool canLoadGameStateCurrently(); + + /** + * Save a game state. + * @param slot the slot into which the savestate should be stored + * @param desc a description for the savestate, entered by the user + * @return returns kNoError on success, else an error code. + */ + virtual Common::Error saveGameState(int slot, const char *desc); + + /** + * Indicates whether a game state can be saved. + */ + virtual bool canSaveGameStateCurrently(); + +protected: + + /** + * Actual implementation of pauseEngine by subclasses. See there + * for details. + */ + virtual void pauseEngineIntern(bool pause); + + //@} + + +public: + + /** + * Request the engine to quit. Sends a EVENT_QUIT event to the Event + * Manager. + */ + static void quitGame(); + + /** + * Pause or resume the engine. This should stop/resume any audio playback + * and other stuff. Called right before the system runs a global dialog + * (like a global pause, main menu, options or 'confirm exit' dialog). + * + * This is a convenience tracker which automatically keeps track on how + * often the engine has been paused, ensuring that after pausing an engine + * e.g. twice, it has to be unpaused twice before actuallying resuming. + * + * @param pause true to pause the engine, false to resume it + */ + void pauseEngine(bool pause); + + /** + * Return whether the engine is currently paused or not. + */ + bool isPaused() const { return _pauseLevel != 0; } + +public: + +protected: + + /** + * Indicate whether an autosave should be performed. + */ + bool shouldPerformAutoSave(int lastSaveTime); + +}; + +#endif diff --git a/engines/engines.mk b/engines/engines.mk new file mode 100644 index 00000000000..5f872fe7317 --- /dev/null +++ b/engines/engines.mk @@ -0,0 +1,2 @@ +DEFINES += +MODULES += engines/grim diff --git a/engines/game.cpp b/engines/game.cpp new file mode 100644 index 00000000000..531d948cb1b --- /dev/null +++ b/engines/game.cpp @@ -0,0 +1,131 @@ +/* Residual - Virtual machine to run LucasArts' 3D adventure games + * + * Residual is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the AUTHORS + * 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. + * + * $URL$ + * $Id$ + * + */ + +#include "engines/game.h" +#include "base/plugins.h" + + +const PlainGameDescriptor *findPlainGameDescriptor(const char *gameid, const PlainGameDescriptor *list) { + const PlainGameDescriptor *g = list; + while (g->gameid) { + if (0 == strcasecmp(gameid, g->gameid)) + return g; + g++; + } + return 0; +} + +GameDescriptor::GameDescriptor() { + setVal("gameid", ""); + setVal("description", ""); +} + +GameDescriptor::GameDescriptor(const PlainGameDescriptor &pgd) { + setVal("gameid", pgd.gameid); + setVal("description", pgd.description); +} + +GameDescriptor::GameDescriptor(const Common::String &g, const Common::String &d, Common::Language l, Common::Platform p) { + setVal("gameid", g); + setVal("description", d); + if (l != Common::UNK_LANG) + setVal("language", Common::getLanguageCode(l)); + if (p != Common::kPlatformUnknown) + setVal("platform", Common::getPlatformCode(p)); +} + +void GameDescriptor::updateDesc(const char *extra) { + // TODO: The format used here (LANG/PLATFORM/EXTRA) is not set in stone. + // We may want to change the order (PLATFORM/EXTRA/LANG, anybody?), or + // the seperator (instead of '/' use ', ' or ' '). + const bool hasCustomLanguage = (language() != Common::UNK_LANG); + const bool hasCustomPlatform = (platform() != Common::kPlatformUnknown); + const bool hasExtraDesc = (extra && extra[0]); + + // Adapt the description string if custom platform/language is set. + if (hasCustomLanguage || hasCustomPlatform || hasExtraDesc) { + Common::String descr = description(); + + descr += " ("; + if (hasExtraDesc) + descr += extra; + if (hasCustomPlatform) { + if (hasExtraDesc) + descr += "/"; + descr += Common::getPlatformDescription(platform()); + } + if (hasCustomLanguage) { + if (hasExtraDesc || hasCustomPlatform) + descr += "/"; + descr += Common::getLanguageDescription(language()); + } + descr += ")"; + setVal("description", descr); + } +} + +bool SaveStateDescriptor::getBool(const Common::String &key) const { + if (contains(key)) { + Common::String value = getVal(key); + if (value.equalsIgnoreCase("true") || + value.equalsIgnoreCase("yes") || + value.equals("1")) + return true; + if (value.equalsIgnoreCase("false") || + value.equalsIgnoreCase("no") || + value.equals("0")) + return false; + error("SaveStateDescriptor: %s '%s' has unknown value '%s' for boolean '%s'", + save_slot().c_str(), description().c_str(), value.c_str(), key.c_str()); + } + return false; +} + +void SaveStateDescriptor::setDeletableFlag(bool state) { + setVal("is_deletable", state ? "true" : "false"); +} + +void SaveStateDescriptor::setWriteProtectedFlag(bool state) { + setVal("is_write_protected", state ? "true" : "false"); +} + +void SaveStateDescriptor::setSaveDate(int year, int month, int day) { + char buffer[32]; + snprintf(buffer, 32, "%.2d.%.2d.%.4d", day, month, year); + setVal("save_date", buffer); +} + +void SaveStateDescriptor::setSaveTime(int hour, int min) { + char buffer[32]; + snprintf(buffer, 32, "%.2d:%.2d", hour, min); + setVal("save_time", buffer); +} + +void SaveStateDescriptor::setPlayTime(int hours, int minutes) { + char buffer[32]; + snprintf(buffer, 32, "%.2d:%.2d", hours, minutes); + setVal("play_time", buffer); +} + diff --git a/engines/game.h b/engines/game.h new file mode 100644 index 00000000000..6a56bbf0da9 --- /dev/null +++ b/engines/game.h @@ -0,0 +1,180 @@ +/* Residual - Virtual machine to run LucasArts' 3D adventure games + * + * Residual is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the AUTHORS + * 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. + * + * $URL$ + * $Id$ + * + */ + +#ifndef ENGINES_GAME_H +#define ENGINES_GAME_H + +#include "common/str.h" +#include "common/array.h" +#include "common/hash-str.h" +#include "common/ptr.h" + + +/** + * A simple structure used to map gameids (like "monkey", "sword1", ...) to + * nice human readable and descriptive game titles (like "The Secret of Monkey Island"). + * This is a plain struct to make it possible to declare NULL-terminated C arrays + * consisting of PlainGameDescriptors. + */ +struct PlainGameDescriptor { + const char *gameid; + const char *description; +}; + +/** + * Given a list of PlainGameDescriptors, returns the first PlainGameDescriptor + * matching the given gameid. If not match is found return 0. + * The end of the list must marked by a PlainGameDescriptor with gameid equal to 0. + */ +const PlainGameDescriptor *findPlainGameDescriptor(const char *gameid, const PlainGameDescriptor *list); + +/** + * A hashmap describing details about a given game. In a sense this is a refined + * version of PlainGameDescriptor, as it also contains a gameid and a description string. + * But in addition, platform and language settings, as well as arbitrary other settings, + * can be contained in a GameDescriptor. + * This is an essential part of the glue between the game engines and the launcher code. + */ +class GameDescriptor : public Common::StringMap { +public: + GameDescriptor(); + GameDescriptor(const PlainGameDescriptor &pgd); + GameDescriptor(const Common::String &gameid, + const Common::String &description, + Common::Language language = Common::UNK_LANG, + Common::Platform platform = Common::kPlatformUnknown); + + /** + * Update the description string by appending (LANG/PLATFORM/EXTRA) to it. + */ + void updateDesc(const char *extra = 0); + + Common::String &gameid() { return getVal("gameid"); } + Common::String &description() { return getVal("description"); } + const Common::String &gameid() const { return getVal("gameid"); } + const Common::String &description() const { return getVal("description"); } + Common::Language language() const { return contains("language") ? Common::parseLanguage(getVal("language")) : Common::UNK_LANG; } + Common::Platform platform() const { return contains("platform") ? Common::parsePlatform(getVal("platform")) : Common::kPlatformUnknown; } + + const Common::String &preferredtarget() const { + return contains("preferredtarget") ? getVal("preferredtarget") : getVal("gameid"); + } +}; + +/** List of games. */ +class GameList : public Common::Array { +public: + GameList() {} + GameList(const GameList &list) : Common::Array(list) {} + GameList(const PlainGameDescriptor *g) { + while (g->gameid) { + push_back(GameDescriptor(g->gameid, g->description)); + g++; + } + } +}; + +/** + * A hashmap describing details about a given save state. + * TODO + * Guaranteed to contain save_slot and description values. + * Additional ideas: Playtime, creation date, thumbnail, ... + */ +class SaveStateDescriptor : public Common::StringMap { +protected: + +public: + SaveStateDescriptor() { + setVal("save_slot", "-1"); // FIXME: default to 0 (first slot) or to -1 (invalid slot) ? + setVal("description", ""); + } + + SaveStateDescriptor(int s, const Common::String &d) { + char buf[16]; + sprintf(buf, "%d", s); + setVal("save_slot", buf); + setVal("description", d); + } + + SaveStateDescriptor(const Common::String &s, const Common::String &d) { + setVal("save_slot", s); + setVal("description", d); + } + + /** The saveslot id, as it would be passed to the "-x" command line switch. */ + Common::String &save_slot() { return getVal("save_slot"); } + + /** The saveslot id, as it would be passed to the "-x" command line switch (read-only variant). */ + const Common::String &save_slot() const { return getVal("save_slot"); } + + /** A human readable description of the save state. */ + Common::String &description() { return getVal("description"); } + + /** A human readable description of the save state (read-only variant). */ + const Common::String &description() const { return getVal("description"); } + + /** Optional entries only included when querying via MetaEngine::querySaveMetaInfo */ + + /** + * Returns the value of a given key as boolean. + * It accepts 'true', 'yes' and '1' for true and + * 'false', 'no' and '0' for false. + * (FIXME:) On unknown value it errors out ScummVM. + * On unknown key it returns false as default. + */ + bool getBool(const Common::String &key) const; + + /** + * Sets the 'is_deletable' key, which indicates if the + * given savestate is safe for deletion. + */ + void setDeletableFlag(bool state); + + /** + * Sets the 'is_write_protected' key, which indicates if the + * given savestate can be overwritten or not + */ + void setWriteProtectedFlag(bool state); + + /** + * Sets the 'save_date' key properly, based on the given values. + */ + void setSaveDate(int year, int month, int day); + + /** + * Sets the 'save_time' key properly, based on the given values. + */ + void setSaveTime(int hour, int min); + + /** + * Sets the 'play_time' key properly, based on the given values. + */ + void setPlayTime(int hours, int minutes); +}; + +/** List of savestates. */ +typedef Common::Array SaveStateList; + +#endif diff --git a/engines/metaengine.h b/engines/metaengine.h new file mode 100644 index 00000000000..89db56bca01 --- /dev/null +++ b/engines/metaengine.h @@ -0,0 +1,240 @@ +/* Residual - Virtual machine to run LucasArts' 3D adventure games + * + * Residual is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the AUTHORS + * 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. + * + * $URL$ + * $Id$ + */ + +#ifndef ENGINES_METAENGINE_H +#define ENGINES_METAENGINE_H + +#include "common/sys.h" +#include "common/error.h" + +#include "engines/game.h" +#include "base/plugins.h" + +class Engine; +class OSystem; + +namespace Common { + class FSList; + class String; +} + +/** + * A meta engine is essentially a factory for Engine instances with the + * added ability of listing and detecting supported games. + * Every engine "plugin" provides a hook to get an instance of a MetaEngine + * subclass for that "engine plugin". E.g. SCUMM povides ScummMetaEngine. + * This is then in turn used by the frontend code to detect games, + * and instantiate actual Engine objects. + */ +class MetaEngine : public PluginObject { +public: + virtual ~MetaEngine() {} + + /** Returns some copyright information about the original engine. */ + virtual const char *getOriginalCopyright() const = 0; + + /** Returns a list of games supported by this engine. */ + virtual GameList getSupportedGames() const = 0; + + /** Query the engine for a GameDescriptor for the specified gameid, if any. */ + virtual GameDescriptor findGame(const char *gameid) const = 0; + + /** + * Runs the engine's game detector on the given list of files, and returns a + * (possibly empty) list of games supported by the engine which it was able + * to detect amongst the given files. + */ + virtual GameList detectGames(const Common::FSList &fslist) const = 0; + + /** + * Tries to instantiate an engine instance based on the settings of + * the currently active ConfMan target. That is, the MetaEngine should + * query the ConfMan singleton for the target, gameid, path etc. data. + * + * @param syst Pointer to the global OSystem object + * @param engine Pointer to a pointer which the MetaEngine sets to + * the newly create Engine, or 0 in case of an error + * @return a Common::Error describing the error which occurred, or kNoError + */ + virtual Common::Error createInstance(OSystem *syst, Engine **engine) const = 0; + + /** + * Return a list of all save states associated with the given target. + * + * The caller has to ensure that this (Meta)Engine is responsible + * for the specified target (by using findGame on it respectively + * on the associated gameid from the relevant ConfMan entry, if present). + * + * The default implementation returns an empty list. + * + * @note MetaEngines must indicate that this function has been implemented + * via the kSupportsListSaves feature flag. + * + * @param target name of a config manager target + * @return a list of save state descriptors + */ + virtual SaveStateList listSaves(const char *target) const { + return SaveStateList(); + } + + /** + * Return the maximum save slot that the engine supports. + * + * @note MetaEngines must indicate that this function has been implemented + * via the kSupportsListSaves feature flag. + * + * The default implementation limits the save slots to zero (0). + * + * @return maximum save slot number supported + */ + virtual int getMaximumSaveSlot() const { + return 0; + } + + /** + * Remove the specified save state. + * + * For most engines this just amounts to calling _saveFileMan->removeSaveFile(). + * Engines which keep an index file will also update it accordingly. + * + * @note MetaEngines must indicate that this function has been implemented + * via the kSupportsDeleteSave feature flag. + * + * @param target name of a config manager target + * @param slot slot number of the save state to be removed + */ + virtual void removeSaveState(const char *target, int slot) const {}; + + /** + * Returns meta infos from the specified save state. + * + * Depending on the MetaEngineFeatures set this can include + * thumbnails, save date / time, play time. + * + * @param target name of a config manager target + * @param slot slot number of the save state + */ + virtual SaveStateDescriptor querySaveMetaInfos(const char *target, int slot) const { + return SaveStateDescriptor(); + } + + /** @name MetaEngineFeature flags */ + //@{ + + /** + * A feature in this context means an ability of the engine which can be + * either available or not. + */ + enum MetaEngineFeature { + /** + * Listing all Save States for a given target is supported, i.e., + * the listSaves() and getMaximumSaveSlot methods are implemented. + * Used for --list-saves support, as well as the GMM load dialog. + */ + kSupportsListSaves, + + /** + * Loading from the Launcher / command line (-x) + */ + kSupportsLoadingDuringStartup, + + /** + * Deleting Saves from the Launcher (i.e. implements the + * removeSaveState() method) + */ + kSupportsDeleteSave, + + /** + * Features meta infos for savestates (i.e. implements the + * querySaveMetaInfos method properly). + * + * Engines implementing meta infos always have to provide + * the following entries in the save state descriptor queried + * by querySaveMetaInfos: + * - 'is_deletable', which indicates if a given save is + * safe for deletion + * - 'is_write_protected', which indicates if a given save + * can be overwritten by the user. + * (note: of course you do not have to + * set this, since it defaults to 'false') + */ + kSavesSupportMetaInfo, + + /** + * Features a thumbnail in savegames (i.e. includes a thumbnail + * in savestates returned via querySaveMetaInfo). + * This flag may only be set when 'kSavesSupportMetaInfo' is set. + */ + kSavesSupportThumbnail, + + /** + * Features 'save_date' and 'save_time' entries in the + * savestate returned by querySaveMetaInfo. Those values + * indicate the date/time the savegame was created. + * This flag may only be set when 'kSavesSupportMetaInfo' is set. + */ + kSavesSupportCreationDate, + + /** + * Features 'play_time' entry in the savestate returned by + * querySaveMetaInfo. It indicates how long the user played + * the game till the save. + * This flag may only be set when 'kSavesSupportMetaInfo' is set. + */ + kSavesSupportPlayTime + }; + + /** + * Determine whether the engine supports the specified MetaEngine feature. + * Used by e.g. the launcher to determine whether to enable the "Load" button. + */ + virtual bool hasFeature(MetaEngineFeature f) const { + return false; + } + + //@} +}; + + +// Engine plugins + +typedef PluginSubclass EnginePlugin; + +/** + * Singleton class which manages all Engine plugins. + */ +class EngineManager : public Common::Singleton { +private: + friend class Common::Singleton; + +public: + GameDescriptor findGame(const Common::String &gameName, const EnginePlugin **plugin = NULL) const; + GameList detectGames(const Common::FSList &fslist) const; + const EnginePlugin::List &getPlugins() const; +}; + +/** Convenience shortcut for accessing the engine manager. */ +#define EngineMan EngineManager::instance() + +#endif diff --git a/engines/module.mk b/engines/module.mk new file mode 100644 index 00000000000..20459c6159b --- /dev/null +++ b/engines/module.mk @@ -0,0 +1,9 @@ +MODULE := engines + +MODULE_OBJS := \ + advancedDetector.o \ + engine.o \ + game.o + +# Include common rules +include $(srcdir)/rules.mk