/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "common/md5.h" #include "common/events.h" #include "common/file.h" #include "common/memstream.h" #include "common/savefile.h" #include "common/config-manager.h" #include "common/debug-channels.h" #include "common/random.h" #include "common/textconsole.h" #include "engines/util.h" #include "base/plugins.h" #include "base/version.h" #include "graphics/cursorman.h" #include "audio/mididrv.h" #include "audio/mixer.h" #include "agi/agi.h" #include "agi/graphics.h" #include "agi/sprite.h" #include "agi/keyboard.h" #include "agi/menu.h" #include "gui/predictivedialog.h" namespace Agi { void AgiEngine::allowSynthetic(bool allow) { _allowSynthetic = allow; } void AgiEngine::processEvents() { Common::Event event; int key = 0; while (_eventMan->pollEvent(event)) { switch (event.type) { case Common::EVENT_PREDICTIVE_DIALOG: { GUI::PredictiveDialog _predictiveDialog; _predictiveDialog.runModal(); strcpy(_predictiveResult, _predictiveDialog.getResult()); if (strcmp(_predictiveResult, "")) { if (_game.inputMode == INPUT_NORMAL) { strcpy((char *)_game.inputBuffer, _predictiveResult); handleKeys(KEY_ENTER); } else if (_game.inputMode == INPUT_GETSTRING) { strcpy(_game.strings[_stringdata.str], _predictiveResult); newInputMode(INPUT_NORMAL); _gfx->printCharacter(_stringdata.x + strlen(_game.strings[_stringdata.str]) + 1, _stringdata.y, ' ', _game.colorFg, _game.colorBg); } else if (_game.inputMode == INPUT_NONE) { for (int n = 0; _predictiveResult[n]; n++) keyEnqueue(_predictiveResult[n]); } } /* if (predictiveDialog()) { if (_game.inputMode == INPUT_NORMAL) { strcpy((char *)_game.inputBuffer, _predictiveResult); handleKeys(KEY_ENTER); } else if (_game.inputMode == INPUT_GETSTRING) { strcpy(_game.strings[_stringdata.str], _predictiveResult); newInputMode(INPUT_NORMAL); _gfx->printCharacter(_stringdata.x + strlen(_game.strings[_stringdata.str]) + 1, _stringdata.y, ' ', _game.colorFg, _game.colorBg); } else if (_game.inputMode == INPUT_NONE) { for (int n = 0; _predictiveResult[n]; n++) keyEnqueue(_predictiveResult[n]); } } */ } break; case Common::EVENT_LBUTTONDOWN: if (_game.mouseEnabled) { key = BUTTON_LEFT; _mouse.button = kAgiMouseButtonLeft; keyEnqueue(key); _mouse.x = event.mouse.x; _mouse.y = event.mouse.y; } break; case Common::EVENT_RBUTTONDOWN: if (_game.mouseEnabled) { key = BUTTON_RIGHT; _mouse.button = kAgiMouseButtonRight; keyEnqueue(key); _mouse.x = event.mouse.x; _mouse.y = event.mouse.y; } break; case Common::EVENT_WHEELUP: if (_game.mouseEnabled) { key = WHEEL_UP; keyEnqueue(key); } break; case Common::EVENT_WHEELDOWN: if (_game.mouseEnabled) { key = WHEEL_DOWN; keyEnqueue(key); } break; case Common::EVENT_MOUSEMOVE: if (_game.mouseEnabled) { _mouse.x = event.mouse.x; _mouse.y = event.mouse.y; if (!_game.mouseFence.isEmpty()) { if (_mouse.x < _game.mouseFence.left) _mouse.x = _game.mouseFence.left; if (_mouse.x > _game.mouseFence.right) _mouse.x = _game.mouseFence.right; if (_mouse.y < _game.mouseFence.top) _mouse.y = _game.mouseFence.top; if (_mouse.y > _game.mouseFence.bottom) _mouse.y = _game.mouseFence.bottom; g_system->warpMouse(_mouse.x, _mouse.y); } } break; case Common::EVENT_LBUTTONUP: case Common::EVENT_RBUTTONUP: if (_game.mouseEnabled) { _mouse.button = kAgiMouseButtonUp; _mouse.x = event.mouse.x; _mouse.y = event.mouse.y; } break; case Common::EVENT_KEYDOWN: if (event.kbd.hasFlags(Common::KBD_CTRL) && event.kbd.keycode == Common::KEYCODE_d) { _console->attach(); break; } switch (key = event.kbd.keycode) { case Common::KEYCODE_LEFT: case Common::KEYCODE_KP4: if (_allowSynthetic || !event.synthetic) key = KEY_LEFT; break; case Common::KEYCODE_RIGHT: case Common::KEYCODE_KP6: if (_allowSynthetic || !event.synthetic) key = KEY_RIGHT; break; case Common::KEYCODE_UP: case Common::KEYCODE_KP8: if (_allowSynthetic || !event.synthetic) key = KEY_UP; break; case Common::KEYCODE_DOWN: case Common::KEYCODE_KP2: if (_allowSynthetic || !event.synthetic) key = KEY_DOWN; break; case Common::KEYCODE_PAGEUP: case Common::KEYCODE_KP9: if (_allowSynthetic || !event.synthetic) key = KEY_UP_RIGHT; break; case Common::KEYCODE_PAGEDOWN: case Common::KEYCODE_KP3: if (_allowSynthetic || !event.synthetic) key = KEY_DOWN_RIGHT; break; case Common::KEYCODE_HOME: case Common::KEYCODE_KP7: if (_allowSynthetic || !event.synthetic) key = KEY_UP_LEFT; break; case Common::KEYCODE_END: case Common::KEYCODE_KP1: if (_allowSynthetic || !event.synthetic) key = KEY_DOWN_LEFT; break; case Common::KEYCODE_KP5: key = KEY_STATIONARY; break; case Common::KEYCODE_PLUS: key = '+'; break; case Common::KEYCODE_MINUS: key = '-'; break; case Common::KEYCODE_TAB: key = 0x0009; break; case Common::KEYCODE_F1: key = 0x3b00; break; case Common::KEYCODE_F2: key = 0x3c00; break; case Common::KEYCODE_F3: key = 0x3d00; break; case Common::KEYCODE_F4: key = 0x3e00; break; case Common::KEYCODE_F5: key = 0x3f00; break; case Common::KEYCODE_F6: key = 0x4000; break; case Common::KEYCODE_F7: key = 0x4100; break; case Common::KEYCODE_F8: key = 0x4200; break; case Common::KEYCODE_F9: key = 0x4300; break; case Common::KEYCODE_F10: key = 0x4400; break; case Common::KEYCODE_F11: key = KEY_STATUSLN; break; case Common::KEYCODE_F12: key = KEY_PRIORITY; break; case Common::KEYCODE_ESCAPE: key = 0x1b; break; case Common::KEYCODE_RETURN: case Common::KEYCODE_KP_ENTER: key = KEY_ENTER; break; case Common::KEYCODE_BACKSPACE: key = KEY_BACKSPACE; break; default: // Not a special key, so get the ASCII code for it key = event.kbd.ascii; if (Common::isAlpha(key)) { // Key is A-Z. // Map Ctrl-A to 1, Ctrl-B to 2, etc. if (event.kbd.flags & Common::KBD_CTRL) { key = toupper(key) - 'A' + 1; } else if (event.kbd.flags & Common::KBD_ALT) { // Map Alt-A, Alt-B etc. to special scancode values according to an internal scancode table. key = scancodeTable[toupper(key) - 'A'] << 8; } } break; } if (key) keyEnqueue(key); break; case Common::EVENT_KEYUP: if (_egoHoldKey) _game.viewTable[0].direction = 0; default: break; } } } void AgiEngine::pollTimer() { _lastTick += 50; while (_system->getMillis() < _lastTick) { processEvents(); _console->onFrame(); _system->delayMillis(10); _system->updateScreen(); } _lastTick = _system->getMillis(); } void AgiEngine::pause(uint32 msec) { uint32 endTime = _system->getMillis() + msec; _gfx->setCursor(_renderMode == Common::kRenderAmiga, true); while (_system->getMillis() < endTime) { processEvents(); _system->updateScreen(); _system->delayMillis(10); } _gfx->setCursor(_renderMode == Common::kRenderAmiga); } void AgiEngine::initPriTable() { int i, p, y = 0; for (p = 1; p < 15; p++) { for (i = 0; i < 12; i++) { _game.priTable[y++] = p < 4 ? 4 : p; } } } int AgiEngine::agiInit() { int ec, i; debug(2, "initializing"); debug(2, "game version = 0x%x", getVersion()); // initialize with adj.ego.move.to.x.y(0, 0) so to speak _game.adjMouseX = _game.adjMouseY = 0; // reset all flags to false and all variables to 0 for (i = 0; i < MAX_FLAGS; i++) _game.flags[i] = 0; for (i = 0; i < MAX_VARS; i++) _game.vars[i] = 0; // clear all resources and events for (i = 0; i < MAX_DIRS; i++) { memset(&_game.views[i], 0, sizeof(struct AgiView)); memset(&_game.pictures[i], 0, sizeof(struct AgiPicture)); memset(&_game.logics[i], 0, sizeof(struct AgiLogic)); memset(&_game.sounds[i], 0, sizeof(class AgiSound *)); // _game.sounds contains pointers now memset(&_game.dirView[i], 0, sizeof(struct AgiDir)); memset(&_game.dirPic[i], 0, sizeof(struct AgiDir)); memset(&_game.dirLogic[i], 0, sizeof(struct AgiDir)); memset(&_game.dirSound[i], 0, sizeof(struct AgiDir)); } // clear view table for (i = 0; i < MAX_VIEWTABLE; i++) memset(&_game.viewTable[i], 0, sizeof(struct VtEntry)); initWords(); if (!_menu) _menu = new Menu(this, _gfx, _picture); initPriTable(); // Clear the string buffer on startup, but not when the game restarts, as // some scripts expect that the game strings remain unaffected after a // restart. An example is script 98 in SQ2, which is not invoked on restart // to ask Ego's name again. The name is supposed to be maintained in string 1. // Fixes bug #3292784. if (!_restartGame) { for (i = 0; i < MAX_STRINGS; i++) _game.strings[i][0] = 0; } // setup emulation switch (getVersion() >> 12) { case 2: debug("Emulating Sierra AGI v%x.%03x", (int)(getVersion() >> 12) & 0xF, (int)(getVersion()) & 0xFFF); break; case 3: debug("Emulating Sierra AGI v%x.002.%03x", (int)(getVersion() >> 12) & 0xF, (int)(getVersion()) & 0xFFF); break; } if (getPlatform() == Common::kPlatformAmiga) _game.gameFlags |= ID_AMIGA; if (getFeatures() & GF_AGDS) _game.gameFlags |= ID_AGDS; // Make the 256 color AGI screen the default AGI screen when AGI256 or AGI256-2 is used if (getFeatures() & (GF_AGI256 | GF_AGI256_2)) _game.sbuf = _game.sbuf256c; if (_game.gameFlags & ID_AMIGA) debug(1, "Amiga padded game detected."); if (_game.gameFlags & ID_AGDS) debug(1, "AGDS mode enabled."); ec = _loader->init(); // load vol files, etc if (ec == errOK) ec = _loader->loadObjects(OBJECTS); // note: demogs has no words.tok if (ec == errOK) ec = _loader->loadWords(WORDS); // FIXME: load IIgs instruments and samples // load_instruments("kq.sys16"); // Load logic 0 into memory if (ec == errOK) ec = _loader->loadResource(rLOGIC, 0); #ifdef __DS__ // Normally, the engine loads the predictive text dictionary when the predictive dialog // is shown. On the DS version, the word completion feature needs the dictionary too. // FIXME - loadDict() no long exists in AGI as this has been moved to within the // GUI Predictive Dialog, but DS Word Completion is probably broken due to this... #endif _egoHoldKey = false; _game.mouseFence.setWidth(0); // Reset return ec; } /* * Public functions */ void AgiEngine::agiUnloadResources() { int i; // Make sure logic 0 is always loaded for (i = 1; i < MAX_DIRS; i++) { _loader->unloadResource(rLOGIC, i); } for (i = 0; i < MAX_DIRS; i++) { _loader->unloadResource(rVIEW, i); _loader->unloadResource(rPICTURE, i); _loader->unloadResource(rSOUND, i); } } int AgiEngine::agiDeinit() { int ec; cleanInput(); // remove all words from memory agiUnloadResources(); // unload resources in memory _loader->unloadResource(rLOGIC, 0); ec = _loader->deinit(); unloadObjects(); unloadWords(); clearImageStack(); return ec; } int AgiEngine::agiLoadResource(int r, int n) { int i; i = _loader->loadResource(r, n); // WORKAROUND: Patches broken picture 147 in a corrupted Amiga version of Gold Rush! (v2.05 1989-03-09). // The picture can be seen in room 147 after dropping through the outhouse's hole in room 146. if (i == errOK && getGameID() == GID_GOLDRUSH && r == rPICTURE && n == 147 && _game.dirPic[n].len == 1982) { uint8 *pic = _game.pictures[n].rdata; Common::MemoryReadStream picStream(pic, _game.dirPic[n].len); Common::String md5str = Common::computeStreamMD5AsString(picStream, _game.dirPic[n].len); if (md5str == "1c685eb048656cedcee4eb6eca2cecea") { pic[0x042] = 0x4B; // 0x49 -> 0x4B pic[0x043] = 0x66; // 0x26 -> 0x66 pic[0x204] = 0x68; // 0x28 -> 0x68 pic[0x6C0] = 0x2D; // 0x25 -> 0x2D pic[0x6F0] = 0xF0; // 0x70 -> 0xF0 pic[0x734] = 0x6F; // 0x2F -> 0x6F } } return i; } int AgiEngine::agiUnloadResource(int r, int n) { return _loader->unloadResource(r, n); } struct GameSettings { const char *gameid; const char *description; byte id; uint32 features; const char *detectname; }; AgiBase::AgiBase(OSystem *syst, const AGIGameDescription *gameDesc) : Engine(syst), _gameDescription(gameDesc) { // Assign default values to the config manager, in case settings are missing ConfMan.registerDefault("originalsaveload", "false"); ConfMan.registerDefault("altamigapalette", "false"); ConfMan.registerDefault("mousesupport", "true"); _noSaveLoadAllowed = false; _rnd = new Common::RandomSource("agi"); _sound = 0; _fontData = NULL; initFeatures(); initVersion(); } AgiBase::~AgiBase() { delete _rnd; delete _sound; } void AgiBase::initRenderMode() { _renderMode = Common::kRenderEGA; if (ConfMan.hasKey("platform")) { Common::Platform platform = Common::parsePlatform(ConfMan.get("platform")); _renderMode = (platform == Common::kPlatformAmiga) ? Common::kRenderAmiga : Common::kRenderEGA; } if (ConfMan.hasKey("render_mode")) { Common::RenderMode tmpMode = Common::parseRenderMode(ConfMan.get("render_mode").c_str()); if (tmpMode != Common::kRenderDefault) _renderMode = tmpMode; } } AgiEngine::AgiEngine(OSystem *syst, const AGIGameDescription *gameDesc) : AgiBase(syst, gameDesc) { // Setup mixer syncSoundSettings(); parseFeatures(); DebugMan.addDebugChannel(kDebugLevelMain, "Main", "Generic debug level"); DebugMan.addDebugChannel(kDebugLevelResources, "Resources", "Resources debugging"); DebugMan.addDebugChannel(kDebugLevelSprites, "Sprites", "Sprites debugging"); DebugMan.addDebugChannel(kDebugLevelInventory, "Inventory", "Inventory debugging"); DebugMan.addDebugChannel(kDebugLevelInput, "Input", "Input events debugging"); DebugMan.addDebugChannel(kDebugLevelMenu, "Menu", "Menu debugging"); DebugMan.addDebugChannel(kDebugLevelScripts, "Scripts", "Scripts debugging"); DebugMan.addDebugChannel(kDebugLevelSound, "Sound", "Sound debugging"); DebugMan.addDebugChannel(kDebugLevelText, "Text", "Text output debugging"); DebugMan.addDebugChannel(kDebugLevelSavegame, "Savegame", "Saving & restoring game debugging"); memset(&_game, 0, sizeof(struct AgiGame)); memset(&_debug, 0, sizeof(struct AgiDebug)); memset(&_mouse, 0, sizeof(struct Mouse)); _game.mouseEnabled = true; if (!ConfMan.getBool("mousesupport")) { // we effectively disable the mouse for games, that explicitly do not want mouse support to be enabled _game.mouseEnabled = false; } // We are currently using the custom font for all fanmade games if (!(getFeatures() & (GF_FANMADE | GF_AGDS))) { _fontData = fontData_Sierra; // original Sierra font } else { _fontData = fontData_FanGames; // our (own?) custom font, that supports umlauts etc. } _game._vm = this; _game.clockEnabled = false; _game.state = STATE_INIT; _keyQueueStart = 0; _keyQueueEnd = 0; _allowSynthetic = false; _intobj = NULL; _menu = NULL; _menuSelected = false; _lastSentence[0] = 0; memset(&_stringdata, 0, sizeof(struct StringData)); _objects = NULL; _restartGame = false; _oldMode = INPUT_NONE; _firstSlot = 0; resetControllers(); setupOpcodes(); _game._curLogic = NULL; _timerHack = 0; _lastSaveTime = 0; _lastTick = 0; memset(_keyQueue, 0, sizeof(_keyQueue)); memset(_predictiveResult, 0, sizeof(_predictiveResult)); _sprites = NULL; _picture = NULL; _loader = NULL; _console = NULL; _egoHoldKey = false; } void AgiEngine::initialize() { // TODO: Some sound emulation modes do not fit our current music // drivers, and I'm not sure what they are. For now, they might // as well be called "PC Speaker" and "Not PC Speaker". // If used platform is Apple IIGS then we must use Apple IIGS sound emulation // because Apple IIGS AGI games use only Apple IIGS specific sound resources. if (getPlatform() == Common::kPlatformApple2GS) { _soundemu = SOUND_EMU_APPLE2GS; } else if (getPlatform() == Common::kPlatformCoCo3) { _soundemu = SOUND_EMU_COCO3; } else if (ConfMan.get("music_driver") == "auto") { // Default sound is the proper PCJr emulation _soundemu = SOUND_EMU_PCJR; } else { switch (MidiDriver::getMusicType(MidiDriver::detectDevice(MDT_PCSPK|MDT_AMIGA|MDT_ADLIB|MDT_PCJR|MDT_MIDI))) { case MT_PCSPK: _soundemu = SOUND_EMU_PC; break; case MT_ADLIB: _soundemu = SOUND_EMU_NONE; break; case MT_PCJR: _soundemu = SOUND_EMU_PCJR; break; case MT_AMIGA: _soundemu = SOUND_EMU_AMIGA; break; default: debug(0, "DEF"); _soundemu = SOUND_EMU_MIDI; break; } } initRenderMode(); _buttonStyle = AgiButtonStyle(_renderMode); _defaultButtonStyle = AgiButtonStyle(); _console = new Console(this); _gfx = new GfxMgr(this); _sound = new SoundMgr(this, _mixer); _picture = new PictureMgr(this, _gfx); _sprites = new SpritesMgr(this, _gfx); _gfx->initMachine(); _game.gameFlags = 0; _game.colorFg = 15; _game.colorBg = 0; _game.name[0] = '\0'; _game.sbufOrig = (uint8 *)calloc(_WIDTH, _HEIGHT * 2); // Allocate space for two AGI screens vertically _game.sbuf16c = _game.sbufOrig + SBUF16_OFFSET; // Make sbuf16c point to the 16 color (+control line & priority info) AGI screen _game.sbuf256c = _game.sbufOrig + SBUF256_OFFSET; // Make sbuf256c point to the 256 color AGI screen _game.sbuf = _game.sbuf16c; // Make sbuf point to the 16 color (+control line & priority info) AGI screen by default _gfx->initVideo(); _lastSaveTime = 0; _lastTick = _system->getMillis(); debugC(2, kDebugLevelMain, "Detect game"); if (agiDetectGame() == errOK) { _game.state = STATE_LOADED; debugC(2, kDebugLevelMain, "game loaded"); } else { warning("Could not open AGI game"); } debugC(2, kDebugLevelMain, "Init sound"); } AgiEngine::~AgiEngine() { // If the engine hasn't been initialized yet via // AgiEngine::initialize(), don't attempt to free any resources, as // they haven't been allocated. Fixes bug #1742432 - AGI: Engine // crashes if no game is detected if (_game.state == STATE_INIT) { return; } agiDeinit(); delete _loader; _gfx->deinitVideo(); delete _sprites; delete _picture; free(_game.sbufOrig); _gfx->deinitMachine(); delete _gfx; delete _console; } Common::Error AgiBase::init() { // Initialize backend initGraphics(320, 200, false); initialize(); _gfx->gfxSetPalette(); return Common::kNoError; } Common::Error AgiEngine::go() { if (_game.mouseEnabled) { CursorMan.showMouse(true); } setTotalPlayTime(0); if (_game.state < STATE_LOADED) { do { mainCycle(); } while (_game.state < STATE_RUNNING); } runGame(); return Common::kNoError; } void AgiEngine::parseFeatures() { /* FIXME: Seems this method doesn't really do anything. It might be a leftover that could be removed, except that some of its intended purpose may still need to be reimplemented. [0:29] can you tell me what the point behind AgiEngine::parseFeatures() is? [0:30] <_sev> when games are created with WAGI studio [0:31] <_sev> it creates .wag site with game-specific features such as full game title, whether to use AGIMOUSE etc [0:32] ... and the "features" config key is created by our detector based on the wag file, I guess? [0:33] <_sev> yes [0:33] it's just that I cant seem to find a place we do that [0:33] <_sev> it is used for fallback [0:34] <_sev> ah, perhaps it was not updated [0:34] I only see us check the value, but never set it [0:34] maybe I am grepping wrong, who knows :) [0:44] _sev: so, unless I miss something, it seem that function does nothing right now [0:45] <_sev> Fingolfin: it could be unfinished. It was part of GSoC 3 years ago [0:45] well [0:45] <_sev> I just don't remember [0:45] but don't we just re-parse the wag when the game is loaded anyway? [0:45] <_sev> but it documents the format [0:45] the advanced meta engine would re-run the detector, wouldn't it? [0:45] <_sev> yep [0:47] so... shouldn't we at least add a comment to the function explaining what it does and that it's unfinished etc.? maybe add a TODO to the wiki? [0:47] otherwise it might stay as it is for another 3 years :) */ if (!ConfMan.hasKey("features")) return; char *features = strdup(ConfMan.get("features").c_str()); const char *feature[100]; int numFeatures = 0; char *tok = strtok(features, " "); if (tok) { do { feature[numFeatures++] = tok; } while ((tok = strtok(NULL, " ")) != NULL); } else { feature[numFeatures++] = features; } const struct Flags { const char *name; uint32 flag; } flags[] = { { "agimouse", GF_AGIMOUSE }, { "agds", GF_AGDS }, { "agi256", GF_AGI256 }, { "agi256-2", GF_AGI256_2 }, { "agipal", GF_AGIPAL }, { 0, 0 } }; for (int i = 0; i < numFeatures; i++) { for (const Flags *flag = flags; flag->name; flag++) { if (!scumm_stricmp(feature[i], flag->name)) { debug(2, "Added feature: %s", flag->name); setFeature(flag->flag); break; } } } free(features); } } // End of namespace Agi