diff --git a/godot-core/src/docs.rs b/godot-core/src/docs.rs index f6bcda41b..9e153f254 100644 --- a/godot-core/src/docs.rs +++ b/godot-core/src/docs.rs @@ -6,7 +6,7 @@ */ use crate::meta::ClassName; -use crate::registry::plugin::PluginItem; +use crate::registry::plugin::{InherentImpl, PluginItem}; use std::collections::HashMap; /// Created for documentation on @@ -77,7 +77,7 @@ pub fn gather_xml_docs() -> impl Iterator { let class_name = x.class_name; match x.item { - PluginItem::InherentImpl { docs, .. } => { + PluginItem::InherentImpl(InherentImpl { docs, .. }) => { map.entry(class_name).or_default().inherent = docs } diff --git a/godot-core/src/meta/mod.rs b/godot-core/src/meta/mod.rs index b5a2e4a6f..c5edd566f 100644 --- a/godot-core/src/meta/mod.rs +++ b/godot-core/src/meta/mod.rs @@ -40,6 +40,8 @@ mod godot_convert; mod method_info; mod property_info; mod ref_arg; +#[cfg(feature = "codegen-full")] +mod rpc_config; mod sealed; mod signature; mod traits; @@ -47,6 +49,8 @@ mod traits; pub mod error; pub use class_name::ClassName; pub use godot_convert::{FromGodot, GodotConvert, ToGodot}; +#[cfg(feature = "codegen-full")] +pub use rpc_config::RpcConfig; pub use traits::{ArrayElement, GodotType, PackedArrayElement}; pub(crate) use crate::impl_godot_as_self; diff --git a/godot-core/src/meta/rpc_config.rs b/godot-core/src/meta/rpc_config.rs new file mode 100644 index 000000000..50568e431 --- /dev/null +++ b/godot-core/src/meta/rpc_config.rs @@ -0,0 +1,50 @@ +/* + * Copyright (c) godot-rust; Bromeon and contributors. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use crate::builtin::{Dictionary, StringName}; +use crate::classes::multiplayer_api::RpcMode; +use crate::classes::multiplayer_peer::TransferMode; +use crate::classes::Node; +use crate::dict; +use crate::meta::ToGodot; + +/// See [Godot documentation](https://docs.godotengine.org/en/stable/tutorials/networking/high_level_multiplayer.html#remote-procedure-calls) +#[derive(Clone, Copy, Debug)] +pub struct RpcConfig { + pub mode: RpcMode, + pub transfer_mode: TransferMode, + pub call_local: bool, + pub transfer_channel: u32, +} + +impl Default for RpcConfig { + fn default() -> Self { + Self { + mode: RpcMode::AUTHORITY, + transfer_mode: TransferMode::UNRELIABLE, + call_local: false, + transfer_channel: 0, + } + } +} + +impl RpcConfig { + /// Register `method` as a remote procedure call on `node`. + pub fn register(self, node: &mut Node, method: impl Into) { + node.rpc_config(method.into(), &self.into_dictionary().to_variant()); + } + + /// Returns a [`Dictionary`] populated with the values required for a call to [`Node::rpc_config`]. + pub fn into_dictionary(self) -> Dictionary { + dict! { + "mode": self.mode, + "transfer_mode": self.transfer_mode, + "call_local": self.call_local, + "transfer_channel": self.transfer_channel, + } + } +} diff --git a/godot-core/src/obj/traits.rs b/godot-core/src/obj/traits.rs index d6541936b..41ddd1d9d 100644 --- a/godot-core/src/obj/traits.rs +++ b/godot-core/src/obj/traits.rs @@ -455,6 +455,7 @@ pub mod cap { use super::*; use crate::builtin::{StringName, Variant}; use crate::obj::{Base, Bounds, Gd}; + use std::any::Any; /// Trait for all classes that are default-constructible from the Godot engine. /// @@ -558,6 +559,8 @@ pub mod cap { fn __register_methods(); #[doc(hidden)] fn __register_constants(); + #[doc(hidden)] + fn __register_rpcs(_: &mut dyn Any) {} } pub trait ImplementsGodotExports: GodotClass { diff --git a/godot-core/src/private.rs b/godot-core/src/private.rs index bc70ce33b..704668cf9 100644 --- a/godot-core/src/private.rs +++ b/godot-core/src/private.rs @@ -8,7 +8,9 @@ pub use crate::gen::classes::class_macros; pub use crate::obj::rtti::ObjectRtti; pub use crate::registry::callbacks; -pub use crate::registry::plugin::{ClassPlugin, ErasedRegisterFn, PluginItem}; +pub use crate::registry::plugin::{ + ClassPlugin, ErasedRegisterFn, ErasedRegisterRpcsFn, InherentImpl, PluginItem, +}; pub use crate::storage::{as_storage, Storage}; pub use sys::out; @@ -17,11 +19,10 @@ pub use crate::meta::trace; use crate::global::godot_error; use crate::meta::error::CallError; -use crate::meta::CallContext; +use crate::meta::{CallContext, ClassName}; use crate::sys; use std::sync::{atomic, Arc, Mutex}; use sys::Global; - // ---------------------------------------------------------------------------------------------------------------------------------------------- // Global variables @@ -128,6 +129,21 @@ pub(crate) fn iterate_plugins(mut visitor: impl FnMut(&ClassPlugin)) { sys::plugin_foreach!(__GODOT_PLUGIN_REGISTRY; visitor); } +pub(crate) fn find_inherent_impl(class_name: ClassName) -> Option { + // We do this manually instead of using `iterate_plugins()` because we want to break as soon as we find a match. + let plugins = __godot_rust_plugin___GODOT_PLUGIN_REGISTRY.lock().unwrap(); + + plugins.iter().find_map(|elem| { + if elem.class_name == class_name { + if let PluginItem::InherentImpl(inherent_impl) = &elem.item { + return Some(inherent_impl.clone()); + } + } + + None + }) +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- // Traits and types diff --git a/godot-core/src/registry/callbacks.rs b/godot-core/src/registry/callbacks.rs index 657389df8..d8fcc0411 100644 --- a/godot-core/src/registry/callbacks.rs +++ b/godot-core/src/registry/callbacks.rs @@ -354,3 +354,7 @@ pub fn register_user_methods_constants(_class_builde T::__register_methods(); T::__register_constants(); } + +pub fn register_user_rpcs(object: &mut dyn Any) { + T::__register_rpcs(object); +} diff --git a/godot-core/src/registry/class.rs b/godot-core/src/registry/class.rs index 3dd60cb38..68da32c63 100644 --- a/godot-core/src/registry/class.rs +++ b/godot-core/src/registry/class.rs @@ -13,7 +13,7 @@ use crate::meta::ClassName; use crate::obj::{cap, GodotClass}; use crate::private::{ClassPlugin, PluginItem}; use crate::registry::callbacks; -use crate::registry::plugin::ErasedRegisterFn; +use crate::registry::plugin::{ErasedRegisterFn, InherentImpl}; use crate::{godot_error, sys}; use sys::{interface_fn, out, Global, GlobalGuard, GlobalLockError}; @@ -71,7 +71,7 @@ impl ClassRegistrationInfo { // Note: when changing this match, make sure the array has sufficient size. let index = match item { PluginItem::Struct { .. } => 0, - PluginItem::InherentImpl { .. } => 1, + PluginItem::InherentImpl(InherentImpl { .. }) => 1, PluginItem::ITraitImpl { .. } => 2, }; @@ -200,6 +200,18 @@ pub fn unregister_classes(init_level: InitLevel) { } } +#[cfg(feature = "codegen-full")] +pub fn auto_register_rpcs(object: &mut T) { + // Find the element that matches our class, and call the closure if it exists. + if let Some(InherentImpl { + register_rpcs_fn: Some(closure), + .. + }) = crate::private::find_inherent_impl(T::class_name()) + { + (closure.raw)(object); + } +} + fn global_loaded_classes() -> GlobalGuard<'static, HashMap>> { match LOADED_CLASSES.try_lock() { Ok(it) => it, @@ -281,11 +293,12 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) { } } - PluginItem::InherentImpl { + PluginItem::InherentImpl(InherentImpl { register_methods_constants_fn, + register_rpcs_fn: _, #[cfg(all(since_api = "4.3", feature = "docs"))] docs: _, - } => { + }) => { c.register_methods_constants_fn = Some(register_methods_constants_fn); } diff --git a/godot-core/src/registry/plugin.rs b/godot-core/src/registry/plugin.rs index 0453dfc1a..55971fcbc 100644 --- a/godot-core/src/registry/plugin.rs +++ b/godot-core/src/registry/plugin.rs @@ -12,7 +12,6 @@ use crate::meta::ClassName; use crate::sys; use std::any::Any; use std::fmt; - // TODO(bromeon): some information coming from the proc-macro API is deferred through PluginItem, while others is directly // translated to code. Consider moving more code to the PluginItem, which allows for more dynamic registration and will // be easier for a future builder API. @@ -45,6 +44,31 @@ impl fmt::Debug for ErasedRegisterFn { } } +#[derive(Copy, Clone)] +pub struct ErasedRegisterRpcsFn { + pub raw: fn(&mut dyn Any), +} + +impl fmt::Debug for ErasedRegisterRpcsFn { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "0x{:0>16x}", self.raw as usize) + } +} + +#[derive(Clone, Debug)] +pub struct InherentImpl { + /// Callback to library-generated function which registers functions and constants in the `impl` block. + /// + /// Always present since that's the entire point of this `impl` block. + pub register_methods_constants_fn: ErasedRegisterFn, + /// Callback to library-generated function which calls [`Node::rpc_config`](crate::classes::Node::rpc_config) for each function annotated with `#[rpc]` on the `impl` block. + /// + /// This function is called in [`UserClass::__before_ready()`](crate::obj::UserClass::__before_ready) definitions generated by the `#[derive(GodotClass)]` macro. + pub register_rpcs_fn: Option, + #[cfg(all(since_api = "4.3", feature = "docs"))] + pub docs: InherentImplDocs, +} + /// Represents the data part of a [`ClassPlugin`] instance. /// /// Each enumerator represents a different item in Rust code, which is processed by an independent proc macro (for example, @@ -102,14 +126,7 @@ pub enum PluginItem { }, /// Collected from `#[godot_api] impl MyClass`. - InherentImpl { - /// Callback to library-generated function which registers functions and constants in the `impl` block. - /// - /// Always present since that's the entire point of this `impl` block. - register_methods_constants_fn: ErasedRegisterFn, - #[cfg(all(since_api = "4.3", feature = "docs"))] - docs: InherentImplDocs, - }, + InherentImpl(InherentImpl), /// Collected from `#[godot_api] impl I... for MyClass`. ITraitImpl { diff --git a/godot-macros/Cargo.toml b/godot-macros/Cargo.toml index 6a422ebe2..fb3e170e5 100644 --- a/godot-macros/Cargo.toml +++ b/godot-macros/Cargo.toml @@ -13,6 +13,7 @@ homepage = "https://godot-rust.github.io" [features] api-custom = ["godot-bindings/api-custom"] docs = ["dep:markdown"] +codegen-full = [] [lib] proc-macro = true @@ -31,7 +32,7 @@ godot-bindings = { path = "../godot-bindings", version = "=0.1.3" } # emit_godot # Reverse dev dependencies so doctests can use `godot::` prefix. [dev-dependencies] -godot = { path = "../godot", default-features = false } +godot = { path = "../godot", default-features = false, features = ["__codegen-full"] } # https://docs.rs/about/metadata [package.metadata.docs.rs] diff --git a/godot-macros/src/class/data_models/field_var.rs b/godot-macros/src/class/data_models/field_var.rs index 93ad2dfca..3066713e9 100644 --- a/godot-macros/src/class/data_models/field_var.rs +++ b/godot-macros/src/class/data_models/field_var.rs @@ -204,6 +204,7 @@ impl GetterSetterImpl { external_attributes: Vec::new(), rename: None, is_script_virtual: false, + rpc_info: None, }, ); diff --git a/godot-macros/src/class/data_models/func.rs b/godot-macros/src/class/data_models/func.rs index 2bf9de228..2e974b107 100644 --- a/godot-macros/src/class/data_models/func.rs +++ b/godot-macros/src/class/data_models/func.rs @@ -5,6 +5,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use crate::class::RpcInfo; use crate::util::{bail_fn, ident, safe_ident}; use crate::{util, ParseResult}; use proc_macro2::{Group, Ident, TokenStream, TokenTree}; @@ -19,6 +20,8 @@ pub struct FuncDefinition { /// The name the function will be exposed as in Godot. If `None`, the Rust function name is used. pub rename: Option, pub is_script_virtual: bool, + /// Information about the RPC configuration, if provided. + pub rpc_info: Option, } /// Returns a C function which acts as the callback when a virtual method of this instance is invoked. diff --git a/godot-macros/src/class/data_models/inherent_impl.rs b/godot-macros/src/class/data_models/inherent_impl.rs index 25d4e9d14..6b6ffb450 100644 --- a/godot-macros/src/class/data_models/inherent_impl.rs +++ b/godot-macros/src/class/data_models/inherent_impl.rs @@ -7,10 +7,11 @@ use crate::class::{ into_signature_info, make_constant_registration, make_method_registration, - make_signal_registrations, ConstDefinition, FuncDefinition, SignalDefinition, SignatureInfo, + make_signal_registrations, ConstDefinition, FuncDefinition, RpcInfo, RpcMode, SignalDefinition, + SignatureInfo, TransferMode, }; -use crate::util::{bail, require_api_version, KvParser}; -use crate::{util, ParseResult}; +use crate::util::{bail, ident, require_api_version, KvParser}; +use crate::{handle_mutually_exclusive_keys, util, ParseResult}; use proc_macro2::{Delimiter, Group, Ident, TokenStream}; use quote::spanned::Spanned; @@ -22,6 +23,7 @@ enum ItemAttrType { rename: Option, is_virtual: bool, has_gd_self: bool, + rpc_info: Option, }, Signal(venial::AttributeValue), Const(#[allow(dead_code)] venial::AttributeValue), @@ -58,6 +60,11 @@ pub fn transform_inherent_impl(mut impl_block: venial::Impl) -> ParseResult = funcs .into_iter() .map(|func_def| make_method_registration(&class_name, func_def)) @@ -77,16 +84,21 @@ pub fn transform_inherent_impl(mut impl_block: venial::Impl) -> ParseResult, }, + register_rpcs_fn: Some(#prv::ErasedRegisterRpcsFn { + raw: #prv::callbacks::register_user_rpcs::<#class_name>, + }), #docs - }, + }), init_level: <#class_name as ::godot::obj::GodotClass>::INIT_LEVEL, }); }; @@ -134,6 +146,7 @@ fn process_godot_fns( rename, is_virtual, has_gd_self, + rpc_info, } => { let external_attributes = function.attributes.clone(); @@ -186,6 +199,7 @@ fn process_godot_fns( external_attributes, rename, is_script_virtual: is_virtual, + rpc_info, }); } ItemAttrType::Signal(ref _attr_val) => { @@ -241,7 +255,7 @@ fn process_godot_constants(decl: &mut venial::Impl) -> ParseResult { - return bail!(constant, "#[func] can only be used on functions") + return bail!(constant, "#[func] and #[rpc] can only be used on functions") } ItemAttrType::Signal(_) => { return bail!(constant, "#[signal] can only be used on functions") @@ -346,6 +360,10 @@ where for<'a> &'a T: Spanned, { let mut found = None; + // Option((index, rpc_info)) + // `#[rpc]` is tracked separately since it's allowed both alone or in conjunction with `#[func]`. + let mut rpc_option = None; + for (index, attr) in attributes.iter().enumerate() { let Some(attr_name) = attr.get_single_path_segment() else { // Attribute of the form #[segmented::path] can't be what we are looking for @@ -381,10 +399,70 @@ where rename, is_virtual, has_gd_self, + rpc_info: None, }, } } + // #[rpc] + name if name == "rpc" => { + if rpc_option.is_some() { + return bail!( + &error_scope, + "`#[rpc]` is only allowed once per function declaration" + ); + } + + // Safe unwrap since #[rpc] must be present if we got to this point + let mut parser = KvParser::parse(attributes, "rpc")?.unwrap(); + + let mode = handle_mutually_exclusive_keys( + &mut parser, + "#[rpc]", + &["any_peer", "authority"], + )? + .map(|idx| RpcMode::from_usize(idx).unwrap()); + + let transfer_mode = handle_mutually_exclusive_keys( + &mut parser, + "#[rpc]", + &["reliable", "unreliable", "unreliable_ordered"], + )? + .map(|idx| TransferMode::from_usize(idx).unwrap()); + + let call_local = handle_mutually_exclusive_keys( + &mut parser, + "#[rpc]", + &["call_local", "call_remote"], + )? + .map(|idx| idx == 0); + + let transfer_channel = parser.handle_usize("channel")?.map(|x| x as u32); + + let config_expr = parser.handle_expr("config")?; + + let rpc_info = match (config_expr, (&mode, &transfer_mode, &call_local, &transfer_channel)) { + // Ok: Only `args = [expr]` is present. + (Some(expr), (None, None, None, None)) => RpcInfo::Expression(expr), + // Err: `args = [expr]` is present along other parameters, which is not allowed. + (Some(_), _) => return bail!(&error_scope, "`#[rpc(config = ...)]` is mutually exclusive with any other parameters(`any_peer`, `reliable`, `call_local`, `channel = 0`)"), + // Ok: `args` is not present, any combination of the other parameters is allowed.. + _ => RpcInfo::SeparatedArgs { + mode, + transfer_mode, + call_local, + transfer_channel, + } + }; + + parser.finish()?; + + rpc_option = Some((index, rpc_info)); + + // Keep parsing, there still might be a `#[func]` attribute. + continue; + } + // #[signal] name if name == "signal" => { // TODO once parameters are supported, this should probably be moved to the struct definition @@ -419,6 +497,40 @@ where found = Some(new_found); } + match (&mut found, rpc_option) { + // If `#[func]` is present, assign `rpc_info` if it was provided. + ( + Some(ItemAttr { + ty: ItemAttrType::Func { rpc_info, .. }, + .. + }), + rpc_option, + ) => { + *rpc_info = rpc_option.map(|(_, info)| info); // ignore index of #[rpc] + } + // If we reached this, `ty` is not a function, and #[rpc] is only allowed in function declarations. + (Some(_), Some(_)) => { + return bail!( + &error_scope, + "`#[rpc]` is only allowed in function declarations" + ); + } + // If only `#[rpc]` is present, assume `#[func]` with no keys. + (None, Some((index, rpc_info))) => { + found = Some(ItemAttr { + attr_name: ident("func"), + index, + ty: ItemAttrType::Func { + rename: None, + is_virtual: false, + has_gd_self: false, + rpc_info: Some(rpc_info), + }, + }) + } + _ => {} + } + Ok(found) } diff --git a/godot-macros/src/class/data_models/rpc.rs b/godot-macros/src/class/data_models/rpc.rs new file mode 100644 index 000000000..9bf95ab17 --- /dev/null +++ b/godot-macros/src/class/data_models/rpc.rs @@ -0,0 +1,159 @@ +/* + * Copyright (c) godot-rust; Bromeon and contributors. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use crate::class::FuncDefinition; +use proc_macro2::{Ident, TokenStream}; +use quote::quote; + +/// Possible ways the user can specify RPC configuration. +pub enum RpcInfo { + // Individual keys in the `rpc` attribute. + // Example: `#[rpc(any_peer, reliable, call_remote, channel = 3)]` + SeparatedArgs { + mode: Option, + transfer_mode: Option, + call_local: Option, + transfer_channel: Option, + }, + // `args` key in the `rpc` attribute. + // Example: + // const RPC_CFG: RpcConfig = RpcConfig { mode: RpcMode::Authority, ..RpcConfig::default() }; + // #[rpc(args = RPC_CFG)] + Expression(TokenStream), +} + +#[derive(Clone, Copy)] +pub enum RpcMode { + AnyPeer, + Authority, +} + +impl RpcMode { + pub fn from_usize(value: usize) -> Option { + match value { + 0 => Some(RpcMode::AnyPeer), + 1 => Some(RpcMode::Authority), + _ => None, + } + } +} + +#[derive(Clone, Copy)] +pub enum TransferMode { + Reliable, + Unreliable, + UnreliableOrdered, +} + +impl TransferMode { + pub fn from_usize(value: usize) -> Option { + match value { + 0 => Some(TransferMode::Reliable), + 1 => Some(TransferMode::Unreliable), + 2 => Some(TransferMode::UnreliableOrdered), + _ => None, + } + } +} + +pub fn make_rpc_registrations_fn(class_name: &Ident, funcs: &[FuncDefinition]) -> TokenStream { + let rpc_registrations = funcs + .iter() + .filter_map(make_rpc_registration) + .collect::>(); + + // This check is necessary because the class might not implement `WithBaseField` or `Inherits`, + // which means `to_gd` wouldn't exist or the trait bounds on `RpcConfig::register` wouldn't be satisfied. + if rpc_registrations.is_empty() { + return TokenStream::new(); + } + + quote! { + fn __register_rpcs(object: &mut dyn ::std::any::Any) { + use ::std::any::Any; + use ::godot::meta::RpcConfig; + use ::godot::classes::multiplayer_api::RpcMode; + use ::godot::classes::multiplayer_peer::TransferMode; + use ::godot::classes::Node; + use ::godot::obj::{WithBaseField, Gd}; + + let mut gd = object + .downcast_mut::<#class_name>() + .expect("bad type erasure when registering RPCs") + .to_gd(); + + let node = gd.upcast_mut::(); + #( #rpc_registrations )* + } + } +} + +fn make_rpc_registration(func_def: &FuncDefinition) -> Option { + let rpc_info = func_def.rpc_info.as_ref()?; + + let create_struct = match rpc_info { + RpcInfo::SeparatedArgs { + mode, + transfer_mode, + call_local, + transfer_channel, + } => { + let override_mode = mode.map(|mode| { + let token = match mode { + RpcMode::Authority => quote! { RpcMode::AUTHORITY }, + RpcMode::AnyPeer => quote! { RpcMode::ANY_PEER }, + }; + + quote! { let args = RpcConfig { mode: #token, ..args }; } + }); + + let override_transfer_mode = transfer_mode.map(|mode| { + let token = match mode { + TransferMode::Reliable => quote! { TransferMode::RELIABLE }, + TransferMode::Unreliable => quote! { TransferMode::UNRELIABLE }, + TransferMode::UnreliableOrdered => quote! { TransferMode::UNRELIABLE_ORDERED }, + }; + + quote! { let args = RpcConfig { transfer_mode: #token, ..args }; } + }); + + let override_call_local = call_local.map(|call_local| { + quote! { let args = RpcConfig { call_local: #call_local, ..args }; } + }); + + let override_transfer_channel = transfer_channel.map(|channel| { + quote! { let args = RpcConfig { transfer_channel: #channel, ..args }; } + }); + + quote! { + let args = RpcConfig::default(); + #override_mode + #override_transfer_mode + #override_call_local + #override_transfer_channel + } + } + RpcInfo::Expression(expr) => { + quote! { let args = #expr; } + } + }; + + let method_name_str = if let Some(rename) = &func_def.rename { + rename.to_string() + } else { + func_def.signature_info.method_name.to_string() + }; + + let registration = quote! { + { + #create_struct + args.register(node, #method_name_str) + } + }; + + Some(registration) +} diff --git a/godot-macros/src/class/derive_godot_class.rs b/godot-macros/src/class/derive_godot_class.rs index 32d66ccb8..55ef01a96 100644 --- a/godot-macros/src/class/derive_godot_class.rs +++ b/godot-macros/src/class/derive_godot_class.rs @@ -13,7 +13,7 @@ use crate::class::{ SignatureInfo, }; use crate::util::{bail, ident, path_ends_with_complex, require_api_version, KvParser}; -use crate::{util, ParseResult}; +use crate::{handle_mutually_exclusive_keys, util, ParseResult}; pub fn derive_godot_class(item: venial::Item) -> ParseResult { let class = item @@ -255,6 +255,12 @@ fn make_user_class_impl( is_tool: bool, all_fields: &[Field], ) -> (TokenStream, bool) { + #[cfg(feature = "codegen-full")] + let rpc_registrations = + quote! { ::godot::register::private::auto_register_rpcs::<#class_name>(self); }; + #[cfg(not(feature = "codegen-full"))] + let rpc_registrations = TokenStream::new(); + let onready_inits = { let mut onready_fields = all_fields .iter() @@ -310,6 +316,7 @@ fn make_user_class_impl( } fn __before_ready(&mut self) { + #rpc_registrations #onready_inits } @@ -555,18 +562,12 @@ fn handle_opposite_keys( attribute: &str, ) -> ParseResult> { let antikey = format!("no_{}", key); + let result = handle_mutually_exclusive_keys(parser, attribute, &[key, &antikey])?; - let is_key = parser.handle_alone(key)?; - let is_no_key = parser.handle_alone(&antikey)?; - - match (is_key, is_no_key) { - (true, false) => Ok(Some(true)), - (false, true) => Ok(Some(false)), - (false, false) => Ok(None), - (true, true) => bail!( - parser.span(), - "#[{attribute}] attribute keys `{key}` and `{antikey}` are mutually exclusive", - ), + if let Some(idx) = result { + Ok(Some(idx == 0)) + } else { + Ok(None) } } diff --git a/godot-macros/src/class/mod.rs b/godot-macros/src/class/mod.rs index 22603268f..7f800c137 100644 --- a/godot-macros/src/class/mod.rs +++ b/godot-macros/src/class/mod.rs @@ -16,6 +16,8 @@ mod data_models { pub mod inherent_impl; pub mod interface_trait_impl; pub mod property; + #[cfg_attr(not(feature = "codegen-full"), allow(dead_code))] + pub mod rpc; pub mod signal; } @@ -27,6 +29,7 @@ pub(crate) use data_models::func::*; pub(crate) use data_models::inherent_impl::*; pub(crate) use data_models::interface_trait_impl::*; pub(crate) use data_models::property::*; +pub(crate) use data_models::rpc::*; pub(crate) use data_models::signal::*; pub(crate) use derive_godot_class::*; pub(crate) use godot_api::*; diff --git a/godot-macros/src/lib.rs b/godot-macros/src/lib.rs index a9b5ae833..414b77176 100644 --- a/godot-macros/src/lib.rs +++ b/godot-macros/src/lib.rs @@ -23,7 +23,7 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::quote; -use crate::util::ident; +use crate::util::{bail, ident, KvParser}; // Below intra-doc link to the trait only works as HTML, not as symbol link. /// Derive macro for [`GodotClass`](../obj/trait.GodotClass.html) on structs. @@ -903,3 +903,41 @@ where TokenStream::from(result2) } + +/// Returns the index of the key in `keys` (if any) that is present. +fn handle_mutually_exclusive_keys( + parser: &mut KvParser, + attribute: &str, + keys: &[&str], +) -> ParseResult> { + let (oks, errs) = keys + .iter() + .enumerate() + .map(|(idx, key)| Ok(parser.handle_alone(key)?.then_some(idx))) + .partition::, _>(|result: &ParseResult>| result.is_ok()); + + if !errs.is_empty() { + return bail!(parser.span(), "{errs:?}"); + } + + let found_idxs = oks + .into_iter() + .filter_map(|r| r.unwrap()) // `partition` guarantees that this is `Ok` + .collect::>(); + + match found_idxs.len() { + 0 => Ok(None), + 1 => Ok(Some(found_idxs[0])), + _ => { + let offending_keys = keys + .iter() + .enumerate() + .filter(|(idx, _)| found_idxs.contains(idx)); + + bail!( + parser.span(), + "{attribute} attribute keys {offending_keys:?} are mutually exclusive" + ) + } + } +} diff --git a/godot/Cargo.toml b/godot/Cargo.toml index 6796e6f6b..e3aa46a6a 100644 --- a/godot/Cargo.toml +++ b/godot/Cargo.toml @@ -43,7 +43,7 @@ api-4-3 = ["godot-core/api-4-3"] default = ["__codegen-full"] # Private features, they are under no stability guarantee -__codegen-full = ["godot-core/codegen-full"] +__codegen-full = ["godot-core/codegen-full", "godot-macros/codegen-full"] __debug-log = ["godot-core/debug-log"] __trace = ["godot-core/trace"] diff --git a/godot/src/lib.rs b/godot/src/lib.rs index 8eada3e91..62bebfd08 100644 --- a/godot/src/lib.rs +++ b/godot/src/lib.rs @@ -178,6 +178,8 @@ pub mod register { /// Re-exports used by proc-macro API. #[doc(hidden)] pub mod private { + #[cfg(feature = "__codegen-full")] + pub use godot_core::registry::class::auto_register_rpcs; pub use godot_core::registry::godot_register_wrappers::*; pub use godot_core::registry::{constant, method}; } diff --git a/itest/rust/src/register_tests/constant_test.rs b/itest/rust/src/register_tests/constant_test.rs index d7b25f9c8..346ee3a32 100644 --- a/itest/rust/src/register_tests/constant_test.rs +++ b/itest/rust/src/register_tests/constant_test.rs @@ -171,13 +171,14 @@ godot::sys::plugin_add!( __GODOT_PLUGIN_REGISTRY in ::godot::private; ::godot::private::ClassPlugin { class_name: HasOtherConstants::class_name(), - item: ::godot::private::PluginItem::InherentImpl { + item: ::godot::private::PluginItem::InherentImpl(::godot::private::InherentImpl { register_methods_constants_fn: ::godot::private::ErasedRegisterFn { raw: ::godot::private::callbacks::register_user_methods_constants::, }, + register_rpcs_fn: None, #[cfg(all(since_api = "4.3", feature = "register-docs"))] docs: ::godot::docs::InherentImplDocs::default(), - }, + }), init_level: HasOtherConstants::INIT_LEVEL, } ); diff --git a/itest/rust/src/register_tests/mod.rs b/itest/rust/src/register_tests/mod.rs index 92b8a8f76..14c586622 100644 --- a/itest/rust/src/register_tests/mod.rs +++ b/itest/rust/src/register_tests/mod.rs @@ -12,6 +12,8 @@ mod func_test; mod gdscript_ffi_test; mod naming_tests; mod option_ffi_test; +#[cfg(feature = "codegen-full")] +mod rpc_test; mod var_test; #[cfg(since_api = "4.3")] diff --git a/itest/rust/src/register_tests/rpc_test.rs b/itest/rust/src/register_tests/rpc_test.rs new file mode 100644 index 000000000..00a89904e --- /dev/null +++ b/itest/rust/src/register_tests/rpc_test.rs @@ -0,0 +1,79 @@ +/* + * Copyright (c) godot-rust; Bromeon and contributors. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +use godot::classes::multiplayer_api::RpcMode; +use godot::classes::multiplayer_peer::TransferMode; +use godot::classes::{Engine, MultiplayerApi}; +use godot::meta::RpcConfig; +use godot::prelude::*; +use godot::test::itest; + +#[derive(GodotClass)] +#[class(init, base = Node2D)] +pub struct RpcTest { + base: Base, +} + +const CACHED_CFG: RpcConfig = RpcConfig { + mode: RpcMode::AUTHORITY, + transfer_mode: TransferMode::RELIABLE, + call_local: false, + transfer_channel: 1, +}; + +#[godot_api] +impl RpcTest { + #[rpc] + pub fn default_args(&mut self) {} + + #[rpc(any_peer)] + pub fn arg_any_peer(&mut self) {} + + #[rpc(authority)] + pub fn arg_authority(&mut self) {} + + #[rpc(reliable)] + pub fn arg_reliable(&mut self) {} + + #[rpc(unreliable)] + pub fn arg_unreliable(&mut self) {} + + #[rpc(unreliable_ordered)] + pub fn arg_unreliable_ordered(&mut self) {} + + #[rpc(call_local)] + pub fn arg_call_local(&mut self) {} + + #[rpc(call_remote)] + pub fn arg_call_remote(&mut self) {} + + #[rpc(channel = 2)] + pub fn arg_channel(&mut self) {} + + #[rpc(config = CACHED_CFG)] + pub fn arg_config(&mut self) {} +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Tests ---------------------------------------------------------------------------------------------------------------------------------------- + +// There's no way to check if the method was registered as an RPC. +// We could set up a multiplayer environment to test this in practice, but that would be a lot of work. +#[itest] +fn node_enters_tree() { + let node = RpcTest::new_alloc(); + + // Registering is done in `UserClass::__before_ready()`, and it requires a multiplayer api to exist. + let mut scene_tree = Engine::singleton() + .get_main_loop() + .unwrap() + .cast::(); + scene_tree.set_multiplayer(MultiplayerApi::create_default_interface()); + let mut root = scene_tree.get_root().unwrap(); + root.add_child(&node); + root.remove_child(&node); + node.free(); +}