Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/pink-feet-strive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@godot-js/editor": minor
---

Node rpc and rpc_id/rpcId methods are now type safe thanks to additional codegen for RPCs.
1 change: 1 addition & 0 deletions bridge/jsb_class_info.h
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ namespace jsb
};

// Safe pointer of ScriptClassInfo
typedef internal::SArray<ScriptClassInfo, ScriptClassID> ScriptClassInfoArray;
typedef internal::SArray<ScriptClassInfo, ScriptClassID>::Pointer ScriptClassInfoPtr;
typedef internal::SArray<ScriptClassInfo, ScriptClassID>::ConstPointer ScriptClassInfoConstPtr;

Expand Down
62 changes: 58 additions & 4 deletions bridge/jsb_editor_utility_funcs.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "jsb_editor_utility_funcs.h"
#include "jsb_type_convert.h"
#include "jsb_environment.h"
#include "core/object/script_language.h"

#if GODOT_4_6_OR_NEWER
using ConstantHashMap = AHashMap<StringName, int64_t>;
Expand Down Expand Up @@ -346,7 +348,7 @@ namespace jsb
set_field(isolate, context, signal_obj, "method_", method_obj);
}

v8::Local<v8::Object> build_class_info(v8::Isolate* isolate, const v8::Local<v8::Context>& context, const StringName& class_name)
v8::Local<v8::Object> build_class_info(v8::Isolate* isolate, const v8::Local<v8::Context>& context, const StringName& class_name, const HashSet<StringName>* class_rpc_methods)
{
v8::Local<v8::Object> class_info_obj = v8::Object::New(isolate);
const HashMap<StringName, ClassDB::ClassInfo>::Iterator class_it = ClassDB::classes.find(class_name);
Expand Down Expand Up @@ -420,6 +422,41 @@ namespace jsb
}
}

// class: rpc methods
{
JSB_HANDLE_SCOPE(isolate);

v8::Local<v8::Array> rpc_methods_obj = v8::Array::New(isolate);
set_field(isolate, context, class_info_obj, "rpc_methods", rpc_methods_obj);

if (class_rpc_methods)
{
int index = 0;

for (const KeyValue<StringName, MethodBind*>& pair : class_info.method_map)
{
MethodBind const * const method_bind = pair.value;

if (method_bind->is_static())
{
continue;
}

const StringName exposed_method_name = internal::NamingUtil::get_member_name(pair.key);

if (!class_rpc_methods->has(pair.key) && !class_rpc_methods->has(exposed_method_name))
{
continue;
}

JSB_HANDLE_SCOPE(isolate);
v8::Local<v8::Object> method_info_obj = v8::Object::New(isolate);
build_method_info(isolate, context, method_bind, method_info_obj);
rpc_methods_obj->Set(context, index++, method_info_obj).Check();
}
}
}

// class: gd virtual methods
{
JSB_HANDLE_SCOPE(isolate);
Expand Down Expand Up @@ -936,15 +973,33 @@ namespace jsb

v8::HandleScope handle_scope(isolate);
v8::Local<v8::Context> context = isolate->GetCurrentContext();
Environment* environment = Environment::wrap(isolate);

List<StringName> exposed_class_list = internal::NamingUtil::get_exposed_original_class_list();
HashMap<StringName, HashSet<StringName>> rpc_method_map;

for (auto& script_class_info : environment->get_script_classes())
{
if (script_class_info.rpc_config.is_empty())
{
continue;
}

HashSet<StringName>& methods = rpc_method_map[script_class_info.js_class_name];

for (const auto& pair : script_class_info.rpc_config)
{
methods.insert(pair.key);
}
}

v8::Local<v8::Array> array = v8::Array::New(isolate, exposed_class_list.size());
int index = 0;

for (auto it = exposed_class_list.begin(); it != exposed_class_list.end(); ++it)
for (auto& class_name : exposed_class_list)
{
JSB_HANDLE_SCOPE(isolate);
array->Set(context, index++, build_class_info(isolate, context, *it)).Check();
array->Set(context, index++, build_class_info(isolate, context, class_name, rpc_method_map.getptr(class_name))).Check();
}

info.GetReturnValue().Set(array);
Expand Down Expand Up @@ -1142,4 +1197,3 @@ namespace jsb
}
}
#endif // endif JSB_WITH_EDITOR_UTILITY_FUNCS

7 changes: 7 additions & 0 deletions bridge/jsb_environment.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@

