/* -*- LPC -*- */ /* * $Locker: $ * $Id: response_mon.c,v 1.31 2003/04/01 21:39:38 ceres Exp $ * */ /** * This file contains the methods to make the npcs in game respond * to soul and says. Allows for a level of npc interation. * <p> * Code originaly by nimmox@igor. Modified for discworld by Pinkfish. * <p> * @change Olorin Sep 97 * event_soul does a convert_message on the mess first, * to avoid spurious matches caused by a message like eg. * "$the_short:/global/lord#64387$ $V$0=smiles,smile$V$ .... in souln * @change who knows when * changed to stop the responses when the npc is in /room/rubbish * @author Pinkfish * @see /obj/monster.c */ #include <soul.h> #include <language.h> #define SPECIAL_DAY "/obj/handlers/special_day" class response_data { object last_ob; string last_mess; int stop_responding; int use_regexp; int last_count; int last_time; int understand_anything; object* only_respond_to; } mixed *_respond_to_with; nosave class response_data _response_data; int match(string str, mixed str2); void event_soul(object per, string mess, object *avoid, string verb, string last, mixed at) ; void create() { _respond_to_with = ({ }); _response_data = new(class response_data); _response_data->only_respond_to = ({ }); } /* create() */ /** * This method sets or clears the flag that allows the npc to understand * any language at all. THis is useful for those times when you need an * npc that speaks/understands any language. * @param flag the new value of the flag */ void set_response_mon_understand_anything(int flag) { _response_data->understand_anything = flag; } /** * This method sets or clears the flag to use regular expressions instead of * the traditional arrays of words. In addition, this flag inhibits * the usual stripping of non-alphanumerics from the input strings. * @param flag the new value of the flag: 1 says to use regular expressions, * 0 to not use them */ void set_response_mon_regexp(int flag) { _response_data->use_regexp = flag; } /** * This method returns the current state of the flag that indicates whether * to use regular expressions instead of the traditional array of words. * @return 1 if using regular expressions, 0 if not */ int query_response_mon_regexp() { return _response_data->use_regexp; } /** * This method checks to see if the response monster code is in * debug mode. * @return 1 if in debug mode, 0 if not * @see set_response_mon_debug_mode() */ int query_response_mon_debug_mode() { return this_object()->query_property("respon mon debug"); } /* query_response_mon_debug_mode() */ /** * This method sets the current debug flag for the response monster * code. * @param flag the new value of the flag, 1 on, 0 off * @see query_response_mon_debug_mode() */ void set_response_mon_debug_mode(int flag) { this_object()->add_property("respon mon debug", flag); } /* set_response_mon_debug_mode() */ /** * This method allows the toggling of the responses of the npc. * If this is set to 1, then the npc will stop responding to * messages. * @param i 1 to make the npc not respond, 0 to make it respond again * @see query_stop_responding() */ void set_stop_responding(int i) { _response_data->stop_responding = i; } /** * This method returns the flag determining the toggling of the responses * of the npc. * If this is set to 1, then the npc will stop responding to * messages. * @return 1 to the npc will not respond, 0 it is responding * @see query_stop_responding() */ int query_stop_responding() { return _response_data->stop_responding; } /** * This method checks to see if the response is allowed for this object. * NB: objects to which this_object is not visible are disallowed. * @param ob the object to check * @param response the response that is matched * @return 1 if the object is allowed, 0 if it is not */ int query_response_allowed(object ob, string *response) { if (!this_object()->query_visible(ob)) { return 0; } return 1; } /* query_response_allowed() */ /** * This method sets the current responses for the npc. * This will overwrite the current responses in the npc. * Please use add_respond_to_with in your NPC's instead of this function * call, as it requires prior knowledge as to the internal structure of * the respond_to_with code that add_respond_to_with handles nicely. * The array is of the formant:<pre> * ({ * trigger1, * response1, * trigger2, * response2, * ... * }) * </pre> * @see add_respond_to_with() * @see query_respond_to_with() */ void set_respond_to_with( mixed map ) { mixed *old_keys; int i; if ( mapp( map ) ) { #ifdef DEBUG log_file("BAD_RESPONSE", "set_respond_to_with() called by %O (%s)\n", previous_object(), previous_object()->query_name()); #endif old_keys = keys(map); _respond_to_with = allocate(sizeof(old_keys) * 2); for (i = sizeof(old_keys); i--; ) { _respond_to_with[2*i] = old_keys[i]; _respond_to_with[2*i+1] = map[old_keys[i]]; } return; } _respond_to_with = map; } /* set_respond_to_with() */ /** * This method returns the current responding to with array. * The array is of the formant:<pre> * ({ * trigger1, * response1, * trigger2, * response2, * ... * }) * </pre> * @see add_respond_to_with() */ mixed *query_respond_to_with() { return _respond_to_with; } /** * This method adds a respond message to respond to into the * current array of responses. * <p> * If response_mon_regexp is not set, the trigger consists of an * array of words to be matched * (in order) in the string. If there is an array instead of * a single word at one point then any of the words in the * array will be matched. If response_mon_regexp is set, the * trigger is a single string, representing the regular expression * to be matched in the string. * <p> * The response is either an array of things to execute * (in which case a random one will be chosen each time) or * a function pointer or a string. If it is a string then * the command of that name will be executed, if the * string starts with a '#' then the function named after that * will be called on the npc. That was exciting wasn't it? * If the string has a $hname$ name in it and it is not a * function call (starts with a #) then the $hname$ will be replaced * with the name of the triggering object. $hcname$ or $short$ will be * replaced with the short of the triggering object. * <p> * In the case of a function call, two arguments are passed into * the function: the first is the person which triggered the effect * and the second in the message which triggered the effect. * <p> * NOTE: The NPC must explicitly know the language being spoken, even * if it's "common". I don't know why. That's just the way it is. See * add_language(). * @example * // Simple response * add_respond_to_with(({ "@say", "bing" }), "'Yes! Bing bing bing!"); * @example * // respond to someone saying 'frog' or 'toad' * add_respond_to_with(({ "@say", ({ "frog", "toad" }) }), * "'Frogs and toads are nice."); * @example * // Randomly say something or bing back at them * add_respond_to_with(({ "@say", "bing" }), * ({ "'Yes! Bing bing bing!", "bing $hname$" })); * @example * // Call the function 'rabbit' on the npc. * add_respond_to_with(({ "@say", "bing" }), "#rabbit"); * @example * // Do something cute with a function pointer * add_respond_to_with(({ "@bing" }), * (: do_command("'something wild for " + $1->a_short()) :)); * @param trigger the trigger to trigger the action on * @param response the response to the action * @see query_respond_to_with() * @see query_response_allowed() * @see set_response_mon_regexp() * @see query_response_mon_regexp() * @see regexp() * @see add_language() */ void add_respond_to_with( mixed *trigger, mixed response ) { _respond_to_with += ({ trigger, response }); } /* add_response() */ /** * This adds an object into the list of objects that we will only respond * to. If this array is empty then we will respond to anyone, if it has * something in it then we will only respond to them. * @param person the person to add to the response array */ void add_only_respond_to(object person) { _response_data->only_respond_to += ({ person }); } /* add_only_respond_to() */ /** * This method removes an object from the list of people to respond to. * @param person the person to remove */ void remove_only_respond_to(object person) { _response_data->only_respond_to -= ({ person }); } /* remove_only_respond_to() */ /** * This method returns the current list of people we are only responding * to. * @return the array of people we are responding to */ object* query_only_respond_to() { return _response_data->only_respond_to; } /* query_only_respond_to() */ /* Check_sub_sequence checks whether the array (words) contains a subsequence as specified in the respond_to_with array */ /** * This method checks the subsequence of words to see if it matches * any of our current response sets. * @param words the words which are to be tested * @return the response to be executed * @see add_respond_to_with() */ protected mixed *check_sub_sequence( mixed *words ) { int word_index; int format_index; /* what do you know....Meaningful names :-) */ int format_size; int word_size; int match_counter; int cu_format_size; string word_str; mixed data; string *patterns; string pattern; string extra; format_index = 0; word_size = sizeof( words ); word_str = implode(words, " "); format_size = sizeof( _respond_to_with ); while (format_index < format_size) { if ( _response_data->use_regexp ) { data = _respond_to_with[ format_index ]; if ( arrayp( data ) && sizeof( data ) > 1 ) { // Excellent. Now, let's add our @say data to the front of // the pattern. if ( arrayp( data[0] ) ) { // Combine the first part into the array, separated // by or's, with at least one match. extra = "(" + implode( data, "|" ) + ")+"; } else { extra = data[0]; } patterns = map( data[1], (: $(extra) + " " + $1 :) ); } else { patterns = ({ data }); } //tell_creator( "taffyd", "%O, %O\n", word_str, patterns ); // Check all of the possible responses. foreach( pattern in patterns ) { // tell_creator( "taffyd", "%O\n", pattern ); if ( regexp( word_str, pattern ) ) { return _respond_to_with[ format_index + 1 ]; } } } else { word_index = 0; match_counter = 0; cu_format_size = sizeof(_respond_to_with[format_index]); while ((word_index < word_size) && (match_counter < cu_format_size)) { match_counter += match(words[word_index], _respond_to_with[format_index][match_counter]); word_index++; } if (match_counter == (cu_format_size)) { return _respond_to_with[format_index + 1]; } } format_index += 2; } return 0; } /* check_sub_sequence() */ /* match returns 1 if str matches the format.... see also the definitions in the example file... */ /** @ignore yes */ protected int match(string str, mixed format) { if (pointerp(format)) { return (member_array(str, format) != -1); } if (str == format) { return 1; } return 0; } /* match() */ /* removing annoying readmarks.... Like .'s and ?'s */ /** * This method removes annoying read marks to make the string easier to * parse. Basically it strips puncutation. * @param str the string to remove the punctuation from * @return the string without any punctuation */ protected string remove_read_marks(string str) { int blij; string result; int size; string temp; size = strlen(str); blij = 0; result = ""; while (blij < size) { temp = str[blij..blij]; if (((temp >= "a") && (temp <= "z")) || ((temp >= "0") && (temp <= "9")) || (temp == " ")) { result += temp; } else { result += " "; } blij++; } return result; } /* remove_read_marks() */ /** * This method runs the command passed in, doing some substitution. * @param str the string to execute * @param per the person who triggered the command */ protected void senddstr(string str, object per) { if ( environment( this_object() ) ) { if ( per ) { command(replace(str, ({ "$hname$", per->query_name(), "$hcname$", per->query_short(), "$hshort$", per->query_short() }))); } else { command( str ); } } } /* sendstr() */ /* Take care not to send to other monsters.... Infinite recursion problem...*/ /* Imagine two monsters chatting with each other......*/ /** * This method executes the response to the matched string. * @param rep the response to execute * @param per the person who initiated the event * @param mess the message that was matched * @see add_respond_to_with() */ protected void exec_response(mixed rep, object per, string mess) { string *rabbit; if (pointerp(rep)) { return exec_response(rep[random(sizeof(rep))], per, mess); } if (functionp(rep)) { evaluate(rep, per, mess); } else if (stringp(rep)) { if (rep[0..0] == "#") { rabbit = explode(rep[1..], "#"); if (sizeof(rabbit) > 1) { call_out((: call_other($1, $2, $3, $4) :), 0, rabbit[0], rabbit[1], per, mess); } else { call_out(rabbit[0], 0, per, mess); } } else { call_out("senddstr", 0, rep, per); } } } /* exec_response() */ /** @ignore yes */ private void do_delay_thingy(string *extra, string mess, object per) { mixed *response; // It does lower case stuff sin... mess = lower_case(mess); if (!_response_data->use_regexp) { mess = remove_read_marks(mess); } if (query_response_mon_debug_mode()) { tell_object(per, "Parsing the text: " + implode(extra, " ") + " " + mess + "\n"); } response = check_sub_sequence(extra + explode(mess, " ")); if (query_response_mon_debug_mode()) { tell_object(per, sprintf("Responding with %O\n", response)); } if (response) { if (query_response_allowed(per, response)) { exec_response(response, per, mess); } } } /* do_delay_thingy() */ /** @ignore yes * Some new loop detection. Track the last object, message we responded * to and when. Don't respond to the same object & message more than twice in * a suitable period. * Clearly this won't detect three-way, or other complex, loops. */ int check_loop(object per, string mess) { // We'll assume players can't get into loops. :) if(interactive(per)) { return 0; } if (_response_data->last_ob == per && (_response_data->last_mess = mess) && _response_data->last_time > time() - 10) { if(_response_data->last_count > 1) { return 1; } else { _response_data->last_count++; } _response_data->last_time = time(); } else { _response_data->last_ob = per; _response_data->last_mess = mess; _response_data->last_count = 1; _response_data->last_time = time(); } return 0; } /** @ignore yes */ private void do_response(string *extra, string mess, object per) { if (sizeof(_response_data->only_respond_to)) { _response_data->only_respond_to -= ({ 0 }); if (sizeof(_response_data->only_respond_to) && member_array(per, _response_data->only_respond_to) == -1) { return ; } } call_out((: do_delay_thingy($1, $2, $3) :), 2, extra, mess, per); } /* do_response() */ /* * respond as defined in respond_to_with array...See also * the definitions in the example monster */ /** * This method is called on the npcs to help in recognising saytos. * Beware... A @say message will also be added, so you need to make sure * you respond to the correct message and not to both. */ void event_person_sayto(object per, string mess, string lang, object* targets) { string skill; if(_response_data->stop_responding || !sizeof(_respond_to_with) || environment() == find_object( "/room/rubbish" ) || environment() == find_object( "/room/void" ) || !per || per == this_object()) { return ; } if(check_loop(per, mess)) { return; } skill = LANGUAGES->query_language_spoken_skill(lang); if (this_object()->query_skill(skill) < 90 || per->query_skill(skill) < 60) { return; } do_response(({ "@sayto" }), mess, per); } /* event_person_say() */ /** @ignore yes */ void event_person_say(object per, string start, string mess, string lang) { string skill; if(_response_data->stop_responding || !sizeof(_respond_to_with) || environment() == find_object( "/room/rubbish" ) || environment() == find_object( "/room/void" ) || !per || per == this_object()) { return ; } if(check_loop(per, mess)) return; if (!_response_data->understand_anything) { skill = LANGUAGES->query_language_spoken_skill(lang); if (this_object()->query_skill(skill) < 90 || per->query_skill(skill) < 60) { return; } } do_response(({ "@say" }), mess, per); } /* event_person_say() */ /** @ignore yes */ varargs void event_soul(object per, string mess, object *avoid, string verb, string last, mixed at) { if (per == find_object(SOUL_OBJECT)) per = previous_object(2); if(!interactive(per) || _response_data->stop_responding || environment() == find_object( "/room/rubbish" ) || environment() == find_object( "/room/void" ) || member_array(this_object(), avoid) != -1 || !sizeof(_respond_to_with)) { return ; } if((!objectp(at) || (objectp(at) && at != this_object())) && environment(per) != environment() ) { return; } mess = this_object()->convert_message( mess ); if(check_loop(per, mess)) return; if (objectp(at)) { do_response(({ "@" + verb, "#" + last, at->query_name() }), mess, per); } else { do_response(({ "@" + verb, "#" + last }), mess, per); } } /* event_soul() */ /** @ignore yes */ varargs void event_whisper(object per, string mess, object *obs, string lang, object me) { string skill; if (!interactive(per) || _response_data->stop_responding || environment() == find_object( "/room/rubbish" ) || !sizeof(_respond_to_with)) { return ; } if(check_loop(per, mess)) { return; } if (!_response_data->understand_anything) { skill = LANGUAGES->query_language_spoken_skill(lang); if (this_object()->query_skill(skill) < 90 || per->query_skill(skill) < 60) { return; } } // Don't let them see the message unless it is actually directed at them. if (member_array(this_object(), obs) == -1) { do_response(({ "@whisper", map(obs, (: $1->query_name() :)) }), "", per); } else { mess = this_object()->convert_message( mess ); do_response(({ "@whisper", map(obs, (: $1->query_name() :)) }), mess, per); } }