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