/* 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 3 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, see . * */ #include "ultima/ultima4/game/script.h" #include "ultima/ultima4/game/armor.h" #include "ultima/ultima4/game/context.h" #include "ultima/ultima4/controllers/inn_controller.h" #include "ultima/ultima4/conversation/conversation.h" #include "ultima/ultima4/core/settings.h" #include "ultima/ultima4/core/utils.h" #include "ultima/ultima4/events/event_handler.h" #include "ultima/ultima4/filesys/savegame.h" #include "ultima/ultima4/game/game.h" #include "ultima/ultima4/game/player.h" #include "ultima/ultima4/game/weapon.h" #include "ultima/ultima4/game/spell.h" #include "ultima/ultima4/views/stats.h" #include "ultima/ultima4/gfx/screen.h" #include "ultima/ultima4/map/tileset.h" #include "ultima/ultima4/sound/music.h" #include "ultima/shared/conf/xml_tree.h" namespace Ultima { namespace Ultima4 { /* * Script::Variable class */ Script::Variable::Variable() : _iVal(0), _sVal(""), _set(false) {} Script::Variable::Variable(const Common::String &v) : _set(true) { _iVal = static_cast(strtol(v.c_str(), nullptr, 10)); _sVal = v; } Script::Variable::Variable(const int &v) : _set(true) { _iVal = v; _sVal = xu4_to_string(v); } int &Script::Variable::getInt() { return _iVal; } Common::String &Script::Variable::getString() { return _sVal; } void Script::Variable::setValue(const int &v) { _iVal = v; } void Script::Variable::setValue(const Common::String &v) { _sVal = v; } void Script::Variable::unset() { _set = false; _iVal = 0; _sVal = ""; } bool Script::Variable::isInt() const { return _iVal > 0; } bool Script::Variable::isString() const { return _iVal == 0; } bool Script::Variable::isSet() const { return _set; } Script::Script() : _vendorScriptDoc(nullptr), _scriptNode(nullptr), _debug(false), _state(STATE_UNLOADED), _currentScript(nullptr), _currentItem(nullptr), _inputType(INPUT_CHOICE), _inputMaxLen(0), _nounName("item"), _idPropName("id"), _iterator(0) { _actionMap["context"] = ACTION_SET_CONTEXT; _actionMap["unset_context"] = ACTION_UNSET_CONTEXT; _actionMap["end"] = ACTION_END; _actionMap["redirect"] = ACTION_REDIRECT; _actionMap["wait_for_keypress"] = ACTION_WAIT_FOR_KEY; _actionMap["wait"] = ACTION_WAIT; _actionMap["stop"] = ACTION_STOP; _actionMap["include"] = ACTION_INCLUDE; _actionMap["for"] = ACTION_FOR_LOOP; _actionMap["random"] = ACTION_RANDOM; _actionMap["move"] = ACTION_MOVE; _actionMap["sleep"] = ACTION_SLEEP; _actionMap["cursor"] = ACTION_CURSOR; _actionMap["pay"] = ACTION_PAY; _actionMap["if"] = ACTION_IF; _actionMap["input"] = ACTION_INPUT; _actionMap["add"] = ACTION_ADD; _actionMap["lose"] = ACTION_LOSE; _actionMap["heal"] = ACTION_HEAL; _actionMap["cast_spell"] = ACTION_CAST_SPELL; _actionMap["damage"] = ACTION_DAMAGE; _actionMap["karma"] = ACTION_KARMA; _actionMap["music"] = ACTION_MUSIC; _actionMap["var"] = ACTION_SET_VARIABLE; _actionMap["ztats"] = ACTION_ZTATS; } Script::~Script() { unload(); // We have many Variables that are allocated but need to have delete called on them. // We do not need to clear the containers (that will happen automatically), but we do need to delete // these things. Do NOT clean up the providers though, it seems the providers map doesn't own its pointers. // Smart pointers anyone? // Clean variables for (auto &item : _variables) { delete item._value; } } void Script::removeCurrentVariable(const Common::String &name) { Common::HashMap::iterator dup = _variables.find(name); if (dup != _variables.end()) { delete dup->_value; _variables.erase(dup); // not strictly necessary, but correct. } } void Script::addProvider(const Common::String &name, Provider *p) { _providers[name] = p; } bool Script::load(const Common::String &filename, const Common::String &baseId, const Common::String &subNodeName, const Common::String &subNodeId) { Shared::XMLNode *root, *node, *child; _state = STATE_NORMAL; /* unload previous script */ unload(); /** * Open and parse the .xml file */ Shared::XMLTree *doc = new Shared::XMLTree( Common::Path(Common::String::format("data/conf/%s", filename.c_str()))); _vendorScriptDoc = root = doc->getTree(); if (!root->id().equalsIgnoreCase("scripts")) error("malformed %s", filename.c_str()); // Check whether script is set to debug _debug = root->getPropertyBool("debug"); /** * Get a new global item name or id name */ if (root->hasProperty("noun")) _nounName = root->getProperty("noun"); if (root->hasProperty("id_prop")) _idPropName = root->getProperty("id_prop"); _currentScript = nullptr; _currentItem = nullptr; for (node = root->firstChild(); node; node = node->getNext()) { if (node->nodeIsText() || !node->id().equalsIgnoreCase("script")) continue; if (baseId == node->getProperty("id")) { /** * We use the base node as our main script node */ if (subNodeName.empty()) { _scriptNode = node; _translationContext.push_back(node); break; } for (child = node->firstChild(); child; child = child->getNext()) { if (child->nodeIsText() || !child->id().equalsIgnoreCase(subNodeName)) continue; Common::String id = child->getProperty("id"); if (id == subNodeId) { _scriptNode = child; _translationContext.push_back(child); /** * Get a new local item name or id name */ if (node->hasProperty("noun")) _nounName = node->getProperty("noun"); if (node->hasProperty("id_prop")) _idPropName = node->getProperty("id_prop"); break; } } if (_scriptNode) break; } } if (_scriptNode) { /** * Get a new local item name or id name */ if (_scriptNode->hasProperty("noun")) _nounName = _scriptNode->getProperty("noun"); if (_scriptNode->hasProperty("id_prop")) _idPropName = _scriptNode->getProperty("id_prop"); if (_debug) debug("\n\n", subNodeName.c_str(), subNodeId.c_str(), baseId.c_str()); } else { if (subNodeName.empty()) error("Couldn't find script '%s' in %s", baseId.c_str(), filename.c_str()); else error("Couldn't find subscript '%s' where id='%s' in script '%s' in %s", subNodeName.c_str(), subNodeId.c_str(), baseId.c_str(), filename.c_str()); } _state = STATE_UNLOADED; return false; } void Script::unload() { if (_vendorScriptDoc) { _vendorScriptDoc->freeDoc(); _vendorScriptDoc = nullptr; } } void Script::run(const Common::String &script) { Shared::XMLNode *scriptNode; Common::String search_id; if (_variables.find(_idPropName) != _variables.end()) { if (_variables[_idPropName]->isSet()) search_id = _variables[_idPropName]->getString(); else search_id = "null"; } scriptNode = find(_scriptNode, script, search_id); if (!scriptNode) error("Script '%s' not found in vendorScript.xml", script.c_str()); execute(scriptNode); } Script::ReturnCode Script::execute(Shared::XMLNode *script, Shared::XMLNode *currentItem, Common::String *output) { Shared::XMLNode *current; Script::ReturnCode retval = RET_OK; if (!script->hasChildren()) { /* redirect the script to another node */ if (script->hasProperty("redirect")) retval = redirect(nullptr, script); /* end the conversation */ else { if (_debug) debug("A script with no children found (nowhere to go). Ending script..."); g_screen->screenMessage("\n"); _state = STATE_DONE; } } /* do we start where we left off, or start from the beginning? */ if (currentItem) { current = currentItem->getNext(); if (_debug) debug("Returning to execution from end of '%s' script", currentItem->id().c_str()); } else { current = script->firstChild(); } for (; current; current = current->getNext()) { Common::String name = current->id(); retval = RET_OK; ActionMap::iterator action; /* nothing left to do */ if (_state == STATE_DONE) break; /* begin execution of script */ /** * Handle Text */ if (current->nodeIsText()) { Common::String content = getContent(current); if (output) *output += content; else g_screen->screenMessage("%s", content.c_str()); if (_debug && content.size()) debug("Output: \n====================\n%s\n====================", content.c_str()); } else { /** * Search for the corresponding action and execute it! */ action = _actionMap.find(name); if (action != _actionMap.end()) { /** * Found it! */ switch (action->_value) { case ACTION_SET_CONTEXT: retval = pushContext(script, current); break; case ACTION_UNSET_CONTEXT: retval = popContext(script, current); break; case ACTION_END: retval = end(script, current); break; case ACTION_REDIRECT: retval = redirect(script, current); break; case ACTION_WAIT_FOR_KEY: retval = waitForKeypress(script, current); break; case ACTION_WAIT: retval = wait(script, current); break; case ACTION_STOP: retval = RET_STOP; break; case ACTION_INCLUDE: retval = include(script, current); break; case ACTION_FOR_LOOP: retval = forLoop(script, current); break; case ACTION_RANDOM: retval = randomScript(script, current); break; case ACTION_MOVE: retval = move(script, current); break; case ACTION_SLEEP: retval = sleep(script, current); break; case ACTION_CURSOR: retval = cursor(script, current); break; case ACTION_PAY: retval = pay(script, current); break; case ACTION_IF: retval = _if(script, current); break; case ACTION_INPUT: retval = input(script, current); break; case ACTION_ADD: retval = add(script, current); break; case ACTION_LOSE: retval = lose(script, current); break; case ACTION_HEAL: retval = heal(script, current); break; case ACTION_CAST_SPELL: retval = castSpell(script, current); break; case ACTION_DAMAGE: retval = damage(script, current); break; case ACTION_KARMA: retval = karma(script, current); break; case ACTION_MUSIC: retval = music(script, current); break; case ACTION_SET_VARIABLE: retval = setVar(script, current); break; case ACTION_ZTATS: retval = ztats(script, current); break; default: break; } } /** * Didn't find the corresponding action... */ else if (_debug) debug("ERROR: '%s' method not found", name.c_str()); /* The script was redirected or stopped, stop now! */ if ((retval == RET_REDIRECTED) || (retval == RET_STOP)) break; } if (_debug) debug("\n"); } return retval; } void Script::_continue() { /* reset our script state to normal */ resetState(); /* there's no target indicated, just start where we left off! */ if (_target.empty()) execute(_currentScript, _currentItem); else run(_target); } void Script::resetState() { _state = STATE_NORMAL; } void Script::setState(Script::State s) { _state = s; } void Script::setTarget(const Common::String &val) { _target = val; } void Script::setChoices(const Common::String &val) { _choices = val; } void Script::setVar(const Common::String &name, const Common::String &val) { removeCurrentVariable(name); _variables[name] = new Variable(val); } void Script::setVar(const Common::String &name, int val) { removeCurrentVariable(name); _variables[name] = new Variable(val); } void Script::unsetVar(const Common::String &name) { // Ensure that the variable at least exists, but has no value if (_variables.find(name) != _variables.end()) _variables[name]->unset(); else _variables[name] = new Variable(); } Script::State Script::getState() { return _state; } Common::String Script::getTarget() { return _target; } Script::InputType Script::getInputType() { return _inputType; } Common::String Script::getChoices() { return _choices; } Common::String Script::getInputName() { return _inputName; } int Script::getInputMaxLen() { return _inputMaxLen; } void Script::translate(Common::String *text) { uint pos; bool nochars = true; Shared::XMLNode *node = _translationContext.back(); /* determine if the script is completely whitespace */ for (Common::String::iterator current = text->begin(); current != text->end(); current++) { if (Common::isAlnum(*current)) { nochars = false; break; } } /* erase scripts that are composed entirely of whitespace */ if (nochars) text->clear(); while ((pos = text->findFirstOf("{")) < text->size()) { Common::String pre = text->substr(0, pos); Common::String post; Common::String item = text->substr(pos + 1); /** * Handle embedded items */ int num_embedded = 0; int total_pos = 0; Common::String current = item; while (true) { uint open = current.findFirstOf("{"), close = current.findFirstOf("}"); if (close == current.size()) error("Error: no closing } found in script."); if (open < close) { num_embedded++; total_pos += open + 1; current = current.substr(open + 1); } if (close < open) { total_pos += close; if (num_embedded == 0) { pos = total_pos; break; } num_embedded--; total_pos += 1; current = current.substr(close + 1); } } /** * Separate the item itself from the pre- and post-data */ post = item.substr(pos + 1); item = item.substr(0, pos); if (_debug) debugN("{%s} == ", item.c_str()); /* translate any stuff contained in the item */ translate(&item); Common::String prop; // Get defined variables if (item[0] == '$') { Common::String varName = item.substr(1); if (_variables.find(varName) != _variables.end()) prop = _variables[varName]->getString(); } // Get the current iterator for our loop else if (item == "iterator") prop = xu4_to_string(_iterator); else if ((pos = item.find("show_inventory:")) < item.size()) { pos = item.find(":"); Common::String itemScript = item.substr(pos + 1); Shared::XMLNode *itemShowScript = find(node, itemScript); Shared::XMLNode *nodePtr; prop.clear(); /** * Save iterator */ int oldIterator = _iterator; /* start iterator at 0 */ _iterator = 0; for (nodePtr = node->firstChild(); nodePtr; nodePtr = nodePtr->getNext()) { if (nodePtr->id().equalsIgnoreCase(_nounName)) { bool hidden = nodePtr->getPropertyBool("hidden"); if (!hidden) { /* make sure the nodePtr's requisites are met */ if (!nodePtr->hasProperty("req") || compare(nodePtr->getProperty("req"))) { /* put a newline after each */ if (_iterator > 0) prop += "\n"; /* set translation context to nodePtr */ _translationContext.push_back(nodePtr); execute(itemShowScript, nullptr, &prop); _translationContext.pop_back(); _iterator++; } } } } /** * Restore iterator to previous value */ _iterator = oldIterator; } /** * Make a Common::String containing the available ids using the * vendor's inventory (i.e. "bcde") */ else if (item == "inventory_choices") { Shared::XMLNode *nodePtr; Common::String ids; for (nodePtr = node->firstChild(); nodePtr; nodePtr = nodePtr->getNext()) { if (nodePtr->id().equalsIgnoreCase(_nounName)) { Common::String id = getPropAsStr(nodePtr, _idPropName.c_str()); /* make sure the nodePtr's requisites are met */ if (!nodePtr->hasProperty("req") || (compare(getPropAsStr(nodePtr, "req")))) ids += id[0]; } } prop = ids; } /** * Ask our providers if they have a valid translation for us */ else if (item.findFirstOf(":") != Common::String::npos) { int index = item.findFirstOf(":"); Common::String provider = item; Common::String to_find; provider = item.substr(0, index); to_find = item.substr(index + 1); if (_providers.find(provider) != _providers.end()) { Std::vector parts = split(to_find, ":"); Provider *p = _providers[provider]; prop = p->translate(parts); } } /** * Resolve as a property name or a function */ else { Common::String funcName, content; funcParse(item, &funcName, &content); /* * Check to see if it's a property name */ if (funcName.empty()) { /* we have the property name, now go get the property value! */ prop = getPropAsStr(_translationContext, item, true); } /** * We have a function, make it work! */ else { /* perform the function on the content */ if (funcName == "math") { if (content.empty()) warning("Error: empty math() function"); prop = xu4_to_string(mathValue(content)); } /** * Does a true/false comparison on the content. * Replaced with "true" if evaluates to true, or "false" if otherwise */ else if (funcName == "compare") { if (compare(content)) prop = "true"; else prop = "false"; } /* make the Common::String upper case */ else if (funcName == "toupper") { Common::String::iterator it; for (it = content.begin(); it != content.end(); it++) *it = toupper(*it); prop = content; } /* make the Common::String lower case */ else if (funcName == "tolower") { Common::String::iterator it; for (it = content.begin(); it != content.end(); it++) *it = tolower(*it); prop = content; } /* generate a random number */ else if (funcName == "random") prop = xu4_to_string(xu4_random((int)strtol(content.c_str(), nullptr, 10))); /* replaced with "true" if content is empty, or "false" if not */ else if (funcName == "isempty") { if (content.empty()) prop = "true"; else prop = "false"; } } } if (prop.empty() && _debug) debug("Warning: dynamic property '{%s}' not found in vendor script (was this intentional?)", item.c_str()); if (_debug) debug("\"%s\"", prop.c_str()); /* put the script back together */ *text = pre + prop + post; } /* remove all unnecessary spaces from xml */ while ((pos = text->find("\t")) < text->size()) text->replace(pos, 1, ""); while ((pos = text->find(" ")) < text->size()) text->replace(pos, 2, ""); while ((pos = text->find("\n ")) < text->size()) text->replace(pos, 2, "\n"); } Shared::XMLNode *Script::find(Shared::XMLNode *node, const Common::String &script_to_find, const Common::String &id, bool _default) { Shared::XMLNode *current; if (node) { for (current = node->firstChild(); current; current = current->getNext()) { if (!current->nodeIsText() && (script_to_find == current->id().c_str())) { if (id.empty() && !current->hasProperty(_idPropName.c_str()) && !_default) return current; else if (current->hasProperty(_idPropName.c_str()) && (id == current->getProperty(_idPropName))) return current; else if (_default && current->hasProperty("default") && current->getPropertyBool("default")) return current; } } /* only search the parent nodes if we haven't hit the base