Add search_resource_async for visualisation

This commit is contained in:
Ben 2024-02-03 23:05:51 +01:00
parent 459bef9c45
commit 6c0f1dcbce
Signed by: ben
GPG key ID: 0F54A7ED232D3319
2 changed files with 109 additions and 50 deletions

View file

@ -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'
```

View file

@ -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)