#!/usr/bin/env python # # Simple manager prototype for xqemu # from PyQt5.QtWidgets import QApplication, QDialog, QFileDialog, QMainWindow, QMessageBox from PyQt5.uic import loadUiType from PyQt5 import QtCore, QtGui from qmp import QEMUMonitorProtocol import sys import os, os.path import json import time import platform SETTINGS_FILE = './settings.json' # Load UI files settings_class, _ = loadUiType('settings.ui') mainwindow_class, _ = loadUiType('mainwindow.ui') class SettingsManager(object): def __init__(self): self.reset() def reset(self): self.settings = { 'xqemu_path': '/path/to/xqemu', 'mcpx_path': '/path/to/mcpx.bin', 'flash_path': '/path/to/flash.bin', 'hdd_path': '/path/to/hdd.img', 'hdd_locked': True, 'dvd_present': True, 'dvd_path': '/path/to/disc.iso', 'short_anim': False, 'sys_memory': '64 MiB', 'use_accelerator': False, 'gdb_enabled': False, 'gdb_wait': False, 'gdb_port': '1234', 'controller_one': 'Not connected', 'controller_two': 'Not connected', 'controller_three': 'Not connected', 'controller_four': 'Not connected', 'xmu_1a_path': '', 'xmu_1b_path': '', 'xmu_2a_path': '', 'xmu_2b_path': '', 'xmu_3a_path': '', 'xmu_3b_path': '', 'xmu_4a_path': '', 'xmu_4b_path': '', 'extra_args': '', } def save(self): with open(SETTINGS_FILE, 'w') as f: f.write(json.dumps(self.settings, indent=2)) def load(self): if os.path.exists(SETTINGS_FILE): with open(SETTINGS_FILE, 'r') as f: d = f.read() self.settings = json.loads(d) else: self.reset() class SettingsWindow(QDialog, settings_class): def __init__(self, settings, *args): super(SettingsWindow, self).__init__(*args) self.settings = settings self.setupUi(self) # Little helper functions to hook up the gui to the model def setTextAttr(widget, var): self.settings.settings[var] = widget.text() def getTextAttr(widget, var): widget.setText(self.settings.settings[var]) def setCheckAttr(widget, var): self.settings.settings[var] = widget.isChecked() def getCheckAttr(widget, var): widget.setChecked(self.settings.settings[var]) def setDropdownAttr(widget, var): self.settings.settings[var] = widget.currentText() def getDropdownAttr(widget, var): widget.setCurrentText(self.settings.settings[var]) def updateLaunchCmd(): self.invocationPreview.setPlainText(Xqemu.launchCmdToString(Xqemu.generateLaunchCmd(self.settings, True))) def updateControllerUi(): # Update all four controllers for i in [1, 2, 3, 4]: controller = getattr(self, 'controller' + str(i)) is_connected = (controller.currentIndex() != 0) group = getattr(self, 'controller' + str(i) + 'Additional') group.setEnabled(is_connected) def bindTextWidget(widget, var): getTextAttr(widget, var) widget.textChanged.connect(lambda:setTextAttr(widget, var)) widget.textChanged.connect(updateLaunchCmd) def bindCheckWidget(widget, var): getCheckAttr(widget, var) widget.stateChanged.connect(lambda:setCheckAttr(widget, var)) widget.stateChanged.connect(updateLaunchCmd) def bindFilePicker(button, text): button.clicked.connect(lambda:self.setSaveFileName(text)) def bindDropdownWidget(widget, var): getDropdownAttr(widget, var) widget.currentIndexChanged.connect(lambda:setDropdownAttr(widget, var)) widget.currentIndexChanged.connect(updateLaunchCmd) bindTextWidget(self.xqemuPath, 'xqemu_path') bindFilePicker(self.setXqemuPath, self.xqemuPath) bindCheckWidget(self.useShortBootAnim, 'short_anim') bindCheckWidget(self.dvdPresent, 'dvd_present') bindTextWidget(self.dvdPath, 'dvd_path') bindFilePicker(self.setDvdPath, self.dvdPath) bindTextWidget(self.mcpxPath, 'mcpx_path') bindFilePicker(self.setMcpxPath, self.mcpxPath) bindTextWidget(self.flashPath, 'flash_path') bindFilePicker(self.setFlashPath, self.flashPath) bindTextWidget(self.hddPath, 'hdd_path') bindFilePicker(self.setHddPath, self.hddPath) bindCheckWidget(self.hddLocked, 'hdd_locked') bindDropdownWidget(self.systemMemory, 'sys_memory') bindCheckWidget(self.useAccelerator, 'use_accelerator') bindDropdownWidget(self.controller1, 'controller_one') bindDropdownWidget(self.controller2, 'controller_two') bindDropdownWidget(self.controller3, 'controller_three') bindDropdownWidget(self.controller4, 'controller_four') bindFilePicker(self.setXmu1A, self.xmu1APath) bindTextWidget(self.xmu1APath, 'xmu_1a_path') bindFilePicker(self.setXmu1B, self.xmu1BPath) bindTextWidget(self.xmu1BPath, 'xmu_1b_path') bindFilePicker(self.setXmu2A, self.xmu2APath) bindTextWidget(self.xmu2APath, 'xmu_2a_path') bindFilePicker(self.setXmu2B, self.xmu2BPath) bindTextWidget(self.xmu2BPath, 'xmu_2b_path') bindFilePicker(self.setXmu3A, self.xmu3APath) bindTextWidget(self.xmu3APath, 'xmu_3a_path') bindFilePicker(self.setXmu3B, self.xmu3BPath) bindTextWidget(self.xmu3BPath, 'xmu_3b_path') bindFilePicker(self.setXmu4A, self.xmu4APath) bindTextWidget(self.xmu4APath, 'xmu_4a_path') bindFilePicker(self.setXmu4B, self.xmu4BPath) bindTextWidget(self.xmu4BPath, 'xmu_4b_path') bindCheckWidget(self.gdbEnabled, 'gdb_enabled') bindCheckWidget(self.waitForGdb, 'gdb_wait') bindTextWidget(self.gdbPort, 'gdb_port') bindTextWidget(self.additionalArgs, 'extra_args') # Controller UI has additional logic self.controller1.currentIndexChanged.connect(updateControllerUi) self.controller2.currentIndexChanged.connect(updateControllerUi) self.controller3.currentIndexChanged.connect(updateControllerUi) self.controller4.currentIndexChanged.connect(updateControllerUi) # Prepare initial UI state updateLaunchCmd() updateControllerUi() def setSaveFileName(self, obj): options = QFileDialog.Options() fileName, _ = QFileDialog.getOpenFileName(self, "Select File", obj.text(), "All Files (*)", options=options) if fileName: obj.setText(fileName) class Xqemu(object): def __init__(self): self._p = QtCore.QProcess() self._qmp = None @staticmethod def generateControllerArg(settings): def genArg(settings, name, port): arg = {'Not connected': '', 'Keyboard': 'usb-xbox-gamepad', 'Gamepad #0': 'usb-xbox-gamepad-sdl,index=0', 'Gamepad #1': 'usb-xbox-gamepad-sdl,index=1', 'Gamepad #2': 'usb-xbox-gamepad-sdl,index=2', 'Gamepad #3': 'usb-xbox-gamepad-sdl,index=3'}.get(settings.settings[name], '') if arg != '': return ['-device', 'usb-hub,port=' + str(port), '-device'] + [arg + ',port=' + str(port) + ".1"] return [] args = [] for controller in zip([3, 4, 1, 2], ['controller_one', 'controller_two', 'controller_three', 'controller_four']): args += genArg(settings, controller[1], controller[0]) return args @staticmethod def generateXmuArg(settings, skipPathChecks): def check_path(path): if not skipPathChecks: if not os.path.exists(path) or os.path.isdir(path): raise Exception('File %s could not be found!' % path) def escape_path(path): return path.replace(',', ',,') def genArg(settings, name, port): port_arr = ['controller_three', 'controller_four', 'controller_one', 'controller_two'] if settings.settings[name] != '' and settings.settings[port_arr[int(port[:1]) - 1]] != 'Not connected': check_path(settings.settings[name]) return ['-drive', 'if=none,id=' + name + ',file=' + escape_path(settings.settings[name]), '-device', 'usb-storage,drive=' + name + ',port=' + port] return [] args = [] for xmu in zip([1, 2, 3, 4], [3, 4, 1, 2]): args += genArg(settings, 'xmu_' + str(xmu[0]) + 'a_path', str(xmu[1]) + '.2') args += genArg(settings, 'xmu_' + str(xmu[0]) + 'b_path', str(xmu[1]) + '.3') return args @staticmethod def generateLaunchCmd(settings, skipPathChecks=False): def check_path(path): if not skipPathChecks: if not os.path.exists(path) or os.path.isdir(path): raise Exception('File %s could not be found!' % path) def escape_path(path): return path.replace(',', ',,') xqemu_path = settings.settings['xqemu_path'] check_path(xqemu_path) mcpx_path = settings.settings['mcpx_path'] check_path(mcpx_path) mcpx_path_arg = escape_path(mcpx_path) flash_path = settings.settings['flash_path'] check_path(flash_path) flash_path_arg = escape_path(flash_path) hdd_path = settings.settings['hdd_path'] check_path(hdd_path) hdd_path_arg = escape_path(hdd_path) short_anim_arg = ',short_animation' if settings.settings['short_anim'] else '' hdd_lock_arg = ',locked' if settings.settings['hdd_locked'] else '' sys_memory = settings.settings['sys_memory'].split(' ')[0]+'M' accel_arg = ',accel=kvm:hax:whpx,kernel_irqchip=off' if settings.settings['use_accelerator'] else '' dvd_path_arg = '' if settings.settings['dvd_present']: check_path(settings.settings['dvd_path']) dvd_path_arg = ',file=' + escape_path(settings.settings['dvd_path']) extra_args = [x for x in settings.settings['extra_args'].split(' ') if x != ''] # Build qemu launch cmd cmd = [xqemu_path, '-cpu','pentium3', '-machine','xbox%(accel_arg)s,bootrom=%(mcpx_path_arg)s%(short_anim_arg)s' % locals(), '-m', '%(sys_memory)s' % locals(), '-bios', '%(flash_path_arg)s' % locals(), '-drive','file=%(hdd_path_arg)s,index=0,media=disk%(hdd_lock_arg)s' % locals(), '-drive','index=1,media=cdrom%(dvd_path_arg)s' % locals(), '-qmp','tcp:localhost:4444,server,nowait', '-display','sdl'] cmd += Xqemu.generateControllerArg(settings) cmd += Xqemu.generateXmuArg(settings, skipPathChecks) if settings.settings['gdb_enabled']: cmd.append('-gdb') cmd.append('tcp::' + settings.settings['gdb_port']) if settings.settings['gdb_wait']: cmd.append('-S') cmd += extra_args return cmd @staticmethod def launchCmdToString(cmd): # Attempt to interpret the constructed command line cmd_escaped = [] for cmd_part in cmd: if ' ' in cmd_part: cmd_escaped += ['"%s"' % cmd_part.replace('"', '\\"')] else: cmd_escaped += [cmd_part] return ' '.join(cmd_escaped) def start(self, settings): cmd = self.generateLaunchCmd(settings) cmd_string = self.launchCmdToString(cmd) print('Running: %s' % cmd_string) self._p.start(cmd_string) i = 0 while True: print('Trying to connect %d' % i) if i > 0: time.sleep(1) try: self._qmp = QEMUMonitorProtocol(('localhost', 4444)) self._qmp.connect() except Exception as e: # If XQEMU quits, we abort the loop if self._p.waitForFinished(1): return if i > 4: raise else: i += 1 continue break def stop(self): if self.isRunning: self._p.terminate() def run_cmd(self, cmd): if type(cmd) is str: cmd = { "execute": cmd, "arguments": {} } resp = self._qmp.cmd_obj(cmd) if resp is None: raise Exception('Disconnected!') return resp def pause(self): return self.run_cmd('stop') def cont(self): return self.run_cmd('cont') def restart(self): return self.run_cmd('system_reset') def screenshot(self): cmd = { "execute": "screendump", "arguments": { "filename": "screenshot.ppm" } } return self.run_cmd(cmd) def isPaused(self): resp = self.run_cmd('query-status') return resp['return']['status'] == 'paused' @property def isRunning(self): return self._p is not None and self._p.state() == QtCore.QProcess.Running class MainWindow(QMainWindow, mainwindow_class): def __init__(self, *args): super(MainWindow, self).__init__(*args) self.setupUi(self) self.inst = Xqemu() self.settings = SettingsManager() self.settings.load() self.runButton.setText('Start') self.pauseButton.setText('Pause') self.pauseButton.setEnabled(False) self.screenshotButton.setText('Screenshot') # Connect signals self.runButton.clicked.connect(self.onRunButtonClicked) self.pauseButton.clicked.connect(self.onPauseButtonClicked) self.screenshotButton.clicked.connect(self.onScreenshotButtonClicked) self.restartButton.clicked.connect(self.onRestartButtonClicked) self.actionExit.triggered.connect(self.onExitClicked) self.actionSettings.triggered.connect(self.onSettingsClicked) self.inst._p.readyReadStandardOutput.connect(self.onReadyReadStandardOutput) self.inst._p.readyReadStandardError.connect(self.onReadyReadStandardError) self.inst._p.stateChanged.connect(self.onXqemuStateChanged) def onReadyReadStandardOutput(self): text = self.inst._p.readAllStandardOutput().data().decode() self.log.moveCursor(QtGui.QTextCursor.End) self.log.insertPlainText(text) self.log.moveCursor(QtGui.QTextCursor.End) def onReadyReadStandardError(self): text = self.inst._p.readAllStandardError().data().decode() self.log.moveCursor(QtGui.QTextCursor.End) self.log.insertPlainText(text) self.log.moveCursor(QtGui.QTextCursor.End) def onXqemuStateChanged(self): if self.inst.isRunning: self.runButton.setText('Stop') self.pauseButton.setEnabled(True) else: self.runButton.setText('Start') self.pauseButton.setEnabled(False) self.pauseButton.setText('Pause') if self.inst._p.exitCode() != 0: QMessageBox.critical(self, 'XQEMU quit prematurely!', 'XQEMU quit prematurely.\n\n' 'This may be a known issue with this specific game, a problem in XQEMU, or an error in your settings.\n' 'Please check your settings for correctness and see the log area for details.\n' 'If you need help resolving this problem, make sure to include this log in your error report, ' 'along with as many details about how and what you were doing when the error happened.') def onRunButtonClicked(self): if not self.inst.isRunning: # No active instance try: self.log.clear() self.inst.start(self.settings) except Exception as e: QMessageBox.critical(self, 'Error!', str(e)) else: # Instance exists self.inst.stop() def onPauseButtonClicked(self): if not self.inst.isRunning: return # We should probably actually pull from event queue to reflect state # here instead of querying during the button press if self.inst.isPaused(): self.inst.cont() self.pauseButton.setText('Pause') else: self.inst.pause() self.pauseButton.setText('Continue') def onScreenshotButtonClicked(self): if not self.inst.isRunning: return self.inst.screenshot() def onRestartButtonClicked(self): if not self.inst.isRunning: return self.inst.restart() def onSettingsClicked(self): s = SettingsWindow(self.settings) s.exec_() self.settings.save() def onExitClicked(self): self.inst.stop() sys.exit(0) def main(): app = QApplication(sys.argv) app.setStyle('Fusion') # Dark theme via https://gist.github.com/gph03n1x/7281135 with modifications palette = QtGui.QPalette() palette.setColor(QtGui.QPalette.Window, QtGui.QColor(53,53,53)) palette.setColor(QtGui.QPalette.WindowText, QtCore.Qt.white) palette.setColor(QtGui.QPalette.Base, QtGui.QColor(15,15,15)) palette.setColor(QtGui.QPalette.AlternateBase, QtGui.QColor(53,53,53)) palette.setColor(QtGui.QPalette.ToolTipBase, QtCore.Qt.white) palette.setColor(QtGui.QPalette.ToolTipText, QtCore.Qt.white) palette.setColor(QtGui.QPalette.Text, QtCore.Qt.white) palette.setColor(QtGui.QPalette.Button, QtGui.QColor(53,53,53)) palette.setColor(QtGui.QPalette.ButtonText, QtCore.Qt.white) palette.setColor(QtGui.QPalette.BrightText, QtCore.Qt.red) palette.setColor(QtGui.QPalette.Highlight, QtGui.QColor(45,197,45).lighter()) palette.setColor(QtGui.QPalette.HighlightedText, QtCore.Qt.black) palette.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Text, QtCore.Qt.darkGray) palette.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.ButtonText, QtCore.Qt.darkGray) palette.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, QtCore.Qt.darkGray) app.setPalette(palette) widget = MainWindow() widget.show() sys.exit(app.exec_()) if __name__ == '__main__': main()