Web Updater

* Forbade interactions outside the Updater screen
* Added Automatic Updates preference
* Updated config vars
* Disabled custom GC values for wider testing purposes
* Added update logo fallback in case of unexpected circumstances
* Added CheckUpdates Screen Action Function
* Simplified parts of the updater code and reduced bloat
Chibis
LoafyLemon 1 year ago
parent 1453df5627
commit 6fa1e0a0fa

@ -171,7 +171,7 @@ screen game_menu(title, scroll=None, yinitial=0.0):
label title anchor (0.5, 0.5) align (0.86, 0.15)
if main_menu:
if main_menu and not title == "Updater":
key "game_menu" action ShowMenu("main_menu")
style game_menu_outer_frame is empty:
@ -220,6 +220,7 @@ screen navigation(title=None):
default show_quick_start = False
default show_dev_start = False
default is_sensitive = not bool(title == "Updater")
key "keydown_K_LSHIFT" action SetLocalVariable("show_quick_start", True)
key "keyup_K_LSHIFT" action SetLocalVariable("show_quick_start", False)
@ -243,37 +244,38 @@ screen navigation(title=None):
if main_menu:
if not title:
if (updater.can_update() or config.developer) and getattr(store, "UPDATE_URL", None):
if new_version:
textbutton "Update available" action install_updates() style_prefix "update_available"
if not renpy.mobile:
if UPDATE_VER:
textbutton "Install updates" action InstallUpdates() style_prefix "update_available"
else:
textbutton "Check for updates" action Function(check_updates, 300)
textbutton "Check for updates" action CheckUpdates(300)
if show_quick_start:
textbutton _("Quick Start") action Start("start_quick")
textbutton _("Quick Start") action Start("start_quick") sensitive is_sensitive
elif show_dev_start:
textbutton _("Developer Start") action Start("start_dev")
textbutton _("Developer Start") action Start("start_dev") sensitive is_sensitive
else:
textbutton _("Start") action Start()
textbutton _("Start") action Start() sensitive is_sensitive
else:
textbutton _("Return") action Return()
textbutton _("Return") action Return() sensitive is_sensitive
else:
textbutton _("Return") action Return()
textbutton _("History") action ShowMenu("history")
textbutton _("Save") action ShowMenu("save")
textbutton _("Return") action Return() sensitive is_sensitive
textbutton _("History") action ShowMenu("history") sensitive is_sensitive
textbutton _("Save") action ShowMenu("save") sensitive is_sensitive
textbutton _("Load") action ShowMenu("load")
textbutton _("Preferences") action ShowMenu("preferences")
textbutton _("Load") action ShowMenu("load") sensitive is_sensitive
textbutton _("Preferences") action ShowMenu("preferences") sensitive is_sensitive
if main_menu:
textbutton _("Mods") sensitive bool(mods_list) action ShowMenu("mods")
textbutton _("Credits") action Jump("credits")
textbutton _("Mods") sensitive (bool(mods_list) and is_sensitive) action ShowMenu("mods")
textbutton _("Credits") action Jump("credits") sensitive is_sensitive
if not renpy.mobile:
textbutton _("Quit") action Quit(confirm=not main_menu)
textbutton _("Quit") action Quit(confirm=not main_menu) sensitive is_sensitive
else:
textbutton _("Help") action ShowMenu("help")
textbutton _("Main Menu") action MainMenu()
textbutton _("Help") action ShowMenu("help") sensitive is_sensitive
textbutton _("Quit to menu") action MainMenu() sensitive is_sensitive
style navigation_vbox:
xsize 250

@ -62,6 +62,7 @@ screen preferences_general():
if not renpy.mobile:
textbutton _("Tooltips") action settings.Toggle("tooltip")
textbutton _("System Cursor") action Preference("system cursor", "toggle")
textbutton _("Automatic Updates") action settings.Toggle("updates")
default trans = config.intra_transition