void Environment::init()
{
jsb::DefaultModuleResolver& resolver = this->add_module_resolver<jsb::DefaultModuleResolver>()

Check warning on line 444 in bridge/jsb_environment.cpp

View workflow job for this annotation

GitHub Actions / ⚙️ Build Godot Version with GodotJS (4.6.1, 4.6, 4.6.1-stable) / 🍏 iOS / iOS (template_debug, qjs-ng)

unused variable 'resolver'

Check warning on line 444 in bridge/jsb_environment.cpp

View workflow job for this annotation

GitHub Actions / ⚙️ Build Godot Version with GodotJS (4.6.1, 4.6, 4.6.1-stable) / 🤖 Android / Android (template_debug, qjs-ng)

unused variable 'resolver'

Check warning on line 444 in bridge/jsb_environment.cpp

View workflow job for this annotation

GitHub Actions / ⚙️ Build Godot Version with GodotJS (4.6.1, 4.6, 4.6.1-stable) / 🌐 Web / Web (template_debug, qjs-ng, threads=yes, dlink=no)

unused variable 'resolver'

Check warning on line 444 in bridge/jsb_environment.cpp

View workflow job for this annotation

GitHub Actions / ⚙️ Build Godot Version with GodotJS (4.6.1, 4.6, 4.6.1-stable) / 🌐 Web / Web (template_debug, browser, threads=yes, dlink=no)

unused variable 'resolver'

Check warning on line 444 in bridge/jsb_environment.cpp

View workflow job for this annotation

GitHub Actions / ⚙️ Build Godot Version with GodotJS (4.6.1, 4.6, 4.6.1-stable) / 🏁 Windows / Windows (editor, v8)

'resolver': local variable is initialized but not referenced

Check warning on line 444 in bridge/jsb_environment.cpp

View workflow job for this annotation

GitHub Actions / ⚙️ Build Godot Version with GodotJS (4.6.1, 4.6, 4.6.1-stable) / 🍎 macOS / Mac (editor, qjs-ng)

unused variable 'resolver'

Check warning on line 444 in bridge/jsb_environment.cpp

View workflow job for this annotation

GitHub Actions / ⚙️ Build Godot Version with GodotJS (4.6.1, 4.6, 4.6.1-stable) / 🍎 macOS / Mac (editor, qjs-ng)

unused variable 'resolver'

Check warning on line 444 in bridge/jsb_environment.cpp

View workflow job for this annotation

GitHub Actions / ⚙️ Build Godot Version with GodotJS (4.6.1, 4.6, 4.6.1-stable) / 🏁 Windows / Windows (editor, qjs-ng)

'resolver': local variable is initialized but not referenced
.add_search_path(jsb::internal::Settings::get_jsb_out_res_path()) // default path of js source (results of compiled ts, at '.godot/GodotJS' by default)
.add_search_path("res://") // use the root directory as custom lib path by default
.add_search_path("res://node_modules") // so far, it's the only supported path for node_modules in GodotJS
Expand Down Expand Up @@ -676,7 +676,7 @@

void _invoke(Environment* p_env, const v8::Local<v8::Context>& p_context, const v8::Local<v8::Function>& p_callback, const Message* p_message)
{
v8::Isolate *isolate = p_env->get_isolate();

Check warning on line 679 in bridge/jsb_environment.cpp

View workflow job for this annotation

GitHub Actions / ⚙️ Build Godot Version with GodotJS (4.6.1, 4.6, 4.6.1-stable) / 🌐 Web / Web (template_debug, browser, threads=yes, dlink=no)

unused variable 'isolate'

#if !JSB_WITH_WEB && !JSB_WITH_JAVASCRIPTCORE
v8::Local<v8::Value> value;
Expand Down Expand Up @@ -2135,6 +2135,13 @@
string_name_cache_.shrink();
source_map_cache_.clear();

// `dispose()` explicitly destroys bound objects, and shutdown-time forced GC has caused
// fatal crashes while V8 is invoking weak callbacks. Skip forced collection in this state.
if (flags_ & EF_PreDispose)
{
return;
}

#if JSB_EXPOSE_GC_FOR_TESTING
isolate_->RequestGarbageCollectionForTesting(v8::Isolate::kFullGarbageCollection);
#else
Expand Down
7 changes: 4 additions & 3 deletions bridge/jsb_environment.h
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,11 @@ namespace jsb
#if JSB_THREADING
internal::DoubleBuffered<AsyncCall> async_calls_;
#endif

#if JSB_V8_CPPGC
std::unique_ptr<v8::CppHeap> cpp_heap_;
#endif

// indirect lookup
// only godot object classes are mapped
HashMap<StringName, NativeClassID> godot_classes_index_;
Expand All @@ -150,7 +150,7 @@ namespace jsb

//TODO all exported default classes inherit native godot class (directly or indirectly)
// they're only collected on a module loaded
internal::SArray<ScriptClassInfo, ScriptClassID> script_classes_;
ScriptClassInfoArray script_classes_;

StringNameCache string_name_cache_;

Expand Down Expand Up @@ -592,6 +592,7 @@ namespace jsb
jsb_force_inline ScriptClassInfoPtr get_script_class(const ScriptClassID p_class_id) { return script_classes_.get_value_scoped(p_class_id); }
jsb_force_inline ScriptClassInfoConstPtr get_script_class(const ScriptClassID p_class_id) const { return script_classes_.get_value_scoped(p_class_id); }
jsb_force_inline ScriptClassInfoPtr find_script_class(const ScriptClassID p_class_id) { return script_classes_.is_valid_index(p_class_id) ? script_classes_.get_value_scoped(p_class_id) : nullptr; }
jsb_force_inline const ScriptClassInfoArray& get_script_classes() { return script_classes_; }

void get_statistics(Statistics& r_stats) const;

Expand Down
104 changes: 104 additions & 0 deletions scripts/jsb.editor/src/jsb.editor.codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
Node,
PropertyInfo,
Resource,
ResourceLoader,
ResourceTypes,
Script,
Variant,
Expand Down Expand Up @@ -500,6 +501,12 @@ const TypeMutations: Record<string, TypeMutation> = {
move_child: mutate_parameter_type(names.get_parameter("child_node"), "NodePathMapChild<Map>"),
remove_child: mutate_parameter_type("node", "NodePathMapChild<Map>"),
validate_property: mutate_parameter_type("property", "GDictionary<PropertyInfo>"),
rpc: [
`${names.get_member("rpc")}<Method extends ${names.get_class("GodotRPCNames")}<this>>(method: Method, ...varargs: ${names.get_class("ResolveGodotRPCParameters")}<this, Method>): Error`,
],
rpc_id: [
`${names.get_member("rpc_id")}<Method extends ${names.get_class("GodotRPCNames")}<this>>(${names.get_parameter("peer_id")}: int64, method: Method, ...varargs: ${names.get_class("ResolveGodotRPCParameters")}<this, Method>): Error`,
],
},
},
// GObject:
Expand Down Expand Up @@ -3746,6 +3753,25 @@ export class TSDCodeGen {
}
}

