915 lines
20 KiB
Python
915 lines
20 KiB
Python
# Copyright 2004-2019 Tom Rothamel <pytom@bishoujo.us>
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person
|
|
# obtaining a copy of this software and associated documentation files
|
|
# (the "Software"), to deal in the Software without restriction,
|
|
# including without limitation the rights to use, copy, modify, merge,
|
|
# publish, distribute, sublicense, and/or sell copies of the Software,
|
|
# and to permit persons to whom the Software is furnished to do so,
|
|
# subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be
|
|
# included in all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
from __future__ import print_function
|
|
import renpy
|
|
import os.path
|
|
from pickle import loads
|
|
from cStringIO import StringIO
|
|
import sys
|
|
import types
|
|
import threading
|
|
import zlib
|
|
import re
|
|
|
|
# Ensure the utf-8 codec is loaded, to prevent recursion when we use it
|
|
# to look up filenames.
|
|
u"".encode("utf-8")
|
|
|
|
|
|
# Physical Paths
|
|
|
|
def get_path(fn):
|
|
"""
|
|
Returns the path to `fn` relative to the gamedir. If any of the directories
|
|
leading to `fn` do not exist, tries to create them.
|
|
|
|
This always returns a path, but the path may or may not be writable.
|
|
"""
|
|
|
|
fn = os.path.join(renpy.config.gamedir, fn)
|
|
dn = os.path.dirname(fn)
|
|
|
|
try:
|
|
if not os.path.exists(dn):
|
|
os.makedirs(dn)
|
|
except:
|
|
pass
|
|
|
|
return fn
|
|
|
|
# Asset Loading
|
|
|
|
|
|
try:
|
|
import android.apk
|
|
|
|
expansion = os.environ.get("ANDROID_EXPANSION", None)
|
|
if expansion is not None:
|
|
print("Using expansion file", expansion)
|
|
|
|
apks = [
|
|
android.apk.APK(apk=expansion, prefix='assets/x-game/'),
|
|
android.apk.APK(apk=expansion, prefix='assets/x-renpy/x-common/'),
|
|
]
|
|
|
|
game_apks = [ apks[0] ]
|
|
|
|
else:
|
|
print("Not using expansion file.")
|
|
|
|
apks = [
|
|
android.apk.APK(prefix='assets/x-game/'),
|
|
android.apk.APK(prefix='assets/x-renpy/x-common/'),
|
|
]
|
|
|
|
game_apks = [ apks[0] ]
|
|
|
|
except ImportError:
|
|
apks = [ ]
|
|
game_apks = [ ]
|
|
|
|
# Files on disk should be checked before archives. Otherwise, among
|
|
# other things, using a new version of bytecode.rpyb will break.
|
|
archives = [ ]
|
|
|
|
# The value of renpy.config.archives the last time index_archives was
|
|
# run.
|
|
old_config_archives = None
|
|
|
|
# A map from lower-case filename to regular-case filename.
|
|
lower_map = { }
|
|
|
|
|
|
def index_archives():
|
|
"""
|
|
Loads in the indexes for the archive files. Also updates the lower_map.
|
|
"""
|
|
|
|
# Index the archives.
|
|
|
|
global old_config_archives
|
|
|
|
if old_config_archives == renpy.config.archives:
|
|
return
|
|
|
|
old_config_archives = renpy.config.archives[:]
|
|
|
|
# Update lower_map.
|
|
lower_map.clear()
|
|
|
|
cleardirfiles()
|
|
|
|
global archives
|
|
archives = [ ]
|
|
|
|
for prefix in renpy.config.archives:
|
|
|
|
try:
|
|
fn = transfn(prefix + ".rpa")
|
|
f = file(fn, "rb")
|
|
l = f.readline()
|
|
|
|
# 3.0 Branch.
|
|
if l.startswith("RPA-3.0 "):
|
|
offset = int(l[8:24], 16)
|
|
key = int(l[25:33], 16)
|
|
f.seek(offset)
|
|
index = loads(f.read().decode("zlib"))
|
|
|
|
# Deobfuscate the index.
|
|
|
|
for k in index.keys():
|
|
|
|
if len(index[k][0]) == 2:
|
|
index[k] = [ (offset ^ key, dlen ^ key) for offset, dlen in index[k] ]
|
|
else:
|
|
index[k] = [ (offset ^ key, dlen ^ key, start) for offset, dlen, start in index[k] ]
|
|
|
|
archives.append((prefix, index))
|
|
|
|
f.close()
|
|
continue
|
|
|
|
# 2.0 Branch.
|
|
if l.startswith("RPA-2.0 "):
|
|
offset = int(l[8:], 16)
|
|
f.seek(offset)
|
|
index = loads(f.read().decode("zlib"))
|
|
archives.append((prefix, index))
|
|
f.close()
|
|
continue
|
|
|
|
# 1.0 Branch.
|
|
f.close()
|
|
|
|
fn = transfn(prefix + ".rpi")
|
|
index = loads(file(fn, "rb").read().decode("zlib"))
|
|
archives.append((prefix, index))
|
|
except:
|
|
raise
|
|
|
|
for dir, fn in listdirfiles(): # @ReservedAssignment
|
|
lower_map[fn.lower()] = fn
|
|
|
|
|
|
def walkdir(dir): # @ReservedAssignment
|
|
rv = [ ]
|
|
|
|
if not os.path.exists(dir) and not renpy.config.developer:
|
|
return rv
|
|
|
|
for i in os.listdir(dir):
|
|
if i[0] == ".":
|
|
continue
|
|
|
|
try:
|
|
i = renpy.exports.fsdecode(i)
|
|
except:
|
|
continue
|
|
|
|
if os.path.isdir(dir + "/" + i):
|
|
for fn in walkdir(dir + "/" + i):
|
|
rv.append(i + "/" + fn)
|
|
else:
|
|
rv.append(i)
|
|
|
|
return rv
|
|
|
|
|
|
# A list of files that make up the game.
|
|
game_files = [ ]
|
|
|
|
# A list of files that are in the common directory.
|
|
common_files = [ ]
|
|
|
|
# A map from filename to if the file is loadable.
|
|
loadable_cache = { }
|
|
|
|
|
|
def cleardirfiles():
|
|
"""
|
|
Clears the lists above when the game has changed.
|
|
"""
|
|
|
|
global game_files
|
|
global common_files
|
|
|
|
game_files = [ ]
|
|
common_files = [ ]
|
|
|
|
|
|
def scandirfiles():
|
|
"""
|
|
Scans directories, archives, and apks and fills out game_files and
|
|
common_files.
|
|
"""
|
|
|
|
seen = set()
|
|
|
|
def add(dn, fn):
|
|
if fn in seen:
|
|
return
|
|
|
|
if fn.startswith("cache/"):
|
|
return
|
|
|
|
if fn.startswith("saves/"):
|
|
return
|
|
|
|
files.append((dn, fn))
|
|
seen.add(fn)
|
|
loadable_cache[fn.lower()] = True
|
|
|
|
for apk in apks:
|
|
|
|
if apk not in game_apks:
|
|
files = common_files # @UnusedVariable
|
|
else:
|
|
files = game_files # @UnusedVariable
|
|
|
|
for f in apk.list():
|
|
|
|
# Strip off the "x-" in front of each filename, which is there
|
|
# to ensure that aapt actually includes every file.
|
|
f = "/".join(i[2:] for i in f.split("/"))
|
|
|
|
add(None, f)
|
|
|
|
for i in renpy.config.searchpath:
|
|
|
|
if (renpy.config.commondir) and (i == renpy.config.commondir):
|
|
files = common_files # @UnusedVariable
|
|
else:
|
|
files = game_files # @UnusedVariable
|
|
|
|
i = os.path.join(renpy.config.basedir, i)
|
|
for j in walkdir(i):
|
|
add(i, j)
|
|
|
|
files = game_files
|
|
|
|
for _prefix, index in archives:
|
|
for j in index.iterkeys():
|
|
add(None, j)
|
|
|
|
|
|
def listdirfiles(common=True):
|
|
"""
|
|
Returns a list of directory, file tuples known to the system. If
|
|
the file is in an archive, the directory is None.
|
|
"""
|
|
|
|
if (not game_files) and (not common_files):
|
|
scandirfiles()
|
|
|
|
if common:
|
|
return game_files + common_files
|
|
else:
|
|
return list(game_files)
|
|
|
|
|
|
class SubFile(object):
|
|
|
|
def __init__(self, fn, base, length, start):
|
|
self.fn = fn
|
|
|
|
self.f = None
|
|
|
|
self.base = base
|
|
self.offset = 0
|
|
self.length = length
|
|
self.start = start
|
|
|
|
if not self.start:
|
|
self.name = fn
|
|
else:
|
|
self.name = None
|
|
|
|
def open(self):
|
|
self.f = open(self.fn, "rb")
|
|
self.f.seek(self.base)
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, _type, value, tb):
|
|
self.close()
|
|
return False
|
|
|
|
def read(self, length=None):
|
|
|
|
if self.f is None:
|
|
self.open()
|
|
|
|
maxlength = self.length - self.offset
|
|
|
|
if length is not None:
|
|
length = min(length, maxlength)
|
|
else:
|
|
length = maxlength
|
|
|
|
rv1 = self.start[self.offset:self.offset + length]
|
|
length -= len(rv1)
|
|
self.offset += len(rv1)
|
|
|
|
if length:
|
|
rv2 = self.f.read(length)
|
|
self.offset += len(rv2)
|
|
else:
|
|
rv2 = ""
|
|
|
|
return (rv1 + rv2)
|
|
|
|
def readline(self, length=None):
|
|
|
|
if self.f is None:
|
|
self.open()
|
|
|
|
maxlength = self.length - self.offset
|
|
if length is not None:
|
|
length = min(length, maxlength)
|
|
else:
|
|
length = maxlength
|
|
|
|
# If we're in the start, then read the line ourselves.
|
|
if self.offset < len(self.start):
|
|
rv = ''
|
|
|
|
while length:
|
|
c = self.read(1)
|
|
rv += c
|
|
if c == '\n':
|
|
break
|
|
length -= 1
|
|
|
|
return rv
|
|
|
|
# Otherwise, let the system read the line all at once.
|
|
rv = self.f.readline(length)
|
|
|
|
self.offset += len(rv)
|
|
|
|
return rv
|
|
|
|
def readlines(self, length=None):
|
|
rv = [ ]
|
|
|
|
while True:
|
|
l = self.readline(length)
|
|
|
|
if not l:
|
|
break
|
|
|
|
if length is not None:
|
|
length -= len(l)
|
|
if l < 0:
|
|
break
|
|
|
|
rv.append(l)
|
|
|
|
return rv
|
|
|
|
def xreadlines(self):
|
|
return self
|
|
|
|
def __iter__(self):
|
|
return self
|
|
|
|
def next(self): # @ReservedAssignment
|
|
rv = self.readline()
|
|
|
|
if not rv:
|
|
raise StopIteration()
|
|
|
|
return rv
|
|
|
|
def flush(self):
|
|
return
|
|
|
|
def seek(self, offset, whence=0):
|
|
|
|
if self.f is None:
|
|
self.open()
|
|
|
|
if whence == 0:
|
|
offset = offset
|
|
elif whence == 1:
|
|
offset = self.offset + offset
|
|
elif whence == 2:
|
|
offset = self.length + offset
|
|
|
|
if offset > self.length:
|
|
offset = self.length
|
|
|
|
self.offset = offset
|
|
|
|
offset = offset - len(self.start)
|
|
if offset < 0:
|
|
offset = 0
|
|
|
|
self.f.seek(offset + self.base)
|
|
|
|
def tell(self):
|
|
return self.offset
|
|
|
|
def close(self):
|
|
if self.f is not None:
|
|
self.f.close()
|
|
self.f = None
|
|
|
|
def write(self, s):
|
|
raise Exception("Write not supported by SubFile")
|
|
|
|
|
|
open_file = open
|
|
|
|
if "RENPY_FORCE_SUBFILE" in os.environ:
|
|
def open_file(name, mode):
|
|
f = open(name, mode)
|
|
|
|
f.seek(0, 2)
|
|
length = f.tell()
|
|
f.seek(0, 0)
|
|
|
|
return SubFile(f, 0, length, '')
|
|
|
|
|
|
def load_core(name):
|
|
"""
|
|
Returns an open python file object of the given type.
|
|
"""
|
|
|
|
name = lower_map.get(name.lower(), name)
|
|
|
|
if renpy.config.file_open_callback:
|
|
rv = renpy.config.file_open_callback(name)
|
|
if rv is not None:
|
|
return rv
|
|
|
|
# Look for the file directly.
|
|
if not renpy.config.force_archives:
|
|
try:
|
|
fn = transfn(name)
|
|
return open_file(fn, "rb")
|
|
except:
|
|
pass
|
|
|
|
# Look for the file in the apk.
|
|
for apk in apks:
|
|
prefixed_name = "/".join("x-" + i for i in name.split("/"))
|
|
|
|
try:
|
|
return apk.open(prefixed_name)
|
|
except IOError:
|
|
pass
|
|
|
|
# Look for it in archive files.
|
|
for prefix, index in archives:
|
|
if not name in index:
|
|
continue
|
|
|
|
afn = transfn(prefix + ".rpa")
|
|
|
|
data = [ ]
|
|
|
|
# Direct path.
|
|
if len(index[name]) == 1:
|
|
|
|
t = index[name][0]
|
|
if len(t) == 2:
|
|
offset, dlen = t
|
|
start = ''
|
|
else:
|
|
offset, dlen, start = t
|
|
|
|
rv = SubFile(afn, offset, dlen, start)
|
|
|
|
# Compatibility path.
|
|
else:
|
|
f = file(afn, "rb")
|
|
|
|
for offset, dlen in index[name]:
|
|
f.seek(offset)
|
|
data.append(f.read(dlen))
|
|
|
|
rv = StringIO(''.join(data))
|
|
f.close()
|
|
|
|
return rv
|
|
|
|
return None
|
|
|
|
|
|
def check_name(name):
|
|
"""
|
|
Checks the name to see if it violates any of Ren'Py's rules.
|
|
"""
|
|
|
|
if renpy.config.reject_backslash and "\\" in name:
|
|
raise Exception("Backslash in filename, use '/' instead: %r" % name)
|
|
|
|
if renpy.config.reject_relative:
|
|
|
|
split = name.split("/")
|
|
|
|
if ("." in split) or (".." in split):
|
|
raise Exception("Filenames may not contain relative directories like '.' and '..': %r" % name)
|
|
|
|
|
|
def get_prefixes(tl=True):
|
|
"""
|
|
Returns a list of prefixes to search for files.
|
|
"""
|
|
|
|
rv = [ ]
|
|
|
|
if tl:
|
|
language = renpy.game.preferences.language # @UndefinedVariable
|
|
else:
|
|
language = None
|
|
|
|
for prefix in renpy.config.search_prefixes:
|
|
|
|
if language is not None:
|
|
rv.append(renpy.config.tl_directory + "/" + language + "/" + prefix)
|
|
|
|
rv.append(prefix)
|
|
|
|
return rv
|
|
|
|
|
|
def load(name, tl=True):
|
|
|
|
if renpy.display.predict.predicting: # @UndefinedVariable
|
|
if threading.current_thread().name == "MainThread":
|
|
raise Exception("Refusing to open {} while predicting.".format(name))
|
|
|
|
if renpy.config.reject_backslash and "\\" in name:
|
|
raise Exception("Backslash in filename, use '/' instead: %r" % name)
|
|
|
|
name = re.sub(r'/+', '/', name).lstrip('/')
|
|
|
|
for p in get_prefixes(tl):
|
|
rv = load_core(p + name)
|
|
if rv is not None:
|
|
return rv
|
|
|
|
raise IOError("Couldn't find file '%s'." % name)
|
|
|
|
|
|
def loadable_core(name):
|
|
"""
|
|
Returns True if the name is loadable with load, False if it is not.
|
|
"""
|
|
|
|
name = lower_map.get(name.lower(), name)
|
|
|
|
if name in loadable_cache:
|
|
return loadable_cache[name]
|
|
|
|
try:
|
|
transfn(name)
|
|
loadable_cache[name] = True
|
|
return True
|
|
except:
|
|
pass
|
|
|
|
for apk in apks:
|
|
prefixed_name = "/".join("x-" + i for i in name.split("/"))
|
|
if prefixed_name in apk.info:
|
|
loadable_cache[name] = True
|
|
return True
|
|
|
|
for _prefix, index in archives:
|
|
if name in index:
|
|
loadable_cache[name] = True
|
|
return True
|
|
|
|
loadable_cache[name] = False
|
|
return False
|
|
|
|
|
|
def loadable(name):
|
|
|
|
name = name.lstrip('/')
|
|
|
|
if (renpy.config.loadable_callback is not None) and renpy.config.loadable_callback(name):
|
|
return True
|
|
|
|
for p in get_prefixes():
|
|
if loadable_core(p + name):
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def transfn(name):
|
|
"""
|
|
Tries to translate the name to a file that exists in one of the
|
|
searched directories.
|
|
"""
|
|
|
|
name = name.lstrip('/')
|
|
|
|
if renpy.config.reject_backslash and "\\" in name:
|
|
raise Exception("Backslash in filename, use '/' instead: %r" % name)
|
|
|
|
name = lower_map.get(name.lower(), name)
|
|
|
|
if isinstance(name, str):
|
|
name = name.decode("utf-8")
|
|
|
|
for d in renpy.config.searchpath:
|
|
fn = os.path.join(renpy.config.basedir, d, name)
|
|
|
|
add_auto(fn)
|
|
|
|
if os.path.exists(fn):
|
|
return fn
|
|
|
|
raise Exception("Couldn't find file '%s'." % name)
|
|
|
|
|
|
hash_cache = dict()
|
|
|
|
|
|
def get_hash(name):
|
|
"""
|
|
Returns the time the file m was last modified, or 0 if it
|
|
doesn't exist or is archived.
|
|
"""
|
|
|
|
rv = hash_cache.get(name, None)
|
|
if rv is not None:
|
|
return rv
|
|
|
|
rv = 0
|
|
|
|
try:
|
|
f = load(name)
|
|
|
|
while True:
|
|
data = f.read(1024 * 1024)
|
|
|
|
if not data:
|
|
break
|
|
|
|
rv = zlib.adler32(data, rv)
|
|
|
|
except:
|
|
pass
|
|
|
|
hash_cache[name] = rv
|
|
|
|
return rv
|
|
|
|
|
|
# Module Loading
|
|
|
|
class RenpyImporter(object):
|
|
"""
|
|
An importer, that tries to load modules from the places where Ren'Py
|
|
searches for data files.
|
|
"""
|
|
|
|
def __init__(self, prefix=""):
|
|
self.prefix = prefix
|
|
|
|
def translate(self, fullname, prefix=None):
|
|
|
|
if prefix is None:
|
|
prefix = self.prefix
|
|
|
|
try:
|
|
fn = (prefix + fullname.replace(".", "/")).decode("utf8")
|
|
except:
|
|
# raise Exception("Could importer-translate %r + %r" % (prefix, fullname))
|
|
return None
|
|
|
|
if loadable(fn + ".py"):
|
|
return fn + ".py"
|
|
|
|
if loadable(fn + "/__init__.py"):
|
|
return fn + "/__init__.py"
|
|
|
|
return None
|
|
|
|
def find_module(self, fullname, path=None):
|
|
if path is not None:
|
|
for i in path:
|
|
if self.translate(fullname, i):
|
|
return RenpyImporter(i)
|
|
|
|
if self.translate(fullname):
|
|
return self
|
|
|
|
def load_module(self, fullname):
|
|
|
|
filename = self.translate(fullname, self.prefix)
|
|
|
|
mod = sys.modules.setdefault(fullname, types.ModuleType(fullname))
|
|
mod.__name__ = fullname
|
|
mod.__file__ = filename
|
|
mod.__loader__ = self
|
|
|
|
if filename.endswith("__init__.py"):
|
|
mod.__path__ = [ filename[:-len("__init__.py")] ]
|
|
|
|
for encoding in [ "utf-8", "latin-1" ]:
|
|
|
|
try:
|
|
|
|
source = load(filename).read().decode(encoding)
|
|
if source and source[0] == u'\ufeff':
|
|
source = source[1:]
|
|
source = source.encode("raw_unicode_escape")
|
|
|
|
source = source.replace("\r", "")
|
|
|
|
code = compile(source, filename, 'exec', renpy.python.old_compile_flags, 1)
|
|
break
|
|
except:
|
|
if encoding == "latin-1":
|
|
raise
|
|
|
|
exec code in mod.__dict__
|
|
|
|
return sys.modules[fullname]
|
|
|
|
def get_data(self, filename):
|
|
return load(filename).read()
|
|
|
|
|
|
meta_backup = [ ]
|
|
|
|
|
|
def add_python_directory(path):
|
|
"""
|
|
:doc: other
|
|
|
|
Adds `path` to the list of paths searched for Python modules and packages.
|
|
The path should be a string relative to the game directory. This must be
|
|
called before an import statement.
|
|
"""
|
|
|
|
if path and not path.endswith("/"):
|
|
path = path + "/"
|
|
|
|
sys.meta_path.insert(0, RenpyImporter(path))
|
|
|
|
|
|
def init_importer():
|
|
meta_backup[:] = sys.meta_path
|
|
|
|
add_python_directory("python-packages/")
|
|
add_python_directory("")
|
|
|
|
|
|
def quit_importer():
|
|
sys.meta_path[:] = meta_backup
|
|
|
|
|
|
# Auto-Reload
|
|
|
|
|
|
# This is set to True if autoreload has detected an autoreload is needed.
|
|
needs_autoreload = False
|
|
|
|
# A map from filename to mtime, or None if the file doesn't exist.
|
|
auto_mtimes = { }
|
|
|
|
# The thread used for autoreload.
|
|
auto_thread = None
|
|
|
|
# True if auto_thread should run. False if it should quit.
|
|
auto_quit_flag = True
|
|
|
|
# The lock used by auto_thread.
|
|
auto_lock = threading.Condition()
|
|
|
|
# Used to indicate that this file is blacklisted.
|
|
auto_blacklisted = renpy.object.Sentinel("auto_blacklisted")
|
|
|
|
|
|
def auto_mtime(fn):
|
|
"""
|
|
Gets the mtime of fn, or None if the file does not exist.
|
|
"""
|
|
|
|
try:
|
|
return os.path.getmtime(fn)
|
|
except:
|
|
return None
|
|
|
|
|
|
def add_auto(fn, force=False):
|
|
"""
|
|
Adds fn as a file we watch for changes. If it's mtime changes or the file
|
|
starts/stops existing, we trigger a reload.
|
|
"""
|
|
|
|
fn = fn.replace("\\", "/")
|
|
|
|
if not renpy.autoreload:
|
|
return
|
|
|
|
if (fn in auto_mtimes) and (not force):
|
|
return
|
|
|
|
for e in renpy.config.autoreload_blacklist:
|
|
if fn.endswith(e):
|
|
with auto_lock:
|
|
auto_mtimes[fn] = auto_blacklisted
|
|
return
|
|
|
|
mtime = auto_mtime(fn)
|
|
|
|
with auto_lock:
|
|
auto_mtimes[fn] = mtime
|
|
|
|
|
|
def auto_thread_function():
|
|
"""
|
|
This thread sets need_autoreload when necessary.
|
|
"""
|
|
|
|
global needs_autoreload
|
|
|
|
while True:
|
|
|
|
with auto_lock:
|
|
|
|
auto_lock.wait(1.5)
|
|
|
|
if auto_quit_flag:
|
|
return
|
|
|
|
items = auto_mtimes.items()
|
|
|
|
for fn, mtime in items:
|
|
|
|
if mtime is auto_blacklisted:
|
|
continue
|
|
|
|
if auto_mtime(fn) != mtime:
|
|
|
|
with auto_lock:
|
|
if auto_mtime(fn) != auto_mtimes[fn]:
|
|
needs_autoreload = True
|
|
|
|
|
|
def auto_init():
|
|
"""
|
|
Starts the autoreload thread.
|
|
"""
|
|
|
|
global auto_thread
|
|
global auto_quit_flag
|
|
global needs_autoreload
|
|
|
|
needs_autoreload = False
|
|
|
|
if not renpy.autoreload:
|
|
return
|
|
|
|
auto_quit_flag = False
|
|
|
|
auto_thread = threading.Thread(target=auto_thread_function)
|
|
auto_thread.daemon = True
|
|
auto_thread.start()
|
|
|
|
|
|
def auto_quit():
|
|
"""
|
|
Terminates the autoreload thread.
|
|
"""
|
|
global auto_quit_flag
|
|
|
|
if auto_thread is None:
|
|
return
|
|
|
|
auto_quit_flag = True
|
|
|
|
with auto_lock:
|
|
auto_lock.notify_all()
|
|
|
|
auto_thread.join()
|