Implemented ReactJS Application Server with basic functionality.

This commit is contained in:
2025-03-20 02:43:13 -06:00
parent 5e6ad27f71
commit cffad033fa
11 changed files with 240 additions and 0 deletions

View File

@ -0,0 +1,78 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Shapes 1.15
import QtQuick.Window 2.15
Item {
id: root
width: Screen.width
height: Screen.height
// Grid overlay is enabled at startup.
property bool editMode: true
// Blue gradient background (edges fading inward) with stops shifted inward.
Rectangle {
id: gradientBackground
width: parent.width
height: parent.height
opacity: 0.5
gradient: Gradient {
// Shifted stops: outer stops moved to 0.1 and 0.9, inner stops to 0.4 and 0.6.
GradientStop { position: 0.1; color: Qt.rgba(0, 100/255, 255/255, 0.5) }
GradientStop { position: 0.4; color: Qt.rgba(0, 50/255, 180/255, 0.2) }
GradientStop { position: 0.5; color: Qt.rgba(0, 0, 0, 0.0) }
GradientStop { position: 0.6; color: Qt.rgba(0, 50/255, 180/255, 0.2) }
GradientStop { position: 0.9; color: Qt.rgba(0, 100/255, 255/255, 0.5) }
}
visible: editMode // Only show the gradient in edit mode
}
// Top & Bottom fade remains unchanged.
Rectangle {
id: topBottomGradient
width: parent.width
height: parent.height
opacity: 0.3
gradient: Gradient {
orientation: Gradient.Vertical
GradientStop { position: 0.0; color: Qt.rgba(0, 100/255, 255/255, 0.4) }
GradientStop { position: 0.3; color: Qt.rgba(0, 50/255, 180/255, 0.1) }
GradientStop { position: 0.5; color: Qt.rgba(0, 0, 0, 0.0) }
GradientStop { position: 0.7; color: Qt.rgba(0, 50/255, 180/255, 0.1) }
GradientStop { position: 1.0; color: Qt.rgba(0, 100/255, 255/255, 0.4) }
}
visible: editMode
}
// Full-Screen Dynamic Grid with 10% increased transparency (grid lines at 0.3 opacity).
Canvas {
id: gridCanvas
width: parent.width
height: parent.height
onPaint: {
var ctx = getContext("2d");
ctx.clearRect(0, 0, width, height);
ctx.strokeStyle = "rgba(255, 255, 255, 0.3)"; // Reduced opacity from 0.4 to 0.3.
ctx.lineWidth = 1;
var step = 120; // Grid spacing remains unchanged.
for (var x = 0; x < width; x += step) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
ctx.stroke();
}
for (var y = 0; y < height; y += step) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(width, y);
ctx.stroke();
}
}
Component.onCompleted: requestPaint()
onVisibleChanged: requestPaint()
visible: editMode // Hide when edit mode is off.
}
}

View File

