/**
* 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 soul
* @change who knows when
* changed to stop the responses when the npc is in /room/rubbish
* @author Pinkfish
* @see /std/npc.c
*/
#include <soul.h>
#include <language.h>
#define SPECIAL_DAY "/handlers/special_day"
mixed respond_to_with;
int stop_responding;
int use_regexp;
int match( string str, mixed str2 );
void event_soul( object per, string mess, object *avoid, string verb,
string last, mixed at );
void create() {
} /* create() */
/**
* 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 ) { 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 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 TO->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 ) {
TO->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 ) { 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 stop_responding; }
/**
* This method sets whether or not we should respond to
* non-interactives (ie. other NPCs).
* @param i 1 if we should respond to other NPCs, 0 if not
*/
void set_respond_non_interactive( int i ) {
if( i )
TO->add_property("RNI", 1 );
else
TO->remove_property("RNI");
} /* set_respond_non_interactive() */
/**
* This method returns whether or not we should respond to other NPCs.
* @return 1 if we should respond to other NPCs, 0 if not
*/
int query_respond_non_interactive() { return TO->query_property("RNI"); }
/**
* This method checks to see if the response is allowed for this object.
* @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 ) {
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 ) ) {
log_file("BAD_RESPONSE", "set_respond_to_with() called by %O (%s)\n",
PO, PO->query_name() );
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.
* <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>
* Also possible with a format of "#filename:function" is calling
* functions on other objects. Those functions will be called
* with the NPC (object) as the first argument, and the person
* triggering the event as the second (object) argument.
* <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()) :));
* @example
* // Calling a function on a room for example
* add_respond_to_with(({ "@bing" }), "#"+PUB+"pub:do_bing_response");
* @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 ) {
if( !pointerp(respond_to_with) )
respond_to_with = ({ });
respond_to_with += ({ trigger, response });
} /* add_response() */
/**
* 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( string *words ) {
int word_index;
int format_index;
int format_size;
int word_size;
int match_counter;
int cu_format_size;
string word_str;
format_index = 0;
word_size = sizeof( words );
word_str = implode( words, " ");
format_size = sizeof( respond_to_with );
while( format_index < format_size ) {
if( use_regexp ) {
if( regexp( word_str, respond_to_with[format_index]) )
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() */
/**
* @ignore yes
* This method returns 1 if str matches the format...
* see also the definitions in the example file...
*/
protected int match( string str, mixed format ) {
if( pointerp(format) )
return member_array( str, format ) != -1;
if( str == format )
return 1;
return 0;
} /* match() */
/**
* This method removes annoying read marks to make the string easier to
* parse. Basically it strips punctuation.
* @param str the string to remove the punctuation from
* @return the string without any punctuation
*/
protected string remove_read_marks( string str ) {
int i, size;
string result, temp;
size = strlen(str);
result = "";
while( i < size ) {
temp = str[i..i];
if( ( temp >= "A" && temp <= "Z" ) ||
( temp >= "a" && temp <= "z" ) ||
( temp >= "0" && temp <= "9" ) || temp == " " ) {
result += temp;
} else {
result += " ";
}
i++;
}
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( ENV(TO) ) {
command( !per ? str : replace( str, ({
"$hname$", per->query_name(),
"$hcname$", per->query_cap_name()
}) ) );
}
} /* 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( !per || ( !interactive(per) && !query_respond_non_interactive() ) )
return 0;
if( !TO->query_visible(per) )
return 0;
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] == "#" ) {
if( sizeof( rabbit = explode( rep[1..], ":") ) == 2 ) {
call_out( (: call_other( $1, $2, $3, $4 ) :), 0,
rabbit[0], rabbit[1], TO, per );
return;
}
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;
mess = lower_case(mess);
if( !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 && query_response_allowed( per, response ) )
exec_response( response, per, mess );
} /* do_delay_thingy() */
/** @ignore yes */
private void do_response( string *extra, string mess, object per ) {
call_out( (: do_delay_thingy( $1, $2, $3 ) :), 2, extra, mess, per );
} /* do_response() */
/** @ignore yes */
void event_person_say( object per, string start, string mess, string lang ) {
string skill;
if( !per || per == TO || stop_responding || !sizeof(respond_to_with) ||
( !interactive(per) && !query_respond_non_interactive() ) )
return;
if( ENV(TO) == find_object("/room/rubbish") ||
ENV(TO) == find_object("/room/void") )
return;
/*
* I know this is a hack, if you can do better, do so,
* don't just take it out or things will break come womans day.
*/
skill = LANGUAGES->query_language_spoken_skill(lang);
if( TO->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( !per || per == TO || stop_responding || !sizeof(respond_to_with) ||
( !interactive(per) && !query_respond_non_interactive() ) )
return;
if( ENV(TO) == find_object("/room/rubbish") ||
ENV(TO) == find_object("/room/void") ||
member_array( TO, avoid ) != -1 )
return;
if( ( !objectp(at) || ( objectp(at) && at != TO ) ) &&
ENV(per) != ENV(TO) )
return;
mess = TO->convert_message( mess );
do_response( ( objectp(at) ? ({ "@" + verb, "#"+last, at->query_name() }) :
({ "@" + verb, "#"+last }) ), mess, per );
} /* event_soul() */
/** @ignore yes */
varargs void event_whisper( object per, string mess, object *obs, string lang,
object me ) {
if( !per || per == TO || stop_responding || !sizeof(respond_to_with) ||
( !interactive(per) && !query_respond_non_interactive() ) )
return;
if( ENV(TO) == find_object("/room/rubbish") ||
ENV(TO) == find_object("/room/void") )
return;
// Don't let them see the message unless it is actually directed at them.
if( member_array( TO, obs ) == -1 ) {
do_response( ({"@whisper"}) + map( obs, (: $1->query_name() :) ),
"", per );
} else {
mess = TO->convert_message( mess );
do_response( ({"@whisper"}) + map( obs, (: $1->query_name() :) ),
mess, per );
}
} /* event_whisper() */