From 6c0f1dcbce5c7a58bef44ad9bb3587ce269934d1 Mon Sep 17 00:00:00 2001 From: Benedikt Ziemons Date: Sat, 3 Feb 2024 23:05:51 +0100 Subject: [PATCH] Add search_resource_async for visualisation --- README.md | 3 +- factorygame/data/vis.py | 156 +++++++++++++++++++++++++++------------- 2 files changed, 109 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index df3ef6e..9df3db3 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Tool for helping with factory planning. ## Installation -Tested are Python 3.11, PySide2 5.15.11 and Qt 5.15.11. The later two are necessary for the visualisation. Install all Python requirements by using +Tested are Python 3.11, Firefox 122, SQLite 3.45.1, PySide2 5.15.11 and Qt 5.15.11. The later two are necessary for the visualisation. Install all Python requirements by using ```sh pipenv --site-packages sync @@ -39,6 +39,5 @@ Thanks to [NodeGraphQt](https://github.com/jchanvfx/NodeGraphQt) a graph base vi The visualisation window can be opened with e.g. ```sh -pipenv run fetch 'Plastic' pipenv run vis 'Plastic' ``` diff --git a/factorygame/data/vis.py b/factorygame/data/vis.py index 059cd06..5c879d3 100755 --- a/factorygame/data/vis.py +++ b/factorygame/data/vis.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 import re +import sys from concurrent.futures import Future +from typing import Callable import click import sqlalchemy @@ -18,7 +20,7 @@ from sqlalchemy.orm import Session from factorygame.data.common import chose_resource, resource_needs_update, chose_recipe from factorygame.data.sfp import SatisfactoryPlus, DEFAULT_IGNORE_FACTORIES -from .models import Recipe, Resource, ResourceFlow +from .models import Recipe, Resource, ResourceFlow, Base WORLD_INPUT_PORT_NAME = "World Input" WORLD_OUTPUT_PORT_NAME = "World Output" @@ -105,7 +107,44 @@ class AsyncResourceFetcher(QThread): resource = session.scalars(Resource.by_label(self.resource_label)).one() resource = data_provider.update_resource_recipes(session=session, resource=resource) if self.debug: - print("Updated resource in separate thread", resource) + print("AsyncResourceFetcher updated resource in separate thread", resource) + session.commit() + + +class AsyncResourceFinder(QThread): + def __init__(self, parent, resource_label: str, engine: sqlalchemy.Engine, prompt: Callable, debug: bool): + super().__init__(parent=parent) + self.resource_label = resource_label + self.engine = engine + self.debug = debug + self.prompt = prompt + self.result = Future() + + def run(self): + try: + with ( + Session(self.engine) as session, + SatisfactoryPlus(ignore_factories=DEFAULT_IGNORE_FACTORIES, debug=self.debug) as data_provider, + ): + search_result_future = data_provider.search_for_resource( + session=session, search=self.resource_label, prompt=self.prompt + ) + search_result = search_result_future.result(timeout=60) + if search_result is None: + if self.debug: + print("AsyncResourceFinder search completed, but no result") + self.result.set_result(None) + return + + resource, exists_in_db = search_result + if not exists_in_db or resource_needs_update(resource): + resource = data_provider.update_resource_recipes(session=session, resource=resource) + if self.debug: + print("AsyncResourceFinder updated resource", resource) + self.result.set_result(resource) + session.commit() + except: + self.result.set_exception(sys.exception()) class NodeSlider(NodeBaseWidget): @@ -410,6 +449,7 @@ class GraphController(QObject): self.debug = debug self.engine = create_engine("sqlite:///file.db", echo=debug) + Base.metadata.create_all(bind=self.engine) self.graph = NodeGraph(parent=self) self.graph.widget.resize(1280, 720) self.graph.register_node(Machine) @@ -421,26 +461,26 @@ class GraphController(QObject): self.graph.port_connected.connect(self.on_port_connected) + self.prompt_dialog = QInputDialog(parent=self.graph.widget) + self.prompt_dialog.setStyleSheet(generate_fgbg_stylesheet()) + self.prompt_dialog.setModal(True) + self._dialog_future = Future() + self.prompt_dialog.rejected.connect(lambda: self._dialog_future.set_result(None)) + self.prompt_dialog.accepted.connect(lambda: self._dialog_future.set_result(self.prompt_dialog.textValue())) + + self.loading_dialog = QDialog(parent=self.graph.widget, f=(Qt.Dialog | Qt.ToolTip)) + self.loading_dialog.setStyleSheet(generate_fgbg_stylesheet(0.5)) + self.loading_dialog.setModal(True) + self.loading_dialog.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + label = QLabel("Loading…", self.loading_dialog) + label.setAlignment(Qt.AlignCenter) + def add_machine_from_search(self, search: str): recipe_selected_future = Future() - def resource_selected_cb(resource_future: Future[Resource | None]): - resource = resource_future.result() + def resource_found_db(resource: Resource | None): if resource is None: - # FIXME: use concurrent resource searching/fetching - # ret = data_provider.search_for_resource(session=session, search=search) - ret = None - if ret is None: - print("Resource not found") - recipe_selected_future.set_result(None) - return - - resource, exists_in_db = ret - if not exists_in_db: - print("Resource unknown") - recipe_selected_future.set_result(None) - return - assert resource is not None + return def chose_recipe_async(): with Session(self.engine) as session: @@ -454,10 +494,13 @@ class GraphController(QObject): else: chose_recipe_async() - def select_recipe_async(): - with Session(self.engine) as session: - resource_future = chose_resource(session=session, resource_label=search, prompt=self.dialog_prompt) - resource_future.add_done_callback(resource_selected_cb) + def resource_selected_or_search_cb(resource_future: Future[Resource | None]): + resource = resource_future.result() + if resource is None: + search_future = self.search_resource_async(resource_label=search) + search_future.add_done_callback(lambda fut: resource_found_db(fut.result())) + else: + resource_found_db(resource) def recipe_selected_cb(recipe_future: Future[Recipe | None]): recipe = recipe_future.result() @@ -473,7 +516,9 @@ class GraphController(QObject): self.graph.center_on([self.global_input, recipe_machine, self.global_output]) recipe_selected_future.add_done_callback(recipe_selected_cb) - select_recipe_async() + with Session(self.engine) as session: + resource_future = chose_resource(session=session, resource_label=search, prompt=self.dialog_prompt) + resource_future.add_done_callback(resource_selected_or_search_cb) def show(self): self.graph.widget.show() @@ -597,44 +642,59 @@ class GraphController(QObject): debug=self.debug, ) fetch_future = Future() - self.dialog_loading(fetch_future) + self.loading_dialog.show() + fetch_future.add_done_callback(lambda fut: self.loading_dialog.hide()) fetcher_thread.finished.connect(lambda: fetch_future.set_result(None)) fetcher_thread.start() return fetch_future + def search_resource_async(self, resource_label) -> Future[Resource]: + if self.debug: + print("Searching resource", resource_label) + + loading_future = Future() + + def show_prompt(*args, **kwargs): + loading_future.set_result(None) + return self.dialog_prompt(*args, **kwargs) + + fetcher_thread = AsyncResourceFinder( + parent=self.graph, + resource_label=resource_label, + engine=self.engine, + prompt=show_prompt, + debug=self.debug, + ) + self.loading_dialog.show() + loading_future.add_done_callback(lambda fut: self.loading_dialog.hide()) + fetcher_thread.start() + return fetcher_thread.result + def dialog_prompt(self, options: dict[int, str], text: str, default: int) -> Future[int | None]: + assert self.prompt_dialog.isHidden(), "Prompt dialog already visible" if self.debug: print("Displaying QInputDialog with options:", ", ".join(options.values())) - dialog = QInputDialog(parent=self.graph.widget) - dialog.setStyleSheet(generate_fgbg_stylesheet()) - dialog.setModal(True) - dialog.setLabelText(text) + self.prompt_dialog.setLabelText(text) if default in options: - dialog.setTextValue(options[default]) + self.prompt_dialog.setTextValue(options[default]) reversed_options: dict[str, int] = {value: key for key, value in options.items()} - dialog.setComboBoxItems(reversed_options.keys()) + self.prompt_dialog.setComboBoxItems(reversed_options.keys()) + ret = Future() - dialog.rejected.connect(lambda: ret.set_result(None)) - dialog.accepted.connect(lambda: ret.set_result(reversed_options[dialog.textValue()])) - dialog.show() + self._dialog_future = Future() + + def map_result(fut: Future): + result = fut.result() + if result is None: + ret.set_result(None) + else: + ret.set_result(reversed_options[result]) + + self._dialog_future.add_done_callback(map_result) + self.prompt_dialog.show() return ret - def dialog_loading(self, future: Future): - dialog = QDialog(parent=self.graph.widget, f=(Qt.Dialog | Qt.ToolTip)) - dialog.setStyleSheet(generate_fgbg_stylesheet(0.5)) - dialog.setModal(True) - dialog.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - label = QLabel("Loading…", dialog) - label.setAlignment(Qt.AlignCenter) - dialog.show() - - def remove_dialog(): - dialog.hide() - dialog.destroy() - - future.add_done_callback(lambda fut: remove_dialog()) - @click.command @click.option("--debug", is_flag=True)