dawn/notes/
dawn/src/
dawn/src/docs/
/**************************************************************************/
// intro.cpp - Introduction system - Kal May 2000
/***************************************************************************
 * The Dawn of Time v1.69r (c)1997-2004 Michael Garratt                    *
 * >> A number of people have contributed to the Dawn codebase, with the   *
 *    majority of code written by Michael Garratt - www.dawnoftime.org     *
 * >> To use this source code, you must fully comply with the dawn license *
 *    in licenses.txt... In particular, you may not remove this copyright  *
 *    notice.                                                              *
 **************************************************************************/
/**************************************************************************/
// Unfinished like most of dawn *grin*, currently uses 2 bit flags to cache
// who knows who... if you have more than 512 people logged in at once make
// sure to increase MAX_CACHED_LOOKUPS ;)
// A future project could be to allow custom names... a bit cache value of 
// 01 could be used to mark these... currently:
// 00 = uncached, 01 = unused, 10 = unknown name, 11 = known name.
/**************************************************************************/
#include "include.h"
#include "intro.h"

#define MAX_KNOW_SET 10000
intro_data *intro_data_database[MAX_KNOW_SET];


#define MAX_CACHED_LOOKUPS (512)// after 512 logins, we reinitialise 
								// the complete intro_cache
#define BYTES_TO_STORE_CACHE (MAX_CACHED_LOOKUPS%4==0? MAX_CACHED_LOOKUPS/4:(MAX_CACHED_LOOKUPS/4)+1)
/**************************************************************************/
// intro debugging log
void ilogf(char *fmt, ...)
{
	if(!GAMESETTING5(GAMESET5_VERBOSE_INTRODUCTION_LOGGING)){
		return; // introduction logging disabled by default
	}
    char buf[MSL*2];
	va_list args;
	va_start(args, fmt);
	vsnprintf(buf, MSL*2, fmt, args);
	va_end(args);
	append_datetimestring_to_file( INTRO_DEBUG_FILE, buf);
	log_string(buf);
}
/**************************************************************************/
unsigned short find_empty_know_node()
{
	for(unsigned short i=10; i<MAX_KNOW_SET; i++){
		if(!intro_data_database[i]){
			return i;
		}
	}
	bugf("unsigned short find_empty_know_node(), couldn't find an empty node!\n"
		"System needs improvement for dynamic increasement here!");
	do_abort();

	return 0;
}
/**************************************************************************/
// returns the know index of a player, based on its player id
unsigned short find_know_index_from_char_id(time_t id)
{
	for(unsigned short i=10; i<MAX_KNOW_SET; i++){
		if(intro_data_database[i]){
			if(intro_data_database[i]->owner_id==id){
				return i;
			}	
		}
	}
	return 0;
}
/**************************************************************************/
void intro_data::allocate_intro_cache()
{
	static int last_allocated_cache_index=0;
	
	last_allocated_cache_index++;

	if(last_allocated_cache_index>MAX_CACHED_LOOKUPS-1){
		// we need to reinitialise the complete cache
		ilogf("intro_data::allocate_cache_details() - No space, reinitialising the complete cache");
		logf("intro_data::allocate_cache_details() - No space, reinitialising the complete cache");

		// restart the index
		last_allocated_cache_index=0;

		// loop thru deallocating the complete cache
		for(unsigned short i=10; i<MAX_KNOW_SET; i++){
			if(intro_data_database[i]){
				if(intro_data_database[i]->intro_cache){
					free(intro_data_database[i]->intro_cache);
					intro_data_database[i]->intro_cache=NULL;
				}
				intro_cache_index=0;
			}
		}

		// now loop thru all the players, reallocating them new cache
	    for( char_data *ch = player_list; ch; ch = ch->next_player ) {
			ch->know->allocate_intro_cache();
		}
	}else{
		intro_cache=(unsigned char *)malloc(BYTES_TO_STORE_CACHE);
		intro_cache_index=last_allocated_cache_index;
		ilogf("Allocated intro_cache_index %d to know_index %d", 
			intro_cache_index, my_know_index);
	}
}
/**************************************************************************/
void intro_data::assign_owner(char_data *ch)
{
	ilogf("intro_data::assign_owner() assigning know index %d to %s(%d)",
		my_know_index, ch->name, ch->know_index);
	assert(ch->id==owner_id);
	assert(ch->know_index==my_know_index);
	owner=ch; 
	last_logged_in=current_time; 
	ch->know=this; 
	if(!intro_cache){ // allocate memory for the cached lookups
		allocate_intro_cache();
	}
	logf("intro_data::assign_owner() assigned know index %d to %s(%d)[%d]",
		my_know_index, ch->name, ch->know_index, intro_cache_index);
	ilogf("intro_data::assign_owner() assigned know index %d to %s(%d)[%d]",
		my_know_index, ch->name, ch->know_index, intro_cache_index);
}
/**************************************************************************/
// link who a player knows to their pointer
void attach_know(char_data* player)
{
	if(IS_NPC(player)){
		player->know=NULL;
		return;
	}

	if(intro_data_database[player->know_index]==NULL){
		player->know_index=0;
	}

	// 'player->know_id' is treated as what their id might possibly 
	// this way if it doesn't match due to database corruption, we can reassign
	if(player->know_index==0){ // new char
		player->know_index=find_know_index_from_char_id(player->id);
		if(player->know_index==0){
			player->know_index=find_empty_know_node();		
			assert(player->know_index<MAX_KNOW_SET);
			assert(intro_data_database[player->know_index]==NULL);
			intro_data_database[player->know_index]=new intro_data;
			intro_data_database[player->know_index]->owner_id=player->id;
			intro_data_database[player->know_index]->my_know_index=player->know_index;
		}
	}

	assert(player->know_index<MAX_KNOW_SET);
	
	if(intro_data_database[player->know_index]->owner_id!=player->id){
		ilogf("intro_data_database[player->know_index]->owner_id!=player->id, player=%s, knowindex=%d",
			player->name, player->know_index);
		bugf("intro_data_database[player->know_index]->owner_id!=player->id, player=%s, knowindex=%d",
			player->name, player->know_index);
		// time to get allocated a new index - rerun the function
		player->know_index=0;
		attach_know(player);
		return;
	}

	assert(player->id==intro_data_database[player->know_index]->owner_id);
	assert(player->know_index==intro_data_database[player->know_index]->my_know_index);

	// set player->know to point the correct location etc
	intro_data_database[player->know_index]->assign_owner(player);
};
/**************************************************************************/
intro_node * intro_node::find_know_node_by_index(unsigned short lookup_know_index)
{
	if(!this){
		return NULL;
	}
	if(know_index==lookup_know_index){
		return this;
	}
	intro_node *node=next;
	for(; node; node=node->next){
		if(node->know_index==lookup_know_index){
			return node;
		}
	};
	return NULL;
};
/**************************************************************************/
// return false if they arent new
bool intro_node::add_person_by_index(unsigned short new_person_know_index, intro_data *know)
{
	if(!know){
		ilogf("intro_node::add_person_by_index(unsigned short new_person_know_index, intro_data *know), know==NULL!");
		make_corefile();
		return false;
	}

	if(know->people && know->people->find_know_node_by_index(new_person_know_index)){
		return false; // already known
	}
	// add a new person
	know->load_person(new_person_know_index, current_time);

	return true;
}
/**************************************************************************/
// load/add a relationship about new_person_id knowing 'this'
bool intro_data::load_person(unsigned short new_person_id, time_t last_seen)
{
	// add a new person
	intro_node *node= new intro_node;
	node->know_index=new_person_id;
	node->last_seen=last_seen;
	node->next=people;
	people=node;
	return true;
}

