cirnogodot/addons/bullet_script_graph_editor/model/graph_adapter.gd
2026-03-01 23:14:33 +01:00

281 lines
9.1 KiB
GDScript

@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()