diff options
Diffstat (limited to 'engines/wage/script.cpp')
-rw-r--r-- | engines/wage/script.cpp | 1175 |
1 files changed, 1175 insertions, 0 deletions
diff --git a/engines/wage/script.cpp b/engines/wage/script.cpp new file mode 100644 index 0000000000..61336dce88 --- /dev/null +++ b/engines/wage/script.cpp @@ -0,0 +1,1175 @@ +/* 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. + * + * MIT License: + * + * Copyright (c) 2009 Alexei Svitkine, Eugene Sandulenko + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#include "wage/wage.h" +#include "wage/entities.h" +#include "wage/script.h" +#include "wage/world.h" + +#include "common/stream.h" + +namespace Wage { + +Common::String Script::Operand::toString() { + switch(_type) { + case NUMBER: + return Common::String::format("%d", _value.number); + case STRING: + case TEXT_INPUT: + return *_value.string; + case OBJ: + return _value.obj->toString(); + case CHR: + return _value.chr->toString(); + case SCENE: + return _value.scene->toString(); + case CLICK_INPUT: + return _value.inputClick->toString(); + default: + error("Unhandled operand type: _type"); + } +} + +Script::Script(Common::SeekableReadStream *data) : _data(data) { + _engine = NULL; + _world = NULL; + + _loopCount = 0; + _inputText = NULL; + _inputClick = NULL; + + _handled = false; + + convertToText(); +} + +Script::~Script() { + for (uint i = 0; i < _scriptText.size(); i++) { + delete _scriptText[i]; + } + + delete _data; +} + +void Script::print() { + for (uint i = 0; i < _scriptText.size(); i++) { + debug(4, "%d [%04x]: %s", i, _scriptText[i]->offset, _scriptText[i]->line.c_str()); + } +} + +void Script::printLine(int offset) { + for (uint i = 0; i < _scriptText.size(); i++) + if (_scriptText[i]->offset >= offset) { + debug(4, "%d [%04x]: %s", i, _scriptText[i]->offset, _scriptText[i]->line.c_str()); + break; + } +} + +bool Script::execute(World *world, int loopCount, Common::String *inputText, Designed *inputClick, WageEngine *engine) { + _world = world; + _loopCount = loopCount; + _inputText = inputText; + _inputClick = inputClick; + _engine = engine; + _handled = false; + Common::String input; + + if (inputText) + input = *inputText; + + _data->seek(12); + while (_data->pos() < _data->size()) { + printLine(_data->pos()); + + byte command = _data->readByte(); + + switch(command) { + case 0x80: // IF + processIf(); + break; + case 0x87: // EXIT + debug(6, "exit at offset %d", _data->pos() - 1); + + return true; + case 0x89: // MOVE + { + Scene *currentScene = _world->_player->_currentScene; + processMove(); + if (_world->_player->_currentScene != currentScene) + return true; + break; + } + case 0x8B: // PRINT + { + Operand *op = readOperand(); + // TODO check op type is string or number, or something good... + _handled = true; + _engine->appendText(op->toString().c_str()); + delete op; + byte d = _data->readByte(); + if (d != 0xFD) + warning("Operand 0x8B (PRINT) End Byte != 0xFD"); + break; + } + case 0x8C: // SOUND + { + Operand *op = readOperand(); + // TODO check op type is string. + _handled = true; + _engine->playSound(op->toString()); + delete op; + byte d = _data->readByte(); + if (d != 0xFD) + warning("Operand 0x8B (PRINT) End Byte != 0xFD"); + break; + } + case 0x8E: // LET + processLet(); + break; + case 0x95: // MENU + { + Operand *op = readStringOperand(); // allows empty menu + // TODO check op type is string. + _engine->setMenu(op->toString()); + delete op; + byte d = _data->readByte(); + if (d != 0xFD) + warning("Operand 0x8B (PRINT) End Byte != 0xFD"); + } + case 0x88: // END + break; + default: + debug(0, "Unknown opcode: %d", _data->pos()); + } + } + + if (_world->_globalScript != this) { + debug(1, "Executing global script..."); + bool globalHandled = _world->_globalScript->execute(_world, _loopCount, &input, _inputClick, _engine); + if (globalHandled) + _handled = true; + } else if (!input.empty()) { + if (input.contains("north")) { + _handled = _engine->handleMoveCommand(NORTH, "north"); + } else if (input.contains("east")) { + _handled = _engine->handleMoveCommand(EAST, "east"); + } else if (input.contains("south")) { + _handled = _engine->handleMoveCommand(SOUTH, "south"); + } else if (input.contains("west")) { + _handled = _engine->handleMoveCommand(WEST, "west"); + } else if (input.hasPrefix("take ")) { + _handled = _engine->handleTakeCommand(&input.c_str()[5]); + } else if (input.hasPrefix("get ")) { + _handled = _engine->handleTakeCommand(&input.c_str()[4]); + } else if (input.hasPrefix("pick up ")) { + _handled = _engine->handleTakeCommand(&input.c_str()[8]); + } else if (input.hasPrefix("drop ")) { + _handled = _engine->handleDropCommand(&input.c_str()[5]); + } else if (input.hasPrefix("aim ")) { + _handled = _engine->handleAimCommand(&input.c_str()[4]); + } else if (input.hasPrefix("wear ")) { + _handled = _engine->handleWearCommand(&input.c_str()[5]); + } else if (input.hasPrefix("put on ")) { + _handled = _engine->handleWearCommand(&input.c_str()[7]); + } else if (input.hasPrefix("offer ")) { + _handled = _engine->handleOfferCommand(&input.c_str()[6]); + } else if (input.contains("look")) { + _handled = _engine->handleLookCommand(); + } else if (input.contains("inventory")) { + _handled = _engine->handleInventoryCommand(); + } else if (input.contains("status")) { + _handled = _engine->handleStatusCommand(); + } else if (input.contains("rest") || input.equals("wait")) { + _handled = _engine->handleRestCommand(); + } else if (_engine->getOffer() != NULL && input.contains("accept")) { + _handled = _engine->handleAcceptCommand(); + } else { + Chr *player = _world->_player; + ObjArray *weapons = player->getWeapons(true); + for (ObjArray::const_iterator weapon = weapons->begin(); weapon != weapons->end(); ++weapon) { + if (_engine->tryAttack(*weapon, input)) { + _handled = _engine->handleAttack(*weapon); + break; + } + } + + delete weapons; + } + // TODO: weapons, offer, etc... + } else if (_inputClick->_classType == OBJ) { + Obj *obj = (Obj *)_inputClick; + if (obj->_type != Obj::IMMOBILE_OBJECT) { + _engine->takeObj(obj); + } else { + _engine->appendText(obj->_clickMessage.c_str()); + } + + _handled = true; + } + + return _handled; +} + +Script::Operand *Script::readOperand() { + byte operandType = _data->readByte(); + + debug(7, "%x: readOperand: 0x%x", _data->pos(), operandType); + + Context *cont = &_world->_player->_context; + switch (operandType) { + case 0xA0: // TEXT$ + return new Operand(_inputText, TEXT_INPUT); + case 0xA1: + return new Operand(_inputClick, CLICK_INPUT); + case 0xC0: // STORAGE@ + return new Operand(_world->_storageScene, SCENE); + case 0xC1: // SCENE@ + return new Operand(_world->_player->_currentScene, SCENE); + case 0xC2: // PLAYER@ + return new Operand(_world->_player, CHR); + case 0xC3: // MONSTER@ + return new Operand(_engine->getMonster(), CHR); + case 0xC4: // RANDOMSCN@ + return new Operand(_world->_orderedScenes[_engine->_rnd->getRandomNumber(_world->_orderedScenes.size())], SCENE); + case 0xC5: // RANDOMCHR@ + return new Operand(_world->_orderedChrs[_engine->_rnd->getRandomNumber(_world->_orderedChrs.size())], CHR); + case 0xC6: // RANDOMOBJ@ + return new Operand(_world->_orderedObjs[_engine->_rnd->getRandomNumber(_world->_orderedObjs.size())], OBJ); + case 0xB0: // VISITS# + return new Operand(cont->_visits, NUMBER); + case 0xB1: // RANDOM# for Star Trek, but VISITS# for some other games? + return new Operand(1 + _engine->_rnd->getRandomNumber(100), NUMBER); + case 0xB5: // RANDOM# // A random number between 1 and 100. + return new Operand(1 + _engine->_rnd->getRandomNumber(100), NUMBER); + case 0xB2: // LOOP# + return new Operand(_loopCount, NUMBER); + case 0xB3: // VICTORY# + return new Operand(cont->_kills, NUMBER); + case 0xB4: // BADCOPY# + return new Operand(0, NUMBER); // \?\?\?? + case 0xFF: + { + // user variable + int value = _data->readByte(); + + // TODO: Verify that we're using the right index. + return new Operand(cont->_userVariables[value - 1], NUMBER); + } + case 0xD0: + return new Operand(cont->_statVariables[PHYS_STR_BAS], NUMBER); + case 0xD1: + return new Operand(cont->_statVariables[PHYS_HIT_BAS], NUMBER); + case 0xD2: + return new Operand(cont->_statVariables[PHYS_ARM_BAS], NUMBER); + case 0xD3: + return new Operand(cont->_statVariables[PHYS_ACC_BAS], NUMBER); + case 0xD4: + return new Operand(cont->_statVariables[SPIR_STR_BAS], NUMBER); + case 0xD5: + return new Operand(cont->_statVariables[SPIR_HIT_BAS], NUMBER); + case 0xD6: + return new Operand(cont->_statVariables[SPIR_ARM_BAS], NUMBER); + case 0xD7: + return new Operand(cont->_statVariables[SPIR_ACC_BAS], NUMBER); + case 0xD8: + return new Operand(cont->_statVariables[PHYS_SPE_BAS], NUMBER); + case 0xE0: + return new Operand(cont->_statVariables[PHYS_STR_CUR], NUMBER); + case 0xE1: + return new Operand(cont->_statVariables[PHYS_HIT_CUR], NUMBER); + case 0xE2: + return new Operand(cont->_statVariables[PHYS_ARM_CUR], NUMBER); + case 0xE3: + return new Operand(cont->_statVariables[PHYS_ACC_CUR], NUMBER); + case 0xE4: + return new Operand(cont->_statVariables[SPIR_STR_CUR], NUMBER); + case 0xE5: + return new Operand(cont->_statVariables[SPIR_HIT_CUR], NUMBER); + case 0xE6: + return new Operand(cont->_statVariables[SPIR_ARM_CUR], NUMBER); + case 0xE7: + return new Operand(cont->_statVariables[SPIR_ACC_CUR], NUMBER); + case 0xE8: + return new Operand(cont->_statVariables[PHYS_SPE_CUR], NUMBER); + default: + if (operandType >= 0x20 && operandType < 0x80) { + _data->seek(-1, SEEK_CUR); + return readStringOperand(); + } else { + debug("Dunno what %x is (index=%d)!\n", operandType, _data->pos()-1); + } + return NULL; + } +} + +void Script::assign(byte operandType, int uservar, uint16 value) { + Context *cont = &_world->_player->_context; + + switch (operandType) { + case 0xFF: + cont->_userVariables[uservar - 1] = value; + break; + case 0xD0: + cont->_statVariables[PHYS_STR_BAS] = value; + break; + case 0xD1: + cont->_statVariables[PHYS_HIT_BAS] = value; + break; + case 0xD2: + cont->_statVariables[PHYS_ARM_BAS] = value; + break; + case 0xD3: + cont->_statVariables[PHYS_ACC_BAS] = value; + break; + case 0xD4: + cont->_statVariables[SPIR_STR_BAS] = value; + break; + case 0xD5: + cont->_statVariables[SPIR_HIT_BAS] = value; + break; + case 0xD6: + cont->_statVariables[SPIR_ARM_BAS] = value; + break; + case 0xD7: + cont->_statVariables[SPIR_ACC_BAS] = value; + break; + case 0xD8: + cont->_statVariables[PHYS_SPE_BAS] = value; + break; + case 0xE0: + cont->_statVariables[PHYS_STR_CUR] = value; + break; + case 0xE1: + cont->_statVariables[PHYS_HIT_CUR] = value; + break; + case 0xE2: + cont->_statVariables[PHYS_ARM_CUR] = value; + break; + case 0xE3: + cont->_statVariables[PHYS_ACC_CUR] = value; + break; + case 0xE4: + cont->_statVariables[SPIR_STR_CUR] = value; + break; + case 0xE5: + cont->_statVariables[SPIR_HIT_CUR] = value; + break; + case 0xE6: + cont->_statVariables[SPIR_ARM_CUR] = value; + break; + case 0xE7: + cont->_statVariables[SPIR_ACC_CUR] = value; + break; + case 0xE8: + cont->_statVariables[PHYS_SPE_CUR] = value; + break; + default: + debug("No idea what I'm supposed to assign! (%x at %d)!\n", operandType, _data->pos()-1); + } +} + +Script::Operand *Script::readStringOperand() { + Common::String *str; + bool allDigits = true; + + str = new Common::String(); + + while (true) { + byte c = _data->readByte(); + if (c >= 0x20 && c < 0x80) + *str += c; + else + break; + if (c < '0' || c > '9') + allDigits = false; + } + _data->seek(-1, SEEK_CUR); + + if (allDigits && !str->empty()) { + int r = atol(str->c_str()); + delete str; + + return new Operand(r, NUMBER); + } else { + // TODO: This string could be a room name or something like that. + return new Operand(str, STRING); + } +} + +const char *Script::readOperator() { + byte cmd = _data->readByte(); + + debug(7, "readOperator: 0x%x", cmd); + switch (cmd) { + case 0x81: + return "="; + case 0x82: + return "<"; + case 0x83: + return ">"; + case 0x8f: + return "+"; + case 0x90: + return "-"; + case 0x91: + return "*"; + case 0x92: + return "/"; + case 0x93: + return "=="; + case 0x94: + return ">>"; + case 0xfd: + return ";"; + default: + warning("UNKNOWN OP %x", cmd); + } + return NULL; +} + +void Script::processIf() { + int logicalOp = 0; // 0 => initial, 1 => and, 2 => or + bool result = true; + bool done = false; + + do { + Operand *lhs = readOperand(); + const char *op = readOperator(); + Operand *rhs = readOperand(); + + bool condResult = eval(lhs, op, rhs); + + delete lhs; + delete rhs; + + if (logicalOp == 1) { + result = (result && condResult); + } else if (logicalOp == 2) { + result = (result || condResult); + } else { // logicalOp == 0 + result = condResult; + } + + byte logical = _data->readByte(); + + if (logical == 0x84) { + logicalOp = 1; // and + } else if (logical == 0x85) { + logicalOp = 2; // or + } else if (logical == 0xFE) { + done = true; // then + } + } while (!done); + + if (result == false) { + skipBlock(); + } +} + +void Script::skipIf() { + do { + Operand *lhs = readOperand(); + readOperator(); + Operand *rhs = readOperand(); + + delete lhs; + delete rhs; + } while (_data->readByte() != 0xFE); +} + +void Script::skipBlock() { + int nesting = 1; + + while (true) { + byte op = _data->readByte(); + + if (_data->eos()) + return; + + if (op == 0x80) { // IF + nesting++; + skipIf(); + } else if (op == 0x88 || op == 0x87) { // END or EXIT + nesting--; + if (nesting == 0) { + return; + } + } else switch (op) { + case 0x8B: // PRINT + case 0x8C: // SOUND + case 0x8E: // LET + case 0x95: // MENU + while (_data->readByte() != 0xFD) + ; + } + } +} + +enum { + kCompEqNumNum, + kCompEqObjScene, + kCompEqChrScene, + kCompEqObjChr, + kCompEqChrChr, + kCompEqSceneScene, + kCompEqStringTextInput, + kCompEqTextInputString, + kCompEqNumberTextInput, + kCompEqTextInputNumber, + kCompLtNumNum, + kCompLtStringTextInput, + kCompLtTextInputString, + kCompLtObjChr, + kCompLtChrObj, + kCompLtObjScene, + kCompGtNumNum, + kCompGtStringString, + kCompGtChrScene, + kMoveObjChr, + kMoveObjScene, + kMoveChrScene +}; + +static const char *typeNames[] = { + "OBJ", + "CHR", + "SCENE", + "NUMBER", + "STRING", + "CLICK_INPUT", + "TEXT_INPUT" +}; + +static const char *operandTypeToStr(int type) { + if (type < 0 || type > 6) + return "UNKNOWN"; + + return typeNames[type]; +} + +struct Comparator { + char op; + OperandType o1; + OperandType o2; + int cmp; +} static comparators[] = { + { '=', NUMBER, NUMBER, kCompEqNumNum }, + { '=', OBJ, SCENE, kCompEqObjScene }, + { '=', CHR, SCENE, kCompEqChrScene }, + { '=', OBJ, CHR, kCompEqObjChr }, + { '=', CHR, CHR, kCompEqChrChr }, + { '=', SCENE, SCENE, kCompEqSceneScene }, + { '=', STRING, TEXT_INPUT, kCompEqStringTextInput }, + { '=', TEXT_INPUT, STRING, kCompEqTextInputString }, + { '=', NUMBER, TEXT_INPUT, kCompEqNumberTextInput }, + { '=', TEXT_INPUT, NUMBER, kCompEqTextInputNumber }, + + { '<', NUMBER, NUMBER, kCompLtNumNum }, + { '<', STRING, TEXT_INPUT, kCompLtStringTextInput }, + { '<', TEXT_INPUT, STRING, kCompLtTextInputString }, + { '<', OBJ, CHR, kCompLtObjChr }, + { '<', CHR, OBJ, kCompLtChrObj }, + { '<', OBJ, SCENE, kCompLtObjScene }, + { '<', CHR, CHR, kCompEqChrChr }, // Same logic as = + { '<', SCENE, SCENE, kCompEqSceneScene }, + + { '>', NUMBER, NUMBER, kCompGtNumNum }, + { '>', TEXT_INPUT, STRING, kCompLtTextInputString }, // Same logic as < + //FIXME: this prevents the below cases from working due to exact + //matches taking precedence over conversions... + //{ '>', STRING, STRING, kCompGtStringString }, // Same logic as < + { '>', OBJ, CHR, kCompLtObjChr }, // Same logic as < + { '>', OBJ, SCENE, kCompLtObjScene }, // Same logic as < + { '>', CHR, SCENE, kCompGtChrScene }, + + { 'M', OBJ, CHR, kMoveObjChr }, + { 'M', OBJ, SCENE, kMoveObjScene }, + { 'M', CHR, SCENE, kMoveChrScene }, + { 0, OBJ, OBJ, 0 } +}; + +bool Script::compare(Operand *o1, Operand *o2, int comparator) { + switch(comparator) { + case kCompEqNumNum: + return o1->_value.number == o2->_value.number; + case kCompEqObjScene: + for (ObjList::const_iterator it = o2->_value.scene->_objs.begin(); it != o2->_value.scene->_objs.end(); ++it) + if (*it == o1->_value.obj) + return true; + return false; + case kCompEqChrScene: + for (ChrList::const_iterator it = o2->_value.scene->_chrs.begin(); it != o2->_value.scene->_chrs.end(); ++it) + if (*it == o1->_value.chr) + return true; + return false; + case kCompEqObjChr: + for (ObjArray::const_iterator it = o2->_value.chr->_inventory.begin(); it != o2->_value.chr->_inventory.end(); ++it) + if (*it == o1->_value.obj) + return true; + return false; + case kCompEqChrChr: + return o1->_value.chr == o2->_value.chr; + case kCompEqSceneScene: + return o1->_value.scene == o2->_value.scene; + case kCompEqStringTextInput: + if (_inputText == NULL) { + return false; + } else { + Common::String s1(*_inputText), s2(*o1->_value.string); + s1.toLowercase(); + s2.toLowercase(); + + return s1.contains(s2); + } + case kCompEqTextInputString: + return compare(o2, o1, kCompEqStringTextInput); + case kCompEqNumberTextInput: + if (_inputText == NULL) { + return false; + } else { + Common::String s1(*_inputText), s2(o1->toString()); + s1.toLowercase(); + s2.toLowercase(); + + return s1.contains(s2); + } + case kCompEqTextInputNumber: + if (_inputText == NULL) { + return false; + } else { + Common::String s1(*_inputText), s2(o2->toString()); + s1.toLowercase(); + s2.toLowercase(); + + return s1.contains(s2); + } + case kCompLtNumNum: + return o1->_value.number < o2->_value.number; + case kCompLtStringTextInput: + return !compare(o1, o2, kCompEqStringTextInput); + case kCompLtTextInputString: + return !compare(o2, o1, kCompEqStringTextInput); + case kCompLtObjChr: + return o1->_value.obj->_currentOwner != o2->_value.chr; + case kCompLtChrObj: + return compare(o2, o1, kCompLtObjChr); + case kCompLtObjScene: + return o1->_value.obj->_currentScene != o2->_value.scene; + case kCompGtNumNum: + return o1->_value.number > o2->_value.number; + case kCompGtStringString: + return o1->_value.string == o2->_value.string; + case kCompGtChrScene: + return (o1->_value.chr != NULL && o1->_value.chr->_currentScene != o2->_value.scene); + case kMoveObjChr: + if (o1->_value.obj->_currentOwner != o2->_value.chr) { + _world->move(o1->_value.obj, o2->_value.chr); + _handled = true; // TODO: Is this correct? + } + break; + case kMoveObjScene: + if (o1->_value.obj->_currentScene != o2->_value.scene) { + _world->move(o1->_value.obj, o2->_value.scene); + // Note: This shouldn't call setHandled() - see + // Sultan's Palace 'Food and Drink' scene. + } + break; + case kMoveChrScene: + _world->move(o1->_value.chr, o2->_value.scene); + _handled = true; // TODO: Is this correct? + break; + } + + return false; +} + +bool Script::evaluatePair(Operand *lhs, const char *op, Operand *rhs) { + debug(7, "HANDLING CASE: [lhs=%s/%s, op=%s rhs=%s/%s]", + operandTypeToStr(lhs->_type), lhs->toString().c_str(), op, operandTypeToStr(rhs->_type), rhs->toString().c_str()); + + for (int cmp = 0; comparators[cmp].op != 0; cmp++) { + if (comparators[cmp].op != op[0]) + continue; + + if (comparators[cmp].o1 == lhs->_type && comparators[cmp].o2 == rhs->_type) + return compare(lhs, rhs, comparators[cmp].cmp); + } + + // Now, try partial matches. + Operand *c1, *c2; + for (int cmp = 0; comparators[cmp].op != 0; cmp++) { + if (comparators[cmp].op != op[0]) + continue; + + if (comparators[cmp].o1 == lhs->_type && + (c2 = convertOperand(rhs, comparators[cmp].o2)) != NULL) { + bool res = compare(lhs, c2, comparators[cmp].cmp); + delete c2; + return res; + } else if (comparators[cmp].o2 == rhs->_type && + (c1 = convertOperand(lhs, comparators[cmp].o1)) != NULL) { + bool res = compare(c1, rhs, comparators[cmp].cmp); + delete c1; + return res; + } + } + + // Now, try double conversion. + for (int cmp = 0; comparators[cmp].op != 0; cmp++) { + if (comparators[cmp].op != op[0]) + continue; + + if (comparators[cmp].o1 == lhs->_type || comparators[cmp].o2 == rhs->_type) + continue; + + if ((c1 = convertOperand(lhs, comparators[cmp].o1)) != NULL) { + if ((c2 = convertOperand(rhs, comparators[cmp].o2)) != NULL) { + bool res = compare(c1, c2, comparators[cmp].cmp); + delete c1; + delete c2; + return res; + } + delete c1; + } + } + + warning("UNHANDLED CASE: [lhs=%s/%s, op=%s rhs=%s/%s]", + operandTypeToStr(lhs->_type), lhs->toString().c_str(), op, operandTypeToStr(rhs->_type), rhs->toString().c_str()); + + return false; +} + +bool Script::eval(Operand *lhs, const char *op, Operand *rhs) { + bool result = false; + + if (lhs->_type == CLICK_INPUT || rhs->_type == CLICK_INPUT) { + return evalClickCondition(lhs, op, rhs); + } else if (!strcmp(op, "==") || !strcmp(op, ">>")) { + // TODO: check if >> can be used for click inputs and if == can be used for other things + // exact string match + if (lhs->_type == TEXT_INPUT) { + if ((rhs->_type != STRING && rhs->_type != NUMBER) || _inputText == NULL) { + result = false; + } else { + result = _inputText->equalsIgnoreCase(rhs->toString()); + } + } else if (rhs->_type == TEXT_INPUT) { + if ((lhs->_type != STRING && lhs->_type != NUMBER) || _inputText == NULL) { + result = false; + } else { + result = _inputText->equalsIgnoreCase(lhs->toString()); + } + } else { + error("UNHANDLED CASE: [lhs=%s/%s, rhs=%s/%s]", + operandTypeToStr(lhs->_type), lhs->toString().c_str(), operandTypeToStr(rhs->_type), rhs->toString().c_str()); + } + if (!strcmp(op, ">>")) { + result = !result; + } + + return result; + } else { + return evaluatePair(lhs, op, rhs); + } + + return false; +} + +Script::Operand *Script::convertOperand(Operand *operand, int type) { + if (operand->_type == type) + error("Incorrect conversion to type %d", type); + + if (type == SCENE) { + if (operand->_type == STRING || operand->_type == NUMBER) { + Common::String key(operand->toString()); + key.toLowercase(); + if (_world->_scenes.contains(key)) + return new Operand(_world->_scenes[key], SCENE); + } + } else if (type == OBJ) { + if (operand->_type == STRING || operand->_type == NUMBER) { + Common::String key = operand->toString(); + key.toLowercase(); + if (_world->_objs.contains(key)) + return new Operand(_world->_objs[key], OBJ); + } else if (operand->_type == CLICK_INPUT) { + if (_inputClick->_classType == OBJ) + return new Operand(_inputClick, OBJ); + } + } else if (type == CHR) { + if (operand->_type == STRING || operand->_type == NUMBER) { + Common::String key = operand->toString(); + key.toLowercase(); + if (_world->_chrs.contains(key)) + return new Operand(_world->_chrs[key], CHR); + } else if (operand->_type == CLICK_INPUT) { + if (_inputClick->_classType == CHR) + return new Operand(_inputClick, CHR); + } + } + + return NULL; +} + +bool Script::evalClickEquality(Operand *lhs, Operand *rhs, bool partialMatch) { + bool result = false; + if (lhs->_value.obj == NULL || rhs->_value.obj == NULL) { + result = false; + } else if (lhs->_value.obj == rhs->_value.obj) { + result = true; + } else if (rhs->_type == STRING) { + Common::String str = rhs->toString(); + str.toLowercase(); + + debug(9, "evalClickEquality(%s, %s, %d)", lhs->_value.designed->_name.c_str(), rhs->_value.designed->_name.c_str(), partialMatch); + debug(9, "l: %s r: %s)", operandTypeToStr(lhs->_type), operandTypeToStr(rhs->_type)); + debug(9, "class: %d", lhs->_value.inputClick->_classType); + + if (lhs->_value.inputClick->_classType == CHR || lhs->_value.inputClick->_classType == OBJ) { + Common::String name = lhs->_value.designed->_name; + name.toLowercase(); + + if (partialMatch) + result = name.contains(str); + else + result = name.equals(str); + } + + debug(9, "result: %d", result); + } + return result; +} + +bool Script::evalClickCondition(Operand *lhs, const char *op, Operand *rhs) { + // TODO: check if >> can be used for click inputs + if (strcmp(op, "==") && strcmp(op, "=") && strcmp(op, "<") && strcmp(op, ">")) { + error("Unknown operation '%s' for Script::evalClickCondition", op); + } + + bool partialMatch = strcmp(op, "=="); + bool result; + if (lhs->_type == CLICK_INPUT) { + result = evalClickEquality(lhs, rhs, partialMatch); + } else { + result = evalClickEquality(rhs, lhs, partialMatch); + } + if (!strcmp(op, "<") || !strcmp(op, ">")) { + // CLICK$<FOO only matches if there was a click + if (_inputClick == NULL) { + result = false; + } else { + result = !result; + } + } + return result; +} + +void Script::processMove() { + Operand *what = readOperand(); + byte skip = _data->readByte(); + if (skip != 0x8a) + error("Incorrect operator for MOVE: %02x", skip); + + Operand *to = readOperand(); + + skip = _data->readByte(); + if (skip != 0xfd) + error("No end for MOVE: %02x", skip); + + evaluatePair(what, "M", to); + + delete what; + delete to; +} + +void Script::processLet() { + const char *lastOp = NULL; + int16 result = 0; + int operandType = _data->readByte(); + int uservar = 0; + + if (operandType == 0xff) { + uservar = _data->readByte(); + } + + byte eq = _data->readByte(); // skip "=" operator + + debug(7, "processLet: 0x%x, uservar: 0x%x, eq: 0x%x", operandType, uservar, eq); + + do { + Operand *operand = readOperand(); + // TODO assert that value is NUMBER + int16 value = operand->_value.number; + delete operand; + if (lastOp != NULL) { + if (lastOp[0] == '+') + result += value; + else if (lastOp[0] == '-') + result -= value; + else if (lastOp[0] == '/') + result = (int16)(value == 0 ? 0 : result / value); + else if (lastOp[0] == '*') + result *= value; + } else { + result = value; + } + lastOp = readOperator(); + + if (lastOp[0] == ';') + break; + } while (true); + //System.out.println("processLet " + buildStringFromOffset(oldIndex - 1, index - oldIndex + 1) + "}"); + + assign(operandType, uservar, result); +} + +enum { + BLOCK_START, + BLOCK_END, + STATEMENT, + OPERATOR, + OPCODE +}; + +struct Mapping { + const char *cmd; + int type; +} static const mapping[] = { + { "IF{", STATEMENT }, // 0x80 + { "=", OPERATOR }, + { "<", OPERATOR }, + { ">", OPERATOR }, + { "}AND{", OPCODE }, + { "}OR{", OPCODE }, + { "\?\?\?(0x86)", OPCODE }, + { "EXIT\n", BLOCK_END }, + { "END\n", BLOCK_END }, // 0x88 + { "MOVE{", STATEMENT }, + { "}TO{", OPCODE }, + { "PRINT{", STATEMENT }, + { "SOUND{", STATEMENT }, + { "\?\?\?(0x8d)", OPCODE }, + { "LET{", STATEMENT }, + { "+", OPERATOR }, + { "-", OPERATOR }, // 0x90 + { "*", OPERATOR }, + { "/", OPERATOR }, + { "==", OPERATOR }, + { ">>", OPERATOR }, + { "MENU{", STATEMENT }, + { "\?\?\?(0x96)", OPCODE }, + { "\?\?\?(0x97)", OPCODE }, + { "\?\?\?(0x98)", OPCODE }, // 0x98 + { "\?\?\?(0x99)", OPCODE }, + { "\?\?\?(0x9a)", OPCODE }, + { "\?\?\?(0x9b)", OPCODE }, + { "\?\?\?(0x9c)", OPCODE }, + { "\?\?\?(0x9d)", OPCODE }, + { "\?\?\?(0x9e)", OPCODE }, + { "\?\?\?(0x9f)", OPCODE }, + { "TEXT$", OPCODE }, // 0xa0 + { "CLICK$", OPCODE }, + { "\?\?\?(0xa2)", OPCODE }, + { "\?\?\?(0xa3)", OPCODE }, + { "\?\?\?(0xa4)", OPCODE }, + { "\?\?\?(0xa5)", OPCODE }, + { "\?\?\?(0xa6)", OPCODE }, + { "\?\?\?(0xa7)", OPCODE }, + { "\?\?\?(0xa8)", OPCODE }, // 0xa8 + { "\?\?\?(0xa9)", OPCODE }, + { "\?\?\?(0xaa)", OPCODE }, + { "\?\?\?(0xab)", OPCODE }, + { "\?\?\?(0xac)", OPCODE }, + { "\?\?\?(0xad)", OPCODE }, + { "\?\?\?(0xae)", OPCODE }, + { "\?\?\?(0xaf)", OPCODE }, + { "VISITS#", OPCODE }, // 0xb0 // The number of scenes the player has visited, including repeated visits. + { "RANDOM#", OPCODE }, // RANDOM# for Star Trek, but VISITS# for some other games? + { "LOOP#", OPCODE }, // The number of commands the player has given in the current scene. + { "VICTORY#", OPCODE }, // The number of characters killed. + { "BADCOPY#", OPCODE }, + { "RANDOM#", OPCODE }, // A random number between 1 and 100. + { "\?\?\?(0xb6)", OPCODE }, + { "\?\?\?(0xb7)", OPCODE }, + { "\?\?\?(0xb8)", OPCODE }, // 0xb8 + { "\?\?\?(0xb9)", OPCODE }, + { "\?\?\?(0xba)", OPCODE }, + { "\?\?\?(0xbb)", OPCODE }, + { "\?\?\?(0xbc)", OPCODE }, + { "\?\?\?(0xbd)", OPCODE }, + { "\?\?\?(0xbe)", OPCODE }, + { "\?\?\?(0xbf)", OPCODE }, + { "STORAGE@", OPCODE }, // 0xc0 + { "SCENE@", OPCODE }, + { "PLAYER@", OPCODE }, + { "MONSTER@", OPCODE }, + { "RANDOMSCN@", OPCODE }, + { "RANDOMCHR@", OPCODE }, + { "RANDOMOBJ@", OPCODE }, + { "\?\?\?(0xc7)", OPCODE }, + { "\?\?\?(0xc8)", OPCODE }, // 0xc8 + { "\?\?\?(0xc9)", OPCODE }, + { "\?\?\?(0xca)", OPCODE }, + { "\?\?\?(0xcb)", OPCODE }, + { "\?\?\?(0xcc)", OPCODE }, + { "\?\?\?(0xcd)", OPCODE }, + { "\?\?\?(0xce)", OPCODE }, + { "\?\?\?(0xcf)", OPCODE }, + { "PHYS.STR.BAS#", OPCODE }, // 0xd0 + { "PHYS.HIT.BAS#", OPCODE }, + { "PHYS.ARM.BAS#", OPCODE }, + { "PHYS.ACC.BAS#", OPCODE }, + { "SPIR.STR.BAS#", OPCODE }, + { "SPIR.HIT.BAS#", OPCODE }, + { "SPIR.ARM.BAS#", OPCODE }, + { "SPIR.ACC.BAS#", OPCODE }, + { "PHYS.SPE.BAS#", OPCODE }, // 0xd8 + { "\?\?\?(0xd9)", OPCODE }, + { "\?\?\?(0xda)", OPCODE }, + { "\?\?\?(0xdb)", OPCODE }, + { "\?\?\?(0xdc)", OPCODE }, + { "\?\?\?(0xdd)", OPCODE }, + { "\?\?\?(0xde)", OPCODE }, + { "\?\?\?(0xdf)", OPCODE }, + { "PHYS.STR.CUR#", OPCODE }, // 0xe0 + { "PHYS.HIT.CUR#", OPCODE }, + { "PHYS.ARM.CUR#", OPCODE }, + { "PHYS.ACC.CUR#", OPCODE }, + { "SPIR.STR.CUR#", OPCODE }, + { "SPIR.HIT.CUR#", OPCODE }, + { "SPIR.ARM.CUR#", OPCODE }, + { "SPIR.ACC.CUR#", OPCODE }, + { "PHYS.SPE.CUR#", OPCODE }, // 0xe8 + { "\?\?\?(0xe9)", OPCODE }, + { "\?\?\?(0xea)", OPCODE }, + { "\?\?\?(0xeb)", OPCODE }, + { "\?\?\?(0xec)", OPCODE }, + { "\?\?\?(0xed)", OPCODE }, + { "\?\?\?(0xee)", OPCODE }, + { "\?\?\?(0xef)", OPCODE }, + { "\?\?\?(0xf0)", OPCODE }, + { "\?\?\?(0xf1)", OPCODE }, + { "\?\?\?(0xf2)", OPCODE }, + { "\?\?\?(0xf3)", OPCODE }, + { "\?\?\?(0xf4)", OPCODE }, + { "\?\?\?(0xf5)", OPCODE }, + { "\?\?\?(0xf6)", OPCODE }, + { "\?\?\?(0xf7)", OPCODE }, + { "\?\?\?(0xf8)", OPCODE }, // 0xf8 + { "\?\?\?(0xf9)", OPCODE }, + { "\?\?\?(0xfa)", OPCODE }, + { "\?\?\?(0xfb)", OPCODE }, + { "\?\?\?(0xfc)", OPCODE }, + { "}\n", OPCODE }, + { "}THEN\n", BLOCK_START }, + { "\?\?\?(0xff)", OPCODE } // Uservar +}; + +void Script::convertToText() { + _data->seek(12); + + int indentLevel = 0; + ScriptText *scr = new ScriptText; + scr->offset = _data->pos(); + + while(true) { + int c = _data->readByte(); + + if (_data->eos()) + break; + + if (c < 0x80) { + if (c < 0x20) + error("convertToText: Unknown code 0x%02x at %d", c, _data->pos()); + + do { + scr->line += c; + c = _data->readByte(); + } while (c < 0x80); + + _data->seek(-1, SEEK_CUR); + } else if (c == 0xff) { + int value = _data->readByte(); + value -= 1; + scr->line += (char)('A' + (value / 9)); + scr->line += (char)('0' + (value % 9) + 1); + scr->line += '#'; + } else { + const char *cmd = mapping[c - 0x80].cmd; + int type = mapping[c - 0x80].type; + + if (type == STATEMENT) { + for (int i = 0; i < indentLevel; i++) + scr->line += ' '; + } else if (type == BLOCK_START) { + indentLevel += 2; + } else if (type == BLOCK_END) { + indentLevel -= 2; + for (int i = 0; i < indentLevel; i++) + scr->line += ' '; + } + + scr->line += cmd; + + if (strchr(cmd, '\n')) { + scr->line.deleteLastChar(); + + _scriptText.push_back(scr); + + scr = new ScriptText; + scr->offset = _data->pos(); + } + } + } + + if (!scr->line.empty()) + _scriptText.push_back(scr); + else + delete scr; +} + +} // End of namespace Wage |