diff --git a/factorygame/data/common.py b/factorygame/data/common.py index 728d616..cd6361b 100644 --- a/factorygame/data/common.py +++ b/factorygame/data/common.py @@ -39,8 +39,14 @@ def chose_resource(session: AlchemySession, resource_label: str, prompt: Callabl return ret -def chose_recipe(session: AlchemySession, resource: Resource, prompt: Callable) -> Future[Recipe | None]: - stmt = select(Recipe).distinct().join(Recipe.results).filter(ResourceFlow.resource_id == resource.id) +def chose_recipe( + session: AlchemySession, resource: Resource, prompt: Callable, ingredient: bool = False +) -> Future[Recipe | None]: + stmt = select(Recipe).distinct() + if ingredient: + stmt = stmt.join(Recipe.ingredients).filter(ResourceFlow.resource_id == resource.id) + else: + stmt = stmt.join(Recipe.results).filter(ResourceFlow.resource_id == resource.id) recipes = session.scalars(stmt).all() ret = Future() if len(recipes) == 0: diff --git a/factorygame/data/fetch.py b/factorygame/data/fetch.py index ca0f759..0b3e85d 100755 --- a/factorygame/data/fetch.py +++ b/factorygame/data/fetch.py @@ -13,7 +13,7 @@ from ..helper import click_prompt @click.command() @click.option("--debug", is_flag=True) @click.option("--refetch", is_flag=True) -@click.option("--ignore-factories", type=list[str], default=["A.I. Fluid Packer", "Craft Bench"]) +@click.option("--ignore-factories", type=list[str], default=["A.I. Fluid Packer", "Craft Bench", "Equipment Workshop"]) @click.argument("search") def main(debug: bool, refetch: bool, ignore_factories: list[str], search: str): engine = create_engine("sqlite:///file.db", echo=debug) diff --git a/factorygame/data/vis.py b/factorygame/data/vis.py index 7adf8e9..eaebfe6 100755 --- a/factorygame/data/vis.py +++ b/factorygame/data/vis.py @@ -19,8 +19,9 @@ from sqlalchemy.orm import Session from factorygame.data.common import chose_resource, resource_needs_update, chose_recipe from .models import Recipe, Resource, ResourceFlow +WORLD_INPUT_PORT_NAME = "World Input" WORLD_OUTPUT_PORT_NAME = "World Output" -WORLD_OUTPUT_COLOR = (0, 139, 41) +WORLD_PORT_COLOR = (0, 139, 41) CREATE_MACHINE_PORT_NAME = "Create Machine" @@ -124,10 +125,10 @@ class GlobalInput(BaseNode): self.output_resources: dict[str, float] = {} def add_special_ports(self): - self.add_output(WORLD_OUTPUT_PORT_NAME, multi_output=False, color=WORLD_OUTPUT_COLOR) + self.add_output(WORLD_OUTPUT_PORT_NAME, multi_output=False, color=WORLD_PORT_COLOR) self.add_output(CREATE_MACHINE_PORT_NAME, multi_output=False, color=OTHER_COLOR) - def reorder_outputs(self): + def reorder_ports(self): self.delete_output(WORLD_OUTPUT_PORT_NAME) self.delete_output(CREATE_MACHINE_PORT_NAME) self.add_special_ports() @@ -136,23 +137,24 @@ class GlobalInput(BaseNode): min_x_pos = min(map(lambda node: node.x_pos(), filter(lambda node: node != self, graph.all_nodes()))) self.set_x_pos(min_x_pos - self.view.width - 150) - def create_global_output(self, graph: NodeGraph, resource_label: str, initial_resource_amount: str) -> Port: + def create_global_output(self, graph: NodeGraph, resource_label: str, initial_resource_amount: float) -> Port: + # FIXME: support multiple resource outputs or add splitters new_output_port = self.add_output(name=resource_label, multi_output=False, color=OUTPUT_COLOR) widget = add_resource_text( node=self, name=out_amount_name(resource_label), - text=initial_resource_amount, + text=resource_amount_to_text(initial_resource_amount), border_color=OUTPUT_COLOR, ) - self.output_resources[resource_label] = parse_resource_amount(initial_resource_amount) + self.output_resources[resource_label] = initial_resource_amount widget.value_changed.connect(lambda: self.update_resource_output(resource_label)) - self.reorder_outputs() + self.reorder_ports() self.update_x_pos(graph=graph) return new_output_port def update_resource_output(self, resource_label: str): - widget: NodeLineEdit = self.get_widget(out_amount_name(resource_label)) assert resource_label in self.output_resources + widget: NodeLineEdit = self.get_widget(out_amount_name(resource_label)) new_amount = parse_resource_amount(widget.get_value()) if self.output_resources[resource_label] != new_amount: self.output_resources[resource_label] = new_amount @@ -164,6 +166,54 @@ class GlobalInput(BaseNode): node.update_input(resource_label, new_amount) +class GlobalOutput(BaseNode): + __identifier__ = "factorygame" + NODE_NAME = "Global Output" + + def __init__(self): + super().__init__() + self.model.width = 240 + self.set_port_deletion_allowed(True) + self.add_special_ports() + self.input_resources: dict[str, float] = {} + + def add_special_ports(self): + self.add_input(WORLD_INPUT_PORT_NAME, multi_input=False, color=WORLD_PORT_COLOR) + self.add_input(CREATE_MACHINE_PORT_NAME, multi_input=False, color=OTHER_COLOR) + + def reorder_ports(self): + self.delete_input(WORLD_INPUT_PORT_NAME) + self.delete_input(CREATE_MACHINE_PORT_NAME) + self.add_special_ports() + + def update_x_pos(self, graph: NodeGraph): + other_nodes = filter(lambda node: node != self, graph.all_nodes()) + max_x = max(map(lambda node: node.x_pos() + node.view.width, other_nodes)) + self.set_x_pos(max_x + 150) + + def create_global_input(self, graph: NodeGraph, resource_label: str, initial_resource_amount: float) -> Port: + # FIXME: support multiple resource inputs or add mergers + new_input_port = self.add_input(name=resource_label, multi_input=False, color=INPUT_COLOR) + widget = add_resource_text( + node=self, + name=in_amount_name(resource_label), + text=resource_amount_to_text(initial_resource_amount), + border_color=INPUT_COLOR, + ) + line_edit_widget: QLineEdit = widget.get_custom_widget() + line_edit_widget.setReadOnly(True) + self.input_resources[resource_label] = initial_resource_amount + self.reorder_ports() + return new_input_port + + def update_input(self, resource_label: str, value: float): + assert resource_label in self.input_resources + widget: NodeLineEdit = self.get_widget(in_amount_name(resource_label)) + if self.input_resources[resource_label] != value: + self.input_resources[resource_label] = value + widget.set_value(resource_amount_to_text(self.input_resources[resource_label])) + + class Machine(BaseNode): __identifier__ = "factorygame" NODE_NAME = "FactoryGame Machine" @@ -245,7 +295,6 @@ class Machine(BaseNode): for port in self.get_output(result_label).connected_ports(): node = port.node() - assert isinstance(node, Machine), f"Connected node must be a machine: {node}" node.update_input( resource_label=result_label, value=new_output_value, @@ -291,6 +340,9 @@ class Machine(BaseNode): else: port = self.add_output(resource_label, multi_output=False, color=OUTPUT_COLOR) port.add_accept_port_type(resource_label, PortTypeEnum.IN.value, "factorygame.Machine") + port.add_accept_port_type(resource_label, PortTypeEnum.IN.value, "factorygame.GlobalOutput") + port.add_accept_port_type(CREATE_MACHINE_PORT_NAME, PortTypeEnum.IN.value, "factorygame.GlobalOutput") + port.add_accept_port_type(WORLD_INPUT_PORT_NAME, PortTypeEnum.IN.value, "factorygame.GlobalOutput") self._add_readonly_resource_text( out_amount_name(resource_label), result, @@ -340,8 +392,10 @@ class GraphController(QObject): self.graph.widget.resize(1280, 720) self.graph.register_node(Machine) self.graph.register_node(GlobalInput) + self.graph.register_node(GlobalOutput) self.global_input: GlobalInput = self.graph.create_node("factorygame.GlobalInput", push_undo=False) + self.global_output: GlobalOutput = self.graph.create_node("factorygame.GlobalOutput", push_undo=False) self.graph.port_connected.connect(self.on_port_connected) @@ -379,17 +433,20 @@ class GraphController(QObject): resource_future = chose_resource(session=session, resource_label=search, prompt=self.dialog_prompt) resource_future.add_done_callback(resource_selected_cb) - def recipe_selections_cb(recipe_future: Future[Recipe | None]): + def recipe_selected_cb(recipe_future: Future[Recipe | None]): recipe = recipe_future.result() if recipe is not None: recipe_machine: Machine = self.graph.create_node("factorygame.Machine", push_undo=False) recipe_machine.assign_recipe(recipe) - self.graph.auto_layout_nodes([self.global_input, recipe_machine]) + recipe_machine.update() + self.graph.auto_layout_nodes([self.global_input, recipe_machine, self.global_output]) self.global_input.set_y_pos(recipe_machine.y_pos()) + self.global_output.set_y_pos(recipe_machine.y_pos()) self.global_input.update_x_pos(graph=self.graph) - self.graph.center_on([self.global_input, recipe_machine]) + self.global_output.update_x_pos(graph=self.graph) + self.graph.center_on([self.global_input, recipe_machine, self.global_output]) - recipe_selected_future.add_done_callback(recipe_selections_cb) + recipe_selected_future.add_done_callback(recipe_selected_cb) select_recipe_async() def show(self): @@ -401,6 +458,7 @@ class GraphController(QObject): output_node = output_port.node() if output_node == self.global_input: resource_label = input_port.name() + input_node = input_port.node() if output_port.name() == CREATE_MACHINE_PORT_NAME: output_port.clear_connections(push_undo=False, emit_signal=False) @@ -410,9 +468,11 @@ class GraphController(QObject): recipe_machine: Machine = self.graph.create_node("factorygame.Machine", push_undo=True) recipe_machine.assign_recipe(recipe) recipe_machine.update() - recipe_machine.set_x_pos(input_port.node().x_pos() - recipe_machine.view.width - 100) + recipe_machine.set_x_pos(input_node.x_pos() - recipe_machine.view.width - 100) recipe_machine.get_output(resource_label).connect_to(input_port) self.global_input.update_x_pos(graph=self.graph) + if isinstance(input_node, Machine): + input_node.update_input(resource_label, recipe_machine.get_resource_output(resource_label)) with Session(self.engine) as session: resource = session.scalars(Resource.by_label(resource_label)).one_or_none() @@ -430,19 +490,56 @@ class GraphController(QObject): # if port.name() == resource_label: # port.connect_to(input_port, push_undo=True, emit_signal=False) # return - if isinstance(input_port.node(), Machine): - machine_node: Machine = input_port.node() - initial_resource_amount = str(machine_node.get_property(in_amount_name(resource_label))) + if isinstance(input_node, Machine): + resource_amount = parse_resource_amount(str(input_node.get_property(in_amount_name(resource_label)))) new_output_port = self.global_input.create_global_output( graph=self.graph, resource_label=resource_label, - initial_resource_amount=initial_resource_amount, + initial_resource_amount=resource_amount, ) new_output_port.connect_to(input_port, push_undo=True, emit_signal=False) + input_node.update_input(resource_label, resource_amount) elif isinstance(output_node, Machine): input_node = input_port.node() resource_label = output_port.name() - if isinstance(input_node, Machine): + if input_node == self.global_output: + if input_port.name() == CREATE_MACHINE_PORT_NAME: + input_port.clear_connections(push_undo=False, emit_signal=False) + + def recipe_selected_cb(recipe_future: Future[Recipe | None]): + recipe = recipe_future.result() + if recipe is not None: + recipe_machine: Machine = self.graph.create_node("factorygame.Machine", push_undo=True) + recipe_machine.assign_recipe(recipe) + recipe_machine.update() + recipe_machine.set_x_pos(output_node.x_pos() + output_node.view.width + 100) + output_port.connect_to(recipe_machine.get_input(resource_label)) + self.global_output.update_x_pos(graph=self.graph) + + with Session(self.engine) as session: + resource = session.scalars(Resource.by_label(resource_label)).one_or_none() + if resource_needs_update(resource, recipe_info_timeout=timedelta(days=365)): + print("Please fetch resource", resource_label, "first.") + return + + chose_recipe( + session=session, resource=resource, prompt=self.dialog_prompt, ingredient=True + ).add_done_callback(recipe_selected_cb) + elif input_port.name() == WORLD_INPUT_PORT_NAME: + input_port.clear_connections(push_undo=False, emit_signal=False) + for idx, port in enumerate(self.global_output.output_ports()): + assert port.name() != resource_label, f"Duplicate output port for {resource_label}" + # if port.name() == resource_label: + # port.connect_to(input_port, push_undo=True, emit_signal=False) + # return + resource_amount = output_node.get_resource_output(resource_label) + new_input_port = self.global_output.create_global_input( + graph=self.graph, + resource_label=resource_label, + initial_resource_amount=resource_amount, + ) + output_port.connect_to(new_input_port, push_undo=True, emit_signal=False) + elif isinstance(input_node, Machine): resource_amount = output_node.get_resource_output(resource_label) input_node.update_input( resource_label,