Fixed Timing / Data Update Logic / Low Health Overlay Issues / Comparison Node and Math Node issues.
This commit is contained in:
parent
d9623193d8
commit
6bac303dea
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -5,8 +5,8 @@ class ArrayNode(BaseNode):
|
||||
Array Node:
|
||||
- Inputs: 'in' (value to store), 'ArraySize' (defines maximum length)
|
||||
- Output: 'Array' (the current array as a string)
|
||||
- Stores incoming values in an array with size defined by ArraySize.
|
||||
When full, it removes the oldest value.
|
||||
- Stores incoming values in an array with a size defined by ArraySize.
|
||||
- Updates are now handled via a global update timer.
|
||||
"""
|
||||
__identifier__ = 'bunny-lab.io.array_node'
|
||||
NODE_NAME = 'Array'
|
||||
|
@ -1,86 +0,0 @@
|
||||
from OdenGraphQt import BaseNode, BaseNodeCircle
|
||||
|
||||
|
||||
class BasicNodeA(BaseNode):
|
||||
"""
|
||||
A node class with 2 inputs and 2 outputs.
|
||||
"""
|
||||
|
||||
# unique node identifier.
|
||||
__identifier__ = 'nodes.basic'
|
||||
|
||||
# initial default node name.
|
||||
NODE_NAME = 'node A'
|
||||
|
||||
def __init__(self):
|
||||
super(BasicNodeA, self).__init__()
|
||||
|
||||
# create node inputs.
|
||||
self.add_input('in A')
|
||||
self.add_input('in B')
|
||||
|
||||
# create node outputs.
|
||||
self.add_output('out A')
|
||||
self.add_output('out B')
|
||||
|
||||
|
||||
class BasicNodeB(BaseNode):
|
||||
"""
|
||||
A node class with 3 inputs and 3 outputs.
|
||||
The last input and last output can take in multiple pipes.
|
||||
"""
|
||||
|
||||
# unique node identifier.
|
||||
__identifier__ = 'nodes.basic'
|
||||
|
||||
# initial default node name.
|
||||
NODE_NAME = 'node B'
|
||||
|
||||
def __init__(self):
|
||||
super(BasicNodeB, self).__init__()
|
||||
|
||||
# create node inputs
|
||||
self.add_input('single 1')
|
||||
self.add_input('single 2')
|
||||
self.add_input('multi in', multi_input=True)
|
||||
|
||||
# create node outputs
|
||||
self.add_output('single 1', multi_output=False)
|
||||
self.add_output('single 2', multi_output=False)
|
||||
self.add_output('multi out')
|
||||
|
||||
|
||||
class CircleNode(BaseNodeCircle):
|
||||
"""
|
||||
A node class with 3 inputs and 3 outputs.
|
||||
This node is a circular design.
|
||||
"""
|
||||
|
||||
# unique node identifier.
|
||||
__identifier__ = 'nodes.basic'
|
||||
|
||||
# initial default node name.
|
||||
NODE_NAME = 'Circle Node'
|
||||
|
||||
def __init__(self):
|
||||
super(CircleNode, self).__init__()
|
||||
self.set_color(10, 24, 38)
|
||||
|
||||
# create node inputs
|
||||
p = self.add_input('in 1')
|
||||
p.add_accept_port_type(
|
||||
port_name='single 1',
|
||||
port_type='out',
|
||||
node_type='nodes.basic.BasicNodeB'
|
||||
)
|
||||
|
||||
self.add_input('in 2')
|
||||
self.add_input('in 3', multi_input=True)
|
||||
self.add_input('in 4', display_name=False)
|
||||
self.add_input('in 5', display_name=False)
|
||||
|
||||
# create node outputs
|
||||
self.add_output('out 1')
|
||||
self.add_output('out 2', multi_output=False)
|
||||
self.add_output('out 3', multi_output=True, display_name=False)
|
||||
self.add_output('out 4', multi_output=True, display_name=False)
|
@ -1,15 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Comparison Node:
|
||||
- Inputs: Two input ports ("A" and "B").
|
||||
- Output: One output port ("Result").
|
||||
- Operation: A dropdown (combo menu) to select:
|
||||
Equal (==), Not Equal (!=), Greater Than (>), Less Than (<),
|
||||
Greater Than or Equal (>=), Less Than or Equal (<=).
|
||||
- Displays the computed result in a read-only text box labeled "Result".
|
||||
Standardized Comparison Node:
|
||||
- Compares two input values using a selected operator (==, !=, >, <, >=, <=).
|
||||
- Outputs a result of 1 (True) or 0 (False).
|
||||
- Uses a global update timer for processing.
|
||||
- Supports an additional 'Input Type' dropdown to choose between 'Number' and 'String'.
|
||||
"""
|
||||
|
||||
from OdenGraphQt import BaseNode
|
||||
from Qt import QtCore
|
||||
|
||||
class ComparisonNode(BaseNode):
|
||||
__identifier__ = 'bunny-lab.io.comparison_node'
|
||||
@ -17,112 +17,106 @@ class ComparisonNode(BaseNode):
|
||||
|
||||
def __init__(self):
|
||||
super(ComparisonNode, self).__init__()
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Initialization Section:
|
||||
# - Create two input ports: A, B
|
||||
# - Create one output port: Result
|
||||
# - Add a combo box for logical operator selection
|
||||
# - Add a text input for displaying the computed result
|
||||
# ----------------------------------------------------------------------
|
||||
self.add_input('A')
|
||||
self.add_input('B')
|
||||
self.add_output('Result')
|
||||
|
||||
# Operator combo box (==, !=, >, <, >=, <=)
|
||||
# Add the Input Type dropdown first.
|
||||
self.add_combo_menu('input_type', 'Input Type', items=['Number', 'String'])
|
||||
self.add_combo_menu('operator', 'Operator', items=[
|
||||
'Equal (==)',
|
||||
'Not Equal (!=)',
|
||||
'Greater Than (>)',
|
||||
'Less Than (<)',
|
||||
'Greater Than or Equal (>=)',
|
||||
'Less Than or Equal (<=)'
|
||||
'Equal (==)', 'Not Equal (!=)', 'Greater Than (>)',
|
||||
'Less Than (<)', 'Greater Than or Equal (>=)', 'Less Than or Equal (<=)'
|
||||
])
|
||||
|
||||
# Text input for displaying the computed result.
|
||||
# We'll make it read-only by accessing the underlying QLineEdit.
|
||||
self.add_text_input('calc_result', 'Result', text='0')
|
||||
result_widget = self.get_widget('calc_result') # This is a NodeLineEdit wrapper
|
||||
if result_widget:
|
||||
# Get the underlying QLineEdit
|
||||
line_edit = result_widget.get_custom_widget()
|
||||
# Make the QLineEdit read-only
|
||||
line_edit.setReadOnly(True)
|
||||
|
||||
# Replace calc_result with a standardized "value" text input.
|
||||
self.add_text_input('value', 'Value', text='0')
|
||||
self.value = 0
|
||||
self.set_name("Comparison Node")
|
||||
self.process_input()
|
||||
self.processing = False # Guard for process_input
|
||||
|
||||
def process_input(self, event=None):
|
||||
"""
|
||||
Compute Section:
|
||||
- For each input port (A, B), if connected, grab the 'value' from
|
||||
the upstream node; otherwise default to 0.0.
|
||||
- Convert to float when possible, apply the selected comparison operator,
|
||||
update the "Result" text box, node title, and output port.
|
||||
"""
|
||||
# Gather input A
|
||||
# Set default properties explicitly
|
||||
self.set_property('input_type', 'Number')
|
||||
self.set_property('operator', 'Equal (==)')
|
||||
|
||||
def process_input(self):
|
||||
if self.processing:
|
||||
return
|
||||
self.processing = True
|
||||
|
||||
# Retrieve input values; if no connection or None, default to "0"
|
||||
input_a = self.input(0)
|
||||
if input_a and input_a.connected_ports():
|
||||
a_raw = input_a.connected_ports()[0].node().get_property('value')
|
||||
else:
|
||||
a_raw = 0.0
|
||||
|
||||
# Gather input B
|
||||
input_b = self.input(1)
|
||||
if input_b and input_b.connected_ports():
|
||||
b_raw = input_b.connected_ports()[0].node().get_property('value')
|
||||
a_raw = (input_a.connected_ports()[0].node().get_property('value')
|
||||
if input_a.connected_ports() else "0")
|
||||
b_raw = (input_b.connected_ports()[0].node().get_property('value')
|
||||
if input_b.connected_ports() else "0")
|
||||
a_raw = a_raw if a_raw is not None else "0"
|
||||
b_raw = b_raw if b_raw is not None else "0"
|
||||
|
||||
# Get input type property
|
||||
input_type = self.get_property('input_type')
|
||||
|
||||
# Convert values based on input type
|
||||
if input_type == 'Number':
|
||||
try:
|
||||
a_val = float(a_raw)
|
||||
except (ValueError, TypeError):
|
||||
a_val = 0.0
|
||||
try:
|
||||
b_val = float(b_raw)
|
||||
except (ValueError, TypeError):
|
||||
b_val = 0.0
|
||||
elif input_type == 'String':
|
||||
a_val = str(a_raw)
|
||||
b_val = str(b_raw)
|
||||
else:
|
||||
b_raw = 0.0
|
||||
try:
|
||||
a_val = float(a_raw)
|
||||
except (ValueError, TypeError):
|
||||
a_val = 0.0
|
||||
try:
|
||||
b_val = float(b_raw)
|
||||
except (ValueError, TypeError):
|
||||
b_val = 0.0
|
||||
|
||||
# Convert raw inputs to float if possible, otherwise keep as-is for string comparison.
|
||||
try:
|
||||
a_val = float(a_raw)
|
||||
b_val = float(b_raw)
|
||||
except (ValueError, TypeError):
|
||||
a_val = a_raw
|
||||
b_val = b_raw
|
||||
|
||||
# Retrieve the selected operator from the combo box.
|
||||
operator = self.get_property('operator')
|
||||
result = False
|
||||
|
||||
if operator == 'Equal (==)':
|
||||
result = a_val == b_val
|
||||
elif operator == 'Not Equal (!=)':
|
||||
result = a_val != b_val
|
||||
elif operator == 'Greater Than (>)':
|
||||
result = a_val > b_val
|
||||
elif operator == 'Less Than (<)':
|
||||
result = a_val < b_val
|
||||
elif operator == 'Greater Than or Equal (>=)':
|
||||
result = a_val >= b_val
|
||||
elif operator == 'Less Than or Equal (<=)':
|
||||
result = a_val <= b_val
|
||||
# Perform the comparison
|
||||
result = {
|
||||
'Equal (==)': a_val == b_val,
|
||||
'Not Equal (!=)': a_val != b_val,
|
||||
'Greater Than (>)': a_val > b_val,
|
||||
'Less Than (<)': a_val < b_val,
|
||||
'Greater Than or Equal (>=)': a_val >= b_val,
|
||||
'Less Than or Equal (<=)': a_val <= b_val
|
||||
}.get(operator, False)
|
||||
|
||||
# Convert boolean result to integer (1 for True, 0 for False)
|
||||
self.value = 1 if result else 0
|
||||
new_value = 1 if result else 0
|
||||
self.value = new_value
|
||||
self.set_property('value', str(self.value))
|
||||
self.transmit_data(self.value)
|
||||
|
||||
# Update the read-only text input and node title.
|
||||
self.set_property('calc_result', str(self.value))
|
||||
|
||||
# Transmit the numeric result to any connected output nodes.
|
||||
output_port = self.output(0)
|
||||
if output_port and output_port.connected_ports():
|
||||
for cp in output_port.connected_ports():
|
||||
connected_node = cp.node()
|
||||
if hasattr(connected_node, 'receive_data'):
|
||||
connected_node.receive_data(self.value, source_port_name='Result')
|
||||
self.processing = False
|
||||
|
||||
def on_input_connected(self, input_port, output_port):
|
||||
self.process_input()
|
||||
pass
|
||||
|
||||
def on_input_disconnected(self, input_port, output_port):
|
||||
self.process_input()
|
||||
pass
|
||||
|
||||
def property_changed(self, property_name):
|
||||
if property_name in ['operator']:
|
||||
self.process_input()
|
||||
pass
|
||||
|
||||
def receive_data(self, data, source_port_name=None):
|
||||
self.process_input()
|
||||
pass
|
||||
|
||||
def transmit_data(self, data):
|
||||
output_port = self.output(0)
|
||||
if output_port and output_port.connected_ports():
|
||||
for connected_port in output_port.connected_ports():
|
||||
connected_node = connected_port.node()
|
||||
if hasattr(connected_node, 'receive_data'):
|
||||
try:
|
||||
data_int = int(data)
|
||||
connected_node.receive_data(data_int, source_port_name='Result')
|
||||
except ValueError:
|
||||
pass
|
||||
|
@ -1,21 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Data Node:
|
||||
- Input: Accepts a value (string, integer, or float) from an input port.
|
||||
- Output: Outputs the current value, either from the input port or set manually via a text box.
|
||||
|
||||
Behavior:
|
||||
- If both input and output are connected:
|
||||
- Acts as a passthrough, displaying the input value and transmitting it to the output.
|
||||
- Manual input is disabled.
|
||||
- If only the output is connected:
|
||||
- Allows manual value entry, which is sent to the output.
|
||||
- If only the input is connected:
|
||||
- Displays the input value but does not transmit it further.
|
||||
Standardized Data Node:
|
||||
- Accepts and transmits values consistently.
|
||||
- Updates its value based on a global update timer.
|
||||
"""
|
||||
|
||||
from OdenGraphQt import BaseNode
|
||||
from Qt import QtCore
|
||||
|
||||
class DataNode(BaseNode):
|
||||
__identifier__ = 'bunny-lab.io.data_node'
|
||||
@ -23,103 +15,58 @@ class DataNode(BaseNode):
|
||||
|
||||
def __init__(self):
|
||||
super(DataNode, self).__init__()
|
||||
# Add input and output ports.
|
||||
self.add_input('Input')
|
||||
self.add_output('Output')
|
||||
# Add a text input widget for manual entry.
|
||||
self.add_text_input('value', 'Value', text='')
|
||||
# Initialize the value from the widget property.
|
||||
self.process_widget_event()
|
||||
self.set_name(f"Data Node")
|
||||
self.set_name("Data Node")
|
||||
# Removed self-contained update timer; global timer now drives updates.
|
||||
|
||||
def post_create(self):
|
||||
"""
|
||||
Called after the node's widget is fully created.
|
||||
Connect the text input widget's textChanged signal to process_widget_event.
|
||||
"""
|
||||
text_widget = self.get_widget('value')
|
||||
if text_widget is not None:
|
||||
try:
|
||||
text_widget.textChanged.connect(self.process_widget_event)
|
||||
# Removed textChanged signal connection; global timer will call process_input.
|
||||
pass
|
||||
except Exception as e:
|
||||
print("Error connecting textChanged signal:", e)
|
||||
|
||||
def process_widget_event(self, event=None):
|
||||
"""
|
||||
Reads the current text from the node's property and updates the node's internal value.
|
||||
"""
|
||||
current_text = self.get_property('value')
|
||||
self.value = current_text
|
||||
self.transmit_data(current_text)
|
||||
|
||||
def property_changed(self, property_name):
|
||||
"""
|
||||
Called when a node property changes. If the 'value' property changes,
|
||||
update the internal value.
|
||||
"""
|
||||
if property_name == 'value':
|
||||
self.process_widget_event()
|
||||
# Immediate update removed; relying on global timer.
|
||||
pass
|
||||
|
||||
def update_stream(self):
|
||||
"""
|
||||
Updates the node's behavior based on the connection states.
|
||||
"""
|
||||
def process_input(self):
|
||||
input_port = self.input(0)
|
||||
output_port = self.output(0)
|
||||
|
||||
if input_port.connected_ports() and output_port.connected_ports():
|
||||
# Both input and output are connected; act as passthrough.
|
||||
self.set_property('value', '')
|
||||
self.get_widget('value').setEnabled(False)
|
||||
if input_port.connected_ports():
|
||||
input_value = input_port.connected_ports()[0].node().get_property('value')
|
||||
self.set_property('value', input_value)
|
||||
self.transmit_data(input_value)
|
||||
elif output_port.connected_ports():
|
||||
# Only output is connected; allow manual input.
|
||||
self.get_widget('value').setEnabled(True)
|
||||
elif input_port.connected_ports():
|
||||
# Only input is connected; display input value.
|
||||
self.get_widget('value').setEnabled(False)
|
||||
input_value = input_port.connected_ports()[0].node().get_property('value')
|
||||
self.set_property('value', input_value)
|
||||
else:
|
||||
# Neither input nor output is connected; allow manual input.
|
||||
self.get_widget('value').setEnabled(True)
|
||||
self.transmit_data(self.get_property('value'))
|
||||
|
||||
def on_input_connected(self, input_port, output_port):
|
||||
"""
|
||||
Called when an input port is connected.
|
||||
"""
|
||||
self.update_stream()
|
||||
# Removed immediate update; global timer handles updates.
|
||||
pass
|
||||
|
||||
def on_input_disconnected(self, input_port, output_port):
|
||||
"""
|
||||
Called when an input port is disconnected.
|
||||
"""
|
||||
self.update_stream()
|
||||
|
||||
def on_output_connected(self, output_port, input_port):
|
||||
"""
|
||||
Called when an output port is connected.
|
||||
"""
|
||||
self.update_stream()
|
||||
|
||||
def on_output_disconnected(self, output_port, input_port):
|
||||
"""
|
||||
Called when an output port is disconnected.
|
||||
"""
|
||||
self.update_stream()
|
||||
# Removed immediate update; global timer handles updates.
|
||||
pass
|
||||
|
||||
def receive_data(self, data, source_port_name=None):
|
||||
"""
|
||||
Receives data from connected nodes and updates the internal value.
|
||||
"""
|
||||
self.set_property('value', str(data)) # Ensure it's always stored as a string
|
||||
self.set_property('value', str(data))
|
||||
self.transmit_data(data)
|
||||
|
||||
# Transmit data further if there's an output connection
|
||||
def transmit_data(self, data):
|
||||
output_port = self.output(0)
|
||||
if output_port and output_port.connected_ports():
|
||||
for connected_port in output_port.connected_ports():
|
||||
connected_node = connected_port.node()
|
||||
if hasattr(connected_node, 'receive_data'):
|
||||
connected_node.receive_data(data, source_port_name)
|
||||
|
||||
|
||||
connected_node.receive_data(data, source_port_name="Output")
|
||||
|
@ -1,47 +1,23 @@
|
||||
#!/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.
|
||||
"""
|
||||
|
||||
from OdenGraphQt import BaseNode
|
||||
from Qt import QtCore, QtGui
|
||||
from Qt import QtCore
|
||||
import requests
|
||||
import traceback
|
||||
|
||||
def get_draw_stat_port(color, border_color=None, alpha=127):
|
||||
"""
|
||||
Returns a custom port painter function that draws a circular port with a
|
||||
semi-transparent fill and then draws text (port label and current value)
|
||||
next to it.
|
||||
"""
|
||||
if border_color is None:
|
||||
border_color = color
|
||||
|
||||
def painter_func(painter, rect, info):
|
||||
painter.save()
|
||||
pen = QtGui.QPen(QtGui.QColor(*border_color))
|
||||
pen.setWidth(1.8)
|
||||
painter.setPen(pen)
|
||||
semi_transparent_color = QtGui.QColor(color[0], color[1], color[2], alpha)
|
||||
painter.setBrush(semi_transparent_color)
|
||||
painter.drawEllipse(rect)
|
||||
port = info.get('port')
|
||||
if port is not None:
|
||||
node = port.node()
|
||||
stat = port.name()
|
||||
value = node.values.get(stat, "N/A") if hasattr(node, 'values') else "N/A"
|
||||
text_rect = rect.adjusted(rect.width() + 4, 0, rect.width() + 70, 0)
|
||||
painter.setPen(QtGui.QColor(0, 0, 0))
|
||||
painter.drawText(text_rect, QtCore.Qt.AlignVCenter | QtCore.Qt.AlignLeft,
|
||||
f"{stat}: {value}")
|
||||
painter.restore()
|
||||
return painter_func
|
||||
|
||||
class CharacterStatusNode(BaseNode):
|
||||
class FlyffCharacterStatusNode(BaseNode):
|
||||
__identifier__ = 'bunny-lab.io.flyff_character_status_node'
|
||||
NODE_NAME = 'Flyff - Character Status'
|
||||
|
||||
def __init__(self):
|
||||
super(CharacterStatusNode, self).__init__()
|
||||
|
||||
# Define exact expected keys to avoid transformation mismatches
|
||||
super(FlyffCharacterStatusNode, self).__init__()
|
||||
self.values = {
|
||||
"HP: Current": "N/A", "HP: Total": "N/A",
|
||||
"MP: Current": "N/A", "MP: Total": "N/A",
|
||||
@ -49,97 +25,60 @@ class CharacterStatusNode(BaseNode):
|
||||
"EXP": "N/A"
|
||||
}
|
||||
|
||||
# Add output ports
|
||||
self.add_output("HP: Current", painter_func=get_draw_stat_port((217, 36, 78)))
|
||||
self.add_output("HP: Total", painter_func=get_draw_stat_port((217, 36, 78)))
|
||||
self.add_output("MP: Current", painter_func=get_draw_stat_port((35, 124, 213)))
|
||||
self.add_output("MP: Total", painter_func=get_draw_stat_port((35, 124, 213)))
|
||||
self.add_output("FP: Current", painter_func=get_draw_stat_port((36, 197, 28)))
|
||||
self.add_output("FP: Total", painter_func=get_draw_stat_port((36, 197, 28)))
|
||||
self.add_output("EXP", painter_func=get_draw_stat_port((52, 195, 250)))
|
||||
for stat in self.values.keys():
|
||||
self.add_output(stat)
|
||||
|
||||
self.set_name("Flyff - Character Status (API Disconnected)")
|
||||
# Removed self-contained polling timer; global timer now drives updates.
|
||||
|
||||
# Start polling timer
|
||||
self.timer = QtCore.QTimer()
|
||||
self.timer.timeout.connect(self.poll_api)
|
||||
self.timer.start(500)
|
||||
|
||||
def poll_api(self):
|
||||
"""
|
||||
Polls the API endpoint to retrieve the latest character stats and updates
|
||||
the node's internal values.
|
||||
"""
|
||||
def process_input(self):
|
||||
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):
|
||||
try:
|
||||
for key, value in data.items():
|
||||
# Ensure the keys match the expected ones exactly
|
||||
formatted_key = {
|
||||
"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"
|
||||
}.get(key, key) # Use mapping or fallback to raw key
|
||||
|
||||
if formatted_key in self.values:
|
||||
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)
|
||||
else:
|
||||
print(f"[WARNING] Unexpected API key: {key} (not mapped)")
|
||||
|
||||
updated = True
|
||||
if updated:
|
||||
self.set_name("Flyff - Character Status (API Connected)")
|
||||
self.update()
|
||||
self.transmit_data()
|
||||
|
||||
except Exception as e:
|
||||
print("[ERROR] Error processing API response data:", e)
|
||||
print("[ERROR] Stack Trace:\n", traceback.format_exc())
|
||||
else:
|
||||
print("[ERROR] Unexpected API response format (not a dict):", data)
|
||||
self.set_name("Flyff - Character Status (API Disconnected)")
|
||||
|
||||
else:
|
||||
print(f"[ERROR] API request failed with status code {response.status_code}")
|
||||
self.set_name("Flyff - Character Status (API Disconnected)")
|
||||
|
||||
except Exception as e:
|
||||
self.set_name("Flyff - Character Status (API Disconnected)")
|
||||
print("[ERROR] Error polling API in CharacterStatusNode:", str(e))
|
||||
print("[ERROR] Stack Trace:\n", traceback.format_exc())
|
||||
|
||||
def transmit_data(self):
|
||||
"""
|
||||
Sends the updated character stats to connected nodes.
|
||||
"""
|
||||
for stat, value in self.values.items():
|
||||
try:
|
||||
port = self.get_output(stat)
|
||||
|
||||
if port is None:
|
||||
print(f"[ERROR] Port '{stat}' not found in node outputs. Skipping...")
|
||||
continue
|
||||
|
||||
if 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())
|
||||
else:
|
||||
print(f"[WARNING] Connected node {connected_node} does not have receive_data method.")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Error while handling port {stat}: {e}")
|
||||
print("[ERROR] Stack Trace:\n", traceback.format_exc())
|
||||
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,177 +1,133 @@
|
||||
import time
|
||||
import sys
|
||||
from OdenGraphQt import BaseNode
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Standardized Flyff Low Health Alert Node:
|
||||
- Monitors an input value (1 = health alert, 0 = normal).
|
||||
- Displays a visual alert and plays a sound if enabled.
|
||||
- Uses a global update timer for processing.
|
||||
- Automatically processes float, int, and string values.
|
||||
"""
|
||||
|
||||
import time
|
||||
from OdenGraphQt import BaseNode
|
||||
from Qt import QtCore, QtWidgets, QtGui
|
||||
|
||||
# Attempt to import winsound (Windows-only)
|
||||
try:
|
||||
import winsound
|
||||
HAS_WINSOUND = True
|
||||
except ImportError:
|
||||
HAS_WINSOUND = False
|
||||
|
||||
|
||||
class OverlayCanvas(QtWidgets.QWidget):
|
||||
"""
|
||||
UI overlay for displaying a red warning box, which can be repositioned by dragging.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
# **Full-screen overlay**
|
||||
screen_geo = QtWidgets.QApplication.primaryScreen().geometry()
|
||||
self.setGeometry(screen_geo) # Set to full screen
|
||||
|
||||
self.setGeometry(screen_geo)
|
||||
self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint)
|
||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True)
|
||||
self.setAttribute(QtCore.Qt.WA_NoSystemBackground, True)
|
||||
self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent, False)
|
||||
self.setAttribute(QtCore.Qt.WA_AlwaysStackOnTop, True)
|
||||
|
||||
# **Draggable Low Health Alert**
|
||||
self.helper_LowHealthAlert = QtCore.QRect(250, 300, 900, 35) # Default Position
|
||||
self.setVisible(False)
|
||||
self.helper_LowHealthAlert = QtCore.QRect(250, 300, 900, 35)
|
||||
self.dragging = False
|
||||
self.drag_offset = QtCore.QPoint()
|
||||
|
||||
self.setVisible(False) # Initially hidden
|
||||
self.drag_offset = None
|
||||
|
||||
def paintEvent(self, event):
|
||||
"""Draw the helper overlay objects."""
|
||||
if not self.isVisible():
|
||||
return # Don't draw anything if invisible
|
||||
|
||||
return
|
||||
painter = QtGui.QPainter(self)
|
||||
painter.setPen(QtCore.Qt.NoPen)
|
||||
painter.setBrush(QtGui.QColor(255, 0, 0)) # Solid red rectangle
|
||||
painter.setBrush(QtGui.QColor(255, 0, 0))
|
||||
painter.drawRect(self.helper_LowHealthAlert)
|
||||
|
||||
# Draw bold white text centered within the rectangle
|
||||
font = QtGui.QFont("Arial", 14, QtGui.QFont.Bold)
|
||||
painter.setFont(font)
|
||||
painter.setPen(QtGui.QColor(255, 255, 255))
|
||||
|
||||
text = "LOW HEALTH"
|
||||
metrics = QtGui.QFontMetrics(font)
|
||||
text_width = metrics.horizontalAdvance(text)
|
||||
text_height = metrics.height()
|
||||
text_x = self.helper_LowHealthAlert.center().x() - text_width // 2
|
||||
text_y = self.helper_LowHealthAlert.center().y() + text_height // 4
|
||||
|
||||
painter.drawText(text_x, text_y, text)
|
||||
text_x = self.helper_LowHealthAlert.center().x() - 50
|
||||
text_y = self.helper_LowHealthAlert.center().y() + 5
|
||||
painter.drawText(text_x, text_y, "LOW HEALTH")
|
||||
|
||||
def toggle_alert(self, state):
|
||||
"""
|
||||
Show or hide the overlay based on the state (1 = show, 0 = hide).
|
||||
"""
|
||||
self.setVisible(state == 1)
|
||||
self.update()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
"""Detect clicks inside the red box and allow dragging."""
|
||||
if event.button() == QtCore.Qt.LeftButton and self.helper_LowHealthAlert.contains(event.pos()):
|
||||
self.dragging = True
|
||||
self.drag_offset = event.pos() - self.helper_LowHealthAlert.topLeft()
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
if self.helper_LowHealthAlert.contains(event.pos()):
|
||||
self.dragging = True
|
||||
self.drag_offset = event.pos() - self.helper_LowHealthAlert.topLeft()
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
"""Handle dragging movement."""
|
||||
if self.dragging:
|
||||
new_x = event.pos().x() - self.drag_offset.x()
|
||||
new_y = event.pos().y() - self.drag_offset.y()
|
||||
self.helper_LowHealthAlert.moveTopLeft(QtCore.QPoint(new_x, new_y))
|
||||
new_top_left = event.pos() - self.drag_offset
|
||||
self.helper_LowHealthAlert.moveTo(new_top_left)
|
||||
self.update()
|
||||
super().mouseMoveEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
"""Stop dragging when the mouse button is released."""
|
||||
self.dragging = False
|
||||
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
self.dragging = False
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
class FlyffLowHealthAlertNode(BaseNode):
|
||||
"""
|
||||
Custom OdenGraphQt node that toggles a visual alert overlay and plays a beep when health is low.
|
||||
"""
|
||||
|
||||
__identifier__ = 'bunny-lab.io.flyff_low_health_alert_node'
|
||||
NODE_NAME = 'Flyff - Low Health Alert'
|
||||
|
||||
overlay_instance = None # Shared overlay instance
|
||||
last_beep_time = 0 # Time tracking for beep interval
|
||||
BEEP_INTERVAL_SECONDS = 2 # Beep every 2 seconds
|
||||
overlay_instance = None
|
||||
last_beep_time = 0
|
||||
BEEP_INTERVAL_SECONDS = 2
|
||||
|
||||
def __init__(self):
|
||||
super(FlyffLowHealthAlertNode, self).__init__()
|
||||
|
||||
# Create checkboxes to decide which kind of alert(s) to utilize
|
||||
self.add_checkbox('cb_1', '', 'Sound Alert', True)
|
||||
self.add_checkbox('cb_2', '', 'Visual Alert', True)
|
||||
|
||||
# Create Input Port
|
||||
self.add_input('Toggle (1 = On | 0 = Off)', color=(200, 100, 0))
|
||||
|
||||
# Add text input widget to display received value
|
||||
self.add_text_input('value', 'Current Value', text='0')
|
||||
self.add_combo_menu('beep_interval', 'Beep Interval', items=["0.5s", "1.0s", "2.0s"])
|
||||
|
||||
# Ensure only one overlay instance exists
|
||||
if not FlyffLowHealthAlertNode.overlay_instance:
|
||||
FlyffLowHealthAlertNode.overlay_instance = OverlayCanvas()
|
||||
FlyffLowHealthAlertNode.overlay_instance.show()
|
||||
|
||||
def process_input(self):
|
||||
"""
|
||||
This function runs every 500ms (via the global update loop).
|
||||
It updates the displayed value and toggles the alert if needed.
|
||||
"""
|
||||
input_port = self.input(0)
|
||||
value = input_port.connected_ports()[0].node().get_property('value') if input_port.connected_ports() else "0"
|
||||
self.receive_data(value)
|
||||
|
||||
# If there is a connected node, fetch its output value
|
||||
if input_port.connected_ports():
|
||||
connected_node = input_port.connected_ports()[0].node()
|
||||
if hasattr(connected_node, 'get_property'):
|
||||
value = connected_node.get_property('value')
|
||||
else:
|
||||
value = "0"
|
||||
else:
|
||||
value = "0" # Default to zero if nothing is connected
|
||||
|
||||
def receive_data(self, data, source_port_name=None):
|
||||
try:
|
||||
input_value = int(value) # Ensure we interpret input as an integer (0 or 1)
|
||||
if isinstance(data, str):
|
||||
data = float(data) if '.' in data else int(data)
|
||||
if isinstance(data, (float, int)):
|
||||
data = 1 if data > 1 else 0 if data <= 0 else int(data)
|
||||
else:
|
||||
data = 0
|
||||
except ValueError:
|
||||
input_value = 0 # Default to off if the input is not valid
|
||||
data = 0
|
||||
|
||||
# Update the value display box
|
||||
self.set_property('value', str(input_value))
|
||||
|
||||
# Check if the "Visual Alert" checkbox is enabled
|
||||
visual_alert_enabled = self.get_property('cb_2')
|
||||
|
||||
# Ensure that if "Visual Alert" is unchecked, the overlay is always hidden
|
||||
if not visual_alert_enabled:
|
||||
FlyffLowHealthAlertNode.overlay_instance.toggle_alert(0)
|
||||
else:
|
||||
FlyffLowHealthAlertNode.overlay_instance.toggle_alert(input_value)
|
||||
|
||||
# Check if "Sound Alert" is enabled and beep if necessary
|
||||
self.handle_beep(input_value)
|
||||
self.set_property('value', str(data))
|
||||
if self.get_property('cb_2'):
|
||||
FlyffLowHealthAlertNode.overlay_instance.toggle_alert(data)
|
||||
self.handle_beep(data)
|
||||
|
||||
def handle_beep(self, input_value):
|
||||
"""
|
||||
Plays a beep sound every 2 seconds when the value is `1` and "Sound Alert" is enabled.
|
||||
"""
|
||||
sound_alert_enabled = self.get_property('cb_1')
|
||||
current_time = time.time()
|
||||
# Update beep interval from the dropdown property
|
||||
interval_str = self.get_property('beep_interval')
|
||||
if interval_str.endswith("s"):
|
||||
interval_seconds = float(interval_str[:-1])
|
||||
else:
|
||||
interval_seconds = float(interval_str)
|
||||
self.BEEP_INTERVAL_SECONDS = interval_seconds
|
||||
|
||||
if input_value == 1 and sound_alert_enabled:
|
||||
if (current_time - FlyffLowHealthAlertNode.last_beep_time) >= FlyffLowHealthAlertNode.BEEP_INTERVAL_SECONDS:
|
||||
if input_value == 1 and self.get_property('cb_1'):
|
||||
current_time = time.time()
|
||||
if (current_time - FlyffLowHealthAlertNode.last_beep_time) >= self.BEEP_INTERVAL_SECONDS:
|
||||
FlyffLowHealthAlertNode.last_beep_time = current_time
|
||||
self.play_beep()
|
||||
else:
|
||||
FlyffLowHealthAlertNode.last_beep_time = 0 # Reset when health is safe
|
||||
|
||||
def play_beep(self):
|
||||
"""
|
||||
Plays a beep using `winsound.Beep` (Windows) or prints a terminal bell (`\a`).
|
||||
"""
|
||||
if HAS_WINSOUND:
|
||||
winsound.Beep(376, 100) # 376 Hz, 100ms duration
|
||||
winsound.Beep(376, 100)
|
||||
else:
|
||||
print('\a', end='') # Terminal bell for non-Windows systems
|
||||
print('\a', end='')
|
||||
|
@ -1,14 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Math Operation Node:
|
||||
- Inputs: Two input ports ("A" and "B").
|
||||
- Output: One output port ("Result").
|
||||
- Operation: A dropdown (combo menu) to select:
|
||||
Add, Subtract, Multiply, Divide, Average.
|
||||
- Displays the computed result in a read-only text box labeled "Result".
|
||||
Standardized Math Operation Node:
|
||||
- Performs mathematical operations (+, -, *, /, avg) on two inputs.
|
||||
- Outputs the computed result.
|
||||
- Uses a global update timer for processing (defined in borealis.py).
|
||||
- Ensures it always has a "value" property that the Comparison Node can read.
|
||||
"""
|
||||
|
||||
from OdenGraphQt import BaseNode
|
||||
from Qt import QtCore
|
||||
|
||||
class MathOperationNode(BaseNode):
|
||||
__identifier__ = 'bunny-lab.io.math_node'
|
||||
@ -16,60 +17,38 @@ class MathOperationNode(BaseNode):
|
||||
|
||||
def __init__(self):
|
||||
super(MathOperationNode, self).__init__()
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Initialization Section:
|
||||
# - Create two input ports: A, B
|
||||
# - Create one output port: Result
|
||||
# - Add a combo box for operator selection
|
||||
# - Add a text input for displaying the computed result
|
||||
# ----------------------------------------------------------------------
|
||||
self.add_input('A')
|
||||
self.add_input('B')
|
||||
self.add_output('Result')
|
||||
|
||||
# Operator combo box (Add, Subtract, Multiply, Divide, Average)
|
||||
# Drop-down to choose which operation we do:
|
||||
self.add_combo_menu('operator', 'Operator', items=[
|
||||
'Add', 'Subtract', 'Multiply', 'Divide', 'Average'
|
||||
])
|
||||
|
||||
# Text input for displaying the computed result.
|
||||
# We'll make it read-only by accessing the underlying QLineEdit.
|
||||
# A text field for showing the result to the user:
|
||||
self.add_text_input('calc_result', 'Result', text='0')
|
||||
result_widget = self.get_widget('calc_result') # This is a NodeLineEdit wrapper
|
||||
if result_widget:
|
||||
# Get the underlying QLineEdit
|
||||
line_edit = result_widget.get_custom_widget()
|
||||
# Make the QLineEdit read-only
|
||||
line_edit.setReadOnly(True)
|
||||
|
||||
# IMPORTANT: define a "value" property that the Comparison Node can read
|
||||
# We do not necessarily need a text input for it, but adding it ensures
|
||||
# it becomes an official property recognized by OdenGraphQt.
|
||||
self.add_text_input('value', 'Internal Value', text='0')
|
||||
|
||||
# Keep a Python-side float of the current computed result:
|
||||
self.value = 0
|
||||
|
||||
# Give the node a nice name:
|
||||
self.set_name("Math Operation")
|
||||
self.process_input()
|
||||
|
||||
def process_input(self, event=None):
|
||||
"""
|
||||
Compute Section:
|
||||
- For each input port (A, B), if connected, grab the 'value' from
|
||||
the upstream node; otherwise default to 0.0.
|
||||
- Convert to float, apply the selected operation from the combo box,
|
||||
update the "Result" text box, node title, and output port.
|
||||
"""
|
||||
# Gather input A
|
||||
# Removed self-contained timer; global timer calls process_input().
|
||||
|
||||
def process_input(self):
|
||||
# Attempt to read "value" from both inputs:
|
||||
input_a = self.input(0)
|
||||
if input_a and input_a.connected_ports():
|
||||
a_raw = input_a.connected_ports()[0].node().get_property('value')
|
||||
else:
|
||||
a_raw = 0.0
|
||||
|
||||
# Gather input B
|
||||
input_b = self.input(1)
|
||||
if input_b and input_b.connected_ports():
|
||||
b_raw = input_b.connected_ports()[0].node().get_property('value')
|
||||
else:
|
||||
b_raw = 0.0
|
||||
a_raw = input_a.connected_ports()[0].node().get_property('value') if input_a.connected_ports() else "0"
|
||||
b_raw = input_b.connected_ports()[0].node().get_property('value') if input_b.connected_ports() else "0"
|
||||
|
||||
# Convert raw inputs to floats (default 0.0 on failure).
|
||||
try:
|
||||
a_val = float(a_raw)
|
||||
except (ValueError, TypeError):
|
||||
@ -79,10 +58,7 @@ class MathOperationNode(BaseNode):
|
||||
except (ValueError, TypeError):
|
||||
b_val = 0.0
|
||||
|
||||
# Retrieve the selected operator from the combo box.
|
||||
operator = self.get_property('operator')
|
||||
result = 0.0
|
||||
|
||||
if operator == 'Add':
|
||||
result = a_val + b_val
|
||||
elif operator == 'Subtract':
|
||||
@ -93,30 +69,41 @@ class MathOperationNode(BaseNode):
|
||||
result = a_val / b_val if b_val != 0 else 0.0
|
||||
elif operator == 'Average':
|
||||
result = (a_val + b_val) / 2.0
|
||||
else:
|
||||
result = 0.0
|
||||
|
||||
self.value = result
|
||||
# If the computed result changed, update our internal properties and transmit
|
||||
if self.value != result:
|
||||
self.value = result
|
||||
|
||||
# Update the read-only text input and node title.
|
||||
self.set_property('calc_result', str(result))
|
||||
# Update the two text fields so the user sees the numeric result:
|
||||
self.set_property('calc_result', str(result))
|
||||
self.set_property('value', str(result)) # <= This is the critical step
|
||||
|
||||
# Transmit the numeric result to any connected output nodes.
|
||||
output_port = self.output(0)
|
||||
if output_port and output_port.connected_ports():
|
||||
for cp in output_port.connected_ports():
|
||||
connected_node = cp.node()
|
||||
if hasattr(connected_node, 'receive_data'):
|
||||
connected_node.receive_data(result, source_port_name='Result')
|
||||
# Let downstream nodes know there's new data:
|
||||
self.transmit_data(result)
|
||||
|
||||
def on_input_connected(self, input_port, output_port):
|
||||
self.process_input()
|
||||
pass
|
||||
|
||||
def on_input_disconnected(self, input_port, output_port):
|
||||
self.process_input()
|
||||
pass
|
||||
|
||||
def property_changed(self, property_name):
|
||||
if property_name in ['operator']:
|
||||
self.process_input()
|
||||
pass
|
||||
|
||||
def receive_data(self, data, source_port_name=None):
|
||||
self.process_input()
|
||||
pass
|
||||
|
||||
def transmit_data(self, data):
|
||||
output_port = self.output(0)
|
||||
if output_port and output_port.connected_ports():
|
||||
for connected_port in output_port.connected_ports():
|
||||
connected_node = connected_port.node()
|
||||
if hasattr(connected_node, 'receive_data'):
|
||||
try:
|
||||
# Attempt to convert to int if possible, else float
|
||||
data_int = int(data)
|
||||
connected_node.receive_data(data_int, source_port_name='Result')
|
||||
except ValueError:
|
||||
connected_node.receive_data(data, source_port_name='Result')
|
||||
|
46
borealis.py
46
borealis.py
@ -1,27 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
## --- Patch QGraphicsScene.setSelectionArea to handle selection arguments ---
|
||||
#from Qt import QtWidgets, QtCore, QtGui
|
||||
#
|
||||
#_original_setSelectionArea = QtWidgets.QGraphicsScene.setSelectionArea
|
||||
#
|
||||
#def _patched_setSelectionArea(self, painterPath, second_arg, *args, **kwargs):
|
||||
# try:
|
||||
# # Try calling the original method with the provided arguments.
|
||||
# return _original_setSelectionArea(self, painterPath, second_arg, *args, **kwargs)
|
||||
# except TypeError as e:
|
||||
# # If a TypeError is raised, assume the call was made with only a QPainterPath
|
||||
# # and an ItemSelectionMode, and patch it by supplying defaults.
|
||||
# # Default operation: ReplaceSelection, default transform: QTransform()
|
||||
# return _original_setSelectionArea(self, painterPath,
|
||||
# QtCore.Qt.ReplaceSelection,
|
||||
# second_arg,
|
||||
# QtGui.QTransform())
|
||||
#
|
||||
## Monkey-patch the setSelectionArea method.
|
||||
#QtWidgets.QGraphicsScene.setSelectionArea = _patched_setSelectionArea
|
||||
#
|
||||
## --- End of patch section ---
|
||||
# --- Patch QGraphicsScene.setSelectionArea to handle selection arguments ---
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
_original_setSelectionArea = QtWidgets.QGraphicsScene.setSelectionArea
|
||||
|
||||
def _patched_setSelectionArea(self, painterPath, second_arg, *args, **kwargs):
|
||||
try:
|
||||
# Try calling the original method with the provided arguments.
|
||||
return _original_setSelectionArea(self, painterPath, second_arg, *args, **kwargs)
|
||||
except TypeError as e:
|
||||
# If a TypeError is raised, assume the call was made with only a QPainterPath
|
||||
# and an ItemSelectionMode, and patch it by supplying defaults.
|
||||
# Default operation: ReplaceSelection, default transform: QTransform()
|
||||
return _original_setSelectionArea(self, painterPath,
|
||||
QtCore.Qt.ReplaceSelection,
|
||||
second_arg,
|
||||
QtGui.QTransform())
|
||||
|
||||
# Monkey-patch the setSelectionArea method.
|
||||
QtWidgets.QGraphicsScene.setSelectionArea = _patched_setSelectionArea
|
||||
|
||||
# --- End of patch section ---
|
||||
|
||||
import sys
|
||||
import pkgutil
|
||||
@ -92,7 +92,6 @@ if __name__ == '__main__':
|
||||
graph.widget.show()
|
||||
|
||||
# Global update timer:
|
||||
# - Call process_input() on every node that implements it.
|
||||
def global_update():
|
||||
for node in graph.all_nodes():
|
||||
if hasattr(node, "process_input"):
|
||||
@ -105,4 +104,3 @@ if __name__ == '__main__':
|
||||
timer.start(500)
|
||||
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
BIN
debug_processed.png
Normal file
BIN
debug_processed.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
BIN
debug_screenshot.png
Normal file
BIN
debug_screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
Loading…
x
Reference in New Issue
Block a user