/**
* Playerinfo database handler.
*
* This handler collects all the fascinating information about players and
* their sins. At the moment, the following events are supported:
* replace (all replacements, added by Presto's wand),
* gag (gagging and ungagging, added by the gagger),
* suspend (player suspension, added by the "suspend" command),
* meteor (meteoring a player, added by the "meteor" command),
* multiplay (various events added by the multiplayer handler),
* harassment (comments about cases of harassment, added via 'addevent'),
* misc (misc. comments, added via 'addevent'),
* cheat (currently unused)
*
* The "replace" and "multiplay" events are debounced (merged). In addition,
* the "replace" events expire in 30 days.
*
* @author Fiona
*/
#include <playerinfo.h>
// This is where our save files go
#define PLAYERINFO_SAVE_DIR "/save/playerinfo"
#define SAVE_FILE PLAYERINFO_SAVE_DIR "/handler_data"
// Originators of function calls. Checked for security reasons
#define WAND "/obj/misc/wand"
#define MULTIPLAY "/handlers/multiplayer"
#define GAGGER "/obj/misc/gagger"
#define GAG "/cmds/creator/gag"
#define UNGAG "/cmds/creator/ungag"
#define METEOR "/cmds/creator/meteor"
#define FRY "/cmds/admin/fry"
#define SKULLWARN "/cmds/admin/skullwarn"
#define WHOOP "/cmds/admin/whoop"
#define SWAT "/cmds/admin/swat"
#define HAMMER "/cmds/admin/hammer"
#define REPORT_COMMAND "/cmds/creator/playerinfo"
#define ADD_COMMAND "/cmds/creator/addevent"
#define DELETE_COMMAND "/cmds/creator/delevent"
#define ADDALT_COMMAND "/cmds/creator/addalt"
#define DELALT_COMMAND "/cmds/creator/delalt"
#define FAMILY_COMMAND "/cmds/creator/family"
#define SUSPENDER "/secure/bastards"
#define SHOWHELP_COMMAND "/cmds/creator/show_help"
#define PATRICIAN_PALACE "/obj/misc/pk_token"
#define REFRESH_HANDLER "/handlers/refresh"
#define PRISON "/d/sur/beta/prison/dungeon"
#define FETCH_COMMAND "/cmds/creator/fetch"
#define REARRANGE_COMMAND "/cmds/guild-race/rearrange"
// These don't really belong here... and we have /include/colour.h...
// ...oh well.
#define CL_CREATOR "%^CYAN%^"
#define CL_EVENT "%^RED%^"
#define CL_RESET "%^RESET%^"
#define CL_HEADER "%^RED%^"
// This is the interval at which debounced events are saved.
#define DEBOUNCE_PERIOD (60*10)
// This is the interval between consecutive checks for timeouts.
// One day is suffucuent.
#define TIMEOUT_PERIOD (60*60*24)
// Function prototypes
mapping query_timeouts();
protected string filename(string name);
string * query_events();
protected int query_debounced(string event);
protected int query_lord_only(string event);
int query_source_ok(string event, string source);
protected int query_deleter_ok(string event, object deleter);
protected int query_can_add(string e, object p);
protected int query_can_delete(string e, object p);
protected int query_can_handle_alts(object p);
protected void do_debouncing(string player, class dbentry entry);
protected void do_timeout();
protected void load_player(string player);
protected void save_player(string player);
void player_remove(string player);
int add_entry(object creator, string player, string event,
string comment, mixed *extra);
int delete_entry(object creator, string player, string event,
int n);
protected void print_header(object source, string player);
protected void print_entry(object source, int idx, class dbentry e);
void print_dossier(object source, string player);
void print_event(object source, string player, string event);
string add_alts(object creator, string player, string *alts);
string delete_alts(object creator, string player, string* alts);
mapping query_alerts();
int query_alerts_for( string player );
int is_alert( string player, int idx );
int acknowledge_alert( object creator, string player,
string event, int idx, string update, mixed * extra );
void clear_alerts_for( string player );
int increment_alerts_for( string player );
int decrement_alerts_for( string player );
class source {
string *add;
string *delete;
}
// This is where the current player's info resides.
private class playerinfo dossier;
// Event timeout information (in seconds)
private nosave mapping timeouts = ([ "replace" : (60*60*24*30) ]);
// Ok guys, NEVER do things like this, it is highly poisonous...
private int local_time;
private nosave mapping _sources;
nosave mapping _alerts; // format: ([ "playername1":n, "playername2":n ... ])
// where n is the number of alerts in their dossier.
nosave string * _lordonly;
void save_handler_data() {
mapping vars = ([ ]);
string tmp;
vars["alerts"] = _alerts;
tmp = save_variable( vars );
unguarded( (: write_file, SAVE_FILE, tmp, 1 :) );
} /* save_handler_data() */
void load_handler_data() {
mapping vars;
string tmp;
if( file_size( SAVE_FILE ) <= 0 )
return;
tmp = unguarded( (: read_file, SAVE_FILE :) );
vars = restore_variable( tmp );
_alerts = vars["alerts"];
} /* load_handler_data() */
void create() {
_sources = ([
"cheat": new(class source,
add : ({ }),
delete : ({ DELETE_COMMAND })),
"discipline": new(class source,
add : ({ ADD_COMMAND, PRISON }),
delete : ({ DELETE_COMMAND })),
"email": new(class source,
add : ({ }),
delete : ({ DELETE_COMMAND })),
"family": new(class source,
add : ({ FAMILY_COMMAND }),
delete : ({ DELETE_COMMAND })),
"gag": new(class source,
add : ({ GAGGER, GAG, UNGAG }),
delete : ({ DELETE_COMMAND })),
"harassment": new(class source,
add : ({ ADD_COMMAND }),
delete : ({ DELETE_COMMAND })),
"meteor": new(class source,
add : ({ METEOR}),
delete : ({ DELETE_COMMAND })),
"fry": new(class source,
add : ({ FRY}),
delete : ({ DELETE_COMMAND })),
"skullwarn": new(class source,
add : ({ SKULLWARN}),
delete : ({ DELETE_COMMAND })),
"whoop": new(class source,
add : ({ WHOOP}),
delete : ({ DELETE_COMMAND })),
"swat": new(class source,
add : ({ SWAT}),
delete : ({ DELETE_COMMAND })),
"misc": new(class source,
add : ({ ADD_COMMAND, PATRICIAN_PALACE,
REARRANGE_COMMAND }),
delete : ({ DELETE_COMMAND })),
"multiplay": new(class source,
add : ({ MULTIPLAY }),
delete : ({ DELETE_COMMAND })),
"replace": new(class source,
add : ({ WAND, ADD_COMMAND, FETCH_COMMAND }),
delete : ({ DELETE_COMMAND })),
"showhelp": new(class source,
add : ({ SHOWHELP_COMMAND }),
delete : ({ DELETE_COMMAND })),
"suspend": new(class source,
add : ({ SUSPENDER }),
delete : ({ })),
"alert": new(class source,
add : ({ ADD_COMMAND, REPORT_COMMAND }),
delete : ({ DELETE_COMMAND })),
"refresh": new(class source,
add : ({ REFRESH_HANDLER }),
delete : ({ DELETE_COMMAND }))
]);
_alerts = ([ ]);
_lordonly = ({ });
seteuid("Room");
load_handler_data();
} /* create() */
// Return the mapping of event timeouts
mapping query_timeouts() {
return timeouts;
} /* query_timeouts() */
/**
* Give the filename for the player's savefile.
* @param name the name of the player
* @return the name of the playerinfo file
* @ignore
*/
protected string filename(string name) {
string p = lower_case(name);
return sprintf("%s/%c/%s.o",PLAYERINFO_SAVE_DIR,p[0],p);
}
/**
* Answer the list of all possible events.
* @return array of all event types recognized by the playerinfo handler
*/
string *query_events() {
return keys(_sources);
} /* query_events() */
/**
* Check if the event should be debounced.
* @param event the name of the event
* @return nonzero if the given event is to be debounced
* @ignore
*/
protected int query_debounced(string event) {
return (event == "replace" || event == "multiplay");
} /* query_debounced() */
/**
* Check if the event is lords-only.
* @param event the name of the event
* @return nonzero if the event can only be added by lords
* @ignore
*/
protected int query_lord_only(string event) {
return ( member_array( event, _lordonly ) >= 0 ) ? 1 : 0;
}
/**
* Check if the event has come from the valid source.
* @param event the name of the event
* @param source the object trying to add the event
* @return nonzero if the event can be added by the given source
*/
int query_source_ok(string event, string source) {
// 0 means no check should be done; ({ }) means nobody can add it
string *reqd;
reqd = _sources[event]->add;
if(reqd == 0) {
return 1;
}
if(sizeof(reqd) == 0) {
return 0;
}
return member_array(source,reqd) >= 0;
}
/**
* Check if the request to delete an event came from the valid source.
* @param event the name of the event
* @param remover the object that tries to remove the event
*/
protected int query_deleter_ok(string event, object deleter) {
// 0 means no check should be done; ({ }) means nobody can delete it
string *reqd;
// for testing purposes only. remember to remove this later!!
if( creatorp(deleter))
return 1;
reqd = _sources[event]->delete;
if (reqd == 0) {
return 1;
}
if (sizeof(reqd) == 0) {
return 0;
}
return member_array(base_name(deleter),reqd) >= 0;
}
/**
* Check if the player is allowed to add the event.
* @param e the name of the event
* @param p the player who is trying to add it
* @return nonzero of the player is allowed to add the event
* @ignore
*/
protected int query_can_add(string e, object p) {
if(member_array(e,query_events()) < 0)
return 0;
if(!interactive(p))
return 1;
return !(query_lord_only(e) && !lordp(p));
}
/**
* Check if the player is allowed to perform delete operations.
* @param p the player who is trying to delete something
* @ignore
*/
protected int query_can_delete(string e, object p) {
if(!interactive(p))
return 1;
// for testing purposes only. remember to remove this later!!
if( creatorp(p))
return 1;
if(e == "misc" || e == "replace")
return seniorp(p->query_name());
return lordp(p);
}
/**
* Check if the player is allowed to add and delete alt characters.
* @param p the player who is trying to add or delete alt(s)
* @ignore
*/
protected int query_can_handle_alts(object p) {
if(!interactive(p))
return 1;
return seniorp(p->query_name());
}
/**
* Check if we can debounce the event. Add a new event or modify
* the last event depending on whether it's debounceable or not.
* @param player the name of the player for which the database entry is added
* @param entry the database entry to be added
* @ignore
*/
protected void do_debouncing(string player, class dbentry entry) {
int n;
class dbentry last;
if (query_debounced(entry->event)) {
//tell_creator("pinkfish", "[playerinfo] Debouncing: %O.\n",entry);
n = sizeof(dossier->data);
if(n != 0) {
last = dossier->data[n-1];
//tell_creator("pinkfish", "[playerinfo] Last: %O.\n",last);
if(entry->event == last->event &&
entry->creator == last->creator &&
entry->time - last->time <= DEBOUNCE_PERIOD) {
// Merge the two events
//tell_creator("pinkfish", "[playerinfo] Merging events.\n");
last->comment += entry->comment;
last->time = entry->time;
if(last->extra != 0) {
if(entry->extra == 0)
entry->extra = ({ });
last->extra += entry->extra;
}
//tell_creator("pinkfish", "[playerinfo] Result: %O.\n",last);
return;
}
}
}
//tell_creator("pinkfish", "[playerinfo] Not merging events.\n");
dossier->data += ({ entry });
return;
}
/**
* Check the currently loaded data for timed out entries and remove them.
* @ignore
*/
protected void do_timeout() {
function not_timed_out = function(class dbentry p)
{
int life = timeouts[p->event];
if(life == 0) // This event cannot be timed out
return 1;
// Time it out if its life period has expired
return local_time <= (p->time + life);
};
local_time = time();
dossier->data = filter(dossier->data, not_timed_out);
dossier->last_check = time();
}
/**
* Load the data of the playerinfo object from its save file. If there's no
* file, create an empty dossier. Don't load anything if the data is
* already loaded.
* @param player the name of the player whose data is to be loaded
* @ignore
*/
protected void load_player(string player) {
string p = lower_case(player);
string fn = filename(p);
if(dossier != 0 && dossier->name == p)
return; // Already have it here
if(file_size(fn) > 0) {
unguarded( (: restore_object, fn :) );
} else {
dossier = new(class playerinfo,
name: p,
last_check: time(),
alts: ({ }),
data: ({ }));
}
}
/**
* Save the data of the playerinfo object to its save file.
* @param player the name of the player whose data is to be saved
* @ignore
*/
protected void save_player(string player) {
if(time() - dossier->last_check >= TIMEOUT_PERIOD)
do_timeout();
unguarded( (: save_object, filename(player) :) );
}
/**
* Remove the player's data file.
* @param player the name of the player
*/
void player_remove(string player) {
unguarded( (: rm, filename(player) :) );
clear_alerts_for(player);
}
/**
* Add a new entry to the player's database.
* @param source the creator or another object trying to add the event
* @param player the name of the player
* @param event event the name of the event to be added
* @param comment arbitrary comment text (more than one line is OK)
* @param extra arbitrary array of arbitrary objects (can be 0)
* @return nonzero if the entry was successfully added to the database
*/
int add_entry( object creator, string player, string event, string comment,
mixed *extra ) {
class dbentry new_entry;
if( !query_can_add( event, creator ) )
return 0; // No permission to add this event
if( !query_source_ok( event, base_name(PO) ) )
return 0; // Wrong object trying to add this event
if( !PLAYER_H->test_user( lower_case(player) ) )
return 0; // No such player
load_player(player);
new_entry = new(class dbentry,
time: time(),
creator: capitalize(creator->query_name()),
event: event,
comment: (comment == 0 ? "" : comment),
extra: extra);
do_debouncing(player, new_entry);
save_player(player);
// Add the player to the alerts mapping (to be checked by the login
// handler which will dispatch warnings to currently online creators
// the next time the player logs in, until the event is acknowledged)
if( event == "alert" )
increment_alerts_for(player);
return 1;
} /* add_entry() */
/**
* Delete an entry from the playerinfo database.
* @param source the creator or another object trying to add the event
* @param player the name of the player
* @param event the name of the event of the entry being deleted
* @param n the index of the entry being deleted
* @return nonzero if the entry was successfully deleted
*/
int delete_entry(object creator, string player, string event, int n) {
int idx = n - 1;
class dbentry * data;
if( !query_can_delete( event, creator ) )
return 0;
if( !query_deleter_ok( event, previous_object() ) )
return 0; // Wrong object trying to delete this event
load_player(player);
if( ( idx < 0 ) || ( idx >= sizeof( dossier->data ) ) )
return 0;
if( dossier->data[idx]->event != event )
return 0;
data = copy( dossier->data );
data = data[0 .. (idx - 1)] + data[(idx + 1) .. <1];
dossier->data = data;
save_player(player);
if( event == "alert" )
decrement_alerts_for(player);
return 1;
}
/**
* Print the header of of the database report.
* @param source the creator who requested the report
* @param player the name of the player
* @ignore
*/
protected void print_header(object source, string player) {
string alt, aka = "", alts = "";
int first = 1;
if(sizeof(dossier->alts) > 0) {
foreach(alt in dossier->alts) {
if(first) first = 0; else alts = alts + ", ";
alts = alts + CL_HEADER + capitalize(alt) + CL_RESET;
}
alts = " aka " + alts;
}
if(dossier->main_alt != 0)
aka = " (alt of " + CL_HEADER + capitalize(dossier->main_alt) +
CL_RESET + ")";
tell_object(source,sprintf("Report for: %s%s%s\n\n",
CL_HEADER+capitalize(player)+CL_RESET,alts,aka));
}
/**
* Print one entry of the dossier.
* @param source the creator who requested the report
* @param idx the index of the database entry to print
* @param e the database entry to print
* @ignore
*/
protected void print_entry(object source, int idx, class dbentry e) {
string date = ctime(e->time);
string creator = e->creator;
string event = e->event;
string *comments = explode(e->comment,"\n");
string line;
int lines = 0;
int cols;
tell_object( source, sprintf("%d. %-26s %s%|20s%s (added by %s%s%s)\n",
idx+1,date, CL_EVENT, event, CL_RESET, CL_CREATOR, creator,
CL_RESET ) );
cols = (int)source->query_cols() || 79;
foreach( line in comments ) {
if( sizeof(line) ) {
tell_object( source, indent( line+"\n", 4, cols ) );
lines++;
}
}
if( !lines )
tell_object( source, sprintf(" (no comments)\n") );
} /* print_entry() */
/**
* Print all entries from the given player's dossier.
* @param source the creator who requested the report
* @param player the name of the player
*/
void print_dossier(object source, string player) {
int i;
class dbentry *list;
// class playerinfo migrated;
load_player(player);
/*
migrated = new(class playerinfo,
name: dossier->name,
last_check: dossier->last_check,
alts: dossier->alts,
data: dossier->data,
main_alt: 0);
dossier = migrated;
save_player(player);
*/
print_header(source,player);
list = dossier->data;
for ( i = 0; i < sizeof( list ); i++ ) {
print_entry(source,i,list[i]);
}
}
/**
* Print all entries from the given player's dossier with the given event
* type.
* @param source the creator who requested the report
* @param player the name of the player
* @param event the name of the event
*/
void print_event(object source, string player, string event) {
int i;
class dbentry *list;
load_player(player);
print_header(source,player);
list = dossier->data;
for (i = sizeof(list)-1; i >= 0; i--) {
if (list[i]->event == event) {
print_entry(source,i,list[i]);
}
}
}
/**
* Add an alt character name to this player's dossier. This function succeeds
* if both characters are not "main", or only one if them is "main". Both
* players will have their dossiers modified.
* @param player the name of the player
* @param alts the names of the alt characters to add
* @return a string describing the outcome of the function call
*/
string add_alts(object creator, string player, string *alts) {
string p1 = lower_case(player);
string *p2 = uniq_array(map(alts, (: lower_case($1) :)));
string alt, *to_add, result;
if(!query_can_handle_alts(this_player()))
return "You are not allowed to add players' alts. \n";
if( !PLAYER_H->test_user(p1) )
return "No such player: "+p1+". \n"; // No such player
foreach(alt in p2) {
if( !PLAYER_H->test_user(alt) )
return "No such player: "+alt+". \n"; // No such player
}
load_player(p1);
if(dossier->main_alt != 0) {
return capitalize(p1)+" is already an alt of "+
capitalize(dossier->main_alt);
}
p2 -= dossier->alts;
p2 -= ({ p1 });
to_add = ({ });
foreach(alt in p2) {
load_player(alt);
if(dossier->main_alt == 0 && sizeof(dossier->alts) == 0) {
dossier->main_alt = p1;
dossier->alts = ({ });
save_player(alt);
to_add += ({ alt });
}
}
if(sizeof(to_add) == 0)
return "Couldn't add any alts. \n";
load_player(p1);
if(dossier->alts == 0)
dossier->alts = ({ }); // We are lazy...
dossier->alts = uniq_array(dossier->alts + to_add); // I LOVE MAPPINGS!
save_player(p1);
// Get ready for an example of a REALLY crazy code!
// (just for reference, all it does is formatting a list of names
// in a nice capitalized, comma-separated way :) ).
result = "Added "+
implode(map(to_add, (: capitalize($1) :)),", ")+
" to "+capitalize(p1)+"'s list of alts. \n";
if(sizeof(p2) != sizeof(to_add))
result += "Couldn't add: "+
implode(map(p2-to_add, (: capitalize($1) :)),", ")+". \n";
return result;
}
/**
* Delete an alt character name from this player's dossier. Note that both
* players have their dossier modified.
* @param player the name of the player
* @param alts the name of the alt characters to delete
* @return a string describing the outcome of the function call
*/
string delete_alts(object creator, string player, string* alts) {
string p1 = lower_case(player);
string *p2 = map(alts, (: lower_case($1) :));
string alt, *to_delete, result;
if( !query_can_handle_alts( TP ) )
return "You are not allowed to delete players' alts. \n";
if( !PLAYER_H->test_user(p1) )
return "No such player: "+p1+". \n"; // No such player
foreach( alt in p2 ) {
if( !PLAYER_H->test_user(alt) )
return "No such player: "+alt+". \n"; // No such player
}
load_player(p1);
if(sizeof(dossier->alts) == 0)
return capitalize(p1)+" doesn't have any alts. \n";
to_delete = ({ });
foreach(alt in p2) {
load_player(alt);
if(dossier->main_alt == p1) {
dossier->main_alt = 0;
dossier->alts = ({ });
save_player(alt);
to_delete += ({ alt });
}
}
if(sizeof(to_delete) == 0)
return "Couldn't delete any alts. \n";
load_player(p1);
if(dossier->alts == 0)
dossier->alts = ({ });
dossier->alts = uniq_array(dossier->alts - to_delete); // (see above)
save_player(p1);
// Same crazy code as above.
result = "Deleted "+
implode(map(to_delete, (: capitalize($1) :)),", ")+
" from "+capitalize(p1)+"'s list of alts. \n";
if(sizeof(p2) != sizeof(to_delete))
result += "Couldn't delete: "+
implode(map(p2-to_delete, (: capitalize($1) :)),", ")+". \n";
return result;
}
/**
* @return The alerts mapping.
*/
mapping query_alerts() {
if( !_alerts )
_alerts = ([ ]);
return _alerts;
} /* query_alerts() */
/**
* @param player Name of the player to query
* @return The number of alerts for that player
*/
int query_alerts_for( string player ) {
player = lower_case(player);
if( !_alerts )
_alerts = ([ ]);
return _alerts[player];
} /* query_alerts_for() */
/**
* @param player The name of the player
* @param idx The number of the event to check for alert status.
* @return 0 if the event is not an alert, 1 if it is.
*/
int is_alert( string player, int idx ) {
if( !PLAYER_H->test_user( lower_case( player ) ) )
return 0;
load_player( player );
return idx > 1 && sizeof(dossier->data) > --idx &&
(dossier->data[idx])->event == "alert";
} /* is_alert() */
/*
* @param player The name of the player.
* @param event The type of event to change the alert to.
* @param idx The number of the event to acknowledge.
* @param update The event description
* @return 1 for success or 0 for failure.
*/
int acknowledge_alert( object creator, string player, string event,
int idx, string update, mixed * extra ) {
class dbentry entry;
string previnfo;
player = lower_case(player);
if( !query_can_add( event, creator ) )
return 0; // No permission to add this event
if( !query_source_ok( "alert", base_name( previous_object() ) ) )
return 0; // Wrong object trying to add this event
if( !PLAYER_H->test_user(player) )
return 0; // No such player
if( member_array( event, keys(_sources) ) < 0 )
return 0; // No such event type
// Update the entry
idx--;
load_player( player );
entry = dossier->data[idx];
previnfo = sprintf( "Originally added by %s%s%s at %s:\n%s\n---\n",
CL_CREATOR, entry->creator, CL_RESET, ctime( entry->time ),
entry->comment );
entry->time = time();
entry->creator = capitalize( creator->query_name() );
entry->event = lower_case( event );
entry->comment = previnfo + update;
dossier->data[idx] = entry;
decrement_alerts_for(player);
save_player( player );
return 1;
} /* acknowledge_alert() */
/**
* @param player The name of the player.
*/
void clear_alerts_for( string player ) {
player = lower_case(player);
if( !_alerts )
_alerts = ([ ]);
map_delete( _alerts, player );
save_handler_data();
} /* clear_alerts_for() */
/**
* @param player The name of the player.
* @return The updated number of alerts for that player.
*/
int increment_alerts_for( string player ) {
player = lower_case(player);
if( !_alerts )
_alerts = ([ ]);
if( !PLAYER_H->test_user(player) )
return 0;
_alerts[player]++;
save_handler_data();
return _alerts[player];
} /* increment_alerts_for() */
/**
* @param player The name of the player.
* @return The updated number of alerts for that player.
*/
int decrement_alerts_for( string player ) {
player = lower_case(player);
if( !_alerts )
_alerts = ([ ]);
if( undefinedp( _alerts[player] ) )
return 0;
if( --_alerts[player] <= 0 ) {
map_delete( _alerts, player );
save_handler_data();
return 0;
}
save_handler_data();
return _alerts[player];
} /* int decrement_alerts_for() */
/**
* @return list of the currently-online players who have unacknowledged alerts
*/
string *query_online_alerts() {
return filter( keys( query_alerts() ),
(: member_array( $1, users()->query_name() ) !=-1 :) );
} /* query_online_alerts() */