//***************************************************************************** // // 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); }