/**************************************************************************/
// Return true if ch knows the target
bool intro_data::knows(char_data *person)
{
	if(IS_NPC(person) || !person->know){
		return false;
	}
	if(!people){
		return false;
	}

	int cache_index=intro_data_database[person->know->my_know_index]->intro_cache_index;
	assert(cache_index); // have to have a cache index.. if they dont, how did they get here?

	// check if we have it cached
	if(IS_SET(intro_cache[cache_index/4], 1<<((cache_index%4)*2))){
		// bit caching is fast :)
		if(IS_SET(intro_cache[cache_index/4], 1<<(((cache_index%4)*2)+1) ) ){
			ilogf("Intro cache hit true for %d looking at %d (%d)", my_know_index, person->know->my_know_index, cache_index);
			return true;
		}

		ilogf("Intro cache hit false for %d looking at %d (%d)", my_know_index, person->know->my_know_index, cache_index);
		return false;
	}
	
	// support cached lookups later?
	intro_node *node= people->find_know_node_by_index(person->know->my_know_index);

	// cache the result
	SET_BIT(intro_cache[cache_index/4], 1<<((cache_index%4)*2)); // we are caching

	ilogf("Recording cache update for %d looking at %d (%d)", my_know_index, person->know->my_know_index, cache_index);

	if(node){
		node->last_seen=current_time; // record so we dont forget ever seeing them
		ilogf("Intro cache miss for %d looking at %d (%d)", my_know_index, person->know->my_know_index, cache_index);

		SET_BIT(intro_cache[cache_index/4], 1<<(((cache_index%4)*2)+1) ); // they do know them (cache it)
		return true;
	}
	REMOVE_BIT(intro_cache[cache_index/4], 1<<(((cache_index%4)*2)+1) ); // they do NOT know them (cache it)
	return false;
};
/**************************************************************************/
// when you are introduced to someone
bool intro_data::introduced_to(char_data *person)
{
	if(!this){
		ilogf("intro_data::introduced_to('%s'), this==NULL!", person?person->name:"null person also");
		return false;
	}
	if(owner->know!=this){
		ilogf("intro_data::introduced_to('%s'), this==NULL!", person?person->name:"null person also");
		return false;
	}
	assert(owner->know==this);

	// update the caching system
	int cache_index=intro_data_database[person->know->my_know_index]->intro_cache_index;
	SET_BIT(intro_cache[cache_index/4], 1<<(((cache_index%4)*2)) ); // they now know them
	SET_BIT(intro_cache[cache_index/4], 1<<(((cache_index%4)*2)+1) ); // they now know them
	ilogf("intro_data::introduced_to(): (introduction) recording cache update for %d knowing %d (%d)", 
		my_know_index, person->know->my_know_index, cache_index);

	return(people->add_person_by_index(person->know->my_know_index, this));
}; 
/**************************************************************************/
// when you choose to forget someone
void intro_data::forgetting(char_data *person)
{
	if(!this){
		ilogf("intro_data::forgetting('%s'), this==NULL!", person?person->name:"null person also");
		return;
	}
	if(owner->know!=this){
		ilogf("intro_data::forgetting('%s'), this==NULL!", person?person->name:"null person also");
		return;
	}
	assert(owner->know==this);

	if(!people){
		return; // if we dont know anyone
	}

	// update the caching system
	int cache_index=intro_data_database[person->know->my_know_index]->intro_cache_index;
	REMOVE_BIT(intro_cache[cache_index/4], 1<<(((cache_index%4)*2)) ); // they now know them
	REMOVE_BIT(intro_cache[cache_index/4], 1<<(((cache_index%4)*2)+1) ); // they now know them
	ilogf("intro_data::forgetting(): (forget) cache cleared for %d not knowing %d (%d)", 
		my_know_index, person->know->my_know_index, cache_index);

	{ // remove them
		ilogf("Removing");
		int mki=person->know->my_know_index;
		intro_node *prev=people;
		if(people->know_index==mki){
			people=people->next;
			delete prev;
			ilogf("delete prev; - removed");
			return;
		}else{
			for(intro_node *delnode=people->next; delnode; delnode=delnode->next){
				if(delnode->know_index==mki){
					prev->next=delnode->next;
					delete delnode;
					ilogf("delete delnode; - removed");
					return;
				}
				prev=delnode;
			}
		}
		ilogf("not removed?!?");
	}
}; 
/**************************************************************************/
// Save the database of 'who knows who' to disk.
// saving is done in 2 stages:
// * the first stage saves the data linking a player id to their 'know index'
// * stage two saves which know indexs know others.
// This means we can discard old know data for deleted players
// when reading in the database.
void save_intro_database()
{
	unsigned short i;
	intro_node *node;
	char filename[MIL];
	sprintf(filename, "%s.write", INTRODB_FILE);

	logf("Saving introduction database to %s", filename);
	FILE *fp=fopen( filename,"w");
	if(!fp){
		bugf("save_intro_database(): Couldn't open file '%s' to save introduction database to!",
			filename);
		return;
	}

	// stage 1
	for(i=10; i<MAX_KNOW_SET; i++){
		if(intro_data_database[i] && intro_data_database[i]->save_in_db){
			// owner player ID is compulsory in the database, since this is 
			// what everything is linked back against in attach_know
			// if an index to player id mapping not saved, the
			// data relating to that node will be dropped reboot

			if(intro_data_database[i]->last_logged_in< current_time- (60*60*24*90)){
				// if you haven't logged on for 90 days you are dropped
				// from the database
				continue;
			}


			fprintf(fp,"-4 %d %d %d\n", 
				i, 
				(int)intro_data_database[i]->owner_id,
				(int)intro_data_database[i]->last_logged_in); 
		}
	}

	// stage 2 - save which indexes know which indexes, and last seen
	for(i=10; i<MAX_KNOW_SET; i++){
		if(intro_data_database[i] && intro_data_database[i]->save_in_db){
			for(node=intro_data_database[i]->people; node; node=node->next){
				if(intro_data_database[node->know_index]){
					fprintf(fp,"%d %d %d\n", i, node->know_index, (int)node->last_seen); 
				}
			}
		}
	}
	int bytes_written=fprintf(fp,"-1\n"); 
	fclose( fp );

	if(   bytes_written != str_len("-1\n") ){
        bugf("save_intro_database(): fprintf to '%s' incomplete - error %d (%s)",
			filename, errno, strerror( errno));
		bugf("Incomplete write of %s, write aborted - check diskspace!", filename);
		autonote(NOTE_SNOTE, "save_intro_database()", 
			"Problems saving class table", "code cc: imm", 
			"Incomplete write of "  INTRODB_FILE ".write, write aborted - check diskspace!\r\n", true);
	}else{		
		logf("Renaming old " INTRODB_FILE " to " INTRODB_FILE ".bak");
		unlink(INTRODB_FILE".bak");
		rename(INTRODB_FILE, INTRODB_FILE".bak");

		logf("Renaming new %s to " INTRODB_FILE,  filename);
		unlink(INTRODB_FILE);
		rename(filename, INTRODB_FILE);
	}
	logf("Finished saving introduction database to %s", INTRODB_FILE);

}
/**************************************************************************/
// called when a character is deleting... we flag their know info so it 
// isn't saved to disk
void intro_player_delete(char_data *player)
{
	if(!player){
		return;
	}

	// bounds check
	assert(player->know_index>0);
	assert(player->know_index<MAX_KNOW_SET);
	
	if(intro_data_database[player->know_index]->owner_id!=player->id){
		ilogf("intro_player_delete(): intro_data_database[player->know_index]->owner_id!=player->id, player=%s, knowindex=%d",
			player->name, player->know_index);
		bugf("intro_player_delete(): intro_data_database[player->know_index]->owner_id!=player->id, player=%s, knowindex=%d",
			player->name, player->know_index);
		// looks like we have some corruption
		do_abort();
		return;
	}

	// do a few double checks
	assert(player->id==intro_data_database[player->know_index]->owner_id);
	assert(player->know_index==intro_data_database[player->know_index]->my_know_index);

	// set player->know to point the correct location etc
	intro_data_database[player->know_index]->save_in_db=false;
	ilogf("intro_player_delete(): player %s(%d)[%d] flagged for intro database removal.",
		player->name, player->id, player->know_index);
	logf("intro_player_delete(): player %s(%d)[%d] flagged for intro database removal.",
		player->name, (int)player->id, player->know_index);
		
}
/**************************************************************************/
void load_intro_database()
{
	logf("Load introduction database from %s", INTRODB_FILE);
	FILE *fp=fopen( INTRODB_FILE,"r");
	memset(&intro_data_database[0], 0, sizeof(intro_data*)* (MAX_KNOW_SET));
	if(!fp){
		bugf("load_intro_database(): Couldn't open file '%s' to load introduction database from!",
			INTRODB_FILE);
		log_note("NOTE: The introduction database stores who knows who, if this is a new mud "
			"then it is expected that this file is missing until a mortal uses the "
			"introduction system.  If this is an existing mud which is making use of the "
			"introduction system, then player introductions will have to be recreated.  "
			"It is safe to delete this file at any stage if required.");
		return;
	}

	int know_index=0;
	while(!feof(fp)){
		know_index=fread_number(fp);

		if(know_index==-1){// end of file marker
			break;
		}

		if(know_index==-2){// comment to end of line
			fread_to_eol(fp); // allow expansion later on/comments
			continue;
		}

		if(know_index==-3 || know_index==-4){// know index to owner id cross reference data (save stage 1)
			int nlast_logged_in;
			bool read_last_login=true;
			if(know_index==-3){
				read_last_login=false;
			}
			know_index=fread_number(fp);

			if(know_index<10 || know_index>MAX_KNOW_SET){
				bugf("load_intro_database(): -3 know_index %d invalid!"
					"- not within level valid range... exiting!", know_index);
				exit_error( 1 , "load_intro_database", "-3 know_index invalid");
			}

			int nid=fread_number(fp);
			if(!intro_data_database[know_index]){
				intro_data_database[know_index]=new intro_data;	
			}

			if(!intro_data_database[know_index]){
				intro_data_database[know_index]=new intro_data;	
			}
			intro_data_database[know_index]->owner_id=nid;
			intro_data_database[know_index]->my_know_index=know_index;

			if(read_last_login){
				nlast_logged_in=fread_number(fp);
			}else{ // set a default value
				nlast_logged_in=(int)current_time;
			}
			intro_data_database[know_index]->last_logged_in=nlast_logged_in;

			fread_to_eol(fp); // allow expansion later on/comments
			continue;
		}

		// below here is assumed to be a result of stage 2 saving
		if(know_index<10 || know_index>MAX_KNOW_SET){
			bugf("load_intro_database(): know_index %d invalid!"
				"- not within level valid range... exiting!", know_index);
			exit_error( 1 , "load_intro_database", "know_index invalid");
		}
		int nindex=fread_number(fp);
		int nlastseen=fread_number(fp);
		fread_to_eol(fp); // allow expansion later on/comments

		// only if both people involved still exist, will it 
		// accept the know info
		if(intro_data_database[know_index] && intro_data_database[nindex]){ 
			intro_data_database[know_index]->load_person(nindex, nlastseen);
		}
	}
	if(feof(fp) && know_index!=-1){
		logf("load_intro_database(): load incomplete, exiting!");
		exit_error( 1 , "load_intro_database", "load incomplete");
	}

	logf("load_intro_database(): Load complete.");
	fclose(fp);
}
/**************************************************************************/
/**************************************************************************/