diff --git a/godot-core/src/meta/mod.rs b/godot-core/src/meta/mod.rs index b5a2e4a6f..df1bf73c2 100644 --- a/godot-core/src/meta/mod.rs +++ b/godot-core/src/meta/mod.rs @@ -40,6 +40,7 @@ mod godot_convert; mod method_info; mod property_info; mod ref_arg; +mod rpc_args; mod sealed; mod signature; mod traits; @@ -47,6 +48,7 @@ mod traits; pub mod error; pub use class_name::ClassName; pub use godot_convert::{FromGodot, GodotConvert, ToGodot}; +pub use rpc_args::RpcArgs; pub use traits::{ArrayElement, GodotType, PackedArrayElement}; pub(crate) use crate::impl_godot_as_self; diff --git a/godot-core/src/meta/rpc_args.rs b/godot-core/src/meta/rpc_args.rs new file mode 100644 index 000000000..e660102ac --- /dev/null +++ b/godot-core/src/meta/rpc_args.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(Debug, Clone, Copy)] +pub struct RpcArgs { + pub mode: RpcMode, + pub transfer_mode: TransferMode, + pub call_local: bool, + pub transfer_channel: u32, +} + +impl Default for RpcArgs { + fn default() -> Self { + Self { + mode: RpcMode::AUTHORITY, + transfer_mode: TransferMode::UNRELIABLE, + call_local: false, + transfer_channel: 0, + } + } +} + +impl RpcArgs { + /// 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..2542db436 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, PluginItem, +}; pub use crate::storage::{as_storage, Storage}; pub use sys::out; 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..c9b0dfa12 100644 --- a/godot-core/src/registry/class.rs +++ b/godot-core/src/registry/class.rs @@ -11,7 +11,7 @@ use std::ptr; use crate::init::InitLevel; use crate::meta::ClassName; use crate::obj::{cap, GodotClass}; -use crate::private::{ClassPlugin, PluginItem}; +use crate::private::{ClassPlugin, PluginItem, __godot_rust_plugin___GODOT_PLUGIN_REGISTRY}; use crate::registry::callbacks; use crate::registry::plugin::ErasedRegisterFn; use crate::{godot_error, sys}; @@ -200,6 +200,28 @@ pub fn unregister_classes(init_level: InitLevel) { } } +pub fn auto_register_rpcs(object: &mut T) { + let class_name = T::class_name(); + + // 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(); + + // Find the element that matches our class, and call the closure if it exists. + for elem in plugins.iter().filter(|elem| elem.class_name == class_name) { + if let PluginItem::InherentImpl { + register_rpcs_fn, .. + } = &elem.item + { + if let Some(closure) = register_rpcs_fn { + (closure.raw)(object); + } + + // We found the element, break out of the loop regardless of the value of `register_rpcs_fn`. + break; + } + } +} + fn global_loaded_classes() -> GlobalGuard<'static, HashMap>> { match LOADED_CLASSES.try_lock() { Ok(it) => it, @@ -283,6 +305,7 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) { PluginItem::InherentImpl { register_methods_constants_fn, + register_rpcs_fn: _, #[cfg(all(since_api = "4.3", feature = "docs"))] docs: _, } => { diff --git a/godot-core/src/registry/plugin.rs b/godot-core/src/registry/plugin.rs index 0453dfc1a..4b115c7fd 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,17 @@ 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) + } +} + /// 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, @@ -107,6 +117,10 @@ pub enum PluginItem { /// /// Always present since that's the entire point of this `impl` block. 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. + register_rpcs_fn: Option, #[cfg(all(since_api = "4.3", feature = "docs"))] docs: InherentImplDocs, }, 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..283d5f038 100644 --- a/godot-macros/src/class/data_models/inherent_impl.rs +++ b/godot-macros/src/class/data_models/inherent_impl.rs @@ -7,12 +7,13 @@ use crate::class::{ into_signature_info, make_constant_registration, make_method_registration, - make_signal_registrations, ConstDefinition, FuncDefinition, SignalDefinition, SignatureInfo, + make_rpc_registrations_fn, make_signal_registrations, ConstDefinition, FuncDefinition, RpcInfo, + RpcMode, RpcSeparatedArgs, SignalDefinition, SignatureInfo, TransferMode, }; use crate::util::{bail, require_api_version, KvParser}; use crate::{util, ParseResult}; -use proc_macro2::{Delimiter, Group, Ident, TokenStream}; +use proc_macro2::{Delimiter, Group, Ident, Span, TokenStream}; use quote::spanned::Spanned; use quote::{format_ident, quote}; @@ -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), @@ -48,7 +50,7 @@ pub fn transform_inherent_impl(mut impl_block: venial::Impl) -> ParseResult ParseResult = funcs .into_iter() .map(|func_def| make_method_registration(&class_name, func_def)) @@ -77,6 +81,8 @@ pub fn transform_inherent_impl(mut impl_block: venial::Impl) -> ParseResult 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 +143,7 @@ fn process_godot_fns( rename, is_virtual, has_gd_self, + rpc_info, } => { let external_attributes = function.attributes.clone(); @@ -186,6 +196,7 @@ fn process_godot_fns( external_attributes, rename, is_script_virtual: is_virtual, + rpc_info, }); } ItemAttrType::Signal(ref _attr_val) => { @@ -241,7 +252,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 +357,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 +396,110 @@ 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 = { + let any_peer = parser.handle_alone("any_peer")?; + let authority = parser.handle_alone("authority")?; + + match (any_peer, authority) { + // Ok: Only `any_peer` is present. + (true, false) => Some(RpcMode::AnyPeer), + // Ok: Only `authority` is present. + (false, true) => Some(RpcMode::Authority), + // Ok: Neither are present, we will assume the default value later on. + (false, false) => None, + // Err: Both are present, which is not allowed. + (true, true) => { + return bail!( + &error_scope, + "the keys `any_peer` and `authority` are mutually exclusive" + ) + } + } + }; + + let transfer_mode = { + let reliable = parser.handle_alone("reliable")?; + let unreliable = parser.handle_alone("unreliable")?; + let unreliable_ordered = parser.handle_alone("unreliable_ordered")?; + + match (reliable, unreliable, unreliable_ordered) { + // Ok: Only `reliable` is present. + (true, false, false) => Some(TransferMode::Reliable), + // Ok: Only `unreliable` is present. + (false, true, false) => Some(TransferMode::Unreliable), + // Ok: Only `unreliable_ordered` is present. + (false, false, true) => Some(TransferMode::UnreliableOrdered), + // Ok: None is present, we will assume the default value later on. + (false, false, false) => None, + // Err: More than one is present, which is not allowed. + _ => return bail!(&error_scope, "the keys `reliable`, `unreliable` and `unreliable_ordered` are mutually exclusive"), + } + }; + + let call_local = { + let call_local = parser.handle_alone("call_local")?; + let call_remote = parser.handle_alone("call_remote")?; + + match (call_local, call_remote) { + // Ok: Only `call_local` is present. + (true, false) => Some(true), + // Ok: Only `call_remote` is present. + (false, true) => Some(false), + // Ok: Neither are present, we will assume the default value later on. + (false, false) => None, + // Err: Both are present, which is not allowed. + _ => { + return bail!( + &error_scope, + "the keys `call_local` and `call_remote` are mutually exclusive" + ) + } + } + }; + + 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 = [expr])]` 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(RpcSeparatedArgs { + 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 +534,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::new("func", Span::call_site()), + 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..57f12711c --- /dev/null +++ b/godot-macros/src/class/data_models/rpc.rs @@ -0,0 +1,150 @@ +/* + * 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(RpcSeparatedArgs), + // `args` key in the `rpc` attribute. + // Example: + // const RPC_ARGS: RpcArgs = RpcArgs { mode: RpcMode::Authority, ..RpcArgs::default() }; + // #[rpc(args = RPC_ARGS)] + Expression(TokenStream), +} + +/// See [Godot documentation](https://docs.godotengine.org/en/stable/tutorials/networking/high_level_multiplayer.html#remote-procedure-calls) +pub struct RpcSeparatedArgs { + pub mode: Option, + pub transfer_mode: Option, + pub call_local: Option, + pub transfer_channel: Option, +} + +pub enum RpcMode { + Authority, + AnyPeer, +} + +pub enum TransferMode { + Reliable, + Unreliable, + UnreliableOrdered, +} + +pub fn make_rpc_registrations_fn( + class_name: &Ident, + funcs: &mut [FuncDefinition], +) -> Option { + let rpc_registrations = funcs + .iter_mut() + .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 `RpcArgs::register` wouldn't be satisfied. + if rpc_registrations.is_empty() { + return None; + } + + let result = quote! { + fn __register_rpcs(object: &mut dyn ::std::any::Any) { + use ::std::any::Any; + use ::godot::meta::RpcArgs; + 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") + .to_gd(); + + let node = gd.upcast_mut::(); + #( #rpc_registrations )* + } + }; + + Some(result) +} + +fn make_rpc_registration(func_def: &mut FuncDefinition) -> Option { + let rpc_info = func_def.rpc_info.take()?; + + let create_struct = match rpc_info { + RpcInfo::SeparatedArgs(RpcSeparatedArgs { + 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 = RpcArgs { 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 = RpcArgs { transfer_mode: #token, ..args }; } + }); + + let override_call_local = call_local.map(|call_local| { + let token = if call_local { + quote! { true } + } else { + quote! { false } + }; + + quote! { let args = RpcArgs { call_local: #token, ..args }; } + }); + + let override_transfer_channel = transfer_channel.map(|channel| { + quote! { let args = RpcArgs { transfer_channel: #channel, ..args }; } + }); + + quote! { + let args = RpcArgs::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..c61da9e34 100644 --- a/godot-macros/src/class/derive_godot_class.rs +++ b/godot-macros/src/class/derive_godot_class.rs @@ -310,6 +310,7 @@ fn make_user_class_impl( } fn __before_ready(&mut self) { + ::godot::register::private::auto_register_rpcs::<#class_name>(self); #onready_inits } diff --git a/godot-macros/src/class/mod.rs b/godot-macros/src/class/mod.rs index 22603268f..a35a5ad6e 100644 --- a/godot-macros/src/class/mod.rs +++ b/godot-macros/src/class/mod.rs @@ -16,6 +16,7 @@ mod data_models { pub mod inherent_impl; pub mod interface_trait_impl; pub mod property; + pub mod rpc; pub mod signal; } @@ -27,6 +28,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/src/lib.rs b/godot/src/lib.rs index 8eada3e91..e3de54aa8 100644 --- a/godot/src/lib.rs +++ b/godot/src/lib.rs @@ -178,6 +178,7 @@ pub mod register { /// Re-exports used by proc-macro API. #[doc(hidden)] pub mod private { + 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..30b13d239 100644 --- a/itest/rust/src/register_tests/constant_test.rs +++ b/itest/rust/src/register_tests/constant_test.rs @@ -175,6 +175,7 @@ godot::sys::plugin_add!( 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(), }, diff --git a/itest/rust/src/register_tests/mod.rs b/itest/rust/src/register_tests/mod.rs index 92b8a8f76..e53da89f1 100644 --- a/itest/rust/src/register_tests/mod.rs +++ b/itest/rust/src/register_tests/mod.rs @@ -12,6 +12,7 @@ mod func_test; mod gdscript_ffi_test; mod naming_tests; mod option_ffi_test; +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..6ccda87b4 --- /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::RpcArgs; +use godot::prelude::*; +use godot::test::itest; + +#[derive(GodotClass)] +#[class(init, base = Node2D)] +pub struct RpcTest { + base: Base, +} + +const CACHED_CFG: RpcArgs = RpcArgs { + 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(); +}