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 that handles the execution of python code 23# contained within the script file. It also handles rolling back the 24# game state to some time in the past. 25 26from __future__ import division, absolute_import, with_statement, print_function, unicode_literals 27from renpy.compat import * 28 29# Import the python ast module, not ours. 30import ast 31 32# Import the future module itself. 33import __future__ 34 35import marshal 36import random 37import weakref 38import re 39import sys 40import time 41import io 42import types 43import copyreg 44import functools 45 46import renpy.audio 47 48# A set of flags that indicate dict should run in future-compatible mode. 49FUTURE_FLAGS = (__future__.CO_FUTURE_DIVISION | __future__.CO_FUTURE_WITH_STATEMENT) 50 51############################################################################## 52# Monkeypatch copy_reg to work around a change in the class that RevertableSet 53# is based on. 54 55 56def _reconstructor(cls, base, state): 57 if (cls is RevertableSet) and (base is object): 58 base = set 59 state = [ ] 60 61 if base is object: 62 obj = object.__new__(cls) 63 else: 64 obj = base.__new__(cls, state) 65 if base.__init__ != object.__init__: 66 base.__init__(obj, state) 67 68 return obj 69 70 71copyreg._reconstructor = _reconstructor 72 73############################################################################## 74# Code that implements the store. 75 76# Deleted is a singleton object that's used to represent an object that has 77# been deleted from the store. 78 79 80class StoreDeleted(object): 81 82 def __reduce__(self): 83 if PY2: 84 return b"deleted" 85 else: 86 return "deleted" 87 88 89deleted = StoreDeleted() 90 91 92class StoreModule(object): 93 """ 94 This class represents one of the modules containing the store of data. 95 """ 96 97 # Set our dict to be the StoreDict. Then proxy over setattr and delattr, 98 # since Python won't call them by default. 99 100 def __reduce__(self): 101 return (get_store_module, (self.__name__,)) 102 103 def __init__(self, d): 104 object.__setattr__(self, "__dict__", d) 105 106 def __setattr__(self, key, value): 107 self.__dict__[key] = value 108 109 def __delattr__(self, key): 110 del self.__dict__[key] 111 112# Used to unpickle a store module. 113 114 115def get_store_module(name): 116 return sys.modules[name] 117 118 119from renpy.pydict import DictItems, find_changes 120 121EMPTY_DICT = { } 122EMPTY_SET = set() 123 124 125class StoreDict(dict): 126 """ 127 This class represents the dictionary of a store module. It logs 128 sets and deletes. 129 """ 130 131 def __reduce__(self): 132 raise Exception("Cannot pickle a reference to a store dictionary.") 133 134 def __init__(self): 135 136 # The value of this dictionary at the start of the current 137 # rollback period (when begin() was last called). 138 self.old = DictItems(self) 139 140 # The set of variables in this StoreDict that changed since the 141 # end of the init phase. 142 self.ever_been_changed = set() 143 144 def reset(self): 145 """ 146 Called to reset this to its initial conditions. 147 """ 148 149 self.ever_been_changed = set() 150 self.clear() 151 self.old = DictItems(self) 152 153 def begin(self): 154 """ 155 Called to mark the start of a rollback period. 156 """ 157 158 self.old = DictItems(self) 159 160 def get_changes(self, cycle): 161 """ 162 For every key that has changed since begin() was called, returns a 163 dictionary mapping the key to its value when begin was called, or 164 deleted if it did not exist when begin was called. 165 166 As a side-effect, updates self.ever_been_changed, and returns the 167 changes to ever_been_changed as well. 168 169 `cycle` 170 If true, this cycles the old changes to the new changes. If 171 False, does not. 172 """ 173 174 new = DictItems(self) 175 rv = find_changes(self.old, new, deleted) 176 177 if cycle: 178 self.old = new 179 180 if rv is None: 181 return EMPTY_DICT, EMPTY_SET 182 183 delta_ebc = set() 184 185 if cycle: 186 187 for k in rv: 188 if k not in self.ever_been_changed: 189 self.ever_been_changed.add(k) 190 delta_ebc.add(k) 191 192 return rv, delta_ebc 193 194 195def begin_stores(): 196 """ 197 Calls .begin on every store dict. 198 """ 199 200 for sd in store_dicts.values(): 201 sd.begin() 202 203 204# A map from the name of a store dict to the corresponding StoreDict object. 205# This isn't reset during a reload, so store objects stay the same in modules. 206store_dicts = { } 207 208# Same, for module objects. 209store_modules = { } 210 211# The store dicts that have been cleared and initialized during the current 212# run. 213initialized_store_dicts = set() 214 215 216def create_store(name): 217 """ 218 Creates the store with `name`. 219 """ 220 221 parent, _, var = name.rpartition('.') 222 223 if parent: 224 create_store(parent) 225 226 name = str(name) 227 228 if name in initialized_store_dicts: 229 return 230 231 initialized_store_dicts.add(name) 232 233 # Create the dict. 234 d = store_dicts.setdefault(name, StoreDict()) 235 d.reset() 236 237 pyname = pystr(name) 238 239 # Set the name. 240 d["__name__"] = pyname 241 d["__package__"] = pyname 242 243 # Set up the default contents of the store. 244 eval("1", d) 245 246 for k, v in renpy.minstore.__dict__.items(): 247 if k not in d: 248 d[k] = v 249 250 # Create or reuse the corresponding module. 251 if name in store_modules: 252 sys.modules[pyname] = store_modules[name] 253 else: 254 store_modules[name] = sys.modules[pyname] = StoreModule(d) 255 256 if parent: 257 store_dicts[parent][var] = sys.modules[pyname] 258 259 260class StoreBackup(): 261 """ 262 This creates a copy of the current store, as it was at the start of 263 the current statement. 264 """ 265 266 def __init__(self): 267 268 # The contents of the store for each store. 269 self.store = { } 270 271 # The contents of old for each store. 272 self.old = { } 273 274 # The contents of ever_been_changed for each store. 275 self.ever_been_changed = { } 276 277 for k in store_dicts: 278 self.backup_one(k) 279 280 def backup_one(self, name): 281 282 d = store_dicts[name] 283 284 self.store[name] = dict(d) 285 self.old[name] = d.old.as_dict() 286 self.ever_been_changed[name] = set(d.ever_been_changed) 287 288 def restore_one(self, name): 289 sd = store_dicts[name] 290 291 sd.clear() 292 sd.update(self.store[name]) 293 294 sd.old = DictItems(self.old[name]) 295 296 sd.ever_been_changed.clear() 297 sd.ever_been_changed.update(self.ever_been_changed[name]) 298 299 def restore(self): 300 301 for k in store_dicts: 302 self.restore_one(k) 303 304 305clean_store_backup = None 306 307 308def make_clean_stores(): 309 """ 310 Copy the clean stores. 311 """ 312 313 global clean_store_backup 314 315 for _k, v in store_dicts.items(): 316 317 v.ever_been_changed.clear() 318 v.begin() 319 320 clean_store_backup = StoreBackup() 321 322 323def clean_stores(): 324 """ 325 Revert the store to the clean copy. 326 """ 327 328 clean_store_backup.restore() 329 330 331def clean_store(name): 332 """ 333 Reverts the named store to its clean copy. 334 """ 335 336 if not name.startswith("store."): 337 name = "store." + name 338 339 clean_store_backup.restore_one(name) 340 341 342def reset_store_changes(name): 343 344 if not name.startswith("store."): 345 name = "store." + name 346 347 sd = store_dicts[name] 348 sd.begin() 349 350# Code that computes reachable objects, which is used to filter 351# the rollback list before rollback or serialization. 352 353 354class NoRollback(object): 355 """ 356 :doc: norollback class 357 358 Instances of classes inheriting from this class do not participate in 359 rollback. Objects reachable through an instance of a NoRollback class 360 only participate in rollback if they are reachable through other paths. 361 """ 362 363 pass 364 365# parents = [ ] 366 367 368def reached(obj, reachable, wait): 369 """ 370 @param obj: The object that was reached. 371 372 `reachable` 373 A map from id(obj) to int. The int is 1 if the object was reached 374 normally, and 0 if it was reached, but inherits from NoRollback. 375 """ 376 377 if wait: 378 wait() 379 380 idobj = id(obj) 381 382 if idobj in reachable: 383 return 384 385 if isinstance(obj, (NoRollback, io.IOBase)): # @UndefinedVariable 386 reachable[idobj] = 0 387 return 388 389 reachable[idobj] = 1 390 391 # Since the store module is the roots, there's no need to 392 # look into it. 393 if isinstance(obj, StoreModule): 394 return 395 396 # parents.append(obj) 397 398 try: 399 # Treat as fields, indexed by strings. 400 for v in vars(obj).values(): 401 reached(v, reachable, wait) 402 except: 403 pass 404 405 try: 406 # Treat as iterable 407 if not isinstance(obj, basestring): 408 for v in obj.__iter__(): 409 reached(v, reachable, wait) 410 except: 411 pass 412 413 try: 414 # Treat as dict. 415 for v in obj.values(): 416 reached(v, reachable, wait) 417 except: 418 pass 419 420 # parents.pop() 421 422 423def reached_vars(store, reachable, wait): 424 """ 425 Marks everything reachable from the variables in the store 426 or from the context info objects as reachable. 427 428 @param store: A map from variable name to variable value. 429 @param reachable: A dictionary mapping reached object ids to 430 the path by which the object was reached. 431 """ 432 433 for v in store.values(): 434 reached(v, reachable, wait) 435 436 for c in renpy.game.contexts: 437 reached(c.info, reachable, wait) 438 reached(c.music, reachable, wait) 439 for d in c.dynamic_stack: 440 for v in d.values(): 441 reached(v, reachable, wait) 442 443# Code that replaces literals will calls to magic constructors. 444 445 446def b(s): 447 if PY2: 448 return s.encode("utf-8") 449 else: 450 return s 451 452 453class WrapNode(ast.NodeTransformer): 454 455 def visit_ClassDef(self, n): 456 n = self.generic_visit(n) 457 458 if not n.bases: 459 n.bases.append(ast.Name(id=b("object"), ctx=ast.Load())) 460 461 return n 462 463 def visit_SetComp(self, n): 464 return ast.Call( 465 func=ast.Name( 466 id=b("__renpy__set__"), 467 ctx=ast.Load() 468 ), 469 args=[ self.generic_visit(n) ], 470 keywords=[ ], 471 starargs=None, 472 kwargs=None) 473 474 def visit_Set(self, n): 475 476 return ast.Call( 477 func=ast.Name( 478 id=b("__renpy__set__"), 479 ctx=ast.Load() 480 ), 481 args=[ self.generic_visit(n) ], 482 keywords=[ ], 483 starargs=None, 484 kwargs=None) 485 486 def visit_ListComp(self, n): 487 return ast.Call( 488 func=ast.Name( 489 id=b("__renpy__list__"), 490 ctx=ast.Load() 491 ), 492 args=[ self.generic_visit(n) ], 493 keywords=[ ], 494 starargs=None, 495 kwargs=None) 496 497 def visit_List(self, n): 498 if not isinstance(n.ctx, ast.Load): 499 return self.generic_visit(n) 500 501 return ast.Call( 502 func=ast.Name( 503 id=b("__renpy__list__"), 504 ctx=ast.Load() 505 ), 506 args=[ self.generic_visit(n) ], 507 keywords=[ ], 508 starargs=None, 509 kwargs=None) 510 511 def visit_DictComp(self, n): 512 return ast.Call( 513 func=ast.Name( 514 id=b("__renpy__dict__"), 515 ctx=ast.Load() 516 ), 517 args=[ self.generic_visit(n) ], 518 keywords=[ ], 519 starargs=None, 520 kwargs=None) 521 522 def visit_Dict(self, n): 523 524 return ast.Call( 525 func=ast.Name( 526 id=b("__renpy__dict__"), 527 ctx=ast.Load() 528 ), 529 args=[ self.generic_visit(n) ], 530 keywords=[ ], 531 starargs=None, 532 kwargs=None) 533 534 535wrap_node = WrapNode() 536 537 538def wrap_hide(tree): 539 """ 540 Wraps code inside a python hide or python early hide block inside a 541 function, so it gets its own scope that works the way Python expects 542 it to. 543 """ 544 545 hide = ast.parse("""\ 546def _execute_python_hide(): pass; 547_execute_python_hide() 548""") 549 550 for i in ast.walk(hide): 551 ast.copy_location(i, hide.body[0]) 552 553 hide.body[0].body = tree.body 554 tree.body = hide.body 555 556 557unicode_re = re.compile(r'[\u0080-\uffff]') 558 559 560def unicode_sub(m): 561 """ 562 If the string s contains a unicode character, make it into a 563 unicode string. 564 """ 565 566 s = m.group(0) 567 568 if not unicode_re.search(s): 569 return s 570 571 prefix = m.group(1) 572 sep = m.group(2) 573 body = m.group(3) 574 575 if "u" not in prefix and "U" not in prefix: 576 prefix = 'u' + prefix 577 578 rv = prefix + sep + body + sep 579 580 return rv 581 582 583string_re = re.compile(r'([uU]?[rR]?)("""|"|\'\'\'|\')((\\.|.)*?)\2') 584 585 586def escape_unicode(s): 587 if unicode_re.search(s): 588 s = string_re.sub(unicode_sub, s) 589 590 return s 591 592 593# Flags used by py_compile. 594old_compile_flags = (__future__.nested_scopes.compiler_flag 595 | __future__.with_statement.compiler_flag 596 ) 597 598new_compile_flags = (old_compile_flags 599 | __future__.absolute_import.compiler_flag 600 | __future__.print_function.compiler_flag 601 | __future__.unicode_literals.compiler_flag 602 ) 603 604py3_compile_flags = (new_compile_flags | 605 __future__.division.compiler_flag) 606 607# The set of files that should be compiled under Python 2 with Python 3 608# semantics. 609py3_files = set() 610 611# A cache for the results of py_compile. 612py_compile_cache = { } 613 614# An old version of the same, that's preserved across reloads. 615old_py_compile_cache = { } 616 617 618# Duplicated from ast.py to prevent a gc cycle. 619def fix_missing_locations(node, lineno, col_offset): 620 if 'lineno' in node._attributes: 621 if not hasattr(node, 'lineno'): 622 node.lineno = lineno 623 else: 624 lineno = node.lineno 625 if 'col_offset' in node._attributes: 626 if not hasattr(node, 'col_offset'): 627 node.col_offset = col_offset 628 else: 629 col_offset = node.col_offset 630 for child in ast.iter_child_nodes(node): 631 fix_missing_locations(child, lineno, col_offset) 632 633 634def quote_eval(s): 635 """ 636 Quotes a string for `eval`. This is necessary when it's in certain places, 637 like as part of an argument string. We need to stick \ at the end of lines 638 that don't have it already, and that aren't in triple-quoted strings. 639 """ 640 641 # No newlines! No problem. 642 if "\n" not in s: 643 return s 644 645 # Characters being added to the string. 646 rv = [ ] 647 648 # Pad out the string, so we don't have to deal with quotes at the end. 649 s += "\0\0" 650 651 len_s = len(s) 652 653 # The index into the string. 654 i = 0 655 656 # Special characters, that aren't just copied into the string. 657 special = "\0\\'\"\n" 658 659 # The string currently being processed. 660 string = None 661 662 while i < len_s: 663 664 c = s[i] 665 666 # Non-special characters. 667 if c not in special: 668 start = i 669 670 while True: 671 i += 1 672 if s[i] in special: 673 break 674 675 rv.append(s[start:i]) 676 continue 677 678 # Null. 679 if c == '\0': 680 rv.append(c) 681 i += 1 682 continue 683 684 # Any escaped character passes. 685 if c == '\\': 686 rv.append(s[i:i + 2]) 687 i += 2 688 continue 689 690 # String delimiters. 691 if c in '\'"': 692 693 if ((string is None) or (len(string) == 3)) and (s[i + 1] == c) and (s[i + 2] == c): 694 delim = c + c + c 695 else: 696 delim = c 697 698 if (string is not None) and (delim == string): 699 string = None 700 elif string is None: 701 string = delim 702 703 rv.append(delim) 704 i += len(delim) 705 706 continue 707 708 # Newline. 709 if c == "\n": 710 if string is None: 711 rv.append('\\') 712 713 rv.append("\n") 714 i += 1 715 continue 716 717 raise Exception("Unknown character %r (can't happen)".format(c)) 718 719 # Since the last 2 characters are \0, those characters need to be stripped. 720 return "".join(rv[:-2]) 721 722 723def py_compile(source, mode, filename='<none>', lineno=1, ast_node=False, cache=True): 724 """ 725 Compiles the given source code using the supplied codegenerator. 726 Lists, List Comprehensions, and Dictionaries are wrapped when 727 appropriate. 728 729 `source` 730 The source code, as a either a string, pyexpr, or ast module 731 node. 732 733 `mode` 734 One of "exec" or "eval". 735 736 `filename` 737 The filename the source comes from. If a pyexpr is given, the 738 filename embedded in the pyexpr is used. 739 740 `lineno` 741 The line number of the first line of source code. If a pyexpr is 742 given, the filename embedded in the pyexpr is used. 743 744 `ast_node` 745 Rather than returning compiled bytecode, returns the AST object 746 that would be used. 747 """ 748 749 if ast_node: 750 cache = False 751 752 if isinstance(source, ast.Module): 753 return compile(source, filename, mode) 754 755 if isinstance(source, renpy.ast.PyExpr): 756 filename = source.filename 757 lineno = source.linenumber 758 759 if cache: 760 key = (lineno, filename, str(source), mode, renpy.script.MAGIC) 761 762 rv = py_compile_cache.get(key, None) 763 if rv is not None: 764 return rv 765 766 rv = old_py_compile_cache.get(key, None) 767 if rv is not None: 768 py_compile_cache[key] = rv 769 return rv 770 771 bytecode = renpy.game.script.bytecode_oldcache.get(key, None) 772 if bytecode is not None: 773 774 renpy.game.script.bytecode_newcache[key] = bytecode 775 rv = marshal.loads(bytecode) 776 py_compile_cache[key] = rv 777 return rv 778 779 source = str(source) 780 source = source.replace("\r", "") 781 782 if mode == "eval": 783 source = quote_eval(source) 784 785 try: 786 line_offset = lineno - 1 787 788 if mode == "hide": 789 py_mode = "exec" 790 else: 791 py_mode = mode 792 793 if filename in py3_files: 794 795 flags = py3_compile_flags 796 tree = compile(source, filename, py_mode, ast.PyCF_ONLY_AST | flags, 1) 797 798 else: 799 800 try: 801 flags = new_compile_flags 802 tree = compile(source, filename, py_mode, ast.PyCF_ONLY_AST | flags, 1) 803 except: 804 flags = old_compile_flags 805 source = escape_unicode(source) 806 tree = compile(source, filename, py_mode, ast.PyCF_ONLY_AST | flags, 1) 807 808 tree = wrap_node.visit(tree) 809 810 if mode == "hide": 811 wrap_hide(tree) 812 813 fix_missing_locations(tree, 1, 0) 814 ast.increment_lineno(tree, lineno - 1) 815 816 line_offset = 0 817 818 if ast_node: 819 return tree.body 820 821 rv = compile(tree, filename, py_mode, flags, 1) 822 823 if cache: 824 py_compile_cache[key] = rv 825 renpy.game.script.bytecode_newcache[key] = marshal.dumps(rv) 826 renpy.game.script.bytecode_dirty = True 827 828 return rv 829 830 except SyntaxError as e: 831 832 if e.lineno is not None: 833 e.lineno += line_offset 834 835 raise e 836 837 838def py_compile_exec_bytecode(source, **kwargs): 839 code = py_compile(source, 'exec', cache=False, **kwargs) 840 return marshal.dumps(code) 841 842 843def py_compile_hide_bytecode(source, **kwargs): 844 code = py_compile(source, 'hide', cache=False, **kwargs) 845 return marshal.dumps(code) 846 847 848def py_compile_eval_bytecode(source, **kwargs): 849 source = source.strip() 850 code = py_compile(source, 'eval', cache=False, **kwargs) 851 return marshal.dumps(code) 852 853# Classes that are exported in place of the normal list, dict, and 854# object. 855 856 857# This is set to True whenever a mutation occurs. The save code uses 858# this to check to see if a background-save is valid. 859mutate_flag = True 860 861 862def mutator(method): 863 864 @functools.wraps(method, ("__name__", "__doc__"), ()) 865 def do_mutation(self, *args, **kwargs): 866 867 global mutate_flag 868 869 mutated = renpy.game.log.mutated # @UndefinedVariable 870 871 if id(self) not in mutated: 872 mutated[id(self)] = (weakref.ref(self), self._clean()) 873 mutate_flag = True 874 875 return method(self, *args, **kwargs) 876 877 return do_mutation 878 879 880class CompressedList(object): 881 """ 882 Compresses the changes in a queue-like list. What this does is to try 883 to find a central sub-list for which has objects in both lists. It 884 stores the location of that in the new list, and then elements before 885 and after in the sub-list. 886 887 This only really works if the objects in the list are unique, but the 888 results are efficient even if this doesn't work. 889 """ 890 891 def __init__(self, old, new): 892 893 # Pick out a pivot element near the center of the list. 894 new_center = (len(new) - 1) // 2 895 new_pivot = new[new_center] 896 897 # Find an element in the old list corresponding to the pivot. 898 old_half = (len(old) - 1) // 2 899 900 for i in range(0, old_half + 1): 901 902 if old[old_half - i] is new_pivot: 903 old_center = old_half - i 904 break 905 906 if old[old_half + i] is new_pivot: 907 old_center = old_half + i 908 break 909 else: 910 # If we couldn't, give up. 911 self.pre = old 912 self.start = 0 913 self.end = 0 914 self.post = [ ] 915 916 return 917 918 # Figure out the position of the overlap in the center of the two lists. 919 new_start = new_center 920 new_end = new_center + 1 921 922 old_start = old_center 923 old_end = old_center + 1 924 925 len_new = len(new) 926 len_old = len(old) 927 928 while new_start and old_start and (new[new_start - 1] is old[old_start - 1]): 929 new_start -= 1 930 old_start -= 1 931 932 while (new_end < len_new) and (old_end < len_old) and (new[new_end] is old[old_end]): 933 new_end += 1 934 old_end += 1 935 936 # Now that we have this, we can put together the object. 937 self.pre = list.__getitem__(old, slice(0, old_start)) 938 self.start = new_start 939 self.end = new_end 940 self.post = list.__getitem__(old, slice(old_end, len_old)) 941 942 def decompress(self, new): 943 return self.pre + new[self.start:self.end] + self.post 944 945 def __repr__(self): 946 return "<CompressedList {} [{}:{}] {}>".format( 947 self.pre, 948 self.start, 949 self.end, 950 self.post) 951 952 953class RevertableList(list): 954 955 def __init__(self, *args): 956 log = renpy.game.log 957 958 if log is not None: 959 log.mutated[id(self)] = None 960 961 list.__init__(self, *args) 962 963 __delitem__ = mutator(list.__delitem__) 964 if PY2: 965 __delslice__ = mutator(list.__delslice__) 966 __setitem__ = mutator(list.__setitem__) 967 if PY2: 968 __setslice__ = mutator(list.__setslice__) 969 __iadd__ = mutator(list.__iadd__) 970 __imul__ = mutator(list.__imul__) 971 append = mutator(list.append) 972 extend = mutator(list.extend) 973 insert = mutator(list.insert) 974 pop = mutator(list.pop) 975 remove = mutator(list.remove) 976 reverse = mutator(list.reverse) 977 sort = mutator(list.sort) 978 979 def wrapper(method): # E0213 @NoSelf 980 981 def newmethod(*args, **kwargs): 982 l = method(*args, **kwargs) 983 return RevertableList(l) 984 985 return newmethod 986 987 __add__ = wrapper(list.__add__) 988 if PY2: 989 __getslice__ = wrapper(list.__getslice__) 990 991 def __getitem__(self, index): 992 rv = list.__getitem__(self, index) 993 994 if isinstance(index, slice): 995 return RevertableList(rv) 996 else: 997 return rv 998 999 def __mul__(self, other): 1000 if not isinstance(other, int): 1001 raise TypeError("can't multiply sequence by non-int of type '{}'.".format(type(other).__name__)) 1002 1003 return RevertableList(list.__mul__(self, other)) 1004 1005 __rmul__ = __mul__ 1006 1007 def copy(self): 1008 return self[:] 1009 1010 def clear(self): 1011 self[:] = [] 1012 1013 def _clean(self): 1014 """ 1015 Gets a clean copy of this object before any mutation occurs. 1016 """ 1017 1018 return self[:] 1019 1020 def _compress(self, clean): 1021 """ 1022 Takes a clean copy of this object, compresses it, and returns compressed 1023 information that can be passed to rollback. 1024 """ 1025 1026 if not self or not clean: 1027 return clean 1028 1029 if renpy.config.list_compression_length is None: 1030 return clean 1031 1032 if len(self) < renpy.config.list_compression_length or len(clean) < renpy.config.list_compression_length: 1033 return clean 1034 1035 return CompressedList(clean, self) 1036 1037 def _rollback(self, compressed): 1038 """ 1039 Rolls this object back, using the information created by _compress. 1040 1041 Since compressed can come from a save file, this method also has to 1042 recognize and deal with old data. 1043 """ 1044 1045 if isinstance(compressed, CompressedList): 1046 self[:] = compressed.decompress(self) 1047 else: 1048 self[:] = compressed 1049 1050 1051def revertable_range(*args): 1052 return RevertableList(range(*args)) 1053 1054 1055def revertable_sorted(*args, **kwargs): 1056 return RevertableList(sorted(*args, **kwargs)) 1057 1058 1059class RevertableDict(dict): 1060 1061 def __init__(self, *args, **kwargs): 1062 log = renpy.game.log 1063 1064 if log is not None: 1065 log.mutated[id(self)] = None 1066 1067 dict.__init__(self, *args, **kwargs) 1068 1069 __delitem__ = mutator(dict.__delitem__) 1070 __setitem__ = mutator(dict.__setitem__) 1071 clear = mutator(dict.clear) 1072 pop = mutator(dict.pop) 1073 popitem = mutator(dict.popitem) 1074 setdefault = mutator(dict.setdefault) 1075 update = mutator(dict.update) 1076 1077 if PY2: 1078 1079 def keys(self): 1080 rv = dict.keys(self) 1081 1082 if (sys._getframe(1).f_code.co_flags & FUTURE_FLAGS) != FUTURE_FLAGS: 1083 rv = RevertableList(rv) 1084 1085 return rv 1086 1087 def values(self): 1088 rv = dict.values(self) 1089 1090 if (sys._getframe(1).f_code.co_flags & FUTURE_FLAGS) != FUTURE_FLAGS: 1091 rv = RevertableList(rv) 1092 1093 return rv 1094 1095 def items(self): 1096 rv = dict.items(self) 1097 1098 if (sys._getframe(1).f_code.co_flags & FUTURE_FLAGS) != FUTURE_FLAGS: 1099 rv = RevertableList(rv) 1100 1101 return rv 1102 1103 def copy(self): 1104 rv = RevertableDict() 1105 rv.update(self) 1106 return rv 1107 1108 def _clean(self): 1109 return list(self.items()) 1110 1111 def _compress(self, clean): 1112 return clean 1113 1114 def _rollback(self, compressed): 1115 self.clear() 1116 1117 for k, v in compressed: 1118 self[k] = v 1119 1120 1121class RevertableSet(set): 1122 1123 def __setstate__(self, state): 1124 if isinstance(state, tuple): 1125 self.update(state[0].keys()) 1126 else: 1127 self.update(state) 1128 1129 def __getstate__(self): 1130 rv = ({ i : True for i in self},) 1131 return rv 1132 1133 # Required to ensure that getstate and setstate are called. 1134 __reduce__ = object.__reduce__ 1135 __reduce_ex__ = object.__reduce_ex__ 1136 1137 def __init__(self, *args): 1138 log = renpy.game.log 1139 1140 if log is not None: 1141 log.mutated[id(self)] = None 1142 1143 set.__init__(self, *args) 1144 1145 __iand__ = mutator(set.__iand__) 1146 __ior__ = mutator(set.__ior__) 1147 __isub__ = mutator(set.__isub__) 1148 __ixor__ = mutator(set.__ixor__) 1149 add = mutator(set.add) 1150 clear = mutator(set.clear) 1151 difference_update = mutator(set.difference_update) 1152 discard = mutator(set.discard) 1153 intersection_update = mutator(set.intersection_update) 1154 pop = mutator(set.pop) 1155 remove = mutator(set.remove) 1156 symmetric_difference_update = mutator(set.symmetric_difference_update) 1157 union_update = mutator(set.update) 1158 update = mutator(set.update) 1159 1160 def wrapper(method): # @NoSelf 1161 1162 def newmethod(*args, **kwargs): 1163 rv = method(*args, **kwargs) 1164 if isinstance(rv, (set, frozenset)): 1165 return RevertableSet(rv) 1166 else: 1167 return rv 1168 1169 return newmethod 1170 1171 __and__ = wrapper(set.__and__) 1172 __sub__ = wrapper(set.__sub__) 1173 __xor__ = wrapper(set.__xor__) 1174 __or__ = wrapper(set.__or__) 1175 copy = wrapper(set.copy) 1176 difference = wrapper(set.difference) 1177 intersection = wrapper(set.intersection) 1178 symmetric_difference = wrapper(set.symmetric_difference) 1179 union = wrapper(set.union) 1180 1181 del wrapper 1182 1183 def _clean(self): 1184 return list(self) 1185 1186 def _compress(self, clean): 1187 return clean 1188 1189 def _rollback(self, compressed): 1190 set.clear(self) 1191 set.update(self, compressed) 1192 1193 1194class RevertableObject(object): 1195 1196 def __new__(cls, *args, **kwargs): 1197 self = super(RevertableObject, cls).__new__(cls) 1198 1199 log = renpy.game.log 1200 if log is not None: 1201 log.mutated[id(self)] = None 1202 1203 return self 1204 1205 def __init__(self, *args, **kwargs): 1206 if (args or kwargs) and renpy.config.developer: 1207 raise TypeError("object() takes no parameters.") 1208 1209 def __setattr__(self, attr, value): 1210 object.__setattr__(self, attr, value) 1211 1212 def __delattr__(self, attr): 1213 object.__delattr__(self, attr) 1214 1215 __setattr__ = mutator(__setattr__) 1216 __delattr__ = mutator(__delattr__) 1217 1218 def _clean(self): 1219 return self.__dict__.copy() 1220 1221 def _compress(self, clean): 1222 return clean 1223 1224 def _rollback(self, compressed): 1225 self.__dict__.clear() 1226 self.__dict__.update(compressed) 1227 1228 1229class AlwaysRollback(RevertableObject): 1230 """ 1231 This is a revertible object that always participates in rollback. 1232 It's used when a revertable object is created by an object that 1233 doesn't participate in the rollback system. 1234 """ 1235 1236 def __new__(cls, *args, **kwargs): 1237 self = super(AlwaysRollback, cls).__new__(cls) 1238 1239 log = renpy.game.log 1240 if log is not None: 1241 del log.mutated[id(self)] 1242 1243 return self 1244 1245 1246class RollbackRandom(random.Random): 1247 """ 1248 This is used for Random objects returned by renpy.random.Random. 1249 """ 1250 1251 def __init__(self): 1252 log = renpy.game.log 1253 1254 if log is not None: 1255 log.mutated[id(self)] = None 1256 1257 super(RollbackRandom, self).__init__() 1258 1259 def _clean(self): 1260 return self.getstate() 1261 1262 def _compress(self, clean): 1263 return clean 1264 1265 def _rollback(self, compressed): 1266 super(RollbackRandom, self).setstate(compressed) 1267 1268 setstate = mutator(random.Random.setstate) 1269 1270 if PY2: 1271 jumpahead = mutator(random.Random.jumpahead) 1272 1273 getrandbits = mutator(random.Random.getrandbits) 1274 seed = mutator(random.Random.seed) 1275 random = mutator(random.Random.random) 1276 1277 def Random(self, seed=None): 1278 """ 1279 Returns a new RNG object separate from the main one. 1280 """ 1281 1282 if seed is None: 1283 seed = self.random() 1284 1285 new = RollbackRandom() 1286 new.seed(seed) 1287 return new 1288 1289 1290class DetRandom(random.Random): 1291 """ 1292 This is renpy.random. 1293 """ 1294 1295 def __init__(self): 1296 super(DetRandom, self).__init__() 1297 self.stack = [ ] 1298 1299 def random(self): 1300 1301 if self.stack: 1302 rv = self.stack.pop() 1303 else: 1304 rv = super(DetRandom, self).random() 1305 1306 log = renpy.game.log 1307 1308 if log.current is not None: 1309 log.current.random.append(rv) 1310 1311 return rv 1312 1313 def pushback(self, l): 1314 """ 1315 Pushes the random numbers in l onto the stack so they will be generated 1316 in the order given. 1317 """ 1318 1319 ll = l[:] 1320 ll.reverse() 1321 1322 self.stack.extend(ll) 1323 1324 def reset(self): 1325 """ 1326 Resets the RNG, removing all of the pushbacked numbers. 1327 """ 1328 1329 del self.stack[:] 1330 1331 def Random(self, seed=None): 1332 """ 1333 Returns a new RNG object separate from the main one. 1334 """ 1335 1336 if seed is None: 1337 seed = self.random() 1338 1339 new = RollbackRandom() 1340 new.seed(seed) 1341 return new 1342 1343 1344rng = DetRandom() 1345 1346# This is the code that actually handles the logging and managing 1347# of the rollbacks. 1348 1349generation = time.time() 1350serial = 0 1351 1352 1353class Rollback(renpy.object.Object): 1354 """ 1355 Allows the state of the game to be rolled back to the point just 1356 before a node began executing. 1357 1358 @ivar context: A shallow copy of the context we were in before 1359 we started executing the node. (Shallow copy also includes 1360 a copy of the associated SceneList.) 1361 1362 @ivar objects: A list of tuples, each containing an object and a 1363 token of information that, when passed to the rollback method on 1364 that object, causes that object to rollback. 1365 1366 @ivar store: A list of updates to store that will cause the state 1367 of the store to be rolled back to the start of node 1368 execution. This is a list of tuples, either (key, value) tuples 1369 representing a value that needs to be assigned to a key, or (key,) 1370 tuples that mean the key should be deleted. 1371 1372 @ivar checkpoint: True if this is a user-visible checkpoint, 1373 false otherwise. 1374 1375 @ivar purged: True if purge_unreachable has already been called on 1376 this Rollback, False otherwise. 1377 1378 @ivar random: A list of random numbers that were generated during the 1379 execution of this element. 1380 """ 1381 1382 __version__ = 5 1383 1384 identifier = None 1385 not_greedy = False 1386 1387 def __init__(self): 1388 1389 super(Rollback, self).__init__() 1390 1391 self.context = renpy.game.context().rollback_copy() 1392 1393 self.objects = [ ] 1394 self.purged = False 1395 self.random = [ ] 1396 self.forward = None 1397 1398 # A map of maps name -> (variable -> value) 1399 self.stores = { } 1400 1401 # A map from store name to the changes to ever_been_changed that 1402 # need to be reverted. 1403 self.delta_ebc = { } 1404 1405 # If true, we retain the data in this rollback when a load occurs. 1406 self.retain_after_load = False 1407 1408 # True if this is a checkpoint we can roll back to. 1409 self.checkpoint = False 1410 1411 # True if this is a hard checkpoint, where the rollback counter 1412 # decreases. 1413 self.hard_checkpoint = False 1414 1415 # True if this is a not-greedy checkpoint, which should end 1416 # rollbacks that occur in greedy mode. 1417 self.not_greedy = False 1418 1419 # A unique identifier for this rollback object. 1420 1421 global serial 1422 self.identifier = (generation, serial) 1423 serial += 1 1424 1425 def after_upgrade(self, version): 1426 1427 if version < 2: 1428 self.stores = { "store" : { } } 1429 1430 for i in self.store: 1431 if len(i) == 2: 1432 k, v = i 1433 self.stores["store"][k] = v 1434 else: 1435 k, = i 1436 self.stores["store"][k] = deleted 1437 1438 if version < 3: 1439 self.retain_after_load = False 1440 1441 if version < 4: 1442 self.hard_checkpoint = self.checkpoint 1443 1444 if version < 5: 1445 self.delta_ebc = { } 1446 1447 def purge_unreachable(self, reachable, wait): 1448 """ 1449 Adds objects that are reachable from the store of this 1450 rollback to the set of reachable objects, and purges 1451 information that is stored about totally unreachable objects. 1452 1453 Returns True if this is the first time this method has been 1454 called, or False if it has already been called once before. 1455 """ 1456 1457 if self.purged: 1458 return False 1459 1460 self.purged = True 1461 1462 # Add objects reachable from the stores. (Objects that might be 1463 # unreachable at the moment.) 1464 for changes in self.stores.values(): 1465 for _k, v in changes.items(): 1466 if v is not deleted: 1467 reached(v, reachable, wait) 1468 1469 # Add in objects reachable through the context. 1470 reached(self.context.info, reachable, wait) 1471 for d in self.context.dynamic_stack: 1472 for v in d.values(): 1473 reached(v, reachable, wait) 1474 1475 # Add in objects reachable through displayables. 1476 reached(self.context.scene_lists.get_all_displayables(), reachable, wait) 1477 1478 # Purge object update information for unreachable objects. 1479 new_objects = [ ] 1480 1481 objects_changed = True 1482 seen = set() 1483 1484 while objects_changed: 1485 1486 objects_changed = False 1487 1488 for o, rb in self.objects: 1489 1490 id_o = id(o) 1491 1492 if (id_o not in seen) and reachable.get(id_o, 0): 1493 seen.add(id_o) 1494 objects_changed = True 1495 1496 new_objects.append((o, rb)) 1497 reached(rb, reachable, wait) 1498 1499 del self.objects[:] 1500 self.objects.extend(new_objects) 1501 1502 return True 1503 1504 def rollback(self): 1505 """ 1506 Reverts the state of the game to what it was at the start of the 1507 previous checkpoint. 1508 """ 1509 1510 for obj, roll in reversed(self.objects): 1511 1512 if roll is not None: 1513 obj._rollback(roll) 1514 1515 for name, changes in self.stores.items(): 1516 store = store_dicts.get(name, None) 1517 if store is None: 1518 continue 1519 1520 for name, value in changes.items(): 1521 if value is deleted: 1522 if name in store: 1523 del store[name] 1524 else: 1525 store[name] = value 1526 1527 for name, changes in self.delta_ebc.items(): 1528 1529 store = store_dicts.get(name, None) 1530 if store is None: 1531 continue 1532 1533 store.ever_been_changed -= changes 1534 1535 rng.pushback(self.random) 1536 1537 renpy.game.contexts.pop() 1538 renpy.game.contexts.append(self.context) 1539 1540 def rollback_control(self): 1541 """ 1542 This rolls back only the control information, while leaving 1543 the data information intact. 1544 """ 1545 1546 renpy.game.contexts.pop() 1547 renpy.game.contexts.append(self.context) 1548 1549 1550class RollbackLog(renpy.object.Object): 1551 """ 1552 This class manages the list of Rollback objects. 1553 1554 @ivar log: The log of rollback objects. 1555 1556 @ivar current: The current rollback object. (Equivalent to 1557 log[-1]) 1558 1559 @ivar rollback_limit: The number of steps left that we can 1560 interactively rollback. 1561 1562 Not serialized: 1563 1564 @ivar mutated: A dictionary that maps object ids to a tuple of 1565 (weakref to object, information needed to rollback that object) 1566 """ 1567 1568 __version__ = 5 1569 1570 nosave = [ 'old_store', 'mutated', 'identifier_cache' ] 1571 identifier_cache = None 1572 force_checkpoint = False 1573 1574 def __init__(self): 1575 1576 super(RollbackLog, self).__init__() 1577 1578 self.log = [ ] 1579 self.current = None 1580 self.mutated = { } 1581 self.rollback_limit = 0 1582 self.rollback_is_fixed = False 1583 self.checkpointing_suspended = False 1584 self.fixed_rollback_boundary = None 1585 self.forward = [ ] 1586 self.old_store = { } 1587 1588 # Did we just do a roll forward? 1589 self.rolled_forward = False 1590 1591 # Reset the RNG on the creation of a new game. 1592 rng.reset() 1593 1594 # True if we should retain data from here to the next checkpoint 1595 # on load. 1596 self.retain_after_load_flag = False 1597 1598 # Has there been an interaction since the last time this log was 1599 # reset? 1600 self.did_interaction = True 1601 1602 # Should we force a checkpoint before completing the current 1603 # statement. 1604 self.force_checkpoint = False 1605 1606 def after_setstate(self): 1607 self.mutated = { } 1608 self.rolled_forward = False 1609 1610 def after_upgrade(self, version): 1611 if version < 2: 1612 self.ever_been_changed = { "store" : set(self.ever_been_changed) } 1613 if version < 3: 1614 self.rollback_is_fixed = False 1615 self.fixed_rollback_boundary = None 1616 if version < 4: 1617 self.retain_after_load_flag = False 1618 1619 if version < 5: 1620 1621 # We changed what the rollback limit represents, so recompute it 1622 # here. 1623 if self.rollback_limit: 1624 nrbl = 0 1625 1626 for rb in self.log[-self.rollback_limit:]: 1627 if rb.hard_checkpoint: 1628 nrbl += 1 1629 1630 self.rollback_limit = nrbl 1631 1632 def begin(self, force=False): 1633 """ 1634 Called before a node begins executing, to indicate that the 1635 state needs to be saved for rollbacking. 1636 """ 1637 1638 self.identifier_cache = None 1639 1640 context = renpy.game.context() 1641 1642 if not context.rollback: 1643 return 1644 1645 # We only begin a checkpoint if the previous statement reached a checkpoint, 1646 # or an interaction took place. (Or we're forced.) 1647 ignore = True 1648 1649 if force: 1650 ignore = False 1651 elif self.did_interaction: 1652 ignore = False 1653 elif self.current is not None: 1654 if self.current.checkpoint: 1655 ignore = False 1656 elif self.current.retain_after_load: 1657 ignore = False 1658 1659 if ignore: 1660 return 1661 1662 self.did_interaction = False 1663 1664 if self.current is not None: 1665 self.complete(True) 1666 else: 1667 begin_stores() 1668 1669 # If the log is too long, prune it. 1670 while len(self.log) > renpy.config.rollback_length: 1671 self.log.pop(0) 1672 1673 # check for the end of fixed rollback 1674 if self.log and self.log[-1] == self.current: 1675 1676 if self.current.context.current == self.fixed_rollback_boundary: 1677 self.rollback_is_fixed = False 1678 1679 elif self.rollback_is_fixed and not self.forward: 1680 # A lack of rollback data in fixed rollback mode ends rollback. 1681 self.fixed_rollback_boundary = self.current.context.current 1682 self.rollback_is_fixed = False 1683 1684 self.current = Rollback() 1685 self.current.retain_after_load = self.retain_after_load_flag 1686 1687 self.log.append(self.current) 1688 1689 self.mutated.clear() 1690 1691 # Flag a mutation as having happened. This is used by the 1692 # save code. 1693 global mutate_flag 1694 mutate_flag = True 1695 1696 self.rolled_forward = False 1697 1698 def replace_node(self, old, new): 1699 """ 1700 Replaces references to the `old` ast node with a reference to the 1701 `new` ast node. 1702 """ 1703 1704 for i in self.log: 1705 i.context.replace_node(old, new) 1706 1707 def complete(self, begin=False): 1708 """ 1709 Called after a node is finished executing, before a save 1710 begins, or right before a rollback is attempted. This may be 1711 called more than once between calls to begin, and should always 1712 be called after an update to the store but before a rollback 1713 occurs. 1714 1715 `begin` 1716 Should be true if called from begin(). 1717 """ 1718 1719 if self.force_checkpoint: 1720 self.checkpoint(hard=False) 1721 self.force_checkpoint = False 1722 1723 # Update self.current.stores with the changes from each store. 1724 # Also updates .ever_been_changed. 1725 for name, sd in store_dicts.items(): 1726 self.current.stores[name], self.current.delta_ebc[name] = sd.get_changes(begin) 1727 1728 # Update the list of mutated objects and what we need to do to 1729 # restore them. 1730 1731 for _i in range(4): 1732 1733 del self.current.objects[:] 1734 1735 try: 1736 for _k, v in self.mutated.items(): 1737 1738 if v is None: 1739 continue 1740 1741 (ref, clean) = v 1742 1743 obj = ref() 1744 if obj is None: 1745 continue 1746 1747 compressed = obj._compress(clean) 1748 self.current.objects.append((obj, compressed)) 1749 1750 break 1751 1752 except RuntimeError: 1753 # This can occur when self.mutated is changed as we're 1754 # iterating over it. 1755 pass 1756 1757 def get_roots(self): 1758 """ 1759 Return a map giving the current roots of the store. This is a 1760 map from a variable name in the store to the value of that 1761 variable. A variable is only in this map if it has ever been 1762 changed since the init phase finished. 1763 """ 1764 1765 rv = { } 1766 1767 for store_name, sd in store_dicts.items(): 1768 for name in sd.ever_been_changed: 1769 if name in sd: 1770 rv[store_name + "." + name] = sd[name] 1771 else: 1772 rv[store_name + "." + name] = deleted 1773 1774 for i in reversed(renpy.game.contexts[1:]): 1775 i.pop_dynamic_roots(rv) 1776 1777 return rv 1778 1779 def purge_unreachable(self, roots, wait=None): 1780 """ 1781 This is called to purge objects that are unreachable from the 1782 roots from the object rollback lists inside the Rollback entries. 1783 1784 This should be called immediately after complete(), so that there 1785 are no changes queued up. 1786 """ 1787 1788 reachable = { } 1789 1790 reached_vars(roots, reachable, wait) 1791 1792 revlog = self.log[:] 1793 revlog.reverse() 1794 1795 for i in revlog: 1796 if not i.purge_unreachable(reachable, wait): 1797 break 1798 1799 def in_rollback(self): 1800 if self.forward: 1801 return True 1802 else: 1803 return False 1804 1805 def in_fixed_rollback(self): 1806 return self.rollback_is_fixed 1807 1808 def forward_info(self): 1809 """ 1810 Returns the current forward info, if any. 1811 """ 1812 1813 if self.forward: 1814 1815 name, data = self.forward[0] 1816 1817 if self.current.context.current == name: 1818 return data 1819 1820 return None 1821 1822 def checkpoint(self, data=None, keep_rollback=False, hard=True): 1823 """ 1824 Called to indicate that this is a checkpoint, which means 1825 that the user may want to rollback to just before this 1826 node. 1827 """ 1828 1829 if self.checkpointing_suspended: 1830 hard = False 1831 1832 self.retain_after_load_flag = False 1833 1834 if self.current.checkpoint: 1835 return 1836 1837 if not renpy.game.context().rollback: 1838 return 1839 1840 self.current.checkpoint = True 1841 1842 if hard and (not self.current.hard_checkpoint): 1843 if self.rollback_limit < renpy.config.hard_rollback_limit: 1844 self.rollback_limit += 1 1845 1846 if hard == "not_greedy": 1847 self.current.not_greedy = True 1848 else: 1849 self.current.hard_checkpoint = hard 1850 1851 if self.in_fixed_rollback() and self.forward: 1852 # use data from the forward stack 1853 fwd_name, fwd_data = self.forward[0] 1854 if self.current.context.current == fwd_name: 1855 self.current.forward = fwd_data 1856 self.forward.pop(0) 1857 else: 1858 self.current.forward = data 1859 del self.forward[:] 1860 1861 elif data is not None: 1862 if self.forward: 1863 # If the data is the same, pop it from the forward stack. 1864 # Otherwise, clear the forward stack. 1865 fwd_name, fwd_data = self.forward[0] 1866 1867 if (self.current.context.current == fwd_name 1868 and data == fwd_data 1869 and (keep_rollback or self.rolled_forward) 1870 ): 1871 self.forward.pop(0) 1872 else: 1873 del self.forward[:] 1874 1875 # Log the data in case we roll back again. 1876 self.current.forward = data 1877 1878 def suspend_checkpointing(self, flag): 1879 """ 1880 Called to temporarily suspend checkpointing, so any rollback 1881 will jump to prior to this statement 1882 """ 1883 1884 self.checkpointing_suspended = flag 1885 1886 def block(self, purge=False): 1887 """ 1888 Called to indicate that the user should not be able to rollback 1889 through this checkpoint. 1890 """ 1891 1892 self.rollback_limit = 0 1893 renpy.game.context().force_checkpoint = True 1894 1895 if purge: 1896 del self.log[:] 1897 1898 def retain_after_load(self): 1899 """ 1900 Called to return data from this statement until the next checkpoint 1901 when the game is loaded. 1902 """ 1903 1904 if renpy.display.predict.predicting: 1905 return 1906 1907 self.retain_after_load_flag = True 1908 self.current.retain_after_load = True 1909 renpy.game.context().force_checkpoint = True 1910 1911 def fix_rollback(self): 1912 if not self.rollback_is_fixed and len(self.log) > 1: 1913 self.fixed_rollback_boundary = self.log[-2].context.current 1914 1915 def can_rollback(self): 1916 """ 1917 Returns True if we can rollback. 1918 """ 1919 1920 return self.rollback_limit > 0 1921 1922 def load_failed(self): 1923 """ 1924 This is called to try to recover when rollback fails. 1925 """ 1926 1927 lfl = renpy.config.load_failed_label 1928 if callable(lfl): 1929 lfl = lfl() 1930 1931 if not lfl: 1932 raise Exception("Couldn't find a place to stop rolling back. Perhaps the script changed in an incompatible way?") 1933 1934 rb = self.log.pop() 1935 rb.rollback() 1936 1937 while renpy.exports.call_stack_depth(): 1938 renpy.exports.pop_call() 1939 1940 renpy.game.contexts[0].force_checkpoint = True 1941 renpy.game.contexts[0].goto_label(lfl) 1942 1943 raise renpy.game.RestartTopContext() 1944 1945 def rollback(self, checkpoints, force=False, label=None, greedy=True, on_load=False, abnormal=True, current_label=None): 1946 """ 1947 This rolls the system back to the first valid rollback point 1948 after having rolled back past the specified number of checkpoints. 1949 1950 If we're currently executing code, it's expected that complete() 1951 will be called before a rollback is attempted. 1952 1953 force makes us throw an exception if we can't find a place to stop 1954 rolling back, otherwise if we run out of log this call has no 1955 effect. 1956 1957 `label` 1958 A label that is called after rollback has finished, if the 1959 label exists. 1960 1961 `greedy` 1962 If true, rollback will keep going until just after the last 1963 checkpoint. If False, it will stop immediately before the 1964 current statement. 1965 1966 `on_load` 1967 Should be true if this rollback is being called in response to a 1968 load. Used to implement .retain_after_load() 1969 1970 `abnormal` 1971 If true, treats this as an abnormal event, suppressing transitions 1972 and so on. 1973 1974 `current_label` 1975 A lable that is called when control returns to the current statement, 1976 after rollback. (At most one of `current_label` and `label` can be 1977 provided.) 1978 """ 1979 1980 # If we have exceeded the rollback limit, and don't have force, 1981 # give up. 1982 if checkpoints and (self.rollback_limit <= 0) and (not force): 1983 return 1984 1985 self.suspend_checkpointing(False) 1986 # will always rollback to before suspension 1987 1988 self.purge_unreachable(self.get_roots()) 1989 1990 revlog = [ ] 1991 1992 # Find the place to roll back to. 1993 while self.log: 1994 rb = self.log.pop() 1995 revlog.append(rb) 1996 1997 if rb.hard_checkpoint: 1998 self.rollback_limit -= 1 1999 2000 if rb.hard_checkpoint or (on_load and rb.checkpoint): 2001 checkpoints -= 1 2002 2003 if checkpoints <= 0: 2004 if renpy.game.script.has_label(rb.context.current): 2005 break 2006 2007 else: 2008 # Otherwise, just give up. 2009 2010 revlog.reverse() 2011 self.log.extend(revlog) 2012 2013 if force: 2014 self.load_failed() 2015 else: 2016 print("Can't find a place to rollback to. Not rolling back.") 2017 2018 return 2019 2020 force_checkpoint = False 2021 2022 # Try to rollback to just after the previous checkpoint. 2023 while greedy and self.log and (self.rollback_limit > 0): 2024 2025 rb = self.log[-1] 2026 2027 if not renpy.game.script.has_label(rb.context.current): 2028 break 2029 2030 if rb.hard_checkpoint: 2031 break 2032 2033 if rb.not_greedy: 2034 break 2035 2036 revlog.append(self.log.pop()) 2037 2038 # Decide if we're replacing the current context (rollback command), 2039 # or creating a new set of contexts (loading). 2040 if renpy.game.context().rollback: 2041 replace_context = False 2042 other_contexts = [ ] 2043 2044 else: 2045 replace_context = True 2046 other_contexts = renpy.game.contexts[1:] 2047 renpy.game.contexts = renpy.game.contexts[0:1] 2048 2049 if on_load and revlog[-1].retain_after_load: 2050 retained = revlog.pop() 2051 self.retain_after_load_flag = True 2052 else: 2053 retained = None 2054 2055 come_from = None 2056 2057 if current_label is not None: 2058 come_from = renpy.game.context().current 2059 label = current_label 2060 2061 # Actually roll things back. 2062 for rb in revlog: 2063 rb.rollback() 2064 2065 if (rb.context.current == self.fixed_rollback_boundary) and (rb.context.current): 2066 self.rollback_is_fixed = True 2067 2068 if rb.forward is not None: 2069 self.forward.insert(0, (rb.context.current, rb.forward)) 2070 2071 if retained is not None: 2072 retained.rollback_control() 2073 self.log.append(retained) 2074 2075 if (label is not None) and (come_from is None): 2076 come_from = renpy.game.context().current 2077 2078 if come_from is not None: 2079 renpy.game.context().come_from(come_from, label) 2080 2081 # Disable the next transition, as it's pointless. (Only when not used with a label.) 2082 renpy.game.interface.suppress_transition = abnormal 2083 2084 # If necessary, reset the RNG. 2085 if force: 2086 rng.reset() 2087 del self.forward[:] 2088 2089 # Flag that we're in the transition immediately after a rollback. 2090 renpy.game.after_rollback = abnormal 2091 2092 # Stop the sounds. 2093 renpy.audio.audio.rollback() 2094 2095 # Stop any hiding in progress. 2096 for i in renpy.game.contexts: 2097 i.scene_lists.remove_all_hidden() 2098 2099 renpy.game.contexts.extend(other_contexts) 2100 2101 renpy.exports.execute_default_statement(False) 2102 2103 self.mutated.clear() 2104 begin_stores() 2105 2106 # Restart the context or the top context. 2107 if replace_context: 2108 2109 if force_checkpoint: 2110 renpy.game.contexts[0].force_checkpoint = True 2111 2112 self.current = Rollback() 2113 self.current.context = renpy.game.contexts[0].rollback_copy() 2114 2115 if self.log is not None: 2116 self.log.append(self.current) 2117 2118 raise renpy.game.RestartTopContext() 2119 2120 else: 2121 2122 self.current = Rollback() 2123 self.current.context = renpy.game.context().rollback_copy() 2124 2125 if self.log is not None: 2126 self.log.append(self.current) 2127 2128 if force_checkpoint: 2129 renpy.game.context().force_checkpoint = True 2130 2131 raise renpy.game.RestartContext() 2132 2133 def freeze(self, wait=None): 2134 """ 2135 This is called to freeze the store and the log, in preparation 2136 for serialization. The next call on log should either be 2137 unfreeze (called after a serialization reload) or discard_freeze() 2138 (called after the save is complete). 2139 """ 2140 2141 # Purge unreachable objects, so we don't save them. 2142 self.complete(False) 2143 roots = self.get_roots() 2144 self.purge_unreachable(roots, wait=wait) 2145 2146 # The current is not purged. 2147 self.current.purged = False 2148 2149 return roots 2150 2151 def discard_freeze(self): 2152 """ 2153 Called to indicate that we will not be restoring from the 2154 frozen state. 2155 """ 2156 2157 def unfreeze(self, roots, label=None): 2158 """ 2159 Used to unfreeze the game state after a load of this log 2160 object. This call will always throw an exception. If we're 2161 lucky, it's the one that indicates load was successful. 2162 2163 @param roots: The roots returned from freeze. 2164 2165 @param label: The label that is jumped to in the game script 2166 after rollback has finished, if it exists. 2167 """ 2168 2169 # Fix up old screens. 2170 renpy.display.screen.before_restart() # @UndefinedVariable 2171 2172 # Set us up as the game log. 2173 renpy.game.log = self 2174 2175 clean_stores() 2176 renpy.translation.init_translation() 2177 2178 for name, value in roots.items(): 2179 2180 if "." in name: 2181 store_name, name = name.rsplit(".", 1) 2182 else: 2183 store_name = "store" 2184 2185 if store_name not in store_dicts: 2186 continue 2187 2188 store = store_dicts[store_name] 2189 store.ever_been_changed.add(name) 2190 2191 if value is deleted: 2192 if name in store: 2193 del store[name] 2194 else: 2195 store[name] = value 2196 2197 # Now, rollback to an acceptable point. 2198 2199 greedy = renpy.session.pop("_greedy_rollback", False) 2200 self.rollback(0, force=True, label=label, greedy=greedy, on_load=True) 2201 2202 # Because of the rollback, we never make it this far. 2203 2204 def build_identifier_cache(self): 2205 2206 if self.identifier_cache is not None: 2207 return 2208 2209 rollback_limit = self.rollback_limit 2210 checkpoints = 1 2211 2212 self.identifier_cache = { } 2213 2214 for i in reversed(self.log): 2215 2216 if i.identifier is not None: 2217 if renpy.game.script.has_label(i.context.current): 2218 self.identifier_cache[i.identifier] = checkpoints 2219 2220 if i.hard_checkpoint: 2221 checkpoints += 1 2222 2223 if i.checkpoint: 2224 rollback_limit -= 1 2225 2226 if not rollback_limit: 2227 break 2228 2229 def get_identifier_checkpoints(self, identifier): 2230 self.build_identifier_cache() 2231 return self.identifier_cache.get(identifier, None) 2232 2233 2234def py_exec_bytecode(bytecode, hide=False, globals=None, locals=None, store="store"): # @ReservedAssignment 2235 2236 if hide: 2237 locals = { } # @ReservedAssignment 2238 2239 if globals is None: 2240 globals = store_dicts[store] # @ReservedAssignment 2241 2242 if locals is None: 2243 locals = globals # @ReservedAssignment 2244 2245 exec(bytecode, globals, locals) 2246 2247 2248def py_exec(source, hide=False, store=None): 2249 2250 if store is None: 2251 store = store_dicts["store"] 2252 2253 if hide: 2254 locals = { } # @ReservedAssignment 2255 else: 2256 locals = store # @ReservedAssignment 2257 2258 exec(py_compile(source, 'exec'), store, locals) 2259 2260 2261def py_eval_bytecode(bytecode, globals=None, locals=None): # @ReservedAssignment 2262 2263 if globals is None: 2264 globals = store_dicts["store"] # @ReservedAssignment 2265 2266 if locals is None: 2267 locals = globals # @ReservedAssignment 2268 2269 return eval(bytecode, globals, locals) 2270 2271 2272def py_eval(code, globals=None, locals=None): # @ReservedAssignment 2273 if isinstance(code, basestring): 2274 code = py_compile(code, 'eval') 2275 2276 return py_eval_bytecode(code, globals, locals) 2277 2278 2279def store_eval(code, globals=None, locals=None): 2280 2281 if globals is None: 2282 globals = sys._getframe(1).f_globals 2283 2284 return py_eval(code, globals, locals) 2285 2286 2287def raise_at_location(e, loc): 2288 """ 2289 Raises `e` (which must be an Exception object) at location `loc`. 2290 2291 `loc` 2292 A location, which should be a (filename, line_number) tuple. 2293 """ 2294 2295 filename, line = loc 2296 2297 node = ast.parse("raise e", filename) 2298 ast.increment_lineno(node, line - 1) 2299 code = compile(node, filename, 'exec') 2300 2301 # PY3 - need to change to exec(). 2302 exec(code, { "e" : e }) 2303 2304 2305# This was used to proxy accesses to the store. Now it's kept around to deal 2306# with cases where it might have leaked into a pickle. 2307class StoreProxy(object): 2308 2309 def __getattr__(self, k): 2310 return getattr(renpy.store, k) # @UndefinedVariable 2311 2312 def __setattr__(self, k, v): 2313 setattr(renpy.store, k, v) # @UndefinedVariable 2314 2315 def __delattr__(self, k): 2316 delattr(renpy.store, k) # @UndefinedVariable 2317 2318 2319if PY2: 2320 2321 # Code for pickling bound methods. 2322 def method_pickle(method): 2323 name = method.im_func.__name__ 2324 2325 obj = method.im_self 2326 2327 if obj is None: 2328 obj = method.im_class 2329 2330 return method_unpickle, (obj, name) 2331 2332 def method_unpickle(obj, name): 2333 return getattr(obj, name) 2334 2335 copyreg.pickle(types.MethodType, method_pickle) 2336 2337# Code for pickling modules. 2338 2339 2340def module_pickle(module): 2341 if renpy.config.developer: 2342 raise Exception("Could not pickle {!r}.".format(module)) 2343 2344 return module_unpickle, (module.__name__,) 2345 2346 2347def module_unpickle(name): 2348 return __import__(name) 2349 2350 2351copyreg.pickle(types.ModuleType, module_pickle) 2352