const rpc_interface_name = `__RPCMap${cls.name}`;
const rpc_interface_writer = cg.interface_(rpc_interface_name, undefined, cls.super && `__RPCMap${cls.super}`);
const rpc_methods = cls.rpc_methods ?? [];
for (const method_info of rpc_methods) {
rpc_interface_writer.property_(
method_info.name,
`(${this._types.make_args(method_info)}) => ${this._types.make_return(method_info)}`,
);
}

// Not really deprecated, but we don't want people using this.
cg.line("/** @deprecated Internal use. Does not exist at runtime. */");
rpc_interface_writer.finish();

const godot_rpc_map_writer = class_cg.property_("__godotRPCMap");
godot_rpc_map_writer.line(rpc_interface_name);
class_cg.line("/** @deprecated Internal use. Does not exist at runtime. */");
godot_rpc_map_writer.finish();

const overrides_interface_name = `__NameMap${cls.name}`;
const overrides_interface_writer = cg.interface_(
overrides_interface_name,
Expand Down Expand Up @@ -3882,12 +3908,22 @@ export class SceneTSDCodeGen {
export class ResourceTSDCodeGen {
private _out_dir: string;
private _resource_paths: string[];
private _script_extensions: string[];
private _types: TypeDB;

constructor(out_dir: string, resource_paths: string[]) {
this._out_dir = out_dir;
this._resource_paths = resource_paths;

const recognized_extensions = godot.ResourceLoader.get_recognized_extensions_for_type("Script");
const length = recognized_extensions.size();
const script_extensions = new Array<string>(length);

for (let i = 0; i < length; i++) {
script_extensions[i] = recognized_extensions.get(i);
}

this._script_extensions = script_extensions;
this._types = new TypeDB();
}

Expand Down Expand Up @@ -3915,6 +3951,71 @@ export class ResourceTSDCodeGen {
return tasks.submit(false);
}

private get_script_rpc_info(resource_path: string): null | { class_name: string; methods: string[] } {
const extension = resource_path.slice(resource_path.lastIndexOf('.') + 1);

if (!this._script_extensions.includes(extension)) {
return null;
}

const resource_loader = godot.ResourceLoader;

let script: undefined | Script;

try {
script = resource_loader.load(resource_path) as Script;
} catch (e) {
console.warn(`Failed to generate RPC types for script: ${resource_path}`, e);
return null;
}

const class_name: string = script.get_global_name();
const rpc_config: null | GDictionary = script.get_rpc_config();

if (!class_name || !rpc_config) {
return null;
}

const methods: string[] = [...rpc_config.keys()].filter(name => name).sort();

if (methods.length === 0) {
return null;
}

return { class_name, methods };
}

private emit_script_rpc_types(module: ModuleWriter, resource_path: string) {
const script_rpc_info = this.get_script_rpc_info(resource_path);

if (!script_rpc_info) {
return;
}

module.add_import(script_rpc_info.class_name, resource_path);

const imported_class_name = module.get_imports()[resource_path]?.default ?? script_rpc_info.class_name;

const rpc_entries_interface = module.interface_(names.get_class("GodotUserRPCEntries"));
const entry_property = rpc_entries_interface.property_(resource_path);
const entry_writer = entry_property.object_();
entry_writer.property_("type", imported_class_name);

const rpc_map_property = entry_writer.property_("procedures");
const rpc_map_writer = rpc_map_property.object_();

for (const method_name of script_rpc_info.methods) {
rpc_map_writer.property_(method_name, `${imported_class_name}[${JSON.stringify(method_name)}]`);
}

rpc_map_writer.finish();
rpc_map_property.finish();

entry_writer.finish();
entry_property.finish();
rpc_entries_interface.finish();
}

private emit_resource_type(resource_path: string) {
try {
const helper = godot.GodotJSEditorHelper;
Expand Down Expand Up @@ -3950,6 +4051,9 @@ export class ResourceTSDCodeGen {
type_descriptor.finish();
resource_property.finish();
resource_types_interface.finish();

this.emit_script_rpc_types(module, resource_path);

module.finish();
file_writer.finish();
} finally {
Expand Down
24 changes: 22 additions & 2 deletions scripts/typings/godot.generated.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,25 @@ declare module "godot" {

class Node<Map extends Record<string, Node> = any> extends Object {}
class Resource {}
class Script extends Resource {}
class Script extends Resource {
get_global_name(): StringName
get_rpc_config(): any
}

namespace ResourceLoader {
enum CacheMode {
CACHE_MODE_IGNORE = 0,
CACHE_MODE_REUSE = 1,
CACHE_MODE_REPLACE = 2,
CACHE_MODE_IGNORE_DEEP = 3,
CACHE_MODE_REPLACE_DEEP = 4,
}
}
class ResourceLoader extends Object {
static load(path: string, type_hint?: string, cache_mode?: ResourceLoader.CacheMode): Resource;
static get_recognized_extensions_for_type(type: string): PackedStringArray;
}

interface ResourceTypes {}

type GArrayCreateSource<T> =
Expand Down Expand Up @@ -62,7 +80,7 @@ declare module "godot" {
push_back(value: GArrayElement<T>): void;
pop_back(): GArrayElement<T>;
has(value: GArrayElement<T>): boolean;
find(what: GArrayElement<T>, from: int64 = 0): int64;
find(what: GArrayElement<T>, from?: int64): int64;
}
type byte = number;
type int32 = number;
Expand Down Expand Up @@ -151,6 +169,8 @@ declare module "godot" {

class PackedStringArray {
append(value: string): boolean;
get(index: int64): string;
size(): int64;
}

namespace FileAccess {
Expand Down
1 change: 1 addition & 0 deletions scripts/typings/godot.minimal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ declare module "godot-jsb" {
super: string;

properties: Array<PropertySetGetInfo>;
rpc_methods?: Array<MethodBind>;
virtual_methods: Array<MethodBind>;
signals: Array<SignalInfo>;
constants?: Array<ConstantInfo>;
Expand Down
Loading
Loading