diff --git a/Modules/__pycache__/data_collector.cpython-312.pyc b/Modules/__pycache__/data_collector.cpython-312.pyc index bb082f3..2cfb315 100644 Binary files a/Modules/__pycache__/data_collector.cpython-312.pyc and b/Modules/__pycache__/data_collector.cpython-312.pyc differ diff --git a/Modules/data_collector.py b/Modules/data_collector.py index 2144aa8..bc2e29e 100644 --- a/Modules/data_collector.py +++ b/Modules/data_collector.py @@ -3,12 +3,13 @@ import threading import time import re +import sys import numpy as np import cv2 import pytesseract from PIL import Image, ImageGrab, ImageFilter -from PyQt5.QtWidgets import QWidget, QApplication +from PyQt5.QtWidgets import QApplication, QWidget from PyQt5.QtCore import QRect, QPoint, Qt, QMutex, QTimer from PyQt5.QtGui import QPainter, QPen, QColor, QFont @@ -21,19 +22,37 @@ LABEL_HEIGHT = 20 collector_mutex = QMutex() regions = {} -overlay_window = None + +app_instance = None + +def _ensure_qapplication(): + """ + Ensures that QApplication is initialized before creating widgets. + """ + global app_instance + if QApplication.instance() is None: + print("Starting QApplication in a separate thread.") + app_instance = QApplication(sys.argv) + threading.Thread(target=app_instance.exec_, daemon=True).start() def create_ocr_region(region_id, x=250, y=50, w=DEFAULT_WIDTH, h=DEFAULT_HEIGHT): + """ + Creates an OCR region with a visible, resizable box on the screen. + """ + print(f"Creating OCR Region: {region_id} at ({x}, {y}, {w}, {h})") + + _ensure_qapplication() # Ensure QApplication is running first + collector_mutex.lock() if region_id in regions: collector_mutex.unlock() return regions[region_id] = { 'bbox': [x, y, w, h], - 'raw_text': "" + 'raw_text': "", + 'widget': OCRRegionWidget(x, y, w, h, region_id) } collector_mutex.unlock() - _ensure_overlay() def get_raw_text(region_id): collector_mutex.lock() @@ -69,6 +88,8 @@ def _update_ocr_loop(): regions[rid]['raw_text'] = raw_text collector_mutex.unlock() + print(f"OCR Text for {rid}: {raw_text}") + time.sleep(0.7) def _preprocess_image(image): @@ -77,130 +98,87 @@ def _preprocess_image(image): thresh = scaled.point(lambda p: 255 if p > 200 else 0) return thresh.filter(ImageFilter.MedianFilter(3)) -def _ensure_overlay(): - """ - Creates the overlay window if none exists. - If no QApplication instance is running yet, schedule the creation after - the main application event loop starts (to avoid "Must construct a QApplication first" errors). - """ - global overlay_window - if overlay_window is not None: - return +class OCRRegionWidget(QWidget): + def __init__(self, x, y, w, h, region_id): + super().__init__() - # If there's already a running QApplication, create overlay immediately. - if QApplication.instance() is not None: - overlay_window = OverlayCanvas() - overlay_window.show() - else: - # Schedule creation for when the app event loop is up. - def delayed_create(): - global overlay_window - if overlay_window is None: - overlay_window = OverlayCanvas() - overlay_window.show() - - QTimer.singleShot(0, delayed_create) - -class OverlayCanvas(QWidget): - def __init__(self, parent=None): - super().__init__(parent) - screen_geo = QApplication.primaryScreen().geometry() - self.setGeometry(screen_geo) - self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) + 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.drag_offset = None self.selected_handle = None - self.selected_region_id = None + self.region_id = region_id + + print(f"OCR Region Widget Created at {x}, {y}, {w}, {h}") + + self.show() def paintEvent(self, event): painter = QPainter(self) pen = QPen(QColor(0, 0, 255)) - pen.setWidth(5) + pen.setWidth(3) painter.setPen(pen) - collector_mutex.lock() - region_copy = {rid: data['bbox'][:] for rid, data in regions.items()} - collector_mutex.unlock() + # Draw main rectangle + painter.drawRect(0, 0, self.width(), self.height()) - for rid, bbox in region_copy.items(): - x, y, w, h = bbox - painter.drawRect(x, y, w, h) - painter.setFont(QFont("Arial", 12, QFont.Bold)) - painter.setPen(QColor(0, 0, 255)) - painter.drawText(x, y - 5, f"OCR Region: {rid}") + # Draw resize handles + painter.setBrush(QColor(0, 0, 255)) + for handle in self._resize_handles(): + painter.drawRect(handle) + + def _resize_handles(self): + w, h = self.width(), self.height() + return [ + QRect(0, 0, HANDLE_SIZE, HANDLE_SIZE), # Top-left + QRect(w - HANDLE_SIZE, h - HANDLE_SIZE, HANDLE_SIZE, HANDLE_SIZE) # Bottom-right + ] def mousePressEvent(self, event): if event.button() == Qt.LeftButton: - collector_mutex.lock() - all_items = list(regions.items()) - collector_mutex.unlock() - - for rid, data in all_items: - x, y, w, h = data['bbox'] - handles = self._resize_handles(x, y, w, h) - for i, handle_rect in enumerate(handles): - if handle_rect.contains(event.pos()): - self.selected_handle = i - self.selected_region_id = rid - return - if QRect(x, y, w, h).contains(event.pos()): - self.drag_offset = event.pos() - QPoint(x, y) - self.selected_region_id = rid + for i, handle in enumerate(self._resize_handles()): + if handle.contains(event.pos()): + self.selected_handle = i return - def mouseMoveEvent(self, event): - if not self.selected_region_id: - return - collector_mutex.lock() - if self.selected_region_id not in regions: - collector_mutex.unlock() - return - bbox = regions[self.selected_region_id]['bbox'] - collector_mutex.unlock() + self.drag_offset = event.pos() - x, y, w, h = bbox + def mouseMoveEvent(self, event): if self.selected_handle is not None: - # resizing - if self.selected_handle == 0: # top-left - new_w = w + (x - event.x()) - new_h = h + (y - event.y()) - new_x = event.x() - new_y = event.y() - if new_w < 10: new_w = 10 - if new_h < 10: new_h = 10 - collector_mutex.lock() - if self.selected_region_id in regions: - regions[self.selected_region_id]['bbox'] = [new_x, new_y, new_w, new_h] - collector_mutex.unlock() - elif self.selected_handle == 1: # bottom-right - new_w = event.x() - x - new_h = event.y() - y - if new_w < 10: new_w = 10 - if new_h < 10: new_h = 10 - collector_mutex.lock() - if self.selected_region_id in regions: - regions[self.selected_region_id]['bbox'] = [x, y, new_w, new_h] - collector_mutex.unlock() + w, h = self.width(), self.height() + if self.selected_handle == 0: # Top-left + new_w = w + (self.x() - event.globalX()) + new_h = h + (self.y() - event.globalY()) + new_x = event.globalX() + new_y = event.globalY() + if new_w < 20: new_w = 20 + if new_h < 20: new_h = 20 + self.setGeometry(new_x, new_y, new_w, new_h) + elif self.selected_handle == 1: # Bottom-right + new_w = event.globalX() - self.x() + new_h = event.globalY() - self.y() + if new_w < 20: new_w = 20 + if new_h < 20: new_h = 20 + self.setGeometry(self.x(), self.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: - # dragging - new_x = event.x() - self.drag_offset.x() - new_y = event.y() - self.drag_offset.y() + 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.selected_region_id in regions: - regions[self.selected_region_id]['bbox'][0] = new_x - regions[self.selected_region_id]['bbox'][1] = new_y + if self.region_id in regions: + regions[self.region_id]['bbox'] = [new_x, new_y, self.width(), self.height()] collector_mutex.unlock() - self.update() def mouseReleaseEvent(self, event): self.selected_handle = None self.drag_offset = None - self.selected_region_id = None - - def _resize_handles(self, x, y, w, h): - return [ - QRect(x - HANDLE_SIZE//2, y - HANDLE_SIZE//2, HANDLE_SIZE, HANDLE_SIZE), # top-left - QRect(x + w - HANDLE_SIZE//2, y + h - HANDLE_SIZE//2, HANDLE_SIZE, HANDLE_SIZE) # bottom-right - ] diff --git a/bars_template.png b/Nodes/Flyff/Resources/bars_template.png similarity index 100% rename from bars_template.png rename to Nodes/Flyff/Resources/bars_template.png diff --git a/Nodes/Flyff/__pycache__/flyff_character_status_node.cpython-312.pyc b/Nodes/Flyff/__pycache__/flyff_character_status_node.cpython-312.pyc index fa3bce4..3192abd 100644 Binary files a/Nodes/Flyff/__pycache__/flyff_character_status_node.cpython-312.pyc and b/Nodes/Flyff/__pycache__/flyff_character_status_node.cpython-312.pyc differ diff --git a/Nodes/Flyff/flyff_character_status_node.py b/Nodes/Flyff/flyff_character_status_node.py index afd9701..3a2b5ef 100644 --- a/Nodes/Flyff/flyff_character_status_node.py +++ b/Nodes/Flyff/flyff_character_status_node.py @@ -1,46 +1,44 @@ #!/usr/bin/env python3 """ -Flyff Character Status Node (New Version): - - Has no inputs/outputs. - - Creates an OCR region in data_collector. - - Periodically grabs raw text from that region, parses it here in the node, - and sets data_manager's HP, MP, FP, EXP accordingly. - - Also updates its own text fields with the parsed values. +Flyff Character Status Node: +- Creates an OCR region in data_collector. +- Periodically grabs raw text from that region and updates status. """ import re from OdenGraphQt import BaseNode from PyQt5.QtWidgets import QMessageBox +from PyQt5.QtCore import QTimer # Corrected import from Modules import data_manager, data_collector class FlyffCharacterStatusNode(BaseNode): - __identifier__ = 'bunny-lab.io.flyff_character_status_node' - NODE_NAME = 'Flyff - Character Status' + __identifier__ = "bunny-lab.io.flyff_character_status_node" + NODE_NAME = "Flyff - Character Status" def __init__(self): super(FlyffCharacterStatusNode, self).__init__() - # Prevent duplicates + 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 text fields for display - 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.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%") - # Create a unique region id for this node (or just "character_status") self.region_id = "character_status" data_collector.create_ocr_region(self.region_id, x=250, y=50, w=180, h=130) - # Start the data_collector background thread (if not already started) data_collector.start_collector() - - # Set the node name 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. @@ -52,6 +50,8 @@ class FlyffCharacterStatusNode(BaseNode): exp_value = 0.0 if len(lines) >= 4: + print("Processing OCR Lines:", lines) # Debugging output + # line 1: HP hp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[0]) if hp_match: @@ -82,15 +82,14 @@ class FlyffCharacterStatusNode(BaseNode): def process_input(self): """ - Called periodically by the global timer in your main application (borealis.py). + Called periodically to update character status from OCR. """ - # Grab raw text from data_collector raw_text = data_collector.get_raw_text(self.region_id) + print("Raw OCR Text:", raw_text) # Debugging OCR text reading - # Parse it hp_c, hp_t, mp_c, mp_t, fp_c, fp_t, exp_v = self.parse_character_stats(raw_text) - # Update data_manager + # Update the data manager with the parsed values data_manager.set_data_bulk({ "hp_current": hp_c, "hp_total": hp_t, @@ -101,8 +100,8 @@ class FlyffCharacterStatusNode(BaseNode): "exp": exp_v }) - # Update the node's 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}%") + # 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}%")