Broken graph editor

This commit is contained in:
MaddoScientisto 2026-03-01 23:14:33 +01:00
commit f996513dca
16 changed files with 1033 additions and 12 deletions

View file

@ -0,0 +1,46 @@
@tool
extends EditorInspectorPlugin
var _plugin: EditorPlugin
func setup(plugin: EditorPlugin) -> void:
_plugin = plugin
func _can_handle(object: Object) -> bool:
return object is Resource and _is_supported_resource(object)
func _parse_begin(object: Object) -> void:
if not (object is Resource):
return
var button := Button.new()
button.text = "Open In Bullet Graph Editor"
button.pressed.connect(_on_open_pressed.bind(object))
add_custom_control(button)
func _on_open_pressed(resource: Resource) -> void:
if _plugin == null:
return
_plugin.call("open_resource_in_graph", resource)
func _is_supported_resource(resource: Resource) -> bool:
var script: Script = resource.get_script()
if script == null:
return false
var path := script.resource_path
if path.contains("/Scripts/AttackPatterns/"):
return true
if path.contains("/Scripts/Resources/") and path.contains("Pattern"):
return true
if path.ends_with("BossScript.cs") \
or path.ends_with("BossPhase.cs") \
or path.ends_with("BulletScript3D.cs") \
or path.ends_with("BulletScript.cs") \
or path.ends_with("AttackPattern.cs") \
or path.ends_with("PatternGroup.cs") \
or path.ends_with("ParallelPatternGroup.cs"):
return true
return resource.get("WaitForCompletion") != null

View file

@ -0,0 +1 @@
uid://bchuuu8pwh3k

View file

