/** * Tibia GIMUD Server - a free and open-source MMORPG server emulator * Copyright (C) 2019 Sabrehaven and Mark Samman * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "otpch.h" #include "behaviourdatabase.h" #include "npc.h" #include "player.h" #include "game.h" #include "spells.h" #include "monster.h" #include "scheduler.h" extern Game g_game; extern Monsters g_monsters; extern Spells* g_spells; BehaviourDatabase::BehaviourDatabase(Npc * _npc) : npc(_npc) { topic = 0; data = -1; type = 0; price = 0; amount = 0; delay = 1000; } BehaviourDatabase::~BehaviourDatabase() { for (NpcBehaviour* behaviour : behaviourEntries) { delete behaviour; } } bool BehaviourDatabase::loadDatabase(ScriptReader& script) { script.readSymbol('{'); script.nextToken(); while (true) { if (script.Token == ENDOFFILE) { break; } if (script.Token == SPECIAL && script.getSpecial() == '}') { break; } if (!loadBehaviour(script)) { return false; } } return true; } bool BehaviourDatabase::loadBehaviour(ScriptReader& script) { NpcBehaviour* behaviour = new NpcBehaviour(); if (!loadConditions(script, behaviour)) { return false; } if (script.Token != SPECIAL || script.getSpecial() != 'I') { script.error("'->' expected"); delete behaviour; return false; } script.nextToken(); if (!loadActions(script, behaviour)) { delete behaviour; return false; } // set this behaviour priority to condition size behaviour->priority += behaviour->conditions.size(); if (priorityBehaviour) { priorityBehaviour->priority += behaviour->priority + 1; priorityBehaviour = nullptr; } // order it correctly auto it = std::lower_bound(behaviourEntries.begin(), behaviourEntries.end(), behaviour, compareBehaviour); behaviourEntries.insert(it, behaviour); // set previous behaviour (*) functionality previousBehaviour = behaviour; return true; } bool BehaviourDatabase::loadConditions(ScriptReader& script, NpcBehaviour* behaviour) { while (true) { std::unique_ptr condition(new NpcBehaviourCondition); bool searchTerm = false; if (script.Token == IDENTIFIER) { std::string identifier = script.getIdentifier(); if (identifier == "address") { condition->situation = SITUATION_ADDRESS; behaviour->situation = SITUATION_ADDRESS; searchTerm = true; } else if (identifier == "busy") { condition->situation = SITUATION_BUSY; behaviour->situation = SITUATION_BUSY; searchTerm = true; } else if (identifier == "vanish") { condition->situation = SITUATION_VANISH; behaviour->situation = SITUATION_VANISH; searchTerm = true; } else if (identifier == "sorcerer") { condition->type = BEHAVIOUR_TYPE_SORCERER; searchTerm = true; } else if (identifier == "knight") { condition->type = BEHAVIOUR_TYPE_KNIGHT; searchTerm = true; } else if (identifier == "paladin") { condition->type = BEHAVIOUR_TYPE_PALADIN; searchTerm = true; } else if (identifier == "druid") { condition->type = BEHAVIOUR_TYPE_DRUID; searchTerm = true; } else if (identifier == "premium") { condition->type = BEHAVIOUR_TYPE_ISPREMIUM; searchTerm = true; } else if (identifier == "pvpenforced") { condition->type = BEHAVIOUR_TYPE_PVPENFORCED; searchTerm = true; } else if (identifier == "female") { condition->type = BEHAVIOUR_TYPE_FEMALE; searchTerm = true; } else if (identifier == "male") { condition->type = BEHAVIOUR_TYPE_MALE; searchTerm = true; } else if (identifier == "pzblock") { condition->type = BEHAVIOUR_TYPE_PZLOCKED; searchTerm = true; } else if (identifier == "promoted") { condition->type = BEHAVIOUR_TYPE_PROMOTED; searchTerm = true; } } else if (script.Token == STRING) { const std::string keyString = asLowerCaseString(script.getString()); condition->setCondition(BEHAVIOUR_TYPE_STRING, 0, keyString); behaviour->priority += keyString.length(); searchTerm = true; } else if (script.Token == SPECIAL) { if (script.getSpecial() == '!') { condition->setCondition(BEHAVIOUR_TYPE_NOP, 0, ""); searchTerm = true; // set this one for behaviour priorityBehaviour = behaviour; } else if (script.getSpecial() == '%') { condition->setCondition(BEHAVIOUR_TYPE_MESSAGE_COUNT, script.readNumber(), ""); searchTerm = true; } else if (script.getSpecial() == ',') { script.nextToken(); continue; } else { break; } } // relational operation search if (!searchTerm) { condition->type = BEHAVIOUR_TYPE_OPERATION; NpcBehaviourNode* headNode = readValue(script); NpcBehaviourNode* nextNode = readFactor(script, headNode); // relational operators if (script.Token != SPECIAL) { script.error("relational operator expected"); delete nextNode; return false; } NpcBehaviourOperator_t operatorType; switch (script.getSpecial()) { case '<': operatorType = BEHAVIOUR_OPERATOR_LESSER_THAN; break; case '=': operatorType = BEHAVIOUR_OPERATOR_EQUALS; break; case '>': operatorType = BEHAVIOUR_OPERATOR_GREATER_THAN; break; case 'G': operatorType = BEHAVIOUR_OPERATOR_GREATER_OR_EQUALS; break; case 'N': operatorType = BEHAVIOUR_OPERATOR_NOT_EQUALS; break; case 'L': operatorType = BEHAVIOUR_OPERATOR_LESSER_OR_EQUALS; break; default: script.error("relational operator expected"); delete nextNode; return false; } script.nextToken(); headNode = new NpcBehaviourNode(); headNode->type = BEHAVIOUR_TYPE_OPERATION; headNode->number = operatorType; headNode->left = nextNode; nextNode = readValue(script); nextNode = readFactor(script, nextNode); headNode->right = nextNode; condition->expression = headNode; } else { script.nextToken(); } behaviour->conditions.push_back(condition.release()); } return true; } bool BehaviourDatabase::loadActions(ScriptReader& script, NpcBehaviour* behaviour) { while (true) { std::unique_ptr action(new NpcBehaviourAction); NpcBehaviourParameterSearch_t searchType = BEHAVIOUR_PARAMETER_NONE; if (script.Token == STRING) { action->type = BEHAVIOUR_TYPE_STRING; action->string = script.getString(); } else if (script.Token == IDENTIFIER) { std::string identifier = script.getIdentifier(); if (identifier == "idle") { action->type = BEHAVIOUR_TYPE_IDLE; } else if (identifier == "nop") { action->type = BEHAVIOUR_TYPE_NOP; } else if (identifier == "queue") { action->type = BEHAVIOUR_TYPE_QUEUE; } else if (identifier == "createmoney") { action->type = BEHAVIOUR_TYPE_CREATEMONEY; } else if (identifier == "deletemoney") { action->type = BEHAVIOUR_TYPE_DELETEMONEY; } else if (identifier == "promote") { action->type = BEHAVIOUR_TYPE_PROMOTE; } else if (identifier == "topic") { action->type = BEHAVIOUR_TYPE_TOPIC; searchType = BEHAVIOUR_PARAMETER_ASSIGN; } else if (identifier == "price") { action->type = BEHAVIOUR_TYPE_PRICE; searchType = BEHAVIOUR_PARAMETER_ASSIGN; } else if (identifier == "amount") { action->type = BEHAVIOUR_TYPE_AMOUNT; searchType = BEHAVIOUR_PARAMETER_ASSIGN; } else if (identifier == "data") { action->type = BEHAVIOUR_TYPE_DATA; searchType = BEHAVIOUR_PARAMETER_ASSIGN; } else if (identifier == "type") { action->type = BEHAVIOUR_TYPE_ITEM; searchType = BEHAVIOUR_PARAMETER_ASSIGN; } else if (identifier == "string") { action->type = BEHAVIOUR_TYPE_TEXT; searchType = BEHAVIOUR_PARAMETER_ASSIGN; } else if (identifier == "hp") { action->type = BEHAVIOUR_TYPE_HEALTH; searchType = BEHAVIOUR_PARAMETER_ASSIGN; } else if (identifier == "withdraw") { action->type = BEHAVIOUR_TYPE_WITHDRAW; searchType = BEHAVIOUR_PARAMETER_ONE; } else if (identifier == "deposit") { action->type = BEHAVIOUR_TYPE_DEPOSIT; searchType = BEHAVIOUR_PARAMETER_ONE; } else if (identifier == "bless") { action->type = BEHAVIOUR_TYPE_BLESS; searchType = BEHAVIOUR_PARAMETER_ONE; } else if (identifier == "effectme") { action->type = BEHAVIOUR_TYPE_EFFECTME; searchType = BEHAVIOUR_PARAMETER_ONE; } else if (identifier == "effectopp") { action->type = BEHAVIOUR_TYPE_EFFECTOPP; searchType = BEHAVIOUR_PARAMETER_ONE; } else if (identifier == "create") { action->type = BEHAVIOUR_TYPE_CREATE; searchType = BEHAVIOUR_PARAMETER_ONE; } else if (identifier == "delete") { action->type = BEHAVIOUR_TYPE_DELETE; searchType = BEHAVIOUR_PARAMETER_ONE; } else if (identifier == "teachspell") { action->type = BEHAVIOUR_TYPE_TEACHSPELL; searchType = BEHAVIOUR_PARAMETER_ONE; } else if (identifier == "town") { action->type = BEHAVIOUR_TYPE_TOWN; searchType = BEHAVIOUR_PARAMETER_ONE; } else if (identifier == "profession") { action->type = BEHAVIOUR_TYPE_PROFESSION; searchType = BEHAVIOUR_PARAMETER_ONE; } else if (identifier == "experience") { action->type = BEHAVIOUR_TYPE_EXPERIENCE; searchType = BEHAVIOUR_PARAMETER_ONE; } else if (identifier == "summon") { action->type = BEHAVIOUR_TYPE_SUMMON; searchType = BEHAVIOUR_PARAMETER_ONE; } else if (identifier == "burning") { action->type = BEHAVIOUR_TYPE_BURNING; searchType = BEHAVIOUR_PARAMETER_TWO; } else if (identifier == "setquestvalue") { action->type = BEHAVIOUR_TYPE_QUESTVALUE; searchType = BEHAVIOUR_PARAMETER_TWO; } else if (identifier == "poison") { action->type = BEHAVIOUR_TYPE_POISON; searchType = BEHAVIOUR_PARAMETER_TWO; } else if (identifier == "teleport") { action->type = BEHAVIOUR_TYPE_TELEPORT; searchType = BEHAVIOUR_PARAMETER_THREE; } else if (identifier == "createcontainer") { action->type = BEHAVIOUR_TYPE_CREATECONTAINER; searchType = BEHAVIOUR_PARAMETER_THREE; } else { script.error("illegal action term"); return false; } } else if (script.Token == SPECIAL) { if (script.getSpecial() == '*') { if (previousBehaviour == nullptr) { script.error("no previous pattern"); return false; } for (NpcBehaviourAction* actionCopy : previousBehaviour->actions) { behaviour->actions.push_back(actionCopy->clone()); } script.nextToken(); return true; } } if (searchType == BEHAVIOUR_PARAMETER_ASSIGN) { script.readSymbol('='); script.nextToken(); NpcBehaviourNode* headNode = readValue(script); NpcBehaviourNode* nextNode = readFactor(script, headNode); action->expression = nextNode; } else if (searchType == BEHAVIOUR_PARAMETER_ONE) { script.readSymbol('('); script.nextToken(); NpcBehaviourNode* headNode = readValue(script); NpcBehaviourNode* nextNode = readFactor(script, headNode); action->expression = nextNode; if (script.Token != SPECIAL || script.getSpecial() != ')') { script.error("')' expected"); return false; } script.nextToken(); } else if (searchType == BEHAVIOUR_PARAMETER_TWO) { script.readSymbol('('); script.nextToken(); NpcBehaviourNode* headNode = readValue(script); NpcBehaviourNode* nextNode = readFactor(script, headNode); action->expression = nextNode; if (script.Token != SPECIAL || script.getSpecial() != ',') { script.error("',' expected"); return false; } script.nextToken(); headNode = readValue(script); nextNode = readFactor(script, headNode); action->expression2 = nextNode; if (script.Token != SPECIAL || script.getSpecial() != ')') { script.error("')' expected"); return false; } script.nextToken(); } else if (searchType == BEHAVIOUR_PARAMETER_THREE) { script.readSymbol('('); script.nextToken(); NpcBehaviourNode* headNode = readValue(script); NpcBehaviourNode* nextNode = readFactor(script, headNode); action->expression = nextNode; if (script.Token != SPECIAL || script.getSpecial() != ',') { script.error("',' expected"); return false; } script.nextToken(); headNode = readValue(script); nextNode = readFactor(script, headNode); action->expression2 = nextNode; if (script.Token != SPECIAL || script.getSpecial() != ',') { script.error("',' expected"); return false; } script.nextToken(); headNode = readValue(script); nextNode = readFactor(script, headNode); action->expression3 = nextNode; if (script.Token != SPECIAL || script.getSpecial() != ')') { script.error("')' expected"); return false; } script.nextToken(); } else { script.nextToken(); } behaviour->actions.push_back(action.release()); if (script.Token == SPECIAL) { if (script.getSpecial() == ',') { script.nextToken(); continue; } } break; } return true; } NpcBehaviourNode* BehaviourDatabase::readValue(ScriptReader& script) { if (script.Token == NUMBER) { NpcBehaviourNode* node = new NpcBehaviourNode(); node->type = BEHAVIOUR_TYPE_NUMBER; node->number = script.getNumber(); script.nextToken(); return node; } if (script.Token == STRING) { NpcBehaviourNode* node = new NpcBehaviourNode(); node->type = BEHAVIOUR_TYPE_STRING; node->string = asLowerCaseString(script.getString()); script.nextToken(); return node; } if (script.Token == SPECIAL) { if (script.getSpecial() != '%') { script.error("illegal character"); return nullptr; } NpcBehaviourNode* node = new NpcBehaviourNode(); node->type = BEHAVIOUR_TYPE_MESSAGE_COUNT; node->number = script.readNumber(); script.nextToken(); return node; } NpcBehaviourNode* node = nullptr; NpcBehaviourParameterSearch_t searchType = BEHAVIOUR_PARAMETER_NONE; std::string identifier = script.getIdentifier(); if (identifier == "topic") { node = new NpcBehaviourNode(); node->type = BEHAVIOUR_TYPE_TOPIC; } else if (identifier == "price") { node = new NpcBehaviourNode(); node->type = BEHAVIOUR_TYPE_PRICE; } else if (identifier == "type") { node = new NpcBehaviourNode(); node->type = BEHAVIOUR_TYPE_ITEM; } else if (identifier == "string") { node = new NpcBehaviourNode(); node->type = BEHAVIOUR_TYPE_TEXT; } else if (identifier == "data") { node = new NpcBehaviourNode(); node->type = BEHAVIOUR_TYPE_DATA; } else if (identifier == "amount") { node = new NpcBehaviourNode(); node->type = BEHAVIOUR_TYPE_AMOUNT; } else if (identifier == "countmoney") { node = new NpcBehaviourNode(); node->type = BEHAVIOUR_TYPE_COUNTMONEY; } else if (identifier == "hp") { node = new NpcBehaviourNode(); node->type = BEHAVIOUR_TYPE_HEALTH; } else if (identifier == "burning") { node = new NpcBehaviourNode(); node->type = BEHAVIOUR_TYPE_BURNING; } else if (identifier == "level") { node = new NpcBehaviourNode(); node->type = BEHAVIOUR_TYPE_LEVEL; } else if (identifier == "poison") { node = new NpcBehaviourNode(); node->type = BEHAVIOUR_TYPE_POISON; } else if (identifier == "balance") { node = new NpcBehaviourNode(); node->type = BEHAVIOUR_TYPE_BALANCE; } else if (identifier == "spellknown") { node = new NpcBehaviourNode(); node->type = BEHAVIOUR_TYPE_SPELLKNOWN; searchType = BEHAVIOUR_PARAMETER_ONE; } else if (identifier == "spelllevel") { node = new NpcBehaviourNode(); node->type = BEHAVIOUR_TYPE_SPELLLEVEL; searchType = BEHAVIOUR_PARAMETER_ONE; } else if (identifier == "questvalue") { node = new NpcBehaviourNode(); node->type = BEHAVIOUR_TYPE_QUESTVALUE; searchType = BEHAVIOUR_PARAMETER_ONE; } else if (identifier == "count") { node = new NpcBehaviourNode(); node->type = BEHAVIOUR_TYPE_COUNT; searchType = BEHAVIOUR_PARAMETER_ONE; } else if (identifier == "random") { node = new NpcBehaviourNode(); node->type = BEHAVIOUR_TYPE_RANDOM; searchType = BEHAVIOUR_PARAMETER_TWO; } if (searchType == BEHAVIOUR_PARAMETER_ONE) { script.readSymbol('('); script.nextToken(); NpcBehaviourNode* nextNode = readValue(script); nextNode = readFactor(script, nextNode); node->left = nextNode; if (script.Token != SPECIAL || script.getSpecial() != ')') { script.error("')' expected"); } } else if (searchType == BEHAVIOUR_PARAMETER_TWO) { script.readSymbol('('); script.nextToken(); NpcBehaviourNode* nextNode = readValue(script); nextNode = readFactor(script, nextNode); node->left = nextNode; if (script.Token != SPECIAL || script.getSpecial() != ',') { script.error("',' expected"); } script.nextToken(); nextNode = readValue(script); nextNode = readFactor(script, nextNode); node->right = nextNode; if (script.Token != SPECIAL || script.getSpecial() != ')') { script.error("')' expected"); } } if (!node) { script.error("unknown value"); } script.nextToken(); return node; } NpcBehaviourNode* BehaviourDatabase::readFactor(ScriptReader& script, NpcBehaviourNode* nextNode) { // * operator while (true) { if (script.Token != SPECIAL) { break; } if (script.getSpecial() != '*') { break; } NpcBehaviourNode* headNode = new NpcBehaviourNode(); headNode->type = BEHAVIOUR_TYPE_OPERATION; headNode->number = BEHAVIOUR_OPERATOR_MULTIPLY; headNode->left = nextNode; script.nextToken(); nextNode = readValue(script); headNode->right = nextNode; nextNode = headNode; } // + - operators while (true) { if (script.Token != SPECIAL) { break; } if (script.getSpecial() != '+' && script.getSpecial() != '-') { break; } NpcBehaviourNode* headNode = new NpcBehaviourNode(); headNode->type = BEHAVIOUR_TYPE_OPERATION; headNode->number = BEHAVIOUR_OPERATOR_SUM; if (script.getSpecial() == '-') { headNode->number = BEHAVIOUR_OPERATOR_RES; } headNode->left = nextNode; script.nextToken(); nextNode = readValue(script); headNode->right = nextNode; nextNode = headNode; } return nextNode; } void BehaviourDatabase::react(BehaviourSituation_t situation, Player* player, const std::string& message) { for (NpcBehaviour* behaviour : behaviourEntries) { bool fulfilled = true; if (situation == SITUATION_ADDRESS && behaviour->situation != SITUATION_ADDRESS) { continue; } if (situation == SITUATION_BUSY && behaviour->situation != SITUATION_BUSY) { continue; } if (situation == SITUATION_VANISH && behaviour->situation != SITUATION_VANISH) { continue; } if (situation == SITUATION_NONE && behaviour->situation != SITUATION_NONE) { continue; } for (const NpcBehaviourCondition* condition : behaviour->conditions) { if (!checkCondition(condition, player, message)) { fulfilled = false; break; } } if (!fulfilled) { continue; } if (player->getID() == npc->focusCreature) { topic = 0; } reset(); if (situation == SITUATION_ADDRESS || npc->focusCreature == player->getID()) { attendCustomer(player->getID()); } if (situation == SITUATION_VANISH) { npc->conversationEndTime = 0; idle(); } for (const NpcBehaviourAction* action : behaviour->actions) { checkAction(action, player, message); } break; } } bool BehaviourDatabase::checkCondition(const NpcBehaviourCondition* condition, Player* player, const std::string& message) { switch (condition->type) { case BEHAVIOUR_TYPE_NOP: break; case BEHAVIOUR_TYPE_MESSAGE_COUNT: { int32_t value = searchDigit(message); if (value < condition->number) { return false; } break; } case BEHAVIOUR_TYPE_STRING: if (!searchWord(condition->string, message)) { return false; } break; case BEHAVIOUR_TYPE_SORCERER: if (player->getVocationId() != 1 && player->getVocationId() != 5) { return false; } break; case BEHAVIOUR_TYPE_DRUID: if (player->getVocationId() != 2 && player->getVocationId() != 6) { return false; } break; case BEHAVIOUR_TYPE_PALADIN: if (player->getVocationId() != 3 && player->getVocationId() != 7) { return false; } break; case BEHAVIOUR_TYPE_KNIGHT: if (player->getVocationId() != 4 && player->getVocationId() != 8) { return false; } break; case BEHAVIOUR_TYPE_ISPREMIUM: if (!player->isPremium()) { return false; } break; case BEHAVIOUR_TYPE_PVPENFORCED: if (g_game.getWorldType() != WORLD_TYPE_PVP_ENFORCED) { return false; } break; case BEHAVIOUR_TYPE_FEMALE: if (player->getSex() != PLAYERSEX_FEMALE) { return false; } break; case BEHAVIOUR_TYPE_MALE: if (player->getSex() != PLAYERSEX_MALE) { return false; } break; case BEHAVIOUR_TYPE_PZLOCKED: if (!player->isPzLocked()) { return false; } break; case BEHAVIOUR_TYPE_PROMOTED: { int32_t value = 0; player->getStorageValue(30018, value); if (value != 1) { return false; } break; } case BEHAVIOUR_TYPE_OPERATION: return checkOperation(player, condition->expression, message) > 0; case BEHAVIOUR_TYPE_SPELLKNOWN: { if (!player->hasLearnedInstantSpell(string)) { return false; } break; } default: std::cout << "[Warning - BehaviourDatabase::react]: Unhandled node type " << condition->type << std::endl; return false; } return true; } void BehaviourDatabase::checkAction(const NpcBehaviourAction* action, Player* player, const std::string& message) { switch (action->type) { case BEHAVIOUR_TYPE_NOP: break; case BEHAVIOUR_TYPE_STRING: { delayedEvents.push_back(g_scheduler.addEvent(createSchedulerTask(delay, std::bind(&Npc::doSay, npc, parseResponse(player, action->string))))); delay += 100 * (message.length() / 5) + 10000; break; } case BEHAVIOUR_TYPE_IDLE: idle(); break; case BEHAVIOUR_TYPE_QUEUE: queueCustomer(player->getID(), message); break; case BEHAVIOUR_TYPE_TOPIC: topic = evaluate(action->expression, player, message); break; case BEHAVIOUR_TYPE_PRICE: price = evaluate(action->expression, player, message); break; case BEHAVIOUR_TYPE_DATA: data = evaluate(action->expression, player, message); break; case BEHAVIOUR_TYPE_ITEM: type = evaluate(action->expression, player, message); break; case BEHAVIOUR_TYPE_AMOUNT: amount = evaluate(action->expression, player, message); break; case BEHAVIOUR_TYPE_TEXT: string = action->expression->string; break; case BEHAVIOUR_TYPE_HEALTH: { int32_t newHealth = evaluate(action->expression, player, message); player->changeHealth(-player->getHealth() + newHealth); break; } case BEHAVIOUR_TYPE_CREATEMONEY: g_game.addMoney(player, price); break; case BEHAVIOUR_TYPE_DELETEMONEY: g_game.removeMoney(player, price); break; case BEHAVIOUR_TYPE_CREATE: { int32_t itemId = evaluate(action->expression, player, message); const ItemType& it = Item::items[itemId]; if (it.stackable) { do { int32_t count = std::min(100, amount); amount -= count; Item* item = Item::CreateItem(itemId, count); if (!item) { break; } ReturnValue ret = g_game.internalPlayerAddItem(player, item); if (ret != RETURNVALUE_NOERROR) { delete item; break; } } while (amount); } else { for (int32_t i = 0; i < std::max(1, amount); i++) { Item* item = Item::CreateItem(itemId, data); if (!item) { break; } ReturnValue ret = g_game.internalPlayerAddItem(player, item); if (ret != RETURNVALUE_NOERROR) { delete item; break; } } } break; } case BEHAVIOUR_TYPE_DELETE: { type = evaluate(action->expression, player, message); const ItemType& itemType = Item::items[type]; if (itemType.stackable || !itemType.hasSubType()) { data = -1; } if (!player->removeItemOfType(type, amount, data, true)) { player->removeItemOfType(type, amount, data, false); } break; } case BEHAVIOUR_TYPE_EFFECTME: g_game.addMagicEffect(npc->getPosition(), evaluate(action->expression, player, message)); break; case BEHAVIOUR_TYPE_EFFECTOPP: g_game.addMagicEffect(player->getPosition(), evaluate(action->expression, player, message)); break; case BEHAVIOUR_TYPE_BURNING: { const int32_t cycles = evaluate(action->expression, player, message); const int32_t count = evaluate(action->expression2, player, message); if (cycles == 0) { player->removeCondition(CONDITION_FIRE, true); break; } ConditionDamage* conditionDamage = new ConditionDamage(CONDITIONID_COMBAT, CONDITION_FIRE); conditionDamage->setParam(CONDITION_PARAM_CYCLE, cycles); conditionDamage->setParam(CONDITION_PARAM_COUNT, count); conditionDamage->setParam(CONDITION_PARAM_MAX_COUNT, count); player->addCondition(conditionDamage); break; } case BEHAVIOUR_TYPE_POISON: { const int32_t cycles = evaluate(action->expression, player, message); const int32_t count = evaluate(action->expression2, player, message); if (cycles == 0) { player->removeCondition(CONDITION_POISON, true); break; } ConditionDamage* conditionDamage = new ConditionDamage(CONDITIONID_COMBAT, CONDITION_POISON); conditionDamage->setParam(CONDITION_PARAM_CYCLE, cycles); conditionDamage->setParam(CONDITION_PARAM_COUNT, count); conditionDamage->setParam(CONDITION_PARAM_MAX_COUNT, count); player->addCondition(conditionDamage); break; } case BEHAVIOUR_TYPE_TOWN: player->setTown(g_game.map.towns.getTown(evaluate(action->expression, player, message))); break; case BEHAVIOUR_TYPE_TEACHSPELL: player->learnInstantSpell(string); break; case BEHAVIOUR_TYPE_QUESTVALUE: { int32_t questNumber = evaluate(action->expression, player, message); int32_t questValue = evaluate(action->expression2, player, message); player->addStorageValue(questNumber, questValue); break; } case BEHAVIOUR_TYPE_TELEPORT: { Position pos; pos.x = evaluate(action->expression, player, message); pos.y = evaluate(action->expression2, player, message); pos.z = evaluate(action->expression3, player, message); g_game.internalTeleport(player, pos); break; } case BEHAVIOUR_TYPE_PROFESSION: { int32_t newVocation = evaluate(action->expression, player, message); player->setVocation(newVocation); break; } case BEHAVIOUR_TYPE_PROMOTE: { int32_t newVocation = player->getVocationId() + 4; player->setVocation(newVocation); player->addStorageValue(30018, 1); break; } case BEHAVIOUR_TYPE_SUMMON: { std::string name = action->expression->string; Monster* monster = Monster::createMonster(name); if (!monster) { break; } if (!g_game.placeCreature(monster, npc->getPosition(), true, true)) { delete monster; } break; } case BEHAVIOUR_TYPE_EXPERIENCE: { int32_t experience = evaluate(action->expression, player, message); player->addExperience(nullptr, experience, false); break; } case BEHAVIOUR_TYPE_WITHDRAW: { int32_t money = evaluate(action->expression, player, message); player->setBankBalance(player->getBankBalance() - money); break; } case BEHAVIOUR_TYPE_DEPOSIT: { int32_t money = evaluate(action->expression, player, message); player->setBankBalance(player->getBankBalance() + money); break; } case BEHAVIOUR_TYPE_BLESS: { uint8_t number = static_cast(evaluate(action->expression, player, message)) - 1; if (!player->hasBlessing(number)) { player->addBlessing(1 << number); } break; } case BEHAVIOUR_TYPE_CREATECONTAINER: { int32_t containerId = evaluate(action->expression, player, message); int32_t itemId = evaluate(action->expression2, player, message); int32_t data = evaluate(action->expression3, player, message); for (int32_t i = 0; i < std::max(1, amount); i++) { Item* container = Item::CreateItem(containerId); if (!container) { std::cout << "[Error - BehaviourDatabase::checkAction]: CreateContainer - failed to create container item" << std::endl; break; } Container* realContainer = container->getContainer(); for (int32_t c = 0; c < std::max(1, realContainer->capacity()); c++) { Item* item = Item::CreateItem(itemId, data); if (!item) { std::cout << "[Error - BehaviourDatabase::checkAction]: CreateContainer - failed to create item" << std::endl; break; } realContainer->internalAddThing(item); } ReturnValue ret = g_game.internalPlayerAddItem(player, container); if (ret != RETURNVALUE_NOERROR) { delete container; break; } } break; } default: std::cout << "[Warning - BehaviourDatabase::checkAction]: Unhandled node type " << action->type << std::endl; break; } } int32_t BehaviourDatabase::evaluate(NpcBehaviourNode* node, Player* player, const std::string& message) { switch (node->type) { case BEHAVIOUR_TYPE_NUMBER: return node->number; case BEHAVIOUR_TYPE_TOPIC: return topic; case BEHAVIOUR_TYPE_PRICE: return price; case BEHAVIOUR_TYPE_DATA: return data; case BEHAVIOUR_TYPE_ITEM: return type; case BEHAVIOUR_TYPE_AMOUNT: return amount; case BEHAVIOUR_TYPE_HEALTH: return player->getHealth(); case BEHAVIOUR_TYPE_COUNT: { int32_t itemId = evaluate(node->left, player, message); const ItemType& itemType = Item::items[itemId]; if (itemType.stackable || !itemType.hasSubType()) { data = -1; } return player->getItemTypeCount(itemId, data); } case BEHAVIOUR_TYPE_COUNTMONEY: return player->getMoney(); case BEHAVIOUR_TYPE_BURNING: { Condition* condition = player->getCondition(CONDITION_FIRE); if (!condition) { return false; } ConditionDamage* damage = static_cast(condition); if (damage == nullptr) { return false; } return damage->getTotalDamage(); } case BEHAVIOUR_TYPE_POISON: { Condition* condition = player->getCondition(CONDITION_POISON); if (!condition) { return false; } ConditionDamage* damage = static_cast(condition); if (damage == nullptr) { return false; } return damage->getTotalDamage(); } case BEHAVIOUR_TYPE_LEVEL: return player->getLevel(); case BEHAVIOUR_TYPE_RANDOM: { int32_t min = evaluate(node->left, player, message); int32_t max = evaluate(node->right, player, message); return normal_random(min, max); } case BEHAVIOUR_TYPE_QUESTVALUE: { int32_t questNumber = evaluate(node->left, player, message); int32_t questValue; player->getStorageValue(questNumber, questValue); return questValue; } case BEHAVIOUR_TYPE_MESSAGE_COUNT: { int32_t value = searchDigit(message); if (value < node->number) { return false; } return value; } case BEHAVIOUR_TYPE_OPERATION: return checkOperation(player, node, message); case BEHAVIOUR_TYPE_BALANCE: return player->getBankBalance(); case BEHAVIOUR_TYPE_SPELLKNOWN: { if (player->hasLearnedInstantSpell(string)) { return true; } break; } case BEHAVIOUR_TYPE_SPELLLEVEL: { InstantSpell* spell = g_spells->getInstantSpellByName(string); if (!spell) { std::cout << "[Warning - BehaviourDatabase::evaluate]: SpellLevel unknown spell " << node->string << std::endl; return std::numeric_limits::max(); } return spell->getLevel(); } default: std::cout << "[Warning - BehaviourDatabase::evaluate]: Unhandled node type " << node->type << std::endl; break; } return false; } int32_t BehaviourDatabase::checkOperation(Player* player, NpcBehaviourNode* node, const std::string& message) { int32_t leftResult = evaluate(node->left, player, message); int32_t rightResult = evaluate(node->right, player, message); switch (node->number) { case BEHAVIOUR_OPERATOR_LESSER_THAN: return leftResult < rightResult; case BEHAVIOUR_OPERATOR_EQUALS: return leftResult == rightResult; case BEHAVIOUR_OPERATOR_GREATER_THAN: return leftResult > rightResult; case BEHAVIOUR_OPERATOR_GREATER_OR_EQUALS: return leftResult >= rightResult; case BEHAVIOUR_OPERATOR_LESSER_OR_EQUALS: return leftResult <= rightResult; case BEHAVIOUR_OPERATOR_NOT_EQUALS: return leftResult != rightResult; case BEHAVIOUR_OPERATOR_MULTIPLY: return leftResult * rightResult; case BEHAVIOUR_OPERATOR_SUM: return leftResult + rightResult; case BEHAVIOUR_OPERATOR_RES: return leftResult - rightResult; default: break; } return false; } int32_t BehaviourDatabase::searchDigit(const std::string& message) { int32_t start = -1; int32_t end = -1; int32_t value = 0; int32_t i = -1; for (char c : message) { i++; if (start == -1 && IsDigit(c)) { start = i; } else if (start != -1 && !IsDigit(c)) { end = i; break; } } try { value = std::stoi(message.substr(start, end).c_str()); } catch (std::invalid_argument) { return 0; } catch (std::out_of_range) { return 0; } if (value > 500) { value = 500; } return value; } bool BehaviourDatabase::searchWord(const std::string& pattern, const std::string& message) { if (pattern.empty() || message.empty()) { return false; } size_t len = pattern.length(); bool wholeWord = false; if (pattern[len - 1] == '$') { len--; wholeWord = true; } std::string newPattern = pattern.substr(0, len); std::string actualMessage = asLowerCaseString(message); if (actualMessage.find(newPattern) == std::string::npos) { return false; } if (wholeWord) { size_t wordPos = actualMessage.find(newPattern); size_t wordEnd = wordPos + newPattern.length() - 1; if (wordEnd + 1 > actualMessage.length()) { return false; } if (wordEnd + 1 == actualMessage.length()) { return true; } if (!isspace(actualMessage[wordEnd + 1])) { return false; } } return true; } std::string BehaviourDatabase::parseResponse(Player* player, const std::string& message) { std::string response = message; replaceString(response, "%A", std::to_string(amount)); replaceString(response, "%D", std::to_string(data)); replaceString(response, "%N", player->getName()); replaceString(response, "%P", std::to_string(price)); int32_t worldTime = g_game.getLightHour(); int32_t hours = std::floor(worldTime / 60); int32_t minutes = worldTime % 60; std::stringstream ss; ss << hours << ":"; if (minutes < 10) { ss << '0' << minutes; } else { ss << minutes; } replaceString(response, "%T", ss.str()); return response; } void BehaviourDatabase::attendCustomer(uint32_t playerId) { std::lock_guard lock(mutex); reset(); npc->conversationStartTime = OTSYS_TIME(); npc->conversationEndTime = OTSYS_TIME() + 60000; npc->focusCreature = playerId; } void BehaviourDatabase::queueCustomer(uint32_t playerId, const std::string& message) { std::lock_guard lock(mutex); for (NpcQueueEntry entry : queueList) { if (entry.playerId == playerId) { return; } } NpcQueueEntry customer; customer.playerId = playerId; customer.text = message; queueList.push_back(customer); } void BehaviourDatabase::idle() { std::lock_guard lock(mutex); if (queueList.empty()) { if (OTSYS_TIME() - npc->conversationStartTime <= 3000) { npc->staticMovementTime = OTSYS_TIME() + 5000; } npc->focusCreature = 0; } else { while (!queueList.empty()) { NpcQueueEntry nextCustomer = queueList.front(); queueList.pop_front(); Player* player = g_game.getPlayerByID(nextCustomer.playerId); if (!player) { continue; } else { if (!Position::areInRange<3, 3>(player->getPosition(), npc->getPosition())) { continue; } react(SITUATION_ADDRESS, player, nextCustomer.text); return; } } npc->focusCreature = 0; } } void BehaviourDatabase::reset() { delay = 1000; for (uint32_t eventId : delayedEvents) { g_scheduler.stopEvent(eventId); } delayedEvents.clear(); } bool NpcBehaviourCondition::setCondition(NpcBehaviourType_t _type, int32_t _number, const std::string & _string) { type = _type; number = _number; string = _string; return false; }