scripts/bin/gamelaunch.py

325 lines
8.2 KiB
Python
Executable file

#!/usr/bin/env python3
'''
A custom launcher for games that can't be run because of their own launchers
Just set the launch option to: /path/to/gamelaunch.py %command%
'''
import json, logging, signal, subprocess, sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5 import uic
from datetime import datetime
from pathlib import Path
from urllib.request import urlretrieve
if len(sys.argv) < 2:
print('Missing arguments')
sys.exit()
elif 'debug' in sys.argv:
args = ['/home/zoey/.steam/steam/steamapps/common/Proton 5.0/proton', 'waitforexitandrun', '/home/zoey/.steam/steam/steamapps/common/BorderlandsGOTYEnhanced/Binaries/Win64/Launcher.exe']
elif 'generate' in sys.argv:
print('Steam launch options:', Path(__file__).resolve(), '%command%')
sys.exit()
else:
args = sys.argv[1:]
datapath = Path('~/.config/barkshark/proton-launcher').expanduser()
configfile = datapath.joinpath('config.json')
gamelist = datapath.joinpath('games.json')
localuifile = Path(__file__).resolve().parent.joinpath('gamelaunch.ui')
uifile = datapath.joinpath('gamelaunch.ui')
if not datapath.exists():
datapath.mkdir(parents=True, exist_ok=True)
if localuifile.exists():
uifile = localuifile
elif not uifile.exists():
print('Downloading ui file')
urlretrieve('https://git.barkshark.xyz/izaliamae/scripts/raw/branch/master/gamelaunch.ui', uifile)
config = json.load(configfile.open()) if configfile.exists() else {'auto_start': True, 'auto_close': True}
games = {
'default': {
'id': '0',
'code': 'none',
'name': 'Unknown Game',
'path': '.'
},
'BorderlandsGOTYEnhanced/Binaries/Win64/Launcher.exe': {
'id': '729040',
'code': 'bl1e',
'name': 'Borderlands GOTY Enhanced',
'path': 'BorderlandsGOTYEnhanced/Binaries/Win64/BorderlandsGOTY.exe'
},
'BorderlandsPreSequel/Binaries/Win32/Launcher.exe': {
'id': '261640',
'code': 'bltps',
'name': 'Borderlands: The Pre-Sequel',
'path': 'BorderlandsGOTYEnhanced/Binaries/Win64/BorderlandsPreSequel.exe'
},
'Borderlands 2/Binaries/Win32/Launcher.exe': {
'id': '49520',
'code': 'bl2',
'name': 'Borderlands 2',
'path': 'Borderlands 2/Binaries/Win32/Borderlands2.exe'
},
'Borderlands 3/OakGame/Binaries/Win64/Borderlands3.exe': {
'id': '397540',
'code': 'bl3',
'name': 'Borderlands 3',
'path': 'Borderlands 3/OakGame/Binaries/Win64/Borderlands3.exe'
}
}
class Launcher(QMainWindow):
def __init__(self, app):
super().__init__()
uic.loadUi(str(uifile), self)
self.app = app
self.common = None
self.game = self._get_game()
self.config = config.get(self.game['id'], {})
self.opts = {}
self.proc = QProcess(self)
self.env = {
'DXVK_HUD': 'fps,api,memory'
}
self.command = ['/usr/bin/env'] + [f'{k}={v}' for k,v in self.env.items()] + args[:-1]
self.command.append(str(self.common.joinpath(self.game['path'])))
self.game_bl1 = self.game_borderlands
self.game_bl1e = self.game_borderlands
self.game_bltps = self.game_borderlands
self.game_bl2 = self.game_borderlands
self.game_bl3 = self.game_borderlands
setupGame = getattr(self, f'game_{self.game["code"]}', None)
if setupGame:
setupGame()
for opt in self.opts.values():
self.options.addWidget(opt)
self._print_command()
self._setup_buttons()
self._center()
self.setWindowTitle(f'Game Launcher: {self.game["name"]} [{self.game["id"]}]')
def log(self, *args):
scrollbar = self.console.verticalScrollBar()
date = datetime.now()
line = ' '.join(args)
if self.console.blockCount() == 1 and self.console.toPlainText() == '':
'nothing'
else:
self.console.insertPlainText('\n')
self.console.insertPlainText(f'{date.strftime("%H:%M:%S")} {line}')
scrollbar.setValue(scrollbar.maximum())
while self.console.blockCount() > 1000:
lineno = self.console.document().findBlockByLineNumber(1)
cursor = QTextCursor(lineno)
cursor.select(QTextCursor.BlockUnderCursor)
cursor.removeSelectedText()
def game_borderlands(self):
self.opts['-NoStartupMovies'] = QCheckBox()
self.opts['-NoStartupMovies'].setText('No Startup Movies')
if config.get('-NoStartupMovies'):
self.opts['-NoStartupMovies'].setCheckState(True)
def save_config(self):
config[self.game['id']] = {
'options': {k: v.checkState() for k,v in self.opts.items()}
}
def _setup_buttons(self):
self.kill.setEnabled(False)
self.autoClose.setChecked(config.get('auto_close', False))
self.autoStart.setChecked(config.get('auto_start', True))
for name, checkbox in self.opts.items():
checkbox.setChecked(self.config.get(name, False))
self._setup_checkbox(checkbox, name)
self.run.clicked.connect(self._run)
self.kill.clicked.connect(self._kill)
self.save.clicked.connect(self._save)
self.autoClose.stateChanged.connect(lambda state: self._checkbox_clicked('close', state))
self.autoStart.stateChanged.connect(lambda state: self._checkbox_clicked('start', state))
self.proc.started.connect(self._started)
self.proc.finished.connect(self._finished)
self.proc.readyReadStandardOutput.connect(self._console_log)
self.proc.readyReadStandardError.connect(self._console_log)
def _setup_checkbox(self, widget, name):
widget.stateChanged.connect(lambda state: self._checkbox_clicked(name, state))
def _get_game(self):
for arg in args:
if arg.endswith('.exe') and 'common' in arg:
steamapps, game_bin = arg.split('/common/', 1)
self.common = Path(steamapps).joinpath('common')
game = games.get(game_bin, games['default'])
if game['name'] != 'default':
return game
def _center(self):
geo = self.frameGeometry()
center_rect = QDesktopWidget().availableGeometry().center()
geo.moveCenter(center_rect)
self.move(geo.topLeft())
def _print_command(self):
self.commandLine.setText(' '.join(self.command))
def _console_log(self):
output = self.proc.readAllStandardOutput().data().decode().split('\n')
error = self.proc.readAllStandardError().data().decode().split('\n')
for line in [*output, *error]:
if line != '' and 'gameoverlayrenderer.so' not in line:
self.log(line)
def _started(self):
self.console.clear()
self.log('Starting')
self.run.setEnabled(False)
self.kill.setEnabled(True)
def _finished(self):
if self.autoClose.checkState():
self.app.quit()
self.log('Program exited')
self.run.setEnabled(True)
self.kill.setEnabled(False)
def _kill(self):
self.proc.terminate()
self.proc.waitForFinished(5000)
if self.proc.processId():
self.proc.kill()
def _checkbox_clicked(self, name, state):
state = True if state else False
if name == 'close':
config['auto_close'] = state
if name == 'start':
config['auto_start'] = state
else:
gameid = str(self.game['id'])
if not config.get(gameid):
config[gameid] = {}
config[gameid] = {name: state}
def _timer_count(self):
if self.proc.state() != 0:
self.count = 0
self.timer.timeout.disconnect()
return
self.count -= 1
self.log(f'Starting in {self.count}s...')
if self.count == 0:
self.timer.timeout.disconnect()
self._run()
def _save(self):
json.dump(config, configfile.open('w'), indent=4)
def _run(self, *args):
command = self.command.copy()
for k,v in self.opts.items():
if v.checkState():
command.append(k)
self.proc.start(command[0], command[1:])
try:
self.timer.timeout.disconnect()
except:
'nothing'
def main():
## Prevent errors from bringing down the whole thing
sys._excepthook = sys.excepthook
sys.excepthook = lambda *args: sys._excepthook(*args)
app = QApplication([])
app.setApplicationName('Proton Game Launcher')
app.setOrganizationName('Barkshark')
app.setOrganizationDomain('barkshark.xyz')
#app.setWindowIcon(Icon('icon'))
window = Launcher(app)
app.aboutToQuit.connect(window.close)
## Run SaveWinState every second. It doesn't do anything if the state hasn't changed
## This was originally here to allow python to handle signals, so why not take advantage of it?
window.count = 5
window.timer = QTimer()
window.timer.start(1000)
if config.get('auto_start', True):
window.log('Starting in 5s...')
window.timer.timeout.connect(window._timer_count)
signal.signal(signal.SIGTERM, lambda *x: app.quit())
signal.signal(signal.SIGINT, lambda *x: app.quit())
window.show()
sys.exit(app.exec())
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
'heck'