@ -0,0 +1,193 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import pkgutil
import importlib
import inspect
import types
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QWidget
from PyQt5.QtCore import Qt, QUrl, QTimer
from PyQt5.QtGui import QGuiApplication
from PyQt5.QtQuick import QQuickView
# OdenGraphQt Fix: Monkey-patch QUndoStack
import OdenGraphQt.base.graph as base_graph
from PyQt5 import QtWidgets
base_graph.QtGui.QUndoStack = QtWidgets.QUndoStack
import OdenGraphQt.base.commands as base_commands
_original_redo = base_commands.NodesRemovedCmd.redo
_original_undo = base_commands.NodesRemovedCmd.undo
def _patched_redo(self):
try:
_original_redo(self)
except TypeError as e:
if "unexpected type" in str(e) and hasattr(self, 'node'):
node_ids = []
if isinstance(self.node, list):
node_ids = [getattr(n, 'id', str(n)) for n in self.node]
else:
node_ids = [getattr(self.node, 'id', str(self.node))]
self.graph.nodes_deleted.emit(node_ids)
else:
raise
def _patched_undo(self):
try:
_original_undo(self)
except TypeError as e:
if "unexpected type" in str(e) and hasattr(self, 'node'):
node_ids = []
if isinstance(self.node, list):
node_ids = [getattr(n, 'id', str(n)) for n in self.node]
else:
node_ids = [getattr(self.node, 'id', str(self.node))]
self.graph.nodes_deleted.emit(node_ids)
else:
raise
base_commands.NodesRemovedCmd.redo = _patched_redo
base_commands.NodesRemovedCmd.undo = _patched_undo
# OdenGraphQt Transparent Viewer
from OdenGraphQt.widgets.viewer import NodeViewer
class TransparentViewer(NodeViewer):
"""A NodeViewer that does not paint anything in drawBackground() -> Fully transparent."""
def drawBackground(self, painter, rect):
pass # Do nothing, ensuring transparency.
# NodeGraph & Node Import Helpers
from OdenGraphQt import NodeGraph, BaseNode
def import_nodes_from_folder(package_name):
imported_nodes = []
package = importlib.import_module(package_name)
for loader, module_name, is_pkg in pkgutil.walk_packages(
package.__path__, package.__name__ + "."):
module = importlib.import_module(module_name)
for name, obj in inspect.getmembers(module, inspect.isclass):
if issubclass(obj, BaseNode) and obj.__module__ == module.__name__:
imported_nodes.append(obj)
return imported_nodes
def make_node_command(graph, node_type):
def command():
try:
graph.create_node(node_type)
except Exception as e:
print(f"Error creating node of type {node_type}: {e}")
return command
# Edit Mode Button
class EditButton(QPushButton):
"""A small, frameless button to toggle edit mode."""
def __init__(self, parent=None):
super().__init__("Toggle Edit Mode", parent)
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
# Dark gray background with white text.
self.setStyleSheet("background-color: #444444; border: 1px solid black; color: white;")
self.resize(140, 40)
# Main Overlay Window
class MainWindow(QMainWindow):
"""A frameless, transparent overlay with OdenGraphQt nodes & edit mode toggle."""
def __init__(self):
super().__init__()
# Full-screen overlay
app = QApplication.instance()
screen_geo = app.primaryScreen().geometry()
self.setGeometry(screen_geo)
# Frameless, top-most, fully transparent
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
self.setAttribute(Qt.WA_TranslucentBackground, True)
# QML Background
self.qml_view = QQuickView()
self.qml_view.setSource(QUrl("qml/background_grid.qml"))
self.qml_view.setFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
self.qml_view.setClearBeforeRendering(True)
self.qml_view.setColor(Qt.transparent)
self.qml_view.show()
# Save the QML root object for later property sync
self.qml_root = self.qml_view.rootObject()
# NodeGraph with TransparentViewer
self.graph = NodeGraph(viewer=TransparentViewer())
self.nodeGraphWidget = self.graph.widget
self.nodeGraphWidget.setStyleSheet("background: transparent; border: none;")
# Transparent central widget
central = QWidget(self)
central.setAttribute(Qt.WA_TranslucentBackground, True)
self.setCentralWidget(central)
self.nodeGraphWidget.setParent(central)
self.nodeGraphWidget.setGeometry(central.rect())
# Edit Mode Button (Python controlled)
self.editButton = EditButton(self)
self.editButton.move(10, 10)
self.editButton.clicked.connect(self.toggleEditMode)
self.isEditMode = True # Set edit mode enabled by default
# Ensure QML grid overlay is enabled at startup
if self.qml_root:
self.qml_root.setProperty("editMode", self.isEditMode)
# Import custom nodes
try:
custom_nodes = import_nodes_from_folder('Nodes')
for node_class in custom_nodes:
self.graph.register_node(node_class)
graph_menu = self.graph.get_context_menu('graph')
for node_class in custom_nodes:
node_type = f"{node_class.__identifier__}.{node_class.__name__}"
node_name = node_class.NODE_NAME
graph_menu.add_command(
f"Add {node_name}",
make_node_command(self.graph, node_type)
)
except Exception as e:
print(f"Error setting up custom nodes: {e}")
# Global update timer
self.timer = QTimer(self)
self.timer.timeout.connect(self.global_update)
self.timer.start(500)
# Timer to ensure the button stays on top (hacky, but effective)
self.raiseTimer = QTimer(self)
self.raiseTimer.timeout.connect(self.editButton.raise_)
self.raiseTimer.start(1000) # Raise the button every 1 second
self.show()
self.nodeGraphWidget.setAttribute(Qt.WA_TransparentForMouseEvents, not self.isEditMode)
def toggleEditMode(self):
"""Toggle edit mode (pass-through clicks vs interactive)."""
self.isEditMode = not self.isEditMode
self.nodeGraphWidget.setAttribute(Qt.WA_TransparentForMouseEvents, not self.isEditMode)
# Button text remains constant.
self.editButton.setText("Toggle Edit Mode")
if self.qml_root:
self.qml_root.setProperty("editMode", self.isEditMode)
def global_update(self):
"""Update all nodes periodically."""
for node in self.graph.all_nodes():
if hasattr(node, "process_input"):
node.process_input()
# Entry Point
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())

View File

