"""Get and modify nodes in the AST tree
This file is free software under the MIT license
"""
import os
import string
import renpy
from renpy import ast
[docs]def get_screen(scr, nodes=None):
"""Get a screen based off of its name
Args:
scr (str): The screen's name
nodes (list): A list of screen variants. If None, default to all screens
Returns:
A :class:`renpy.display.screen.Screen` object
"""
return renpy.display.screen.get_screen_variant(scr, nodes)
[docs]def get_slscreen(scr, nodes=None):
"""Get a screen's executable based off of its name
Args:
scr (str): The screen's name
nodes (list): A list of screen variants. If None, default to all screens
Returns:
A :class:`renpy.display.screen.Screen` object
"""
return get_screen(scr, nodes).ast
[docs]def find_label(label):
"""Find a label based off of its name
Args:
label (str): The label's name
Returns:
A :class:`renpy.ast.Label` object
"""
return renpy.game.script.lookup(label)
[docs]def find_jump_target(target_label, one=True):
"""Find a jump node (all if one is False) which is bound to the target_label
Args:
target_label (str): The target label's name
Returns:
Union[Node, List[Node]]
"""
results = []
for node in renpy.game.script.all_stmts:
if isinstance(node, ast.Jump) and node.target == target_label:
if one:
return node
results.append(node)
return results
[docs]def search_for_node_type(node, type_, max_depth=200):
"""Search for a specific type of node
Args:
node (Node): The node to start the search
type_ (class): The node class, not an instance of the class, to search for
max_depth (int): The number of nodes to search before giving up
Defaults to 200. The higher the number, the slower the process
Returns:
A :class:`renpy.ast.Node` object if a match occurs or None if no match occurs
"""
for _ in range(1, max_depth):
node = node.next
if node:
if isinstance(node, type_):
return node
else:
return None
[docs]def search_for_node_with_criteria(node, func, max_depth=200):
"""Search for a node and check with ``func``
If ``func`` returns a truthy value, return the node. Else, skip it
Args:
node (Node): The node to start the search
func (function): Function to check for the given node. Given one argument,
node (of type :class:`renpy.ast.Node`), which is the node that is at the current depth.
Do not make the functions complex or it will slow down the game significantly.
max_depth (int): The number of nodes to search before giving up
Defaults to 200. The higher the number, the slower the process
Returns:
A :class:`renpy.ast.Node` object if a match occurs or None if no match occurs
"""
for _ in range(1, max_depth):
node = node.next
#TODO: Figure out why ``if node and func(node):`` doesn't work
if node:
if func(node):
return node
else:
return None
[docs]def remove_slif(scr, comparison):
"""Remove a block (equivalent to a Node) from a :class:`renpy.sl2.slast.SLIf` object
Args:
scr (SLScreen): The SLScreen node to start the search
comparison (str): The comparison string for the expression
Returns:
True if removed, False if not
"""
for child in scr.children:
if isinstance(child, renpy.sl2.slast.SLIf):
for condition, block in child.entries:
if condition == comparison:
block.children = []
return True
return False
[docs]def find_say(needle):
"""Find a :class:`renpy.ast.Say` node based on what is said
This searches the entire AST tree for the specified statement.
Args:
needle (str): The statement to search for
Returns:
A :class:`renpy.ast.Node` node
"""
for node in renpy.game.script.all_stmts:
if isinstance(node, ast.Say) and node.what == needle:
return node
return None
[docs]def find_all_hide(hide_name):
"""Find a list of :class:`renpy.ast.Hide` nodes based on a string
This searches the entire AST tree for the all the instances of the specified statement.
Args:
hide_name (str): The string to search in Hide nodes
Returns:
A list of :class:`renpy.ast.Node` nodes
"""
# Make a list so we can store all applicable nodes in
result = []
# Loop over every node in the game
for node in renpy.game.script.all_stmts:
# Ignore non-Hide nodes
if isinstance(node, ast.Hide):
# Compare the search string and the object the node is hiding
# Note: The comma makes it a one-element tuple, which impsec is
if node.imspec[0] == (hide_name,):
result.append(node)
return result # Return the list
[docs]def find_all_show(show_name):
"""Find a list of :class:`renpy.ast.Show` nodes based on a string
This searches the entire AST tree for the all the instances of the specified statement.
Args:
show_name (str): The string to search in Show nodes
Returns:
A list of :class:`renpy.ast.Node` nodes
"""
rtn = []
for node in renpy.game.script.all_stmts:
if isinstance(node, ast.Show):
if node.imspec[0] == (show_name,):
rtn.append(node)
return rtn
[docs]def find_in_source_code(line_number, file_name):
"""Find a node by line number and file name
Note:
Line numbers and file names can change between versions. Use the
function as a last resort only.
Args:
line_number (int): The line number
file_name (str): The file name
Returns:
The node or None if it doesn't meet the criteria provided
"""
for node in renpy.game.script.all_stmts:
head, tail = os.path.split(node.filename)
node_file_name = tail or os.path.basename(head)
if node.linenumber == line_number and node_file_name == file_name:
return node
return None
[docs]def find_python_statement(statement, all=False):
"""Find a specific Python node in the entire AST
Args:
statement (str): The Python statement to look for
all (bool): If you want to return all such python statements or just one. Defaults to False.
Returns:
The Python node if found, None if not
Or a list of nodes which satisfy the conditions
"""
rtn = []
for node in renpy.game.script.all_stmts:
if isinstance(node, ast.Python) and node.code.source == statement:
if all:
rtn.append(node)
else:
return node
if all:
return rtn
return None
ROT13 = string.maketrans(
"NOPQRSTUVWXYZnopqrstuvwxyzABCDEFGHIJKLMabcdefghijklm",
"ABCDEFGHIJKLMabcdefghijklmNOPQRSTUVWXYZnopqrstuvwxyz")
[docs]class ASTHook(ast.Node):
"""A custom :class:`renpy.ast.Node` that acts as a hook between
other node objects.
Note:
Don't instantiate this class directly. Ren'Py uses an
internal serial for the call stack and other uses. This
class emulates that at the base class.
Attributes:
hook_func: A function that's called when the node is executed.
If the function returns a non-None value, the next
node will be skipped.
from_op: The original node before hooking
old_next: The original next node before hooking was done
"""
_serial = 1
def __init__(self, loc, hook_func_=None, from_op_=None):
super(ASTHook, self).__init__(loc)
self.hook_func = hook_func_
self.from_op = from_op_
self.old_next = None
# Create a unique name
self.name = "AWSWModOp_" + str(ASTHook._serial)
ASTHook._serial += 1
renpy.game.script.namemap[self.name] = self
[docs] def execute(self):
"""Execute hook after node is called"""
ast.statement_name("hook")
ret = None
if self.hook_func:
ret = self.hook_func(self)
if not ret:
self.exec_continue()
[docs] def exec_continue(self):
"""Continue"""
ast.next_node(self.next)
[docs] def unhook(self):
"""Remove the hook"""
self.from_op.next = self.old_next
[docs]def hook_opcode(node, func):
"""Hook ``func`` to ``node``
Args:
node (Node): The node object for the function to hook
func (function): The function to be executed when the node is executed
Todo:
Check if a hook already exists and make the code more cohesive
Returns:
An :class:`ASTHook` object
"""
# Keep a copy of the node's original next node
next_statement = node.next
# Make a new ASTHook and hook it to the node
# The tuple is in the format (filename, filenumber)
# This is used by the renpy stacktrace
hook = ASTHook(("AWSWMod", 1), func, node)
node.next = hook
# Put the original next node to the hook node
# Also keep a copy of the original next node in the hook node, allowing us to unhook it
hook.chain(next_statement)
hook.old_next = next_statement
return hook
[docs]def call_hook(node, dest_node, func=None, return_node=None):
"""Hook ``func`` to ``node`` and once executed, redirect execution to
``dest_node``
Args:
node (Node): The node to hook
dest_node (Node): the node to go after ``node`` is executed
func (function): The function to call
Returns:
An :class:`ASTHook` object
"""
hook = hook_opcode(node, None)
def call_function(hook):
# pylint: disable=missing-docstring
if func:
func(hook)
#TODO: Better understand this line
label = renpy.game.context().call(dest_node.name,
return_site=hook.old_next.name if return_node is None else
return_node.name)
hook.chain(label)
hook.hook_func = call_function
return hook
[docs]def unhook_label(label):
"""Unhook a hook from a label
Args:
label (str): The label's name
"""
#TODO: Test this
found_node = find_label(label)
if isinstance(found_node, ASTHook):
found_node.from_op.next = found_node.next
[docs]def disable_slast_cache():
"""Disable SLAst's load cache"""
renpy.sl2.slast.load_cache = lambda *_: None
[docs]def disable_bytecode_cache():
"""Disable bytecode cache"""
renpy.game.script.init_bytecode = lambda *_: None
[docs]def get_node_after_nodes(node, location):
"""Get the ``location``th node after ``node``
Note:
This skips :class:`ASTHook` nodes
Args:
node (Node): The starting search node
location (int): The number of nodes to skip
Returns:
A :class:`renpy.ast.Node` object
"""
for _ in range(0, location):
node = node.next
# Effectively skip the ASTHook nodes by continuing on
while node and isinstance(node, ASTHook):
node = node.next
return node
[docs]def get_renpy_global(key):
"""Get a Ren'Py global
Args:
key (str): The dictionary key
Returns:
The value put into the key or None if it doesn't exist
"""
store = renpy.python.store_dicts["store"]
if key in store:
return store[key]
[docs]def set_renpy_global(key, val):
"""Set a Ren'Py glboal
Ren'Py globals can be used during execution of rpy.
Args:
key (str): The dictionary key
val (str): The value of the dictionary object
"""
renpy.python.store_dicts["store"][key] = val
[docs]def jump_ret(node, dest_node, return_node, func=None):
"""Hook ``func`` to ``node`` and once executed, redirect execution to
``dest_node`` and allow ``return_node`` to be executed after
``dest_node`` returns
Args:
node (Node): The node to hook
dest_node (Node): The node to go after ``node`` is executed
return_node (Node): The node that is executed after ``dest_node`` returns
func (function): The function hook
Returns:
An :class:`ASTHook` object
"""
return call_hook(node, dest_node, func, return_node)
[docs]def hook_label(label, func):
"""Hook a function to a label
Args:
label (renpy.ast.Label): The label
func (function): The function to be hooked
Returns:
An :class:`ASTHook` object
"""
node_label = find_label(label)
return hook_opcode(node_label, func)