/* * psnpcdialog.cpp * * Copyright (C) 2001 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 "psstdint.h" //CS includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //CEL includes // #include #include #include //#include // PS Includes #include "util/strutil.h" #include "util/psstring.h" #include "util/serverconsole.h" #include "psnpcdialog.h" #include "util/log.h" #include "../iserver/idal.h" #include "dictionary.h" #include "../psserver.h" #include "util/psdatabase.h" #include "../gem.h" #include "../globals.h" #include "../playergroup.h" #include "psraceinfo.h" //---------------------------------------------------------------------------- void NpcTriggerSentence::AddToSentence(NpcTerm *next_word) { assert(next_word!=NULL); terms.Push(next_word); str=""; } const csString& NpcTriggerSentence::GetString() { if (str.Length()) return str; for (size_t i=0; iterm); if (i= terms.Length()) return false; str = ""; return terms[which]->GetInterleavedHypernym(depth); } //---------------------------------------------------------------------------- psNPCDialog::psNPCDialog(gemNPC *npc) { db = NULL; randomgen = NULL; self = npc; } psNPCDialog::~psNPCDialog() { } bool psNPCDialog::Initialize( iDataConnection *db ) { this->db = db; if (!dict) { dict = new NPCDialogDict; if (!dict->Initialize(db)) { delete dict; dict=NULL; return false; } } return true; } bool psNPCDialog::Initialize(iDataConnection *db,int NPCID) { randomgen = psserver->rng; this->db = db; // Initialize base dictionary if (!dict) { dict = new NPCDialogDict; if (!dict->Initialize(db)) { delete dict; dict=NULL; return false; } } else dict->IncRef(); return LoadKnowledgeAreas(NPCID); } bool psNPCDialog::LoadKnowledgeAreas(int NPCID) { Result result(db->Select("select area,priority" " from npc_knowledge_areas" " where player_id=%d",NPCID)); if (!result.IsValid() ) { Error1("Cannot load knowledge areas into dictionary from database."); return false; } for (unsigned int i=0; iarea = result[i]["area"]; newarea->priority = result[i].GetInt("priority"); knowareas.Insert(newarea,TREE_OWNS_DATA); // TRUE means tree owns data } return true; } bool psNPCDialog::CheckPronouns(psString& text) { int wordnum=1; psString word("temp"); while (word.Length()) { word = GetWordNumber(text,wordnum++); if (word == "him" || word=="he") { if (!antecedent_him.IsEmpty()) text.ReplaceSubString(word,antecedent_him); else { return false; } } if (word == "her" || word=="she") { if (!antecedent_her.IsEmpty()) text.ReplaceSubString(word,antecedent_her); else { return false; } } if (word == "them" || word=="they") { if (!antecedent_them.IsEmpty()) text.ReplaceSubString(word,antecedent_them); else { return false; } } if (word == "it") { if (!antecedent_them.IsEmpty()) text.ReplaceSubString(word,antecedent_them); else { return false; } } } return true; } void psNPCDialog::CleanPunctuation(psString& str) { for (unsigned int i=0; iGetEntity()->GetName(); int num = 1; while (wordInName.Length()) { wordInName = GetWordNumber(npc_name,num); if (!wordInName.Length()) break; int pos = (int)str.FindSubString(wordInName,0,XML_CASE_INSENSITIVE); if (pos != -1) { str.DeleteAt(pos,wordInName.Length()); } num++; } } void psNPCDialog::FilterKnownTerms(const psString & text, NpcTriggerSentence &trigger) { const size_t MAX_SENTENCE_LENGTH = 4; NpcTerm* term; WordArray words(text); size_t numWordsInPhrase = words.GetCount(); size_t firstWord=0; csString candidate; if (!dict) // Pointless to try if no dictionary loaded. return; Debug2(LOG_NPC, 0,"Recognizing phrases in '%s'", text.GetData()); while (firstWordFindTermOrSynonym(candidate); if (term) { trigger.AddToSentence(term); firstWord += numWordsInPhrase; numWordsInPhrase = words.GetCount() - firstWord; } else if (numWordsInPhrase > 1) numWordsInPhrase--; else { firstWord ++; numWordsInPhrase = (int)words.GetCount() - firstWord; } } Debug2(LOG_NPC, 0,"Phrases recognized: '%s'", trigger.GetString().GetData()); } void psNPCDialog::AddBadText(const char *text, const char *trigger) { csString escText,escTrigger; csString escName; csString escSelfName; db->Escape( escText, text ); db->Escape( escName, currentClient->GetName() ); db->Escape( escSelfName, self->GetEntity()->GetName() ); db->Escape( escTrigger, trigger); int ret = db->Command("insert into npc_bad_text " "(badtext,triggertext,player,npc,occurred) " "values ('%s','%s','%s','%s',Now() )", escText.GetData(), escTrigger.GetData(), escName.GetData(), escSelfName.GetData()); if (ret == QUERY_FAILED) { Error2("Inserting npc_bad_text failed: %s",db->GetLastError() ); } } NpcResponse *psNPCDialog::FindResponse(csString& trigger,const char *text) { BinaryRBIterator loop(&knowareas); KnowledgeArea *area; NpcResponse *resp = NULL; csString trigger_error; if(trigger.GetData() == NULL) return NULL; trigger.Downcase(); trigger_error = trigger; trigger_error.Append(" error"); for (area = loop.First(); area; area = ++loop) { Debug4(LOG_NPC, 0,"NPC checking %s for trigger %s , with lastResponseID %d...", (const char *)area->area,(const char *)trigger,currentClient->GetLastResponse()); resp = dict->FindResponse(self, area->area,trigger,0,currentClient->GetLastResponse(),currentClient); if (!resp) // If no response found, try search for error trigger { resp = dict->FindResponse(self, area->area,trigger_error,0,currentClient->GetLastResponse(),currentClient); if (!resp) // If no respons found, try search without last response { if (currentClient->GetLastResponse() == -1) { // No point testing without last response // if last response where no last response. continue; } resp = dict->FindResponse(self, area->area,trigger,0,-1,currentClient); if (!resp) // If no respons found, try search for error trigger without last response { resp = dict->FindResponse(self, area->area,trigger_error,0,-1,currentClient); if (resp) { // Force setting of type Error if error trigger found resp->type = NpcResponse::ERROR_RESPONSE; } } } else { // Force setting of type Error if error trigger found resp->type = NpcResponse::ERROR_RESPONSE; } } if (resp) { Debug3(LOG_NPC, 0,"Found response %d: %s",resp->id,resp->GetResponse()); UpdateAntecedents(resp); break; } } return resp; } void psNPCDialog::SubstituteKeywords(Client * player, csString& resp) const { psString dollarsign("$"),response(resp); int where = response.FindSubString(dollarsign); while (where!=-1) { psString word2,word; response.GetWord(where+1,word2,psString::NO_PUNCT); word = "$"; word.Append(word2); // include $sign in subst. if (strcmp(word.GetData(),"$playername")==0) { if (!response.ReplaceSubString(word,player->GetName())) { Error4("Failed to replace substring %s in %s with %s",word.GetData(),response.GetData(),player->GetName()); } } else if (strcmp(word.GetData(),"$playerrace")==0) { if (!response.ReplaceSubString(word, player->GetCharacterData()->raceinfo->name)) { Error4("Failed to replace substring %s in %s with %s",word.GetData(),response.GetData(),player->GetName()); } } else if (strcmp(word.GetData(),"$sir")==0) { const char* sir; if ( player->GetCharacterData()->raceinfo->gender == PSCHARACTER_GENDER_FEMALE ) sir = "Madam"; else sir = "Sir"; if (!response.ReplaceSubString(word,sir)) { Error4("Failed to replace substring %s in %s with %s",word.GetData(),response.GetData(),player->GetName()); } } where = response.FindSubString(dollarsign,where+1); } resp = response; } NpcResponse *psNPCDialog::FindOrGeneralizeTrigger(Client *client,NpcTriggerSentence& trigger, const csArray& gen_terms) { NpcResponse *resp; csStringArray generalized; bool hit; // Perform breadth-first search on generalisations // Copy all terms into stringarray for(size_t i=0;iterm); // We do at least one search with no generalisations size_t depth = 0; do { hit = false; for(size_t i=0;iGetInterleavedHypernym(depth); if(hypernym) { generalized.Put(gen_terms[i],hypernym); hit = true; } csString generalized_copy; // Merge string for(size_t i=0;iCheckForTriggerGroup(generalized_copy); // substitute master trigger if this is child trigger in group resp = FindResponse( generalized_copy, trigger.GetString()); if (resp) { Debug2(LOG_NPC, 0,"Found response to: '%s'", generalized_copy.GetData()); // // Removed till we find a better way to manage repeated responses // // At the moment are annoying since you cannot restart the conversation from // // a certain point // int times; // csTicks when; // if (dialogHistory.EverSaid(client->GetPlayerID(), resp->id, when, times)) // { // return RepeatedResponse(trigger.GetString(), resp, when, times); // } // else // dialogHistory.AddToHistory(client->GetPlayerID(), resp->id, csGetTicks() ); return resp; // Found what we are looking for } } depth++; } while (hit == true); return NULL; #ifdef USE_OLD_SERACH // This code was replaced with the code abou at 7th dec. 2005. NpcResponse *resp; csString generalized_copy = generalized.GetString(); dict->CheckForTriggerGroup(generalized_copy); // substitute master trigger if this is child trigger in group resp = FindResponse( generalized_copy, trigger.GetString()); if (resp != NULL) { Debug2(LOG_NPC, 0, "Found response to: '%s'", generalized.GetString().GetData()); // // Removed till we find a better way to manage repeated responses // // At the moment are annoying since you cannot restart the conversation from // // a certain point // int times; // csTicks when; // if (dialogHistory.EverSaid(client->GetPlayerID(), resp->id, when, times)) // { // return RepeatedResponse(trigger.GetString(), resp, when, times); // } // else // dialogHistory.AddToHistory(client->GetPlayerID(), resp->id, csGetTicks() ); return resp; } else // Try a further generalizations at lower levels, then this level { if (level < trigger.TermLength()-1) { resp = FindOrGeneralizeTrigger(client,trigger,generalized,level+1); if (resp) return resp; } // Reset later words back to original so re-search covers all again for (int i=(int)level+1; i<(int)trigger.TermLength(); i++) generalized.Term(i) = trigger.Term(i); // Attempt to move up a level with this term bool found_more_general = generalized.GeneralizeTerm(dict,level); if (!found_more_general) // no generalization possible, so give up at this level return NULL; else return FindOrGeneralizeTrigger(client,trigger,generalized,level); } #endif } NpcResponse *psNPCDialog::Respond(const char * text,Client *client) { NpcResponse *resp; NpcTriggerSentence trigger,generalized; psString pstext(text); currentplayer = client->GetActor(); currentClient = client; // Removes everything except alphanumeric character and spaces // Removes the NPC name CleanPunctuation(pstext); // Replace him/he,her/she,them/they,it with the stored // antecedent if (!CheckPronouns(pstext)) { Debug2(LOG_NPC, 0,"Failed pronouns check for \"%s\"",pstext.GetDataSafe()); return ErrorResponse(pstext,"(none)"); } // Replace custom known terms to get standard terms // eg. hello is replaced with gretting FilterKnownTerms(pstext, trigger); if (trigger.TermLength() == 0) { Debug1(LOG_NPC, 0,"Failed filter known terms check"); return ErrorResponse(pstext,"(no known words)"); } csString copy; copy = trigger.GetString(); dict->CheckForTriggerGroup(copy); // substitute master trigger if this is child trigger in group resp = FindResponse(copy, trigger.GetString()); if (resp) { Debug2(LOG_NPC, 0,"Found response to: '%s'", copy.GetData()); // // Removed till we find a better way to manage repeated responses // // At the moment are annoying since you cannot restart the conversation from // // a certain point // int times; // csTicks when; // if (dialogHistory.EverSaid(client->GetPlayerID(), resp->id, when, times)) // { // return RepeatedResponse(trigger.GetString(), resp, when, times); // } // else // dialogHistory.AddToHistory(client->GetPlayerID(), resp->id, csGetTicks() ); Debug3(LOG_NPC, 0,"Setting last response %d: %s",resp->id,resp->GetResponse()); currentClient->SetLastResponse(resp->id); return resp; // Found what we are looking for } else { Debug1(LOG_NPC, 0,"No response found"); return ErrorResponse(pstext,trigger.GetString() ); } } NpcResponse *psNPCDialog::FindXMLResponse(Client *client, csString trigger) { if(!client) return NULL; currentplayer = client->GetActor(); currentClient = client; return FindResponse(trigger, trigger.GetDataSafe()); } NpcResponse *psNPCDialog::RepeatedResponse(const psString& text, NpcResponse *resp, csTicks when, int times) { const char *time_description; if (when < 30000) // 30 seconds time_description = "just now"; else if (when < 300000) // 5 minutes time_description = "recently"; else time_description = "already"; csString key; key.Format("repeat %s %d",time_description,times); resp = FindResponse(key,text); if (!resp) { key.Format("repeat %s",time_description); resp = FindResponse(key,text); if (!resp) { key = "repeat"; resp = FindResponse(key,text); } } return resp; } NpcResponse *psNPCDialog::ErrorResponse(const psString & text, const char *trigger) { AddBadText(text,trigger); psString error("error"); NpcResponse * resp = FindResponse(error,text); return resp; } void psNPCDialog::UpdateAntecedents(NpcResponse *resp) { // later this will need to be kept on a per player basis // but for now, its just one set if (resp->her.Length()) antecedent_her = resp->her; if (resp->him.Length()) antecedent_him = resp->him; if (resp->it.Length()) antecedent_it = resp->it; if (resp->them.Length()) antecedent_them = resp->them; } bool psNPCDialog::AddWord(const char *) { return false; } bool psNPCDialog::AddSynonym(const char *,const char *) { return false; } bool psNPCDialog::AddKnowledgeArea(const char *) { return false; } bool psNPCDialog::AddResponse(const char *area,const char *words,const char *response,const char *minfaction) { (void) area; (void) words; (void) response; (void) minfaction; return false; } bool psNPCDialog::AssignNPCArea(const char *npcname,const char *areaname) { (void) npcname; (void) areaname; return false; } void psNPCDialog::AddNewTrigger( int databaseID ) { if ( !dict ) return; else dict->AddTrigger( db, databaseID ); } void psNPCDialog::AddNewResponse ( int databaseID ) { if ( !dict ) return; else dict->AddResponse( db, databaseID ); } void DialogHistory::AddToHistory(int playerID, int responseID, csTicks when) { DialogHistoryEntry entry; entry.playerID = playerID; entry.responseID= responseID; entry.when = when; if (history.Length() < MAX_HISTORY_LEN) history.Push(entry); else { history.Put(counter,entry); counter++; if (counter == MAX_HISTORY_LEN) counter=0; } } bool DialogHistory::EverSaid(int playerID, int responseID, csTicks& howLongAgo, int& times) { times = 0; howLongAgo = 0; for (size_t i=0; i howLongAgo) howLongAgo = history[i].when; } } if (howLongAgo) howLongAgo = csGetTicks() - howLongAgo; // need the delta here return (times > 0); }