Restructured the project and optimized several nodes.
This commit is contained in:
parent
670ae774ef
commit
e30ba4ec4f
BIN
Modules/__pycache__/data_collector.cpython-312.pyc
Normal file
BIN
Modules/__pycache__/data_collector.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Modules/__pycache__/data_manager.cpython-312.pyc
Normal file
BIN
Modules/__pycache__/data_manager.cpython-312.pyc
Normal file
Binary file not shown.
@ -1,326 +1,206 @@
|
|||||||
#!/usr/bin/env python3
|
# Modules/data_collector.py
|
||||||
|
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
import threading
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import cv2
|
import cv2
|
||||||
import pytesseract
|
import pytesseract
|
||||||
from flask import Flask, jsonify
|
|
||||||
from PIL import Image, ImageGrab, ImageFilter
|
from PIL import Image, ImageGrab, ImageFilter
|
||||||
from PyQt5.QtWidgets import QApplication, QWidget
|
|
||||||
from PyQt5.QtCore import QTimer, QRect, QPoint, Qt, QMutex
|
from PyQt5.QtWidgets import QWidget, QApplication
|
||||||
|
from PyQt5.QtCore import QRect, QPoint, Qt, QMutex, QTimer
|
||||||
from PyQt5.QtGui import QPainter, QPen, QColor, QFont
|
from PyQt5.QtGui import QPainter, QPen, QColor, QFont
|
||||||
|
|
||||||
pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"
|
pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# Global Config
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
POLLING_RATE_MS = 500
|
|
||||||
MAX_DATA_POINTS = 8
|
|
||||||
DEFAULT_WIDTH = 180
|
DEFAULT_WIDTH = 180
|
||||||
DEFAULT_HEIGHT = 130
|
DEFAULT_HEIGHT = 130
|
||||||
HANDLE_SIZE = 8
|
HANDLE_SIZE = 8
|
||||||
LABEL_HEIGHT = 20
|
LABEL_HEIGHT = 20
|
||||||
|
|
||||||
# Template Matching Threshold (Define it here)
|
collector_mutex = QMutex()
|
||||||
MATCH_THRESHOLD = 0.4 # Set to 0.4 as a typical value for correlation threshold
|
regions = {}
|
||||||
|
overlay_window = None
|
||||||
|
|
||||||
# Flask API setup
|
def create_ocr_region(region_id, x=250, y=50, w=DEFAULT_WIDTH, h=DEFAULT_HEIGHT):
|
||||||
app = Flask(__name__)
|
collector_mutex.lock()
|
||||||
|
if region_id in regions:
|
||||||
# **Shared Region Data (Thread-Safe)**
|
collector_mutex.unlock()
|
||||||
region_lock = QMutex() # Mutex to synchronize access between UI and OCR thread
|
return
|
||||||
shared_region = {
|
regions[region_id] = {
|
||||||
"x": 250,
|
'bbox': [x, y, w, h],
|
||||||
"y": 50,
|
'raw_text': ""
|
||||||
"w": DEFAULT_WIDTH,
|
|
||||||
"h": DEFAULT_HEIGHT
|
|
||||||
}
|
|
||||||
|
|
||||||
# Global variable for OCR data
|
|
||||||
latest_data = {
|
|
||||||
"hp_current": 0,
|
|
||||||
"hp_total": 0,
|
|
||||||
"mp_current": 0,
|
|
||||||
"mp_total": 0,
|
|
||||||
"fp_current": 0,
|
|
||||||
"fp_total": 0,
|
|
||||||
"exp": 0.0000
|
|
||||||
}
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# OCR Data Collection
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
def preprocess_image(image):
|
|
||||||
"""
|
|
||||||
Preprocess the image for OCR: convert to grayscale, resize, and apply thresholding.
|
|
||||||
"""
|
|
||||||
gray = image.convert("L") # Convert to grayscale
|
|
||||||
scaled = gray.resize((gray.width * 3, gray.height * 3)) # Upscale the image for better accuracy
|
|
||||||
thresh = scaled.point(lambda p: p > 200 and 255) # Apply a threshold to clean up the image
|
|
||||||
return thresh.filter(ImageFilter.MedianFilter(3)) # Apply a median filter to remove noise
|
|
||||||
|
|
||||||
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 locate_bars_opencv(template_path, threshold=MATCH_THRESHOLD):
|
|
||||||
"""
|
|
||||||
Attempt to locate the bars via OpenCV template matching.
|
|
||||||
"""
|
|
||||||
screenshot_pil = ImageGrab.grab()
|
|
||||||
screenshot_np = np.array(screenshot_pil)
|
|
||||||
screenshot_bgr = cv2.cvtColor(screenshot_np, cv2.COLOR_RGB2BGR)
|
|
||||||
template_bgr = cv2.imread(template_path, cv2.IMREAD_COLOR)
|
|
||||||
if template_bgr is None:
|
|
||||||
return None
|
|
||||||
result = cv2.matchTemplate(screenshot_bgr, template_bgr, cv2.TM_CCOEFF_NORMED)
|
|
||||||
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
|
|
||||||
th, tw, _ = template_bgr.shape
|
|
||||||
if max_val >= threshold:
|
|
||||||
found_x, found_y = max_loc
|
|
||||||
return (found_x, found_y, tw, th)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def parse_all_stats(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:
|
collector_mutex.unlock()
|
||||||
return stats_dict
|
_ensure_overlay()
|
||||||
|
|
||||||
hp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[0])
|
def get_raw_text(region_id):
|
||||||
if hp_match:
|
collector_mutex.lock()
|
||||||
stats_dict["hp"] = (int(hp_match.group(1)), int(hp_match.group(2)))
|
if region_id not in regions:
|
||||||
|
collector_mutex.unlock()
|
||||||
|
return ""
|
||||||
|
text = regions[region_id]['raw_text']
|
||||||
|
collector_mutex.unlock()
|
||||||
|
return text
|
||||||
|
|
||||||
mp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[1])
|
def start_collector():
|
||||||
if mp_match:
|
t = threading.Thread(target=_update_ocr_loop, daemon=True)
|
||||||
stats_dict["mp"] = (int(mp_match.group(1)), int(mp_match.group(2)))
|
t.start()
|
||||||
|
|
||||||
fp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[2])
|
def _update_ocr_loop():
|
||||||
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
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# Region & UI
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
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),
|
|
||||||
]
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# Flask API Server
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
@app.route('/data')
|
|
||||||
def get_data():
|
|
||||||
"""Returns the latest OCR data as JSON."""
|
|
||||||
return jsonify(latest_data)
|
|
||||||
|
|
||||||
def collect_ocr_data():
|
|
||||||
"""
|
|
||||||
Collects OCR data every 0.5 seconds and updates global latest_data.
|
|
||||||
"""
|
|
||||||
global latest_data
|
|
||||||
while True:
|
while True:
|
||||||
# **Fetch updated region values from UI (thread-safe)**
|
collector_mutex.lock()
|
||||||
region_lock.lock() # Lock for thread safety
|
region_ids = list(regions.keys())
|
||||||
x, y, w, h = shared_region["x"], shared_region["y"], shared_region["w"], shared_region["h"]
|
collector_mutex.unlock()
|
||||||
region_lock.unlock()
|
|
||||||
|
|
||||||
# **Grab the image of the updated region**
|
for rid in region_ids:
|
||||||
screenshot = ImageGrab.grab(bbox=(x, y, x + w, y + h))
|
collector_mutex.lock()
|
||||||
|
bbox = regions[rid]['bbox'][:]
|
||||||
|
collector_mutex.unlock()
|
||||||
|
|
||||||
# **Debug: Save screenshots to verify capture**
|
x, y, w, h = bbox
|
||||||
screenshot.save("debug_screenshot.png")
|
screenshot = ImageGrab.grab(bbox=(x, y, x + w, y + h))
|
||||||
|
processed = _preprocess_image(screenshot)
|
||||||
|
raw_text = pytesseract.image_to_string(processed, config='--psm 4 --oem 1')
|
||||||
|
|
||||||
# Preprocess image
|
collector_mutex.lock()
|
||||||
processed = preprocess_image(screenshot)
|
if rid in regions:
|
||||||
processed.save("debug_processed.png") # Debug: Save processed image
|
regions[rid]['raw_text'] = raw_text
|
||||||
|
collector_mutex.unlock()
|
||||||
|
|
||||||
# Run OCR
|
time.sleep(0.7)
|
||||||
text = pytesseract.image_to_string(processed, config='--psm 4 --oem 1')
|
|
||||||
|
|
||||||
stats = parse_all_stats(text.strip())
|
def _preprocess_image(image):
|
||||||
hp_cur, hp_max = stats["hp"]
|
gray = image.convert("L")
|
||||||
mp_cur, mp_max = stats["mp"]
|
scaled = gray.resize((gray.width * 3, gray.height * 3))
|
||||||
fp_cur, fp_max = stats["fp"]
|
thresh = scaled.point(lambda p: 255 if p > 200 else 0)
|
||||||
exp_val = stats["exp"]
|
return thresh.filter(ImageFilter.MedianFilter(3))
|
||||||
|
|
||||||
# Update latest data
|
|
||||||
latest_data = {
|
|
||||||
"hp_current": hp_cur,
|
|
||||||
"hp_total": hp_max,
|
|
||||||
"mp_current": mp_cur,
|
|
||||||
"mp_total": mp_max,
|
|
||||||
"fp_current": fp_cur,
|
|
||||||
"fp_total": fp_max,
|
|
||||||
"exp": exp_val
|
|
||||||
}
|
|
||||||
|
|
||||||
# DEBUG OUTPUT
|
def _ensure_overlay():
|
||||||
print(f"Flyff - Character Status: HP: {hp_cur}/{hp_max}, MP: {mp_cur}/{mp_max}, FP: {fp_cur}/{fp_max}, EXP: {exp_val}%")
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
time.sleep(0.5)
|
# 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)
|
||||||
# OverlayCanvas (UI)
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
class OverlayCanvas(QWidget):
|
class OverlayCanvas(QWidget):
|
||||||
"""
|
|
||||||
UI overlay that allows dragging/resizing of the OCR region.
|
|
||||||
"""
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
# **Full-screen overlay**
|
|
||||||
screen_geo = QApplication.primaryScreen().geometry()
|
screen_geo = QApplication.primaryScreen().geometry()
|
||||||
self.setGeometry(screen_geo) # Set to full screen
|
self.setGeometry(screen_geo)
|
||||||
|
|
||||||
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
|
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
|
||||||
self.setAttribute(Qt.WA_TranslucentBackground, True)
|
self.setAttribute(Qt.WA_TranslucentBackground, True)
|
||||||
|
|
||||||
# **Load shared region**
|
|
||||||
self.region = shared_region
|
|
||||||
self.drag_offset = None
|
self.drag_offset = None
|
||||||
self.selected_handle = None
|
self.selected_handle = None
|
||||||
|
self.selected_region_id = None
|
||||||
|
|
||||||
def paintEvent(self, event):
|
def paintEvent(self, event):
|
||||||
"""Draw the blue OCR region."""
|
|
||||||
painter = QPainter(self)
|
painter = QPainter(self)
|
||||||
pen = QPen(QColor(0, 0, 255))
|
pen = QPen(QColor(0, 0, 255))
|
||||||
pen.setWidth(5) # Thicker lines
|
pen.setWidth(5)
|
||||||
painter.setPen(pen)
|
painter.setPen(pen)
|
||||||
painter.drawRect(self.region["x"], self.region["y"], self.region["w"], self.region["h"])
|
|
||||||
painter.setFont(QFont("Arial", 12, QFont.Bold))
|
collector_mutex.lock()
|
||||||
painter.setPen(QColor(0, 0, 255))
|
region_copy = {rid: data['bbox'][:] for rid, data in regions.items()}
|
||||||
painter.drawText(self.region["x"], self.region["y"] - 5, "Character Status")
|
collector_mutex.unlock()
|
||||||
|
|
||||||
|
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}")
|
||||||
|
|
||||||
def mousePressEvent(self, event):
|
def mousePressEvent(self, event):
|
||||||
"""Handle drag and resize interactions."""
|
|
||||||
if event.button() == Qt.LeftButton:
|
if event.button() == Qt.LeftButton:
|
||||||
region_lock.lock() # Lock for thread safety
|
collector_mutex.lock()
|
||||||
x, y, w, h = self.region["x"], self.region["y"], self.region["w"], self.region["h"]
|
all_items = list(regions.items())
|
||||||
region_lock.unlock()
|
collector_mutex.unlock()
|
||||||
|
|
||||||
for i, handle in enumerate(self.resize_handles()):
|
for rid, data in all_items:
|
||||||
if handle.contains(event.pos()):
|
x, y, w, h = data['bbox']
|
||||||
self.selected_handle = i
|
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
|
||||||
return
|
return
|
||||||
|
|
||||||
if QRect(x, y, w, h).contains(event.pos()):
|
|
||||||
self.drag_offset = event.pos() - QPoint(x, y)
|
|
||||||
|
|
||||||
def mouseMoveEvent(self, event):
|
def mouseMoveEvent(self, event):
|
||||||
"""Allow dragging and resizing."""
|
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()
|
||||||
|
|
||||||
|
x, y, w, h = bbox
|
||||||
if self.selected_handle is not None:
|
if self.selected_handle is not None:
|
||||||
region_lock.lock()
|
# resizing
|
||||||
sr = self.region
|
if self.selected_handle == 0: # top-left
|
||||||
if self.selected_handle == 0: # Top-left
|
new_w = w + (x - event.x())
|
||||||
sr["w"] += sr["x"] - event.x()
|
new_h = h + (y - event.y())
|
||||||
sr["h"] += sr["y"] - event.y()
|
new_x = event.x()
|
||||||
sr["x"] = event.x()
|
new_y = event.y()
|
||||||
sr["y"] = event.y()
|
if new_w < 10: new_w = 10
|
||||||
elif self.selected_handle == 1: # Bottom-right
|
if new_h < 10: new_h = 10
|
||||||
sr["w"] = event.x() - sr["x"]
|
collector_mutex.lock()
|
||||||
sr["h"] = event.y() - sr["y"]
|
if self.selected_region_id in regions:
|
||||||
|
regions[self.selected_region_id]['bbox'] = [new_x, new_y, new_w, new_h]
|
||||||
sr["w"] = max(sr["w"], 10)
|
collector_mutex.unlock()
|
||||||
sr["h"] = max(sr["h"], 10)
|
elif self.selected_handle == 1: # bottom-right
|
||||||
region_lock.unlock()
|
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()
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
elif self.drag_offset:
|
elif self.drag_offset:
|
||||||
region_lock.lock()
|
# dragging
|
||||||
self.region["x"] = event.x() - self.drag_offset.x()
|
new_x = event.x() - self.drag_offset.x()
|
||||||
self.region["y"] = event.y() - self.drag_offset.y()
|
new_y = event.y() - self.drag_offset.y()
|
||||||
region_lock.unlock()
|
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
|
||||||
|
collector_mutex.unlock()
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def mouseReleaseEvent(self, event):
|
def mouseReleaseEvent(self, event):
|
||||||
"""End drag or resize event."""
|
|
||||||
self.selected_handle = None
|
self.selected_handle = None
|
||||||
self.drag_offset = None
|
self.drag_offset = None
|
||||||
|
self.selected_region_id = None
|
||||||
|
|
||||||
def resize_handles(self):
|
def _resize_handles(self, x, y, w, h):
|
||||||
"""Get the resizing handles of the region."""
|
|
||||||
return [
|
return [
|
||||||
QRect(self.region["x"] - HANDLE_SIZE // 2, self.region["y"] - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),
|
QRect(x - HANDLE_SIZE//2, y - HANDLE_SIZE//2, HANDLE_SIZE, HANDLE_SIZE), # top-left
|
||||||
QRect(self.region["x"] + self.region["w"] - HANDLE_SIZE // 2, self.region["y"] + self.region["h"] - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE)
|
QRect(x + w - HANDLE_SIZE//2, y + h - HANDLE_SIZE//2, HANDLE_SIZE, HANDLE_SIZE) # bottom-right
|
||||||
]
|
]
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# Start Application
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
def run_flask_app():
|
|
||||||
"""Runs the Flask API server in a separate thread."""
|
|
||||||
app.run(host="127.0.0.1", port=5000)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
# Start the OCR thread
|
|
||||||
collector_thread = threading.Thread(target=collect_ocr_data, daemon=True)
|
|
||||||
collector_thread.start()
|
|
||||||
|
|
||||||
# Start the Flask API thread
|
|
||||||
flask_thread = threading.Thread(target=run_flask_app, daemon=True)
|
|
||||||
flask_thread.start()
|
|
||||||
|
|
||||||
# Start PyQt5 GUI
|
|
||||||
app_gui = QApplication([])
|
|
||||||
overlay_window = OverlayCanvas()
|
|
||||||
overlay_window.show()
|
|
||||||
|
|
||||||
# Run event loop
|
|
||||||
app_gui.exec_()
|
|
||||||
|
66
Modules/data_manager.py
Normal file
66
Modules/data_manager.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
# Modules/data_manager.py
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from flask import Flask, jsonify
|
||||||
|
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
|
||||||
|
|
||||||
|
# Flask Application
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
@app.route('/data')
|
||||||
|
def data_api():
|
||||||
|
"""
|
||||||
|
Returns the current character metrics as JSON.
|
||||||
|
"""
|
||||||
|
return jsonify(get_data())
|
||||||
|
|
||||||
|
def start_api_server():
|
||||||
|
"""
|
||||||
|
Starts the Flask API server in a separate daemon thread.
|
||||||
|
"""
|
||||||
|
def run():
|
||||||
|
app.run(host="127.0.0.1", port=5000)
|
||||||
|
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()
|
BIN
Nodes/Flyff/__pycache__/flyff_EXP_current.cpython-312.pyc
Normal file
BIN
Nodes/Flyff/__pycache__/flyff_EXP_current.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Nodes/Flyff/__pycache__/flyff_FP_current.cpython-312.pyc
Normal file
BIN
Nodes/Flyff/__pycache__/flyff_FP_current.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Nodes/Flyff/__pycache__/flyff_FP_total.cpython-312.pyc
Normal file
BIN
Nodes/Flyff/__pycache__/flyff_FP_total.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Nodes/Flyff/__pycache__/flyff_HP_current.cpython-312.pyc
Normal file
BIN
Nodes/Flyff/__pycache__/flyff_HP_current.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Nodes/Flyff/__pycache__/flyff_HP_total.cpython-312.pyc
Normal file
BIN
Nodes/Flyff/__pycache__/flyff_HP_total.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Nodes/Flyff/__pycache__/flyff_MP_current.cpython-312.pyc
Normal file
BIN
Nodes/Flyff/__pycache__/flyff_MP_current.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Nodes/Flyff/__pycache__/flyff_MP_total.cpython-312.pyc
Normal file
BIN
Nodes/Flyff/__pycache__/flyff_MP_total.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
108
Nodes/Flyff/flyff_character_status_node.py
Normal file
108
Nodes/Flyff/flyff_character_status_node.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
#!/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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from OdenGraphQt import BaseNode
|
||||||
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
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__()
|
||||||
|
# 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%")
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
|
||||||
|
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 by the global timer in your main application (borealis.py).
|
||||||
|
"""
|
||||||
|
# Grab raw text from data_collector
|
||||||
|
raw_text = data_collector.get_raw_text(self.region_id)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
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 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}%")
|
BIN
Nodes/Organization/__pycache__/backdrop_node.cpython-312.pyc
Normal file
BIN
Nodes/Organization/__pycache__/backdrop_node.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,126 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
"""
|
|
||||||
Standardized Flyff Character Status Node:
|
|
||||||
- Polls an API for character stats and updates values dynamically.
|
|
||||||
- Uses a global update timer for processing.
|
|
||||||
- Immediately transmits updated values to connected nodes.
|
|
||||||
- If the API is unreachable, it will wait for 5 seconds before retrying
|
|
||||||
and log the error only once per retry period.
|
|
||||||
- Calls self.view.draw_node() after updating the node name, ensuring
|
|
||||||
the node's bounding box recalculates even when the API is disconnected.
|
|
||||||
- Port colors adjusted to match earlier styling.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from OdenGraphQt import BaseNode
|
|
||||||
from Qt import QtCore
|
|
||||||
import requests
|
|
||||||
import traceback
|
|
||||||
import time
|
|
||||||
|
|
||||||
class FlyffCharacterStatusNode(BaseNode):
|
|
||||||
__identifier__ = 'bunny-lab.io.flyff_character_status_node'
|
|
||||||
NODE_NAME = 'Flyff - Character Status'
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super(FlyffCharacterStatusNode, self).__init__()
|
|
||||||
self.values = {
|
|
||||||
"HP: Current": "N/A",
|
|
||||||
"HP: Total": "N/A",
|
|
||||||
"MP: Current": "N/A",
|
|
||||||
"MP: Total": "N/A",
|
|
||||||
"FP: Current": "N/A",
|
|
||||||
"FP: Total": "N/A",
|
|
||||||
"EXP": "N/A"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Set each output with a custom color:
|
|
||||||
# (Choose values close to the screenshot for each port.)
|
|
||||||
self.add_output('HP: Current', color=(126, 36, 57))
|
|
||||||
self.add_output('HP: Total', color=(126, 36, 57))
|
|
||||||
self.add_output('MP: Current', color=(35, 89, 144))
|
|
||||||
self.add_output('MP: Total', color=(35, 89, 144))
|
|
||||||
self.add_output('FP: Current', color=(36, 116, 32))
|
|
||||||
self.add_output('FP: Total', color=(36, 116, 32))
|
|
||||||
self.add_output('EXP', color=(48, 116, 143))
|
|
||||||
|
|
||||||
self.set_name("Flyff - Character Status (API Disconnected)")
|
|
||||||
self.view.draw_node() # ensure bounding box updates initially
|
|
||||||
|
|
||||||
# Variables to handle API downtime gracefully:
|
|
||||||
self._api_down = False
|
|
||||||
self._last_api_attempt = 0
|
|
||||||
self._retry_interval = 5 # seconds to wait before retrying after a failure
|
|
||||||
self._last_error_printed = 0
|
|
||||||
|
|
||||||
def process_input(self):
|
|
||||||
current_time = time.time()
|
|
||||||
# If the API is down, only retry after _retry_interval seconds
|
|
||||||
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)
|
|
||||||
if response.status_code == 200:
|
|
||||||
data = response.json()
|
|
||||||
if isinstance(data, dict):
|
|
||||||
mapping = {
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
updated = False
|
|
||||||
for key, value in data.items():
|
|
||||||
if key in mapping:
|
|
||||||
formatted_key = mapping[key]
|
|
||||||
if str(value) != self.values.get(formatted_key, None):
|
|
||||||
self.values[formatted_key] = str(value)
|
|
||||||
updated = True
|
|
||||||
self._api_down = False
|
|
||||||
if updated:
|
|
||||||
self.set_name("Flyff - Character Status (API Connected)")
|
|
||||||
self.view.draw_node() # recalc bounding box on connect
|
|
||||||
self.transmit_data()
|
|
||||||
else:
|
|
||||||
if current_time - self._last_error_printed >= self._retry_interval:
|
|
||||||
print("[ERROR] Unexpected API response format (not a dict):", data)
|
|
||||||
self._last_error_printed = current_time
|
|
||||||
self.set_name("Flyff - Character Status (API Disconnected)")
|
|
||||||
self.view.draw_node() # recalc bounding box on disconnect
|
|
||||||
self._api_down = True
|
|
||||||
else:
|
|
||||||
if current_time - self._last_error_printed >= self._retry_interval:
|
|
||||||
print(f"[ERROR] API request failed with status code {response.status_code}")
|
|
||||||
self._last_error_printed = current_time
|
|
||||||
self.set_name("Flyff - Character Status (API Disconnected)")
|
|
||||||
self.view.draw_node()
|
|
||||||
self._api_down = True
|
|
||||||
except Exception as e:
|
|
||||||
if current_time - self._last_error_printed >= self._retry_interval:
|
|
||||||
print("[ERROR] Error polling API in CharacterStatusNode:", str(e))
|
|
||||||
self._last_error_printed = current_time
|
|
||||||
self.set_name("Flyff - Character Status (API Disconnected)")
|
|
||||||
self.view.draw_node()
|
|
||||||
self._api_down = True
|
|
||||||
|
|
||||||
def transmit_data(self):
|
|
||||||
for stat, value in self.values.items():
|
|
||||||
port = self.get_output(stat)
|
|
||||||
if port and port.connected_ports():
|
|
||||||
for connected_port in port.connected_ports():
|
|
||||||
connected_node = connected_port.node()
|
|
||||||
if hasattr(connected_node, 'receive_data'):
|
|
||||||
try:
|
|
||||||
connected_node.receive_data(value, stat)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[ERROR] Error transmitting data to {connected_node}: {e}")
|
|
||||||
print("[ERROR] Stack Trace:\n", traceback.format_exc())
|
|
||||||
|
|
||||||
def receive_data(self, data, source_port_name=None):
|
|
||||||
# This node only transmits data; it does not receive external data.
|
|
||||||
pass
|
|
@ -1,21 +0,0 @@
|
|||||||
from OdenGraphQt import GroupNode
|
|
||||||
|
|
||||||
|
|
||||||
class MyGroupNode(GroupNode):
|
|
||||||
"""
|
|
||||||
example test group node with a in port and out port.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# set a unique node identifier.
|
|
||||||
__identifier__ = 'nodes.group'
|
|
||||||
|
|
||||||
# set the initial default node name.
|
|
||||||
NODE_NAME = 'group node'
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super(MyGroupNode, self).__init__()
|
|
||||||
self.set_color(50, 8, 25)
|
|
||||||
|
|
||||||
# create input and output port.
|
|
||||||
self.add_input('in')
|
|
||||||
self.add_output('out')
|
|
@ -1,155 +0,0 @@
|
|||||||
from OdenGraphQt import BaseNode
|
|
||||||
from OdenGraphQt.constants import NodePropWidgetEnum
|
|
||||||
from OdenGraphQt.widgets.node_widgets import NodeLineEditValidatorCheckBox
|
|
||||||
|
|
||||||
|
|
||||||
class DropdownMenuNode(BaseNode):
|
|
||||||
"""
|
|
||||||
An example node with a embedded added QCombobox menu.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# unique node identifier.
|
|
||||||
__identifier__ = 'nodes.widget'
|
|
||||||
|
|
||||||
# initial default node name.
|
|
||||||
NODE_NAME = 'menu'
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super(DropdownMenuNode, self).__init__()
|
|
||||||
|
|
||||||
# create input & output ports
|
|
||||||
self.add_input('in 1')
|
|
||||||
self.add_output('out 1')
|
|
||||||
self.add_output('out 2')
|
|
||||||
|
|
||||||
# create the QComboBox menu.
|
|
||||||
items = ["item 1", "item 2", "item 3"]
|
|
||||||
self.add_combo_menu(
|
|
||||||
"my_menu",
|
|
||||||
"Menu Test",
|
|
||||||
items=items,
|
|
||||||
tooltip="example custom tooltip",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TextInputNode(BaseNode):
|
|
||||||
"""
|
|
||||||
An example of a node with a embedded QLineEdit.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# unique node identifier.
|
|
||||||
__identifier__ = 'nodes.widget'
|
|
||||||
|
|
||||||
# initial default node name.
|
|
||||||
NODE_NAME = 'text'
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
pattern = r"^[A-Za-z0-9]*$"
|
|
||||||
placeholder = ""
|
|
||||||
tooltip = "Valid characters: A-Z a-z 0-9"
|
|
||||||
is_case_sensitive = True
|
|
||||||
checkbox_label = "Use Parser?"
|
|
||||||
|
|
||||||
# create input & output ports
|
|
||||||
self.add_input('in')
|
|
||||||
self.add_output('out')
|
|
||||||
|
|
||||||
# create QLineEdit text input widget.
|
|
||||||
self.add_text_input('my_input', 'Text Input', tab='widgets')
|
|
||||||
|
|
||||||
tool_btn_kwargs = {
|
|
||||||
"func": self._callback,
|
|
||||||
"tooltip": "Awesome"
|
|
||||||
}
|
|
||||||
kwargs = {
|
|
||||||
"validator": {
|
|
||||||
"pattern": pattern,
|
|
||||||
"placeholder": placeholder,
|
|
||||||
"tooltip": tooltip,
|
|
||||||
"is_case_insensitive": is_case_sensitive,
|
|
||||||
"checkbox_visible": True,
|
|
||||||
"tool_btn_visible": True,
|
|
||||||
},
|
|
||||||
"checkbox_label": checkbox_label,
|
|
||||||
"tool_btn": tool_btn_kwargs,
|
|
||||||
}
|
|
||||||
node_widget = NodeLineEditValidatorCheckBox(
|
|
||||||
"src_path",
|
|
||||||
pattern,
|
|
||||||
placeholder,
|
|
||||||
tooltip,
|
|
||||||
is_case_sensitive,
|
|
||||||
checkbox_label,
|
|
||||||
checkbox_visible=True,
|
|
||||||
tool_btn_visible=True,
|
|
||||||
widget_label="src_path",
|
|
||||||
parent=self.view,
|
|
||||||
)
|
|
||||||
node_widget.get_custom_widget().set_tool_btn(**tool_btn_kwargs)
|
|
||||||
self.add_custom_widget(
|
|
||||||
node_widget,
|
|
||||||
NodePropWidgetEnum.LINEEDIT_VALIDATOR_CHECKBOX.value,
|
|
||||||
"widgets",
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
kwargs2 = {
|
|
||||||
"validator": {
|
|
||||||
"pattern": pattern,
|
|
||||||
"placeholder": placeholder,
|
|
||||||
"tooltip": tooltip,
|
|
||||||
"is_case_insensitive": is_case_sensitive,
|
|
||||||
"checkbox_visible": False,
|
|
||||||
"tool_btn_visible": False,
|
|
||||||
},
|
|
||||||
"checkbox_label": "Check In Luggage?",
|
|
||||||
"tool_btn": tool_btn_kwargs,
|
|
||||||
}
|
|
||||||
node_widget2 = NodeLineEditValidatorCheckBox(
|
|
||||||
"dst_path",
|
|
||||||
pattern,
|
|
||||||
placeholder,
|
|
||||||
tooltip,
|
|
||||||
is_case_sensitive,
|
|
||||||
"Check In Luggage?",
|
|
||||||
checkbox_visible=False,
|
|
||||||
tool_btn_visible=False,
|
|
||||||
widget_label="dst_path",
|
|
||||||
parent=self.view,
|
|
||||||
)
|
|
||||||
node_widget2.get_custom_widget().set_tool_btn(**tool_btn_kwargs)
|
|
||||||
node_widget2.set_checkbox_visible(False)
|
|
||||||
node_widget2.set_tool_btn_visible(False)
|
|
||||||
self.add_custom_widget(
|
|
||||||
node_widget2,
|
|
||||||
NodePropWidgetEnum.LINEEDIT_VALIDATOR_CHECKBOX.value,
|
|
||||||
"widgets",
|
|
||||||
**kwargs2,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _callback(self):
|
|
||||||
print(f"YOU HAVE CLICKED ON '{self.NODE_NAME}'")
|
|
||||||
|
|
||||||
|
|
||||||
class CheckboxNode(BaseNode):
|
|
||||||
"""
|
|
||||||
An example of a node with 2 embedded QCheckBox widgets.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# set a unique node identifier.
|
|
||||||
__identifier__ = 'nodes.widget'
|
|
||||||
|
|
||||||
# set the initial default node name.
|
|
||||||
NODE_NAME = 'checkbox'
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super(CheckboxNode, self).__init__()
|
|
||||||
|
|
||||||
# create the checkboxes.
|
|
||||||
self.add_checkbox('cb_1', '', 'Checkbox 1', True)
|
|
||||||
self.add_checkbox('cb_2', '', 'Checkbox 2', False)
|
|
||||||
|
|
||||||
# create input and output port.
|
|
||||||
self.add_input('in', color=(200, 100, 0))
|
|
||||||
self.add_output('out', color=(0, 100, 200))
|
|
82
borealis.py
82
borealis.py
@ -1,11 +1,29 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
from Qt import QtWidgets, QtCore, QtGui
|
|
||||||
import sys
|
import sys
|
||||||
import pkgutil
|
import pkgutil
|
||||||
import importlib
|
import importlib
|
||||||
import inspect
|
import inspect
|
||||||
|
import os
|
||||||
|
|
||||||
|
from Qt import QtWidgets, QtCore, QtGui
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# MONKEY-PATCH to fix "module 'qtpy.QtGui' has no attribute 'QUndoStack'"
|
||||||
|
# OdenGraphQt tries to do QtGui.QUndoStack(self).
|
||||||
|
# We'll import QUndoStack from QtWidgets and attach it to QtGui.
|
||||||
|
try:
|
||||||
|
from qtpy.QtWidgets import QUndoStack
|
||||||
|
import qtpy
|
||||||
|
# Force QtGui.QUndoStack to reference QtWidgets.QUndoStack
|
||||||
|
qtpy.QtGui.QUndoStack = QUndoStack
|
||||||
|
except ImportError:
|
||||||
|
print("WARNING: Could not monkey-patch QUndoStack. You may see an error if OdenGraphQt needs it.")
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Import your data_manager so we can start the Flask server
|
||||||
|
from Modules import data_manager
|
||||||
|
|
||||||
# --- BEGIN MONKEY PATCH FOR PIPE COLOR (Optional) ---
|
# --- BEGIN MONKEY PATCH FOR PIPE COLOR (Optional) ---
|
||||||
# If you want custom pipe colors, uncomment this patch:
|
# If you want custom pipe colors, uncomment this patch:
|
||||||
@ -59,34 +77,78 @@ from OdenGraphQt import NodeGraph, BaseNode
|
|||||||
|
|
||||||
def import_nodes_from_folder(package_name):
|
def import_nodes_from_folder(package_name):
|
||||||
"""
|
"""
|
||||||
Dynamically import all modules from the given package
|
Recursively import all modules from the given package
|
||||||
and return a list of classes that subclass BaseNode.
|
and return a list of classes that subclass BaseNode.
|
||||||
"""
|
"""
|
||||||
imported_nodes = []
|
imported_nodes = []
|
||||||
package = importlib.import_module(package_name)
|
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)
|
# Get the root directory of the package
|
||||||
for name, obj in inspect.getmembers(module, inspect.isclass):
|
package_path = package.__path__[0]
|
||||||
if issubclass(obj, BaseNode) and obj.__module__ == module.__name__:
|
|
||||||
imported_nodes.append(obj)
|
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
|
||||||
|
|
||||||
|
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__:
|
||||||
|
imported_nodes.append(obj)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to import {module_name}: {e}")
|
||||||
|
|
||||||
return imported_nodes
|
return imported_nodes
|
||||||
|
|
||||||
def make_node_command(graph, node_type_str):
|
def make_node_command(graph, node_type_str):
|
||||||
"""
|
"""
|
||||||
Return a function that creates a node of the given type at the current cursor position.
|
Return a function that creates a node of the given type at the current cursor position.
|
||||||
|
For the Flyff Character Status Collector node, check if one already exists.
|
||||||
|
If so, schedule an error message to be shown.
|
||||||
|
|
||||||
|
Also ensure that node creation is delayed until after QApplication is up, to avoid
|
||||||
|
'QWidget: Must construct a QApplication before a QWidget' errors.
|
||||||
"""
|
"""
|
||||||
def command():
|
def real_create():
|
||||||
|
# Check if we are about to create a duplicate Character Status Collector node.
|
||||||
|
if node_type_str.startswith("bunny-lab.io.flyff_character_status_node"):
|
||||||
|
for node in graph.all_nodes():
|
||||||
|
if node.__class__.__name__ == "FlyffCharacterStatusNode":
|
||||||
|
# Show error message about duplicates
|
||||||
|
QtWidgets.QMessageBox.critical(
|
||||||
|
None,
|
||||||
|
"Error",
|
||||||
|
"Only one Flyff Character Status Collector node is allowed. If you added more, things would break (really) badly."
|
||||||
|
)
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
pos = graph.cursor_pos()
|
pos = graph.cursor_pos()
|
||||||
graph.create_node(node_type_str, pos=pos)
|
graph.create_node(node_type_str, pos=pos)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error creating node of type {node_type_str}: {e}")
|
QtWidgets.QMessageBox.critical(None, "Error", str(e))
|
||||||
|
|
||||||
|
def command():
|
||||||
|
# If there's already a QApplication running, just create the node now.
|
||||||
|
if QtWidgets.QApplication.instance():
|
||||||
|
real_create()
|
||||||
|
else:
|
||||||
|
# Otherwise, schedule the node creation for the next event cycle.
|
||||||
|
QtCore.QTimer.singleShot(0, real_create)
|
||||||
|
|
||||||
return command
|
return command
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
# Create the QApplication first
|
||||||
app = QtWidgets.QApplication([])
|
app = QtWidgets.QApplication([])
|
||||||
|
|
||||||
# Create the NodeGraph controller.
|
# Start the Flask server from data_manager so /data is always available
|
||||||
|
data_manager.start_api_server()
|
||||||
|
|
||||||
|
# Create the NodeGraph controller
|
||||||
|
# (the monkey-patch ensures NodeGraph won't crash if it tries QtGui.QUndoStack(self))
|
||||||
graph = NodeGraph()
|
graph = NodeGraph()
|
||||||
graph.widget.setWindowTitle("Project Borealis - Flyff Information Overlay")
|
graph.widget.setWindowTitle("Project Borealis - Flyff Information Overlay")
|
||||||
|
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 2.5 KiB |
Binary file not shown.
Before Width: | Height: | Size: 12 KiB |
Loading…
x
Reference in New Issue
Block a user