@ -0,0 +1,160 @@
import sys
import pkgutil
import importlib
import inspect
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QGraphicsView, QGraphicsScene, QGraphicsItem, QMenu
from PyQt5.QtCore import Qt, QTimer, QRectF, QPointF
from PyQt5.QtGui import QColor, QPainter, QPen, QBrush, QGradient, QLinearGradient
from PyQt5 import QtWidgets, QtCore, QtGui
from OdenGraphQt import NodeGraph, BaseNode
# --- Fix Missing QUndoStack in QtGui ---
import OdenGraphQt.base.graph as base_graph
base_graph.QtGui.QUndoStack = QtWidgets.QUndoStack # Monkey-patch the missing QUndoStack
# --- Custom Graph Scene ---
class CustomGraphScene(QGraphicsScene):
"""
Custom scene that draws a blueprint-style transparent grid with gradient shading.
"""
def __init__(self, parent=None):
super().__init__(parent)
self.setBackgroundBrush(QtCore.Qt.transparent)
self.grid_color = QtGui.QColor(100, 160, 160, 160) # Blueprint grid color (10% more transparent)
self.grid_size = 115
def drawBackground(self, painter, rect):
"""
Custom draw function to render a blueprint-style grid with gradient shading.
"""
painter.save()
painter.setRenderHint(QPainter.Antialiasing, False)
painter.setBrush(QtCore.Qt.NoBrush) # No background fill
pen = QPen(self.grid_color, 0.5)
left = int(rect.left()) - (int(rect.left()) % self.grid_size)
top = int(rect.top()) - (int(rect.top()) % self.grid_size)
# Draw vertical lines
lines = []
for x in range(left, int(rect.right()), self.grid_size):
lines.append(QtCore.QLineF(x, rect.top(), x, rect.bottom()))
# Draw horizontal lines
for y in range(top, int(rect.bottom()), self.grid_size):
lines.append(QtCore.QLineF(rect.left(), y, rect.right(), y))
painter.setPen(pen)
painter.drawLines(lines)
# Draw gradient shading (top and bottom)
gradient = QLinearGradient(QPointF(rect.left(), rect.top()), QPointF(rect.left(), rect.bottom()))
gradient.setColorAt(0.0, QColor(0, 40, 100, 220)) # Darker blue at the top
gradient.setColorAt(0.5, QColor(0, 0, 0, 0)) # Transparent in the middle
gradient.setColorAt(1.0, QColor(0, 40, 100, 220)) # Darker blue at the bottom
painter.fillRect(rect, QBrush(gradient))
painter.restore()
# --- Node Management ---
def import_nodes_from_folder(package_name):
imported_nodes = []
package = importlib.import_module(package_name)
for loader, module_name, is_pkg in pkgutil.walk_packages(package.__path__, package.__name__ + "."):
module = importlib.import_module(module_name)
for name, obj in inspect.getmembers(module, inspect.isclass):
if issubclass(obj, BaseNode) and obj.__module__ == module.__name__:
imported_nodes.append(obj)
return imported_nodes
# --- Custom Graph View ---
class CustomGraphView(QGraphicsView):
"""
Custom view for the graph that applies full transparency and handles right-click context menu.
"""
def __init__(self, scene, graph, parent=None):
super().__init__(scene, parent)
self.graph = graph # Reference to NodeGraph
self.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform)
self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setStyleSheet("background: transparent; border: none;")
self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True)
# Enable context menu on right-click
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.show_context_menu)
def show_context_menu(self, position):
"""
Displays the node creation context menu with dynamically loaded nodes.
"""
menu = QMenu()
for node_class in self.graph.registered_nodes():
node_name = getattr(node_class, "NODE_NAME", node_class.__name__)
menu.addAction(f"Create {node_name}", lambda nc=node_class: self.create_node(nc))
menu.exec_(self.mapToGlobal(position))
def create_node(self, node_class):
"""
Creates a node instance of the given class in the NodeGraph.
"""
try:
node = self.graph.create_node(f"{node_class.__identifier__}.{node_class.__name__}")
print(f"Created node: {node_class.__name__}")
except Exception as e:
print(f"Error creating node: {e}")
# --- Main Window ---
class MainWindow(QMainWindow):
"""A frameless, transparent overlay with a custom graph."""
def __init__(self):
super().__init__()
# Full-screen overlay
app = QApplication.instance()
screen_geo = app.primaryScreen().geometry()
self.setGeometry(screen_geo)
# Frameless, top-most, fully transparent
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
self.setAttribute(Qt.WA_TranslucentBackground, True)
# Transparent central widget
central = QWidget(self)
central.setAttribute(Qt.WA_TranslucentBackground, True)
layout = QVBoxLayout(central)
layout.setContentsMargins(0, 0, 0, 0)
self.setCentralWidget(central)
# Initialize NodeGraph
self.graph = NodeGraph()
# Load custom nodes
custom_nodes = import_nodes_from_folder('Nodes')
for node_class in custom_nodes:
self.graph.register_node(node_class)
# Initialize Custom Graph Scene & View
self.scene = CustomGraphScene()
self.view = CustomGraphView(self.scene, self.graph, self)
layout.addWidget(self.view)
# Global update timer
self.timer = QTimer(self)
self.timer.timeout.connect(self.global_update)
self.timer.start(500)
def global_update(self):
"""Update all nodes periodically."""
for node in self.graph.all_nodes():
if hasattr(node, "process_input"):
node.process_input()
# --- Entry Point ---
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())

View File

