# console.rpy # Ren'Py debug console # Copyright (C) 2012-2013 Shiz, C, idea by delta. # Released under the WTFPL: see http://sam.zoy.org/wtfpl/COPYING for details. # # Usage: # With config.developer set to True, press tilde (~) in-game to open the console. # Type 'help' for in-console help. # # The following styles are offered for customization: # - debug_console_prompt: the '>' or '...' preceding a command input. # - debug_console_input: the command input text. # - debug_console_command: a command in the command history. # - debug_console_result: the result from a command in the command history. # # Known bugs/defects: # - Transform or image definitions do not work because they must be defined at init-time. # - Menus can mess with flow. This is solved by adding 'return' to the end of every console-defined menu option. # - When writing multi-line blocks, you can't go back to a previous line to edit it. # Tested on Ren'Py 6.13.11. Not guaranteed to work on any other version. # This label is required for renpy.call_in_new_context(), # because renpy.invoke_in_new_context() has no support for renpy.jump_out_of_context() for jumps. label debug_console: $ debug_console.interact() init python: # Simple circular buffer to hold the command history; # will automatically delete older commands once the limit has reached. class RingBuffer: def __init__(self, size, default_value=None): self.size = size self.default_value = default_value self.data = [ self.default_value for i in xrange(size)] def push(self, value): self.data.pop(0) self.data.append(value) def pop(self): self.data.append(self.default_value) return self.data.pop(0) def get(self, i): return self.data[i] def put(self, i, value): self.data[i] = value class DebugConsole: def __init__(self, layer='debug_console', history_size=20): self.enabled = False self.layer = layer self.history_size = history_size self.history = RingBuffer(self.history_size, default_value=(None, None)) self.help_message = ("" "Ren\'Py debug console\n" " by Shiz and C. Idea by delta.\n" "commands:\n" " - execute Ren'Py command as if it were placed in a script file.\n" " $ - execute Python command.\n" " clear - clear the command history.\n" " crash - crash the game with an appropriate error message.\n" " help - show this message.\n" " exit/quit/~ - close the console.\n") def enable(self): if not self.enabled: self.enabled = True renpy.call_in_new_context('debug_console') def disable(self): if self.enabled: ui.layer(self.layer) ui.clear() ui.close() self.enabled = False # Accept a command, allowing for recursive blocks. def take_command(self): command = "" depth = 0 lines = [] # Draw initial command prompt. ui.layer(self.layer) ui.text('> ', id='debug_console_prompt', style='debug_console_prompt', xpos=25, ypos=40) ui.input('', id='debug_console_input', style='debug_console_input', xpos=50, ypos=40) ui.close() while True: # Get input. mean = ui.interact() lines.append((mean, depth)) if mean == '' or mean == 'end': depth -= 1 else: command += "\n" + " " * depth + mean # Command enters in :, meaning a new block. Increase nesting. if mean.endswith(':'): depth += 1 # Draw another prompt if we are at a sublevel. if depth >= 1: ui.layer(self.layer) ui.clear() ui.close() ui.layer(self.layer) # Print previous lines. line_count = 0 for line, d in lines: if d == 0: ui.text('> ', id='debug_console_prompt', style='debug_console_prompt', xpos=25, ypos=40) ui.text(line, id='debug_console_line_' + str(line_count), style='debug_console_input', xpos=50, ypos=40) else: ui.text('...' * d, id='debug_console_more_' + str(line_count), style='debug_console_prompt', xpos=50, ypos=40 + 20 * line_count) ui.text(line, id='debug_console_line_' + str(line_count), style='debug_console_input', xpos=75 + 15 * d, ypos=40 + 20 * line_count) line_count += 1 # Print current input. ui.text('...' * depth, id='debug_console_more', style='debug_console_prompt', xpos=50, ypos=40 + 20 * len(lines)) ui.input('', id='debug_console_input', style='debug_console_input', xpos=75 + 15 * depth, ypos=40 + 20 * len(lines)) ui.close() # Re-print history at appropriate position self.print_history(70 + 20 * len(lines)) # Else, interaction is done. else: break # Clear all input lines now that we're done. ui.layer(self.layer) ui.clear() ui.close() return command.strip("\n") def interact(self, **kwargs): # Set script re-entry points and node list 'tail' for AST injection. self.script_entry_point = renpy.game.context(-2).next_node self.parent_node = renpy.game.context(-2) self.parent_is_context = True # Print previous results in case we are re-entering the console. self.print_history() while self.enabled: command = self.take_command() # 'quit', 'exit', an empty string or '~' exit the console interaction. if command == 'quit' or command == 'exit' or command == '~' or command == '': self.disable() return # 'clear' clears the command history and thus the screen. elif command == 'clear': for i in range(self.history_size): self.history.push((None, None)) else: result = self.execute(command) self.history.push((command, result)) # Allow the UI to adapt to its consequences. renpy.restart_interaction() # Print command history including the fresh result. self.print_history() def print_history(self, y=70): ui.layer(self.layer) elems = range(self.history.size) elems.reverse() for i in elems: command, result = self.history.get(i) if command is None: continue ui.text(command, id='debug_console_command_' + str(i), style='debug_console_command', xpos=60, ypos=y) y += 25 * (1 + command.count("\n")) if result is not None: ui.text(result, id='debug_console_result_' + str(i), style='debug_console_result', xpos=70, ypos=y) y += 25 * (1 + result.count("\n")) ui.close() def execute(self, command): # Crash the game with an appropriate error message. if command == 'crash': errors = [ 'Expected statement', 'Cannot start an interaction in the middle of an interaction, without creating a new context.', 'screen() got an unexpected keyword argument _name' 'indentation mismatch', 'Could not find user-defined statement at runtime.', 'Context not capable of executing Ren\'Py code.' 'error in Python block starting at line %d: TypeError' % renpy.random.randint(1, 25) ]; raise TypeError(renpy.random.choice(errors)) # Easter eggs. elif command == '...': return '...' # Show help. elif command == 'help': return self.help_message # Easter egg help. elif command == 'halp': return self.help_message.replace('e', 'a') # Python code. elif command.startswith('$'): try: # Because eval can't handle assignments, we have to pass on assignments to exec if encountered. # No, it's not pretty. command = command.lstrip('$ ') try: result = repr(eval(command)) except SyntaxError, e: if "=" in command: # Use renpy.store as locals, to properly store assignments. exec command in globals(), vars(renpy.store) result = None else: raise except: (name, value, traceback) = sys.exc_info() result = repr(value) finally: return result # Ren'Py statement. else: # Transform command into logical parser lines. line_number = 1 lines = [] for line in command.splitlines(): if line.strip() == '': continue lines.append(('', line_number, line)) line_number += 1 # Generate logical blocks and feed them to the lexer. blocks = renpy.parser.group_logical_lines(lines) lexer = renpy.parser.Lexer(blocks) # Feed the lexer to the block parser, effectively parsing the command. nodes = renpy.parser.parse_block(lexer) if renpy.parser.parse_errors: # One or more errors occurred, display them. errors = renpy.parser.parse_errors renpy.parser.parse_errors = [] return "\n".join(errors) else: # Inject generated nodes into the AST and execute them. # This is not nice. try: for node in nodes: # We have to handle jump and call as special cases, since they involve context wizardry, # and we're in a different (debug console) context. if isinstance(node, renpy.ast.Jump): renpy.jump_out_of_context(node.target) elif isinstance(node, renpy.ast.Call): renpy.call_in_new_context(node.label) else: self.inject_node(node) node.execute() except renpy.game.JumpOutException: # Pass JumpOutExceptions back to call_in_new_context, which will then perform the proper jump. raise except: (name, value, traceback) = sys.exc_info() return repr(value) def inject_node(self, node): # Perform actual AST injection. Don't ask. # Set next node of current script tail node. if self.parent_is_context: self.parent_node.next_node = node self.parent_is_context = False else: self.parent_node.next = node # Set the new script tail node, and its next node to be back at our original script. self.parent_node = node self.parent_node.next = self.script_entry_point if config.developer: debug_console = DebugConsole() style.create('debug_console_prompt', 'text') style.create('debug_console_input', 'input') style.create('debug_console_command', 'text') style.create('debug_console_result', 'text') config.top_layers.append(debug_console.layer) def debug_overlay(): ui.keymap(shift_K_BACKQUOTE=debug_console.enable) config.overlay_functions.append(debug_overlay)