Initial Commit

Legacy Code Removed
This commit is contained in:
Nicole Rappe 2025-03-28 21:14:53 -06:00
parent 10436ae587
commit 0692005760
35 changed files with 0 additions and 4607 deletions

View File

@ -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.
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = """
<html>
<head>
<title>Borealis - Live Status</title>
<script>
// Reload the <img> every second
setInterval(function(){
var img = document.getElementById('status_img');
img.src = '/flyff/status_rawdata?random=' + Math.random();
}, 1000);
</script>
</head>
<body>
<img id="status_img" src="/flyff/status_rawdata" />
</body>
</html>
"""
return html
@app.route('/flyff/status_rawdata')
def status_screenshot_data():
"""
Serves the raw PNG bytes (decoded from base64) used by <img> 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()

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -1,50 +0,0 @@
#!/usr/bin/env python3
"""
Flyff EXP Node (Final Combined Version)
- Pulls the EXP value directly from data_manager.py
- Outputs only the "exp" value as a string
- Uses color (48, 116, 143) for its output port
- Displays "exp" in a text field labeled "Value"
- Retrieves the port with self.outputs().get('value')
"""
import time
import traceback
from OdenGraphQt import BaseNode
from Modules import data_manager # Importing data_manager from Modules
class FlyffEXPCurrentNode(BaseNode):
__identifier__ = 'bunny-lab.io.flyff_exp_current_node'
NODE_NAME = 'Flyff - EXP'
def __init__(self):
super(FlyffEXPCurrentNode, 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=(48, 116, 143))
self.set_name("Flyff - EXP")
def process_input(self):
try:
new_value = data_manager.get_data().get("exp", "N/A")
new_value_str = str(new_value)
self.set_property('value', new_value_str)
self.transmit_data(new_value_str)
except Exception as e:
tb = traceback.format_exc()
print(f"[ERROR] Exception in FlyffEXPCurrentNode: {e}\nTraceback:\n{tb}")
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}")

View File

@ -1,93 +0,0 @@
#!/usr/bin/env python3
"""
Flyff FP Current Node (Final Combined Version)
- Polls the API at http://127.0.0.1:5000/data
- Outputs only the "fp_current" value as a string
- Uses color (36, 116, 32) for its output port
- Displays "fp_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 FlyffFPCurrentNode(BaseNode):
__identifier__ = 'bunny-lab.io.flyff_fp_current_node'
NODE_NAME = 'Flyff - FP Current'
def __init__(self):
super(FlyffFPCurrentNode, 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 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] FlyffFPCurrentNode: 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 Current (API Connected)")
new_value = data.get("fp_current", "N/A")
print(f"[DEBUG] FlyffFPCurrentNode: fp_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 FlyffFPCurrentNode")
self._api_down = True
except Exception as e:
tb = traceback.format_exc()
self._handle_api_error(f"Exception in FlyffFPCurrentNode: {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 Current (API Disconnected)")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}%")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

View File

@ -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)"
]
}
]
}

View File

@ -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"
]
}
]
}

View File

@ -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"
]
}
]
}

View File

@ -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"
}
}
}
}

View File

@ -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 = (
'<a href="http://127.0.0.1:5000/data" '
'style="color: rgb(60, 120, 180); text-decoration: none;">'
'http://127.0.0.1:5000/data</a>'
)
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_())

View File

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