diff --git a/data_collector.py b/data_collector.py new file mode 100644 index 0000000..e3774a5 --- /dev/null +++ b/data_collector.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +""" +Collector Process: +- Runs the OCR engine. +- Updates OCR data every 0.5 seconds. +- Exposes the latest data via an HTTP API using Flask. +""" + +import time +import threading +from flask import Flask, jsonify + +app = Flask(__name__) + +# Global variable to hold the latest stats (HP, MP, FP, EXP) +latest_data = { + "hp": "0/0", + "mp": "0/0", + "fp": "0/0", + "exp": "0.0000" +} + +def ocr_collector(): + """ + This function simulates the OCR process. + Replace the code below with your actual OCR logic. + """ + global latest_data + counter = 0 + while True: + # Simulate updating stats: + hp_current = 50 + counter % 10 + hp_max = 100 + mp_current = 30 + counter % 5 + mp_max = 50 + fp_current = 20 # fixed, for example + fp_max = 20 + exp_val = round(10.0 + (counter * 0.1), 4) + + latest_data = { + "hp": f"{hp_current}/{hp_max}", + "mp": f"{mp_current}/{mp_max}", + "fp": f"{fp_current}/{fp_max}", + "exp": f"{exp_val:.4f}" + } + + counter += 1 + time.sleep(0.5) + +@app.route('/data') +def get_data(): + """Return the latest OCR data as JSON.""" + return jsonify(latest_data) + +if __name__ == '__main__': + # Start the OCR collector in a background thread. + collector_thread = threading.Thread(target=ocr_collector) + collector_thread.daemon = True + collector_thread.start() + + # Run the Flask app on localhost:5000. + app.run(host="127.0.0.1", port=5000) diff --git a/flow_UI.py b/flow_UI.py new file mode 100644 index 0000000..557ce77 --- /dev/null +++ b/flow_UI.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +""" +Modified Flow UI Application: +- The character node is now named "Character Status" (display title). +- DisplayValueNodes dynamically update their title to show the value from the connected output port. +- Newly spawned DisplayValueNodes (via the context menu) follow the same dynamic behavior. +- Node creation strings now include the class name to match NodeGraphQt requirements. +""" + +import sys +import requests +from Qt import QtWidgets, QtCore, QtGui +from NodeGraphQt import NodeGraph, BaseNode + +# ----------------------------------------------------------------------------- +# Custom Port Painter Function for Character Status Node Ports (Semi-Transparent) +# ----------------------------------------------------------------------------- +def get_draw_stat_port(color, border_color=None, alpha=127): + """ + Returns a custom port painter function that draws a circular port with a semi-transparent fill, + then draws text (port label and current value) next to it. + + Args: + color (tuple): RGB tuple for the fill color. + border_color (tuple, optional): RGB tuple for the border color. + Defaults to the same as color if not provided. + alpha (int, optional): Alpha value (0-255) for transparency. Default is 127. + """ + if border_color is None: + border_color = color + + def painter_func(painter, rect, info): + painter.save() + # --- Section: Port Drawing --- + pen = QtGui.QPen(QtGui.QColor(*border_color)) + pen.setWidth(1.8) + painter.setPen(pen) + # Create semi-transparent fill using the provided alpha value. + semi_transparent_color = QtGui.QColor(color[0], color[1], color[2], alpha) + painter.setBrush(semi_transparent_color) + painter.drawEllipse(rect) + # --- Section: Port Label and Value Drawing --- + port = info.get('port') + if port is not None: + node = port.node() # Parent node (e.g. Character Status) + stat = port.name() # Port label (e.g. "HP") + # Retrieve the current value from the node's values dict (if available) + value = node.values.get(stat, "N/A") if hasattr(node, 'values') else "N/A" + # Create a text rectangle to the right of the port. + text_rect = rect.adjusted(rect.width() + 4, 0, rect.width() + 70, 0) + painter.setPen(QtGui.QColor(0, 0, 0)) + painter.drawText(text_rect, QtCore.Qt.AlignVCenter | QtCore.Qt.AlignLeft, + f"{stat}: {value}") + painter.restore() + return painter_func + +# ----------------------------------------------------------------------------- +# Custom Node Classes +# ----------------------------------------------------------------------------- + +class CharacterStatusNode(BaseNode): + """ + Character Status Node + + This node represents the character's current status. + It has no input ports and four output ports: "HP", "MP", "FP", and "EXP". + The current values are stored in self.values and are drawn using a custom painter. + """ + __identifier__ = 'io.github.nicole.status' + NODE_NAME = 'Character Status' + + def __init__(self): + super(CharacterStatusNode, self).__init__() + # --- Section: Initialization of Stat Values and Ports --- + self.values = {"HP": "N/A", "MP": "N/A", "FP": "N/A", "EXP": "N/A"} + self.add_output("HP", painter_func=get_draw_stat_port((255, 0, 0))) # Red for HP + self.add_output("MP", painter_func=get_draw_stat_port((0, 0, 255))) # Blue for MP + self.add_output("FP", painter_func=get_draw_stat_port((0, 255, 0))) # Green for FP + self.add_output("EXP", painter_func=get_draw_stat_port((127, 255, 212))) # Aquamarine for EXP + # Set the node title to "Character Status" + self.set_name("Character Status") + + def update_value(self, stat, value): + """ + Update the value for a given stat. + + Args: + stat (str): Stat name (e.g., "HP", "MP", etc.). + value (str): New value for the stat. + """ + self.values[stat] = value + +class DisplayValueNode(BaseNode): + """ + Display Value Node + + This node displays a single input value. + Its title is updated to show the value from the connected output port. + If no port is connected, it displays "Display Value: No Input Configured". + """ + __identifier__ = 'io.github.nicole.display' + NODE_NAME = 'Display Value Node' + + def __init__(self): + super(DisplayValueNode, self).__init__() + # --- Section: Initialization and Default State --- + self.add_input("in") + self.add_output("out") + self.value = "No Input Configured" + self.update_display() + + def update_display(self): + """ + Update the node's title to reflect the current displayed value. + """ + self.set_name(f"Display Value: {self.value}") + + def process_input(self): + """ + Processes the input connection: + - If connected, retrieve the value from the connected node's output port. + - If not, set the value to "No Input Configured". + """ + input_port = self.input(0) + connections = input_port.connections() if input_port is not None else [] + if connections: + # --- Section: Processing Connected Input --- + connection = connections[0] + out_port = connection.out_port() + if out_port: + parent_node = out_port.node() + port_name = out_port.name() + self.value = parent_node.values.get(port_name, "N/A") + else: + self.value = "No Input Configured" + else: + # --- Section: No Connection Found --- + self.value = "No Input Configured" + self.update_display() + +# ----------------------------------------------------------------------------- +# API Polling Functions +# ----------------------------------------------------------------------------- +def poll_api(): + """ + Polls the Flask API for JSON data. + Expected JSON format: {"hp": "150/150", "mp": "75/100", "fp": "20/20", "exp": "10.0000"} + + Returns: + dict: The JSON data, or None on error. + """ + api_url = "http://127.0.0.1:5000/data" + try: + response = requests.get(api_url, timeout=1) + if response.status_code == 200: + return response.json() + else: + print("API error: status code", response.status_code) + except Exception as e: + print("Error polling API:", e) + return None + +def update_nodes(graph, character_status_node): + """ + Polls the API, updates the Character Status node's values, + and then updates all DisplayValueNodes in the graph. + + Args: + graph (NodeGraph): The NodeGraphQt graph instance. + character_status_node (CharacterStatusNode): The character status node. + """ + data = poll_api() + if data is None: + return + # --- Section: Update Character Status Node with Latest Data --- + character_status_node.update_value("HP", data.get('hp', "N/A")) + character_status_node.update_value("MP", data.get('mp', "N/A")) + character_status_node.update_value("FP", data.get('fp', "N/A")) + character_status_node.update_value("EXP", data.get('exp', "N/A")) + + # --- Section: Update All DisplayValueNodes --- + for node in graph.all_nodes(): + if hasattr(node, "process_input"): + node.process_input() + +# ----------------------------------------------------------------------------- +# Context Menu: Add DisplayValueNode Command +# ----------------------------------------------------------------------------- +def add_display_node(graph): + """ + Adds a new DisplayValueNode to the graph at a default position. + + Args: + graph (NodeGraph): The NodeGraphQt graph instance. + """ + # Use NodeGraphQt's create_node() with the correct node type string. + new_node = graph.create_node('io.github.nicole.display.DisplayValueNode', + name='Display Value Node', pos=[400, 300]) + print("Added a new Display Value node.") + +# ----------------------------------------------------------------------------- +# Main Application +# ----------------------------------------------------------------------------- +if __name__ == '__main__': + app = QtWidgets.QApplication([]) + + # --- Section: Setup NodeGraphQT --- + graph = NodeGraph() + graph.widget.setWindowTitle("Project Borealis - Flyff Information Overlay") + # Register nodes using their classes. + graph.register_node(CharacterStatusNode) + graph.register_node(DisplayValueNode) + + # --- Section: Setup Context Menu for Adding DisplayValueNodes --- + graph_context_menu = graph.get_context_menu('graph') + graph_context_menu.add_command( + 'Add Display Value Node', + lambda: add_display_node(graph), + shortcut='Ctrl+D' + ) + + # --- Section: Create and Connect Nodes --- + # Create the Character Status node. + character_status_node = graph.create_node('io.github.nicole.status.CharacterStatusNode', + name='Character Status', pos=[0, 0]) + # Create a Display Value node. + display_node = graph.create_node('io.github.nicole.display.DisplayValueNode', + name='Display Value Node', pos=[500, 200]) + # Connect the EXP output (index 3) from the Character Status node to the display node's input. + character_status_node.set_output(3, display_node.input(0)) + + # --- Section: Timer Setup for API Polling and Node Updates --- + timer = QtCore.QTimer() + timer.timeout.connect(lambda: update_nodes(graph, character_status_node)) + timer.start(500) + + graph.widget.resize(1000, 600) + graph.widget.show() + app.exec_()