Skip to content

Commit 0f084e7

Browse files
feat: Ergonomics overhaul. Camel-case, TS types, codegen + more.
- There's now a project setting which can be toggle to swap to a more idiomatic JS naming scheme for Godot bindings. We use camel and pascal case to more closely align with typical JavaScript/TypeScript conventions. For @decorators we've gone with pascal case, which is used in libraries like Angular. Camel case is perhaps more popular, but pascal case allows us to avoid reserved names and thus we can cleanly write @export, instead of needing to include the trailing underscore on @export_. - TypeScript types have been improved. Particularly Signal<> and Callable<>. Signal1, Signal2, etc. are now deprecated as are AnySignal and AnyCallable, since the Signal<T> and Callable<T> types now handle an arbitrary number of parameters. Importantly, Callable.bind(...) is now accurately typed, so you'll receive type errors when connecting to signals. - GArray and GDictionary now have a static .create<T>() method which allows you to create nested data structures from literals and benefit from full type checking. When a GArray or GDictionary is expected as a property, a .proxy() can be provided in its place. - Partially worked around microsoft/TypeScript#43826 whereby or proxied GArray and GDictionary always return proxied nested values, but will accept non-proxied values when mutating a property. Basically there's now GArrayReadProxy and GDictionaryReadProxy, these aren't runtime types, they're just TS types that make it easier to work with proxies. Under normal circumstances, users won't need to know they exist. - Codegen leveled up. Any TS module can now export a function named `codegen` with the type CodeGenHandler. This function will be called during codegen to allow users to optionally augment type generation involving user defined types. Consider for example the SceneNodes codegen which previously only knew how to handle Godot/native types in the scene hierarchy. When a user type was encountered, it'd write the native type, which is still useful, but it'd be nice to be able to include user types. The reason we don't by default is user types are not required to follow our generic parameter convention where each node is passed a Map argument. Let's see an example: export default class CardCollection<Card extends CardNode = CardNode> extends GameNode<SceneNodes['scenes/card_collection_3d.tscn']> the type above does not take a Map. Perhaps more interesting, it takes a different generic parameter, a CardNode. If we encounter a CardCollection script attached to a node in the scene somewhere GodotJS' internal codegen can't possibly know what that generic parameter ought to be. So we can help it out. In the same file where CardCollection is defined, we could provide a codegen handler like so: export const codegen: CodeGenHandler = rawRequest => { const request = rawRequest.proxy(); switch (request.type) { case CodeGenType.ScriptNodeTypeDescriptor: { const cardNodeScript = request.node.get('cardNodeScript'); return GDictionary.create<UserTypeDescriptor>({ type: DescriptorType.User, name: 'CardCollection', resource: 'res://src/card-collection.ts', arguments: GArray.create<TypeDescriptor>([ GDictionary.create<UserTypeDescriptor>({ type: DescriptorType.User, name: cardNodeScript?.getGlobalName() ?? 'CardNode', resource: cardNodeScript?.resourcePath ?? 'res://src/card-node.ts', }), ]), }); } } return undefined; }; Above we handle the codegen request to determine the node type of the provided `request.node`. What's *really* neat here is we don't need to hard-code that generic. We've instead exported a a configurable Script reference for use in the editor: @ExportObject(Script) cardNodeScript: Script = ResourceLoader.load('res://src/card-node.ts') as Script; So the codegen logic simply grabs the type exported from the chosen script provides it as a generic argument to CardCollection<>. One thing worth noting, your class does NOT need to be a @tool. In the above example, CardCollection<T> is not a @tool, and hence the node script is not instantiated during codegen, which is why we've used `request.node.get('cardNodeScript')` rather than trying to access the property directly. That said, if you want, codegen can be combined with @tool. Right now we only have the one type of codegen request. However, in my example above you may have noted I had to cast the return value of ResourceLoader.load() to a Script. That's another area that's ripe for codegen; string literal paths ought to result in a known resource type. If someone doesn't beat me to it, I'll contribute this too. - There's also a bunch of logging/error reporting improvements.
1 parent 0b3544b commit 0f084e7

36 files changed

+3933
-2606
lines changed

