From 0692005760d62d818fbac5bfa93a6616074b9c18 Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Fri, 28 Mar 2025 21:14:53 -0600 Subject: [PATCH] Initial Commit Legacy Code Removed --- .../Transparent Nodes/QML/blueprint_grid.qml | 78 --- .../Transparent Nodes/blueprint_grid.py | 193 ------- .../Transparent Nodes/borealis_transparent.py | 160 ------ Data/Experiments/borealis_overlay.py | 542 ------------------ Data/Experiments/flowpipe.py | 80 --- Data/Experiments/gui_elements.py | 98 ---- Data/Modules/data_collector.py | 398 ------------- Data/Modules/data_manager.py | 156 ----- Data/Nodes/Experimental/blueprint_node.py | 38 -- Data/Nodes/Flyff/Resources/bars_template.png | Bin 5684 -> 0 bytes Data/Nodes/Flyff/flyff_EXP_current.py | 50 -- Data/Nodes/Flyff/flyff_FP_current.py | 93 --- Data/Nodes/Flyff/flyff_FP_total.py | 93 --- Data/Nodes/Flyff/flyff_HP_current.py | 112 ---- Data/Nodes/Flyff/flyff_HP_total.py | 93 --- Data/Nodes/Flyff/flyff_MP_current.py | 93 --- Data/Nodes/Flyff/flyff_MP_total.py | 93 --- .../Flyff/flyff_character_status_node.py | 129 ----- .../Flyff/flyff_leveling_predictor_node.py | 141 ----- .../Flyff/flyff_low_health_alert_node.py | 134 ----- .../Flyff/flyff_mob_identification_overlay.py | 103 ---- Data/Nodes/General Purpose/array_node.py | 49 -- Data/Nodes/General Purpose/comparison_node.py | 122 ---- Data/Nodes/General Purpose/data_node.py | 72 --- .../General Purpose/math_operation_node.py | 109 ---- Data/Nodes/Organization/backdrop_node.py | 161 ------ Data/Nodes/Reporting/Export_to_CSV.py | 3 - Data/Nodes/Reporting/Export_to_Image.py | 4 - Data/Nodes/__init__.py | 0 .../Flyff/Flyff - Low Health Alert.json | 379 ------------ Data/Workflows/Flyff/Flyff EXP Predictor.json | 183 ------ .../Testing/Basic_Data_Node_Connection.json | 101 ---- .../Testing/Identification_Overlay.json | 57 -- Data/borealis.py | 440 -------------- Launch-Borealis-Legacy.ps1 | 50 -- 35 files changed, 4607 deletions(-) delete mode 100644 Data/Experiments/Transparent Nodes/QML/blueprint_grid.qml delete mode 100644 Data/Experiments/Transparent Nodes/blueprint_grid.py delete mode 100644 Data/Experiments/Transparent Nodes/borealis_transparent.py delete mode 100644 Data/Experiments/borealis_overlay.py delete mode 100644 Data/Experiments/flowpipe.py delete mode 100644 Data/Experiments/gui_elements.py delete mode 100644 Data/Modules/data_collector.py delete mode 100644 Data/Modules/data_manager.py delete mode 100644 Data/Nodes/Experimental/blueprint_node.py delete mode 100644 Data/Nodes/Flyff/Resources/bars_template.png delete mode 100644 Data/Nodes/Flyff/flyff_EXP_current.py delete mode 100644 Data/Nodes/Flyff/flyff_FP_current.py delete mode 100644 Data/Nodes/Flyff/flyff_FP_total.py delete mode 100644 Data/Nodes/Flyff/flyff_HP_current.py delete mode 100644 Data/Nodes/Flyff/flyff_HP_total.py delete mode 100644 Data/Nodes/Flyff/flyff_MP_current.py delete mode 100644 Data/Nodes/Flyff/flyff_MP_total.py delete mode 100644 Data/Nodes/Flyff/flyff_character_status_node.py delete mode 100644 Data/Nodes/Flyff/flyff_leveling_predictor_node.py delete mode 100644 Data/Nodes/Flyff/flyff_low_health_alert_node.py delete mode 100644 Data/Nodes/Flyff/flyff_mob_identification_overlay.py delete mode 100644 Data/Nodes/General Purpose/array_node.py delete mode 100644 Data/Nodes/General Purpose/comparison_node.py delete mode 100644 Data/Nodes/General Purpose/data_node.py delete mode 100644 Data/Nodes/General Purpose/math_operation_node.py delete mode 100644 Data/Nodes/Organization/backdrop_node.py delete mode 100644 Data/Nodes/Reporting/Export_to_CSV.py delete mode 100644 Data/Nodes/Reporting/Export_to_Image.py delete mode 100644 Data/Nodes/__init__.py delete mode 100644 Data/Workflows/Flyff/Flyff - Low Health Alert.json delete mode 100644 Data/Workflows/Flyff/Flyff EXP Predictor.json delete mode 100644 Data/Workflows/Testing/Basic_Data_Node_Connection.json delete mode 100644 Data/Workflows/Testing/Identification_Overlay.json delete mode 100644 Data/borealis.py delete mode 100644 Launch-Borealis-Legacy.ps1 diff --git a/Data/Experiments/Transparent Nodes/QML/blueprint_grid.qml b/Data/Experiments/Transparent Nodes/QML/blueprint_grid.qml deleted file mode 100644 index f21574d..0000000 --- a/Data/Experiments/Transparent Nodes/QML/blueprint_grid.qml +++ /dev/null @@ -1,78 +0,0 @@ -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Shapes 1.15 -import QtQuick.Window 2.15 - -Item { - id: root - width: Screen.width - height: Screen.height - - // Grid overlay is enabled at startup. - property bool editMode: true - - // Blue gradient background (edges fading inward) with stops shifted inward. - Rectangle { - id: gradientBackground - width: parent.width - height: parent.height - opacity: 0.5 - gradient: Gradient { - // Shifted stops: outer stops moved to 0.1 and 0.9, inner stops to 0.4 and 0.6. - GradientStop { position: 0.1; color: Qt.rgba(0, 100/255, 255/255, 0.5) } - GradientStop { position: 0.4; color: Qt.rgba(0, 50/255, 180/255, 0.2) } - GradientStop { position: 0.5; color: Qt.rgba(0, 0, 0, 0.0) } - GradientStop { position: 0.6; color: Qt.rgba(0, 50/255, 180/255, 0.2) } - GradientStop { position: 0.9; color: Qt.rgba(0, 100/255, 255/255, 0.5) } - } - visible: editMode // Only show the gradient in edit mode - } - - // Top & Bottom fade remains unchanged. - Rectangle { - id: topBottomGradient - width: parent.width - height: parent.height - opacity: 0.3 - gradient: Gradient { - orientation: Gradient.Vertical - GradientStop { position: 0.0; color: Qt.rgba(0, 100/255, 255/255, 0.4) } - GradientStop { position: 0.3; color: Qt.rgba(0, 50/255, 180/255, 0.1) } - GradientStop { position: 0.5; color: Qt.rgba(0, 0, 0, 0.0) } - GradientStop { position: 0.7; color: Qt.rgba(0, 50/255, 180/255, 0.1) } - GradientStop { position: 1.0; color: Qt.rgba(0, 100/255, 255/255, 0.4) } - } - visible: editMode - } - - // Full-Screen Dynamic Grid with 10% increased transparency (grid lines at 0.3 opacity). - Canvas { - id: gridCanvas - width: parent.width - height: parent.height - onPaint: { - var ctx = getContext("2d"); - ctx.clearRect(0, 0, width, height); - ctx.strokeStyle = "rgba(255, 255, 255, 0.3)"; // Reduced opacity from 0.4 to 0.3. - ctx.lineWidth = 1; - - var step = 120; // Grid spacing remains unchanged. - - for (var x = 0; x < width; x += step) { - ctx.beginPath(); - ctx.moveTo(x, 0); - ctx.lineTo(x, height); - ctx.stroke(); - } - for (var y = 0; y < height; y += step) { - ctx.beginPath(); - ctx.moveTo(0, y); - ctx.lineTo(width, y); - ctx.stroke(); - } - } - Component.onCompleted: requestPaint() - onVisibleChanged: requestPaint() - visible: editMode // Hide when edit mode is off. - } -} diff --git a/Data/Experiments/Transparent Nodes/blueprint_grid.py b/Data/Experiments/Transparent Nodes/blueprint_grid.py deleted file mode 100644 index 4c029ba..0000000 --- a/Data/Experiments/Transparent Nodes/blueprint_grid.py +++ /dev/null @@ -1,193 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import sys -import pkgutil -import importlib -import inspect -import types -from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QWidget -from PyQt5.QtCore import Qt, QUrl, QTimer -from PyQt5.QtGui import QGuiApplication -from PyQt5.QtQuick import QQuickView - -# OdenGraphQt Fix: Monkey-patch QUndoStack -import OdenGraphQt.base.graph as base_graph -from PyQt5 import QtWidgets -base_graph.QtGui.QUndoStack = QtWidgets.QUndoStack - -import OdenGraphQt.base.commands as base_commands -_original_redo = base_commands.NodesRemovedCmd.redo -_original_undo = base_commands.NodesRemovedCmd.undo - -def _patched_redo(self): - try: - _original_redo(self) - except TypeError as e: - if "unexpected type" in str(e) and hasattr(self, 'node'): - node_ids = [] - if isinstance(self.node, list): - node_ids = [getattr(n, 'id', str(n)) for n in self.node] - else: - node_ids = [getattr(self.node, 'id', str(self.node))] - self.graph.nodes_deleted.emit(node_ids) - else: - raise - -def _patched_undo(self): - try: - _original_undo(self) - except TypeError as e: - if "unexpected type" in str(e) and hasattr(self, 'node'): - node_ids = [] - if isinstance(self.node, list): - node_ids = [getattr(n, 'id', str(n)) for n in self.node] - else: - node_ids = [getattr(self.node, 'id', str(self.node))] - self.graph.nodes_deleted.emit(node_ids) - else: - raise - -base_commands.NodesRemovedCmd.redo = _patched_redo -base_commands.NodesRemovedCmd.undo = _patched_undo - -# OdenGraphQt Transparent Viewer -from OdenGraphQt.widgets.viewer import NodeViewer - -class TransparentViewer(NodeViewer): - """A NodeViewer that does not paint anything in drawBackground() -> Fully transparent.""" - def drawBackground(self, painter, rect): - pass # Do nothing, ensuring transparency. - -# NodeGraph & Node Import Helpers -from OdenGraphQt import NodeGraph, BaseNode - -def import_nodes_from_folder(package_name): - imported_nodes = [] - package = importlib.import_module(package_name) - for loader, module_name, is_pkg in pkgutil.walk_packages( - package.__path__, package.__name__ + "."): - module = importlib.import_module(module_name) - for name, obj in inspect.getmembers(module, inspect.isclass): - if issubclass(obj, BaseNode) and obj.__module__ == module.__name__: - imported_nodes.append(obj) - return imported_nodes - -def make_node_command(graph, node_type): - def command(): - try: - graph.create_node(node_type) - except Exception as e: - print(f"Error creating node of type {node_type}: {e}") - return command - -# Edit Mode Button -class EditButton(QPushButton): - """A small, frameless button to toggle edit mode.""" - def __init__(self, parent=None): - super().__init__("Toggle Edit Mode", parent) - self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) - # Dark gray background with white text. - self.setStyleSheet("background-color: #444444; border: 1px solid black; color: white;") - self.resize(140, 40) - -# Main Overlay Window -class MainWindow(QMainWindow): - """A frameless, transparent overlay with OdenGraphQt nodes & edit mode toggle.""" - def __init__(self): - super().__init__() - - # Full-screen overlay - app = QApplication.instance() - screen_geo = app.primaryScreen().geometry() - self.setGeometry(screen_geo) - - # Frameless, top-most, fully transparent - self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) - self.setAttribute(Qt.WA_TranslucentBackground, True) - - # QML Background - self.qml_view = QQuickView() - self.qml_view.setSource(QUrl("qml/background_grid.qml")) - self.qml_view.setFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) - self.qml_view.setClearBeforeRendering(True) - self.qml_view.setColor(Qt.transparent) - self.qml_view.show() - - # Save the QML root object for later property sync - self.qml_root = self.qml_view.rootObject() - - # NodeGraph with TransparentViewer - self.graph = NodeGraph(viewer=TransparentViewer()) - self.nodeGraphWidget = self.graph.widget - self.nodeGraphWidget.setStyleSheet("background: transparent; border: none;") - - # Transparent central widget - central = QWidget(self) - central.setAttribute(Qt.WA_TranslucentBackground, True) - self.setCentralWidget(central) - - self.nodeGraphWidget.setParent(central) - self.nodeGraphWidget.setGeometry(central.rect()) - - # Edit Mode Button (Python controlled) - self.editButton = EditButton(self) - self.editButton.move(10, 10) - self.editButton.clicked.connect(self.toggleEditMode) - self.isEditMode = True # Set edit mode enabled by default - - # Ensure QML grid overlay is enabled at startup - if self.qml_root: - self.qml_root.setProperty("editMode", self.isEditMode) - - # Import custom nodes - try: - custom_nodes = import_nodes_from_folder('Nodes') - for node_class in custom_nodes: - self.graph.register_node(node_class) - - graph_menu = self.graph.get_context_menu('graph') - for node_class in custom_nodes: - node_type = f"{node_class.__identifier__}.{node_class.__name__}" - node_name = node_class.NODE_NAME - graph_menu.add_command( - f"Add {node_name}", - make_node_command(self.graph, node_type) - ) - except Exception as e: - print(f"Error setting up custom nodes: {e}") - - # Global update timer - self.timer = QTimer(self) - self.timer.timeout.connect(self.global_update) - self.timer.start(500) - - # Timer to ensure the button stays on top (hacky, but effective) - self.raiseTimer = QTimer(self) - self.raiseTimer.timeout.connect(self.editButton.raise_) - self.raiseTimer.start(1000) # Raise the button every 1 second - - self.show() - self.nodeGraphWidget.setAttribute(Qt.WA_TransparentForMouseEvents, not self.isEditMode) - - def toggleEditMode(self): - """Toggle edit mode (pass-through clicks vs interactive).""" - self.isEditMode = not self.isEditMode - self.nodeGraphWidget.setAttribute(Qt.WA_TransparentForMouseEvents, not self.isEditMode) - # Button text remains constant. - self.editButton.setText("Toggle Edit Mode") - if self.qml_root: - self.qml_root.setProperty("editMode", self.isEditMode) - - def global_update(self): - """Update all nodes periodically.""" - for node in self.graph.all_nodes(): - if hasattr(node, "process_input"): - node.process_input() - -# Entry Point -if __name__ == '__main__': - app = QApplication(sys.argv) - window = MainWindow() - window.show() - sys.exit(app.exec_()) \ No newline at end of file diff --git a/Data/Experiments/Transparent Nodes/borealis_transparent.py b/Data/Experiments/Transparent Nodes/borealis_transparent.py deleted file mode 100644 index 0560967..0000000 --- a/Data/Experiments/Transparent Nodes/borealis_transparent.py +++ /dev/null @@ -1,160 +0,0 @@ -import sys -import pkgutil -import importlib -import inspect -from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QGraphicsView, QGraphicsScene, QGraphicsItem, QMenu -from PyQt5.QtCore import Qt, QTimer, QRectF, QPointF -from PyQt5.QtGui import QColor, QPainter, QPen, QBrush, QGradient, QLinearGradient -from PyQt5 import QtWidgets, QtCore, QtGui -from OdenGraphQt import NodeGraph, BaseNode - -# --- Fix Missing QUndoStack in QtGui --- -import OdenGraphQt.base.graph as base_graph -base_graph.QtGui.QUndoStack = QtWidgets.QUndoStack # Monkey-patch the missing QUndoStack - -# --- Custom Graph Scene --- -class CustomGraphScene(QGraphicsScene): - """ - Custom scene that draws a blueprint-style transparent grid with gradient shading. - """ - def __init__(self, parent=None): - super().__init__(parent) - self.setBackgroundBrush(QtCore.Qt.transparent) - self.grid_color = QtGui.QColor(100, 160, 160, 160) # Blueprint grid color (10% more transparent) - self.grid_size = 115 - - def drawBackground(self, painter, rect): - """ - Custom draw function to render a blueprint-style grid with gradient shading. - """ - painter.save() - painter.setRenderHint(QPainter.Antialiasing, False) - painter.setBrush(QtCore.Qt.NoBrush) # No background fill - pen = QPen(self.grid_color, 0.5) - - left = int(rect.left()) - (int(rect.left()) % self.grid_size) - top = int(rect.top()) - (int(rect.top()) % self.grid_size) - - # Draw vertical lines - lines = [] - for x in range(left, int(rect.right()), self.grid_size): - lines.append(QtCore.QLineF(x, rect.top(), x, rect.bottom())) - - # Draw horizontal lines - for y in range(top, int(rect.bottom()), self.grid_size): - lines.append(QtCore.QLineF(rect.left(), y, rect.right(), y)) - - painter.setPen(pen) - painter.drawLines(lines) - - # Draw gradient shading (top and bottom) - gradient = QLinearGradient(QPointF(rect.left(), rect.top()), QPointF(rect.left(), rect.bottom())) - gradient.setColorAt(0.0, QColor(0, 40, 100, 220)) # Darker blue at the top - gradient.setColorAt(0.5, QColor(0, 0, 0, 0)) # Transparent in the middle - gradient.setColorAt(1.0, QColor(0, 40, 100, 220)) # Darker blue at the bottom - painter.fillRect(rect, QBrush(gradient)) - - painter.restore() - -# --- Node Management --- -def import_nodes_from_folder(package_name): - imported_nodes = [] - package = importlib.import_module(package_name) - for loader, module_name, is_pkg in pkgutil.walk_packages(package.__path__, package.__name__ + "."): - module = importlib.import_module(module_name) - for name, obj in inspect.getmembers(module, inspect.isclass): - if issubclass(obj, BaseNode) and obj.__module__ == module.__name__: - imported_nodes.append(obj) - return imported_nodes - -# --- Custom Graph View --- -class CustomGraphView(QGraphicsView): - """ - Custom view for the graph that applies full transparency and handles right-click context menu. - """ - def __init__(self, scene, graph, parent=None): - super().__init__(scene, parent) - self.graph = graph # Reference to NodeGraph - self.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform) - self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate) - self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.setStyleSheet("background: transparent; border: none;") - self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) - - # Enable context menu on right-click - self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - self.customContextMenuRequested.connect(self.show_context_menu) - - def show_context_menu(self, position): - """ - Displays the node creation context menu with dynamically loaded nodes. - """ - menu = QMenu() - for node_class in self.graph.registered_nodes(): - node_name = getattr(node_class, "NODE_NAME", node_class.__name__) - menu.addAction(f"Create {node_name}", lambda nc=node_class: self.create_node(nc)) - menu.exec_(self.mapToGlobal(position)) - - def create_node(self, node_class): - """ - Creates a node instance of the given class in the NodeGraph. - """ - try: - node = self.graph.create_node(f"{node_class.__identifier__}.{node_class.__name__}") - print(f"Created node: {node_class.__name__}") - except Exception as e: - print(f"Error creating node: {e}") - -# --- Main Window --- -class MainWindow(QMainWindow): - """A frameless, transparent overlay with a custom graph.""" - def __init__(self): - super().__init__() - - # Full-screen overlay - app = QApplication.instance() - screen_geo = app.primaryScreen().geometry() - self.setGeometry(screen_geo) - - # Frameless, top-most, fully transparent - self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) - self.setAttribute(Qt.WA_TranslucentBackground, True) - - # Transparent central widget - central = QWidget(self) - central.setAttribute(Qt.WA_TranslucentBackground, True) - layout = QVBoxLayout(central) - layout.setContentsMargins(0, 0, 0, 0) - self.setCentralWidget(central) - - # Initialize NodeGraph - self.graph = NodeGraph() - - # Load custom nodes - custom_nodes = import_nodes_from_folder('Nodes') - for node_class in custom_nodes: - self.graph.register_node(node_class) - - # Initialize Custom Graph Scene & View - self.scene = CustomGraphScene() - self.view = CustomGraphView(self.scene, self.graph, self) - layout.addWidget(self.view) - - # Global update timer - self.timer = QTimer(self) - self.timer.timeout.connect(self.global_update) - self.timer.start(500) - - def global_update(self): - """Update all nodes periodically.""" - for node in self.graph.all_nodes(): - if hasattr(node, "process_input"): - node.process_input() - -# --- Entry Point --- -if __name__ == '__main__': - app = QApplication(sys.argv) - window = MainWindow() - window.show() - sys.exit(app.exec_()) diff --git a/Data/Experiments/borealis_overlay.py b/Data/Experiments/borealis_overlay.py deleted file mode 100644 index 74b9d8f..0000000 --- a/Data/Experiments/borealis_overlay.py +++ /dev/null @@ -1,542 +0,0 @@ -#!/usr/bin/env python3 - -import sys -import time -import re -import numpy as np -import cv2 -import pytesseract - -try: - import winsound - HAS_WINSOUND = True -except ImportError: - HAS_WINSOUND = False - -from PyQt5.QtWidgets import QApplication, QWidget -from PyQt5.QtCore import Qt, QRect, QPoint, QTimer -from PyQt5.QtGui import QPainter, QPen, QColor, QFont -from PIL import Image, ImageGrab, ImageFilter - -from rich.console import Console, Group -from rich.table import Table -from rich.progress import Progress, BarColumn, TextColumn -from rich.text import Text -from rich.live import Live - -# ============================================================================= -# Global Config -# ============================================================================= - -pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe" - -POLLING_RATE_MS = 500 -MAX_DATA_POINTS = 8 - -# We still use these defaults for Region size. -DEFAULT_WIDTH = 180 -DEFAULT_HEIGHT = 130 -HANDLE_SIZE = 8 -LABEL_HEIGHT = 20 - -GREEN_HEADER_STYLE = "bold green" - -BEEP_INTERVAL_SECONDS = 1.0 # Only beep once every 1 second - -# STATUS BAR AUTO-LOCATOR LOGIC (WILL BE BUILT-OUT TO BE MORE ROBUST LATER) -TEMPLATE_PATH = "G:\\Nextcloud\\Projects\\Scripting\\bars_template.png" # Path to your bars template file -MATCH_THRESHOLD = 0.4 # The correlation threshold to consider a "good" match - -# ============================================================================= -# Helper Functions -# ============================================================================= - -def beep_hp_warning(): - """ - Only beep if enough time has elapsed since the last beep (BEEP_INTERVAL_SECONDS). - """ - current_time = time.time() - if (beep_hp_warning.last_beep_time is None or - (current_time - beep_hp_warning.last_beep_time >= BEEP_INTERVAL_SECONDS)): - - beep_hp_warning.last_beep_time = current_time - if HAS_WINSOUND: - # frequency=376 Hz, duration=100 ms - winsound.Beep(376, 100) - else: - # Attempt terminal bell - print('\a', end='') - -beep_hp_warning.last_beep_time = None - - -def locate_bars_opencv(template_path, threshold=MATCH_THRESHOLD): - """ - Attempt to locate the bars via OpenCV template matching: - 1) Grab the full screen using PIL.ImageGrab. - 2) Convert to NumPy array in BGR format for cv2. - 3) Load template from `template_path`. - 4) Use cv2.matchTemplate to find the best match location. - 5) If max correlation > threshold, return (x, y, w, h). - 6) Else return None. - """ - # 1) Capture full screen - screenshot_pil = ImageGrab.grab() - screenshot_np = np.array(screenshot_pil) # shape (H, W, 4) possibly - # Convert RGBA or RGB to BGR - screenshot_bgr = cv2.cvtColor(screenshot_np, cv2.COLOR_RGB2BGR) - - # 2) Load template from file - template_bgr = cv2.imread(template_path, cv2.IMREAD_COLOR) - if template_bgr is None: - print(f"[WARN] Could not load template file: {template_path}") - return None - - # 3) Template matching - result = cv2.matchTemplate(screenshot_bgr, template_bgr, cv2.TM_CCOEFF_NORMED) - - # 4) Find best match - min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result) - # template width/height - th, tw, _ = template_bgr.shape - - if max_val >= threshold: - # max_loc is top-left corner of the best match - found_x, found_y = max_loc - return (found_x, found_y, tw, th) - else: - return None - - -def format_duration(seconds): - if seconds is None: - return "???" - seconds = int(seconds) - hours = seconds // 3600 - leftover = seconds % 3600 - mins = leftover // 60 - secs = leftover % 60 - if hours > 0: - return f"{hours}h {mins}m {secs}s" - else: - return f"{mins}m {secs}s" - - -def sanitize_experience_string(raw_text): - text_no_percent = raw_text.replace('%', '') - text_no_spaces = text_no_percent.replace(' ', '') - cleaned = re.sub(r'[^0-9\.]', '', text_no_spaces) - match = re.search(r'\d+(?:\.\d+)?', cleaned) - if not match: - return None - val = float(match.group(0)) - if val < 0: - val = 0 - elif val > 100: - val = 100 - return round(val, 4) - - -def format_experience_value(value): - if value < 0: - value = 0 - elif value > 100: - value = 100 - float_4 = round(value, 4) - raw_str = f"{float_4:.4f}" - int_part, dec_part = raw_str.split('.') - if int_part == "100": - pass - elif len(int_part) == 1 and int_part != "0": - int_part = "0" + int_part - elif int_part == "0": - int_part = "00" - return f"{int_part}.{dec_part}" - -# ----------------------------------------------------------------------------- -# Region Class -# ----------------------------------------------------------------------------- -class Region: - """ - Defines a draggable/resizable screen region for OCR capture. - """ - def __init__(self, x, y, label="Region", color=QColor(0,0,255)): - self.x = x - self.y = y - self.w = DEFAULT_WIDTH - self.h = DEFAULT_HEIGHT - self.label = label - self.color = color - self.visible = True - self.data = "" - - def rect(self): - return QRect(self.x, self.y, self.w, self.h) - - def label_rect(self): - return QRect(self.x, self.y - LABEL_HEIGHT, self.w, LABEL_HEIGHT) - - def resize_handles(self): - return [ - QRect(self.x - HANDLE_SIZE // 2, self.y - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE), - QRect(self.x + self.w - HANDLE_SIZE // 2, self.y - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE), - QRect(self.x - HANDLE_SIZE // 2, self.y + self.h - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE), - QRect(self.x + self.w - HANDLE_SIZE // 2, self.y + self.h - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE), - ] - -# ----------------------------------------------------------------------------- -# OverlayCanvas Class -# ----------------------------------------------------------------------------- -class OverlayCanvas(QWidget): - """ - Renders the overlay & handles region dragging/resizing. - """ - def __init__(self, regions, parent=None): - super().__init__(parent) - self.regions = regions - self.edit_mode = True - self.selected_region = None - self.selected_handle = None - self.drag_offset = QPoint() - - def paintEvent(self, event): - painter = QPainter(self) - for region in self.regions: - if region.visible: - pen = QPen(region.color) - pen.setWidth(3) - painter.setPen(pen) - painter.drawRect(region.x, region.y, region.w, region.h) - - painter.setFont(QFont("Arial", 12, QFont.Bold)) - painter.setPen(region.color) - painter.drawText(region.x, region.y - 5, region.label) - - if self.edit_mode: - for handle in region.resize_handles(): - painter.fillRect(handle, region.color) - - def mousePressEvent(self, event): - if not self.edit_mode: - return - if event.button() == Qt.LeftButton: - for region in reversed(self.regions): - for i, handle in enumerate(region.resize_handles()): - if handle.contains(event.pos()): - self.selected_region = region - self.selected_handle = i - return - if region.label_rect().contains(event.pos()): - self.selected_region = region - self.selected_handle = None - self.drag_offset = event.pos() - QPoint(region.x, region.y) - return - if region.rect().contains(event.pos()): - self.selected_region = region - self.selected_handle = None - self.drag_offset = event.pos() - QPoint(region.x, region.y) - return - - def mouseMoveEvent(self, event): - if not self.edit_mode or self.selected_region is None: - return - - if self.selected_handle is None: - self.selected_region.x = event.x() - self.drag_offset.x() - self.selected_region.y = event.y() - self.drag_offset.y() - else: - sr = self.selected_region - if self.selected_handle == 0: # top-left - sr.w += sr.x - event.x() - sr.h += sr.y - event.y() - sr.x = event.x() - sr.y = event.y() - elif self.selected_handle == 1: # top-right - sr.w = event.x() - sr.x - sr.h += sr.y - event.y() - sr.y = event.y() - elif self.selected_handle == 2: # bottom-left - sr.w += sr.x - event.x() - sr.h = event.y() - sr.y - sr.x = event.x() - elif self.selected_handle == 3: # bottom-right - sr.w = event.x() - sr.x - sr.h = event.y() - sr.y - - sr.w = max(sr.w, 10) - sr.h = max(sr.h, 10) - - self.update() - - def mouseReleaseEvent(self, event): - if not self.edit_mode: - return - if event.button() == Qt.LeftButton: - self.selected_region = None - self.selected_handle = None - -# ----------------------------------------------------------------------------- -# BorealisOverlay Class -# ----------------------------------------------------------------------------- -class BorealisOverlay(QWidget): - """ - Single Region Overlay for Player Stats (HP/MP/FP/EXP) with: - - Automatic location via OpenCV template matching at startup - - OCR scanning - - Low-HP beep - - Rich Live updates in terminal - """ - def __init__(self, live=None): - super().__init__() - screen_geo = QApplication.primaryScreen().geometry() - self.setGeometry(screen_geo) - self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) - self.setAttribute(Qt.WA_TranslucentBackground, True) - - # Try to find the bars automatically - # If found => use that location, else default - initial_x, initial_y = 250, 50 - region_w, region_h = DEFAULT_WIDTH, DEFAULT_HEIGHT - - match_result = locate_bars_opencv(TEMPLATE_PATH, MATCH_THRESHOLD) - if match_result is not None: - found_x, found_y, w, h = match_result - print(f"Character Status Located at {found_x}, {found_y} with confidence >= {MATCH_THRESHOLD}.") - initial_x, initial_y = found_x, found_y - # Optionally override region size with template size - region_w, region_h = w, h - else: - print("Could not auto-locate the character status page. Set your theme to Masquerade and Interface Scale to 140%, and browser zoom level to 110%. Using default region.") - - region = Region(initial_x, initial_y, label="Character Status") - region.w = region_w - region.h = region_h - self.regions = [region] - - self.canvas = OverlayCanvas(self.regions, self) - self.canvas.setGeometry(self.rect()) - - # Tesseract - self.engine = pytesseract - - # Keep history of EXP data - self.points = [] - - self.live = live - - # Timer for periodic OCR scanning - self.timer = QTimer(self) - self.timer.timeout.connect(self.collect_ocr_data) - self.timer.start(POLLING_RATE_MS) - - def set_live(self, live): - self.live = live - - def collect_ocr_data(self): - for region in self.regions: - if region.visible: - screenshot = ImageGrab.grab( - bbox=(region.x, region.y, region.x + region.w, region.y + region.h) - ) - processed = self.preprocess_image(screenshot) - text = pytesseract.image_to_string(processed, config='--psm 4 --oem 1') - region.data = text.strip() - - if self.live is not None: - renderable = self.build_renderable() - self.live.update(renderable) - - def preprocess_image(self, image): - gray = image.convert("L") - scaled = gray.resize((gray.width * 3, gray.height * 3)) - thresh = scaled.point(lambda p: p > 200 and 255) - return thresh.filter(ImageFilter.MedianFilter(3)) - - def parse_all_stats(self, raw_text): - raw_lines = raw_text.splitlines() - lines = [l.strip() for l in raw_lines if l.strip()] - stats_dict = { - "hp": (0,1), - "mp": (0,1), - "fp": (0,1), - "exp": None - } - if len(lines) < 4: - return stats_dict - - hp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[0]) - if hp_match: - stats_dict["hp"] = (int(hp_match.group(1)), int(hp_match.group(2))) - - mp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[1]) - if mp_match: - stats_dict["mp"] = (int(mp_match.group(1)), int(mp_match.group(2))) - - fp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[2]) - if fp_match: - stats_dict["fp"] = (int(fp_match.group(1)), int(fp_match.group(2))) - - exp_val = sanitize_experience_string(lines[3]) - stats_dict["exp"] = exp_val - return stats_dict - - def update_points(self, new_val): - now = time.time() - if self.points: - _, last_v = self.points[-1] - if abs(new_val - last_v) < 1e-6: - return - if new_val < last_v: - self.points.clear() - self.points.append((now, new_val)) - if len(self.points) > MAX_DATA_POINTS: - self.points.pop(0) - - def compute_time_to_100(self): - n = len(self.points) - if n < 2: - return None - first_t, first_v = self.points[0] - last_t, last_v = self.points[-1] - diff_v = last_v - first_v - if diff_v <= 0: - return None - - steps = n - 1 - total_time = last_t - first_t - if total_time <= 0: - return None - - avg_change = diff_v / steps - remain = 100.0 - last_v - if remain <= 0: - return None - - avg_time = total_time / steps - rate_per_s = avg_change / avg_time if avg_time > 0 else 0 - if rate_per_s <= 0: - return None - - return int(remain / rate_per_s) - - def build_renderable(self): - raw_text = self.regions[0].data - stats = self.parse_all_stats(raw_text) - hp_cur, hp_max = stats["hp"] - mp_cur, mp_max = stats["mp"] - fp_cur, fp_max = stats["fp"] - exp_val = stats["exp"] - - # HP beep logic - if hp_max > 0: - hp_ratio = hp_cur / hp_max - if 0 < hp_ratio <= 0.40: - beep_hp_warning() - - if exp_val is not None: - self.update_points(exp_val) - current_exp = self.points[-1][1] if self.points else 0.0 - - # Title - title_text = Text("Project Borealis\n", style="bold white") - subtitle_text = Text("Flyff Information Overlay\n\n", style="dim") - - # HP / MP / FP bars - bar_progress = Progress( - "{task.description}", - BarColumn(bar_width=30), - TextColumn(" {task.completed}/{task.total} ({task.percentage:>5.2f}%)"), - transient=False, - auto_refresh=False - ) - bar_progress.add_task("[bold red]HP[/bold red]", total=hp_max, completed=hp_cur, - style="red", complete_style="red") - bar_progress.add_task("[bold blue]MP[/bold blue]", total=mp_max, completed=mp_cur, - style="blue", complete_style="blue") - bar_progress.add_task("[bold green]FP[/bold green]", total=fp_max, completed=fp_cur, - style="green", complete_style="green") - bar_progress.refresh() - - # Historical EXP table - table = Table(show_header=True, header_style=GREEN_HEADER_STYLE, style=None) - table.add_column("Historical EXP", justify="center", style="green") - table.add_column("Time Since Last Kill", justify="center", style="green") - table.add_column("Average EXP Per Kill", justify="center", style="green") - table.add_column("Average Time Between Kills", justify="center", style="green") - - n = len(self.points) - if n == 0: - table.add_row("N/A", "N/A", "N/A", "N/A") - elif n == 1: - _, v0 = self.points[0] - exp_str = f"[green]{format_experience_value(v0)}%[/green]" - table.add_row(exp_str, "N/A", "N/A", "N/A") - else: - for i in range(1, n): - t_cur, v_cur = self.points[i] - t_prev, v_prev = self.points[i - 1] - delta_v = v_cur - v_prev - delta_str = f"{delta_v:+.4f}%" - exp_main = format_experience_value(v_cur) - exp_str = f"[green]{exp_main}%[/green] [dim]({delta_str})[/dim]" - - delta_t = t_cur - t_prev - t_since_str = f"{delta_t:.1f}s" - - diff_v = v_cur - self.points[0][1] - steps = i - avg_exp_str = f"{diff_v/steps:.4f}%" - - total_time = t_cur - self.points[0][0] - avg_kill_time = total_time / steps - avg_time_str = f"{avg_kill_time:.1f}s" - - table.add_row(exp_str, t_since_str, avg_exp_str, avg_time_str) - - # Predicted Time to Level - secs_left = self.compute_time_to_100() - time_str = format_duration(secs_left) - - time_bar = Progress( - TextColumn("[bold white]Predicted Time to Level:[/bold white] "), - BarColumn(bar_width=30, complete_style="magenta"), - TextColumn(" [green]{task.percentage:>5.2f}%[/green] "), - TextColumn(f"[magenta]{time_str}[/magenta] until 100%", justify="right"), - transient=False, - auto_refresh=False - ) - time_bar.add_task("", total=100, completed=current_exp) - time_bar.refresh() - - return Group( - title_text, - subtitle_text, - bar_progress, - table, - time_bar - ) - -# ----------------------------------------------------------------------------- -# main -# ----------------------------------------------------------------------------- -def main(): - """ - 1) Attempt to locate HP/MP/FP/Exp bars using OpenCV template matching. - 2) Position overlay region accordingly if found, else default. - 3) Start PyQt, periodically OCR the region, update Rich Live in terminal. - """ - app = QApplication(sys.argv) - window = BorealisOverlay() - window.setWindowTitle("Project Borealis Overlay (HP/MP/FP/EXP)") - window.show() - - console = Console() - - with Live(console=console, refresh_per_second=4) as live: - window.set_live(live) - exit_code = app.exec_() - - sys.exit(exit_code) - -if __name__ == "__main__": - main() diff --git a/Data/Experiments/flowpipe.py b/Data/Experiments/flowpipe.py deleted file mode 100644 index 47aedaf..0000000 --- a/Data/Experiments/flowpipe.py +++ /dev/null @@ -1,80 +0,0 @@ -from flask import Flask, jsonify -from flowpipe.node import Node -from flowpipe.graph import Graph -from flowpipe.plug import InputPlug, OutputPlug - -app = Flask(__name__) - -# =========================== -# Define Custom Nodes -# =========================== - -class MultiplyNode(Node): - """Multiplies an input value by a factor""" - factor = InputPlug() - value = InputPlug() - result = OutputPlug() - - def compute(self): - self.result.value = self.value.value * self.factor.value - - -class AddNode(Node): - """Adds two input values""" - input1 = InputPlug() - input2 = InputPlug() - sum = OutputPlug() - - def compute(self): - self.sum.value = self.input1.value + self.input2.value - - -class OutputNode(Node): - """Outputs the final result""" - input_value = InputPlug() - output_value = OutputPlug() - - def compute(self): - self.output_value.value = self.input_value.value - - -# =========================== -# Define Graph Workflow -# =========================== - -def create_workflow(): - """Creates a sample workflow using nodes""" - graph = Graph(name="Sample Workflow") - - # Create nodes - multiply = MultiplyNode(name="Multiplier", graph=graph) - add = AddNode(name="Adder", graph=graph) - output = OutputNode(name="Output", graph=graph) - - # Connect nodes - multiply.result.connect(add.input1) # Multiply output -> Add input1 - add.sum.connect(output.input_value) # Add output -> Output node - - # Set static input values - multiply.factor.value = 2 - multiply.value.value = 5 # 5 * 2 = 10 - add.input2.value = 3 # 10 + 3 = 13 - - return graph - - -@app.route('/run-workflow', methods=['GET']) -def run_workflow(): - """Runs the defined node-based workflow""" - graph = create_workflow() - graph.evaluate() # Execute the graph - - # Extract the final result from the output node - output_node = graph.nodes["Output"] - result = output_node.output_value.value - - return jsonify({"workflow_result": result}) - - -if __name__ == '__main__': - app.run(debug=True) diff --git a/Data/Experiments/gui_elements.py b/Data/Experiments/gui_elements.py deleted file mode 100644 index f65f513..0000000 --- a/Data/Experiments/gui_elements.py +++ /dev/null @@ -1,98 +0,0 @@ -# example_qt_interface.py -import sys -from PySide6.QtCore import Qt -from PySide6.QtGui import QAction, QIcon -from PySide6.QtWidgets import ( - QApplication, QMainWindow, QWidget, QVBoxLayout, - QLabel, QMenuBar, QToolBar, QSplitter, QListWidget, - QTextEdit, QStatusBar, QFileDialog, QPushButton -) - - -class MainWindow(QMainWindow): - def __init__(self): - super().__init__() - - self.setWindowTitle("Example Qt Interface") - - # Create and set up the menu bar. - menu_bar = QMenuBar(self) - self.setMenuBar(menu_bar) - - # File menu. - file_menu = menu_bar.addMenu("File") - - # Create some actions to populate the File menu. - open_action = QAction("Open", self) - open_action.triggered.connect(self.open_file) - file_menu.addAction(open_action) - - save_action = QAction("Save", self) - save_action.triggered.connect(self.save_file) - file_menu.addAction(save_action) - - exit_action = QAction("Exit", self) - exit_action.triggered.connect(self.close) - file_menu.addAction(exit_action) - - # Create a toolbar and add some actions. - tool_bar = QToolBar("Main Toolbar", self) - tool_bar.addAction(open_action) - tool_bar.addAction(save_action) - self.addToolBar(Qt.TopToolBarArea, tool_bar) - - # Set up a status bar at the bottom. - self.setStatusBar(QStatusBar(self)) - self.statusBar().showMessage("Ready") - - # Create your central widget area. - central_widget = QWidget() - self.setCentralWidget(central_widget) - layout = QVBoxLayout(central_widget) - - # A splitter as an example container that can hold multiple widgets side-by-side. - splitter = QSplitter() - - # Left side: a simple list widget. - self.list_widget = QListWidget() - self.list_widget.addItem("Item A") - self.list_widget.addItem("Item B") - self.list_widget.addItem("Item C") - splitter.addWidget(self.list_widget) - - # Right side: a text edit widget. - self.text_edit = QTextEdit() - self.text_edit.setPlainText("Type here...") - splitter.addWidget(self.text_edit) - - layout.addWidget(splitter) - - # Example button in the central widget area. - example_button = QPushButton("Click Me") - example_button.clicked.connect(self.on_button_clicked) - layout.addWidget(example_button) - - def open_file(self): - file_name, _ = QFileDialog.getOpenFileName(self, "Open File", "", "All Files (*.*)") - if file_name: - self.statusBar().showMessage(f"Opened: {file_name}") - - def save_file(self): - file_name, _ = QFileDialog.getSaveFileName(self, "Save File", "", "All Files (*.*)") - if file_name: - self.statusBar().showMessage(f"Saved: {file_name}") - - def on_button_clicked(self): - self.statusBar().showMessage("Button clicked!") - - -def main(): - app = QApplication(sys.argv) - window = MainWindow() - window.resize(800, 600) - window.show() - sys.exit(app.exec()) - - -if __name__ == "__main__": - main() diff --git a/Data/Modules/data_collector.py b/Data/Modules/data_collector.py deleted file mode 100644 index 74d025c..0000000 --- a/Data/Modules/data_collector.py +++ /dev/null @@ -1,398 +0,0 @@ -# Modules/data_collector.py - -import threading -import time -import re -import sys -import numpy as np -import cv2 -import concurrent.futures - -# Vision-related Imports -import pytesseract -import easyocr -import torch - -from PIL import Image, ImageGrab, ImageFilter - -from PyQt5.QtWidgets import QApplication, QWidget -from PyQt5.QtCore import QRect, QPoint, Qt, QMutex, QTimer -from PyQt5.QtGui import QPainter, QPen, QColor, QFont - -# Initialize EasyOCR with CUDA support -reader_cpu = None -reader_gpu = None - -def initialize_ocr_engines(): - global reader_cpu, reader_gpu - reader_cpu = easyocr.Reader(['en'], gpu=False) - reader_gpu = easyocr.Reader(['en'], gpu=True if torch.cuda.is_available() else False) - -pytesseract.pytesseract.tesseract_cmd = r"C:\\Program Files\\Tesseract-OCR\\tesseract.exe" - -DEFAULT_WIDTH = 180 -DEFAULT_HEIGHT = 130 -HANDLE_SIZE = 5 -LABEL_HEIGHT = 20 - -collector_mutex = QMutex() -regions = {} - -app_instance = None - -def _ensure_qapplication(): - """ - Ensures that QApplication is initialized before creating widgets. - Must be called from the main thread. - """ - global app_instance - if app_instance is None: - app_instance = QApplication(sys.argv) # Start in main thread - -def capture_region_as_image(region_id): - collector_mutex.lock() - if region_id not in regions: - collector_mutex.unlock() - return None - x, y, w, h = regions[region_id]['bbox'][:] - collector_mutex.unlock() - screenshot = ImageGrab.grab(bbox=(x, y, x + w, y + h)) - return screenshot - -def create_ocr_region(region_id, x=250, y=50, w=DEFAULT_WIDTH, h=DEFAULT_HEIGHT, color=(255, 255, 0), thickness=2): - """ - Creates an OCR region with a visible, resizable box on the screen. - Allows setting custom color (RGB) and line thickness. - """ - _ensure_qapplication() - - collector_mutex.lock() - if region_id in regions: - collector_mutex.unlock() - return - regions[region_id] = { - 'bbox': [x, y, w, h], - 'raw_text': "", - 'widget': OCRRegionWidget(x, y, w, h, region_id, color, thickness) - } - collector_mutex.unlock() - -def get_raw_text(region_id): - collector_mutex.lock() - if region_id not in regions: - collector_mutex.unlock() - return "" - text = regions[region_id]['raw_text'] - collector_mutex.unlock() - return text - -def start_collector(): - initialize_ocr_engines() - t = threading.Thread(target=_update_ocr_loop, daemon=True) - t.start() - -def _update_ocr_loop(): - while True: - collector_mutex.lock() - region_ids = list(regions.keys()) - collector_mutex.unlock() - - for rid in region_ids: - collector_mutex.lock() - bbox = regions[rid]['bbox'][:] - collector_mutex.unlock() - - x, y, w, h = bbox - screenshot = ImageGrab.grab(bbox=(x, y, x + w, y + h)) - processed = _preprocess_image(screenshot) - raw_text = pytesseract.image_to_string(processed, config='--psm 6 --oem 1') - - collector_mutex.lock() - if rid in regions: - regions[rid]['raw_text'] = raw_text - collector_mutex.unlock() - - time.sleep(0.7) - -def _preprocess_image(image): - gray = image.convert("L") - scaled = gray.resize((gray.width * 3, gray.height * 3)) - thresh = scaled.point(lambda p: 255 if p > 200 else 0) - return thresh.filter(ImageFilter.MedianFilter(3)) - - -def find_word_positions(region_id, word, offset_x=0, offset_y=0, margin=5, ocr_engine="CPU", num_slices=1): - """ - Uses user-defined horizontal slices and threading for faster inference. - """ - collector_mutex.lock() - if region_id not in regions: - collector_mutex.unlock() - return [] - - bbox = regions[region_id]['bbox'] - collector_mutex.unlock() - - x, y, w, h = bbox - left, top, right, bottom = x, y, x + w, y + h - - if right <= left or bottom <= top: - print(f"[ERROR] Invalid OCR region bounds: {bbox}") - return [] - - try: - image = ImageGrab.grab(bbox=(left, top, right, bottom)) - orig_width, orig_height = image.size - - word_positions = [] - - # Ensure number of slices does not exceed image height - num_slices = min(num_slices, orig_height) - strip_height = max(1, orig_height // num_slices) - - def process_strip(strip_id): - strip_y = strip_id * strip_height - strip = image.crop((0, strip_y, orig_width, strip_y + strip_height)) - - strip_np = np.array(strip) - - detected_positions = [] - if ocr_engine == "CPU": - ocr_data = pytesseract.image_to_data(strip, config='--psm 6 --oem 1', output_type=pytesseract.Output.DICT) - - for i in range(len(ocr_data['text'])): - if re.search(rf"\b{word}\b", ocr_data['text'][i], re.IGNORECASE): - x_scaled = int(ocr_data['left'][i]) - y_scaled = int(ocr_data['top'][i]) + strip_y - w_scaled = int(ocr_data['width'][i]) - h_scaled = int(ocr_data['height'][i]) - - detected_positions.append((x_scaled + offset_x, y_scaled + offset_y, w_scaled + (margin * 2), h_scaled + (margin * 2))) - - else: - results = reader_gpu.readtext(strip_np) - for (bbox, text, _) in results: - if re.search(rf"\b{word}\b", text, re.IGNORECASE): - (x_min, y_min), (x_max, y_max) = bbox[0], bbox[2] - - x_scaled = int(x_min) - y_scaled = int(y_min) + strip_y - w_scaled = int(x_max - x_min) - h_scaled = int(y_max - y_min) - - detected_positions.append((x_scaled + offset_x, y_scaled + offset_y, w_scaled + (margin * 2), h_scaled + (margin * 2))) - - return detected_positions - - with concurrent.futures.ThreadPoolExecutor(max_workers=num_slices) as executor: - strip_results = list(executor.map(process_strip, range(num_slices))) - - for strip_result in strip_results: - word_positions.extend(strip_result) - - return word_positions - - except Exception as e: - print(f"[ERROR] Failed to capture OCR region: {e}") - return [] - -def draw_identification_boxes(region_id, positions, color=(0, 0, 255), thickness=2): - """ - Draws non-interactive rectangles at specified positions within the given OCR region. - Uses a separate rendering thread to prevent blocking OCR processing. - """ - collector_mutex.lock() - if region_id in regions and 'widget' in regions[region_id]: - widget = regions[region_id]['widget'] - widget.update_draw_positions(positions, color, thickness) - collector_mutex.unlock() - -def update_region_slices(region_id, num_slices): - """ - Updates the number of visual slices in the OCR region. - """ - collector_mutex.lock() - if region_id in regions and 'widget' in regions[region_id]: - widget = regions[region_id]['widget'] - widget.set_num_slices(num_slices) - collector_mutex.unlock() - -class OCRRegionWidget(QWidget): - def __init__(self, x, y, w, h, region_id, color, thickness): - super().__init__() - - self.setGeometry(x, y, w, h) - self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool) - self.setAttribute(Qt.WA_TranslucentBackground, True) - self.setAttribute(Qt.WA_TransparentForMouseEvents, False) - - self.region_id = region_id - self.box_color = QColor(*color) - self.line_thickness = thickness - self.draw_positions = [] - self.previous_positions = [] # This prevents redundant redraws - self.num_slices = 1 # Ensures slice count is initialized - - # --- Initialization for interactive handles --- - self.selected_handle = None # Tracks which handle is being dragged/resized - self.drag_offset = None # Tracks the offset for moving the widget - - self.show() - - def paintEvent(self, event): - painter = QPainter(self) - pen = QPen(self.box_color) - pen.setWidth(self.line_thickness) - painter.setPen(pen) - - # Draw main rectangle - painter.drawRect(0, 0, self.width(), self.height()) - - # Draw detected word overlays - for x, y, w, h in self.draw_positions: - painter.drawRect(x, y, w, h) - - # Draw faint slice division lines - if self.num_slices > 1: - strip_height = self.height() // self.num_slices - pen.setColor(QColor(150, 150, 150, 100)) # Light gray, semi-transparent - pen.setWidth(1) - painter.setPen(pen) - - for i in range(1, self.num_slices): # Do not draw the last one at the bottom - painter.drawLine(0, i * strip_height, self.width(), i * strip_height) - - # --- Draw interactive handles (grabbers) with reduced opacity (15%) --- - # 15% opacity of 255 is approximately 38 - handle_color = QColor(0, 0, 0, 50) - for handle in self._resize_handles(): - painter.fillRect(handle, handle_color) - painter.drawRect(handle) # Optional: draw a border around the handle - - def set_draw_positions(self, positions, color, thickness): - """ - Updates the overlay positions and visual settings. - """ - self.draw_positions = positions - self.box_color = QColor(*color) - self.line_thickness = thickness - self.update() - - def update_draw_positions(self, positions, color, thickness): - """ - Updates the overlay positions and redraws only if the positions have changed. - This prevents unnecessary flickering. - """ - if positions == self.previous_positions: - return # No change, do not update - - self.previous_positions = positions # Store last known positions - self.draw_positions = positions - self.box_color = QColor(*color) - self.line_thickness = thickness - self.update() # Redraw only if needed - - def set_num_slices(self, num_slices): - """ - Updates the number of horizontal slices for visualization. - """ - self.num_slices = num_slices - self.update() - - def _resize_handles(self): - """ - Returns a list of QRect objects representing the interactive handles: - - Index 0: Top-left (resize) - - Index 1: Top-right (resize) - - Index 2: Bottom-left (resize) - - Index 3: Bottom-right (resize) - - Index 4: Top-center (dragger) - """ - w, h = self.width(), self.height() - handles = [ - QRect(0, 0, HANDLE_SIZE, HANDLE_SIZE), # Top-left - QRect(w - HANDLE_SIZE, 0, HANDLE_SIZE, HANDLE_SIZE), # Top-right - QRect(0, h - HANDLE_SIZE, HANDLE_SIZE, HANDLE_SIZE), # Bottom-left - QRect(w - HANDLE_SIZE, h - HANDLE_SIZE, HANDLE_SIZE, HANDLE_SIZE) # Bottom-right - ] - # Top-center handle: centered along the top edge - top_center_x = (w - HANDLE_SIZE) // 2 - top_center = QRect(top_center_x, 0, HANDLE_SIZE, HANDLE_SIZE) - handles.append(top_center) - return handles - - def mousePressEvent(self, event): - if event.button() == Qt.LeftButton: - # Check if any handle (including the new top-center) is clicked - for i, handle in enumerate(self._resize_handles()): - if handle.contains(event.pos()): - self.selected_handle = i - # For the top-center handle (index 4), initialize drag offset for moving - if i == 4: - self.drag_offset = event.pos() - return - # If no handle is clicked, allow dragging by clicking anywhere in the widget - self.drag_offset = event.pos() - - def mouseMoveEvent(self, event): - if self.selected_handle is not None: - if self.selected_handle == 4: - # --- Top-center handle dragging --- - new_x = event.globalX() - self.drag_offset.x() - new_y = event.globalY() - self.drag_offset.y() - self.move(new_x, new_y) - collector_mutex.lock() - if self.region_id in regions: - regions[self.region_id]["bbox"] = [new_x, new_y, self.width(), self.height()] - collector_mutex.unlock() - self.update() - else: - # --- Resizing logic for corner handles --- - if self.selected_handle == 0: # Top-left - new_w = self.width() + (self.x() - event.globalX()) - new_h = self.height() + (self.y() - event.globalY()) - new_x = event.globalX() - new_y = event.globalY() - elif self.selected_handle == 1: # Top-right - new_w = event.globalX() - self.x() - new_h = self.height() + (self.y() - event.globalY()) - new_x = self.x() - new_y = event.globalY() - elif self.selected_handle == 2: # Bottom-left - new_w = self.width() + (self.x() - event.globalX()) - new_h = event.globalY() - self.y() - new_x = event.globalX() - new_y = self.y() - elif self.selected_handle == 3: # Bottom-right - new_w = event.globalX() - self.x() - new_h = event.globalY() - self.y() - new_x = self.x() - new_y = self.y() - - if new_w < 20: - new_w = 20 - if new_h < 20: - new_h = 20 - - self.setGeometry(new_x, new_y, new_w, new_h) - collector_mutex.lock() - if self.region_id in regions: - regions[self.region_id]["bbox"] = [self.x(), self.y(), self.width(), self.height()] - collector_mutex.unlock() - self.update() - elif self.drag_offset: - # --- General widget dragging (if no handle was clicked) --- - new_x = event.globalX() - self.drag_offset.x() - new_y = event.globalY() - self.drag_offset.y() - self.move(new_x, new_y) - collector_mutex.lock() - if self.region_id in regions: - regions[self.region_id]["bbox"] = [new_x, new_y, self.width(), self.height()] - collector_mutex.unlock() - - def mouseReleaseEvent(self, event): - """ - Resets the drag/resize state once the mouse button is released. - """ - self.selected_handle = None - self.drag_offset = None diff --git a/Data/Modules/data_manager.py b/Data/Modules/data_manager.py deleted file mode 100644 index 28071bf..0000000 --- a/Data/Modules/data_manager.py +++ /dev/null @@ -1,156 +0,0 @@ -import threading -import time -import base64 -from flask import Flask, jsonify, Response -from PyQt5.QtCore import QMutex - -# Global datastore for character metrics -data_store = { - "hp_current": 0, - "hp_total": 0, - "mp_current": 0, - "mp_total": 0, - "fp_current": 0, - "fp_total": 0, - "exp": 0.0 -} - -# Mutex for thread safety -data_mutex = QMutex() - -# Flag to ensure only one character status collector node exists -character_status_collector_exists = False - -# A place to store the screenshot in base64 -status_screenshot_base64 = "" - -# Flask Application -app = Flask(__name__) - -@app.route('/data') -def data_api(): - """ - Returns the current character metrics as JSON. - """ - return jsonify(get_data()) - -@app.route('/exp') -def exp_api(): - """ - Returns the EXP data. - """ - return jsonify({"exp": get_data()["exp"]}) - -@app.route('/hp') -def hp_api(): - """ - Returns the HP data. - """ - return jsonify({ - "hp_current": get_data()["hp_current"], - "hp_total": get_data()["hp_total"] - }) - -@app.route('/mp') -def mp_api(): - """ - Returns the MP data. - """ - return jsonify({ - "mp_current": get_data()["mp_current"], - "mp_total": get_data()["mp_total"] - }) - -@app.route('/fp') -def fp_api(): - """ - Returns the FP data. - """ - return jsonify({ - "fp_current": get_data()["fp_current"], - "fp_total": get_data()["fp_total"] - }) - -@app.route('/flyff/status') -def status_screenshot(): - """ - Returns an HTML page that displays the stored screenshot and - automatically refreshes it every second without requiring a manual page reload. - """ - html = """ - - - Borealis - Live Status - - - - - - - """ - return html - -@app.route('/flyff/status_rawdata') -def status_screenshot_data(): - """ - Serves the raw PNG bytes (decoded from base64) used by in /status_screenshot. - """ - data_mutex.lock() - encoded = status_screenshot_base64 - data_mutex.unlock() - - if not encoded: - # No image captured yet, return HTTP 204 "No Content" - return "", 204 - - raw_img = base64.b64decode(encoded) - return Response(raw_img, mimetype='image/png') - -def start_api_server(): - """ - Starts the Flask API server in a separate daemon thread. - """ - def run(): - app.run(host="0.0.0.0", port=5000) # Allows external connections - t = threading.Thread(target=run, daemon=True) - t.start() - -def get_data(): - """ - Return a copy of the global data_store. - """ - data_mutex.lock() - data_copy = data_store.copy() - data_mutex.unlock() - return data_copy - -def set_data(key, value): - """ - Set a single metric in the global data_store. - """ - data_mutex.lock() - data_store[key] = value - data_mutex.unlock() - -def set_data_bulk(metrics_dict): - """ - Update multiple metrics in the global data_store at once. - """ - data_mutex.lock() - data_store.update(metrics_dict) - data_mutex.unlock() - -def set_status_screenshot(encoded_str): - """ - Called by the OCR node to store the base64-encoded screenshot data. - """ - global status_screenshot_base64 - data_mutex.lock() - status_screenshot_base64 = encoded_str - data_mutex.unlock() diff --git a/Data/Nodes/Experimental/blueprint_node.py b/Data/Nodes/Experimental/blueprint_node.py deleted file mode 100644 index cd6c742..0000000 --- a/Data/Nodes/Experimental/blueprint_node.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python3 - -from OdenGraphQt import BaseNode -from Qt import QtCore - -class BlueprintNode(BaseNode): - """ - A placeholder node used to preview placement before spawning - the real node. It has a distinct color and minimal UI. - """ - __identifier__ = 'bunny-lab.io.blueprint' - NODE_NAME = 'Blueprint Node' - - def __init__(self): - super(BlueprintNode, self).__init__() - # Display a name so the user sees "Click to Place Node" - self.set_name("Click to Place Node") - - # Give it a bluish color + white text, for visibility - self.set_color(60, 120, 220) # R, G, B - self.view.text_color = (255, 255, 255, 200) - self.view.border_color = (255, 255, 255, 180) - - # Make it slightly transparent if desired (alpha=150) - self.view._bg_color = (60, 120, 220, 150) - - # Remove any default inputs/outputs (make it minimal) - for port in self.input_ports() + self.output_ports(): - self.model.delete_port(port.name(), port.port_type) - - # Store the "actual node" we want to spawn - self.create_property("actual_node_type", "", widget_type=0) - - def process_input(self): - """ - We do nothing here; it is purely a placeholder node. - """ - pass diff --git a/Data/Nodes/Flyff/Resources/bars_template.png b/Data/Nodes/Flyff/Resources/bars_template.png deleted file mode 100644 index 179037ba159e6cec5e2d9ef22e0129a9cfc774b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5684 zcmbVQ2{=@5+n+)9BFU1aF@$6ph8f#fhoS6LB+D3s!I)`g?1b!uNS4qdl9ZHnC|jno zEBo4CF(slbSrXsqUtRC}eebuu=eo{0&v~BTy*|JDKG!*kRu-uJoJTkT0Kk4z6Qm8} zp3OK3>`aWSpEm6rd;I$J6L;!$&qBuKyioLm+CWe4lM7tB*u!=!= zA_ENoXzK}bH#Q7n>OASq65Y8O|KC5O9F((>gy>KQWB&=pp z0!CR?4FuOvRE9xSH8kWwDiCFLC5XC`GE_ksrl|_iR8ax_I=~ETBzF%@8>I0sTZ|_J z*o#6TYAPuO1_mkyLKO)lPbFmy4GkrTijsrGBups?O;l1=gzSa%6X=flgCqKre195u$0%Wa zv3RT>h0MSz|G^Ty2owU@i}2s5{SRZTUxCIqGcVO$}9L(K{9IFI1}3RTmDKpA`dPf!MH z+|d;D{|a`;XnGJxcr+ts93Jh7RU-O%fWscA^?C6E|~8Qwwvao*I>(27Lx!1*#3 z$Tlbgkg1U&Oj!d4Q&3S<{^_o{xu&TfnS%DiU`>$-Fe5*TIGj6!j&5#PHH?RXDjMsq z09R90Rd9pC86;L$g`m-B6%BVd{I@)kfbrjB!Ebr@|EIhq3CE~HwC}&IXRkK*=%Z{S8+jM<|A)*bw78uxE!;BU2oURZ|G|A7~O!^i{= zN+6nq)%Rp%>;FPMC5Hb>du97~C`$i1%AafhP~yMgjJ~lq{L`=)4}V%7){oKWNQ{Oy zC?O>U0Pr@NBK7Tp@;^H{`ic$*@9aQ5zqRI>q!%KsGx<$x{K~NnS>80`{34Dd0R=v( zaQ3mnaM?bv-Z;SMQsc~R9_1sqx($DgQn33_rsMz+lyn%R579LpO#dJV^uID<&&S*a__E!~<`Vjut=Bm} zbYXSIaQ$=EQTcM0r%xzz)#IM5FJC4*t_uhh)EHeBRaQm1RJMNlv8@PP z?M112EbdQn%M`pJn`M#wINPPky!m5=>CsTv<*Biu3u>#glc-mpjT4(y`pn~sIgM9J&HELO%n?ruZDhV!}CK7!da-rtWDSBw^(K|)sGi;e54_D99r-;^>xZwhs^5YA-= z&+Cq`yn2veknqlur!HcKk4kl_AK|zp5}qkVlj(T5U}N8XUbfjeW}mD}pfK6nr$sT` zvrs;^S$I6ATLAWhl_jj0`_*;i?UDqR%)qCLHOm78DIL=Q*6;E!R!VM?IIPA9S{!Dr z+i+_Ej_#t5hl}LDX|rERl$q28S5vmh)BDXl0FGY7`-aMgfQUx%9 zE6u9`MEE!fvlTAxC9$Qo_)Ga=?KSWiZum^l-I_$2#lVodV~S%tuuc>*4CXn)W$>P- zIsb{kfIv>M+g5hhShU-Q! zLgD>)+Ggj|a{l(j*V`}d`umfA;2OVEyV^3-FI<204Tsc(|AmuK1)p{do^Tep^X$FY zHylw`g`e&!%DfUVr(2sfHEp0nm?1lC(t2+OExy^N)n+-5J`LB=3TYONnIF10LVmwl z#zNV(Yi$l3m&Jt|7W5nK@|&VPn!i{OMrmi^OPQvKFv_;AXUWX%#BJ)K_f}z>Tg&~$ z)Ft;tCSHkyvMNFe{c2~|9%R>X03E4?Icg#;_gw4U0J51$qud402)zuMco{bADBU*R zn2k(ts9euF!$mWQVv%_;l91_mw1@B2naQJ*Rbm`m>F+wF?n*yuNZjHm04kiYxs13& zwbW7ZCz2{2gZ)t&R@%*y<_!oRc;@au12G`%P|3B8^5t3A;ykc+rHYZlR$X1z^NFn>rl{Q8zJrN#=E<&12V*2+UJJA&0I=f~lVmhI zpMgoB=yB+%naDXDoBeU@i8D#3!|S^Nm7!%zvb@bw+!8S$i3#y)TH_@&-Y?|9c_vqO zbPnr8dwA`|==)`032upiEQ{&{b^7&sV`w&+iPH=a#W6E5=~C6SObc{rtK1$qC8Q<+^-!9GdMNLNqM#B0>(L{ARCQ;bztDxr0=S3DW+ zceruQMc47%D4+#$<*Da=jfE?l=kIu(UN}e33xVP6GA0nV01Qcf7j+YH(d#XUJwxI9 z!}YVy)BsQXp|tJk+HVD0QoIvd{jl^1W$QG|oyLt0GH>$Ph@jbqh~)(u6_u8@`zGC5 zqrom{ZLEMx_XOReN08R`u0ZYV{^Nebc9)tF9c~#T)k>w;%IJ|x&S{T&H~|mx2MQ-J zmWZ9!YQe?vIcA{rZr|cg`njLeeHA8v6u*t%Vr{X;;Xmz340i&O6iO6 z)>prIXg6grDxj4!^X@Pi;}w|sB46NnO|i(s&x0c0J}nG1M9A!K`8IP07Wpt^s3wwq z5^D#~HoxO|O~xnKyp+$U{Hwj)*o))v`+V%)GSFKm{YpN88U(L~rmVlIo(_-7q)o9E z%YWO4)O+qgCBrFa3*q88FF7=-`U0p30R@6RTIr z60d5TV_mW{B720}$E+ww}al5xV09OSaw&#Xv4+V{=$Pa00 zgscvuYj7UK4XqzqZti2(9EDnb2p81G)yB~C%HMZfhgA(NSYKNzNP3I8VPhGaS{lu6O)_CwYt6 z7*R?Rumc4)6;9_Xbc3d32?blPK3?TnNJmMph^(h@{`Dy(bhXuXc^SmFZg%XJ=Z9IR zhVbiCP^0-+ptVl;?ng-!ipP>J71_@gAHTBvgZ8eV+TgZ5KYx?w{lmIWD@F~8=cPjM#vqAq;h~@6LBkPH|N0>hI zo*D~Cdi|nC{cF|Y2JL9K^JWKGH+a71IV!N=x^Oyt$?xgr&Zkr4^(USvu0wfT>wfjZ zhSCLil9`d+>t^Fbiy2>4visM*t?!f-=&n)8Ip`3lUyPr;`* zNG6I!vg5B8zH*hZBCPFCcrBcmckf!6h1-xXa%S~5KeO!Nq~N?XF(GvXLbdvYBrrcm zT(qN84V3Z!|L%6bn(SAFnH&?w;7f-1g@oiJtpKpThSHT`GCS?{{t4nQ> zL%>A{?*a%4D~j<|-qLyto;SHN@S`V_NeIJC|8R7?GD4n9&u^rz(LOA+vBH}EC}Qrc?oMBo ztzKX%tJkfQa=@-Ao|;b+KKkW%E2%Ek@vH+!g836Jv>u7=0+9kIE3%F#7EdPWUJTz+ z+MV9Kx$}0ceXixsch;HKj-m0D*mZ$q&(ze*o=NMAb)k2;El-EF=I{0l8N)L~xvahj zy=$}?dIo-(SgM|tZd?$umT6X`OC1_1G^J58c+Y5giWEY9hSR=SKASQu5s1{~6YJ$a zs;#p6VoeRetP$NKCN?hXP{KoRT+TIp8)Xs6zo=AR`Ej)h$?I%ybxhA0aJ}QjKEAjY z55o@04kZr)6)c2oHsnr=0d9>HpA3F^5^zn*OI|bT$s5AHo%!9JFC8h}VK0iVeviMn z7VOzIdH~*?{sE0MC%nF}-^z!yBnB! z^~8ZpyiBb62f2aL6@e@!96~eU_nlAdLlSef&GAafYI0`fsvfSH6I}E8b0v-UN4d{b zv?$`mOD!^NoGo@aI$UqLH!Nt5XvI7;+*mxp;9R5L`h#f>IGHB}8OUnmw7^EO6ts=? zIl4Fe-BO(g_!sU?wcWjOcn_Fc=VSjJbHwn&OTFH?~NF~8q^Jf)u! zH2oy?t}k36XViPd8uKuOLoJTG$XT*A&Dtu}TrWy!rU{4wF0>Kr#Pv8I8UTp8WXaqM z!4eG-T{+@qVTbYowvo{=C=-kIt8IFo4ddP2SH~mtWmO1L=3zFh=5^96Rpt(BviXQ# zzDgVV&~=|E=4Q~o<;(XjNobGf3Erz%$^^%K=_YWq(z7?|Pr7|ye=U`G&g-d@vH9jv z%Eqv1r0RrTa}50uU1%mj?lRx4eH5rB5Y!txO_OSRU8%u!o?LHVX>~~cOump=qry0k zy6+~im+58WwLy4x{N*{UV`Ut3#CKgE!c&(gc%!B;uk2!e;ngpXoo;GrsUzs+mTJh^ zaaqmubPMiD+O$^5s+Qh|LH*_Vj@qlVRK54|txX3hZf#U`3OwOXdJR}cyUAB~#b&xX z@a(9EE&Jv?4|fyH@#)3!kj=3%)KYJ+*-QeUg4bF#C`rghXL%Rg&UHf*W%0%A*tTZM zdahc(xA5~kQ_M3-`j^si+35V<92NfPr;)@YVw-t0CqNl)@aXe{7iwagYO5jHItlucBiO z^kx0sYZ;OSLRV}YKJ_K!%1oSdYN~aX7f7<3*RmZnmEfE|7ucV^s5EwA{P;Vc;6&zW zP7HAI{yh6E&oSlbTz= self._retry_interval: - print(f"[ERROR] {msg}") - self._last_error_printed = current_time - - self.set_name("Flyff - FP Current (API Disconnected)") diff --git a/Data/Nodes/Flyff/flyff_FP_total.py b/Data/Nodes/Flyff/flyff_FP_total.py deleted file mode 100644 index 61a0aa5..0000000 --- a/Data/Nodes/Flyff/flyff_FP_total.py +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env python3 -""" -Flyff FP Total Node (Final Combined Version) - - Polls the API at http://127.0.0.1:5000/data - - Outputs only the "fp_total" value as a string - - Uses color (36, 116, 32) for its output port - - Displays "fp_total" in a text field labeled "Value" - - Retrieves the port with self.outputs().get('value') -""" - -import time -import requests -import traceback -from OdenGraphQt import BaseNode - -class FlyffFPTotalNode(BaseNode): - __identifier__ = 'bunny-lab.io.flyff_fp_total_node' - NODE_NAME = 'Flyff - FP Total' - - def __init__(self): - super(FlyffFPTotalNode, self).__init__() - - # 1) Text input property named "value" for UI display - self.add_text_input('value', 'Value', text='N/A') - - # 2) Output port also named "value" - self.add_output('value', color=(36, 116, 32)) - - self._api_down = True - self._last_api_attempt = 0.0 - self._retry_interval = 5.0 - self._last_error_printed = 0.0 - - self.set_name("Flyff - FP Total (API Disconnected)") - - def process_input(self): - current_time = time.time() - if self._api_down and (current_time - self._last_api_attempt < self._retry_interval): - return - - self._last_api_attempt = current_time - - try: - response = requests.get("http://127.0.0.1:5000/data", timeout=1) - status_code = response.status_code - print(f"[DEBUG] FlyffFPTotalNode: HTTP Status Code = {status_code}") - - if status_code == 200: - try: - data = response.json() or {} - except ValueError: - data = {} - - if isinstance(data, list): - data = {} - - self._api_down = False - self.set_name("Flyff - FP Total (API Connected)") - - new_value = data.get("fp_total", "N/A") - print(f"[DEBUG] FlyffFPTotalNode: fp_total = {new_value}") - - new_value_str = str(new_value) - self.set_property('value', new_value_str) - self.transmit_data(new_value_str) - - else: - self._handle_api_error(f"HTTP {status_code} from FlyffFPTotalNode") - self._api_down = True - - except Exception as e: - tb = traceback.format_exc() - self._handle_api_error(f"Exception in FlyffFPTotalNode: {e}\nTraceback:\n{tb}") - self._api_down = True - - def transmit_data(self, data): - output_port = self.outputs().get('value') - if output_port and output_port.connected_ports(): - for connected_port in output_port.connected_ports(): - connected_node = connected_port.node() - if hasattr(connected_node, 'receive_data'): - try: - connected_node.receive_data(data, source_port_name='value') - except Exception as e: - print(f"[ERROR] Error transmitting data to {connected_node}: {e}") - - def _handle_api_error(self, msg): - current_time = time.time() - if (current_time - self._last_error_printed) >= self._retry_interval: - print(f"[ERROR] {msg}") - self._last_error_printed = current_time - - self.set_name("Flyff - FP Total (API Disconnected)") diff --git a/Data/Nodes/Flyff/flyff_HP_current.py b/Data/Nodes/Flyff/flyff_HP_current.py deleted file mode 100644 index 5075d4d..0000000 --- a/Data/Nodes/Flyff/flyff_HP_current.py +++ /dev/null @@ -1,112 +0,0 @@ -#!/usr/bin/env python3 -""" -Flyff HP Current Node (Final Combined Version) - - Polls the API at http://127.0.0.1:5000/data - - Outputs only the "hp_current" value as a string - - Uses color (126, 36, 57) for its output port - - Displays "hp_current" in a text field labeled "Value" - - Avoids "list indices must be integers" by retrieving the port with self.outputs().get('value') -""" - -import time -import requests -import traceback -from OdenGraphQt import BaseNode - -class FlyffHPCurrentNode(BaseNode): - __identifier__ = 'bunny-lab.io.flyff_hp_current_node' - NODE_NAME = 'Flyff - HP Current' - - def __init__(self): - super(FlyffHPCurrentNode, self).__init__() - - # 1) Add a text input property named "value" for UI display - self.add_text_input('value', 'Value', text='N/A') - - # 2) Add an output port also named "value" - self.add_output('value', color=(126, 36, 57)) - - # Start in "disconnected" state - self._api_down = True - self._last_api_attempt = 0.0 - self._retry_interval = 5.0 - self._last_error_printed = 0.0 - - # Default node title - self.set_name("Flyff - HP Current (API Disconnected)") - - def process_input(self): - """ - Called periodically by the global timer in borealis.py - """ - current_time = time.time() - if self._api_down and (current_time - self._last_api_attempt < self._retry_interval): - return - - self._last_api_attempt = current_time - - try: - response = requests.get("http://127.0.0.1:5000/data", timeout=1) - status_code = response.status_code - print(f"[DEBUG] FlyffHPCurrentNode: HTTP Status Code = {status_code}") - - if status_code == 200: - # Attempt to parse JSON - try: - data = response.json() or {} - except ValueError: - data = {} - - # If data is a list, ignore or convert to {} - if isinstance(data, list): - data = {} - - # Mark node as connected - self._api_down = False - self.set_name("Flyff - HP Current (API Connected)") - - # Retrieve hp_current (default "N/A" if missing) - new_value = data.get("hp_current", "N/A") - print(f"[DEBUG] FlyffHPCurrentNode: hp_current = {new_value}") - - # Convert to string - new_value_str = str(new_value) - - # 3) Update the text input property so the user sees it - self.set_property('value', new_value_str) - - # 4) Transmit to downstream nodes - self.transmit_data(new_value_str) - - else: - # Non-200 => disconnected - self._handle_api_error(f"HTTP {status_code} from FlyffHPCurrentNode") - self._api_down = True - - except Exception as e: - tb = traceback.format_exc() - self._handle_api_error(f"Exception in FlyffHPCurrentNode: {e}\nTraceback:\n{tb}") - self._api_down = True - - def transmit_data(self, data): - """ - Sends 'data' to any connected node via the "value" port. - (Uses self.outputs().get('value') instead of self.output('value')) - """ - output_port = self.outputs().get('value') - if output_port and output_port.connected_ports(): - for connected_port in output_port.connected_ports(): - connected_node = connected_port.node() - if hasattr(connected_node, 'receive_data'): - try: - connected_node.receive_data(data, source_port_name='value') - except Exception as e: - print(f"[ERROR] Error transmitting data to {connected_node}: {e}") - - def _handle_api_error(self, msg): - current_time = time.time() - if (current_time - self._last_error_printed) >= self._retry_interval: - print(f"[ERROR] {msg}") - self._last_error_printed = current_time - - self.set_name("Flyff - HP Current (API Disconnected)") diff --git a/Data/Nodes/Flyff/flyff_HP_total.py b/Data/Nodes/Flyff/flyff_HP_total.py deleted file mode 100644 index fc01a48..0000000 --- a/Data/Nodes/Flyff/flyff_HP_total.py +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env python3 -""" -Flyff HP Total Node (Final Combined Version) - - Polls the API at http://127.0.0.1:5000/data - - Outputs only the "hp_total" value as a string - - Uses color (126, 36, 57) for its output port - - Displays "hp_total" in a text field labeled "Value" - - Retrieves the port with self.outputs().get('value') -""" - -import time -import requests -import traceback -from OdenGraphQt import BaseNode - -class FlyffHPTotalNode(BaseNode): - __identifier__ = 'bunny-lab.io.flyff_hp_total_node' - NODE_NAME = 'Flyff - HP Total' - - def __init__(self): - super(FlyffHPTotalNode, self).__init__() - - # 1) Text input property named "value" for UI display - self.add_text_input('value', 'Value', text='N/A') - - # 2) Output port also named "value" - self.add_output('value', color=(126, 36, 57)) - - self._api_down = True - self._last_api_attempt = 0.0 - self._retry_interval = 5.0 - self._last_error_printed = 0.0 - - self.set_name("Flyff - HP Total (API Disconnected)") - - def process_input(self): - current_time = time.time() - if self._api_down and (current_time - self._last_api_attempt < self._retry_interval): - return - - self._last_api_attempt = current_time - - try: - response = requests.get("http://127.0.0.1:5000/data", timeout=1) - status_code = response.status_code - print(f"[DEBUG] FlyffHPTotalNode: HTTP Status Code = {status_code}") - - if status_code == 200: - try: - data = response.json() or {} - except ValueError: - data = {} - - if isinstance(data, list): - data = {} - - self._api_down = False - self.set_name("Flyff - HP Total (API Connected)") - - new_value = data.get("hp_total", "N/A") - print(f"[DEBUG] FlyffHPTotalNode: hp_total = {new_value}") - - new_value_str = str(new_value) - self.set_property('value', new_value_str) - self.transmit_data(new_value_str) - - else: - self._handle_api_error(f"HTTP {status_code} from FlyffHPTotalNode") - self._api_down = True - - except Exception as e: - tb = traceback.format_exc() - self._handle_api_error(f"Exception in FlyffHPTotalNode: {e}\nTraceback:\n{tb}") - self._api_down = True - - def transmit_data(self, data): - output_port = self.outputs().get('value') - if output_port and output_port.connected_ports(): - for connected_port in output_port.connected_ports(): - connected_node = connected_port.node() - if hasattr(connected_node, 'receive_data'): - try: - connected_node.receive_data(data, source_port_name='value') - except Exception as e: - print(f"[ERROR] Error transmitting data to {connected_node}: {e}") - - def _handle_api_error(self, msg): - current_time = time.time() - if (current_time - self._last_error_printed) >= self._retry_interval: - print(f"[ERROR] {msg}") - self._last_error_printed = current_time - - self.set_name("Flyff - HP Total (API Disconnected)") diff --git a/Data/Nodes/Flyff/flyff_MP_current.py b/Data/Nodes/Flyff/flyff_MP_current.py deleted file mode 100644 index bf74f05..0000000 --- a/Data/Nodes/Flyff/flyff_MP_current.py +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env python3 -""" -Flyff MP Current Node (Final Combined Version) - - Polls the API at http://127.0.0.1:5000/data - - Outputs only the "mp_current" value as a string - - Uses color (35, 89, 144) for its output port - - Displays "mp_current" in a text field labeled "Value" - - Retrieves the port with self.outputs().get('value') -""" - -import time -import requests -import traceback -from OdenGraphQt import BaseNode - -class FlyffMPCurrentNode(BaseNode): - __identifier__ = 'bunny-lab.io.flyff_mp_current_node' - NODE_NAME = 'Flyff - MP Current' - - def __init__(self): - super(FlyffMPCurrentNode, self).__init__() - - # 1) Text input property named "value" for UI display - self.add_text_input('value', 'Value', text='N/A') - - # 2) Output port also named "value" - self.add_output('value', color=(35, 89, 144)) - - self._api_down = True - self._last_api_attempt = 0.0 - self._retry_interval = 5.0 - self._last_error_printed = 0.0 - - self.set_name("Flyff - MP Current (API Disconnected)") - - def process_input(self): - current_time = time.time() - if self._api_down and (current_time - self._last_api_attempt < self._retry_interval): - return - - self._last_api_attempt = current_time - - try: - response = requests.get("http://127.0.0.1:5000/data", timeout=1) - status_code = response.status_code - print(f"[DEBUG] FlyffMPCurrentNode: HTTP Status Code = {status_code}") - - if status_code == 200: - try: - data = response.json() or {} - except ValueError: - data = {} - - if isinstance(data, list): - data = {} - - self._api_down = False - self.set_name("Flyff - MP Current (API Connected)") - - new_value = data.get("mp_current", "N/A") - print(f"[DEBUG] FlyffMPCurrentNode: mp_current = {new_value}") - - new_value_str = str(new_value) - self.set_property('value', new_value_str) - self.transmit_data(new_value_str) - - else: - self._handle_api_error(f"HTTP {status_code} from FlyffMPCurrentNode") - self._api_down = True - - except Exception as e: - tb = traceback.format_exc() - self._handle_api_error(f"Exception in FlyffMPCurrentNode: {e}\nTraceback:\n{tb}") - self._api_down = True - - def transmit_data(self, data): - output_port = self.outputs().get('value') - if output_port and output_port.connected_ports(): - for connected_port in output_port.connected_ports(): - connected_node = connected_port.node() - if hasattr(connected_node, 'receive_data'): - try: - connected_node.receive_data(data, source_port_name='value') - except Exception as e: - print(f"[ERROR] Error transmitting data to {connected_node}: {e}") - - def _handle_api_error(self, msg): - current_time = time.time() - if (current_time - self._last_error_printed) >= self._retry_interval: - print(f"[ERROR] {msg}") - self._last_error_printed = current_time - - self.set_name("Flyff - MP Current (API Disconnected)") diff --git a/Data/Nodes/Flyff/flyff_MP_total.py b/Data/Nodes/Flyff/flyff_MP_total.py deleted file mode 100644 index 1f1cdcd..0000000 --- a/Data/Nodes/Flyff/flyff_MP_total.py +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env python3 -""" -Flyff MP Total Node (Final Combined Version) - - Polls the API at http://127.0.0.1:5000/data - - Outputs only the "mp_total" value as a string - - Uses color (35, 89, 144) for its output port - - Displays "mp_total" in a text field labeled "Value" - - Retrieves the port with self.outputs().get('value') -""" - -import time -import requests -import traceback -from OdenGraphQt import BaseNode - -class FlyffMPTotalNode(BaseNode): - __identifier__ = 'bunny-lab.io.flyff_mp_total_node' - NODE_NAME = 'Flyff - MP Total' - - def __init__(self): - super(FlyffMPTotalNode, self).__init__() - - # 1) Text input property named "value" for UI display - self.add_text_input('value', 'Value', text='N/A') - - # 2) Output port also named "value" - self.add_output('value', color=(35, 89, 144)) - - self._api_down = True - self._last_api_attempt = 0.0 - self._retry_interval = 5.0 - self._last_error_printed = 0.0 - - self.set_name("Flyff - MP Total (API Disconnected)") - - def process_input(self): - current_time = time.time() - if self._api_down and (current_time - self._last_api_attempt < self._retry_interval): - return - - self._last_api_attempt = current_time - - try: - response = requests.get("http://127.0.0.1:5000/data", timeout=1) - status_code = response.status_code - print(f"[DEBUG] FlyffMPTotalNode: HTTP Status Code = {status_code}") - - if status_code == 200: - try: - data = response.json() or {} - except ValueError: - data = {} - - if isinstance(data, list): - data = {} - - self._api_down = False - self.set_name("Flyff - MP Total (API Connected)") - - new_value = data.get("mp_total", "N/A") - print(f"[DEBUG] FlyffMPTotalNode: mp_total = {new_value}") - - new_value_str = str(new_value) - self.set_property('value', new_value_str) - self.transmit_data(new_value_str) - - else: - self._handle_api_error(f"HTTP {status_code} from FlyffMPTotalNode") - self._api_down = True - - except Exception as e: - tb = traceback.format_exc() - self._handle_api_error(f"Exception in FlyffMPTotalNode: {e}\nTraceback:\n{tb}") - self._api_down = True - - def transmit_data(self, data): - output_port = self.outputs().get('value') - if output_port and output_port.connected_ports(): - for connected_port in output_port.connected_ports(): - connected_node = connected_port.node() - if hasattr(connected_node, 'receive_data'): - try: - connected_node.receive_data(data, source_port_name='value') - except Exception as e: - print(f"[ERROR] Error transmitting data to {connected_node}: {e}") - - def _handle_api_error(self, msg): - current_time = time.time() - if (current_time - self._last_error_printed) >= self._retry_interval: - print(f"[ERROR] {msg}") - self._last_error_printed = current_time - - self.set_name("Flyff - MP Total (API Disconnected)") diff --git a/Data/Nodes/Flyff/flyff_character_status_node.py b/Data/Nodes/Flyff/flyff_character_status_node.py deleted file mode 100644 index 9d5511c..0000000 --- a/Data/Nodes/Flyff/flyff_character_status_node.py +++ /dev/null @@ -1,129 +0,0 @@ -#!/usr/bin/env python3 -""" -Flyff Character Status Node: -- Creates an OCR region in data_collector. -- Periodically captures a screenshot and updates Flask. -- If OCR is enabled, it extracts character status and updates the data_manager. -""" - -import re -import base64 -from io import BytesIO - -from OdenGraphQt import BaseNode -from PyQt5.QtWidgets import QMessageBox -from PyQt5.QtCore import QTimer - -# Import the existing modules -from Modules import data_manager, data_collector - -class FlyffCharacterStatusNode(BaseNode): - __identifier__ = "bunny-lab.io.flyff_character_status_node" - NODE_NAME = "Flyff - Character Status" - - def __init__(self): - super(FlyffCharacterStatusNode, self).__init__() - - if data_manager.character_status_collector_exists: - QMessageBox.critical(None, "Error", "Only one Flyff Character Status Collector node is allowed.") - raise Exception("Duplicate Character Status Node.") - data_manager.character_status_collector_exists = True - - # Add the Data Collection dropdown menu - self.add_combo_menu("data_collection", "Data Collection", items=["Disabled", "Enabled"]) - self.set_property("data_collection", "Disabled") # Default to Disabled - - self.add_text_input("hp", "HP", text="HP: 0/0") - self.add_text_input("mp", "MP", text="MP: 0/0") - self.add_text_input("fp", "FP", text="FP: 0/0") - self.add_text_input("exp", "EXP", text="EXP: 0%") - - self.region_id = "character_status" - data_collector.create_ocr_region( - self.region_id, x=250, y=50, w=180, h=130, - color=(255, 255, 0), thickness=2 - ) - - data_collector.start_collector() - self.set_name("Flyff - Character Status") - - # Set up a timer to periodically update character stats - self.timer = QTimer() - self.timer.timeout.connect(self.process_input) - self.timer.start(1000) # Update every second - - def parse_character_stats(self, raw_text): - """ - Extract HP, MP, FP, EXP from the raw OCR text lines. - """ - lines = [l.strip() for l in raw_text.splitlines() if l.strip()] - hp_current, hp_total = 0, 0 - mp_current, mp_total = 0, 0 - fp_current, fp_total = 0, 0 - exp_value = 0.0 - - if len(lines) >= 4: - # line 1: HP - hp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[0]) - if hp_match: - hp_current = int(hp_match.group(1)) - hp_total = int(hp_match.group(2)) - - # line 2: MP - mp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[1]) - if mp_match: - mp_current = int(mp_match.group(1)) - mp_total = int(mp_match.group(2)) - - # line 3: FP - fp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[2]) - if fp_match: - fp_current = int(fp_match.group(1)) - fp_total = int(fp_match.group(2)) - - # line 4: EXP - exp_match = re.search(r"(\d+(?:\.\d+)?)", lines[3]) - if exp_match: - val = float(exp_match.group(1)) - if val < 0: val = 0 - if val > 100: val = 100 - exp_value = val - - return hp_current, hp_total, mp_current, mp_total, fp_current, fp_total, exp_value - - def process_input(self): - """ - Called periodically to capture a screenshot and update character status (if enabled). - """ - # Always capture the screenshot, regardless of OCR status - screenshot_img = data_collector.capture_region_as_image(self.region_id) - if screenshot_img: - buf = BytesIO() - screenshot_img.save(buf, format='PNG') - image_b64 = base64.b64encode(buf.getvalue()).decode('utf-8') - data_manager.set_status_screenshot(image_b64) - - # If OCR is disabled, return early (skip OCR processing) - if self.get_property("data_collection") == "Disabled": - return - - # Process OCR if enabled - raw_text = data_collector.get_raw_text(self.region_id) - hp_c, hp_t, mp_c, mp_t, fp_c, fp_t, exp_v = self.parse_character_stats(raw_text) - - # Update data_manager with parsed values - data_manager.set_data_bulk({ - "hp_current": hp_c, - "hp_total": hp_t, - "mp_current": mp_c, - "mp_total": mp_t, - "fp_current": fp_c, - "fp_total": fp_t, - "exp": exp_v - }) - - # Update the node's UI text fields - self.set_property("hp", f"HP: {hp_c}/{hp_t}") - self.set_property("mp", f"MP: {mp_c}/{mp_t}") - self.set_property("fp", f"FP: {fp_c}/{fp_t}") - self.set_property("exp", f"EXP: {exp_v}%") diff --git a/Data/Nodes/Flyff/flyff_leveling_predictor_node.py b/Data/Nodes/Flyff/flyff_leveling_predictor_node.py deleted file mode 100644 index 43e56a2..0000000 --- a/Data/Nodes/Flyff/flyff_leveling_predictor_node.py +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env python3 -""" -Flyff - Leveling Predictor Node: -- Tracks the last N changes in EXP values. -- Calculates the average change rate and time intervals. -- Predicts the estimated time to reach level 100. -""" - -import time -import numpy as np -from OdenGraphQt import BaseNode -from PyQt5.QtCore import QTimer -from Modules import data_manager - -class FlyffLevelingPredictorNode(BaseNode): - __identifier__ = "bunny-lab.io.flyff_leveling_predictor_node" - NODE_NAME = "Flyff - Leveling Predictor" - - def __init__(self): - super(FlyffLevelingPredictorNode, self).__init__() - - # Input port for EXP values - self.add_input("exp", "EXP") - - # User-defined number of changes to track - self.add_text_input("exp_track_count", "# of EXP Changes to Track", text="7") - - # Output widgets - self.add_text_input("time_to_level", "Time to Level", text="Calculating...") - self.add_text_input("time_between_kills", "Time Between Kills", text="N/A") - self.add_text_input("exp_per_kill", "EXP Per Kill", text="N/A") - - # Internal tracking lists - self.exp_history = [] - self.time_intervals = [] - self.last_exp_value = None - self.last_update_time = None - - # Timer to periodically process EXP changes - self.timer = QTimer() - self.timer.timeout.connect(self.process_exp_change) - self.timer.start(1000) # Check for updates every second - - def reset_tracking_arrays(self): - """ - Resets the EXP history and time interval arrays when a level-up is detected. - """ - self.exp_history.clear() - self.time_intervals.clear() - self.last_exp_value = None - self.last_update_time = None - - def process_exp_change(self): - """ - Monitors changes in EXP values and calculates various statistics. - """ - exp_value = data_manager.get_data().get("exp", None) - if exp_value is None: - return - - exp_track_count = self.get_property("exp_track_count") - try: - exp_track_count = int(exp_track_count) - except ValueError: - exp_track_count = 7 # Default to 7 if invalid input - - # Reset if EXP value decreases (indicating a level-up) - if self.last_exp_value is not None and exp_value < self.last_exp_value: - self.reset_tracking_arrays() - - if self.last_exp_value is not None and exp_value != self.last_exp_value: - current_time = time.time() - - # Store EXP change history - self.exp_history.append(exp_value) - if len(self.exp_history) > exp_track_count: - self.exp_history.pop(0) - - # Store time intervals - if self.last_update_time is not None: - interval = current_time - self.last_update_time - self.time_intervals.append(interval) - if len(self.time_intervals) > exp_track_count: - self.time_intervals.pop(0) - - # Perform calculations - self.calculate_time_to_level() - self.calculate_additional_metrics() - - # Update last tracking values - self.last_update_time = current_time - - self.last_exp_value = exp_value - - def calculate_time_to_level(self): - """ - Calculates the estimated time to reach level 100 based on EXP change history. - """ - if len(self.exp_history) < 2 or len(self.time_intervals) < 1: - self.set_property("time_to_level", "Insufficient data") - return - - exp_deltas = np.diff(self.exp_history) # Compute EXP change per interval - avg_exp_change = np.mean(exp_deltas) if len(exp_deltas) > 0 else 0 - avg_time_change = np.mean(self.time_intervals) - - if avg_exp_change <= 0: - self.set_property("time_to_level", "Not gaining EXP") - return - - current_exp = self.exp_history[-1] - remaining_exp = 100.0 - current_exp # Distance to level 100 - - estimated_time = (remaining_exp / avg_exp_change) * avg_time_change - - # Convert estimated time into hours, minutes, and seconds - hours = int(estimated_time // 3600) - minutes = int((estimated_time % 3600) // 60) - seconds = int(estimated_time % 60) - - time_str = f"{hours}h {minutes}m {seconds}s" - self.set_property("time_to_level", time_str) - - def calculate_additional_metrics(self): - """ - Calculates and updates the "Time Between Kills" and "EXP Per Kill". - """ - if len(self.time_intervals) > 0: - avg_time_between_kills = np.mean(self.time_intervals) - minutes = int(avg_time_between_kills // 60) - seconds = int(avg_time_between_kills % 60) - self.set_property("time_between_kills", f"{minutes}m {seconds}s") - else: - self.set_property("time_between_kills", "N/A") - - if len(self.exp_history) > 1: - exp_deltas = np.diff(self.exp_history) - avg_exp_per_kill = np.mean(exp_deltas) if len(exp_deltas) > 0 else 0 - self.set_property("exp_per_kill", f"{avg_exp_per_kill:.2f}%") - else: - self.set_property("exp_per_kill", "N/A") diff --git a/Data/Nodes/Flyff/flyff_low_health_alert_node.py b/Data/Nodes/Flyff/flyff_low_health_alert_node.py deleted file mode 100644 index f1006a7..0000000 --- a/Data/Nodes/Flyff/flyff_low_health_alert_node.py +++ /dev/null @@ -1,134 +0,0 @@ -#!/usr/bin/env python3 - -""" -Standardized Flyff Low Health Alert Node: - - Monitors an input value (1 = health alert, 0 = normal). - - Displays a visual alert and plays a sound if enabled. - - Uses a global update timer for processing. - - Automatically processes float, int, and string values. -""" - -import time -from OdenGraphQt import BaseNode -from Qt import QtCore, QtWidgets, QtGui - -try: - import winsound - HAS_WINSOUND = True -except ImportError: - winsound = None - HAS_WINSOUND = False - -class OverlayCanvas(QtWidgets.QWidget): - """ - UI overlay for displaying a red warning box, which can be repositioned by dragging. - """ - def __init__(self, parent=None): - super().__init__(parent) - screen_geo = QtWidgets.QApplication.primaryScreen().geometry() - self.setGeometry(screen_geo) - self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint) - self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) - self.setVisible(False) - self.helper_LowHealthAlert = QtCore.QRect(250, 300, 900, 35) - self.dragging = False - self.drag_offset = None - - def paintEvent(self, event): - if not self.isVisible(): - return - painter = QtGui.QPainter(self) - painter.setPen(QtCore.Qt.NoPen) - painter.setBrush(QtGui.QColor(255, 0, 0)) - painter.drawRect(self.helper_LowHealthAlert) - font = QtGui.QFont("Arial", 14, QtGui.QFont.Bold) - painter.setFont(font) - painter.setPen(QtGui.QColor(255, 255, 255)) - text_x = self.helper_LowHealthAlert.center().x() - 50 - text_y = self.helper_LowHealthAlert.center().y() + 5 - painter.drawText(text_x, text_y, "LOW HEALTH") - - def toggle_alert(self, state): - self.setVisible(state == 1) - self.update() - - def mousePressEvent(self, event): - if event.button() == QtCore.Qt.LeftButton: - if self.helper_LowHealthAlert.contains(event.pos()): - self.dragging = True - self.drag_offset = event.pos() - self.helper_LowHealthAlert.topLeft() - super().mousePressEvent(event) - - def mouseMoveEvent(self, event): - if self.dragging: - new_top_left = event.pos() - self.drag_offset - self.helper_LowHealthAlert.moveTo(new_top_left) - self.update() - super().mouseMoveEvent(event) - - def mouseReleaseEvent(self, event): - if event.button() == QtCore.Qt.LeftButton: - self.dragging = False - super().mouseReleaseEvent(event) - -class FlyffLowHealthAlertNode(BaseNode): - __identifier__ = 'bunny-lab.io.flyff_low_health_alert_node' - NODE_NAME = 'Flyff - Low Health Alert' - - overlay_instance = None - last_beep_time = 0 - BEEP_INTERVAL_SECONDS = 2 - - def __init__(self): - super(FlyffLowHealthAlertNode, self).__init__() - self.add_checkbox('cb_1', '', 'Sound Alert', True) - self.add_checkbox('cb_2', '', 'Visual Alert', True) - self.add_input('Toggle (1 = On | 0 = Off)', color=(200, 100, 0)) - self.add_text_input('value', 'Current Value', text='0') - self.add_combo_menu('beep_interval', 'Beep Interval', items=["0.5s", "1.0s", "2.0s"]) - - if not FlyffLowHealthAlertNode.overlay_instance: - FlyffLowHealthAlertNode.overlay_instance = OverlayCanvas() - FlyffLowHealthAlertNode.overlay_instance.show() - - def process_input(self): - input_port = self.input(0) - value = input_port.connected_ports()[0].node().get_property('value') if input_port.connected_ports() else "0" - self.receive_data(value) - - def receive_data(self, data, source_port_name=None): - try: - if isinstance(data, str): - data = float(data) if '.' in data else int(data) - if isinstance(data, (float, int)): - data = 1 if data > 1 else 0 if data <= 0 else int(data) - else: - data = 0 - except ValueError: - data = 0 - - self.set_property('value', str(data)) - if self.get_property('cb_2'): - FlyffLowHealthAlertNode.overlay_instance.toggle_alert(data) - self.handle_beep(data) - - def handle_beep(self, input_value): - # Update beep interval from the dropdown property - interval_str = self.get_property('beep_interval') - if interval_str.endswith("s"): - interval_seconds = float(interval_str[:-1]) - else: - interval_seconds = float(interval_str) - self.BEEP_INTERVAL_SECONDS = interval_seconds - - if input_value == 1 and self.get_property('cb_1'): - current_time = time.time() - if (current_time - FlyffLowHealthAlertNode.last_beep_time) >= self.BEEP_INTERVAL_SECONDS: - FlyffLowHealthAlertNode.last_beep_time = current_time - self.play_beep() - - def play_beep(self): - if HAS_WINSOUND: - winsound.Beep(376, 100) - else: - print('\a', end='') diff --git a/Data/Nodes/Flyff/flyff_mob_identification_overlay.py b/Data/Nodes/Flyff/flyff_mob_identification_overlay.py deleted file mode 100644 index b8df463..0000000 --- a/Data/Nodes/Flyff/flyff_mob_identification_overlay.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python3 -""" -Identification Overlay Node: -- Users can configure threads/slices for parallel processing. -""" - -import re -from OdenGraphQt import BaseNode -from PyQt5.QtCore import QTimer -from PyQt5.QtGui import QColor -from Modules import data_collector - - -class IdentificationOverlayNode(BaseNode): - __identifier__ = "bunny-lab.io.identification_overlay_node" - NODE_NAME = "Identification Overlay" - - def __init__(self): - super(IdentificationOverlayNode, self).__init__() - - # User-configurable options - self.add_text_input("search_term", "Search Term", text="Aibatt") - self.add_text_input("offset_value", "Offset Value (X,Y)", text="0,0") # X,Y Offset - self.add_text_input("margin", "Margin", text="5") # Box Margin - self.add_text_input("polling_freq", "Polling Frequency (ms)", text="500") # Polling Rate - self.add_combo_menu("ocr_engine", "Type", items=["CPU", "GPU"]) - self.set_property("ocr_engine", "CPU") # Default to CPU mode - - # Custom overlay options - self.add_text_input("overlay_color", "Overlay Color (RGB)", text="0,0,255") # Default blue - self.add_text_input("thickness", "Line Thickness", text="2") # Default 2px - self.add_text_input("threads_slices", "Threads / Slices", text="8") # Default 8 threads/slices - - self.region_id = "identification_overlay" - data_collector.create_ocr_region(self.region_id, x=250, y=50, w=300, h=200, color=(0, 0, 255), thickness=2) - - data_collector.start_collector() - self.set_name("Identification Overlay") - - # Timer for updating overlays - self.timer = QTimer() - self.timer.timeout.connect(self.update_overlay) - - # Set initial polling frequency - self.update_polling_frequency() - - def update_polling_frequency(self): - polling_text = self.get_property("polling_freq") - try: - polling_interval = max(50, int(polling_text)) - except ValueError: - polling_interval = 500 - - self.timer.start(polling_interval) - - def update_overlay(self): - search_term = self.get_property("search_term") - offset_text = self.get_property("offset_value") - margin_text = self.get_property("margin") - ocr_engine = self.get_property("ocr_engine") - threads_slices_text = self.get_property("threads_slices") - - self.update_polling_frequency() - - try: - offset_x, offset_y = map(int, offset_text.split(",")) - except ValueError: - offset_x, offset_y = 0, 0 - - try: - margin = int(margin_text) - except ValueError: - margin = 5 - - color_text = self.get_property("overlay_color") - try: - color = tuple(map(int, color_text.split(","))) - except ValueError: - color = (0, 0, 255) - - thickness_text = self.get_property("thickness") - try: - thickness = max(1, int(thickness_text)) - except ValueError: - thickness = 2 - - try: - num_slices = max(1, int(threads_slices_text)) # Ensure at least 1 slice - except ValueError: - num_slices = 1 - - if not search_term: - return - - detected_positions = data_collector.find_word_positions( - self.region_id, search_term, offset_x, offset_y, margin, ocr_engine, num_slices - ) - - # Ensure slice count is updated visually in the region widget - data_collector.update_region_slices(self.region_id, num_slices) - - data_collector.draw_identification_boxes(self.region_id, detected_positions, color=color, thickness=thickness) - diff --git a/Data/Nodes/General Purpose/array_node.py b/Data/Nodes/General Purpose/array_node.py deleted file mode 100644 index 8f4e09e..0000000 --- a/Data/Nodes/General Purpose/array_node.py +++ /dev/null @@ -1,49 +0,0 @@ -from OdenGraphQt import BaseNode - -class ArrayNode(BaseNode): - """ - Array Node: - - Inputs: 'in' (value to store), 'ArraySize' (defines maximum length) - - Output: 'Array' (the current array as a string) - - Stores incoming values in an array with a size defined by ArraySize. - - Updates are now handled via a global update timer. - """ - __identifier__ = 'bunny-lab.io.array_node' - NODE_NAME = 'Array' - - def __init__(self): - super(ArrayNode, self).__init__() - self.values = {} # Ensure values is a dictionary. - self.add_input('in') - self.add_input('ArraySize') - self.add_output('Array') - self.array = [] - self.value = "[]" # Output as a string. - self.array_size = 10 # Default array size. - self.set_name("Array: []") - - def process_input(self): - # Get array size from 'ArraySize' input if available. - size_port = self.input('ArraySize') - connected_size = size_port.connected_ports() if size_port is not None else [] - if connected_size: - connected_port = connected_size[0] - parent_node = connected_port.node() - try: - self.array_size = int(float(getattr(parent_node, 'value', 10))) - except (ValueError, TypeError): - self.array_size = 10 - - # Get new value from 'in' input if available. - in_port = self.input('in') - connected_in = in_port.connected_ports() if in_port is not None else [] - if connected_in: - connected_port = connected_in[0] - parent_node = connected_port.node() - new_value = getattr(parent_node, 'value', None) - if new_value is not None: - self.array.append(new_value) - while len(self.array) > self.array_size: - self.array.pop(0) - self.value = str(self.array) - self.set_name(f"Array: {self.value}") diff --git a/Data/Nodes/General Purpose/comparison_node.py b/Data/Nodes/General Purpose/comparison_node.py deleted file mode 100644 index 34ad35a..0000000 --- a/Data/Nodes/General Purpose/comparison_node.py +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env python3 - -""" -Standardized Comparison Node: - - Compares two input values using a selected operator (==, !=, >, <, >=, <=). - - Outputs a result of 1 (True) or 0 (False). - - Uses a global update timer for processing. - - Supports an additional 'Input Type' dropdown to choose between 'Number' and 'String'. -""" - -from OdenGraphQt import BaseNode -from Qt import QtCore - -class ComparisonNode(BaseNode): - __identifier__ = 'bunny-lab.io.comparison_node' - NODE_NAME = 'Comparison Node' - - def __init__(self): - super(ComparisonNode, self).__init__() - self.add_input('A') - self.add_input('B') - self.add_output('Result') - - # Add the Input Type dropdown first. - self.add_combo_menu('input_type', 'Input Type', items=['Number', 'String']) - self.add_combo_menu('operator', 'Operator', items=[ - 'Equal (==)', 'Not Equal (!=)', 'Greater Than (>)', - 'Less Than (<)', 'Greater Than or Equal (>=)', 'Less Than or Equal (<=)' - ]) - # Replace calc_result with a standardized "value" text input. - self.add_text_input('value', 'Value', text='0') - self.value = 0 - self.set_name("Comparison Node") - self.processing = False # Guard for process_input - - # Set default properties explicitly - self.set_property('input_type', 'Number') - self.set_property('operator', 'Equal (==)') - - def process_input(self): - if self.processing: - return - self.processing = True - - # Retrieve input values; if no connection or None, default to "0" - input_a = self.input(0) - input_b = self.input(1) - a_raw = (input_a.connected_ports()[0].node().get_property('value') - if input_a.connected_ports() else "0") - b_raw = (input_b.connected_ports()[0].node().get_property('value') - if input_b.connected_ports() else "0") - a_raw = a_raw if a_raw is not None else "0" - b_raw = b_raw if b_raw is not None else "0" - - # Get input type property - input_type = self.get_property('input_type') - - # Convert values based on input type - if input_type == 'Number': - try: - a_val = float(a_raw) - except (ValueError, TypeError): - a_val = 0.0 - try: - b_val = float(b_raw) - except (ValueError, TypeError): - b_val = 0.0 - elif input_type == 'String': - a_val = str(a_raw) - b_val = str(b_raw) - else: - try: - a_val = float(a_raw) - except (ValueError, TypeError): - a_val = 0.0 - try: - b_val = float(b_raw) - except (ValueError, TypeError): - b_val = 0.0 - - operator = self.get_property('operator') - - # Perform the comparison - result = { - 'Equal (==)': a_val == b_val, - 'Not Equal (!=)': a_val != b_val, - 'Greater Than (>)': a_val > b_val, - 'Less Than (<)': a_val < b_val, - 'Greater Than or Equal (>=)': a_val >= b_val, - 'Less Than or Equal (<=)': a_val <= b_val - }.get(operator, False) - - new_value = 1 if result else 0 - self.value = new_value - self.set_property('value', str(self.value)) - self.transmit_data(self.value) - - self.processing = False - - def on_input_connected(self, input_port, output_port): - pass - - def on_input_disconnected(self, input_port, output_port): - pass - - def property_changed(self, property_name): - pass - - def receive_data(self, data, source_port_name=None): - pass - - def transmit_data(self, data): - output_port = self.output(0) - if output_port and output_port.connected_ports(): - for connected_port in output_port.connected_ports(): - connected_node = connected_port.node() - if hasattr(connected_node, 'receive_data'): - try: - data_int = int(data) - connected_node.receive_data(data_int, source_port_name='Result') - except ValueError: - pass diff --git a/Data/Nodes/General Purpose/data_node.py b/Data/Nodes/General Purpose/data_node.py deleted file mode 100644 index 6803833..0000000 --- a/Data/Nodes/General Purpose/data_node.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python3 - -""" -Standardized Data Node: - - Accepts and transmits values consistently. - - Updates its value based on a global update timer. -""" - -from OdenGraphQt import BaseNode -from Qt import QtCore - -class DataNode(BaseNode): - __identifier__ = 'bunny-lab.io.data_node' - NODE_NAME = 'Data Node' - - def __init__(self): - super(DataNode, self).__init__() - self.add_input('Input') - self.add_output('Output') - self.add_text_input('value', 'Value', text='') - self.process_widget_event() - self.set_name("Data Node") - # Removed self-contained update timer; global timer now drives updates. - - def post_create(self): - text_widget = self.get_widget('value') - if text_widget is not None: - try: - # Removed textChanged signal connection; global timer will call process_input. - pass - except Exception as e: - print("Error connecting textChanged signal:", e) - - def process_widget_event(self, event=None): - current_text = self.get_property('value') - self.value = current_text - self.transmit_data(current_text) - - def property_changed(self, property_name): - if property_name == 'value': - # Immediate update removed; relying on global timer. - pass - - def process_input(self): - input_port = self.input(0) - output_port = self.output(0) - if input_port.connected_ports(): - input_value = input_port.connected_ports()[0].node().get_property('value') - self.set_property('value', input_value) - self.transmit_data(input_value) - elif output_port.connected_ports(): - self.transmit_data(self.get_property('value')) - - def on_input_connected(self, input_port, output_port): - # Removed immediate update; global timer handles updates. - pass - - def on_input_disconnected(self, input_port, output_port): - # Removed immediate update; global timer handles updates. - pass - - def receive_data(self, data, source_port_name=None): - self.set_property('value', str(data)) - self.transmit_data(data) - - def transmit_data(self, data): - output_port = self.output(0) - if output_port and output_port.connected_ports(): - for connected_port in output_port.connected_ports(): - connected_node = connected_port.node() - if hasattr(connected_node, 'receive_data'): - connected_node.receive_data(data, source_port_name="Output") diff --git a/Data/Nodes/General Purpose/math_operation_node.py b/Data/Nodes/General Purpose/math_operation_node.py deleted file mode 100644 index 1aea0fa..0000000 --- a/Data/Nodes/General Purpose/math_operation_node.py +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env python3 - -""" -Standardized Math Operation Node: - - Performs mathematical operations (+, -, *, /, avg) on two inputs. - - Outputs the computed result. - - Uses a global update timer for processing (defined in borealis.py). - - Ensures it always has a "value" property that the Comparison Node can read. -""" - -from OdenGraphQt import BaseNode -from Qt import QtCore - -class MathOperationNode(BaseNode): - __identifier__ = 'bunny-lab.io.math_node' - NODE_NAME = 'Math Operation' - - def __init__(self): - super(MathOperationNode, self).__init__() - self.add_input('A') - self.add_input('B') - self.add_output('Result') - - # Drop-down to choose which operation we do: - self.add_combo_menu('operator', 'Operator', items=[ - 'Add', 'Subtract', 'Multiply', 'Divide', 'Average' - ]) - - # A text field for showing the result to the user: - self.add_text_input('calc_result', 'Result', text='0') - - # IMPORTANT: define a "value" property that the Comparison Node can read - # We do not necessarily need a text input for it, but adding it ensures - # it becomes an official property recognized by OdenGraphQt. - self.add_text_input('value', 'Internal Value', text='0') - - # Keep a Python-side float of the current computed result: - self.value = 0 - - # Give the node a nice name: - self.set_name("Math Operation") - - # Removed self-contained timer; global timer calls process_input(). - - def process_input(self): - # Attempt to read "value" from both inputs: - input_a = self.input(0) - input_b = self.input(1) - a_raw = input_a.connected_ports()[0].node().get_property('value') if input_a.connected_ports() else "0" - b_raw = input_b.connected_ports()[0].node().get_property('value') if input_b.connected_ports() else "0" - - try: - a_val = float(a_raw) - except (ValueError, TypeError): - a_val = 0.0 - try: - b_val = float(b_raw) - except (ValueError, TypeError): - b_val = 0.0 - - operator = self.get_property('operator') - if operator == 'Add': - result = a_val + b_val - elif operator == 'Subtract': - result = a_val - b_val - elif operator == 'Multiply': - result = a_val * b_val - elif operator == 'Divide': - result = a_val / b_val if b_val != 0 else 0.0 - elif operator == 'Average': - result = (a_val + b_val) / 2.0 - else: - result = 0.0 - - # If the computed result changed, update our internal properties and transmit - if self.value != result: - self.value = result - - # Update the two text fields so the user sees the numeric result: - self.set_property('calc_result', str(result)) - self.set_property('value', str(result)) # <= This is the critical step - - # Let downstream nodes know there's new data: - self.transmit_data(result) - - def on_input_connected(self, input_port, output_port): - pass - - def on_input_disconnected(self, input_port, output_port): - pass - - def property_changed(self, property_name): - pass - - def receive_data(self, data, source_port_name=None): - pass - - def transmit_data(self, data): - output_port = self.output(0) - if output_port and output_port.connected_ports(): - for connected_port in output_port.connected_ports(): - connected_node = connected_port.node() - if hasattr(connected_node, 'receive_data'): - try: - # Attempt to convert to int if possible, else float - data_int = int(data) - connected_node.receive_data(data_int, source_port_name='Result') - except ValueError: - connected_node.receive_data(data, source_port_name='Result') diff --git a/Data/Nodes/Organization/backdrop_node.py b/Data/Nodes/Organization/backdrop_node.py deleted file mode 100644 index e2167f3..0000000 --- a/Data/Nodes/Organization/backdrop_node.py +++ /dev/null @@ -1,161 +0,0 @@ -#!/usr/bin/env python3 - -from Qt import QtWidgets, QtGui, QtCore -from OdenGraphQt import BaseNode -from OdenGraphQt.constants import NodePropWidgetEnum -from OdenGraphQt.qgraphics.node_backdrop import BackdropNodeItem - -class BackdropNode(BaseNode): - """ - Backdrop Node: - - Allows grouping or annotating other nodes by resizing a large rectangle. - - Title is set by double-clicking in the title area. - """ - - __identifier__ = 'bunny-lab.io.backdrop' - NODE_NAME = 'Backdrop' - - def __init__(self): - # Use BackdropNodeItem for the specialized QGraphicsItem. - super(BackdropNode, self).__init__(qgraphics_item=BackdropNodeItem) - - # Default color (teal). - self.model.color = (5, 129, 138, 255) - - # Set default title without prompting: - self.set_name("Double-Click to Add Name to Backdrop") - - # Multi-line text property for storing the backdrop text. - self.create_property( - 'backdrop_text', - '', - widget_type=NodePropWidgetEnum.QTEXT_EDIT.value, - tab='Backdrop' - ) - - # Override the view's double-click event to allow editing the title. - original_double_click = self.view.mouseDoubleClickEvent - - def new_double_click_event(event): - # Assume the title is in the top 30 pixels of the node. - if event.pos().y() < 30: - new_title, ok = QtWidgets.QInputDialog.getText( - None, "Edit Title", "Enter new backdrop title:", text=self.name() - ) - if ok and new_title: - self.set_name(new_title) - self.view.update() # force immediate update of the node title - else: - if original_double_click: - original_double_click(event) - - self.view.mouseDoubleClickEvent = new_double_click_event - - # -------------------------------------------------------------------------- - # Resizing / Geometry - # -------------------------------------------------------------------------- - def on_backdrop_updated(self, update_prop, value=None): - """ - Triggered when the user resizes or double-clicks the backdrop sizer handle. - """ - if not self.graph: - return - - if update_prop == 'sizer_mouse_release': - # User finished dragging the resize handle - self.view.prepareGeometryChange() - self.graph.begin_undo(f'resized "{self.name()}"') - self.set_property('width', value['width']) - self.set_property('height', value['height']) - self.set_pos(*value['pos']) - self.graph.end_undo() - self.view.update() - - elif update_prop == 'sizer_double_clicked': - # User double-clicked the resize handle (auto-resize) - self.view.prepareGeometryChange() - self.graph.begin_undo(f'"{self.name()}" auto resize') - self.set_property('width', value['width']) - self.set_property('height', value['height']) - self.set_pos(*value['pos']) - self.graph.end_undo() - self.view.update() - - def auto_size(self): - """ - Auto-resize the backdrop to fit around intersecting nodes. - """ - if not self.graph: - return - self.view.prepareGeometryChange() - self.graph.begin_undo(f'"{self.name()}" auto resize') - size = self.view.calc_backdrop_size() - self.set_property('width', size['width']) - self.set_property('height', size['height']) - self.set_pos(*size['pos']) - self.graph.end_undo() - self.view.update() - - def wrap_nodes(self, nodes): - """ - Fit the backdrop around the specified nodes. - """ - if not self.graph or not nodes: - return - self.view.prepareGeometryChange() - self.graph.begin_undo(f'"{self.name()}" wrap nodes') - size = self.view.calc_backdrop_size([n.view for n in nodes]) - self.set_property('width', size['width']) - self.set_property('height', size['height']) - self.set_pos(*size['pos']) - self.graph.end_undo() - self.view.update() - - def nodes(self): - """ - Return a list of nodes wrapped by this backdrop. - """ - node_ids = [n.id for n in self.view.get_nodes()] - return [self.graph.get_node_by_id(nid) for nid in node_ids] - - def set_text(self, text=''): - """ - Set the multi-line text in the backdrop. - """ - self.set_property('backdrop_text', text) - - def text(self): - """ - Return the text content in the backdrop. - """ - return self.get_property('backdrop_text') - - def set_size(self, width, height): - """ - Manually set the backdrop size. - """ - if self.graph: - self.view.prepareGeometryChange() - self.graph.begin_undo('backdrop size') - self.set_property('width', width) - self.set_property('height', height) - self.graph.end_undo() - self.view.update() - else: - self.view.width, self.view.height = width, height - self.model.width, self.model.height = width, height - - def size(self): - """ - Return (width, height) of the backdrop. - """ - self.model.width = self.view.width - self.model.height = self.view.height - return self.model.width, self.model.height - - # No ports for a backdrop: - def inputs(self): - return - - def outputs(self): - return diff --git a/Data/Nodes/Reporting/Export_to_CSV.py b/Data/Nodes/Reporting/Export_to_CSV.py deleted file mode 100644 index 89b1985..0000000 --- a/Data/Nodes/Reporting/Export_to_CSV.py +++ /dev/null @@ -1,3 +0,0 @@ -# HIGH-LEVEL OVERVIEW -# - This node takes an input source and either replaces or appends data fed into it into a CSV file on disk. -# - There will be a checkbox to allow the user to change the behavior (Replace / Append) \ No newline at end of file diff --git a/Data/Nodes/Reporting/Export_to_Image.py b/Data/Nodes/Reporting/Export_to_Image.py deleted file mode 100644 index 5cec7e8..0000000 --- a/Data/Nodes/Reporting/Export_to_Image.py +++ /dev/null @@ -1,4 +0,0 @@ -# HIGH-LEVEL OVERVIEW -# - This node takes an input source and dumps the data to disk in a dropdown menu of various image formats -# - Ability to view image processing results would be an interesting bonus if displayed within the node. -# - Could be used to show the life cycle of an image processing pipeline. \ No newline at end of file diff --git a/Data/Nodes/__init__.py b/Data/Nodes/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/Data/Workflows/Flyff/Flyff - Low Health Alert.json b/Data/Workflows/Flyff/Flyff - Low Health Alert.json deleted file mode 100644 index 19bcc9b..0000000 --- a/Data/Workflows/Flyff/Flyff - Low Health Alert.json +++ /dev/null @@ -1,379 +0,0 @@ -{ - "graph":{ - "layout_direction":0, - "acyclic":true, - "pipe_collision":false, - "pipe_slicing":true, - "pipe_style":1, - "accept_connection_types":{}, - "reject_connection_types":{} - }, - "nodes":{ - "0x2697e9777d0":{ - "type_":"bunny-lab.io.flyff_character_status_node.FlyffCharacterStatusNode", - "icon":null, - "name":"Flyff - Character Status", - "color":[ - 13, - 18, - 23, - 255 - ], - "border_color":[ - 74, - 84, - 85, - 255 - ], - "text_color":[ - 255, - 255, - 255, - 180 - ], - "disabled":false, - "selected":false, - "visible":true, - "width":278.0, - "height":200.20000000000002, - "pos":[ - -162.4474451079301, - 412.29351565404465 - ], - "layout_direction":0, - "port_deletion_allowed":false, - "subgraph_session":{}, - "custom":{ - "hp":"HP: 0/0", - "mp":"MP: 0/0", - "fp":"FP: 0/0", - "exp":"EXP: 0.0%" - } - }, - "0x2697f589250":{ - "type_":"bunny-lab.io.data_node.DataNode", - "icon":null, - "name":"Data Node", - "color":[ - 13, - 18, - 23, - 255 - ], - "border_color":[ - 74, - 84, - 85, - 255 - ], - "text_color":[ - 255, - 255, - 255, - 180 - ], - "disabled":false, - "selected":false, - "visible":true, - "width":269.0, - "height":74.2, - "pos":[ - -46.54926789642434, - 276.44565220121416 - ], - "layout_direction":0, - "port_deletion_allowed":false, - "subgraph_session":{}, - "custom":{ - "value":"0.40" - } - }, - "0x2697eeb2960":{ - "type_":"bunny-lab.io.math_node.MathOperationNode", - "icon":null, - "name":"Math Operation", - "color":[ - 13, - 18, - 23, - 255 - ], - "border_color":[ - 74, - 84, - 85, - 255 - ], - "text_color":[ - 255, - 255, - 255, - 180 - ], - "disabled":false, - "selected":false, - "visible":true, - "width":269.0, - "height":162.4, - "pos":[ - 263.14586137366473, - 175.74723593547986 - ], - "layout_direction":0, - "port_deletion_allowed":false, - "subgraph_session":{}, - "custom":{ - "operator":"Multiply", - "calc_result":"0.0", - "value":"0.0" - } - }, - "0x2697ea1b560":{ - "type_":"bunny-lab.io.flyff_hp_current_node.FlyffHPCurrentNode", - "icon":null, - "name":"Flyff - HP Current (API Connected)", - "color":[ - 13, - 18, - 23, - 255 - ], - "border_color":[ - 74, - 84, - 85, - 255 - ], - "text_color":[ - 255, - 255, - 255, - 180 - ], - "disabled":false, - "selected":false, - "visible":true, - "width":378.0, - "height":74.2, - "pos":[ - 188.09704170391905, - 29.44953683243171 - ], - "layout_direction":0, - "port_deletion_allowed":false, - "subgraph_session":{}, - "custom":{ - "value":"0" - } - }, - "0x2697f589be0":{ - "type_":"bunny-lab.io.flyff_hp_total_node.FlyffHPTotalNode", - "icon":null, - "name":"Flyff - HP Total (API Connected)", - "color":[ - 13, - 18, - 23, - 255 - ], - "border_color":[ - 74, - 84, - 85, - 255 - ], - "text_color":[ - 255, - 255, - 255, - 180 - ], - "disabled":false, - "selected":false, - "visible":true, - "width":364.0, - "height":74.2, - "pos":[ - -138.69781863016254, - 175.74723593547975 - ], - "layout_direction":0, - "port_deletion_allowed":false, - "subgraph_session":{}, - "custom":{ - "value":"0" - } - }, - "0x2697eb0e8d0":{ - "type_":"bunny-lab.io.backdrop.BackdropNode", - "icon":null, - "name":"Calculate 40% of Total HP", - "color":[ - 5, - 129, - 138, - 255 - ], - "border_color":[ - 74, - 84, - 85, - 255 - ], - "text_color":[ - 255, - 255, - 255, - 180 - ], - "disabled":false, - "selected":false, - "visible":true, - "width":728.2402137175101, - "height":257.0476243986018, - "pos":[ - -164.34741522615138, - 125.39802780261283 - ], - "layout_direction":0, - "port_deletion_allowed":false, - "subgraph_session":{}, - "custom":{ - "backdrop_text":"" - } - }, - "0x2697e856d20":{ - "type_":"bunny-lab.io.comparison_node.ComparisonNode", - "icon":null, - "name":"Comparison Node", - "color":[ - 13, - 18, - 23, - 255 - ], - "border_color":[ - 74, - 84, - 85, - 255 - ], - "text_color":[ - 255, - 255, - 255, - 180 - ], - "disabled":false, - "selected":false, - "visible":true, - "width":322.0, - "height":166.6, - "pos":[ - 625.0901688948422, - 218.49656359546154 - ], - "layout_direction":0, - "port_deletion_allowed":false, - "subgraph_session":{}, - "custom":{ - "input_type":"Number", - "operator":"Less Than or Equal (<=)", - "value":"1" - } - }, - "0x2697eeb1100":{ - "type_":"bunny-lab.io.flyff_low_health_alert_node.FlyffLowHealthAlertNode", - "icon":null, - "name":"Flyff - Low Health Alert", - "color":[ - 13, - 18, - 23, - 255 - ], - "border_color":[ - 74, - 84, - 85, - 255 - ], - "text_color":[ - 255, - 255, - 255, - 180 - ], - "disabled":false, - "selected":false, - "visible":true, - "width":324.0, - "height":181.3, - "pos":[ - 630.7900792495066, - 585.1907964121928 - ], - "layout_direction":0, - "port_deletion_allowed":false, - "subgraph_session":{}, - "custom":{ - "cb_1":true, - "cb_2":true, - "value":"1", - "beep_interval":"1.0s" - } - } - }, - "connections":[ - { - "out":[ - "0x2697f589250", - "Output" - ], - "in":[ - "0x2697eeb2960", - "B" - ] - }, - { - "in":[ - "0x2697eeb2960", - "A" - ], - "out":[ - "0x2697f589be0", - "value" - ] - }, - { - "out":[ - "0x2697eeb2960", - "Result" - ], - "in":[ - "0x2697e856d20", - "B" - ] - }, - { - "out":[ - "0x2697ea1b560", - "value" - ], - "in":[ - "0x2697e856d20", - "A" - ] - }, - { - "out":[ - "0x2697e856d20", - "Result" - ], - "in":[ - "0x2697eeb1100", - "Toggle (1 = On | 0 = Off)" - ] - } - ] -} \ No newline at end of file diff --git a/Data/Workflows/Flyff/Flyff EXP Predictor.json b/Data/Workflows/Flyff/Flyff EXP Predictor.json deleted file mode 100644 index 2f64762..0000000 --- a/Data/Workflows/Flyff/Flyff EXP Predictor.json +++ /dev/null @@ -1,183 +0,0 @@ -{ - "graph":{ - "layout_direction":0, - "acyclic":true, - "pipe_collision":false, - "pipe_slicing":true, - "pipe_style":1, - "accept_connection_types":{}, - "reject_connection_types":{} - }, - "nodes":{ - "0x191410fec90":{ - "type_":"bunny-lab.io.flyff_character_status_node.FlyffCharacterStatusNode", - "icon":null, - "name":"Flyff - Character Status", - "color":[ - 13, - 18, - 23, - 255 - ], - "border_color":[ - 74, - 84, - 85, - 255 - ], - "text_color":[ - 255, - 255, - 255, - 180 - ], - "disabled":false, - "selected":false, - "visible":true, - "width":278.0, - "height":200.20000000000002, - "pos":[ - -234.47843187544638, - 171.50740184739476 - ], - "layout_direction":0, - "port_deletion_allowed":false, - "subgraph_session":{}, - "custom":{ - "hp":"HP: 5848/5848", - "mp":"MP: 955/555", - "fp":"FP: 0/0", - "exp":"EXP: 49.0%" - } - }, - "0x19173496de0":{ - "type_":"bunny-lab.io.flyff_exp_current_node.FlyffEXPCurrentNode", - "icon":null, - "name":"Flyff - EXP (API Connected)", - "color":[ - 13, - 18, - 23, - 255 - ], - "border_color":[ - 74, - 84, - 85, - 255 - ], - "text_color":[ - 255, - 255, - 255, - 180 - ], - "disabled":false, - "selected":false, - "visible":true, - "width":339.0, - "height":74.2, - "pos":[ - -237.34556433027646, - 77.62806051403777 - ], - "layout_direction":0, - "port_deletion_allowed":false, - "subgraph_session":{}, - "custom":{ - "value":"49.0" - } - }, - "0x191735ae690":{ - "type_":"bunny-lab.io.flyff_leveling_predictor_node.FlyffLevelingPredictorNode", - "icon":null, - "name":"Flyff - Leveling Predictor", - "color":[ - 13, - 18, - 23, - 255 - ], - "border_color":[ - 74, - 84, - 85, - 255 - ], - "text_color":[ - 255, - 255, - 255, - 180 - ], - "disabled":false, - "selected":false, - "visible":true, - "width":324.0, - "height":200.20000000000002, - "pos":[ - 170.42482250783007, - 77.62806051403777 - ], - "layout_direction":0, - "port_deletion_allowed":false, - "subgraph_session":{}, - "custom":{ - "exp_track_count":"7", - "time_to_level":"Insufficient data", - "time_between_kills":"N/A", - "exp_per_kill":"N/A" - } - }, - "0x191735ae9c0":{ - "type_":"bunny-lab.io.backdrop.BackdropNode", - "icon":null, - "name":"Track EXP Changes Over Time to Predict Leveling Up", - "color":[ - 5, - 129, - 138, - 255 - ], - "border_color":[ - 74, - 84, - 85, - 255 - ], - "text_color":[ - 255, - 255, - 255, - 180 - ], - "disabled":false, - "selected":false, - "visible":true, - "width":777.8842478973615, - "height":380.82117975084645, - "pos":[ - -264.113861059255, - 23.199190498448075 - ], - "layout_direction":0, - "port_deletion_allowed":false, - "subgraph_session":{}, - "custom":{ - "backdrop_text":"" - } - } - }, - "connections":[ - { - "out":[ - "0x19173496de0", - "value" - ], - "in":[ - "0x191735ae690", - "exp" - ] - } - ] - } \ No newline at end of file diff --git a/Data/Workflows/Testing/Basic_Data_Node_Connection.json b/Data/Workflows/Testing/Basic_Data_Node_Connection.json deleted file mode 100644 index 902306b..0000000 --- a/Data/Workflows/Testing/Basic_Data_Node_Connection.json +++ /dev/null @@ -1,101 +0,0 @@ -{ - "graph":{ - "layout_direction":0, - "acyclic":true, - "pipe_collision":false, - "pipe_slicing":true, - "pipe_style":1, - "accept_connection_types":{}, - "reject_connection_types":{} - }, - "nodes":{ - "0x1ad82a5c620":{ - "type_":"bunny-lab.io.data_node.DataNode", - "icon":null, - "name":"Data Node", - "color":[ - 13, - 18, - 23, - 255 - ], - "border_color":[ - 74, - 84, - 85, - 255 - ], - "text_color":[ - 255, - 255, - 255, - 180 - ], - "disabled":false, - "selected":false, - "visible":true, - "width":269.0, - "height":74.2, - "pos":[ - -93.6890385514249, - 181.13214119942148 - ], - "layout_direction":0, - "port_deletion_allowed":false, - "subgraph_session":{}, - "custom":{ - "value":"57" - } - }, - "0x1ad82a5cef0":{ - "type_":"bunny-lab.io.data_node.DataNode", - "icon":null, - "name":"Data Node 1", - "color":[ - 13, - 18, - 23, - 255 - ], - "border_color":[ - 74, - 84, - 85, - 255 - ], - "text_color":[ - 255, - 255, - 255, - 180 - ], - "disabled":false, - "selected":false, - "visible":true, - "width":269.0, - "height":74.2, - "pos":[ - 361.37200584121035, - 287.313051557703 - ], - "layout_direction":0, - "port_deletion_allowed":false, - "subgraph_session":{}, - "custom":{ - "value":"57" - } - } - }, - "connections":[ - { - "out":[ - "0x1ad82a5c620", - "Output" - ], - "in":[ - "0x1ad82a5cef0", - "Input" - ] - } - ] -} \ No newline at end of file diff --git a/Data/Workflows/Testing/Identification_Overlay.json b/Data/Workflows/Testing/Identification_Overlay.json deleted file mode 100644 index 834a649..0000000 --- a/Data/Workflows/Testing/Identification_Overlay.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "graph":{ - "layout_direction":0, - "acyclic":true, - "pipe_collision":false, - "pipe_slicing":true, - "pipe_style":1, - "accept_connection_types":{}, - "reject_connection_types":{} - }, - "nodes":{ - "0x20c129abb30":{ - "type_":"bunny-lab.io.identification_overlay_node.IdentificationOverlayNode", - "icon":null, - "name":"Identification Overlay", - "color":[ - 13, - 18, - 23, - 255 - ], - "border_color":[ - 74, - 84, - 85, - 255 - ], - "text_color":[ - 255, - 255, - 255, - 180 - ], - "disabled":false, - "selected":false, - "visible":true, - "width":271.0, - "height":330.40000000000003, - "pos":[ - 44.64929777820301, - 256.49596595988965 - ], - "layout_direction":0, - "port_deletion_allowed":false, - "subgraph_session":{}, - "custom":{ - "search_term":"Aibatt", - "offset_value":"-10,-10", - "margin":"10", - "polling_freq":"50", - "ocr_engine":"GPU", - "overlay_color":"255,255,255", - "thickness":"5" - } - } - } -} \ No newline at end of file diff --git a/Data/borealis.py b/Data/borealis.py deleted file mode 100644 index 32d1d6d..0000000 --- a/Data/borealis.py +++ /dev/null @@ -1,440 +0,0 @@ -# -*- coding: utf-8 -*- -#!/usr/bin/env python3 - -import sys -import pkgutil -import importlib -import inspect -import os - -from Qt import QtWidgets, QtCore, QtGui - -# -------------------------------------------------------# -# MONKEY PATCHES - MODIFICATIONS TO OdenGraphQT BEHAVIOR # -# -------------------------------------------------------# - -# PATCH: Override the color of interconnection pipes between nodes -try: - from OdenGraphQt.qgraphics.pipe import PipeItem - from OdenGraphQt.qgraphics.node_base import NodeItem - from qtpy.QtGui import QPen, QColor - from qtpy import QtCore - - # If you want the original paint logic, capture it first: - _orig_paint_pipe = PipeItem.paint - _orig_paint_node = NodeItem.paint - - # Custom pipe painting function - def _custom_paint_pipe(self, painter, option, widget=None): - painter.save() - my_pen = QPen(QColor(0, 161, 115, 255)) # Match desired RGBA - my_pen.setWidthF(2.0) - painter.setPen(my_pen) - _orig_paint_pipe(self, painter, option, widget) - painter.restore() - - # Custom node painting function - def _custom_paint_node(self, painter, option, widget=None): - painter.save() - _orig_paint_node(self, painter, option, widget) # Call original method - if self.isSelected(): - pen = QPen(QColor(0, 161, 115, 255)) # Set selected border color - pen.setWidth(3) - painter.setPen(pen) - painter.drawRect(self.boundingRect()) - painter.restore() - - # Apply the patches - PipeItem.paint = _custom_paint_pipe - NodeItem.paint = _custom_paint_node - -except ImportError as e: - print(f"WARNING: Could not patch PipeItem or NodeItem: {e}") -except Exception as e: - print(f"Patch for PipeItem or NodeItem override failed: {e}") - -## PATCH: Fix "module 'qtpy.QtGui' has no attribute 'QUndoStack'" (KEEP AROUND FOR LEGACY DOCUMENTATION) -#try: -# from qtpy.QtWidgets import QUndoStack -# import qtpy -# qtpy.QtGui.QUndoStack = QUndoStack -#except ImportError: -# print("WARNING: Could not monkey-patch QUndoStack.") - -# PATCH: Fix "'BackdropNodeItem' object has no attribute 'widgets'" by giving BackdropNodeItem a trivial widgets dictionary. -try: - from OdenGraphQt.nodes.backdrop_node import BackdropNodeItem - if not hasattr(BackdropNodeItem, "widgets"): - BackdropNodeItem.widgets = {} -except ImportError: - print("WARNING: Could not monkey-patch BackdropNodeItem to add `widgets`.") - -# PATCH: BEGIN ROBUST PATCH FOR QGraphicsScene.setSelectionArea -_original_setSelectionArea = QtWidgets.QGraphicsScene.setSelectionArea - -def _patched_setSelectionArea(self, *args, **kwargs): - """ - A robust patch that handles various call signatures for QGraphicsScene.setSelectionArea(). - """ - try: - return _original_setSelectionArea(self, *args, **kwargs) - except TypeError: - if not args: - raise - painterPath = args[0] - selection_op = QtCore.Qt.ReplaceSelection - selection_mode = QtCore.Qt.IntersectsItemShape - transform = QtGui.QTransform() - return _original_setSelectionArea(self, painterPath, selection_op, selection_mode, transform) - -QtWidgets.QGraphicsScene.setSelectionArea = _patched_setSelectionArea - -# ----------------------------------------------------------------------------------------------------- # - -# Import data_manager so we can start the Flask server -from Modules import data_manager - -from OdenGraphQt import NodeGraph, BaseNode -from OdenGraphQt.widgets.dialogs import FileDialog - -def import_nodes_from_folder(package_name): - """ - Recursively import all modules from the given package. - Returns a dictionary where keys are subfolder names, and values are lists of BaseNode subclasses. - """ - nodes_by_category = {} - package = importlib.import_module(package_name) - package_path = package.__path__[0] - - for root, _, files in os.walk(package_path): - rel_path = os.path.relpath(root, package_path).replace(os.sep, '.') - module_prefix = f"{package_name}.{rel_path}" if rel_path != '.' else package_name - category_name = os.path.basename(root) - - for file in files: - if file.endswith(".py") and file != "__init__.py": - module_name = f"{module_prefix}.{file[:-3]}" - try: - module = importlib.import_module(module_name) - for name, obj in inspect.getmembers(module, inspect.isclass): - if issubclass(obj, BaseNode) and obj.__module__ == module.__name__: - if category_name not in nodes_by_category: - nodes_by_category[category_name] = [] - nodes_by_category[category_name].append(obj) - except Exception as e: - print(f"Failed to import {module_name}: {e}") - - return nodes_by_category - - -def make_node_command(graph, node_type_str): - """ - Return a function that creates a node of the given type at the current cursor position. - Ensures that only one FlyffCharacterStatusNode exists. - """ - def real_create(): - if node_type_str.startswith("bunny-lab.io.flyff_character_status_node"): - for node in graph.all_nodes(): - if node.__class__.__name__ == "FlyffCharacterStatusNode": - QtWidgets.QMessageBox.critical( - None, - "Error", - "Only one Flyff Character Status Collector node is allowed." - ) - return - try: - pos = graph.cursor_pos() - graph.create_node(node_type_str, pos=pos) - except Exception as e: - QtWidgets.QMessageBox.critical(None, "Error", str(e)) - - def command(): - if QtWidgets.QApplication.instance(): - real_create() - else: - QtCore.QTimer.singleShot(0, real_create) - - return command - -def ensure_workflows_folder(): - """ - Ensures a 'Workflows' subfolder exists. - """ - if not os.path.exists("Workflows"): - os.makedirs("Workflows") - -def close_workflow(graph: NodeGraph): - """ - Closes the current workflow (removes all nodes and connections). - """ - graph.clear_session() - -def save_workflow(graph: NodeGraph): - """ - Saves the current workflow (including custom names, positions, wires, etc.) into a JSON file - in the 'Workflows' subfolder. - """ - ensure_workflows_folder() - file_filter = "JSON Files (*.json);;All Files (*.*)" - dlg = FileDialog.getSaveFileName(None, "Save Workflow", os.path.join("Workflows", ""), file_filter) - file_path = dlg[0] - if not file_path: - return # User canceled - - if not file_path.lower().endswith(".json"): - file_path += ".json" - - try: - graph.save_session(file_path) - print(f"Workflow saved to {file_path}") - except Exception as e: - QtWidgets.QMessageBox.critical(None, "Error Saving Workflow", str(e)) - -def load_workflow(graph: NodeGraph): - """ - Loads a workflow (including node values, connections, positions, etc.) from a specified JSON file - and centers it within the graph. - """ - ensure_workflows_folder() - file_filter = "JSON Files (*.json);;All Files (*.*)" - dlg = FileDialog.getOpenFileName(None, "Load Workflow", os.path.join("Workflows", ""), file_filter) - file_path = dlg[0] - if not file_path: - return # User canceled - - try: - graph.load_session(file_path) - print(f"Workflow loaded from {file_path}") - - # Center the workflow within the graph - nodes = graph.all_nodes() - if nodes: - graph.center_on(nodes) - else: - print("No nodes found in the loaded workflow.") - - except Exception as e: - QtWidgets.QMessageBox.critical(None, "Error Loading Workflow", str(e)) - -if __name__ == "__main__": - app = QtWidgets.QApplication([]) - - # Start Flask API Server - data_manager.start_api_server() - - # Create the NodeGraph - graph = NodeGraph() - graph.widget.setWindowTitle("Borealis - Workflow Automation Tool") - - # Dynamically import custom node classes from the 'Nodes' package. - custom_nodes_by_category = import_nodes_from_folder("Nodes") - - # Register each node in its category - for category, node_classes in custom_nodes_by_category.items(): - for node_class in node_classes: - graph.register_node(node_class) - - # Recursively apply the stylesheet to all submenus - def apply_styles_to_submenus(menu): - """ Recursively applies the stylesheet to all submenus in the menu. """ - menu.setStyleSheet(menu_stylesheet) - for action in menu.actions(): - if action.menu(): # Check if action has a submenu - apply_styles_to_submenus(action.menu()) - - # Override the Color of the Context Menu to Blue - menu_stylesheet = """ - QMenu { - background-color: rgb(30, 30, 30); - border: 1px solid rgba(200, 200, 200, 60); - } - QMenu::item { - padding: 5px 18px 2px; - background-color: transparent; - } - QMenu::item:selected { - color: rgb(255, 255, 255); - background-color: rgba(60, 120, 180, 150); - } - QMenu::separator { - height: 1px; - background: rgba(255, 255, 255, 50); - margin: 4px 8px; - } - """ - - # Create categorized context menu - graph_context_menu = graph.get_context_menu("graph") - add_node_menu = graph_context_menu.add_menu("Add Node") - - for category, node_classes in custom_nodes_by_category.items(): - category_menu = add_node_menu.add_menu(category) # Create submenu - category_menu.qmenu.setStyleSheet(menu_stylesheet) # Apply to submenu - - for node_class in node_classes: - node_type = f"{node_class.__identifier__}.{node_class.__name__}" - node_name = node_class.NODE_NAME - category_menu.add_command(f"{node_name}", make_node_command(graph, node_type)) - - # Ensure styles are propagated across all dynamically created submenus - apply_styles_to_submenus(graph_context_menu.qmenu) - - # Add a "Remove Selected Node" command - graph_context_menu.add_command( - "Remove Selected Node", - lambda: [graph.remove_node(node) for node in graph.selected_nodes()] if graph.selected_nodes() else None - ) - - # ------------------------------# - # WRAPPER: QMainWindow Integration with Additional UI Elements - # ------------------------------# - # SECTION: Enhanced Graph Wrapper for QMainWindow - # This section wraps the NodeGraph widget in a QMainWindow with: - # - A menu bar at the top (named "Workflows" menu) - # - A status bar at the bottom - # - A central QSplitter dividing the window horizontally: - # * Left side (2/3): the NodeGraph widget - # * Right side (1/3): an empty text box for future use - _original_show = graph.widget.show # Save original method - - def _wrapped_show(): - """ - Wrap the NodeGraph widget inside a QMainWindow with a "Workflows" menu, - a status bar, and a central splitter for layout. - """ - # Create a new QMainWindow instance - main_window = QtWidgets.QMainWindow() - - # Create a menu bar and add a "Workflows" menu - menu_bar = main_window.menuBar() - workflows_menu = menu_bar.addMenu("Workflows") - - # Add "Open" action - open_action = QtWidgets.QAction("Open", main_window) - open_action.triggered.connect(lambda: load_workflow(graph)) - workflows_menu.addAction(open_action) - - # Add "Save" action - save_action = QtWidgets.QAction("Save", main_window) - save_action.triggered.connect(lambda: save_workflow(graph)) - workflows_menu.addAction(save_action) - - # Add "Close" action - close_action = QtWidgets.QAction("Close", main_window) - close_action.triggered.connect(lambda: close_workflow(graph)) - workflows_menu.addAction(close_action) - - # Create and set a blank status bar at the bottom. - main_window.setStatusBar(QtWidgets.QStatusBar()) - - # --------------------------------------------------------------------- - # SECTION: Status Bar Enhancement - Dynamic Status Display - # Add a QLabel to the status bar that shows: - # - The number of nodes in the graph. - # - A fixed update rate (500ms). - # - A clickable hyperlink to the Flask API server. - status_bar = main_window.statusBar() - - status_label = QtWidgets.QLabel() - status_label.setTextFormat(QtCore.Qt.RichText) # Enable rich text for clickable links. - status_label.setStyleSheet("color: white;") # Set default text color to white. - status_label.setOpenExternalLinks(True) # Allow hyperlinks to be clickable. - status_bar.setSizeGripEnabled(False) # Disable resizing via the size grip. - status_bar.addWidget(status_label) - status_bar.setStyleSheet(""" - QStatusBar::item { - border: none; /* remove the line around items */ - } - """) - - def update_status(): - node_count = len(graph.all_nodes()) - api_link = ( - '' - 'http://127.0.0.1:5000/data' - ) - status_label.setText( - f'Nodes: {node_count} | Update Rate: 500ms | Flask API Server: {api_link}' - ) - - # Create the timer, pass the main_window as parent, and store the reference. - status_timer = QtCore.QTimer(main_window) - status_timer.timeout.connect(update_status) - status_timer.start(500) - - main_window._status_timer = status_timer # Keep a reference so it's not GCed - # --------------------------------------------------------------------- - - # Create a QSplitter for horizontal division. - splitter = QtWidgets.QSplitter(QtCore.Qt.Horizontal) - - # SECTION: Left Pane - Graph Widget - splitter.addWidget(graph.widget) - - # SECTION: Right Pane - Empty Text Box - text_edit = QtWidgets.QTextEdit() - splitter.addWidget(text_edit) - - # Set stretch factors - splitter.setStretchFactor(0, 2) # Split of Left Side - splitter.setStretchFactor(1, 3) # Split of Right Side - - # Reduce the Size of the Splitter Handle - splitter.setHandleWidth(1) - splitter.setStyleSheet(""" - QSplitter::handle { - background: none; - } - """) - - # Set the splitter as the central widget of the main window. - main_window.setCentralWidget(splitter) - - # Transfer the window title from the graph widget to the main window. - main_window.setWindowTitle(graph.widget.windowTitle()) - # Resize the main window using the size set for the graph widget. - main_window.resize(graph.widget.size()) - - # Store a reference to the main window to prevent it from being garbage collected. - graph.widget._main_window = main_window - # Show the main window instead of the standalone graph widget. - main_window.show() - - # Monkey-patch the show method of the graph widget. - graph.widget.show = _wrapped_show - - # Grid styling changes - graph.set_background_color(20, 20, 20) # Dark gray - graph.set_grid_color(60, 60, 60) # Gray grid lines - - # Add gradient background - scene = graph.scene() - gradient = QtGui.QLinearGradient(0, 0, 0, 1) - gradient.setCoordinateMode(QtGui.QGradient.ObjectBoundingMode) - gradient.setColorAt(0.0, QtGui.QColor(9, 44, 68)) - gradient.setColorAt(0.3, QtGui.QColor(30, 30, 30)) - gradient.setColorAt(0.7, QtGui.QColor(30, 30, 30)) - gradient.setColorAt(1.0, QtGui.QColor(9, 44, 68)) - scene.setBackgroundBrush(QtGui.QBrush(gradient)) - - # Resize and show the graph widget (which now triggers the QMainWindow wrapper) - graph.widget.resize(1600, 900) - graph.widget.show() - - graph_context_menu.qmenu.setStyleSheet(menu_stylesheet) - - # Global update function - def global_update(): - for node in graph.all_nodes(): - if hasattr(node, "process_input"): - try: - node.process_input() - except Exception as e: - print("Error updating node", node, e) - - timer = QtCore.QTimer() - timer.timeout.connect(global_update) - timer.start(500) - - sys.exit(app.exec_()) diff --git a/Launch-Borealis-Legacy.ps1 b/Launch-Borealis-Legacy.ps1 deleted file mode 100644 index 372d851..0000000 --- a/Launch-Borealis-Legacy.ps1 +++ /dev/null @@ -1,50 +0,0 @@ -# Bootstrap Borealis Virtual Python Environment -# Run Script: "Set-ExecutionPolicy Unrestricted -Scope Process .\Start_Windows.ps1" - -# Define paths -$venvPath = "Borealis-Workflow-Automation-Tool" -$dataSource = "Data" -$dataDestination = "$venvPath\Borealis" - -# Check if virtual environment exists -if (!(Test-Path "$venvPath\Scripts\Activate")) { - Write-Output "Creating virtual environment '$venvPath'..." - python -m venv $venvPath -} - -# Ensure the Data folder exists before copying -if (Test-Path $dataSource) { - Write-Output "Copying Data folder into virtual environment..." - - # Remove old data if it exists - if (Test-Path $dataDestination) { - Remove-Item -Recurse -Force $dataDestination - } - - # Create the Borealis directory inside the virtual environment - New-Item -Path $dataDestination -ItemType Directory -Force | Out-Null - - # Copy Data into the virtual environment under Borealis - Copy-Item -Path "$dataSource\*" -Destination $dataDestination -Recurse -} else { - Write-Output "Warning: Data folder not found, skipping copy." -} - -# Activate virtual environment -Write-Output "Activating virtual environment..." -. "$venvPath\Scripts\Activate" - -# Install dependencies -if (Test-Path "requirements.txt") { - Write-Output "Installing dependencies..." - pip install -q -r requirements.txt -} else { - Write-Output "No requirements.txt found, skipping installation." -} - -# Run the main script from inside the copied Data folder -Write-Output "Starting Borealis Workflow Automation Tool..." -python "$dataDestination\borealis.py" - -# Deactivate after execution -deactivate \ No newline at end of file