//*****************************************************************************
//
// persistent.c
//
// handles all the goings-on for persistent rooms. If a room is to be loaded,
// first check if it has a persistent copy on disk. Read that in. Otherwise,
// run the rproto as usual. When a persistent room's state changes, make sure
// it is saved to disk. When persistent rooms need to be loaded back up after
// a copyover or reboot, make sure that happens.
//
//*****************************************************************************
#include "../mud.h"
#include "../utils.h"
#include "../world.h"
#include "../auxiliary.h"
#include "../storage.h"
#include "../room.h"
#include "../handler.h"
#include "../hooks.h"
#include "../event.h"
#include "../character.h"
//*****************************************************************************
// mandatory modules
//*****************************************************************************
#include "../scripts/scripts.h"
#include "../scripts/pyroom.h"
//*****************************************************************************
// auxiliary data
//*****************************************************************************
typedef struct {
bool dirty; // do we need to be saved?
bool persistent; // are we persistent or not?
int activity; // how many 'things' are going on in us? If activity is
// > 0, we have to make sure we force-load at startup
time_t last_use; // the last time someone entered our room
} PERSISTENT_DATA;
PERSISTENT_DATA *newPersistentData(void) {
PERSISTENT_DATA *data = malloc(sizeof(PERSISTENT_DATA));
data->persistent = FALSE;
data->activity = 0;
data->last_use = current_time;
data->dirty = FALSE;
return data;
}
void deletePersistentData(PERSISTENT_DATA *data) {
free(data);
}
void persistentDataCopyTo(PERSISTENT_DATA *from, PERSISTENT_DATA *to) {
*to = *from;
}
PERSISTENT_DATA *persistentDataCopy(PERSISTENT_DATA *data) {
PERSISTENT_DATA *newdata = newPersistentData();
persistentDataCopyTo(data, newdata);
return newdata;
}
STORAGE_SET *persistentDataStore(PERSISTENT_DATA *data) {
STORAGE_SET *set = new_storage_set();
store_bool(set, "persistent", data->persistent);
store_int(set, "activity", data->activity);
return set;
}
PERSISTENT_DATA *persistentDataRead(STORAGE_SET *set) {
PERSISTENT_DATA *data = newPersistentData();
data->persistent = read_bool(set, "persistent");
data->activity = read_int(set, "activity");
return data;
}
//*****************************************************************************
// local functions
//*****************************************************************************
LIST *p_to_save = NULL; // our list of persistent rooms to save to disk
// at 1 million rooms, this should mean 1000000 / (64 * 64) = 244 files/folder
#define WORLD_BINS 64
//*****************************************************************************
// interaction with the database of persistent rooms
//*****************************************************************************
bool persistentRoomExists(WORLD_DATA *world, const char *key) {
static char fname[MAX_BUFFER];
if(!*key)
return FALSE;
*fname = '\0';
sprintf(fname, "%s/persistent/%lu/%lu/%s",
worldGetPath(world),
pearson_hash8_1(key) % WORLD_BINS,
pearson_hash8_2(key) % WORLD_BINS,
key);
return file_exists(fname);
}
//
// store a room in the persistent database
void worldClearPersistentRoom(WORLD_DATA *world, const char *key) {
static char fname[MAX_BUFFER];
if(!*key)
return;
*fname = '\0';
sprintf(fname, "%s/persistent/%lu/%lu/%s",
worldGetPath(world),
pearson_hash8_1(key) % WORLD_BINS,
pearson_hash8_2(key) % WORLD_BINS,
key);
// log_string("Clearing persistent room, %s", key);
if(file_exists(fname))
unlink(fname);
}
//
// store a room in the persistent database
void worldStorePersistentRoom(WORLD_DATA *world, const char *key,
ROOM_DATA *room) {
static char fname[MAX_BUFFER];
static char dir1[SMALL_BUFFER];
static char dir2[SMALL_BUFFER];
if(!*key)
return;
unsigned long hash1 = pearson_hash8_1(key) % WORLD_BINS;
unsigned long hash2 = pearson_hash8_2(key) % WORLD_BINS;
*fname = '\0';
*dir1 = '\0';
*dir2 = '\0';
// make sure our hash bins exist
sprintf(dir1, "%s/persistent/%lu",
worldGetPath(world), hash1);
if(!dir_exists(dir1))
mkdir(dir1, S_IRWXU | S_IRWXG);
// and the second one as well
sprintf(dir2, "%s/persistent/%lu/%lu",
worldGetPath(world), hash1, hash2);
if(!dir_exists(dir2))
mkdir(dir2, S_IRWXU | S_IRWXG);
// now, store the room
sprintf(fname, "%s/persistent/%lu/%lu/%s",
worldGetPath(world), hash1, hash2, key);
STORAGE_SET *set = roomStore(room);
storage_write(set, fname);
storage_close(set);
// log_string("stored persistent room :: %s", fname);
}
//
// pre-emptively, we're preparing for very large persistent world (1mil+rooms).
// Something like this, it would be real nice to have database storage for.
// Alas, we're using flat files... so we're going to have to do some pretty
// creative hashing, so the folders don't overflow and become impossible to
// access. make two layers of directories, each with 500 folders. That will
// give us 4 room files to a folder, for a 1mil room persistent world.
ROOM_DATA *worldGetPersistentRoom(WORLD_DATA *world, const char *key) {
static char fname[MAX_BUFFER];
if(!*key)
return NULL;
*fname = '\0';
sprintf(fname, "%s/persistent/%lu/%lu/%s",
worldGetPath(world),
pearson_hash8_1(key) % WORLD_BINS,
pearson_hash8_2(key) % WORLD_BINS,
key);
if(!file_exists(fname))
return NULL;
else {
// log_string("%-30s get persistent: %s", key, fname);
STORAGE_SET *set = storage_read(fname);
ROOM_DATA *room = roomRead(set);
storage_close(set);
worldPutRoom(world, key, room);
room_to_game(room);
return room;
}
}
//*****************************************************************************
// interaction with the persistent aux data
//*****************************************************************************
void roomUpdateLastUse(ROOM_DATA *room) {
PERSISTENT_DATA *data = roomGetAuxiliaryData(room, "persistent_data");
data->last_use = current_time;
}
time_t roomGetLastUse(ROOM_DATA *room) {
PERSISTENT_DATA *data = roomGetAuxiliaryData(room, "persistent_data");
return data->last_use;
}
void roomSetPersistent(ROOM_DATA *room, bool val) {
PERSISTENT_DATA *data = roomGetAuxiliaryData(room, "persistent_data");
// if it was persistent before and not now, clear our database entry
if(data->persistent == TRUE && val == FALSE)
worldClearPersistentRoom(gameworld, roomGetClass(room));
data->persistent = val;
}
bool roomIsPersistent(ROOM_DATA *room) {
PERSISTENT_DATA *data = roomGetAuxiliaryData(room, "persistent_data");
return data->persistent;
}
bool roomIsPersistentDirty(ROOM_DATA *room) {
PERSISTENT_DATA *data = roomGetAuxiliaryData(room, "persistent_data");
return data->dirty;
}
void roomSetPersistentDirty(ROOM_DATA *room) {
PERSISTENT_DATA *data = roomGetAuxiliaryData(room, "persistent_data");
data->dirty = TRUE;
}
void roomClearPersistentDirty(ROOM_DATA *room) {
PERSISTENT_DATA *data = roomGetAuxiliaryData(room, "persistent_data");
data->dirty = FALSE;
}
//
// add 'activity' to a persistent room. If a persistent room is active, make it
// automatically load at bootup, so the activity can continue
void roomAddActivity(ROOM_DATA *room) {
PERSISTENT_DATA *data = roomGetAuxiliaryData(room, "persistent_data");
data->activity++;
data->last_use = current_time;
// add us to the list of active rooms
//***********
// FINISH ME
//***********
}
void roomRemoveActivity(ROOM_DATA *room) {
PERSISTENT_DATA *data = roomGetAuxiliaryData(room, "persistent_data");
data->activity--;
// remove us from the list of active rooms
if(data->activity == 0) {
//***********
// FINISH ME
//***********
}
}
//*****************************************************************************
// Python extensions
//*****************************************************************************
//
// prepare a persistent room to be saved to disc
PyObject *PyRoom_dirtyPersistence(PyObject *pyroom) {
ROOM_DATA *room = PyRoom_AsRoom(pyroom);
if(room == NULL) {
PyErr_Format(PyExc_TypeError, "tried to dirty nonexistent room.");
return NULL;
}
// if we're not persistent, ignore
if(!roomIsPersistent(room))
return Py_BuildValue("i", 0);
else if(!roomIsPersistentDirty(room)) {
listPut(p_to_save, room);
roomSetPersistentDirty(room);
}
return Py_BuildValue("");
}
//
// unload a persistent room from memory. Will not work if PCs are present.
PyObject *PyRoom_unloadPersistence(PyObject *pyroom) {
ROOM_DATA *room = PyRoom_AsRoom(pyroom);
if(room == NULL) {
PyErr_Format(PyExc_TypeError, "tried to save nonexistent room.");
return NULL;
}
// it's not pesistent
if(!roomIsPersistent(room))
return Py_BuildValue("i", 0);
// does it contain a PC?
LIST_ITERATOR *ch_i = newListIterator(roomGetCharacters(room));
CHAR_DATA *ch = NULL;
bool pc_found = FALSE;
ITERATE_LIST(ch, ch_i) {
if(!charIsNPC(ch)) {
pc_found = TRUE;
break;
}
} deleteListIterator(ch_i);
if(pc_found)
return Py_BuildValue("i", 0);
worldStorePersistentRoom(gameworld, roomGetClass(room), room);
extract_room(room);
return Py_BuildValue("");
}
PyObject *PyRoom_getpersistent(PyObject *self, void *closure) {
ROOM_DATA *room = PyRoom_AsRoom(self);
if(room != NULL) return Py_BuildValue("i", roomIsPersistent(room));
else return NULL;
}
int PyRoom_setpersistent(PyObject *self, PyObject *arg) {
ROOM_DATA *room = PyRoom_AsRoom(self);
if(room == NULL)
return -1;
else if(arg == Py_True)
roomSetPersistent(room, TRUE);
else if(arg == Py_False)
roomSetPersistent(room, FALSE);
else
return -1;
return 0;
}
//*****************************************************************************
// hooks
//*****************************************************************************
void update_persistent_char_to_room(const char *info) {
CHAR_DATA *ch = NULL;
ROOM_DATA *room = NULL;
hookParseInfo(info, &ch, &room);
if(!charIsNPC(ch))
roomUpdateLastUse(room);
if(charIsNPC(ch) && roomIsPersistent(room) && !roomIsExtracted(room) &&
!roomIsPersistentDirty(room)) {
listPut(p_to_save, room);
roomSetPersistentDirty(room);
}
}
void update_persistent_char_from_room(const char *info) {
CHAR_DATA *ch = NULL;
ROOM_DATA *room = NULL;
hookParseInfo(info, &ch, &room);
if(charIsNPC(ch) && roomIsPersistent(room) && !roomIsExtracted(room) &&
!roomIsPersistentDirty(room)) {
listPut(p_to_save, room);
roomSetPersistentDirty(room);
}
}
void update_persistent_obj_to_room(const char *info) {
OBJ_DATA *obj = NULL;
ROOM_DATA *room = NULL;
hookParseInfo(info, &obj, &room);
if(roomIsPersistent(room) && !roomIsExtracted(room) &&
!roomIsPersistentDirty(room)) {
listPut(p_to_save, room);
roomSetPersistentDirty(room);
}
}
void update_persistent_obj_from_room(const char *info) {
OBJ_DATA *obj = NULL;
ROOM_DATA *room = NULL;
hookParseInfo(info, &obj, &room);
if(roomIsPersistent(room)&& !roomIsExtracted(room)&&
!roomIsPersistentDirty(room)) {
listPut(p_to_save, room);
roomSetPersistentDirty(room);
}
}
void update_persistent_obj_from_obj(const char *info) {
OBJ_DATA *obj = NULL;
OBJ_DATA *container = NULL;
ROOM_DATA *root = NULL;
hookParseInfo(info, &obj, &container);
if(container == NULL || obj == NULL)
return;
root = objGetRootRoom(container);
if(root == NULL)
return;
if(roomIsPersistent(root) && !roomIsExtracted(root) &&
!roomIsPersistentDirty(root)) {
listPut(p_to_save, root);
roomSetPersistentDirty(root);
}
}
void update_persistent_obj_to_obj(const char *info) {
OBJ_DATA *obj = NULL;
OBJ_DATA *container = NULL;
ROOM_DATA *root = NULL;
hookParseInfo(info, &obj, &container);
if(container == NULL || obj == NULL)
return;
root = objGetRootRoom(container);
if(root == NULL)
return;
if(roomIsPersistent(root) && !roomIsExtracted(root) &&
!roomIsPersistentDirty(root)) {
listPut(p_to_save, root);
roomSetPersistentDirty(root);
}
}
void update_persistent_room_from_game(const char *info) {
ROOM_DATA *room = NULL;
hookParseInfo(info, &room);
listRemove(p_to_save, room);
// have we been replaced by a non-persistent room?
ROOM_DATA *new_room = worldGetRoom(gameworld, roomGetClass(room));
if(roomIsPersistent(room) && new_room != NULL && !roomIsPersistent(new_room))
worldClearPersistentRoom(gameworld, roomGetClass(room));
}
void update_persistent_room_change(const char *info) {
ROOM_DATA *room = NULL;
hookParseInfo(info, &room);
if(roomIsPersistent(room) && !roomIsExtracted(room) &&
!roomIsPersistentDirty(room)) {
listPut(p_to_save, room);
roomSetPersistentDirty(room);
}
}
//*****************************************************************************
// events
//*****************************************************************************
//
// save all of our pending persistent rooms to disc
void flush_persistent_rooms_event(void *owner, void *data, const char *arg) {
ROOM_DATA *room = NULL;
while( (room = listPop(p_to_save)) != NULL) {
worldStorePersistentRoom(gameworld, roomGetClass(room), room);
roomClearPersistentDirty(room);
}
}
//
// every pulse, randomly sample our room table. If we find a persistent
// room that hasn't been active for awhile, unload it to disk so we aren't
// hogging up memory usage with a ton of unused rooms. Notably, this function
// kind of sucks because rooms get their UIDs from the same pool as objects
// and characters. That means a randomly generated UID is not always a room
// uid. It may also select room UIDs that have already been unloaded. What we
// really want to do is just sample a room from a known set of existing rooms.
//
// This function has been disabled until it is improved a little.
//
void close_unused_rooms_event(void *owner, void *unused, const char *arg) {
int top = top_uid();
if(top == NOTHING)
return;
// randomly sample from the room table
int uid_to_try = (rand() % (top - START_UID)) + START_UID;
ROOM_DATA *room = propertyTableGet(room_table, uid_to_try);
if(room == NULL)
return;
PERSISTENT_DATA *data = roomGetAuxiliaryData(room, "persistent_data");
// we've been inactive for more than 15 minutes, unload us
if(data->persistent && data->activity == 0 &&
difftime(current_time, roomGetLastUse(room)) > 60 * 15)
extract_room(room);
}
//*****************************************************************************
// initialization
//*****************************************************************************
void init_persistent(void) {
p_to_save = newList();
auxiliariesInstall("persistent_data",
newAuxiliaryFuncs(AUXILIARY_TYPE_ROOM,
newPersistentData, deletePersistentData,
persistentDataCopyTo, persistentDataCopy,
persistentDataStore,persistentDataRead));
// start our flushing of persistent rooms that need to be saved
start_update(NULL, 1, flush_persistent_rooms_event, NULL,NULL,NULL);
//
// disabled until a better implementation is written.
//
// start_update(NULL, 1, close_unused_rooms_event, NULL,NULL,NULL);
// listen for objects and characters entering
// or leaving rooms. Update those rooms' statuses
hookAdd("char_to_room", update_persistent_char_to_room);
hookAdd("char_from_room", update_persistent_char_from_room);
hookAdd("obj_to_room", update_persistent_obj_to_room);
hookAdd("obj_from_room", update_persistent_obj_from_room);
hookAdd("obj_from_obj", update_persistent_obj_from_obj);
hookAdd("obj_to_obj", update_persistent_obj_to_obj);
hookAdd("room_from_game", update_persistent_room_from_game);
hookAdd("room_change", update_persistent_room_change);
// add accessibility to Python
/*
PyRoom_addMethod("add_activity", PyRoom_addActivity, METH_NOARGS, NULL);
PyRoom_addMethod("rem_activity", PyRoom_remActivity, METH_NOARGS, NULL);
*/
PyRoom_addMethod("dirty", PyRoom_dirtyPersistence, METH_NOARGS,NULL);
PyRoom_addMethod("unload", PyRoom_unloadPersistence, METH_NOARGS,NULL);
PyRoom_addGetSetter("persistent",
PyRoom_getpersistent,
PyRoom_setpersistent, NULL);
}