From faee07b720d0df9473ee0729b0123f3a5628efbe Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Fri, 14 Feb 2025 00:38:26 -0700 Subject: [PATCH] Transparent Blueprint Grid Implemented (But Broken) --- Borealis.ui | 114 ---- Experimental/accept_reject_example.py | 192 ------- Experimental/experimental_nodes.py | 102 ---- Legacy/Orphaned Code/data_collector.py | 71 --- Legacy/borealis_overlay.py | 542 ------------------ .../data_collector.py | 0 .../overlay_helpers.py | 0 .../character_status_node.cpython-312.pyc | Bin 8370 -> 0 bytes .../custom_ports_node.cpython-312.pyc | Bin 5487 -> 0 bytes Nodes/custom_ports_node.py | 121 ---- Project_Borealis.zip | Bin 0 -> 72878 bytes QML/background_grid.qml | 78 +++ borealis_transparent.py | 237 +++++--- debug_processed.png | Bin 2823 -> 0 bytes debug_screenshot.png | Bin 13652 -> 0 bytes 15 files changed, 238 insertions(+), 1219 deletions(-) delete mode 100644 Borealis.ui delete mode 100644 Experimental/accept_reject_example.py delete mode 100644 Experimental/experimental_nodes.py delete mode 100644 Legacy/Orphaned Code/data_collector.py delete mode 100644 Legacy/borealis_overlay.py rename data_collector_v2.py => Modules/data_collector.py (100%) rename screen_overlays.py => Modules/overlay_helpers.py (100%) delete mode 100644 Nodes/__pycache__/character_status_node.cpython-312.pyc delete mode 100644 Nodes/__pycache__/custom_ports_node.cpython-312.pyc delete mode 100644 Nodes/custom_ports_node.py create mode 100644 Project_Borealis.zip create mode 100644 QML/background_grid.qml delete mode 100644 debug_processed.png delete mode 100644 debug_screenshot.png diff --git a/Borealis.ui b/Borealis.ui deleted file mode 100644 index 4de4046..0000000 --- a/Borealis.ui +++ /dev/null @@ -1,114 +0,0 @@ - - - ProjectBorealis - - - - 0 - 0 - 925 - 698 - - - - Project Borealis - Flyff Information Overlay - - - false - - - - - - 10 - 10 - 260 - 41 - - - - - Microsoft YaHei UI Light - 24 - 75 - true - - - - Project Borealis - - - - - - 0 - 391 - 921 - 271 - - - - QFrame::StyledPanel - - - QFrame::Plain - - - - - - 10 - 50 - 211 - 31 - - - - - Microsoft YaHei UI Light - 12 - - - - Flyff Information Overlay - - - - - - 250 - 150 - 401 - 211 - - - - 0 - - - - Tab 1 - - - - - Tab 2 - - - - - - - 30 - 280 - 113 - 20 - - - - - - - - - diff --git a/Experimental/accept_reject_example.py b/Experimental/accept_reject_example.py deleted file mode 100644 index fd6f028..0000000 --- a/Experimental/accept_reject_example.py +++ /dev/null @@ -1,192 +0,0 @@ -import signal - -from qtpy import QtWidgets - -from OdenGraphQt import BaseNode, NodeGraph -from OdenGraphQt.constants import PortTypeEnum -from OdenGraphQt.qgraphics.node_base import NodeItem - - -class PublishWriteNodeItem(NodeItem): - def _align_widgets_horizontal(self, v_offset: int): - if not self._widgets: - return - - rect = self.boundingRect() - y = rect.y() + v_offset - for widget in self._widgets.values(): - if not widget.isVisible(): - continue - - widget_rect = widget.boundingRect() - x = rect.center().x() - (widget_rect.width() / 2) - widget.widget().setTitleAlign('center') - widget.setPos(x, y) - y += widget_rect.height() - - -class PrevNextNode(BaseNode): - __identifier__ = "action" - NODE_NAME = "Action Node" - - def __init__(self): - super().__init__() - - # create an input port. - input_port = self.add_input("_prev", color=(180, 80, 0), multi_input=False) - # create an output port. - output_port = self.add_output("_next", multi_output=False) - - input_port.port_item.set_allow_partial_match_constraint(True) - input_port.port_item.set_accept_constraint( - port_name=output_port.name(), - port_type=PortTypeEnum.OUT.value, - node_identifier=self.__identifier__, - ) - - output_port.port_item.set_allow_partial_match_constraint(True) - output_port.port_item.set_accept_constraint( - port_name=input_port.name(), - port_type=PortTypeEnum.IN.value, - node_identifier=self.__identifier__, - ) - - -class IngredientNode(BaseNode): - __identifier__ = "ingredient" - - -class SpamNode(IngredientNode): - __identifier__ = "spam" - NODE_NAME = "Spam" - - def __init__(self): - super().__init__() - spam_port = self.add_output( - "spam", - color=(50, 150, 222), - ) - - -class EggNode(IngredientNode): - __identifier__ = "egg" - NODE_NAME = "Egg" - - def __init__(self): - super().__init__() - egg_port = self.add_output( - "egg", - color=(50, 150, 222), - ) - - -class MealNode(BaseNode): - NODE_NAME = "Meal" - - def __init__(self): - super().__init__() - spam_port = self.add_input("spam", color=(222, 15, 0), multi_input=False) - spam_port.port_item.set_reject_constraint( - port_name="egg", - port_type=PortTypeEnum.OUT.value, - node_identifier="egg", - ) - egg_port = self.add_input("egg", color=(222, 15, 0), multi_input=False) - egg_port.port_item.set_reject_constraint( - port_name="spam", - port_type=PortTypeEnum.OUT.value, - node_identifier="spam", - ) - - -class BasePublishNode(PrevNextNode): - __identifier__ = "publish" - allow_multiple_write = False - - def __init__(self): - super().__init__() - port = self.add_output( - "write", - color=(184, 150, 0), - multi_output=self.allow_multiple_write, - ) - port.port_item.set_accept_constraint( - port_name="src", - port_type=PortTypeEnum.IN.value, - node_identifier="publish", - ) - - -class PubNode(PrevNextNode): - __identifier__ = "pub" - NODE_NAME = "Not Tavern" - - -class PublishFileActionNode(BasePublishNode): - NODE_NAME = "Publish File" - allow_multiple_write = False - - -class PublishFileToManyActionNode(BasePublishNode): - NODE_NAME = "Publish File to Many" - allow_multiple_write = True - - -class PublishWriteNode(BaseNode): - __identifier__ = "publish" - NODE_NAME = "Publish Write" - - def __init__(self): - super().__init__(qgraphics_item=PublishWriteNodeItem) - self.set_color(164, 130, 0) - self.add_text_input("write", "Path:") - - port = self.add_input("src", multi_input=False) - port.port_item.set_accept_constraint( - port_name="write", - port_type=PortTypeEnum.OUT.value, - node_identifier="publish", - ) - - -if __name__ == '__main__': - - # handle SIGINT to make the app terminate on CTRL+C - signal.signal(signal.SIGINT, signal.SIG_DFL) - - app = QtWidgets.QApplication([]) - - # create graph controller. - graph = NodeGraph() - - # set up context menu for the node graph. - graph.set_context_menu_from_file('../examples/hotkeys/hotkeys.json') - - # registered example nodes. - graph.register_nodes([ - SpamNode, - EggNode, - MealNode, - PubNode, - PublishFileActionNode, - PublishFileToManyActionNode, - PublishWriteNode, - ]) - - # add nodes - graph.add_node(SpamNode()) - graph.add_node(EggNode()) - graph.add_node(MealNode()) - graph.add_node(PubNode()) - graph.add_node(PublishFileToManyActionNode()) - graph.add_node(PublishFileActionNode()) - graph.add_node(PublishWriteNode()) - graph.auto_layout_nodes() - graph.clear_selection() - - # show the node graph widget. - graph_widget = graph.widget - graph_widget.resize(1100, 800) - graph_widget.show() - - app.exec_() \ No newline at end of file diff --git a/Experimental/experimental_nodes.py b/Experimental/experimental_nodes.py deleted file mode 100644 index d88b197..0000000 --- a/Experimental/experimental_nodes.py +++ /dev/null @@ -1,102 +0,0 @@ -#!/usr/bin/env python3 -""" -Standalone NodeGraphQT Math Node Example - -This example defines a custom "Math Node" that: - - Uses two text inputs for numeric operands (via add_text_input) - - Provides a combo box for operator selection (via add_combo_menu) - - Offers a checkbox to enable/disable the operation (via add_checkbox) - - Computes a result and updates its title accordingly. -""" - -from NodeGraphQt import NodeGraph, BaseNode - -class MathNode(BaseNode): - """ - Math Node: - - Operands: Two text inputs (Operand 1 and Operand 2) - - Operator: Combo box to select 'Add', 'Subtract', 'Multiply', or 'Divide' - - Enable: Checkbox to enable/disable the math operation - - Output: Result of the math operation (if enabled) - """ - __identifier__ = 'example.math' - NODE_NAME = 'Math Node' - - def __init__(self): - super(MathNode, self).__init__() - - # Add two text inputs for operands. - self.add_text_input('operand1', 'Operand 1', text='10') - self.add_text_input('operand2', 'Operand 2', text='5') - - # Add a combo box for operator selection. - self.add_combo_menu('operator', 'Operator', items=['Add', 'Subtract', 'Multiply', 'Divide']) - - # Add a checkbox to enable/disable the operation. - self.add_checkbox('enable', 'Enable Operation', state=True) - - # Add an output port to transmit the result. - self.add_output('Result') - - self.value = 0 - self.set_name("Math Node") - self.process_input() - - def process_input(self): - """ - Gather values from the widgets, perform the math operation if enabled, - update the node title, and send the result to connected nodes. - """ - try: - op1 = float(self.get_property('operand1')) - except (ValueError, TypeError): - op1 = 0.0 - try: - op2 = float(self.get_property('operand2')) - except (ValueError, TypeError): - op2 = 0.0 - - operator = self.get_property('operator') - enable = self.get_property('enable') - - if enable: - if operator == 'Add': - result = op1 + op2 - elif operator == 'Subtract': - result = op1 - op2 - elif operator == 'Multiply': - result = op1 * op2 - elif operator == 'Divide': - result = op1 / op2 if op2 != 0 else 0.0 - else: - result = 0.0 - else: - result = 0.0 - - self.value = result - self.set_name(f"Result: {result}") - - 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(result, source_port_name='Result') - - -if __name__ == '__main__': - import sys - try: - from PySide2.QtWidgets import QApplication - except ImportError: - from PySide6.QtWidgets import QApplication - - app = QApplication(sys.argv) - graph = NodeGraph() - graph.register_node(MathNode) - node = graph.create_node('example.math.MathNode', name='Math Node') - node.set_pos(100, 100) - graph.widget.resize(1200, 800) - graph.widget.setWindowTitle("NodeGraphQT Math Node Demo") - graph.widget.show() - sys.exit(app.exec_()) diff --git a/Legacy/Orphaned Code/data_collector.py b/Legacy/Orphaned Code/data_collector.py deleted file mode 100644 index a191645..0000000 --- a/Legacy/Orphaned Code/data_collector.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python3 -""" -Collector Process: -- Runs the OCR engine. -- Updates OCR data every 0.5 seconds. -- Exposes the latest data via an HTTP API using Flask. - -This version splits the HP, MP, and FP values into 'current' and 'total' before -sending them via the API, so the Character Status Node can ingest them directly. -""" - -import time -import threading -from flask import Flask, jsonify - -app = Flask(__name__) - -# Global variable to hold the latest stats (HP, MP, FP, EXP) -latest_data = { - "hp_current": 0, - "hp_total": 0, - "mp_current": 0, - "mp_total": 0, - "fp_current": 0, - "fp_total": 0, - "exp": 0.0000 -} - -def ocr_collector(): - """ - This function simulates the OCR process. - Replace the code below with your actual OCR logic. - """ - global latest_data - counter = 0 - while True: - # Simulate updating stats: - hp_current = 50 + counter % 10 - hp_total = 100 - mp_current = 30 + counter % 5 - mp_total = 50 - fp_current = 20 # fixed, for example - fp_total = 20 - exp_val = round(10.0 + (counter * 0.1), 4) - - latest_data = { - "hp_current": hp_current, - "hp_total": hp_total, - "mp_current": mp_current, - "mp_total": mp_total, - "fp_current": fp_current, - "fp_total": fp_total, - "exp": exp_val - } - - counter += 1 - time.sleep(0.5) - -@app.route('/data') -def get_data(): - """Return the latest OCR data as JSON.""" - return jsonify(latest_data) - -if __name__ == '__main__': - # Start the OCR collector in a background thread. - collector_thread = threading.Thread(target=ocr_collector) - collector_thread.daemon = True - collector_thread.start() - - # Run the Flask app on localhost:5000. - app.run(host="127.0.0.1", port=5000) diff --git a/Legacy/borealis_overlay.py b/Legacy/borealis_overlay.py deleted file mode 100644 index 74b9d8f..0000000 --- a/Legacy/borealis_overlay.py +++ /dev/null @@ -1,542 +0,0 @@ -#!/usr/bin/env python3 - -import sys -import time -import re -import numpy as np -import cv2 -import pytesseract - -try: - import winsound - HAS_WINSOUND = True -except ImportError: - HAS_WINSOUND = False - -from PyQt5.QtWidgets import QApplication, QWidget -from PyQt5.QtCore import Qt, QRect, QPoint, QTimer -from PyQt5.QtGui import QPainter, QPen, QColor, QFont -from PIL import Image, ImageGrab, ImageFilter - -from rich.console import Console, Group -from rich.table import Table -from rich.progress import Progress, BarColumn, TextColumn -from rich.text import Text -from rich.live import Live - -# ============================================================================= -# Global Config -# ============================================================================= - -pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe" - -POLLING_RATE_MS = 500 -MAX_DATA_POINTS = 8 - -# We still use these defaults for Region size. -DEFAULT_WIDTH = 180 -DEFAULT_HEIGHT = 130 -HANDLE_SIZE = 8 -LABEL_HEIGHT = 20 - -GREEN_HEADER_STYLE = "bold green" - -BEEP_INTERVAL_SECONDS = 1.0 # Only beep once every 1 second - -# STATUS BAR AUTO-LOCATOR LOGIC (WILL BE BUILT-OUT TO BE MORE ROBUST LATER) -TEMPLATE_PATH = "G:\\Nextcloud\\Projects\\Scripting\\bars_template.png" # Path to your bars template file -MATCH_THRESHOLD = 0.4 # The correlation threshold to consider a "good" match - -# ============================================================================= -# Helper Functions -# ============================================================================= - -def beep_hp_warning(): - """ - Only beep if enough time has elapsed since the last beep (BEEP_INTERVAL_SECONDS). - """ - current_time = time.time() - if (beep_hp_warning.last_beep_time is None or - (current_time - beep_hp_warning.last_beep_time >= BEEP_INTERVAL_SECONDS)): - - beep_hp_warning.last_beep_time = current_time - if HAS_WINSOUND: - # frequency=376 Hz, duration=100 ms - winsound.Beep(376, 100) - else: - # Attempt terminal bell - print('\a', end='') - -beep_hp_warning.last_beep_time = None - - -def locate_bars_opencv(template_path, threshold=MATCH_THRESHOLD): - """ - Attempt to locate the bars via OpenCV template matching: - 1) Grab the full screen using PIL.ImageGrab. - 2) Convert to NumPy array in BGR format for cv2. - 3) Load template from `template_path`. - 4) Use cv2.matchTemplate to find the best match location. - 5) If max correlation > threshold, return (x, y, w, h). - 6) Else return None. - """ - # 1) Capture full screen - screenshot_pil = ImageGrab.grab() - screenshot_np = np.array(screenshot_pil) # shape (H, W, 4) possibly - # Convert RGBA or RGB to BGR - screenshot_bgr = cv2.cvtColor(screenshot_np, cv2.COLOR_RGB2BGR) - - # 2) Load template from file - template_bgr = cv2.imread(template_path, cv2.IMREAD_COLOR) - if template_bgr is None: - print(f"[WARN] Could not load template file: {template_path}") - return None - - # 3) Template matching - result = cv2.matchTemplate(screenshot_bgr, template_bgr, cv2.TM_CCOEFF_NORMED) - - # 4) Find best match - min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result) - # template width/height - th, tw, _ = template_bgr.shape - - if max_val >= threshold: - # max_loc is top-left corner of the best match - found_x, found_y = max_loc - return (found_x, found_y, tw, th) - else: - return None - - -def format_duration(seconds): - if seconds is None: - return "???" - seconds = int(seconds) - hours = seconds // 3600 - leftover = seconds % 3600 - mins = leftover // 60 - secs = leftover % 60 - if hours > 0: - return f"{hours}h {mins}m {secs}s" - else: - return f"{mins}m {secs}s" - - -def sanitize_experience_string(raw_text): - text_no_percent = raw_text.replace('%', '') - text_no_spaces = text_no_percent.replace(' ', '') - cleaned = re.sub(r'[^0-9\.]', '', text_no_spaces) - match = re.search(r'\d+(?:\.\d+)?', cleaned) - if not match: - return None - val = float(match.group(0)) - if val < 0: - val = 0 - elif val > 100: - val = 100 - return round(val, 4) - - -def format_experience_value(value): - if value < 0: - value = 0 - elif value > 100: - value = 100 - float_4 = round(value, 4) - raw_str = f"{float_4:.4f}" - int_part, dec_part = raw_str.split('.') - if int_part == "100": - pass - elif len(int_part) == 1 and int_part != "0": - int_part = "0" + int_part - elif int_part == "0": - int_part = "00" - return f"{int_part}.{dec_part}" - -# ----------------------------------------------------------------------------- -# Region Class -# ----------------------------------------------------------------------------- -class Region: - """ - Defines a draggable/resizable screen region for OCR capture. - """ - def __init__(self, x, y, label="Region", color=QColor(0,0,255)): - self.x = x - self.y = y - self.w = DEFAULT_WIDTH - self.h = DEFAULT_HEIGHT - self.label = label - self.color = color - self.visible = True - self.data = "" - - def rect(self): - return QRect(self.x, self.y, self.w, self.h) - - def label_rect(self): - return QRect(self.x, self.y - LABEL_HEIGHT, self.w, LABEL_HEIGHT) - - def resize_handles(self): - return [ - QRect(self.x - HANDLE_SIZE // 2, self.y - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE), - QRect(self.x + self.w - HANDLE_SIZE // 2, self.y - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE), - QRect(self.x - HANDLE_SIZE // 2, self.y + self.h - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE), - QRect(self.x + self.w - HANDLE_SIZE // 2, self.y + self.h - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE), - ] - -# ----------------------------------------------------------------------------- -# OverlayCanvas Class -# ----------------------------------------------------------------------------- -class OverlayCanvas(QWidget): - """ - Renders the overlay & handles region dragging/resizing. - """ - def __init__(self, regions, parent=None): - super().__init__(parent) - self.regions = regions - self.edit_mode = True - self.selected_region = None - self.selected_handle = None - self.drag_offset = QPoint() - - def paintEvent(self, event): - painter = QPainter(self) - for region in self.regions: - if region.visible: - pen = QPen(region.color) - pen.setWidth(3) - painter.setPen(pen) - painter.drawRect(region.x, region.y, region.w, region.h) - - painter.setFont(QFont("Arial", 12, QFont.Bold)) - painter.setPen(region.color) - painter.drawText(region.x, region.y - 5, region.label) - - if self.edit_mode: - for handle in region.resize_handles(): - painter.fillRect(handle, region.color) - - def mousePressEvent(self, event): - if not self.edit_mode: - return - if event.button() == Qt.LeftButton: - for region in reversed(self.regions): - for i, handle in enumerate(region.resize_handles()): - if handle.contains(event.pos()): - self.selected_region = region - self.selected_handle = i - return - if region.label_rect().contains(event.pos()): - self.selected_region = region - self.selected_handle = None - self.drag_offset = event.pos() - QPoint(region.x, region.y) - return - if region.rect().contains(event.pos()): - self.selected_region = region - self.selected_handle = None - self.drag_offset = event.pos() - QPoint(region.x, region.y) - return - - def mouseMoveEvent(self, event): - if not self.edit_mode or self.selected_region is None: - return - - if self.selected_handle is None: - self.selected_region.x = event.x() - self.drag_offset.x() - self.selected_region.y = event.y() - self.drag_offset.y() - else: - sr = self.selected_region - if self.selected_handle == 0: # top-left - sr.w += sr.x - event.x() - sr.h += sr.y - event.y() - sr.x = event.x() - sr.y = event.y() - elif self.selected_handle == 1: # top-right - sr.w = event.x() - sr.x - sr.h += sr.y - event.y() - sr.y = event.y() - elif self.selected_handle == 2: # bottom-left - sr.w += sr.x - event.x() - sr.h = event.y() - sr.y - sr.x = event.x() - elif self.selected_handle == 3: # bottom-right - sr.w = event.x() - sr.x - sr.h = event.y() - sr.y - - sr.w = max(sr.w, 10) - sr.h = max(sr.h, 10) - - self.update() - - def mouseReleaseEvent(self, event): - if not self.edit_mode: - return - if event.button() == Qt.LeftButton: - self.selected_region = None - self.selected_handle = None - -# ----------------------------------------------------------------------------- -# BorealisOverlay Class -# ----------------------------------------------------------------------------- -class BorealisOverlay(QWidget): - """ - Single Region Overlay for Player Stats (HP/MP/FP/EXP) with: - - Automatic location via OpenCV template matching at startup - - OCR scanning - - Low-HP beep - - Rich Live updates in terminal - """ - def __init__(self, live=None): - super().__init__() - screen_geo = QApplication.primaryScreen().geometry() - self.setGeometry(screen_geo) - self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) - self.setAttribute(Qt.WA_TranslucentBackground, True) - - # Try to find the bars automatically - # If found => use that location, else default - initial_x, initial_y = 250, 50 - region_w, region_h = DEFAULT_WIDTH, DEFAULT_HEIGHT - - match_result = locate_bars_opencv(TEMPLATE_PATH, MATCH_THRESHOLD) - if match_result is not None: - found_x, found_y, w, h = match_result - print(f"Character Status Located at {found_x}, {found_y} with confidence >= {MATCH_THRESHOLD}.") - initial_x, initial_y = found_x, found_y - # Optionally override region size with template size - region_w, region_h = w, h - else: - print("Could not auto-locate the character status page. Set your theme to Masquerade and Interface Scale to 140%, and browser zoom level to 110%. Using default region.") - - region = Region(initial_x, initial_y, label="Character Status") - region.w = region_w - region.h = region_h - self.regions = [region] - - self.canvas = OverlayCanvas(self.regions, self) - self.canvas.setGeometry(self.rect()) - - # Tesseract - self.engine = pytesseract - - # Keep history of EXP data - self.points = [] - - self.live = live - - # Timer for periodic OCR scanning - self.timer = QTimer(self) - self.timer.timeout.connect(self.collect_ocr_data) - self.timer.start(POLLING_RATE_MS) - - def set_live(self, live): - self.live = live - - def collect_ocr_data(self): - for region in self.regions: - if region.visible: - screenshot = ImageGrab.grab( - bbox=(region.x, region.y, region.x + region.w, region.y + region.h) - ) - processed = self.preprocess_image(screenshot) - text = pytesseract.image_to_string(processed, config='--psm 4 --oem 1') - region.data = text.strip() - - if self.live is not None: - renderable = self.build_renderable() - self.live.update(renderable) - - def preprocess_image(self, image): - gray = image.convert("L") - scaled = gray.resize((gray.width * 3, gray.height * 3)) - thresh = scaled.point(lambda p: p > 200 and 255) - return thresh.filter(ImageFilter.MedianFilter(3)) - - def parse_all_stats(self, raw_text): - raw_lines = raw_text.splitlines() - lines = [l.strip() for l in raw_lines if l.strip()] - stats_dict = { - "hp": (0,1), - "mp": (0,1), - "fp": (0,1), - "exp": None - } - if len(lines) < 4: - return stats_dict - - hp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[0]) - if hp_match: - stats_dict["hp"] = (int(hp_match.group(1)), int(hp_match.group(2))) - - mp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[1]) - if mp_match: - stats_dict["mp"] = (int(mp_match.group(1)), int(mp_match.group(2))) - - fp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[2]) - if fp_match: - stats_dict["fp"] = (int(fp_match.group(1)), int(fp_match.group(2))) - - exp_val = sanitize_experience_string(lines[3]) - stats_dict["exp"] = exp_val - return stats_dict - - def update_points(self, new_val): - now = time.time() - if self.points: - _, last_v = self.points[-1] - if abs(new_val - last_v) < 1e-6: - return - if new_val < last_v: - self.points.clear() - self.points.append((now, new_val)) - if len(self.points) > MAX_DATA_POINTS: - self.points.pop(0) - - def compute_time_to_100(self): - n = len(self.points) - if n < 2: - return None - first_t, first_v = self.points[0] - last_t, last_v = self.points[-1] - diff_v = last_v - first_v - if diff_v <= 0: - return None - - steps = n - 1 - total_time = last_t - first_t - if total_time <= 0: - return None - - avg_change = diff_v / steps - remain = 100.0 - last_v - if remain <= 0: - return None - - avg_time = total_time / steps - rate_per_s = avg_change / avg_time if avg_time > 0 else 0 - if rate_per_s <= 0: - return None - - return int(remain / rate_per_s) - - def build_renderable(self): - raw_text = self.regions[0].data - stats = self.parse_all_stats(raw_text) - hp_cur, hp_max = stats["hp"] - mp_cur, mp_max = stats["mp"] - fp_cur, fp_max = stats["fp"] - exp_val = stats["exp"] - - # HP beep logic - if hp_max > 0: - hp_ratio = hp_cur / hp_max - if 0 < hp_ratio <= 0.40: - beep_hp_warning() - - if exp_val is not None: - self.update_points(exp_val) - current_exp = self.points[-1][1] if self.points else 0.0 - - # Title - title_text = Text("Project Borealis\n", style="bold white") - subtitle_text = Text("Flyff Information Overlay\n\n", style="dim") - - # HP / MP / FP bars - bar_progress = Progress( - "{task.description}", - BarColumn(bar_width=30), - TextColumn(" {task.completed}/{task.total} ({task.percentage:>5.2f}%)"), - transient=False, - auto_refresh=False - ) - bar_progress.add_task("[bold red]HP[/bold red]", total=hp_max, completed=hp_cur, - style="red", complete_style="red") - bar_progress.add_task("[bold blue]MP[/bold blue]", total=mp_max, completed=mp_cur, - style="blue", complete_style="blue") - bar_progress.add_task("[bold green]FP[/bold green]", total=fp_max, completed=fp_cur, - style="green", complete_style="green") - bar_progress.refresh() - - # Historical EXP table - table = Table(show_header=True, header_style=GREEN_HEADER_STYLE, style=None) - table.add_column("Historical EXP", justify="center", style="green") - table.add_column("Time Since Last Kill", justify="center", style="green") - table.add_column("Average EXP Per Kill", justify="center", style="green") - table.add_column("Average Time Between Kills", justify="center", style="green") - - n = len(self.points) - if n == 0: - table.add_row("N/A", "N/A", "N/A", "N/A") - elif n == 1: - _, v0 = self.points[0] - exp_str = f"[green]{format_experience_value(v0)}%[/green]" - table.add_row(exp_str, "N/A", "N/A", "N/A") - else: - for i in range(1, n): - t_cur, v_cur = self.points[i] - t_prev, v_prev = self.points[i - 1] - delta_v = v_cur - v_prev - delta_str = f"{delta_v:+.4f}%" - exp_main = format_experience_value(v_cur) - exp_str = f"[green]{exp_main}%[/green] [dim]({delta_str})[/dim]" - - delta_t = t_cur - t_prev - t_since_str = f"{delta_t:.1f}s" - - diff_v = v_cur - self.points[0][1] - steps = i - avg_exp_str = f"{diff_v/steps:.4f}%" - - total_time = t_cur - self.points[0][0] - avg_kill_time = total_time / steps - avg_time_str = f"{avg_kill_time:.1f}s" - - table.add_row(exp_str, t_since_str, avg_exp_str, avg_time_str) - - # Predicted Time to Level - secs_left = self.compute_time_to_100() - time_str = format_duration(secs_left) - - time_bar = Progress( - TextColumn("[bold white]Predicted Time to Level:[/bold white] "), - BarColumn(bar_width=30, complete_style="magenta"), - TextColumn(" [green]{task.percentage:>5.2f}%[/green] "), - TextColumn(f"[magenta]{time_str}[/magenta] until 100%", justify="right"), - transient=False, - auto_refresh=False - ) - time_bar.add_task("", total=100, completed=current_exp) - time_bar.refresh() - - return Group( - title_text, - subtitle_text, - bar_progress, - table, - time_bar - ) - -# ----------------------------------------------------------------------------- -# main -# ----------------------------------------------------------------------------- -def main(): - """ - 1) Attempt to locate HP/MP/FP/Exp bars using OpenCV template matching. - 2) Position overlay region accordingly if found, else default. - 3) Start PyQt, periodically OCR the region, update Rich Live in terminal. - """ - app = QApplication(sys.argv) - window = BorealisOverlay() - window.setWindowTitle("Project Borealis Overlay (HP/MP/FP/EXP)") - window.show() - - console = Console() - - with Live(console=console, refresh_per_second=4) as live: - window.set_live(live) - exit_code = app.exec_() - - sys.exit(exit_code) - -if __name__ == "__main__": - main() diff --git a/data_collector_v2.py b/Modules/data_collector.py similarity index 100% rename from data_collector_v2.py rename to Modules/data_collector.py diff --git a/screen_overlays.py b/Modules/overlay_helpers.py similarity index 100% rename from screen_overlays.py rename to Modules/overlay_helpers.py diff --git a/Nodes/__pycache__/character_status_node.cpython-312.pyc b/Nodes/__pycache__/character_status_node.cpython-312.pyc deleted file mode 100644 index 02e454c6b38cc8f67fef6577abd08358254c714f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8370 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 Fully transparent.""" + def drawBackground(self, painter, rect): + pass # Do nothing, ensuring transparency. + +# NodeGraph & Node Import Helpers from OdenGraphQt import NodeGraph, BaseNode -# Force qtpy to use PyQt5 explicitly -os.environ["QT_API"] = "pyqt5" +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 -# --- PATCH OdenGraphQt to use QtWidgets.QUndoStack instead of QtGui.QUndoStack --- -import qtpy.QtGui -import qtpy.QtWidgets +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 -if not hasattr(qtpy.QtGui, "QUndoStack"): # Ensure patching only if necessary - qtpy.QtGui.QUndoStack = qtpy.QtWidgets.QUndoStack +# Edit Mode Button +class EditButton(QPushButton): + """A small, frameless button to toggle edit mode.""" + def __init__(self, parent=None): + super().__init__("Enter Edit Mode", parent) + self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) + self.setStyleSheet("background-color: rgba(255,255,255,200); border: 1px solid black;") + self.resize(140, 40) -# --- END PATCH --- - -class TransparentGraphWindow(QMainWindow): +# Main Overlay Window +class MainWindow(QMainWindow): + """A frameless, transparent overlay with OdenGraphQt nodes & edit mode toggle.""" def __init__(self): super().__init__() - # Enable transparency & always-on-top behavior - self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool) + # 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) - # Get full-screen size for overlay - screen_geometry = QApplication.primaryScreen().geometry() - self.setGeometry(screen_geometry) + # 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() - # Create Node Graph - self.graph = NodeGraph() - self.graph.widget.setParent(self) - self.graph.widget.setGeometry(self.rect()) + # Save the QML root object for later property sync + self.qml_root = self.qml_view.rootObject() - # Make bottom-left corner interactive for context menu - self.context_menu_area = QRect(10, self.height() - 40, 50, 30) + # NodeGraph with TransparentViewer + self.graph = NodeGraph(viewer=TransparentViewer()) + self.nodeGraphWidget = self.graph.widget + self.nodeGraphWidget.setStyleSheet("background: transparent; border: none;") - # Load nodes dynamically - self.import_nodes() + # Transparent central widget + central = QWidget(self) + central.setAttribute(Qt.WA_TranslucentBackground, True) + self.setCentralWidget(central) - # Global update timer for processing nodes - self.timer = QTimer() - self.timer.timeout.connect(self.update_nodes) + 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 + self.editButton.setText("Exit Edit Mode") # Reflect that grid is active + + # 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) - def import_nodes(self): - """Dynamically import all custom node classes from the 'Nodes' package.""" - try: - package_name = '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__: - self.graph.register_node(obj) - except Exception as e: - print(f"Error loading nodes: {e}") + self.show() + self.nodeGraphWidget.setAttribute(Qt.WA_TransparentForMouseEvents, not self.isEditMode) - def update_nodes(self): - """Calls process_input() on all nodes, if applicable.""" + 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) + self.editButton.setText("Exit Edit Mode" if self.isEditMode else "Enter 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"): - try: - node.process_input() - except Exception as e: - print(f"Error processing node {node}: {e}") + node.process_input() - def mousePressEvent(self, event): - """Override mouse press to handle context menu in bottom-left area.""" - if event.button() == Qt.RightButton and self.context_menu_area.contains(event.pos()): - self.show_context_menu(event.globalPos()) - - def show_context_menu(self, position): - """Displays a right-click context menu.""" - menu = QMenu(self) - quit_action = QAction("Quit", self) - quit_action.triggered.connect(self.close) - menu.addAction(quit_action) - menu.exec_(position) - - def paintEvent(self, event): - """Render transparent overlay and the small clickable menu area.""" - painter = QPainter(self) - painter.setRenderHint(QPainter.Antialiasing) - - # Draw semi-transparent context menu area - painter.setBrush(QBrush(QColor(50, 50, 50, 150))) # Dark semi-transparent box - painter.setPen(QPen(QColor(200, 200, 200, 200))) - painter.drawRect(self.context_menu_area) - - painter.setFont(self.font()) - painter.setPen(QColor(255, 255, 255)) - painter.drawText(self.context_menu_area, Qt.AlignCenter, "Menu") - -if __name__ == "__main__": +# Entry Point +if __name__ == '__main__': app = QApplication(sys.argv) - window = TransparentGraphWindow() + window = MainWindow() window.show() sys.exit(app.exec_()) diff --git a/debug_processed.png b/debug_processed.png deleted file mode 100644 index 85eebd722147fd0d524016f46dd764e44308233b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2823 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 m{Cp!dl>QhW<7uSdh5jE~$S_yT^0>DE0000