/**
* @main
* This is a generalised hospital inheritable for the creation of NPCs.
* <p>
* The NPCs within the hospital are defined in the configuration files which
* are parsed by the hospital. The configuration files can define zones,
* groups and npcs.
* <p>
* A zone is a region of the mud. It can have any name you choose and is then
* used as the first parameter to load_random_npc(). A zone may be made up of
* other zones, groups and npcs.
*
* <p>
* A group is a group of NPCs. It may be made up of other groups and npcs. A
* group has a name by which it is referenced by zones and other groups.
* <p>
* An npc is the definition of a specific type of NPC.
*
* @example
* A typical hospital setup would look like this:
*
* void setup() {
* set_save_file(SAVE + "/hospital");
* set_data_directory(DATA + "/hospital");
* set_log_directory(LOG + "/hospital");
* set_npc_path(CHARS + "/hospital");
* set_not_replaceable(1);
*
* set_short("Our hospital");
* set_long("This is the hospital.\n");
* }
*
* @example
* These are examples of zones, groups & npcs.
*
* (zone
* (name "desert")
* (busy 5)
* (group
* (name "vultures")
* (chance 1)
* )
* (npc
* ("name "lizard")
* (chance 1)
* )
* )
*
* (group
* (name "vultures")
* (transient "true")
* (npc
* (name "vulture")
* (quantity 1 + 1d5)
* )
* )
*
* (npc
* (name "vulture")
* (path "vulture")
* (transient true)
* )
* (npc
* (name "lizard")
* (path "lizard")
* (transient true)
* (diurnal true)
* )
*
* The above configuration file defines a zone "desert". The zone has a
* business factor of 5 which means rooms in the zone will typically get a
* chance at cloning npcs 5% of the time (every 20 rooms).
* <p>
* The zone is made up of a group called "vultures" and an npc called
* "lizard".
* <p>
* The vultures group is transient (ie. the NPCs will be removed from the
* room when the player leaves the room, rather like wandering monsters) and
* contains 1 + 1d5 "vulture" npcs.
* <p>
* The vulture NPC is transient and can be found in the filename "vulture.c" in
* the path defined by set_npc_path(). The lizard NPC is also transient and
* diurnal (it only comes out during the day) and can be found in the filename
* "lizard.c" in the path defined by set_npc_path().
*
* <p>
* @author Taffyd
* @changed Ceres November 14th 2002<br>
* Altered to work as a hospital for terrain NPCs especially the transient
* ones. It now handles transient npcs as well as conditions for
* diurnal, nocturnal and seasonal npcs.
* @changed Pinkfish March 10th<br>
* Major rewrite of bits to make it use a lisp like data file format.
* @changed Carmine April 9th<br>
* Added some alignment-generation code.
* @started December 3, 1998.
* @index hospital
*/
#include <armoury.h>
#include <data.h>
#include <weather.h>
/* Comment this out if you want to disable hospital generation
of NPC's through the new data compiler system. */
#define CREATE_NPCS 1
// #define REGENERATE_AFTER_DEATH
#define REGEN_TIME 1 * 60 * 60
#define DEFAULT_MAX_CACHE 10
#define DEFAULT_LOG_DIRECTORY "/log"
#define DEFAULT_POPULATION 3
#define MINIMUM_DEATH_REGEN 3
//
// How often we recheck the population to make sure 0's etc have not snuck
// in.
//
#define POPULATION_UPDATE_TIME 15 * 60
/*
* These defines set the shape of the distribution of alignments handed out
* by pick_al. Using the standard normal distribution, split into ten steps
* on each side of the average, we can safely disregard the probability of
* values outside of the range of -3.0 to 3.0 (99.7% of values will fall
* within this range of three * standard deviation from the average).
* Range Prob.
* P( 0.0 < z < 0.3 ) = 0.1179
* P( 0.3 < z < 0.6 ) = 0.1078
* P( 0.6 < z < 0.9 ) = 0.0902
* P( 0.9 < z < 1.2 ) = 0.0690
* P( 1.2 < z < 1.5 ) = 0.0483
* P( 1.5 < z < 1.8 ) = 0.0309
* P( 1.8 < z < 2.1 ) = 0.0180
* P( 2.1 < z < 2.4 ) = 0.0097
* P( 2.4 < z < 2.7 ) = 0.0047
* P( 2.7 < z ) = 0.0035
*
* These probabilites are then converted into cumulative limits out of
* a total chance, as integers:
*/
#define TOTAL_CHANCE 150
#define CENTRE 0
#define LIMIT_ONE 35
#define LIMIT_TWO 68
#define LIMIT_THREE 95
#define LIMIT_FOUR 115
#define LIMIT_FIVE 130
#define LIMIT_SIX 139
#define LIMIT_SEVEN 145
#define LIMIT_EIGHT 147
#define LIMIT_NINE 149
inherit "/std/room";
class regenerated_npc {
string type;
string load_position;
}
/**
* This class stores all the information about a zone.
* @param wealth The wealthiness of the zone (unused)
* @param busy The business of the zone. ie. how frequently NPCs should be
* created.
* @param npc_chance The total chances for an npc in this zone.
* @param npcs A list of the NPCs that are members of this zone.
* @param group_chance The total chances for a group in this zone.
* @param groups A list of the groups of NPCs in this zone.
* @param zone_chance The total chances for a zone in this zone.
* @param zones A list of the zones within this zone.
*/
class zone_info {
int wealth;
int busy;
int npc_chance;
mapping npcs;
int group_chance;
mapping groups;
int zone_chance;
mapping zones;
}
/**
* This class stores all the information about unique NPCs.
* @param next_regen_time When this npc will next regenerate.
*/
class unique_npc {
int next_regen_time;
}
/**
* This class stores all the information about a particular NPC.
*
* @param path The relative path of the NPC
* @param unique Is the NPC unique?
* @param delay The regeneration delay for this NPC
* @param transient If this NPC is transient.
* @param nocturnal If this NPC is nocturnal.
* @param diurnal If this NPC is diurnal.
* @param seasonal A list of seasons during which this NPC is active.
* @param max_population How many of these NPCs can exist at a time.
* @param move_zones The move zones for this NPC
* @param commands Commands to be executed by the NPC when it is created.
* @param population A cache of the current population
* @param no_deaths The number of recorded deaths
* @param no_created The number of these NPCs we've created
* @param no_reused How many of these NPCs have been reused (recycled)
* @param next_update When the population data will next be updated.
*/
class npc_info {
string path;
int unique;
int delay; // used by unique npcs
int transient;
int nocturnal;
int diurnal;
string* seasonal;
int max_population;
string* move_zones;
string* commands;
// Tracking information
object* population;
int no_deaths;
int no_created;
int no_reused;
int next_update;
}
/**
* This class stores all the information about an NPC group.
*
* @param move_zones The move zones for this NPC
* @param npcs A list of the NPCs within this group.
* @param npc_commands A list of commands to be executed on each of the group
* members when the group is created.
* @param protect Should these group members protect each other.
* @param defend Should these group members defend each other.
* @param transient Should these group members be considered transient.
* @param max_population How many of these NPCs can exist at a time.
* @param storage A cache of the population of NPCs in this group.
* @param no_creates The number of these NPCs we've created
* @param next_update When the population data will next be updated.
*/
class group_info {
string* move_zones;
// This is a list of npcs with a name as the key and a quantity as the value.
mapping npcs;
mapping npc_commands;
int protect;
int defend;
int transient;
int max_population;
// Tracking details.
mixed* storage;
int no_creates;
int next_update;
}
object* load_random_npc(string zone, int wealth, int busy);
protected void load_file();
protected void save_file();
protected void set_save_file(string new_save);
public void hospital_log_file(string file, string format, mixed *args ...);
public int get_item(object destination, string *items);
public string query_save_file();
public int *query_npcs();
public mapping query_hospital_npcs();
public void add_npc_type(string type, string *data);
public void regen_after_death( object dead_npc );
private nosave string _save_file;
private nosave string _hospital_type;
private nosave mixed *_regenerated_npcs;
private nosave int _hospital_call_id;
private nosave mapping _log_file_info;
private nosave string _hospital_log_directory;
private nosave string _data_directory;
private nosave string _npc_path;
private nosave string _domain;
private nosave int _max_cache;
// Used when the system is in debug mode.
private nosave int _disable_npc_generation;
private mapping _unique_npcs;
private mapping _hospital_npcs;
private mapping _cache_inventory;
private int _last_npc_check;
private int _zone_npcs;
private mapping _group_info;
private mapping _npc_info;
private mapping _zone_info;
private mapping _path_to_npc;
// Details on the files to load.
private mapping _file_modified_time;
/** @ignore yes */
void create() {
seteuid(master()->creator_file(file_name(this_object())));
do_setup++;
::create();
do_setup--;
if (!_group_info) {
_group_info = ([ ]);
}
if (!_npc_info) {
_npc_info = ([ ]);
}
if (!_zone_info) {
_zone_info = ([ ]);
}
if (!_path_to_npc) {
_path_to_npc = ([ ]);
}
if (!_file_modified_time) {
_file_modified_time = ([ ]);
}
if (!_unique_npcs) {
_unique_npcs = ([ ]);
}
if (!_cache_inventory) {
_cache_inventory = ([ ]);
}
_regenerated_npcs = ({ });
_log_file_info = ([ ]);
if(!_max_cache)
_max_cache = DEFAULT_MAX_CACHE;
if (!do_setup) {
set_short( "Un-configured Hospital" );
set_long("This is an unconfigured hospital. Find the right one.\n");
add_property("determinate", "the ");
set_light(60);
// Setup extra stuff if they wish
this_object()->setup();
}
} /* create() */
/** @ignore yes */
int ok_to_clone() {
return 1;
}
/**
* This function produces a random alignment for an npc, based on a
* normal distribution around a supplied average, with a maximum and
* minimum limit provided by a requested range.
* <p>
* The alignment produced will be within the range[ average - range,
* average + range ]
* @param average The average alignment for this npc
* @param range The range request for alignment - the difference between
* average and maximum possible alignment
* <p>
*/
int pick_al( int average, int range ) {
int choice, section, section_width, result;
// Must be a positive range.
if( range < 0 ) {
range = -range;
}
// Must have a range of 10 at least.
if( range < 10 ) {
range = 10;
}
section_width = floor( range / 10 );
choice = random( TOTAL_CHANCE );
switch( choice ) {
case CENTRE..LIMIT_ONE:
section = 0;
break;
case ( LIMIT_ONE + 1 )..LIMIT_TWO:
section = 1;
break;
case ( LIMIT_TWO + 1 )..LIMIT_THREE:
section = 2;
break;
case ( LIMIT_THREE + 1 )..LIMIT_FOUR:
section = 3;
break;
case ( LIMIT_FOUR + 1 )..LIMIT_FIVE:
section = 4;
break;
case ( LIMIT_FIVE + 1 )..LIMIT_SIX:
section = 5;
break;
case ( LIMIT_SIX + 1 )..LIMIT_SEVEN:
section = 6;
break;
case ( LIMIT_SEVEN + 1 )..LIMIT_EIGHT:
section = 7;
break;
case ( LIMIT_EIGHT + 1 )..LIMIT_NINE:
section = 8;
break;
default:
section = 9;
break;
}
result = section * section_width;
result = result + random( section_width );
switch( random( 2 ) ) {
case( 0 ):
result = average + result;
break;
default:
result = average - result;
break;
}
result = result + average;
return result;
} /* pick_al */
/**
* This function returns an array of the gods in Ankh-Morpork
* <p>
*/
public string *query_deities() {
return ({ "fish", "pishe", "sek", "hat", "gapp",
"gufnork", "sandelfon" });
}
/**
* This function provides an interface to the armoury, selecting
* an item at random from the 'items' array and attempting to
* clone it.
* <p>
* Any items of jewellery, clothing, weapons, armour or scabbards
* that are available in the armoury can be accessed using this
* function.
* <p>
* @param destination where to place the item that is fetched
* from the armoury.
* @param items an array of items, of which one will be selected,
* cloned, and then moved to destination.
* <p>
* @see /obj/handlers/armoury
* @return 1 if the item was successfully created, 0 if it was not.
*/
public int get_item(object destination, string *items) {
int i;
object item;
if (!objectp(destination)) {
return 0;
}
while (!item) {
i = random(sizeof(items));
item = ARMOURY->request_item(items[i], 80 + random(21), _domain);
if (!item) {
item = ARMOURY->request_item(items[i], 80 + random(21) );
if ( !item ) {
items = items[0..i-1] + items[i+1..];
continue;
}
}
if (!sizeof(items)) {
hospital_log_file("BROKEN_ITEMS", "Unable to select any items "
"for %s in the Ankh-Morpork hospital.\n",
previous_object()->short());
break;
}
}
if (objectp(item)) {
item->move(destination);
return 1;
}
return 0;
} /* get_item() */
/** @ignore yes */
protected void load_file() {
class npc_info npc;
class group_info group;
string name;
if (file_size(_save_file + ".o") > -1) {
unguarded((: restore_object, _save_file :));
if (!_group_info) {
_group_info = ([ ]);
} else {
// Zap all the population data
foreach (name, group in _group_info) {
group->storage = 0;
}
}
if (!_npc_info) {
_npc_info = ([ ]);
} else {
// Zap all the population data
foreach (name, npc in _npc_info) {
npc->population = ({ });
}
}
if (!_zone_info) {
_zone_info = ([ ]);
}
if (!_path_to_npc) {
_path_to_npc = ([ ]);
}
if (!_file_modified_time) {
_file_modified_time = ([ ]);
}
if (!_unique_npcs) {
_unique_npcs = ([ ]);
}
}
} /* load_file() */
/** @ignore yes */
protected void save_file() {
unguarded((: save_object, _save_file :));
} /* save_file() */
/**
* This method is used to set the save file for the hospital. This
* file is where all of the hospitals NPC data is stored while it is
* not active and in between reboots.
* <p>
* If this file does not exist when the hospital is loaded, then the
* variables will be initialised to their defaults and
* <b>restore_default_npcs()</b> called.
* <p>
* @param new_save the file name to save the hospital data in
*/
protected void set_save_file(string new_save) {
_save_file = new_save;
load_file();
} /* set_save_file() */
/**
* This returns the location where hospital data will be stored.
*
* @see set_save_file()
*/
public string query_save_file() {
return _save_file;
} /* query_save_file() */
/**
* This method sets the directory to scan for new files. The data directory
* stores the configuration files for the hospital that define the zones,
* groups and npcs.
*
* @param dir the directory to scan for new files
*/
void set_data_directory(string dir) {
_data_directory = dir;
call_out("scan_for_new_data", 2);
}
/**
* This method returns the directory to scan for new files.
*
* @return the directory to scan new data
* @see set_data_directory()
*/
string query_data_directory() {
return _data_directory;
}
/**
* This method sets the directory for npcs. The filename for npcs defined
* in the configuration files is realtive to this path.
*
* @param dir the director for npcs.
*/
void set_npc_path(string path) {
_npc_path = path;
}
/**
* This method sets the domain string used when requesting things
* from the hospital.
*
* @param domain the domain name
*/
void set_domain(string domain) {
_domain = domain;
}
/**
* This method sets the maximum number of transient npcs of a given type
* that the hospital will cache.
*
* @param num the maximum number
*/
void set_max_cache(int num) {
_max_cache = num;
}
/**
* This method sets the flag which enables or disables npc generation. This
* is mostly used for debugging.
*
* @param value the new value of the flag
*/
void set_disable_npc_generation(int value) {
_disable_npc_generation = value;
}
/**
* This method returns the flag that disables the npc generation.
*
* @return the flag that disables the npc generation
* @see set_disable_npc_generation
*/
int query_disable_npc_generation() {
return _disable_npc_generation;
}
/**
* This method returns whether or not it is time to regenerate a
* unique NPC. The regeneration time is controlled by the REGEN_TIME
* #define.
* <p>
* @param who this is the name or object reference of the NPC
* @return 1 if it is time to make the unique NPC, or 0 if it is not.
*/
public int make_unique(mixed who) {
int delay;
if (!_unique_npcs) {
return 0;
}
if (objectp(who)) {
who = who->query_name();
}
if (!_unique_npcs[who]) {
_unique_npcs[who] = new(class unique_npc);
}
if ( _unique_npcs[who]->next_regen_time > time() ) {
return 0;
}
if (_npc_info[who] && _npc_info[who]->delay) {
delay = _npc_info[who]->delay;
} else {
delay = REGEN_TIME;
}
_unique_npcs[who]->next_regen_time = time() + delay;
save_file();
return 1;
} /* make_unique() */
/**
* This method resets the unique status of the npc so it can be tested for
* loading over again.
* @param npc the name of the npc
*/
public void reset_unique(string who) {
if (!_unique_npcs[who]) {
return ;
}
_unique_npcs[who]->next_regen_time = 0;
}
#ifdef REGENERATE_AFTER_DEATH
/**
* This method is called by the npc.death effect after an NPC that is handled
* by the hospital has died. It tells the room where it was generated to
* make a new NPC and to all sorts of funky things.
* <p>
* This is not fully implemented yet.
*/
public void regen_after_death( object dead_npc ) {
object *monsters, destination;
class regenerated_npc new_npc;
int i, max;
if ( !dead_npc )
return;
_regenerated_npcs += ({ new( class regenerated_npc,
type : dead_npc->query_property("monster_type"),
load_position : previous_object()->query_property("start location") ) });
if ( !ok_to_clone(_hospital_type) || sizeof(_regenerated_npcs) < MINIMUM_DEATH_REGEN )
return;
new_npc = _regenerated_npcs[0];
if ( !classp( new_npc ) )
return;
_regenerated_npcs = _regenerated_npcs[1..];
if (!new_npc->load_position || !new_npc->type );
return;
max = random(5);
for ( i = 0; i < max; i++ ){
destination = find_object( new_npc->load_position );
if ( !objectp( destination ) )
return;
monsters = get_monster(new_npc->type);
destination->replace_monster(previous_object(), monsters);
call_out((: $1->announce_entry($2) :), 8 + random(6), destination,
monsters);
}
} /* regen_after_death() */
#endif
/**
* This method expands the path for the given npc by combining the npc_path
* set in set_npc_path with the relative path defined in the configuration
* file.
*
* @param path the path to expand
* @return the expanded path
*/
string npc_path(string str) {
return _npc_path + "/" + str;
}
/**
* This method provides log_file() functionality but for /d/am/log
* instead. You should use this method to record errors and whatever
* that are domain specific instead of using log_file().
* @example
* hospital_log_file("BAD_HOSPITAL_NPC", "%s: Bad NPC hospital data, %O.\n",
* ctime(time()), _regenerated_npcs);
* // This would log to /d/am/log/BAD_HOSPITAL_NPC.
* @param file the file name to log to. This will have /d/am/log/
* inserted before it.
* @param format the text to log, can contain sprintf() format specifiers.
* @param args the arguments for any sprintf format thingies
*/
public void hospital_log_file(string file, string format, mixed *args ...) {
string filename;
filename = _hospital_log_directory;
/* We are restricted to logging in our domain, let's be neat about it. */
if (file[0] == '/') {
file = file[ strsrch( file, "/", -1 ) + 1 .. ];
}
/* Link to sefun */
log_file( _hospital_log_directory + "/" + file, ctime(time()) + ": " + format, args ...);
} /* hospital_log_file() */
/**
* This method sets the directory in to which hospital_log_file will
* log by default. If the directory does not exist, then it is set to
* DEFAULT_LOG_DIRECTORY.
* @param new_dir the directory to log to.
* @example
* set_log_directory( "/d/cwc/Bes_Pelargic/log" );
* // log directory is now /d/cwc/Bes_Pelargic/log
* @example
* // (cre) MacChirton: ithinkmykeyboardisbroken
* set_log_directory( "/d/cWc/BeS_pelarGic/lrg" );
* // Displays: "/d/cwc/hospital/bes_pelargic: Invalid log
* // directory, defaulting to /log"
*/
protected void set_log_directory(string new_dir) {
if (file_size(new_dir) != -2) {
tell_creator( previous_object( 1 ), "%s: Invalid "
"log directory -- defaulting to %s.\n",
file_name( this_object() ), DEFAULT_LOG_DIRECTORY );
new_dir = DEFAULT_LOG_DIRECTORY;
}
_hospital_log_directory = new_dir;
} /* set_log_directory() */
/** @ignore yes */
private void parse_zone(string fname, mapping data) {
string name;
class zone_info info;
mapping bing;
int chance;
string* bits;
if (!data["name"]) {
debug_printf("No name for zone %O\n", data);
hospital_log_file("COMPILE_ERROR", "No name for zone %O\n", data);
return 0;
}
name = data["name"];
if (data["group"] && !arrayp(data["group"])) {
data["group"] = ({ data["group"] });
} else if (!arrayp(data["groups"])) {
data["group"] = ({ });
}
if (data["npc"] && !arrayp(data["npc"])) {
data["npc"] = ({ data["npc"] });
} else if (!arrayp(data["npc"])) {
data["npc"] = ({ });
}
if (data["zone"] && !arrayp(data["zone"])) {
data["zone"] = ({ data["zone"] });
} else if (!arrayp(data["zone"])) {
data["zone"] = ({ });
}
info = new(class zone_info, npcs : ([ ]), groups : ([ ]), zones : ([ ]));
chance = 0;
foreach (bing in data["npc"]) {
if (!mapp(bing) || !stringp(bing["name"]) || !intp(bing["chance"])) {
debug_printf("Invalid data for zone (%s) %O\n", name, bing);
hospital_log_file("COMPILE_ERROR", "Invalid data for zone (%s) %O\n", name, bing);
} else {
info->npcs[bing["name"]] = bing["chance"];
chance += bing["chance"];
}
}
info->npc_chance = chance;
chance = 0;
foreach (bing in data["group"]) {
if (!mapp(bing) || !stringp(bing["name"]) || !intp(bing["chance"])) {
debug_printf("Invalid data for zone (%s) %O\n", name, bing);
hospital_log_file("COMPILE_ERROR", "Invalid data for zone (%s) %O\n", name, bing);
} else {
info->groups[bing["name"]] = bing["chance"];
chance += bing["chance"];
}
}
info->group_chance = chance;
chance = 0;
foreach (bing in data["zone"]) {
if (!mapp(bing) || !stringp(bing["name"]) || !intp(bing["chance"])) {
debug_printf("Invalid data for zone (%s) %O\n", name, bing);
hospital_log_file("COMPILE_ERROR", "Invalid data for zone (%s) %O\n", name, bing);
} else {
info->zones[bing["name"]] = bing["chance"];
chance += bing["chance"];
}
}
info->zone_chance = chance;
info->busy = data["busy"];
bits = keys(data) - ({ "zone", "group", "npc", "name", "busy" });
if (sizeof(bits)) {
debug_printf("Unknown keys %s in zone %s\n", query_multiple_short(bits),
name);
hospital_log_file("COMPILE_ERROR", "Unknown keys %s in %s\n",
query_multiple_short(bits), name);
}
_zone_info[name] = info;
save_file();
} /* parse_zone() */
private void parse_npc(string fname, mapping data) {
string name;
class npc_info info;
string* bits;
if (!data["name"]) {
debug_printf("No name for npc %O\n", data);
hospital_log_file("COMPILE_ERROR", "No name for npc %O\n", data);
return 0;
}
name = data["name"];
if (!data["path"]) {
hospital_log_file("COMPILE_ERROR", "Required path field not present for npc %s\n", name);
return 0;
}
if (file_size(npc_path(data["path"]) + ".c") < 0) {
hospital_log_file("BAD_NPC_PATH", "Bad path for npc %s (%s)\n", name, data["path"]);
}
if (data["command"] && !arrayp(data["command"])) {
data["command"] = ({ data["command"] });
}
if (data["move_zone"] && !arrayp(data["move_zone"])) {
data["move_zone"] = ({ data["move_zone"] });
}
info = new(class npc_info, move_zones : ({ }), commands : ({ }));
info->unique = data["unique"];
info->path = data["path"];
info->delay = data["delay"];
info->max_population = data["population"];
if(data["transient"])
info->transient = 1;
if(data["nocturnal"])
info->nocturnal = 1;
if(data["diurnal"])
info->diurnal = 1;
if(data["seasonal"]) {
if(!arrayp(data["group"]))
info->seasonal = ({ data["seasonal"] });
else
info->seasonal = data["seasonal"];
}
if (data["move_zone"]) {
info->move_zones = data["move_zone"];
}
if (data["command"]) {
info->commands = data["commands"];
}
bits = keys(data) - ({ "unique", "path", "move_zone", "population", "name", "delay", "transient", "nocturnal" });
if (sizeof(bits)) {
debug_printf("Unknown keys %s in npc %s\n", query_multiple_short(bits),
name);
hospital_log_file("COMPILE_ERROR", "Unknown keys %s in npc %s\n",
query_multiple_short(bits), name);
}
if (_path_to_npc[info->path] && _path_to_npc[info->path] != name) {
hospital_log_file("NPC_NAME_CLASH", "NPC path %s is used by %s and %s.\n",
info->path, name, _path_to_npc[info->path]);
}
_path_to_npc[info->path] = name;
if (_npc_info[name]) {
info->population = _npc_info[name]->population;
info->no_created = _npc_info[name]->no_created;
info->no_reused = _npc_info[name]->no_reused;
info->no_deaths = _npc_info[name]->no_deaths;
}
_npc_info[name] = info;
save_file();
} /* parse_npc() */
/** @ignore yes */
private void parse_group(string fname, mapping data) {
string name;
class group_info info;
mapping bing;
int chance;
string* bits;
if (!data["name"]) {
debug_printf("No name for group %O\n", data);
hospital_log_file("COMPILE_ERROR", "No name for group %O\n", data);
return 0;
}
name = data["name"];
if (data["npc"] && !arrayp(data["npc"])) {
data["npc"] = ({ data["npc"] });
} else if (!arrayp(data["npc"])) {
data["npc"] = ({ });
}
info = new(class group_info, npcs : ([ ]), move_zones : ({ }),
npc_commands : ([ ]));
chance = 0;
foreach (bing in data["npc"]) {
if (!mapp(bing) || !stringp(bing["name"]) ||
(!intp(bing["quantity"]) && !classp(bing["quantity"]))) {
debug_printf("Invalid data for group (%s) %O\n", name, bing);
hospital_log_file("COMPILE_ERROR", "Invalid data for group (%s) %O\n", name, bing);
} else {
info->npcs[bing["name"]] = bing["quantity"];
if (bing["command"]) {
if (arrayp(bing["command"])) {
info->npc_commands[bing["name"]] = bing["command"];
} else {
info->npc_commands[bing["name"]] = ({ bing["command"] });
}
}
}
}
if (data["move_zone"]) {
info->move_zones = data["move_zone"];
}
info->max_population = data["population"];
info->protect = data["protect"];
info->defend = data["defend"];
info->transient = data["transient"];
bits = keys(data) - ({ "npc", "name", "population", "move_zone", "protect", "defend", "transient", });
if (sizeof(bits)) {
debug_printf("Unknown keys %s in group %s\n", query_multiple_short(bits),
name);
hospital_log_file("COMPILE_ERROR", "Unknown keys %s in group %s\n",
query_multiple_short(bits), name);
}
if (_group_info[name]) {
info->storage = _group_info[name]->storage;
}
_group_info[name] = info;
} /* parse_group() */
/**
* This is called by the data compiler when the system has
* finished compiling the data file.
* @param data the data to deal with
*/
protected void finish_compiling(string fname, mapping data) {
mixed* bits;
mapping thing;
string name;
foreach (name, bits in data) {
switch (name) {
case "zone" :
foreach (thing in bits) {
parse_zone(fname, thing);
}
break;
case "npc" :
foreach (thing in bits) {
parse_npc(fname, thing);
}
break;
case "group" :
foreach (thing in bits) {
parse_group(fname, thing);
}
break;
default :
debug_printf("Unknown data area %s in %s\n", name, fname);
hospital_log_file("COMPILE_ERROR", "Unknown data area %s in %s\n", name, fname);
break;
}
}
_file_modified_time[fname] = unguarded( (: stat($(fname)) :))[1];
save_file();
}
/**
* This method scans through all the files and sees if anything has
* changed.
*/
void scan_for_new_data() {
string *files;
string fname;
string *bits;
debug_printf("Scan for new data.");
if (unguarded( (: stat(__FILE__)[1] :)) !=
_file_modified_time[__FILE__]) {
_file_modified_time = ([ ]);
}
files = ({ _data_directory });
while (sizeof(files)) {
fname = files[0];
files = files[1..];
if (unguarded( (: file_size($(fname)) :)) == -2) {
// Directory!
bits = get_dir(fname + "/");
if (bits) {
bits -= ({ "RCS", ".", ".." });
bits = filter(bits, (: $1[0] != '.' :));
files += map(bits, (: $2 + "/" + $1 :), fname);
}
} else {
if (unguarded( (: stat($(fname)) :))[1] != _file_modified_time[fname]) {
debug_printf("Compiling %s", fname);
DATA_HANDLER->compile_file(fname,
(: finish_compiling :));
}
}
}
_file_modified_time[__FILE__] =
unguarded( (: stat(__FILE__)[1] :));
} /* scan_for_new_data() */
/**
* This method rolls the dice, if a dice is specified.
* @param dice the dice or number
* @return the actual number
*/
int roll_dice(mixed die) {
int result;
int i;
if (intp(die)) {
return die;
}
if (classp(die)) {
for (i = 0; i < die->number; i++) {
result += random(die->die) + 1;
}
result += die->modifier;
return result;
}
return 0;
}
/**
* This method returns the maximum population of this npc.
* @param npc the name of the npc
* @return the npc population
*/
int query_npc_max_population( string npc) {
if (!_npc_info[npc]) {
return -1;
}
return ((class npc_info)_npc_info[npc])->max_population;
} /* query_npc_max_population() */
/**
* This method returns the current population of this npc
* @param npc the name of the npc
* @return the npc population
*/
int query_npc_current_population( string npc, int clean) {
class npc_info data;
if (!_npc_info[npc]) {
return -1;
}
data = _npc_info[npc];
if (!data->population) {
data->population = children(npc_path(data->path));
data->next_update = time() + POPULATION_UPDATE_TIME;
}
if (clean || data->next_update < time()) {
data->population = filter(data->population, (: objectp($1) :));
data->next_update = time() + POPULATION_UPDATE_TIME;
}
return sizeof(data->population);
} /* query_npc_current_population() */
/**
* THis method checks to see if the number of npcs of this
* type is under the maximum population.
* @param npc the npc to check to see if it is under population max
* @return 1 if is under, 0 if is not
*/
int is_npc_under_max_population(string name) {
class npc_info info;
info = _npc_info[name];
if (!info) {
return 0;
}
// There are no population limits for transient NPCs since they
// are (theoretically) destroyed as soon as the players leave their
// room.
if(info->transient)
return 1;
if (info->max_population) {
return query_npc_current_population(name, 1) < info->max_population;
}
return 1;
} /* is_npc_under_max_population() */
/**
* This method checks to see if the number of npcs of this
* type is under the maximum population.
* @param npc the npc to check to see if it is under population max
* @return 1 if is under, 0 if is not
*/
int is_group_under_max_population(string name) {
class group_info group;
group = _group_info[name];
if (!group) {
return 0;
}
if (!group->max_population || group->transient) {
return 1;
}
if (!group->storage) {
group->storage = ({ });
} else if (group->next_update < time()) {
group->storage = map(group->storage, (: filter($1, (: objectp($1) :)) :));
group->storage = filter(group->storage, (: sizeof($1) :));
group->next_update = time() + POPULATION_UPDATE_TIME;
}
/*
* If we have more NPC's than the population, then stop right now.
*/
if (sizeof(group->storage) >= group->max_population) {
return 0;
}
return 1;
} /* is_group_under_max_population() */
/**
* This method loads the specific npc. It will do everything it needs
* to create the npc. In the case of unique npcs they are not cloned.
* @param npc_name the name of the npc to load
* @return the npc object
*/
public object load_npc_object(string npc_name) {
class npc_info info;
object npc;
string zone;
string cmd;
if ( undefinedp(_npc_info[npc_name])) {
debug_printf("Unable to find npc %O", npc_name);
return 0;
}
info = _npc_info[npc_name];
// Validity checks for given NPCs.
if(info->diurnal && !WEATHER->query_day(previous_object()))
return 0;
if(info->nocturnal && !WEATHER->query_day(previous_object()))
return 0;
if(info->seasonal &&
member_array(WEATHER->query_season(), info->seasonal) == -1)
return 0;
if (stringp(info->path)) {
if ( info->unique ) {
npc = find_object( npc_path(info->path) );
if ((!npc || !environment(npc)) && make_unique(npc_name)) {
debug_printf("Unique npc %O", info);
if (catch(npc = load_object( npc_path(info->path) ))) {
hospital_log_file("BAD_NPC_PATH", "Error loading %s (%s)\n", npc_name, info->path);
return 0;
}
if (!objectp(npc) || npc == 0) {
hospital_log_file("BAD_NPC_PATH", "Failed to load %s (%s)\n", npc_name, info->path);
return 0;
}
/* If the NPC has an environment, then we have to
bail because the NPC is already loaded somewhere. */
if ( environment( npc ) ) {
debug_printf("NPC unique and already exists %O", npc_name);
return 0;
}
}
} else if(info->transient) {
if(!_cache_inventory)
_cache_inventory = ([ ]);
else if(_cache_inventory[npc_name])
_cache_inventory[npc_name] -= ({ 0 });
if(sizeof(_cache_inventory[npc_name])) {
npc = _cache_inventory[npc_name][0];
_cache_inventory[npc_name] -= ({ npc });
info->no_reused++;
} else {
npc = clone_object( npc_path(info->path) );
if(npc) {
npc->add_property("transient", 1);
npc->add_property("npc_id", npc_name);
npc->add_property("hospital", base_name(this_object()));
info->no_created++;
} else
debug_printf("Failed to clone %s", npc_path(info->path));
}
} else {
if (is_npc_under_max_population(npc_name)) {
npc = clone_object( npc_path(info->path) );
info->no_created++;
if (!npc) {
debug_printf("Npc path does not exist %O (%O)", npc, npc_path(info->path));
}
} else {
debug_printf("NPC over max population %O", npc_name);
}
}
} else {
debug_printf("Broken npc data %O", info->path);
return 0;
}
if (npc && !info->transient) {
if( info->population == 0 )
info->population = ({ });
info->population += ({ npc });
npc->add_property("npc_id", npc_name);
foreach (zone in info->move_zones) {
npc->add_move_zone(zone);
}
foreach (cmd in info->commands) {
npc->init_command(cmd, 2);
}
}
return npc;
} /* load_npc_object() */
/**
* This method creates the npcs for the specificed group of npcs.
* @param group_name the name of the group
* @return the npcs
*/
object *load_group_npcs(string group_name) {
class group_info group;
int quantity;
int i;
object *npcs;
string npc_name;
object ob;
object ob_bing;
string cmd;
if ( undefinedp(_group_info[group_name]) ||
!is_group_under_max_population(group_name)) {
return 0;
}
group = _group_info[group_name];
/* Determine the population from the class, if this is undefined
* then we make set it to DEFAULT_POPULATION.
*/
npcs = ({ });
foreach (npc_name, quantity in group->npcs) {
quantity = roll_dice(quantity);
for (i = 0; i < quantity; i++) {
// If we have a problem loading one. Dest them all.
ob = load_npc_object(npc_name);
if (!ob) {
npcs->move("/room/rubbish");
return ({ });
}
npcs += ({ ob });
if (group->npc_commands[npc_name]) {
foreach (cmd in group->npc_commands[npc_name]) {
ob->init_command(cmd, 2);
}
}
}
}
if (!group->storage) {
group->storage = ({ });
}
group->storage += ({ npcs });
npcs->add_property("group_id", group_name);
// Do any extra stuff on the npcs.
foreach (ob in npcs) {
foreach (ob_bing in npcs) {
if (ob != ob_bing) {
ob->add_follower(ob_bing);
if (group->defend) {
ob->add_defender(ob_bing);
}
if (group->protect) {
ob->add_protector(ob_bing);
}
}
}
}
return npcs;
} /* load_group_npcs() */
/**
* This returns the npc info class for the npc in question. THis should
* only be used for debugging.
* @param npc the name of the npc to get the info on
* @return the class of the npc
*/
public class npc_info query_npc_info( string npc ) {
if ( !_npc_info ) {
return 0;
}
return _npc_info[ npc ];
} /* get_npc_info() */
/**
* This returns all the npcs in the debug data array.
* @return this returns the debug information for the npcs
*/
mapping query_debug_npc_info() {
return _npc_info;
}
/**
* This returns the group info class for the group in question. THis should
* only be used for debugging.
* @param group the name of the group to get the info on
* @return the class of the group
*/
public class group_info query_group_info( string group ) {
if ( !_group_info ) {
return 0;
}
return _group_info[ group ];
} /* query_group_info() */
/**
* This method is called when a specific npc dies so we can do some
* tracking on it.
* @param npc_ob the npc object
*/
void npc_died(object ob) {
string npc_id;
class npc_info info;
int i;
mixed* stuff;
npc_id = ob->query_property("npc_id");
info = _npc_info[npc_id];
if (!info || !npc_id) {
return ;
}
info->population -= ({ ob });
info->no_deaths++;
// Find the npc in thr group if it is set.
npc_id = ob->query_property("group_id");
stuff = _group_info[npc_id]->storage;
if (sizeof(stuff)) {
for (i = 0; i < sizeof(stuff); i++) {
stuff[i] -= ({ ob, 0 });
}
_group_info[npc_id]->storage = filter(_group_info[npc_id]->storage,
(: sizeof($1) :));
}
} /* npc_died() */
/** @ignore yes */
mapping query_all_npc_info() {
return _npc_info;
}
/** @ignore yes */
mapping query_all_group_info() {
return _group_info;
}
/** @ignore yes */
mapping query_all_zone_info() {
return _zone_info;
}
private int count_chances_in_zone(string zone) {
class zone_info info;
int chance;
info = _zone_info[zone];
if (!zone) {
return 0;
}
chance = info->npc_chance + info->group_chance + info->zone_chance;
return chance;
} /* count_chance_in_zone() */
private object* create_npcs_in_zone(string zone, int pos) {
class zone_info info;
string extra;
string npc;
object ob;
object* obs;
int chance;
info = _zone_info[zone];
if (!zone) {
return ({ });
}
if(_zone_info[zone]->busy &&
_zone_info[zone]->busy < random(random(100)))
return ({ });
if (pos < info->npc_chance) {
foreach (npc, chance in info->npcs) {
if (pos < chance) {
ob = load_npc_object(npc);
if (ob) {
return ({ ob });
}
}
pos -= chance;
}
}
pos -= info->npc_chance;
if (pos < info->group_chance) {
foreach (npc, chance in info->groups) {
if (pos < chance) {
obs = load_group_npcs(npc);
if (sizeof(obs)) {
return obs;
}
}
pos -= chance;
}
}
pos -= info->group_chance;
foreach (extra, chance in info->zones) {
if (pos < chance) {
return load_random_npc(extra, -1, -1);
}
}
return ({ });
} /* create_npcs_in_zone() */
/**
* This method randomly selects an NPC or a group of NPC's from
* the list of defined NPC's for the room/region.
*
* @param zone This is a zone (as defined in the configuration files).
* @param wealth How wealthy this particular part of this zone is.
* @param busy How busy this particular part of this zone is.
*
* @return Returns a group of NPCs randomly selected.
*/
object* load_random_npc(string zone, int wealth, int busy) {
int chance;
int pos;
if (_disable_npc_generation) {
return ({ });
}
//
// Try and see if we can find a specific zone for the
// busyness/wealth of this zone
//
if (_zone_info[zone + "-w" + wealth + "-b" + busy]) {
zone = zone + "-w" + wealth + "-b" + busy;
} else if (_zone_info[zone + "-w" + wealth]) {
zone = zone + "-w" + wealth;
} else if (_zone_info[zone + "-b" + busy]) {
zone = zone + "-b" + busy;
}
if (!_zone_info[zone]) {
return ({ });
}
chance = count_chances_in_zone(zone);
if (!chance) {
return ({ });
}
pos = random(chance);
return create_npcs_in_zone(zone, pos);
} /* load_random_npc() */
/**
* Prints a report of all the npcs in the hospital currently.
*/
int do_report() {
string name;
class group_info group;
class npc_info npc;
string txt;
int total;
int max;
int pop;
txt = "";
total = 0;
txt += "%^WHITE%^NPCs%^RESET%^\n";
foreach( name, npc in _npc_info ) {
if(stringp(npc->path) && file_size(npc_path(npc->path) + ".c") > 0) {
pop = query_npc_current_population(name, 0);
} else {
pop = 0;
}
if(npc->transient)
txt += sprintf( "$I$5=Name: %s. (%s) (Transient) Reused %d Created %d Died %d\n", name + "",
(stringp(npc->path)?npc->path:"Broken!"),
npc->no_reused, npc->no_created, npc->no_deaths);
else
txt += sprintf( "$I$5=Name: %s. (%s) Population: (%d/%d) Created %d Died %d\n", name + "",
(stringp(npc->path)?npc->path:"Broken!"),
pop, npc->max_population, npc->no_created, npc->no_deaths );
total += pop;
max += npc->max_population;
}
txt += "$I$0=%^WHITE%^Groups%^RESET%^\n";
foreach( name, group in _group_info ) {
if(group->transient)
txt += sprintf( "$I$5=Name: %s. (%s) (Transient)\n", name,
query_multiple_short(keys(group->npcs)));
else
txt += sprintf( "$I$5=Name: %s. (%s) Population: (%d/%d)\n", name,
query_multiple_short(keys(group->npcs)),
sizeof( group->storage ), group->max_population );
}
txt += "$I$0=%^WHITE%^Cache%^RESET%^\n";
foreach(name in keys(_cache_inventory))
_cache_inventory[name] -= ({ 0 });
foreach(name, group in _cache_inventory)
txt += sprintf("$I$5=Name: %s %d\n", name, sizeof(group));
txt += "\nTotal NPCs in hospital: " + total + "/" +
max + ".\n";
write("$P$Report$P$" + txt );
return 1;
} /* create_hospital_report() */
/** @ignore yes */
void init() {
object ob;
ob = this_player();
add_command("report", "", (: do_report :));
// Add returning npcs to our cache.
if(ob->query_property("npc_id") && ob->query_property("hospital") &&
ob->query_property("hospital") == base_name(this_object())) {
if(!_cache_inventory[ob->query_property("npc_id")])
_cache_inventory[ob->query_property("npc_id")] = ({ ob });
else if(sizeof(_cache_inventory[ob->query_property("npc_id")]) >
_max_cache)
ob->move("/room/rubbish");
else
_cache_inventory[ob->query_property("npc_id")] += ({ ob });
} else if(base_name(ob) == "/obj/corpse")
ob->move("/room/rubbish");
} /* init() */
/** @ignore yes */
int no_attack() { return 1; }
/** @ignore yes */
mapping query_dynamic_auto_load() {
string group_name;
string npc;
class group_info data;
class npc_info npc_data;
mapping tmp;
mapping tmp2;
tmp = allocate_mapping( sizeof( _group_info ) );
foreach( group_name, data in _group_info ) {
tmp[ group_name ] = data->storage;
}
tmp2 = allocate_mapping( sizeof( _npc_info) );
foreach( npc, npc_data in _npc_info ) {
tmp2[ npc ] = npc_data->population;
}
return ([ file_name( this_object() ) : tmp, "npcs" : tmp2 ]);
} /* query_dynamic_auto_load() */
/** @ignore yes */
void init_dynamic_arg( mapping tmp ) {
string group_name;
object *npcs;
if ( tmp[ file_name( this_object() ) ] ) {
foreach( group_name, npcs in tmp[ file_name( this_object() ) ] ) {
if (_group_info[group_name]) {
_group_info[ group_name ]->storage = npcs;
}
}
foreach( group_name, npcs in tmp[ "npcs" ]) {
if (_npc_info[group_name]) {
_npc_info[ group_name ]->population = npcs;
}
}
}
} /* init_dynamic_arg() */
/** @ignore yes
* This is a debugging function. Beware, if you have a large hospital this
* may lockup/crash the mud!
*/
mixed query_all_npcs() {
return values( map( _group_info, (: $2->storage :) ) );
}
/** @ignore yes
* This is a debugging function.
*/
mapping query_inventory_cache() {
return copy(_cache_inventory);
}
/**
* This function returns the ids of all the NPCs which are registered with the
* hospital, ie the ones that have to be specified with add_hospital_npc().
*
* @return All the ids.
*/
string* query_registered_npcs() {
return keys( _npc_info );
} /* string* query_registered_npcs() */