""" 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. """ from __future__ import division import time from calendar import monthrange from datetime import datetime, timedelta from django.conf import settings 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 # Only set if gametime_reset was called at some point. GAME_TIME_OFFSET = ServerConfig.objects.conf("gametime_offset", default=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). class TimeScript(DefaultScript): """Gametime-sensitive script.""" def at_script_creation(self): """The script is created.""" self.key = "unknown scr" self.interval = 100 self.start_delay = True self.persistent = True def at_repeat(self): """Call the callback and reset interval.""" callback = self.db.callback if callback: callback() seconds = real_seconds_until(**self.db.gametime) self.restart(interval=seconds) # Access functions 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 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 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 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() 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 gtime = epoch + (runtime() - GAME_TIME_OFFSET) * TIMEFACTOR return gtime 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 days_in_month = monthrange(s_year, s_month)[1] days_in_year = sum(monthrange(s_year, m + 1)[1] for m in range(12)) if month is not None: projected += timedelta(days=days_in_year) elif day is not None: projected += timedelta(days=days_in_month) 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 def schedule(callback, repeat=False, sec=None, min=None, hour=None, day=None, month=None, year=None): """ 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. 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. Returns: script (Script): The created Script handling the sceduling. 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, } return script 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)