604 lines
18 KiB
Python
604 lines
18 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.display
|
|
import renpy.style
|
|
import renpy.sl2
|
|
import renpy.test
|
|
|
|
import renpy.game as game
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
import zipfile
|
|
import gc
|
|
|
|
import __main__
|
|
|
|
last_clock = time.time()
|
|
|
|
|
|
def log_clock(s):
|
|
global last_clock
|
|
now = time.time()
|
|
s = "{} took {:.2f}s".format(s, now - last_clock)
|
|
|
|
renpy.display.log.write(s)
|
|
if renpy.android and not renpy.config.log_to_stdout:
|
|
print(s)
|
|
|
|
# Pump the presplash window to prevent marking
|
|
# our process as unresponsive by OS
|
|
renpy.display.presplash.pump_window()
|
|
|
|
last_clock = now
|
|
|
|
|
|
def reset_clock():
|
|
global last_clock
|
|
last_clock = time.time()
|
|
|
|
|
|
def run(restart):
|
|
"""
|
|
This is called during a single run of the script. Restarting the script
|
|
will cause this to change.
|
|
"""
|
|
|
|
reset_clock()
|
|
|
|
# Reset the store to a clean version of itself.
|
|
renpy.python.clean_stores()
|
|
log_clock("Cleaning stores")
|
|
|
|
# Init translation.
|
|
renpy.translation.init_translation()
|
|
log_clock("Init translation")
|
|
|
|
# Rebuild the various style caches.
|
|
renpy.style.build_styles() # @UndefinedVariable
|
|
log_clock("Build styles")
|
|
|
|
renpy.sl2.slast.load_cache()
|
|
log_clock("Load screen analysis")
|
|
|
|
# Analyze the screens.
|
|
renpy.display.screen.analyze_screens()
|
|
log_clock("Analyze screens")
|
|
|
|
if not restart:
|
|
renpy.sl2.slast.save_cache()
|
|
log_clock("Save screen analysis")
|
|
|
|
# Prepare the screens.
|
|
renpy.display.screen.prepare_screens()
|
|
|
|
log_clock("Prepare screens")
|
|
|
|
if not restart:
|
|
renpy.pyanalysis.save_cache()
|
|
log_clock("Save pyanalysis.")
|
|
|
|
renpy.game.script.save_bytecode()
|
|
log_clock("Save bytecode.")
|
|
|
|
# Handle arguments and commands.
|
|
if not renpy.arguments.post_init():
|
|
renpy.exports.quit()
|
|
|
|
if renpy.config.clear_lines:
|
|
renpy.scriptedit.lines.clear()
|
|
|
|
# Sleep to finish the presplash.
|
|
renpy.display.presplash.sleep()
|
|
|
|
# Re-Initialize the log.
|
|
game.log = renpy.python.RollbackLog()
|
|
|
|
# Switch contexts, begin logging.
|
|
game.contexts = [ renpy.execution.Context(True) ]
|
|
|
|
# Jump to an appropriate start label.
|
|
if game.script.has_label("_start"):
|
|
start_label = '_start'
|
|
else:
|
|
start_label = 'start'
|
|
|
|
game.context().goto_label(start_label)
|
|
|
|
try:
|
|
renpy.exports.log("--- " + time.ctime())
|
|
renpy.exports.log("")
|
|
except:
|
|
pass
|
|
|
|
# Note if this is a restart.
|
|
renpy.store._restart = restart
|
|
|
|
# We run until we get an exception.
|
|
renpy.display.interface.enter_context()
|
|
|
|
log_clock("Running {}".format(start_label))
|
|
|
|
renpy.execution.run_context(True)
|
|
|
|
|
|
def load_rpe(fn):
|
|
|
|
zfn = zipfile.ZipFile(fn)
|
|
autorun = zfn.read("autorun.py")
|
|
zfn.close()
|
|
|
|
sys.path.insert(0, fn)
|
|
exec autorun in dict()
|
|
|
|
|
|
def choose_variants():
|
|
|
|
if "RENPY_VARIANT" in os.environ:
|
|
renpy.config.variants = list(os.environ["RENPY_VARIANT"].split()) + [ None ]
|
|
return
|
|
|
|
renpy.config.variants = [ None ]
|
|
|
|
if renpy.android: # @UndefinedVariable
|
|
|
|
renpy.config.variants.insert(0, 'mobile')
|
|
renpy.config.variants.insert(0, 'android')
|
|
|
|
import android # @UnresolvedImport
|
|
import math
|
|
import pygame_sdl2 as pygame
|
|
|
|
from jnius import autoclass # @UnresolvedImport
|
|
|
|
# Manufacturer/Model-specific variants.
|
|
try:
|
|
Build = autoclass("android.os.Build")
|
|
|
|
manufacturer = Build.MANUFACTURER
|
|
model = Build.MODEL
|
|
|
|
print("Manufacturer", manufacturer, "model", model)
|
|
|
|
if manufacturer == "Amazon" and model.startswith("AFT"):
|
|
print("Running on a Fire TV.")
|
|
renpy.config.variants.insert(0, "firetv")
|
|
except:
|
|
pass
|
|
|
|
# Are we running on OUYA or Google TV or something similar?
|
|
package_manager = android.activity.getPackageManager()
|
|
|
|
if package_manager.hasSystemFeature("android.hardware.type.television"):
|
|
print("Running on a television.")
|
|
renpy.config.variants.insert(0, "tv")
|
|
renpy.config.variants.insert(0, "small")
|
|
return
|
|
|
|
# Otherwise, a phone or tablet.
|
|
renpy.config.variants.insert(0, 'touch')
|
|
|
|
pygame.display.init()
|
|
|
|
info = renpy.display.get_info()
|
|
diag = math.hypot(info.current_w, info.current_h) / android.get_dpi()
|
|
print("Screen diagonal is", diag, "inches.")
|
|
|
|
if diag >= 6:
|
|
renpy.config.variants.insert(0, 'tablet')
|
|
renpy.config.variants.insert(0, 'medium')
|
|
else:
|
|
renpy.config.variants.insert(0, 'phone')
|
|
renpy.config.variants.insert(0, 'small')
|
|
|
|
elif renpy.ios:
|
|
renpy.config.variants.insert(0, 'ios')
|
|
renpy.config.variants.insert(0, 'touch')
|
|
|
|
from pyobjus import autoclass # @UnresolvedImport @Reimport
|
|
UIDevice = autoclass("UIDevice")
|
|
|
|
idiom = UIDevice.currentDevice().userInterfaceIdiom
|
|
|
|
print("iOS device idiom", idiom)
|
|
|
|
# idiom 0 is iPhone, 1 is iPad. We assume any bigger idiom will
|
|
# be tablet-like.
|
|
if idiom >= 1:
|
|
renpy.config.variants.insert(0, 'tablet')
|
|
renpy.config.variants.insert(0, 'medium')
|
|
else:
|
|
renpy.config.variants.insert(0, 'phone')
|
|
renpy.config.variants.insert(0, 'small')
|
|
|
|
elif renpy.emscripten:
|
|
import emscripten, re
|
|
|
|
# web
|
|
renpy.config.variants.insert(0, 'web')
|
|
|
|
# mobile
|
|
userAgent = emscripten.run_script_string(r'''navigator.userAgent''')
|
|
mobile = re.search('Mobile|Android|iPad|iPhone', userAgent)
|
|
if mobile:
|
|
renpy.config.variants.insert(0, 'mobile')
|
|
# Reserve android/ios for when the OS API is exposed
|
|
#if re.search('Android', userAgent):
|
|
# renpy.config.variants.insert(0, 'android')
|
|
#if re.search('iPad|iPhone', userAgent):
|
|
# renpy.config.variants.insert(0, 'ios')
|
|
|
|
# touch
|
|
touch = emscripten.run_script_int(r'''
|
|
('ontouchstart' in window) ||
|
|
(navigator.maxTouchPoints > 0) ||
|
|
(navigator.msMaxTouchPoints > 0)''')
|
|
if touch == 1:
|
|
# mitigate hybrids (e.g. ms surface) by restricting touch to mobile
|
|
if mobile:
|
|
renpy.config.variants.insert(0, 'touch')
|
|
|
|
# large/medium/small
|
|
# tablet/phone
|
|
# screen.width/height is auto-adjusted by browser,
|
|
# so it can be used as a physical sizereference
|
|
# (see also window.devicePixelRatio)
|
|
# e.g. Galaxy S5:
|
|
# - physical / OpenGL: 1080x1920
|
|
# - web screen: 360x640 w/ devicePixelRatio=3
|
|
ref_width = emscripten.run_script_int(r'''screen.width''')
|
|
ref_height = emscripten.run_script_int(r'''screen.height''')
|
|
# medium reference point: ipad 1024x768, ipad pro 1336x1024 (browser "pixels")
|
|
if mobile:
|
|
if (ref_width < 768 or ref_height < 768):
|
|
renpy.config.variants.insert(0, 'small')
|
|
renpy.config.variants.insert(0, 'phone')
|
|
else:
|
|
renpy.config.variants.insert(0, 'medium')
|
|
renpy.config.variants.insert(0, 'tablet')
|
|
else:
|
|
renpy.config.variants.insert(0, 'large')
|
|
|
|
else:
|
|
renpy.config.variants.insert(0, 'pc')
|
|
|
|
renpy.config.variants.insert(0, 'large')
|
|
|
|
|
|
def main():
|
|
|
|
log_clock("Bootstrap to the start of init.init")
|
|
|
|
renpy.game.exception_info = 'Before loading the script.'
|
|
|
|
# Get ready to accept new arguments.
|
|
renpy.arguments.pre_init()
|
|
|
|
# Init the screen language parser.
|
|
renpy.sl2.slparser.init()
|
|
|
|
# Init the config after load.
|
|
renpy.config.init()
|
|
|
|
# Set up variants.
|
|
choose_variants()
|
|
renpy.display.touch = "touch" in renpy.config.variants
|
|
|
|
log_clock("Early init")
|
|
|
|
# Note the game directory.
|
|
game.basepath = renpy.config.gamedir
|
|
renpy.config.searchpath = [ renpy.config.gamedir ]
|
|
|
|
# Find the common directory.
|
|
commondir = __main__.path_to_common(renpy.config.renpy_base) # E1101 @UndefinedVariable
|
|
|
|
if os.path.isdir(commondir):
|
|
renpy.config.searchpath.append(commondir)
|
|
renpy.config.commondir = commondir
|
|
else:
|
|
renpy.config.commondir = None
|
|
|
|
# Add path from env variable, if any
|
|
if "RENPY_SEARCHPATH" in os.environ:
|
|
renpy.config.searchpath.extend(os.environ["RENPY_SEARCHPATH"].split("::"))
|
|
|
|
if renpy.android:
|
|
renpy.config.searchpath = [ ]
|
|
renpy.config.commondir = None
|
|
|
|
if "ANDROID_PUBLIC" in os.environ:
|
|
android_game = os.path.join(os.environ["ANDROID_PUBLIC"], "game")
|
|
|
|
print("Android searchpath: ", android_game)
|
|
|
|
if os.path.exists(android_game):
|
|
renpy.config.searchpath.insert(0, android_game)
|
|
|
|
# Load Ren'Py extensions.
|
|
for dir in renpy.config.searchpath: # @ReservedAssignment
|
|
for fn in os.listdir(dir):
|
|
if fn.lower().endswith(".rpe"):
|
|
load_rpe(dir + "/" + fn)
|
|
|
|
# The basename is the final component of the path to the gamedir.
|
|
for i in sorted(os.listdir(renpy.config.gamedir)):
|
|
|
|
if not i.endswith(".rpa"):
|
|
continue
|
|
|
|
i = i[:-4]
|
|
renpy.config.archives.append(i)
|
|
|
|
renpy.config.archives.reverse()
|
|
|
|
# Initialize archives.
|
|
renpy.loader.index_archives()
|
|
|
|
# Start auto-loading.
|
|
renpy.loader.auto_init()
|
|
|
|
log_clock("Loader init")
|
|
|
|
# Initialize the log.
|
|
game.log = renpy.python.RollbackLog()
|
|
|
|
# Initialize the store.
|
|
renpy.store.store = sys.modules['store']
|
|
|
|
# Set up styles.
|
|
game.style = renpy.style.StyleManager() # @UndefinedVariable
|
|
renpy.store.style = game.style
|
|
|
|
# Run init code in its own context. (Don't log.)
|
|
game.contexts = [ renpy.execution.Context(False) ]
|
|
game.contexts[0].init_phase = True
|
|
|
|
renpy.execution.not_infinite_loop(60)
|
|
|
|
# Load the script.
|
|
renpy.game.exception_info = 'While loading the script.'
|
|
renpy.game.script = renpy.script.Script()
|
|
|
|
if renpy.session.get("compile", False):
|
|
renpy.game.args.compile = True
|
|
|
|
# Set up error handling.
|
|
renpy.exports.load_module("_errorhandling")
|
|
|
|
if renpy.exports.loadable("tl/None/common.rpym") or renpy.exports.loadable("tl/None/common.rpymc"):
|
|
renpy.exports.load_module("tl/None/common")
|
|
|
|
renpy.config.init_system_styles()
|
|
renpy.style.build_styles() # @UndefinedVariable
|
|
|
|
log_clock("Loading error handling")
|
|
|
|
# If recompiling everything, remove orphan .rpyc files.
|
|
# Otherwise, will fail in case orphan .rpyc have same
|
|
# labels as in other scripts (usually happens on script rename).
|
|
if (renpy.game.args.command == 'compile') and not (renpy.game.args.keep_orphan_rpyc): # @UndefinedVariable
|
|
|
|
for (fn, dn) in renpy.game.script.script_files:
|
|
|
|
if dn is None:
|
|
continue
|
|
|
|
if not os.path.isfile(os.path.join(dn, fn + ".rpy")):
|
|
|
|
try:
|
|
name = os.path.join(dn, fn + ".rpyc")
|
|
os.rename(name, name + ".bak")
|
|
except OSError:
|
|
# This perhaps shouldn't happen since either .rpy or .rpyc should exist
|
|
pass
|
|
|
|
# Update script files list, so that it doesn't contain removed .rpyc's
|
|
renpy.loader.cleardirfiles()
|
|
renpy.game.script.scan_script_files()
|
|
|
|
# Load all .rpy files.
|
|
renpy.game.script.load_script() # sets renpy.game.script.
|
|
log_clock("Loading script")
|
|
|
|
if renpy.game.args.command == 'load-test': # @UndefinedVariable
|
|
start = time.time()
|
|
|
|
for i in range(5):
|
|
print(i)
|
|
renpy.game.script = renpy.script.Script()
|
|
renpy.game.script.load_script()
|
|
|
|
print(time.time() - start)
|
|
sys.exit(0)
|
|
|
|
renpy.game.exception_info = 'After loading the script.'
|
|
|
|
# Find the save directory.
|
|
if renpy.config.savedir is None:
|
|
renpy.config.savedir = __main__.path_to_saves(renpy.config.gamedir) # E1101 @UndefinedVariable
|
|
|
|
if renpy.game.args.savedir: # @UndefinedVariable
|
|
renpy.config.savedir = renpy.game.args.savedir # @UndefinedVariable
|
|
|
|
# Init preferences.
|
|
game.persistent = renpy.persistent.init()
|
|
game.preferences = game.persistent._preferences
|
|
|
|
for i in renpy.game.persistent._seen_translates: # @UndefinedVariable
|
|
if i in renpy.game.script.translator.default_translates:
|
|
renpy.game.seen_translates_count += 1
|
|
|
|
if game.persistent._virtual_size:
|
|
renpy.config.screen_width, renpy.config.screen_height = game.persistent._virtual_size
|
|
|
|
# Init save locations and loadsave.
|
|
renpy.savelocation.init()
|
|
|
|
# We need to be 100% sure we kill the savelocation thread.
|
|
try:
|
|
|
|
# Init save slots.
|
|
renpy.loadsave.init()
|
|
|
|
log_clock("Loading save slot metadata.")
|
|
|
|
# Load persistent data from all save locations.
|
|
renpy.persistent.update()
|
|
game.preferences = game.persistent._preferences
|
|
log_clock("Loading persistent")
|
|
|
|
# Clear the list of seen statements in this game.
|
|
game.seen_session = { }
|
|
|
|
# Initialize persistent variables.
|
|
renpy.store.persistent = game.persistent
|
|
renpy.store._preferences = game.preferences
|
|
renpy.store._test = renpy.test.testast._test
|
|
|
|
if renpy.parser.report_parse_errors():
|
|
raise renpy.game.ParseErrorException()
|
|
|
|
renpy.game.exception_info = 'While executing init code:'
|
|
|
|
for _prio, node in game.script.initcode:
|
|
|
|
if isinstance(node, renpy.ast.Node):
|
|
renpy.game.context().run(node)
|
|
else:
|
|
# An init function.
|
|
node()
|
|
|
|
renpy.game.exception_info = 'After initialization, but before game start.'
|
|
|
|
# Check if we should simulate android.
|
|
renpy.android = renpy.android or renpy.config.simulate_android # @UndefinedVariable
|
|
|
|
# Re-set up the logging.
|
|
renpy.log.post_init()
|
|
|
|
# Run the post init code, if any.
|
|
for i in renpy.game.post_init:
|
|
i()
|
|
|
|
renpy.game.script.report_duplicate_labels()
|
|
|
|
# Sort the images.
|
|
renpy.display.image.image_names.sort()
|
|
|
|
game.persistent._virtual_size = renpy.config.screen_width, renpy.config.screen_height
|
|
|
|
log_clock("Running init code")
|
|
|
|
renpy.pyanalysis.load_cache()
|
|
log_clock("Loading analysis data")
|
|
|
|
# Analyze the script and compile ATL.
|
|
renpy.game.script.analyze()
|
|
renpy.atl.compile_all()
|
|
log_clock("Analyze and compile ATL")
|
|
|
|
# Index the archive files. We should not have loaded an image
|
|
# before this point. (As pygame will not have been initialized.)
|
|
# We need to do this again because the list of known archives
|
|
# may have changed.
|
|
renpy.loader.index_archives()
|
|
log_clock("Index archives")
|
|
|
|
# Check some environment variables.
|
|
renpy.game.less_memory = "RENPY_LESS_MEMORY" in os.environ
|
|
renpy.game.less_mouse = "RENPY_LESS_MOUSE" in os.environ
|
|
renpy.game.less_updates = "RENPY_LESS_UPDATES" in os.environ
|
|
|
|
renpy.dump.dump(False)
|
|
renpy.game.script.make_backups()
|
|
log_clock("Dump and make backups.")
|
|
|
|
# Initialize image cache.
|
|
renpy.display.im.cache.init()
|
|
log_clock("Cleaning cache")
|
|
|
|
# Make a clean copy of the store.
|
|
renpy.python.make_clean_stores()
|
|
log_clock("Making clean stores")
|
|
|
|
gc.collect()
|
|
|
|
if renpy.config.manage_gc:
|
|
gc.set_threshold(*renpy.config.gc_thresholds)
|
|
|
|
gc_debug = int(os.environ.get("RENPY_GC_DEBUG", 0))
|
|
|
|
if renpy.config.gc_print_unreachable:
|
|
gc_debug |= gc.DEBUG_SAVEALL
|
|
|
|
gc.set_debug(gc_debug)
|
|
|
|
log_clock("Initial gc.")
|
|
|
|
# Start debugging file opens.
|
|
renpy.debug.init_main_thread_open()
|
|
|
|
# (Perhaps) Initialize graphics.
|
|
if not game.interface:
|
|
renpy.display.core.Interface()
|
|
log_clock("Creating interface object")
|
|
|
|
# Start things running.
|
|
restart = None
|
|
|
|
while True:
|
|
|
|
if restart:
|
|
renpy.display.screen.before_restart()
|
|
|
|
try:
|
|
try:
|
|
run(restart)
|
|
finally:
|
|
restart = (renpy.config.end_game_transition, "_invoke_main_menu", "_main_menu")
|
|
renpy.persistent.update(True)
|
|
|
|
except game.FullRestartException as e:
|
|
restart = e.reason
|
|
|
|
finally:
|
|
|
|
# Flush any pending interface work.
|
|
renpy.display.interface.finish_pending()
|
|
|
|
# Give Ren'Py a couple of seconds to finish saving.
|
|
renpy.loadsave.autosave_not_running.wait(3.0)
|
|
|
|
finally:
|
|
|
|
gc.set_debug(0)
|
|
|
|
renpy.loader.auto_quit()
|
|
renpy.savelocation.quit()
|
|
renpy.translation.write_updated_strings()
|
|
|
|
# This is stuff we do on a normal, non-error return.
|
|
if not renpy.display.error.error_handled:
|
|
renpy.display.render.check_at_shutdown()
|