/* 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