def test(): import sys from PySide6.QtWidgets import QApplication app = QApplication(sys.argv) win = QMainWindow() MainWindowUI().setup_ui(win) win.show() sys.exit(app.exec_())
def test(): import sys from tester.AutoLab.utils.qthelpers import create_qt_app from tester.config.manager import get_settings app = create_qt_app() win = QMainWindow() settings = get_settings() MainWindowUI().setup_ui(win, settings) win.show() sys.exit(app.exec_())
def QVTKRenderWidgetConeExample(): """A simple example that uses the QVTKRenderWindowInteractor class.""" from vtkmodules.vtkFiltersSources import vtkConeSource from vtkmodules.vtkRenderingCore import vtkActor, vtkPolyDataMapper, vtkRenderer # load implementations for rendering and interaction factory classes import vtkmodules.vtkRenderingOpenGL2 import vtkmodules.vtkInteractionStyle # every QT app needs an app app = QApplication(['QVTKRenderWindowInteractor']) window = QMainWindow() # create the widget widget = QVTKRenderWindowInteractor(window) window.setCentralWidget(widget) # if you don't want the 'q' key to exit comment this. widget.AddObserver("ExitEvent", lambda o, e, a=app: a.quit()) ren = vtkRenderer() widget.GetRenderWindow().AddRenderer(ren) cone = vtkConeSource() cone.SetResolution(8) coneMapper = vtkPolyDataMapper() coneMapper.SetInputConnection(cone.GetOutputPort()) coneActor = vtkActor() coneActor.SetMapper(coneMapper) ren.AddActor(coneActor) # show the widget window.show() widget.Initialize() widget.Start() # start event processing # Source: https://doc.qt.io/qtforpython/porting_from2.html # 'exec_' is deprecated and will be removed in the future. # Use 'exec' instead. try: app.exec() except AttributeError: app.exec_()
alignment = self.ui.legendComboBox.itemData(idx) if not alignment: for chart_view in self.charts: chart_view.chart().legend().hide() else: for chart_view in self.charts: alignment_name = Qt.AlignTop if alignment == 32: alignment_name = Qt.AlignTop elif alignment == 64: alignment_name = Qt.AlignBottom elif alignment == 1: alignment_name = Qt.AlignLeft elif alignment == 2: alignment_name = Qt.AlignRight chart_view.chart().legend().setAlignment(alignment_name) chart_view.chart().legend().show() if __name__ == "__main__": app = QApplication(sys.argv) window = QMainWindow() widget = ThemeWidget(None) window.setCentralWidget(widget) available_geometry = app.desktop().availableGeometry(window) size = available_geometry.height() * 0.75 window.setFixedSize(size, size * 0.8) window.show() sys.exit(app.exec_())
import sys from PySide6.QtCore import QFile from PySide6.QtWidgets import QMainWindow, QApplication from PySide6.QtUiTools import QUiLoader from . import LinearStageControlGUI def load_ui(ui_file, parent=None): loader = QUiLoader() file = QFile(ui_file) file.open(QFile.ReadOnly) myWidget = loader.load(file, None) myWidget.show() file.close() myWidget.show() return myWidget if __name__ == "__main__": # init application app = QApplication(sys.argv) widget = QMainWindow() widget.setWindowTitle("Linear Stage Control") widget.resize(309, 202) widget.ui = LinearStageControlGUI(widget) widget.show() # execute qt main loop sys.exit(app.exec_())
class PredictionWindow(QWidget): """Subwindow dedicated to random forest prediction functions.""" # Private Instance Attributes: # - _predict_btn: Button that signals for a prediction to be made. # - _predict_btn: Button that signals for the selected model to be deleted. # - _model: The RandomForest model currently loaded. # - _model_info: QPlainTextEdit widget displaying information about the selected model. # - _target_date: Calendar widget for the user to select a target prediction date. # -_plot_window: Window for displaying historical information with a prediction. _predict_btn: QDialogButtonBox _delete_btn: QDialogButtonBox _model: QComboBox _model_info: QPlainTextEdit _target_date: QCalendarWidget _plot_window: QMainWindow def __init__(self) -> None: super().__init__() main_layout = QGridLayout() button_box = self._create_button_box() main_layout.addWidget(self._create_options_group_box(), 0, 0) main_layout.addWidget(button_box, 1, 0) main_layout.setSizeConstraint(QLayout.SetMinimumSize) self.setLayout(main_layout) self._refresh_model_info() self.setWindowTitle('Predict') self._plot_window = QMainWindow() def show(self) -> None: """Override of QWidget's show() function. Refreshes window and then shows the window. """ self._refresh_lists() return super().show() @property def _selected_model(self) -> Union[RandomForest, None]: """Gets the currently selected model.""" model_name = self._model.currentText() if model_name != '': try: return load_model(model_name) except pickle.UnpicklingError: self._error_event(f'{model_name} is an invalid model.') return None else: return None def _create_button_box(self) -> QDialogButtonBox: """Creates the lower control buttons at the bottom of the window.""" button_box = QDialogButtonBox() self._predict_btn = button_box.addButton('Predict', QDialogButtonBox.ActionRole) self._delete_btn = button_box.addButton('Delete Model', QDialogButtonBox.ActionRole) refresh_btn = button_box.addButton('Refresh &Options', QDialogButtonBox.ActionRole) self._predict_btn.clicked.connect(self._predict) refresh_btn.clicked.connect(self._refresh_lists) self._delete_btn.clicked.connect(self._delete) return button_box def _create_options_group_box(self) -> QGroupBox: """Returns the group of prediction options.""" options_group_box = QGroupBox("Options") options_layout = QGridLayout() left_options = QGridLayout() right_options = QGridLayout() date_label = QLabel("Target Date:") self._target_date = QCalendarWidget() left_options.addWidget(date_label, 0, 0) left_options.addWidget(self._target_date, 1, 0, 1, 3) left_options.setColumnStretch(0, 1) self._model = QComboBox() self._model_info = QPlainTextEdit() self._model_info.setReadOnly(True) self._model.currentTextChanged.connect(self._refresh_model_info) self._refresh_lists() models_label = QLabel("Models:") info_label = QLabel("Model Information:") right_options.addWidget(models_label, 0, 0) right_options.addWidget(self._model, 1, 0) right_options.addWidget(info_label, 2, 0) right_options.addWidget(self._model_info, 3, 0) options_layout.addLayout(left_options, 0, 0) options_layout.addLayout(right_options, 0, 1) options_group_box.setLayout(options_layout) return options_group_box def _delete(self) -> None: """Deletes the currently selected dataset.""" self.setEnabled(False) name = self._model.currentText() warning = f'Are you sure you want to delete {name}?' response = QMessageBox.warning(self, self.tr("Delete Model"), warning, QMessageBox.Yes, QMessageBox.No) if response == QMessageBox.Yes: data_ingest.delete_data(name, file_type='model') self._refresh_lists() self.setEnabled(True) def _refresh_lists(self) -> None: """Refreshes avaliable datasets for training.""" self._model.clear() data_list = data_ingest.get_avaliable_data(search_type='model') self._model.addItems(data_list) def _refresh_model_info(self) -> None: """Refreshes avaliable features for the selected target.""" self._predict_btn.setEnabled(False) self._target_date.setEnabled(False) self._model_info.clear() model_name = self._model.currentText() model = self._selected_model if model is None: self._delete_btn.setEnabled(False) return None self._delete_btn.setEnabled(True) self._display_model_info(model) req_features = model.window.req_features avaliable_sym = data_ingest.get_avaliable_sym() if len(req_features - avaliable_sym) != 0: self._error_event( f'Missing required data for {model_name}: {req_features - avaliable_sym}' ) return None dfs = load_corresponding_dataframes(model) grouped_dataframe = data_ingest.create_grouped_dataframe(dfs) date_offset = pd.DateOffset(days=model.window.target_shift) self._target_date.setMaximumDate(grouped_dataframe.index.max() + date_offset) self._target_date.setMinimumDate(grouped_dataframe.index.min() + date_offset) self._target_date.setEnabled(True) self._predict_btn.setEnabled(True) return None def _display_model_info(self, model: RandomForest) -> None: """Updates model info box to display current model's information.""" self._model_info.appendPlainText( f'Target Feature Name: \n{model.window.target_lbl}') self._model_info.appendPlainText('Window Information:') self._model_info.appendPlainText( f'\t- Window Size: {model.window.window_size}') self._model_info.appendPlainText( f'\t- Target Shift: {model.window.target_shift}') self._model_info.appendPlainText( f'\t- Required Features: {model.window.req_features}') self._model_info.appendPlainText('Forest Information:') self._model_info.appendPlainText( f'\t- Number of Trees: {model.forest.n_trees}') self._model_info.appendPlainText( f'\t- Tree Max Depth: {model.forest.max_depth}') self._model_info.appendPlainText(f'\t- Seed: {model.forest.seed}') def _predict(self) -> None: """Creates a model prediction using the selected target date and model.""" self.setEnabled(False) self._predict_btn.setEnabled(False) model = self._selected_model if model is None: self.setEnabled(True) return None target_date = self._target_date.selectedDate().toPython() try: dfs = load_corresponding_dataframes(model) prediction_input = data_ingest.create_input( model.window.window_size, model.window.target_shift, target_date, dfs) except ce.MissingData: self._error_event( 'Missing required data. Could be that loaded datasets have holes.' ) self.setEnabled(True) return None prediction = model.predict(prediction_input) historical_dfs = load_corresponding_dataframes(model, 'target') if len(historical_dfs) == 0: self._prediction_historical_error(prediction) else: self._plot_prediction(historical_dfs, model, prediction, target_date) self._predict_btn.setEnabled(True) self.setEnabled(True) return None def _plot_prediction(self, historical_dfs: list[pd.DataFrame], model: RandomForest, prediction: ndarray, target_date: datetime.date) -> None: """Opens a window with a plot of the historical target data as well as the prediction the model made.""" hdf = historical_dfs[0] for frame in historical_dfs[1:]: hdf = hdf.combine_first(frame) window_end = target_date - \ pd.DateOffset(days=model.window.target_shift) window_start = window_end - pd.DateOffset(days=30 - 1) hdf = pd.Series( hdf.loc[window_end:window_start][model.window.target_lbl]) hdf_data = hdf.to_list() hdf_dates = hdf.index hdf_dates = [ts.to_pydatetime().timestamp() for ts in hdf_dates] b_axis = pg.DateAxisItem(orientation='bottom') b_axis.setLabel('Date') plot = PlotWidget(axisItems={'bottom': b_axis}) target_time = datetime.combine(target_date, datetime.min.time()) plot.addLegend() plot.plot(x=hdf_dates, y=hdf_data, name=f'Historical {model.window.target_lbl}') plot.plot(x=[target_time.timestamp()], y=prediction, pen=None, symbol='o', name=f'Predicted Value: {prediction[0]}') model_name = self._model.currentText() self._plot_window.setWindowTitle(f'{model_name} Prediction') self._plot_window.setCentralWidget(plot) self._plot_window.show() def _prediction_historical_error(self, prediction: list) -> None: """Displays a message for when historical target is unavalable such that a graph can't be made.""" QMessageBox.information( self, self.tr("Information"), f'Prediction was: {prediction}. \n ' 'Unable to display graph due to missing historical data.', QtWidgets.QMessageBox.Ok) def _error_event( self, error: str, choice: bool = False, btn: QMessageBox = QMessageBox.Abort ) -> Union[QMessageBox.Ignore, QMessageBox.Abort, None]: """Displays an error message with the given error.""" if choice: response = QMessageBox.critical(self, self.tr("Error"), error, btn, QMessageBox.Ignore) return response else: QMessageBox.critical(self, self.tr("Error"), error, QMessageBox.Ok) return None
class MainUI(QApplication): def __init__(self, saveDefaultPath: List[str] = None, loadFile: List[str] = None, time: float = None, enableIa: bool = None): super(MainUI, self).__init__(argv) if loadFile: self.game = Game.loadJson(loadFile[0], self.trigger, allowedTime=time) else: self.game = Game(self.trigger, allowedTime=time) self.enableIa = enableIa self.saveDefaultPath = None if not saveDefaultPath or saveDefaultPath.__class__ != list else saveDefaultPath[ 0] self.ia = MinMax(game=self.game) def initialize(self): screen = QGuiApplication.primaryScreen().size() size = screen.height() * 0.7 self.window = QMainWindow() self.window.setAutoFillBackground(True) self.window.setWindowFlag( PySide6.QtCore.Qt.WindowType.WindowMaximizeButtonHint, False) self.window.setWindowTitle('Chess') self.window.resize(size, size) self.window.move((screen.width() - self.window.width()) // 2, (screen.height() - self.window.height()) // 2) self.window.show() self.margin = 10 self.button_size = (size - 2 * self.margin) // 8 def construct(self): self.container = QWidget() self.content = QWidget(parent=self.container) # self.content = QWidget() self.window.setCentralWidget(self.container) self.constructMenu() self.constructButtons() self.constructChoices() self.createClock() self.updateView() self.window.resizeEvent = self.onResize @staticmethod def originalTileColor(i: int, j: int) -> ColorPalette: return ColorPalette.TILE_ONE if i % 2 == j % 2 else ColorPalette.TILE_TWO def onGridClicked(self, i, j): if self.selected != None and (self.selected, (i, j)) in self.previousSuggestions: self.game.move(self.selected, (i, j)) if self.enableIa and not self.game.player: move = self.ia.generate() self.game.move(frm=move[0], to=move[1]) self.selected = None self.previousSuggestions = None if self.game.map[i, j] != Piece.EMPTY: self.selected = (i, j) self.previousSuggestions = self.game.getAvailableMoves(i, j) self.updateView() def onUpgrade(self, pieceIndex): self.game.choice(self.getChoices()[pieceIndex]) def updateView(self, event: QResizeEvent = None): self.content.setEnabled(self.game.winner() == Result.RUNNING) if self.game.upgrade: self.choicesView.show() self.content.setDisabled(True) else: self.choicesView.hide() self.content.setDisabled(False) if not event or event.oldSize().width() - event.size().width(): self.button_size = (self.window.width() - 2 * self.margin) // 8 elif not event or event.oldSize().height() - event.size().height(): self.button_size = (self.window.height() - 2 * self.margin - self.window.menuBar().height() - self.clockContainer.height()) // 8 boardSize = 2 * self.margin + 8 * self.button_size self.container.resize(boardSize, boardSize + self.clockContainer.height()) self.window.resize( self.container.width(), self.container.height() + self.window.menuBar().height()) self.choicesView.resize( len(self.choicesButtons) * (self.margin + self.button_size) + self.margin, self.margin * 2 + self.button_size) self.choicesView.move( (boardSize - self.choicesView.width()) // 2, (self.content.height() - self.choicesView.height()) // 2) self.content.resize(boardSize, boardSize) self.clockContainer.resize(boardSize, self.clockContainer.height()) self.content.move(0, self.clockContainer.height()) self.clockB.move(boardSize - self.clockB.width() - self.margin, self.margin) for i, j in self.buttons: self.buttons[i, j].resize(self.button_size, self.button_size) self.buttons[i, j].move(self.button_size * j + self.margin, self.button_size * i + self.margin) self.buttons[i, j].setIcon(self.game.map[i, j].getIcon()) self.setBackgourdColor(self.buttons[i, j], self.originalTileColor(i, j)) self.buttons[i, j].setIconSize( QSize(self.buttons[i, j].width() * 2 / 3, self.buttons[i, j].height() * 2 / 3)) winner = self.game.winner() if winner != Result.RUNNING: self.showDialog( str(winner) + "\nPlease start a new game.", "Info", QMessageBox.Ok, lambda _: None) return for i in range(len(self.choicesButtons)): self.choicesButtons[i].setIcon(self.getChoices()[i].getIcon()) self.choicesButtons[i].setIconSize( QSize(self.button_size * 2 / 3, self.button_size * 2 / 3)) self.choicesButtons[i].resize(self.button_size, self.button_size) btnW = self.button_size + self.margin self.choicesButtons[i].move( self.margin // 2 + (self.choicesView.width() - btnW * len(self.choicesButtons)) // 2 + btnW * i, (self.choicesView.height() - self.button_size) // 2) if self.selected: self.setBackgourdColor( self.buttons[self.selected[0], self.selected[1]], ColorPalette.ACTIVE_COLOR) if (self.previousSuggestions): for (_, (mi, mj)) in self.previousSuggestions: self.setBackgourdColor( self.buttons[mi, mj], ColorPalette.ATTACK if self.game.map[mi, mj] != Piece.EMPTY else ColorPalette.SUGGESTED_MOVE) if self.game.check(): self.setBackgourdColor( self.buttons[self.game.getPiece( Piece.P1_KING if self.game.player else Piece.P2_KING)], ColorPalette.CHECK) self.undo.setEnabled(len(self.game.undoes) > 0) self.redo.setEnabled(len(self.game.redos) > 0) @staticmethod def setBackgourdColor(widget: QWidget, color: ColorPalette): if 'background-color' in widget.styleSheet(): bk_index_start = widget.styleSheet().index('background-color') bk_index_end = widget.styleSheet().index(';', bk_index_start) + 1 widget.setStyleSheet(widget.styleSheet()[:bk_index_start] + widget.styleSheet()[bk_index_end:] + 'background-color: ' + color.value + ';') else: widget.setStyleSheet(widget.styleSheet() + 'background-color: ' + color.value + ';') def onResize(self, event: QResizeEvent): self.updateView(event) def getChoices(self) -> List[Piece]: return [ Piece(choice.value % Piece.P2_PAWN.value + (0 if self.game.player else Piece.P2_PAWN.value)) for choice in [Piece.P1_ROOK, Piece.P1_KNIGHT, Piece.P1_BISHOP, Piece.P1_QUEEN] ] def trigger(self): self.updateView() def constructChoices(self): self.game.trigger = self.trigger self.choicesView: QWidget = QWidget(parent=self.container) # self.choicesBackground: QWidget = QWidget(parent=self.choicesView) self.choicesButtons: list[QPushButton] = [] self.choicesView.setStyleSheet("border: solid;border-width: 2;") self.setBackgourdColor(widget=self.choicesView, color=ColorPalette.CHOICES_BACKGROUND) for i in range(len(self.getChoices())): self.choicesButtons.append(QPushButton(self.choicesView)) self.choicesButtons[i].clicked.connect( functools.partial(self.onUpgrade, i)) def constructButtons(self): self.buttons: List[QPushButton] = {} self.selected = None self.previousSuggestions = None for i in range(8): for j in range(8): self.buttons[i, j] = QPushButton(self.content) self.buttons[i, j].clicked.connect( functools.partial(self.onGridClicked, i, j)) self.buttons[i, j].setStyleSheet( f"background-color: {self.originalTileColor(i, j).value};") def constructMenu(self): menuBar = QMenuBar() self.window.setMenuBar(menuBar) file = menuBar.addMenu('File') edit = menuBar.addMenu('Edit') file.addAction('New Game').triggered.connect( functools.partial(self.newGame, )) self.undo = edit.addAction('Undo') self.undo.triggered.connect(lambda _: self.game.undo()) self.redo = edit.addAction('Redo') self.redo.triggered.connect(lambda _: self.game.redo()) sv = file.addMenu('Save') sv.addAction('as text').triggered.connect( functools.partial(self.save, self.game.saveAsText)) sv.addAction('as PGN').triggered.connect( functools.partial(self.save, self.game.saveAsPGN)) sv.addAction('as JSON').triggered.connect( functools.partial(self.save, self.game.saveAsJSON)) sva = file.addMenu('Save As') sva.addAction('as text').triggered.connect( functools.partial(self.saveAs, self.game.saveAsText)) sva.addAction('as PGN').triggered.connect( functools.partial(self.saveAs, self.game.saveAsPGN)) sva.addAction('as JSON').triggered.connect( functools.partial(self.saveAs, self.game.saveAsJSON)) file.addAction('Load Game').triggered.connect( functools.partial(self.load, self.game.loadJson)) file.addAction('Abandon').triggered.connect( functools.partial(self.showDialog, "Do you want really to abandon?", "Warning", QMessageBox.No | QMessageBox.Yes, self.abandon)) file.addAction('Draw').triggered.connect( functools.partial(self.showDialog, ("White" if self.game.player else "Black") + " is proposing draw", "Info", QMessageBox.No | QMessageBox.Yes, self.proposeDraw)) file.addAction('Quit').triggered.connect(lambda _: exit(0)) def save(self, f): if self.saveDefaultPath: f(self.saveDefaultPath) else: self.saveAs(f) def saveAs(self, f): saveAs = QFileDialog() saveAs.setDefaultSuffix('.json') self.timer.stop() fileName, _ = saveAs.getSaveFileName(parent=self.window, filter="JSON (*.json)", caption="Save as json file.") self.timer.start(1) if fileName: self.saveDefaultPath = fileName self.save(f) def load(self, f): self.timer.stop() fileName, _ = QFileDialog.getOpenFileName(parent=self.window, filter="JSON (*.json)") self.timer.start() if fileName: self.saveDefaultPath = fileName self.game: Game = f(fileName, trigger=self.trigger, maxTime=self.game.allowedTime) self.updateView() def newGame(self): self.game: Game = Game(self.trigger, allowedTime=self.game.allowedTime) self.construct() def createClock(self): self.clockContainer = QWidget(self.container) self.clockW: QLabel = QLabel(parent=self.clockContainer, text=self.format( self.game.getRemainingTime(True))) self.clockB: QLabel = QLabel(parent=self.clockContainer, text=self.format( self.game.getRemainingTime(False))) self.clockW.move(self.margin, self.margin) self.timer: QTimer = QTimer(self.clockContainer) self.timer.timeout.connect(self.updateTime) self.timer.start(1000) def showDialog(self, text: str, title: str, buttons, callback): dialog = QMessageBox(self.window) dialog.setWindowTitle(title) dialog.setText(text) dialog.setStandardButtons(buttons) dialog.buttonClicked.connect(callback) dialog.exec_() def abandon(self, value: QPushButton): if value.text().lower().count('yes'): self.game.abandon = self.game.player self.updateView() def proposeDraw(self, value: QPushButton): if value.text().lower().count('yes'): self.game.draw = True self.updateView() def updateTime(self): wr = self.game.getRemainingTime(True) br = self.game.getRemainingTime(False) if self.game.winner() == Result.RUNNING: c = wr if self.game.player else br self.timer.start(c - int(c) if c > 0 else 1) else: self.timer.stop() self.updateView() self.clockW.setText(self.format(wr)) self.clockB.setText(self.format(br)) def format(self, time: float): if time <= 0: return "tmout" st = "" if time >= 3600: st += f"{time//3600}:" time = time % 3600 st += '0' if time // 60 < 10 else '' st += f"{time//60}:" time = time % 60 st += '0' if time < 10 else '' st += f"{int(time)}" return st
def getInputDir(self): dir = QFileDialog.getExistingDirectory(None, "Select folder", self.userDesktop) if dir: path = dir.replace("/", "\\") self.InputLineEdit.setText(path) def getOuputDir(self): dir = QFileDialog.getExistingDirectory(None, "Select save folder location", self.userDesktop) if dir: path = dir.replace("/", "\\") self.OutputLineEdit.setText(path) if __name__ == "__main__": os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" # For High DPI Displays app = QApplication(sys.argv) # Create the Qt Application app.setAttribute( QtCore.Qt.AA_EnableHighDpiScaling) # For High DPI Displays MainWindow = QMainWindow() # Create an instance ui = TheApp(MainWindow) # Show the window and start the app MainWindow.show() app.exec()
class DebugExecutorWindow(MemoryOperationExecutor, Ui_DebugBackendWindow): _connected: bool = False _timer: QtCore.QTimer pickups: List[PickupEntry] def __init__(self): super().__init__() self.logger.setLevel(logging.DEBUG) self.window = QMainWindow() self.setupUi(self.window) common_qt_lib.set_default_window_icon(self.window) self.pickups = [] self.collect_location_combo.setVisible(False) self.setup_collect_location_combo_button = QtWidgets.QPushButton( self.window) self.setup_collect_location_combo_button.setText( "Load list of locations") self.setup_collect_location_combo_button.clicked.connect( self._setup_locations_combo) self.gridLayout.addWidget(self.setup_collect_location_combo_button, 0, 0, 1, 2) self.collect_location_button.clicked.connect(self._emit_collection) self.collect_location_button.setEnabled(False) self.collect_randomly_check.stateChanged.connect( self._on_collect_randomly_toggle) self._timer = QtCore.QTimer(self.window) self._timer.timeout.connect(self._collect_randomly) self._timer.setInterval(10000) self._used_version = echoes_dol_versions.ALL_VERSIONS[0] self._connector = EchoesRemoteConnector(self._used_version) self.game = default_database.game_description_for( RandovaniaGame.METROID_PRIME_ECHOES) self._game_memory = bytearray(24 * (2**20)) self._game_memory_initialized = False def _on_collect_randomly_toggle(self, value: int): if bool(value): self._timer.start() else: self._timer.stop() def _collect_randomly(self): row = random.randint(0, self.collect_location_combo.count()) self._collect_location(self.collect_location_combo.itemData(row)) async def _ensure_initialized_game_memory(self): if self._game_memory_initialized: return world = self.game.world_list.worlds[0] await self.perform_memory_operations([ # Build String MemoryOperation(self._used_version.build_string_address, write_bytes=self._used_version.build_string), # current CWorld MemoryOperation(self._used_version.game_state_pointer, offset=4, write_bytes=world.extra['asset_id'].to_bytes( 4, "big")), # CPlayer VTable MemoryOperation( self._used_version.cstate_manager_global + 0x14fc, offset=0, write_bytes=self._used_version.cplayer_vtable.to_bytes( 4, "big")), # CPlayerState MemoryOperation(self._used_version.cstate_manager_global + 0x150c, write_bytes=0xA00000.to_bytes(4, "big")), ]) self._game_memory_initialized = True def _read_memory(self, address: int, count: int): address &= ~0x80000000 return self._game_memory[address:address + count] def _read_memory_format(self, format_string: str, address: int): return struct.unpack_from(format_string, self._game_memory, address & ~0x80000000) def _write_memory(self, address: int, data: bytes): address &= ~0x80000000 self._game_memory[address:address + len(data)] = data self.logger.info(f"Wrote {data.hex()} to {hex(address)}") async def _update_inventory_label(self): inventory = await self._connector.get_inventory(self) s = "<br />".join(f"{name} x {quantity.amount}/{quantity.capacity}" for name, quantity in inventory.items()) self.inventory_label.setText(s) def show(self): self.window.show() def _get_magic_address(self): multiworld_magic_item = self.game.resource_database.multiworld_magic_item return _echoes_powerup_address(multiworld_magic_item.index) def _read_magic(self): return self._read_memory_format(">II", self._get_magic_address()) def _write_magic(self, magic_amount, magic_capacity): self._write_memory(self._get_magic_address(), struct.pack(">II", magic_amount, magic_capacity)) def _emit_collection(self): self._collect_location(self.collect_location_combo.currentData()) def _collect_location(self, location: int): new_magic_value = location + 1 magic_amount, magic_capacity = self._read_magic() if magic_amount == 0: magic_amount += new_magic_value magic_capacity += new_magic_value self._write_magic(magic_amount, magic_capacity) @asyncSlot() @handle_network_errors async def _setup_locations_combo(self): network_client = common_qt_lib.get_network_client() game_session = network_client.current_game_session user = network_client.current_user game = self.game index_to_name = { node.pickup_index.index: game.world_list.area_name(area) for world, area, node in game.world_list.all_worlds_areas_nodes if isinstance(node, PickupNode) } if game_session is None: names = index_to_name else: patcher_data = await network_client.session_admin_player( user.id, SessionAdminUserAction.CREATE_PATCHER_FILE, BaseCosmeticPatches().as_json) names = { pickup["pickup_index"]: "{}: {}".format(index_to_name[pickup["pickup_index"]], pickup["hud_text"][0].split(", ", 1)[0]) for pickup in patcher_data["pickups"] } self.collect_location_combo.clear() for index, name in sorted(names.items(), key=lambda t: t[1]): self.collect_location_combo.addItem(name, index) self.collect_location_button.setEnabled(True) self.collect_location_combo.setVisible(True) self.setup_collect_location_combo_button.deleteLater() def clear(self): self.messages_list.clear() self.pickups.clear() def _memory_operation(self, op: MemoryOperation) -> Optional[bytes]: op.validate_byte_sizes() address = op.address if op.offset is not None: address = self._read_memory_format(">I", address)[0] address += op.offset result = None if op.read_byte_count is not None: result = self._read_memory(address, op.read_byte_count) if op.write_bytes is not None: self._write_memory(address, op.write_bytes) return result def _add_power_up(self, registers: Dict[int, int]): item_id = registers[4] delta = registers[5] item = next(item for item in self.game.resource_database.item if item.extra.get("item_id") == item_id) address = _echoes_powerup_address(item_id) amount, capacity = self._read_memory_format(">II", address) capacity = max(0, min(capacity + delta, item.max_capacity)) amount = min(amount, capacity) self._write_memory(address, struct.pack(">II", amount, capacity)) def _incr_pickup(self, registers: Dict[int, int]): item_id = registers[4] delta = registers[5] address = _echoes_powerup_address(item_id) amount, capacity = self._read_memory_format(">II", address) amount = max(0, min(amount + delta, capacity)) self._write_memory(address, struct.pack(">I", amount)) def _decr_pickup(self, registers: Dict[int, int]): item_id = registers[4] delta = registers[5] address = _echoes_powerup_address(item_id) amount, capacity = self._read_memory_format(">II", address) amount = max(0, min(amount - delta, capacity)) self._write_memory(address, struct.pack(">I", amount)) def _display_hud_memo(self, registers): string_start = self._used_version.string_display.message_receiver_string_ref message_bytes = self._read_memory( string_start, self._used_version.string_display.max_message_size + 2) message = message_bytes[:message_bytes.find(b"\x00\x00")].decode( "utf-16_be") self.messages_list.addItem(message) def _handle_remote_execution(self): has_message_address = self._used_version.cstate_manager_global + 0x2 if self._read_memory(has_message_address, 1) == b"\x00": return self.logger.debug("has code to execute, start parsing ppc") registers: Dict[int, int] = {i: 0 for i in range(32)} registers[3] = self._used_version.sda2_base functions = { self._used_version.powerup_functions.add_power_up: self._add_power_up, self._used_version.powerup_functions.incr_pickup: self._incr_pickup, self._used_version.powerup_functions.decr_pickup: self._decr_pickup, self._used_version.string_display.wstring_constructor: lambda reg: None, self._used_version.string_display.display_hud_memo: self._display_hud_memo, } patch_address, _ = all_prime_dol_patches.create_remote_execution_body( self._used_version.string_display, []) current_address = patch_address while current_address - patch_address < 0x2000: instruction = self._read_memory(current_address, 4) inst_value = int.from_bytes(instruction, "big") header = inst_value >> 26 def get_signed_16(): return struct.unpack("h", struct.pack("H", inst_value & 65535))[0] if header == 19: # subset of branch without address. Consider these blr end_address = current_address self.logger.debug("blr") break if header == 14: # addi output_reg = (inst_value >> 21) & 31 input_reg = (inst_value >> 16) & 31 value = get_signed_16() if input_reg == 0: self.logger.debug(f"addi r{output_reg}, {value}") registers[output_reg] = value else: self.logger.debug( f"addi r{output_reg}, r{input_reg}, {value}") registers[output_reg] = registers[input_reg] + value elif header == 15: # addis output_reg = (inst_value >> 21) & 31 input_reg = (inst_value >> 16) & 31 value = inst_value & 65535 if input_reg == 0: self.logger.debug(f"lis r{output_reg}, {value}") registers[output_reg] = value << 16 else: self.logger.debug( f"addis r{output_reg}, r{input_reg}, {value}") registers[output_reg] = (registers[input_reg] + value) << 16 elif header == 18: # b and bl (relative branch) jump_offset = (inst_value >> 2) & ((2 << 24) - 1) link = bool(inst_value & 1) address = current_address + (jump_offset * 4) if address in functions: function_name = functions[address].__name__ functions[address](registers) else: function_name = "unknown" self.logger.debug( f"b{'l' if link else ''} 0x{address:0x} [{function_name}]") elif header == 24: # ori output_reg = (inst_value >> 21) & 31 input_reg = (inst_value >> 16) & 31 value = inst_value & 65535 self.logger.debug(f"ori r{output_reg}, r{input_reg}, {value}") registers[output_reg] = registers[input_reg] | value elif header == 36 or header == 38: # stw (36) and stb (38) input_reg = (inst_value >> 21) & 31 output_reg = (inst_value >> 16) & 31 value = get_signed_16() if header == 36: inst = 'stw' size = 4 else: inst = 'stb' size = 1 self.logger.debug( f"{inst} r{input_reg}, 0x{value:0x} (r{output_reg})") # ignore r1, as it's writing to stack if output_reg != 1: self._write_memory( registers[output_reg] + value, registers[input_reg].to_bytes(size, "big")) elif header == 32: # lwz output_reg = (inst_value >> 21) & 31 input_reg = (inst_value >> 16) & 31 value = get_signed_16() self.logger.debug( f"lwz r{input_reg}, 0x{value:0x} (r{output_reg})") ea = value if input_reg != 0: ea += registers[input_reg] registers[output_reg] = int.from_bytes( self._read_memory(ea, 4), "big") else: self.logger.debug( f"unsupported instruction: {instruction.hex()}; {header}") current_address += 4 self._write_memory(has_message_address, b"\x00") # MemoryOperationExecutor @property def lock_identifier(self) -> Optional[str]: return None @property def backend_choice(self): return DebugGameBackendChoice() async def connect(self) -> bool: await self._ensure_initialized_game_memory() self._handle_remote_execution() await self._update_inventory_label() self._connected = True return True async def disconnect(self): self._connected = False def is_connected(self) -> bool: return self._connected async def perform_memory_operations( self, ops: List[MemoryOperation]) -> Dict[MemoryOperation, bytes]: result = {} for op in ops: op_result = self._memory_operation(op) if op_result is not None: result[op] = op_result return result