ff_u_area_create
Recreate areas left empty by players.
When the last player leaves an area, the area is tracked with a timestamp. If a player enters the area again, it is no longer tracked. Once an area has been empty for GC_AREA_RECREATE_DURATION minutes, it is destroyed and recreated from its resref, with the same tag and name: creatures, placeables, doors, items and encounters are back to their initial state.
The tracked areas are checked every real-time minute. While no player is on the server, GC_EMPTY_AREA_RECREATE_DURATION is used instead of GC_AREA_RECREATE_DURATION.
When the last player leaves the server, the module is tracked the same way. Once the server has been empty for GC_MODULE_RECREATE_DURATION minutes, all tracked areas are recreated, whatever their age.
Areas are recreated after a short random delay, to spread the load.
The area holding the module’s starting location, and the areas with the PERSISTENT flag, are never recreated.
Variables
ENABLE__FF_U_AREA_RECREATE: (string) (opt-in) Set to "Y" on module to enable the unit.
GC_AREA_RECREATE_DURATION: (int) Real-time minutes an area must be empty
before it is recreated. Read from the area, then the module. Set to -1 to
never recreate the area while players are on the server. Defaults to 60.
GC_EMPTY_AREA_RECREATE_DURATION: (int) Real-time minutes an area must
be empty before it is recreated, when no player is on the server. Read from
the area, then the module. Set to -1 to never recreate the area while the
server is empty. Defaults to 10.
GC_MODULE_RECREATE_DURATION: (int) Real-time minutes the server must be
empty before all tracked areas are recreated. Read from the module. Set to
-1 to disable. Defaults to 5.
PERSISTENT: (string) Set to "Y" on an area to never recreate it.
Source code
// @code
#include "ff_i_core"
const string ENABLE__FF_U_AREA_RECREATE = "ENABLE__FF_U_AREA_RECREATE";
const string FF_U_AREA_CREATE__AREA_TIMESTAMP = "__FF_U_AREA_CREATE__AREA_TIMESTAMP";
const string FF_U_AREA_CREATE__MODULE_TIMESTAMP = "__FF_U_AREA_CREATE__MODULE_TIMESTAMP";
const string ARRAY_NAME = "__FF_U_AREA_CREATE__ARRAY";
const string GC_AREA_RECREATE_DURATION = "GC_AREA_RECREATE_DURATION";
const string GC_EMPTY_AREA_RECREATE_DURATION = "GC_EMPTY_AREA_RECREATE_DURATION";
const string GC_MODULE_RECREATE_DURATION = "GC_MODULE_RECREATE_DURATION";
const string PERSISTENT = "PERSISTENT";
const int DEFAULT = 60; // Real-time minutes
const int DEFAULT_EMPTY = 10; // Real-time minutes
const int DEFAULT_MODULE = 5; // Real-time minutes
//! @brief Seconds between a player leaving an area and the player count check.
const float EXIT_DELAY = 1.0f;
// -----------------------------------------------------------------------------
//! @brief Get whether oArea may be recreated.
//! @param oArea An area.
//! @return FALSE if oArea is PERSISTENT, or if it holds the module's starting
//! location.
int GetCanRecreateArea(object oArea);
int GetCanRecreateArea(object oArea)
{
if (GetObjectFlag(oArea, PERSISTENT))
return FALSE;
if (oArea == GetAreaFromLocation(GetStartingLocation()))
return FALSE;
return TRUE;
}
// -----------------------------------------------------------------------------
//! @brief Start tracking oArea if it is empty of players, and may be recreated.
//! @param oArea An area.
void TrackArea(object oArea);
void TrackArea(object oArea)
{
if (!GetIsArea(oArea))
return;
if (NWNX_Area_GetNumberOfPlayersInArea(oArea) > 0)
return;
if (!GetCanRecreateArea(oArea))
return;
if (!ObjectArrayContains(ARRAY_NAME, oArea))
ObjectArrayAdd(ARRAY_NAME, oArea);
TrackGCObject(oArea, FF_U_AREA_CREATE__AREA_TIMESTAMP);
}
// -----------------------------------------------------------------------------
//! @brief Stop tracking oArea.
//! @param oArea An area.
void UntrackArea(object oArea);
void UntrackArea(object oArea)
{
if (!GetIsArea(oArea))
return;
ObjectArrayDelete(ARRAY_NAME, oArea);
UntrackGCObject(oArea, FF_U_AREA_CREATE__AREA_TIMESTAMP);
}
// -----------------------------------------------------------------------------
//! @brief Stop tracking oArea, then recreate it after a random delay.
//! @param oArea An area.
//! @param sReason Why oArea is recreated, for the log.
void RecreateTrackedArea(object oArea, string sReason);
void RecreateTrackedArea(object oArea, string sReason)
{
LogInfo(__FILE__ + ": Recreating " + GetAreaInfo(oArea) + ", " + sReason + ".");
UntrackArea(oArea);
DelayCommand(GetRandomMaxDelay(MINUTE_MAX_DELAY), RecreateAreaVoid(oArea));
}
// -----------------------------------------------------------------------------
//! @brief Recreate all tracked areas, whatever their age.
void ClearModule();
void ClearModule()
{
UntrackGCObject(GetModule(), FF_U_AREA_CREATE__MODULE_TIMESTAMP);
sqlquery sqlQuery = ObjectArrayIterate(ARRAY_NAME);
while (SqlStep(sqlQuery))
{
string sUUID = SqlGetString(sqlQuery, 0);
object oArea = GetObjectByUUID(sUUID);
if (!GetIsArea(oArea))
{
StringArrayDelete(ARRAY_NAME, sUUID);
continue;
}
RecreateTrackedArea(oArea, "the server is empty");
}
}
// -----------------------------------------------------------------------------
//! @brief Recreate the tracked areas that have been empty for long enough.
//! While no player is on the server, GC_EMPTY_AREA_RECREATE_DURATION is used
//! instead of GC_AREA_RECREATE_DURATION.
void ClearAreas();
void ClearAreas()
{
string sDuration = GC_AREA_RECREATE_DURATION;
int nDefault = DEFAULT;
if (!GetIsObjectValid(GetFirstPC()))
{
sDuration = GC_EMPTY_AREA_RECREATE_DURATION;
nDefault = DEFAULT_EMPTY;
}
sqlquery sqlQuery = ObjectArrayIterate(ARRAY_NAME);
while (SqlStep(sqlQuery))
{
string sUUID = SqlGetString(sqlQuery, 0);
object oArea = GetObjectByUUID(sUUID);
if (!GetIsArea(oArea))
{
StringArrayDelete(ARRAY_NAME, sUUID);
continue;
}
// -1: never recreate, but keep tracking the area.
if (GetRecursiveInt(oArea, sDuration, nDefault) < 0)
continue;
int nMaxAge = GetGCMaxAge(oArea, sDuration, nDefault);
if (GetGCObjectExpired(oArea, FF_U_AREA_CREATE__AREA_TIMESTAMP, nMaxAge))
RecreateTrackedArea(oArea, "empty for too long");
}
}
// -----------------------------------------------------------------------------
//! @brief Recreate all tracked areas if the server has been empty for long
//! enough, else the tracked areas that have been empty for long enough.
void OnTimer();
void OnTimer()
{
object oModule = GetModule();
int nMaxAge = GetGCMaxAge(oModule, GC_MODULE_RECREATE_DURATION, DEFAULT_MODULE);
if (GetGCObjectExpired(oModule, FF_U_AREA_CREATE__MODULE_TIMESTAMP, nMaxAge))
ClearModule();
else
ClearAreas();
}
// -----------------------------------------------------------------------------
//! @brief Track oArea, then the module if no player is on the server, once
//! the exiting player has left.
//! @param oArea The area the player left.
void OnAreaExit(object oArea);
void OnAreaExit(object oArea)
{
TrackArea(oArea);
if (GetIsObjectValid(GetFirstPC()))
return;
object oModule = GetModule();
if (GetRecursiveInt(oModule, GC_MODULE_RECREATE_DURATION, DEFAULT_MODULE) < 0)
return;
TrackGCObject(oModule, FF_U_AREA_CREATE__MODULE_TIMESTAMP);
}
// -----------------------------------------------------------------------------
void main()
{
string sEvent = NWNX_Events_GetCurrentEvent();
if (sEvent == ON_REGISTER)
{
if (!GetModuleFlag(ENABLE__FF_U_AREA_RECREATE, FALSE))
return;
CreateObjectArray(ARRAY_NAME);
SubscribeToEvent(ON_REALTIME_MINUTE, __FILE__);
SubscribeToEvent(ON_DEFAULT_AREA_ENTER, __FILE__);
SubscribeToEvent(ON_DEFAULT_AREA_EXIT, __FILE__);
}
else if (sEvent == ON_DEFAULT_AREA_ENTER)
{
if (GetIsPC(GetEnteringObject()))
{
UntrackArea(OBJECT_SELF);
UntrackGCObject(GetModule(), FF_U_AREA_CREATE__MODULE_TIMESTAMP);
}
}
else if (sEvent == ON_DEFAULT_AREA_EXIT)
{
if (GetIsPC(GetExitingObject()))
DelayCommand(EXIT_DELAY, OnAreaExit(OBJECT_SELF));
}
else if (sEvent == ON_REALTIME_MINUTE)
DelayCommand(MINUTE_MAX_DELAY + GetRandomMaxDelay(MINUTE_MAX_DELAY), OnTimer());
}