2212 lines
61 KiB
Python
2212 lines
61 KiB
Python
# Copyright 2004-2019 Tom Rothamel <pytom@bishoujo.us>
|
|
#
|
|
# 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.
|
|
|
|
|
|
#########################################################################
|
|
# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
|
|
#
|
|
# When adding fields to a class in an __init__ method, we need to ensure that
|
|
# field is copied in the copy() method.
|
|
|
|
from __future__ import print_function
|
|
|
|
import ast
|
|
import collections
|
|
import linecache
|
|
from cPickle import loads, dumps
|
|
import zlib
|
|
import weakref
|
|
|
|
import renpy.display
|
|
import renpy.pyanalysis
|
|
import renpy.sl2
|
|
|
|
from renpy.display.transform import Transform, ATLTransform
|
|
from renpy.display.layout import Fixed
|
|
from renpy.display.predict import displayable as predict_displayable
|
|
|
|
from renpy.python import py_eval_bytecode
|
|
from renpy.pyanalysis import Analysis, NOT_CONST, LOCAL_CONST, GLOBAL_CONST, ccache
|
|
|
|
import hashlib
|
|
import time
|
|
|
|
# This file contains the abstract syntax tree for a screen language
|
|
# screen.
|
|
|
|
# A serial number that makes each SLNode unique.
|
|
serial = int(time.time() * 1000000)
|
|
|
|
# A sentinel used to indicate we should use the value found in the
|
|
# expression.
|
|
use_expression = renpy.object.Sentinel("use_expression")
|
|
|
|
# The filename that's currently being compiled.
|
|
filename = '<screen language>'
|
|
|
|
# A log that's used for profiling information.
|
|
profile_log = renpy.log.open("profile_screen", developer=True, append=False, flush=False)
|
|
|
|
|
|
def compile_expr(node):
|
|
"""
|
|
Wraps the node in a python AST, and compiles it.
|
|
"""
|
|
|
|
expr = ast.Expression(body=node)
|
|
ast.fix_missing_locations(expr)
|
|
return compile(expr, filename, "eval")
|
|
|
|
|
|
class SLContext(renpy.ui.Addable):
|
|
"""
|
|
A context object that can be passed to the execute methods, and can also
|
|
be placed in renpy.ui.stack.
|
|
"""
|
|
|
|
def __init__(self, parent=None):
|
|
if parent is not None:
|
|
self.__dict__.update(parent.__dict__)
|
|
return
|
|
|
|
# The local scope that python code is evaluated in.
|
|
self.scope = { }
|
|
|
|
# The global scope that python code is evaluated in.
|
|
self.globals = { }
|
|
|
|
# A list of child displayables that will be added to an outer
|
|
# displayable.
|
|
self.children = [ ]
|
|
|
|
# A map from keyword arguments to their values.
|
|
self.keywords = { }
|
|
|
|
# The style prefix that is given to children of this displayable.
|
|
self.style_prefix = None
|
|
|
|
# A cache associated with this context. The cache maps from
|
|
# statement serial to information associated with the statement.
|
|
self.new_cache = { }
|
|
|
|
# The old cache, used to take information from the old version of
|
|
# this displayable.
|
|
self.old_cache = { }
|
|
|
|
# The miss cache, used to take information that isn't present in
|
|
# old_cache.
|
|
self.miss_cache = { }
|
|
|
|
# The number of times a particular use statement has been called
|
|
# in the current screen. We use this to generate a unique name for
|
|
# each call site.
|
|
self.use_index = collections.defaultdict(int)
|
|
|
|
# When a constant node uses the scope, we add it to this list, so
|
|
# it may be reused. (If None, no list is used.)
|
|
self.uses_scope = None
|
|
|
|
# When a constant node has an id, we added it to this dict, so it
|
|
# may be reused. (If None, no dict is used.)
|
|
self.widgets = None
|
|
|
|
# True if we should dump debug information to the profile log.
|
|
self.debug = False
|
|
|
|
# True if we're predicting the screen.
|
|
self.predicting = False
|
|
|
|
# True if we're updating the screen.
|
|
self.updating = False
|
|
|
|
# A list of nodes we've predicted, for cases where predicting more than
|
|
# once could be a performance problem.
|
|
self.predicted = set()
|
|
|
|
# True if we're in a true showif block, False if we're in a false showif
|
|
# block, or None if we're not in a showif block.
|
|
self.showif = None
|
|
|
|
# True if there was a failure in this statement or any of its children.
|
|
# Fails can only occur when predicting, as otherwise an exception
|
|
# would be thrown.
|
|
self.fail = False
|
|
|
|
# The parent context of a use statement with a block.
|
|
self.parent = None
|
|
|
|
# The use statement containing the transcluded block.
|
|
self.transclude = None
|
|
|
|
# True if it's unlikely this node will run. This is used in prediction
|
|
# to speed things up.
|
|
self.unlikely = False
|
|
|
|
# The old and new generations of the use_cache.
|
|
self.new_use_cache = { }
|
|
self.old_use_cache = { }
|
|
|
|
def add(self, d, key):
|
|
self.children.append(d)
|
|
|
|
def close(self, d):
|
|
raise Exception("Spurious ui.close().")
|
|
|
|
|
|
class SLNode(object):
|
|
"""
|
|
The base class for screen language nodes.
|
|
"""
|
|
|
|
# The type of constant this node is.
|
|
constant = GLOBAL_CONST
|
|
|
|
# True if this node has at least one keyword that applies to its
|
|
# parent. False otherwise.
|
|
has_keyword = False
|
|
|
|
# True if this node should be the last keyword parsed.
|
|
last_keyword = False
|
|
|
|
def __init__(self, loc):
|
|
global serial
|
|
serial += 1
|
|
|
|
# A unique serial number assigned to this node.
|
|
self.serial = serial
|
|
|
|
# The location of this node, a (file, line) tuple.
|
|
self.location = loc
|
|
|
|
def instantiate(self, transclude):
|
|
"""
|
|
Instantiates a new instance of this class, copying the global
|
|
attributes of this class onto the new instance.
|
|
"""
|
|
|
|
cls = type(self)
|
|
|
|
rv = cls.__new__(cls)
|
|
rv.serial = self.serial
|
|
rv.location = self.location
|
|
|
|
return rv
|
|
|
|
def copy(self, transclude):
|
|
"""
|
|
Makes a copy of this node.
|
|
|
|
`transclude`
|
|
The constness of transclude statements.
|
|
"""
|
|
|
|
raise Exception("copy not implemented by " + type(self).__name__)
|
|
|
|
def report_traceback(self, name, last):
|
|
if last:
|
|
return None
|
|
|
|
filename, line = self.location
|
|
|
|
return [ (filename, line, name, None) ]
|
|
|
|
def analyze(self, analysis):
|
|
"""
|
|
Performs static analysis on Python code used in this statement.
|
|
"""
|
|
|
|
# By default, does nothing.
|
|
|
|
def prepare(self, analysis):
|
|
"""
|
|
This should be called before the execute code is called, and again
|
|
after init-level code (like the code in a .rpym module or an init
|
|
python block) is called.
|
|
|
|
`analysis`
|
|
A pyanalysis.Analysis object containing the analysis of this screen.
|
|
"""
|
|
|
|
# By default, does nothing.
|
|
|
|
def execute(self, context):
|
|
"""
|
|
Execute this node, updating context as appropriate.
|
|
"""
|
|
|
|
raise Exception("execute not implemented by " + type(self).__name__)
|
|
|
|
def keywords(self, context):
|
|
"""
|
|
Execute this node, updating context.keywords as appropriate.
|
|
"""
|
|
|
|
# By default, does nothing.
|
|
return
|
|
|
|
def copy_on_change(self, cache):
|
|
"""
|
|
Flags the displayables that are created by this node and its children
|
|
as copy-on-change.
|
|
"""
|
|
|
|
return
|
|
|
|
def debug_line(self):
|
|
"""
|
|
Writes information about the line we're on to the debug log.
|
|
"""
|
|
|
|
filename, lineno = self.location
|
|
full_filename = renpy.exports.unelide_filename(filename)
|
|
|
|
line = linecache.getline(full_filename, lineno) or ""
|
|
line = line.decode("utf-8")
|
|
|
|
profile_log.write(" %s:%d %s", filename, lineno, line.rstrip())
|
|
|
|
if self.constant:
|
|
profile_log.write(" potentially constant")
|
|
|
|
def used_screens(self, callback):
|
|
"""
|
|
Calls callback with the name of each screen this node and its
|
|
children use.
|
|
"""
|
|
|
|
return
|
|
|
|
def has_transclude(self):
|
|
"""
|
|
Returns true if this node is a transclude or has a transclude as a child.
|
|
"""
|
|
|
|
return False
|
|
|
|
def has_python(self):
|
|
"""
|
|
Returns true if this node is Python or has a python node as a child.
|
|
"""
|
|
|
|
return False
|
|
|
|
|
|
# A sentinel used to indicate a keyword argument was not given.
|
|
NotGiven = renpy.object.Sentinel("NotGiven")
|
|
|
|
|
|
class SLBlock(SLNode):
|
|
"""
|
|
Represents a screen language block that can contain keyword arguments
|
|
and child displayables.
|
|
"""
|
|
|
|
# RawBlock from parse or None if not present.
|
|
atl_transform = None
|
|
|
|
def __init__(self, loc):
|
|
SLNode.__init__(self, loc)
|
|
|
|
# A list of keyword argument, expr tuples.
|
|
self.keyword = [ ]
|
|
|
|
# A list of child SLNodes.
|
|
self.children = [ ]
|
|
|
|
def instantiate(self, transclude):
|
|
rv = SLNode.instantiate(self, transclude)
|
|
rv.keyword = self.keyword
|
|
rv.children = [ i.copy(transclude) for i in self.children ]
|
|
|
|
return rv
|
|
|
|
def copy(self, transclude):
|
|
return self.instantiate(transclude)
|
|
|
|
def analyze(self, analysis):
|
|
|
|
for i in self.children:
|
|
i.analyze(analysis)
|
|
|
|
def prepare(self, analysis):
|
|
|
|
for i in self.children:
|
|
i.prepare(analysis)
|
|
self.constant = min(self.constant, i.constant)
|
|
|
|
# Compile the keywords.
|
|
|
|
keyword_values = { }
|
|
keyword_keys = [ ]
|
|
keyword_exprs = [ ]
|
|
|
|
for k, expr in self.keyword:
|
|
|
|
node = ccache.ast_eval(expr)
|
|
|
|
const = analysis.is_constant(node)
|
|
|
|
if const == GLOBAL_CONST:
|
|
keyword_values[k] = py_eval_bytecode(compile_expr(node))
|
|
else:
|
|
keyword_keys.append(ast.Str(s=k))
|
|
keyword_exprs.append(node) # Will be compiled as part of ast.Dict below.
|
|
|
|
self.constant = min(self.constant, const)
|
|
|
|
if keyword_values:
|
|
self.keyword_values = keyword_values
|
|
else:
|
|
self.keyword_values = None
|
|
|
|
if keyword_keys:
|
|
node = ast.Dict(keys=keyword_keys, values=keyword_exprs)
|
|
ast.copy_location(node, keyword_exprs[0])
|
|
self.keyword_exprs = compile_expr(node)
|
|
else:
|
|
self.keyword_exprs = None
|
|
|
|
self.has_keyword = bool(self.keyword)
|
|
self.keyword_children = [ ]
|
|
|
|
if self.atl_transform is not None:
|
|
self.has_keyword = True
|
|
|
|
self.atl_transform.mark_constant()
|
|
const = self.atl_transform.constant
|
|
self.constant = min(self.constant, const)
|
|
|
|
for i in self.children:
|
|
if i.has_keyword:
|
|
self.keyword_children.append(i)
|
|
self.has_keyword = True
|
|
|
|
if i.last_keyword:
|
|
self.last_keyword = True
|
|
break
|
|
|
|
def execute(self, context):
|
|
|
|
# Note: SLBlock.execute() is inlined in various locations for performance
|
|
# reasons.
|
|
|
|
for i in self.children:
|
|
|
|
try:
|
|
i.execute(context)
|
|
except:
|
|
if not context.predicting:
|
|
raise
|
|
|
|
def keywords(self, context):
|
|
|
|
keyword_values = self.keyword_values
|
|
|
|
if keyword_values is not None:
|
|
context.keywords.update(keyword_values)
|
|
|
|
keyword_exprs = self.keyword_exprs
|
|
|
|
if keyword_exprs is not None:
|
|
context.keywords.update(eval(keyword_exprs, context.globals, context.scope))
|
|
|
|
for i in self.keyword_children:
|
|
i.keywords(context)
|
|
|
|
if self.atl_transform is not None:
|
|
transform = ATLTransform(self.atl_transform, context=context.scope)
|
|
context.keywords["at"] = transform
|
|
|
|
style_prefix = context.keywords.pop("style_prefix", NotGiven)
|
|
|
|
if style_prefix is NotGiven:
|
|
style_prefix = context.keywords.pop("style_group", NotGiven)
|
|
|
|
if style_prefix is not NotGiven:
|
|
context.style_prefix = style_prefix
|
|
|
|
def copy_on_change(self, cache):
|
|
for i in self.children:
|
|
i.copy_on_change(cache)
|
|
|
|
def used_screens(self, callback):
|
|
for i in self.children:
|
|
i.used_screens(callback)
|
|
|
|
def has_transclude(self):
|
|
for i in self.children:
|
|
if i.has_transclude():
|
|
return True
|
|
|
|
return False
|
|
|
|
def has_python(self):
|
|
return any(i.has_python() for i in self.children)
|
|
|
|
def has_noncondition_child(self):
|
|
"""
|
|
Returns true if this block has a child that is not an SLIf statement,
|
|
or false otherwise.
|
|
"""
|
|
|
|
worklist = list(self.children)
|
|
|
|
while worklist:
|
|
|
|
n = worklist.pop(0)
|
|
|
|
if type(n) is SLBlock:
|
|
worklist.extend(n.children)
|
|
elif isinstance(n, SLIf):
|
|
for _, block in n.entries:
|
|
worklist.append(block)
|
|
else:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
list_or_tuple = (list, tuple)
|
|
|
|
|
|
class SLCache(object):
|
|
"""
|
|
The type of cache associated with an SLDisplayable.
|
|
"""
|
|
|
|
def __init__(self):
|
|
|
|
# The displayable object created.
|
|
self.displayable = None
|
|
|
|
# The positional arguments that were used to create the displayable.
|
|
self.positional = None
|
|
|
|
# The keyword arguments that were used to created the displayable.
|
|
self.keywords = None
|
|
|
|
# A list of the children that were added to self.displayable.
|
|
self.children = None
|
|
|
|
# The old transform created.
|
|
self.transform = None
|
|
|
|
# The transform that was used to create self.transform.
|
|
self.raw_transform = None
|
|
|
|
# The imagemap stack entry we reuse.
|
|
self.imagemap = None
|
|
|
|
# If this can be represented as a single constant displayable,
|
|
# do so.
|
|
self.constant = None
|
|
|
|
# For a constant statement, a list of our children that use
|
|
# the scope.
|
|
self.constant_uses_scope = [ ]
|
|
|
|
# For a constant statement, a map from children to widgets.
|
|
self.constant_widgets = { }
|
|
|
|
# True if the displayable should be re-created if its arguments
|
|
# or children are changed.
|
|
self.copy_on_change = False
|
|
|
|
# The ShowIf this statement was wrapped in the last time it was wrapped.
|
|
self.old_showif = None
|
|
|
|
# The SLUse that was transcluded by this SLCache statement.
|
|
self.transclude = None
|
|
|
|
# The style prefix used when this statement was first created.
|
|
self.style_prefix = None
|
|
|
|
|
|
# A magic value that, if returned by a displayable function, is not added to
|
|
# the parent.
|
|
NO_DISPLAYABLE = renpy.display.layout.Null()
|
|
|
|
|
|
class SLDisplayable(SLBlock):
|
|
"""
|
|
A screen language AST node that corresponds to a displayable being
|
|
added to the tree.
|
|
"""
|
|
|
|
hotspot = False
|
|
variable = None
|
|
|
|
# A list of variables that are locally constant.
|
|
local_constant = [ ]
|
|
|
|
def __init__(self, loc, displayable, scope=False, child_or_fixed=False, style=None, text_style=None, pass_context=False, imagemap=False, replaces=False, default_keywords={}, hotspot=False, variable=None):
|
|
"""
|
|
`displayable`
|
|
A function that, when called with the positional and keyword
|
|
arguments, causes the displayable to be displayed.
|
|
|
|
`scope`
|
|
If true, the scope is supplied as an argument to the displayable.
|
|
|
|
`child_or_fixed`
|
|
If true and the number of children of this displayable is not one,
|
|
the children are added to a Fixed, and the Fixed is added to the
|
|
displayable.
|
|
|
|
`style`
|
|
The base name of the main style.
|
|
|
|
`pass_context`
|
|
If given, the context is passed in as the first positonal argument
|
|
of the displayable.
|
|
|
|
`imagemap`
|
|
True if this is an imagemap, and should be handled as one.
|
|
|
|
`hotspot`
|
|
True if this is a hotspot that depends on the imagemap it was
|
|
first displayed with.
|
|
|
|
`replaces`
|
|
True if the object this displayable replaces should be
|
|
passed to it.
|
|
|
|
`default_keywords`
|
|
The default keyword arguments to supply to the displayable.
|
|
|
|
`variable`
|
|
A variable that the main displayable is assigned to.
|
|
"""
|
|
|
|
SLBlock.__init__(self, loc)
|
|
|
|
self.displayable = displayable
|
|
|
|
self.scope = scope
|
|
self.child_or_fixed = child_or_fixed
|
|
self.style = style
|
|
self.pass_context = pass_context
|
|
self.imagemap = imagemap
|
|
self.hotspot = hotspot
|
|
self.replaces = replaces
|
|
self.default_keywords = default_keywords
|
|
self.variable = variable
|
|
|
|
# Positional argument expressions.
|
|
self.positional = [ ]
|
|
|
|
def copy(self, transclude):
|
|
rv = self.instantiate(transclude)
|
|
|
|
rv.displayable = self.displayable
|
|
rv.scope = self.scope
|
|
rv.child_or_fixed = self.child_or_fixed
|
|
rv.style = self.style
|
|
rv.pass_context = self.pass_context
|
|
rv.imagemap = self.imagemap
|
|
rv.hotspot = self.hotspot
|
|
rv.replaces = self.replaces
|
|
rv.default_keywords = self.default_keywords
|
|
rv.variable = self.variable
|
|
rv.positional = self.positional
|
|
|
|
return rv
|
|
|
|
def analyze(self, analysis):
|
|
|
|
if self.imagemap:
|
|
|
|
const = GLOBAL_CONST
|
|
|
|
for _k, expr in self.keyword:
|
|
const = min(const, analysis.is_constant_expr(expr))
|
|
|
|
analysis.push_control(imagemap=(const != GLOBAL_CONST))
|
|
|
|
if self.hotspot:
|
|
self.constant = min(analysis.imagemap(), self.constant)
|
|
|
|
SLBlock.analyze(self, analysis)
|
|
|
|
if self.imagemap:
|
|
analysis.pop_control()
|
|
|
|
# If we use a scope, store the local constants that need to be
|
|
# kept and placed into the scope.
|
|
if self.scope:
|
|
self.local_constant = list(analysis.local_constant)
|
|
|
|
if self.variable is not None:
|
|
const = self.constant
|
|
|
|
for i in self.positional:
|
|
const = min(self.constant, analysis.is_constant_expr(i))
|
|
|
|
for k, v in self.keyword:
|
|
const = min(self.constant, analysis.is_constant_expr(v))
|
|
|
|
if k == "id":
|
|
const = NOT_CONST
|
|
|
|
if const == LOCAL_CONST:
|
|
analysis.mark_constant(self.variable)
|
|
elif const == NOT_CONST:
|
|
analysis.mark_not_constant(self.variable)
|
|
|
|
def prepare(self, analysis):
|
|
|
|
SLBlock.prepare(self, analysis)
|
|
|
|
# Prepare the positional arguments.
|
|
|
|
exprs = [ ]
|
|
values = [ ]
|
|
has_exprs = False
|
|
has_values = False
|
|
|
|
for a in self.positional:
|
|
node = ccache.ast_eval(a)
|
|
|
|
const = analysis.is_constant(node)
|
|
|
|
if const == GLOBAL_CONST:
|
|
values.append(py_eval_bytecode(compile_expr(node)))
|
|
exprs.append(ast.Num(n=0))
|
|
has_values = True
|
|
else:
|
|
values.append(use_expression)
|
|
exprs.append(node) # Will be compiled as part of the tuple.
|
|
has_exprs = True
|
|
|
|
self.constant = min(self.constant, const)
|
|
|
|
if has_values:
|
|
self.positional_values = values
|
|
else:
|
|
self.positional_values = None
|
|
|
|
if has_exprs:
|
|
t = ast.Tuple(elts=exprs, ctx=ast.Load())
|
|
ast.copy_location(t, exprs[0])
|
|
self.positional_exprs = compile_expr(t)
|
|
else:
|
|
self.positional_exprs = None
|
|
|
|
# We do not pass keywords to our parents.
|
|
self.has_keyword = False
|
|
|
|
# We want to preserve last_keyword, however, in case we run a
|
|
# python block.
|
|
|
|
# If we have the id property, we're not constant - since we may get
|
|
# additional keywords via id. (It's unlikely, but id should be pretty
|
|
# rare.)
|
|
for k, _expr in self.keyword:
|
|
if k == "id":
|
|
self.constant = NOT_CONST
|
|
|
|
if self.variable is not None:
|
|
self.constant = NOT_CONST
|
|
|
|
def keywords(self, context):
|
|
# We do not want to pass keywords to our parents, so just return.
|
|
return
|
|
|
|
def execute(self, context):
|
|
|
|
debug = context.debug
|
|
|
|
screen = renpy.ui.screen
|
|
|
|
cache = context.old_cache.get(self.serial, None) or context.miss_cache.get(self.serial, None)
|
|
|
|
if not isinstance(cache, SLCache):
|
|
cache = SLCache()
|
|
|
|
context.new_cache[self.serial] = cache
|
|
|
|
copy_on_change = cache.copy_on_change
|
|
|
|
if debug:
|
|
self.debug_line()
|
|
|
|
if cache.constant and (cache.style_prefix == context.style_prefix):
|
|
|
|
for i, local_scope in cache.constant_uses_scope:
|
|
|
|
if local_scope:
|
|
scope = dict(context.scope)
|
|
scope.update(local_scope)
|
|
else:
|
|
scope = context.scope
|
|
|
|
if copy_on_change:
|
|
if i._scope(scope, False):
|
|
cache.constant = None
|
|
break
|
|
else:
|
|
i._scope(scope, True)
|
|
|
|
else:
|
|
|
|
d = cache.constant
|
|
|
|
if d is not NO_DISPLAYABLE:
|
|
if context.showif is not None:
|
|
d = self.wrap_in_showif(d, context, cache)
|
|
|
|
context.children.append(d)
|
|
|
|
if context.uses_scope is not None:
|
|
context.uses_scope.extend(cache.constant_uses_scope)
|
|
|
|
if debug:
|
|
profile_log.write(" reused constant displayable")
|
|
|
|
return
|
|
|
|
# Create the context.
|
|
ctx = SLContext(context)
|
|
|
|
# True if we encountered an exception that we're recovering from
|
|
# due to being in prediction mode.
|
|
fail = False
|
|
|
|
# The main displayable we're predicting.
|
|
main = None
|
|
|
|
# True if we've pushed something onto the imagemap stack.
|
|
imagemap = False
|
|
|
|
try:
|
|
# Evaluate the positional arguments.
|
|
positional_values = self.positional_values
|
|
positional_exprs = self.positional_exprs
|
|
|
|
if positional_values and positional_exprs:
|
|
values = eval(positional_exprs, context.globals, context.scope)
|
|
positional = [ b if (a is use_expression) else a for a, b in zip(positional_values, values) ]
|
|
elif positional_values:
|
|
positional = positional_values
|
|
elif positional_exprs:
|
|
positional = eval(positional_exprs, context.globals, context.scope)
|
|
else:
|
|
positional = [ ]
|
|
|
|
keywords = ctx.keywords = self.default_keywords.copy()
|
|
|
|
if self.constant:
|
|
ctx.uses_scope = [ ]
|
|
|
|
SLBlock.keywords(self, ctx)
|
|
|
|
# Get the widget id and transform, if any.
|
|
widget_id = keywords.pop("id", None)
|
|
transform = keywords.pop("at", None)
|
|
|
|
arguments = keywords.pop("arguments", None)
|
|
properties = keywords.pop("properties", None)
|
|
style_suffix = keywords.pop("style_suffix", None) or self.style
|
|
|
|
if arguments:
|
|
positional += arguments
|
|
|
|
if properties:
|
|
keywords.update(properties)
|
|
|
|
# If we don't know the style, figure it out.
|
|
if ("style" not in keywords) and style_suffix:
|
|
if ctx.style_prefix is None:
|
|
keywords["style"] = style_suffix
|
|
else:
|
|
keywords["style"] = ctx.style_prefix + "_" + style_suffix
|
|
|
|
if widget_id and (widget_id in screen.widget_properties):
|
|
keywords.update(screen.widget_properties[widget_id])
|
|
|
|
old_d = cache.displayable
|
|
if old_d:
|
|
old_main = old_d._main or old_d
|
|
else:
|
|
old_main = None
|
|
|
|
reused = False
|
|
|
|
if debug:
|
|
self.report_arguments(cache, positional, keywords, transform)
|
|
|
|
can_reuse = (old_d is not None) and (positional == cache.positional) and (keywords == cache.keywords) and (context.style_prefix == cache.style_prefix)
|
|
if (self.variable is not None) and copy_on_change:
|
|
can_reuse = False
|
|
|
|
# A hotspot can only be reused if the imagemap it belongs to has
|
|
# not changed.
|
|
if self.hotspot:
|
|
|
|
imc = renpy.ui.imagemap_stack[-1]
|
|
if cache.imagemap is not imc:
|
|
can_reuse = False
|
|
|
|
cache.imagemap = imc
|
|
|
|
if can_reuse:
|
|
reused = True
|
|
d = old_d
|
|
|
|
# The main displayable, if d is a composite displayable. (This is
|
|
# the one that gets the scope, and gets children added to it.)
|
|
main = old_main
|
|
|
|
if widget_id and not ctx.unlikely:
|
|
screen.widgets[widget_id] = main
|
|
|
|
if self.scope and main._uses_scope:
|
|
if copy_on_change:
|
|
if main._scope(ctx.scope, False):
|
|
reused = False
|
|
else:
|
|
main._scope(ctx.scope, True)
|
|
|
|
if reused and self.imagemap:
|
|
imagemap = True
|
|
cache.imagemap.reuse()
|
|
renpy.ui.imagemap_stack.append(cache.imagemap)
|
|
|
|
if not reused:
|
|
cache.positional = positional
|
|
cache.keywords = keywords.copy()
|
|
|
|
# This child creation code is copied below, for the copy_on_change
|
|
# case.
|
|
if self.scope:
|
|
keywords["scope"] = ctx.scope
|
|
|
|
if self.replaces and ctx.updating:
|
|
keywords['replaces'] = old_main
|
|
|
|
# Pass the context
|
|
if self.pass_context:
|
|
keywords['context'] = ctx
|
|
|
|
d = self.displayable(*positional, **keywords)
|
|
main = d._main or d
|
|
|
|
main._location = self.location
|
|
|
|
if widget_id and not ctx.unlikely:
|
|
screen.widgets[widget_id] = main
|
|
# End child creation code.
|
|
|
|
imagemap = self.imagemap
|
|
|
|
cache.copy_on_change = False # We no longer need to copy on change.
|
|
cache.children = None # Re-add the children.
|
|
|
|
if debug:
|
|
if reused:
|
|
profile_log.write(" reused displayable")
|
|
elif self.constant:
|
|
profile_log.write(" created constant displayable")
|
|
else:
|
|
profile_log.write(" created displayable")
|
|
|
|
except:
|
|
if not context.predicting:
|
|
raise
|
|
fail = True
|
|
|
|
if self.variable is not None:
|
|
context.scope[self.variable] = main
|
|
|
|
ctx.children = [ ]
|
|
ctx.showif = None
|
|
|
|
stack = renpy.ui.stack
|
|
stack.append(ctx)
|
|
|
|
try:
|
|
|
|
# Evaluate children. (Inlined SLBlock.execute)
|
|
for i in self.children:
|
|
try:
|
|
i.execute(ctx)
|
|
except:
|
|
if not context.predicting:
|
|
raise
|
|
fail = True
|
|
|
|
finally:
|
|
|
|
ctx.keywords = None
|
|
|
|
stack.pop()
|
|
|
|
if imagemap:
|
|
cache.imagemap = renpy.ui.imagemap_stack.pop()
|
|
cache.imagemap.cache.finish()
|
|
|
|
# If a failure occurred during prediction, predict main (if known),
|
|
# and ctx.children, and return.
|
|
if fail:
|
|
predict_displayable(main)
|
|
|
|
for i in ctx.children:
|
|
predict_displayable(i)
|
|
|
|
context.fail = True
|
|
|
|
return
|
|
|
|
if ctx.children != cache.children:
|
|
|
|
if reused and copy_on_change:
|
|
|
|
# This is a copy of the child creation code from above.
|
|
if self.scope:
|
|
keywords["scope"] = ctx.scope
|
|
|
|
if self.replaces and context.updating:
|
|
keywords['replaces'] = old_main
|
|
|
|
if self.pass_context:
|
|
keywords['context'] = ctx
|
|
|
|
d = self.displayable(*positional, **keywords)
|
|
main = d._main or d
|
|
|
|
main._location = self.location
|
|
|
|
if widget_id:
|
|
screen.widgets[widget_id] = main
|
|
# End child creation code.
|
|
|
|
cache.copy_on_change = False
|
|
reused = False
|
|
|
|
if reused:
|
|
main._clear()
|
|
|
|
if self.child_or_fixed and len(ctx.children) != 1:
|
|
f = Fixed()
|
|
|
|
for i in ctx.children:
|
|
f.add(i)
|
|
|
|
main.add(f)
|
|
|
|
else:
|
|
for i in ctx.children:
|
|
main.add(i)
|
|
|
|
# Inform the focus system about replacement displayables.
|
|
if (not context.predicting) and (old_d is not None):
|
|
replaced_by = renpy.display.focus.replaced_by
|
|
replaced_by[id(old_d)] = d
|
|
|
|
if d is not main:
|
|
for old_part, new_part in zip(old_d._composite_parts, d._composite_parts):
|
|
replaced_by[id(old_part)] = new_part
|
|
|
|
cache.displayable = d
|
|
cache.children = ctx.children
|
|
cache.style_prefix = context.style_prefix
|
|
|
|
if (transform is not None) and (d is not NO_DISPLAYABLE):
|
|
if reused and (transform == cache.raw_transform):
|
|
|
|
if isinstance(cache.transform, renpy.display.transform.Transform):
|
|
if cache.transform.child is not d:
|
|
cache.transform.set_child(d, duplicate=False)
|
|
|
|
d = cache.transform
|
|
else:
|
|
cache.raw_transform = transform
|
|
|
|
if isinstance(transform, Transform):
|
|
d = transform(child=d)
|
|
d._unique()
|
|
|
|
elif isinstance(transform, list_or_tuple):
|
|
for t in transform:
|
|
if isinstance(t, Transform):
|
|
d = t(child=d)
|
|
else:
|
|
d = t(d)
|
|
|
|
d._unique()
|
|
|
|
else:
|
|
d = transform(d)
|
|
d._unique()
|
|
|
|
if isinstance(d, Transform):
|
|
old_transform = cache.transform
|
|
|
|
if not context.updating:
|
|
old_transform = None
|
|
|
|
d.take_state(old_transform)
|
|
d.take_execution_state(old_transform)
|
|
|
|
cache.transform = d
|
|
|
|
else:
|
|
cache.transform = None
|
|
cache.raw_transform = None
|
|
|
|
if ctx.fail:
|
|
context.fail = True
|
|
|
|
else:
|
|
if self.constant:
|
|
cache.constant = d
|
|
|
|
if self.scope and main._uses_scope:
|
|
|
|
local_scope = { }
|
|
|
|
for i in self.local_constant:
|
|
if i in ctx.scope:
|
|
local_scope[i] = ctx.scope[i]
|
|
|
|
ctx.uses_scope.append((main, local_scope))
|
|
|
|
cache.constant_uses_scope = ctx.uses_scope
|
|
|
|
if context.uses_scope is not None:
|
|
context.uses_scope.extend(ctx.uses_scope)
|
|
|
|
if d is not NO_DISPLAYABLE:
|
|
|
|
if context.showif is not None:
|
|
d = self.wrap_in_showif(d, context, cache)
|
|
|
|
context.children.append(d)
|
|
|
|
def wrap_in_showif(self, d, context, cache):
|
|
"""
|
|
Wraps `d` in a ShowIf displayable.
|
|
"""
|
|
|
|
rv = renpy.sl2.sldisplayables.ShowIf(context.showif, cache.old_showif)
|
|
rv.add(d)
|
|
|
|
if not context.predicting:
|
|
cache.old_showif = rv
|
|
|
|
return rv
|
|
|
|
def report_arguments(self, cache, positional, keywords, transform):
|
|
if positional:
|
|
report = [ ]
|
|
|
|
values = self.positional_values or ([ use_expression ] * len(positional))
|
|
|
|
for i in range(len(positional)):
|
|
|
|
if values[i] is not use_expression:
|
|
report.append("const")
|
|
elif cache.positional is None:
|
|
report.append("new")
|
|
elif cache.positional[i] == positional[i]:
|
|
report.append("equal")
|
|
else:
|
|
report.append("not-equal")
|
|
|
|
profile_log.write(" args: %s", " ".join(report))
|
|
|
|
values = self.keyword_values or { }
|
|
|
|
if keywords:
|
|
report = { }
|
|
|
|
if cache.keywords is None:
|
|
for k in keywords:
|
|
|
|
if k in values:
|
|
report[k] = "const"
|
|
continue
|
|
|
|
report[k] = "new"
|
|
|
|
else:
|
|
for k in keywords:
|
|
k = str(k)
|
|
|
|
if k in values:
|
|
report[k] = "const"
|
|
continue
|
|
|
|
if k not in cache.keywords:
|
|
report[k] = "new-only"
|
|
continue
|
|
|
|
if keywords[k] == cache.keywords[k]:
|
|
report[k] = "equal"
|
|
else:
|
|
report[k] = "not-equal"
|
|
|
|
for k in cache.keywords:
|
|
if k not in keywords:
|
|
report[k] = "old-only"
|
|
|
|
profile_log.write(" kwargs: %r", report)
|
|
|
|
if transform is not None:
|
|
if "at" in values:
|
|
profile_log.write(" at: const")
|
|
elif cache.raw_transform is None:
|
|
profile_log.write(" at: new")
|
|
elif cache.raw_transform == transform:
|
|
profile_log.write(" at: equal")
|
|
else:
|
|
profile_log.write(" at: not-equal")
|
|
|
|
def copy_on_change(self, cache):
|
|
c = cache.get(self.serial, None)
|
|
|
|
if isinstance(c, SLCache):
|
|
c.copy_on_change = True
|
|
|
|
for i in self.children:
|
|
i.copy_on_change(cache)
|
|
|
|
|
|
class SLIf(SLNode):
|
|
"""
|
|
A screen language AST node that corresponds to an If/Elif/Else statement.
|
|
"""
|
|
|
|
def __init__(self, loc):
|
|
"""
|
|
An AST node that represents an if statement.
|
|
"""
|
|
SLNode.__init__(self, loc)
|
|
|
|
# A list of entries, with each consisting of an expression (or
|
|
# None, for the else block) and a SLBlock.
|
|
self.entries = [ ]
|
|
|
|
def copy(self, transclude):
|
|
rv = self.instantiate(transclude)
|
|
|
|
rv.entries = [ (expr, block.copy(transclude)) for expr, block in self.entries ]
|
|
|
|
return rv
|
|
|
|
def analyze(self, analysis):
|
|
|
|
const = GLOBAL_CONST
|
|
|
|
for cond, _block in self.entries:
|
|
if cond is not None:
|
|
const = min(const, analysis.is_constant_expr(cond))
|
|
|
|
analysis.push_control(const)
|
|
|
|
for _cond, block in self.entries:
|
|
block.analyze(analysis)
|
|
|
|
analysis.pop_control()
|
|
|
|
def prepare(self, analysis):
|
|
|
|
# A list of prepared entries, with each consisting of expression
|
|
# bytecode and a SLBlock.
|
|
self.prepared_entries = [ ]
|
|
|
|
for cond, block in self.entries:
|
|
if cond is not None:
|
|
node = ccache.ast_eval(cond)
|
|
|
|
self.constant = min(self.constant, analysis.is_constant(node))
|
|
|
|
cond = compile_expr(node)
|
|
|
|
block.prepare(analysis)
|
|
self.constant = min(self.constant, block.constant)
|
|
self.prepared_entries.append((cond, block))
|
|
|
|
self.has_keyword = self.has_keyword or block.has_keyword
|
|
self.last_keyword = self.last_keyword or block.last_keyword
|
|
|
|
def execute(self, context):
|
|
|
|
if context.predicting:
|
|
self.execute_predicting(context)
|
|
return
|
|
|
|
for cond, block in self.prepared_entries:
|
|
if cond is None or eval(cond, context.globals, context.scope):
|
|
for i in block.children:
|
|
i.execute(context)
|
|
return
|
|
|
|
def execute_predicting(self, context):
|
|
# A variant of the this code that runs while predicting, executing
|
|
# all paths of the if.
|
|
|
|
# True if no block has been the main choice yet.
|
|
first = True
|
|
|
|
# Has any instance of this node been predicted? We only predict
|
|
# once per node, for performance reasons.
|
|
predicted = self.serial in context.predicted
|
|
|
|
if not predicted:
|
|
context.predicted.add(self.serial)
|
|
|
|
for cond, block in self.prepared_entries:
|
|
try:
|
|
cond_value = (cond is None) or eval(cond, context.globals, context.scope)
|
|
except:
|
|
cond_value = False
|
|
|
|
# The taken branch.
|
|
if first and cond_value:
|
|
first = False
|
|
|
|
for i in block.children:
|
|
try:
|
|
i.execute(context)
|
|
except:
|
|
pass
|
|
|
|
# Not-taken branches, only if not already predicted.
|
|
elif not predicted:
|
|
|
|
ctx = SLContext(context)
|
|
ctx.children = [ ]
|
|
ctx.unlikely = True
|
|
|
|
for i in block.children:
|
|
try:
|
|
i.execute(ctx)
|
|
except:
|
|
pass
|
|
|
|
for i in ctx.children:
|
|
predict_displayable(i)
|
|
|
|
def keywords(self, context):
|
|
|
|
for cond, block in self.prepared_entries:
|
|
if cond is None or eval(cond, context.globals, context.scope):
|
|
block.keywords(context)
|
|
return
|
|
|
|
def copy_on_change(self, cache):
|
|
for _cond, block in self.entries:
|
|
block.copy_on_change(cache)
|
|
|
|
def used_screens(self, callback):
|
|
for _cond, block in self.entries:
|
|
block.used_screens(callback)
|
|
|
|
def has_transclude(self):
|
|
for _cond, block in self.entries:
|
|
if block.has_transclude():
|
|
return True
|
|
|
|
return False
|
|
|
|
def has_python(self):
|
|
return any(i[1].has_python() for i in self.entries)
|
|
|
|
|
|
class SLShowIf(SLNode):
|
|
"""
|
|
The AST node that corresponds to the showif statement.
|
|
"""
|
|
|
|
def __init__(self, loc):
|
|
"""
|
|
An AST node that represents an if statement.
|
|
"""
|
|
SLNode.__init__(self, loc)
|
|
|
|
# A list of entries, with each consisting of an expression (or
|
|
# None, for the else block) and a SLBlock.
|
|
self.entries = [ ]
|
|
|
|
def copy(self, transclude):
|
|
rv = self.instantiate(transclude)
|
|
|
|
rv.entries = [ (expr, block.copy(transclude)) for expr, block in self.entries ]
|
|
|
|
return rv
|
|
|
|
def analyze(self, analysis):
|
|
|
|
for _cond, block in self.entries:
|
|
block.analyze(analysis)
|
|
|
|
def prepare(self, analysis):
|
|
|
|
# A list of prepared entries, with each consisting of expression
|
|
# bytecode and a SLBlock.
|
|
self.prepared_entries = [ ]
|
|
|
|
for cond, block in self.entries:
|
|
if cond is not None:
|
|
node = ccache.ast_eval(cond)
|
|
|
|
self.constant = min(self.constant, analysis.is_constant(node))
|
|
|
|
cond = compile_expr(node)
|
|
|
|
block.prepare(analysis)
|
|
self.constant = min(self.constant, block.constant)
|
|
self.prepared_entries.append((cond, block))
|
|
|
|
self.last_keyword = True
|
|
|
|
def execute(self, context):
|
|
|
|
# This is true when the block should be executed - when no outer
|
|
# showif is False, and when no prior block in this showif has
|
|
# executed.
|
|
first_true = context.showif is not False
|
|
|
|
for cond, block in self.prepared_entries:
|
|
|
|
ctx = SLContext(context)
|
|
|
|
if not first_true:
|
|
ctx.showif = False
|
|
else:
|
|
if cond is None or eval(cond, context.globals, context.scope):
|
|
ctx.showif = True
|
|
first_true = False
|
|
else:
|
|
ctx.showif = False
|
|
|
|
for i in block.children:
|
|
i.execute(ctx)
|
|
|
|
if ctx.fail:
|
|
context.fail = True
|
|
|
|
def copy_on_change(self, cache):
|
|
for _cond, block in self.entries:
|
|
block.copy_on_change(cache)
|
|
|
|
def used_screens(self, callback):
|
|
for _cond, block in self.entries:
|
|
block.used_screens(callback)
|
|
|
|
def has_transclude(self):
|
|
for _cond, block in self.entries:
|
|
if block.has_transclude():
|
|
return True
|
|
|
|
return False
|
|
|
|
def has_python(self):
|
|
return any(i[1].has_python() for i in self.entries)
|
|
|
|
|
|
class SLFor(SLBlock):
|
|
"""
|
|
The AST node that corresponds to a for statement. This only supports
|
|
simple for loops that assign a single variable.
|
|
"""
|
|
|
|
index_expression = None
|
|
|
|
def __init__(self, loc, variable, expression, index_expression):
|
|
SLBlock.__init__(self, loc)
|
|
|
|
self.variable = variable
|
|
self.expression = expression
|
|
self.index_expression = index_expression
|
|
|
|
def copy(self, transclude):
|
|
rv = self.instantiate(transclude)
|
|
|
|
rv.variable = self.variable
|
|
rv.expression = self.expression
|
|
|
|
return rv
|
|
|
|
def analyze(self, analysis):
|
|
|
|
if analysis.is_constant_expr(self.expression) == GLOBAL_CONST:
|
|
analysis.push_control(True)
|
|
analysis.mark_constant(self.variable)
|
|
else:
|
|
analysis.push_control(False)
|
|
analysis.mark_not_constant(self.variable)
|
|
|
|
SLBlock.analyze(self, analysis)
|
|
|
|
analysis.pop_control()
|
|
|
|
def prepare(self, analysis):
|
|
node = ccache.ast_eval(self.expression)
|
|
|
|
const = analysis.is_constant(node)
|
|
|
|
if const == GLOBAL_CONST:
|
|
self.expression_value = py_eval_bytecode(compile_expr(node))
|
|
self.expression_expr = None
|
|
else:
|
|
self.expression_value = None
|
|
self.expression_expr = compile_expr(node)
|
|
|
|
self.constant = min(self.constant, const)
|
|
|
|
SLBlock.prepare(self, analysis)
|
|
|
|
self.last_keyword = True
|
|
|
|
def execute(self, context):
|
|
|
|
variable = self.variable
|
|
expr = self.expression_expr
|
|
|
|
try:
|
|
if expr is not None:
|
|
value = eval(expr, context.globals, context.scope)
|
|
else:
|
|
value = self.expression_value
|
|
except:
|
|
if not context.predicting:
|
|
raise
|
|
|
|
value = [ 0 ]
|
|
|
|
newcaches = { }
|
|
|
|
oldcaches = context.old_cache.get(self.serial, newcaches) or { }
|
|
|
|
if not isinstance(oldcaches, dict):
|
|
oldcaches = { }
|
|
|
|
misscaches = context.miss_cache.get(self.serial, newcaches) or { }
|
|
|
|
if not isinstance(misscaches, dict):
|
|
misscaches = { }
|
|
|
|
ctx = SLContext(context)
|
|
|
|
for index, v in enumerate(value):
|
|
|
|
ctx.scope[variable] = v
|
|
|
|
if self.index_expression is not None:
|
|
index = eval(self.index_expression, ctx.globals, ctx.scope)
|
|
|
|
ctx.old_cache = oldcaches.get(index, None) or { }
|
|
|
|
if not isinstance(ctx.old_cache, dict):
|
|
ctx.old_cache = {}
|
|
|
|
ctx.miss_cache = misscaches.get(index, None) or { }
|
|
|
|
if not isinstance(ctx.miss_cache, dict):
|
|
ctx.miss_cache = {}
|
|
|
|
newcaches[index] = ctx.new_cache = { }
|
|
|
|
# Inline of SLBlock.execute.
|
|
|
|
for i in self.children:
|
|
try:
|
|
i.execute(ctx)
|
|
except:
|
|
if not context.predicting:
|
|
raise
|
|
|
|
if context.unlikely:
|
|
break
|
|
|
|
context.new_cache[self.serial] = newcaches
|
|
|
|
if ctx.fail:
|
|
context.fail = True
|
|
|
|
def keywords(self, context):
|
|
return
|
|
|
|
def copy_on_change(self, cache):
|
|
c = cache.get(self.serial, None)
|
|
|
|
if not isinstance(c, dict):
|
|
return
|
|
|
|
for child_cache in c.values():
|
|
for i in self.children:
|
|
i.copy_on_change(child_cache)
|
|
|
|
|
|
class SLPython(SLNode):
|
|
|
|
def __init__(self, loc, code):
|
|
SLNode.__init__(self, loc)
|
|
|
|
# A pycode object.
|
|
self.code = code
|
|
|
|
def copy(self, transclude):
|
|
rv = self.instantiate(transclude)
|
|
|
|
rv.code = self.code
|
|
|
|
return rv
|
|
|
|
def analyze(self, analysis):
|
|
analysis.python(self.code.source)
|
|
|
|
def execute(self, context):
|
|
exec self.code.bytecode in context.globals, context.scope
|
|
|
|
def prepare(self, analysis):
|
|
self.constant = NOT_CONST
|
|
self.last_keyword = True
|
|
|
|
def has_python(self):
|
|
return True
|
|
|
|
|
|
class SLPass(SLNode):
|
|
|
|
def execute(self, context):
|
|
return
|
|
|
|
def copy(self, transclude):
|
|
rv = self.instantiate(transclude)
|
|
|
|
return rv
|
|
|
|
|
|
class SLDefault(SLNode):
|
|
|
|
def __init__(self, loc, variable, expression):
|
|
SLNode.__init__(self, loc)
|
|
|
|
self.variable = variable
|
|
self.expression = expression
|
|
|
|
def copy(self, transclude):
|
|
rv = self.instantiate(transclude)
|
|
|
|
rv.variable = self.variable
|
|
rv.expression = self.expression
|
|
|
|
return rv
|
|
|
|
def analyze(self, analysis):
|
|
analysis.mark_not_constant(self.variable)
|
|
|
|
def prepare(self, analysis):
|
|
self.expr = compile_expr(ccache.ast_eval(self.expression))
|
|
self.constant = NOT_CONST
|
|
self.last_keyword = True
|
|
|
|
def execute(self, context):
|
|
scope = context.scope
|
|
variable = self.variable
|
|
|
|
if variable in scope:
|
|
return
|
|
|
|
scope[variable] = eval(self.expr, context.globals, scope)
|
|
|
|
def has_python(self):
|
|
return True
|
|
|
|
|
|
class SLUse(SLNode):
|
|
|
|
id = None
|
|
block = None
|
|
|
|
def __init__(self, loc, target, args, id_expr, block):
|
|
|
|
SLNode.__init__(self, loc)
|
|
|
|
# The name of the screen we're accessing.
|
|
self.target = target
|
|
|
|
# If the target is an SL2 screen, the SLScreen node at the root of
|
|
# the ast for that screen.
|
|
self.ast = None
|
|
|
|
# If arguments are given, those arguments.
|
|
self.args = args
|
|
|
|
# An expression, if the id property is given.
|
|
self.id = id_expr
|
|
|
|
# A block for transclusion, or None if the statement does not have a
|
|
# block.
|
|
self.block = block
|
|
|
|
def copy(self, transclude):
|
|
|
|
rv = self.instantiate(transclude)
|
|
|
|
rv.target = self.target
|
|
rv.args = self.args
|
|
rv.id = self.id
|
|
|
|
if self.block is not None:
|
|
rv.block = self.block.copy(transclude)
|
|
else:
|
|
rv.block = None
|
|
|
|
rv.ast = None
|
|
|
|
return rv
|
|
|
|
def analyze(self, analysis):
|
|
|
|
self.last_keyword = True
|
|
|
|
if self.id:
|
|
self.constant = NOT_CONST
|
|
|
|
if self.block:
|
|
self.block.analyze(analysis)
|
|
|
|
def prepare(self, analysis):
|
|
|
|
self.ast = None
|
|
|
|
if self.block:
|
|
self.block.prepare(analysis)
|
|
|
|
if self.block.constant == GLOBAL_CONST:
|
|
const = True
|
|
else:
|
|
const = False
|
|
else:
|
|
const = False
|
|
|
|
if isinstance(self.target, renpy.ast.PyExpr):
|
|
|
|
self.constant = NOT_CONST
|
|
const = False
|
|
self.ast = None
|
|
|
|
else:
|
|
|
|
target = renpy.display.screen.get_screen_variant(self.target)
|
|
|
|
if target is None:
|
|
self.constant = NOT_CONST
|
|
|
|
if renpy.config.developer:
|
|
raise Exception("A screen named {} does not exist.".format(self.target))
|
|
else:
|
|
return
|
|
|
|
if target.ast is None:
|
|
self.constant = NOT_CONST
|
|
return
|
|
|
|
if const:
|
|
self.ast = target.ast.const_ast
|
|
else:
|
|
self.ast = target.ast.not_const_ast
|
|
|
|
self.constant = min(self.constant, self.ast.constant)
|
|
|
|
def execute_use_screen(self, context):
|
|
|
|
# Create an old-style displayable name for this call site.
|
|
serial = context.use_index[self.serial]
|
|
context.use_index[self.serial] = serial + 1
|
|
|
|
name = (
|
|
context.scope.get("_name", ()),
|
|
self.serial,
|
|
serial)
|
|
|
|
if self.args:
|
|
args, kwargs = self.args.evaluate(context.scope)
|
|
else:
|
|
args = [ ]
|
|
kwargs = { }
|
|
|
|
renpy.display.screen.use_screen(self.target, _name=name, _scope=context.scope, *args, **kwargs)
|
|
|
|
def execute(self, context):
|
|
|
|
if isinstance(self.target, renpy.ast.PyExpr):
|
|
target_name = eval(self.target, context.globals, context.scope)
|
|
target = renpy.display.screen.get_screen_variant(target_name)
|
|
|
|
if target is None:
|
|
raise Exception("A screen named {} does not exist.".format(target_name))
|
|
|
|
ast = target.ast.not_const_ast
|
|
|
|
id_prefix = "_use_expression"
|
|
|
|
else:
|
|
id_prefix = self.target
|
|
ast = self.ast
|
|
|
|
# If self.ast is not an SL2 screen, run it using renpy.display.screen.use_screen.
|
|
if ast is None:
|
|
self.execute_use_screen(context)
|
|
return
|
|
|
|
# Otherwise, run the use statement directly.
|
|
|
|
# Figure out the cache to use.
|
|
|
|
ctx = SLContext(context)
|
|
ctx.new_cache = context.new_cache[self.serial] = { }
|
|
ctx.miss_cache = context.miss_cache.get(self.serial, None) or { }
|
|
ctx.uses_scope = [ ]
|
|
|
|
if self.id:
|
|
|
|
use_id = (id_prefix, eval(self.id, context.globals, context.scope))
|
|
|
|
ctx.old_cache = context.old_use_cache.get(use_id, None) or context.old_cache.get(self.serial, None) or { }
|
|
|
|
if use_id in ctx.old_use_cache:
|
|
ctx.updating = True
|
|
|
|
ctx.new_use_cache[use_id] = ctx.new_cache
|
|
|
|
else:
|
|
|
|
ctx.old_cache = context.old_cache.get(self.serial, None) or { }
|
|
|
|
if not isinstance(ctx.old_cache, dict):
|
|
ctx.old_cache = { }
|
|
if not isinstance(ctx.miss_cache, dict):
|
|
ctx.miss_cache = { }
|
|
|
|
# Evaluate the arguments.
|
|
try:
|
|
if self.args:
|
|
args, kwargs = self.args.evaluate(context.scope)
|
|
else:
|
|
args = [ ]
|
|
kwargs = { }
|
|
except:
|
|
if not context.predicting:
|
|
raise
|
|
|
|
args = [ ]
|
|
kwargs = { }
|
|
|
|
# Apply the arguments to the parameters (if present) or to the scope of the used screen.
|
|
if ast.parameters is not None:
|
|
new_scope = ast.parameters.apply(args, kwargs, ignore_errors=context.predicting)
|
|
|
|
scope = ctx.old_cache.get("scope", None) or ctx.miss_cache.get("scope", None) or { }
|
|
scope.update(new_scope)
|
|
|
|
else:
|
|
|
|
if args:
|
|
raise Exception("Screen {} does not take positional arguments. ({} given)".format(self.target, len(args)))
|
|
|
|
scope = context.scope.copy()
|
|
scope.update(kwargs)
|
|
|
|
scope["_scope"] = scope
|
|
ctx.new_cache["scope"] = scope
|
|
|
|
# Run the child screen.
|
|
ctx.scope = scope
|
|
ctx.parent = weakref.ref(context)
|
|
|
|
ctx.transclude = self.block
|
|
|
|
try:
|
|
ast.execute(ctx)
|
|
finally:
|
|
del scope["_scope"]
|
|
|
|
if ctx.fail:
|
|
context.fail = True
|
|
|
|
def copy_on_change(self, cache):
|
|
|
|
c = cache.get(self.serial, None)
|
|
if c is None:
|
|
return
|
|
|
|
if self.ast is not None:
|
|
self.ast.copy_on_change(c)
|
|
|
|
def used_screens(self, callback):
|
|
callback(self.target)
|
|
|
|
|
|
class SLTransclude(SLNode):
|
|
|
|
def __init__(self, loc):
|
|
SLNode.__init__(self, loc)
|
|
|
|
def copy(self, transclude):
|
|
rv = self.instantiate(transclude)
|
|
rv.constant = transclude
|
|
return rv
|
|
|
|
def execute(self, context):
|
|
|
|
if not context.transclude:
|
|
return
|
|
|
|
parent = context.parent
|
|
if parent is not None:
|
|
parent = parent()
|
|
|
|
ctx = SLContext(parent)
|
|
ctx.new_cache = context.new_cache[self.serial] = { }
|
|
ctx.old_cache = context.old_cache.get(self.serial, None) or { }
|
|
ctx.miss_cache = context.miss_cache.get(self.serial, None) or { }
|
|
|
|
if not isinstance(ctx.old_cache, dict):
|
|
ctx.old_cache = { }
|
|
if not isinstance(ctx.miss_cache, dict):
|
|
ctx.miss_cache = { }
|
|
|
|
ctx.new_cache["transclude"] = context.transclude
|
|
|
|
ctx.children = context.children
|
|
ctx.showif = context.showif
|
|
|
|
try:
|
|
renpy.ui.stack.append(ctx)
|
|
context.transclude.keywords(ctx)
|
|
context.transclude.execute(ctx)
|
|
finally:
|
|
renpy.ui.stack.pop()
|
|
|
|
if ctx.fail:
|
|
context.fail = True
|
|
|
|
def copy_on_change(self, cache):
|
|
|
|
c = cache.get(self.serial, None)
|
|
|
|
if c is None or "transclude" not in c:
|
|
return
|
|
|
|
SLBlock.copy_on_change(c["transclude"], c)
|
|
|
|
def has_transclude(self):
|
|
return True
|
|
|
|
|
|
class SLScreen(SLBlock):
|
|
"""
|
|
This represents a screen defined in the screen language 2.
|
|
"""
|
|
|
|
version = 0
|
|
|
|
# This screen's AST when the transcluded block is entirely
|
|
# constant (or there is no transcluded block at all). This may be
|
|
# the actual AST, or a copy.
|
|
const_ast = None
|
|
|
|
# A copy of this screen's AST when the transcluded block is not
|
|
# constant.
|
|
not_const_ast = None
|
|
|
|
# The analysis
|
|
analysis = None
|
|
|
|
layer = "'screens'"
|
|
sensitive = "True"
|
|
|
|
def __init__(self, loc):
|
|
|
|
SLBlock.__init__(self, loc)
|
|
|
|
# The name of the screen.
|
|
self.name = None
|
|
|
|
# Should this screen be declared as modal?
|
|
self.modal = "False"
|
|
|
|
# The screen's zorder.
|
|
self.zorder = "0"
|
|
|
|
# The screen's tag.
|
|
self.tag = None
|
|
|
|
# The variant of screen we're defining.
|
|
self.variant = "None" # expr.
|
|
|
|
# Should we predict this screen?
|
|
self.predict = "None" # expr.
|
|
|
|
# Should this screen be sensitive.
|
|
self.sensitive = "True"
|
|
|
|
# The parameters this screen takes.
|
|
self.parameters = None
|
|
|
|
# The analysis object used for this screen, if the screen has
|
|
# already been analyzed.
|
|
self.analysis = None
|
|
|
|
# True if this screen has been prepared.
|
|
self.prepared = False
|
|
|
|
def copy(self, transclude):
|
|
rv = self.instantiate(transclude)
|
|
|
|
rv.name = self.name
|
|
rv.modal = self.modal
|
|
rv.zorder = self.zorder
|
|
rv.tag = self.tag
|
|
rv.variant = self.variant
|
|
rv.predict = self.predict
|
|
rv.parameters = self.parameters
|
|
rv.sensitive = self.sensitive
|
|
|
|
rv.prepared = False
|
|
rv.analysis = None
|
|
rv.ast = None
|
|
|
|
return rv
|
|
|
|
def define(self, location):
|
|
"""
|
|
Defines a screen.
|
|
"""
|
|
|
|
renpy.display.screen.define_screen(
|
|
self.name,
|
|
self,
|
|
modal=self.modal,
|
|
zorder=self.zorder,
|
|
tag=self.tag,
|
|
variant=renpy.python.py_eval(self.variant),
|
|
predict=renpy.python.py_eval(self.predict),
|
|
parameters=self.parameters,
|
|
location=self.location,
|
|
layer=renpy.python.py_eval(self.layer),
|
|
sensitive=self.sensitive,
|
|
)
|
|
|
|
def analyze(self, analysis):
|
|
|
|
SLBlock.analyze(self, analysis)
|
|
|
|
def analyze_screen(self):
|
|
|
|
# Have we already been analyzed?
|
|
if self.const_ast:
|
|
return
|
|
|
|
key = (self.name, self.variant)
|
|
|
|
if key in scache.const_analyzed:
|
|
self.const_ast = scache.const_analyzed[key]
|
|
self.not_const_ast = scache.not_const_analyzed[key]
|
|
return
|
|
|
|
self.const_ast = self
|
|
|
|
if self.has_transclude():
|
|
self.not_const_ast = self.copy(NOT_CONST)
|
|
targets = [ self.const_ast, self.not_const_ast ]
|
|
else:
|
|
self.not_const_ast = self.const_ast
|
|
targets = [ self.const_ast ]
|
|
|
|
for ast in targets:
|
|
analysis = ast.analysis = Analysis(None)
|
|
|
|
if ast.parameters:
|
|
analysis.parameters(ast.parameters)
|
|
|
|
ast.analyze(analysis)
|
|
|
|
while not analysis.at_fixed_point():
|
|
ast.analyze(analysis)
|
|
|
|
scache.const_analyzed[key] = self.const_ast
|
|
scache.not_const_analyzed[key] = self.not_const_ast
|
|
scache.updated = True
|
|
|
|
def unprepare_screen(self):
|
|
self.prepared = False
|
|
|
|
def prepare_screen(self):
|
|
|
|
if self.prepared:
|
|
return
|
|
|
|
self.analyze_screen()
|
|
|
|
# This version ensures we're not using the cache from an old
|
|
# version of the screen.
|
|
self.version += 1
|
|
|
|
self.const_ast.prepare(self.const_ast.analysis)
|
|
|
|
if self.not_const_ast is not self.const_ast:
|
|
self.not_const_ast.prepare(self.not_const_ast.analysis)
|
|
|
|
self.prepared = True
|
|
|
|
if renpy.display.screen.get_profile(self.name).const:
|
|
profile_log.write("CONST ANALYSIS %s", self.name)
|
|
|
|
new_constants = [ i for i in self.const_ast.analysis.global_constant if i not in renpy.pyanalysis.constants ]
|
|
new_constants.sort()
|
|
profile_log.write(' global_const: %s', " ".join(new_constants))
|
|
|
|
local_constants = list(self.const_ast.analysis.local_constant)
|
|
local_constants.sort()
|
|
profile_log.write(' local_const: %s', " ".join(local_constants))
|
|
|
|
not_constants = list(self.const_ast.analysis.not_constant)
|
|
not_constants.sort()
|
|
profile_log.write(' not_const: %s', " ".join(not_constants))
|
|
|
|
def execute(self, context):
|
|
self.keywords(context)
|
|
SLBlock.execute(self, context)
|
|
|
|
def report_traceback(self, name, last):
|
|
if last:
|
|
return None
|
|
|
|
if name == "__call__":
|
|
return [ ]
|
|
|
|
return SLBlock.report_traceback(self, name, last)
|
|
|
|
def __call__(self, *args, **kwargs):
|
|
scope = kwargs["_scope"]
|
|
debug = kwargs.get("_debug", False)
|
|
|
|
if self.parameters:
|
|
|
|
args = scope.get("_args", ())
|
|
kwargs = scope.get("_kwargs", { })
|
|
|
|
values = renpy.ast.apply_arguments(self.parameters, args, kwargs, ignore_errors=renpy.display.predict.predicting)
|
|
scope.update(values)
|
|
|
|
if not self.prepared:
|
|
self.prepare_screen()
|
|
|
|
current_screen = renpy.display.screen.current_screen()
|
|
|
|
if current_screen.screen_name[0] in renpy.config.profile_screens:
|
|
debug = True
|
|
|
|
context = SLContext()
|
|
|
|
context.scope = scope
|
|
context.globals = renpy.python.store_dicts["store"]
|
|
context.debug = debug
|
|
context.predicting = renpy.display.predict.predicting
|
|
context.updating = (current_screen.phase == renpy.display.screen.UPDATE)
|
|
|
|
name = scope["_name"]
|
|
|
|
def get_cache(d):
|
|
rv = d.get(name, None)
|
|
|
|
if (not isinstance(rv, dict)) or (rv.get("version", None) != self.version):
|
|
rv = { "version" : self.version }
|
|
d[name] = rv
|
|
|
|
return rv
|
|
|
|
context.old_cache = get_cache(current_screen.cache)
|
|
context.miss_cache = get_cache(current_screen.miss_cache)
|
|
context.new_cache = { "version" : self.version }
|
|
|
|
context.old_use_cache = current_screen.use_cache
|
|
context.new_use_cache = { }
|
|
|
|
self.const_ast.execute(context)
|
|
|
|
for i in context.children:
|
|
renpy.ui.implicit_add(i)
|
|
|
|
current_screen.cache[name] = context.new_cache
|
|
current_screen.use_cache = context.new_use_cache
|
|
|
|
|
|
class ScreenCache(object):
|
|
|
|
def __init__(self):
|
|
self.version = 1
|
|
|
|
self.const_analyzed = { }
|
|
self.not_const_analyzed = { }
|
|
|
|
self.updated = False
|
|
|
|
|
|
scache = ScreenCache()
|
|
|
|
CACHE_FILENAME = "cache/screens.rpyb"
|
|
|
|
|
|
def load_cache():
|
|
if renpy.game.args.compile: # @UndefinedVariable
|
|
return
|
|
|
|
try:
|
|
f = renpy.loader.load(CACHE_FILENAME)
|
|
|
|
digest = f.read(hashlib.md5().digest_size)
|
|
if digest != renpy.game.script.digest.digest():
|
|
return
|
|
|
|
s = loads(zlib.decompress(f.read()))
|
|
f.close()
|
|
|
|
if s.version == scache.version:
|
|
renpy.game.script.update_bytecode()
|
|
scache.const_analyzed.update(s.const_analyzed)
|
|
scache.not_const_analyzed.update(s.not_const_analyzed)
|
|
|
|
except:
|
|
pass
|
|
|
|
|
|
def save_cache():
|
|
if not scache.updated:
|
|
return
|
|
|
|
if renpy.macapp:
|
|
return
|
|
|
|
try:
|
|
data = zlib.compress(dumps(scache, 2), 9)
|
|
|
|
with open(renpy.loader.get_path(CACHE_FILENAME), "wb") as f:
|
|
f.write(renpy.game.script.digest.digest())
|
|
f.write(data)
|
|
except:
|
|
pass
|