Files
scummvm-cursorfix/engines/ultima/ultima4/game/script.cpp
2026-02-02 04:50:13 +01:00

1536 lines
42 KiB
C++

/* 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 <http://www.gnu.org/licenses/>.
*
*/
#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<int>(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<Common::String, Script::Variable *>::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<Loaded subscript '%s' where id='%s' for script '%s'>\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<Common::String> 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 <math> 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 <script> node */
if (!node->id().equalsIgnoreCase("script"))
current = find(node->getParent(), script_to_find, id);
/* find the default script instead */
if (!current && !id.empty() && !_default)
current = find(node, script_to_find, "", true);
return current;
}
return nullptr;
}
Common::String Script::getPropAsStr(Common::List<Shared::XMLNode *> &nodes, const Common::String &prop, bool recursive) {
Common::String propvalue;
Common::List<Shared::XMLNode *>::iterator i;
for (i = nodes.reverse_begin(); i != nodes.end(); --i) {
Shared::XMLNode *node = *i;
if (node->hasProperty(prop)) {
propvalue = node->getProperty(prop);
break;
}
}
if (propvalue.empty() && recursive) {
for (i = nodes.reverse_begin(); i != nodes.end(); --i) {
Shared::XMLNode *node = *i;
if (node->getParent()) {
propvalue = getPropAsStr(node->getParent(), prop, recursive);
break;
}
}
}
translate(&propvalue);
return propvalue;
}
Common::String Script::getPropAsStr(Shared::XMLNode *node, const Common::String &prop, bool recursive) {
Common::List<Shared::XMLNode *> list;
list.push_back(node);
return getPropAsStr(list, prop, recursive);
}
int Script::getPropAsInt(Common::List<Shared::XMLNode *> &nodes, const Common::String &prop, bool recursive) {
Common::String propvalue = getPropAsStr(nodes, prop, recursive);
return mathValue(propvalue);
}
int Script::getPropAsInt(Shared::XMLNode *node, const Common::String &prop, bool recursive) {
Common::String propvalue = getPropAsStr(node, prop, recursive);
return mathValue(propvalue);
}
Common::String Script::getContent(Shared::XMLNode *node) {
Common::String content = node->text();
translate(&content);
return content;
}
Script::ReturnCode Script::pushContext(Shared::XMLNode *script, Shared::XMLNode *current) {
Common::String nodeName = getPropAsStr(current, "name");
Common::String search_id;
if (current->hasProperty(_idPropName.c_str()))
search_id = getPropAsStr(current, _idPropName);
else if (_variables.find(_idPropName) != _variables.end()) {
if (_variables[_idPropName]->isSet())
search_id = _variables[_idPropName]->getString();
else
search_id = "null";
}
// When looking for a new context, start from within our old one
_translationContext.push_back(find(_translationContext.back(), nodeName, search_id));
if (_debug) {
if (!_translationContext.back())
debug("Warning!!! Invalid translation context <%s %s=\"%s\" ...>", nodeName.c_str(), _idPropName.c_str(), search_id.c_str());
else
debug("Changing translation context to <%s %s=\"%s\" ...>", nodeName.c_str(), _idPropName.c_str(), search_id.c_str());
}
return RET_OK;
}
Script::ReturnCode Script::popContext(Shared::XMLNode *script, Shared::XMLNode *current) {
if (_translationContext.size() > 1) {
_translationContext.pop_back();
if (_debug)
debug("Reverted translation context to <%s ...>", _translationContext.back()->id().c_str());
}
return RET_OK;
}
Script::ReturnCode Script::end(Shared::XMLNode *script, Shared::XMLNode *current) {
/**
* See if there's a global 'end' node declared for cleanup
*/
Shared::XMLNode *endScript = find(_scriptNode, "end");
if (endScript)
execute(endScript);
if (_debug)
debug("<End script>");
_state = STATE_DONE;
return RET_STOP;
}
Script::ReturnCode Script::waitForKeypress(Shared::XMLNode *script, Shared::XMLNode *current) {
_currentScript = script;
_currentItem = current;
_choices = "abcdefghijklmnopqrstuvwxyz01234567890\015 \033";
_target.clear();
_state = STATE_INPUT;
_inputType = INPUT_KEYPRESS;
if (_debug)
debug("<Wait>");
return RET_STOP;
}
Script::ReturnCode Script::redirect(Shared::XMLNode *script, Shared::XMLNode *current) {
Common::String target;
if (current->hasProperty("redirect"))
target = getPropAsStr(current, "redirect");
else
target = getPropAsStr(current, "target");
/* set a new search id */
Common::String search_id = getPropAsStr(current, _idPropName);
Shared::XMLNode *newScript = find(_scriptNode, target, search_id);
if (!newScript)
error("Error: redirect failed -- could not find target script '%s' with %s=\"%s\"", target.c_str(), _idPropName.c_str(), search_id.c_str());
if (_debug) {
debugN("Redirected to <%s", target.c_str());
if (search_id.size())
debugN(" %s=\"%s\"", _idPropName.c_str(), search_id.c_str());
debug(" .../>");
}
execute(newScript);
return RET_REDIRECTED;
}
Script::ReturnCode Script::include(Shared::XMLNode *script, Shared::XMLNode *current) {
Common::String scriptName = getPropAsStr(current, "script");
Common::String id = getPropAsStr(current, _idPropName);
Shared::XMLNode *newScript = find(_scriptNode, scriptName, id);
if (!newScript)
error("Error: include failed -- could not find target script '%s' with %s=\"%s\"", scriptName.c_str(), _idPropName.c_str(), id.c_str());
if (_debug) {
debugN("Included script <%s", scriptName.c_str());
if (!id.empty())
debugN(" %s=\"%s\"", _idPropName.c_str(), id.c_str());
debug(" .../>");
}
execute(newScript);
return RET_OK;
}
Script::ReturnCode Script::wait(Shared::XMLNode *script, Shared::XMLNode *current) {
int msecs = getPropAsInt(current, "msecs");
EventHandler::wait_msecs(msecs);
return RET_OK;
}
Script::ReturnCode Script::forLoop(Shared::XMLNode *script, Shared::XMLNode *current) {
Script::ReturnCode retval = RET_OK;
int start = getPropAsInt(current, "start"),
end = getPropAsInt(current, "end"),
/* save the iterator in case this loop is nested */
oldIterator = _iterator,
i;
if (_debug)
debug("\n<For Start=%d End=%d>", start, end);
for (i = start, _iterator = start;
i <= end;
i++, _iterator++) {
if (_debug)
debug("%d: ", i);
retval = execute(current);
if ((retval == RET_REDIRECTED) || (retval == RET_STOP))
break;
}
/* restore the previous iterator */
_iterator = oldIterator;
return retval;
}
Script::ReturnCode Script::randomScript(Shared::XMLNode *script, Shared::XMLNode *current) {
int perc = getPropAsInt(current, "chance");
int num = xu4_random(100);
Script::ReturnCode retval = RET_OK;
if (num < perc)
retval = execute(current);
if (_debug)
debug("Random (%d%%): rolled %d (%s)", perc, num, (num < perc) ? "Succeeded" : "Failed");
return retval;
}
Script::ReturnCode Script::move(Shared::XMLNode *script, Shared::XMLNode *current) {
if (current->hasProperty("x"))
g_context->_location->_coords.x = getPropAsInt(current, "x");
if (current->hasProperty("y"))
g_context->_location->_coords.y = getPropAsInt(current, "y");
if (current->hasProperty("z"))
g_context->_location->_coords.z = getPropAsInt(current, "z");
if (_debug)
debug("Move: x-%d y-%d z-%d", g_context->_location->_coords.x, g_context->_location->_coords.y, g_context->_location->_coords.z);
gameUpdateScreen();
return RET_OK;
}
Script::ReturnCode Script::sleep(Shared::XMLNode *script, Shared::XMLNode *current) {
if (_debug)
debug("Sleep!");
CombatController *cc = new InnController();
cc->begin();
return RET_OK;
}
Script::ReturnCode Script::cursor(Shared::XMLNode *script, Shared::XMLNode *current) {
bool enable = current->getPropertyBool("enable");
if (enable)
g_screen->screenEnableCursor();
else
g_screen->screenDisableCursor();
return RET_OK;
}
Script::ReturnCode Script::pay(Shared::XMLNode *script, Shared::XMLNode *current) {
int price = getPropAsInt(current, "price");
int quant = getPropAsInt(current, "quantity");
Common::String cantpay = getPropAsStr(current, "cantpay");
if (price < 0)
error("Error: could not find price for item");
if (_debug) {
debug("Pay: price(%d) quantity(%d)", price, quant);
debug("\tParty gold: %d -", g_ultima->_saveGame->_gold);
debug("\tTotal price: %d", price * quant);
}
price *= quant;
if (price > g_ultima->_saveGame->_gold) {
if (_debug)
debug("\t=== Can't pay! ===");
run(cantpay);
return RET_STOP;
} else {
g_context->_party->adjustGold(-price);
}
if (_debug)
debug("\tBalance: %d\n", g_ultima->_saveGame->_gold);
return RET_OK;
}
Script::ReturnCode Script::_if(Shared::XMLNode *script, Shared::XMLNode *current) {
Common::String test = getPropAsStr(current, "test");
Script::ReturnCode retval = RET_OK;
if (_debug)
debugN("If(%s) - ", test.c_str());
if (compare(test)) {
if (_debug)
debug("True - Executing '%s'", current->id().c_str());
retval = execute(current);
} else if (_debug)
debug("False");
return retval;
}
Script::ReturnCode Script::input(Shared::XMLNode *script, Shared::XMLNode *current) {
Common::String type = getPropAsStr(current, "type");
_currentScript = script;
_currentItem = current;
if (current->hasProperty("target"))
_target = getPropAsStr(current, "target");
else
_target.clear();
_state = STATE_INPUT;
_inputName = "input";
// Does the variable have a maximum length?
if (current->hasProperty("maxlen"))
_inputMaxLen = getPropAsInt(current, "maxlen");
else
_inputMaxLen = Conversation::BUFFERLEN;
// Should we name the variable something other than "input"
if (current->hasProperty("name"))
_inputName = getPropAsStr(current, "name");
else {
if (type == "choice")
_inputName = _idPropName;
}
if (type == "number")
_inputType = INPUT_NUMBER;
else if (type == "keypress")
_inputType = INPUT_KEYPRESS;
else if (type == "choice") {
_inputType = INPUT_CHOICE;
_choices = getPropAsStr(current, "options");
_choices += " \015\033";
} else if (type == "text")
_inputType = INPUT_STRING;
else if (type == "direction")
_inputType = INPUT_DIRECTION;
else if (type == "player")
_inputType = INPUT_PLAYER;
if (_debug)
debug("Input: %s", type.c_str());
/* the script stops here, at least for now */
return RET_STOP;
}
Script::ReturnCode Script::add(Shared::XMLNode *script, Shared::XMLNode *current) {
Common::String type = getPropAsStr(current, "type");
Common::String subtype = getPropAsStr(current, "subtype");
int quant = getPropAsInt(_translationContext.back(), "quantity");
if (quant == 0)
quant = getPropAsInt(current, "quantity");
else
quant *= getPropAsInt(current, "quantity");
if (_debug) {
debugN("Add: %s ", type.c_str());
if (!subtype.empty())
debug("- %s ", subtype.c_str());
}
if (type == "gold")
g_context->_party->adjustGold(quant);
else if (type == "food") {
quant *= 100;
g_context->_party->adjustFood(quant);
} else if (type == "horse")
g_context->_party->setTransport(g_tileSets->findTileByName("horse")->getId());
else if (type == "torch") {
AdjustValueMax(g_ultima->_saveGame->_torches, quant, 99);
g_context->_party->notifyOfChange(0, PartyEvent::INVENTORY_ADDED);
} else if (type == "gem") {
AdjustValueMax(g_ultima->_saveGame->_gems, quant, 99);
g_context->_party->notifyOfChange(0, PartyEvent::INVENTORY_ADDED);
} else if (type == "key") {
AdjustValueMax(g_ultima->_saveGame->_keys, quant, 99);
g_context->_party->notifyOfChange(0, PartyEvent::INVENTORY_ADDED);
} else if (type == "sextant") {
AdjustValueMax(g_ultima->_saveGame->_sextants, quant, 99);
g_context->_party->notifyOfChange(0, PartyEvent::INVENTORY_ADDED);
} else if (type == "weapon") {
AdjustValueMax(g_ultima->_saveGame->_weapons[subtype[0] - 'a'], quant, 99);
g_context->_party->notifyOfChange(0, PartyEvent::INVENTORY_ADDED);
} else if (type == "armor") {
AdjustValueMax(g_ultima->_saveGame->_armor[subtype[0] - 'a'], quant, 99);
g_context->_party->notifyOfChange(0, PartyEvent::INVENTORY_ADDED);
} else if (type == "reagent") {
int reagent;
static const Common::String reagents[] = {
"ash", "ginseng", "garlic", "silk", "moss", "pearl", "mandrake", "nightshade", ""
};
for (reagent = 0; reagents[reagent].size(); reagent++) {
if (reagents[reagent] == subtype)
break;
}
if (reagents[reagent].size()) {
AdjustValueMax(g_ultima->_saveGame->_reagents[reagent], quant, 99);
g_context->_party->notifyOfChange(0, PartyEvent::INVENTORY_ADDED);
g_context->_stats->resetReagentsMenu();
} else {
warning("Error: reagent '%s' not found", subtype.c_str());
}
}
if (_debug)
debug("(x%d)", quant);
return RET_OK;
}
Script::ReturnCode Script::lose(Shared::XMLNode *script, Shared::XMLNode *current) {
Common::String type = getPropAsStr(current, "type");
Common::String subtype = getPropAsStr(current, "subtype");
int quant = getPropAsInt(current, "quantity");
if (type == "weapon")
AdjustValueMin(g_ultima->_saveGame->_weapons[subtype[0] - 'a'], -quant, 0);
else if (type == "armor")
AdjustValueMin(g_ultima->_saveGame->_armor[subtype[0] - 'a'], -quant, 0);
if (_debug) {
debugN("Lose: %s ", type.c_str());
if (subtype.size())
debug("- %s ", subtype.c_str());
debug("(x%d)", quant);
}
return RET_OK;
}
Script::ReturnCode Script::heal(Shared::XMLNode *script, Shared::XMLNode *current) {
Common::String type = getPropAsStr(current, "type");
PartyMember *p = g_context->_party->member(getPropAsInt(current, "player") - 1);
if (type == "cure")
p->heal(HT_CURE);
else if (type == "heal")
p->heal(HT_HEAL);
else if (type == "fullheal")
p->heal(HT_FULLHEAL);
else if (type == "resurrect")
p->heal(HT_RESURRECT);
return RET_OK;
}
Script::ReturnCode Script::castSpell(Shared::XMLNode *script, Shared::XMLNode *current) {
g_spells->spellEffect('r', -1, SOUND_MAGIC);
if (_debug)
debug("<Spell effect>");
return RET_OK;
}
Script::ReturnCode Script::damage(Shared::XMLNode *script, Shared::XMLNode *current) {
int player = getPropAsInt(current, "player") - 1;
int pts = getPropAsInt(current, "pts");
PartyMember *p;
p = g_context->_party->member(player);
p->applyDamage(pts);
if (_debug)
debug("Damage: %d damage to player %d", pts, player + 1);
return RET_OK;
}
Script::ReturnCode Script::karma(Shared::XMLNode *script, Shared::XMLNode *current) {
Common::String action = getPropAsStr(current, "action");
if (_debug)
debugN("Karma: adjusting - '%s'", action.c_str());
typedef Common::HashMap<Common::String, KarmaAction /*, Std::less<Common::String> */> KarmaActionMap;
static KarmaActionMap action_map;
if (action_map.size() == 0) {
action_map["found_item"] = KA_FOUND_ITEM;
action_map["stole_chest"] = KA_STOLE_CHEST;
action_map["gave_to_beggar"] = KA_GAVE_TO_BEGGAR;
action_map["bragged"] = KA_BRAGGED;
action_map["humble"] = KA_HUMBLE;
action_map["hawkwind"] = KA_HAWKWIND;
action_map["meditation"] = KA_MEDITATION;
action_map["bad_mantra"] = KA_BAD_MANTRA;
action_map["attacked_good"] = KA_ATTACKED_GOOD;
action_map["fled_evil"] = KA_FLED_EVIL;
action_map["fled_good"] = KA_FLED_GOOD;
action_map["healthy_fled_evil"] = KA_HEALTHY_FLED_EVIL;
action_map["killed_evil"] = KA_KILLED_EVIL;
action_map["spared_good"] = KA_SPARED_GOOD;
action_map["gave_blood"] = KA_DONATED_BLOOD;
action_map["didnt_give_blood"] = KA_DIDNT_DONATE_BLOOD;
action_map["cheated_merchant"] = KA_CHEAT_REAGENTS;
action_map["honest_to_merchant"] = KA_DIDNT_CHEAT_REAGENTS;
action_map["used_skull"] = KA_USED_SKULL;
action_map["destroyed_skull"] = KA_DESTROYED_SKULL;
}
KarmaActionMap::iterator ka = action_map.find(action);
if (ka != action_map.end())
g_context->_party->adjustKarma(ka->_value);
else if (_debug)
debug(" <FAILED - action '%s' not found>", action.c_str());
return RET_OK;
}
Script::ReturnCode Script::music(Shared::XMLNode *script, Shared::XMLNode *current) {
if (current->getPropertyBool("reset"))
g_music->playMapMusic();
else {
Common::String type = getPropAsStr(current, "type");
if (current->getPropertyBool("play"))
g_music->playMapMusic();
if (current->getPropertyBool("stop"))
g_music->stop();
else if (type == "shopping")
g_music->shopping();
else if (type == "camp")
g_music->camp();
}
return RET_OK;
}
Script::ReturnCode Script::setVar(Shared::XMLNode *script, Shared::XMLNode *current) {
Common::String name = getPropAsStr(current, "name");
Common::String value = getPropAsStr(current, "value");
if (name.empty()) {
if (_debug)
debug("Variable name empty!");
return RET_STOP;
}
removeCurrentVariable(name);
_variables[name] = new Variable(value);
if (_debug)
debug("Set Variable: %s=%s", name.c_str(), _variables[name]->getString().c_str());
return RET_OK;
}
Script::ReturnCode Script::ztats(Shared::XMLNode *script, Shared::XMLNode *current) {
typedef Common::HashMap<Common::String, StatsView/*, Std::less<Common::String>*/ > StatsViewMap;
static StatsViewMap view_map;
if (view_map.size() == 0) {
view_map["party"] = STATS_PARTY_OVERVIEW;
view_map["party1"] = STATS_CHAR1;
view_map["party2"] = STATS_CHAR2;
view_map["party3"] = STATS_CHAR3;
view_map["party4"] = STATS_CHAR4;
view_map["party5"] = STATS_CHAR5;
view_map["party6"] = STATS_CHAR6;
view_map["party7"] = STATS_CHAR7;
view_map["party8"] = STATS_CHAR8;
view_map["weapons"] = STATS_WEAPONS;
view_map["armor"] = STATS_ARMOR;
view_map["equipment"] = STATS_EQUIPMENT;
view_map["item"] = STATS_ITEMS;
view_map["reagents"] = STATS_REAGENTS;
view_map["mixtures"] = STATS_MIXTURES;
}
if (current->hasProperty("screen")) {
Common::String screen = getPropAsStr(current, "screen");
StatsViewMap::iterator view;
if (_debug)
debug("Ztats: %s", screen.c_str());
/**
* Find the correct stats view
*/
view = view_map.find(screen);
if (view != view_map.end())
g_context->_stats->setView(view->_value); /* change it! */
else if (_debug)
debug(" <FAILED - view could not be found>");
} else {
g_context->_stats->setView(STATS_PARTY_OVERVIEW);
}
return RET_OK;
}
void Script::mathParseChildren(Shared::XMLNode *math, Common::String *result) {
Shared::XMLNode *current;
result->clear();
for (current = math->firstChild(); current; current = current->getNext()) {
if (current->nodeIsText()) {
*result = getContent(current);
} else if (current->id().equalsIgnoreCase("math")) {
Common::String children_results;
mathParseChildren(current, &children_results);
*result = xu4_to_string(mathValue(children_results));
}
}
}
bool Script::mathParse(const Common::String &str, int *lval, int *rval, Common::String *op) {
Common::String left, right;
parseOperation(str, &left, &right, op);
if (op->empty())
return false;
if (left.size() == 0 || right.size() == 0)
return false;
/* make sure that we're dealing with numbers */
if (!Common::isDigit(left[0]) || !Common::isDigit(right[0]))
return false;
*lval = (int)strtol(left.c_str(), nullptr, 10);
*rval = (int)strtol(right.c_str(), nullptr, 10);
return true;
}
void Script::parseOperation(const Common::String &str, Common::String *left, Common::String *right, Common::String *op) {
/* list the longest operators first, so they're detected correctly */
static const Common::String ops[] = {"==", ">=", "<=", "+", "-", "*", "/", "%", "=", ">", "<", ""};
int pos = 0,
i = 0;
pos = str.find(ops[i]);
while ((pos <= 0) && !ops[i].empty()) {
i++;
pos = str.find(ops[i]);
}
if (ops[i].empty()) {
op->clear();
return;
} else {
*op = ops[i];
}
*left = str.substr(0, pos);
*right = str.substr(pos + ops[i].size());
}
int Script::mathValue(const Common::String &str) {
int lval, rval;
Common::String op;
/* something was invalid, just return the integer value */
if (!mathParse(str, &lval, &rval, &op))
return (int)strtol(str.c_str(), nullptr, 10);
else
return math(lval, rval, op);
}
int Script::math(int lval, int rval, Common::String &op) {
if (op == "+")
return lval + rval;
else if (op == "-")
return lval - rval;
else if (op == "*")
return lval * rval;
else if (op == "/")
return lval / rval;
else if (op == "%")
return lval % rval;
else if ((op == "=") || (op == "=="))
return lval == rval;
else if (op == ">")
return lval > rval;
else if (op == "<")
return lval < rval;
else if (op == ">=")
return lval >= rval;
else if (op == "<=")
return lval <= rval;
else
error("Error: invalid 'math' operation attempted in vendorScript.xml");
return 0;
}
bool Script::compare(const Common::String &statement) {
Common::String str = statement;
int lval, rval;
Common::String left, right, op;
int and_pos, or_pos;
bool invert = false,
_and = false;
/**
* Handle parsing of complex comparisons
* For example:
*
* true&&true&&true||false
*
* Since this resolves right-to-left, this would evaluate
* similarly to (true && (true && (true || false))), returning
* true.
*/
and_pos = str.findFirstOf("&&");
or_pos = str.findFirstOf("||");
if ((and_pos > 0) || (or_pos > 0)) {
bool retfirst, retsecond;
int pos;
if ((or_pos < 0) || ((and_pos > 0) && (and_pos < or_pos)))
_and = true;
if (_and)
pos = and_pos;
else
pos = or_pos;
retsecond = compare(str.substr(pos + 2));
str = str.substr(0, pos);
retfirst = compare(str);
if (_and)
return (retfirst && retsecond);
else
return (retfirst || retsecond);
}
if (str[0] == '!') {
str = str.substr(1);
invert = true;
}
if (str == "true")
return !invert;
else if (str == "false")
return invert;
else if (mathParse(str, &lval, &rval, &op))
return (bool)math(lval, rval, op) ? !invert : invert;
else {
parseOperation(str, &left, &right, &op);
/* can only really do equality comparison */
if ((op[0] == '=') && (left == right))
return !invert;
}
return invert;
}
void Script::funcParse(const Common::String &str, Common::String *funcName, Common::String *contents) {
uint pos;
*funcName = str;
pos = funcName->findFirstOf("(");
if (pos < funcName->size()) {
*funcName = funcName->substr(0, pos);
*contents = str.substr(pos + 1);
pos = contents->findFirstOf(")");
if (pos >= contents->size())
warning("Error: No closing ) in function %s()", funcName->c_str());
else
*contents = contents->substr(0, pos);
} else {
funcName->clear();
}
}
} // End of namespace Ultima4
} // End of namespace Ultima