Files
tekton/addons/godot_ai/utils/class_introspection.gd
T

240 lines
8.1 KiB
GDScript

@tool
extends RefCounted
## Builds stable, JSON-safe metadata for any class registered in ClassDB.
const VariantSerializer := preload("res://addons/godot_ai/utils/variant_serializer.gd")
const DEFAULT_SECTIONS := ["properties", "methods", "signals", "enums", "constants"]
const KNOWN_SECTIONS := ["properties", "methods", "signals", "enums", "constants", "inheritors"]
const MAX_DEFAULT_ITEMS := 100
static func build(type_name: String, options: Dictionary = {}) -> Dictionary:
var sections := _sections(options.get("sections", DEFAULT_SECTIONS))
var include_inherited := bool(options.get("include_inherited", false))
var include_inheritors := bool(options.get("include_inheritors", false))
var offset := max(0, int(options.get("offset", 0)))
var limit := int(options.get("limit", MAX_DEFAULT_ITEMS))
if limit < 0:
limit = MAX_DEFAULT_ITEMS
var can_instantiate := ClassDB.can_instantiate(type_name)
var data := {
"class_name": type_name,
"engine_version": Engine.get_version_info().get("string", ""),
"parent_class": str(ClassDB.get_parent_class(type_name)),
"inheritance_chain": _inheritance_chain(type_name),
"can_instantiate": can_instantiate,
"is_singleton": Engine.has_singleton(type_name),
"include_inherited": include_inherited,
"offset": offset,
"limit": limit,
}
if include_inheritors or sections.has("inheritors"):
_add_paged(data, "inheritor", "inheritors", _inheritors(type_name, false), offset, limit)
_add_paged(
data,
"concrete_inheritor",
"concrete_inheritors",
_inheritors(type_name, true),
offset,
limit
)
if sections.has("properties"):
_add_paged(data, "property", "properties", _properties(type_name, include_inherited), offset, limit)
if sections.has("methods"):
_add_paged(data, "method", "methods", _methods(type_name, include_inherited), offset, limit)
if sections.has("signals"):
_add_paged(data, "signal", "signals", _signals(type_name, include_inherited), offset, limit)
if sections.has("enums"):
_add_paged(data, "enum", "enums", _enums(type_name, include_inherited), offset, limit)
if sections.has("constants"):
_add_paged(
data,
"constant",
"constants",
_unscoped_constants(type_name, include_inherited),
offset,
limit
)
return data
static func validate_sections(raw_sections: Variant) -> Dictionary:
var sections := _sections(raw_sections)
var invalid: Array[String] = []
for section in sections:
if not KNOWN_SECTIONS.has(section):
invalid.append(section)
return {"sections": sections, "invalid": invalid}
static func _inheritance_chain(type_name: String) -> Array[String]:
var chain: Array[String] = []
var current := type_name
while not current.is_empty():
chain.append(current)
current = str(ClassDB.get_parent_class(current))
return chain
static func _sections(raw_sections: Variant) -> Array[String]:
var result: Array[String] = []
var values: Array = []
if raw_sections is String:
values = raw_sections.split(",", false)
elif raw_sections is Array:
values = raw_sections
else:
values = DEFAULT_SECTIONS
for raw_section in values:
var section := str(raw_section).strip_edges().to_lower()
if not section.is_empty() and not result.has(section):
result.append(section)
if result.is_empty():
result.assign(DEFAULT_SECTIONS)
return result
static func _add_paged(
data: Dictionary,
singular: String,
key: String,
items: Array,
offset: int,
limit: int
) -> void:
var end := items.size() if limit == 0 else min(items.size(), offset + limit)
var page: Array = []
if offset < items.size():
page = items.slice(offset, end)
data[key] = page
data["%s_count" % singular] = items.size()
data["%s_returned_count" % singular] = page.size()
static func _inheritors(type_name: String, concrete_only: bool) -> Array[String]:
var result: Array[String] = []
for inheritor in ClassDB.get_inheriters_from_class(type_name):
var inheritor_name := str(inheritor)
if concrete_only and not ClassDB.can_instantiate(inheritor_name):
continue
result.append(inheritor_name)
result.sort()
return result
static func _properties(type_name: String, include_inherited: bool) -> Array[Dictionary]:
var result: Array[Dictionary] = []
for raw_prop in ClassDB.class_get_property_list(type_name, not include_inherited):
var prop: Dictionary = raw_prop
var usage := int(prop.get("usage", 0))
if not (usage & PROPERTY_USAGE_EDITOR):
continue
var prop_name := str(prop.get("name", ""))
result.append({
"name": prop_name,
"type": type_string(int(prop.get("type", TYPE_NIL))),
"class_name": str(prop.get("class_name", "")),
"hint": int(prop.get("hint", PROPERTY_HINT_NONE)),
"hint_string": str(prop.get("hint_string", "")),
"usage": usage,
"default": VariantSerializer.serialize(
ClassDB.class_get_property_default_value(type_name, prop_name)
),
})
result.sort_custom(func(a, b): return a.name < b.name)
return result
static func _methods(type_name: String, include_inherited: bool) -> Array[Dictionary]:
var result: Array[Dictionary] = []
for raw_method in ClassDB.class_get_method_list(type_name, not include_inherited):
var method: Dictionary = raw_method
var args: Array[Dictionary] = []
for raw_arg in method.get("args", []):
args.append(_argument_info(raw_arg))
var defaults: Array = []
for value in method.get("default_args", []):
defaults.append(VariantSerializer.serialize(value))
result.append({
"name": str(method.get("name", "")),
"arguments": args,
"default_arguments": defaults,
"return": _argument_info(method.get("return", {})),
"flags": int(method.get("flags", 0)),
})
result.sort_custom(func(a, b): return a.name < b.name)
return result
static func _signals(type_name: String, include_inherited: bool) -> Array[Dictionary]:
var result: Array[Dictionary] = []
for raw_signal in ClassDB.class_get_signal_list(type_name, not include_inherited):
var signal_info: Dictionary = raw_signal
var args: Array[Dictionary] = []
for raw_arg in signal_info.get("args", []):
args.append(_argument_info(raw_arg))
var defaults: Array = []
for value in signal_info.get("default_args", []):
defaults.append(VariantSerializer.serialize(value))
result.append({
"name": str(signal_info.get("name", "")),
"arguments": args,
"default_arguments": defaults,
"flags": int(signal_info.get("flags", 0)),
})
result.sort_custom(func(a, b): return a.name < b.name)
return result
static func _argument_info(raw_info: Variant) -> Dictionary:
var info: Dictionary = raw_info if raw_info is Dictionary else {}
return {
"name": str(info.get("name", "")),
"type": type_string(int(info.get("type", TYPE_NIL))),
"class_name": str(info.get("class_name", "")),
"hint": int(info.get("hint", PROPERTY_HINT_NONE)),
"hint_string": str(info.get("hint_string", "")),
"usage": int(info.get("usage", 0)),
}
static func _enums(type_name: String, include_inherited: bool) -> Array[Dictionary]:
var result: Array[Dictionary] = []
var enum_names: Array[String] = []
for enum_name in ClassDB.class_get_enum_list(type_name, not include_inherited):
enum_names.append(str(enum_name))
enum_names.sort()
for enum_name in enum_names:
var values: Array[Dictionary] = []
for constant_name in ClassDB.class_get_enum_constants(type_name, enum_name, not include_inherited):
values.append({
"name": str(constant_name),
"value": ClassDB.class_get_integer_constant(type_name, constant_name),
})
values.sort_custom(func(a, b): return a.name < b.name)
result.append({
"name": enum_name,
"is_bitfield": ClassDB.is_class_enum_bitfield(type_name, enum_name, not include_inherited),
"values": values,
})
return result
static func _unscoped_constants(type_name: String, include_inherited: bool) -> Array[Dictionary]:
var result: Array[Dictionary] = []
for constant_name in ClassDB.class_get_integer_constant_list(type_name, not include_inherited):
var enum_name := str(
ClassDB.class_get_integer_constant_enum(type_name, constant_name, not include_inherited)
)
if not enum_name.is_empty():
continue
result.append({
"name": str(constant_name),
"value": ClassDB.class_get_integer_constant(type_name, constant_name),
})
result.sort_custom(func(a, b): return a.name < b.name)
return result