399 lines
14 KiB
Plaintext
399 lines
14 KiB
Plaintext
# 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
|