934 lines
27 KiB
Python
934 lines
27 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.
|
|
|
|
# This file contains code responsible for managing the execution of a
|
|
# renpy object, as well as the context object.
|
|
|
|
from __future__ import print_function
|
|
import sys
|
|
import time
|
|
|
|
import renpy.display
|
|
import renpy.test
|
|
from renpy import six
|
|
|
|
pyast = __import__("ast", { })
|
|
|
|
# The number of statements that have been run since the last infinite loop
|
|
# check.
|
|
il_statements = 0
|
|
|
|
# The deadline for reporting we're not in an infinite loop.
|
|
il_time = 0
|
|
|
|
|
|
def check_infinite_loop():
|
|
global il_statements
|
|
|
|
il_statements += 1
|
|
|
|
if il_statements <= 1000:
|
|
return
|
|
|
|
il_statements = 0
|
|
|
|
global il_time
|
|
|
|
now = time.time()
|
|
|
|
if now > il_time:
|
|
il_time = now + 60
|
|
raise Exception("Possible infinite loop.")
|
|
|
|
if renpy.config.developer and (il_time > now + 60):
|
|
il_time = now + 60
|
|
|
|
return
|
|
|
|
|
|
def not_infinite_loop(delay):
|
|
"""
|
|
:doc: other
|
|
|
|
Resets the infinite loop detection timer to `delay` seconds.
|
|
"""
|
|
|
|
# Give more time in non-developer mode, since computers can be crazy slow
|
|
# and the player can't do much about it.
|
|
if not renpy.config.developer:
|
|
delay *= 5
|
|
|
|
global il_time
|
|
il_time = time.time() + delay
|
|
|
|
|
|
class Delete(object):
|
|
pass
|
|
|
|
|
|
class PredictInfo(renpy.object.Object):
|
|
"""
|
|
Not used anymore, but needed for backwards compatibility.
|
|
"""
|
|
|
|
|
|
class LineLogEntry(object):
|
|
|
|
def __init__(self, filename, line, node, abnormal):
|
|
self.filename = filename
|
|
self.line = line
|
|
self.node = node
|
|
self.abnormal = abnormal
|
|
|
|
for i in renpy.config.line_log_callbacks:
|
|
i(self)
|
|
|
|
def __eq__(self, other):
|
|
if not isinstance(other, LineLogEntry):
|
|
return False
|
|
|
|
return (self.filename == other.filename) and (self.line == other.line) and (self.node is other.node)
|
|
|
|
def __ne__(self, other):
|
|
return not (self == other)
|
|
|
|
|
|
class Context(renpy.object.Object):
|
|
"""
|
|
This is the context object which stores the current context
|
|
of the game interpreter.
|
|
|
|
@ivar current: The name of the node that is currently being
|
|
executed.
|
|
|
|
@ivar return_stack: A list of names of nodes that should be
|
|
returned to when the return statement executes. (When a return
|
|
occurs, the name is looked up, and name.text is then executed.)
|
|
|
|
@ivar scene_lists: The scene lists associated with the current
|
|
context.
|
|
|
|
@ivar rollback: True if this context participates in rollbacks.
|
|
|
|
@ivar runtime: The time spent in this context, in milliseconds.
|
|
|
|
@ivar info: An object that is made available to user code. This object
|
|
does participates in rollback.
|
|
"""
|
|
|
|
__version__ = 16
|
|
|
|
nosave = [ 'next_node' ]
|
|
|
|
next_node = None
|
|
|
|
force_checkpoint = False
|
|
|
|
come_from_name = None
|
|
come_from_label = None
|
|
|
|
temporary_attributes = None
|
|
|
|
deferred_translate_identifier = None
|
|
|
|
def __repr__(self):
|
|
|
|
if not self.current:
|
|
return "<Context>"
|
|
|
|
node = renpy.game.script.lookup(self.current)
|
|
|
|
return "<Context: {}:{} {!r}>".format(
|
|
node.filename,
|
|
node.linenumber,
|
|
node.diff_info(),
|
|
)
|
|
|
|
def after_upgrade(self, version):
|
|
if version < 1:
|
|
self.scene_lists.image_predict_info = self.predict_info.images
|
|
|
|
if version < 2:
|
|
self.abnormal = False
|
|
self.last_abnormal = False
|
|
|
|
if version < 3:
|
|
self.music = { }
|
|
|
|
if version < 4:
|
|
self.interacting = False
|
|
|
|
if version < 5:
|
|
self.modes = renpy.python.RevertableList([ "start" ])
|
|
self.use_modes = True
|
|
|
|
if version < 6:
|
|
self.images = self.predict_info.images
|
|
|
|
if version < 7:
|
|
self.init_phase = False
|
|
self.next_node = None
|
|
|
|
if version < 8:
|
|
self.defer_rollback = None
|
|
|
|
if version < 9:
|
|
self.translate_language = None
|
|
self.translate_identifier = None
|
|
|
|
if version < 10:
|
|
self.exception_handler = None
|
|
|
|
if version < 11:
|
|
self.say_attributes = None
|
|
|
|
if version < 13:
|
|
self.line_log = [ ]
|
|
|
|
if version < 14:
|
|
self.movie = { }
|
|
|
|
if version < 15:
|
|
self.abnormal_stack = [ False ] * len(self.return_stack)
|
|
|
|
if version < 16:
|
|
self.alternate_translate_identifier = None
|
|
|
|
def __init__(self, rollback, context=None, clear=False):
|
|
"""
|
|
`clear`
|
|
True if we should clear out the context_clear_layers.
|
|
"""
|
|
|
|
super(Context, self).__init__()
|
|
|
|
self.current = None
|
|
self.call_location_stack = [ ]
|
|
self.return_stack = [ ]
|
|
|
|
# The value of abnormal at the time of the call.
|
|
self.abnormal_stack = [ ]
|
|
|
|
# Two deeper then the return stack and call location stack.
|
|
# 1 deeper is for the context top-level, 2 deeper is for
|
|
# _args, _kwargs, and _return.
|
|
self.dynamic_stack = [ { } ]
|
|
|
|
self.rollback = rollback
|
|
self.runtime = 0
|
|
self.info = renpy.python.RevertableObject()
|
|
self.seen = False
|
|
|
|
# True if there has just been an abnormal transfer of control,
|
|
# like the start of a context, a jump, or a call. (Returns are
|
|
# considered to be normal.)
|
|
#
|
|
# Set directly by ast.Call and ast.Jump.
|
|
self.abnormal = True
|
|
|
|
# True if the last statement caused an abnormal transfer of
|
|
# control.
|
|
self.last_abnormal = False
|
|
|
|
# A map from the name of a music channel to the MusicContext
|
|
# object corresponding to that channel.
|
|
self.music = renpy.python.RevertableDict()
|
|
|
|
# True if we're in the middle of a call to ui.interact. This
|
|
# will cause Ren'Py to generate an error if we call ui.interact
|
|
# again.
|
|
self.interacting = False
|
|
|
|
# True if we're in the init phase. (Isn't inherited.)
|
|
self.init_phase = False
|
|
|
|
# When deferring a rollback, the arguments to pass to renpy.exports.rollback.
|
|
self.defer_rollback = None
|
|
|
|
# The exception handler that is called when an exception occurs while executing
|
|
# code. If None, a default handler is used. This is reset when run is called.
|
|
self.exception_handler = None
|
|
|
|
# The attributes that are used by the current say statement.
|
|
self.say_attributes = None
|
|
self.temporary_attributes = None
|
|
|
|
# A list of lines that were run since the last time this log was
|
|
# cleared.
|
|
self.line_log = [ ]
|
|
|
|
# Do we want to force a checkpoint before the next statement
|
|
# executed?
|
|
self.force_checkpoint = False
|
|
|
|
# A map from a channel to the Movie playing on that channel.
|
|
self.movie = { }
|
|
|
|
if context:
|
|
oldsl = context.scene_lists
|
|
self.runtime = context.runtime
|
|
|
|
vars(self.info).update(vars(context.info))
|
|
|
|
for k, v in context.music.items():
|
|
self.music[k] = v.copy()
|
|
|
|
self.movie = dict(context.movie)
|
|
|
|
self.images = renpy.display.image.ShownImageInfo(context.images)
|
|
|
|
else:
|
|
oldsl = None
|
|
self.images = renpy.display.image.ShownImageInfo(None)
|
|
|
|
self.scene_lists = renpy.display.core.SceneLists(oldsl, self.images)
|
|
|
|
for i in renpy.config.context_copy_remove_screens:
|
|
self.scene_lists.remove("screens", i, None)
|
|
|
|
self.make_dynamic([ "_return", "_args", "_kwargs", "mouse_visible", "suppress_overlay", "_side_image_attributes" ])
|
|
self.dynamic_stack.append({ })
|
|
|
|
if clear:
|
|
for i in renpy.config.context_clear_layers:
|
|
self.scene_lists.clear(layer=i)
|
|
|
|
# A list of modes that the context has been in.
|
|
self.modes = renpy.python.RevertableList([ "start" ])
|
|
self.use_modes = True
|
|
|
|
# The language we started with.
|
|
self.translate_language = renpy.game.preferences.language
|
|
|
|
# The identifier of the current translate block.
|
|
self.translate_identifier = None
|
|
|
|
# The alternate identifier of the current translate block.
|
|
self.alternate_translate_identifier = None
|
|
|
|
# The translate identifier of the last say statement with
|
|
# interact = False.
|
|
self.deferred_translate_identifier = None
|
|
|
|
def replace_node(self, old, new):
|
|
|
|
def replace_one(name):
|
|
n = renpy.game.script.lookup(name)
|
|
if n is old:
|
|
return new.name
|
|
|
|
return name
|
|
|
|
self.current = replace_one(self.current)
|
|
self.return_stack = [ replace_one(i) for i in self.return_stack ]
|
|
|
|
def make_dynamic(self, names, context=False):
|
|
"""
|
|
Makes the variable names listed in names dynamic, by backing up
|
|
their current value (if not already dynamic in the current call).
|
|
"""
|
|
|
|
store = renpy.store.__dict__
|
|
|
|
if context:
|
|
index = 0
|
|
else:
|
|
index = -1
|
|
|
|
for i in names:
|
|
|
|
if i in self.dynamic_stack[index]:
|
|
continue
|
|
|
|
if i in store:
|
|
self.dynamic_stack[index][i] = store[i]
|
|
else:
|
|
self.dynamic_stack[index][i] = Delete()
|
|
|
|
def pop_dynamic(self):
|
|
"""
|
|
Pops one level of the dynamic stack. Called when the return
|
|
statement is run.
|
|
"""
|
|
|
|
if not self.dynamic_stack:
|
|
return
|
|
|
|
store = renpy.store.__dict__
|
|
|
|
dynamic = self.dynamic_stack.pop()
|
|
|
|
for k, v in dynamic.iteritems():
|
|
if isinstance(v, Delete):
|
|
store.pop(k, None)
|
|
else:
|
|
store[k] = v
|
|
|
|
def pop_all_dynamic(self):
|
|
"""
|
|
Pops all levels of the dynamic stack. Called when we jump
|
|
out of a context.
|
|
"""
|
|
|
|
while self.dynamic_stack:
|
|
self.pop_dynamic()
|
|
|
|
def pop_dynamic_roots(self, roots):
|
|
|
|
for dynamic in reversed(self.dynamic_stack):
|
|
|
|
for k, v in dynamic.iteritems():
|
|
name = "store." + k
|
|
|
|
if isinstance(v, Delete) and (name in roots):
|
|
del roots[name]
|
|
else:
|
|
roots[name] = v
|
|
|
|
def goto_label(self, node_name):
|
|
"""
|
|
Sets the name of the node that will be run when this context
|
|
next executes.
|
|
"""
|
|
|
|
self.current = node_name
|
|
|
|
def check_stacks(self):
|
|
"""
|
|
Check and fix stack corruption.
|
|
"""
|
|
|
|
if len(self.dynamic_stack) != len(self.return_stack) + 2:
|
|
|
|
e = Exception("Potential return stack corruption: dynamic={} return={}".format(len(self.dynamic_stack), len(self.return_stack)))
|
|
|
|
while len(self.dynamic_stack) < len(self.return_stack) + 2:
|
|
self.dynamic_stack.append({})
|
|
|
|
while len(self.dynamic_stack) > len(self.return_stack) + 2:
|
|
self.pop_dynamic()
|
|
|
|
raise e
|
|
|
|
def report_traceback(self, name, last):
|
|
|
|
if last:
|
|
return
|
|
|
|
rv = [ ]
|
|
|
|
for i in self.call_location_stack:
|
|
try:
|
|
node = renpy.game.script.lookup(i)
|
|
if not node.filename.replace("\\", "/").startswith("common/"):
|
|
rv.append((node.filename, node.linenumber, "script call", None))
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
node = renpy.game.script.lookup(self.current)
|
|
if not node.filename.replace("\\", "/").startswith("common/"):
|
|
rv.append((node.filename, node.linenumber, "script", None))
|
|
except:
|
|
pass
|
|
|
|
return rv
|
|
|
|
def report_coverage(self, node):
|
|
"""
|
|
Execs a python pass statement on the line of code corresponding to
|
|
`node`. This indicates to python coverage tools that this line has
|
|
been executed.
|
|
"""
|
|
|
|
ps = pyast.Pass(lineno=node.linenumber, col_offset=0)
|
|
module = pyast.Module(lineno=node.linenumber, col_offset=0, body=[ ps ])
|
|
code = compile(module, node.filename, 'exec')
|
|
exec(code)
|
|
|
|
def come_from(self, name, label):
|
|
"""
|
|
When control reaches name, call label. Only for internal use.
|
|
"""
|
|
|
|
self.come_from_name = name
|
|
self.come_from_label = label
|
|
|
|
def run(self, node=None):
|
|
"""
|
|
Executes as many nodes as possible in the current context. If the
|
|
node argument is given, starts executing from that node. Otherwise,
|
|
looks up the node given in self.current, and executes from there.
|
|
"""
|
|
|
|
self.exception_handler = None
|
|
|
|
self.abnormal = True
|
|
|
|
if node is None:
|
|
node = renpy.game.script.lookup(self.current)
|
|
|
|
developer = renpy.config.developer
|
|
tracing = sys.gettrace() is not None
|
|
|
|
# Is this the first time through the loop?
|
|
first = True
|
|
|
|
while node:
|
|
|
|
if node.name == self.come_from_name:
|
|
self.come_from_name = None
|
|
node = self.call(self.come_from_label, return_site=node.name)
|
|
self.make_dynamic([ "_return", "_begin_rollback" ])
|
|
renpy.store._begin_rollback = False
|
|
|
|
this_node = node
|
|
type_node_name = type(node).__name__
|
|
|
|
renpy.plog(1, "--- start {} ({}:{})", type_node_name, node.filename, node.linenumber)
|
|
|
|
self.current = node.name
|
|
self.last_abnormal = self.abnormal
|
|
self.abnormal = False
|
|
self.defer_rollback = None
|
|
|
|
if renpy.config.line_log:
|
|
ll_entry = LineLogEntry(node.filename, node.linenumber, node, self.last_abnormal)
|
|
|
|
if ll_entry not in self.line_log:
|
|
self.line_log.append(ll_entry)
|
|
|
|
if not renpy.store._begin_rollback:
|
|
update_rollback = False
|
|
force_rollback = False
|
|
elif first or self.force_checkpoint or (node.rollback == "force"):
|
|
update_rollback = True
|
|
force_rollback = True
|
|
elif not renpy.config.all_nodes_rollback and (node.rollback == "never"):
|
|
update_rollback = False
|
|
force_rollback = False
|
|
else:
|
|
update_rollback = True
|
|
force_rollback = False
|
|
|
|
# Force a new rollback to start to match things in the forward log.
|
|
if renpy.game.log.forward and renpy.game.log.forward[0][0] == node.name:
|
|
update_rollback = True
|
|
force_rollback = True
|
|
|
|
first = False
|
|
|
|
if update_rollback:
|
|
|
|
if self.rollback and renpy.game.log:
|
|
renpy.game.log.begin(force=force_rollback)
|
|
|
|
if self.rollback and self.force_checkpoint:
|
|
renpy.game.log.force_checkpoint = True
|
|
self.force_checkpoint = False
|
|
|
|
self.seen = False
|
|
|
|
renpy.test.testexecution.take_name(self.current)
|
|
|
|
try:
|
|
try:
|
|
check_infinite_loop()
|
|
|
|
if tracing:
|
|
self.report_coverage(node)
|
|
|
|
renpy.game.exception_info = "While running game code:"
|
|
|
|
self.next_node = None
|
|
|
|
renpy.plog(2, " before execute {} ({}:{})", type_node_name, node.filename, node.linenumber)
|
|
|
|
node.execute()
|
|
|
|
renpy.plog(2, " after execute {} ({}:{})", type_node_name, node.filename, node.linenumber)
|
|
|
|
if developer and self.next_node:
|
|
self.check_stacks()
|
|
|
|
except renpy.game.CONTROL_EXCEPTIONS as e:
|
|
|
|
# An exception ends the current translation.
|
|
self.translate_interaction = None
|
|
|
|
raise
|
|
|
|
except Exception as e:
|
|
self.translate_interaction = None
|
|
|
|
exc_info = sys.exc_info()
|
|
short, full, traceback_fn = renpy.error.report_exception(e, editor=False)
|
|
|
|
try:
|
|
handled = False
|
|
|
|
if self.exception_handler is not None:
|
|
self.exception_handler(short, full, traceback_fn)
|
|
handled = True
|
|
elif renpy.config.exception_handler is not None:
|
|
handled = renpy.config.exception_handler(short, full, traceback_fn)
|
|
|
|
if not handled:
|
|
if renpy.display.error.report_exception(short, full, traceback_fn):
|
|
raise
|
|
except renpy.game.CONTROL_EXCEPTIONS as ce:
|
|
raise ce
|
|
except Exception as ce:
|
|
six.reraise(exc_info[0], exc_info[1], exc_info[2])
|
|
|
|
node = self.next_node
|
|
|
|
except renpy.game.JumpException as e:
|
|
node = renpy.game.script.lookup(e.args[0])
|
|
self.abnormal = True
|
|
|
|
except renpy.game.CallException as e:
|
|
|
|
if e.from_current:
|
|
return_site = getattr(node, "statement_start", node).name
|
|
else:
|
|
if self.next_node is None:
|
|
raise Exception("renpy.call can't be used when the next node is undefined.")
|
|
return_site = self.next_node.name
|
|
|
|
node = self.call(e.label, return_site=return_site)
|
|
self.abnormal = True
|
|
renpy.store._args = e.args
|
|
renpy.store._kwargs = e.kwargs
|
|
|
|
if self.seen:
|
|
renpy.game.persistent._seen_ever[self.current] = True # @UndefinedVariable
|
|
renpy.game.seen_session[self.current] = True
|
|
|
|
renpy.plog(2, " end {} ({}:{})", type_node_name, this_node.filename, this_node.linenumber)
|
|
|
|
if self.rollback and renpy.game.log:
|
|
renpy.game.log.complete()
|
|
|
|
def mark_seen(self):
|
|
"""
|
|
Marks the current statement as one that has been seen by the user.
|
|
"""
|
|
|
|
self.seen = True
|
|
|
|
def call(self, label, return_site=None):
|
|
"""
|
|
Calls the named label.
|
|
"""
|
|
|
|
if not self.current:
|
|
raise Exception("Context not capable of executing Ren'Py code.")
|
|
|
|
if return_site is None:
|
|
return_site = self.current
|
|
|
|
self.call_location_stack.append(self.current)
|
|
|
|
self.return_stack.append(return_site)
|
|
self.dynamic_stack.append({ })
|
|
self.abnormal_stack.append(self.last_abnormal)
|
|
self.current = label
|
|
|
|
self.make_dynamic([ "_args", "_kwargs" ])
|
|
renpy.store._args = None
|
|
renpy.store._kwargs = None
|
|
|
|
return renpy.game.script.lookup(label)
|
|
|
|
def pop_call(self):
|
|
"""
|
|
Blindly pops the top call record from the stack.
|
|
"""
|
|
|
|
if not self.return_stack:
|
|
if renpy.config.developer:
|
|
raise Exception("No call on call stack.")
|
|
|
|
return
|
|
|
|
self.return_stack.pop()
|
|
self.call_location_stack.pop()
|
|
self.pop_dynamic()
|
|
self.abnormal_stack.pop()
|
|
|
|
def lookup_return(self, pop=True):
|
|
"""
|
|
Returns the node to return to, or None if there is no
|
|
such node.
|
|
"""
|
|
|
|
while self.return_stack:
|
|
|
|
node = None
|
|
|
|
if renpy.game.script.has_label(self.return_stack[-1]):
|
|
node = renpy.game.script.lookup(self.return_stack[-1])
|
|
elif renpy.game.script.has_label(self.call_location_stack[-1]):
|
|
node = renpy.game.script.lookup(self.call_location_stack[-1]).next
|
|
|
|
if node is None:
|
|
|
|
if renpy.config.developer:
|
|
raise Exception("Could not find return label {!r}.".format(self.return_stack[-1]))
|
|
|
|
self.return_stack.pop()
|
|
self.call_location_stack.pop()
|
|
self.pop_dynamic()
|
|
self.abnormal = self.abnormal_stack.pop()
|
|
|
|
continue
|
|
|
|
if pop:
|
|
self.return_stack.pop()
|
|
self.call_location_stack.pop()
|
|
self.abnormal = self.abnormal_stack.pop()
|
|
|
|
return node
|
|
|
|
return None
|
|
|
|
def rollback_copy(self):
|
|
"""
|
|
Makes a copy of this object, suitable for rolling back to.
|
|
"""
|
|
|
|
rv = Context(self.rollback, self)
|
|
rv.call_location_stack = self.call_location_stack[:]
|
|
rv.return_stack = self.return_stack[:]
|
|
rv.dynamic_stack = [ i.copy() for i in self.dynamic_stack ]
|
|
rv.current = self.current
|
|
|
|
rv.runtime = self.runtime
|
|
rv.info = self.info
|
|
|
|
rv.translate_language = self.translate_language
|
|
rv.translate_identifier = self.translate_identifier
|
|
|
|
rv.abnormal = self.abnormal
|
|
rv.last_abnormal = self.last_abnormal
|
|
rv.abnormal_stack = list(self.abnormal_stack)
|
|
|
|
return rv
|
|
|
|
def predict_call(self, label, return_site):
|
|
"""
|
|
This is called by the prediction code to indicate that a call to
|
|
`label` will occur.
|
|
|
|
`return_site`
|
|
The name of the return site to push on the predicted return
|
|
stack.
|
|
|
|
Returns the node corresponding to `label`
|
|
"""
|
|
|
|
self.predict_return_stack = list(self.predict_return_stack)
|
|
self.predict_return_stack.append(return_site)
|
|
|
|
return renpy.game.script.lookup(label)
|
|
|
|
def predict_return(self):
|
|
"""
|
|
This predicts that a return will occur.
|
|
|
|
It returns the node we predict will be returned to.
|
|
"""
|
|
|
|
if not self.predict_return_stack:
|
|
return None
|
|
|
|
self.predict_return_stack = list(self.predict_return_stack)
|
|
label = self.predict_return_stack.pop()
|
|
|
|
return renpy.game.script.lookup(label)
|
|
|
|
def predict(self):
|
|
"""
|
|
Performs image prediction, calling the given callback with each
|
|
images that we predict to be loaded, in the rough order that
|
|
they will be potentially loaded.
|
|
"""
|
|
|
|
if not self.current:
|
|
return
|
|
|
|
if renpy.config.predict_statements_callback is None:
|
|
return
|
|
|
|
old_images = self.images
|
|
|
|
# A worklist of (node, images, return_stack) tuples.
|
|
nodes = [ ]
|
|
|
|
# The set of nodes we've seen. (We only consider each node once.)
|
|
seen = set()
|
|
|
|
# Find the roots.
|
|
for label in renpy.config.predict_statements_callback(self.current):
|
|
|
|
if not renpy.game.script.has_label(label):
|
|
return
|
|
|
|
node = renpy.game.script.lookup(label)
|
|
|
|
if node in seen:
|
|
continue
|
|
|
|
nodes.append((node, self.images, self.return_stack))
|
|
seen.add(node)
|
|
|
|
# Predict statements.
|
|
for i in range(0, renpy.config.predict_statements):
|
|
|
|
if i >= len(nodes):
|
|
break
|
|
|
|
node, images, return_stack = nodes[i]
|
|
|
|
self.images = renpy.display.image.ShownImageInfo(images)
|
|
self.predict_return_stack = return_stack
|
|
|
|
try:
|
|
|
|
for n in node.predict():
|
|
if n is None:
|
|
continue
|
|
|
|
if n not in seen:
|
|
nodes.append((n, self.images, self.predict_return_stack))
|
|
seen.add(n)
|
|
|
|
except:
|
|
|
|
if renpy.config.debug_image_cache:
|
|
import traceback
|
|
|
|
print()
|
|
traceback.print_exc()
|
|
print("While predicting images.")
|
|
|
|
self.images = old_images
|
|
self.predict_return_stack = None
|
|
|
|
yield True
|
|
|
|
yield False
|
|
|
|
def seen_current(self, ever):
|
|
"""
|
|
Returns a true value if we have finshed the current statement
|
|
at least once before.
|
|
|
|
@param ever: If True, we're checking to see if we've ever
|
|
finished this statement. If False, we're checking to see if
|
|
we've finished this statement in the current session.
|
|
"""
|
|
|
|
if not self.current:
|
|
return False
|
|
|
|
if ever:
|
|
seen = renpy.game.persistent._seen_ever # @UndefinedVariable
|
|
else:
|
|
seen = renpy.game.seen_session
|
|
|
|
return self.current in seen
|
|
|
|
def do_deferred_rollback(self):
|
|
"""
|
|
Called to cause deferred rollback to occur.
|
|
"""
|
|
|
|
if not self.defer_rollback:
|
|
return
|
|
|
|
force, checkpoints = self.defer_rollback
|
|
|
|
self.defer_rollback = None
|
|
|
|
renpy.exports.rollback(force, checkpoints)
|
|
|
|
def get_return_stack(self):
|
|
return list(self.return_stack)
|
|
|
|
def set_return_stack(self, return_stack):
|
|
self.return_stack = list(return_stack)
|
|
|
|
while len(self.call_location_stack) > len(self.return_stack):
|
|
self.call_location_stack.pop()
|
|
|
|
d = self.dynamic_stack.pop()
|
|
d.update(self.dynamic_stack[-1])
|
|
self.dynamic_stack[-1] = d
|
|
|
|
while len(self.call_location_stack) < len(self.return_stack):
|
|
self.call_location_stack.append("unknown location")
|
|
self.dynamic_stack.append({})
|
|
|
|
|
|
def run_context(top):
|
|
"""
|
|
Runs the current context until it can't be run anymore, while handling
|
|
the RestartContext and RestartTopContext exceptions.
|
|
"""
|
|
|
|
if renpy.config.context_callback is not None:
|
|
renpy.config.context_callback()
|
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
context = renpy.game.context()
|
|
|
|
context.run()
|
|
|
|
rv = renpy.store._return
|
|
|
|
context.pop_all_dynamic()
|
|
|
|
return rv
|
|
|
|
except renpy.game.RestartContext as e:
|
|
|
|
# Apply defaults.
|
|
renpy.exports.execute_default_statement(False)
|
|
continue
|
|
|
|
except renpy.game.RestartTopContext as e:
|
|
if top:
|
|
|
|
# Apply defaults.
|
|
renpy.exports.execute_default_statement(False)
|
|
continue
|
|
|
|
else:
|
|
raise
|
|
|
|
except:
|
|
context.pop_all_dynamic()
|
|
raise
|