@ -0,0 +1,542 @@
#!/usr/bin/env python3
import sys
import time
import re
import numpy as np
import cv2
import pytesseract
try:
import winsound
HAS_WINSOUND = True
except ImportError:
HAS_WINSOUND = False
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtCore import Qt, QRect, QPoint, QTimer
from PyQt5.QtGui import QPainter, QPen, QColor, QFont
from PIL import Image, ImageGrab, ImageFilter
from rich.console import Console, Group
from rich.table import Table
from rich.progress import Progress, BarColumn, TextColumn
from rich.text import Text
from rich.live import Live
# =============================================================================
# Global Config
# =============================================================================
pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"
POLLING_RATE_MS = 500
MAX_DATA_POINTS = 8
# We still use these defaults for Region size.
DEFAULT_WIDTH = 180
DEFAULT_HEIGHT = 130
HANDLE_SIZE = 8
LABEL_HEIGHT = 20
GREEN_HEADER_STYLE = "bold green"
BEEP_INTERVAL_SECONDS = 1.0 # Only beep once every 1 second
# STATUS BAR AUTO-LOCATOR LOGIC (WILL BE BUILT-OUT TO BE MORE ROBUST LATER)
TEMPLATE_PATH = "G:\\Nextcloud\\Projects\\Scripting\\bars_template.png" # Path to your bars template file
MATCH_THRESHOLD = 0.4 # The correlation threshold to consider a "good" match
# =============================================================================
# Helper Functions
# =============================================================================
def beep_hp_warning():
"""
Only beep if enough time has elapsed since the last beep (BEEP_INTERVAL_SECONDS).
"""
current_time = time.time()
if (beep_hp_warning.last_beep_time is None or
(current_time - beep_hp_warning.last_beep_time >= BEEP_INTERVAL_SECONDS)):
beep_hp_warning.last_beep_time = current_time
if HAS_WINSOUND:
# frequency=376 Hz, duration=100 ms
winsound.Beep(376, 100)
else:
# Attempt terminal bell
print('\a', end='')
beep_hp_warning.last_beep_time = None
def locate_bars_opencv(template_path, threshold=MATCH_THRESHOLD):
"""
Attempt to locate the bars via OpenCV template matching:
1) Grab the full screen using PIL.ImageGrab.
2) Convert to NumPy array in BGR format for cv2.
3) Load template from `template_path`.
4) Use cv2.matchTemplate to find the best match location.
5) If max correlation > threshold, return (x, y, w, h).
6) Else return None.
"""
# 1) Capture full screen
screenshot_pil = ImageGrab.grab()
screenshot_np = np.array(screenshot_pil) # shape (H, W, 4) possibly
# Convert RGBA or RGB to BGR
screenshot_bgr = cv2.cvtColor(screenshot_np, cv2.COLOR_RGB2BGR)
# 2) Load template from file
template_bgr = cv2.imread(template_path, cv2.IMREAD_COLOR)
if template_bgr is None:
print(f"[WARN] Could not load template file: {template_path}")
return None
# 3) Template matching
result = cv2.matchTemplate(screenshot_bgr, template_bgr, cv2.TM_CCOEFF_NORMED)
# 4) Find best match
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
# template width/height
th, tw, _ = template_bgr.shape
if max_val >= threshold:
# max_loc is top-left corner of the best match
found_x, found_y = max_loc
return (found_x, found_y, tw, th)
else:
return None
def format_duration(seconds):
if seconds is None:
return "???"
seconds = int(seconds)
hours = seconds // 3600
leftover = seconds % 3600
mins = leftover // 60
secs = leftover % 60
if hours > 0:
return f"{hours}h {mins}m {secs}s"
else:
return f"{mins}m {secs}s"
def sanitize_experience_string(raw_text):
text_no_percent = raw_text.replace('%', '')
text_no_spaces = text_no_percent.replace(' ', '')
cleaned = re.sub(r'[^0-9\.]', '', text_no_spaces)
match = re.search(r'\d+(?:\.\d+)?', cleaned)
if not match:
return None
val = float(match.group(0))
if val < 0:
val = 0
elif val > 100:
val = 100
return round(val, 4)
def format_experience_value(value):
if value < 0:
value = 0
elif value > 100:
value = 100
float_4 = round(value, 4)
raw_str = f"{float_4:.4f}"
int_part, dec_part = raw_str.split('.')
if int_part == "100":
pass
elif len(int_part) == 1 and int_part != "0":
int_part = "0" + int_part
elif int_part == "0":
int_part = "00"
return f"{int_part}.{dec_part}"
# -----------------------------------------------------------------------------
# Region Class
# -----------------------------------------------------------------------------
class Region:
"""
Defines a draggable/resizable screen region for OCR capture.
"""
def __init__(self, x, y, label="Region", color=QColor(0,0,255)):
self.x = x
self.y = y
self.w = DEFAULT_WIDTH
self.h = DEFAULT_HEIGHT
self.label = label
self.color = color
self.visible = True
self.data = ""
def rect(self):
return QRect(self.x, self.y, self.w, self.h)
def label_rect(self):
return QRect(self.x, self.y - LABEL_HEIGHT, self.w, LABEL_HEIGHT)
def resize_handles(self):
return [
QRect(self.x - HANDLE_SIZE // 2, self.y - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),
QRect(self.x + self.w - HANDLE_SIZE // 2, self.y - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),
QRect(self.x - HANDLE_SIZE // 2, self.y + self.h - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),
QRect(self.x + self.w - HANDLE_SIZE // 2, self.y + self.h - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),
]
# -----------------------------------------------------------------------------
# OverlayCanvas Class
# -----------------------------------------------------------------------------
class OverlayCanvas(QWidget):
"""
Renders the overlay & handles region dragging/resizing.
"""
def __init__(self, regions, parent=None):
super().__init__(parent)
self.regions = regions
self.edit_mode = True
self.selected_region = None
self.selected_handle = None
self.drag_offset = QPoint()
def paintEvent(self, event):
painter = QPainter(self)
for region in self.regions:
if region.visible:
pen = QPen(region.color)
pen.setWidth(3)
painter.setPen(pen)
painter.drawRect(region.x, region.y, region.w, region.h)
painter.setFont(QFont("Arial", 12, QFont.Bold))
painter.setPen(region.color)
painter.drawText(region.x, region.y - 5, region.label)
if self.edit_mode:
for handle in region.resize_handles():
painter.fillRect(handle, region.color)
def mousePressEvent(self, event):
if not self.edit_mode:
return
if event.button() == Qt.LeftButton:
for region in reversed(self.regions):
for i, handle in enumerate(region.resize_handles()):
if handle.contains(event.pos()):
self.selected_region = region
self.selected_handle = i
return
if region.label_rect().contains(event.pos()):
self.selected_region = region
self.selected_handle = None
self.drag_offset = event.pos() - QPoint(region.x, region.y)
return
if region.rect().contains(event.pos()):
self.selected_region = region
self.selected_handle = None
self.drag_offset = event.pos() - QPoint(region.x, region.y)
return
def mouseMoveEvent(self, event):
if not self.edit_mode or self.selected_region is None:
return
if self.selected_handle is None:
self.selected_region.x = event.x() - self.drag_offset.x()
self.selected_region.y = event.y() - self.drag_offset.y()
else:
sr = self.selected_region
if self.selected_handle == 0: # top-left
sr.w += sr.x - event.x()
sr.h += sr.y - event.y()
sr.x = event.x()
sr.y = event.y()
elif self.selected_handle == 1: # top-right
sr.w = event.x() - sr.x
sr.h += sr.y - event.y()
sr.y = event.y()
elif self.selected_handle == 2: # bottom-left
sr.w += sr.x - event.x()
sr.h = event.y() - sr.y
sr.x = event.x()
elif self.selected_handle == 3: # bottom-right
sr.w = event.x() - sr.x
sr.h = event.y() - sr.y
sr.w = max(sr.w, 10)
sr.h = max(sr.h, 10)
self.update()
def mouseReleaseEvent(self, event):
if not self.edit_mode:
return
if event.button() == Qt.LeftButton:
self.selected_region = None
self.selected_handle = None
# -----------------------------------------------------------------------------
# BorealisOverlay Class
# -----------------------------------------------------------------------------
class BorealisOverlay(QWidget):
"""
Single Region Overlay for Player Stats (HP/MP/FP/EXP) with:
- Automatic location via OpenCV template matching at startup
- OCR scanning
- Low-HP beep
- Rich Live updates in terminal
"""
def __init__(self, live=None):
super().__init__()
screen_geo = QApplication.primaryScreen().geometry()
self.setGeometry(screen_geo)
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
self.setAttribute(Qt.WA_TranslucentBackground, True)
# Try to find the bars automatically
# If found => use that location, else default
initial_x, initial_y = 250, 50
region_w, region_h = DEFAULT_WIDTH, DEFAULT_HEIGHT
match_result = locate_bars_opencv(TEMPLATE_PATH, MATCH_THRESHOLD)
if match_result is not None:
found_x, found_y, w, h = match_result
print(f"Character Status Located at {found_x}, {found_y} with confidence >= {MATCH_THRESHOLD}.")
initial_x, initial_y = found_x, found_y
# Optionally override region size with template size
region_w, region_h = w, h
else:
print("Could not auto-locate the character status page. Set your theme to Masquerade and Interface Scale to 140%, and browser zoom level to 110%. Using default region.")
region = Region(initial_x, initial_y, label="Character Status")
region.w = region_w
region.h = region_h
self.regions = [region]
self.canvas = OverlayCanvas(self.regions, self)
self.canvas.setGeometry(self.rect())
# Tesseract
self.engine = pytesseract
# Keep history of EXP data
self.points = []
self.live = live
# Timer for periodic OCR scanning
self.timer = QTimer(self)
self.timer.timeout.connect(self.collect_ocr_data)
self.timer.start(POLLING_RATE_MS)
def set_live(self, live):
self.live = live
def collect_ocr_data(self):
for region in self.regions:
if region.visible:
screenshot = ImageGrab.grab(
bbox=(region.x, region.y, region.x + region.w, region.y + region.h)
)
processed = self.preprocess_image(screenshot)
text = pytesseract.image_to_string(processed, config='--psm 4 --oem 1')
region.data = text.strip()
if self.live is not None:
renderable = self.build_renderable()
self.live.update(renderable)
def preprocess_image(self, image):
gray = image.convert("L")
scaled = gray.resize((gray.width * 3, gray.height * 3))
thresh = scaled.point(lambda p: p > 200 and 255)
return thresh.filter(ImageFilter.MedianFilter(3))
def parse_all_stats(self, raw_text):
raw_lines = raw_text.splitlines()
lines = [l.strip() for l in raw_lines if l.strip()]
stats_dict = {
"hp": (0,1),
"mp": (0,1),
"fp": (0,1),
"exp": None
}
if len(lines) < 4:
return stats_dict
hp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[0])
if hp_match:
stats_dict["hp"] = (int(hp_match.group(1)), int(hp_match.group(2)))
mp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[1])
if mp_match:
stats_dict["mp"] = (int(mp_match.group(1)), int(mp_match.group(2)))
fp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[2])
if fp_match:
stats_dict["fp"] = (int(fp_match.group(1)), int(fp_match.group(2)))
exp_val = sanitize_experience_string(lines[3])
stats_dict["exp"] = exp_val
return stats_dict
def update_points(self, new_val):
now = time.time()
if self.points:
_, last_v = self.points[-1]
if abs(new_val - last_v) < 1e-6:
return
if new_val < last_v:
self.points.clear()
self.points.append((now, new_val))
if len(self.points) > MAX_DATA_POINTS:
self.points.pop(0)
def compute_time_to_100(self):
n = len(self.points)
if n < 2:
return None
first_t, first_v = self.points[0]
last_t, last_v = self.points[-1]
diff_v = last_v - first_v
if diff_v <= 0:
return None
steps = n - 1
total_time = last_t - first_t
if total_time <= 0:
return None
avg_change = diff_v / steps
remain = 100.0 - last_v
if remain <= 0:
return None
avg_time = total_time / steps
rate_per_s = avg_change / avg_time if avg_time > 0 else 0
if rate_per_s <= 0:
return None
return int(remain / rate_per_s)
def build_renderable(self):
raw_text = self.regions[0].data
stats = self.parse_all_stats(raw_text)
hp_cur, hp_max = stats["hp"]
mp_cur, mp_max = stats["mp"]
fp_cur, fp_max = stats["fp"]
exp_val = stats["exp"]
# HP beep logic
if hp_max > 0:
hp_ratio = hp_cur / hp_max
if 0 < hp_ratio <= 0.40:
beep_hp_warning()
if exp_val is not None:
self.update_points(exp_val)
current_exp = self.points[-1][1] if self.points else 0.0
# Title
title_text = Text("Project Borealis\n", style="bold white")
subtitle_text = Text("Flyff Information Overlay\n\n", style="dim")
# HP / MP / FP bars
bar_progress = Progress(
"{task.description}",
BarColumn(bar_width=30),
TextColumn(" {task.completed}/{task.total} ({task.percentage:>5.2f}%)"),
transient=False,
auto_refresh=False
)
bar_progress.add_task("[bold red]HP[/bold red]", total=hp_max, completed=hp_cur,
style="red", complete_style="red")
bar_progress.add_task("[bold blue]MP[/bold blue]", total=mp_max, completed=mp_cur,
style="blue", complete_style="blue")
bar_progress.add_task("[bold green]FP[/bold green]", total=fp_max, completed=fp_cur,
style="green", complete_style="green")
bar_progress.refresh()
# Historical EXP table
table = Table(show_header=True, header_style=GREEN_HEADER_STYLE, style=None)
table.add_column("Historical EXP", justify="center", style="green")
table.add_column("Time Since Last Kill", justify="center", style="green")
table.add_column("Average EXP Per Kill", justify="center", style="green")
table.add_column("Average Time Between Kills", justify="center", style="green")
n = len(self.points)
if n == 0:
table.add_row("N/A", "N/A", "N/A", "N/A")
elif n == 1:
_, v0 = self.points[0]
exp_str = f"[green]{format_experience_value(v0)}%[/green]"
table.add_row(exp_str, "N/A", "N/A", "N/A")
else:
for i in range(1, n):
t_cur, v_cur = self.points[i]
t_prev, v_prev = self.points[i - 1]
delta_v = v_cur - v_prev
delta_str = f"{delta_v:+.4f}%"
exp_main = format_experience_value(v_cur)
exp_str = f"[green]{exp_main}%[/green] [dim]({delta_str})[/dim]"
delta_t = t_cur - t_prev
t_since_str = f"{delta_t:.1f}s"
diff_v = v_cur - self.points[0][1]
steps = i
avg_exp_str = f"{diff_v/steps:.4f}%"
total_time = t_cur - self.points[0][0]
avg_kill_time = total_time / steps
avg_time_str = f"{avg_kill_time:.1f}s"
table.add_row(exp_str, t_since_str, avg_exp_str, avg_time_str)
# Predicted Time to Level
secs_left = self.compute_time_to_100()
time_str = format_duration(secs_left)
time_bar = Progress(
TextColumn("[bold white]Predicted Time to Level:[/bold white] "),
BarColumn(bar_width=30, complete_style="magenta"),
TextColumn(" [green]{task.percentage:>5.2f}%[/green] "),
TextColumn(f"[magenta]{time_str}[/magenta] until 100%", justify="right"),
transient=False,
auto_refresh=False
)
time_bar.add_task("", total=100, completed=current_exp)
time_bar.refresh()
return Group(
title_text,
subtitle_text,
bar_progress,
table,
time_bar
)
# -----------------------------------------------------------------------------
# main
# -----------------------------------------------------------------------------
def main():
"""
1) Attempt to locate HP/MP/FP/Exp bars using OpenCV template matching.
2) Position overlay region accordingly if found, else default.
3) Start PyQt, periodically OCR the region, update Rich Live in terminal.
"""
app = QApplication(sys.argv)
window = BorealisOverlay()
window.setWindowTitle("Project Borealis Overlay (HP/MP/FP/EXP)")
window.show()
console = Console()
with Live(console=console, refresh_per_second=4) as live:
window.set_live(live)
exit_code = app.exec_()
sys.exit(exit_code)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,80 @@
from flask import Flask, jsonify
from flowpipe.node import Node
from flowpipe.graph import Graph
from flowpipe.plug import InputPlug, OutputPlug
app = Flask(__name__)
# ===========================
# Define Custom Nodes
# ===========================
class MultiplyNode(Node):
"""Multiplies an input value by a factor"""
factor = InputPlug()
value = InputPlug()
result = OutputPlug()
def compute(self):
self.result.value = self.value.value * self.factor.value
class AddNode(Node):
"""Adds two input values"""
input1 = InputPlug()
input2 = InputPlug()
sum = OutputPlug()
def compute(self):
self.sum.value = self.input1.value + self.input2.value
class OutputNode(Node):
"""Outputs the final result"""
input_value = InputPlug()
output_value = OutputPlug()
def compute(self):
self.output_value.value = self.input_value.value
# ===========================
# Define Graph Workflow
# ===========================
def create_workflow():
"""Creates a sample workflow using nodes"""
graph = Graph(name="Sample Workflow")
# Create nodes
multiply = MultiplyNode(name="Multiplier", graph=graph)
add = AddNode(name="Adder", graph=graph)
output = OutputNode(name="Output", graph=graph)
# Connect nodes
multiply.result.connect(add.input1) # Multiply output -> Add input1
add.sum.connect(output.input_value) # Add output -> Output node
# Set static input values
multiply.factor.value = 2
multiply.value.value = 5 # 5 * 2 = 10
add.input2.value = 3 # 10 + 3 = 13
return graph
@app.route('/run-workflow', methods=['GET'])
def run_workflow():
"""Runs the defined node-based workflow"""
graph = create_workflow()
graph.evaluate() # Execute the graph
# Extract the final result from the output node
output_node = graph.nodes["Output"]
result = output_node.output_value.value
return jsonify({"workflow_result": result})
if __name__ == '__main__':
app.run(debug=True)

View File

@ -0,0 +1,98 @@
# example_qt_interface.py
import sys
from PySide6.QtCore import Qt
from PySide6.QtGui import QAction, QIcon
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout,
QLabel, QMenuBar, QToolBar, QSplitter, QListWidget,
QTextEdit, QStatusBar, QFileDialog, QPushButton
)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Example Qt Interface")
# Create and set up the menu bar.
menu_bar = QMenuBar(self)
self.setMenuBar(menu_bar)
# File menu.
file_menu = menu_bar.addMenu("File")
# Create some actions to populate the File menu.
open_action = QAction("Open", self)
open_action.triggered.connect(self.open_file)
file_menu.addAction(open_action)
save_action = QAction("Save", self)
save_action.triggered.connect(self.save_file)
file_menu.addAction(save_action)
exit_action = QAction("Exit", self)
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
# Create a toolbar and add some actions.
tool_bar = QToolBar("Main Toolbar", self)
tool_bar.addAction(open_action)
tool_bar.addAction(save_action)
self.addToolBar(Qt.TopToolBarArea, tool_bar)
# Set up a status bar at the bottom.
self.setStatusBar(QStatusBar(self))
self.statusBar().showMessage("Ready")
# Create your central widget area.
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout(central_widget)
# A splitter as an example container that can hold multiple widgets side-by-side.
splitter = QSplitter()
# Left side: a simple list widget.
self.list_widget = QListWidget()
self.list_widget.addItem("Item A")
self.list_widget.addItem("Item B")
self.list_widget.addItem("Item C")
splitter.addWidget(self.list_widget)
# Right side: a text edit widget.
self.text_edit = QTextEdit()
self.text_edit.setPlainText("Type here...")
splitter.addWidget(self.text_edit)
layout.addWidget(splitter)
# Example button in the central widget area.
example_button = QPushButton("Click Me")
example_button.clicked.connect(self.on_button_clicked)
layout.addWidget(example_button)
def open_file(self):
file_name, _ = QFileDialog.getOpenFileName(self, "Open File", "", "All Files (*.*)")
if file_name:
self.statusBar().showMessage(f"Opened: {file_name}")
def save_file(self):
file_name, _ = QFileDialog.getSaveFileName(self, "Save File", "", "All Files (*.*)")
if file_name:
self.statusBar().showMessage(f"Saved: {file_name}")
def on_button_clicked(self):
self.statusBar().showMessage("Button clicked!")
def main():
app = QApplication(sys.argv)
window = MainWindow()
window.resize(800, 600)
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()