Diff for: bridge/jsb_bridge_helper.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ namespace jsb
3535
const v8::Local<v8::Object> enumeration = v8::Object::New(isolate);
3636
for (const KeyValue<StringName, int64_t>& kv : enum_values)
3737
{
38-
const v8::Local<v8::String> name = impl::Helper::new_string(isolate, kv.key);
38+
const v8::Local<v8::String> name = impl::Helper::new_string(isolate, internal::NamingUtil::get_enum_value_name(kv.key));
3939
const v8::Local<v8::Value> value = impl::Helper::new_integer(isolate, kv.value);
4040
enumeration->Set(context, name, value).Check();
4141
// represents the value back to string for convenient uses, such as MyColor[MyColor.White] => 'White'

Diff for: bridge/jsb_bridge_module_loader.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,7 @@ namespace jsb
456456
#else
457457
jsb_obj->Set(context, impl::Helper::new_string_ascii(isolate, "DEBUG_ENABLED"), v8::Boolean::New(isolate, false)).Check();
458458
#endif
459+
jsb_obj->Set(context, impl::Helper::new_string_ascii(isolate, "CAMEL_CASE_BINDINGS_ENABLED"), v8::Boolean::New(isolate, internal::Settings::get_camel_case_bindings_enabled())).Check();
459460
jsb_obj->Set(context, impl::Helper::new_string_ascii(isolate, "version"), impl::Helper::new_string(isolate, JSB_STRINGIFY(JSB_MAJOR_VERSION) "." JSB_STRINGIFY(JSB_MINOR_VERSION) "." JSB_STRINGIFY(JSB_PATCH_VERSION))).Check();
460461
jsb_obj->Set(context, impl::Helper::new_string_ascii(isolate, "impl"), impl::Helper::new_string(isolate, JSB_IMPL_VERSION_STRING)).Check();
461462
jsb_obj->Set(context, impl::Helper::new_string_ascii(isolate, "callable"), JSB_NEW_FUNCTION(context, _new_callable, {})).Check();

Diff for: bridge/jsb_editor_utility_funcs.cpp

+61-53
Large diffs are not rendered by default.

Diff for: bridge/jsb_environment.cpp

+19-2
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,21 @@ namespace jsb
311311
module_cache_.init(isolate_, cache_obj);
312312
}
313313

314+
// Populate StringNames replacement list so that classes can be lazily loaded by their exposed class name.
315+
{
316+
List<StringName> exposed_class_list = internal::NamingUtil::get_exposed_class_list();
317+
318+
for (auto it = exposed_class_list.begin(); it != exposed_class_list.end(); ++it)
319+
{
320+
String exposed_name = internal::NamingUtil::get_class_name(*it);
321+
322+
if (exposed_name != *it)
323+
{
324+
internal::StringNames::get_singleton().add_replacement(*it, exposed_name);
325+
}
326+
}
327+
}
328+
314329
#if !JSB_WITH_WEB && !JSB_WITH_JAVASCRIPTCORE
315330
Worker::register_(context, global);
316331
#endif
@@ -1308,12 +1323,14 @@ namespace jsb
13081323
return nullptr;
13091324
}
13101325

1311-
if (const NativeClassID* it = godot_classes_index_.getptr(p_class_info->name))
1326+
String class_name = internal::NamingUtil::get_class_name(p_class_info->name);
1327+
1328+
if (const NativeClassID* it = godot_classes_index_.getptr(class_name))
13121329
{
13131330
if (r_class_id) *r_class_id = *it;
13141331
NativeClassInfoPtr class_info = native_classes_.get_value_scoped(*it);
13151332
JSB_LOG(VeryVerbose, "return cached native class %s (%d) addr:%s", class_info->name, *it, class_info.ptr());
1316-
jsb_check(class_info->name == p_class_info->name);
1333+
jsb_check(internal::NamingUtil::get_class_name(class_info->name) == class_name);
13171334
jsb_check(!class_info->clazz.IsEmpty());
13181335
return class_info;
13191336
}

Diff for: bridge/jsb_environment.h

