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