1# Copyright 2004-2021 Tom Rothamel <pytom@bishoujo.us>
2#
3# Permission is hereby granted, free of charge, to any person
4# obtaining a copy of this software and associated documentation files
5# (the "Software"), to deal in the Software without restriction,
6# including without limitation the rights to use, copy, modify, merge,
7# publish, distribute, sublicense, and/or sell copies of the Software,
8# and to permit persons to whom the Software is furnished to do so,
9# subject to the following conditions:
10#
11# The above copyright notice and this permission notice shall be
12# included in all copies or substantial portions of the Software.
13#
14# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
22# This file contains code responsible for managing the execution of a
23# renpy object, as well as the context object.
24
25from __future__ import division, absolute_import, with_statement, print_function, unicode_literals
26from renpy.compat import *
27from future.utils import reraise
28
29import sys
30import time
31
32import renpy.display
33import renpy.test
34
35import ast as pyast
36
37# The number of statements that have been run since the last infinite loop
38# check.
39il_statements = 0
40
41# The deadline for reporting we're not in an infinite loop.
42il_time = 0
43
44
45def check_infinite_loop():
46    global il_statements
47
48    il_statements += 1
49
50    if il_statements <= 1000:
51        return
52
53    il_statements = 0
54
55    global il_time
56
57    now = time.time()
58
59    if now > il_time:
60        il_time = now + 60
61        raise Exception("Possible infinite loop.")
62
63    if renpy.config.developer and (il_time > now + 60):
64        il_time = now + 60
65
66    return
67
68
69def not_infinite_loop(delay):
70    """
71    :doc: other
72
73    Resets the infinite loop detection timer to `delay` seconds.
74    """
75
76    # Give more time in non-developer mode, since computers can be crazy slow
77    # and the player can't do much about it.
78    if not renpy.config.developer:
79        delay *= 5
80
81    global il_time
82    il_time = time.time() + delay
83
84
85class Delete(object):
86    pass
87
88
89class PredictInfo(renpy.object.Object):
90    """
91    Not used anymore, but needed for backwards compatibility.
92    """
93
94
95class LineLogEntry(object):
96
97    def __init__(self, filename, line, node, abnormal):
98        self.filename = filename
99        self.line = line
100        self.node = node
101        self.abnormal = abnormal
102
103        for i in renpy.config.line_log_callbacks:
104            i(self)
105
106    def __eq__(self, other):
107        if not isinstance(other, LineLogEntry):
108            return False
109
110        return (self.filename == other.filename) and (self.line == other.line) and (self.node is other.node)
111
112    def __ne__(self, other):
113        return not (self == other)
114
115
116class Context(renpy.object.Object):
117    """
118    This is the context object which stores the current context
119    of the game interpreter.
120
121    @ivar current: The name of the node that is currently being
122    executed.
123
124    @ivar return_stack: A list of names of nodes that should be
125    returned to when the return statement executes. (When a return
126    occurs, the name is looked up, and name.text is then executed.)
127
128    @ivar scene_lists: The scene lists associated with the current
129    context.
130
131    @ivar rollback: True if this context participates in rollbacks.
132
133    @ivar runtime: The time spent in this context, in milliseconds.
134
135    @ivar info: An object that is made available to user code. This object
136    does participates in rollback.
137    """
138
139    __version__ = 16
140
141    nosave = [ 'next_node' ]
142
143    next_node = None
144
145    force_checkpoint = False
146
147    come_from_name = None
148    come_from_label = None
149
150    temporary_attributes = None
151
152    deferred_translate_identifier = None
153
154    def __repr__(self):
155
156        if not self.current:
157            return "<Context>"
158
159        node = renpy.game.script.lookup(self.current)
160
161        return "<Context: {}:{} {!r}>".format(
162            node.filename,
163            node.linenumber,
164            node.diff_info(),
165            )
166
167    def after_upgrade(self, version):
168        if version < 1:
169            self.scene_lists.image_predict_info = self.predict_info.images
170
171        if version < 2:
172            self.abnormal = False
173            self.last_abnormal = False
174
175        if version < 3:
176            self.music = { }
177
178        if version < 4:
179            self.interacting = False
180
181        if version < 5:
182            self.modes = renpy.python.RevertableList([ "start" ])
183            self.use_modes = True
184
185        if version < 6:
186            self.images = self.predict_info.images
187
188        if version < 7:
189            self.init_phase = False
190            self.next_node = None
191
192        if version < 8:
193            self.defer_rollback = None
194
195        if version < 9:
196            self.translate_language = None
197            self.translate_identifier = None
198
199        if version < 10:
200            self.exception_handler = None
201
202        if version < 11:
203            self.say_attributes = None
204
205        if version < 13:
206            self.line_log = [ ]
207
208        if version < 14:
209            self.movie = { }
210
211        if version < 15:
212            self.abnormal_stack = [ False ] * len(self.return_stack)
213
214        if version < 16:
215            self.alternate_translate_identifier = None
216
217    def __init__(self, rollback, context=None, clear=False):
218        """
219        `clear`
220            True if we should clear out the context_clear_layers.
221        """
222
223        super(Context, self).__init__()
224
225        self.current = None
226        self.call_location_stack = [ ]
227        self.return_stack = [ ]
228
229        # The value of abnormal at the time of the call.
230        self.abnormal_stack = [ ]
231
232        # Two deeper then the return stack and call location stack.
233        # 1 deeper is for the context top-level, 2 deeper is for
234        # _args, _kwargs, and _return.
235        self.dynamic_stack = [ { } ]
236
237        self.rollback = rollback
238        self.runtime = 0
239        self.info = renpy.python.RevertableObject()
240        self.seen = False
241
242        # True if there has just been an abnormal transfer of control,
243        # like the start of a context, a jump, or a call. (Returns are
244        # considered to be normal.)
245        #
246        # Set directly by ast.Call and ast.Jump.
247        self.abnormal = True
248
249        # True if the last statement caused an abnormal transfer of
250        # control.
251        self.last_abnormal = False
252
253        # A map from the name of a music channel to the MusicContext
254        # object corresponding to that channel.
255        self.music = { }
256
257        # True if we're in the middle of a call to ui.interact. This
258        # will cause Ren'Py to generate an error if we call ui.interact
259        # again.
260        self.interacting = False
261
262        # True if we're in the init phase. (Isn't inherited.)
263        self.init_phase = False
264
265        # When deferring a rollback, the arguments to pass to renpy.exports.rollback.
266        self.defer_rollback = None
267
268        # The exception handler that is called when an exception occurs while executing
269        # code. If None, a default handler is used. This is reset when run is called.
270        self.exception_handler = None
271
272        # The attributes that are used by the current say statement.
273        self.say_attributes = None
274        self.temporary_attributes = None
275
276        # A list of lines that were run since the last time this log was
277        # cleared.
278        self.line_log = [ ]
279
280        # Do we want to force a checkpoint before the next statement
281        # executed?
282        self.force_checkpoint = False
283
284        # A map from a channel to the Movie playing on that channel.
285        self.movie = { }
286
287        if context:
288            oldsl = context.scene_lists
289            self.runtime = context.runtime
290
291            vars(self.info).update(vars(context.info))
292
293            self.music = dict(context.music)
294            self.movie = dict(context.movie)
295
296            self.images = renpy.display.image.ShownImageInfo(context.images)
297
298        else:
299            oldsl = None
300            self.images = renpy.display.image.ShownImageInfo(None)
301
302        self.scene_lists = renpy.display.core.SceneLists(oldsl, self.images)
303
304        for i in renpy.config.context_copy_remove_screens:
305            self.scene_lists.remove("screens", i, None)
306
307        self.make_dynamic([ "_return", "_args", "_kwargs", "mouse_visible", "suppress_overlay", "_side_image_attributes" ])
308        self.dynamic_stack.append({ })
309
310        if clear:
311            for i in renpy.config.context_clear_layers:
312                self.scene_lists.clear(layer=i)
313
314        # A list of modes that the context has been in.
315        self.modes = renpy.python.RevertableList([ "start" ])
316        self.use_modes = True
317
318        # The language we started with.
319        self.translate_language = renpy.game.preferences.language
320
321        # The identifier of the current translate block.
322        self.translate_identifier = None
323
324        # The alternate identifier of the current translate block.
325        self.alternate_translate_identifier = None
326
327        # The translate identifier of the last say statement with
328        # interact = False.
329        self.deferred_translate_identifier = None
330
331    def replace_node(self, old, new):
332
333        def replace_one(name):
334            n = renpy.game.script.lookup(name)
335            if n is old:
336                return new.name
337
338            return name
339
340        self.current = replace_one(self.current)
341        self.return_stack = [ replace_one(i) for i in self.return_stack ]
342
343    def make_dynamic(self, names, context=False):
344        """
345        Makes the variable names listed in names dynamic, by backing up
346        their current value (if not already dynamic in the current call).
347        """
348
349        store = renpy.store.__dict__
350
351        if context:
352            index = 0
353        else:
354            index = -1
355
356        for i in names:
357
358            if i in self.dynamic_stack[index]:
359                continue
360
361            if i in store:
362                self.dynamic_stack[index][i] = store[i]
363            else:
364                self.dynamic_stack[index][i] = Delete()
365
366    def pop_dynamic(self):
367        """
368        Pops one level of the dynamic stack. Called when the return
369        statement is run.
370        """
371
372        if not self.dynamic_stack:
373            return
374
375        store = renpy.store.__dict__
376
377        dynamic = self.dynamic_stack.pop()
378
379        for k, v in dynamic.items():
380            if isinstance(v, Delete):
381                store.pop(k, None)
382            else:
383                store[k] = v
384
385    def pop_all_dynamic(self):
386        """
387        Pops all levels of the dynamic stack. Called when we jump
388        out of a context.
389        """
390
391        while self.dynamic_stack:
392            self.pop_dynamic()
393
394    def pop_dynamic_roots(self, roots):
395
396        for dynamic in reversed(self.dynamic_stack):
397
398            for k, v in dynamic.items():
399                name = "store." + k
400
401                if isinstance(v, Delete) and (name in roots):
402                    del roots[name]
403                else:
404                    roots[name] = v
405
406    def goto_label(self, node_name):
407        """
408        Sets the name of the node that will be run when this context
409        next executes.
410        """
411
412        self.current = node_name
413
414    def check_stacks(self):
415        """
416        Check and fix stack corruption.
417        """
418
419        if len(self.dynamic_stack) != len(self.return_stack) + 2:
420
421            e = Exception("Potential return stack corruption: dynamic={} return={}".format(len(self.dynamic_stack), len(self.return_stack)))
422
423            while len(self.dynamic_stack) < len(self.return_stack) + 2:
424                self.dynamic_stack.append({})
425
426            while len(self.dynamic_stack) > len(self.return_stack) + 2:
427                self.pop_dynamic()
428
429            raise e
430
431    def report_traceback(self, name, last):
432
433        if last:
434            return
435
436        rv = [ ]
437
438        for i in self.call_location_stack:
439            try:
440                node = renpy.game.script.lookup(i)
441                if not node.filename.replace("\\", "/").startswith("common/"):
442                    rv.append((node.filename, node.linenumber, "script call", None))
443            except:
444                pass
445
446        try:
447            node = renpy.game.script.lookup(self.current)
448            if not node.filename.replace("\\", "/").startswith("common/"):
449                rv.append((node.filename, node.linenumber, "script", None))
450        except:
451            pass
452
453        return rv
454
455    def report_coverage(self, node):
456        """
457        Execs a python pass statement on the line of code corresponding to
458        `node`. This indicates to python coverage tools that this line has
459        been executed.
460        """
461
462        ps = pyast.Pass(lineno=node.linenumber, col_offset=0)
463        module = pyast.Module(lineno=node.linenumber, col_offset=0, body=[ ps ])
464        code = compile(module, node.filename, 'exec')
465        exec(code)
466
467    def come_from(self, name, label):
468        """
469        When control reaches name, call label. Only for internal use.
470        """
471
472        self.come_from_name = name
473        self.come_from_label = label
474
475    def run(self, node=None):
476        """
477        Executes as many nodes as possible in the current context. If the
478        node argument is given, starts executing from that node. Otherwise,
479        looks up the node given in self.current, and executes from there.
480        """
481
482        self.exception_handler = None
483
484        self.abnormal = True
485
486        if node is None:
487            node = renpy.game.script.lookup(self.current)
488
489        developer = renpy.config.developer
490        tracing = sys.gettrace() is not None
491
492        # Is this the first time through the loop?
493        first = True
494
495        while node:
496
497            if node.name == self.come_from_name:
498                self.come_from_name = None
499                node = self.call(self.come_from_label, return_site=node.name)
500                self.make_dynamic([ "_return", "_begin_rollback" ])
501                renpy.store._begin_rollback = False
502
503            this_node = node
504            type_node_name = type(node).__name__
505
506            renpy.plog(1, "--- start {} ({}:{})", type_node_name, node.filename, node.linenumber)
507
508            self.current = node.name
509            self.last_abnormal = self.abnormal
510            self.abnormal = False
511            self.defer_rollback = None
512
513            if renpy.config.line_log:
514                ll_entry = LineLogEntry(node.filename, node.linenumber, node, self.last_abnormal)
515
516                if ll_entry not in self.line_log:
517                    self.line_log.append(ll_entry)
518
519            if not renpy.store._begin_rollback:
520                update_rollback = False
521                force_rollback = False
522            elif first or self.force_checkpoint or (node.rollback == "force"):
523                update_rollback = True
524                force_rollback = True
525            elif not renpy.config.all_nodes_rollback and (node.rollback == "never"):
526                update_rollback = False
527                force_rollback = False
528            else:
529                update_rollback = True
530                force_rollback = False
531
532            # Force a new rollback to start to match things in the forward log.
533            if renpy.game.log.forward and renpy.game.log.forward[0][0] == node.name:
534                update_rollback = True
535                force_rollback = True
536
537            first = False
538
539            if update_rollback:
540
541                if self.rollback and renpy.game.log:
542                    renpy.game.log.begin(force=force_rollback)
543
544                if self.rollback and self.force_checkpoint:
545                    renpy.game.log.force_checkpoint = True
546                    self.force_checkpoint = False
547
548            self.seen = False
549
550            renpy.test.testexecution.take_name(self.current)
551
552            try:
553                try:
554                    check_infinite_loop()
555
556                    if tracing:
557                        self.report_coverage(node)
558
559                    renpy.game.exception_info = "While running game code:"
560
561                    self.next_node = None
562
563                    renpy.plog(2, "    before execute {} ({}:{})", type_node_name, node.filename, node.linenumber)
564
565                    node.execute()
566
567                    renpy.plog(2, "    after execute {} ({}:{})", type_node_name, node.filename, node.linenumber)
568
569                    if developer and self.next_node:
570                        self.check_stacks()
571
572                except renpy.game.CONTROL_EXCEPTIONS as e:
573
574                    # An exception ends the current translation.
575                    self.translate_interaction = None
576
577                    raise
578
579                except Exception as e:
580                    self.translate_interaction = None
581
582                    exc_info = sys.exc_info()
583                    short, full, traceback_fn = renpy.error.report_exception(e, editor=False)
584
585                    try:
586                        handled = False
587
588                        if self.exception_handler is not None:
589                            self.exception_handler(short, full, traceback_fn)
590                            handled = True
591                        elif renpy.config.exception_handler is not None:
592                            handled = renpy.config.exception_handler(short, full, traceback_fn)
593
594                        if not handled:
595                            if renpy.display.error.report_exception(short, full, traceback_fn):
596                                raise
597                    except renpy.game.CONTROL_EXCEPTIONS as ce:
598                        raise ce
599                    except Exception as ce:
600                        reraise(exc_info[0], exc_info[1], exc_info[2])
601
602                node = self.next_node
603
604            except renpy.game.JumpException as e:
605                node = renpy.game.script.lookup(e.args[0])
606                self.abnormal = True
607
608            except renpy.game.CallException as e:
609
610                if e.from_current:
611                    return_site = getattr(node, "statement_start", node).name
612                else:
613                    if self.next_node is None:
614                        raise Exception("renpy.call can't be used when the next node is undefined.")
615                    return_site = self.next_node.name
616
617                node = self.call(e.label, return_site=return_site)
618                self.abnormal = True
619                renpy.store._args = e.args
620                renpy.store._kwargs = e.kwargs
621
622            if self.seen:
623                renpy.game.persistent._seen_ever[self.current] = True # @UndefinedVariable
624                renpy.game.seen_session[self.current] = True
625
626            renpy.plog(2, "    end {} ({}:{})", type_node_name, this_node.filename, this_node.linenumber)
627
628        if self.rollback and renpy.game.log:
629            renpy.game.log.complete()
630
631    def mark_seen(self):
632        """
633        Marks the current statement as one that has been seen by the user.
634        """
635
636        self.seen = True
637
638    def call(self, label, return_site=None):
639        """
640        Calls the named label.
641        """
642
643        if not self.current:
644            raise Exception("Context not capable of executing Ren'Py code.")
645
646        if return_site is None:
647            return_site = self.current
648
649        self.call_location_stack.append(self.current)
650
651        self.return_stack.append(return_site)
652        self.dynamic_stack.append({ })
653        self.abnormal_stack.append(self.last_abnormal)
654        self.current = label
655
656        self.make_dynamic([ "_args", "_kwargs" ])
657        renpy.store._args = None
658        renpy.store._kwargs = None
659
660        return renpy.game.script.lookup(label)
661
662    def pop_call(self):
663        """
664        Blindly pops the top call record from the stack.
665        """
666
667        if not self.return_stack:
668            if renpy.config.developer:
669                raise Exception("No call on call stack.")
670
671            return
672
673        self.return_stack.pop()
674        self.call_location_stack.pop()
675        self.pop_dynamic()
676        self.abnormal_stack.pop()
677
678    def lookup_return(self, pop=True):
679        """
680        Returns the node to return to, or None if there is no
681        such node.
682        """
683
684        while self.return_stack:
685
686            node = None
687
688            if renpy.game.script.has_label(self.return_stack[-1]):
689                node = renpy.game.script.lookup(self.return_stack[-1])
690            elif renpy.game.script.has_label(self.call_location_stack[-1]):
691                node = renpy.game.script.lookup(self.call_location_stack[-1]).next
692
693            if node is None:
694
695                if not pop:
696                    return None
697
698                # If we can't find anything, try to recover.
699
700                if renpy.config.return_not_found_label:
701
702                    while len(self.return_stack) > 1:
703                        self.pop_call()
704
705                    node = renpy.game.script.lookup(renpy.config.return_not_found_label)
706
707                else:
708
709                    if renpy.config.developer:
710                        raise Exception("Could not find return label {!r}.".format(self.return_stack[-1]))
711
712                    self.return_stack.pop()
713                    self.call_location_stack.pop()
714                    self.pop_dynamic()
715                    self.abnormal = self.abnormal_stack.pop()
716
717                    continue
718
719            if pop:
720                self.return_stack.pop()
721                self.call_location_stack.pop()
722                self.abnormal = self.abnormal_stack.pop()
723
724            return node
725
726        return None
727
728    def rollback_copy(self):
729        """
730        Makes a copy of this object, suitable for rolling back to.
731        """
732
733        rv = Context(self.rollback, self)
734        rv.call_location_stack = self.call_location_stack[:]
735        rv.return_stack = self.return_stack[:]
736        rv.dynamic_stack = [ i.copy() for i in self.dynamic_stack ]
737        rv.current = self.current
738
739        rv.runtime = self.runtime
740        rv.info = self.info
741
742        rv.translate_language = self.translate_language
743        rv.translate_identifier = self.translate_identifier
744
745        rv.abnormal = self.abnormal
746        rv.last_abnormal = self.last_abnormal
747        rv.abnormal_stack = list(self.abnormal_stack)
748
749        return rv
750
751    def predict_call(self, label, return_site):
752        """
753        This is called by the prediction code to indicate that a call to
754        `label` will occur.
755
756        `return_site`
757            The name of the return site to push on the predicted return
758            stack.
759
760        Returns the node corresponding to `label`
761        """
762
763        self.predict_return_stack = list(self.predict_return_stack)
764        self.predict_return_stack.append(return_site)
765
766        return renpy.game.script.lookup(label)
767
768    def predict_return(self):
769        """
770        This predicts that a return will occur.
771
772        It returns the node we predict will be returned to.
773        """
774
775        if not self.predict_return_stack:
776            return None
777
778        self.predict_return_stack = list(self.predict_return_stack)
779        label = self.predict_return_stack.pop()
780
781        return renpy.game.script.lookup(label)
782
783    def predict(self):
784        """
785        Performs image prediction, calling the given callback with each
786        images that we predict to be loaded, in the rough order that
787        they will be potentially loaded.
788        """
789
790        if not self.current:
791            return
792
793        if renpy.config.predict_statements_callback is None:
794            return
795
796        old_images = self.images
797
798        # A worklist of (node, images, return_stack) tuples.
799        nodes = [ ]
800
801        # The set of nodes we've seen. (We only consider each node once.)
802        seen = set()
803
804        # Find the roots.
805        for label in renpy.config.predict_statements_callback(self.current):
806
807            if not renpy.game.script.has_label(label):
808                continue
809
810            node = renpy.game.script.lookup(label)
811
812            if node in seen:
813                continue
814
815            nodes.append((node, self.images, self.return_stack))
816            seen.add(node)
817
818        # Predict statements.
819        for i in range(0, renpy.config.predict_statements):
820
821            if i >= len(nodes):
822                break
823
824            node, images, return_stack = nodes[i]
825
826            self.images = renpy.display.image.ShownImageInfo(images)
827            self.predict_return_stack = return_stack
828
829            try:
830
831                for n in node.predict():
832                    if n is None:
833                        continue
834
835                    if n not in seen:
836                        nodes.append((n, self.images, self.predict_return_stack))
837                        seen.add(n)
838
839            except:
840
841                if renpy.config.debug_prediction:
842                    import traceback
843
844                    print("While predicting images.")
845                    traceback.print_exc()
846                    print()
847
848            self.images = old_images
849            self.predict_return_stack = None
850
851            yield True
852
853        yield False
854
855    def seen_current(self, ever):
856        """
857        Returns a true value if we have finshed the current statement
858        at least once before.
859
860        @param ever: If True, we're checking to see if we've ever
861        finished this statement. If False, we're checking to see if
862        we've finished this statement in the current session.
863        """
864
865        if not self.current:
866            return False
867
868        if ever:
869            seen = renpy.game.persistent._seen_ever # @UndefinedVariable
870        else:
871            seen = renpy.game.seen_session
872
873        return self.current in seen
874
875    def do_deferred_rollback(self):
876        """
877        Called to cause deferred rollback to occur.
878        """
879
880        if not self.defer_rollback:
881            return
882
883        force, checkpoints = self.defer_rollback
884
885        self.defer_rollback = None
886
887        renpy.exports.rollback(force, checkpoints)
888
889    def get_return_stack(self):
890        return list(self.return_stack)
891
892    def set_return_stack(self, return_stack):
893        self.return_stack = list(return_stack)
894
895        while len(self.call_location_stack) > len(self.return_stack):
896            self.call_location_stack.pop()
897
898            d = self.dynamic_stack.pop()
899            d.update(self.dynamic_stack[-1])
900            self.dynamic_stack[-1] = d
901
902        while len(self.call_location_stack) < len(self.return_stack):
903            self.call_location_stack.append("unknown location")
904            self.dynamic_stack.append({})
905
906
907def run_context(top):
908    """
909    Runs the current context until it can't be run anymore, while handling
910    the RestartContext and RestartTopContext exceptions.
911    """
912
913    if renpy.config.context_callback is not None:
914        renpy.config.context_callback()
915
916    while True:
917
918        try:
919
920            context = renpy.game.context()
921
922            context.run()
923
924            rv = renpy.store._return
925
926            context.pop_all_dynamic()
927
928            return rv
929
930        except renpy.game.RestartContext as e:
931            continue
932
933        except renpy.game.RestartTopContext as e:
934            if top:
935                continue
936
937            else:
938                raise
939
940        except:
941            context.pop_all_dynamic()
942            raise
943