# This file contains functions, utilities, and hacks for the Ren'py engine. Sometimes a dev must do something to get it to work with Ren'py. :) python early hide: import functools if renpy.windows: # On windows, Renpy does not support backslashes in some of its functions, # but because the code needs to be platform-independent, # we require to monkey patch those functions in order # to remain compatible with all platforms without losing functionality. @renpy.pure def _loadable(filename): filename = filename.replace("\\", "/") return renpy.loader.loadable(filename) renpy.loadable = _loadable # renpy.list_files does not use cached results, let's fix that. @functools.cache def _list_files(common=False): rv = [fn for dir, fn in renpy.loader.listdirfiles(common) if not fn.startswith("saves/")] rv.sort() return rv renpy.list_files = _list_files # Default focus behaviour restarts the interaction whenever # any element that contains a tooltip is being hovered or unhovered. # Restarting interactions in a short timespan causes massive lag spikes, # in order to fix it, we'll refresh just the tooltip screen and skip the rest. def set_focused(widget, arg, screen): global _tooltip renpy.display.focus.argument = arg renpy.display.focus.screen_of_focused = screen if screen is not None: renpy.display.focus.screen_of_focused_names = { screen.screen_name[0], screen.tag } else: renpy.display.focus.screen_of_focused_names = set() renpy.game.context().scene_lists.focused = widget renpy.display.tts.displayable(widget) # Figure out the tooltip. _tooltip = widget._get_tooltip() if widget else None # setattr(renpy.store, "widget", widget) # DEBUG # if renpy.display.focus.tooltip != new_tooltip: # renpy.display.focus.tooltip = new_tooltip # renpy.display.focus.capture_focus("tooltip") # renpy.exports.restart_interaction() # if renpy.display.focus.tooltip is not None: # renpy.display.focus.last_tooltip = renpy.display.focus.tooltip # renpy.display.focus.screen_of_last_focused_names = renpy.display.focus.screen_of_focused_names renpy.display.focus.set_focused = set_focused # Add callbacks for label calls and jumps, we use them for the event class # in order to figure out if the event was completed or if it's just a call, # and also for debugging. # class _CallException(Exception): # from_current = False # def __init__(self, label, args, kwargs, from_current=False): # Exception.__init__(self) # self.label = label # self.args = args # self.kwargs = kwargs # self.from_current = from_current # for i in renpy.config.call_callbacks: # i(label, args, kwargs) # def __reduce__(self): # return (_CallException, (self.label, self.args, self.kwargs, self.from_current)) # class _JumpException(Exception): # def __init__(self, label): # for i in renpy.config._label_callbacks: # i(label) # class _JumpOutException(Exception): # def __init__(self, label): # for i in renpy.config._label_callbacks: # i(label) # renpy.game.CONTROL_EXCEPTIONS = tuple(list(renpy.game.CONTROL_EXCEPTIONS) + [_CallException, _JumpException, _JumpOutException]) # renpy.game.CallException = _CallException # renpy.game.JumpException = _JumpException # renpy.game.JumpOutException = _JumpOutException # class _Call(renpy.ast.Call): # def execute(self): # statement_name("call") # label = self.label # if self.expression: # label = renpy.python.py_eval(label) # rv = renpy.game.context().call(label, return_site=self.next.name) # next_node(rv) # renpy.game.context().abnormal = True # if self.arguments: # args, kwargs = self.arguments.evaluate() # renpy.store._args = args # renpy.store._kwargs = kwargs # else: # renpy.store._args = None # renpy.store._kwargs = None # setattr(store, "_last_label_call", label) # if config.developer: # last_label = getattr(store, "_last_label_jump", None) # caller_id = renpy.get_filename_line() # print(f"Called '{stdcol.PURPLE}{label}{stdcol.END}' from '{stdcol.PURPLE}{last_label}{stdcol.END}' with ARGS:'{stdcol.YELLOW}{args}{stdcol.END}' KWARGS:'{stdcol.YELLOW}{kwargs}{stdcol.END}' caller '{stdcol.BLUE}{caller_id}{stdcol.END}'...") # class _Jump(renpy.ast.Jump): # def execute(self): # statement_name("jump") # target = self.target # if self.expression: # target = renpy.python.py_eval(target) # rv = renpy.game.script.lookup(target) # renpy.game.context().abnormal = True # next_node(rv) # setattr(store, "_last_label_jump", target) # if config.developer: # last_label = getattr(store, "_last_label_jump", None) # caller_id = renpy.get_filename_line() # print(f"Jumped '{stdcol.PURPLE}{target}{stdcol.END}' from '{stdcol.PURPLE}{last_label}{stdcol.END}' with ARGS:'{stdcol.YELLOW}{args}{stdcol.END}' KWARGS:'{stdcol.YELLOW}{kwargs}{stdcol.END}' caller '{stdcol.BLUE}{caller_id}{stdcol.END}'...") # renpy.ast.Call = _Call # renpy.ast.Jump = _Jump default _last_label = None python early: def catch_label_call(label, abnormal): if config.developer: ignore = ["_console", "_console_return"] # last_label = renpy.game.context().come_from_label <- Doesn't work as expected, other methods are just as unreliable. # from '{stdcol.PURPLE}{last_label}{stdcol.END}' caller_id = renpy.get_filename_line() if not label in ignore: print(f"Reached '{stdcol.PURPLE}{label}{stdcol.END}' caller '{stdcol.BLUE}{caller_id}{stdcol.END}'...") setattr(store, "_last_label", label) renpy.config.label_callbacks.append(catch_label_call) # def catch_label_call(label, args, kwargs): # # Used in event queue system to differentiate jumps from calls. # if config.developer: # last_label = getattr(store, "_last_label_jump", None) # caller_id = renpy.get_filename_line() # print(f"Called '{stdcol.PURPLE}{label}{stdcol.END}' from '{stdcol.PURPLE}{last_label}{stdcol.END}' with ARGS:'{stdcol.YELLOW}{args}{stdcol.END}' KWARGS:'{stdcol.YELLOW}{kwargs}{stdcol.END}' caller '{stdcol.BLUE}{caller_id}{stdcol.END}'...") # setattr(store, "_last_label_call", label) # def catch_label_jump(label): # # if getattr(store, "_last_label_call", None) == label: # # return # if config.developer: # last_label = getattr(store, "_last_label_jump", None) # caller_id = renpy.get_filename_line() # print(f"Jumped '{stdcol.PURPLE}{label}{stdcol.END}' from '{stdcol.PURPLE}{last_label}{stdcol.END}' caller '{stdcol.BLUE}{caller_id}{stdcol.END}'...") # setattr(store, "_last_label_jump", label) # renpy.config.call_callbacks = [catch_label_call] # CallException callback # renpy.config._label_callbacks = [catch_label_jump] # JumpException and JumpOutException callbacks; Please note this is not the same as `config.label_callbacks` init -100 python: # Due to the sheer number of Doll-type objects we create and keep per each character, # we have to use each and every optimization technique we can get our hands on. # The below code implements revertable __slots__ support, which reduces memory, # without losing any functionality relevant to the objects that are using it. # # The code is taken from a PR proposed by Andykl: # https://github.com/renpy/renpy/pull/3282 class _RevertableObject(_object): # Reimplementation of the Ren'py class, # but with slots support. def __new__(cls, *args, **kwargs): self = super(_RevertableObject, cls).__new__(cls) log = renpy.game.log if log is not None: log.mutated[id(self)] = None return self def __init__(self, *args, **kwargs): if (args or kwargs) and renpy.config.developer: raise TypeError("object() takes no parameters.") __setattr__ = renpy.revertable.mutator(object.__setattr__) # type: ignore __delattr__ = renpy.revertable.mutator(object.__delattr__) # type: ignore def _clean(self): return None def _compress(self, clean): return clean def _rollback(self, compressed): pass class SlottedObject(_RevertableObject): __slots__ = () def __init_subclass__(cls): if renpy.config.developer: if hasattr(cls, "__getstate__"): raise TypeError("slotted_object subclasses can't have " "__getstate__ method. Use __reduce__ instead.") for slot in cls.__dict__.get("__slots__", ()): if slot.startswith("__") and not slot.endswith("__"): raise ValueError("slotted_object __slots__ can not be mangled. " "If you need it, mangle it by yourself.") def _clean(self): rv = object.__reduce_ex__(self, 2)[2] # We need to make a copy of __dict__ to avoid its futher mutations. # No attributes are set if rv is None: rv = { } # Only __dict__ have attributes elif isinstance(rv, dict): rv = rv.copy() # Only __slots__ have attributes elif rv[0] is None: rv = rv[1] # Otherwise it is (__dict__, __slots__), so merge it together else: rv = dict(rv[0], **rv[1]) return rv def _rollback(self, compressed): if hasattr(self, "__dict__"): self.__dict__.clear() for k, v in compressed.items(): setattr(self, k, v) # The original does not support nested actions. # Nor does it support string expressions evaluated at runtime. @renpy.pure def If(expression, true=None, false=None): if isinstance(expression, Action): expression = expression() elif isinstance(expression, str): expression = eval(expression) return true if expression else false @renpy.pure class IfExpr(Action): def __init__(self, expr, true=None, false=None): self.expr = expr self.true = true self.false = false def __call__(self): result = If(self.expr, self.true, self.false) if isinstance(result, Action): return result() return result # Adds support for nested stores for replay scope def _call_replay(label, scope={}): renpy.display.focus.clear_focus() renpy.game.log.complete() old_log = renpy.game.log renpy.game.log = renpy.python.RollbackLog() sb = renpy.python.StoreBackup() renpy.python.clean_stores() context = renpy.execution.Context(True) renpy.game.contexts.append(context) if renpy.display.interface is not None: renpy.display.interface.enter_context() # This has to be here, to ensure the scope stuff works. renpy.exports.execute_default_statement() for k, v in renpy.config.replay_scope.items(): stores = k.split(".") current_obj = renpy.store for store in stores[:-1]: current_obj = getattr(current_obj, store) setattr(current_obj, stores[-1], v) for k, v in scope.items(): stores = k.split(".") current_obj = renpy.store for store in stores[:-1]: current_obj = getattr(current_obj, store) setattr(current_obj, stores[-1], v) renpy.store._in_replay = label try: context.goto_label("_start_replay") renpy.execution.run_context(False) except renpy.game.EndReplay: pass finally: context.pop_all_dynamic() renpy.game.contexts.pop() renpy.game.log = old_log sb.restore() if renpy.game.interface and renpy.game.interface.restart_interaction and renpy.game.contexts: renpy.game.contexts[-1].scene_lists.focused = None renpy.config.skipping = None if renpy.config.after_replay_callback: renpy.config.after_replay_callback() renpy.call_replay = _call_replay # Implement pseudo jump with arguments required to simplify code, @renpy.pure class JumpWith(Action, DictEquality): """ :doc: control_action Causes control to transfer to `label`, given as a string. """ args = tuple() kwargs = dict() def __init__(self, label, *args, **kwargs): self.label = label self.args = args self.kwargs = kwargs def __call__(self): renpy.set_return_stack([]) renpy.call(self.label, *self.args, **self.kwargs) # Allows calling screens in new contexts directly. @renpy.pure def _call_screen_in_new_context(screen, *args, **kwargs): return renpy.call_in_new_context("__call_screen_in_new_context", screen, *args, **kwargs) renpy.call_screen_in_new_context = _call_screen_in_new_context label __call_screen_in_new_context(screen, *args, **kwargs): call screen expression screen pass (*args, **kwargs) return