@ -22,6 +22,7 @@ init python:
settings.default("tutorials", True)
settings.default("preserve_aspect_ratio", True)
settings.default("animations", True)
settings.default("updates", True)
# Configuration
# https://www.renpy.org/doc/html/config.html
@ -35,10 +36,10 @@ define config.console = True
# Game version and naming
define config.version = "1.43.0"
define compatible_version = 1.43
define config.name = "WT Silver"
define config.name = "Witch Trainer Silver"
# Application window settings
define config.window_title = "Witch Trainer: Silver (v{}) ({}) ({}-bit)".format(config.version, get_renderer(), renpy.bits)
define config.window_title = "{} (v{}) ({}) ({}-bit)".format(config.name, config.version, get_renderer(), renpy.bits)
define config.window_icon = "gui/icon.webp"
define config.screen_width = 1080
define config.screen_height = 600
@ -89,7 +90,7 @@ init -1:
define config.late_images_scan = True
# Saving and loading
define config.save_directory = "WTS"
define config.save_directory = "Witch Trainer Silver"
define config.has_autosave = True
define config.autosave_on_quit = True
define config.autosave_on_choice = True
@ -123,10 +124,10 @@ define config.exit_replay_transition = None
define config.say_attribute_transition = d3
# Garbage Collector
define config.manage_gc = True
define config.gc_thresholds = (25000, 10, 10)
define config.idle_gc_count = 10000
define config.gc_print_unreachable = False
# define config.manage_gc = True
# define config.gc_thresholds = (25000, 10, 10)
# define config.idle_gc_count = 10000
# define config.gc_print_unreachable = False
################################################
## Build configuration ##
@ -140,33 +141,20 @@ init python:
build.include_old_themes = False
build.exclude_empty_directories = True
build.archive("scripts", "all")
build.archive("characters", "all")
build.archive("images", "all")
build.archive("sounds", "all")
build.archive("music", "all")
build.archive("gui", "all")
build.archive("old", "all")
build.classify("game/scripts/**.rpy", "scripts")
build.classify("game/scripts/**.rpyc", "scripts")
build.classify("old-game/scripts/**.rpy", "old")
build.classify("old-game/scripts/**.rpyc", "old")
build.classify("game/images.whitespace", "scripts")
build.classify("game/characters/**.webp", "characters")
build.classify("game/images/**.webp", "images")
build.classify("game/images/**.webm", "images")
build.classify("game/sounds/**.ogg", "sounds")
build.classify("game/music/**.ogg", "music")
build.classify("game/gui/**", "gui")
build.classify("game/interface/**.webp", "gui")
build.classify("android-icon_*.png", "android gui")
build.classify("android-presplash.jpg", "android gui")
build.classify("icon.icns", "mac gui")
build.classify("icon.ico", "windows gui")
build.classify("game/presplash_*.png", "mac windows linux gui")
build.classify("game/outfits/**", "mac windows linux")
build.classify("**.rpy", "renpy")
build.classify("**.rpyc", "all")
build.classify("game/images.whitespace", "all")
build.classify("**.webp", "all")
build.classify("**.webm", "all")
build.classify("**.ogg", "all")
build.classify("game/gui/**", "all")
build.classify("android-icon_*.png", "android")
build.classify("android-presplash.jpg", "android")
build.classify("icon.icns", "mac")
build.classify("icon.ico", "windows")
build.classify("game/presplash_*.png", "renpy")
build.classify("game/outfits/**", "all")
build.classify("**.py", None)
build.classify("**.txt", None)

@ -1,54 +0,0 @@
# This file can be safely deleted to permanently disable update checks.
init python:
import requests
import binascii
UPDATE_URL = ""
LOGO_URL = ""
new_version = None
def check_updates(interval=3600*6):
if not UPDATE_URL:
return
global new_version
new_version = updater.UpdateVersion(binascii.unhexlify(UPDATE_URL), check_interval=interval)
def install_updates():
if not UPDATE_URL:
return None
return updater.Update(binascii.unhexlify(UPDATE_URL), patch=True)
def fetch_update_logo(url):
if not (updater.can_update() or config.developer) or new_version is None or not LOGO_URL:
return Null()
filename = "logo_{}.png".format(new_version)
path = os.path.join(config.basedir, "updates/{}".format(filename))
# Read file if exists
if os.path.isfile(path):
with open(path, "rb") as f:
data = f.read()
return im.Data(data, path)
# Fetch file if doesn't exist
try:
url = binascii.unhexlify(url)
response = requests.get(url, timeout=5)
data = response.content
except:
return Null()
if not data:
return Null()
with open(path, "wb") as f:
f.write(data)
return im.Data(data, path)
check_updates()

