diff options
Diffstat (limited to 'src/crnonpl.cc')
| -rw-r--r-- | src/crnonpl.cc | 2110 |
1 files changed, 2110 insertions, 0 deletions
diff --git a/src/crnonpl.cc b/src/crnonpl.cc new file mode 100644 index 0000000..433718a --- /dev/null +++ b/src/crnonpl.cc @@ -0,0 +1,2110 @@ +#include "cr.hh" +#include "config.hh" +#include "containers.hh" +#include "info.hh" +#include "operate.hh" + +#include "stubs.hh" + +static vector<TNonplayer*> NonplayerList(0, 10000, 1000, NULL); +static int FirstFreeNonplayer; + +static vector<TMonsterhome> Monsterhome(1, 5000, 1000); +static int Monsterhomes; + +static store<TBehaviourNode, 256> BehaviourNodeTable; + +// Behaviour Database +// ============================================================================= +static TBehaviourNode *NewBehaviourNode(int Type, TBehaviourNode *Left, TBehaviourNode *Right){ + TBehaviourNode *Node = BehaviourNodeTable.getFreeItem(); + Node->Type = Type; + Node->Data = 0; + Node->Left = Left; + Node->Right = Right; + return Node; +} + +static TBehaviourNode *NewBehaviourNode(int Type, int Data){ + TBehaviourNode *Node = BehaviourNodeTable.getFreeItem(); + Node->Type = Type; + Node->Data = Data; + Node->Left = NULL; + Node->Right = NULL; + return Node; +} + +static void DeleteBehaviourNode(TBehaviourNode *Node){ + if(Node != NULL){ + DeleteBehaviourNode(Node->Left); + DeleteBehaviourNode(Node->Right); + BehaviourNodeTable.putFreeItem(Node); + } +} + +bool TBehaviourCondition::set(int Type, void *Data){ + if(Type != BEHAVIOUR_CONDITION_SHORTCIRCUIT && Data == NULL){ + error("TBehaviourCondition::set: Data ist NULL.\n"); + this->Type = BEHAVIOUR_CONDITION_NONE; + return false; + } + + this->Type = Type; + switch(Type){ + case BEHAVIOUR_CONDITION_TEXT:{ + this->Text = AddDynamicString((const char*)Data); + break; + } + + case BEHAVIOUR_CONDITION_PROPERTY:{ + const char *Property = (const char*)Data; + this->Property = BEHAVIOUR_PROPERTY_NONE; + if(strcmp(Property, "address") == 0){ + this->Property = BEHAVIOUR_PROPERTY_ADDRESS; + }else if(strcmp(Property, "busy") == 0){ + this->Property = BEHAVIOUR_PROPERTY_BUSY; + }else if(strcmp(Property, "vanish") == 0){ + this->Property = BEHAVIOUR_PROPERTY_VANISH; + }else if(strcmp(Property, "male") == 0){ + this->Property = BEHAVIOUR_PROPERTY_MALE; + }else if(strcmp(Property, "female") == 0){ + this->Property = BEHAVIOUR_PROPERTY_FEMALE; + }else if(strcmp(Property, "knight") == 0){ + this->Property = BEHAVIOUR_PROPERTY_KNIGHT; + }else if(strcmp(Property, "paladin") == 0){ + this->Property = BEHAVIOUR_PROPERTY_PALADIN; + }else if(strcmp(Property, "sorcerer") == 0){ + this->Property = BEHAVIOUR_PROPERTY_SORCERER; + }else if(strcmp(Property, "druid") == 0){ + this->Property = BEHAVIOUR_PROPERTY_DRUID; + }else if(strcmp(Property, "premium") == 0){ + this->Property = BEHAVIOUR_PROPERTY_PREMIUM; + }else if(strcmp(Property, "promoted") == 0){ + this->Property = BEHAVIOUR_PROPERTY_PROMOTED; + }else if(strcmp(Property, "pzblock") == 0){ + this->Property = BEHAVIOUR_PROPERTY_PZBLOCK; + }else if(strcmp(Property, "nonpvp") == 0){ + this->Property = BEHAVIOUR_PROPERTY_NONPVP; + }else if(strcmp(Property, "pvpenforced") == 0){ + this->Property = BEHAVIOUR_PROPERTY_PVPENFORCED; + } + + if(this->Property == BEHAVIOUR_PROPERTY_NONE){ + return false; + } + break; + } + + case BEHAVIOUR_CONDITION_PARAMETER:{ + this->Number = *(int*)Data; + break; + } + + case BEHAVIOUR_CONDITION_EXPRESSION:{ + this->Expression = (TBehaviourNode*)Data; + break; + } + + case BEHAVIOUR_CONDITION_SHORTCIRCUIT:{ + // no-op + break; + } + + default:{ + error("TBehaviourCondition::set: Ungültiger Bedingungstyp %d\n", Type); + return false; + } + } + + return true; +} + +void TBehaviourCondition::clear(void){ + if(this->Type == BEHAVIOUR_CONDITION_TEXT){ + DeleteDynamicString(this->Text); + }else if(this->Type == BEHAVIOUR_CONDITION_EXPRESSION){ + DeleteBehaviourNode(this->Expression); + } + + this->Type = BEHAVIOUR_CONDITION_NONE; +} + +bool TBehaviourAction::set(int Type, void *Data, void *Data2, void *Data3, void *Data4){ + this->Type = Type; + switch(Type){ + case BEHAVIOUR_ACTION_REPLY:{ + this->Text = AddDynamicString((const char*)Data); + break; + } + + case BEHAVIOUR_ACTION_SET_VARIABLE: + case BEHAVIOUR_ACTION_SET_SKILL: + case BEHAVIOUR_ACTION_FUNCTION1:{ + this->Number = *(int*)Data; + this->Expression = (TBehaviourNode*)Data2; + break; + } + + case BEHAVIOUR_ACTION_FUNCTION2: + case BEHAVIOUR_ACTION_SET_SKILL_TIMER:{ + this->Number = *(int*)Data; + this->Expression = (TBehaviourNode*)Data2; + this->Expression2 = (TBehaviourNode*)Data3; + break; + } + + case BEHAVIOUR_ACTION_FUNCTION0: + case BEHAVIOUR_ACTION_CHANGESTATE:{ + this->Number = *(int*)Data; + break; + } + + case BEHAVIOUR_ACTION_REPEAT:{ + // no-op + break; + } + + case BEHAVIOUR_ACTION_FUNCTION3:{ + this->Number = *(int*)Data; + this->Expression = (TBehaviourNode*)Data2; + this->Expression2 = (TBehaviourNode*)Data3; + this->Expression3 = (TBehaviourNode*)Data4; + break; + } + + default:{ + error("TAction::set: Ungültiger Aktionstyp %d\n", Type); + return false; + } + } + + return true; +} + +void TBehaviourAction::clear(void){ + switch(this->Type){ + case BEHAVIOUR_ACTION_REPLY:{ + DeleteDynamicString(this->Text); + break; + } + + case BEHAVIOUR_ACTION_SET_VARIABLE: + case BEHAVIOUR_ACTION_SET_SKILL: + case BEHAVIOUR_ACTION_FUNCTION1:{ + DeleteBehaviourNode(this->Expression); + break; + } + + case BEHAVIOUR_ACTION_FUNCTION2: + case BEHAVIOUR_ACTION_SET_SKILL_TIMER:{ + DeleteBehaviourNode(this->Expression); + DeleteBehaviourNode(this->Expression2); + break; + } + + case BEHAVIOUR_ACTION_FUNCTION3:{ + DeleteBehaviourNode(this->Expression); + DeleteBehaviourNode(this->Expression2); + DeleteBehaviourNode(this->Expression3); + break; + } + + default:{ + break; + } + } + + this->Type = BEHAVIOUR_ACTION_NONE; +} + +TBehaviour::TBehaviour(void) : + Condition(0, 5, 5), + Action(0, 5, 5) +{ + this->Conditions = 0; + this->Actions = 0; +} + +TBehaviour::~TBehaviour(void){ + for(int i = 0; i < this->Conditions; i += 1){ + this->Condition.at(i)->clear(); + } + + for(int i = 0; i < this->Actions; i += 1){ + this->Action.at(i)->clear(); + } +} + +bool TBehaviour::addCondition(int Type, void *Data){ + bool Result = this->Condition.at(this->Conditions)->set(Type, Data); + if(Result){ + this->Conditions += 1; + } + return Result; +} + +bool TBehaviour::addAction(int Type, void *Data, void *Data2, void *Data3, void *Data4){ + bool Result = this->Action.at(this->Actions)->set(Type, Data, Data2, Data3, Data4); + if(Result){ + this->Actions += 1; + } + return Result; +} + +TBehaviour::TBehaviour(const TBehaviour &Other) : + TBehaviour() +{ + this->operator=(Other); +} + +void TBehaviour::operator=(const TBehaviour &Other){ + this->Conditions = Other.Conditions; + for(int i = 0; i < Other.Conditions; i += 1){ + *this->Condition.at(i) = Other.Condition.copyAt(i); + } + + this->Actions = Other.Actions; + for(int i = 0; i < Other.Actions; i += 1){ + *this->Action.at(i) = Other.Action.copyAt(i); + } +} + + +TBehaviourDatabase::TBehaviourDatabase(TReadScriptFile *Script) : + Behaviour(0, 50, 25) +{ + this->Behaviours = 0; + Script->readSymbol('{'); + Script->nextToken(); + while(Script->Token != SPECIAL || Script->getSpecial() != '}'){ + TBehaviour *Behaviour = this->Behaviour.at(this->Behaviours); + + // NOTE(fusion): Optional conditions. + if(Script->Token != SPECIAL || Script->getSpecial() != 'I'){ + while(true){ + bool Ok = false; + if(Script->Token == STRING){ + Ok = Behaviour->addCondition(BEHAVIOUR_CONDITION_TEXT, Script->getString()); + }else if(Script->Token == IDENTIFIER){ + Ok = Behaviour->addCondition(BEHAVIOUR_CONDITION_PROPERTY, Script->getIdentifier()); + }else if(Script->Token == SPECIAL){ + if(Script->getSpecial() == '!'){ + Ok = Behaviour->addCondition(BEHAVIOUR_CONDITION_SHORTCIRCUIT, NULL); + }else if(Script->getSpecial() == '%'){ + int Parameter = Script->readNumber(); + if(Parameter != 1 && Parameter != 2){ + Script->error("illegal ordinal number"); + } + Ok = Behaviour->addCondition(BEHAVIOUR_CONDITION_PARAMETER, &Parameter); + } + } + + if(!Ok){ + TBehaviourNode *Left = this->readTerm(Script); + Script->nextToken(); + if(Script->Token != SPECIAL){ + Script->error("relational operator expected"); + } + + int Operator = BEHAVIOUR_NODE_NONE; + switch(Script->getSpecial()){ + case '<': Operator = BEHAVIOUR_NODE_CMP_LT; break; + case '>': Operator = BEHAVIOUR_NODE_CMP_GT; break; + case '=': Operator = BEHAVIOUR_NODE_CMP_EQ; break; + case 'N': Operator = BEHAVIOUR_NODE_CMP_NEQ; break; + case 'L': Operator = BEHAVIOUR_NODE_CMP_LE; break; + case 'G': Operator = BEHAVIOUR_NODE_CMP_GE; break; + default:{ + Script->error("relational operator expected"); + break; + } + } + + Script->nextToken(); + TBehaviourNode *Right = this->readTerm(Script); + Behaviour->addCondition(BEHAVIOUR_CONDITION_EXPRESSION, + NewBehaviourNode(Operator, Right, Left)); + } + + Script->nextToken(); + if(Script->Token == SPECIAL && Script->getSpecial() == ','){ + Script->nextToken(); + }else{ + break; + } + } + } + + if(Script->Token != SPECIAL || Script->getSpecial() != 'I'){ + Script->error("'->' expected"); + } + + // NOTE(fusion): Required Actions. + Script->nextToken(); + while(true){ + if(Script->Token == STRING){ + Behaviour->addAction(BEHAVIOUR_ACTION_REPLY, Script->getString(), NULL, NULL, NULL); + }else if(Script->Token == IDENTIFIER){ + int Type = -1; + int Data = 0; + const char *Identifier = Script->getIdentifier(); + if(strcmp(Identifier, "topic") == 0){ + Type = BEHAVIOUR_ACTION_SET_VARIABLE; + Data = 1; // BEHAVIOUR_VARIABLE_TOPIC ? + }else if(strcmp(Identifier, "price") == 0){ + Type = BEHAVIOUR_ACTION_SET_VARIABLE; + Data = 2; // BEHAVIOUR_VARIABLE_PRICE ? + }else if(strcmp(Identifier, "amount") == 0){ + Type = BEHAVIOUR_ACTION_SET_VARIABLE; + Data = 3; // BEHAVIOUR_VARIABLE_AMOUNT ? + }else if(strcmp(Identifier, "type") == 0){ + Type = BEHAVIOUR_ACTION_SET_VARIABLE; + Data = 4; // BEHAVIOUR_VARIABLE_TYPE + }else if(strcmp(Identifier, "data") == 0){ + Type = BEHAVIOUR_ACTION_SET_VARIABLE; + Data = 6; // BEHAVIOUR_VARIABLE_DATA + }else if(strcmp(Identifier, "hp") == 0){ + Type = BEHAVIOUR_ACTION_SET_SKILL; + Data = SKILL_HITPOINTS; + }else if(strcmp(Identifier, "poison") == 0){ + Type = BEHAVIOUR_ACTION_SET_SKILL_TIMER; + Data = SKILL_POISON; + }else if(strcmp(Identifier, "burning") == 0){ + Type = BEHAVIOUR_ACTION_SET_SKILL_TIMER; + Data = SKILL_BURNING; + }else if(strcmp(Identifier, "setquestvalue") == 0){ + Type = BEHAVIOUR_ACTION_FUNCTION2; + Data = 3; // BEHAVIOUR_FUNCTION2_SETQUESTVALUE ? + }else if(strcmp(Identifier, "effectme") == 0){ + Type = BEHAVIOUR_ACTION_FUNCTION1; + Data = 1; // BEHAVIOUR_FUNCTION1_EFFECTME ? + }else if(strcmp(Identifier, "effectopp") == 0){ + Type = BEHAVIOUR_ACTION_FUNCTION1; + Data = 2; // BEHAVIOUR_FUNCTION1_EFFECTOPP ? + }else if(strcmp(Identifier, "profession") == 0){ + Type = BEHAVIOUR_ACTION_FUNCTION1; + Data = 3; // BEHAVIOUR_FUNCTION1_SETPROFESSION ? + }else if(strcmp(Identifier, "teachspell") == 0){ + Type = BEHAVIOUR_ACTION_FUNCTION1; + Data = 4; // BEHAVIOUR_FUNCTION1_TEACHSPELL ? + }else if(strcmp(Identifier, "summon") == 0){ + Type = BEHAVIOUR_ACTION_FUNCTION1; + Data = 5; // BEHAVIOUR_FUNCTION1_SUMMON ? + }else if(strcmp(Identifier, "create") == 0){ + Type = BEHAVIOUR_ACTION_FUNCTION1; + Data = 6; // BEHAVIOUR_FUNCTION1_CREATE ? + }else if(strcmp(Identifier, "delete") == 0){ + Type = BEHAVIOUR_ACTION_FUNCTION1; + Data = 7; // BEHAVIOUR_FUNCTION1_DELETE ? + }else if(strcmp(Identifier, "createmoney") == 0){ + Type = BEHAVIOUR_ACTION_FUNCTION0; + Data = 1; // BEHAVIOUR_FUNCTION0_CREATEMONEY ? + }else if(strcmp(Identifier, "deletemoney") == 0){ + Type = BEHAVIOUR_ACTION_FUNCTION0; + Data = 2; // BEHAVIOUR_FUNCTION0_DELETEMONEY ? + }else if(strcmp(Identifier, "queue") == 0){ + Type = BEHAVIOUR_ACTION_FUNCTION0; + Data = 3; // BEHAVIOUR_FUNCTION0_ENQUEUE ? + }else if(strcmp(Identifier, "teleport") == 0){ + Type = BEHAVIOUR_ACTION_FUNCTION3; + Data = 1; // BEHAVIOUR_FUNCTION3_TELEPORT ? + }else if(strcmp(Identifier, "startposition") == 0){ + Type = BEHAVIOUR_ACTION_FUNCTION3; + Data = 2; // BEHAVIOUR_FUNCTION3_SETSTART ? + }else if(strcmp(Identifier, "idle") == 0){ + Type = BEHAVIOUR_ACTION_CHANGESTATE; + Data = IDLE; + }else if(strcmp(Identifier, "nop") == 0){ + Type = BEHAVIOUR_ACTION_NONE; + Data = 0; + } + + switch(Type){ + case BEHAVIOUR_ACTION_NONE:{ + // no-op + break; + } + + case BEHAVIOUR_ACTION_SET_VARIABLE: + case BEHAVIOUR_ACTION_SET_SKILL:{ + Script->readSymbol('='); + Script->nextToken(); + TBehaviourNode *Value = this->readTerm(Script); + + Behaviour->addAction(Type, &Data, Value, NULL, NULL); + break; + } + + case BEHAVIOUR_ACTION_FUNCTION1:{ + Script->readSymbol('('); + Script->nextToken(); + TBehaviourNode *Param = this->readTerm(Script); + if(Script->Token != SPECIAL || Script->getSpecial() != ')'){ + Script->error(") expected"); + } + + Behaviour->addAction(Type, &Data, Param, NULL, NULL); + break; + } + + case BEHAVIOUR_ACTION_FUNCTION2: + case BEHAVIOUR_ACTION_SET_SKILL_TIMER:{ + Script->readSymbol('('); + + Script->nextToken(); + TBehaviourNode *Param1 = this->readTerm(Script); + if(Script->Token != SPECIAL || Script->getSpecial() != ','){ + Script->error(", expected"); + } + + Script->nextToken(); + TBehaviourNode *Param2 = this->readTerm(Script); + if(Script->Token != SPECIAL || Script->getSpecial() != ')'){ + Script->error(") expected"); + } + + Behaviour->addAction(Type, &Data, Param1, Param2, NULL); + break; + } + + case BEHAVIOUR_ACTION_FUNCTION0: + case BEHAVIOUR_ACTION_CHANGESTATE:{ + Behaviour->addAction(Type, &Data, NULL, NULL, NULL); + break; + } + + case BEHAVIOUR_ACTION_FUNCTION3:{ + Script->readSymbol('('); + + Script->nextToken(); + TBehaviourNode *Param1 = this->readTerm(Script); + if(Script->Token != SPECIAL || Script->getSpecial() != ','){ + Script->error(", expected"); + } + + Script->nextToken(); + TBehaviourNode *Param2 = this->readTerm(Script); + if(Script->Token != SPECIAL || Script->getSpecial() != ','){ + Script->error(", expected"); + } + + Script->nextToken(); + TBehaviourNode *Param3 = this->readTerm(Script); + if(Script->Token != SPECIAL || Script->getSpecial() != ')'){ + Script->error(") expected"); + } + + Behaviour->addAction(Type, &Data, Param1, Param2, Param3); + break; + } + + default:{ + Script->error("unknown identifier"); + break; + } + } + }else if(Script->Token == SPECIAL && Script->getSpecial() == '*'){ + if(this->Behaviours == 0){ + Script->error("no previous pattern"); + } + Behaviour->addAction(BEHAVIOUR_ACTION_REPEAT, NULL, NULL, NULL, NULL); + }else{ + Script->error("illegal action"); + } + + Script->nextToken(); + if(Script->Token == SPECIAL && Script->getSpecial() == ','){ + Script->nextToken(); + }else{ + break; + } + } + + this->Behaviours += 1; + } +} + +TBehaviourNode *TBehaviourDatabase::readValue(TReadScriptFile *Script){ + TBehaviourNode *Node = NULL; + if(Script->Token == NUMBER){ + Node = NewBehaviourNode(BEHAVIOUR_NODE_NUMBER, Script->readNumber()); + }else if(Script->Token == IDENTIFIER){ + const char *Identifier = Script->getIdentifier(); + if(strcmp(Identifier, "topic") == 0){ + Node = NewBehaviourNode(BEHAVIOUR_NODE_TOPIC, 0); + }else if(strcmp(Identifier, "price") == 0){ + Node = NewBehaviourNode(BEHAVIOUR_NODE_PRICE, 0); + }else if(strcmp(Identifier, "amount") == 0){ + Node = NewBehaviourNode(BEHAVIOUR_NODE_AMOUNT, 0); + }else if(strcmp(Identifier, "level") == 0){ + Node = NewBehaviourNode(BEHAVIOUR_NODE_SKILL, SKILL_LEVEL); + }else if(strcmp(Identifier, "magiclevel") == 0){ + Node = NewBehaviourNode(BEHAVIOUR_NODE_SKILL, SKILL_MAGIC_LEVEL); + }else if(strcmp(Identifier, "hp") == 0){ + Node = NewBehaviourNode(BEHAVIOUR_NODE_SKILL, SKILL_HITPOINTS); + }else if(strcmp(Identifier, "poison") == 0){ + Node = NewBehaviourNode(BEHAVIOUR_NODE_SKILL, SKILL_POISON); + }else if(strcmp(Identifier, "burning") == 0){ + Node = NewBehaviourNode(BEHAVIOUR_NODE_SKILL, SKILL_BURNING); + }else if(strcmp(Identifier, "count") == 0){ + Script->readSymbol('('); + Script->nextToken(); + TBehaviourNode *Left = this->readTerm(Script); + if(Script->Token != SPECIAL || Script->getSpecial() != ')'){ + Script->error(") expected"); + } + + Node = NewBehaviourNode(BEHAVIOUR_NODE_COUNT, Left, NULL); + }else if(strcmp(Identifier, "countmoney") == 0){ + Node = NewBehaviourNode(BEHAVIOUR_NODE_COUNTMONEY, 0); + }else if(strcmp(Identifier, "type") == 0){ + Node = NewBehaviourNode(BEHAVIOUR_NODE_TYPE, 0); + }else if(strcmp(Identifier, "data") == 0){ + Node = NewBehaviourNode(BEHAVIOUR_NODE_DATA, 0); + }else if(strcmp(Identifier, "spellknown") == 0){ + Script->readSymbol('('); + Script->nextToken(); + TBehaviourNode *Left = this->readTerm(Script); + if(Script->Token != SPECIAL || Script->getSpecial() != ')'){ + Script->error(") expected"); + } + + Node = NewBehaviourNode(BEHAVIOUR_NODE_SPELLKNOWN, Left, NULL); + }else if(strcmp(Identifier, "spelllevel") == 0){ + Script->readSymbol('('); + Script->nextToken(); + TBehaviourNode *Left = this->readTerm(Script); + if(Script->Token != SPECIAL || Script->getSpecial() != ')'){ + Script->error(") expected"); + } + + Node = NewBehaviourNode(BEHAVIOUR_NODE_SPELLLEVEL, Left, NULL); + }else if(strcmp(Identifier, "random") == 0){ + Script->readSymbol('('); + + Script->nextToken(); + TBehaviourNode *Left = this->readTerm(Script); + if(Script->Token != SPECIAL || Script->getSpecial() != ','){ + Script->error(", expected"); + } + + Script->nextToken(); + TBehaviourNode *Right = this->readTerm(Script); + if(Script->Token != SPECIAL || Script->getSpecial() != ')'){ + Script->error(") expected"); + } + + Node = NewBehaviourNode(BEHAVIOUR_NODE_RANDOM, Left, Right); + }else if(strcmp(Identifier, "questvalue") == 0){ + Script->readSymbol('('); + Script->nextToken(); + TBehaviourNode *Left = this->readTerm(Script); + if(Script->Token != SPECIAL || Script->getSpecial() != ')'){ + Script->error(") expected"); + } + + Node = NewBehaviourNode(BEHAVIOUR_NODE_QUESTVALUE, Left, NULL); + } + }else if(Script->Token == SPECIAL){ + if(Script->getSpecial() != '%'){ + Script->error("illegal character"); + } + Node = NewBehaviourNode(BEHAVIOUR_NODE_PARAMETER, Script->readNumber()); + }else{ + Script->error("illegal value"); + } + + if(Node == NULL){ + Script->error("unknown value"); + } + + Script->nextToken(); + return Node; +} + +TBehaviourNode *TBehaviourDatabase::readFactor(TReadScriptFile *Script){ + TBehaviourNode *Node = this->readValue(Script); + while(Script->Token == SPECIAL && Script->getSpecial() == '*'){ + Script->nextToken(); + Node = NewBehaviourNode(BEHAVIOUR_NODE_MUL, Node, this->readValue(Script)); + } + return Node; +} + +TBehaviourNode *TBehaviourDatabase::readTerm(TReadScriptFile *Script){ + TBehaviourNode *Node = this->readFactor(Script); + while(Script->Token == SPECIAL){ + int Type = BEHAVIOUR_NODE_NONE; + if(Script->getSpecial() == '+'){ + Type = BEHAVIOUR_NODE_ADD; + }else if(Script->getSpecial() == '-'){ + Type = BEHAVIOUR_NODE_SUB; + }else{ + break; + } + + Script->nextToken(); + Node = NewBehaviourNode(Type, Node, this->readFactor(Script)); + } + return Node; +} + +int TBehaviourDatabase::evaluate(TNPC *Npc, TBehaviourNode *Node, int *Parameters){ + if(Npc == NULL){ + error("TBehaviourDatabase::evaluate: NPC existiert nicht.\n"); + return 0; + } + + if(Node == NULL){ + error("TBehaviourDatabase::evaluate: Knoten existiert nicht.\n"); + return 0; + } + + if(Parameters == NULL){ + error("TBehaviourDatabase::evaluate: Zahlen existieren nicht.\n"); + return 0; + } + + uint32 InterlocutorID = Npc->Interlocutor; + TPlayer *Interlocutor = GetPlayer(InterlocutorID); + if(Interlocutor == NULL){ + error("TBehaviourDatabase::evaluate: Gesprächspartner existiert nicht.\n"); + return 0; + } + + int Result = 0; + switch(Node->Type){ + case BEHAVIOUR_NODE_CMP_LT:{ + int Left = this->evaluate(Npc, Node->Left, Parameters); + int Right = this->evaluate(Npc, Node->Right, Parameters); + Result = Left < Right; + break; + } + + case BEHAVIOUR_NODE_CMP_GT:{ + int Left = this->evaluate(Npc, Node->Left, Parameters); + int Right = this->evaluate(Npc, Node->Right, Parameters); + Result = Left > Right; + break; + } + + case BEHAVIOUR_NODE_CMP_EQ:{ + int Left = this->evaluate(Npc, Node->Left, Parameters); + int Right = this->evaluate(Npc, Node->Right, Parameters); + Result = Left == Right; + break; + } + + case BEHAVIOUR_NODE_CMP_NEQ:{ + int Left = this->evaluate(Npc, Node->Left, Parameters); + int Right = this->evaluate(Npc, Node->Right, Parameters); + Result = Left != Right; + break; + } + + case BEHAVIOUR_NODE_CMP_LE:{ + int Left = this->evaluate(Npc, Node->Left, Parameters); + int Right = this->evaluate(Npc, Node->Right, Parameters); + Result = Left <= Right; + break; + } + + case BEHAVIOUR_NODE_CMP_GE:{ + int Left = this->evaluate(Npc, Node->Left, Parameters); + int Right = this->evaluate(Npc, Node->Right, Parameters); + Result = Left >= Right; + break; + } + + case BEHAVIOUR_NODE_ADD:{ + int Left = this->evaluate(Npc, Node->Left, Parameters); + int Right = this->evaluate(Npc, Node->Right, Parameters); + Result = Left + Right; + break; + } + + case BEHAVIOUR_NODE_SUB:{ + int Left = this->evaluate(Npc, Node->Left, Parameters); + int Right = this->evaluate(Npc, Node->Right, Parameters); + Result = Left - Right; + break; + } + + case BEHAVIOUR_NODE_MUL:{ + int Left = this->evaluate(Npc, Node->Left, Parameters); + int Right = this->evaluate(Npc, Node->Right, Parameters); + Result = Left * Right; + break; + } + + case BEHAVIOUR_NODE_PARAMETER:{ + if(Node->Data != 1 && Node->Data != 2){ + error("TBehaviourDatabase::evaluate: Ungültiger Zahl-Parameter %d.\n", Node->Data); + }else if(Parameters[Node->Data - 1] < 0){ + error("TBehaviourDatabase::evaluate: Zahl-Parameter %d nicht belegt.\n", Node->Data); + }else{ + Result = Parameters[Node->Data - 1]; + } + break; + } + + case BEHAVIOUR_NODE_NUMBER:{ + Result = Node->Data; + break; + } + + case BEHAVIOUR_NODE_TOPIC:{ + Result = Npc->Topic; + break; + } + + case BEHAVIOUR_NODE_PRICE:{ + Result = Npc->Price; + break; + } + + case BEHAVIOUR_NODE_AMOUNT:{ + Result = Npc->Amount; + break; + } + + case BEHAVIOUR_NODE_SKILL:{ + int SkillNr = Node->Data; + if(SkillNr == SKILL_LEVEL + || SkillNr == SKILL_MAGIC_LEVEL + || SkillNr == SKILL_HITPOINTS){ + Result = Interlocutor->Skills[SkillNr]->Get(); + }else if(SkillNr == SKILL_POISON + || SkillNr == SKILL_BURNING){ + Result = Interlocutor->Skills[SkillNr]->TimerValue(); + }else{ + error("TBehaviourDatabase::evaluate: Ungültiger Skill %d.\n", SkillNr); + } + break; + } + + case BEHAVIOUR_NODE_COUNT:{ + int TypeID = this->evaluate(Npc, Node->Left, Parameters); + Result = CountInventoryObjects(InterlocutorID, TypeID, Npc->Data); + break; + } + + case BEHAVIOUR_NODE_COUNTMONEY:{ + Result = CountInventoryMoney(InterlocutorID); + break; + } + + case BEHAVIOUR_NODE_TYPE:{ + Result = Npc->TypeID; + break; + } + + case BEHAVIOUR_NODE_DATA:{ + Result = Npc->Data; + break; + } + + case BEHAVIOUR_NODE_SPELLKNOWN:{ + int SpellNr = this->evaluate(Npc, Node->Left, Parameters); + Result = Interlocutor->SpellKnown(SpellNr); + break; + } + + case BEHAVIOUR_NODE_SPELLLEVEL:{ + int SpellNr = this->evaluate(Npc, Node->Left, Parameters); + Result = GetSpellLevel(SpellNr); + break; + } + + case BEHAVIOUR_NODE_RANDOM:{ + int Left = this->evaluate(Npc, Node->Left, Parameters); + int Right = this->evaluate(Npc, Node->Right, Parameters); + Result = random(Left, Right); + break; + } + + case BEHAVIOUR_NODE_QUESTVALUE:{ + int QuestNr = this->evaluate(Npc, Node->Left, Parameters); + Result = Interlocutor->GetQuestValue(QuestNr); + break; + } + + default:{ + error("TBehaviourDatabase::evaluate: Ungültiger Knotentyp %d.\n", Node->Type); + break; + } + } + return Result; +} + +// NOTE(fusion): These smaller functions were inside `TBehaviourDatabase::react` +// but I figured it would be better to pull them out for readability. +static bool CheckBehaviourProperty(int Property, SITUATION Situation, TPlayer *Interlocutor){ + if(Interlocutor == NULL){ + error("CheckBehaviourProperty: Interlocutor ist NULL."); + return false; + } + + bool Result = false; + switch(Property){ + case BEHAVIOUR_PROPERTY_ADDRESS: + Result = (Situation == ADDRESS || Situation == ADDRESSQUEUE); + break; + case BEHAVIOUR_PROPERTY_BUSY: + Result = (Situation == BUSY); + break; + case BEHAVIOUR_PROPERTY_VANISH: + Result = (Situation == VANISH); + break; + case BEHAVIOUR_PROPERTY_MALE: + Result = (Interlocutor->Sex == 1); + break; + case BEHAVIOUR_PROPERTY_FEMALE: + Result = (Interlocutor->Sex == 2); + break; + case BEHAVIOUR_PROPERTY_KNIGHT: + Result = (Interlocutor->GetEffectiveProfession() == PROFESSION_KNIGHT); + break; + case BEHAVIOUR_PROPERTY_PALADIN: + Result = (Interlocutor->GetEffectiveProfession() == PROFESSION_PALADIN); + break; + case BEHAVIOUR_PROPERTY_SORCERER: + Result = (Interlocutor->GetEffectiveProfession() == PROFESSION_SORCERER); + break; + case BEHAVIOUR_PROPERTY_DRUID: + Result = (Interlocutor->GetEffectiveProfession() == PROFESSION_DRUID); + break; + case BEHAVIOUR_PROPERTY_PREMIUM: + Result = CheckRight(Interlocutor->ID, PREMIUM_ACCOUNT); + break; + case BEHAVIOUR_PROPERTY_PROMOTED: + Result = Interlocutor->GetActivePromotion(); + break; + case BEHAVIOUR_PROPERTY_PZBLOCK: + Result = (Interlocutor->EarliestProtectionZoneRound > RoundNr); + break; + case BEHAVIOUR_PROPERTY_NONPVP: + Result = (WorldType == NON_PVP); + break; + case BEHAVIOUR_PROPERTY_PVPENFORCED: + Result = (WorldType == PVP_ENFORCED); + break; + } + + return Result; +} + +static bool FormatNpcResponse(char *Buffer, int BufferSize, + const char *Template, TNPC *Npc, TPlayer *Interlocutor){ + if(Buffer == NULL || BufferSize <= 0){ + error("FormatNpcResponse: Invalid response buffer."); + return false; + } + + if(Template == NULL || Template[0] == 0){ + error("FormatNpcResponse: Template is NULL or empty."); + return false; + } + + if(Npc == NULL){ + error("FormatNpcResponse: Npc is NULL.\n"); + return false; + } + + if(Interlocutor == NULL){ + error("FormatNpcResponse: Interlocutor is NULL.\n"); + return false; + } + + int WritePos = 0; + int ReadPos = 0; + while(Template[ReadPos] != 0){ + if(Template[ReadPos] == '%' && Template[ReadPos + 1] != 0){ + char Help[50] = {}; + switch(Template[ReadPos + 1]){ + case 'N': strcpy(Help, Interlocutor->Name); break; + case 'A': sprintf(Help, "%d", Npc->Amount); break; + case 'P': sprintf(Help, "%d", Npc->Price); break; + case 'T':{ + int Hour, Minute; + GetTime(&Hour, &Minute); + if(Hour < 12){ + sprintf(Help, "%d:%.2d am", Hour, Minute); + }else{ + sprintf(Help, "%d:%.2d pm", (Hour - 12), Minute); + } + break; + } + } + + // NOTE(fusion): Make sure we don't overflow the buffer. + int HelpLen = strlen(Help); + if(HelpLen > 0){ + if((WritePos + HelpLen) > BufferSize){ + HelpLen = BufferSize - WritePos; + } + + if(HelpLen > 0){ + memcpy(&Buffer[WritePos], Help, HelpLen); + WritePos += HelpLen; + } + } + + ReadPos += 2; + }else{ + // NOTE(fusion): Make sure we don't overflow the buffer. + if(WritePos < BufferSize){ + Buffer[WritePos] = Template[ReadPos]; + WritePos += 1; + } + + ReadPos += 1; + } + } + + if(WritePos < BufferSize){ + Buffer[WritePos] = 0; + return true; + }else{ + Buffer[BufferSize - 1] = 0; + return false; + } +} + +void TBehaviourDatabase::react(TNPC *Npc, const char *Text, SITUATION Situation){ + if(Npc == NULL){ + error("TBehaviourDatabase::react: NPC existiert nicht.\n"); + return; + } + + if(Text == NULL){ + error("TBehaviourDatabase::react: Übergebener Text existiert nicht.\n"); + return; + } + + uint32 InterlocutorID = Npc->Interlocutor; + TPlayer *Interlocutor = GetPlayer(InterlocutorID); + if(Interlocutor == NULL){ + error("TBehaviourDatabase::react: Gesprächspartner existiert nicht" + " (Text=%s, Situation=%d).\n", Text, Situation); + return; + } + + int Parameters[2] = {-1, -1}; + int BestMatch = -1; + int MaxConditions = -1; + for(int BehaviourNr = 0; + BehaviourNr < this->Behaviours; + BehaviourNr += 1){ + TBehaviour *Behaviour = this->Behaviour.at(BehaviourNr); + bool Match = true; + bool ShortCircuit = false; + const char *TextPtr = Text; + for(int ConditionNr = 0; + ConditionNr < Behaviour->Conditions && Match; + ConditionNr += 1){ + TBehaviourCondition *Condition = Behaviour->Condition.at(ConditionNr); + if(Condition->Type == BEHAVIOUR_CONDITION_SHORTCIRCUIT){ + ShortCircuit = true; + break; + } + + switch(Condition->Type){ + case BEHAVIOUR_CONDITION_TEXT:{ + const char *Pattern = GetDynamicString(Condition->Text); + const char *Word = SearchForWord(Pattern, TextPtr); + if(Word != NULL){ + TextPtr = Word + strlen(Pattern); + }else{ + Match = false; + } + break; + } + + case BEHAVIOUR_CONDITION_PROPERTY:{ + if(!CheckBehaviourProperty(Condition->Property, Situation, Interlocutor)){ + Match = false; + } + break; + } + + case BEHAVIOUR_CONDITION_PARAMETER:{ + // TODO(fusion): The original function wouldn't check before + // writing to `Parameters` which could be a problem. + const char *Parameter = SearchForNumber(Condition->Number, TextPtr); + if(Parameter != NULL + && Condition->Number >= 1 + && Condition->Number <= NARRAY(Parameters)){ + Parameters[Condition->Number - 1] = atoi(Parameter); + if(Parameters[Condition->Number - 1] > 500){ + Parameters[Condition->Number - 1] = 500; + } + + // TODO(fusion): This could be problematic if the number + // has more than one digit. + TextPtr = Parameter + 1; + }else{ + Match = false; + } + break; + } + + case BEHAVIOUR_CONDITION_EXPRESSION:{ + if(this->evaluate(Npc, Condition->Expression, Parameters) == 0){ + Match = false; + } + break; + } + + case BEHAVIOUR_CONDITION_SHORTCIRCUIT:{ + // TODO(fusion): Select current and stop loop. + break; + } + } + } + + if(ShortCircuit || (Match && Behaviour->Conditions > MaxConditions)){ + BestMatch = BehaviourNr; + MaxConditions = Behaviour->Conditions; + if(ShortCircuit){ + break; + } + } + } + + if(BestMatch == -1){ + return; + } + + if(Situation != BUSY){ + Npc->Topic = 0; + } + + bool Repeat = false; + bool StartToDo = false; + int TalkDelay = 1000; + int BehaviourNr = BestMatch; + do{ + Repeat = false; + TBehaviour *Behaviour = this->Behaviour.at(BehaviourNr); + for(int ActionNr = 0; + ActionNr < Behaviour->Actions; + ActionNr += 1){ + TBehaviourAction *Action = Behaviour->Action.at(ActionNr); + if(Action->Type == BEHAVIOUR_ACTION_REPEAT){ + if(BehaviourNr > 0){ + BehaviourNr -= 1; + Repeat = true; + }else{ + error("TBehaviourDatabase::react (9): Kein vorheriges Muster.\n"); + } + break; + } + + switch(Action->Type){ + case BEHAVIOUR_ACTION_REPLY:{ + char Response[256] = {}; + const char *Template = GetDynamicString(Action->Text); + if(FormatNpcResponse(Response, sizeof(Response), Template, Npc, Interlocutor)){ + Npc->ToDoWait(TalkDelay); + Npc->ToDoTalk(TALK_SAY, NULL, Response, false); + TalkDelay += 3100 + (int)strlen(Response) * 100; + StartToDo = true; + }else{ + Response[20] = 0; + error("TBehaviourDatabase::react: Text von NPC %s wird zu lang (%s...).\n", + Npc->Name, Response); + } + break; + } + + case BEHAVIOUR_ACTION_SET_VARIABLE:{ + int Variable = Action->Number; + int Value = this->evaluate(Npc, Action->Expression, Parameters); + switch(Variable){ + case 1: Npc->Topic = Value; break; + case 2: Npc->Price = Value; break; + case 3: Npc->Amount = Value; break; + case 4: Npc->TypeID = Value; break; + case 6: Npc->Data = Value; break; + default:{ + error("TBehaviourDatabase::react: Ungültige Variable.\n"); + break; + } + } + break; + } + + case BEHAVIOUR_ACTION_SET_SKILL:{ + int SkillNr = Action->Number; + int Value = this->evaluate(Npc, Action->Expression, Parameters); + if(SkillNr == SKILL_HITPOINTS){ + Interlocutor->Skills[SKILL_HITPOINTS]->Set(Value); + if(Interlocutor->Skills[SKILL_HITPOINTS]->Get() <= 0){ + error("TBehaviourDatabase::react: NPC %s tötet Spieler.\n", Npc->Name); + } + }else{ + error("TBehaviourDatabase::react: Ungültiger Skill.\n"); + } + break; + } + + case BEHAVIOUR_ACTION_FUNCTION2:{ + int FunctionNr = Action->Number; + int Param1 = this->evaluate(Npc, Action->Expression, Parameters); + int Param2 = this->evaluate(Npc, Action->Expression2, Parameters); + switch(FunctionNr){ + case 3: Interlocutor->SetQuestValue(Param1, Param2); break; + default:{ + error("TBehaviourDatabase::react (4): Ungültige Funktionsnummer.\n"); + break; + } + } + break; + } + + case BEHAVIOUR_ACTION_FUNCTION1:{ + int FunctionNr = Action->Number; + int Param = this->evaluate(Npc, Action->Expression, Parameters); + switch(FunctionNr){ + case 1: GraphicalEffect(Npc->CrObject, Param); break; + case 2: GraphicalEffect(Interlocutor->CrObject, Param); break; + case 3: Interlocutor->SetProfession(Param); break; + case 4: Interlocutor->LearnSpell(Param); break; + case 5: CreateMonster(Param, Npc->posx, Npc->posy, Npc->posz, 0, 0, true); break; + case 6: Npc->GiveTo(Param, Npc->Amount); break; + case 7: Npc->GetFrom(Param, Npc->Amount); break; + default:{ + error("TBehaviourDatabase::react (5): Ungültige Funktionsnummer.\n"); + break; + } + } + break; + } + + case BEHAVIOUR_ACTION_FUNCTION0:{ + int FunctionNr = Action->Number; + switch(FunctionNr){ + case 1: Npc->GiveMoney(Npc->Price); break; + case 2: Npc->GetMoney(Npc->Price); break; + case 3:{ + if(Situation == BUSY){ + Npc->Enqueue(InterlocutorID, Text); + }else{ + error("TBehaviourDatabase::react (6): falsche Situation für Aktion \"Queue\".\n"); + } + break; + } + default:{ + error("TBehaviourDatabase::react (6): Ungültige Funktionsnummer.\n"); + break; + } + } + break; + } + + case BEHAVIOUR_ACTION_CHANGESTATE:{ + int NewState = Action->Number; + if(!StartToDo){ + Npc->ChangeState((STATE)NewState, false); + if(Situation != ADDRESSQUEUE){ + StartToDo = true; + }else{ + error("TBehaviourDatabase::react: NPC %s reagiert nicht auf Anrede %s.\n", + Npc->Name, Text); + } + }else{ + if(NewState == IDLE){ + Npc->ChangeState(LEAVING, false); + } + Npc->ToDoChangeState(NewState); + } + break; + } + + case BEHAVIOUR_ACTION_SET_SKILL_TIMER:{ + int SkillNr = Action->Number; + int Cycle = this->evaluate(Npc, Action->Expression, Parameters); + int MaxCount = this->evaluate(Npc, Action->Expression2, Parameters); + if(SkillNr == SKILL_POISON || SkillNr == SKILL_BURNING){ + if(Cycle == 0 || Interlocutor->Skills[SkillNr]->TimerValue() < Cycle){ + Interlocutor->SetTimer(SkillNr, Cycle, MaxCount, MaxCount, -1); + if(SkillNr == SKILL_POISON){ + Interlocutor->PoisonDamageOrigin = 0; + }else{ + Interlocutor->FireDamageOrigin = 0; + } + } + }else{ + error("TBehaviourDatabase::react (8): Ungültiger Skill.\n"); + } + break; + } + + case BEHAVIOUR_ACTION_FUNCTION3:{ + int FunctionNr = Action->Number; + int Param1 = this->evaluate(Npc, Action->Expression, Parameters); + int Param2 = this->evaluate(Npc, Action->Expression2, Parameters); + int Param3 = this->evaluate(Npc, Action->Expression3, Parameters); + switch(FunctionNr){ + case 1:{ + print(3, "NPC teleportiert Gesprächspartner nach [%d,%d,%d].\n", + Param1, Param2, Param3); + try{ + Object Dest = GetMapContainer(Param1, Param2, Param3); + Move(0, Interlocutor->CrObject, Dest, -1, false, NONE); + }catch(RESULT r){ + error("TBehaviourDatabase::react (10): Exception %d.\n", r); + } + break; + } + + case 2:{ + print(3, "NPC setzt Startkoordinate für Gesprächspartner auf [%d,%d,%d].\n", + Param1, Param2, Param3); + Interlocutor->startx = Param1; + Interlocutor->starty = Param2; + Interlocutor->startz = Param3; + Interlocutor->SaveData(); + break; + } + + default:{ + error("TBehaviourDatabase::react (10): Ungültige Unternummer.\n"); + break; + } + } + break; + } + } + } + }while(Repeat); + + if(StartToDo){ + Npc->ToDoWait(TalkDelay); + Npc->ToDoStart(); + if(Situation != BUSY){ + Npc->LastTalk = (TalkDelay / 1000) + RoundNr; + } + } +} + +// Monster Homes +// ============================================================================= +void StartMonsterhomeTimer(int Nr){ + if(Nr < 1 || Nr > Monsterhomes){ + error("StartMonsterhomeTimer: Ungültige Monsterhome-Nummer %d.\n", Nr); + return; + } + + TMonsterhome *MH = Monsterhome.at(Nr); + if(MH->Timer > 0){ + error("StartMonsterhomeTimer: Zähler läuft schon.\n"); + return; + } + + if(MH->ActMonsters >= MH->MaxMonsters){ + error("StartMonsterhomeTimer: Maximale Monsterzahl schon erreicht.\n"); + error("# Monsterhome mit Rasse %d an [%d,%d,%d]\n", MH->Race, MH->x, MH->y, MH->z); + return; + } + + int MaxTimer = MH->RegenerationTime; + int NumPlayers = GetNumberOfPlayers(); + if(NumPlayers > 800){ + MaxTimer = (MaxTimer * 2) / 5; + }else if(NumPlayers > 200){ + MaxTimer = (MaxTimer * 200) / ((NumPlayers / 2) + 100); + } + + MH->Timer = random(MaxTimer / 2, MaxTimer); +} + +void LoadMonsterhomes(void){ + print(1, "Initialisiere Monsterhomes ...\n"); + + char FileName[4096]; + snprintf(FileName, sizeof(FileName), "%s/monster.db", DATAPATH); + + TReadScriptFile Script; + Script.open(FileName); + + Monsterhomes = 0; + while(true){ + int Race = Script.readNumber(); + if(Race == 0){ + break; + } + + Monsterhomes += 1; + TMonsterhome *MH = Monsterhome.at(Monsterhomes); + MH->Race = Race; + MH->x = Script.readNumber(); + MH->y = Script.readNumber(); + MH->z = Script.readNumber(); + MH->Radius = Script.readNumber(); + MH->MaxMonsters = Script.readNumber(); + MH->ActMonsters = 0; + MH->RegenerationTime = Script.readNumber(); + MH->Timer = 0; + + if(!IsOnMap(MH->x, MH->y, MH->z)){ + print(1, "WARNUNG: Monsterhome [%d,%d,%d] befindet sich außerhalb der Karte.\n", MH->x, MH->y, MH->z); + } + } + + print(1, "%d Monsterhomes geladen.\n", Monsterhomes); + Script.close(); + + for(int i = 0; i < Monsterhomes; i += 1){ + TMonsterhome *MH = Monsterhome.at(i); + for (int j = 0; j < MH->MaxMonsters; j += 1){ + int SpawnX = MH->x; + int SpawnY = MH->y; + int SpawnZ = MH->z; + int SpawnRadius = MH->Radius; + + if(j == 0){ + if(SpawnRadius > 1){ + SpawnRadius = 1; + } + }else{ + if(SpawnRadius > 10){ + SpawnRadius = 10; + } + + // NOTE(fusion): `SearchSpawnField` performs an extended search + // if the radius is negative. + SpawnRadius = -SpawnRadius; + } + + if(SearchSpawnField(&SpawnX, &SpawnY, &SpawnZ, SpawnRadius, false)){ + CreateMonster(MH->Race, SpawnX, SpawnY, SpawnZ, i, 0, false); + MH->ActMonsters += 1; + } + } + + if(MH->Timer > 0){ + error("LoadMonsterhomes: Timer läuft schon (Rasse %d an [%d,%d,%d]).\n", + MH->Race, MH->x, MH->y, MH->z); + }else if(MH->ActMonsters < MH->MaxMonsters){ + StartMonsterhomeTimer(i); + } + } +} + +void ProcessMonsterhomes(void){ + for(int i = 0; i < Monsterhomes; i += 1){ + TMonsterhome *MH = Monsterhome.at(i); + if(MH->Timer > 0){ + MH->Timer -= 1; + } + + if(MH->Timer > 0){ + continue; + } + + int MaxRadius = MH->Radius; + if(MaxRadius > 10){ + MaxRadius = 10; + } + + TFindCreatures Search(MaxRadius + 9, MaxRadius + 7, MH->x, MH->y, FIND_PLAYERS); + while(true){ + uint32 CharacterID = Search.getNext(); + if(CharacterID == 0){ + break; + } + + TPlayer *Player = GetPlayer(CharacterID); + if(Player == NULL){ + error("ProcessMonsterhomes: Kreatur existiert nicht.\n"); + break; + } + + // TODO(fusion): There is probably a helper function to check whether + // a floor is visible from another. + if(Player->posz <= 7){ + if(MH->z > 7){ + continue; + } + }else{ + if(std::abs(Player->posz - MH->z) > 2){ + continue; + } + } + + int DistanceX = std::abs(Player->posx - MH->x); + int DistanceY = std::abs(Player->posy - MH->y); + int Radius = std::max<int>(DistanceX - 9, DistanceY - 7); + if(Radius < MaxRadius){ + MaxRadius = Radius; + } + } + + if(MaxRadius >= 0){ + int SpawnX = MH->x; + int SpawnY = MH->y; + int SpawnZ = MH->z; + int SpawnRadius = MaxRadius; + + if(MH->ActMonsters == 0){ + if(SpawnRadius > 1){ + SpawnRadius = 1; + } + }else{ + // NOTE(fusion): `SearchSpawnField` performs an extended search + // if the radius is negative. + SpawnRadius = -SpawnRadius; + } + + if(SearchSpawnField(&SpawnX, &SpawnY, &SpawnZ, SpawnRadius, false)){ + CreateMonster(MH->Race, SpawnX, SpawnY, SpawnZ, i, 0, false); + MH->ActMonsters += 1; + + // TODO(fusion): Not sure why this check is here. + if(MH->Timer > 0){ + error("ProcessMonsterhomes: Timer läuft schon (Rasse %d an [%d,%d,%d]).\n", + MH->Race, SpawnX, SpawnY, SpawnZ); + } + } + } + + if(MH->ActMonsters < MH->MaxMonsters){ + StartMonsterhomeTimer(i); + } + } +} + +void NotifyMonsterhomeOfDeath(int Nr){ + if(Nr < 1 || Nr > Monsterhomes){ + error("NotifyMonsterhomeOfDeath: Ungültige Monsterhome-Nummer %d.\n", Nr); + return; + } + + TMonsterhome *MH = Monsterhome.at(Nr); + if(MH->ActMonsters < 1){ + error("NotifyMonsterhomeOfDeath: Monsterhome hat keine lebenden Kreaturen.\n"); + return; + } + + MH->ActMonsters -= 1; + if(MH->ActMonsters >= MH->MaxMonsters){ + error("NotifyMonsterhomeOfDeath: Monsterhome %d hatte zu viele Monster (%d statt %d).\n", + Nr, (MH->ActMonsters + 1), MH->MaxMonsters); + return; + } + + if(MH->Timer == 0){ + StartMonsterhomeTimer(Nr); + } +} + +bool MonsterhomeInRange(int Nr, int x, int y, int z){ + if(Nr == 0){ + return true; + } + + if(Nr < 1 || Nr > Monsterhomes){ + error("MonsterhomeInRange: Ungültige Monsterhome-Nummer %d.\n", Nr); + return false; + } + + TMonsterhome *MH = Monsterhome.at(Nr); + return std::abs(z - MH->z) <= 2 + && std::abs(x - MH->x) <= MH->Radius + && std::abs(y - MH->y) <= MH->Radius; +} + +// TNonplayer +// ============================================================================= +TNonplayer::TNonplayer(void) : TCreature() { + this->State = SLEEPING; +} + +TNonplayer::~TNonplayer(void){ + this->DelInList(); +} + +void TNonplayer::SetInList(void){ + TCreature::SetInCrList(); + *NonplayerList.at(FirstFreeNonplayer) = this; + FirstFreeNonplayer += 1; +} + +void TNonplayer::DelInList(void){ + bool Removed = false; + for(int i = 0; i < FirstFreeNonplayer; i += 1){ + if(*NonplayerList.at(i) == this){ + FirstFreeNonplayer -= 1; + *NonplayerList.at(i) = *NonplayerList.at(FirstFreeNonplayer); + *NonplayerList.at(FirstFreeNonplayer) = NULL; + Removed = true; + break; + } + } + + if(!Removed){ + error("TNonplayer::DelInList: Kreatur nicht gefunden.\n"); + } +} + +// TNPC +// ============================================================================= +TNPC::TNPC(const char *FileName) : + TNonplayer(), + QueuedPlayers(0, 9, 10), + QueuedAddresses(0, 9, 10) +{ + this->Type = NPC; + this->posz = 255; + this->Interlocutor = 0; + this->Topic = 0; + this->Price = 0; + this->Amount = 1; + this->TypeID = 0; + this->Data = 0; + this->LastTalk = 0; + this->QueueLength = 0; + this->Behaviour = NULL; + + TReadScriptFile Script; + Script.open(FileName); + while(true){ + Script.nextToken(); + if(Script.Token == ENDOFFILE){ + Script.close(); + break; + } + + if(Script.Token != IDENTIFIER){ + Script.error("identifier expected"); + } + + char Identifier[MAX_IDENT_LENGTH]; + strcpy(Identifier, Script.getIdentifier()); + Script.readSymbol('='); + + if(strcmp(Identifier, "name") == 0){ + strcpy(this->Name, Script.readString()); + }else if(strcmp(Identifier, "sex") == 0){ + const char *Sex = Script.readIdentifier(); + if(strcmp(Sex, "male") == 0){ + this->Sex = 1; + }else if(strcmp(Sex, "female") == 0){ + this->Sex = 2; + }else{ + Script.error("unknown constant"); + } + }else if(strcmp(Identifier, "race") == 0){ + this->Race = Script.readNumber(); + this->SetSkills(this->Race); + }else if(strcmp(Identifier, "outfit") == 0){ + this->OrgOutfit = ReadOutfit(&Script); + this->Outfit = this->OrgOutfit; + }else if(strcmp(Identifier, "home") == 0){ + Script.readCoordinate(&this->startx, &this->starty, &this->startx); + this->posx = this->startx; + this->posy = this->starty; + this->posz = this->startz; + }else if(strcmp(Identifier, "radius") == 0){ + this->Radius = Script.readNumber(); + }else if(strcmp(Identifier, "gostrength") == 0){ + if(this->Race == 0){ + Script.error("gostrength before race in npc-script-file"); + } + int GoStrength = Script.readNumber(); + this->Skills[SKILL_GO_STRENGTH]->Act = GoStrength; + this->Skills[SKILL_GO_STRENGTH]->Max = GoStrength; + }else if(strcmp(Identifier, "behaviour") == 0){ + if(this->Behaviour != NULL){ + Script.error("behaviour database specified twice for NPC"); + } + this->Behaviour = new TBehaviourDatabase(&Script); + }else{ + Script.error("unknown npc property"); + } + } + + if(this->Name[0] == 0){ + throw "no name specified for NPC"; + } + + if(this->posz == 255){ + throw "no startpoint specified for NPC"; + } + + if(this->Behaviour == NULL){ + throw "no behaviour database specified for NPC"; + } + + if(!IsOnMap(this->posx, this->posy, this->posz)){ + print(1, "WARNUNG: NPC '%s' steht außerhalb der Karte.\n", this->Name); + return; + } + + this->SetID(0); + this->SetInList(); + if(!this->SetOnMap()){ + print(1, "WARNUNG: Kann NPC \'%s\' nicht setzen.\n", this->Name); + return; + } + + this->ToDoYield(); +} + +TNPC::~TNPC(void){ + delete this->Behaviour; +} + +bool TNPC::MovePossible(int x, int y, int z, bool Execute, bool Jump){ + return CoordinateFlag(x, y, z, BANK) + && !CoordinateFlag(x, y, z, UNPASS) + && !CoordinateFlag(x, y, z, AVOID) + && z == this->startz + && std::abs(x - this->startx) <= this->Radius + && std::abs(y - this->starty) <= this->Radius + && !IsHouse(x, y, z); +} + +void TNPC::TalkStimulus(uint32 SpeakerID, const char *Text){ + if(SpeakerID == this->ID){ + return; + } + + if(Text == NULL){ + error("TNPC::TalkStimulus: Übergebener Text existiert nicht.\n"); + return; + } + + if(this->State == TALKING || this->QueueLength != 0){ + uint32 InterlocutorID = this->Interlocutor; + if(SpeakerID == InterlocutorID){ + this->LastTalk = RoundNr; + this->Behaviour->react(this, Text, DEFAULT); + }else{ + this->Interlocutor = SpeakerID; + this->Behaviour->react(this, Text, BUSY); + this->Interlocutor = InterlocutorID; + } + }else{ + this->ToDoClear(); + this->ChangeState(TALKING, false); + this->Interlocutor = SpeakerID; + this->LastTalk = RoundNr; + this->Behaviour->react(this, Text, ADDRESS); + if(this->State == TALKING){ + this->TurnToInterlocutor(); + } + } +} + +void TNPC::DamageStimulus(uint32 AttackerID, int Damage, int DamageType){ + // no-op +} + +void TNPC::IdleStimulus(void){ + if(this->State == TALKING){ + if((this->LastTalk + 30) > RoundNr){ + this->ToDoWait(2000); + this->ToDoStart(); + return; + } + + this->Behaviour->react(this, "", VANISH); + this->ChangeState(IDLE, false); + } + + if(this->State == IDLE){ + while(this->QueueLength > 0){ + uint32 InterlocutorID = *this->QueuedPlayers.at(0); + uint32 Text = *this->QueuedAddresses.at(0); + + // NOTE(fusion): Ordered removal. + this->QueueLength -= 1; + for(int i = 0; i < this->QueueLength; i += 1){ + *this->QueuedPlayers.at(i) = *this->QueuedPlayers.at(i + 1); + *this->QueuedAddresses.at(i) = *this->QueuedAddresses.at(i + 1); + } + + this->ToDoClear(); + + TCreature *Interlocutor = GetCreature(InterlocutorID); + if(Interlocutor != NULL){ + this->ChangeState(TALKING, false); + this->Interlocutor = InterlocutorID; + this->Behaviour->react(this, GetDynamicString(Text), ADDRESSQUEUE); + }else{ + error("TNPC::IdleStimulus: Gesprächspartner existiert nicht.\n"); + } + + DeleteDynamicString(Text); + if(this->State == TALKING){ + this->TurnToInterlocutor(); + this->LastTalk = RoundNr; + return; + } + } + + if(!this->LockToDo){ + TFindCreatures Search(10, 10, this->ID, FIND_PLAYERS); + if(Search.getNext() == 0){ + this->ChangeState(SLEEPING, false); + return; + } + + bool FoundDest = false; + int DestX, DestY, DestZ; + for(int i = 0; i < 10; i += 1){ + DestX = this->posx; + DestY = this->posy; + DestZ = this->posz; + switch(rand() % 4){ + case 0: DestX -= 1; break; + case 1: DestX += 1; break; + case 2: DestY -= 1; break; + case 3: DestY += 1; break; + } + + if(this->MovePossible(DestX, DestY, DestZ, true, false)){ + FoundDest = true; + break; + } + } + + if(!FoundDest){ + this->ToDoWait(2000); + this->ToDoStart(); + return; + } + + try{ + this->ToDoGo(DestX, DestY, DestZ, true, INT_MAX); + this->ToDoWait(2000); + this->ToDoStart(); + }catch(RESULT r){ + error("TNPC::IdleStimulus: Exception %d!\n", r); + } + } + } +} + +void TNPC::CreatureMoveStimulus(uint32 CreatureID, int Type){ + for(int i = 0; i < this->QueueLength; i += 1){ + uint32 QueuedPlayerID = *this->QueuedPlayers.at(i); + if(CreatureID != QueuedPlayerID && CreatureID != this->ID){ + continue; + } + + TCreature *QueuedPlayer = GetCreature(QueuedPlayerID); + if(Type != OBJECT_DELETED + && QueuedPlayer->posz == this->posz + && std::abs(QueuedPlayer->posx - this->posx) < 5 + && std::abs(QueuedPlayer->posy - this->posy) < 4){ + continue; + } + + // NOTE(fusion): Ordered removal. + this->QueueLength -= 1; + for(int j = i; j < this->QueueLength; j += 1){ + *this->QueuedPlayers.at(j) = *this->QueuedPlayers.at(j + 1); + *this->QueuedAddresses.at(j) = *this->QueuedAddresses.at(j + 1); + } + + // NOTE(fusion): The ordered removal has shifted queued players back one + // place so we need to check the current queue position again. + i -= 1; + } + + if(this->State == TALKING || this->State == LEAVING){ + if(CreatureID == this->Interlocutor || CreatureID == this->ID){ + TCreature *Interlocutor = GetCreature(this->Interlocutor); + if(Interlocutor == NULL){ + error("TNPC::CreatureMoveStimulus: Gesprächspartner existiert nicht.\n"); + this->ChangeState(IDLE, true); + return; + } + + if(Type != OBJECT_DELETED){ + this->Rotate(Interlocutor); + } + + if(this->State == TALKING){ + if(Type == OBJECT_DELETED + || Interlocutor->posz != this->posz + || std::abs(Interlocutor->posx - this->posx) >= 5 + || std::abs(Interlocutor->posy - this->posy) >= 4){ + this->Behaviour->react(this, "", VANISH); + this->ChangeState(IDLE, true); + } + } + } + } + + if(CreatureID != this->ID + && this->State == SLEEPING + && Type != OBJECT_DELETED){ + this->ChangeState(IDLE, true); + } +} + +void TNPC::GiveTo(ObjectType Type, int Amount){ + if(Amount == 0){ + return; + } + + if(Type.isMapContainer() || Type.getName(1) == NULL){ + error("TNPC::GiveTo: %s will Objekte vom Typ %d erschaffen.\n", + this->Name, Type.TypeID); + return; + } + + Log("npc", "%s -> %u: %d %s\n", this->Name, this->Interlocutor, Amount, Type.getName(1)); + + if(Type.getFlag(CUMULATIVE)){ + while(Amount > 0){ + int StackSize = std::min<int>(Amount, 100); + CreateAtCreature(this->Interlocutor, Type, StackSize); + Amount -= StackSize; + } + }else{ + while(Amount > 0){ + CreateAtCreature(this->Interlocutor, Type, this->Data); + Amount -= 1; + } + } +} + +void TNPC::GetFrom(ObjectType Type, int Amount){ + if(Amount > 0){ + Log("npc", "%s <- %u: %d %s\n", this->Name, this->Interlocutor, Amount, Type.getName(1)); + DeleteAtCreature(this->Interlocutor, Type, Amount, this->Data); + } +} + +void TNPC::GiveMoney(int Amount){ + int Crystal = (Amount / 10000); + int Platinum = (Amount % 10000) / 100; + int Gold = (Amount % 10000) % 100; + this->GiveTo(GetSpecialObject(MONEY_TENTHOUSAND), Crystal); + this->GiveTo(GetSpecialObject(MONEY_HUNDRED), Platinum); + this->GiveTo(GetSpecialObject(MONEY_ONE), Gold); +} + +void TNPC::GetMoney(int Amount){ + int Crystal = CountInventoryObjects(this->Interlocutor, GetSpecialObject(MONEY_TENTHOUSAND), 0); + int Platinum = CountInventoryObjects(this->Interlocutor, GetSpecialObject(MONEY_HUNDRED), 0); + int Gold = CountInventoryObjects(this->Interlocutor, GetSpecialObject(MONEY_ONE), 0); + CalculateChange(Amount, &Gold, &Platinum, &Crystal); + + ASSERT(Crystal >= 0); + if(Crystal > 0){ + this->GetFrom(GetSpecialObject(MONEY_TENTHOUSAND), Crystal); + } + + if(Platinum > 0){ + this->GetFrom(GetSpecialObject(MONEY_HUNDRED), Platinum); + }else if(Platinum < 0){ + this->GiveTo(GetSpecialObject(MONEY_HUNDRED), -Platinum); + } + + if(Gold > 0){ + this->GetFrom(GetSpecialObject(MONEY_ONE), Gold); + }else if(Gold < 0){ + this->GiveTo(GetSpecialObject(MONEY_ONE), -Gold); + } +} + +void TNPC::Enqueue(uint32 InterlocutorID, const char *Text){ + if(InterlocutorID == 0){ + error("TNPC::Enqueue: Gesprächspartner ist Null.\n"); + return; + } + + if(Text == NULL){ + error("TNPC::Enqueue: Text ist NULL.\n"); + return; + } + + bool AlreadyQueued = false; + for(int i = 0; i < this->QueueLength; i += 1){ + if(*this->QueuedPlayers.at(i) == InterlocutorID){ + AlreadyQueued = true; + break; + } + } + + if(!AlreadyQueued){ + *this->QueuedPlayers.at(this->QueueLength) = InterlocutorID; + *this->QueuedAddresses.at(this->QueueLength) = AddDynamicString(Text); + this->QueueLength += 1; + } +} + +void TNPC::TurnToInterlocutor(void){ + TCreature *Interlocutor = GetCreature(this->Interlocutor); + if(Interlocutor == NULL){ + error("TNPC::TurnToInterlocutor: Gesprächspartner existiert nicht.\n"); + return; + } + + this->Rotate(Interlocutor); +} + +void TNPC::ChangeState(STATE NewState, bool Stimulus){ + this->State = NewState; + if(Stimulus){ + this->ToDoYield(); + } +} + +void ChangeNPCState(TCreature *Npc, int NewState, bool Stimulus){ + if(Npc == NULL){ + error("ChangeNPCState: npc ist NULL.\n"); + return; + } + + if(Npc->Type != NPC){ + error("ChangeNPCState: npc ist kein NPC.\n"); + return; + } + + ((TNPC*)Npc)->ChangeState((STATE)NewState, Stimulus); +} + +// TMonster +// ============================================================================= +TMonster::TMonster(int Race, int x, int y, int z, int Home, uint32 MasterID) : + TNonplayer() +{ + this->Type = MONSTER; + this->Race = Race; + this->startx = x; + this->starty = y; + this->startz = z; + this->posx = x; + this->posx = y; + this->posx = z; + this->State = IDLE; + this->Home = Home; + this->Master = MasterID; + this->Target = 0; + + while(this->Master != 0){ + TCreature *Master = GetCreature(this->Master); + // TODO(fusion): Do we actually want to return here? + if(Master == NULL){ + error("TMonster::TMonster: Master existiert nicht.\n"); + return; + } + + if(Master->Type != MONSTER || ((TMonster*)Master)->Master != 0){ + this->LifeEndRound = Master->LifeEndRound; + Master->SummonedCreatures += 1; + break; + } + + error("TMonster::TMonster: Kinder dürfen keine eigenen Kinder erschaffen.\n"); + this->Master = ((TMonster*)Master)->Master; + } + + if(RaceData[Race].Article[0] == 0){ + strcpy(this->Name, RaceData[Race].Name); + }else{ + snprintf(this->Name, sizeof(this->Name), "%s %s", + RaceData[Race].Article, RaceData[Race].Name); + } + + this->SetSkills(Race); + this->OrgOutfit = RaceData[Race].Outfit; + this->Outfit = RaceData[Race].Outfit; + this->SetID(0); + this->SetInList(); + if(!this->SetOnMap()){ + error("TMonster::TMonster: Kann Monster nicht auf die Karte setzen.\n"); + return; + } + + try{ + if(this->Master == 0 && RaceData[Race].Items > 0){ + Object Bag = Create(GetBodyContainer(this->ID, INVENTORY_BAG), + GetSpecialObject(DEFAULT_CONTAINER), + 0); + for(int i = 0; i < RaceData[Race].Items; i += 1){ + TItemData *ItemData = RaceData[Race].Item.at(i); + if(random(0, 999) > ItemData->Probability){ + continue; + } + + ObjectType ItemType = ItemData->Type; + int Amount = random(1, ItemData->Maximum); + int Repeat = 1; + if(!ItemType.getFlag(CUMULATIVE)){ + Repeat = Amount; + Amount = 0; + } + + for(int j = 0; j < Repeat; j += 1){ + // TODO(fusion): Same as `ProcessMonsterRaids`. + Object Item = NONE; + try{ + if(ItemType.getFlag(WEAPON) + || ItemType.getFlag(SHIELD) + || ItemType.getFlag(BOW) + || ItemType.getFlag(THROW) + || ItemType.getFlag(WAND) + || ItemType.getFlag(WEAROUT) + || ItemType.getFlag(EXPIRE) + || ItemType.getFlag(EXPIRESTOP)){ + Item = Create(Bag, ItemType, 0); + }else{ + Item = CreateAtCreature(this->ID, ItemType, Amount); + } + }catch(RESULT r){ + error("TMonster::TMonster: Exception %d bei Rasse %d, ggf." + " CarryStrength erhöhen.\n", r, Race); + break; + } + + // NOTE(fusion): Prevent items from being dropped onto the map. + if(Item.getContainer().getObjectType().isMapContainer()){ + error("TMonster::TMonster: Objekt fällt auf die Karte." + " CarryStrength für Rasse %d erhöhen.\n", Race); + Delete(Item, -1); + } + } + } + + // NOTE(fusion): `Bag` could be empty if we failed to add any + // items to it in the loop above. + if(GetFirstContainerObject(Bag) == NONE){ + Delete(Bag, -1); + } + } + }catch(RESULT r){ + error("TMonster::TMonster: Exception %d bei Rasse %d.\n", r, Race); + } + + this->Combat.CheckCombatValues(); + this->Combat.SetAttackMode(ATTACK_MODE_BALANCED); + this->ToDoYield(); +} + +TMonster::~TMonster(void){ + if(!this->IsDead){ + GraphicalEffect(this->posx, this->posy, this->posz, EFFECT_POFF); + }else if(this->Master == 0){ + this->Combat.DistributeExperiencePoints(RaceData[this->Race].ExperiencePoints); + } + + if(this->Master != 0){ + TCreature *Master = GetCreature(this->Master); + if(Master != NULL && Master->SummonedCreatures > 0){ + Master->SummonedCreatures -= 1; + } + } + + if(this->Home != 0){ + NotifyMonsterhomeOfDeath(this->Home); + } +} |