+12-4
Original file line numberDiff line numberDiff line change
@@ -249,16 +249,24 @@ namespace jsb
249249
void add_class_register(const Variant::Type p_type, const ClassRegisterFunc p_func)
250250
{
251251
jsb_check(!internal::VariantUtil::is_valid_name(godot_primitive_map_[p_type]));
252-
const StringName type_name = Variant::get_type_name(p_type);
252+
const String original_name = Variant::get_type_name(p_type);
253+
const StringName type_name = internal::NamingUtil::get_class_name(original_name);
254+
255+
if (type_name != original_name)
256+
{
257+
internal::StringNames::get_singleton().add_replacement(original_name, type_name);
258+
}
259+
253260
godot_primitive_map_[p_type] = type_name;
254261
add_class_register(type_name, p_func);
255262
}
256263

257264
void add_class_register(const StringName& p_type_name, const ClassRegisterFunc p_func)
258265
{
259-
jsb_check(internal::VariantUtil::is_valid_name(p_type_name));
260-
jsb_check(!class_register_map_.has(p_type_name));
261-
class_register_map_.insert(p_type_name, { {}, p_func });
266+
String name = internal::NamingUtil::get_class_name(p_type_name);
267+
jsb_check(internal::VariantUtil::is_valid_name(name));
268+
jsb_check(!class_register_map_.has(name));
269+
class_register_map_.insert(name, { {}, p_func });
262270
}
263271

264272
//TODO temp, get C++ function pointer (include class methods)

Diff for: bridge/jsb_godot_module_loader.cpp

+22-22
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,16 @@ namespace jsb
3030
"please use replaced name '%s' for '%s' in scripts (regenerate d.ts files)",
3131
p_type_name, internal::StringNames::get_singleton().get_replaced_name(p_type_name));
3232
}
33-
const StringName type_name = internal::StringNames::get_singleton().get_original_name(p_type_name);
33+
const StringName original_name = internal::StringNames::get_singleton().get_original_name(p_type_name);
3434

3535
//NOTE do not break the order in `GDScriptLanguage::init()`
3636

