/** * 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(); int set_group_short( string, string); /** @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( 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 method renames the group. * @param group the old group name * @param new_group the new group name */ int rename_group(string group, string new_group) { object member; if (is_group(new_group) || new_group == group) { return 0; } _groups[new_group] = _groups[group]; map_delete(_groups, group); set_group_short(new_group, new_group); foreach (member in _groups[new_group]->members) { if (!member) { continue; } // Remove the old group stuff. BROADCASTER->remove_object_from_channel( "group_" + group, member ); member->group_membership_removed(); member->add_effect( EFFECT, new_group); // Add him to his group's channel. BROADCASTER->add_object_to_channel( "group_" + new_group, member ); member->set_title( GROUP_TITLE, "a member of " + query_group_short( new_group ) ); } notify_group( new_group, this_object(), ({ "", "The group has been renamed to " + new_group + "." }) ); return 1; } /** * 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. 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. 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. if (sizeof(members_of(name)) == 1) { notify_group( name, this_object(), "The current leader has left " "the group, you are now all alone. Better start recruiting."); } else { 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. if (person && objectp(person)) { 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 ) { 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, (: $1 && 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() */ mapping query_dynamic_auto_load() { return ([ "groups" : _groups, "groups formed" : _groups_formed ]); } void init_dynamic_arg(mapping map) { _groups = map["groups"]; _groups_formed = map["groups formed"]; }