811 lines
22 KiB
Text
811 lines
22 KiB
Text
# console.rpy
|
|
# Ren'Py console
|
|
# Copyright (C) 2012-2017 Shiz, C, delta, PyTom
|
|
#
|
|
# This program is free software. It comes without any warranty, to the extent permitted by applicable law.
|
|
# You can redistribute it and/or modify it under the terms of the Do What The Fuck You Want To Public License,
|
|
# Version 2, as published by Sam Hocevar. See http://sam.zoy.org/wtfpl/COPYING for more details.
|
|
#
|
|
# Usage:
|
|
# With config.developer or config.console set to True, press the console key (`, the backtick, by default) to open the console.
|
|
# Type 'help' for in-console help. Press escape or right-click to close the console.
|
|
#
|
|
# The following configuration variables are offered for customization:
|
|
# - config.console_history_size: the number of commands to store in history. default: 100
|
|
# - config.console_custom_commands: a simple name -> function dictionary for custom commands. Command functions should take a
|
|
# lexer, and return result text.
|
|
# The following styles are offered for customization:
|
|
# - _console: the debug console frame.
|
|
#
|
|
# - _console_input: the input frame.
|
|
# - _console_prompt: the '>' or '...' text preceding a command input.
|
|
# - _console_input_text: the actual text that is being input by the user.
|
|
#
|
|
# - _console_history: the history frame.
|
|
# - _console_history_item: an item frame in the history.
|
|
# - _console_command: a command frame in the command history.
|
|
# - _console_command_text: the actual command text.
|
|
# - _console_result: the result frame from a command in the command history, if applicable.
|
|
# - _console_result_text: the actual result text, if no error occurred.
|
|
# - _console_result_text: the actual result text, if an error occurred.
|
|
#
|
|
# - _console_trace: the trace box used to show expression and variable traces.
|
|
# - _console_trace_var: the variable in a trace box.
|
|
# - _console_trace_value: the value in a trace box.
|
|
|
|
# Console styles.
|
|
init -1500:
|
|
|
|
style _console is _default:
|
|
xpadding gui._scale(20)
|
|
ypadding gui._scale(10)
|
|
xfill True
|
|
yfill True
|
|
background "#d0d0d0d0"
|
|
|
|
style _console_backdrop:
|
|
background "#d0d0d0"
|
|
|
|
style _console_vscrollbar is _vscrollbar
|
|
|
|
style _console_text is _default:
|
|
size gui._scale(16)
|
|
|
|
style _console_input is _default:
|
|
xfill True
|
|
|
|
style _console_prompt is _console_text:
|
|
minwidth gui._scale(22)
|
|
text_align 1.0
|
|
|
|
style _console_input_text is _console_text:
|
|
color "#000000"
|
|
adjust_spacing False
|
|
|
|
style _console_history is _default:
|
|
xfill True
|
|
|
|
style _console_history_item is _default:
|
|
xfill True
|
|
bottom_margin gui._scale(8)
|
|
|
|
style _console_command is _default:
|
|
left_padding gui._scale(26)
|
|
|
|
style _console_command_text is _console_text:
|
|
color "#000000"
|
|
|
|
style _console_result is _default:
|
|
left_padding gui._scale(26)
|
|
|
|
style _console_result_text is _console_text
|
|
|
|
style _console_error_text is _console_text:
|
|
color "#603030"
|
|
# color "#ff8080"
|
|
|
|
style _console_trace is _default:
|
|
background "#00000040"
|
|
xalign 1.0
|
|
top_margin 20
|
|
right_margin 20
|
|
xpadding 2
|
|
ypadding 2
|
|
|
|
style _console_trace_text is _default:
|
|
color "#fff"
|
|
size gui._scale(16)
|
|
|
|
style _console_trace_var is _console_trace_text:
|
|
bold True
|
|
|
|
style _console_trace_value is _console_trace_text
|
|
|
|
# Configuration and style initalization.
|
|
init -1500 python:
|
|
|
|
# If true, the console is enabled despite config.developer being False.
|
|
config.console = False
|
|
|
|
config.console_history_size = 100
|
|
config.console_history_lines = 1000
|
|
|
|
config.console_commands = { }
|
|
|
|
# If not None, this is called with the command that's about to be run
|
|
# by the console. (The command is represented as a list of strings.) It
|
|
# is expected to return a list of strings, which is the command that will
|
|
# be actually run.
|
|
config.console_callback = None
|
|
|
|
default persistent._console_short = True
|
|
|
|
init -1500 python in _console:
|
|
from store import config, persistent, NoRollback
|
|
import sys
|
|
import traceback
|
|
import store
|
|
|
|
import repr as reprlib
|
|
aRepr = reprlib.Repr()
|
|
aRepr.maxtuple = 20
|
|
aRepr.maxlist = 20
|
|
aRepr.maxarray = 20
|
|
aRepr.maxdict = 10
|
|
aRepr.maxset = 20
|
|
aRepr.maxfrozenset = 20
|
|
aRepr.maxstring = 60
|
|
aRepr.maxother = 200
|
|
|
|
aRepr.repr_RevertableList = aRepr.repr_list
|
|
aRepr.repr_RevertableDict = aRepr.repr_dict
|
|
aRepr.repr_RevertableSet = aRepr.repr_set
|
|
aRepr.repr_defaultdict = aRepr.repr_dict
|
|
aRepr.repr_OrderedDict = aRepr.repr_dict
|
|
|
|
# The list of traced expressions.
|
|
class TracedExpressionsList(NoRollback, list):
|
|
pass
|
|
|
|
class BoundedList(list):
|
|
"""
|
|
A list that's bounded at a certain size.
|
|
"""
|
|
|
|
def __init__(self, size, lines=None):
|
|
self.size = size
|
|
self.lines = lines
|
|
|
|
def append(self, value):
|
|
super(BoundedList, self).append(value)
|
|
|
|
while len(self) >= self.size:
|
|
self.pop(0)
|
|
|
|
if self.lines is not None:
|
|
while (len(self) > 1) and (sum(i.lines for i in self) > self.lines):
|
|
self.pop(0)
|
|
|
|
def clear(self):
|
|
self[:] = [ ]
|
|
|
|
class ConsoleHistoryEntry(object):
|
|
"""
|
|
Represents an entry in the history list.
|
|
"""
|
|
|
|
lines = 0
|
|
|
|
def __init__(self, command, result=None, is_error=False):
|
|
self.command = command
|
|
self.result = result
|
|
self.is_error = is_error
|
|
|
|
def update_lines(self):
|
|
|
|
if self.result is None:
|
|
return
|
|
|
|
lines = self.result.split("\n")
|
|
lines = lines[-config.console_history_lines:]
|
|
self.result = "\n".join(lines)
|
|
self.lines = len(lines)
|
|
|
|
HistoryEntry = ConsoleHistoryEntry
|
|
|
|
|
|
stdio_lines = _list()
|
|
|
|
def stdout_line(l):
|
|
if not config.developer:
|
|
return
|
|
|
|
stdio_lines.append((False, l))
|
|
|
|
while len(stdio_lines) > config.console_history_lines:
|
|
stdio_lines.pop(0)
|
|
|
|
def stderr_line(l):
|
|
if not config.developer:
|
|
return
|
|
|
|
stdio_lines.append((True, l))
|
|
|
|
while len(stdio_lines) > config.console_history_lines:
|
|
stdio_lines.pop(0)
|
|
|
|
|
|
config.stdout_callbacks.append(stdout_line)
|
|
config.stderr_callbacks.append(stderr_line)
|
|
|
|
|
|
class ScriptErrorHandler(object):
|
|
"""
|
|
Handles error in Ren'Py script.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.target_depth = renpy.call_stack_depth()
|
|
|
|
def __call__(self, short, full, traceback_fn):
|
|
he = console.history[-1]
|
|
he.result = short.split("\n")[-2]
|
|
he.is_error = True
|
|
|
|
while renpy.call_stack_depth() > self.target_depth:
|
|
renpy.pop_call()
|
|
|
|
renpy.jump("_console")
|
|
|
|
|
|
class DebugConsole(object):
|
|
|
|
def __init__(self):
|
|
|
|
self.history = BoundedList(config.console_history_size, config.console_history_lines + config.console_history_size)
|
|
self.line_history = BoundedList(config.console_history_size)
|
|
self.line_index = 0
|
|
|
|
if persistent._console_history is not None:
|
|
for i in persistent._console_history:
|
|
he = ConsoleHistoryEntry(i[0], i[1], i[2])
|
|
he.update_lines()
|
|
self.history.append(he)
|
|
|
|
if persistent._console_line_history is not None:
|
|
self.line_history.extend(persistent._console_line_history)
|
|
|
|
self.first_time = True
|
|
|
|
self.reset()
|
|
|
|
def backup(self):
|
|
|
|
persistent._console_history = [ (i.command, i.result, i.is_error) for i in self.history ]
|
|
persistent._console_line_history = list(self.line_history)
|
|
|
|
def start(self):
|
|
he = ConsoleHistoryEntry(None)
|
|
|
|
message = ""
|
|
|
|
if self.first_time:
|
|
message += __("Press <esc> to exit console. Type help for help.\n")
|
|
self.first_time = False
|
|
|
|
if self.can_renpy():
|
|
message += __("Ren'Py script enabled.")
|
|
else:
|
|
message += __("Ren'Py script disabled.")
|
|
|
|
he.result = message
|
|
he.update_lines()
|
|
self.history.append(he)
|
|
|
|
def reset(self):
|
|
|
|
# The list of lines that have been entered by the user, but not yet
|
|
# processed.
|
|
self.lines = [ "" ]
|
|
self.line_index = len(self.line_history)
|
|
|
|
def recall_line(self, offset):
|
|
|
|
self.line_index += offset
|
|
|
|
if self.line_index < 0:
|
|
self.line_index = 0
|
|
|
|
if self.line_index > len(self.line_history):
|
|
self.line_index = len(self.line_history)
|
|
|
|
if self.line_index == len(self.line_history):
|
|
self.lines = [ "" ]
|
|
else:
|
|
self.lines = list(self.line_history[self.line_index])
|
|
|
|
renpy.jump("_console")
|
|
|
|
def older(self):
|
|
self.recall_line(-1)
|
|
|
|
def newer(self):
|
|
self.recall_line(1)
|
|
|
|
def interact(self):
|
|
|
|
self.show_stdio()
|
|
|
|
def get_indent(s):
|
|
"""
|
|
Computes the indentation for the line following line s.
|
|
"""
|
|
|
|
rv = ""
|
|
|
|
for i in s:
|
|
if i == " ":
|
|
rv += " "
|
|
else:
|
|
break
|
|
|
|
if s.rstrip().endswith(":"):
|
|
rv += " "
|
|
|
|
if not s.rstrip():
|
|
rv = rv[:-4]
|
|
|
|
return rv
|
|
|
|
renpy.game.context().exception_handler = None
|
|
|
|
renpy.show_screen("_console", lines=self.lines[:-1], default=self.lines[-1], history=self.history, _transient=True)
|
|
line = ui.interact()
|
|
|
|
self.lines.pop()
|
|
self.lines.append(line)
|
|
|
|
indent = get_indent(line)
|
|
if indent:
|
|
self.lines.append(indent)
|
|
return
|
|
|
|
lines = self.lines
|
|
self.line_history.append(lines)
|
|
|
|
self.reset()
|
|
|
|
if config.console_callback is not None:
|
|
lines = config.console_callback(lines)
|
|
|
|
if not lines:
|
|
return
|
|
|
|
try:
|
|
self.run(lines)
|
|
finally:
|
|
self.backup()
|
|
|
|
def show_stdio(self):
|
|
|
|
old_entry = None
|
|
|
|
if persistent._console_short:
|
|
if len(stdio_lines) > 30:
|
|
stdio_lines[:] = stdio_lines[:10] + [ (False, " ... ") ] + stdio_lines[-20:]
|
|
|
|
for error, l in stdio_lines:
|
|
if persistent._console_short:
|
|
if len(l) > 200:
|
|
l = l[:100] + "..." + l[-100:]
|
|
|
|
if (old_entry is not None) and (error == old_entry.is_error):
|
|
old_entry.result += "\n" + l
|
|
else:
|
|
e = ConsoleHistoryEntry(None, l, error)
|
|
e.update_lines()
|
|
self.history.append(e)
|
|
old_entry = e
|
|
|
|
if old_entry is not None:
|
|
old_entry.update_lines()
|
|
|
|
stdio_lines[:] = _list()
|
|
|
|
def can_renpy(self):
|
|
"""
|
|
Returns true if we can run Ren'Py code.
|
|
"""
|
|
|
|
return renpy.game.context().rollback
|
|
|
|
def format_exception(self):
|
|
etype, evalue, etb = sys.exc_info()
|
|
return traceback.format_exception_only(etype, evalue)[-1]
|
|
|
|
def run(self, lines):
|
|
|
|
line_count = len(lines)
|
|
code = "\n".join(lines)
|
|
|
|
he = ConsoleHistoryEntry(code)
|
|
self.history.append(he)
|
|
|
|
try:
|
|
|
|
# If we have 1 line, try to parse it as a command.
|
|
if line_count == 1:
|
|
block = [ ( "<console>", 1, code, [ ]) ]
|
|
l = renpy.parser.Lexer(block)
|
|
l.advance()
|
|
|
|
# Command can be None, but that's okay, since the lookup will fail.
|
|
command = l.word()
|
|
|
|
command_fn = config.console_commands.get(command, None)
|
|
|
|
if command_fn is not None:
|
|
he.result = command_fn(l)
|
|
he.update_lines()
|
|
return
|
|
|
|
error = None
|
|
|
|
# Try to run it as Ren'Py.
|
|
if self.can_renpy():
|
|
|
|
# TODO: Can we run Ren'Py code?
|
|
name = renpy.load_string(code + "\nreturn")
|
|
|
|
if name is not None:
|
|
renpy.game.context().exception_handler = ScriptErrorHandler()
|
|
renpy.call(name)
|
|
else:
|
|
error = "\n\n".join(renpy.get_parse_errors())
|
|
|
|
# Try to eval it.
|
|
try:
|
|
renpy.python.py_compile(code, 'eval')
|
|
except:
|
|
pass
|
|
else:
|
|
result = renpy.python.py_eval(code)
|
|
if persistent._console_short:
|
|
he.result = aRepr.repr(result)
|
|
else:
|
|
he.result = repr(result)
|
|
|
|
he.update_lines()
|
|
return
|
|
|
|
# Try to exec it.
|
|
try:
|
|
renpy.python.py_compile(code, "exec")
|
|
except:
|
|
if error is None:
|
|
error = self.format_exception()
|
|
else:
|
|
renpy.python.py_exec(code)
|
|
return
|
|
|
|
if error is not None:
|
|
he.result = error
|
|
he.update_lines()
|
|
he.is_error = True
|
|
|
|
except renpy.game.CONTROL_EXCEPTIONS:
|
|
raise
|
|
|
|
except:
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
he.result = self.format_exception().rstrip()
|
|
he.update_lines()
|
|
he.is_error = True
|
|
|
|
|
|
console = None
|
|
|
|
def enter():
|
|
"""
|
|
Called to enter the debug console.
|
|
"""
|
|
|
|
if console is None:
|
|
return
|
|
|
|
console.start()
|
|
|
|
if renpy.game.context().rollback:
|
|
try:
|
|
renpy.rollback(checkpoints=0, force=True, greedy=False, current_label="_console")
|
|
except renpy.game.CONTROL_EXCEPTIONS:
|
|
raise
|
|
except:
|
|
pass
|
|
|
|
renpy.call_in_new_context("_console")
|
|
|
|
# Has to run after 00library.
|
|
init 1701 python in _console:
|
|
|
|
if config.developer or config.console:
|
|
console = DebugConsole()
|
|
|
|
init -1500 python in _console:
|
|
|
|
def command(help=None):
|
|
def wrap(f):
|
|
f.help = help
|
|
config.console_commands[f.__name__] = f
|
|
return f
|
|
|
|
return wrap
|
|
|
|
@command(_("help: show this help"))
|
|
def help(l):
|
|
keys = list(config.console_commands.iterkeys())
|
|
keys.sort()
|
|
|
|
rv = __("commands:\n")
|
|
|
|
for k in keys:
|
|
f = config.console_commands[k]
|
|
if f.help is None:
|
|
continue
|
|
|
|
rv += " " + __(f.help) + "\n"
|
|
|
|
if console.can_renpy():
|
|
rv += __(" <renpy script statement>: run the statement\n")
|
|
|
|
rv += __(" <python expression or statement>: run the expression or statement")
|
|
|
|
return rv
|
|
|
|
@command()
|
|
def halp(l):
|
|
return help(l).replace("e", "a")
|
|
|
|
@command(_("clear: clear the console history"))
|
|
def clear(l):
|
|
console.history[:] = [ ]
|
|
|
|
@command(_("exit: exit the console"))
|
|
def exit(l):
|
|
renpy.jump("_console_return")
|
|
|
|
@command()
|
|
def quit(l):
|
|
renpy.jump("_console_return")
|
|
|
|
@command(_("load <slot>: loads the game from slot"))
|
|
def load(l):
|
|
name = l.rest().strip()
|
|
|
|
if not name:
|
|
raise Exception("Slot name must not be empty")
|
|
|
|
try:
|
|
renpy.load(name)
|
|
finally:
|
|
console.history[-1].result = "Loading slot {!r}.".format(name)
|
|
|
|
|
|
@command(_("save <slot>: saves the game in slot"))
|
|
def save(l):
|
|
name = l.rest().strip()
|
|
|
|
if not name:
|
|
raise Exception("Slot name must not be empty")
|
|
|
|
renpy.save(name)
|
|
|
|
return "Saved slot {!r}.".format(name)
|
|
|
|
@command(_("reload: reloads the game, refreshing the scripts"))
|
|
def reload(l):
|
|
store._reload_game()
|
|
|
|
@command()
|
|
def R(l):
|
|
store._reload_game()
|
|
|
|
@command(_("watch <expression>: watch a python expression"))
|
|
def watch(l):
|
|
expr = l.rest()
|
|
expr.strip()
|
|
renpy.python.py_compile(expr, 'eval')
|
|
|
|
traced_expressions.append(expr)
|
|
renpy.show_screen("_trace_screen")
|
|
|
|
def renpy_watch(expr):
|
|
"""
|
|
:name: renpy.watch
|
|
:doc: debug
|
|
|
|
This watches the given Python expression, by displaying it in the
|
|
upper-right corner of the screen.
|
|
"""
|
|
|
|
block = [ ( "<console>", 1, expr, [ ]) ]
|
|
|
|
l = renpy.parser.Lexer(block)
|
|
l.advance()
|
|
watch(l)
|
|
|
|
renpy.watch = renpy_watch
|
|
|
|
@command(_("unwatch <expression>: stop watching an expression"))
|
|
def unwatch(l):
|
|
expr = l.rest()
|
|
expr.strip()
|
|
|
|
if expr in traced_expressions:
|
|
traced_expressions.remove(expr)
|
|
|
|
def watch_after_load():
|
|
if config.developer and traced_expressions:
|
|
renpy.show_screen("_trace_screen")
|
|
|
|
config.after_load_callbacks.append(watch_after_load)
|
|
|
|
def renpy_unwatch(expr):
|
|
"""
|
|
:name: renpy.unwatch
|
|
:doc: debug
|
|
|
|
Stops watching the given Python expression.
|
|
"""
|
|
|
|
block = [ ( "<console>", 1, expr, [ ]) ]
|
|
|
|
l = renpy.parser.Lexer(block)
|
|
l.advance()
|
|
unwatch(l)
|
|
|
|
renpy.unwatch = renpy_unwatch
|
|
|
|
|
|
@command(_("unwatchall: stop watching all expressions"))
|
|
def unwatchall(l):
|
|
traced_expressions[:] = [ ]
|
|
renpy.hide_screen("_trace_screen")
|
|
|
|
def renpy_unwatchall():
|
|
"""
|
|
:name: renpy.unwatch
|
|
:doc: debug
|
|
|
|
Stops watching all Python expressions.
|
|
"""
|
|
|
|
unwatchall(None)
|
|
|
|
renpy.unwatchall = renpy_unwatchall
|
|
|
|
@command(_("jump <label>: jumps to label"))
|
|
def jump(l):
|
|
label = l.label_name()
|
|
|
|
if label is None:
|
|
raise Exception("Could not parse label. (Unqualified local labels are not allowed.)")
|
|
|
|
if not console.can_renpy():
|
|
raise Exception("Ren'Py script not enabled. Not jumping.")
|
|
|
|
if not renpy.has_label(label):
|
|
raise Exception("Label %s not found." % label)
|
|
|
|
renpy.pop_call()
|
|
renpy.jump(label)
|
|
|
|
@command(_("short: Shorten the representation of objects on the console (default)."))
|
|
def short(l):
|
|
persistent._console_short = True
|
|
|
|
|
|
@command(_("long: Print the full representation of objects on the console."))
|
|
def long(l):
|
|
persistent._console_short = False
|
|
|
|
|
|
screen _console:
|
|
# This screen takes as arguments:
|
|
#
|
|
# lines
|
|
# The current set of lines in the input buffer.
|
|
# indent
|
|
# Indentation to apply to the new line.
|
|
# history
|
|
# A list of command, result, is_error tuples.
|
|
zorder 1500
|
|
modal True
|
|
|
|
if not _console.console.can_renpy():
|
|
frame:
|
|
style "_console_backdrop"
|
|
|
|
frame:
|
|
style "_console"
|
|
|
|
has viewport:
|
|
style_prefix "_console"
|
|
mousewheel True
|
|
scrollbars "vertical"
|
|
yinitial 1.0
|
|
|
|
has vbox
|
|
|
|
# Draw historical console input.
|
|
|
|
frame style "_console_history":
|
|
|
|
has vbox:
|
|
xfill True
|
|
|
|
for he in history:
|
|
|
|
frame style "_console_history_item":
|
|
has vbox
|
|
|
|
if he.command is not None:
|
|
frame style "_console_command":
|
|
xfill True
|
|
text "[he.command!q]" style "_console_command_text"
|
|
|
|
if he.result is not None:
|
|
|
|
frame style "_console_result":
|
|
if he.is_error:
|
|
text "[he.result!q]" style "_console_error_text"
|
|
else:
|
|
text "[he.result!q]" style "_console_result_text"
|
|
|
|
# Draw the current input.
|
|
frame style "_console_input":
|
|
|
|
has vbox
|
|
|
|
for line in lines:
|
|
hbox:
|
|
spacing 4
|
|
|
|
if line[:1] != " ":
|
|
text "> " style "_console_prompt"
|
|
else:
|
|
text "... " style "_console_prompt"
|
|
|
|
text "[line!q]" style "_console_input_text"
|
|
|
|
hbox:
|
|
spacing 4
|
|
|
|
if default[:1] != " ":
|
|
text "> " style "_console_prompt"
|
|
else:
|
|
text "... " style "_console_prompt"
|
|
|
|
input default default style "_console_input_text" exclude "" copypaste True
|
|
|
|
|
|
key "game_menu" action Jump("_console_return")
|
|
key "console_older" action _console.console.older
|
|
key "console_newer" action _console.console.newer
|
|
|
|
default _console.traced_expressions = _console.TracedExpressionsList()
|
|
|
|
screen _trace_screen:
|
|
|
|
zorder 1501
|
|
|
|
if _console.traced_expressions:
|
|
|
|
frame style "_console_trace":
|
|
|
|
vbox:
|
|
|
|
for expr in _console.traced_expressions:
|
|
python:
|
|
try:
|
|
value = repr(eval(expr))
|
|
except:
|
|
value = "eval failed"
|
|
|
|
hbox:
|
|
text "[expr!q]: " style "_console_trace_var"
|
|
text "[value!q]" style "_console_trace_value"
|
|
|
|
# The label that is called by _console.enter to actually run the console.
|
|
# This can be called in the current context (for normal Ren'Py code) or
|
|
# in a new context (in menus).
|
|
label _console:
|
|
|
|
while True:
|
|
python in _console:
|
|
console.interact()
|
|
|
|
label _console_return:
|
|
return
|