3
Data/WebUI/Canary.txt Normal file
View File

@ -0,0 +1,3 @@
This file exists to show that the custom reactjs code is being copied into the reactJS app before building it.
Additional code to flesh out the application will be added in the future.

25
Data/WebUI/src/App.js Normal file
View File

@ -0,0 +1,25 @@
import logo from './logo.svg';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Borealis Workflow Automation Tool
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;

117
Data/server.py Normal file
View File

@ -0,0 +1,117 @@
from flask import Flask, send_from_directory, jsonify, request, abort
import os
import importlib
import inspect
# Determine the absolute path for the React build folder
build_folder = os.path.join(os.getcwd(), "web-interface", "build")
if not os.path.exists(build_folder):
print("WARNING: web-interface build folder not found. Please run your React build process to generate 'web-interface/build'.")
app = Flask(__name__, static_folder=build_folder, static_url_path="/")
# Directory where nodes are stored
NODES_PACKAGE = "Nodes"
# In-memory workflow storage (a simple scaffold)
workflow_data = {
"nodes": [],
"connections": []
}
def import_nodes_from_folder(package_name):
"""
Recursively import all modules from the given package.
Returns a dictionary where keys are subfolder names, and values are lists of node names.
"""
nodes_by_category = {}
package = importlib.import_module(package_name)
package_path = package.__path__[0]
for root, _, files in os.walk(package_path):
rel_path = os.path.relpath(root, package_path).replace(os.sep, ".")
module_prefix = f"{package_name}.{rel_path}" if rel_path != '.' else package_name
category_name = os.path.basename(root)
for file in files:
if file.endswith(".py") and file != "__init__.py":
module_name = f"{module_prefix}.{file[:-3]}"
try:
module = importlib.import_module(module_name)
for name, obj in inspect.getmembers(module, inspect.isclass):
if hasattr(obj, "NODE_NAME"):
if category_name not in nodes_by_category:
nodes_by_category[category_name] = []
nodes_by_category[category_name].append(obj.NODE_NAME)
except Exception as e:
print(f"Failed to import {module_name}: {e}")
return nodes_by_category
@app.route("/")
def serve_frontend():
index_path = os.path.join(build_folder, "index.html")
if os.path.exists(index_path):
return send_from_directory(app.static_folder, "index.html")
else:
return "<h1>React App Not Found</h1><p>Please build the web-interface application.</p>", 404
@app.route("/api/nodes", methods=["GET"])
def get_available_nodes():
"""Returns a list of available node categories and node types."""
nodes = import_nodes_from_folder(NODES_PACKAGE)
return jsonify(nodes)
@app.route("/api/workflow", methods=["GET"])
def get_workflow():
"""Returns the current workflow data."""
return jsonify(workflow_data)
@app.route("/api/workflow", methods=["POST"])
def save_workflow():
"""Saves the workflow data (nodes and connections) sent from the UI."""
global workflow_data
data = request.get_json()
if not data:
abort(400, "Invalid workflow data")
workflow_data = data
return jsonify({"status": "success", "workflow": workflow_data})
@app.route("/api/node", methods=["POST"])
def create_node():
"""Creates a new node. Expects JSON with 'nodeType' and optionally 'position' and 'properties'."""
data = request.get_json()
if not data or "nodeType" not in data:
abort(400, "Invalid node data")
node = {
"id": len(workflow_data["nodes"]) + 1, # simple incremental ID
"type": data["nodeType"],
"position": data.get("position", {"x": 0, "y": 0}),
"properties": data.get("properties", {})
}
workflow_data["nodes"].append(node)
return jsonify({"status": "success", "node": node})
@app.route("/api/node/<int:node_id>", methods=["DELETE"])
def delete_node(node_id):
"""Deletes the node with the given ID."""
global workflow_data
nodes = workflow_data["nodes"]
workflow_data["nodes"] = [n for n in nodes if n["id"] != node_id]
return jsonify({"status": "success", "deletedNode": node_id})
@app.route("/api/node/<int:node_id>", methods=["PUT"])
def update_node(node_id):
"""Updates an existing node's position or properties."""
data = request.get_json()
if not data:
abort(400, "Invalid node data")
for node in workflow_data["nodes"]:
if node["id"] == node_id:
node["position"] = data.get("position", node["position"])
node["properties"] = data.get("properties", node["properties"])
return jsonify({"status": "success", "node": node})
abort(404, "Node not found")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)