Files
godot-shader-experiments/addons/gecs/debug/gecs_editor_debugger_tab.gd
2026-01-15 15:27:48 +01:00

1502 lines
48 KiB
GDScript

@tool
class_name GECSEditorDebuggerTab
extends Control
@onready var query_builder_check_box: CheckBox = %QueryBuilderCheckBox
@onready var entities_filter_line_edit: LineEdit = %EntitiesQueryLineEdit
@onready var systems_filter_line_edit: LineEdit = %SystemsQueryLineEdit
@onready var collapse_all_btn: Button = %CollapseAllBtn
@onready var expand_all_btn: Button = %ExpandAllBtn
@onready var systems_collapse_all_btn: Button = %SystemsCollapseAllBtn
@onready var systems_expand_all_btn: Button = %SystemsExpandAllBtn
@onready var pop_out_btn: Button = %PopOutBtn
var ecs_data: Dictionary = {}
var default_system := {"path": "", "active": true, "metrics": {}, "group": ""}
var default_entity := {"path": "", "active": true, "components": {}, "relationships": {}}
var timer = 5
var active := false
var _pending_components: Dictionary = {} # ent_id -> Array[Dictionary] of pending component data
var _popup_window: Window = null
var _debugger_session: EditorDebuggerSession = null
@onready var system_tree: Tree = %SystemsTree
@onready var entities_tree: Tree = %EntitiesTree
@onready var entity_status_bar: TextEdit = %EntityStatusBar
@onready var systems_status_bar: TextEdit = %SystemsStatusBar
@onready var debug_mode_overlay: Panel = %DebugModeOverlay
# Sorting state
var _system_sort_column: int = -1 # -1 means no sorting
var _system_sort_ascending: bool = true
var _entity_sort_column: int = -1 # -1 means no sorting
var _entity_sort_ascending: bool = true
# Pinned items
var _pinned_entities: Dictionary = {} # entity_id -> bool
var _pinned_systems: Dictionary = {} # system_id -> bool
# Icon constants (using Unicode characters)
const ICON_ENTITY = "📦" # Entity icon
const ICON_COMPONENT = "🔧" # Component icon
const ICON_FLAG = "🚩" # Flag component (no properties)
const ICON_RELATIONSHIP = "🔗" # Relationship icon
const ICON_PIN = "📌" # Pinned item icon
func _ready() -> void:
_update_debug_mode_overlay()
if system_tree:
# Five columns: name, group, execution time, status, and order
system_tree.columns = 5
system_tree.set_column_expand(0, true) # Name column expands
system_tree.set_column_expand(1, false) # Group column resizable
system_tree.set_column_expand(2, false) # Execution time column resizable
system_tree.set_column_expand(3, false) # Status column resizable
system_tree.set_column_expand(4, false) # Order column resizable
# Set column widths
system_tree.set_column_custom_minimum_width(1, 100) # Group: 100px min
system_tree.set_column_custom_minimum_width(2, 100) # Execution time: 100px min
system_tree.set_column_custom_minimum_width(3, 100) # Status: 100px min
system_tree.set_column_custom_minimum_width(4, 60) # Order: 60px min
# Enable column resizing (clip content allows manual resizing)
system_tree.set_column_clip_content(0, true)
system_tree.set_column_clip_content(1, true)
system_tree.set_column_clip_content(2, true)
system_tree.set_column_clip_content(3, true)
system_tree.set_column_clip_content(4, true)
# Set column titles (clickable for sorting)
system_tree.set_column_title(0, "Name")
system_tree.set_column_title(1, "Group")
system_tree.set_column_title(2, "Time (ms)")
system_tree.set_column_title(3, "Status")
system_tree.set_column_title(4, "Order")
system_tree.set_column_titles_visible(true)
# Create root item
if system_tree.get_root() == null:
system_tree.create_item()
if entities_tree:
# Four columns: name, components count, relationships count, nodes count
entities_tree.columns = 4
entities_tree.set_column_expand(0, true) # Name column expands
entities_tree.set_column_expand(1, false) # Components count resizable
entities_tree.set_column_expand(2, false) # Relationships count resizable
entities_tree.set_column_expand(3, false) # Nodes count resizable
# Set column widths
entities_tree.set_column_custom_minimum_width(1, 80) # Components: 80px min
entities_tree.set_column_custom_minimum_width(2, 80) # Relationships: 80px min
entities_tree.set_column_custom_minimum_width(3, 80) # Nodes: 80px min
# Enable column resizing (clip content allows manual resizing)
entities_tree.set_column_clip_content(0, true)
entities_tree.set_column_clip_content(1, true)
entities_tree.set_column_clip_content(2, true)
entities_tree.set_column_clip_content(3, true)
# Set column titles
entities_tree.set_column_title(0, "Entity")
entities_tree.set_column_title(1, "Comps")
entities_tree.set_column_title(2, "Rels")
entities_tree.set_column_title(3, "Nodes")
entities_tree.set_column_titles_visible(true)
# Create root item
if entities_tree.get_root() == null:
entities_tree.create_item()
# Polling & pinning removed; tree updates only via incoming messages
if entities_filter_line_edit and not entities_filter_line_edit.text_changed.is_connected(_on_entities_filter_changed):
entities_filter_line_edit.text_changed.connect(_on_entities_filter_changed)
if systems_filter_line_edit and not systems_filter_line_edit.text_changed.is_connected(_on_systems_filter_changed):
systems_filter_line_edit.text_changed.connect(_on_systems_filter_changed)
if collapse_all_btn and not collapse_all_btn.pressed.is_connected(_on_collapse_all_pressed):
collapse_all_btn.pressed.connect(_on_collapse_all_pressed)
if expand_all_btn and not expand_all_btn.pressed.is_connected(_on_expand_all_pressed):
expand_all_btn.pressed.connect(_on_expand_all_pressed)
if systems_collapse_all_btn and not systems_collapse_all_btn.pressed.is_connected(_on_systems_collapse_all_pressed):
systems_collapse_all_btn.pressed.connect(_on_systems_collapse_all_pressed)
if systems_expand_all_btn and not systems_expand_all_btn.pressed.is_connected(_on_systems_expand_all_pressed):
systems_expand_all_btn.pressed.connect(_on_systems_expand_all_pressed)
if pop_out_btn and not pop_out_btn.pressed.is_connected(_on_pop_out_pressed):
pop_out_btn.pressed.connect(_on_pop_out_pressed)
# Connect to system tree for clicking (single click to toggle)
if system_tree and not system_tree.item_mouse_selected.is_connected(_on_system_tree_item_mouse_selected):
system_tree.item_mouse_selected.connect(_on_system_tree_item_mouse_selected)
# Connect to system tree for column clicking (for sorting)
if system_tree and not system_tree.column_title_clicked.is_connected(_on_system_tree_column_clicked):
system_tree.column_title_clicked.connect(_on_system_tree_column_clicked)
# Connect to entities tree for column clicking (for sorting)
if entities_tree and not entities_tree.column_title_clicked.is_connected(_on_entities_tree_column_clicked):
entities_tree.column_title_clicked.connect(_on_entities_tree_column_clicked)
# Connect to entities tree for right-click context menu
if entities_tree and not entities_tree.item_mouse_selected.is_connected(_on_entities_tree_item_mouse_selected):
entities_tree.item_mouse_selected.connect(_on_entities_tree_item_mouse_selected)
# Connect to system tree for right-click context menu
if system_tree and not system_tree.button_clicked.is_connected(_on_system_tree_button_clicked):
system_tree.button_clicked.connect(_on_system_tree_button_clicked)
func _process(delta: float) -> void:
# No periodic polling; rely on debugger messages only
pass
func _update_debug_mode_overlay() -> void:
if not debug_mode_overlay:
return
# Check if debug mode is enabled in project settings
var debug_enabled = ProjectSettings.get_setting(GecsSettings.SETTINGS_DEBUG_MODE, false)
# Show overlay if debug mode is disabled, hide if enabled
debug_mode_overlay.visible = not debug_enabled
# --- External setters expected by debugger plugin ---
func set_debugger_session(_session):
# Store session reference for sending messages to game
_debugger_session = _session
func set_editor_interface(_editor_interface):
# Store editor interface reference for future use (e.g., selecting nodes)
# Currently not used, but expected by the debugger plugin
pass
# Send a message from editor to the running game
func send_to_game(message: String, data: Array = []) -> bool:
if _debugger_session == null:
push_warning("GECS Debug: No active debugger session")
return false
_debugger_session.send_message(message, data)
return true
func clear_all_data():
ecs_data.clear()
_pending_components.clear()
# Clear system tree
if system_tree:
system_tree.clear()
# Recreate root
system_tree.create_item()
# Clear entities tree
if entities_tree:
entities_tree.clear()
# Recreate root
entities_tree.create_item()
# Reset status bars
_update_entity_status_bar()
_update_systems_status_bar()
# ---- Filters & Refresh Helpers ----
func _on_entities_filter_changed(new_text: String):
_refresh_entity_tree_filter()
func _on_systems_filter_changed(new_text: String):
_refresh_system_tree_filter()
# ---- Button Handlers ----
func _on_collapse_all_pressed():
collapse_all_entities()
func _on_expand_all_pressed():
expand_all_entities()
func _on_systems_collapse_all_pressed():
collapse_all_systems()
func _on_systems_expand_all_pressed():
expand_all_systems()
func _on_pop_out_pressed():
if _popup_window != null:
# Window already exists, close it and restore content
_on_popup_window_closed()
return
# Create a new window
_popup_window = Window.new()
_popup_window.title = "GECS Debug Viewer"
_popup_window.size = Vector2i(1200, 800)
_popup_window.initial_position = Window.WINDOW_INITIAL_POSITION_CENTER_SCREEN_WITH_MOUSE_FOCUS
# Move the main content to the window (not duplicate)
var hsplit = get_node("HSplit")
remove_child(hsplit)
_popup_window.add_child(hsplit)
# Add window to the scene tree
add_child(_popup_window)
# Connect close signal
_popup_window.close_requested.connect(_on_popup_window_closed)
# Show the window
_popup_window.show()
# Update the button text
pop_out_btn.text = "Pop In"
func _on_popup_window_closed():
if _popup_window != null:
# Move content back to main tab
var hsplit = _popup_window.get_node("HSplit")
_popup_window.remove_child(hsplit)
add_child(hsplit)
move_child(hsplit, 0) # Move to beginning
# Close and cleanup window
_popup_window.queue_free()
_popup_window = null
pop_out_btn.text = "Pop Out"
func _on_system_tree_item_mouse_selected(position: Vector2, mouse_button_index: int):
# When user clicks on a system tree item, check if clicking on status column to toggle or right-click for menu
var selected = system_tree.get_selected()
if not selected:
return
# Check if has system_id metadata with safe default
var system_id = selected.get_meta("system_id", null)
if system_id == null:
return
# Only process top-level system items (not child details)
if selected.get_parent() == system_tree.get_root():
if mouse_button_index == MOUSE_BUTTON_LEFT:
# Get the column that was clicked
var column = system_tree.get_column_at_position(position)
if column == 3: # Status column (now column 3)
_toggle_system_active()
elif mouse_button_index == MOUSE_BUTTON_RIGHT:
_show_system_context_menu(selected, position)
func _on_system_tree_button_clicked(item: TreeItem, column: int, id: int, mouse_button_index: int):
# Handle button clicks in tree (currently unused, but keeping for future)
pass
func _on_entities_tree_item_mouse_selected(position: Vector2, mouse_button_index: int):
# Handle right-click on entity tree items
if mouse_button_index != MOUSE_BUTTON_RIGHT:
return
var selected = entities_tree.get_selected()
if not selected:
return
# Check if has entity_id metadata (top-level entity item)
var entity_id = selected.get_meta("entity_id", null)
if entity_id == null:
return
# Only show context menu for top-level entity items
if selected.get_parent() == entities_tree.get_root():
_show_entity_context_menu(selected, position)
func _show_entity_context_menu(item: TreeItem, position: Vector2):
var entity_id = item.get_meta("entity_id", null)
if entity_id == null:
return
var popup = PopupMenu.new()
add_child(popup)
var is_pinned = _pinned_entities.get(entity_id, false)
if is_pinned:
popup.add_item("Unpin Entity", 0)
else:
popup.add_item("Pin Entity", 0)
# Position the popup at the mouse position (use get_screen_position for proper screen coords)
var screen_pos = entities_tree.get_screen_position() + position
popup.position = screen_pos
popup.popup()
# Connect the selection signal
popup.id_pressed.connect(func(id):
if id == 0:
_toggle_entity_pin(entity_id, item)
popup.queue_free()
)
# Clean up when popup closes
popup.popup_hide.connect(func():
if is_instance_valid(popup):
popup.queue_free()
)
func _show_system_context_menu(item: TreeItem, position: Vector2):
var system_id = item.get_meta("system_id", null)
if system_id == null:
return
var popup = PopupMenu.new()
add_child(popup)
var is_pinned = _pinned_systems.get(system_id, false)
if is_pinned:
popup.add_item("Unpin System", 0)
else:
popup.add_item("Pin System", 0)
# Position the popup at the mouse position (use get_screen_position for proper screen coords)
var screen_pos = system_tree.get_screen_position() + position
popup.position = screen_pos
popup.popup()
# Connect the selection signal
popup.id_pressed.connect(func(id):
if id == 0:
_toggle_system_pin(system_id, item)
popup.queue_free()
)
# Clean up when popup closes
popup.popup_hide.connect(func():
if is_instance_valid(popup):
popup.queue_free()
)
func _toggle_entity_pin(entity_id: int, item: TreeItem):
var is_pinned = _pinned_entities.get(entity_id, false)
_pinned_entities[entity_id] = not is_pinned
_update_entity_pin_display(item, not is_pinned)
# Re-sort to move pinned items to top
if _entity_sort_column != -1 or not is_pinned:
_sort_entity_tree()
func _toggle_system_pin(system_id: int, item: TreeItem):
var is_pinned = _pinned_systems.get(system_id, false)
_pinned_systems[system_id] = not is_pinned
_update_system_pin_display(item, not is_pinned)
# Re-sort to move pinned items to top
if _system_sort_column != -1 or not is_pinned:
_sort_system_tree()
func _update_entity_pin_display(item: TreeItem, is_pinned: bool):
var current_text = item.get_text(0)
# Remove existing pin icon if present
if current_text.begins_with(ICON_PIN + " "):
current_text = current_text.substr(2)
if is_pinned:
item.set_text(0, ICON_PIN + " " + current_text)
else:
item.set_text(0, current_text)
func _update_system_pin_display(item: TreeItem, is_pinned: bool):
var current_text = item.get_text(0) # Name is in column 0 now
# Remove existing pin icon if present
if current_text.begins_with(ICON_PIN + " "):
current_text = current_text.substr(2)
if is_pinned:
item.set_text(0, ICON_PIN + " " + current_text)
else:
item.set_text(0, current_text)
func _toggle_system_active():
var selected = system_tree.get_selected()
if not selected:
push_warning("GECS Debug: No system selected")
return
var system_id = selected.get_meta("system_id", null)
if system_id == null:
push_warning("GECS Debug: No system selected")
return
var systems_data = ecs_data.get("systems", {})
var system_data = systems_data.get(system_id, {})
var current_active = system_data.get("active", true)
# Toggle the state
var new_active = not current_active
# Send message to game to toggle system active state
send_to_game("gecs:set_system_active", [system_id, new_active])
# Optimistically update local state (will be confirmed by game)
system_data["active"] = new_active
_update_system_active_display(selected, new_active)
func _update_system_active_display(system_item: TreeItem, is_active: bool):
# Update the visual display of the system in column 3 as a button
if is_active:
system_item.set_text(3, "ACTIVE")
system_item.set_custom_color(3, Color(0.5, 1.0, 0.5)) # Green text
else:
system_item.set_text(3, "INACTIVE")
system_item.set_custom_color(3, Color(1.0, 0.3, 0.3)) # Red text
# Make the status column a clickable button
system_item.set_cell_mode(3, TreeItem.CELL_MODE_STRING)
system_item.set_selectable(3, true)
system_item.set_editable(3, false)
func _on_system_tree_column_clicked(column: int, mouse_button_index: int):
# Only sort on left click
if mouse_button_index != MOUSE_BUTTON_LEFT:
return
# Cycle through: None -> Asc -> Desc -> None
if _system_sort_column == column:
if _system_sort_ascending:
# Currently ascending, switch to descending
_system_sort_ascending = false
else:
# Currently descending, remove sorting
_system_sort_column = -1
_system_sort_ascending = true
else:
# New column, start with ascending
_system_sort_column = column
_system_sort_ascending = true
# Update column title indicators
_update_system_column_indicators()
# Sort the tree
_sort_system_tree()
func _update_system_column_indicators():
# Clear all column indicators first
for i in range(5):
var title = ""
match i:
0: title = "Name"
1: title = "Group"
2: title = "Time (ms)"
3: title = "Status"
4: title = "Order"
# Add arrow indicator if this is the sort column
if i == _system_sort_column:
if _system_sort_ascending:
title += ""
else:
title += ""
system_tree.set_column_title(i, title)
func _sort_system_tree():
if not system_tree:
return
var root = system_tree.get_root()
if not root:
return
# Collect all system items with their data
var systems: Array = []
var pinned_systems: Array = []
var child = root.get_first_child()
while child:
var system_id = child.get_meta("system_id", null)
var system_data = {
"item": child,
"name": child.get_text(0),
"group": child.get_text(1),
"time": 0.0,
"status": child.get_text(3),
"order": int(child.get_text(4)) if child.get_text(4).is_valid_int() else 0,
"system_id": system_id,
"is_pinned": _pinned_systems.get(system_id, false)
}
# Get execution time from text (remove " ms" suffix if present)
var time_text = child.get_text(2)
if time_text:
system_data["time"] = float(time_text.replace(" ms", ""))
if system_data["is_pinned"]:
pinned_systems.append(system_data)
else:
systems.append(system_data)
child = child.get_next()
# Sort based on column (if sorting is active)
if _system_sort_column != -1:
match _system_sort_column:
0: # Name
if _system_sort_ascending:
systems.sort_custom(func(a, b): return a["name"].nocasecmp_to(b["name"]) < 0)
else:
systems.sort_custom(func(a, b): return a["name"].nocasecmp_to(b["name"]) > 0)
1: # Group
if _system_sort_ascending:
systems.sort_custom(func(a, b): return a["group"].nocasecmp_to(b["group"]) < 0)
else:
systems.sort_custom(func(a, b): return a["group"].nocasecmp_to(b["group"]) > 0)
2: # Time
if _system_sort_ascending:
systems.sort_custom(func(a, b): return a["time"] < b["time"])
else:
systems.sort_custom(func(a, b): return a["time"] > b["time"])
3: # Status
if _system_sort_ascending:
systems.sort_custom(func(a, b): return a["status"] < b["status"])
else:
systems.sort_custom(func(a, b): return a["status"] > b["status"])
4: # Order
if _system_sort_ascending:
systems.sort_custom(func(a, b): return a["order"] < b["order"])
else:
systems.sort_custom(func(a, b): return a["order"] > b["order"])
# Rebuild tree: pinned items first, then sorted items
for system_data in pinned_systems:
var item = system_data["item"]
root.remove_child(item)
root.add_child(item)
for system_data in systems:
var item = system_data["item"]
root.remove_child(item)
root.add_child(item)
func _on_entities_tree_column_clicked(column: int, mouse_button_index: int):
# Only sort on left click
if mouse_button_index != MOUSE_BUTTON_LEFT:
return
# Cycle through: None -> Asc -> Desc -> None
if _entity_sort_column == column:
if _entity_sort_ascending:
# Currently ascending, switch to descending
_entity_sort_ascending = false
else:
# Currently descending, remove sorting
_entity_sort_column = -1
_entity_sort_ascending = true
else:
# New column, start with ascending
_entity_sort_column = column
_entity_sort_ascending = true
# Update column title indicators
_update_entity_column_indicators()
# Sort the tree
_sort_entity_tree()
func _update_entity_column_indicators():
# Clear all column indicators first
for i in range(4):
var title = ""
match i:
0: title = "Entity"
1: title = "Comps"
2: title = "Rels"
3: title = "Nodes"
# Add arrow indicator if this is the sort column
if i == _entity_sort_column:
if _entity_sort_ascending:
title += ""
else:
title += ""
entities_tree.set_column_title(i, title)
func _sort_entity_tree():
if not entities_tree:
return
var root = entities_tree.get_root()
if not root:
return
# Collect all entity items with their data
var entities: Array = []
var pinned_entities: Array = []
var child = root.get_first_child()
while child:
var entity_id = child.get_meta("entity_id", null)
var entity_data = {
"item": child,
"name": child.get_text(0),
"comps": 0,
"rels": 0,
"nodes": 0,
"entity_id": entity_id,
"is_pinned": _pinned_entities.get(entity_id, false)
}
# Get numeric counts from columns
var comps_text = child.get_text(1)
if comps_text:
entity_data["comps"] = int(comps_text)
var rels_text = child.get_text(2)
if rels_text:
entity_data["rels"] = int(rels_text)
var nodes_text = child.get_text(3)
if nodes_text:
entity_data["nodes"] = int(nodes_text)
if entity_data["is_pinned"]:
pinned_entities.append(entity_data)
else:
entities.append(entity_data)
child = child.get_next()
# Sort based on column (if sorting is active)
if _entity_sort_column != -1:
match _entity_sort_column:
0: # Name
if _entity_sort_ascending:
entities.sort_custom(func(a, b): return a["name"].nocasecmp_to(b["name"]) < 0)
else:
entities.sort_custom(func(a, b): return a["name"].nocasecmp_to(b["name"]) > 0)
1: # Components
if _entity_sort_ascending:
entities.sort_custom(func(a, b): return a["comps"] < b["comps"])
else:
entities.sort_custom(func(a, b): return a["comps"] > b["comps"])
2: # Relationships
if _entity_sort_ascending:
entities.sort_custom(func(a, b): return a["rels"] < b["rels"])
else:
entities.sort_custom(func(a, b): return a["rels"] > b["rels"])
3: # Nodes
if _entity_sort_ascending:
entities.sort_custom(func(a, b): return a["nodes"] < b["nodes"])
else:
entities.sort_custom(func(a, b): return a["nodes"] > b["nodes"])
# Rebuild tree: pinned items first, then sorted items
for entity_data in pinned_entities:
var item = entity_data["item"]
root.remove_child(item)
root.add_child(item)
for entity_data in entities:
var item = entity_data["item"]
root.remove_child(item)
root.add_child(item)
# --- Utilities ---
func get_or_create_dict(dict: Dictionary, key, default_val = {}) -> Dictionary:
if not dict.has(key):
dict[key] = default_val
return dict[key]
func collapse_all_entities():
if not entities_tree:
return
var root = entities_tree.get_root()
if root == null:
return
var item = root.get_first_child()
while item:
_collapse_item_recursive(item)
item = item.get_next()
func expand_all_entities():
if not entities_tree:
return
var root = entities_tree.get_root()
if root == null:
return
var item = root.get_first_child()
while item:
_expand_item_recursive(item)
item = item.get_next()
func collapse_all_systems():
if not system_tree:
return
var root = system_tree.get_root()
if root == null:
return
var item = root.get_first_child()
while item:
_collapse_item_recursive(item)
item = item.get_next()
func expand_all_systems():
if not system_tree:
return
var root = system_tree.get_root()
if root == null:
return
var item = root.get_first_child()
while item:
_expand_item_recursive(item)
item = item.get_next()
func _collapse_item_recursive(item: TreeItem):
if item == null:
return
item.collapsed = true
var child = item.get_first_child()
while child:
_collapse_item_recursive(child)
child = child.get_next()
func _expand_item_recursive(item: TreeItem):
if item == null:
return
item.collapsed = false
var child = item.get_first_child()
while child:
_expand_item_recursive(child)
child = child.get_next()
# ---- Filters ----
func _refresh_system_tree_filter():
if not system_tree:
return
var root = system_tree.get_root()
if root == null:
return
var filter = systems_filter_line_edit.text.to_lower() if systems_filter_line_edit else ""
var item = root.get_first_child()
while item:
var name = item.get_text(0).to_lower()
item.visible = filter == "" or name.find(filter) != -1
item = item.get_next()
func _refresh_entity_tree_filter():
if not entities_tree:
return
var root = entities_tree.get_root()
if root == null:
return
var filter = entities_filter_line_edit.text.to_lower() if entities_filter_line_edit else ""
var item = root.get_first_child()
while item:
var label = item.get_text(0).to_lower()
var matches = filter == "" or label.find(filter) != -1
if not matches:
var comp_child = item.get_first_child()
while comp_child and not matches:
if comp_child.get_text(0).to_lower().find(filter) != -1:
matches = true
break
var prop_row = comp_child.get_first_child()
while prop_row and not matches:
if prop_row.get_text(0).to_lower().find(filter) != -1:
matches = true
break
prop_row = prop_row.get_next()
comp_child = comp_child.get_next()
item.visible = matches
item = item.get_next()
func world_init(world_id: int, world_path: NodePath):
# Initialize world tracking
var world_dict := get_or_create_dict(ecs_data, "world")
world_dict["id"] = world_id
world_dict["path"] = world_path
# Update debug mode overlay in case settings changed
_update_debug_mode_overlay()
func set_world(world_id: int, world_path: NodePath):
# Set or update current world
var world_dict := get_or_create_dict(ecs_data, "world")
world_dict["id"] = world_id
world_dict["path"] = world_path
func process_world(delta: float, group_name: String):
var world_dict := get_or_create_dict(ecs_data, "world")
world_dict["delta"] = delta
world_dict["active_group"] = group_name
func exit_world():
ecs_data["exited"] = true
func _update_entity_counts(entity_item: TreeItem, ent_id: int):
# Update the count columns for an entity
if not entity_item:
return
var entities = ecs_data.get("entities", {})
var entity_data = entities.get(ent_id, {})
# Count components
var components = entity_data.get("components", {})
entity_item.set_text(1, str(components.size()))
# Count relationships
var relationships = entity_data.get("relationships", {})
entity_item.set_text(2, str(relationships.size()))
# Count child nodes (entities tree doesn't track this from game data)
# We'll count the child TreeItems instead (components + relationships)
var child_count = 0
var child = entity_item.get_first_child()
while child:
# Count only component and relationship children, not property rows
if child.has_meta("component_id") or child.has_meta("relationship_id"):
child_count += 1
child = child.get_next()
entity_item.set_text(3, str(child_count))
func _is_flag_component(component_data: Dictionary) -> bool:
# A flag component has no serializable properties
# Check if data is empty or only has the placeholder
if component_data.is_empty():
return true
if component_data.size() == 1 and component_data.has("<no_serialized_properties>"):
return true
return false
func entity_added(ent: int, path: NodePath) -> void:
var entities := get_or_create_dict(ecs_data, "entities")
# Merge with any existing (temporary) entry that may already have buffered components/relationships
var existing := entities.get(ent, {})
var existing_components: Dictionary = existing.get("components", {})
var existing_relationships: Dictionary = existing.get("relationships", {})
# Update in place instead of overwrite to avoid losing buffered component data
entities[ent] = {
"path": path,
"active": true,
"components": existing_components,
"relationships": existing_relationships
}
# Add to entities tree
if entities_tree:
var root = entities_tree.get_root()
if root == null:
root = entities_tree.create_item()
var item = entities_tree.create_item(root)
# Column 0: Entity name with icon (and pin icon if pinned)
var display_name = ICON_ENTITY + " " + str(path).get_file()
if _pinned_entities.get(ent, false):
display_name = ICON_PIN + " " + display_name
item.set_text(0, display_name)
item.set_tooltip_text(0, str(ent) + " : " + str(path))
# Columns 1-3: Counts (will be updated as components/relationships are added)
item.set_text(1, "0")
item.set_text(2, "0")
item.set_text(3, "0")
item.set_meta("entity_id", ent)
item.set_meta("path", path)
item.collapsed = true # Start collapsed
# Flush any pending components that arrived before the entity node was created
if _pending_components.has(ent):
for comp_info in _pending_components[ent]:
_attach_component_to_entity_item(item, ent, comp_info.comp_id, comp_info.comp_path, comp_info.data)
_pending_components.erase(ent)
# Update counts
_update_entity_counts(item, ent)
# Re-sort if we have an active sort column
if _entity_sort_column != -1:
_sort_entity_tree()
_update_entity_status_bar()
func entity_removed(ent: int, path: NodePath) -> void:
var entities := get_or_create_dict(ecs_data, "entities")
entities.erase(ent)
# Remove from tree
if entities_tree and entities_tree.get_root():
var root = entities_tree.get_root()
var child = root.get_first_child()
while child:
if child.get_meta("entity_id", null) == ent:
root.remove_child(child)
break
child = child.get_next()
# Clean up pinned state
_pinned_entities.erase(ent)
_update_entity_status_bar()
func entity_disabled(ent: int, path: NodePath) -> void:
var entities = get_or_create_dict(ecs_data, "entities")
if entities.has(ent):
entities[ent]["active"] = false
if entities_tree and entities_tree.get_root():
var child = entities_tree.get_root().get_first_child()
while child:
if child.get_meta("entity_id", null) == ent:
child.set_text(0, child.get_text(0) + " (disabled)")
break
child = child.get_next()
func entity_enabled(ent: int, path: NodePath) -> void:
var entities = get_or_create_dict(ecs_data, "entities")
if entities.has(ent):
entities[ent]["active"] = true
if entities_tree and entities_tree.get_root():
var child = entities_tree.get_root().get_first_child()
while child:
if child.get_meta("entity_id", null) == ent:
# Remove any (disabled) suffix
var txt = child.get_text(0)
if txt.ends_with(" (disabled)"):
child.set_text(0, txt.substr(0, txt.length() - 11))
break
child = child.get_next()
func system_added(
sys: int, group: String, process_empty: bool, active: bool, paused: bool, path: NodePath
) -> void:
var systems_data := get_or_create_dict(ecs_data, "systems")
systems_data[sys] = default_system.duplicate()
systems_data[sys]["path"] = path
systems_data[sys]["group"] = group
systems_data[sys]["process_empty"] = process_empty
systems_data[sys]["active"] = active
systems_data[sys]["paused"] = paused
_update_systems_status_bar()
func system_removed(sys: int, path: NodePath) -> void:
var systems_data := get_or_create_dict(ecs_data, "systems")
systems_data.erase(sys)
# Clean up pinned state
_pinned_systems.erase(sys)
_update_systems_status_bar()
func system_metric(system: int, system_name: String, time: float):
var systems_data := get_or_create_dict(ecs_data, "systems")
var sys_entry := get_or_create_dict(systems_data, system, default_system.duplicate())
# Track the last run time separately so it's always visible even when aggregation occurs
sys_entry["last_time"] = time
var sys_metrics = ecs_data["systems"][system]["metrics"]
if not sys_metrics:
# Initialize metrics if not present
sys_metrics = {"min_time": time, "max_time": time, "avg_time": time, "count": 1, "last_time": time}
sys_metrics["min_time"] = min(sys_metrics["min_time"], time)
sys_metrics["max_time"] = max(sys_metrics["max_time"], time)
sys_metrics["count"] += 1
sys_metrics["avg_time"] = (
((sys_metrics["avg_time"] * (sys_metrics["count"] - 1)) + time) / sys_metrics["count"]
)
sys_metrics["last_time"] = time
ecs_data["systems"][system]["metrics"] = sys_metrics
_update_systems_status_bar()
func system_last_run_data(system_id: int, system_name: String, last_run_data: Dictionary):
var systems_data := get_or_create_dict(ecs_data, "systems")
var sys_entry := get_or_create_dict(systems_data, system_id, default_system.duplicate())
sys_entry["last_run_data"] = last_run_data
# Update or create tree item
if system_tree:
var root = system_tree.get_root()
if root == null:
root = system_tree.create_item()
# Try to find existing item by metadata matching system_id
var existing: TreeItem = null
var child = root.get_first_child()
while child != null:
if child.get_meta("system_id", null) == system_id:
existing = child
break
child = child.get_next()
if existing == null:
existing = system_tree.create_item(root)
existing.set_meta("system_id", system_id)
existing.collapsed = true # Start collapsed
# Set main system name in column 0
var display_name = system_name
# Check if this system is pinned and update display
if _pinned_systems.get(system_id, false):
if not display_name.begins_with(ICON_PIN + " "):
display_name = ICON_PIN + " " + display_name
existing.set_text(0, display_name)
# Set group in column 1
var group = sys_entry.get("group", "")
existing.set_text(1, group)
# Set execution time in column 2
var exec_ms = last_run_data.get("execution_time_ms", 0.0)
existing.set_text(2, String.num(exec_ms, 3) + " ms")
# Set active status in column 3
var is_active = sys_entry.get("active", true)
_update_system_active_display(existing, is_active)
# Get execution order (index in systems array from last_run_data) - column 4
var execution_order = last_run_data.get("execution_order", -1)
if execution_order >= 0:
existing.set_text(4, str(execution_order))
else:
existing.set_text(4, "-")
# Clear previous children to avoid stale data
var prev_child = existing.get_first_child()
while prev_child:
var next_child = prev_child.get_next()
existing.remove_child(prev_child)
prev_child = next_child
# Create nested rows for key info
var ent_count = last_run_data.get("entity_count", null)
var arch_count = last_run_data.get("archetype_count", null)
var parallel = last_run_data.get("parallel", false)
var nested_data := {
"execution_time_ms": String.num(exec_ms, 3),
"entity_count": ent_count,
"archetype_count": arch_count,
"parallel": parallel,
}
for k in nested_data.keys():
var v = nested_data[k]
if v == null:
continue
var row = system_tree.create_item(existing)
row.set_text(0, str(k) + ": " + str(v))
# Subsystem details (numeric keys in last_run_data)
for key in last_run_data.keys():
if typeof(key) == TYPE_INT and last_run_data[key] is Dictionary:
var sub = last_run_data[key]
var sub_row = system_tree.create_item(existing)
sub_row.set_text(0, "subsystem[" + str(key) + "] entity_count: " + str(sub.get("entity_count", 0)))
# Optionally store raw json in metadata for tooltip or future expansion
existing.set_meta("last_run_data", last_run_data.duplicate())
# Re-sort if we have an active sort column
if _system_sort_column != -1:
_sort_system_tree()
# Update status bar with latest system data
_update_systems_status_bar()
func entity_component_added(ent: int, comp: int, comp_path: String, data: Dictionary):
var entities := get_or_create_dict(ecs_data, "entities")
var entity := get_or_create_dict(entities, ent)
if not entity.has("components"):
entity["components"] = {}
# Fallback: if serialized data is empty, attempt reflection of exported properties
var final_data = data
if final_data.is_empty():
final_data = {}
# Try to get the actual Object from instance_id (editor debugger gives us ID only). We can't reliably from here; leave empty.
# As a workaround store a placeholder so UI shows component node.
final_data["<no_serialized_properties>"] = true
entity["components"][comp] = final_data
# Update tree with component node and property children
if entities_tree:
var root = entities_tree.get_root()
if root != null:
var entity_item: TreeItem = null
var child = root.get_first_child()
while child:
if child.get_meta("entity_id", null) == ent:
entity_item = child
break
child = child.get_next()
if entity_item:
# Try to find existing component item to update instead of duplicating
var existing_comp_item: TreeItem = null
var comp_child = entity_item.get_first_child()
while comp_child:
if comp_child.has_meta("component_id") and comp_child.get_meta("component_id") == comp:
existing_comp_item = comp_child
break
comp_child = comp_child.get_next()
if existing_comp_item:
# Clear previous property rows
var prev = existing_comp_item.get_first_child()
while prev:
var nxt = prev.get_next()
existing_comp_item.remove_child(prev)
prev = nxt
# Update title/path with icon
var icon = ICON_FLAG if _is_flag_component(final_data) else ICON_COMPONENT
existing_comp_item.set_text(0, icon + " " + comp_path.get_file().get_basename())
existing_comp_item.set_tooltip_text(0, comp_path)
existing_comp_item.set_meta("component_path", comp_path)
_add_serialized_rows(existing_comp_item, final_data)
else:
_attach_component_to_entity_item(entity_item, ent, comp, comp_path, final_data)
# Update entity counts
_update_entity_counts(entity_item, ent)
else:
# Buffer component until entity_added arrives
if not _pending_components.has(ent):
_pending_components[ent] = []
_pending_components[ent].append({"comp_id": comp, "comp_path": comp_path, "data": final_data})
# Re-sort if we have an active sort column
if _entity_sort_column != -1:
_sort_entity_tree()
_update_entity_status_bar()
func _attach_component_to_entity_item(entity_item: TreeItem, ent: int, comp: int, comp_path: String, final_data: Dictionary) -> void:
var comp_item = entities_tree.create_item(entity_item)
# Use flag icon for components with no properties, otherwise use component icon
var icon = ICON_FLAG if _is_flag_component(final_data) else ICON_COMPONENT
comp_item.set_text(0, icon + " " + comp_path.get_file().get_basename())
comp_item.set_tooltip_text(0, comp_path)
comp_item.set_meta("component_id", comp)
comp_item.set_meta("component_path", comp_path)
comp_item.collapsed = true # Start collapsed
# Add property rows with recursive serialization
_add_serialized_rows(comp_item, final_data)
# Update entity counts
_update_entity_counts(entity_item, ent)
func entity_component_removed(ent: int, comp: int):
var entities = get_or_create_dict(ecs_data, "entities")
if entities.has(ent) and entities[ent].has("components"):
entities[ent]["components"].erase(comp)
if entities_tree and entities_tree.get_root():
var entity_item: TreeItem = null
var child = entities_tree.get_root().get_first_child()
while child:
if child.get_meta("entity_id", null) == ent:
entity_item = child
break
child = child.get_next()
if entity_item:
var comp_child = entity_item.get_first_child()
while comp_child:
if comp_child.has_meta("component_id") and comp_child.get_meta("component_id") == comp:
entity_item.remove_child(comp_child)
break
comp_child = comp_child.get_next()
# Update entity counts
_update_entity_counts(entity_item, ent)
_update_entity_status_bar()
func entity_component_property_changed(
ent: int, comp: int, property_name: String, old_value: Variant, new_value: Variant
):
var entities = get_or_create_dict(ecs_data, "entities")
if entities.has(ent) and entities[ent].has("components"):
var component = entities[ent]["components"].get(comp)
if component:
component[property_name] = new_value
# Update tree property row
if entities_tree and entities_tree.get_root():
var entity_item: TreeItem = null
var child = entities_tree.get_root().get_first_child()
while child:
if child.get_meta("entity_id", null) == ent:
entity_item = child
break
child = child.get_next()
if entity_item:
var comp_child = entity_item.get_first_child()
while comp_child:
if comp_child.has_meta("component_id") and comp_child.get_meta("component_id") == comp:
var prop_row = comp_child.get_first_child()
var updated := false
while prop_row:
if prop_row.has_meta("property_name") and prop_row.get_meta("property_name") == property_name:
prop_row.set_text(0, property_name + ": " + str(new_value))
updated = true
break
prop_row = prop_row.get_next()
# If property row not found (added dynamically), append it
if not updated:
var new_row = entities_tree.create_item(comp_child)
new_row.set_text(0, property_name + ": " + str(new_value))
new_row.set_meta("property_name", property_name)
# Done updating this component; no need to scan further
break
comp_child = comp_child.get_next()
# ---- Recursive Serialization Rendering ----
func _add_serialized_rows(parent_item: TreeItem, data: Dictionary):
for key in data.keys():
var value = data[key]
var row = entities_tree.create_item(parent_item)
row.set_text(0, str(key) + ": " + _value_to_string(value))
row.set_meta("property_name", key)
if value is Dictionary:
_add_serialized_rows(row, value)
elif value is Array:
_add_array_rows(row, value)
func _add_array_rows(parent_item: TreeItem, arr: Array):
for i in range(arr.size()):
var value = arr[i]
var row = entities_tree.create_item(parent_item)
row.set_text(0, "[" + str(i) + "] " + _value_to_string(value))
row.set_meta("property_name", str(i))
if value is Dictionary:
_add_serialized_rows(row, value)
elif value is Array:
_add_array_rows(row, value)
func _value_to_string(v):
match typeof(v):
TYPE_DICTIONARY:
return "{...}" # expanded in children
TYPE_ARRAY:
return "[..." + str(v.size()) + "]"
TYPE_STRING:
return '"' + v + '"'
TYPE_OBJECT:
if v is Resource:
return "Resource(" + v.resource_path.get_file() + ")"
return str(v)
_:
return str(v)
func entity_relationship_added(ent: int, rel: int, rel_data: Dictionary):
var entities := get_or_create_dict(ecs_data, "entities")
var entity := get_or_create_dict(entities, ent)
var relationships := get_or_create_dict(entity, "relationships")
relationships[rel] = rel_data
# Add to tree
if entities_tree:
var root = entities_tree.get_root()
if root != null:
var entity_item: TreeItem = null
var child = root.get_first_child()
while child:
if child.get_meta("entity_id", null) == ent:
entity_item = child
break
child = child.get_next()
if entity_item:
# Try to find existing relationship item to update
var existing_rel_item: TreeItem = null
var rel_child = entity_item.get_first_child()
while rel_child:
if rel_child.has_meta("relationship_id") and rel_child.get_meta("relationship_id") == rel:
existing_rel_item = rel_child
break
rel_child = rel_child.get_next()
if existing_rel_item:
# Clear and rebuild
var prev = existing_rel_item.get_first_child()
while prev:
var nxt = prev.get_next()
existing_rel_item.remove_child(prev)
prev = nxt
_update_relationship_item(existing_rel_item, rel_data)
else:
# Create new relationship item
var rel_item = entities_tree.create_item(entity_item)
rel_item.set_meta("relationship_id", rel)
rel_item.collapsed = true # Start collapsed
_update_relationship_item(rel_item, rel_data)
# Update entity counts
_update_entity_counts(entity_item, ent)
# Re-sort if we have an active sort column
if _entity_sort_column != -1:
_sort_entity_tree()
_update_entity_status_bar()
func _update_relationship_item(rel_item: TreeItem, rel_data: Dictionary):
# Format the relationship display
var relation_type = rel_data.get("relation_type", "Unknown")
var target_type = rel_data.get("target_type", "Unknown")
# Build the title based on target type with relationship icon
var title = ICON_RELATIONSHIP + " " + relation_type + " -> "
if target_type == "Entity":
var target_data = rel_data.get("target_data", {})
title += "Entity " + str(target_data.get("path", "Unknown"))
elif target_type == "Component":
var target_data = rel_data.get("target_data", {})
title += target_data.get("type", "Unknown")
elif target_type == "Archetype":
var target_data = rel_data.get("target_data", {})
var script_path = target_data.get("script_path", "")
title += "Archetype " + script_path.get_file().get_basename()
elif target_type == "null":
title += "Wildcard"
else:
title += target_type
rel_item.set_text(0, title)
# Add relation data as children
var relation_data = rel_data.get("relation_data", {})
if not relation_data.is_empty():
var rel_data_item = entities_tree.create_item(rel_item)
rel_data_item.set_text(0, "Relation Properties:")
_add_serialized_rows(rel_data_item, relation_data)
# Add target data as children (for components with properties)
if target_type == "Component":
var target_data = rel_data.get("target_data", {})
var target_comp_data = target_data.get("data", {})
if not target_comp_data.is_empty():
var target_data_item = entities_tree.create_item(rel_item)
target_data_item.set_text(0, "Target Properties:")
_add_serialized_rows(target_data_item, target_comp_data)
func entity_relationship_removed(ent: int, rel: int):
var entities = get_or_create_dict(ecs_data, "entities")
if entities.has(ent) and entities[ent].has("relationships"):
entities[ent]["relationships"].erase(rel)
# Remove from tree
if entities_tree and entities_tree.get_root():
var entity_item: TreeItem = null
var child = entities_tree.get_root().get_first_child()
while child:
if child.get_meta("entity_id", null) == ent:
entity_item = child
break
child = child.get_next()
if entity_item:
var rel_child = entity_item.get_first_child()
while rel_child:
if rel_child.has_meta("relationship_id") and rel_child.get_meta("relationship_id") == rel:
entity_item.remove_child(rel_child)
break
rel_child = rel_child.get_next()
# Update entity counts
_update_entity_counts(entity_item, ent)
_update_entity_status_bar()
# ---- Status Bar Updates ----
## Update the entity status bar with current counts
func _update_entity_status_bar():
if not entity_status_bar:
return
var entities = ecs_data.get("entities", {})
var entity_count = entities.size()
# Count total components across all entities
var total_components = 0
for entity_data in entities.values():
var components = entity_data.get("components", {})
total_components += components.size()
# Count total relationships across all entities
var total_relationships = 0
for entity_data in entities.values():
var relationships = entity_data.get("relationships", {})
total_relationships += relationships.size()
entity_status_bar.text = "Entities: %d | Components: %d | Relationships: %d" % [
entity_count,
total_components,
total_relationships
]
## Update the systems status bar with execution metrics
func _update_systems_status_bar():
if not systems_status_bar:
return
var systems_data = ecs_data.get("systems", {})
var system_count = systems_data.size()
# Calculate total execution time and find most expensive system
var total_time_ms = 0.0
var most_expensive_name = ""
var most_expensive_time = 0.0
for system_id in systems_data.keys():
var system_data = systems_data[system_id]
# Get execution time from last_run_data if available (more accurate than last_time)
var last_run_data = system_data.get("last_run_data", {})
var exec_time_ms = last_run_data.get("execution_time_ms", 0.0)
total_time_ms += exec_time_ms
if exec_time_ms > most_expensive_time:
most_expensive_time = exec_time_ms
# Try to get a readable system name from last_run_data first, then path
var system_name = last_run_data.get("system_name", "")
if not system_name:
var path = system_data.get("path", "")
if path:
system_name = str(path).get_file().get_basename()
else:
system_name = "System_%d" % system_id
most_expensive_name = system_name
# Format the status bar text
if most_expensive_name:
systems_status_bar.text = "Systems: %d | Total ms: %.1fms | Most Expensive: %s (%.1fms)" % [
system_count,
total_time_ms,
most_expensive_name,
most_expensive_time
]
else:
systems_status_bar.text = "Systems: %d | Total ms: %.1fms" % [
system_count,
total_time_ms
]