Time spent in a game.

A place for Ren'Py tutorials and reusable Ren'Py code.
Forum rules
Do not post questions here!

This forum is for example code you want to show other people. Ren'Py questions should be asked in the Ren'Py Questions and Announcements forum.
Post Reply
Message
Author
User avatar
Vladya
Regular
Posts: 34
Joined: Sun Sep 20, 2020 3:16 am
Github: NyashniyVladya
Contact:

Time spent in a game.

#1 Post by Vladya »

Small API that allows display game time in a convenient form. It can work with the game time of a specific save, with the total time of a game and with user input (you can find a more detailed description in the code below).

How to use it:
  • Copy the API code into a new .rpy file in your project.

    Code: Select all

    init -99 python in _game_runtime_setting:
    
        import datetime
        import threading
        import store
        from store import (
            config,
            persistent,
            Null,
            NoRollback,
            DynamicDisplayable,
            Text
        )
    
        """
    
        API doc:
    
            :current_runtime.TextFormat(text_format, **text_kwargs):
    
                It works like a normal 'Text',
                but replaces the data inside "[]" with time values
                corresponding to the current game time.
    
                Data will be represented by those elements
                that are present in the text.
    
                For example, the game time is 26 hours:
    
                If the query looks like: "[day] days [hour] hours.",
                it will show: "1 days 2 hours",
    
                And if it looks like this: "[hour] hours.",
                it will show "26 hours".
    
                etc.
    
                Available values:
                    "second"
                    "minute"
                    "hour"
                    "day"
                    "week"
                    "century"
    
            :current_runtime.strf(text_format):
                Logic is similar to the previous function, but returns a string.
    
            :current_runtime.Formatter(expressed_as=None, result_handler=None):
    
                Returns an object that has properties presented above
                in the "Available values" list.
    
                :expressed_as:
                    Which properties to use to describe the time value.
                    Read more in the description "_TimeFormatter.__init__".
    
                :result_handler:
                    (Callable)
                    Property will be called with this object, before returning.
    
    
            :current_runtime.seconds():
                Returns game time in seconds.
    
            :current_runtime.get_timedelta():
                Returns 'datetime.timedelta' object
                initialized with the current game time.
    
            #################################################################
    
            :total_runtime.TextFormat(text_format, **text_kwargs):
            :total_runtime.strf(text_format):
            :total_runtime.Formatter(expressed_as=None, result_handler=None):
            :total_runtime.seconds():
            :total_runtime.get_timedelta():
    
                Similar to the functions of the same name,
                but returns values for the total game time (for all saves).
    
            #################################################################
    
            :tformat.TextFormat(user_input, text_format, **text_kwargs):
            :tformat.strf(user_input, text_format):
            :tformat.Formatter(user_input, expressed_as=None, result_handler=None):
            :tformat.seconds(user_input):
            :tformat.get_timedelta(user_input):
    
                Functions are similar to those of the same name,
                but as the first argument should pass value in seconds
                (or "datetime.timedelta" object)
                with which the interaction will be performed.
    
        """
    
        class _TimeFormatter(NoRollback):
    
            __author__ = "Vladya"
    
            time_values_in_seconds = {
                "second":  (1.),  # It makes sense, doesn't it?
                "minute":  (60.),
                "hour":    (60. * 60.),
                "day":     (60. * 60. * 24.),
                "week":    (60. * 60. * 24. * 7.),
                "century": (((60. * 60. * 24.) * 365.25) * 100.)
                # Month and year do not have permanent value.
            }
    
            def __init__(
                self,
                time_data,
                expressed_as=None,
                _result_handler=None,
                _timer_mode="current"
            ):
    
                """
                :time_data:
                    An initialized object of class "_PlayTimer",
                    numeric value (in seconds),
                    datetime.timedelta object
                    or "_TimeFormatter" object.
    
                :expressed_as:
                    Iterable object whose elements should be text time values
                    in which the result will be expressed.
                    (or None)
    
                    For example:
                        (Let "time_data" be 123456789 seconds)
                        ########################################
                        expressed_as = ("second",)
                        Values will look like this:
                            century == 0
                            week    == 0
                            day     == 0
                            hour    == 0
                            minute  == 0
                            second  == 123456789
                        ########################################
                        expressed_as = ("minute", "hour")
                        Values will look like this:
                            century == 0
                            week    == 0
                            day     == 0
                            hour    == 34293
                            minute  == 33.15
                            second  == 0
                        ########################################
                        expressed_as = ("week", "day")
                        Values will look like this:
                            century == 0
                            week    == 204
                            day     == 0.9
                            hour    == 0
                            minute  == 0
                            second  == 0
                        ########################################
                        expressed_as = None
                        Values will look like this:
                            century == 0
                            week    == 204
                            day     == 0
                            hour    == 21
                            minute  == 33
                            second  == 9
                        etc.
                :_result_handler:
                    Callable object or None.
                    If callable, this will be called for the returned value.
                    If None, value will be adapted to the situation.
                :_timer_mode:
                    It is used only if "time_data" is "_PlayTimer" instance.
                    Can take two values: "current" and "total".
                    if _timer_mode == "current":
                        The time value for the current playthrough will be used.
                    if _timer_mode == "total":
                        The common time for all games will be used.
                """
    
                self.__timer_mode = None
                if isinstance(time_data, _PlayTimer):
                    self.__time_data = time_data
                    if _timer_mode is None:
                        _timer_mode = "current"
                    if _timer_mode not in ("current", "total"):
                        raise ValueError("Incorrect '_timer_mode' value.")
                    self.__timer_mode = _timer_mode
                elif isinstance(time_data, datetime.timedelta):
                    self.__time_data = time_data.total_seconds()
                elif isinstance(time_data, _TimeFormatter):
                    self.__time_data = time_data._get_timedata_object()
                else:
                    self.__time_data = float(time_data)
    
                self._set_expressed_as(expressed_as)
    
                if not ((_result_handler is None) or callable(_result_handler)):
                    raise ValueError("Incorrect '_result_handler' value.")
                self.__result_handler = _result_handler
    
            def __getstate__(self):
                return {
                    "time_data": self.__time_data,
                    "expressed_as": self.__expressed_as,
                    "_timer_mode": self.__timer_mode
                }
    
            def __setstate__(self, init_kwargs):
                self.__init__(**init_kwargs)
    
            @staticmethod
            def _default_handler(value):
                value = float(value)
                if value.is_integer():
                    return int(value)
                return round(value, 2)
    
            @property
            def total_seconds(self):
                if isinstance(self.__time_data, _PlayTimer):
                    self.__time_data._update_time(True)
                    if self.__timer_mode == "current":
                        delta = self.__time_data._total_playtime_current
                    else:
                        delta = self.__time_data._total_playtime_global
                    return delta.total_seconds()
                return self.__time_data
    
            @property
            def timedelta(self):
                return datetime.timedelta(seconds=self.total_seconds)
    
            def _get_timedata_object(self):
                return self.__time_data
    
            def _set_expressed_as(self, new_expressed_as):
    
                if new_expressed_as is None:
                    new_expressed_as = self.time_values_in_seconds.iterkeys()
    
                new_expressed_as = frozenset(
                    map(lambda x: x.strip().lower(), new_expressed_as)
                )
    
                for value in new_expressed_as:
                    if value not in self.time_values_in_seconds:
                        raise ValueError("{0} is incorrect value.".format(value))
    
                self.__expressed_as = new_expressed_as
    
            def __contains__(self, key):
                if key in self.time_values_in_seconds:
                    return True
                return False
    
            def __getitem__(self, key):
                if key not in self:
                    raise KeyError(key)
                value = self._get_value(key)
                _handler = (self.__result_handler or self._default_handler)
                return _handler(value)
    
            def __getattr__(self, name):
    
                try:
                    value = self.__getitem__(name)
                except KeyError as ex:
                    raise AttributeError(*ex.args)
                else:
                    return value
    
            def _get_value(self, name):
    
                assert (name in self)
                if name not in self.__expressed_as:
                    return .0
    
                _as_dict = dict(
                    map(
                        lambda val: (val, self.time_values_in_seconds[val]),
                        self.__expressed_as
                    )
                )
                values = tuple(sorted(_as_dict.iteritems(), key=lambda x: x[-1]))
    
                _size = len(values)
                min_unit = None
                for i, (current_unit, in_seconds) in enumerate(values):
    
                    if min_unit is None:
                        min_unit = current_unit
    
                    if current_unit == name:
    
                        next_unit = next_unit_in_seconds = None
                        if i < (_size - 1):
                            next_unit, next_unit_in_seconds = values[(i + 1)]
    
                        result = self.total_seconds
                        if next_unit is not None:
                            result %= next_unit_in_seconds
    
                        if current_unit == min_unit:
                            result /= in_seconds
                        else:
                            result //= in_seconds
    
                        return result
    
                raise Exception("Unknown error.")
    
        class _PlayTimer(Null, NoRollback):
    
            __author__ = "Vladya"
    
            _json_value_name = "game_runtime_save_value"
            _persistent_value_name = "total_game_runtime_value"
    
            single_instance = None
    
            def __new__(cls):
                if cls.single_instance is None:
                    cls.single_instance = super(_PlayTimer, cls).__new__(cls)
                    cls.single_instance._already_init = False
                return cls.single_instance
    
            def __init__(self):
    
                if self._already_init:
                    return
    
                super(_PlayTimer, self).__init__()
                self.reset_time()
                self.__update_lock = threading.Lock()
                self.__last_update_persistent = None
                self._already_init = True
                self._is_enabled = False
    
            def __getstate__(self):
                #  This is not necessary for a singleton.
                return None
    
            def __setstate__(self, value):
                self.__init__()
    
            @property
            def _total_playtime_global(self):
                """
                Not only for a specific save.
                """
                if getattr(persistent, self._persistent_value_name) is None:
                    _start_delta = datetime.timedelta(seconds=.0)
                    setattr(persistent, self._persistent_value_name, _start_delta)
                return getattr(persistent, self._persistent_value_name)
    
            @_total_playtime_global.setter
            def _total_playtime_global(self, new_playtime):
                setattr(persistent, self._persistent_value_name, new_playtime)
    
            @property
            def _total_playtime_current(self):
                """
                For a specific save.
                """
                return self.__current_store_runtime
    
            @classmethod
            def turn_on(cls):
    
                assert renpy.is_init_phase()
                timer_object = cls()
                if timer_object._is_enabled:
                    return
    
                config.start_callbacks.append(timer_object.reset_time)
                config.save_json_callbacks.append(timer_object._save_callback)
                config.overlay_functions.append(timer_object._add)
    
                timer_object._create_api()
    
                # Decorate 'renpy.load' so that it loads time from the save file.
                renpy.load = timer_object._load_func_decorator(renpy.load)
    
                timer_object._is_enabled = True
    
            def _create_api(self):
    
                stores = {
                    "current_runtime": "current",
                    "total_runtime": "total",
                    "tformat": "user_input"
                }
    
                def _get_mode_decorator(mode):
                    def _mode_decorator(func):
                        if mode == "user_input":
                            def _new_func(user_input, *args, **kwargs):
                                return func(mode, user_input, *args, **kwargs)
                        else:
                            def _new_func(*args, **kwargs):
                                return func(mode, None, *args, **kwargs)
                        _new_func.__doc__ = func.__doc__
                        return _new_func
                    return _mode_decorator
    
                for store_name, mode_name in stores.iteritems():
                    store_name = "store.{0}".format(store_name)
                    renpy.ast.create_store(store_name)
                    namespace, _special = renpy.ast.get_namespace(store_name)
                    _decorator = _get_mode_decorator(mode_name)
    
                    namespace.set("TextFormat", _decorator(self._get_dynamic_text))
                    namespace.set("strf", _decorator(self._substitute))
                    namespace.set("Formatter", _decorator(self._get_formatter))
                    namespace.set("seconds", _decorator(self._get_total_seconds))
                    namespace.set("get_timedelta", _decorator(self._get_timedelta))
    
            def reset_time(self):
                self.__current_store_runtime = datetime.timedelta(seconds=.0)
                self.__last_update_time = None
    
            def _save_callback(self, save_dict):
                self._update_time(True)
                _runtime_in_sec = self.__current_store_runtime.total_seconds()
                save_dict[self._json_value_name] = _runtime_in_sec
    
            def _load_func_decorator(self, function):
    
                def _new_func(filename):
                    _json_dict = renpy.slot_json(filename)
                    if _json_dict:
                        value = _json_dict.get(self._json_value_name, .0)
                        self.__current_store_runtime = datetime.timedelta(
                            seconds=value
                        )
                        self.__last_update_time = None
                    return function(filename)
    
                _new_func.__doc__ = function.__doc__
    
                return _new_func
    
            # API methods. #####################################################
    
            def _get_formatter(
                self,
                mode,
                user_input,
                expressed_as=None,
                result_handler=None
            ):
                # API name: 'Formatter'
                if mode == "user_input":
                    return _TimeFormatter(user_input, expressed_as, result_handler)
                return _TimeFormatter(self, expressed_as, result_handler, mode)
    
            def _get_total_seconds(self, mode, user_input):
                # API name: 'seconds'
                _formatter = self._get_formatter(mode, user_input)
                return _formatter.total_seconds
    
            def _get_timedelta(self, mode, user_input):
                # API name: 'get_timedelta'
                _formatter = self._get_formatter(mode, user_input)
                return _formatter.timedelta
    
            def _get_time_formatter_for_text(self, mode, user_input, text_format):
                # API function without name.
                values = set()
                for val in renpy.substitutions.formatter.parse(text_format):
                    _literal, value, _format, _conversion = val
                    if value is None:
                        continue
                    if value in _TimeFormatter.time_values_in_seconds:
                        values.add(value)
    
                return self._get_formatter(mode, user_input, values)
    
            def _substitute(self, mode, user_input, text_format):
                # API name: 'strf'
                scope = self._get_time_formatter_for_text(
                    mode,
                    user_input,
                    text_format
                )
                result = renpy.substitutions.substitute(
                    text_format,
                    scope=scope,
                    force=True
                )
                if isinstance(result, basestring):
                    return result
                return result[0]
    
            def _get_dynamic_text(self, mode, user_input, text_format, **kwargs):
                # API name: 'TextFormat'
                scope = self._get_time_formatter_for_text(
                    mode,
                    user_input,
                    text_format
                )
    
                if kwargs.get("scope", None) is not None:
                    scope = renpy.substitutions.MultipleDict(
                        scope,
                        kwargs["scope"],
                        store.__dict__
                    )
                kwargs.update({"scope": scope, "substitute": True})
    
                def _f(*_args, **_kwargs):
                    return (Text(text_format, **kwargs), .0)
    
                return DynamicDisplayable(_f)
    
            # ##################################################################
    
            def _add(self):
                ui.add(self)
    
            def _update_time(self, force=False):
    
                with self.__update_lock:
    
                    _current = datetime.datetime.today()
    
                    if self.__last_update_time:
                        _delta = _current - self.__last_update_time
                        self.__current_store_runtime += _delta
                    self.__last_update_time = _current
    
                    if self.__last_update_persistent:
                        _delta = _current - self.__last_update_persistent
                        if (_delta.total_seconds() < 5.) and (not force):
                            # Overwriting persistent is an expensive operation,
                            # as the hard disk is being accessed.
                            return
                        self._total_playtime_global += _delta
                    self.__last_update_persistent = _current
    
            def render(self, *args, **kwargs):
                self._update_time()
                renpy.redraw(self, .0)
                return super(_PlayTimer, self).render(*args, **kwargs)
    
        _PlayTimer.turn_on()
    
    
  • And that's it. You can use it. Here are a couple of examples:
    • Displaying game time on the "confirm" screen:

      Code: Select all

      screen confirm(message, yes_action, no_action):
      
          modal True
          zorder 200
          style_prefix "confirm"
          add "gui/overlay/confirm.png"
          frame:
              vbox:
                  xalign .5
                  yalign .5
                  spacing 45
                  label _(message):
                      style "confirm_prompt"
                      xalign .5
      
                  add current_runtime.TextFormat(
                      "You spent [second:.0f] seconds on the current playthrough.",
                      #  Accepts all arguments accepted by the "Text" class.
                      text_align=.5,
                      size=50
                  )
                  add current_runtime.TextFormat(
                      "It can also be expressed as [minute] minutes.",
                      text_align=.5,
                      size=35
                  )
                  add total_runtime.TextFormat(
                      "Full time: [minute] min [second] sec.",
                      size=30
                  )
      
                  hbox:
                      xalign .5
                      spacing 150
                      textbutton _("Yes") action yes_action
                      textbutton _("No") action no_action
      
          key "game_menu" action no_action
      
      
    • Example of use in a script:

      Code: Select all

      label start:
      
          $ _string = tformat.strf((4 * 60 * 60), "I slept only [hour] hours that night!")
          "Some guy" "I am so tired... [_string]"
      
          $ _formatter = tformat.Formatter((2 * 60 * 60), expressed_as=("hour",))
          "Some girl" "You are lucky! I managed to sleep only [_formatter.hour] hours."
      
          $ _string = tformat.strf(_formatter, "[second] seconds")
          "Some guy" "Thoughts are more positive. After all, it's as much as [_string]!"
      
          "..."
      
          $ result = total_runtime.strf("[hour]h [minute]m [second:.0f]s")
          "Full time for all games: [result]"
          return
      
      

User avatar
gas
Miko-Class Veteran
Posts: 842
Joined: Mon Jan 26, 2009 7:21 pm
Contact:

Re: Time spent in a game.

#2 Post by gas »

What an uncanny good piece of code is that!
My compliments.
If you want to debate on a reply I gave to your posts, please QUOTE ME or i'll not be notified about. << now red so probably you'll see it.

10 ? "RENPY"
20 GOTO 10

RUN

User avatar
_ticlock_
Miko-Class Veteran
Posts: 910
Joined: Mon Oct 26, 2020 5:41 pm
Contact:

Re: Time spent in a game.

#3 Post by _ticlock_ »

Vladya,

Totally agree with gas.

Thank you.

PS: I wish I would write documentation like that.

User avatar
Tiger Lyz
Newbie
Posts: 11
Joined: Fri Jan 17, 2020 4:09 am
Projects: Curvy heroine&Virtual Boyfriend, Homunculus, Voice of Paradaise, The Ugly Princess...
Contact:

Re: Time spent in a game.

#4 Post by Tiger Lyz »

Thanks! It's very usefull. :D

Post Reply

Who is online

Users browsing this forum: No registered users