Fixed a bunch of crap

This commit is contained in:
Nicole Rappe 2025-02-11 23:48:09 -07:00
parent 6c757e72a4
commit 584e229c1c
22 changed files with 513 additions and 213 deletions

0
Nodes/__init__.py Normal file
View File

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.

Binary file not shown.

Binary file not shown.

49
Nodes/array_node.py Normal file
View File

@ -0,0 +1,49 @@
from NodeGraphQt import BaseNode
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.
"""
__identifier__ = 'io.github.nicole.array'
NODE_NAME = 'Array'
def __init__(self):
super(ArrayNode, self).__init__()
self.values = {} # Ensure values is a dictionary.
self.add_input('in')
self.add_input('ArraySize')
self.add_output('Array')
self.array = []
self.value = "[]" # Output as a string.
self.array_size = 10 # Default array size.
self.set_name("Array: []")
def process_input(self):
# Get array size from 'ArraySize' input if available.
size_port = self.input('ArraySize')
connected_size = size_port.connected_ports() if size_port is not None else []
if connected_size:
connected_port = connected_size[0]
parent_node = connected_port.node()
try:
self.array_size = int(float(getattr(parent_node, 'value', 10)))
except (ValueError, TypeError):
self.array_size = 10
# Get new value from 'in' input if available.
in_port = self.input('in')
connected_in = in_port.connected_ports() if in_port is not None else []
if connected_in:
connected_port = connected_in[0]
parent_node = connected_port.node()
new_value = getattr(parent_node, 'value', None)
if new_value is not None:
self.array.append(new_value)
while len(self.array) > self.array_size:
self.array.pop(0)
self.value = str(self.array)
self.set_name(f"Array: {self.value}")

36
Nodes/average_node.py Normal file
View File

@ -0,0 +1,36 @@
from NodeGraphQt import BaseNode
class AverageNode(BaseNode):
"""
Average Node:
- Inputs: A, B, C (adjustable as needed)
- Output: Result (the average of the inputs)
"""
__identifier__ = 'io.github.nicole.average'
NODE_NAME = 'Average'
def __init__(self):
super(AverageNode, self).__init__()
self.values = {} # Ensure values is a dictionary.
self.add_input('A')
self.add_input('B')
self.add_input('C')
self.add_output('Result')
self.value = 0
self.set_name("Average: 0")
def process_input(self):
values = []
for port_name in ['A', 'B', 'C']:
port = self.input(port_name)
connected = port.connected_ports() if port is not None else []
if connected:
connected_port = connected[0]
parent_node = connected_port.node()
try:
values.append(float(getattr(parent_node, 'value', 0)))
except (ValueError, TypeError):
pass
avg = sum(values) / len(values) if values else 0
self.value = avg
self.set_name(f"Average: {avg}")

View File

@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""
Character Status Node
This node represents the character's status. It has no input ports and four output ports:
- HP, MP, FP, EXP.
It polls an API endpoint (http://127.0.0.1:5000/data) every 500 ms to update its values.
If the API call is successful, the node's title is set to "Character Status (API Connected)".
If the API is down or returns an error, the title is set to "Character Status (API Disconnected)".
"""
from NodeGraphQt import BaseNode
from Qt import QtCore, QtGui
import requests
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()
# Draw the port circle.
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)
# Draw the label and current value.
port = info.get('port')
if port is not None:
node = port.node()
stat = port.name()
# Use the node's 'values' dictionary if available.
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):
__identifier__ = 'io.github.nicole.status'
NODE_NAME = 'Character Status'
def __init__(self):
super(CharacterStatusNode, self).__init__()
# Initialize the output values as a dictionary.
self.values = {"HP": "N/A", "MP": "N/A", "FP": "N/A", "EXP": "N/A"}
# Add output ports for each stat with custom painters.
self.add_output("HP", painter_func=get_draw_stat_port((255, 0, 0))) # Red for HP
self.add_output("MP", painter_func=get_draw_stat_port((0, 0, 255))) # Blue for MP
self.add_output("FP", painter_func=get_draw_stat_port((0, 255, 0))) # Green for FP
self.add_output("EXP", painter_func=get_draw_stat_port((127, 255, 212))) # Aquamarine for EXP
# Set an initial title.
self.set_name("Character Status (API Disconnected)")
# Create a QTimer that polls the API every 500ms.
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. Expects a JSON response with keys:
- "hp", "mp", "fp", "exp"
"""
try:
response = requests.get("http://127.0.0.1:5000/data", timeout=1)
if response.status_code == 200:
data = response.json()
# Update the values dictionary.
self.values["HP"] = data.get("hp", "0/0")
self.values["MP"] = data.get("mp", "0/0")
self.values["FP"] = data.get("fp", "0/0")
self.values["EXP"] = data.get("exp", "0.0000")
self.set_name("Character Status (API Connected)")
self.update()
else:
self.set_name("Character Status (API Disconnected)")
except Exception as e:
self.set_name("Character Status (API Disconnected)")
print("Error polling API in CharacterStatusNode:", e)

View File

@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""
Convert to Percent Node
This node takes an input string formatted as "numerator/denom" (e.g., "50/50"),
splits it into two numbers, computes (numerator / denom) * 100, and outputs the result
as a float formatted to 4 decimal places. If an error occurs, an error message is stored.
The node's title is always "Convert to Percent".
"""
from NodeGraphQt import BaseNode
from Qt import QtCore
class ConvertToPercentNode(BaseNode):
__identifier__ = 'io.github.nicole.convert'
NODE_NAME = 'Convert to Percent'
def __init__(self):
super(ConvertToPercentNode, self).__init__()
# Add one input port (expects a string in "numerator/denom" format).
self.add_input("in")
# Add one output port.
self.add_output("Percent")
# Initialize internal value.
self.value = "No Input"
# Set the node title to a static string.
self.set_name(self.NODE_NAME)
# Initialize a values dictionary so that connected Display nodes can read output.
self.values = {}
def process_input(self):
input_port = self.input(0)
connected_ports = input_port.connected_ports() if input_port is not None else []
if connected_ports:
connected_output = connected_ports[0]
parent_node = connected_output.node()
port_name = connected_output.name()
# Use parent's values dictionary if available, else use its value attribute.
if hasattr(parent_node, 'values') and isinstance(parent_node.values, dict):
input_value = parent_node.values.get(port_name, "")
else:
input_value = getattr(parent_node, 'value', "")
input_str = str(input_value).strip()
try:
parts = input_str.split('/')
if len(parts) != 2:
raise ValueError("Input must be in the format 'num/denom'")
numerator = float(parts[0].strip())
denominator = float(parts[1].strip())
if denominator == 0:
raise ZeroDivisionError("Division by zero")
percent = (numerator / denominator) * 100
formatted_percent = f"{percent:.4f}"
self.value = formatted_percent
except Exception as e:
self.value = f"Error: {e}"
else:
self.value = "No Input"
# Always keep the title static.
self.set_name(self.NODE_NAME)
# Store the computed value in the values dictionary under the output port key.
self.values["Percent"] = self.value

