Restructured the project and optimized several nodes.

This commit is contained in:
2025-02-16 18:53:21 -07:00
parent 670ae774ef
commit e30ba4ec4f
32 changed files with 390 additions and 576 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

View File

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

View File

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

View File

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