325 lines
8.2 KiB
Python
Executable file
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'
|