@ -0,0 +1,281 @@
@tool
extends RefCounted
const SUPPORTED_SCRIPT_SUFFIXES := [
"BossScript.cs",
"BossPhase.cs",
"BulletScript3D.cs",
"BulletScript.cs",
"AttackPattern.cs",
"PatternGroup.cs",
"ParallelPatternGroup.cs",
"MovementPattern.cs"
]
func is_supported_resource(resource: Resource) -> bool:
if resource == null:
return false
var script: Script = resource.get_script()
var path := ""
if script != null:
path = script.resource_path
if path != "" and path.contains("/Scripts/AttackPatterns/"):
return true
if path != "" and path.contains("/Scripts/Resources/") and path.contains("Pattern"):
return true
for suffix in SUPPORTED_SCRIPT_SUFFIXES:
if path != "" and path.ends_with(suffix):
return true
# Heuristic for AttackPattern-derived resources exposed from C# exports.
if resource.get("WaitForCompletion") != null:
return true
return false
func build_graph(root: Resource) -> Dictionary:
var graph := {
"nodes": {},
"edges": [],
"edge_index": {},
"root_id": ""
}
if root == null:
return graph
var ctx := {
"graph": graph,
"visited": {}
}
var root_id := _visit_resource(root, "", "root", "root", ctx)
graph.erase("edge_index")
graph["root_id"] = root_id
return graph
func _visit_resource(resource: Resource, parent_id: String, edge_type: String, label: String, ctx: Dictionary) -> String:
var id := _make_node_id(resource)
var nodes: Dictionary = ctx["graph"]["nodes"]
if not nodes.has(id):
nodes[id] = {
"id": id,
"title": _make_title(resource, label),
"subtitle": _make_subtitle(resource),
"kind": _get_kind(resource),
"resource": resource,
"incoming": [],
"outgoing": []
}
if parent_id != "":
_add_edge(parent_id, id, edge_type, ctx)
var visited: Dictionary = ctx["visited"]
if visited.has(id):
return id
visited[id] = true
_process_children(resource, id, ctx)
return id
func _add_edge(from_id: String, to_id: String, edge_type: String, ctx: Dictionary) -> void:
var edge_key := "%s|%s|%s" % [from_id, to_id, edge_type]
var edge_index: Dictionary = ctx["graph"]["edge_index"]
if edge_index.has(edge_key):
return
edge_index[edge_key] = true
ctx["graph"]["edges"].append({
"from": from_id,
"to": to_id,
"type": edge_type
})
var nodes: Dictionary = ctx["graph"]["nodes"]
if nodes.has(from_id):
nodes[from_id]["outgoing"].append(edge_type)
if nodes.has(to_id):
nodes[to_id]["incoming"].append(edge_type)
func _ensure_virtual_node(id: String, title: String, subtitle: String, kind: String, ctx: Dictionary) -> void:
var nodes: Dictionary = ctx["graph"]["nodes"]
if nodes.has(id):
return
nodes[id] = {
"id": id,
"title": title,
"subtitle": subtitle,
"kind": kind,
"resource": null,
"incoming": [],
"outgoing": []
}
func _process_children(resource: Resource, id: String, ctx: Dictionary) -> void:
var kind := _get_kind(resource)
if kind == "BossScript":
_visit_ordered_array(resource, "Phases", id, "phase", "phase", ctx)
return
if kind == "BossPhase":
var bullet_lane_id := "%s::bullet_lane" % id
var legacy_lane_id := "%s::legacy_lane" % id
var bullet_script: Variant = _get_prop(resource, "BulletScript3D")
if bullet_script is Resource:
_ensure_virtual_node(bullet_lane_id, "BulletScript3D Lane", "Preferred execution path", "Lane", ctx)
_add_edge(id, bullet_lane_id, "bullet_script_lane", ctx)
_visit_resource(bullet_script, bullet_lane_id, "bullet_script", "BulletScript3D", ctx)
var legacy_values: Array = _get_array_prop(resource, "Patterns")
if legacy_values.size() > 0:
_ensure_virtual_node(legacy_lane_id, "Legacy Patterns Lane", "Compatibility path", "LegacyLane", ctx)
_add_edge(id, legacy_lane_id, "legacy_lane", ctx)
_visit_ordered_array(resource, "Patterns", legacy_lane_id, "legacy", "pattern", ctx)
return
if kind == "BulletScript3D" or kind == "BulletScript":
_visit_ordered_array(resource, "Patterns", id, "sequence", "pattern", ctx)
return
if kind == "PatternGroup":
_visit_ordered_array_fallback(resource, ["Patterns", "patterns"], id, "group", "child", ctx)
return
if kind == "ParallelPatternGroup":
_visit_parallel_array(resource, "Patterns", id, "parallel", "child", ctx)
return
if kind == "MovementPattern":
var child: Variant = _get_prop(resource, "shootingPattern")
if child is Resource:
_visit_resource(child, id, "embedded", "shootingPattern", ctx)
return
_process_common_links(resource, id, ctx)
func _process_common_links(resource: Resource, id: String, ctx: Dictionary) -> void:
for property_name in ["BulletScript3D", "BulletScript", "Script", "shootingPattern"]:
var linked: Variant = _get_prop(resource, property_name)
if linked is Resource:
_visit_resource(linked, id, "reference", property_name, ctx)
# Generic fallback for pattern arrays in custom pattern resources.
for array_name in ["Patterns", "patterns"]:
var values := _get_array_prop(resource, array_name)
if values.size() > 0:
_visit_ordered_array_fallback(resource, [array_name], id, "sequence", "pattern", ctx)
return
func _visit_ordered_array(owner: Resource, property_name: String, parent_id: String, first_edge_type: String, label_prefix: String, ctx: Dictionary) -> void:
var values: Array = _get_array_prop(owner, property_name)
var prev := parent_id
for i in values.size():
var item = values[i]
if not (item is Resource):
continue
var edge_type := first_edge_type if prev == parent_id else "sequence"
var label := "%s[%d]" % [label_prefix, i]
prev = _visit_resource(item, prev, edge_type, label, ctx)
func _visit_ordered_array_fallback(owner: Resource, property_names: Array, parent_id: String, first_edge_type: String, label_prefix: String, ctx: Dictionary) -> void:
for property_name in property_names:
var values: Array = _get_array_prop(owner, property_name)
if values.size() == 0:
continue
var prev := parent_id
for i in values.size():
var item = values[i]
if not (item is Resource):
continue
var edge_type := first_edge_type if prev == parent_id else "sequence"
var label := "%s[%d]" % [label_prefix, i]
prev = _visit_resource(item, prev, edge_type, label, ctx)
return
func _visit_parallel_array(owner: Resource, property_name: String, parent_id: String, edge_type: String, label_prefix: String, ctx: Dictionary) -> void:
var values: Array = _get_array_prop(owner, property_name)
for i in values.size():
var item = values[i]
if not (item is Resource):
continue
var label := "%s[%d]" % [label_prefix, i]
_visit_resource(item, parent_id, edge_type, label, ctx)
func _get_array_prop(resource: Resource, property_name: String) -> Array:
var value = _get_prop(resource, property_name)
return value if value is Array else []
func _get_prop(resource: Resource, property_name: String):
if resource == null:
return null
if not _has_property(resource, property_name):
return null
if resource.has_method("get"):
return resource.get(property_name)
return null
func _has_property(resource: Object, property_name: String) -> bool:
for property_data in resource.get_property_list():
if property_data is Dictionary and property_data.get("name", "") == property_name:
return true
return false
func _make_node_id(resource: Resource) -> String:
if resource.resource_path != "":
return resource.resource_path
return "%s:%s" % [_get_kind(resource), str(resource.get_instance_id())]
func _make_title(resource: Resource, label: String) -> String:
var kind := _get_kind(resource)
# Prefer explicit Title property if present
var title_prop = _get_prop(resource, "Title")
if title_prop != null and str(title_prop) != "":
return "%s" % str(title_prop)
if kind == "BossScript":
var boss_name = _get_prop(resource, "BossName")
if boss_name != null and str(boss_name) != "":
return "BossScript: %s" % str(boss_name)
if kind == "BossPhase":
var phase_name = _get_prop(resource, "PhaseName")
if phase_name != null and str(phase_name) != "":
return "BossPhase: %s" % str(phase_name)
if label != "" and label != "root":
return "%s (%s)" % [kind, label]
return kind
func _make_subtitle(resource: Resource) -> String:
# Prefer Description property when available
var desc = _get_prop(resource, "Description")
if desc != null and str(desc) != "":
return str(desc)
if resource.resource_path != "":
return resource.resource_path
return "subresource"
func _get_kind(resource: Resource) -> String:
var script: Script = resource.get_script()
if script == null:
return resource.get_class()
var path := script.resource_path
if path.ends_with("BossScript.cs"):
return "BossScript"
if path.ends_with("BossPhase.cs"):
return "BossPhase"
if path.ends_with("BulletScript3D.cs"):
return "BulletScript3D"
if path.ends_with("BulletScript.cs"):
return "BulletScript"
if path.ends_with("PatternGroup.cs"):
return "PatternGroup"
if path.ends_with("ParallelPatternGroup.cs"):
return "ParallelPatternGroup"
if path.ends_with("MovementPattern.cs"):
return "MovementPattern"
if path.ends_with("AttackPattern.cs"):
return "AttackPattern"
if path.contains("/Scripts/AttackPatterns/"):
return path.get_file().trim_suffix(".cs")
var file_name := path.get_file().trim_suffix(".cs")
return file_name if file_name != "" else resource.get_class()

