/* * questmanager.cpp * * Copyright (C) 2003 Atomic Blue (info@planeshift.it, http://www.atomicblue.org) * * * 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 (version 2 of the License) * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * */ #include #include #include #include "util/serverconsole.h" #include "util/log.h" #include "questmanager.h" #include "gem.h" #include "playergroup.h" #include "cachemanager.h" #include "netmanager.h" #include "psserver.h" #include "globals.h" #include "client.h" #include "events.h" #include "entitymanager.h" #include "util/psstring.h" #include "util/strutil.h" #include "util/psxmlparser.h" #include "util/psdatabase.h" #include "bulkobjects/pscharacterloader.h" #include "bulkobjects/psitem.h" #include "bulkobjects/dictionary.h" QuestManager::QuestManager() { psserver->GetEventManager()->Subscribe(this,MSGTYPE_QUESTINFO,REQUIRE_READY_CLIENT); psserver->GetEventManager()->Subscribe(this,MSGTYPE_QUESTREWARD,REQUIRE_READY_CLIENT|REQUIRE_ALIVE); } QuestManager::~QuestManager() { psserver->GetEventManager()->Unsubscribe(this,MSGTYPE_QUESTINFO); psserver->GetEventManager()->Unsubscribe(this,MSGTYPE_QUESTREWARD); if (dict) delete dict; } bool QuestManager::Initialize() { if (!dict) { dict = new NPCDialogDict; if (!dict->Initialize(db)) { delete dict; dict=NULL; return false; } } return LoadQuestScripts(); } bool QuestManager::LoadQuestScripts() { // Load quest scripts from database. Result quests(db->Select("SELECT * from quest_scripts order by quest_id")); if (quests.IsValid()) { int i,count=quests.Count(); for (i=0; iSelect("SELECT * from quest_scripts where quest_id=%d", id)); if (quests.IsValid()) { int i,count=quests.Count(); for (i=0; i", mainQuest->GetName(),block.GetData()); return false; } // When parsing responses, this tracks which one goes with which which_trigger = 0; for (size_t i=0; i! Failing.",block.GetData() ); return false; } if (pending_triggers.Length() == 0 || which_trigger >= pending_triggers.Length() ) { Error2("Found response <%s> without a preceding trigger to match it.",response_text.GetData() ); return false; } Debug4( LOG_QUESTS, 0,"NPC %s responds with '%s', or the error " "response '%s'", current_npc.GetData(), response_text.GetData(), error_text.GetData() ); // Now add this response to the npc dialog dict if (which_trigger == 0) // new sequence next_to_last_response_id = last_response_id; last_response = AddResponse(current_npc,response_text,last_response_id,quest,him,her,it,them); if (last_response) { bool ret = AddTrigger(current_npc,pending_triggers[which_trigger++],next_to_last_response_id,last_response_id, quest); if (!ret) return false; // Append prerequisites for this trigger. // First is the quest needed to be assigned or not csString op = "
";
                csString post = "