@ -1,3 +1,122 @@
init python:
import binascii
import requests
UPDATE_URL = binascii.unhexlify("687474703a2f2f3135332e39322e3232312e3234362f757064617465732e6a736f6e")
UPDATE_VER = ""
@renpy.pure
class CheckUpdates(Action):
def __init__(self, interval=3600*6, simulate=None, onetime=False, autostart=True, **kwargs):
self.url = UPDATE_URL
self.interval = interval
self.simulate = simulate
self.onetime = onetime
self.autostart = autostart
self.kwargs = kwargs
def __call__(self):
# Since source files are publicly available,
# we need to forbid updater from affecting
# GIT-supplied versions of the game to avoid bugs.
if (not updater.can_update() or config.developer) and not self.simulate:
return
check = True
url = self.url
if self.onetime and url in updater.checked:
check = False
if time.time() < persistent._update_last_checked.get(url, 0) + self.interval:
check = False
if check:
updater.checked.add(url)
persistent._update_last_checked[url] = time.time()
updater.Updater(url, check_only=True, simulate=self.simulate, **self.kwargs)
global UPDATE_VER
UPDATE_VER = persistent._update_version.get(url, "")
if self.autostart:
renpy.invoke_in_new_context(updater.update, self.url, simulate=self.simulate, **self.kwargs)
@renpy.pure
class InstallUpdates(Action):
def __init__(self, simulate=None, **kwargs):
self.url = UPDATE_URL
self.simulate = simulate
self.kwargs = kwargs
def __call__(self):
renpy.invoke_in_new_context(updater.update, self.url, simulate=self.simulate, **self.kwargs)
def version_float():
control, major, minor = config.version.split(" ")[0].split(".")
return float("{}.{}{}".format(control, major, minor))
def version_patch():
if renpy.is_init_phase():
# Don't update save files from when game recovers from a crash.
return
latest = version_float()
if not hasattr(renpy.store, "version"):
if hasattr(renpy.store, "save_internal_version"):
raise Exception("Loaded save file is incompatible. (Save Version: {}, Game Version: {})".format(getattr(renpy.store, "save_internal_version")), latest)
raise Exception("Loaded save file is incompatible. (Save Version: Unknown, Game Version: {})".format(latest))
current = version
if current > latest:
raise Exception("Loaded save file is incompatible. (Save Version: {}, Game Version: {})".format(current, latest))
if current < latest:
setattr(renpy.store, "version", latest)
message = "Have fun!"
achievements.attempt_repair()
renpy.call_in_new_context("modal_popup", "Update Successful", "\nYour save file has been successfully updated to version {{b}}{}{{/b}}.\n\n{}".format(config.version, message), None, "Hurray!")
renpy.block_rollback()
return
def version_logo():
url = UPDATE_URL
filename = "logo_{}.webp".format(UPDATE_VER)
path = os.path.join(config.basedir, "update/{}".format(filename))
# Read file if exists
if os.path.isfile(path):
with open(path, "rb") as f:
data = f.read()
return im.Data(data, path)
# Fetch file if doesn't exist
url = url.split("updates.json")[0] + "logo.webp"
try:
response = requests.get(url, timeout=5)
response.raise_for_status()
data = response.content
with open(path, "wb") as f:
f.write(data)
return im.Data(data, path)
except:
path = os.path.join(config.basedir, "update/generic.webp")
with open(path, "rb") as f:
data = f.read()
return Fixed(im.Data(data, path), Text(UPDATE_VER, size=96, align=(0.5, 0.8), color="#000000", outlines=[( 1, "#ffffff", 0, 0 )]), fit_first=True)
config.after_load_callbacks.append(version_patch)
define update_message_list = [
"Spinning up disks",
"Lubricating Hermione",
@ -72,7 +191,7 @@ screen updater:
tag menu
default msg = renpy.random.choice(update_message_list)
default logo = fetch_update_logo(LOGO_URL) # http://update.silverstudiogames.org/logo.webp
default logo = version_logo()
use game_menu(_("Updater"), scroll="viewport"):
@ -138,8 +257,6 @@ screen updater:
style updater_text:
xalign 0.5
style updater_button
style updater_button_text is gui_button_text
style updater_label is gui_label:
xsize 209
@ -163,43 +280,8 @@ style update_available_button_text:
hover_color "#fff"
xalign 0.5
init python early:
def version_float():
control, major, minor = config.version.split(" ")[0].split(".")
return float("{}.{}{}".format(control, major, minor))
def version_patch():
if renpy.is_init_phase():
# Don't update save files from when game recovers from a crash.
return
latest = version_float()
if not hasattr(renpy.store, "version"):
if hasattr(renpy.store, "save_internal_version"):
raise Exception("Loaded save file is incompatible. (Save Version: {}, Game Version: {})".format(getattr(renpy.store, "save_internal_version")), latest)
raise Exception("Loaded save file is incompatible. (Save Version: Unknown, Game Version: {})".format(latest))
current = version
if current > latest:
raise Exception("Loaded save file is incompatible. (Save Version: {}, Game Version: {})".format(current, latest))
if current < latest:
setattr(renpy.store, "version", latest)
message = "Have fun!"
achievements.attempt_repair()
renpy.call_in_new_context("modal_popup", "Update Successful", "\nYour save file has been successfully updated to version {{b}}{}{{/b}}.\n\n{}".format(config.version, message), None, "Hurray!")
renpy.block_rollback()
return
init python:
config.after_load_callbacks.append(version_patch)
label before_main_menu():
# Add screen
python:
if settings.get("updates"):
CheckUpdates(onetime=True, autostart=False)()
return

@ -2,3 +2,4 @@
*
# Except this file
!.gitignore
!generic.webp

BIN
update/generic.webp (Stored with Git LFS)

Binary file not shown.
Loading…
Cancel
Save