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());
}