"; if (substep_requireop || response_requireop) { op.Append(""); post = ""; } if (quest_assigned_already) { // If quest has been assigned in the script, we need to have every response // verify that the quest have been assigned. op.AppendFmt("", mainQuest->GetName()); } else { // If quest has not been assigned in the script, we need to have every response // verify that the quest have not been assigned. op.AppendFmt("", mainQuest->GetName()); } if (substep_requireop) { op.Append(substep_requireop); } if (response_requireop) { op.Append(response_requireop); // Response require op only valid for one response. response_requireop.Free(); } op.Append(post); // Will insert the new op in and new "and" list if not pressent allready in the script. if (!last_response->ParsePrerequisiteScript(op,true)) // Insert at start of list { Error2("Could not append '%s' to prerequisite script!",op.GetData()); return false; } else { Debug2( LOG_QUESTS, 0,"Parsed %s successfully.",op.GetData() ); } } else return false; // Add response for error condition. if (error_text != "") { int error_response_id = 0; error_response = AddResponse(current_npc,error_text,error_response_id,quest,him,her,it,them); if (error_response) { error_response->quest = NULL; // Force quest to NULL, to prevent available checks only prerequisite tests. csString error_trigger = pending_triggers[(which_trigger-1)]; error_trigger.Append(" error"); bool ret = AddTrigger(current_npc,error_trigger,next_to_last_response_id,error_response_id, quest); if (!ret) return false; } else return false; } } else if (!strncmp(block,"Player ",7)) // player does something { WordArray words(block); if (words[1] == "gives") { which_trigger = 0; int numwords = GetNPCFromBlock(words,current_npc); if (numwords==-1) { Error3("NPC '%s' is not present in db, but used in %s!",words[2].GetData(),block.GetData()); } csString itemlist; if (ParseItemList(words,2+numwords,itemlist)) { pending_triggers.Empty(); pending_triggers.Push(itemlist); // next response will use this itemlist } else return false; } else { Error2("Unknown Player function in '%s' !",block.GetData()); return false; } } else if (!strncmp(block,"...",3)) // New substep. Syntax: "... [NoRepeat]" { WordArray words(block); // generate a sub step quest for the next block step_count++; // increment substep count csString newquestname; newquestname.Format("%s Step %d",mainQuest->GetName(),step_count); Debug2( LOG_QUESTS, 0,"Quest <%s> is getting added dynamically.",newquestname.GetData()); quest = CacheManager::GetSingleton().AddDynamicQuest(newquestname, mainQuest, step_count); quest_id = quest->GetID(); next_to_last_response_id = last_response_id = -1; substep_requireop.Free(); // Check if this is a non repeatable substep. // Note: NoRepeat can either be an option to ... or a separate command. if (words[1] == "NoRepeat") { substep_requireop.AppendFmt("", quest->GetName() ); } } else // command { Debug2( LOG_QUESTS, 0,"Got command '%s'", block.GetData() ); csString op; csString previous; block.Trim(); // Check for multiple commands separated by dots csArray commands; // Loop through and find all dot separated commands. size_t dot = block.Find("."); size_t l = block.Length(); while (dot != SIZET_NOT_FOUND && dot+1 < l) { csString first = block.Slice(0,block.Find(".")); commands.Push(first.Trim()); block = block.Slice(block.Find(".")+1,block.Length()); dot = block.Find("."); } // If there are more last command didn't have a dot // make sure we include that command to. if (block.Length()) commands.Push(block.Trim()); for (size_t i = 0 ; i < commands.Length () ; i++) { block = commands.Get(i); // Take off trailing dots(.) if (block[block.Length()-1] == '.') { block.DeleteAt(block.Length()-1); } if (!strncmp(block,"Assign Quest",12)) { op.Format("",mainQuest->GetName()); quest_assigned_already = true; } else if (!strncmp(block,"Complete",8)) { csString questname = block.Slice(8,block.Length()-1).Trim(); op.Format("",questname.GetData()); } else if (!strncmp(block,"Give",4)) { WordArray words(block); if (words.GetInt(1) != 0 && words.Get(2).CompareNoCase("tria")) // give tria money { op.Format("",words.GetInt(1) ); } else if (words.GetInt(1) != 0 && words.Get(2).CompareNoCase("hexa")) // give hexa money { op.Format("",words.GetInt(1) ); } else if (words.GetInt(1) != 0 && words.Get(2).CompareNoCase("octa")) // give octa money { op.Format("",words.GetInt(1) ); } else if (words.GetInt(1) != 0 && words.Get(2).CompareNoCase("circle")) // give circle money { op.Format("",words.GetInt(1) ); } else if (words.GetInt(1) != 0 && words.Get(2).CompareNoCase("exp")) // give experience points { op.Format("",words.GetInt(1) ); } else { if (words.FindStr("or") != SIZET_NOT_FOUND) { op.Format(""); size_t start = 1,end; while (start < words.GetCount() ) { end = words.FindStr("or",(int)start); if (end == SIZET_NOT_FOUND) end = words.GetCount(); csString item = words.GetWords(start,end); op.AppendFmt("",item.GetData() ); start = end+1; } op.Append(""); } else { csString count; int item_start = 1; if (words.GetInt(1) != 0) { count.Format("count=\"%d\" ",words.GetInt(1)); item_start+=1; } op.Format("",words.GetTail(item_start).GetData(),count.GetDataSafe() ); } } } else if (!strncmp(block,"Require completion of",21)) { csString questname = block.Slice(21,block.Length()-1).Trim(); response_requireop.AppendFmt("", questname.GetData() ); } else if (!strncmp(block,"NoRepeat",21)) { substep_requireop.AppendFmt("", quest->GetName() ); } else if (!strncmp(block,"Run script",10)) { csString script = block.Slice(10,block.Length()-1).Trim(); // Find params if any, up to 3 csString param[3]; int p = 0; size_t start = script.FindStr("("); size_t end = script.FindStr(")"); if (start != SIZET_NOT_FOUND && end != SIZET_NOT_FOUND && start == 0 && end > start) { csString params = script.Slice(start+1,end-start-1).Trim(); script.DeleteAt(start,end-start+1).Trim(); size_t next; do { next = params.FindStr(","); if (next == SIZET_NOT_FOUND) { param[p] = params.Trim(); } else { param[p] = params.Slice(0,next).Trim(); params.DeleteAt(0,next+1); } p++; } while (next != SIZET_NOT_FOUND && p < 3); } // Build the op op.Format(""); } else // unknown block { Error2("Unknown command '%s' !",block.GetData()); return false; } previous.Append(op); op.Empty(); // Don't want to include the same op multiple times. } // end for() commands op = ""; op.Append(previous); op.Append(""); // add script to last response if (!last_response->ParseResponseScript(op)) { Error2("Could not append '%s' to response script!",op.GetData()); return false; } else { Debug2( LOG_QUESTS, 0,"Parsed successfully and added to last response: %s .", op.GetData() ); } } } if (quest_assigned_already && last_response) { // Make sure the quest is 'completed' at the end of the script. csString op; op.Format("",mainQuest->GetName()); if (!last_response->ParseResponseScript(op)) { Error2("Could not append '%s' to response script!",op.GetData()); return false; } else { Debug2( LOG_QUESTS, 0,"Parsed %s successfully.", op.GetData() ); } } else { Error2("Quest script <%s> never assigned a quest or had any responses.",mainQuest->GetName()); return false; } return true; } int QuestManager::GetNPCFromBlock(WordArray words,csString& current_npc) { csString select; // First check single name: "Player gives Sharven ..." csString first = words.Get(2); select.Format ("SELECT * from characters where name='%s' and lastname='' and npc_master_id!=0",first.GetData() ); // check if NPC exists Result npc_db(db->Select(select)); if (npc_db.IsValid() && npc_db.Count()>0) { current_npc = first; return 1; } else // Than check double name: "Player gives Menlil Toresun ..." { csString last = words.Get(3); select.Format("SELECT * from characters where name='%s' and lastname='%s' and npc_master_id!=0",first.GetData(),last.GetData()); Result npc_db(db->Select(select)); if (npc_db.IsValid() && npc_db.Count()>0) { current_npc.Format("%s %s",first.GetData(),last.GetData() ); return 2; } } return -1; } void QuestManager::FormatItem(csString& itemlist,size_t count, csString& item_name) { item_name.Downcase(); // Required format is like: if (item_name == "circle" || item_name == "circles") { itemlist.Format("", count); } else if (item_name == "octa" || item_name == "octas") { itemlist.Format("", count); } else if (item_name == "hexa" || item_name == "hexas") { itemlist.Format("", count); } else if (item_name == "tria" || item_name == "trias") { itemlist.Format("", count); } else { if (!itemlist.Length()) itemlist = ""; itemlist.AppendFmt("",item_name.GetData(),count); } } bool QuestManager::ParseItemList(WordArray& words,size_t startWord,csString& itemlist) { size_t i=startWord; size_t count=1; csString item_name; itemlist.Clear(); while (i"); Debug2( LOG_QUESTS, 0,"Item list parsing created this: %s", itemlist.GetData() ); return true; } bool QuestManager::BuildTriggerList(csString& block,csStringArray& list) { size_t start=0,end; csString response; while (start < block.Length()) { start = block.Find("P:",start); if (start == SIZET_NOT_FOUND) return true; start += 2; // skip the actual P: // Now find next P:, if any end = block.Find("P:",start); if (end == SIZET_NOT_FOUND) end = block.Length(); block.SubString(response,start,end-start); response.Trim(); if (response[response.Length()-1] == '.') // take off trailing . { response.DeleteAt(response.Length()-1); } // This isn't truely a "any trigger" but will work if (response == "*") response = "error"; list.Push(response); start = end; // Start at next P: or exit loop } return true; } void CutOutParenthesis(csString &response, csString &within,char start_char,char end_char) { // now look for error msg in parenthesis size_t start = response.FindLast(start_char); if (start != SIZET_NOT_FOUND) { size_t end = response.FindLast(end_char); if (end != SIZET_NOT_FOUND && end > start) { response.SubString(within,start+1,end-start-1); within.Trim(); response.DeleteAt(start,end-start+1); // cut out parenthesis. } } else { within.Clear(); } } bool QuestManager::GetResponseText(csString& block,csString& response,csString& error, csString& him, csString& her, csString& it, csString& them) { size_t start; csString pron; start = block.FindFirst(':'); if (start == SIZET_NOT_FOUND) return false; start++; // skip colon block.SubString(response,start,block.Length()-start); CutOutParenthesis(response,error,'(',')'); CutOutParenthesis(response,pron,'{','}'); him = ""; her = ""; it = ""; them = ""; if (pron.Length()) { csArray prons = psSplit(pron,','); for (size_t i = 0; i < prons.Length(); i++) { csArray tmp = psSplit(prons[i],':'); if (tmp.Length() == 2) { if (tmp[0] == "him" || tmp[0] == "he") him = tmp[1]; if (tmp[0] == "her" || tmp[0] == "she") her = tmp[1]; if (tmp[0] == "it") it = tmp[1]; if (tmp[0] == "them"|| tmp[0] == "they") them = tmp[1]; } else { Error2("Pronoun(%s) doesn't have the form pron:name", pron.GetDataSafe()); } } } response.Trim(); return true; } NpcResponse *QuestManager::AddResponse(csString& current_npc,const char *response_text,int& last_response_id, psQuest * quest, csString him, csString her, csString it, csString them) { last_response_id = 0; // let AddResponse autoset this if set to 0 Debug2( LOG_QUESTS, 0,"Adding response %s to dictionary...", response_text ); return dict->AddResponse(response_text,him,her,it,them,last_response_id,quest); } bool QuestManager::AddTrigger(csString& current_npc,const char *trigger,int prior_response_id,int trig_response, psQuest * quest) { // search for multiple triggers csString temp(trigger); temp.Downcase(); csArray array = psSplit(temp,'.'); bool result = false; for (size_t i=0;iAddTrigger(current_npc,new_trigger,prior_response_id,trig_response); quest->AddTriggerResponse(npcTrigger, trig_response); if(!npcTrigger) return false; else result = true; } return result; } void QuestManager::HandleMessage(MsgEntry *me,Client *who) { if (me->GetType() == MSGTYPE_QUESTINFO) { psQuestInfoMessage msg(me); QuestAssignment *q = who->GetActor()->GetCharacterData()->IsQuestAssigned(msg.id); if (q) { if (msg.command == psQuestInfoMessage::CMD_DISCARD) { // Discard quest assignment on request who->GetActor()->GetCharacterData()->DiscardQuest(q); } else { psQuestInfoMessage response(me->clientnum,psQuestInfoMessage::CMD_INFO, q->quest->GetID(),q->quest->GetName(),q->quest->GetTask()); response.SendMessage(); } } else { if (msg.command == psQuestInfoMessage::CMD_DISCARD) { Error3("Client %s requested discard of unassigned quest id #%u!",who->GetName(),msg.id); } else { Error3("Client %s requested unassigned quest id #%u!",who->GetName(),msg.id); } } } else if (me->GetType() == MSGTYPE_QUESTREWARD) { psQuestRewardMessage msg(me); if (msg.msgType==psQuestRewardMessage::selectReward) { // verify that this item was really offered to the client as a // possible reward for (size_t z=0;zclientID==me->clientnum) { for (size_t x=0;xitems.Length();x++) { uint32 itemID = (uint32)atoi(msg.newValue.GetData()); if (offer->items[x]->GetUID()==itemID) { // this item has indeed been offered to the client // so the item can now be given to client (player) GiveRewardToPlayer(who, offer->items[x]); // remove the offer from the list offers.DeleteIndex(z); return; } } } } } } } void QuestManager::OfferRewardsToPlayer(Client *who, csArray &offer) { csString rewardList; // create a xml string that will be used to generate the listbox (on // the client side) from which a user can select a reward rewardList=""; for (size_t x=0;xGetImageName(); csString name = offer[x]->GetName(); csString desc = offer[x]->GetDescription(); int id = offer[x]->GetUID(); psString temp; csString escpxml_image = EscpXML(image); csString escpxml_name = EscpXML(name); csString escpxml_desc = EscpXML(desc); temp.Format( "", escpxml_image.GetData(), escpxml_name.GetData(), id, escpxml_desc.GetData()); rewardList += ""; rewardList += temp; rewardList += ""; } rewardList+=""; CPrintf(CON_DEBUG, "REWARD: %s\n",rewardList.GetData()); // store the combination of client and reward offers (temporarily) QuestRewardOffer* rewardOffer = new QuestRewardOffer; rewardOffer->clientID = who->GetClientNum(); rewardOffer->items = offer; offers.Push(rewardOffer); // send a message, containing the rewardlist xml string, to the client, psQuestRewardMessage message(who->GetClientNum(), rewardList, psQuestRewardMessage::offerRewards); message.SendMessage(); } bool QuestManager::GiveRewardToPlayer(Client *who, psItemStats* itemstat) { // check for valid item if (itemstat==NULL) return false; psCharacter* chardata = who->GetActor()->GetCharacterData(); if (chardata==NULL) return false; // create the item psItem *item = itemstat->InstantiateBasicItem(); if (item==NULL) return false; csString itemName = item->GetName(); // Attempt to give, and drop on the ground if full inventory if ( chardata->MoveToInventory(item) ) { psSystemMessage given(who->GetClientNum(),MSG_INFO,"%s has received a %s!",who->GetName(),itemName.GetData()); who->GetActor()->SendGroupMessage(given.msg); } else { psserver->SendSystemInfo(who->GetClientNum(),"No space in your inventory for a %s. Item will be dropped", itemstat->GetName()); chardata->DropItem( item ); } item->SetLoaded(); // Item is fully created item->Save(); // First save // player got his reward return true; } void QuestManager::Assign(psQuest *quest, Client *who,gemNPC *assigner) { who->GetActor()->GetCharacterData()->AssignQuest(quest,assigner->GetPlayerID() ); psserver->SendSystemOK(who->GetClientNum(),"You got a quest!"); psserver->SendSystemInfo(who->GetClientNum(),"You now have the %s quest.",quest->GetName() ); // Post tutorial event psGenericEvent evt(who->GetClientNum(), psGenericEvent::QUEST_ASSIGN); evt.FireEvent(); } bool QuestManager::Complete(psQuest *quest, Client *who) { bool ret = who->GetActor()->GetCharacterData()->CompleteQuest(quest); // if it's a substep don't send additional info if (quest->GetParentQuest()) return true; if (ret) { psserver->SendSystemOK(who->GetClientNum(),"Quest Completed!"); psserver->SendSystemInfo(who->GetClientNum(),"You have completed the %s quest!", quest->GetName() ); // TOFIX: we should clean all substeps of this quest from the character_quests db table. } else { Debug3( LOG_QUESTS, who->GetAccountID(),"Cannot complete quest %s for player %d ", quest->GetName(), who->GetAccountID()); } return true; }