From 3b4214e13bb8387987ac69685ac501b38fa8f455 Mon Sep 17 00:00:00 2001 From: JustDooooIt <844730658@qq.com> Date: Wed, 2 Sep 2026 16:11:51 +0800 Subject: [PATCH 1/4] fix: update napi_match_expr to only check for BigInt type --- generator/builtin_classes_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generator/builtin_classes_generator.py b/generator/builtin_classes_generator.py index f478c51..2f73f6f 100644 --- a/generator/builtin_classes_generator.py +++ b/generator/builtin_classes_generator.py @@ -21,7 +21,7 @@ def napi_match_expr(type_name, index): value = f"info[{index}]" if type_name == 'int': - return f"({value}.IsNumber() || {value}.IsBigInt())" + return f"({value}.IsBigInt())" if type_name == 'float': return f"{value}.IsNumber()" if type_name == 'bool': From 1775310ac55e05a0b94537e442368f71d2454e71 Mon Sep 17 00:00:00 2001 From: JustDooooIt <844730658@qq.com> Date: Sun, 6 Sep 2026 22:11:42 +0800 Subject: [PATCH 2/4] feat: add TypeScript interface resource support and related functionality --- example/project.godot | 1 - .../scripts/tests/runtime_integration_test.ts | 10 + example/scripts/tests/tests_runner.gd | 8 + .../script/typescript_interface_resource.h | 49 +++++ include/script/typescript_script.h | 1 + src/register_runtime_types.cpp | 2 + src/script/script_instance.cpp | 11 ++ src/script/typescript_interface_resource.cpp | 182 ++++++++++++++++++ src/script/typescript_script.cpp | 101 +++++++++- 9 files changed, 358 insertions(+), 7 deletions(-) create mode 100644 include/script/typescript_interface_resource.h create mode 100644 src/script/typescript_interface_resource.cpp diff --git a/example/project.godot b/example/project.godot index 280cbc7..ec8298b 100644 --- a/example/project.godot +++ b/example/project.godot @@ -37,5 +37,4 @@ paths=["res://addons/gode/binary/gode.gdextension"] [rendering] rendering_device/driver.windows="d3d12" -renderer/rendering_method="forward_plus" textures/vram_compression/import_etc2_astc=true diff --git a/example/scripts/tests/runtime_integration_test.ts b/example/scripts/tests/runtime_integration_test.ts index f913adb..03d01ed 100644 --- a/example/scripts/tests/runtime_integration_test.ts +++ b/example/scripts/tests/runtime_integration_test.ts @@ -35,6 +35,11 @@ const PROPERTY_HINT_RESOURCE_TYPE = 17; const PROPERTY_HINT_NODE_TYPE = 34; const RUNTIME_ARRAY_RESOURCE_SCRIPT_PATH = "res://scripts/tests/runtime_array_resource.ts"; +interface RuntimeExportInterfaceArrayElement { + label: string; + amount: number; +} + function assert(condition: boolean, message: string): void { if (!condition) { throw new Error(message); @@ -163,6 +168,9 @@ class RuntimeIntegrationTest extends RuntimeBaseModule.RuntimeIntegrationBase { @Export() editor_namespace_alias_resource_array: RuntimeExportTypes.RuntimeImportedResourceArray = []; + @Export() + editor_interface_array: RuntimeExportInterfaceArrayElement[] = []; + @Export() editor_mixed_union: "automatic" | number = "automatic"; @@ -393,6 +401,7 @@ class RuntimeIntegrationTest extends RuntimeBaseModule.RuntimeIntegrationBase { "editor_imported_generic_external_resource_array", "editor_imported_generic_resource_dictionary", "editor_namespace_alias_resource_array", + "editor_interface_array", "editor_mixed_union", "editor_mixed_object_union", "editor_explicit_hint_enum", @@ -443,6 +452,7 @@ class RuntimeIntegrationTest extends RuntimeBaseModule.RuntimeIntegrationBase { assertArrayExportMetadata("editor_imported_generic_external_resource_array", `${VARIANT_TYPE_OBJECT}/${PROPERTY_HINT_RESOURCE_TYPE}:RuntimeExternalResource`); assertDictionaryExportMetadata("editor_imported_generic_resource_dictionary", `${VARIANT_TYPE_STRING}:;${VARIANT_TYPE_OBJECT}/${PROPERTY_HINT_RESOURCE_TYPE}:RuntimeArrayResource`); assertArrayExportMetadata("editor_namespace_alias_resource_array", `${VARIANT_TYPE_OBJECT}/${PROPERTY_HINT_RESOURCE_TYPE}:RuntimeArrayResource`); + assertArrayExportMetadata("editor_interface_array", `${VARIANT_TYPE_OBJECT}/${PROPERTY_HINT_RESOURCE_TYPE}:TypeScriptInterfaceResource`); assertArrayExportMetadata("static_number_array", `${VARIANT_TYPE_FLOAT}:`); assertArrayExportMetadata("static_custom_resource_array", `${VARIANT_TYPE_OBJECT}/${PROPERTY_HINT_RESOURCE_TYPE}:RuntimeArrayResource`); assertDictionaryExportMetadata("static_record_dictionary", `${VARIANT_TYPE_STRING}:;${VARIANT_TYPE_FLOAT}:`); diff --git a/example/scripts/tests/tests_runner.gd b/example/scripts/tests/tests_runner.gd index ba7c516..58deab4 100644 --- a/example/scripts/tests/tests_runner.gd +++ b/example/scripts/tests/tests_runner.gd @@ -61,6 +61,14 @@ func _run_next() -> void: if current_test.get("editor_record_dictionary") != {"from_gdscript": 8}: _fail("RuntimeIntegrationTest typed string-key Dictionary did not round-trip through ScriptInstance") return + var interface_element: Object = ClassDB.instantiate("TypeScriptInterfaceResource") + current_test.set("editor_interface_array", [interface_element]) + var interface_fields: Array[String] = [] + for property in interface_element.get_property_list(): + interface_fields.append(str(property.name)) + if not interface_fields.has("label") or not interface_fields.has("amount"): + _fail("RuntimeIntegrationTest interface array element did not expose interface fields") + return var int_key_dictionary := {} int_key_dictionary[7] = "seven" current_test.set("editor_int_key_map", int_key_dictionary) diff --git a/include/script/typescript_interface_resource.h b/include/script/typescript_interface_resource.h new file mode 100644 index 0000000..6c6e6d9 --- /dev/null +++ b/include/script/typescript_interface_resource.h @@ -0,0 +1,49 @@ +#ifndef GODE_TYPESCRIPT_INTERFACE_RESOURCE_H +#define GODE_TYPESCRIPT_INTERFACE_RESOURCE_H + +#include +#include +#include +#include +#include + +namespace gode { + +class TypeScriptInterfaceResource : public godot::Resource { + GDCLASS(TypeScriptInterfaceResource, godot::Resource) + + struct Schema { + godot::StringName interface_name; + godot::HashMap> interfaces; + }; + + static godot::HashMap schemas; + + godot::StringName schema_id; + godot::Vector fields; + godot::HashMap nested_interfaces; + godot::HashMap nested_interface_arrays; + godot::HashMap values; + + void apply_schema(); + void configure_nested_value(const godot::StringName &p_name, const godot::Variant &p_value); + +protected: + static void _bind_methods(); + bool _set(const godot::StringName &p_name, const godot::Variant &p_value); + bool _get(const godot::StringName &p_name, godot::Variant &r_value) const; + void _get_property_list(godot::List *p_list) const; + +public: + static void register_schema( + const godot::StringName &p_schema_id, + const godot::StringName &p_interface_name, + const godot::HashMap> &p_interfaces); + void configure(const godot::StringName &p_schema_id); + int64_t get_interface_field_count() const { return fields.size(); } + godot::StringName get_interface_field_name(int64_t p_index) const; +}; + +} // namespace gode + +#endif // GODE_TYPESCRIPT_INTERFACE_RESOURCE_H diff --git a/include/script/typescript_script.h b/include/script/typescript_script.h index 754660e..e5643fa 100644 --- a/include/script/typescript_script.h +++ b/include/script/typescript_script.h @@ -41,6 +41,7 @@ class TypeScriptScript : public godot::ScriptExtension { mutable godot::HashMap signals; mutable godot::HashMap properties; mutable godot::Vector property_list; + mutable godot::HashMap interface_array_schemas; mutable godot::HashMap property_defaults; mutable godot::HashMap constants; mutable godot::HashMap member_lines; diff --git a/src/register_runtime_types.cpp b/src/register_runtime_types.cpp index 2023929..a4dc396 100644 --- a/src/register_runtime_types.cpp +++ b/src/register_runtime_types.cpp @@ -2,6 +2,7 @@ #include "runtime/gode_runtime_bridge.h" #include "runtime/node_runtime.h" #include "script/typescript_language.h" +#include "script/typescript_interface_resource.h" #include "script/typescript_loader.h" #include "script/typescript_saver.h" #include "script/typescript_script.h" @@ -22,6 +23,7 @@ void initialize_gode_runtime_module(godot::ModuleInitializationLevel p_level) { return; } GDREGISTER_CLASS(gode::TypeScriptScript); + GDREGISTER_CLASS(gode::TypeScriptInterfaceResource); GDREGISTER_CLASS(gode::TypeScriptLanguage); GDREGISTER_CLASS(gode::TypeScriptSaver); GDREGISTER_CLASS(gode::TypeScriptLoader); diff --git a/src/script/script_instance.cpp b/src/script/script_instance.cpp index b1e3edb..9b13206 100644 --- a/src/script/script_instance.cpp +++ b/src/script/script_instance.cpp @@ -2,6 +2,7 @@ #include "runtime/napi_error_utils.h" #include "runtime/node_runtime.h" #include "runtime/value_convert.h" +#include "script/typescript_interface_resource.h" #include "script/typescript_script.h" #include #include @@ -332,6 +333,16 @@ void ScriptInstance::reload(bool p_keep_state) { } bool ScriptInstance::set(const StringName &p_name, const Variant &p_value) { + if (script.is_valid() && script->interface_array_schemas.has(p_name) && p_value.get_type() == Variant::ARRAY) { + const StringName schema_id = script->interface_array_schemas[p_name]; + Array array = p_value; + for (int64_t i = 0; i < array.size(); i++) { + TypeScriptInterfaceResource *resource = Object::cast_to(array[i]); + if (resource) { + resource->configure(schema_id); + } + } + } if (placeholder) { placeholder_properties[p_name] = p_value; return true; diff --git a/src/script/typescript_interface_resource.cpp b/src/script/typescript_interface_resource.cpp new file mode 100644 index 0000000..cd166b8 --- /dev/null +++ b/src/script/typescript_interface_resource.cpp @@ -0,0 +1,182 @@ +#include "script/typescript_interface_resource.h" + +#include +#include + +#include + +using namespace godot; + +namespace gode { + +namespace { + +constexpr const char *SCHEMA_PROPERTY = "_gode_interface_schema"; + +static std::string trim_type_text(const std::string &value) { + size_t start = 0; + while (start < value.size() && std::isspace(static_cast(value[start]))) { + start++; + } + size_t end = value.size(); + while (end > start && std::isspace(static_cast(value[end - 1]))) { + end--; + } + return value.substr(start, end - start); +} + +static std::string interface_array_element(const StringName &class_name) { + std::string type = trim_type_text(String(class_name).utf8().get_data()); + if (type.size() > 2 && type.substr(type.size() - 2) == "[]") { + return trim_type_text(type.substr(0, type.size() - 2)); + } + if (type.rfind("Array<", 0) == 0 && type.back() == '>') { + return trim_type_text(type.substr(6, type.size() - 7)); + } + if (type.rfind("ReadonlyArray<", 0) == 0 && type.back() == '>') { + return trim_type_text(type.substr(14, type.size() - 15)); + } + return {}; +} + +} // namespace + +HashMap TypeScriptInterfaceResource::schemas; + +void TypeScriptInterfaceResource::_bind_methods() { +} + +void TypeScriptInterfaceResource::register_schema( + const StringName &p_schema_id, + const StringName &p_interface_name, + const HashMap> &p_interfaces) { + Schema schema; + schema.interface_name = p_interface_name; + schema.interfaces = p_interfaces; + schemas[p_schema_id] = schema; +} + +void TypeScriptInterfaceResource::configure(const StringName &p_schema_id) { + if (schema_id == p_schema_id && !fields.is_empty()) { + return; + } + schema_id = p_schema_id; + apply_schema(); + notify_property_list_changed(); +} + +void TypeScriptInterfaceResource::apply_schema() { + fields.clear(); + nested_interfaces.clear(); + nested_interface_arrays.clear(); + + const Schema *schema = schemas.getptr(schema_id); + if (!schema || !schema->interfaces.has(schema->interface_name)) { + return; + } + set_name(String(schema->interface_name)); + + for (const PropertyInfo &source_field : schema->interfaces[schema->interface_name]) { + PropertyInfo field = source_field; + const StringName structural_type = source_field.class_name; + if (!structural_type.is_empty() && schema->interfaces.has(structural_type)) { + nested_interfaces[field.name] = structural_type; + field.type = Variant::OBJECT; + field.hint = PROPERTY_HINT_RESOURCE_TYPE; + field.hint_string = get_class_static(); + field.class_name = get_class_static(); + } else if (field.type == Variant::ARRAY && !structural_type.is_empty()) { + const StringName element_type(interface_array_element(structural_type).c_str()); + if (!element_type.is_empty() && schema->interfaces.has(element_type)) { + nested_interface_arrays[field.name] = element_type; + field.hint = PROPERTY_HINT_ARRAY_TYPE; + field.hint_string = String::num_int64(Variant::OBJECT) + "/" + String::num_int64(PROPERTY_HINT_RESOURCE_TYPE) + ":" + String(get_class_static()); + field.class_name = StringName(); + } else { + field.class_name = StringName(); + } + } else if (field.type != Variant::OBJECT) { + field.class_name = StringName(); + } + fields.push_back(field); + } +} + +void TypeScriptInterfaceResource::configure_nested_value(const StringName &p_name, const Variant &p_value) { + const Schema *schema = schemas.getptr(schema_id); + if (!schema) { + return; + } + if (nested_interfaces.has(p_name)) { + TypeScriptInterfaceResource *resource = Object::cast_to(p_value); + if (resource) { + const StringName nested_schema_id(String(schema_id) + "::" + String(nested_interfaces[p_name])); + register_schema(nested_schema_id, nested_interfaces[p_name], schema->interfaces); + resource->configure(nested_schema_id); + } + return; + } + if (nested_interface_arrays.has(p_name) && p_value.get_type() == Variant::ARRAY) { + const StringName nested_name = nested_interface_arrays[p_name]; + const StringName nested_schema_id(String(schema_id) + "::" + String(nested_name)); + register_schema(nested_schema_id, nested_name, schema->interfaces); + Array array = p_value; + for (int64_t i = 0; i < array.size(); i++) { + TypeScriptInterfaceResource *resource = Object::cast_to(array[i]); + if (resource) { + resource->configure(nested_schema_id); + } + } + } +} + +bool TypeScriptInterfaceResource::_set(const StringName &p_name, const Variant &p_value) { + if (p_name == StringName(SCHEMA_PROPERTY)) { + configure(StringName(String(p_value))); + return true; + } + for (const PropertyInfo &field : fields) { + if (field.name == p_name) { + configure_nested_value(p_name, p_value); + values[p_name] = p_value; + emit_changed(); + return true; + } + } + return false; +} + +bool TypeScriptInterfaceResource::_get(const StringName &p_name, Variant &r_value) const { + if (p_name == StringName(SCHEMA_PROPERTY)) { + r_value = String(schema_id); + return true; + } + if (values.has(p_name)) { + r_value = values[p_name]; + return true; + } + for (const PropertyInfo &field : fields) { + if (field.name == p_name) { + r_value = Variant(); + return true; + } + } + return false; +} + +void TypeScriptInterfaceResource::_get_property_list(List *p_list) const { + PropertyInfo schema_property(Variant::STRING, SCHEMA_PROPERTY, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_INTERNAL); + p_list->push_back(schema_property); + for (const PropertyInfo &field : fields) { + p_list->push_back(field); + } +} + +StringName TypeScriptInterfaceResource::get_interface_field_name(int64_t p_index) const { + if (p_index < 0 || p_index >= fields.size()) { + return StringName(); + } + return fields[p_index].name; +} + +} // namespace gode diff --git a/src/script/typescript_script.cpp b/src/script/typescript_script.cpp index bbdaa7c..3691088 100644 --- a/src/script/typescript_script.cpp +++ b/src/script/typescript_script.cpp @@ -2,6 +2,7 @@ #include "runtime/node_runtime.h" #include "runtime/value_convert.h" #include "script/typescript_compile_service.h" +#include "script/typescript_interface_resource.h" #include "script/typescript_language.h" #include @@ -193,6 +194,10 @@ static StringName class_name_from_class_node(TSNode class_node, const std::strin return StringName(node_text(source, name_node).c_str()); } +static StringName interface_name_from_interface_node(TSNode interface_node, const std::string &source) { + return class_name_from_class_node(interface_node, source); +} + static bool node_text_is_default(TSNode node, const std::string &source) { return !ts_node_is_null(node) && node_text(source, node) == "default"; } @@ -300,6 +305,32 @@ static TSNode find_class_declaration_by_name(TSNode root_node, uint32_t child_co return {}; } +static TSNode find_interface_declaration_by_name(TSNode root_node, uint32_t child_count, const std::string &source, const StringName &name) { + if (name.is_empty()) { + return {}; + } + + for (uint32_t i = 0; i < child_count; i++) { + TSNode child = ts_node_child(root_node, i); + if (strcmp(ts_node_type(child), "class_declaration") == 0) { + if (class_name_from_class_node(child, source) == name) { + return child; + } + continue; + } + if (strcmp(ts_node_type(child), "export_statement") != 0) { + continue; + } + for (uint32_t j = 0; j < ts_node_child_count(child); j++) { + TSNode exported_child = ts_node_child(child, j); + if (strcmp(ts_node_type(exported_child), "interface_declaration") == 0 && interface_name_from_interface_node(exported_child, source) == name) { + return exported_child; + } + } + } + return {}; +} + struct TypeAliasDefinition { std::string value; std::vector type_parameters; @@ -745,6 +776,7 @@ enum class ExportTypeKind { OBJECT, RESOURCE, NODE, + INTERFACE }; struct ExportTypeClassification { @@ -1292,7 +1324,7 @@ static ExportObjectResolution resolve_typescript_object_kind( } } - if (!ts_node_is_null(class_node)) { + else if (!ts_node_is_null(class_node)) { StringName resolved_class_name = class_name_from_class_node(class_node, next_source); if (resolved_class_name.is_empty()) { resolved_class_name = lookup_class_name; @@ -1554,6 +1586,12 @@ static void apply_export_type_classification(PropertyInfo &property, const Expor } return; } + case ExportTypeKind::INTERFACE: { + property.type = Variant::OBJECT; + property.hint = PROPERTY_HINT_RESOURCE_TYPE; + property.hint_string = TypeScriptInterfaceResource::get_class_static(); + property.class_name = TypeScriptInterfaceResource::get_class_static(); + } break; case ExportTypeKind::OBJECT: case ExportTypeKind::RESOURCE: case ExportTypeKind::NODE: @@ -2068,6 +2106,35 @@ static void expand_interface_fields( visited.erase(iface_name); } +static void expand_interfaces_fields( + const StringName &iface_name, + const std::string &prefix, + int depth, + HashSet &visited, + const HashMap> &interfaces, + HashMap &properties, + Vector &property_list) { + if (visited.has(iface_name)) { + return; + } + visited.insert(iface_name); + + for (const PropertyInfo &field : interfaces[iface_name]) { + std::string field_prefix = prefix + String(field.name).utf8().get_data(); + StringName nested_iface(field.class_name); + if (!nested_iface.is_empty() && interfaces.has(nested_iface)) { + expand_interfaces_fields(nested_iface, field_prefix + "::", depth + 1, visited, interfaces, properties, property_list); + continue; + } + PropertyInfo expanded = field; + expanded.name = StringName(field_prefix.c_str()); + properties[expanded.name] = expanded; + property_list.push_back(expanded); + } + + visited.erase(iface_name); +} + static void parse_signal_params(TSNode func_type_node, const std::string &source, MethodInfo &mi) { // func_type_node is the function_type node: (params) => void. if (strcmp(ts_node_type(func_type_node), "function_type") != 0) { @@ -2277,9 +2344,9 @@ static bool parse_int_metadata_value(TSNode value, const std::string &source, in static bool resolve_property_hint_member(const std::string &member_name, PropertyHint &r_hint) { #define MATCH_PROPERTY_HINT(m_hint) \ - if (member_name == #m_hint) { \ - r_hint = m_hint; \ - return true; \ + if (member_name == #m_hint) { \ + r_hint = m_hint; \ + return true; \ } MATCH_PROPERTY_HINT(PROPERTY_HINT_NONE) MATCH_PROPERTY_HINT(PROPERTY_HINT_RANGE) @@ -2546,7 +2613,7 @@ static bool method_node_is_accessor(TSNode method_node, TSNode name_node) { return false; } -static void parse_class_members(TSNode class_node, const std::string &source, const String &file_path, TSNode root_node, uint32_t child_count, HashMap &properties, Vector &property_list, HashMap &property_defaults, HashMap &methods, HashMap &static_methods, HashMap &signals, HashMap &rpc_configs, HashMap &member_lines, const HashMap> &interfaces) { +static void parse_class_members(TSNode class_node, const std::string &source, const String &file_path, TSNode root_node, uint32_t child_count, HashMap &properties, Vector &property_list, HashMap &interface_array_schemas, HashMap &property_defaults, HashMap &methods, HashMap &static_methods, HashMap &signals, HashMap &rpc_configs, HashMap &member_lines, const HashMap> &interfaces) { TSNode body_node = ts_node_child_by_field_name(class_node, "body", 4); if (ts_node_is_null(body_node)) { return; @@ -2707,6 +2774,27 @@ static void parse_class_members(TSNode class_node, const std::string &source, co if (!ts_node_is_null(default_object_node) && strcmp(ts_node_type(default_object_node), "object") == 0) { parse_object_defaults(default_object_node, source, prefix, property_defaults); } + } else if (!type_str.empty() && iface_key.contains("[]")) { + StringName inner_type = iface_key.substr(0, iface_key.length() - 2); + if (interfaces.has(inner_type)) { + const StringName schema_id(String(file_path) + "::" + String(inner_type)); + // The container remains a regular Godot Array. Its typed element is a + // Resource whose dynamic property list is populated from the interface. + pi.type = Variant::ARRAY; + pi.hint = PROPERTY_HINT_ARRAY_TYPE; + pi.hint_string = String::num_int64(Variant::OBJECT) + "/" + String::num_int64(PROPERTY_HINT_RESOURCE_TYPE) + ":" + String(TypeScriptInterfaceResource::get_class_static()); + pi.class_name = StringName(); + interface_array_schemas[field_name] = schema_id; + TypeScriptInterfaceResource::register_schema(schema_id, inner_type, interfaces); + properties[field_name] = pi; + property_list.push_back(pi); + if (!ts_node_is_null(field_value_node)) { + Variant default_value; + if (parse_default_value(field_value_node, source, pi.type, default_value)) { + property_defaults[field_name] = default_value; + } + } + } } else { properties[field_name] = pi; property_list.push_back(pi); @@ -2979,6 +3067,7 @@ bool TypeScriptScript::compile() const { base_class_name = StringName(); base_script_path = String(); property_list.clear(); + interface_array_schemas.clear(); methods.clear(); static_methods.clear(); signals.clear(); @@ -3047,7 +3136,7 @@ bool TypeScriptScript::compile() const { base_class_qualifier = qualifier_from_extends_node(base_class_node, source); } base_script_path = resolve_imported_class_path(get_path(), source, root_node, child_count, base_class_name, base_class_qualifier); - parse_class_members(class_node, source, get_path(), root_node, child_count, properties, property_list, property_defaults, methods, static_methods, signals, rpc_configs, member_lines, interfaces); + parse_class_members(class_node, source, get_path(), root_node, child_count, properties, property_list, interface_array_schemas, property_defaults, methods, static_methods, signals, rpc_configs, member_lines, interfaces); parse_static_exports(class_node, source, get_path(), root_node, child_count, properties, property_list, property_defaults); parse_exported_field_defaults(class_node, source, properties, property_defaults); collect_parent_properties(base_class_name, base_class_qualifier, source, root_node, child_count, get_path(), properties, property_list, property_defaults); From ea1341d33ce77bf055e46790b96b1bfa492a9c57 Mon Sep 17 00:00:00 2001 From: JustDooooIt <844730658@qq.com> Date: Sun, 6 Sep 2026 23:22:37 +0800 Subject: [PATCH 3/4] feat: enhance TypeScript interface resource with additional fields and default value handling --- .../scripts/tests/runtime_integration_test.ts | 3 + example/scripts/tests/tests_runner.gd | 16 +++++- .../script/typescript_interface_resource.h | 1 + src/script/typescript_interface_resource.cpp | 57 +++++++++++++++++-- src/script/typescript_script.cpp | 29 ---------- 5 files changed, 71 insertions(+), 35 deletions(-) diff --git a/example/scripts/tests/runtime_integration_test.ts b/example/scripts/tests/runtime_integration_test.ts index 03d01ed..9757144 100644 --- a/example/scripts/tests/runtime_integration_test.ts +++ b/example/scripts/tests/runtime_integration_test.ts @@ -38,6 +38,9 @@ const RUNTIME_ARRAY_RESOURCE_SCRIPT_PATH = "res://scripts/tests/runtime_array_re interface RuntimeExportInterfaceArrayElement { label: string; amount: number; + enabled: boolean; + tags: string[]; + metadata: Record; } function assert(condition: boolean, message: string): void { diff --git a/example/scripts/tests/tests_runner.gd b/example/scripts/tests/tests_runner.gd index 58deab4..3c68c58 100644 --- a/example/scripts/tests/tests_runner.gd +++ b/example/scripts/tests/tests_runner.gd @@ -61,14 +61,28 @@ func _run_next() -> void: if current_test.get("editor_record_dictionary") != {"from_gdscript": 8}: _fail("RuntimeIntegrationTest typed string-key Dictionary did not round-trip through ScriptInstance") return + if not current_test.get("editor_interface_array").is_empty(): + _fail("RuntimeIntegrationTest interface array default was not empty") + return var interface_element: Object = ClassDB.instantiate("TypeScriptInterfaceResource") current_test.set("editor_interface_array", [interface_element]) var interface_fields: Array[String] = [] for property in interface_element.get_property_list(): interface_fields.append(str(property.name)) - if not interface_fields.has("label") or not interface_fields.has("amount"): + if not interface_fields.has("label") or not interface_fields.has("amount") or not interface_fields.has("enabled") or not interface_fields.has("tags") or not interface_fields.has("metadata"): _fail("RuntimeIntegrationTest interface array element did not expose interface fields") return + if interface_element.get("label") != "" or interface_element.get("amount") != 0 or interface_element.get("enabled") != false: + _fail("RuntimeIntegrationTest interface array scalar fields did not use Godot defaults") + return + if not interface_element.get("tags").is_empty() or not interface_element.get("metadata").is_empty(): + _fail("RuntimeIntegrationTest interface array container fields did not use Godot defaults") + return + interface_element.set("label", "configured") + interface_element.set("amount", 9) + if interface_element.get("label") != "configured" or interface_element.get("amount") != 9: + _fail("RuntimeIntegrationTest interface array element edits did not round-trip") + return var int_key_dictionary := {} int_key_dictionary[7] = "seven" current_test.set("editor_int_key_map", int_key_dictionary) diff --git a/include/script/typescript_interface_resource.h b/include/script/typescript_interface_resource.h index 6c6e6d9..bb2d2c9 100644 --- a/include/script/typescript_interface_resource.h +++ b/include/script/typescript_interface_resource.h @@ -23,6 +23,7 @@ class TypeScriptInterfaceResource : public godot::Resource { godot::Vector fields; godot::HashMap nested_interfaces; godot::HashMap nested_interface_arrays; + godot::HashMap default_values; godot::HashMap values; void apply_schema(); diff --git a/src/script/typescript_interface_resource.cpp b/src/script/typescript_interface_resource.cpp index cd166b8..1582c5c 100644 --- a/src/script/typescript_interface_resource.cpp +++ b/src/script/typescript_interface_resource.cpp @@ -39,6 +39,53 @@ static std::string interface_array_element(const StringName &class_name) { return {}; } +static Variant default_value_for_type(Variant::Type type) { + switch (type) { + case Variant::BOOL: return false; + case Variant::INT: return int64_t(0); + case Variant::FLOAT: return 0.0; + case Variant::STRING: return String(); + case Variant::VECTOR2: return Vector2(); + case Variant::VECTOR2I: return Vector2i(); + case Variant::RECT2: return Rect2(); + case Variant::RECT2I: return Rect2i(); + case Variant::VECTOR3: return Vector3(); + case Variant::VECTOR3I: return Vector3i(); + case Variant::TRANSFORM2D: return Transform2D(); + case Variant::VECTOR4: return Vector4(); + case Variant::VECTOR4I: return Vector4i(); + case Variant::PLANE: return Plane(); + case Variant::QUATERNION: return Quaternion(); + case Variant::AABB: return AABB(); + case Variant::BASIS: return Basis(); + case Variant::TRANSFORM3D: return Transform3D(); + case Variant::PROJECTION: return Projection(); + case Variant::COLOR: return Color(); + case Variant::STRING_NAME: return StringName(); + case Variant::NODE_PATH: return NodePath(); + case Variant::RID: return RID(); + case Variant::CALLABLE: return Callable(); + case Variant::SIGNAL: return Signal(); + case Variant::DICTIONARY: return Dictionary(); + case Variant::ARRAY: return Array(); + case Variant::PACKED_BYTE_ARRAY: return PackedByteArray(); + case Variant::PACKED_INT32_ARRAY: return PackedInt32Array(); + case Variant::PACKED_INT64_ARRAY: return PackedInt64Array(); + case Variant::PACKED_FLOAT32_ARRAY: return PackedFloat32Array(); + case Variant::PACKED_FLOAT64_ARRAY: return PackedFloat64Array(); + case Variant::PACKED_STRING_ARRAY: return PackedStringArray(); + case Variant::PACKED_VECTOR2_ARRAY: return PackedVector2Array(); + case Variant::PACKED_VECTOR3_ARRAY: return PackedVector3Array(); + case Variant::PACKED_COLOR_ARRAY: return PackedColorArray(); + case Variant::PACKED_VECTOR4_ARRAY: return PackedVector4Array(); + case Variant::NIL: + case Variant::OBJECT: + case Variant::VARIANT_MAX: + return Variant(); + } + return Variant(); +} + } // namespace HashMap TypeScriptInterfaceResource::schemas; @@ -69,6 +116,7 @@ void TypeScriptInterfaceResource::apply_schema() { fields.clear(); nested_interfaces.clear(); nested_interface_arrays.clear(); + default_values.clear(); const Schema *schema = schemas.getptr(schema_id); if (!schema || !schema->interfaces.has(schema->interface_name)) { @@ -99,6 +147,7 @@ void TypeScriptInterfaceResource::apply_schema() { field.class_name = StringName(); } fields.push_back(field); + default_values[field.name] = default_value_for_type(field.type); } } @@ -155,11 +204,9 @@ bool TypeScriptInterfaceResource::_get(const StringName &p_name, Variant &r_valu r_value = values[p_name]; return true; } - for (const PropertyInfo &field : fields) { - if (field.name == p_name) { - r_value = Variant(); - return true; - } + if (default_values.has(p_name)) { + r_value = default_values[p_name]; + return true; } return false; } diff --git a/src/script/typescript_script.cpp b/src/script/typescript_script.cpp index 3691088..b56a842 100644 --- a/src/script/typescript_script.cpp +++ b/src/script/typescript_script.cpp @@ -2106,35 +2106,6 @@ static void expand_interface_fields( visited.erase(iface_name); } -static void expand_interfaces_fields( - const StringName &iface_name, - const std::string &prefix, - int depth, - HashSet &visited, - const HashMap> &interfaces, - HashMap &properties, - Vector &property_list) { - if (visited.has(iface_name)) { - return; - } - visited.insert(iface_name); - - for (const PropertyInfo &field : interfaces[iface_name]) { - std::string field_prefix = prefix + String(field.name).utf8().get_data(); - StringName nested_iface(field.class_name); - if (!nested_iface.is_empty() && interfaces.has(nested_iface)) { - expand_interfaces_fields(nested_iface, field_prefix + "::", depth + 1, visited, interfaces, properties, property_list); - continue; - } - PropertyInfo expanded = field; - expanded.name = StringName(field_prefix.c_str()); - properties[expanded.name] = expanded; - property_list.push_back(expanded); - } - - visited.erase(iface_name); -} - static void parse_signal_params(TSNode func_type_node, const std::string &source, MethodInfo &mi) { // func_type_node is the function_type node: (params) => void. if (strcmp(ts_node_type(func_type_node), "function_type") != 0) { From 4ba0d2bd9e2873f220354e42c99344d88066d132 Mon Sep 17 00:00:00 2001 From: moluopro Date: Mon, 7 Sep 2026 11:16:31 +0800 Subject: [PATCH 4/4] fix: preserve integer and array export compatibility --- generator/builtin_classes_generator.py | 2 +- src/script/typescript_script.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/generator/builtin_classes_generator.py b/generator/builtin_classes_generator.py index 2f73f6f..f478c51 100644 --- a/generator/builtin_classes_generator.py +++ b/generator/builtin_classes_generator.py @@ -21,7 +21,7 @@ def napi_match_expr(type_name, index): value = f"info[{index}]" if type_name == 'int': - return f"({value}.IsBigInt())" + return f"({value}.IsNumber() || {value}.IsBigInt())" if type_name == 'float': return f"{value}.IsNumber()" if type_name == 'bool': diff --git a/src/script/typescript_script.cpp b/src/script/typescript_script.cpp index b56a842..80b099a 100644 --- a/src/script/typescript_script.cpp +++ b/src/script/typescript_script.cpp @@ -1324,7 +1324,7 @@ static ExportObjectResolution resolve_typescript_object_kind( } } - else if (!ts_node_is_null(class_node)) { + if (!ts_node_is_null(class_node)) { StringName resolved_class_name = class_name_from_class_node(class_node, next_source); if (resolved_class_name.is_empty()) { resolved_class_name = lookup_class_name; @@ -2745,7 +2745,7 @@ static void parse_class_members(TSNode class_node, const std::string &source, co if (!ts_node_is_null(default_object_node) && strcmp(ts_node_type(default_object_node), "object") == 0) { parse_object_defaults(default_object_node, source, prefix, property_defaults); } - } else if (!type_str.empty() && iface_key.contains("[]")) { + } else if (!type_str.empty() && iface_key.contains("[]") && interfaces.has(iface_key.substr(0, iface_key.length() - 2))) { StringName inner_type = iface_key.substr(0, iface_key.length() - 2); if (interfaces.has(inner_type)) { const StringName schema_id(String(file_path) + "::" + String(inner_type));