View file

@ -0,0 +1 @@
uid://dkea0407rg2ar

View file

@ -0,0 +1,7 @@
[plugin]
name="Bullet Script Graph Editor"
description="Graph editor for BossScript and BulletScript resources."
author="GitHub Copilot"
version="0.1.0"
script="plugin.gd"

View file

@ -0,0 +1,32 @@
@tool
extends EditorPlugin
const GraphEditorDockScene := preload("res://addons/bullet_script_graph_editor/ui/graph_editor_dock.tscn")
const InspectorPluginScript := preload("res://addons/bullet_script_graph_editor/inspector_plugin.gd")
var _dock_instance: PanelContainer
var _inspector_plugin: EditorInspectorPlugin
func _enter_tree() -> void:
_dock_instance = GraphEditorDockScene.instantiate()
_dock_instance.setup(get_editor_interface())
add_control_to_bottom_panel(_dock_instance, "Bullet Graph")
_inspector_plugin = InspectorPluginScript.new()
_inspector_plugin.setup(self)
add_inspector_plugin(_inspector_plugin)
func _exit_tree() -> void:
if _inspector_plugin:
remove_inspector_plugin(_inspector_plugin)
_inspector_plugin = null
if _dock_instance:
remove_control_from_bottom_panel(_dock_instance)
_dock_instance.queue_free()
_dock_instance = null
func open_resource_in_graph(resource: Resource) -> void:
if _dock_instance == null:
return
_dock_instance.open_resource(resource)

View file

@ -0,0 +1 @@
uid://q2ctk5gbyvve