3737
// (1) singletons have the top priority (in GDScriptLanguage::init, singletons will overwrite the globals slot even if a type/const has the same name)
3838
// check before getting to avoid error prints in `get_singleton_object`
39-
if (Engine::get_singleton()->has_singleton(type_name))
40-
if (Object* gd_singleton = Engine::get_singleton()->get_singleton_object(type_name))
39+
if (Engine::get_singleton()->has_singleton(original_name))
40+
if (Object* gd_singleton = Engine::get_singleton()->get_singleton_object(original_name))
4141
{
42-
JSB_LOG(VeryVerbose, "exposing singleton object %s", (String) type_name);
42+
JSB_LOG(VeryVerbose, "exposing singleton object %s", (String) original_name);
4343
if (v8::Local<v8::Object> rval;
4444
TypeConvert::gd_obj_to_js(isolate, context, gd_singleton, rval) && !rval.IsEmpty())
4545
{
@@ -52,7 +52,7 @@ namespace jsb
5252
}
5353

5454
// (2) (global) utility functions.
55-
if (Variant::has_utility_function(type_name))
55+
if (Variant::has_utility_function(original_name))
5656
{
5757
//TODO check static bindings at first, and dynamic bindings as a fallback
5858

@@ -62,59 +62,59 @@ namespace jsb
6262
env->get_variant_info_collection().utility_funcs.append({});
6363
internal::FUtilityMethodInfo& method_info = env->get_variant_info_collection().utility_funcs.write[utility_func_index];
6464

65-
const int argument_count = Variant::get_utility_function_argument_count(type_name);
65+
const int argument_count = Variant::get_utility_function_argument_count(original_name);
6666
method_info.argument_types.resize(argument_count);
6767
for (int index = 0, num = argument_count; index < num; ++index)
6868
{
69-
method_info.argument_types.write[index] = Variant::get_utility_function_argument_type(type_name, index);
69+
method_info.argument_types.write[index] = Variant::get_utility_function_argument_type(original_name, index);
7070
}
7171
//NOTE currently, utility functions have no default argument.
7272
// method_info.default_arguments = ...
73-
method_info.return_type = Variant::get_utility_function_return_type(type_name);
74-
method_info.is_vararg = Variant::is_utility_function_vararg(type_name);
75-
method_info.set_debug_name(type_name);
76-
method_info.utility_func = Variant::get_validated_utility_function(type_name);
77-
JSB_LOG(VeryVerbose, "expose godot utility function %s (%d)", type_name, utility_func_index);
73+
method_info.return_type = Variant::get_utility_function_return_type(original_name);
74+
method_info.is_vararg = Variant::is_utility_function_vararg(original_name);
75+
method_info.set_debug_name(internal::NamingUtil::get_member_name(original_name));
76+
method_info.utility_func = Variant::get_validated_utility_function(original_name);
77+
JSB_LOG(VeryVerbose, "expose godot utility function %s (%d)", original_name, utility_func_index);
7878
jsb_check(method_info.utility_func);
7979

8080
info.GetReturnValue().Set(JSB_NEW_FUNCTION(context, ObjectReflectBindingUtil::_godot_utility_func, v8::Int32::New(isolate, utility_func_index)));
8181
return;
8282
}
8383

8484
// (3) global_constants
85-
if (CoreConstants::is_global_constant(type_name))
85+
if (CoreConstants::is_global_constant(original_name))
8686
{
87-
const int constant_index = CoreConstants::get_global_constant_index(type_name);
87+
const int constant_index = CoreConstants::get_global_constant_index(original_name);
8888
const int64_t constant_value = CoreConstants::get_global_constant_value(constant_index);
8989
info.GetReturnValue().Set(impl::Helper::new_integer(isolate, constant_value));
9090
return;
9191
}
9292

9393
// (4) classes in ClassDB/PrimitiveTypes
9494
{
95-
if (const NativeClassInfoPtr class_info = env->expose_class(type_name))
95+
if (const NativeClassInfoPtr class_info = env->expose_class(p_type_name))
9696
{
97-
jsb_check(class_info->name == type_name);
97+
jsb_check(class_info->name == p_type_name);
9898
jsb_check(!class_info->clazz.IsEmpty());
9999
info.GetReturnValue().Set(class_info->clazz.Get(isolate));
100100
return;
101101
}
102102

103103
// dynamic binding: godot class types
104-
if (const NativeClassInfoPtr class_info = env->expose_godot_object_class(ClassDB::classes.getptr(type_name)))
104+
if (const NativeClassInfoPtr class_info = env->expose_godot_object_class(ClassDB::classes.getptr(original_name)))
105105
{
106-
jsb_check(class_info->name == type_name);
106+
jsb_check(class_info->name == original_name);
107107
jsb_check(!class_info->clazz.IsEmpty());
108108
info.GetReturnValue().Set(class_info->clazz.Get(isolate));
109109
return;
110110
}
111111
}
112112

113113
// (5) global_enums
114-
if (CoreConstants::is_global_enum(type_name))
114+
if (CoreConstants::is_global_enum(original_name))
115115
{
116116
HashMap<StringName, int64_t> enum_values;
117-
CoreConstants::get_enum_values(type_name, &enum_values);
117+
CoreConstants::get_enum_values(original_name, &enum_values);
118118
info.GetReturnValue().Set(BridgeHelper::to_global_enum(isolate, context, enum_values));
119119
return;
120120
}
@@ -124,7 +124,7 @@ namespace jsb
124124
// VARIANT_ENUM_CAST(Variant::Type);
125125
// VARIANT_ENUM_CAST(Variant::Operator);
126126
// they are exposed as `Variant.Type` in global constants in godot
127-
if (type_name == jsb_string_name(Variant))
127+
if (original_name == jsb_string_name(Variant))
128128
{
129129
const v8::Local<v8::Object> obj = v8::Object::New(isolate);
130130
obj->Set(context, impl::Helper::new_string(isolate, "Type"), BridgeHelper::to_global_enum(isolate, context, "Variant.Type")).Check();
@@ -133,7 +133,7 @@ namespace jsb
133133
return;
134134
}
135135

136-
impl::Helper::throw_error(isolate, jsb_format("godot class not found '%s'", type_name));
136+
impl::Helper::throw_error(isolate, jsb_format("godot class not found '%s'", original_name));
137137
}
138138

139139
v8::Local<v8::Object> GodotModuleLoader::_get_loader_proxy(Environment* p_env)

0 commit comments

Comments
 (0)