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 22from __future__ import division, absolute_import, with_statement, print_function, unicode_literals 23from renpy.compat import * 24 25import renpy 26import os.path 27import sys 28import types 29import threading 30import zlib 31import re 32import io 33import unicodedata 34 35from renpy.compat.pickle import loads 36from renpy.webloader import DownloadNeeded 37 38# Ensure the utf-8 codec is loaded, to prevent recursion when we use it 39# to look up filenames. 40u"".encode(u"utf-8") 41 42# Physical Paths 43 44 45def get_path(fn): 46 """ 47 Returns the path to `fn` relative to the gamedir. If any of the directories 48 leading to `fn` do not exist, tries to create them. 49 50 This always returns a path, but the path may or may not be writable. 51 """ 52 53 fn = os.path.join(renpy.config.gamedir, fn) 54 dn = os.path.dirname(fn) 55 56 try: 57 if not os.path.exists(dn): 58 os.makedirs(dn) 59 except: 60 pass 61 62 return fn 63 64# Asset Loading 65 66 67if renpy.android: 68 import android.apk 69 70 expansion = os.environ.get("ANDROID_EXPANSION", None) 71 if expansion is not None: 72 print("Using expansion file", expansion) 73 74 apks = [ 75 android.apk.APK(apk=expansion, prefix='assets/x-game/'), 76 android.apk.APK(apk=expansion, prefix='assets/x-renpy/x-common/'), 77 ] 78 79 game_apks = [ apks[0] ] 80 81 else: 82 print("Not using expansion file.") 83 84 apks = [ 85 android.apk.APK(prefix='assets/x-game/'), 86 android.apk.APK(prefix='assets/x-renpy/x-common/'), 87 ] 88 89 game_apks = [ apks[0] ] 90 91else: 92 apks = [ ] 93 game_apks = [ ] 94 95# Files on disk should be checked before archives. Otherwise, among 96# other things, using a new version of bytecode.rpyb will break. 97archives = [ ] 98 99# The value of renpy.config.archives the last time index_archives was 100# run. 101old_config_archives = None 102 103# A map from lower-case filename to regular-case filename. 104lower_map = { } 105 106# A list containing archive handlers. 107archive_handlers = [ ] 108 109 110class RPAv3ArchiveHandler(object): 111 """ 112 Archive handler handling RPAv3 archives. 113 """ 114 115 @staticmethod 116 def get_supported_extensions(): 117 return [ ".rpa" ] 118 119 @staticmethod 120 def get_supported_headers(): 121 return [ b"RPA-3.0 " ] 122 123 @staticmethod 124 def read_index(infile): 125 l = infile.read(40) 126 offset = int(l[8:24], 16) 127 key = int(l[25:33], 16) 128 infile.seek(offset) 129 index = loads(zlib.decompress(infile.read())) 130 131 # Deobfuscate the index. 132 133 for k in index.keys(): 134 135 if len(index[k][0]) == 2: 136 index[k] = [ (offset ^ key, dlen ^ key) for offset, dlen in index[k] ] 137 else: 138 index[k] = [ (offset ^ key, dlen ^ key, start) for offset, dlen, start in index[k] ] 139 return index 140 141 142archive_handlers.append(RPAv3ArchiveHandler) 143 144 145class RPAv2ArchiveHandler(object): 146 """ 147 Archive handler handling RPAv2 archives. 148 """ 149 150 @staticmethod 151 def get_supported_extensions(): 152 return [ ".rpa" ] 153 154 @staticmethod 155 def get_supported_headers(): 156 return [ b"RPA-2.0 " ] 157 158 @staticmethod 159 def read_index(infile): 160 l = infile.read(24) 161 offset = int(l[8:], 16) 162 infile.seek(offset) 163 index = loads(zlib.decompress(infile.read())) 164 165 return index 166 167 168archive_handlers.append(RPAv2ArchiveHandler) 169 170 171class RPAv1ArchiveHandler(object): 172 """ 173 Archive handler handling RPAv1 archives. 174 """ 175 176 @staticmethod 177 def get_supported_extensions(): 178 return [ ".rpi" ] 179 180 @staticmethod 181 def get_supported_headers(): 182 return [ b"\x78\x9c" ] 183 184 @staticmethod 185 def read_index(infile): 186 return loads(zlib.decompress(infile.read())) 187 188 189archive_handlers.append(RPAv1ArchiveHandler) 190 191 192def index_archives(): 193 """ 194 Loads in the indexes for the archive files. Also updates the lower_map. 195 """ 196 197 # Index the archives. 198 199 global old_config_archives 200 201 if old_config_archives == renpy.config.archives: 202 return 203 204 old_config_archives = renpy.config.archives[:] 205 206 # Update lower_map. 207 lower_map.clear() 208 209 cleardirfiles() 210 211 global archives 212 archives = [ ] 213 214 max_header_length = 0 215 for handler in archive_handlers: 216 for header in handler.get_supported_headers(): 217 header_len = len(header) 218 if header_len > max_header_length: 219 max_header_length = header_len 220 221 archive_extensions = [ ] 222 for handler in archive_handlers: 223 for ext in handler.get_supported_extensions(): 224 if not (ext in archive_extensions): 225 archive_extensions.append(ext) 226 227 for prefix in renpy.config.archives: 228 for ext in archive_extensions: 229 fn = None 230 f = None 231 try: 232 fn = transfn(prefix + ext) 233 f = open(fn, "rb") 234 except: 235 continue 236 with f: 237 file_header = f.read(max_header_length) 238 for handler in archive_handlers: 239 try: 240 archive_handled = False 241 for header in handler.get_supported_headers(): 242 if file_header.startswith(header): 243 f.seek(0, 0) 244 index = handler.read_index(f) 245 archives.append((prefix + ext, index)) 246 archive_handled = True 247 break 248 if archive_handled == True: 249 break 250 except: 251 raise 252 253 for dir, fn in listdirfiles(): # @ReservedAssignment 254 lower_map[unicodedata.normalize('NFC', fn.lower())] = fn 255 256 for fn in remote_files: 257 lower_map[unicodedata.normalize('NFC', fn.lower())] = fn 258 259 260def walkdir(dir): # @ReservedAssignment 261 rv = [ ] 262 263 if not os.path.exists(dir) and not renpy.config.developer: 264 return rv 265 266 for i in os.listdir(dir): 267 if i[0] == ".": 268 continue 269 270 try: 271 i = renpy.exports.fsdecode(i) 272 except: 273 continue 274 275 if os.path.isdir(dir + "/" + i): 276 for fn in walkdir(dir + "/" + i): 277 rv.append(i + "/" + fn) 278 else: 279 rv.append(i) 280 281 return rv 282 283 284# A list of files that make up the game. 285game_files = [ ] 286 287# A list of files that are in the common directory. 288common_files = [ ] 289 290# A map from filename to if the file is loadable. 291loadable_cache = { } 292 293# A map from filename to if the file is downloadable. 294remote_files = { } 295 296 297def cleardirfiles(): 298 """ 299 Clears the lists above when the game has changed. 300 """ 301 302 global game_files 303 global common_files 304 305 game_files = [ ] 306 common_files = [ ] 307 308 309# A list of callbacks to fill out the lists above. 310scandirfiles_callbacks = [ ] 311 312 313def scandirfiles(): 314 """ 315 Scans directories, archives, and apks and fills out game_files and 316 common_files. 317 """ 318 319 seen = set() 320 321 def add(dn, fn, files, seen): 322 323 fn = unicode(fn) 324 325 if fn in seen: 326 return 327 328 if fn.startswith("cache/"): 329 return 330 331 if fn.startswith("saves/"): 332 return 333 334 files.append((dn, fn)) 335 seen.add(fn) 336 loadable_cache[unicodedata.normalize('NFC', fn.lower())] = True 337 338 for i in scandirfiles_callbacks: 339 i(add, seen) 340 341 342def scandirfiles_from_apk(add, seen): 343 """ 344 Scans apks and fills out game_files and common_files. 345 """ 346 347 for apk in apks: 348 349 if apk not in game_apks: 350 files = common_files # @UnusedVariable 351 else: 352 files = game_files # @UnusedVariable 353 354 for f in apk.list(): 355 356 # Strip off the "x-" in front of each filename, which is there 357 # to ensure that aapt actually includes every file. 358 f = "/".join(i[2:] for i in f.split("/")) 359 360 add(None, f, files, seen) 361 362 363if renpy.android: 364 scandirfiles_callbacks.append(scandirfiles_from_apk) 365 366 367def scandirfiles_from_remote_file(add, seen): 368 """ 369 Fills out game_files from renpyweb_remote_files.txt. 370 """ 371 372 # HTML5 remote files 373 index_filename = os.path.join(renpy.config.gamedir, 'renpyweb_remote_files.txt') 374 if os.path.exists(index_filename): 375 files = game_files 376 with open(index_filename, 'rb') as remote_index: 377 while True: 378 f = remote_index.readline() 379 metadata = remote_index.readline() 380 if f == '' or metadata == '': # end of file 381 break 382 383 f = f.rstrip("\r\n") 384 metadata = metadata.rstrip("\r\n") 385 (entry_type, entry_size) = metadata.split(' ') 386 if entry_type == 'image': 387 entry_size = [int(i) for i in entry_size.split(',')] 388 389 add('/game', f, files, seen) 390 remote_files[f] = {'type':entry_type, 'size':entry_size} 391 392 393if renpy.emscripten or os.environ.get('RENPY_SIMULATE_DOWNLOAD', False): 394 scandirfiles_callbacks.append(scandirfiles_from_remote_file) 395 396 397def scandirfiles_from_filesystem(add, seen): 398 """ 399 Scans directories and fills out game_files and common_files. 400 """ 401 402 for i in renpy.config.searchpath: 403 404 if (renpy.config.commondir) and (i == renpy.config.commondir): 405 files = common_files # @UnusedVariable 406 else: 407 files = game_files # @UnusedVariable 408 409 i = os.path.join(renpy.config.basedir, i) 410 for j in walkdir(i): 411 add(i, j, files, seen) 412 413 414scandirfiles_callbacks.append(scandirfiles_from_filesystem) 415 416 417def scandirfiles_from_archives(add, seen): 418 """ 419 Scans archives and fills out game_files. 420 """ 421 422 files = game_files 423 424 for _prefix, index in archives: 425 for j in index: 426 add(None, j, files, seen) 427 428 429scandirfiles_callbacks.append(scandirfiles_from_archives) 430 431 432def listdirfiles(common=True): 433 """ 434 Returns a list of directory, file tuples known to the system. If 435 the file is in an archive, the directory is None. 436 """ 437 438 if (not game_files) and (not common_files): 439 scandirfiles() 440 441 if common: 442 return game_files + common_files 443 else: 444 return list(game_files) 445 446 447class SubFile(object): 448 449 def __init__(self, fn, base, length, start): 450 self.fn = fn 451 452 self.f = None 453 454 self.base = base 455 self.offset = 0 456 self.length = length 457 self.start = start 458 459 if not self.start: 460 self.name = fn 461 else: 462 self.name = None 463 464 def open(self): 465 self.f = open(self.fn, "rb") 466 self.f.seek(self.base) 467 468 def __enter__(self): 469 return self 470 471 def __exit__(self, _type, value, tb): 472 self.close() 473 return False 474 475 def read(self, length=None): 476 477 if self.f is None: 478 self.open() 479 480 maxlength = self.length - self.offset 481 482 if length is not None: 483 length = min(length, maxlength) 484 else: 485 length = maxlength 486 487 rv1 = self.start[self.offset:self.offset + length] 488 length -= len(rv1) 489 self.offset += len(rv1) 490 491 if length: 492 rv2 = self.f.read(length) 493 self.offset += len(rv2) 494 else: 495 rv2 = "" 496 497 return (rv1 + rv2) 498 499 def readline(self, length=None): 500 501 if self.f is None: 502 self.open() 503 504 maxlength = self.length - self.offset 505 if length is not None: 506 length = min(length, maxlength) 507 else: 508 length = maxlength 509 510 # If we're in the start, then read the line ourselves. 511 if self.offset < len(self.start): 512 rv = '' 513 514 while length: 515 c = self.read(1) 516 rv += c 517 if c == '\n': 518 break 519 length -= 1 520 521 return rv 522 523 # Otherwise, let the system read the line all at once. 524 rv = self.f.readline(length) 525 526 self.offset += len(rv) 527 528 return rv 529 530 def readlines(self, length=None): 531 rv = [ ] 532 533 while True: 534 l = self.readline(length) 535 536 if not l: 537 break 538 539 if length is not None: 540 length -= len(l) 541 if l < 0: 542 break 543 544 rv.append(l) 545 546 return rv 547 548 def xreadlines(self): 549 return self 550 551 def __iter__(self): 552 return self 553 554 def __next__(self): # @ReservedAssignment 555 rv = self.readline() 556 557 if not rv: 558 raise StopIteration() 559 560 return rv 561 562 next = __next__ 563 564 def flush(self): 565 return 566 567 def seek(self, offset, whence=0): 568 569 if self.f is None: 570 self.open() 571 572 if whence == 0: 573 offset = offset 574 elif whence == 1: 575 offset = self.offset + offset 576 elif whence == 2: 577 offset = self.length + offset 578 579 if offset > self.length: 580 offset = self.length 581 582 self.offset = offset 583 584 offset = offset - len(self.start) 585 if offset < 0: 586 offset = 0 587 588 self.f.seek(offset + self.base) 589 590 def tell(self): 591 return self.offset 592 593 def close(self): 594 if self.f is not None: 595 self.f.close() 596 self.f = None 597 598 def write(self, s): 599 raise Exception("Write not supported by SubFile") 600 601 602open_file = open 603 604if "RENPY_FORCE_SUBFILE" in os.environ: 605 606 def open_file(name, mode): 607 f = open(name, mode) 608 609 f.seek(0, 2) 610 length = f.tell() 611 f.seek(0, 0) 612 613 return SubFile(f, 0, length, '') 614 615# A list of callbacks to open an open python file object of the given type. 616file_open_callbacks = [ ] 617 618 619def load_core(name): 620 """ 621 Returns an open python file object of the given type. 622 """ 623 624 name = lower_map.get(unicodedata.normalize('NFC', name.lower()), name) 625 626 for i in file_open_callbacks: 627 rv = i(name) 628 if rv is not None: 629 return rv 630 631 return None 632 633 634def load_from_file_open_callback(name): 635 """ 636 Returns an open python file object of the given type from the file open callback. 637 """ 638 639 if renpy.config.file_open_callback: 640 return renpy.config.file_open_callback(name) 641 642 return None 643 644 645file_open_callbacks.append(load_from_file_open_callback) 646 647 648def load_from_filesystem(name): 649 """ 650 Returns an open python file object of the given type from the filesystem. 651 """ 652 653 if not renpy.config.force_archives: 654 try: 655 fn = transfn(name) 656 return open_file(fn, "rb") 657 except: 658 pass 659 660 return None 661 662 663file_open_callbacks.append(load_from_filesystem) 664 665 666def load_from_apk(name): 667 """ 668 Returns an open python file object of the given type from the apk. 669 """ 670 671 for apk in apks: 672 prefixed_name = "/".join("x-" + i for i in name.split("/")) 673 674 try: 675 return apk.open(prefixed_name) 676 except IOError: 677 pass 678 679 return None 680 681 682if renpy.android: 683 file_open_callbacks.append(load_from_apk) 684 685 686def load_from_archive(name): 687 """ 688 Returns an open python file object of the given type from an archive file. 689 """ 690 691 for prefix, index in archives: 692 if not name in index: 693 continue 694 695 afn = transfn(prefix) 696 697 data = [ ] 698 699 # Direct path. 700 if len(index[name]) == 1: 701 702 t = index[name][0] 703 if len(t) == 2: 704 offset, dlen = t 705 start = b'' 706 else: 707 offset, dlen, start = t 708 709 rv = SubFile(afn, offset, dlen, start) 710 711 # Compatibility path. 712 else: 713 with open(afn, "rb") as f: 714 for offset, dlen in index[name]: 715 f.seek(offset) 716 data.append(f.read(dlen)) 717 718 rv = io.BytesIO(b''.join(data)) 719 720 return rv 721 722 return None 723 724 725file_open_callbacks.append(load_from_archive) 726 727 728def load_from_remote_file(name): 729 """ 730 Defer loading a file if it has not been downloaded yet but exists on the remote server. 731 """ 732 733 if name in remote_files: 734 raise DownloadNeeded(relpath=name, rtype=remote_files[name]['type'], size=remote_files[name]['size']) 735 736 return None 737 738 739if renpy.emscripten or os.environ.get('RENPY_SIMULATE_DOWNLOAD', False): 740 file_open_callbacks.append(load_from_remote_file) 741 742 743def check_name(name): 744 """ 745 Checks the name to see if it violates any of Ren'Py's rules. 746 """ 747 748 if renpy.config.reject_backslash and "\\" in name: 749 raise Exception("Backslash in filename, use '/' instead: %r" % name) 750 751 if renpy.config.reject_relative: 752 753 split = name.split("/") 754 755 if ("." in split) or (".." in split): 756 raise Exception("Filenames may not contain relative directories like '.' and '..': %r" % name) 757 758 759def get_prefixes(tl=True): 760 """ 761 Returns a list of prefixes to search for files. 762 """ 763 764 rv = [ ] 765 766 if tl: 767 language = renpy.game.preferences.language # @UndefinedVariable 768 else: 769 language = None 770 771 for prefix in renpy.config.search_prefixes: 772 773 if language is not None: 774 rv.append(renpy.config.tl_directory + "/" + language + "/" + prefix) 775 776 rv.append(prefix) 777 778 return rv 779 780 781def load(name, tl=True): 782 783 if renpy.display.predict.predicting: # @UndefinedVariable 784 if threading.current_thread().name == "MainThread": 785 if not (renpy.emscripten or os.environ.get('RENPY_SIMULATE_DOWNLOAD', False)): 786 raise Exception("Refusing to open {} while predicting.".format(name)) 787 788 if renpy.config.reject_backslash and "\\" in name: 789 raise Exception("Backslash in filename, use '/' instead: %r" % name) 790 791 name = re.sub(r'/+', '/', name).lstrip('/') 792 793 for p in get_prefixes(tl): 794 rv = load_core(p + name) 795 if rv is not None: 796 return rv 797 798 raise IOError("Couldn't find file '%s'." % name) 799 800 801def loadable_core(name): 802 """ 803 Returns True if the name is loadable with load, False if it is not. 804 """ 805 806 name = lower_map.get(unicodedata.normalize('NFC', name.lower()), name) 807 808 if name in loadable_cache: 809 return loadable_cache[name] 810 811 try: 812 transfn(name) 813 loadable_cache[name] = True 814 return True 815 except: 816 pass 817 818 for apk in apks: 819 prefixed_name = "/".join("x-" + i for i in name.split("/")) 820 if prefixed_name in apk.info: 821 loadable_cache[name] = True 822 return True 823 824 for _prefix, index in archives: 825 if name in index: 826 loadable_cache[name] = True 827 return True 828 829 if name in remote_files: 830 loadable_cache[name] = True 831 return name 832 833 loadable_cache[name] = False 834 return False 835 836 837def loadable(name): 838 839 name = name.lstrip('/') 840 841 if (renpy.config.loadable_callback is not None) and renpy.config.loadable_callback(name): 842 return True 843 844 for p in get_prefixes(): 845 if loadable_core(p + name): 846 return True 847 848 return False 849 850 851def transfn(name): 852 """ 853 Tries to translate the name to a file that exists in one of the 854 searched directories. 855 """ 856 857 name = name.lstrip('/') 858 859 if renpy.config.reject_backslash and "\\" in name: 860 raise Exception("Backslash in filename, use '/' instead: %r" % name) 861 862 name = lower_map.get(unicodedata.normalize('NFC', name.lower()), name) 863 864 if isinstance(name, bytes): 865 name = name.decode("utf-8") 866 867 for d in renpy.config.searchpath: 868 fn = os.path.join(renpy.config.basedir, d, name) 869 870 add_auto(fn) 871 872 if os.path.isfile(fn): 873 return fn 874 875 raise Exception("Couldn't find file '%s'." % name) 876 877 878hash_cache = dict() 879 880 881def get_hash(name): 882 """ 883 Returns the time the file m was last modified, or 0 if it 884 doesn't exist or is archived. 885 """ 886 887 rv = hash_cache.get(name, None) 888 if rv is not None: 889 return rv 890 891 rv = 0 892 893 try: 894 f = load(name) 895 896 while True: 897 data = f.read(1024 * 1024) 898 899 if not data: 900 break 901 902 rv = zlib.adler32(data, rv) 903 904 except: 905 pass 906 907 hash_cache[name] = rv 908 909 return rv 910 911# Module Loading 912 913 914class RenpyImporter(object): 915 """ 916 An importer, that tries to load modules from the places where Ren'Py 917 searches for data files. 918 """ 919 920 def __init__(self, prefix=""): 921 self.prefix = prefix 922 923 def translate(self, fullname, prefix=None): 924 925 if prefix is None: 926 prefix = self.prefix 927 928 try: 929 if not isinstance(fullname, str): 930 fullname = fullname.decode("utf-8") 931 932 fn = prefix + fullname.replace(".", "/") 933 934 except: 935 # raise Exception("Could importer-translate %r + %r" % (prefix, fullname)) 936 return None 937 938 if loadable(fn + ".py"): 939 return fn + ".py" 940 941 if loadable(fn + "/__init__.py"): 942 return fn + "/__init__.py" 943 944 return None 945 946 def find_module(self, fullname, path=None): 947 if path is not None: 948 for i in path: 949 if self.translate(fullname, i): 950 return RenpyImporter(i) 951 952 if self.translate(fullname): 953 return self 954 955 def load_module(self, fullname): 956 957 filename = self.translate(fullname, self.prefix) 958 959 pyname = pystr(fullname) 960 961 mod = sys.modules.setdefault(pyname, types.ModuleType(pyname)) 962 mod.__name__ = pyname 963 mod.__file__ = filename 964 mod.__loader__ = self 965 966 if filename.endswith("__init__.py"): 967 mod.__path__ = [ filename[:-len("__init__.py")] ] 968 969 for encoding in [ "utf-8", "latin-1" ]: 970 971 try: 972 973 source = load(filename).read().decode(encoding) 974 if source and source[0] == u'\ufeff': 975 source = source[1:] 976 source = source.encode("raw_unicode_escape") 977 source = source.replace(b"\r", b"") 978 979 code = compile(source, filename, 'exec', renpy.python.old_compile_flags, 1) 980 break 981 except: 982 if encoding == "latin-1": 983 raise 984 985 exec(code, mod.__dict__) 986 987 return sys.modules[fullname] 988 989 def get_data(self, filename): 990 return load(filename).read() 991 992 993meta_backup = [ ] 994 995 996def add_python_directory(path): 997 """ 998 :doc: other 999 1000 Adds `path` to the list of paths searched for Python modules and packages. 1001 The path should be a string relative to the game directory. This must be 1002 called before an import statement. 1003 """ 1004 1005 if path and not path.endswith("/"): 1006 path = path + "/" 1007 1008 sys.meta_path.insert(0, RenpyImporter(path)) 1009 1010 1011def init_importer(): 1012 meta_backup[:] = sys.meta_path 1013 1014 add_python_directory("python-packages/") 1015 add_python_directory("") 1016 1017 1018def quit_importer(): 1019 sys.meta_path[:] = meta_backup 1020 1021# Auto-Reload 1022 1023 1024# A list of files for which autoreload is needed. 1025needs_autoreload = set() 1026 1027# A map from filename to mtime, or None if the file doesn't exist. 1028auto_mtimes = { } 1029 1030# The thread used for autoreload. 1031auto_thread = None 1032 1033# True if auto_thread should run. False if it should quit. 1034auto_quit_flag = True 1035 1036# The lock used by auto_thread. 1037auto_lock = threading.Condition() 1038 1039# Used to indicate that this file is blacklisted. 1040auto_blacklisted = renpy.object.Sentinel("auto_blacklisted") 1041 1042 1043def auto_mtime(fn): 1044 """ 1045 Gets the mtime of fn, or None if the file does not exist. 1046 """ 1047 1048 try: 1049 return os.path.getmtime(fn) 1050 except: 1051 return None 1052 1053 1054def add_auto(fn, force=False): 1055 """ 1056 Adds fn as a file we watch for changes. If it's mtime changes or the file 1057 starts/stops existing, we trigger a reload. 1058 """ 1059 1060 fn = fn.replace("\\", "/") 1061 1062 if not renpy.autoreload: 1063 return 1064 1065 if (fn in auto_mtimes) and (not force): 1066 return 1067 1068 for e in renpy.config.autoreload_blacklist: 1069 if fn.endswith(e): 1070 with auto_lock: 1071 auto_mtimes[fn] = auto_blacklisted 1072 return 1073 1074 mtime = auto_mtime(fn) 1075 1076 with auto_lock: 1077 auto_mtimes[fn] = mtime 1078 1079 1080def auto_thread_function(): 1081 """ 1082 This thread sets need_autoreload when necessary. 1083 """ 1084 1085 global needs_autoreload 1086 1087 while True: 1088 1089 with auto_lock: 1090 1091 auto_lock.wait(1.5) 1092 1093 if auto_quit_flag: 1094 return 1095 1096 items = list(auto_mtimes.items()) 1097 1098 for fn, mtime in items: 1099 1100 if mtime is auto_blacklisted: 1101 continue 1102 1103 if auto_mtime(fn) != mtime: 1104 1105 with auto_lock: 1106 if auto_mtime(fn) != auto_mtimes[fn]: 1107 needs_autoreload.add(fn) 1108 1109 1110def check_autoreload(): 1111 """ 1112 Checks to see if autoreload is required. 1113 """ 1114 1115 while needs_autoreload: 1116 fn = next(iter(needs_autoreload)) 1117 mtime = auto_mtime(fn) 1118 1119 with auto_lock: 1120 needs_autoreload.discard(fn) 1121 auto_mtimes[fn] = mtime 1122 1123 if not renpy.autoreload: 1124 return 1125 1126 for regex, func in renpy.config.autoreload_functions: 1127 if re.search(regex, fn, re.I): 1128 fn = os.path.relpath(fn, renpy.config.gamedir).replace("\\", "/") 1129 func(fn) 1130 break 1131 else: 1132 renpy.exports.reload_script() 1133 1134 1135def auto_init(): 1136 """ 1137 Starts the autoreload thread. 1138 """ 1139 1140 global auto_thread 1141 global auto_quit_flag 1142 global needs_autoreload 1143 1144 needs_autoreload = set() 1145 1146 if not renpy.autoreload: 1147 return 1148 1149 auto_quit_flag = False 1150 1151 auto_thread = threading.Thread(target=auto_thread_function) 1152 auto_thread.daemon = True 1153 auto_thread.start() 1154 1155 1156def auto_quit(): 1157 """ 1158 Terminates the autoreload thread. 1159 """ 1160 global auto_quit_flag 1161 1162 if auto_thread is None: 1163 return 1164 1165 auto_quit_flag = True 1166 1167 with auto_lock: 1168 auto_lock.notify_all() 1169 1170 auto_thread.join() 1171