View file

@ -0,0 +1,499 @@
@tool
extends PanelContainer
const GraphAdapterScript := preload("res://addons/bullet_script_graph_editor/model/graph_adapter.gd")
const META_LAYOUT_KEY := "_bullet_graph_layout_v1"
var _editor_interface: EditorInterface
var _graph_adapter: RefCounted
var _current_resource: Resource
var _path_edit: LineEdit
var _status_label: Label
var _graph_edit: GraphEdit
var _file_dialog: FileDialog
var _open_selected_button: Button
var _inspect_selected_button: Button
var _selected_node_resource: Resource
var _wait_checkbox: CheckBox
func setup(editor_interface: EditorInterface) -> void:
_editor_interface = editor_interface
func _ready() -> void:
_graph_adapter = GraphAdapterScript.new()
_build_ui()
func is_supported_resource(resource: Resource) -> bool:
if _graph_adapter == null:
return false
return _graph_adapter.is_supported_resource(resource)
func open_resource(resource: Resource) -> void:
if resource == null:
_set_status("No resource selected.", true)
return
if not is_supported_resource(resource):
_set_status("Unsupported resource: %s" % resource.resource_path, true)
return
# Build the graph first to detect whether any nodes will be generated (diagnostic)
var built_graph: Dictionary = _graph_adapter.build_graph(resource)
var built_nodes: Dictionary = built_graph.get("nodes", {})
_current_resource = resource
_path_edit.text = resource.resource_path
_render_graph()
if built_nodes.size() == 0:
_set_status("Opened: %s (no nodes detected)" % _display_path(resource), true)
else:
_set_status("Opened: %s" % _display_path(resource), false)
func _build_ui() -> void:
var root := VBoxContainer.new()
root.size_flags_horizontal = Control.SIZE_EXPAND_FILL
root.size_flags_vertical = Control.SIZE_EXPAND_FILL
add_child(root)
var toolbar := HBoxContainer.new()
root.add_child(toolbar)
var open_button := Button.new()
open_button.text = "Open Resource"
open_button.pressed.connect(_on_open_pressed)
toolbar.add_child(open_button)
var open_inspected_button := Button.new()
open_inspected_button.text = "Open Inspected"
open_inspected_button.pressed.connect(_on_open_inspected_pressed)
toolbar.add_child(open_inspected_button)
var save_layout_button := Button.new()
save_layout_button.text = "Save Layout"
save_layout_button.pressed.connect(_save_layout_metadata)
toolbar.add_child(save_layout_button)
_open_selected_button = Button.new()
_open_selected_button.text = "Open Selected Node"
_open_selected_button.disabled = true
_open_selected_button.pressed.connect(_on_open_selected_node_pressed)
toolbar.add_child(_open_selected_button)
_inspect_selected_button = Button.new()
_inspect_selected_button.text = "Inspect Selected Node"
_inspect_selected_button.disabled = true
_inspect_selected_button.pressed.connect(_on_inspect_selected_node_pressed)
toolbar.add_child(_inspect_selected_button)
_path_edit = LineEdit.new()
_path_edit.placeholder_text = "res://Resources/..."
_path_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_path_edit.text_submitted.connect(_on_path_submitted)
toolbar.add_child(_path_edit)
_status_label = Label.new()
_status_label.text = "Open a BossScript/BossPhase/BulletScript to render graph."
root.add_child(_status_label)
_wait_checkbox = CheckBox.new()
_wait_checkbox.text = "Wait For Completion (selected pattern)"
_wait_checkbox.visible = false
_wait_checkbox.toggled.connect(_on_wait_for_completion_toggled)
root.add_child(_wait_checkbox)
_graph_edit = GraphEdit.new()
_graph_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_graph_edit.size_flags_vertical = Control.SIZE_EXPAND_FILL
_graph_edit.right_disconnects = true
_graph_edit.end_node_move.connect(_save_layout_metadata)
_graph_edit.node_selected.connect(_on_graph_node_selected)
root.add_child(_graph_edit)
_file_dialog = FileDialog.new()
_file_dialog.access = FileDialog.ACCESS_RESOURCES
_file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
_file_dialog.add_filter("*.tres; Godot Resource")
_file_dialog.add_filter("*.res; Godot Resource")
_file_dialog.file_selected.connect(_on_file_selected)
add_child(_file_dialog)
func _on_open_pressed() -> void:
_file_dialog.popup_centered_ratio(0.8)
func _on_open_inspected_pressed() -> void:
if _editor_interface == null:
_set_status("Editor interface is not available.", true)
return
var inspector := _editor_interface.get_inspector()
if inspector == null:
_set_status("Inspector is not available.", true)
return
var edited_object := inspector.get_edited_object()
if not (edited_object is Resource):
_set_status("Inspector object is not a supported resource.", true)
return
open_resource(edited_object)
func _on_file_selected(path: String) -> void:
var loaded = ResourceLoader.load(path)
if not (loaded is Resource):
_set_status("Failed to load resource: %s" % path, true)
return
open_resource(loaded)
func _on_path_submitted(path: String) -> void:
if path.strip_edges() == "":
return
_on_file_selected(path)
func _clear_graph() -> void:
for child in _graph_edit.get_children():
if child is GraphNode:
child.queue_free()
func _render_graph() -> void:
if _current_resource == null:
return
_clear_graph()
var graph: Dictionary = _graph_adapter.build_graph(_current_resource)
var nodes: Dictionary = graph.get("nodes", {})
var edges: Array = graph.get("edges", [])
var node_name_by_id := {}
_selected_node_resource = null
_open_selected_button.disabled = true
_inspect_selected_button.disabled = true
_wait_checkbox.visible = false
var layout := _load_layout_metadata()
var auto_index := 0
for node_id in nodes.keys():
var node_data: Dictionary = nodes[node_id]
var graph_node := GraphNode.new()
graph_node.name = _node_name_from_id(node_id)
graph_node.title = node_data.get("title", "Node")
graph_node.resizable = false
graph_node.custom_minimum_size = Vector2(240, 84)
graph_node.set_meta("graph_id", node_id)
var info := VBoxContainer.new()
var subtitle := Label.new()
subtitle.text = node_data.get("subtitle", "")
subtitle.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
subtitle.clip_text = true
subtitle.size_flags_horizontal = Control.SIZE_EXPAND_FILL
info.add_child(subtitle)
# Collapsible details handled below when there are actual details to show
# Determine if there are details to show (description or exported properties or patterns add button)
var potential_details := []
var node_resource: Resource = node_data.get("resource", null)
if node_resource != null:
graph_node.set_meta("resource", node_resource)
if node_resource != null:
var desc_text := str(node_data.get("subtitle", ""))
if desc_text != "":
potential_details.append("desc")
# check for exported properties (editor usage)
for property_data in node_resource.get_property_list():
if property_data is Dictionary:
var pname := property_data.get("name", "")
if pname == "" or pname.begins_with("_") or pname == "Patterns":
continue
var usage := int(property_data.get("usage", 0))
if usage & PROPERTY_USAGE_EDITOR:
potential_details.append(pname)
# Add Pattern button is considered a detail for BulletScript3D
if node_data.get("kind", "") == "BulletScript3D":
potential_details.append("add_pattern")
var has_details := potential_details.size() > 0
var details: VBoxContainer = null
var toggle: Button = null
if has_details:
details = VBoxContainer.new()
details.name = "details"
details.visible = false
details.size_flags_horizontal = Control.SIZE_EXPAND_FILL
info.add_child(details)
# Toggle button to expand/collapse
toggle = Button.new()
toggle.text = "Details"
toggle.toggle_mode = true
# default is not pressed; use setter to avoid assigning to a constant
toggle.set_pressed(false)
# bind graph_node and details so we can resize the node when toggling
toggle.pressed.connect(Callable(self, "_on_toggle_details_pressed").bind(graph_node, details))
info.add_child(toggle)
graph_node.add_child(info)
# Add quick action buttons on each node for convenience
if node_resource != null:
var actions := HBoxContainer.new()
actions.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var btn_open := Button.new()
btn_open.text = "Open"
btn_open.pressed.connect(Callable(self, "_on_node_open_pressed").bind(graph_node))
actions.add_child(btn_open)
var btn_inspect := Button.new()
btn_inspect.text = "Inspect"
btn_inspect.pressed.connect(Callable(self, "_on_node_inspect_pressed").bind(graph_node))
actions.add_child(btn_inspect)
info.add_child(actions)
graph_node.set_slot(0, true, 0, Color(0.75, 0.75, 0.9), true, 0, Color(0.75, 0.9, 0.75))
if layout.has(node_id):
graph_node.position_offset = layout[node_id]
else:
graph_node.position_offset = Vector2(60 + (auto_index % 5) * 320, 36 + int(auto_index / 5) * 160)
auto_index += 1
_graph_edit.add_child(graph_node)
node_name_by_id[node_id] = graph_node.name
for edge_data in edges:
var from_id: String = edge_data.get("from", "")
var to_id: String = edge_data.get("to", "")
if not (node_name_by_id.has(from_id) and node_name_by_id.has(to_id)):
continue
var from_name: StringName = StringName(node_name_by_id[from_id])
var to_name: StringName = StringName(node_name_by_id[to_id])
if from_name == to_name:
continue
if _graph_edit.is_node_connected(from_name, 0, to_name, 0):
continue
_graph_edit.connect_node(from_name, 0, to_name, 0)
func _load_layout_metadata() -> Dictionary:
if _current_resource == null:
return {}
if not _current_resource.has_meta(META_LAYOUT_KEY):
return {}
var data = _current_resource.get_meta(META_LAYOUT_KEY, {})
return data if data is Dictionary else {}
func _save_layout_metadata() -> void:
if _current_resource == null:
return
var layout := {}
for child in _graph_edit.get_children():
if not (child is GraphNode):
continue
var graph_id: String = child.get_meta("graph_id", "")
if graph_id == "":
continue
layout[graph_id] = child.position_offset
_current_resource.set_meta(META_LAYOUT_KEY, layout)
if _current_resource.resource_path != "":
var err := ResourceSaver.save(_current_resource, _current_resource.resource_path)
if err != OK:
_set_status("Failed to save graph layout metadata.", true)
return
_set_status("Saved graph layout metadata.", false)
func _node_name_from_id(node_id: String) -> String:
return "n_%s" % str(abs(node_id.hash()))
func _display_path(resource: Resource) -> String:
if resource == null:
return ""
if resource.resource_path != "":
return resource.resource_path
return "<subresource>"
func _set_status(message: String, is_error: bool) -> void:
if _status_label == null:
return
_status_label.text = message
_status_label.modulate = Color(1, 0.55, 0.55) if is_error else Color(1, 1, 1)
func _on_graph_node_selected(node_arg) -> void:
# GraphEdit may send different types (StringName, NodePath, or the node object itself).
var graph_node: GraphNode = null
if typeof(node_arg) == TYPE_OBJECT and node_arg is GraphNode:
graph_node = node_arg
else:
# Try to resolve by path or name
var resolved := _graph_edit.get_node_or_null(node_arg)
if resolved is GraphNode:
graph_node = resolved
if not (graph_node is GraphNode):
_selected_node_resource = null
_open_selected_button.disabled = true
_inspect_selected_button.disabled = true
_wait_checkbox.visible = false
return
_selected_node_resource = _get_graph_node_resource(graph_node)
var enabled := _selected_node_resource != null
_open_selected_button.disabled = not enabled
_inspect_selected_button.disabled = not enabled
if enabled and _resource_has_property(_selected_node_resource, "WaitForCompletion"):
_wait_checkbox.visible = true
_wait_checkbox.set_pressed_no_signal(bool(_selected_node_resource.get("WaitForCompletion")))
else:
_wait_checkbox.visible = false
_set_status("Selected: %s" % _display_path(_selected_node_resource), false)
func _on_node_open_pressed(graph_node: GraphNode) -> void:
if not (graph_node is GraphNode):
return
var res := _get_graph_node_resource(graph_node)
if res == null:
_set_status("Node has no resource to open.", true)
return
open_resource(res)
func _on_toggle_details_pressed(graph_node: GraphNode, details: VBoxContainer) -> void:
if details == null or graph_node == null:
return
details.visible = not details.visible
if details.visible:
var extra := details.get_combined_minimum_size().y
graph_node.custom_minimum_size = Vector2(240, 84 + extra)
else:
graph_node.custom_minimum_size = Vector2(240, 84)
func _on_prop_text_changed(resource: Resource, prop_name: String, new_text: String) -> void:
if resource == null:
return
resource.set(prop_name, new_text)
if resource.resource_path != "":
ResourceSaver.save(resource, resource.resource_path)
_set_status("Updated %s" % prop_name, false)
func _on_prop_float_changed(resource: Resource, prop_name: String, value: float) -> void:
if resource == null:
return
resource.set(prop_name, value)
if resource.resource_path != "":
ResourceSaver.save(resource, resource.resource_path)
_set_status("Updated %s" % prop_name, false)
func _on_prop_bool_changed(resource: Resource, prop_name: String, pressed: bool) -> void:
if resource == null:
return
resource.set(prop_name, pressed)
if resource.resource_path != "":
ResourceSaver.save(resource, resource.resource_path)
_set_status("Updated %s" % prop_name, false)
func _on_add_shooting_pattern_pressed(resource: Resource) -> void:
if resource == null:
_set_status("No resource to add pattern to.", true)
return
# Try to instantiate a ShootingPattern3D (GlobalClass C# type)
var new_pattern = null
# attempt to instantiate ShootingPattern3D; fall back to generic Resource if not available
# Instantiating a C# AttackPattern from GDScript can be fragile; create a generic Resource for now.
new_pattern = Resource.new()
# Append to Patterns array if present
if _resource_has_property(resource, "Patterns"):
var arr := resource.get("Patterns")
if not (arr is Array):
arr = []
arr.append(new_pattern)
resource.set("Patterns", arr)
if resource.resource_path != "":
ResourceSaver.save(resource, resource.resource_path)
_set_status("Added new pattern.", false)
_render_graph()
else:
_set_status("Resource has no Patterns array.", true)
func _on_node_inspect_pressed(graph_node: GraphNode) -> void:
if not (graph_node is GraphNode):
return
var res := _get_graph_node_resource(graph_node)
if res == null:
_set_status("Node has no resource to inspect.", true)
return
if _editor_interface == null:
_set_status("Editor interface not available.", true)
return
# Defer opening the inspector to avoid any re-entrancy or script-parse conflicts
call_deferred("_deferred_edit_resource", res)
# Immediate status update; the actual inspector open happens shortly after
_set_status("Inspecting (deferred): %s" % _display_path(res), false)
func _deferred_edit_resource(res: Resource) -> void:
if _editor_interface == null or res == null:
return
_editor_interface.edit_resource(res)
_set_status("Inspecting: %s" % _display_path(res), false)
func _on_open_selected_node_pressed() -> void:
if _selected_node_resource == null:
_set_status("No node resource selected.", true)
return
open_resource(_selected_node_resource)
func _on_inspect_selected_node_pressed() -> void:
if _selected_node_resource == null:
_set_status("No node resource selected.", true)
return
if _editor_interface == null:
_set_status("Editor interface is not available.", true)
return
_editor_interface.edit_resource(_selected_node_resource)
_set_status("Inspected selected node resource.", false)
func _unique_strings(values: Array) -> PackedStringArray:
var seen := {}
var result := PackedStringArray()
for value in values:
var text := str(value)
if text == "" or seen.has(text):
continue
seen[text] = true
result.append(text)
if result.is_empty():
result.append("-")
return result
func _on_wait_for_completion_toggled(pressed: bool) -> void:
if _selected_node_resource == null:
return
if not _resource_has_property(_selected_node_resource, "WaitForCompletion"):
return
_selected_node_resource.set("WaitForCompletion", pressed)
if _selected_node_resource.resource_path != "":
var err := ResourceSaver.save(_selected_node_resource, _selected_node_resource.resource_path)
if err != OK:
_set_status("Failed to save WaitForCompletion.", true)
return
_set_status("Updated WaitForCompletion on selected pattern.", false)
_render_graph()
func _resource_has_property(resource: Object, property_name: String) -> bool:
for property_data in resource.get_property_list():
if property_data is Dictionary and property_data.get("name", "") == property_name:
return true
return false
func _get_graph_node_resource(graph_node: GraphNode) -> Resource:
if graph_node == null:
return null
if not graph_node.has_meta("resource"):
return null
var value = graph_node.get_meta("resource")
return value if value is Resource else null

View file

@ -0,0 +1 @@
uid://duc4xqfaiyjq5

View file

@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://addons/bullet_script_graph_editor/ui/graph_editor_dock.gd" id="1_5n6ty"]
[node name="GraphEditorDock" type="PanelContainer"]
script = ExtResource("1_5n6ty")