diff --git a/AO2XP.py b/AO2XP.py index c4c86ea..9b7d2d4 100644 --- a/AO2XP.py +++ b/AO2XP.py @@ -1,9 +1,11 @@ -import sys, thread, time, os, platform, __builtin__ +import subprocess, sys, thread, time, os, platform, __builtin__ from os.path import exists, abspath from PyQt4 import QtGui, QtCore import audio as AUDIO +import ini + __builtin__.audio = AUDIO del AUDIO @@ -62,6 +64,7 @@ class gamewindow(QtGui.QMainWindow): self.settingsgui.showSettings() if not debugmode: + # Vanilla downloader force_downloader = len(sys.argv) > 1 and sys.argv[1] == "download" if force_downloader or (not exists("base/background") and not exists("base/characters") and not exists("base/sounds") and not exists("base/evidence")): jm = QtGui.QMessageBox.information(None, "Warning", "You seem to be missing the included Attorney Online content.\nWould you like to download them automatically?", QtGui.QMessageBox.Yes | QtGui.QMessageBox.No) @@ -72,7 +75,18 @@ if not debugmode: else: os._exit(-3) -import gameview, mainmenu, options, ini + # AO2XP update checker + can_update = ini.read_ini_bool("AO2XP.ini", "General", "install updates", True) + force_update = "forceupdate" in sys.argv[1:] + if can_update or force_update: + import updater + code = updater.checkForUpdates(force_update) + if code == 0: + subprocess.Popen(["./AO2XPupdat"]) + os._exit(0) + + +import gameview, mainmenu, options audio.init() shit = gamewindow() diff --git a/build.bat b/build.bat index d673cc2..8cc67a4 100644 --- a/build.bat +++ b/build.bat @@ -1,3 +1,4 @@ pyinstaller AO2XP.spec pyinstaller AO2XP_console.spec +pyinstaller install_update.spec move .\dist\* . diff --git a/install_update.py b/install_update.py new file mode 100644 index 0000000..c0eb541 --- /dev/null +++ b/install_update.py @@ -0,0 +1,30 @@ +import zipfile, tarfile, platform, os, time, shutil + +ext = { + "Windows": "zip", + "Darwin": "zip", + "Linux": "gz" +} + + +def extractzip(): # Mac + archive = zipfile.ZipFile("update.zip") + if platform.system() == "Darwin": shutil.rmtree("AO2XP.app", ignore_errors=True) # delete the old app package. + archive.extractall() # extract the new version + +def extractgz(): # Linux + archive = tarfile.open("update.tar.gz") + archive.extractall() + + +if os.path.exists("update." + ext[platform.system()]): + print "Waiting 3 seconds for AO2XP to close..." + time.sleep(3) + print "Extracting update." + ext[platform.system()] + "..." + globals()["extract" + ext[platform.system()]]() # call the extract function according to OS + print "Done!" + os.remove("update." + ext[platform.system()]) + +else: + print "This program will be automatically run by AO2XP to apply updates.\nYou do not need to run this yourself." + raw_input("Press enter to exit.\n") diff --git a/install_update.spec b/install_update.spec new file mode 100644 index 0000000..0e2c7b9 --- /dev/null +++ b/install_update.spec @@ -0,0 +1,34 @@ +# -*- mode: python ; coding: utf-8 -*- + +block_cipher = None + + +a = Analysis(['install_update.py'], + pathex=['C:\\Users\\Public\\1.7.5 Cut Content Patch\\Client'], + binaries=[], + datas=[], + hiddenimports=[], + hookspath=[], + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False) +pyz = PYZ(a.pure, a.zipped_data, + cipher=block_cipher) +exe = EXE(pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='AO2XPupdat', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + manifest=None, + console=True ) diff --git a/options.py b/options.py index 4516362..6f1db3f 100644 --- a/options.py +++ b/options.py @@ -1,6 +1,7 @@ from ConfigParser import ConfigParser from os.path import exists -from os import listdir +from os import listdir, _exit +import subprocess from PyQt4 import QtGui, QtCore @@ -46,9 +47,9 @@ class Settings(QtGui.QDialog): cancelbtn.clicked.connect(self.onCancelClicked) separators = [] - for i in range(4): + for i in range(5): separator = QtGui.QFrame() - separator.setFixedSize(separator.size().width(), 16) + separator.setFixedSize(separator.size().width(), 12) separators.append(separator) ###### General tab ###### @@ -92,7 +93,15 @@ class Settings(QtGui.QDialog): self.currtheme.addItem(theme) currtheme_layout.addWidget(currtheme_label) currtheme_layout.addWidget(self.currtheme) - + + update_layout = QtGui.QHBoxLayout() + self.check_updates = QtGui.QCheckBox("Check for AO2XP updates on startup") + self.check_updates_btn = QtGui.QPushButton() + self.check_updates_btn.setText("Check now...") + self.check_updates_btn.clicked.connect(self.onUpdateClicked) + update_layout.addWidget(self.check_updates) + update_layout.addWidget(self.check_updates_btn) + savechangeswarn = QtGui.QLabel() savechangeswarn.setText("* Change takes effect upon restarting the client") @@ -106,6 +115,8 @@ class Settings(QtGui.QDialog): general_layout.addLayout(allowdownload_layout) general_layout.addWidget(separators[2]) general_layout.addLayout(currtheme_layout) + general_layout.addWidget(separators[3]) + general_layout.addLayout(update_layout) general_layout.addWidget(savechangeswarn, 50, QtCore.Qt.AlignBottom) ###### Audio tab ###### @@ -113,7 +124,7 @@ class Settings(QtGui.QDialog): self.device_list = QtGui.QComboBox() audio_layout.setWidget(0, QtGui.QFormLayout.LabelRole, device_label) audio_layout.setWidget(0, QtGui.QFormLayout.FieldRole, self.device_list) - audio_layout.setWidget(1, QtGui.QFormLayout.FieldRole, separators[3]) + audio_layout.setWidget(1, QtGui.QFormLayout.FieldRole, separators[4]) volumelabel = QtGui.QLabel("Sound volume") musiclabel = QtGui.QLabel("Music") @@ -176,6 +187,7 @@ class Settings(QtGui.QDialog): self.allowdownload_music.setChecked(ini.read_ini_bool(self.inifile, "General", "download music")) self.allowdownload_evidence.setChecked(ini.read_ini_bool(self.inifile, "General", "download evidence")) self.currtheme.setCurrentIndex(self.themes.index(ini.read_ini(self.inifile, "General", "theme", "default"))) + self.check_updates.setChecked(ini.read_ini_bool(self.inifile, "General", "install updates", True)) self.device_list.setCurrentIndex(ini.read_ini_int(self.inifile, "Audio", "device", audio.getcurrdevice())) self.musicslider.setValue(ini.read_ini_int(self.inifile, "Audio", "Music volume", 100)) @@ -190,6 +202,7 @@ class Settings(QtGui.QDialog): self.allowdownload_music.setChecked(True) self.allowdownload_evidence.setChecked(True) self.currtheme.setCurrentIndex(self.themes.index("default")) + self.check_updates.setChecked(True) self.device_list.setCurrentIndex(audio.getcurrdevice()) self.musicslider.setValue(100) @@ -223,6 +236,7 @@ class Settings(QtGui.QDialog): self.inifile.set("General", "download music", self.allowdownload_music.isChecked()) self.inifile.set("General", "download evidence", self.allowdownload_evidence.isChecked()) self.inifile.set("General", "theme", self.currtheme.currentText()) + self.inifile.set("General", "install updates", self.check_updates.isChecked()) self.inifile.set("Audio", "device", self.device_list.currentIndex()) self.inifile.set("Audio", "Music volume", self.musicslider.value()) @@ -238,3 +252,10 @@ class Settings(QtGui.QDialog): def onCancelClicked(self): self.hide() + + def onUpdateClicked(self): + import updater + code = updater.checkForUpdates() + if code == 0: + subprocess.Popen(["./AO2XPupdat"]) + _exit(0) \ No newline at end of file diff --git a/updater.py b/updater.py new file mode 100644 index 0000000..b9ecd86 --- /dev/null +++ b/updater.py @@ -0,0 +1,184 @@ +import json, urllib, sys, requests, time, os, platform +from PyQt4 import QtGui, QtCore +from game_version import * + +returncode = -1 +assetfile = { + "Windows": "AO2XP-Windows.zip", + "Darwin": "AO2XP-macOS.zip", + "Linux": "AO2XP-Linux.tar.gz" +} + +def checkForUpdates(force=False): + update_dialog = QtGui.QProgressDialog() + update_dialog.setWindowTitle("AO2XP updater") + update_dialog.setAutoClose(False) + update_dialog.setAutoReset(False) + update_dialog.setLabelText("Checking for client updates...") + update_dialog.resize(512, 96) + update_dialog.setMinimum(0) + update_dialog.setMaximum(0) + update_dialog.setValue(0) + + thr = updateThread(update_dialog, force) + + def setProgressValue(value): + update_dialog.setValue(value) + def setLabelText(msg): + update_dialog.setLabelText(msg) + def showMessageBox(icon, title, msg): + getattr(QtGui.QMessageBox, str(icon))(None, title, msg) + def updateAvailableCall(name, version, body): + updateWindow = updateAvailableWidget(name, version, body) + result = updateWindow.show() + thr.waitConfirm = 1 if result else -1 + if not result: + update_dialog.close() + returncode = -2 + else: + update_dialog.setMinimum(0) + update_dialog.setMaximum(100) + + thr.progressValue.connect(setProgressValue) + thr.labelText.connect(setLabelText) + thr.showMessageBox.connect(showMessageBox) + thr.updateAvailable.connect(updateAvailableCall) + thr.finished.connect(update_dialog.close) + thr.start() + + update_dialog.exec_() + if not update_dialog.wasCanceled() and thr.isRunning(): thr.wait() + + print "update return code:", returncode + return returncode + +class updateAvailableWidget(QtGui.QDialog): + def __init__(self, title, version, body): + super(self.__class__, self).__init__() + self.layout = QtGui.QVBoxLayout(self) + self.layout.setAlignment(QtCore.Qt.AlignTop) + + self.available = QtGui.QLabel(text="Changelog for update \"%s\":" % title) + self.changelog = QtGui.QTextEdit() + self.confirmtext = QtGui.QLabel(text="Do you want to download and install the update?") + self.yesnolayout = QtGui.QHBoxLayout() + self.yes = QtGui.QPushButton(text="Yes") + self.no = QtGui.QPushButton(text="No") + + self.setModal(True) + self.setWindowTitle("Update %s available" % version) + self.changelog.setText(body) + self.changelog.setReadOnly(True) + self.yes.clicked.connect(self.confirm) + self.no.clicked.connect(self.cancel) + + self.yesnolayout.addWidget(self.yes) + self.yesnolayout.addWidget(self.no) + self.layout.addWidget(self.available) + self.layout.addWidget(self.changelog) + self.layout.addWidget(self.confirmtext) + self.layout.addLayout(self.yesnolayout, QtCore.Qt.AlignBottom) + self.result = False + + self.resize(640, self.sizeHint().height()) + + def confirm(self): + self.close() + self.result = True + + def cancel(self): + self.close() + self.result = False + + def show(self): + super(self.__class__, self).show() + self.exec_() + return self.result + +class updateThread(QtCore.QThread): + progressValue = QtCore.pyqtSignal(int) + labelText = QtCore.pyqtSignal(str) + showMessageBox = QtCore.pyqtSignal(str, str, str) + updateAvailable = QtCore.pyqtSignal(str, str, str) + finished = QtCore.pyqtSignal() + waitConfirm = 0 + + def __init__(self, nouis, force): + super(self.__class__, self).__init__() + self.jm = nouis + self.force = force + + def run(self): + global returncode + + try: + manifest = json.load(urllib.urlopen("http://api.github.com/repos/headshot2017/AO2XP/releases")) + except: + self.showMessageBox.emit("critical", "Error", "Failed to check for updates.\nPlease check your internet connection.") + self.finished.emit() + return + + if self.jm.wasCanceled(): + return + + if GAME_VERSION != manifest[0]["tag_name"] or self.force: # update available + self.updateAvailable.emit(manifest[0]["name"], manifest[0]["tag_name"], manifest[0]["body"]) + self.waitConfirm = 0 + + while self.waitConfirm == 0: pass + + if self.waitConfirm > 0: + resume_bytes = 0 + filename = assetfile[platform.system()] + link = "" + for asset in manifest[0]["assets"]: + if asset["name"] == filename: + link = asset["browser_download_url"] + + if not link: + self.showMessageBox.emit("critical", "Error", "Release %s is missing the '%s' file.\nCannot continue updating." % (manifest[0]["tag_name"], filename)) + self.finished.emit() + return + + updatezip = "update" + os.path.splitext(filename)[1] + if not os.path.exists(updatezip): + downloadfile = open(updatezip, "wb") + else: + existing_data = open(updatezip, "rb").read() + downloadfile = open(updatezip, "ab") + resume_bytes = len(existing_data) + print resume_bytes + del existing_data + + self.labelText.emit("Downloading '%s' for update '%s'..." % (filename, manifest[0]["name"])) + dl = resume_bytes + speed = 0.0 + start = time.clock() + calcspeed_time = time.time() + zip = requests.get(link, stream=True, headers={"Range": "bytes=%d-" % resume_bytes}) + length = resume_bytes + int(zip.headers.get("content-length")) + + for noby in zip.iter_content(chunk_size=4096): + if not self.jm.isVisible(): + downloadfile.close() + return + + downloadfile.write(noby) + dl += len(noby) + percent = 100 * dl / length + if percent != self.jm.value(): + self.progressValue.emit(percent) + self.labelText.emit("Downloading '%s' for update '%s'... %.1f KB/s" % (filename, manifest[0]["name"], speed)) + + if (time.time() - calcspeed_time) >= 0.5: + calcspeed_time = time.time() + speed = ((dl-resume_bytes)/(time.clock() - start)) / 1024. + self.labelText.emit("Downloading '%s' for update '%s'... %.1f KB/s" % (filename, manifest[0]["name"], speed)) + + print "downloaded update" + downloadfile.close() + returncode = 0 + else: + returncode = -2 + + self.finished.emit()