From dc1c0b327a5427057e4268be74da8b2e46f0f98e Mon Sep 17 00:00:00 2001 From: Alex Xu Date: Sun, 6 Apr 2014 13:19:51 -0400 Subject: reimplement imachine --- ast2json/imachine2json.py | 4 +- ast2json/renpy/__init__.py | 272 ++----- ast2json/renpy/ast.py | 1023 ++++++++--------------- ast2json/renpy/atl.py | 179 ++-- ast2json/renpy/game.py | 259 ++---- ast2json/renpy/object.py | 120 +-- ast2json/renpy/parser.py | 1842 ++++++++++++++++++++++++++++++++++++++++++ ast2json/renpy/statements.py | 686 ++++++++++++++++ ast2json/rpyc2json.py | 6 + 9 files changed, 3127 insertions(+), 1264 deletions(-) create mode 100644 ast2json/renpy/parser.py create mode 100644 ast2json/renpy/statements.py (limited to 'ast2json') diff --git a/ast2json/imachine2json.py b/ast2json/imachine2json.py index db1878c..e865a73 100755 --- a/ast2json/imachine2json.py +++ b/ast2json/imachine2json.py @@ -8,9 +8,9 @@ def imachine2json(ast): if ast[0]['_type'] != 'Label': raise TypeError('obj does not start with Label, wrong file?') for label in ast: - if label['parameters'] is not None or label['hide']: + if label['parameters'] is not None: raise NotImplementedError() - ret[label['name']] = label + ret[label['name']] = label['block'] return ret with open(sys.argv[1], 'r') as f: diff --git a/ast2json/renpy/__init__.py b/ast2json/renpy/__init__.py index f2bd463..a4190b1 100644 --- a/ast2json/renpy/__init__.py +++ b/ast2json/renpy/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2004-2013 Tom Rothamel +# Copyright 2004-2010 PyTom # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files @@ -22,250 +22,110 @@ # This file ensures that renpy packages will be imported in the right # order. -import sys -import os +# Some version numbers and things. -# Version numbers. -try: - from renpy.vc_version import vc_version; vc_version -except ImportError: - vc_version = 0 - -# The tuple giving the version. This needs to be updated when -# we bump the version. -# -# Be sure to change script_version in launcher/script_version.rpy. -# Be sure to change config.version in tutorial/game/options.rpy. -version_tuple = (6, 15, 4, vc_version) - -# A verbose string computed from that version. -version = "Ren'Py " + ".".join(str(i) for i in version_tuple) - -# Other versions. +# ***** ***** ***** ***** ***** ***** **** ***** ***** ***** ***** +# Be sure to change script_version in launcher/script_version.rpy, too! +# Also check to see if we have to update renpy.py. +version = "Ren'Py 6.10.2e" script_version = 5003000 savegame_suffix = "-LT1.save" -bytecode_version = 1 - -# True if this is the first time we've started - even including -# utter restarts. -first_utter_start = True - -def setup_modulefinder(modulefinder): - import _renpy - - libexec = os.path.dirname(_renpy.__file__) - - for i in [ "display", "gl", "angle", "text" ]: - - displaypath = os.path.join(libexec, "renpy", i) - - if os.path.exists(displaypath): - modulefinder.AddPackagePath('renpy.' + i, displaypath) - -def import_cython(): - """ - Never called, but necessary to ensure that modulefinder will properly - grab the various cython modules. - """ - - import renpy.arguments #@UnresolvedImport - - import renpy.display.accelerator #@UnresolvedImport - import renpy.display.render #@UnresolvedImport - import renpy.gl.gldraw #@UnresolvedImport - import renpy.gl.glenviron_fixed #@UnresolvedImport - import renpy.gl.glenviron_limited #@UnresolvedImport - import renpy.gl.glenviron_shader #@UnresolvedImport - import renpy.gl.glrtt_copy #@UnresolvedImport - import renpy.gl.glrtt_fbo #@UnresolvedImport - import renpy.gl.gltexture #@UnresolvedImport - - import renpy.angle.gldraw #@UnresolvedImport - import renpy.angle.glenviron_shader #@UnresolvedImport - import renpy.angle.glrtt_copy #@UnresolvedImport - import renpy.angle.glrtt_fbo #@UnresolvedImport - import renpy.angle.gltexture #@UnresolvedImport - - def import_all(): - - import renpy.display #@UnresolvedImport - - # Adds in the Ren'Py loader. - import renpy.loader #@UnresolvedImport - - import renpy.ast #@UnresolvedImport - import renpy.atl #@UnresolvedImport - import renpy.curry #@UnresolvedImport - import renpy.easy #@UnresolvedImport - import renpy.execution #@UnresolvedImport - import renpy.loadsave #@UnresolvedImport - import renpy.parser #@UnresolvedImport - import renpy.python #@UnresolvedImport - import renpy.script #@UnresolvedImport - import renpy.statements #@UnresolvedImport - import renpy.style #@UnresolvedImport - import renpy.substitutions #@UnresolvedImport - import renpy.translation #@UnresolvedImport - - import renpy.display.presplash #@UnresolvedImport - import renpy.display.pgrender #@UnresolvedImport - import renpy.display.scale #@UnresolvedImport - import renpy.display.module #@UnresolvedImport + import renpy.game - def update_path(package): - """ - Update the __path__ of package, to import binary modules from a libexec - directory. - """ - - name = package.__name__.split(".") - - import _renpy #@UnresolvedImport - libexec = os.path.dirname(_renpy.__file__) - package.__path__.append(os.path.join(libexec, *name)) - - # Also find encodings, to deal with the way py2exe lays things out. - import encodings - libexec = os.path.dirname(encodings.__path__[0]) - package.__path__.append(os.path.join(libexec, *name)) + # Should probably be early, as we will add it as a base to serialized things. + import renpy.object - update_path(renpy.display) - - import renpy.display.render # Most display stuff depends on this. @UnresolvedImport - import renpy.display.core # object @UnresolvedImport - - import renpy.text #@UnresolvedImport - update_path(renpy.text) - - import renpy.text.ftfont #@UnresolvedImport - import renpy.text.font #@UnresolvedImport - import renpy.text.textsupport #@UnresolvedImport - import renpy.text.texwrap #@UnresolvedImport - import renpy.text.text #@UnresolvedImport - import renpy.text.extras #@UnresolvedImport - - sys.modules['renpy.display.text'] = renpy.text.text - - import renpy.gl #@UnresolvedImport - update_path(renpy.gl) - - import renpy.angle #@UnresolvedImport - update_path(renpy.angle) - - import renpy.display.layout # core @UnresolvedImport - import renpy.display.motion # layout @UnresolvedImport - import renpy.display.behavior # layout @UnresolvedImport - import renpy.display.transition # core, layout @UnresolvedImport - import renpy.display.movetransition # core @UnresolvedImport - import renpy.display.im #@UnresolvedImport - import renpy.display.imagelike #@UnresolvedImport - import renpy.display.image # core, behavior, im, imagelike @UnresolvedImport - import renpy.display.video #@UnresolvedImport - import renpy.display.focus #@UnresolvedImport - import renpy.display.anim #@UnresolvedImport - import renpy.display.particle #@UnresolvedImport - import renpy.display.joystick #@UnresolvedImport - import renpy.display.minigame #@UnresolvedImport - import renpy.display.screen #@UnresolvedImport - import renpy.display.dragdrop #@UnresolvedImport - import renpy.display.imagemap #@UnresolvedImport - import renpy.display.predict #@UnresolvedImport + # Adds in the Ren'Py loader. + import renpy.loader + + import renpy.ast + import renpy.atl + import renpy.curry + import renpy.easy + import renpy.execution + import renpy.loadsave + import renpy.parser + import renpy.python # object + import renpy.remote + import renpy.script + import renpy.statements + import renpy.style + + import renpy.display + import renpy.display.presplash + import renpy.display.iliad # Must be before scale and pgrender. + import renpy.display.pgrender + import renpy.display.scale # Must be before module. + import renpy.display.module + import renpy.display.render # Most display stuff depends on this. + import renpy.display.core # object + import renpy.display.font + import renpy.display.text # core, font + import renpy.display.layout # core + import renpy.display.motion # layout + import renpy.display.behavior # layout + import renpy.display.transition # core, layout + import renpy.display.im + import renpy.display.image # core, behavior, im + import renpy.display.video + import renpy.display.focus + import renpy.display.anim + import renpy.display.particle + import renpy.display.joystick + import renpy.display.minigame + import renpy.display.error - import renpy.display.error #@UnresolvedImport - # Note: For windows to work, renpy.audio.audio needs to be after # renpy.display.module. - import renpy.audio.audio #@UnresolvedImport - import renpy.audio.music #@UnresolvedImport - import renpy.audio.sound #@UnresolvedImport - - import renpy.ui #@UnresolvedImport - import renpy.screenlang #@UnresolvedImport - - import renpy.lint #@UnresolvedImport - import renpy.warp #@UnresolvedImport - - import renpy.editor #@UnresolvedImport - import renpy.exports #@UnresolvedImport - import renpy.character # depends on exports. @UnresolvedImport - - import renpy.dump #@UnresolvedImport + import renpy.audio.audio + import renpy.audio.music + import renpy.audio.sound - import renpy.config # depends on lots. @UnresolvedImport - import renpy.minstore # depends on lots. @UnresolvedImport - import renpy.defaultstore # depends on everything. @UnresolvedImport - import renpy.main #@UnresolvedImport + import renpy.ui - # Create the store. - renpy.python.create_store("store") + import renpy.lint + import renpy.warp - # Import the contents of renpy.defaultstore into renpy.store, and set - # up an alias as we do. - renpy.store = sys.modules['store'] - sys.modules['renpy.store'] = sys.modules['store'] + import renpy.exports + import renpy.character # depends on exports. - import subprocess - sys.modules['renpy.subprocess'] = subprocess - - for k, v in renpy.defaultstore.__dict__.items(): - renpy.store.__dict__.setdefault(k, v) + import renpy.config # depends on lots. + import renpy.store # depends on everything. + import renpy.main # Import everything into renpy.exports, provided it isn't # already there. for k, v in globals().items(): vars(renpy.exports).setdefault(k, v) -# Fool the analyzer. -if False: - import renpy.defaultstore as store - # This reloads all modules. def reload_all(): - import renpy #@UnresolvedImport - + import renpy + # Shut down the cache thread. renpy.display.im.cache.quit() + # Cleans out the RenpyImporter. + import sys + sys.meta_path.pop() + blacklist = [ "renpy", "renpy.bootstrap", "renpy.display", + "renpy.display.iliad", "renpy.display.pgrender", "renpy.display.scale" ] for i in list(sys.modules.keys()): if i.startswith("renpy") and i not in blacklist: del sys.modules[i] - - if i.startswith("store"): - del sys.modules[i] import gc gc.collect() - renpy.display.draw = None - import_all() -# Information about the platform we're running on. We break the platforms -# up into 4 groups - windows-like, mac-like, linux-like, and android-like. -windows = False -macintosh = False -linux = False -android = False - -import platform - -if platform.win32_ver()[0]: - windows = True -elif platform.mac_ver()[0]: - macintosh = True -else: - linux = True - -# The android init code in renpy.py will set linux=False and android=True. - - diff --git a/ast2json/renpy/ast.py b/ast2json/renpy/ast.py index 6fbd58f..b5d2200 100644 --- a/ast2json/renpy/ast.py +++ b/ast2json/renpy/ast.py @@ -1,4 +1,4 @@ -# Copyright 2004-2013 Tom Rothamel +# Copyright 2004-2010 PyTom # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files @@ -27,20 +27,13 @@ # updating. import renpy - import re import time -import hashlib -import collections - -def next_node(n): - """ - Indicates the next node that should be executed. When a statement - can crash, this should be set as early as possible, so that ignore - can bring us there. - """ - renpy.game.context().next_node = n +# Called to set the state of a Node, when necessary. +def setstate(node, state): + for k, v in state[1].items(): + setattr(node, k, v) class ParameterInfo(object): """ @@ -82,14 +75,14 @@ class ArgumentInfo(object): def __newobj__(cls, *args): - return cls.__new__(cls, *args) + return cls.__new__(cls, *args) # This represents a string containing python code. class PyExpr(str): - __slots__ = [ + __slots__ = [ 'filename', - 'linenumber', + 'linenumber' ] def __new__(cls, s, filename, linenumber): @@ -100,7 +93,8 @@ class PyExpr(str): def __getnewargs__(self): return (str(self), self.filename, self.linenumber) # E1101 - + + class PyCode(object): __slots__ = [ @@ -108,24 +102,19 @@ class PyCode(object): 'location', 'mode', 'bytecode', - 'hash', ] + # All PyCodes known to the system. + extent = [ ] + def __getstate__(self): return (1, self.source, self.location, self.mode) def __setstate__(self, state): (_, self.source, self.location, self.mode) = state self.bytecode = None - - if renpy.game.script.record_pycode: - renpy.game.script.all_pycode.append(self) - + def __init__(self, source, loc=('', 1), mode='exec'): - - if isinstance(source, PyExpr): - loc = (source.filename, source.linenumber, source) - # The source code. self.source = source @@ -137,27 +126,7 @@ class PyCode(object): # This will be initialized later on, after we are serialized. self.bytecode = None - if renpy.game.script.record_pycode: - renpy.game.script.all_pycode.append(self) - - self.hash = None - - def get_hash(self): - try: - if self.hash is not None: - return self.hash - except: - pass - - code = self.source - if isinstance(code, renpy.python.ast.AST): #@UndefinedVariable - code = renpy.python.ast.dump(code) #@UndefinedVariable - - self.hash = chr(renpy.bytecode_version) + hashlib.md5(repr(self.location) + code.encode("utf-8")).digest() - return self.hash - - -def chain_block(block, next): #@ReservedAssignment +def chain_block(block, next): """ This is called to chain together all of the nodes in a block. Node n is chained with node n+1, while the last node is chained with @@ -183,13 +152,13 @@ class Scry(object): def __getattr__(self, name): return None - def __next__(self): #@ReservedAssignment + def __next__(self): if self._next is None: return None else: return self._next.scry() - + class Node(object): """ A node in the abstract syntax tree of the program. @@ -199,7 +168,7 @@ class Node(object): @ivar filename: The filename where this node comes from. @ivar linenumber: The line number of the line on which this node is defined. """ - + __slots__ = [ 'name', 'filename', @@ -207,19 +176,6 @@ class Node(object): 'next', ] - # True if this node is translatable, false otherwise. (This can be set on - # the class or the instance.) - translatable = False - - # Called to set the state of a Node, when necessary. - def __setstate__(self, state): - for k, v in state[1].items(): - try: - setattr(self, k, v) - except AttributeError: - pass - - def __init__(self, loc): """ Initializes this Node object. @@ -231,7 +187,7 @@ class Node(object): self.filename, self.linenumber = loc self.name = None self.next = None - + def diff_info(self): """ Returns a tuple of diff info about ourself. This is used to @@ -260,14 +216,14 @@ class Node(object): """ return None - - def chain(self, next): #@ReservedAssignment + + def chain(self, next): """ This is called with the Node node that should be followed after executing this node, and all nodes that this node executes. (For example, if this node is a block label, the next is the node that should be executed after all nodes in - the block.) + the block.) """ self.next = next @@ -275,22 +231,20 @@ class Node(object): def execute(self): """ Causes this node to execute, and any action it entails to be - performed. The node should call next_node with the node to - be executed after this one. + performed. The node is then responsible for returning the node + that should be executed after this one, or None to end the + program or init block. """ assert False, "Node subclass forgot to define execute." - def early_execute(self): - """ - Called when the module is loaded. - """ - - def predict(self): + def predict(self, callback): """ - This is called to predictively load images from this node. It - should cause renpy.display.predict.image and - renpy.display.predict.screen to be called as necessary. + This is called to predictively load images from this node. The + callback needs to be passed into the predict method of any + images this ast node will probably load, and the method should + return a list containing the nodes that this node will + probably execute next. """ if self.__next__: @@ -298,6 +252,14 @@ class Node(object): else: return [ ] + def get_pycode(self): + """ + Returns a list of PyCode objects associated with this Node, + or None if no objects are associated with it. + """ + + return [ ] + def scry(self): """ Called to return an object with some general, user-definable information @@ -308,30 +270,6 @@ class Node(object): rv._next = self.__next__ # W0201 return rv - def restructure(self, callback): - """ - Called to restructure the AST. - - When this method is called, callback is called once for each child - block of the node. The block, a list, can be updated by the callback - using slice assignment to the list. - """ - - # Does nothing for nodes that do not contain child blocks. - return - - def get_code(self, dialogue_filter=None): - """ - Returns the canonical form of the code corresponding to this statement. - This only needs to be defined if the statement is translatable. - - `filter` - If present, a filter that should be applied to human-readable - text in the statement. - """ - - raise Exception("Not Implemented") - def say_menu_with(expression, callback): """ This handles the with clause of a say or menu statement. @@ -350,7 +288,7 @@ def say_menu_with(expression, callback): if renpy.game.preferences.transitions: # renpy.game.interface.set_transition(what) callback(what) - + class Say(Node): __slots__ = [ @@ -359,18 +297,12 @@ class Say(Node): 'what', 'with_', 'interact', - 'attributes', ] def diff_info(self): return (Say, self.who, self.what) - def __setstate__(self, state): - self.attributes = None - self.interact = True - Node.__setstate__(self, state) - - def __init__(self, loc, who, what, with_, interact=True, attributes=None): + def __init__(self, loc, who, what, with_, interact=True): super(Say, self).__init__(loc) @@ -382,131 +314,68 @@ class Say(Node): else: self.who_fast = False else: - self.who = None + self.who = None self.who_fast = False - + self.what = what self.with_ = with_ self.interact = interact - # A tuple of attributes that are applied to the character that's - # speaking, or None to disable this behavior. - self.attributes = attributes - - def get_code(self, dialogue_filter=None): - rv = [ ] - - if self.who: - rv.append(self.who) - - if self.attributes is not None: - rv.extend(self.attributes) - - what = self.what - if dialogue_filter is not None: - what = dialogue_filter(what) - - rv.append(renpy.translation.encode_say_string(what)) - - if not self.interact: - rv.append("nointeract") - - if self.with_: - rv.append("with") - rv.append(self.with_) - - return " ".join(rv) - def execute(self): - next_node(self.__next__) - - try: - - renpy.exports.say_attributes = self.attributes - - if self.who is not None: - if self.who_fast: - who = getattr(renpy.store, self.who, None) - if who is None: - raise Exception("Sayer '%s' is not defined." % self.who.encode("utf-8")) - else: - who = renpy.python.py_eval(self.who) + if self.who is not None: + if self.who_fast: + who = getattr(renpy.store, self.who, None) + if who is None: + raise Exception("Sayer '%s' is not defined." % self.who.encode("utf-8")) else: - who = None - - if not ( - (who is None) or - isinstance(who, collections.Callable) or - isinstance(who, str) ): - - raise Exception("Sayer %s is not a function or string." % self.who.encode("utf-8")) - - what = self.what - if renpy.config.say_menu_text_filter: - what = renpy.config.say_menu_text_filter(what) # E1102 - - if getattr(who, "record_say", True): - renpy.store._last_say_who = self.who - renpy.store._last_say_what = what - - say_menu_with(self.with_, renpy.game.interface.set_transition) - renpy.exports.say(who, what, interact=self.interact) - - finally: - renpy.exports.say_attributes = None - + who = renpy.python.py_eval(self.who) + else: + who = None - def predict(self): + what = self.what + if renpy.config.say_menu_text_filter: + what = renpy.config.say_menu_text_filter(what) # E1102 + + say_menu_with(self.with_, renpy.game.interface.set_transition) + renpy.exports.say(who, what, interact=getattr(self, 'interact', True)) - old_attributes = renpy.exports.say_attributes + if getattr(who, "record_say", True): + renpy.store._last_say_who = self.who + renpy.store._last_say_what = what - try: + return self.__next__ - renpy.exports.say_attributes = self.attributes + def predict(self, callback): - if self.who is not None: - if self.who_fast: - who = getattr(renpy.store, self.who) - else: - who = renpy.python.py_eval(self.who) + if self.who is not None: + if self.who_fast: + who = getattr(renpy.store, self.who) else: - who = None - - def predict_with(trans): - renpy.display.predict.displayable(trans(old_widget=None, new_widget=None)) + who = renpy.python.py_eval(self.who) + else: + who = None - say_menu_with(self.with_, predict_with) + def predict_with(trans): + trans(old_widget=None, new_widget=None).predict(callback) - what = self.what - if renpy.config.say_menu_text_filter: - what = renpy.config.say_menu_text_filter(what) + say_menu_with(self.with_, predict_with) - renpy.exports.predict_say(who, what) + what = self.what + if renpy.config.say_menu_text_filter: + what = renpy.config.say_menu_text_filter(what) - finally: - renpy.exports.say_attributes = old_attributes + for i in renpy.exports.predict_say(who, what): + if i is not None: + i.predict(callback) return [ self.__next__ ] def scry(self): rv = Node.scry(self) - - if self.who is not None: - if self.who_fast: - who = getattr(renpy.store, self.who) - else: - who = renpy.python.py_eval(self.who) - else: - who = None - - if self.interact: - renpy.exports.scry_say(who, rv) - else: - rv.interacts = False - + rv.interacts = True # W0201 return rv - + # Copy the descriptor. setattr(Say, "with", Say.with_) # E1101 @@ -533,87 +402,14 @@ class Init(Node): # We handle chaining specially. We want to chain together the nodes in # the block, but we want that chain to end in None, and we also want # this node to just continue on to the next node in normal execution. - def chain(self, next): #@ReservedAssignment + def chain(self, next): self.next = next chain_block(self.block, None) def execute(self): - next_node(self.__next__) - - def restructure(self, callback): - callback(self.block) - -def apply_arguments(params, args, kwargs): - """ - Applies arguments to parameters to update scope. - - `scope` - A dict. - - `params` - The parameters object. - - `args`, `kwargs` - Positional and keyword arguments. - """ - - values = { } - rv = { } - - if args is None: - args = () - - if kwargs is None: - kwargs = { } - - if params is None: - if args or kwargs: - raise Exception("Arguments supplied, but parameter list not present") - else: - return rv - - for name, value in zip(params.positional, args): - if name in values: - raise Exception("Parameter %s has two values." % name) - - values[name] = value - - extrapos = tuple(args[len(params.positional):]) - - for name, value in kwargs.items(): - if name in values: - raise Exception("Parameter %s has two values." % name) - - values[name] = value - - for name, default in params.parameters: - - if name not in values: - if default is None: - raise Exception("Required parameter %s has no value." % name) - else: - rv[name] = renpy.python.py_eval(default) - - else: - rv[name] = values[name] - del values[name] - - # Now, values has the left-over keyword arguments, and extrapos - # has the left-over positional arguments. - - if params.extrapos: - rv[params.extrapos] = extrapos - elif extrapos: - raise Exception("Too many arguments in call (expected %d, got %d)." % (len(params.positional), len(args))) - - if params.extrakw: - rv[params.extrakw] = values - else: - if values: - raise Exception("Unknown keyword arguments: %s" % ( ", ".join(list(values.keys())))) - - return rv + return self.__next__ + class Label(Node): @@ -621,15 +417,13 @@ class Label(Node): 'name', 'parameters', 'block', - 'hide', ] def __setstate__(self, state): self.parameters = None - self.hide = False - Node.__setstate__(self, state) - - def __init__(self, loc, name, block, parameters, hide=False): + setstate(self, state) + + def __init__(self, loc, name, block, parameters): """ Constructs a new Label node. @@ -643,85 +437,126 @@ class Label(Node): self.name = name self.block = block self.parameters = parameters - self.hide = hide def diff_info(self): return (Label, self.name) def get_children(self): - return self.block + return self.block - def chain(self, next): #@ReservedAssignment + def chain(self, next): if self.block: self.next = self.block[0] chain_block(self.block, next) else: self.next = next - + def execute(self): - next_node(self.__next__) - renpy.game.context().mark_seen() + + args = renpy.store._args + kwargs = renpy.store._kwargs + + if self.parameters is None: + if (args is not None) or (kwargs is not None): + raise Exception("Arguments supplied, but label does not take parameters.") + else: + if renpy.config.label_callback: + renpy.config.label_callback(self.name, renpy.game.context().last_abnormal) + + return self.__next__ + else: + if args is None: + args = () + + if kwargs is None: + kwargs = { } + + values = { } + params = self.parameters + + for name, value in zip(params.positional, args): + if name in values: + raise Exception("Parameter %s has two values." % name) + + values[name] = value - values = apply_arguments(self.parameters, renpy.store._args, renpy.store._kwargs) + extrapos = tuple(args[len(params.positional):]) - for k, v in values.items(): - renpy.exports.dynamic(k) - setattr(renpy.store, k, v) + for name, value in kwargs.items(): + if name in values: + raise Exception("Parameter %s has two values." % name) + + values[name] = value + + for name, default in params.parameters: + + if name not in values: + if default is None: + raise Exception("Required parameter %s has no value." % name) + else: + values[name] = renpy.python.py_eval(default) + + + renpy.exports.dynamic(name) + setattr(renpy.store, name, values[name]) + del values[name] + + # Now, values has the left-over keyword arguments, and extrapos + # has the left-over positional arguments. + + if params.extrapos: + renpy.exports.dynamic(params.extrapos) + setattr(renpy.store, params.extrapos, extrapos) + else: + if extrapos: + raise Exception("Too many arguments in call (expected %d, got %d)." % (len(params.positional), len(args))) + + if params.extrakw: + renpy.exports.dynamic(params.extrakw) + setattr(renpy.store, params.extrakw, renpy.python.RevertableDict(values)) + else: + if values: + raise Exception("Unknown keyword arguments: %s" % ( ", ".join(list(values.keys())))) renpy.store._args = None renpy.store._kwargs = None if renpy.config.label_callback: renpy.config.label_callback(self.name, renpy.game.context().last_abnormal) - - def restructure(self, callback): - callback(self.block) - + + return self.__next__ class Python(Node): __slots__ = [ 'hide', 'code', - 'store', ] - def __setstate__(self, state): - self.store = "store" - super(Python, self).__setstate__(state) - - def __init__(self, loc, python_code, hide=False, store="store"): + def __init__(self, loc, python_code, hide=False): """ @param code: A PyCode object. @param hide: If True, the code will be executed with its own local dictionary. """ - + super(Python, self).__init__(loc) self.hide = hide self.code = PyCode(python_code, loc=loc, mode='exec') - self.store = store + + def get_pycode(self): + return [ self.code ] def diff_info(self): return (Python, self.code.source) - def early_execute(self): - renpy.python.create_store(self.store) - def execute(self): - next_node(self.__next__) - - try: - renpy.python.py_exec_bytecode(self.code.bytecode, self.hide, store=self.store) - finally: - - if not renpy.game.context().init_phase: - for i in renpy.config.python_callbacks: - i() + renpy.python.py_exec_bytecode(self.code.bytecode, self.hide) + return self.__next__ def scry(self): rv = Node.scry(self) @@ -733,38 +568,32 @@ class EarlyPython(Node): __slots__ = [ 'hide', 'code', - 'store', ] - def __setstate__(self, state): - self.store = "store" - super(EarlyPython, self).__setstate__(state) - - def __init__(self, loc, python_code, hide=False, store="store"): + def __init__(self, loc, python_code, hide=False): """ @param code: A PyCode object. @param hide: If True, the code will be executed with its own local dictionary. """ - + super(EarlyPython, self).__init__(loc) self.hide = hide self.code = PyCode(python_code, loc=loc, mode='exec') - self.store = store + + def get_pycode(self): + return [ self.code ] def diff_info(self): return (EarlyPython, self.code.source) def execute(self): - next_node(self.__next__) + return self.__next__ def early_execute(self): - renpy.python.create_store(self.store) - - if self.code.bytecode: - renpy.python.py_exec_bytecode(self.code.bytecode, self.hide, store=self.store) + renpy.python.py_exec_bytecode(self.code.bytecode, self.hide) class Image(Node): @@ -783,7 +612,7 @@ class Image(Node): """ super(Image, self).__init__(loc) - + self.imgname = name if expr: @@ -792,17 +621,21 @@ class Image(Node): else: self.code = None self.atl = atl - - def diff_info(self): + + def diff_info(self): return (Image, tuple(self.imgname)) + def get_pycode(self): + if self.code: + return [ self.code ] + else: + return [ ] + def execute(self): # Note: We should always check that self.code is None before # accessing self.atl, as self.atl may not always exist. - next_node(self.__next__) - if self.code is not None: img = renpy.python.py_eval_bytecode(self.code.bytecode) else: @@ -810,8 +643,10 @@ class Image(Node): renpy.exports.image(self.imgname, img) + return self.__next__ + class Transform(Node): __slots__ = [ @@ -823,52 +658,52 @@ class Transform(Node): 'atl', # The parameters associated with the transform, if any. - 'parameters', + 'parameters', ] default_parameters = ParameterInfo([ ], [ ], None, None) - + def __init__(self, loc, name, atl=None, parameters=default_parameters): super(Transform, self).__init__(loc) - + self.varname = name self.atl = atl self.parameters = parameters - - def diff_info(self): + + def diff_info(self): return (Transform, self.varname) def execute(self): - next_node(self.__next__) - parameters = getattr(self, "parameters", None) if parameters is None: parameters = Transform.default_parameters trans = renpy.display.motion.ATLTransform(self.atl, parameters=parameters) - renpy.dump.transforms.append((self.varname, self.filename, self.linenumber)) + renpy.exports.definitions[self.varname].append((self.filename, self.linenumber, "transform")) setattr(renpy.store, self.varname, trans) + + return self.__next__ - -def predict_imspec(imspec, scene=False, atl=None): + +def predict_imspec(imspec, callback, scene=False): """ Call this to use the given callback to predict the image named in imspec. """ if len(imspec) == 7: - name, expression, tag, _at_list, layer, _zorder, _behind = imspec + name, expression, tag, at_list, layer, zorder, behind = imspec elif len(imspec) == 6: - name, expression, tag, _at_list, layer, _zorder = imspec + name, expression, tag, at_list, layer, zorder = imspec elif len(imspec) == 3: - name, _at_list, layer = imspec - - + name, at_list, layer = imspec + + if expression: try: img = renpy.python.py_eval(expression) @@ -877,7 +712,7 @@ def predict_imspec(imspec, scene=False, atl=None): return else: - img = renpy.display.image.images.get(name, None) + img = renpy.exports.images.get(name, None) if img is None: return @@ -886,37 +721,29 @@ def predict_imspec(imspec, scene=False, atl=None): full_name = (tag,) + full_name[1:] if scene: - renpy.game.context().images.predict_scene(layer) - - renpy.game.context().images.predict_show(tag or name, layer) - - if atl is not None: - try: - img = renpy.display.motion.ATLTransform(atl, child=img) - except: - import traceback - traceback.print_exc() - - renpy.display.predict.displayable(img) - - + renpy.game.context().predict_info.images.predict_scene(layer) + + renpy.game.context().predict_info.images.predict_show(tag or name, layer) + + img.predict(callback) + def show_imspec(imspec, atl=None): if len(imspec) == 7: name, expression, tag, at_list, layer, zorder, behind = imspec - + elif len(imspec) == 6: name, expression, tag, at_list, layer, zorder = imspec behind = [ ] - + elif len(imspec) == 3: name, at_list, layer = imspec expression = None tag = None zorder = None behind = [ ] - + if zorder is not None: zorder = renpy.python.py_eval(zorder) else: @@ -954,19 +781,20 @@ class Show(Node): self.imspec = imspec self.atl = atl - - def diff_info(self): + + def diff_info(self): return (Show, tuple(self.imspec[0])) def execute(self): - next_node(self.__next__) show_imspec(self.imspec, atl=getattr(self, "atl", None)) - def predict(self): - predict_imspec(self.imspec, atl=getattr(self, "atl", None)) - return [ self.__next__ ] + return self.__next__ + def predict(self, callback): + predict_imspec(self.imspec, callback) + return [ self.__next__ ] + class Scene(Node): @@ -989,7 +817,7 @@ class Scene(Node): self.layer = layer self.atl = atl - def diff_info(self): + def diff_info(self): if self.imspec: data = tuple(self.imspec[0]) @@ -1000,21 +828,21 @@ class Scene(Node): def execute(self): - next_node(self.__next__) - renpy.config.scene(self.layer) if self.imspec: show_imspec(self.imspec, atl=getattr(self, "atl", None)) - def predict(self): - + return self.__next__ + + def predict(self, callback): + if self.imspec: - predict_imspec(self.imspec, atl=getattr(self, "atl", None), scene=True) + predict_imspec(self.imspec, callback, scene=True) return [ self.__next__ ] - + class Hide(Node): __slots__ = [ @@ -1032,48 +860,46 @@ class Hide(Node): self.imspec = imgspec - def diff_info(self): + def diff_info(self): return (Hide, tuple(self.imspec[0])) - def predict(self): + def predict(self, callback): if len(self.imspec) == 3: - name, _at_list, layer = self.imspec + name, at_list, layer = self.imspec + expression = None tag = None - _expression = None - _zorder = None - _behind = None + zorder = 0 elif len(self.imspec) == 6: - name, _expression, tag, _at_list, layer, _zorder = self.imspec - _behind = None + name, expression, tag, at_list, layer, zorder = self.imspec elif len(self.imspec) == 7: - name, _expression, tag, _at_list, layer, _zorder, _behind = self.imspec + name, expression, tag, at_list, layer, zorder, behind = self.imspec if tag is None: tag = name[0] + + renpy.game.context().predict_info.images.predict_hide(tag, layer) - renpy.game.context().images.predict_hide(tag, layer) - - return [ self.__next__ ] - + return [ ] + def execute(self): - next_node(self.__next__) - if len(self.imspec) == 3: - name, _at_list, layer = self.imspec - _expression = None + name, at_list, layer = self.imspec + expression = None tag = None - _zorder = 0 + zorder = 0 elif len(self.imspec) == 6: - name, _expression, tag, _at_list, layer, _zorder = self.imspec + name, expression, tag, at_list, layer, zorder = self.imspec elif len(self.imspec) == 7: - name, _expression, tag, _at_list, layer, _zorder, _behind = self.imspec - + name, expression, tag, at_list, layer, zorder, behind = self.imspec + renpy.config.hide(tag or name, layer) + return self.__next__ + class With(Node): __slots__ = [ @@ -1083,8 +909,8 @@ class With(Node): def __setstate__(self, state): self.paired = None - Node.__setstate__(self, state) - + setstate(self, state) + def __init__(self, loc, expr, paired=None): """ @param expr: An expression giving a transition or None. @@ -1096,35 +922,34 @@ class With(Node): def diff_info(self): return (With, self.expr) - + def execute(self): - next_node(self.__next__) - trans = renpy.python.py_eval(self.expr) if self.paired is not None: paired = renpy.python.py_eval(self.paired) else: - paired = None + paired = None renpy.exports.with_statement(trans, paired) - def predict(self): + return self.__next__ + + def predict(self, callback): try: trans = renpy.python.py_eval(self.expr) if trans: - renpy.display.predict.displayable(trans(old_widget=None, new_widget=None)) - + trans(old_widget=None, new_widget=None).predict(callback) except: pass - + return [ self.__next__ ] - - + + class Call(Node): __slots__ = [ @@ -1135,8 +960,8 @@ class Call(Node): def __setstate__(self, state): self.arguments = None - Node.__setstate__(self, state) - + setstate(self, state) + def __init__(self, loc, label, expression, arguments): super(Call, self).__init__(loc) @@ -1154,7 +979,6 @@ class Call(Node): 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: @@ -1187,8 +1011,12 @@ class Call(Node): renpy.store._args = tuple(args) renpy.store._kwargs = kwargs - - def predict(self): + + + return rv + + + def predict(self, callback): if self.expression: return [ ] else: @@ -1206,17 +1034,17 @@ class Return(Node): def __setstate__(self, state): self.expression = None - Node.__setstate__(self, state) - + setstate(self, state) + def __init__(self, loc, expression): super(Return, self).__init__(loc) self.expression = expression - + def diff_info(self): return (Return, ) # We don't care what the next node is. - def chain(self, next): #@ReservedAssignment + def chain(self, next): self.next = None return @@ -1228,10 +1056,10 @@ class Return(Node): renpy.store._return = None renpy.game.context().pop_dynamic() + + return renpy.game.context().lookup_return(pop=True) - next_node(renpy.game.context().lookup_return(pop=True)) - - def predict(self): + def predict(self, callback): site = renpy.game.context().lookup_return(pop=False) if site: return [ site ] @@ -1243,7 +1071,7 @@ class Return(Node): rv._next = None return rv - + class Menu(Node): __slots__ = [ @@ -1252,7 +1080,7 @@ class Menu(Node): 'with_', ] - def __init__(self, loc, items, set, with_): #@ReservedAssignment + def __init__(self, loc, items, set, with_): super(Menu, self).__init__(loc) self.items = items @@ -1265,65 +1093,57 @@ class Menu(Node): def get_children(self): rv = [ ] - for _label, _condition, block in self.items: + for label, condition, block in self.items: if block: rv.extend(block) return rv # Blocks of statements in a choice continue after the menu. - def chain(self, next): #@ReservedAssignment + def chain(self, next): self.next = next - for (_label, _condition, block) in self.items: + for (label, condition, block) in self.items: if block: chain_block(block, next) def execute(self): - next_node(self.__next__) - choices = [ ] - narration = [ ] - + for i, (label, condition, block) in enumerate(self.items): if renpy.config.say_menu_text_filter: label = renpy.config.say_menu_text_filter(label) if block is None: - if renpy.config.narrator_menu and label: - narration.append(label) - else: - choices.append((label, condition, None)) + choices.append((label, condition, None)) else: choices.append((label, condition, i)) - next_node(block[0]) - - if narration: - renpy.exports.say(None, "\n".join(narration), interact=False) say_menu_with(self.with_, renpy.game.interface.set_transition) choice = renpy.exports.menu(choices, self.set) - if choice is not None: - next_node(self.items[choice][2][0]) + if choice is None: + return self.__next__ else: - next_node(self.__next__) - + return self.items[choice][2][0] + - def predict(self): + def predict(self, callback): rv = [ ] def predict_with(trans): - renpy.display.predict.displayable(trans(old_widget=None, new_widget=None)) + trans(old_widget=None, new_widget=None).predict(callback) say_menu_with(self.with_, predict_with) - renpy.store.predict_menu() + for i in renpy.store.predict_menu(): + if i is not None: + i.predict(callback) - for _label, _condition, block in self.items: + for label, condition, block in self.items: if block: rv.append(block[0]) @@ -1334,17 +1154,12 @@ class Menu(Node): rv._next = None rv.interacts = True return rv - - def restructure(self, callback): - for _label, _condition, block in self.items: - if block is not None: - callback(block) - + setattr(Menu, "with", Menu.with_) # E1101 # Goto is considered harmful. So we decided to name it "jump" -# instead. +# instead. class Jump(Node): __slots__ = [ @@ -1362,7 +1177,7 @@ class Jump(Node): return (Jump, self.target, self.expression) # We don't care what our next node is. - def chain(self, next): #@ReservedAssignment + def chain(self, next): self.next = None return @@ -1374,10 +1189,9 @@ class Jump(Node): rv = renpy.game.script.lookup(target) renpy.game.context().abnormal = True + return rv - next_node(rv) - - def predict(self): + def predict(self, callback): if self.expression: return [ ] @@ -1390,7 +1204,7 @@ class Jump(Node): rv._next = None else: rv._next = renpy.game.script.lookup(self.target) - + return rv @@ -1403,7 +1217,7 @@ class Pass(Node): return (Pass,) def execute(self): - next_node(self.__next__) + return self.__next__ class While(Node): @@ -1425,28 +1239,25 @@ class While(Node): def get_children(self): return self.block - def chain(self, next): #@ReservedAssignment + def chain(self, next): self.next = next chain_block(self.block, self) def execute(self): - next_node(self.__next__) - if renpy.python.py_eval(self.condition): - next_node(self.block[0]) + return self.block[0] + else: + return self.__next__ - def predict(self): + def predict(self, callback): return [ self.block[0], self.__next__ ] - + def scry(self): rv = Node.scry(self) rv._next = None return rv - def restructure(self, callback): - callback(self.block) - class If(Node): __slots__ = [ 'entries' ] @@ -1466,29 +1277,28 @@ class If(Node): def get_children(self): rv = [ ] - for _condition, block in self.entries: + for condition, block in self.entries: rv.extend(block) return rv - def chain(self, next): #@ReservedAssignment + def chain(self, next): self.next = next - for _condition, block in self.entries: + for condition, block in self.entries: chain_block(block, next) def execute(self): - next_node(self.__next__) - for condition, block in self.entries: if renpy.python.py_eval(condition): - next_node(block[0]) - return + return block[0] + + return self.__next__ - def predict(self): + def predict(self, callback): - return [ block[0] for _condition, block in self.entries ] + \ + return [ block[0] for condition, block in self.entries ] + \ [ self.__next__ ] def scry(self): @@ -1496,50 +1306,40 @@ class If(Node): rv._next = None return rv - def restructure(self, callback): - for _condition, block in self.entries: - callback(block) class UserStatement(Node): - __slots__ = [ - 'line', - 'parsed', - 'block', - 'translatable' ] - - def __setstate__(self, state): - self.block = [ ] - self.translatable = False - Node.__setstate__(self, state) + __slots__ = [ 'line', 'parsed' ] - def __init__(self, loc, line, block): + def __init__(self, loc, line): super(UserStatement, self).__init__(loc) self.line = line - self.block = block self.parsed = None # Do not store the parse quite yet. - _parse_info = renpy.statements.parse(self, self.line, self.block) - + renpy.statements.parse(self, self.line) + def diff_info(self): return (UserStatement, self.line) def execute(self): - next_node(self.get_next()) - self.call("execute") + return self.get_next() - def predict(self): - self.call("predict") - return [ self.get_next() ] + def predict(self, callback): + predicted = self.call("predict") or [ ] + for i in predicted: + callback(i) + + return [ self.get_next() ] + def call(self, method, *args, **kwargs): - - parsed = self.parsed + + parsed = self.parsed if parsed is None: - parsed = renpy.statements.parse(self, self.line, self.block) + parsed = renpy.statements.parse(self, self.line) self.parsed = parsed renpy.statements.call(method, parsed, *args, **kwargs) @@ -1550,17 +1350,14 @@ class UserStatement(Node): return renpy.game.script.lookup(rv) else: return self.__next__ - + def scry(self): rv = Node.scry(self) rv._next = self.get_next() self.call("scry", rv) return rv - - def get_code(self, dialogue_filter=None): - return self.line - - + + class Define(Node): __slots__ = [ @@ -1577,181 +1374,23 @@ class Define(Node): """ super(Define, self).__init__(loc) - + self.varname = name self.code = PyCode(expr, loc=loc, mode='eval') - - def diff_info(self): + + def diff_info(self): return (Define, tuple(self.varname)) + def get_pycode(self): + if self.code: + return [ self.code ] + else: + return [ ] + def execute(self): - next_node(self.__next__) - value = renpy.python.py_eval_bytecode(self.code.bytecode) - renpy.dump.definitions.append((self.varname, self.filename, self.linenumber)) - setattr(renpy.store, self.varname, value) - - -class Screen(Node): - - __slots__ = [ - 'screen', - ] - - def __init__(self, loc, screen): - """ - @param name: The name of the image being defined. - - @param expr: An expression yielding a Displayable that is - assigned to the image. - """ - - super(Screen, self).__init__(loc) - - self.screen = screen - - def diff_info(self): - return (Screen, self.screen.name) - - def execute(self): - next_node(self.__next__) - self.screen.define() - renpy.dump.screens.append((self.screen.name, self.filename, self.linenumber)) - - -################################################################################ -# Translations -################################################################################ - -class Translate(Node): - """ - A translation block, produced either by explicit translation statements - or implicit translation blocs. - - If language is None, when executed this transfers control to the translate - statement in the current language, if any, and otherwise runs the block. - If language is not None, causes an error to occur if control reaches this - statement. - - When control normally leaves a translate statement, in any language, it - goes to the end of the translate statement in the None language. - """ - - __slots__ = [ - "identifier", - "language", - "block", - ] - - def __init__(self, loc, identifier, language, block): - super(Translate, self).__init__(loc) - - self.identifier = identifier - self.language = language - self.block = block - - def diff_info(self): - return (Translate, self.identifier, self.language) - - def chain(self, next): #@ReservedAssignment - self.next = next - chain_block(self.block, next) - - def execute(self): - - if self.language is not None: - next_node(self.__next__) - raise Exception("Translation nodes cannot be run directly.") - - next_node(renpy.game.script.translator.lookup_translate(self.identifier)) - renpy.game.context().translate_identifier = self.identifier - - def predict(self): - node = renpy.game.script.translator.lookup_translate(self.identifier) - return [ node ] - - def scry(self): - rv = Scry() - rv._next = renpy.game.script.translator.lookup_translate(self.identifier) - return rv - - def get_children(self): - return self.block - - def restructure(self, callback): - return callback(self.block) - - -class EndTranslate(Node): - """ - A node added implicitly after each translate block. It's responsible for - resetting the translation identifier. - """ - - def __init__(self, loc): - super(EndTranslate, self).__init__(loc) - - def diff_info(self): - return (EndTranslate,) - - def execute(self): - next_node(self.__next__) - renpy.game.context().translate_identifier = None - - -class TranslateString(Node): - """ - A node used for translated strings. - """ - - __slots__ = [ - "language", - "old", - "new" - ] - - def __init__(self, loc, language, old, new): - super(TranslateString, self).__init__(loc) - self.language = language - self.old = old - self.new = new - - def diff_info(self): - return (TranslateString,) - - def execute(self): - next_node(self.__next__) - renpy.translation.add_string_translation(self.language, self.old, self.new) - -class TranslatePython(Node): - - __slots__ = [ - 'language', - 'code', - ] - - def __init__(self, loc, language, python_code): - """ - @param code: A PyCode object. - - @param hide: If True, the code will be executed with its - own local dictionary. - """ - - super(TranslatePython, self).__init__(loc) - - self.language = language - self.code = PyCode(python_code, loc=loc, mode='exec') - - def diff_info(self): - return (TranslatePython, self.code.source) - - def execute(self): - next_node(self.__next__) - - # def early_execute(self): - # renpy.python.create_store(self.store) - # renpy.python.py_exec_bytecode(self.code.bytecode, self.hide, store=self.store) - - + renpy.exports.definitions[self.varname].append((self.filename, self.linenumber, "define")) + setattr(renpy.store, self.varname, value) + + return self.__next__ diff --git a/ast2json/renpy/atl.py b/ast2json/renpy/atl.py index a321e6b..01c76d3 100644 --- a/ast2json/renpy/atl.py +++ b/ast2json/renpy/atl.py @@ -1,4 +1,4 @@ -# Copyright 2004-2013 Tom Rothamel +# Copyright 2004-2010 PyTom # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files @@ -23,12 +23,12 @@ import renpy import random def compiling(loc): - file, number = loc #@ReservedAssignment + file, number = loc renpy.game.exception_info = "Compiling ATL code at %s:%d" % (file, number) def executing(loc): - file, number = loc #@ReservedAssignment + file, number = loc renpy.game.exception_info = "Executing ATL code at %s:%d" % (file, number) @@ -69,8 +69,6 @@ PROPERTIES = { "xalign" : float, "yalign" : float, "rotate" : float, - "rotate_pad" : bool, - "transform_anchor" : bool, "xzoom" : float, "yzoom" : float, "zoom" : float, @@ -85,29 +83,20 @@ PROPERTIES = { "corner2" : (float, float), "subpixel" : bool, "delay" : float, - "xoffset" : float, - "yoffset" : float, - "offset" : (int, int), - "xcenter" : position, - "ycenter" : position, } - def correct_type(v, b, ty): """ Corrects the type of v to match ty. b is used to inform the match. """ if ty is position: - if v is None: - return None - else: - return type(b)(v) + return type(b)(v) else: return ty(v) -def interpolate(t, a, b, type): #@ReservedAssignment +def interpolate(t, a, b, type): """ Linearly interpolate the arguments. """ @@ -116,10 +105,7 @@ def interpolate(t, a, b, type): #@ReservedAssignment return b # Recurse into tuples. - if isinstance(b, tuple): - if a is None: - a = [ None ] * len(b) - + if isinstance(b, tuple): return tuple(interpolate(t, i, j, ty) for i, j, ty in zip(a, b, type)) # Deal with booleans, nones, etc. @@ -142,9 +128,6 @@ def interpolate_spline(t, spline): if isinstance(spline[-1], tuple): return tuple(interpolate_spline(t, i) for i in zip(*spline)) - - if spline[0] is None: - return spline[-1] if len(spline) == 2: t_p = 1.0 - t @@ -180,9 +163,8 @@ class Context(object): def __init__(self, context): self.context = context - def eval(self, expr): #@ReservedAssignment - expr = renpy.python.escape_unicode(expr) - return eval(expr, renpy.store.__dict__, self.context) #@UndefinedVariable + def eval(self, expr): + return eval(expr, renpy.store.__dict__, self.context) # This is intended to be subclassed by ATLTransform. It takes care of # managing ATL execution, which allows ATLTransform itself to not care @@ -194,8 +176,8 @@ class ATLTransformBase(renpy.object.Object): def __init__(self, atl, context, parameters): - # The constructor will be called by atltransform. - + super(ATLTransformBase, self).__init__() + if parameters is None: parameters = ATLTransformBase.parameters @@ -238,11 +220,6 @@ class ATLTransformBase(renpy.object.Object): requires that t.atl is self.atl. """ - super(ATLTransformBase, self).take_execution_state(t) - - if t.atl is not self.atl: - return - self.done = t.done self.block = t.block self.atl_state = t.atl_state @@ -250,15 +227,6 @@ class ATLTransformBase(renpy.object.Object): self.last_transform_event = t.last_transform_event self.last_child_transform_event = t.last_child_transform_event - self.st = t.st - self.at = t.at - self.st_offset = t.st_offset - self.at_offset = t.at_offset - - if self.child is renpy.display.motion.null: - self.child = t.child - - def __call__(self, *args, **kwargs): context = self.context.context.copy() @@ -269,8 +237,8 @@ class ATLTransformBase(renpy.object.Object): positional = list(self.parameters.positional) args = list(args) - - child = None + + child = self.child if not positional and args: child = args.pop(0) @@ -301,12 +269,6 @@ class ATLTransformBase(renpy.object.Object): else: raise Exception('Parameter %r is not known by ATL Transform.' % k) - if child is None: - child = self.child - - if child is None: - child = renpy.display.motion.get_null() - # Create a new ATL Transform. parameters = renpy.ast.ParameterInfo({}, positional, None, None) @@ -318,11 +280,11 @@ class ATLTransformBase(renpy.object.Object): parameters=parameters) rv.take_state(self) - + return rv - def compile(self): #@ReservedAssignment + def compile(self): """ Compiles the ATL code into a block. As necessary, updates the properties. @@ -359,13 +321,10 @@ class ATLTransformBase(renpy.object.Object): if self.child.transform_event != self.last_child_transform_event: self.last_child_transform_event = self.child.transform_event self.transform_event = self.child.transform_event - + # Hide request. if trans.hide_request: self.transform_event = "hide" - - if trans.replaced_request: - self.transform_event = "replaced" # Notice transform events. if self.transform_event != self.last_transform_event: @@ -380,7 +339,7 @@ class ATLTransformBase(renpy.object.Object): timebase = at else: timebase = st - + action, arg, pause = self.block.execute(trans, timebase, self.atl_state, event) renpy.game.exception_info = old_exception_info @@ -393,9 +352,11 @@ class ATLTransformBase(renpy.object.Object): self.done = True return pause + - def predict_one(self): - self.atl.predict(self.context) + def predict(self, callback): + self.atl.predict(self.context, callback) + def visit(self): if not self.block: @@ -413,12 +374,13 @@ class RawStatement(renpy.object.Object): # Compiles this RawStatement into a Statement, by using ctx to # evaluate expressions as necessary. - def compile(self, ctx): #@ReservedAssignment + def compile(self, ctx): raise Exception("Compile not implemented.") # Predicts the images used by this statement. - def predict(self, ctx): + def predict(self, ctx, callback): return + # The base class for compiled ATL Statements. class Statement(renpy.object.Object): @@ -474,16 +436,16 @@ class RawBlock(RawStatement): self.animation = animation - def compile(self, ctx): #@ReservedAssignment + def compile(self, ctx): compiling(self.loc) statements = [ i.compile(ctx) for i in self.statements ] return Block(self.loc, statements) - def predict(self, ctx): + def predict(self, ctx, callback): for i in self.statements: - i.predict(ctx) + i.predict(ctx, callback) # A compiled ATL block. @@ -544,7 +506,7 @@ class Block(Statement): return "next", target - start, None - # Find the statement and try to run it. + # Find the statement and try to run it. stmt = self.statements[index] action, arg, pause = stmt.execute(trans, target - start, child_state, event) @@ -573,13 +535,13 @@ class Block(Statement): loop_end = target - arg duration = loop_end - loop_start - if duration <= 0: - raise Exception("ATL appears to be in an infinite loop.") - # Figure how many durations can occur between the # start of the loop and now. new_repeats = int((target - loop_start) / duration) + if duration <= 0: + raise Exception("ATL appears to be in an infinite loop.") + if count is not None: if repeats + new_repeats >= count: new_repeats = count - repeats @@ -655,7 +617,7 @@ class RawMultipurpose(RawStatement): def add_spline(self, name, exprs): self.splines.append((name, exprs)) - def compile(self, ctx): #@ReservedAssignment + def compile(self, ctx): compiling(self.loc) @@ -717,7 +679,7 @@ class RawMultipurpose(RawStatement): splines.append((name, values)) - for expr, _with in self.expressions: + for expr, with_ in self.expressions: try: value = ctx.eval(expr) except: @@ -739,9 +701,9 @@ class RawMultipurpose(RawStatement): return Interpolation(self.loc, warper, duration, properties, self.revolution, circles, splines) - def predict(self, ctx): + def predict(self, ctx, callback): - for i, _j in self.expressions: + for i, j in self.expressions: try: i = ctx.eval(i) @@ -749,14 +711,17 @@ class RawMultipurpose(RawStatement): continue if isinstance(i, ATLTransformBase): - i.atl.predict(ctx) + i.atl.predict(ctx, callback) return try: - renpy.easy.predict(i) + i = renpy.easy.displayable(i) except: continue - + + if isinstance(i, renpy.display.core.Displayable): + i.predict(callback) + # This lets us have an ATL transform as our child. class RawContainsExpr(RawStatement): @@ -766,7 +731,7 @@ class RawContainsExpr(RawStatement): self.expression = expr - def compile(self, ctx): #@ReservedAssignment + def compile(self, ctx): compiling(self.loc) child = ctx.eval(self.expression) return Child(self.loc, child, None) @@ -781,7 +746,7 @@ class RawChild(RawStatement): self.children = [ child ] - def compile(self, ctx): #@ReservedAssignment + def compile(self, ctx): box = renpy.display.layout.MultiBox(layout='fixed') for i in self.children: @@ -806,9 +771,9 @@ class Child(Statement): old_child = trans.raw_child - if (old_child is not None) and (old_child is not renpy.display.motion.null) and (self.transition is not None): + if old_child is not None and self.transition is not None: child = self.transition(old_widget=old_child, - new_widget=self.child) + new_widget=self.child) else: child = self.child @@ -865,10 +830,10 @@ class Interpolation(Statement): # Now, the things we change linearly are in the difference # between the new and old states. linear = trans.state.diff(newts) - + revolution = None splines = [ ] - + # Clockwise revolution. if self.revolution is not None: @@ -919,19 +884,12 @@ class Interpolation(Statement): state = (linear, revolution, splines) - # Ensure that we set things, even if they don't actually - # change from the old state. - for k, v in self.properties: - if k not in linear: - setattr(trans.state, k, v) - else: linear, revolution, splines = state - + # Linearly interpolate between the things in linear. for k, (old, new) in linear.items(): value = interpolate(complete, old, new, PROPERTIES[k]) - setattr(trans.state, k, value) # Handle the revolution. @@ -940,7 +898,6 @@ class Interpolation(Statement): trans.state.angle = interpolate(complete, startangle, endangle, float) trans.state.radius = interpolate(complete, startradius, endradius, float) - # Handle any splines we might have. for name, values in splines: value = interpolate_spline(complete, values) @@ -964,7 +921,7 @@ class RawRepeat(RawStatement): self.repeats = repeats - def compile(self, ctx): #@ReservedAssignment + def compile(self, ctx): compiling(self.loc) @@ -996,12 +953,12 @@ class RawParallel(RawStatement): super(RawParallel, self).__init__(loc) self.blocks = [ block ] - def compile(self, ctx): #@ReservedAssignment + def compile(self, ctx): return Parallel(self.loc, [i.compile(ctx) for i in self.blocks]) - def predict(self, ctx): + def predict(self, ctx, callback): for i in self.blocks: - i.predict(ctx) + i.predict(ctx, callback) class Parallel(Statement): @@ -1058,13 +1015,13 @@ class RawChoice(RawStatement): self.choices = [ (chance, block) ] - def compile(self, ctx): #@ReservedAssignment + def compile(self, ctx): compiling(self.loc) return Choice(self.loc, [ (ctx.eval(chance), block.compile(ctx)) for chance, block in self.choices]) - def predict(self, ctx): - for _i, j in self.choices: - j.predict(ctx) + def predict(self, ctx, callback): + for i, j in self.choices: + j.predict(ctx, callback) class Choice(Statement): @@ -1116,7 +1073,7 @@ class RawTime(RawStatement): super(RawTime, self).__init__(loc) self.time = time - def compile(self, ctx): #@ReservedAssignment + def compile(self, ctx): compiling(self.loc) return Time(self.loc, ctx.eval(self.time)) @@ -1140,7 +1097,7 @@ class RawOn(RawStatement): self.handlers = { name : block } - def compile(self, ctx): #@ReservedAssignment + def compile(self, ctx): compiling(self.loc) @@ -1151,9 +1108,9 @@ class RawOn(RawStatement): return On(self.loc, handlers) - def predict(self, ctx): + def predict(self, ctx, callback): for i in self.handlers.values(): - i.predict(ctx) + i.predict(ctx, callback) class On(Statement): @@ -1172,6 +1129,7 @@ class On(Statement): else: name, start, cstate = state + # If we have an external event, and we have a handler for it, # handle it. if event in self.handlers: @@ -1182,7 +1140,6 @@ class On(Statement): name = event start = st cstate = None - while True: @@ -1194,19 +1151,18 @@ class On(Statement): # If we get a continue, save our state. if action == "continue": - + # If it comes from a hide block, indicate that. - if name == "hide" or name == "replaced": + if name == "hide": trans.hide_response = False - trans.replaced_response = False - + return "continue", (name, start, arg), pause # If we get a next, then try going to the default # event, unless we're already in default, in which case we # go to None. elif action == "next": - if name == "default" or name == "hide" or name == "replaced": + if name == "default" or name == "hide": name = None else: name = "default" @@ -1242,7 +1198,7 @@ class RawEvent(RawStatement): self.name = name - def compile(self, ctx): #@ReservedAssignment + def compile(self, ctx): return Event(self.loc, self.name) @@ -1264,11 +1220,10 @@ class RawFunction(RawStatement): self.expr = expr - def compile(self, ctx): #@ReservedAssignment + def compile(self, ctx): compiling(self.loc) return Function(self.loc, ctx.eval(self.expr)) - class Function(Statement): def __init__(self, loc, function): @@ -1283,6 +1238,8 @@ class Function(Statement): return "continue", None, fr else: return "next", 0, None + + # This parses an ATL block. diff --git a/ast2json/renpy/game.py b/ast2json/renpy/game.py index d46043f..f297fc9 100644 --- a/ast2json/renpy/game.py +++ b/ast2json/renpy/game.py @@ -1,4 +1,4 @@ -# Copyright 2004-2013 Tom Rothamel +# Copyright 2004-2010 PyTom # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files @@ -22,6 +22,13 @@ # This module is intended to be used as a singleton object. # It's purpose is to store in one global all of the data that would # be to annoying to lug around otherwise. +# +# Many modules will probablt want to import this using a command like: +# +# import renpy.game as game +# +# These modules will then be able to access the various globals defined +# in this module as fields on game. import renpy @@ -33,17 +40,25 @@ basepath = None searchpath = [ ] # The options that were read off the command line. -args = None +options = None # The game's script. script = None +# A shallow copy of the store dictionary made at the end of the init +# phase. If a key in here points to the same value here as it does in +# the store, it is not saved. +clean_store = None + # A stack of execution contexts. contexts = [ ] # The interface that the game uses to interact with the user. interface = None +# Are we still running init blocks? +init_phase = True + # Are we inside lint? lint = False @@ -80,9 +95,6 @@ less_updates = False # Should we never show the mouse? less_mouse = False -# Should we not imagedissiolve? -less_imagedissolve = False - # The class that's used to hold the persistent data. class Persistent(object): @@ -97,107 +109,75 @@ class Persistent(object): return None # The persistent data that's kept from session to session -persistent = Persistent() +persistent = None -class Preferences(renpy.object.Object): +class Preferences(object): """ Stores preferences that will one day be persisted. """ - __version__ = 5 - - def after_upgrade(self, version): - if version < 1: - self.mute_volumes = 0 - if version < 2: - self.using_afm_enable = False - if version < 3: - self.physical_size = None - if version < 4: - self.renderer = "auto" - self.performance_test = True - if version < 5: - self.language = None - - def __init__(self): - self.fullscreen = False - self.skip_unseen = False - self.text_cps = 0 - self.afm_time = 0 - self.afm_enable = True + def reinit(self): + self.fullscreen = False # W0201 + self.skip_unseen = False # W0201 + self.text_cps = 0 # W0201 + self.afm_time = 0 # W0201 + self.afm_enable = True # W0201 + # These will be going away soon. - self.sound = True - self.music = True + self.sound = True # W0201 + self.music = True # W0201 + # 2 - All transitions. # 1 - Only non-default transitions. # 0 - No transitions. - self.transitions = 2 + self.transitions = 2 # W0201 - self.skip_after_choices = False + self.skip_after_choices = False # W0201 # Mixer channel info. - - # A map from channel name to the current volume (between 0 and 1). - self.volumes = { } - - # True if the channel should not play music. False - # otherwise. (Not used anymore.) - self.mute = { } + self.volumes = { } # W0201 + self.mute = { } # W0201 # Joystick mappings. - self.joymap = dict( + self.joymap = dict( # W0201 joy_left="Axis 0.0 Negative", joy_right="Axis 0.0 Positive", joy_up="Axis 0.1 Negative", joy_down="Axis 0.1 Positive", joy_dismiss="Button 0.0") - - # The size of the window, or None if we don't know it yet. - self.physical_size = None - - # The graphics renderer we use. - self.renderer = "auto" - - # Should we do a performance test on startup? - self.performance_test = True - # The language we use for translations. - self.language = None - def set_volume(self, mixer, volume): + if volume == 0: + self.mute[mixer] = True + else: + self.mute[mixer] = False + self.volumes[mixer] = volume def get_volume(self, mixer): - return self.volumes.get(mixer, 0) + return self.volumes[mixer] - def set_mute(self, mixer, mute): - self.mute[mixer] = mute + def __setstate__(self, state): + self.reinit() + vars(self).update(state) - def get_mute(self, mixer): - return self.mute[mixer] - -# The current preferences. -preferences = Preferences() - -class RestartContext(Exception): - """ - Restarts the current context. If `label` is given, calls that label - in the restarted context. - """ + def __init__(self): + self.reinit() - def __init__(self, label): - self.label = label +# The current preferences. +preferences = None -class RestartTopContext(Exception): +class RestartException(Exception): """ - Restarts the top context. If `label` is given, calls that label - in the restarted context. + This class will be used to convey to the system that the context has + been changed, and therefore execution needs to be restarted. """ - def __init__(self, label): + def __init__(self, contexts, label): # W0231 + self.contexts = contexts self.label = label - + class FullRestartException(Exception): """ An exception of this type forces a hard restart, completely @@ -207,6 +187,7 @@ class FullRestartException(Exception): def __init__(self, reason="end_game"): # W0231 self.reason = reason + class UtterRestartException(Exception): """ An exception of this type forces an even harder restart, causing @@ -216,17 +197,9 @@ class UtterRestartException(Exception): class QuitException(Exception): """ An exception of this class will let us force a safe quit, from - anywhere in the program. - - `relaunch` - If given, the program will run another copy of itself, with the - same arguments. + anywhere in the program. Do not pass go, do not collect $200. """ - def __init__(self, relaunch=False): - Exception.__init__(self) - self.relaunch = relaunch - class JumpException(Exception): """ This should be raised with a label as the only argument. This causes @@ -240,47 +213,11 @@ class JumpOutException(Exception): the current context, and then raises a JumpException. """ -class CallException(Exception): - """ - Raise this exception to cause the current statement to terminate, - and control to be transferred to the named label. - """ - - def __init__(self, label, args, kwargs): - Exception.__init__(self) - - self.label = label - self.args = args - self.kwargs = kwargs - -class EndReplay(Exception): - """ - Raise this exception to end the current replay (the current call to - call_replay). - """ - class ParseErrorException(Exception): """ This is raised when a parse error occurs, after it has been reported to the user. """ - -# A tuple of exceptions that should not be caught by the -# exception reporting mechanism. -CONTROL_EXCEPTIONS = ( - RestartContext, - RestartTopContext, - FullRestartException, - UtterRestartException, - QuitException, - JumpException, - JumpOutException, - CallException, - EndReplay, - ParseErrorException, - KeyboardInterrupt, - ) - def context(index=-1): """ @@ -290,7 +227,7 @@ def context(index=-1): return contexts[index] -def invoke_in_new_context(callable, *args, **kwargs): #@ReservedAssignment +def invoke_in_new_context(callable, *args, **kwargs): """ This pushes the current context, and invokes the given python function in a new context. When that function returns or raises an @@ -312,30 +249,18 @@ def invoke_in_new_context(callable, *args, **kwargs): #@ReservedAssignment inside an interaction. """ - context = renpy.execution.Context(False, contexts[-1], clear=True) + context = renpy.execution.Context(False, contexts[-1]) contexts.append(context) - if renpy.display.interface is not None: - renpy.display.interface.enter_context() - try: - return callable(*args, **kwargs) - - except renpy.game.JumpOutException as e: - - raise renpy.game.JumpException(e.args[0]) - finally: - contexts.pop() - contexts[-1].do_deferred_rollback() - if interface.restart_interaction and contexts: + if interface.restart_interaction: contexts[-1].scene_lists.focused = None - def call_in_new_context(label, *args, **kwargs): """ This code creates a new context, and starts executing code from @@ -347,11 +272,9 @@ def call_in_new_context(label, *args, **kwargs): inside an interaction. """ - context = renpy.execution.Context(False, contexts[-1], clear=True) + context = renpy.execution.Context(False, contexts[-1]) contexts.append(context) - if renpy.display.interface is not None: - renpy.display.interface.enter_context() if args: renpy.store._args = args @@ -366,72 +289,22 @@ def call_in_new_context(label, *args, **kwargs): try: context.goto_label(label) - renpy.execution.run_context(False) + context.run() - rv = renpy.store._return #@UndefinedVariable + rv = renpy.store._return + context.pop_all_dynamic() + contexts.pop() return rv except renpy.game.JumpOutException as e: + context.pop_all_dynamic() + contexts.pop() raise renpy.game.JumpException(e.args[0]) finally: - - contexts.pop() - contexts[-1].do_deferred_rollback() - - if interface.restart_interaction and contexts: + if interface.restart_interaction: contexts[-1].scene_lists.focused = None - -def call_replay(label, scope={}): - """ - :doc: replay - Calls a label as a memory. - - Keyword arguments are used to set the initial values of variables in the - memory context. - """ - - 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) - contexts.append(context) - - if renpy.display.interface is not None: - renpy.display.interface.enter_context() - - for k, v in scope.items(): - setattr(renpy.store, k, v) - renpy.store._in_replay = label - - try: - - context.goto_label("_start_replay") - renpy.execution.run_context(False) - - except EndReplay: - pass - - finally: - contexts.pop() - renpy.game.log = old_log - sb.restore() - - if interface.restart_interaction and contexts: - contexts[-1].scene_lists.focused = None - - -# Type information. -if False: - script = renpy.script.Script() - interface = renpy.display.core.Interface() - log = renpy.python.RollbackLog() diff --git a/ast2json/renpy/object.py b/ast2json/renpy/object.py index c595e8c..98b8a16 100644 --- a/ast2json/renpy/object.py +++ b/ast2json/renpy/object.py @@ -1,60 +1,60 @@ -# Copyright 2004-2013 Tom Rothamel -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the "Software"), to deal in the Software without restriction, -# including without limitation the rights to use, copy, modify, merge, -# publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -class Object(object): - """ - Our own base class. Contains methods to simplify serialization. - """ - - __version__ = 0 - - nosave = [ ] - - def __getstate__(self): - rv = vars(self).copy() - - for f in self.nosave: - if f in rv: - del rv[f] - - - rv["__version__"] = self.__version__ - - return rv - - - # None, to prevent this from being called when unnecessary. - after_setstate = None - - def __setstate__(self, new_dict): - - version = new_dict.pop("__version__", 0) - - self.__dict__.update(new_dict) - - if version != self.__version__: - self.after_upgrade(version) # E1101 - - if self.after_setstate: - self.after_setstate() # E1102 - -# We don't handle slots with this mechanism, since the call to vars should -# throw an error. +# Copyright 2004-2010 PyTom +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +class Object(object): + """ + Our own base class. Contains methods to simplify serialization. + """ + + __version__ = 0 + + nosave = [ ] + + def __getstate__(self): + rv = vars(self).copy() + + for f in self.nosave: + if f in rv: + del rv[f] + + + rv["__version__"] = self.__version__ + + return rv + + + # None, to prevent this from being called when unnecessary. + after_setstate = None + + def __setstate__(self, new_dict): + + version = new_dict.pop("__version__", 0) + + self.__dict__.update(new_dict) + + if version != self.__version__: + self.after_upgrade(version) # E1101 + + if self.after_setstate: + self.after_setstate() # E1102 + +# We don't handle slots with this mechanism, since the call to vars should +# throw an error. diff --git a/ast2json/renpy/parser.py b/ast2json/renpy/parser.py new file mode 100644 index 0000000..c0d9164 --- /dev/null +++ b/ast2json/renpy/parser.py @@ -0,0 +1,1842 @@ +# Copyright 2004-2010 PyTom +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# This module contains the parser for the Ren'Py script language. It's +# called when parsing is necessary, and creats an AST from the script. + +import codecs +import re +import os +import os.path + +import renpy +import renpy.ast as ast + +# A list of parse error messages. +parse_errors = [ ] + +class ParseError(Exception): + + def __init__(self, filename, number, msg, line=None, pos=None, first=False): + message = "On line %d of %s: %s" % (number, unicode_filename(filename), msg) + + if line: + lines = line.split('\n') + + if len(lines) > 1: + open_string = None + i = 0 + + while i < len(lines[0]): + c = lines[0][i] + + if c == "\\": + i += 1 + elif c == open_string: + open_string = None + elif open_string: + pass + elif c == '`' or c == '\'' or c == '"': + open_string = c + + i += 1 + + if open_string: + message += "\n(Perhaps you left out a %s at the end of the first line.)" % open_string + + for l in lines: + message += "\n" + l + + if pos is not None: + if pos <= len(l): + message += "\n" + " " * pos + "^" + pos = None + else: + pos -= len(l) + + if first: + break + + self.message = message + + Exception.__init__(self, message) + + def __unicode__(self): + return self.message + +# Something to hold the expected line number. +class LineNumberHolder(object): + """ + Holds the expected line number. + """ + + def __init__(self): + self.line = 0 + +def unicode_filename(fn): + """ + Converts the supplied filename to unicode. + """ + + if isinstance(fn, str): + return fn + + # Windows. + try: + return fn.decode("mbcs") + except: + pass + + # Mac and (sane) Unix + try: + return fn.decode("utf-8") + except: + pass + + # Insane systems, mojibake. + return fn.decode("latin-1") + +# Matches either a word, or something else. Most magic is taken care of +# before this. +lllword = re.compile(r'__(\w+)|\w+| +|.', re.S) + +def munge_filename(fn): + # The prefix that's used when __ is found in the file. + rv = os.path.basename(fn) + rv = os.path.splitext(rv)[0] + rv = rv.replace(" ", "_") + + def munge_char(m): + return hex(ord(m.group(0))) + + rv = re.sub(r'[^a-zA-Z0-9_]', munge_char, rv) + + return "_m1_" + rv + "__" + +def list_logical_lines(filename): + """ + This reads the specified filename, and divides it into logical + line. The return value of this function is a list of (filename, + line number, line text) triples. + """ + + f = codecs.open(filename, "r", "utf-8") + data = f.read() + f.close() + + data = data.replace("\r\n", "\n") + data = data.replace("\r", "\n") + + if "RENPY_PATH_ELIDE" in os.environ: + old, new = os.environ["RENPY_PATH_ELIDE"].split(':') + filename = filename.replace(old, new) + + prefix = munge_filename(filename) + + # Add some newlines, to fix lousy editors. + data += "\n\n" + + # The result. + rv = [] + + # The line number in the physical file. + number = 1 + + # The current position we're looking at in the buffer. + pos = 0 + + # Skip the BOM, if any. + if len(data) and data[0] == '\ufeff': + pos += 1 + + # Looping over the lines in the file. + while pos < len(data): + + # The line number of the start of this logical line. + start_number = number + + # The line that we're building up. + line = "" + + # The number of open parenthesis there are right now. + parendepth = 0 + + # Looping over the characters in a single logical line. + while pos < len(data): + + c = data[pos] + + if c == '\t': + raise Exception("%s contains a tab character on line %d. Tab characters are not allowed in Ren'Py scripts." % (filename, number)) + + if c == '\n': + number += 1 + + if c == '\n' and not parendepth: + # If not blank... + if not re.match("^\s*$", line): + + # Add to the results. + rv.append((filename, start_number, line)) + + pos += 1 + # This helps out error checking. + line = "" + break + + # Backslash/newline. + if c == "\\" and data[pos+1] == "\n": + pos += 2 + number += 1 + line += "\\\n" + continue + + # Parenthesis. + if c in ('(', '[', '{'): + parendepth += 1 + + if c in ('}', ']', ')') and parendepth: + parendepth -= 1 + + # Comments. + if c == '#': + while data[pos] != '\n': + pos += 1 + + continue + + # Strings. + if c in ('"', "'", "`"): + delim = c + line += c + pos += 1 + + escape = False + + while pos < len(data): + + c = data[pos] + + if c == '\n': + number += 1 + + if escape: + escape = False + pos += 1 + line += c + continue + + if c == delim: + pos += 1 + line += c + break + + if c == '\\': + escape = True + + line += c + pos += 1 + + continue + + continue + + m = lllword.match(data, pos) + + word = m.group(0) + rest = m.group(1) + + if rest and "__" not in rest: + word = prefix + rest + + line += word + pos = m.end(0) + + # print repr(data[pos:]) + + + if not line == "": + raise ParseError(filename, start_number, "is not terminated with a newline. (Check strings and parenthesis.)", line=line, first=True) + + return rv + + + +def group_logical_lines(lines): + """ + This takes as input the list of logical line triples output from + list_logical_lines, and breaks the lines into blocks. Each block + is represented as a list of (filename, line number, line text, + block) triples, where block is a block list (which may be empty if + no block is associated with this line.) + """ + + # Returns the depth of a line, and the rest of the line. + def depth_split(l): + + depth = 0 + index = 0 + + while True: + if l[index] == ' ': + depth += 1 + index += 1 + continue + + # if l[index] == '\t': + # index += 1 + # depth = depth + 8 - (depth % 8) + # continue + + break + + return depth, l[index:] + + # i, min_depth -> block, new_i + def gll_core(i, min_depth): + + rv = [] + depth = None + + while i < len(lines): + + filename, number, text = lines[i] + + line_depth, rest = depth_split(text) + + # This catches a block exit. + if line_depth < min_depth: + break + + if depth is None: + depth = line_depth + + if depth != line_depth: + raise ParseError(filename, number, "indentation mismatch.") + + # Advance to the next line. + i += 1 + + # Try parsing a block associated with this line. + block, i = gll_core(i, depth + 1) + + rv.append((filename, number, rest, block)) + + return rv, i + + return gll_core(0, 0)[0] + +class Lexer(object): + """ + The lexer that is used to lex script files. This works on the idea + that we want to lex each line in a block individually, and use + sub-lexers to lex sub-blocks. + """ + + # A list of keywords which should not be parsed as names, because + # there is a huge chance of confusion. + keywords = set([ + 'as', + 'at', + 'behind', + 'call', + 'expression', + 'hide', + 'if', + 'image', + 'init', + 'jump', + 'menu', + 'onlayer', + 'python', + 'return', + 'scene', + 'set', + 'show', + 'with', + 'while', + 'zorder', + 'transform', + ]) + + + def __init__(self, block, init=False): + + # Are we underneath an init block? + self.init = init + + self.block = block + self.eob = False + + self.line = -1 + + # These are set by advance. + self.filename = "" + self.text = "" + self.number = 0 + self.subblock = [ ] + self.pos = 0 + self.word_cache_pos = -1 + self.word_cache_newpos = -1 + self.word_cache = "" + + def advance(self): + """ + Advances this lexer to the next line in the block. The lexer + starts off before the first line, so advance must be called + before any matching can be done. Returns True if we've + successfully advanced to a line in the block, or False if we + have advanced beyond all lines in the block. In general, once + this method has returned False, the lexer is in an undefined + state, and it doesn't make sense to call any method other than + advance (which will always return False) on the lexer. + """ + + self.line += 1 + + if self.line >= len(self.block): + self.eob = True + return + + self.filename, self.number, self.text, self.subblock = self.block[self.line] + self.pos = 0 + self.word_cache_pos = -1 + + return True + + def match_regexp(self, regexp): + """ + Tries to match the given regexp at the current location on the + current line. If it succeds, it returns the matched text (if + any), and updates the current position to be after the + match. Otherwise, returns None and the position is unchanged. + """ + + if self.eob: + return None + + if self.pos == len(self.text): + return None + + m = re.compile(regexp, re.DOTALL).match(self.text, self.pos) + + if not m: + return None + + self.pos = m.end() + + return m.group(0) + + def skip_whitespace(self): + """ + Advances the current position beyond any contiguous whitespace. + """ + + # print self.text[self.pos].encode('unicode_escape') + + self.match_regexp(r"(\s+|\\\n)+") + + def match(self, regexp): + """ + Matches something at the current position, skipping past + whitespace. Even if we can't match, the current position is + still skipped past the leading whitespace. + """ + + self.skip_whitespace() + return self.match_regexp(regexp) + + + def keyword(self, word): + """ + Matches a keyword at the current position. A keyword is a word + that is surrounded by things that aren't words, like + whitespace. (This prevents a keyword from matching a prefix.) + """ + + oldpos = self.pos + if self.word() == word: + return word + + self.pos = oldpos + return '' + + + def error(self, msg): + """ + Convenience function for reporting a parse error at the current + location. + """ + + raise ParseError(self.filename, self.number, msg, self.text, self.pos) + + def eol(self): + """ + Returns True if, after skipping whitespace, the current + position is at the end of the end of the current line, or + False otherwise. + """ + + self.skip_whitespace() + return self.pos >= len(self.text) + + def expect_eol(self): + """ + If we are not at the end of the line, raise an error. + """ + + if not self.eol(): + self.error('end of line expected.') + + def expect_noblock(self, stmt): + """ + Called to indicate this statement does not expect a block. + If a block is found, raises an error. + """ + + if self.subblock: + self.error('%s does not expect a block. Please check the indentation of the line after this one.' % stmt) + + def expect_block(self, stmt): + """ + Called to indicate that the statement requires that a non-empty + block is present. + """ + + if not self.subblock: + self.error('%s expects a non-empty block.' % stmt) + + + def subblock_lexer(self, init=False): + """ + Returns a new lexer object, equiped to parse the block + associated with this line. + """ + + init = self.init or init + + return Lexer(self.subblock, init=init) + + def string(self): + """ + Lexes a string, and returns the string to the user, or none if + no string could be found. This also takes care of expanding + escapes and collapsing whitespace. + + Be a little careful, as this can return an empty string, which is + different than None. + """ + + s = self.match(r'r?"([^\\"]|\\.)*"') + + if s is None: + s = self.match(r"r?'([^\\']|\\.)*'") + + if s is None: + s = self.match(r"r?`([^\\`]|\\.)*`") + + if s is None: + return None + + if s[0] == 'r': + raw = True + s = s[1:] + else: + raw = False + + # Strip off delimiters. + s = s[1:-1] + + if not raw: + + # Collapse runs of whitespace into single spaces. + s = re.sub(r'\s+', ' ', s) + + s = s.replace("\\n", "\n") + s = s.replace("\\{", "{{") + s = s.replace("\\%", "%%") + s = re.sub(r'\\u([0-9a-fA-F]{1,4})', + lambda m : chr(int(m.group(1), 16)), s) + s = re.sub(r'\\(.)', r'\1', s) + + return s + + def integer(self): + """ + Tries to parse an integer. Returns a string containing the + integer, or None. + """ + + return self.match(r'(\+|\-)?\d+') + + def float(self): + """ + Tries to parse a number (float). Returns a string containing the + number, or None. + """ + + return self.match(r'(\+|\-)?(\d+\.?\d*|\.\d+)([eE][-+]?\d+)?') + + def word(self): + """ + Parses a name, which may be a keyword or not. + """ + + if self.pos == self.word_cache_pos: + self.pos = self.word_cache_newpos + return self.word_cache + + self.word_cache_pos = self.pos + rv = self.match(r'[a-zA-Z_\u00a0-\ufffd][0-9a-zA-Z_\u00a0-\ufffd]*') + self.word_cache = rv + self.word_cache_newpos = self.pos + + return rv + + + def name(self): + """ + This tries to parse a name. Returns the name or None. + """ + + oldpos = self.pos + rv = self.word() + + if rv in self.keywords: + self.pos = oldpos + return None + + return rv + + def python_string(self): + """ + This tries to match a python string at the current + location. If it matches, it returns True, and the current + position is updated to the end of the string. Otherwise, + returns False. + """ + + if self.eol(): + return False + + c = self.text[self.pos] + + # Allow unicode strings. + if c == 'u': + self.pos += 1 + + if self.pos == len(self.text): + self.pos -= 1 + return False + + c = self.text[self.pos] + + if c not in ('"', "'"): + self.pos -= 1 + return False + + elif c not in ('"', "'"): + return False + + delim = c + + while True: + self.pos += 1 + + if self.eol(): + self.error("end of line reached while parsing string.") + + c = self.text[self.pos] + + if c == delim: + break + + if c == '\\': + self.pos += 1 + + self.pos += 1 + return True + + + def dotted_name(self): + """ + This tries to match a dotted name, which is one or more names, + separated by dots. Returns the dotted name if it can, or None + if it cannot. + + Once this sees the first name, it commits to parsing a + dotted_name. It will report an error if it then sees a dot + without a name behind it. + """ + + rv = self.name() + + if not rv: + return None + + while self.match(r'\.'): + n = self.name() + if not n: + self.error('expecting name.') + + rv += "." + n + + return rv + + def delimited_python(self, delim): + """ + This matches python code up to, but not including, the non-whitespace + delimiter characters. Returns a string containing the matched code, + which may be empty if the first thing is the delimiter. Raises an + error if EOL is reached before the delimiter. + """ + + start = self.pos + + while not self.eol(): + + c = self.text[self.pos] + + if c in delim: + return renpy.ast.PyExpr(self.text[start:self.pos], self.filename, self.number) + + if c == '"' or c == "'": + self.python_string() + continue + + if self.parenthesised_python(): + continue + + self.pos += 1 + + self.error("reached end of line when expecting '%s'." % delim) + + def python_expression(self): + """ + Returns a python expression, which is arbitrary python code + extending to a colon. + """ + + pe = self.delimited_python(':') + + if not pe: + self.error("expected python_expression") + + rv = renpy.ast.PyExpr(pe.strip(), pe.filename, pe.linenumber) # E1101 + + return rv + + def parenthesised_python(self): + """ + Tries to match a parenthesised python expression. If it can, + returns true and updates the current position to be after the + closing parenthesis. Returns False otherewise. + """ + + c = self.text[self.pos] + + if c == '(': + self.pos += 1 + self.delimited_python(')') + self.pos += 1 + return True + + if c == '[': + self.pos += 1 + self.delimited_python(']') + self.pos += 1 + return True + + + if c == '{': + self.pos += 1 + self.delimited_python('}') + self.pos += 1 + return True + + return False + + + def simple_expression(self): + """ + Tries to parse a simple_expression. Returns the text if it can, or + None if it cannot. + """ + + self.skip_whitespace() + if self.eol(): + return None + + start = self.pos + + # We start with either a name, a python_string, or parenthesized + # python + if (not self.python_string() and + not self.name() and + not self.float() and + not self.parenthesised_python()): + + return None + + while not self.eol(): + self.skip_whitespace() + + if self.eol(): + break + + # If we see a dot, expect a dotted name. + if self.match(r'\.'): + n = self.name() + if not n: + self.error("expecting name after dot.") + + continue + + # Otherwise, try matching parenthesised python. + if self.parenthesised_python(): + continue + + break + + return self.text[start:self.pos] + + def checkpoint(self): + """ + Returns an opaque representation of the lexer state. This can be + passed to revert to back the lexer up. + """ + + return self.filename, self.number, self.text, self.subblock, self.pos + + def revert(self, state): + """ + Reverts the lexer to the given state. State must have been returned + by a previous checkpoint operation on this lexer. + """ + + self.filename, self.number, self.text, self.subblock, self.pos = state + self.word_cache_pos = -1 + + def get_location(self): + """ + Returns a (filename, line number) tuple representing the current + physical location of the start of the current logical line. + """ + + return self.filename, self.number + + def require(self, thing, name=None): + """ + Tries to parse thing, and reports an error if it cannot be done. + + If thing is a string, tries to parse it using + self.match(thing). Otherwise, thing must be a method on this lexer + object, which is called directly. + """ + + if isinstance(thing, str): + name = name or thing + rv = self.match(thing) + else: + name = name or thing.__func__.__name__ + rv = thing() + + if rv is None: + self.error("expected '%s' not found." % name) + + return rv + + def rest(self): + """ + Skips whitespace, then returns the rest of the current + line, and advances the current position to the end of + the current line. + """ + + self.skip_whitespace() + + pos = self.pos + self.pos = len(self.text) + return self.text[pos:] + + def python_block(self): + """ + Returns the subblock of this code, and subblocks of that + subblock, as indented python code. This tries to insert + whitespace to ensure line numbers match up. + """ + + rv = [ ] + + o = LineNumberHolder() + o.line = self.number + + def process(block, indent): + + for fn, ln, text, subblock in block: + + if o.line > ln: + assert False + + while o.line < ln: + rv.append(indent + '\n') + o.line += 1 + + linetext = indent + text + '\n' + + rv.append(linetext) + o.line += linetext.count('\n') + + process(subblock, indent + ' ') + + process(self.subblock, '') + return ''.join(rv) + +def parse_image_name(l): + """ + This parses an image name, and returns it as a tuple. It requires + that the image name be present. + """ + + rv = [ l.require(l.name) ] + + while True: + n = l.simple_expression() + if not n: + break + + rv.append(n.strip()) + + return tuple(rv) + +def parse_simple_expression_list(l): + """ + This parses a comma-separated list of simple_expressions, and + returns a list of strings. It requires at least one + simple_expression be present. + """ + + rv = [ l.require(l.simple_expression) ] + + while True: + if not l.match(','): + break + + e = l.simple_expression() + + if not e: + break + + rv.append(e) + + return rv + +def parse_image_specifier(l): + """ + This parses an image specifier. + """ + + tag = None + layer = None + at_list = [ ] + zorder = None + behind = [ ] + + if l.keyword("expression") or l.keyword("image"): + expression = l.require(l.simple_expression) + image_name = ( expression.strip(), ) + else: + image_name = parse_image_name(l) + expression = None + + while True: + + if l.keyword("onlayer"): + if layer: + l.error("multiple onlayer clauses are prohibited.") + else: + layer = l.require(l.name) + + continue + + if l.keyword("at"): + + if at_list: + l.error("multiple at clauses are prohibited.") + else: + at_list = parse_simple_expression_list(l) + + continue + + if l.keyword("as"): + + if tag: + l.error("multiple as clauses are prohibited.") + else: + tag = l.require(l.name) + + continue + + if l.keyword("zorder"): + + if zorder is not None: + l.error("multiple zorder clauses are prohibited.") + else: + zorder = l.require(l.simple_expression) + + continue + + if l.keyword("behind"): + + if behind: + l.error("multiple behind clauses are prohibited.") + + while True: + bhtag = l.require(l.name) + behind.append(bhtag) + if not l.match(','): + break + + continue + + break + + if layer is None: + layer = 'master' + + + + return image_name, expression, tag, at_list, layer, zorder, behind + +def parse_with(l, node): + """ + Tries to parse the with clause associated with this statement. If + one exists, then the node is wrapped in a list with the + appropriate pair of With nodes. Otherwise, just returns the + statement by itself. + """ + + loc = l.get_location() + + if not l.keyword('with'): + return node + + expr = l.require(l.simple_expression) + + return [ ast.With(loc, "None", expr), + node, + ast.With(loc, expr) ] + + + +def parse_menu(stmtl, loc): + + l = stmtl.subblock_lexer() + + has_choice = False + + has_say = False + has_caption = False + + with_ = None + set = None + + say_who = None + say_what = None + + # Tuples of (label, condition, block) + items = [ ] + + l.advance() + + while not l.eob: + + if l.keyword('with'): + with_ = l.require(l.simple_expression) + l.expect_eol() + l.expect_noblock('with clause') + l.advance() + + continue + + if l.keyword('set'): + set = l.require(l.simple_expression) + l.expect_eol() + l.expect_noblock('set menuitem') + l.advance() + + continue + + # Try to parse a say menuitem. + state = l.checkpoint() + + who = l.simple_expression() + what = l.string() + + if who is not None and what is not None: + + l.expect_eol() + l.expect_noblock("say menuitem") + + if has_caption: + l.error("Say menuitems and captions may not exist in the same menu.") + + if has_say: + l.error("Only one say menuitem may exist per menu.") + + has_say = True + say_who = who + say_what = what + + l.advance() + + continue + + l.revert(state) + + + label = l.string() + + if label is None: + l.error('expected menuitem') + + # A string on a line by itself is a caption. + if l.eol(): + l.expect_noblock('caption menuitem') + + if label and has_say: + l.error("Captions and say menuitems may not exist in the same menu.") + + # Only set this if the caption is not "". + if label: + has_caption = True + + items.append((label, "True", None)) + l.advance() + + continue + + # Otherwise, we have a choice. + has_choice = True + + condition = "True" + + if l.keyword('if'): + condition = l.require(l.python_expression) + + l.require(':') + l.expect_eol() + l.expect_block('choice menuitem') + + block = parse_block(l.subblock_lexer()) + + items.append((label, condition, block)) + l.advance() + + if not has_choice: + stmtl.error("Menu does not contain any choices.") + + rv = [ ] + if has_say: + rv.append(ast.Say(loc, say_who, say_what, None, interact=False)) + + rv.append(ast.Menu(loc, items, set, with_)) + + return rv + +def parse_parameters(l): + + parameters = [ ] + positional = [ ] + extrapos = None + extrakw = None + + add_positional = True + + names = set() + + if not l.match(r'\('): + return None + + while True: + + if l.match('\)'): + break + + if l.match(r'\*\*'): + + if extrakw is not None: + l.error('a label may have only one ** parameter') + + extrakw = l.require(l.name) + + if extrakw in names: + l.error('parameter %s appears twice.' % extrakw) + + names.add(extrakw) + + + elif l.match(r'\*'): + + if not add_positional: + l.error('a label may have only one * parameter') + + add_positional = False + + extrapos = l.name() + + if extrapos is not None: + + if extrapos in names: + l.error('parameter %s appears twice.' % extrapos) + + names.add(extrapos) + + else: + + name = l.require(l.name) + + if name in names: + l.error('parameter %s appears twice.' % name) + + names.add(name) + + if l.match(r'='): + default = l.delimited_python("),") + else: + default = None + + parameters.append((name, default)) + + if add_positional: + positional.append(name) + + if l.match(r'\)'): + break + + l.require(r',') + + return renpy.ast.ParameterInfo(parameters, positional, extrapos, extrakw) + +def parse_arguments(l): + """ + Parse a list of arguments, if one is present. + """ + + arguments = [ ] + extrakw = None + extrapos = None + + if not l.match(r'\('): + return None + + while True: + + if l.match('\)'): + break + + if l.match(r'\*\*'): + + if extrakw is not None: + l.error('a call may have only one ** argument') + + extrakw = l.delimited_python("),") + + + elif l.match(r'\*'): + if extrapos is not None: + l.error('a call may have only one * argument') + + extrapos = l.delimited_python("),") + + else: + + state = l.checkpoint() + + name = l.name() + if not (name and l.match(r'=')): + l.revert(state) + name = None + + arguments.append((name, l.delimited_python("),"))) + + if l.match(r'\)'): + break + + l.require(r',') + + return renpy.ast.ArgumentInfo(arguments, extrapos, extrakw) + + + + +def parse_statement(l): + """ + This parses a Ren'Py statement. l is expected to be a Ren'Py lexer + that has been advanced to a logical line. This function will + advance l beyond the last logical line making up the current + statement, and will return an AST object representing this + statement, or a list of AST objects representing this statement. + """ + + # Store the current location. + loc = l.get_location() + + ### If statement + if l.keyword('if'): + entries = [ ] + + condition = l.require(l.python_expression) + l.require(':') + l.expect_eol() + l.expect_block('if statement') + + block = parse_block(l.subblock_lexer()) + + entries.append((condition, block)) + + l.advance() + + while l.keyword('elif'): + + condition = l.require(l.python_expression) + l.require(':') + l.expect_eol() + l.expect_block('elif clause') + + block = parse_block(l.subblock_lexer()) + + entries.append((condition, block)) + + l.advance() + + if l.keyword('else'): + l.require(':') + l.expect_eol() + l.expect_block('else clause') + + block = parse_block(l.subblock_lexer()) + + entries.append(('True', block)) + + l.advance() + + return ast.If(loc, entries) + + if l.keyword('elif'): + l.error('elif clause must be associated with an if statement.') + + if l.keyword('else'): + l.error('else clause must be associated with an if statement.') + + + ### While statement + if l.keyword('while'): + condition = l.require(l.python_expression) + l.require(':') + l.expect_eol() + l.expect_block('while statement') + block = parse_block(l.subblock_lexer()) + l.advance() + + return ast.While(loc, condition, block) + + + ### Pass statement + if l.keyword('pass'): + l.expect_noblock('pass statement') + l.expect_eol() + l.advance() + + return ast.Pass(loc) + + + ### Menu statement. + if l.keyword('menu'): + l.expect_block('menu statement') + label = l.name() + l.require(':') + l.expect_eol() + + menu = parse_menu(l, loc) + + l.advance() + + rv = [ ] + + if label: + rv.append(ast.Label(loc, label, [], None)) + + rv.extend(menu) + + return rv + + ### Return statement. + if l.keyword('return'): + l.expect_noblock('return statement') + + rest = l.rest() + if not rest: + rest = None + + l.expect_eol() + l.advance() + + return ast.Return(loc, rest) + + ### Jump statement + if l.keyword('jump'): + l.expect_noblock('jump statement') + + if l.keyword('expression'): + expression = True + target = l.require(l.simple_expression) + else: + expression = False + target = l.require(l.name) + + l.expect_eol() + l.advance() + + return ast.Jump(loc, target, expression) + + + ### Call/From statement. + if l.keyword('call'): + l.expect_noblock('call statment') + + if l.keyword('expression'): + expression = True + target = l.require(l.simple_expression) + + else: + expression = False + target = l.require(l.name) + + # Optional pass, to let someone write: + # call expression foo pass (bar, baz) + l.keyword('pass') + + arguments = parse_arguments(l) + + rv = [ ast.Call(loc, target, expression, arguments) ] + + if l.keyword('from'): + name = l.require(l.name) + rv.append(ast.Label(loc, name, [], None)) + else: + rv.append(ast.Pass(loc)) + + l.expect_eol() + l.advance() + + return rv + + ### Scene statement. + if l.keyword('scene'): + + if l.keyword('onlayer'): + layer = l.require(l.name) + else: + layer = "master" + + # Empty. + if l.eol(): + l.advance() + return ast.Scene(loc, None, layer) + + imspec = parse_image_specifier(l) + stmt = ast.Scene(loc, imspec, imspec[4]) + rv = parse_with(l, stmt) + + if l.match(':'): + stmt.atl = renpy.atl.parse_atl(l.subblock_lexer()) + else: + l.expect_noblock('scene statement') + + l.expect_eol() + l.advance() + + return rv + + ### Show statement. + if l.keyword('show'): + imspec = parse_image_specifier(l) + stmt = ast.Show(loc, imspec) + rv = parse_with(l, stmt) + + if l.match(':'): + stmt.atl = renpy.atl.parse_atl(l.subblock_lexer()) + else: + l.expect_noblock('show statement') + + l.expect_eol() + l.advance() + + return rv + + ### Hide statement. + if l.keyword('hide'): + imspec = parse_image_specifier(l) + rv = parse_with(l, ast.Hide(loc, imspec)) + + l.expect_eol() + l.expect_noblock('hide statement') + l.advance() + + return rv + + ### With statement. + if l.keyword('with'): + expr = l.require(l.simple_expression) + l.expect_eol() + l.expect_noblock('with statement') + l.advance() + + return ast.With(loc, expr) + + ### Image statement. + if l.keyword('image'): + + name = parse_image_name(l) + + if l.match(':'): + l.expect_eol() + expr = None + atl = renpy.atl.parse_atl(l.subblock_lexer()) + else: + l.require('=') + expr = l.rest() + atl = None + l.expect_noblock('image statement') + + rv = ast.Image(loc, name, expr, atl) + + if not l.init: + rv = ast.Init(loc, [ rv ], 990) + + l.advance() + + return rv + + ### Define statement. + if l.keyword('define'): + + priority = l.integer() + if priority: + priority = int(priority) + else: + priority = 0 + + name = l.require(l.name) + l.require('=') + expr = l.rest() + + l.expect_noblock('define statement') + + rv = ast.Define(loc, name, expr) + + if not l.init: + rv = ast.Init(loc, [ rv ], priority) + + l.advance() + + return rv + + ### Transform statement. + if l.keyword('transform'): + + priority = l.integer() + if priority: + priority = int(priority) + else: + priority = 0 + + name = l.require(l.name) + parameters = parse_parameters(l) + + if parameters and (parameters.extrakw or parameters.extrapos): + l.error('transform statement does not take a variable number of parameters') + + l.require(':') + l.expect_eol() + + atl = renpy.atl.parse_atl(l.subblock_lexer()) + + rv = ast.Transform(loc, name, atl, parameters) + + if not l.init: + rv = ast.Init(loc, [ rv ], priority) + + l.advance() + + return rv + + ### One-line python statement. + if l.match(r'\$'): + python_code = l.rest() + l.expect_noblock('one-line python statement') + l.advance() + + return ast.Python(loc, python_code) + + ### Python block. + if l.keyword('python'): + + hide = False + early = False + + if l.keyword('early'): + early = True + + if l.keyword('hide'): + hide = True + + l.require(':') + l.expect_block('python block') + + python_code = l.python_block() + + l.advance() + + if early: + return ast.EarlyPython(loc, python_code, hide) + else: + return ast.Python(loc, python_code, hide) + + ### Label Statement + if l.keyword('label'): + name = l.require(l.name) + + parameters = parse_parameters(l) + + l.require(':') + l.expect_eol() + + # Optional block here. It's empty if no block is associated with + # this statement. + block = parse_block(l.subblock_lexer()) + + l.advance() + return ast.Label(loc, name, block, parameters) + + ### Init Statement + if l.keyword('init'): + + p = l.integer() + + if p: + priority = int(p) + else: + priority = 0 + + if l.keyword('python'): + + hide = False + if l.keyword('hide'): + hide = True + + l.require(':') + l.expect_block('python block') + + python_code = l.python_block() + + l.advance() + block = [ ast.Python(loc, python_code, hide) ] + + else: + l.require(':') + + l.expect_eol() + l.expect_block('init statement') + + block = parse_block(l.subblock_lexer(True)) + + l.advance() + + return ast.Init(loc, block, priority) + + # Try parsing as a user-statement. If that doesn't work, revert and + # try as a say. + + state = l.checkpoint() + + word = l.word() + if (word,) in renpy.statements.registry: + text = l.text + + l.expect_noblock(word + ' statement') + l.advance() + + renpy.exports.push_error_handler(l.error) + try: + rv = ast.UserStatement(loc, text) + finally: + renpy.exports.pop_error_handler() + + return rv + + l.revert(state) + + # Try parsing as the default statement. + if () in renpy.statements.registry: + text = l.text + l.expect_noblock('default statement') + l.advance() + + renpy.exports.push_error_handler(l.error) + try: + rv = ast.UserStatement(loc, text) + finally: + renpy.exports.pop_error_handler() + + return rv + + # The one and two arguement say statements are ambiguous in terms + # of lookahead. So we first try parsing as a one-argument, then a + # two-argument. + + # We're using the checkpoint from above. + + what = l.string() + + if l.keyword('with'): + with_ = l.require(l.simple_expression) + else: + with_ = None + + if what is not None and l.eol(): + # We have a one-argument say statement. + l.expect_noblock('say statement') + l.advance() + return ast.Say(loc, None, what, with_) + + l.revert(state) + + # Try for a two-argument say statement. + who = l.simple_expression() + what = l.string() + + if l.keyword('with'): + with_ = l.require(l.simple_expression) + else: + with_ = None + + if who and what is not None: + l.expect_eol() + l.expect_noblock('say statement') + l.advance() + return ast.Say(loc, who, what, with_) + + l.error('expected statement.') + +def parse_block(l): + """ + This parses a block of Ren'Py statements. It returns a list of the + statements contained within the block. l is a new Lexer object, for + this block. + """ + + l.advance() + rv = [ ] + + while not l.eob: + try: + + stmt = parse_statement(l) + if isinstance(stmt, list): + rv.extend(stmt) + else: + rv.append(stmt) + + except ParseError as e: + parse_errors.append(e.message) + l.advance() + + return rv + +def parse(fn): + """ + Parses a Ren'Py script contained within the file with the given + filename. Returns a list of AST objects representing the + statements that were found at the top level of the file. + """ + + renpy.game.exception_info = 'While parsing ' + fn + '.' + + try: + lines = list_logical_lines(fn) + nested = group_logical_lines(lines) + except ParseError as e: + parse_errors.append(e.message) + return None + + l = Lexer(nested) + + rv = parse_block(l) + + if parse_errors: + return None + + return rv + +def report_parse_errors(): + + if not parse_errors: + return False + + erfile = "errors.txt" + f = file(erfile, "w") + opat_err = os.path.realpath(erfile) + f.write(codecs.BOM_UTF8) + + print("I'm sorry, but errors were detected in your script. Please correct the", file=f) + print("errors listed below, and try again.", file=f) + print(file=f) + + for i in parse_errors: + + try: + i = i.encode("utf-8") + except: + pass + + print() + print(file=f) + print(i) + print(i, file=f) + + print(file=f) + print("Ren'Py Version:", renpy.version, file=f) + + f.close() + + try: + if renpy.config.editor: + renpy.exports.launch_editor([ opat_err ], 1, transient=1) + else: + os.startfile(erfile) # E1101 + except: + pass + + return True + + diff --git a/ast2json/renpy/statements.py b/ast2json/renpy/statements.py new file mode 100644 index 0000000..3acd950 --- /dev/null +++ b/ast2json/renpy/statements.py @@ -0,0 +1,686 @@ +# Copyright 2004-2010 PyTom +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# This module contains code to support user-defined statements. + +import renpy + +# The statement registry. It's a map from tuples giving the prefixes of +# statements to dictionaries giving the methods used for that statement. +registry = { } + +def register(name, parse=None, lint=None, execute=None, predict=None, next=None, scry=None): + + if name == "": + name = () + else: + name = tuple(name.split()) + + registry[name] = dict(parse=parse, + lint=lint, + execute=execute, + predict=predict, + next=next, + scry=scry) + + while True: + name = name[:-1] + if not name: + break + + if name not in registry: + registry[name] = None + +def parse(node, line): + + block = [ (node.filename, node.linenumber, line, [ ]) ] + l = renpy.parser.Lexer(block) + l.advance() + + name = () + + while True: + cpt = l.checkpoint() + word = l.word() + + if word is None: + break + + newname = name + (word,) + + if newname not in registry: + break + + name = newname + + l.revert(cpt) + + if registry[name] is None: + return None + + return ( name, registry[name]["parse"](l) ) + + +def call(method, parsed, *args, **kwargs): + name, parsed = parsed + + method = registry[name].get(method) + if method is None: + return None + + return method(parsed, *args, **kwargs) + +# Music play - The example of a full statement. + +def parse_play_music(l): + + + file = l.simple_expression() + if not file: + renpy.error("play requires a file") + + fadeout = "None" + fadein = "0" + channel = None + loop = None + if_changed = False + + while True: + + if l.eol(): + break + + if l.keyword('fadeout'): + fadeout = l.simple_expression() + if fadeout is None: + renpy.error('expected simple expression') + + continue + + if l.keyword('fadein'): + fadein = l.simple_expression() + if fadein is None: + renpy.error('expected simple expression') + + continue + + if l.keyword('channel'): + channel = l.simple_expression() + if channel is None: + renpy.error('expected simple expression') + + continue + + if l.keyword('loop'): + loop = True + continue + + if l.keyword('noloop'): + loop = False + continue + + if l.keyword('if_changed'): + if_changed = True + continue + + renpy.error('could not parse statement.') + + return dict(file=file, + fadeout=fadeout, + fadein=fadein, + channel=channel, + loop=loop, + if_changed=if_changed) + +def execute_play_music(p): + + if p["channel"] is not None: + channel = eval(p["channel"]) + else: + channel = "music" + + renpy.music.play(eval(p["file"]), + fadeout=eval(p["fadeout"]), + fadein=eval(p["fadein"]), + channel=channel, + loop=p.get("loop", None), + if_changed=p.get("if_changed", False)) + +def predict_play_music(p): + return [ ] + +def lint_play_music(p, channel="music"): + + file = _try_eval(p["file"], 'filename') + + if p["channel"] is not None: + channel = _try_eval(p["channel"], 'channel') + + if not isinstance(file, list): + file = [ file ] + + for fn in file: + if isinstance(fn, basestring): + try: + if not renpy.music.playable(fn, channel): + renpy.error("%r is not loadable" % fn) + except: + pass + +register('play music', + parse=parse_play_music, + execute=execute_play_music, + predict=predict_play_music, + lint=lint_play_music) + +# From here on, we'll steal bits of other statements when defining other +# statements. + +def parse_queue_music(l): + + file = l.simple_expression() + if not file: + renpy.error("queue requires a file") + + channel = None + loop = None + + while not l.eol(): + + if l.keyword('channel'): + channel = l.simple_expression() + if channel is None: + renpy.error('expected simple expression') + + if l.keyword('loop'): + loop = True + continue + + if l.keyword('noloop'): + loop = False + continue + + renpy.error('expected end of line') + + return dict(file=file, channel=channel, loop=loop) + +def execute_queue_music(p): + if p["channel"] is not None: + channel = eval(p["channel"]) + else: + channel = "music" + + renpy.music.queue( + eval(p["file"]), + channel=channel, + loop=p.get("loop", None)) + + +register('queue music', + parse=parse_queue_music, + execute=execute_queue_music, + lint=lint_play_music) + +def parse_stop_music(l): + fadeout = "None" + + if l.keyword("fadeout"): + fadeout = l.simple_expression() + + channel = None + + if l.keyword('channel'): + channel = l.simple_expression() + if channel is None: + renpy.error('expected simple expression') + + if not l.eol(): + renpy.error('expected end of line') + + if fadeout is None: + renpy.error('expected simple expression') + + return dict(fadeout=fadeout, channel=channel) + +def execute_stop_music(p): + if p["channel"] is not None: + channel = eval(p["channel"]) + else: + channel = "music" + + renpy.music.stop(fadeout=eval(p["fadeout"]), channel=channel) + +register('stop music', + parse=parse_stop_music, + execute=execute_stop_music) + + +# Sound statements. They share alot with the equivalent music +# statements. + +def execute_play_sound(p): + + if p["channel"] is not None: + channel = eval(p["channel"]) + else: + channel = "sound" + + fadeout = eval(p["fadeout"]) or 0 + + renpy.sound.play(eval(p["file"]), + fadeout=fadeout, + fadein=eval(p["fadein"]), + channel=channel) + +def lint_play_sound(p, lint_play_music=lint_play_music): + return lint_play_music(p, channel="sound") + +register('play sound', + parse=parse_play_music, + execute=execute_play_sound, + lint=lint_play_sound) + +def execute_queue_sound(p): + if p["channel"] is not None: + channel = eval(p["channel"]) + else: + channel = "sound" + + renpy.sound.queue(eval(p["file"]), channel=channel) + + +register('queue sound', + parse=parse_queue_music, + execute=execute_queue_sound, + lint=lint_play_music) + +def execute_stop_sound(p): + if p["channel"] is not None: + channel = eval(p["channel"]) + else: + channel = "sound" + + fadeout = eval(p["fadeout"]) or 0 + + renpy.sound.stop(fadeout=fadeout, channel=channel) + +register('stop sound', + parse=parse_stop_music, + execute=execute_stop_sound) + + +# Generic play/queue/stop statements. These take a channel name as +# the second thing. + +def parse_play_generic(l, parse_play_music=parse_play_music): + channel = l.name() + + if channel is None: + renpy.error('play requires a channel') + + rv = parse_play_music(l) + if rv["channel"] is None: + rv["channel"] = repr(channel) + + return rv + +def parse_queue_generic(l, parse_queue_music=parse_queue_music): + channel = l.name() + + if channel is None: + renpy.error('queue requires a channel') + + rv = parse_queue_music(l) + if rv["channel"] is None: + rv["channel"] = repr(channel) + + return rv + +def parse_stop_generic(l, parse_stop_music=parse_stop_music): + channel = l.name() + + if channel is None: + renpy.error('stop requires a channel') + + rv = parse_stop_music(l) + if rv["channel"] is None: + rv["channel"] = repr(channel) + + return rv + +def lint_play_generic(p, lint_play_music=lint_play_music): + channel = eval(p["channel"]) + + if not renpy.music.channel_defined(channel): + renpy.error("channel %r is not defined" % channel) + + lint_play_music(p, channel) + +def lint_stop_generic(p): + channel = eval(p["channel"]) + + if not renpy.music.channel_defined(channel): + renpy.error("channel %r is not defined" % channel) + +register('play', + parse=parse_play_generic, + execute=execute_play_music, + predict=predict_play_music, + lint=lint_play_generic) + +register('queue', + parse=parse_queue_generic, + execute=execute_queue_music, + lint=lint_play_generic) + +register('stop', + parse=parse_stop_generic, + execute=execute_stop_music, + lint=lint_stop_generic) + + +########################################################################## +# "window show" and "window hide" statements. + +def parse_window(l): + p = l.simple_expression() + if not l.eol(): + renpy.error('expected end of line') + + return p + +def lint_window(p): + if p is not None: + _try_eval(p, 'window transition') + +def execute_window_show(p): + if store._window: + return + + if p is not None: + trans = eval(p) + + renpy.with_statement(None) + store._window = True + renpy.with_statement(trans) + +def execute_window_hide(p): + if not _window: + return + + if p is not None: + trans = eval(p) + + renpy.with_statement(None) + store._window = False + renpy.with_statement(trans) + +register('window show', + parse=parse_window, + execute=execute_window_show, + lint=lint_window) + +register('window hide', + parse=parse_window, + execute=execute_window_hide, + lint=lint_window) + +########################################################################## +# Pause statement. + +def parse_pause(l): + + delay = l.simple_expression() + + if not l.eol(): + renpy.error("expected end of line.") + + return { "delay" : delay } + +def lint_pause(p): + + if p["delay"]: + _try_eval(p["delay"], 'pause statement') + +def execute_pause(p): + + if p["delay"]: + delay = eval(p["delay"]) + renpy.with_statement(Pause(delay)) + else: + renpy.pause() + + +register('pause', + parse=parse_pause, + lint=lint_pause, + execute=execute_pause) + + + +def _try_eval(e, what): + try: + return eval(e) + except: + renpy.error('unable to evaluate %s %r' % (what, e)) + +############################################################################## +# Screen-related statements. + +def parse_show_call_screen(l): + + # Parse a name. + name = l.require(l.name) + + # Parse the list of arguments. + arguments = renpy.parser.parse_arguments(l) + l.expect_eol() + + return dict(name=name, arguments=arguments) + +def parse_hide_screen(l): + name = l.require(l.name) + + l.expect_eol() + + return dict(name=name) + +def predict_screen(p): + if not p["arguments"]: + renpy.predict_screen(p["arguments"]) + +def execute_show_screen(p): + + name = p["name"] + a = p["arguments"] + + args = [ ] + kwargs = { } + + if a is not None: + + for k, v in a.arguments: + if k is not None: + kwargs[k] = eval(v) + else: + args.append(eval(v)) + + if a.extrapos is not None: + args.extend(eval(a.extrapos)) + + if a.extrakw is not None: + kwargs.update(eval(a.extrakw)) + + renpy.show_screen(name, *args, **kwargs) + +def execute_call_screen(p): + name = p["name"] + a = p["arguments"] + + args = [ ] + kwargs = { } + + if a is not None: + + for k, v in a.arguments: + if k is not None: + kwargs[k] = eval(v) + else: + args.append(eval(v)) + + if a.extrapos is not None: + args.extend(eval(a.extrapos)) + + if a.extrakw is not None: + kwargs.update(eval(a.extrakw)) + + store._return = renpy.call_screen(name, *args, **kwargs) + +def execute_hide_screen(p): + name = p["name"] + renpy.hide_screen(name) + +def lint_screen(p): + name = p["name"] + if not renpy.has_screen(name): + renpy.error("Screen %s does not exist." % name) + + +register("show screen", + parse=parse_show_call_screen, + execute=execute_show_screen, + predict=predict_screen, + lint=lint_screen) + +register("call screen", + parse=parse_show_call_screen, + execute=execute_call_screen, + predict=predict_screen, + lint=lint_screen) + +register("hide screen", + parse=parse_hide_screen, + execute=execute_hide_screen) + + + +def parse_jump_in(l): + is_expr = False + firstword = l.word() + if firstword == "expression": + label = l.simple_expression() + is_expr = True + else: + label = firstword + if not label: + renpy.error("parse error when evaluating custom jump") + return dict(label=label,is_expr=is_expr) + +def execute_jump_in(p): + global save_name, last_scene_label + if p["is_expr"]: + label = eval(p["label"]) + else: + label = p["label"] + last_scene_label = label + save_name = label + scene_register(label) + renpy.jump(label) + +def predict_jump_in(p): + return [] + +def next_jump_in(p): + return p["label"] + +def lint_jump_in(p): + label = p["label"] + if not label: + renpy.error("no target given to custom jump statement.") + if not renpy.has_label(label) and not p["is_expr"]: + renpy.error("custom jump to nonexistent label.") + +def execute_jump_out(p): + global playthroughflag, mycontext, ss_desc + nvl_clear() + if not playthroughflag: + replay_end() + execute_jump_in(p) + +register('jump_in', + parse=parse_jump_in, + execute=execute_jump_in, + predict=predict_jump_in, + lint=lint_jump_in, + next=next_jump_in) + +register('jump_out', + parse=parse_jump_in, + execute=execute_jump_out, + predict=predict_jump_in, + lint=lint_jump_in, + next=next_jump_in) +def parse_nvl_show_hide(l): + rv = l.simple_expression() + if rv is None: + renpy.error('expected simple expression') + + if not l.eol(): + renpy.error('expected end of line') + + return rv + +def lint_nvl_show_hide(trans): + _try_eval(trans, 'transition') + +def execute_nvl_show(trans): + nvl_show(eval(trans)) + +def execute_nvl_hide(trans): + nvl_hide(eval(trans)) + +register("nvl show", + parse=parse_nvl_show_hide, + execute=execute_nvl_show, + lint=lint_nvl_show_hide) + +register("nvl hide", + parse=parse_nvl_show_hide, + execute=execute_nvl_hide, + lint=lint_nvl_show_hide) + +def parse_nvl_clear(l): + if not l.eol(): + renpy.error('expected end of line') + + return None + +def execute_nvl_clear(parse): + nvl_clear() + +def scry_nvl_clear(parse, scry): + scry.nvl_clear = True + +register('nvl clear', + parse=parse_nvl_clear, + execute=execute_nvl_clear, + scry=scry_nvl_clear) + diff --git a/ast2json/rpyc2json.py b/ast2json/rpyc2json.py index 1bf9b73..b1dc592 100755 --- a/ast2json/rpyc2json.py +++ b/ast2json/rpyc2json.py @@ -42,6 +42,8 @@ import renpy.game renpy.game.script = Dummy() import renpy.ast import renpy.atl +import renpy.statements +import renpy.parser def pretty_print_ast(out_file, ast): json.dump(rast2json(ast), out_file, separators=(',', ':')) @@ -49,6 +51,8 @@ def pretty_print_ast(out_file, ast): def node2json(node): to_return = {} to_return['_type'] = node.__class__.__name__ + if isinstance(node, renpy.ast.UserStatement): + node.parsed = renpy.statements.parse(node, node.line) for attr in node.__slots__: to_return[attr] = get_value(getattr(node, attr)) @@ -64,6 +68,8 @@ def get_value(attr_value): return attr_value if isinstance(attr_value, list) or isinstance(attr_value, tuple): return [get_value(x) for x in attr_value] + if isinstance(attr_value, dict): + return attr_value if isinstance(attr_value, renpy.ast.Node): return node2json(attr_value) if isinstance(attr_value, renpy.ast.PyCode): -- cgit v1.2.3-54-g00ecf