Restructured the project and optimized several nodes.
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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}%")
|
||||
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))
|
||||
Reference in New Issue
Block a user