CampBuddy/Camp.Buddy v2.2.1/Camp_Buddy-2.2.1-pc/renpy/loader.py
2025-03-03 23:00:33 +01:00

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()