113
Nodes/data_node.py Normal file
View File

@ -0,0 +1,113 @@
#!/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.
"""
from NodeGraphQt import BaseNode
class DataNode(BaseNode):
__identifier__ = 'io.github.nicole.data'
NODE_NAME = 'Data Node'
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.value}")
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:
# Connect the textChanged signal if available.
text_widget.textChanged.connect(self.process_widget_event)
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.set_name(f"Data Node: {self.value}")
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()
def update_stream(self):
"""
Updates the node's behavior based on the connection states.
"""
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)
input_value = input_port.connected_ports()[0].node().get_property('value')
self.set_property('value', input_value)
output_port.send_data(input_value)
elif output_port.connected_ports():
# Only output is connected; allow manual input.
self.get_widget('value').setEnabled(True)
output_port.send_data(self.get_property('value'))
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)
def on_input_connected(self, input_port, output_port):
"""
Called when an input port is connected.
"""
self.update_stream()
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()

36
Nodes/multiply_node.py Normal file
View File

@ -0,0 +1,36 @@
from NodeGraphQt import BaseNode
class MultiplyNode(BaseNode):
"""
Multiply Node:
- Inputs: A, B
- Output: Result (A * B)
"""
__identifier__ = 'io.github.nicole.multiply'
NODE_NAME = 'Multiply'
def __init__(self):
super(MultiplyNode, self).__init__()
self.values = {} # Ensure values is a dictionary.
self.add_input('A')
self.add_input('B')
self.add_output('Result')
self.value = 0
def process_input(self):
inputs = {}
for port_name in ['A', 'B']:
port = self.input(port_name)
connected = port.connected_ports() if port is not None else []
if connected:
connected_port = connected[0]
parent_node = connected_port.node()
try:
inputs[port_name] = float(getattr(parent_node, 'value', 0))
except (ValueError, TypeError):
inputs[port_name] = 0.0
else:
inputs[port_name] = 0.0
result = inputs['A'] * inputs['B']
self.value = result
self.set_name(f"Multiply: {result}")

37
Nodes/subtract_node.py Normal file
View File

@ -0,0 +1,37 @@
# Nodes/subtract_node.py
from NodeGraphQt import BaseNode
class SubtractNode(BaseNode):
"""
Subtract Node:
- Inputs: A, B
- Output: Result (A - B)
"""
__identifier__ = 'io.github.nicole.subtract'
NODE_NAME = 'Subtract'
def __init__(self):
super(SubtractNode, self).__init__()
self.add_input('A')
self.add_input('B')
self.add_output('Result')
self.value = 0
def process_input(self):
inputs = {}
for port_name in ['A', 'B']:
port = self.input(port_name)
connected = port.connected_ports() if port is not None else []
if connected:
connected_port = connected[0]
parent_node = connected_port.node()
try:
inputs[port_name] = float(getattr(parent_node, 'value', 0))
except (ValueError, TypeError):
inputs[port_name] = 0.0
else:
inputs[port_name] = 0.0
result = inputs['A'] - inputs['B']
self.value = result
self.set_name(f"Subtract: {result}")

View File

@ -1,239 +1,117 @@
#!/usr/bin/env python3
"""
Modified Flow UI Application:
- The character node is now named "Character Status" (display title).
- DisplayValueNodes dynamically update their title to show the value from the connected output port.
- Newly spawned DisplayValueNodes (via the context menu) follow the same dynamic behavior.
- Node creation strings now include the class name to match NodeGraphQt requirements.
Main Application (flow_UI.py)
This file dynamically imports custom node classes from the 'Nodes' package,
registers them with NodeGraphQt, and sets up an empty graph.
Nodes can be added dynamically via the graphs right-click context menu,
and a "Remove Selected Node" option is provided.
A global update timer periodically calls process_input() on nodes.
Additionally, this file patches QGraphicsScene.setSelectionArea to handle
selection behavior properly (so that multiple nodes can be selected).
"""
import sys
import requests
# --- 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
import importlib
import inspect
from Qt import QtWidgets, QtCore
from NodeGraphQt import NodeGraph, BaseNode
# -----------------------------------------------------------------------------
# Custom Port Painter Function for Character Status Node Ports (Semi-Transparent)
# -----------------------------------------------------------------------------
def get_draw_stat_port(color, border_color=None, alpha=127):
def import_nodes_from_folder(package_name):
"""
Returns a custom port painter function that draws a circular port with a semi-transparent fill,
then draws text (port label and current value) next to it.
Args:
color (tuple): RGB tuple for the fill color.
border_color (tuple, optional): RGB tuple for the border color.
Defaults to the same as color if not provided.
alpha (int, optional): Alpha value (0-255) for transparency. Default is 127.
Dynamically import all modules from the given package and return a list of
classes that subclass BaseNode.
"""
if border_color is None:
border_color = color
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 painter_func(painter, rect, info):
painter.save()
# --- Section: Port Drawing ---
pen = QtGui.QPen(QtGui.QColor(*border_color))
pen.setWidth(1.8)
painter.setPen(pen)
# Create semi-transparent fill using the provided alpha value.
semi_transparent_color = QtGui.QColor(color[0], color[1], color[2], alpha)
painter.setBrush(semi_transparent_color)
painter.drawEllipse(rect)
# --- Section: Port Label and Value Drawing ---
port = info.get('port')
if port is not None:
node = port.node() # Parent node (e.g. Character Status)
stat = port.name() # Port label (e.g. "HP")
# Retrieve the current value from the node's values dict (if available)
value = node.values.get(stat, "N/A") if hasattr(node, 'values') else "N/A"
# Create a text rectangle to the right of the port.
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
# -----------------------------------------------------------------------------
# Custom Node Classes
# -----------------------------------------------------------------------------
class CharacterStatusNode(BaseNode):
def make_node_command(graph, nt):
"""
Character Status Node
This node represents the character's current status.
It has no input ports and four output ports: "HP", "MP", "FP", and "EXP".
The current values are stored in self.values and are drawn using a custom painter.
Given a NodeGraph instance and a node type string nt, return a command
function that creates a node of that type.
"""
__identifier__ = 'io.github.nicole.status'
NODE_NAME = 'Character Status'
def __init__(self):
super(CharacterStatusNode, self).__init__()
# --- Section: Initialization of Stat Values and Ports ---
self.values = {"HP": "N/A", "MP": "N/A", "FP": "N/A", "EXP": "N/A"}
self.add_output("HP", painter_func=get_draw_stat_port((255, 0, 0))) # Red for HP
self.add_output("MP", painter_func=get_draw_stat_port((0, 0, 255))) # Blue for MP
self.add_output("FP", painter_func=get_draw_stat_port((0, 255, 0))) # Green for FP
self.add_output("EXP", painter_func=get_draw_stat_port((127, 255, 212))) # Aquamarine for EXP
# Set the node title to "Character Status"
self.set_name("Character Status")
def update_value(self, stat, value):
"""
Update the value for a given stat.
Args:
stat (str): Stat name (e.g., "HP", "MP", etc.).
value (str): New value for the stat.
"""
self.values[stat] = value
class DisplayValueNode(BaseNode):
"""
Display Value Node
This node displays a single input value.
Its title is updated to show the value from the connected output port.
If no port is connected, it displays "Display Value: No Input Configured".
"""
__identifier__ = 'io.github.nicole.display'
NODE_NAME = 'Display Value Node'
def __init__(self):
super(DisplayValueNode, self).__init__()
# --- Section: Initialization and Default State ---
self.add_input("in")
self.add_output("out")
self.value = "No Input Configured"
self.update_display()
def update_display(self):
"""
Update the node's title to reflect the current displayed value.
"""
self.set_name(f"Display Value: {self.value}")
def process_input(self):
"""
Processes the input connection:
- If connected, retrieve the value from the connected node's output port.
- If not, set the value to "No Input Configured".
"""
input_port = self.input(0)
connections = input_port.connections() if input_port is not None else []
if connections:
# --- Section: Processing Connected Input ---
connection = connections[0]
out_port = connection.out_port()
if out_port:
parent_node = out_port.node()
port_name = out_port.name()
self.value = parent_node.values.get(port_name, "N/A")
else:
self.value = "No Input Configured"
else:
# --- Section: No Connection Found ---
self.value = "No Input Configured"
self.update_display()
# -----------------------------------------------------------------------------
# API Polling Functions
# -----------------------------------------------------------------------------
def poll_api():
"""
Polls the Flask API for JSON data.
Expected JSON format: {"hp": "150/150", "mp": "75/100", "fp": "20/20", "exp": "10.0000"}
Returns:
dict: The JSON data, or None on error.
"""
api_url = "http://127.0.0.1:5000/data"
def command():
try:
response = requests.get(api_url, timeout=1)
if response.status_code == 200:
return response.json()
else:
print("API error: status code", response.status_code)
node = graph.create_node(nt)
# (No need to force top-level if nodes are created as top-level by default.)
except Exception as e:
print("Error polling API:", e)
return None
print(f"Error creating node of type {nt}: {e}")
return command
def update_nodes(graph, character_status_node):
"""
Polls the API, updates the Character Status node's values,
and then updates all DisplayValueNodes in the graph.
Args:
graph (NodeGraph): The NodeGraphQt graph instance.
character_status_node (CharacterStatusNode): The character status node.
"""
data = poll_api()
if data is None:
return
# --- Section: Update Character Status Node with Latest Data ---
character_status_node.update_value("HP", data.get('hp', "N/A"))
character_status_node.update_value("MP", data.get('mp', "N/A"))
character_status_node.update_value("FP", data.get('fp', "N/A"))
character_status_node.update_value("EXP", data.get('exp', "N/A"))
# --- Section: Update All DisplayValueNodes ---
for node in graph.all_nodes():
if hasattr(node, "process_input"):
node.process_input()
# -----------------------------------------------------------------------------
# Context Menu: Add DisplayValueNode Command
# -----------------------------------------------------------------------------
def add_display_node(graph):
"""
Adds a new DisplayValueNode to the graph at a default position.
Args:
graph (NodeGraph): The NodeGraphQt graph instance.
"""
# Use NodeGraphQt's create_node() with the correct node type string.
new_node = graph.create_node('io.github.nicole.display.DisplayValueNode',
name='Display Value Node', pos=[400, 300])
print("Added a new Display Value node.")
# -----------------------------------------------------------------------------
# Main Application
# -----------------------------------------------------------------------------
if __name__ == '__main__':
app = QtWidgets.QApplication([])
# --- Section: Setup NodeGraphQT ---
# Create the NodeGraph controller.
graph = NodeGraph()
graph.widget.setWindowTitle("Project Borealis - Flyff Information Overlay")
# Register nodes using their classes.
graph.register_node(CharacterStatusNode)
graph.register_node(DisplayValueNode)
graph.widget.setWindowTitle("Modular Nodes Demo")
# --- Section: Setup Context Menu for Adding DisplayValueNodes ---
# Dynamically import custom node classes from the 'Nodes' package.
custom_nodes = import_nodes_from_folder('Nodes')
for node_class in custom_nodes:
graph.register_node(node_class)
# Add context menu commands for dynamic node creation.
graph_context_menu = graph.get_context_menu('graph')
for node_class in custom_nodes:
# Build the node type string: "<__identifier__>.<ClassName>"
node_type = f"{node_class.__identifier__}.{node_class.__name__}"
node_name = node_class.NODE_NAME
graph_context_menu.add_command(
'Add Display Value Node',
lambda: add_display_node(graph),
shortcut='Ctrl+D'
f"Add {node_name}",
make_node_command(graph, node_type)
)
# --- Section: Create and Connect Nodes ---
# Create the Character Status node.
character_status_node = graph.create_node('io.github.nicole.status.CharacterStatusNode',
name='Character Status', pos=[0, 0])
# Create a Display Value node.
display_node = graph.create_node('io.github.nicole.display.DisplayValueNode',
name='Display Value Node', pos=[500, 200])
# Connect the EXP output (index 3) from the Character Status node to the display node's input.
character_status_node.set_output(3, display_node.input(0))
# Add a "Remove Selected Node" command to the graph context menu.
graph_context_menu.add_command(
"Remove Selected Node",
lambda: [graph.remove_node(node) for node in graph.selected_nodes()] if graph.selected_nodes() else None
)
# --- Section: Timer Setup for API Polling and Node Updates ---
# Resize and show the graph widget.
graph.widget.resize(1200, 800)
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"):
try:
node.process_input()
except Exception as e:
print("Error updating node", node, e)
timer = QtCore.QTimer()
timer.timeout.connect(lambda: update_nodes(graph, character_status_node))
timer.timeout.connect(global_update)
timer.start(500)
graph.widget.resize(1000, 600)
graph.widget.show()
app.exec_()
sys.exit(app.exec())