/* 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/scummsys.h" #include "common/config-manager.h" #include "common/debug.h" #include "common/error.h" #include "common/file.h" #include "common/system.h" #include "common/events.h" #include "common/stream.h" #include "common/savefile.h" #include "common/random.h" #include "engines/util.h" #include "graphics/palette.h" #include "graphics/thumbnail.h" #include "adl/adl.h" #include "adl/display_a2.h" #include "adl/detection.h" #include "adl/graphics.h" #include "adl/sound.h" namespace Adl { class ScriptEnv_6502 : public ScriptEnv { public: ScriptEnv_6502(const Command &cmd, byte room, byte verb, byte noun) : ScriptEnv(cmd, room, verb, noun), _remCond(cmd.numCond), _remAct(cmd.numAct) { } private: kOpType getOpType() const { if (_remCond > 0) return kOpTypeCond; if (_remAct > 0) return kOpTypeAct; return kOpTypeDone; } void next(uint numArgs) { _ip += numArgs + 1; if (_remCond > 0) --_remCond; else if (_remAct > 0) --_remAct; } byte _remCond, _remAct; }; AdlEngine::~AdlEngine() { delete _display; delete _graphics; delete _console; delete _dumpFile; delete _inputScript; delete _random; } AdlEngine::AdlEngine(OSystem *syst, const AdlGameDescription *gd) : Engine(syst), _dumpFile(nullptr), _display(nullptr), _graphics(nullptr), _textMode(false), _linesPrinted(0), _isRestarting(false), _isRestoring(false), _isQuitting(false), _abortScript(false), _gameDescription(gd), _inputScript(nullptr), _scriptDelay(1000), _scriptPaused(false), _console(nullptr), _messageIds(), _saveVerb(0), _saveNoun(0), _restoreVerb(0), _restoreNoun(0), _canSaveNow(false), _canRestoreNow(false) { _random = new Common::RandomSource("adl"); DebugMan.addDebugChannel(kDebugChannelScript, "Script", "Trace script execution"); } bool AdlEngine::pollEvent(Common::Event &event) const { _console->onFrame(); if (g_system->getEventManager()->pollEvent(event)) { if (event.type != Common::EVENT_KEYDOWN) return false; if (event.kbd.flags & Common::KBD_CTRL) { if (event.kbd.keycode == Common::KEYCODE_q) { quitGame(); return false; } if (event.kbd.keycode == Common::KEYCODE_d) { _console->attach(); return false; } } return true; } return false; } Common::String AdlEngine::readString(Common::ReadStream &stream, byte until) const { Common::String str; while (1) { byte b = stream.readByte(); if (stream.eos() || stream.err()) error("Error reading string"); if (b == until) break; str += b; }; return str; } Common::String AdlEngine::readStringAt(Common::SeekableReadStream &stream, uint offset, byte until) const { stream.seek(offset); return readString(stream, until); } void AdlEngine::openFile(Common::File &file, const Common::String &name) const { if (!file.open(name)) error("Error opening '%s'", name.c_str()); } void AdlEngine::printMessage(uint idx) { printString(loadMessage(idx)); } Common::String AdlEngine::getItemDescription(const Item &item) const { if (item.description > 0) return loadMessage(item.description); else return Common::String(); } void AdlEngine::delay(uint32 ms) const { if (_inputScript && !_scriptPaused) return; uint32 now = g_system->getMillis(); const uint32 end = now + ms; while (!shouldQuit() && now < end) { Common::Event event; pollEvent(event); g_system->delayMillis(end - now < 16 ? end - now : 16); now = g_system->getMillis(); } } Common::String AdlEngine::inputString(byte prompt) const { Common::String s; if (prompt > 0) _display->printString(Common::String(prompt)); while (1) { byte b = inputKey(); if (_inputScript) { // If debug script is active, read input line from file Common::String line(getScriptLine()); // Debug script terminated, go back to keyboard input if (line.empty()) continue; line += '\r'; Common::String native; for (uint i = 0; i < line.size(); ++i) native += _display->asciiToNative(line[i]); _display->printString(native); // Set pause flag to activate regular behaviour of delay and inputKey _scriptPaused = true; if (_scriptDelay > 0) delay(_scriptDelay); else inputKey(); _scriptPaused = false; return native; } if (shouldQuit() || _isRestoring) return Common::String(); if (b == 0) continue; if (b == ('\r' | 0x80)) { s += b; _display->printString(Common::String(b)); return s; } if (b < 0xa0) { switch (b) { case Common::KEYCODE_BACKSPACE | 0x80: if (!s.empty()) { _display->moveCursorBackward(); _display->setCharAtCursor(_display->asciiToNative(' ')); s.deleteLastChar(); } break; default: break; }; } else { if (s.size() < 255) { s += b; _display->printString(Common::String(b)); } } } } byte AdlEngine::inputKey(bool showCursor) const { byte key = 0; // If debug script is active, we fake a return press for the text overflow handling if (_inputScript && !_scriptPaused) return _display->asciiToNative('\r'); if (showCursor) _display->showCursor(true); while (!shouldQuit() && !_isRestoring && key == 0) { Common::Event event; if (pollEvent(event)) { if (event.type != Common::EVENT_KEYDOWN) continue; switch (event.kbd.keycode) { case Common::KEYCODE_BACKSPACE: case Common::KEYCODE_RETURN: key = convertKey(event.kbd.keycode); break; default: if (event.kbd.ascii >= 0x20 && event.kbd.ascii < 0x80) key = convertKey(event.kbd.ascii); }; } // If debug script was activated in the meantime, abort input if (_inputScript && !_scriptPaused) return _display->asciiToNative('\r'); _display->renderText(); g_system->delayMillis(16); } _display->showCursor(false); return key; } void AdlEngine::loadWords(Common::ReadStream &stream, WordMap &map, Common::StringArray &pri) const { uint index = 0; map.clear(); pri.clear(); while (1) { ++index; byte buf[IDI_WORD_SIZE]; if (stream.read(buf, IDI_WORD_SIZE) < IDI_WORD_SIZE) error("Error reading word list"); Common::String word((char *)buf, IDI_WORD_SIZE); if (!map.contains(word)) map[word] = index; pri.push_back(Console::toAscii(word)); byte synonyms = stream.readByte(); if (stream.err() || stream.eos()) error("Error reading word list"); if (synonyms == 0xff) break; // WORKAROUND: Missing verb list terminator in hires3 if (getGameType() == GAME_TYPE_HIRES3 && index == 72 && synonyms == 0) return; // WORKAROUND: Missing noun list terminator in hires3 if (getGameType() == GAME_TYPE_HIRES3 && index == 113) return; // WORKAROUND: Missing noun list terminator in hires5 region 15 if (getGameType() == GAME_TYPE_HIRES5 && _state.region == 15 && index == 81) return; for (uint i = 0; i < synonyms; ++i) { if (stream.read((char *)buf, IDI_WORD_SIZE) < IDI_WORD_SIZE) error("Error reading word list"); word = Common::String((char *)buf, IDI_WORD_SIZE); if (!map.contains(word)) map[word] = index; } } } void AdlEngine::readCommands(Common::ReadStream &stream, Commands &commands) { commands.clear(); while (1) { Command command; command.room = stream.readByte(); if (command.room == 0xff) return; command.verb = stream.readByte(); command.noun = stream.readByte(); byte scriptSize = stream.readByte() - 6; command.numCond = stream.readByte(); command.numAct = stream.readByte(); for (uint i = 0; i < scriptSize; ++i) command.script.push_back(stream.readByte()); if (stream.eos() || stream.err()) error("Failed to read commands"); if (command.numCond == 0 && command.script[0] == IDO_ACT_SAVE) { _saveVerb = command.verb; _saveNoun = command.noun; } if (command.numCond == 0 && command.script[0] == IDO_ACT_LOAD) { _restoreVerb = command.verb; _restoreNoun = command.noun; } commands.push_back(command); } } void AdlEngine::removeCommand(Commands &commands, uint idx) { Commands::iterator cmds; uint i = 0; for (cmds = commands.begin(); cmds != commands.end(); ++cmds) { if (i++ == idx) { commands.erase(cmds); return; } } error("Command %d not found", idx); } Command &AdlEngine::getCommand(Commands &commands, uint idx) { Commands::iterator cmds; uint i = 0; for (cmds = commands.begin(); cmds != commands.end(); ++cmds) { if (i++ == idx) return *cmds; } error("Command %d not found", idx); } void AdlEngine::checkInput(byte verb, byte noun) { // Try room-local command list first if (doOneCommand(_roomData.commands, verb, noun)) return; // If no match was found, try the global list if (!doOneCommand(_roomCommands, verb, noun)) printMessage(_messageIds.dontUnderstand); } bool AdlEngine::isInputValid(byte verb, byte noun, bool &is_any) { if (isInputValid(_roomData.commands, verb, noun, is_any)) return true; return isInputValid(_roomCommands, verb, noun, is_any); } bool AdlEngine::isInputValid(const Commands &commands, byte verb, byte noun, bool &is_any) { Commands::const_iterator cmd; is_any = false; for (cmd = commands.begin(); cmd != commands.end(); ++cmd) { Common::ScopedPtr env(createScriptEnv(*cmd, _state.room, verb, noun)); if (matchCommand(*env)) { if (cmd->verb == IDI_ANY || cmd->noun == IDI_ANY) is_any = true; return true; } } return false; } void AdlEngine::setupOpcodeTables() { _condOpcodes.resize(0x0b); _condOpcodes[0x03] = opcode(&AdlEngine::o_isItemInRoom); _condOpcodes[0x05] = opcode(&AdlEngine::o_isMovesGT); _condOpcodes[0x06] = opcode(&AdlEngine::o_isVarEQ); _condOpcodes[0x09] = opcode(&AdlEngine::o_isCurPicEQ); _condOpcodes[0x0a] = opcode(&AdlEngine::o_isItemPicEQ); _actOpcodes.resize(0x1e); _actOpcodes[0x01] = opcode(&AdlEngine::o_varAdd); _actOpcodes[0x02] = opcode(&AdlEngine::o_varSub); _actOpcodes[0x03] = opcode(&AdlEngine::o_varSet); _actOpcodes[0x04] = opcode(&AdlEngine::o_listInv); _actOpcodes[0x05] = opcode(&AdlEngine::o_moveItem); _actOpcodes[0x06] = opcode(&AdlEngine::o_setRoom); _actOpcodes[0x07] = opcode(&AdlEngine::o_setCurPic); _actOpcodes[0x08] = opcode(&AdlEngine::o_setPic); _actOpcodes[0x09] = opcode(&AdlEngine::o_printMsg); _actOpcodes[0x0a] = opcode(&AdlEngine::o_setLight); _actOpcodes[0x0b] = opcode(&AdlEngine::o_setDark); _actOpcodes[0x0d] = opcode(&AdlEngine::o_quit); _actOpcodes[0x0f] = opcode(&AdlEngine::o_save); _actOpcodes[0x10] = opcode(&AdlEngine::o_restore); _actOpcodes[0x11] = opcode(&AdlEngine::o_restart); _actOpcodes[0x12] = opcode(&AdlEngine::o_placeItem); _actOpcodes[0x13] = opcode(&AdlEngine::o_setItemPic); _actOpcodes[0x14] = opcode(&AdlEngine::o_resetPic); _actOpcodes[0x15] = opcode(&AdlEngine::o_goNorth); _actOpcodes[0x16] = opcode(&AdlEngine::o_goSouth); _actOpcodes[0x17] = opcode(&AdlEngine::o_goEast); _actOpcodes[0x18] = opcode(&AdlEngine::o_goWest); _actOpcodes[0x19] = opcode(&AdlEngine::o_goUp); _actOpcodes[0x1a] = opcode(&AdlEngine::o_goDown); _actOpcodes[0x1b] = opcode(&AdlEngine::o_takeItem); _actOpcodes[0x1c] = opcode(&AdlEngine::o_dropItem); _actOpcodes[0x1d] = opcode(&AdlEngine::o_setRoomPic); } void AdlEngine::initState() { _state = State(); initGameState(); } void AdlEngine::switchRoom(byte roomNr) { getCurRoom().curPicture = getCurRoom().picture; _state.room = roomNr; } byte AdlEngine::roomArg(byte room) const { return room; } void AdlEngine::loadDroppedItemOffsets(Common::ReadStream &stream, byte count) { for (uint i = 0; i < count; ++i) { Common::Point p; p.x = stream.readByte(); p.y = stream.readByte(); _itemOffsets.push_back(p); } } void AdlEngine::drawPic(byte pic, Common::Point pos) const { if (_roomData.pictures.contains(pic)) _graphics->drawPic(*_roomData.pictures[pic]->createReadStream(), pos); else if (_pictures.contains(pic)) _graphics->drawPic(*_pictures[pic]->createReadStream(), pos); else error("Picture %d not found", pic); } void AdlEngine::bell(uint count) const { Tones tones; for (uint i = 0; i < count - 1; ++i) { tones.push_back(Tone(940.0, 100.0)); tones.push_back(Tone(0.0, 12.0)); } tones.push_back(Tone(940.0, 100.0)); playTones(tones, false); } bool AdlEngine::playTones(const Tones &tones, bool isMusic, bool allowSkip) const { if (_inputScript && !_scriptPaused) return false; Audio::SoundHandle handle; Audio::AudioStream *stream = new Sound(tones); g_system->getMixer()->playStream((isMusic ? Audio::Mixer::kMusicSoundType : Audio::Mixer::kSFXSoundType), &handle, stream, -1, 25); while (!g_engine->shouldQuit() && g_system->getMixer()->isSoundHandleActive(handle)) { Common::Event event; pollEvent(event); if (allowSkip && event.type == Common::EVENT_KEYDOWN) { // FIXME: Preserve this event g_system->getMixer()->stopHandle(handle); return true; } g_system->delayMillis(16); } return false; } const Region &AdlEngine::getRegion(uint i) const { if (i < 1 || i > _state.regions.size()) error("Region %i out of range [1, %i]", i, _state.regions.size()); return _state.regions[i - 1]; } Region &AdlEngine::getRegion(uint i) { if (i < 1 || i > _state.regions.size()) error("Region %i out of range [1, %i]", i, _state.regions.size()); return _state.regions[i - 1]; } const Room &AdlEngine::getRoom(uint i) const { if (i < 1 || i > _state.rooms.size()) error("Room %i out of range [1, %i]", i, _state.rooms.size()); return _state.rooms[i - 1]; } Room &AdlEngine::getRoom(uint i) { if (i < 1 || i > _state.rooms.size()) error("Room %i out of range [1, %i]", i, _state.rooms.size()); return _state.rooms[i - 1]; } const Region &AdlEngine::getCurRegion() const { return getRegion(_state.region); } Region &AdlEngine::getCurRegion() { return getRegion(_state.region); } const Room &AdlEngine::getCurRoom() const { return getRoom(_state.room); } Room &AdlEngine::getCurRoom() { return getRoom(_state.room); } const Item &AdlEngine::getItem(uint i) const { Common::List::const_iterator item; for (item = _state.items.begin(); item != _state.items.end(); ++item) if (item->id == i) return *item; error("Item %i not found", i); } Item &AdlEngine::getItem(uint i) { Common::List::iterator item; for (item = _state.items.begin(); item != _state.items.end(); ++item) if (item->id == i) return *item; error("Item %i not found", i); } byte AdlEngine::getVar(uint i) const { if (i >= _state.vars.size()) error("Variable %i out of range [0, %i]", i, _state.vars.size() - 1); return _state.vars[i]; } void AdlEngine::setVar(uint i, byte value) { if (i >= _state.vars.size()) error("Variable %i out of range [0, %i]", i, _state.vars.size() - 1); _state.vars[i] = value; } void AdlEngine::takeItem(byte noun) { Common::List::iterator item; for (item = _state.items.begin(); item != _state.items.end(); ++item) { if (item->noun == noun && item->room == _state.room && item->region == _state.region) { if (item->state == IDI_ITEM_DOESNT_MOVE) { printMessage(_messageIds.itemDoesntMove); return; } if (item->state == IDI_ITEM_DROPPED) { item->room = IDI_ANY; return; } Common::Array::const_iterator pic; for (pic = item->roomPictures.begin(); pic != item->roomPictures.end(); ++pic) { if (*pic == getCurRoom().curPicture) { item->room = IDI_ANY; item->state = IDI_ITEM_DROPPED; return; } } } } printMessage(_messageIds.itemNotHere); } void AdlEngine::dropItem(byte noun) { Common::List::iterator item; for (item = _state.items.begin(); item != _state.items.end(); ++item) { if (item->noun == noun && item->room == IDI_ANY) { item->room = _state.room; item->region = _state.region; item->state = IDI_ITEM_DROPPED; return; } } printMessage(_messageIds.dontUnderstand); } void AdlEngine::gameLoop() { uint verb = 0, noun = 0; _isRestarting = false; // When restoring from the launcher, we don't read // input on the first iteration. This is needed to // ensure that restoring from the launcher and // restoring in-game brings us to the same game state. // (Also see comment below.) if (!_isRestoring) { showRoom(); if (_isRestarting) return; _canSaveNow = _canRestoreNow = true; getInput(verb, noun); _canSaveNow = _canRestoreNow = false; if (shouldQuit()) return; _linesPrinted = 0; // If we just restored from the GMM, we skip this command // set, as no command has been input by the user if (!_isRestoring) checkInput(verb, noun); } if (_isRestoring) { // We restored from the GMM or launcher. As restoring // with "RESTORE GAME" does not end command processing, // we don't break it off here either. This essentially // means that restoring a game will always run through // the global commands and increase the move counter // before the first user input. _display->printAsciiString("\r"); _isRestoring = false; verb = _restoreVerb; noun = _restoreNoun; } // Restarting does end command processing if (_isRestarting) return; doAllCommands(_globalCommands, verb, noun); if (_isRestarting) return; advanceClock(); _state.moves++; } Common::Error AdlEngine::run() { _display = Display_A2_create(); if (!_display) return Common::kUnsupportedColorMode; _console = new Console(this); _display->init(); setupOpcodeTables(); init(); int saveSlot = ConfMan.getInt("save_slot"); if (saveSlot >= 0) { if (loadGameState(saveSlot).getCode() != Common::kNoError) error("Failed to load save game from slot %i", saveSlot); _display->moveCursorTo(Common::Point(0, 23)); _isRestoring = true; } else { runIntro(); initState(); _display->printAsciiString(_strings.lineFeeds); } _display->setMode(Display::kModeMixed); while (!(_isQuitting || shouldQuit())) gameLoop(); return Common::kNoError; } bool AdlEngine::hasFeature(EngineFeature f) const { switch (f) { case kSupportsLoadingDuringRuntime: case kSupportsSavingDuringRuntime: case kSupportsRTL: return true; default: return false; } } Common::String AdlEngine::getScriptLine() const { Common::String line; do { line = _inputScript->readLine(); if (_inputScript->eos() || _inputScript->err()) { stopScript(); return Common::String(); } line.trim(); } while (line.size() == 0 || line.firstChar() == ';'); return line; } void AdlEngine::runScript(const char *filename) const { // Debug functionality to read input from a text file _inputScript = new Common::File; if (!_inputScript->open(filename)) { stopScript(); return; } Common::String line(getScriptLine()); if (!line.empty()) { // Read random seed _random->setSeed((uint32)line.asUint64()); } } void AdlEngine::stopScript() const { delete _inputScript; _inputScript = nullptr; } void AdlEngine::loadState(Common::ReadStream &stream) { _state.room = stream.readByte(); _state.moves = stream.readByte(); _state.isDark = stream.readByte(); _state.time.hours = stream.readByte(); _state.time.minutes = stream.readByte(); uint32 size = stream.readUint32BE(); if (size != _state.rooms.size()) error("Room count mismatch (expected %i; found %i)", _state.rooms.size(), size); for (uint i = 0; i < size; ++i) { _state.rooms[i].picture = stream.readByte(); _state.rooms[i].curPicture = stream.readByte(); _state.rooms[i].isFirstTime = stream.readByte(); } // NOTE: _state.curPicture is part of the save state in the original engine. We // reconstruct it instead. This is believed to be safe for at least hires 0-2, but // this may need to be re-evaluated for later games. _state.curPicture = getCurRoom().curPicture; size = stream.readUint32BE(); if (size != _state.items.size()) error("Item count mismatch (expected %i; found %i)", _state.items.size(), size); Common::List::iterator item; for (item = _state.items.begin(); item != _state.items.end(); ++item) { item->room = stream.readByte(); item->picture = stream.readByte(); item->position.x = stream.readByte(); item->position.y = stream.readByte(); item->state = stream.readByte(); } size = stream.readUint32BE(); if (size != _state.vars.size()) error("Variable count mismatch (expected %i; found %i)", _state.vars.size(), size); for (uint i = 0; i < size; ++i) _state.vars[i] = stream.readByte(); } Common::Error AdlEngine::loadGameState(int slot) { Common::String fileName = Common::String::format("%s.s%02d", _targetName.c_str(), slot); Common::InSaveFile *inFile = getSaveFileManager()->openForLoading(fileName); if (!inFile) { warning("Failed to open file '%s'", fileName.c_str()); return Common::kUnknownError; } if (inFile->readUint32BE() != MKTAG('A', 'D', 'L', ':')) { warning("No header found in '%s'", fileName.c_str()); delete inFile; return Common::kUnknownError; } byte saveVersion = inFile->readByte(); if (saveVersion != SAVEGAME_VERSION) { warning("Save game version %i not supported", saveVersion); delete inFile; return Common::kUnknownError; } // Skip description inFile->seek(SAVEGAME_NAME_LEN, SEEK_CUR); // Skip save time inFile->seek(6, SEEK_CUR); uint32 playTime = inFile->readUint32BE(); Graphics::skipThumbnail(*inFile); initState(); loadState(*inFile); if (inFile->err() || inFile->eos()) error("Failed to load game '%s'", fileName.c_str()); delete inFile; setTotalPlayTime(playTime); _isRestoring = true; return Common::kNoError; } bool AdlEngine::canLoadGameStateCurrently() { return _canRestoreNow; } void AdlEngine::saveState(Common::WriteStream &stream) { stream.writeByte(_state.room); stream.writeByte(_state.moves); stream.writeByte(_state.isDark); stream.writeByte(_state.time.hours); stream.writeByte(_state.time.minutes); stream.writeUint32BE(_state.rooms.size()); for (uint i = 0; i < _state.rooms.size(); ++i) { stream.writeByte(_state.rooms[i].picture); stream.writeByte(_state.rooms[i].curPicture); stream.writeByte(_state.rooms[i].isFirstTime); } stream.writeUint32BE(_state.items.size()); Common::List::const_iterator item; for (item = _state.items.begin(); item != _state.items.end(); ++item) { stream.writeByte(item->room); stream.writeByte(item->picture); stream.writeByte(item->position.x); stream.writeByte(item->position.y); stream.writeByte(item->state); } stream.writeUint32BE(_state.vars.size()); for (uint i = 0; i < _state.vars.size(); ++i) stream.writeByte(_state.vars[i]); } Common::Error AdlEngine::saveGameState(int slot, const Common::String &desc) { Common::String fileName = Common::String::format("%s.s%02d", _targetName.c_str(), slot); Common::OutSaveFile *outFile = getSaveFileManager()->openForSaving(fileName); if (!outFile) { warning("Failed to open file '%s'", fileName.c_str()); return Common::kUnknownError; } outFile->writeUint32BE(MKTAG('A', 'D', 'L', ':')); outFile->writeByte(SAVEGAME_VERSION); char name[SAVEGAME_NAME_LEN] = { }; if (!desc.empty()) strncpy(name, desc.c_str(), sizeof(name) - 1); else { Common::String defaultName("Save "); defaultName += 'A' + slot; strncpy(name, defaultName.c_str(), sizeof(name) - 1); } outFile->write(name, sizeof(name)); TimeDate t; g_system->getTimeAndDate(t); outFile->writeUint16BE(t.tm_year); outFile->writeByte(t.tm_mon); outFile->writeByte(t.tm_mday); outFile->writeByte(t.tm_hour); outFile->writeByte(t.tm_min); uint32 playTime = getTotalPlayTime(); outFile->writeUint32BE(playTime); Graphics::saveThumbnail(*outFile); saveState(*outFile); outFile->finalize(); if (outFile->err()) { delete outFile; warning("Failed to save game '%s'", fileName.c_str()); return Common::kUnknownError; } delete outFile; return Common::kNoError; } bool AdlEngine::canSaveGameStateCurrently() { if (!_canSaveNow) return false; Commands::const_iterator cmd; // Here we check whether or not the game currently accepts the command // "SAVE GAME". This prevents saving via the GMM in situations where // it wouldn't otherwise be possible to do so. for (cmd = _roomData.commands.begin(); cmd != _roomData.commands.end(); ++cmd) { Common::ScopedPtr env(createScriptEnv(*cmd, _state.room, _saveVerb, _saveNoun)); if (matchCommand(*env)) return env->op() == IDO_ACT_SAVE; } for (cmd = _roomCommands.begin(); cmd != _roomCommands.end(); ++cmd) { Common::ScopedPtr env(createScriptEnv(*cmd, _state.room, _saveVerb, _saveNoun)); if (matchCommand(*env)) return env->op() == IDO_ACT_SAVE; } return false; } byte AdlEngine::convertKey(uint16 ascii) const { ascii = toupper(ascii); if (ascii >= 0x80) return 0; ascii |= 0x80; if (ascii >= 0x80 && ascii <= 0xe0) return ascii; return 0; } Common::String AdlEngine::getLine() { while (1) { Common::String line = inputString(_display->asciiToNative('?')); if (shouldQuit() || _isRestoring) return Common::String(); if ((byte)line[0] == ('\r' | 0x80)) { _textMode = !_textMode; _display->setMode(_textMode ? Display::kModeText : Display::kModeMixed); continue; } // Remove the return line.deleteLastChar(); return line; } } Common::String AdlEngine::getWord(const Common::String &line, uint &index) const { Common::String str; const char spaceChar = _display->asciiToNative(' '); for (uint i = 0; i < 8; ++i) str += spaceChar; int copied = 0; // Skip initial whitespace while (1) { if (index == line.size()) return str; if (line[index] != spaceChar) break; ++index; } // Copy up to 8 characters while (1) { if (copied < 8) str.setChar(line[index], copied++); index++; if (index == line.size() || line[index] == spaceChar) return str; } } Common::String AdlEngine::formatVerbError(const Common::String &verb) const { Common::String err = _strings.verbError; for (uint i = 0; i < verb.size(); ++i) err.setChar(verb[i], i + 19); return err; } Common::String AdlEngine::formatNounError(const Common::String &verb, const Common::String &noun) const { Common::String err = _strings.nounError; for (uint i = 0; i < verb.size(); ++i) err.setChar(verb[i], i + 19); for (uint i = 0; i < noun.size(); ++i) err.setChar(noun[i], i + 30); return err; } void AdlEngine::getInput(uint &verb, uint &noun) { while (1) { _display->printString(_strings.enterCommand); Common::String line = getLine(); if (shouldQuit() || _isRestoring) return; uint index = 0; Common::String verbString = getWord(line, index); if (!_verbs.contains(verbString)) { _display->printString(formatVerbError(verbString)); continue; } verb = _verbs[verbString]; Common::String nounString = getWord(line, index); if (!_nouns.contains(nounString)) { _display->printString(formatNounError(verbString, nounString)); continue; } noun = _nouns[nounString]; return; } } bool AdlEngine::op_debug(const char *fmt, ...) const { if (DebugMan.isDebugChannelEnabled(kDebugChannelScript)) { va_list va; va_start(va, fmt); Common::String output = Common::String::vformat(fmt, va); va_end(va); output += '\n'; if (_dumpFile) { _dumpFile->write(output.c_str(), output.size()); return true; } else debugN("%s", output.c_str()); } return false; } int AdlEngine::o_isItemInRoom(ScriptEnv &e) { OP_DEBUG_2("\t&& GET_ITEM_ROOM(%s) == %s", itemStr(e.arg(1)).c_str(), itemRoomStr(e.arg(2)).c_str()); if (getItem(e.arg(1)).room == roomArg(e.arg(2))) return 2; return -1; } int AdlEngine::o_isMovesGT(ScriptEnv &e) { OP_DEBUG_1("\t&& MOVES > %d", e.arg(1)); if (_state.moves > e.arg(1)) return 1; return -1; } int AdlEngine::o_isVarEQ(ScriptEnv &e) { OP_DEBUG_2("\t&& VARS[%d] == %d", e.arg(1), e.arg(2)); if (getVar(e.arg(1)) == e.arg(2)) return 2; return -1; } int AdlEngine::o_isCurPicEQ(ScriptEnv &e) { OP_DEBUG_1("\t&& GET_CURPIC() == %d", e.arg(1)); if (_state.curPicture == e.arg(1)) return 1; return -1; } int AdlEngine::o_isItemPicEQ(ScriptEnv &e) { OP_DEBUG_2("\t&& GET_ITEM_PIC(%s) == %d", itemStr(e.arg(1)).c_str(), e.arg(2)); if (getItem(e.arg(1)).picture == e.arg(2)) return 2; return -1; } int AdlEngine::o_varAdd(ScriptEnv &e) { OP_DEBUG_2("\tVARS[%d] += %d", e.arg(2), e.arg(1)); setVar(e.arg(2), getVar(e.arg(2)) + e.arg(1)); return 2; } int AdlEngine::o_varSub(ScriptEnv &e) { OP_DEBUG_2("\tVARS[%d] -= %d", e.arg(2), e.arg(1)); setVar(e.arg(2), getVar(e.arg(2)) - e.arg(1)); return 2; } int AdlEngine::o_varSet(ScriptEnv &e) { OP_DEBUG_2("\tVARS[%d] = %d", e.arg(1), e.arg(2)); setVar(e.arg(1), e.arg(2)); return 2; } int AdlEngine::o_listInv(ScriptEnv &e) { OP_DEBUG_0("\tLIST_INVENTORY()"); Common::List::const_iterator item; for (item = _state.items.begin(); item != _state.items.end(); ++item) if (item->room == IDI_ANY) printString(getItemDescription(*item)); return 0; } int AdlEngine::o_moveItem(ScriptEnv &e) { OP_DEBUG_2("\tSET_ITEM_ROOM(%s, %s)", itemStr(e.arg(1)).c_str(), itemRoomStr(e.arg(2)).c_str()); getItem(e.arg(1)).room = e.arg(2); return 2; } int AdlEngine::o_setRoom(ScriptEnv &e) { OP_DEBUG_1("\tROOM = %d", e.arg(1)); switchRoom(e.arg(1)); return 1; } int AdlEngine::o_setCurPic(ScriptEnv &e) { OP_DEBUG_1("\tSET_CURPIC(%d)", e.arg(1)); getCurRoom().curPicture = e.arg(1); return 1; } int AdlEngine::o_setPic(ScriptEnv &e) { OP_DEBUG_1("\tSET_PIC(%d)", e.arg(1)); getCurRoom().picture = getCurRoom().curPicture = e.arg(1); return 1; } int AdlEngine::o_printMsg(ScriptEnv &e) { OP_DEBUG_1("\tPRINT(%s)", msgStr(e.arg(1)).c_str()); printMessage(e.arg(1)); return 1; } int AdlEngine::o_setLight(ScriptEnv &e) { OP_DEBUG_0("\tLIGHT()"); _state.isDark = false; return 0; } int AdlEngine::o_setDark(ScriptEnv &e) { OP_DEBUG_0("\tDARK()"); _state.isDark = true; return 0; } int AdlEngine::o_save(ScriptEnv &e) { OP_DEBUG_0("\tSAVE_GAME()"); saveGameState(0, ""); return 0; } int AdlEngine::o_restore(ScriptEnv &e) { OP_DEBUG_0("\tRESTORE_GAME()"); loadGameState(0); _isRestoring = false; return 0; } int AdlEngine::o_restart(ScriptEnv &e) { OP_DEBUG_0("\tRESTART_GAME()"); _display->printString(_strings.playAgain); Common::String input = inputString(); if (input.size() == 0 || input[0] != _display->asciiToNative('N')) { _isRestarting = true; _graphics->clearScreen(); _display->renderGraphics(); _display->printString(_strings.pressReturn); initState(); _display->printAsciiString(_strings.lineFeeds); return -1; } return o_quit(e); } int AdlEngine::o_quit(ScriptEnv &e) { OP_DEBUG_0("\tQUIT_GAME()"); printMessage(_messageIds.thanksForPlaying); // Wait for a key here to ensure that the user gets a chance // to read the thank-you message _display->printAsciiString("PRESS ANY KEY TO QUIT"); inputKey(); // We use _isRestarting to abort the current game loop iteration _isQuitting = _isRestarting = true; return -1; } int AdlEngine::o_placeItem(ScriptEnv &e) { OP_DEBUG_4("\tPLACE_ITEM(%s, %s, (%d, %d))", itemStr(e.arg(1)).c_str(), itemRoomStr(e.arg(2)).c_str(), e.arg(3), e.arg(4)); Item &item = getItem(e.arg(1)); item.room = roomArg(e.arg(2)); item.position.x = e.arg(3); item.position.y = e.arg(4); return 4; } int AdlEngine::o_setItemPic(ScriptEnv &e) { OP_DEBUG_2("\tSET_ITEM_PIC(%s, %d)", itemStr(e.arg(2)).c_str(), e.arg(1)); getItem(e.arg(2)).picture = e.arg(1); return 2; } int AdlEngine::o_resetPic(ScriptEnv &e) { OP_DEBUG_0("\tRESET_PIC()"); getCurRoom().curPicture = getCurRoom().picture; return 0; } int AdlEngine::goDirection(ScriptEnv &e, Direction dir) { OP_DEBUG_0((Common::String("\tGO_") + dirStr(dir) + "()").c_str()); byte room = getCurRoom().connections[dir]; if (room == 0) { printMessage(_messageIds.cantGoThere); return -1; } switchRoom(room); return -1; } int AdlEngine::o_takeItem(ScriptEnv &e) { OP_DEBUG_0("\tTAKE_ITEM()"); takeItem(e.getNoun()); return 0; } int AdlEngine::o_dropItem(ScriptEnv &e) { OP_DEBUG_0("\tDROP_ITEM()"); dropItem(e.getNoun()); return 0; } int AdlEngine::o_setRoomPic(ScriptEnv &e) { OP_DEBUG_2("\tSET_ROOM_PIC(%d, %d)", e.arg(1), e.arg(2)); getRoom(e.arg(1)).picture = getRoom(e.arg(1)).curPicture = e.arg(2); return 2; } bool AdlEngine::matchCommand(ScriptEnv &env) const { if (!env.isMatch() && !_dumpFile) return false; if (DebugMan.isDebugChannelEnabled(kDebugChannelScript)) { (void)op_debug("IF\n\tROOM == %s", roomStr(env.getCommand().room).c_str()); (void)op_debug("\t&& SAID(%s, %s)", verbStr(env.getCommand().verb).c_str(), nounStr(env.getCommand().noun).c_str()); } while (env.getOpType() == ScriptEnv::kOpTypeCond) { byte op = env.op(); if (op >= _condOpcodes.size() || !_condOpcodes[op] || !_condOpcodes[op]->isValid()) error("Unimplemented condition opcode %02x", op); int numArgs = (*_condOpcodes[op])(env); if (numArgs < 0) { if (DebugMan.isDebugChannelEnabled(kDebugChannelScript)) (void)op_debug("FAIL\n"); return false; } env.next(numArgs); } return true; } void AdlEngine::doActions(ScriptEnv &env) { if (DebugMan.isDebugChannelEnabled(kDebugChannelScript)) (void)op_debug("THEN"); while (env.getOpType() == ScriptEnv::kOpTypeAct) { byte op = env.op(); if (op >= _actOpcodes.size() || !_actOpcodes[op] || !_actOpcodes[op]->isValid()) error("Unimplemented action opcode %02x", op); int numArgs = (*_actOpcodes[op])(env); if (numArgs < 0) { if (DebugMan.isDebugChannelEnabled(kDebugChannelScript)) (void)op_debug("ABORT\n"); return; } env.next(numArgs); } if (DebugMan.isDebugChannelEnabled(kDebugChannelScript)) (void)op_debug("END\n"); } bool AdlEngine::doOneCommand(const Commands &commands, byte verb, byte noun) { Commands::const_iterator cmd; for (cmd = commands.begin(); cmd != commands.end(); ++cmd) { Common::ScopedPtr env(createScriptEnv(*cmd, _state.room, verb, noun)); if (matchCommand(*env)) { doActions(*env); return true; } if (_abortScript) { _abortScript = false; return false; } } return false; } void AdlEngine::doAllCommands(const Commands &commands, byte verb, byte noun) { Commands::const_iterator cmd; for (cmd = commands.begin(); cmd != commands.end(); ++cmd) { Common::ScopedPtr env(createScriptEnv(*cmd, _state.room, verb, noun)); if (matchCommand(*env)) { doActions(*env); // The original long jumps on restart, so we need to abort here if (_isRestarting) return; } if (_abortScript) { _abortScript = false; return; } } } ScriptEnv *AdlEngine::createScriptEnv(const Command &cmd, byte room, byte verb, byte noun) { return new ScriptEnv_6502(cmd, room, verb, noun); } Common::String AdlEngine::toAscii(const Common::String &str) { Common::String ascii = Console::toAscii(str); if (ascii.lastChar() == '\r') ascii.deleteLastChar(); // FIXME: remove '\r's inside string? return ascii; } Common::String AdlEngine::itemStr(uint i) const { const Item &item(getItem(i)); Common::String name = Common::String::format("%d", i); if (item.noun > 0) { name += "/"; name += _priNouns[item.noun - 1]; } Common::String desc = getItemDescription(item); if (!desc.empty()) { name += "/"; name += toAscii(desc); } return name; } Common::String AdlEngine::itemRoomStr(uint i) const { switch (i) { case IDI_ANY: return "CARRYING"; case IDI_VOID_ROOM: return "GONE"; case IDI_CUR_ROOM: return "HERE"; default: return Common::String::format("%d", i); } } Common::String AdlEngine::roomStr(uint i) const { if (i == IDI_ANY) return "*"; else return Common::String::format("%d", i); } Common::String AdlEngine::verbStr(uint i) const { if (i == IDI_ANY) return "*"; else return Common::String::format("%d/%s", i, (i - 1 < _priVerbs.size() ? _priVerbs[i - 1].c_str() : "")); } Common::String AdlEngine::nounStr(uint i) const { if (i == IDI_ANY) return "*"; else return Common::String::format("%d/%s", i, (i - 1 < _priNouns.size() ? _priNouns[i - 1].c_str() : "")); } Common::String AdlEngine::msgStr(uint i) const { return Common::String::format("%d/%s", i, toAscii(loadMessage(i)).c_str()); } Common::String AdlEngine::dirStr(Direction dir) const { static const char *dirs[] = { "NORTH", "SOUTH", "EAST", "WEST", "UP", "DOWN" }; return dirs[dir]; } } // End of namespace Adl