Source code for evennia.utils.gametime

"""
The gametime module handles the global passage of time in the mud.

It also supplies some useful methods to convert between
in-mud time and real-world time as well allows to get the
total runtime of the server and the current uptime.

"""

import time
from datetime import datetime, timedelta

from django.conf import settings
from django.db.utils import OperationalError

import evennia
from evennia import DefaultScript
from evennia.server.models import ServerConfig
from evennia.utils.create import create_script

# Speed-up factor of the in-game time compared
# to real time.

TIMEFACTOR = settings.TIME_FACTOR
IGNORE_DOWNTIMES = settings.TIME_IGNORE_DOWNTIMES


# Only set if gametime_reset was called at some point.
try:
    GAME_TIME_OFFSET = ServerConfig.objects.conf("gametime_offset", default=0)
except OperationalError:
    # the db is not initialized
    print("Gametime offset could not load - db not set up.")
    GAME_TIME_OFFSET = 0

# Common real-life time measure, in seconds.
# You should not change this.

# these are kept updated by the server maintenance loop
SERVER_START_TIME = 0.0
SERVER_RUNTIME_LAST_UPDATED = 0.0
SERVER_RUNTIME = 0.0

# note that these should not be accessed directly since they may
# need further processing. Access from server_epoch() and game_epoch().
_SERVER_EPOCH = None
_GAME_EPOCH = None

# Helper Script dealing in gametime (created by `schedule` function
# below).


[docs]class TimeScript(DefaultScript): """Gametime-sensitive script."""
[docs] def at_script_creation(self): """The script is created.""" self.key = "unknown scr" self.interval = 100 self.start_delay = True self.persistent = True
[docs] def at_repeat(self): """Call the callback and reset interval.""" callback = self.db.callback args = self.db.schedule_args or [] kwargs = self.db.schedule_kwargs or {} if callback: callback(*args, **kwargs) seconds = real_seconds_until(**self.db.gametime) self.start(interval=seconds, force_restart=True)
# Access functions
[docs]def runtime(): """ Get the total runtime of the server since first start (minus downtimes) Args: format (bool, optional): Format into a time representation. Returns: time (float or tuple): The runtime or the same time split up into time units. """ return SERVER_RUNTIME + time.time() - SERVER_RUNTIME_LAST_UPDATED
[docs]def server_epoch(): """ Get the server epoch. We may need to calculate this on the fly. """ global _SERVER_EPOCH if not _SERVER_EPOCH: _SERVER_EPOCH = ( ServerConfig.objects.conf("server_epoch", default=None) or time.time() - runtime() ) return _SERVER_EPOCH
[docs]def uptime(): """ Get the current uptime of the server since last reload Args: format (bool, optional): Format into time representation. Returns: time (float or tuple): The uptime or the same time split up into time units. """ return time.time() - SERVER_START_TIME
[docs]def portal_uptime(): """ Get the current uptime of the portal. Returns: time (float): The uptime of the portal. """ return time.time() - evennia.SESSION_HANDLER.portal_start_time
[docs]def game_epoch(): """ Get the game epoch. """ game_epoch = settings.TIME_GAME_EPOCH return game_epoch if game_epoch is not None else server_epoch()
[docs]def gametime(absolute=False): """ Get the total gametime of the server since first start (minus downtimes) Args: absolute (bool, optional): Get the absolute game time, including the epoch. This could be converted to an absolute in-game date. Returns: time (float): The gametime as a virtual timestamp. Notes: If one is using a standard calendar, one could convert the unformatted return to a date using Python's standard `datetime` module like this: `datetime.datetime.fromtimestamp(gametime(absolute=True))` """ epoch = game_epoch() if absolute else 0 if IGNORE_DOWNTIMES: gtime = epoch + (time.time() - server_epoch()) * TIMEFACTOR else: gtime = epoch + (runtime() - GAME_TIME_OFFSET) * TIMEFACTOR return gtime
[docs]def real_seconds_until(sec=None, min=None, hour=None, day=None, month=None, year=None): """ Return the real seconds until game time. Args: sec (int or None): number of absolute seconds. min (int or None): number of absolute minutes. hour (int or None): number of absolute hours. day (int or None): number of absolute days. month (int or None): number of absolute months. year (int or None): number of absolute years. Returns: The number of real seconds before the given game time is up. Example: real_seconds_until(hour=5, min=10, sec=0) If the game time is 5:00, TIME_FACTOR is set to 2 and you ask the number of seconds until it's 5:10, then this function should return 300 (5 minutes). """ current = datetime.fromtimestamp(gametime(absolute=True)) s_sec = sec if sec is not None else current.second s_min = min if min is not None else current.minute s_hour = hour if hour is not None else current.hour s_day = day if day is not None else current.day s_month = month if month is not None else current.month s_year = year if year is not None else current.year projected = datetime(s_year, s_month, s_day, s_hour, s_min, s_sec) if projected <= current: # We increase one unit of time depending on parameters if month is not None: projected = projected.replace(year=s_year + 1) elif day is not None: try: projected = projected.replace(month=s_month + 1) except ValueError: projected = projected.replace(month=1) elif hour is not None: projected += timedelta(days=1) elif min is not None: projected += timedelta(seconds=3600) else: projected += timedelta(seconds=60) # Get the number of gametime seconds between these two dates seconds = (projected - current).total_seconds() return seconds / TIMEFACTOR
[docs]def schedule( callback, repeat=False, sec=None, min=None, hour=None, day=None, month=None, year=None, *args, **kwargs, ): """ Call a callback at a given in-game time. Args: callback (function): The callback function that will be called. Note that the callback must be a module-level function, since the script will be persistent. The callable should be on the form `callable(*args, **kwargs)` where args/kwargs are passed into this schedule. repeat (bool, optional): Defines if the callback should be called regularly at the specified time. sec (int or None): Number of absolute game seconds at which to run repeat. min (int or None): Number of absolute minutes. hour (int or None): Number of absolute hours. day (int or None): Number of absolute days. month (int or None): Number of absolute months. year (int or None): Number of absolute years. *args: Passed into the callable. Must be possible to store in Attribute. **kwargs: Passed into the callable. Must be possible to store in Attribute. Returns: Script: The created Script handling the scheduling. Examples: :: schedule(func, min=5, sec=0) # Will call 5 minutes past the next (in-game) hour. schedule(func, hour=2, min=30, sec=0) # Will call the next (in-game) day at 02:30. """ seconds = real_seconds_until(sec=sec, min=min, hour=hour, day=day, month=month, year=year) script = create_script( "evennia.utils.gametime.TimeScript", key="TimeScript", desc="A gametime-sensitive script", interval=seconds, start_delay=True, repeats=-1 if repeat else 1, ) script.db.callback = callback script.db.gametime = { "sec": sec, "min": min, "hour": hour, "day": day, "month": month, "year": year, } script.db.schedule_args = args script.db.schedule_kwargs = kwargs return script
[docs]def reset_gametime(): """ Resets the game time to make it start from the current time. Note that the epoch set by `settings.TIME_GAME_EPOCH` will still apply. """ global GAME_TIME_OFFSET GAME_TIME_OFFSET = runtime() ServerConfig.objects.conf("gametime_offset", GAME_TIME_OFFSET)