/**
* This is the group handler. It does all the handling of
* player-run adventuring groups, but not alone. It's
* accompanied by the group commands, effect and shadow.
* For paths to these files, consult /include/group_handler.h
*
* This system owes its design first and foremost to the irreplaceable
* Ceres who wrote the original system. Next come all the valuable
* Creators who shared their knowledge of LPC. Last but not least
* are the countless playtesters who dedicated their time and worked
* out all (or most of) the kinks and provided feedback and suggestions.
*
* So give 'em all a big hug.
*
* @author Tape
* @started February 99
*/
#include <group_handler.h>
#include <broadcaster.h>
#include <login_handler.h>
int _loaded;
int _groups_formed;
mapping _groups;
class group
{
int start_time; // Time() when group was created
string short; // Group's short description
string leader_name; // Group's leader's name
object leader; // Group's leader
object *members; // Group's members
object *invited; // People invited to group
}
int is_group( string group );
int is_member( string name, object person );
int is_invited( string name, object person );
object *invitations_to( string name );
string query_group_short( string name );
object *members_of( string name );
object leader_of( string name );
string short_to_name( string short );
int create_group( string name );
int remove_group( string name );
int add_invite( string name, object person, int flag );
int remove_invite( string name, object person );
int add_member( string name, object person );
int remove_member( string name, object person );
varargs int set_leader( string name, object person, object appointer );
void notify_group( string name, object broadcaster, mixed message );
varargs void disband_group( string name, mixed message );
varargs object shuffle_new_leader( string group, int way, object *exclude );
void leader_goes_linkdead( string player, string event_type );
void handle_group_follow( string group, object who, object *what,
int unfollow, int silent );
void broadcast_to_groups( string *name, string message );
string *query_groups();
/** @ignore yes */
void create()
{
_groups = ([ ]);
_loaded = time();
_groups_formed = 0;
} /* create() */
/** @ignore yes */
void dest_me() {
string name;
broadcast_to_groups( 0, "%^BOLD%^WARNING%^RESET%^: The group handler is being destructed."
" All active groups will be disbanded. It should be possible to "
"recreate the group almost immediately afterwards. If not, please "
"file a bug report for the \"group\" command." );
foreach( name in query_groups() ) {
disband_group( name, 0 );
}
} /* dest_me() */
/** @ignore yes */
void stats_please() {
printf( "The handler was loaded on %s. Since then, "
"%i groups have been formed.\n", ctime( _loaded ),
_groups_formed );
} /* stats_please() */
/**
* This returns the names of all groups currently in the database.
* @return the names of all groups in database
*/
string *query_groups() {
return keys( _groups );
} /* query_groups() */
/**
* This function checks if such a group exists at the moment.
*
* @param group the name of the group
* @return 1 if the group exists, 0 otherwise
*/
int is_group( string group ) {
return !undefinedp( _groups[ group ] );
} /* is_group() */
/**
* This function allows you to check whether a person is a member
* of a group.
*
* @param name the name of the group
* @param person the person you want to check for membership
*
* @return 1 if he is a member, 0 if he isn't (or the group doesn't exist)
*
*/
int is_member( string name, object person ) {
if( !is_group( name ) ) {
return 0;
}
if( member_array( person, _groups[ name ]->members ) == -1 ) {
return 0;
}
return 1;
} /* is_member() */
/**
* This function allows you to check whether an invitation for a
* person is pending in a specific group.
*
* @param name the name of the group
* @param person the person to be checked
*
* @return 1 if such an invitation exists, 0 if not
*/
int is_invited( string name, object person )
{
if( member_array( person, _groups[ name ]->invited ) != -1 )
{
return 1;
}
return 0;
} /* is_invited() */
/**
* This function lists all invitations to a particular group.
* @param name the name of the group
* @return objects who have been invited to the group
*/
object *invitations_to( string name )
{
if( !is_group( name ) )
{
return 0;
}
return _groups[ name ]->invited;
} /* invitations_to() */
/**
* This returns the short description of a group.
* @param name the name of the group
* @return the short of the group
*/
string query_group_short( string name )
{
if( !is_group( name ) )
{
return 0;
}
return _groups[ name ]->short;
} /* query_group_short() */
/**
* This function returns all the members of a group.
* @param name the name of the group
* @return an object * of the members
*/
object *members_of( string name )
{
if( !is_group( name ) )
{
return 0;
}
return _groups[ name ]->members;
} /* members_of() */
/**
* This function returns the person who is currently
* leading the group, if any.
*
* @param name the name of the group
* @return the object pointing to the leader, 0 if none
*/
object leader_of( string name )
{
if( !is_group( name ) )
{
return 0;
}
return _groups[ name ]->leader;
} /* leader_of() */
/**
* This function returns the time when the group was created.
* @param name the name of the group
* @return the time when the group was created
*/
int query_start_time( string name )
{
if( !is_group( name ) )
{
return 0;
}
return _groups[ name ]->start_time;
} /* query_start_time() */
/**
* This function concatenates the short of a group to a valid name
* that can then be used with create_group(). The function will
* also return invalid if a channel with such a name exists in the
* broadcaster.
*
* @param short the short of the group
* @return the name of the group, or "" if invalid
*/
string short_to_name( string short )
{
string *words;
short = lower_case( short );
words = explode( short, " " );
words -= INVALID_WORDS;
if( !sizeof( words ) ) {
return "";
}
short = implode( words, " " );
if( "/handlers/broadcaster"->query_channel_members( "group_" + short ) ) {
return "";
}
return short;
} /* short_to_name() */
/**
* This function allows you to create a new group. Note that the
* name of the group shouldn't be just any name. It should first
* be filtered through short_to_name(). The short, set with
* set_group_short() can be set to anything. The "name" of a
* group is mostly used internally to query and set stuff in
* the handler. The short is visible to players.
*
* @param name the name of the new group
* @param founder points to the thing creating the group
*
* @see short_to_name()
* @see set_group_short()
*
* @return 1 on success, 0 if the group already exists
*/
int create_group( string name )
{
if( is_group( name ) ) {
return 0;
}
_groups += ([ name : new( class group ) ]);
_groups[ name ]->members = ({ });
_groups[ name ]->invited = ({ });
_groups[ name ]->start_time = time();
// For stats.
_groups_formed++;
return 1;
} /* create_group() */
/**
* This sets the short description of a group.
*
* @param name the name of the group
* @param short_desc the short of the group
*
* @return 1 on success, 0 if group doesn't exist
*/
int set_group_short( string name, string short_desc )
{
if( !is_group( name ) ) {
return 0;
}
_groups[ name ]->short = short_desc;
return 1;
} /* set_group_short() */
/**
* This function removes a group from the handler and is only used
* internally. No notification of this is given to players, and
* no cleanup is done on their part.
* If you want to force the deletion of a group, use disband_group()
* instead.
*
* @param name name of the group to be disbanded
* @return 1 on success, 0 if group doesn't exist
* @see disband_group()
*/
int remove_group( string name )
{
if( !is_group( name ) ) {
return 0;
}
if( _groups[ name ]->leader_name ) {
LOGIN_HANDLER->remove_login_call(
_groups[ name ]->leader_name, "leader_goes_linkdead",
this_object() );
}
map_delete( _groups, name );
return 1;
} /* remove_group() */
/**
* This function allows you to add a person to the invite array
* of a group. Only invited people are allowed to join a group.
* If the call succeeds, an internal call_out is started which
* runs out after INVITE_TIMEOUT seconds and removes the person
* from the array.
*
* @param name the name of the group
* @param person the person we're inviting
* @param flag set to 1 if you don't want the auto-removal of the invite
*
* @return 1 on success, 0 if the group doesn't exist or the person is
* already invited
*
* @see /include/group_handler.h
*/
int add_invite( string name, object person, int flag )
{
if( !is_group( name ) )
{
return 0;
}
if( is_member( name, person ) )
{
return 0;
}
if( member_array( person, _groups[ name ]->invited ) != -1 )
{
return 0;
}
_groups[ name ]->invited += ({ person });
if( !flag )
{
call_out( (: remove_invite, name, person :), INVITE_TIMEOUT );
}
return 1;
} /* add_invite() */
/**
* This function allows you to remove an invite of a person from
* a group.
*
* @param name the name of the group
* @param person the person you want to remove
*
* @return 1 on success, 0 if group doesn't exist or person hasn't been
* invited
*/
int remove_invite( string name, object person )
{
if( !is_group( name ) )
{
return 0;
}
if( !is_invited( name, person ) )
{
return 0;
}
_groups[ name ]->invited -= ({ person, 0 });
return 1;
} /* remove_invite() */
/**
* This function allows you to add a member to a group.
*
* @param name the name of the group
* @param person the person you're adding
* @return 1 on success, 0 if the group doesn't exist or person is
* already a member
*/
int add_member( string name, object person )
{
if( !is_group( name ) )
{
return 0;
}
if( is_member( name, person ) )
{
return 0;
}
_groups[ name ]->members += ({ person });
_groups[ name ]->invited -= ({ person });
// Add the group effect.
person->add_effect( EFFECT, name );
// Add him to his group's channel.
"/handlers/broadcaster"->add_object_to_channel( "group_" + name, person );
notify_group( name, person, ({ "You have joined the group.",
person->query_cap_name() + " has joined the group." }) );
if( sizeof( _groups[ name ]->members ) > 1 )
{
handle_group_follow( name, person, ({ _groups[ name ]->leader }), 0, 0 );
}
person->set_title( GROUP_TITLE, "a member of " +
query_group_short( name ) );
return 1;
} /* add_member() */
/**
* This function allows you to remove a person from a group.
*
* @param name the name of the group
* @param person the person you want removed
*
* @return 1 on success, 0 if group doesn't exist or person isn't a
* member
*/
int remove_member( string name, object person )
{
object member;
if( !is_group( name ) )
{
return 0;
}
if( !is_member( name, person ) )
{
// Not a member.
return 0;
}
if( person )
{
notify_group( name, person, ({
"You have left the group.",
person->query_cap_name() + " has left the group." }) );
foreach( member in person->query_assisting() )
{
// Stop assisting members.
if( !member )
{
continue;
}
member->remove_assister( person );
}
foreach( member in person->query_assisters() )
{
// Stop being assisted.
if( !member )
{
continue;
}
member->remove_assisting( person );
}
}
_groups[ name ]->members -= ({ person });
// Stop listening to the group's channel.
"/handlers/broadcaster"->remove_object_from_channel( "group_" + name, person );
if( person )
{
// Delete the effect.
person->group_membership_removed();
}
if( person == leader_of( name ) && sizeof( members_of( name ) ) )
{
// If the person was the leader, assign a new one.
notify_group( name, this_object(), "The current leader has left "
"the group. A new leader will be chosen randomly." );
if( !shuffle_new_leader( name, 0 ) )
{
// Couldn't find a suitable new leader (which really shouldn't
// happen unless everyone is net dead, afaik).
notify_group( name, this_object(), "The choosing of a new "
"leader has failed (oh dear). The group is hereby "
"disbanded.\n" );
call_out( "disband_group", 0, name );
return 1;
}
}
// Stop following everyone.
handle_group_follow( name, person, _groups[ name ]->members, 1, 1 );
foreach( member in _groups[ name ]->members ) {
// Make sure people stop following you.
handle_group_follow( name, member, ({ person }), 1, 1 );
}
// Remove the "whois" title.
person->remove_title( GROUP_TITLE );
// If the group's empty, remove it altogether.
if( !sizeof( members_of( name ) ) ) {
remove_group( name );
}
return 1;
} /* remove_member() */
/**
* This function allows you to set a new leader for the group.
*
* @param name the name of the group
* @param person the new leader
* @param appointer if this != 0, group is told he set the new leader
*
* @return 1 on success, 0 if the group doesn't exist
*/
varargs int set_leader( string name, object person, object appointer )
{
object *followers, old_leader, member;
if( !is_group( name ) )
{
return 0;
}
if( !is_member( name, person ) )
{
return 0;
}
if( !person )
{
return 0;
}
if( _groups[ name ]->leader_name )
{
LOGIN_HANDLER->remove_dynamic_login_call(
_groups[ name ]->leader_name, "leader_goes_linkdead",
base_name( this_object() ) );
}
old_leader = _groups[ name ]->leader;
if( old_leader )
{
// Those who were following the old leader will now follow the new
// leader.
followers = ( old_leader->query_followers() & _groups[ name ]->members );
followers -= ({ 0 });
followers += ({ old_leader });
foreach( member in followers )
{
handle_group_follow( name, member, ({ old_leader }), 1, 1 );
handle_group_follow( name, member, ({ person }), 0, 1 );
}
// Set the title back to "only" a member.
old_leader->set_title( GROUP_TITLE, "a member of " +
query_group_short( name ) );
}
_groups[ name ]->leader = person;
if( userp( person ) ) {
_groups[ name ]->leader_name = person->query_name();
LOGIN_HANDLER->add_dynamic_login_call( person->query_name(),
"leader_goes_linkdead", base_name( this_object() ) );
}
else
{
// How can a non-user become the leader? Beats me, but I'm paranoid.
_groups[ name ]->leader_name = 0;
}
if( !appointer )
{
notify_group( name, person, ({ "You are now the leader of "
"the group.", person->query_cap_name() + " is now the leader "
"of the group." }) );
}
else
{
notify_group( name, appointer, "By the power vested in " +
appointer->query_cap_name() + ", " + person->query_cap_name() +
" has been appointed as the new leader of the group." );
}
person->set_title( GROUP_TITLE, "the leader of " +
GROUP->query_group_short( name ) );
return 1;
} /* set_leader() */
/**
* This function broadcasts a message to the group's channel using
* the broadcaster handler. The first argument specifies the
* group's name (not short), which also acts as the channel
* name. The second argument is the object doing the broadcasting.
* The third argument varies. It can either be a simple string,
* in which case that string is printed as the message. It can
* also be a two-element string array. The first element is
* printed only to the object specified in the second argument.
* The second element is printed to everyone else.
*
* @param name the name of the group
* @param object person or object doing the broadcasting
* @param message the message to be broadcasted
*
*/
void notify_group( string name, object broadcaster, mixed message ) {
"/handlers/broadcaster"->broadcast_to_channel( broadcaster, "group_" + name,
({ message, time() }) );
} /* notify_group() */
/**
* This function does a clean removal of a group from the handler.
* If a message is specified, it is broadcasted to the group
* before all members are removed.
*
* @param name the name of the group to be disbanded
* @param message message to be broadcasted, if any
* @see remove_group()
*/
varargs void disband_group( string name, mixed message ) {
object bugger, leader, *members;
if( !is_group( name ) ) {
return;
}
if( message ) {
notify_group( name, this_object(), message );
}
members = members_of( name );
leader = leader_of( name );
if( leader ) {
members -= ({ leader });
}
foreach( bugger in members ) {
remove_member( name, bugger );
}
remove_member( name, leader );
remove_group( name );
} /* disband_group() */
/**
* This function allows you to choose a new leader for the group
* in a variety of ways. It only includes players who are
* interactive().
*
* @param group the name of the group
* @param way 0 for random (no other ways present atm)
* @param exclude an object array of members to exclude from the start
* @return an object pointing to the new leader, or 0
*/
varargs object shuffle_new_leader( string group, int way, object *exclude ) {
object leader;
object *members;
if( !is_group( group ) ) {
return 0;
}
members = members_of( group );
if( !sizeof( members ) ) {
return 0;
}
if( exclude ) {
members -= exclude;
}
members = filter( members, (: interactive( $1 ) :) );
if( !sizeof( members ) ) {
return 0;
}
switch( way ) {
case 0:
leader = members[ random( sizeof( members ) ) ];
if( set_leader( group, leader ) ) {
return leader;
}
return 0;
default:
return 0;
}
} /* shuffle_new_leader() */
// Caution: The following function contains undocumented, convoluted code.
// Browse at your own risk.
/** @ignore yes */
void leader_goes_linkdead( string player, string event_type ) {
string group;
object player_ob, *members;
// This function only takes care of leader reassignment when
// the current leader goes netdead. Quitting or otherwise
// destructing is taken care of in the shadow. Don't ask me
// why, I'm just lazy.
if( event_type != NETDEATH && event_type != RECONNECT ) {
LOGIN_HANDLER->remove_dynamic_login_call( player,
"leader_goes_linkdead", base_name( this_object() ) );
return;
}
if( !player_ob = find_player( player ) ) {
LOGIN_HANDLER->remove_dynamic_login_call( player,
"leader_goes_linkdead", base_name( this_object() ) );
return;
}
group = player_ob->query_group();
if( !group ) {
LOGIN_HANDLER->remove_dynamic_login_call( player,
"leader_goes_linkdead", base_name( this_object() ) );
return;
}
if( _groups[ group ]->leader_name != player ) {
// Well that was weird. A login call should only be added for
// the current leader.
LOGIN_HANDLER->remove_dynamic_login_call( player,
"leader_goes_linkdead", base_name( this_object() ) );
return;
}
members = members_of( group );
members -= ({ player_ob });
if( !sizeof( members ) ) {
// Looks like a one-man group. Let's wait until he wakes up.
return;
}
LOGIN_HANDLER->remove_dynamic_login_call( player,
"leader_goes_linkdead", base_name( this_object() ) );
notify_group( group, this_object(), "The current leader "
"has gone netdead. A new leader will be selected at random." );
if( !shuffle_new_leader( group, 0, 0 ) ) {
notify_group( group, this_object(), "No eligible leaders "
"found. The group is disbanded." );
disband_group( group );
}
} /* leader_goes_linkdead() */
/** @ignore */
void handle_group_follow( string group, object who, object *what,
int unfollow, int silent ) {
string short, mess_to_me, mess_to_others;
switch( unfollow ) {
case 0:
what = filter( what, (: $1->add_follower( $( who ) ) :) );
if( !sizeof( what ) ) {
mess_to_me = "You begin following noone.";
break;
}
short = query_multiple_short( what );
mess_to_me = "You begin following " + short + ".";
mess_to_others = who->query_cap_name() + " begins following " +
short + ".";
break;
case 1:
what = filter( what, (: $1->remove_follower( $( who ) ) :) );
if( !sizeof( what ) ) {
mess_to_me = "You stop following noone.";
mess_to_others = 0;
break;
}
short = query_multiple_short( what );
mess_to_me = "You stop following " + short + ".";
mess_to_others = who->query_cap_name() + " stops following " +
short + ".";
break;
default:
printf( "Barf.\n" );
return;
}
if( !silent ) {
notify_group( group, who, ({ mess_to_me, mess_to_others }) );
}
} /* handle_group_follow() */
/**
* With this function you can broadcaster a message to all or
* some of the groups currently active. It will not include
* your name, so make sure you identify yourself if necessary.
*
* If "name" is empty or 0, the message will be broadcasted
* to all groups.
*
* @param name a string array with names of groups
* @message the message to be broadcasted
*/
void broadcast_to_groups( string *name, string message ) {
string group;
string *groups;
if( name && sizeof( name ) ) {
groups = name;
}
else {
groups = keys( _groups );
}
foreach( group in groups ) {
notify_group( group, this_player(), message );
}
} /* broadcast_to_groups() */