diff --git a/godot-codegen/src/generator/virtual_traits.rs b/godot-codegen/src/generator/virtual_traits.rs index 3db45cc35..809a23851 100644 --- a/godot-codegen/src/generator/virtual_traits.rs +++ b/godot-codegen/src/generator/virtual_traits.rs @@ -104,6 +104,43 @@ fn special_virtual_methods(notification_enum_name: &Ident) -> TokenStream { unimplemented!() } + /// Called whenever the Godot engine needs to determine the properties an object has. + /// + /// This method can be used to dynamically update the properties displayed by the editor depending on various conditions. This should + /// usually be combined with `#[class(tool)]` so that the code actually runs in the editor. Additionally if the property list changes + /// you need to call [`notify_property_list_changed`](crate::engine::Object::notify_property_list_changed) to actually notify the engine + /// that the property list has changed, otherwise nothing will appear to have happened. + /// + /// The returned `Vec` cannot have more than `u32::MAX` or `usize::MAX - 1` elements, whichever is smaller. + /// + /// See also in Godot docs: + /// * [`Object::_get_property_list`](https://docs.godotengine.org/en/stable/classes/class_object.html#class-object-private-method-get-property-list). + fn get_property_list(&self) -> Vec { + unimplemented!() + } + + /// Called by Godot to determine if a property has a custom default value. + /// + /// Should return `true` when the property has a custom default value, otherwise should return `false`. Must be used in conjunction with + /// [`property_get_revert()`] to specify the custom default value. + /// + /// See also in Godot docs: + /// * [`Object::_property_can_revert`](https://docs.godotengine.org/en/stable/classes/class_object.html#class-object-private-method-property-can-revert). + fn property_can_revert(&self, property: StringName) -> bool { + unimplemented!() + } + + /// Called by Godot to determine the custom default value of a property. + /// + /// Should return the given property's custom default value as `Some(value)`, or `None` if the given property doesn't have a custom + /// default value. + /// + /// See also in Godot docs: + /// * [`Object::_property_get_revert`](https://docs.godotengine.org/en/stable/classes/class_object.html#class-object-private-method-property-get-revert). + fn property_get_revert(&self, property: StringName) -> Option { + unimplemented!() + } + } } diff --git a/godot-codegen/src/util.rs b/godot-codegen/src/util.rs index d82370291..bfe10cc8c 100644 --- a/godot-codegen/src/util.rs +++ b/godot-codegen/src/util.rs @@ -22,7 +22,7 @@ pub fn make_imports() -> TokenStream { quote! { use godot_ffi as sys; use crate::builtin::*; - use crate::builtin::meta::{ClassName, PtrcallReturnUnit, PtrcallReturnT, PtrcallReturnOptionGdT, PtrcallSignatureTuple, VarcallSignatureTuple}; + use crate::builtin::meta::{ClassName, PtrcallReturnUnit, PtrcallReturnT, PtrcallReturnOptionGdT, PtrcallSignatureTuple, VarcallSignatureTuple, PropertyInfo}; use crate::engine::native::*; use crate::engine::Object; use crate::obj::Gd; diff --git a/godot-core/src/builtin/meta/mod.rs b/godot-core/src/builtin/meta/mod.rs index d56dffef1..7eb4bf92d 100644 --- a/godot-core/src/builtin/meta/mod.rs +++ b/godot-core/src/builtin/meta/mod.rs @@ -23,6 +23,7 @@ pub(crate) use godot_convert::convert_error::*; use crate::builtin::*; use crate::engine::global; +use crate::property::{Export, PropertyHintInfo, Var}; use godot_ffi as sys; use registration::method::MethodParamOrReturnInfo; use sys::{GodotFfi, GodotNullableFfi}; @@ -241,21 +242,93 @@ where // ---------------------------------------------------------------------------------------------------------------------------------------------- -/// Rusty abstraction of `sys::GDExtensionPropertyInfo`. +/// Representation of a property in Godot. /// -/// Keeps the actual allocated values (the `sys` equivalent only keeps pointers, which fall out of scope). +/// Stores all info needed to inform Godot about how to interpret a property. This is used for actual properties, function arguments, +/// return types, signal arguments, and other similar cases. +/// +/// A mismatch between property info and the actual type of a property may lead to runtime errors as Godot tries to use the property in the +/// wrong way, such as by inserting the wrong type or expecting a different type to be returned. +/// +/// Rusty abstraction of `sys::GDExtensionPropertyInfo`, keeps the actual allocated values (the `sys` equivalent only keeps pointers, which +/// fall out of scope). #[derive(Debug)] -// Note: is not #[non_exhaustive], so adding fields is a breaking change. Mostly used internally at the moment though. +// It is uncertain if we want to add more fields to this in the future, so we'll mark it `non_exhaustive` as a precautionary measure. +#[non_exhaustive] pub struct PropertyInfo { + /// The variant type of the property. + /// + /// Note that for classes this will be `Object`, and the `class_name` field will specify what specific class this property is. pub variant_type: VariantType, + /// The class name of the property. + /// + /// This only matters if `variant_type` is `Object`. Otherwise it's ignored by Godot. pub class_name: ClassName, + /// The name this property will have in Godot. pub property_name: StringName, + /// The property hint that will determine how Godot interprets this value. + /// + /// See Godot docs for more information: + /// * [`PropertyHint`](https://docs.godotengine.org/en/stable/classes/class_%40globalscope.html#enum-globalscope-propertyhint). pub hint: global::PropertyHint, + /// Extra information used in conjunction with `hint`. pub hint_string: GString, + /// How Godot will use this property. + /// + /// See Godot docs for more inormation: + /// * [`PropertyUsageFlags`](https://docs.godotengine.org/en/stable/classes/class_%40globalscope.html#enum-globalscope-propertyusageflags). pub usage: global::PropertyUsageFlags, } impl PropertyInfo { + /// Create a new `PropertyInfo` for a property that isn't exported to the editor. + /// + /// `P` is the type the property will be declared as, and `property_name` is the name the property will have. + pub fn new_var(property_name: &str) -> Self { + let PropertyHintInfo { hint, hint_string } = P::property_hint(); + + Self { + hint, + hint_string, + usage: global::PropertyUsageFlags::NO_EDITOR, + ..P::Via::property_info(property_name) + } + } + + /// Create a new `PropertyInfo` for a property that is exported to the editor. + /// + /// `P` is the type the property will be declared as, and `property_name` is the name the property will have. + pub fn new_export(property_name: &str) -> Self { + let PropertyHintInfo { hint, hint_string } = P::default_export_info(); + + Self { + hint, + hint_string, + usage: global::PropertyUsageFlags::DEFAULT, + ..P::Via::property_info(property_name) + } + } + + /// Create a new `PropertyInfo` for the return type of a method. + /// + /// `P` is the type the property will be declared as. + pub fn new_return() -> Self { + Self { + usage: global::PropertyUsageFlags::NONE, + ..P::Via::property_info("") + } + } + + /// Create a new `PropertyInfo` for an argument of a method. + /// + /// `P` is the type the property will be declared as, and `property_name` is the name the argument will have. + pub fn new_arg(arg_name: &str) -> Self { + Self { + usage: global::PropertyUsageFlags::NONE, + ..P::Via::property_info(arg_name) + } + } + /// Converts to the FFI type. Keep this object allocated while using that! pub fn property_sys(&self) -> sys::GDExtensionPropertyInfo { use crate::obj::EngineBitfield as _; @@ -271,6 +344,44 @@ impl PropertyInfo { } } + /// Converts to the FFI type. + /// + /// Unlike [`property_sys`](PropertyInfo::property_sys) this object does not need to be kept allocated while using the returned value, + /// however if you do not explicitly free the returned value at some point then this will lead to a memory leak. See + /// [`drop_property_sys`](PropertyInfo::drop_property_sys). + pub fn into_property_sys(self) -> sys::GDExtensionPropertyInfo { + use crate::obj::EngineBitfield as _; + use crate::obj::EngineEnum as _; + + let Self { + variant_type, + class_name, + property_name, + hint, + hint_string, + usage, + } = self; + + sys::GDExtensionPropertyInfo { + type_: variant_type.sys(), + name: property_name.into_string_sys(), + class_name: class_name.string_sys(), + hint: u32::try_from(hint.ord()).expect("hint.ord()"), + hint_string: hint_string.into_string_sys(), + usage: u32::try_from(usage.ord()).expect("usage.ord()"), + } + } + + /// Consumes a [sys::GDExtensionPropertyInfo]. + /// + /// # Safety + /// + /// The given property info must have been returned from a call to [`into_property_sys`](PropertyInfo::into_property_sys). + pub unsafe fn drop_property_sys(property_sys: sys::GDExtensionPropertyInfo) { + let _property_name = StringName::from_string_sys(property_sys.name); + let _hint_string = GString::from_string_sys(property_sys.hint_string); + } + pub fn empty_sys() -> sys::GDExtensionPropertyInfo { use crate::obj::EngineBitfield as _; use crate::obj::EngineEnum as _; diff --git a/godot-core/src/builtin/string/gstring.rs b/godot-core/src/builtin/string/gstring.rs index 435a76560..258de41fa 100644 --- a/godot-core/src/builtin/string/gstring.rs +++ b/godot-core/src/builtin/string/gstring.rs @@ -142,6 +142,12 @@ impl GString { pub fn as_inner(&self) -> inner::InnerString { inner::InnerString::from_outer(self) } + + #[doc(hidden)] + pub fn into_string_sys(self) -> sys::GDExtensionStringPtr { + let string_name = Box::leak(Box::new(self)); + string_name.string_sys() + } } // SAFETY: diff --git a/godot-core/src/builtin/string/string_name.rs b/godot-core/src/builtin/string/string_name.rs index 4ce8508b4..c631acf0b 100644 --- a/godot-core/src/builtin/string/string_name.rs +++ b/godot-core/src/builtin/string/string_name.rs @@ -130,6 +130,12 @@ impl StringName { inner::InnerStringName::from_outer(self) } + #[doc(hidden)] + pub fn into_string_sys(self) -> sys::GDExtensionStringNamePtr { + let string_name = Box::leak(Box::new(self)); + string_name.string_sys() + } + /// Increment ref-count. This may leak memory if used wrongly. fn inc_ref(&self) { std::mem::forget(self.clone()); diff --git a/godot-core/src/obj/traits.rs b/godot-core/src/obj/traits.rs index f85411e41..15d68ff5a 100644 --- a/godot-core/src/obj/traits.rs +++ b/godot-core/src/obj/traits.rs @@ -406,6 +406,7 @@ where /// Capability traits, providing dedicated functionalities for Godot classes pub mod cap { use super::*; + use crate::builtin::meta::PropertyInfo; use crate::builtin::{StringName, Variant}; use crate::obj::{Base, Bounds, Gd}; @@ -486,6 +487,24 @@ pub mod cap { fn __godot_set_property(&mut self, property: StringName, value: Variant) -> bool; } + #[doc(hidden)] + pub trait GodotGetPropertyList: GodotClass { + #[doc(hidden)] + fn __godot_get_property_list(&self) -> Vec; + } + + #[doc(hidden)] + pub trait GodotPropertyCanRevert: GodotClass { + #[doc(hidden)] + fn __godot_property_can_revert(&self, property: StringName) -> bool; + } + + #[doc(hidden)] + pub trait GodotPropertyGetRevert: GodotClass { + #[doc(hidden)] + fn __godot_property_get_revert(&self, property: StringName) -> Option; + } + /// Auto-implemented for `#[godot_api] impl MyClass` blocks pub trait ImplementsGodotApi: GodotClass { #[doc(hidden)] diff --git a/godot-core/src/registry/callbacks.rs b/godot-core/src/registry/callbacks.rs index 5ed7a2333..58a212870 100644 --- a/godot-core/src/registry/callbacks.rs +++ b/godot-core/src/registry/callbacks.rs @@ -11,6 +11,7 @@ #![allow(clippy::missing_safety_doc)] use crate::builder::ClassBuilder; +use crate::builtin::meta::PropertyInfo; use crate::builtin::{StringName, Variant}; use crate::obj::{cap, Base, GodotClass, UserClass}; use crate::storage::{as_storage, InstanceStorage, Storage, StorageRefCounted}; @@ -198,6 +199,162 @@ pub unsafe extern "C" fn set_property( T::__godot_set_property(&mut *instance, property, value) as sys::GDExtensionBool } +pub unsafe extern "C" fn get_property_list( + instance: sys::GDExtensionClassInstancePtr, + count: *mut u32, +) -> *const sys::GDExtensionPropertyInfo { + let storage = as_storage::(instance); + let instance = storage.get(); + + let property_infos = T::__godot_get_property_list(&*instance); + let property_count: u32 = property_infos + .len() + .try_into() + .expect("cannot pass more properties than `u32::MAX`"); + let vec_length: usize = property_count + .try_into() + .expect("gdext does not support targets with `u32` bigger than `usize`"); + // This can only fail if vec_length = u32::MAX and usize is the same size as u32. + let vec_length = vec_length + .checked_add(1) + .expect("cannot pass more properties than `usize::MAX - 1`"); + + // Use `ManuallyDrop` here so we intentionally leak this vec, as we want to pass ownership of this array to Godot until ownership is + // returned to us in `free_property_list`. + let mut list = Vec::with_capacity(vec_length); + list.extend( + property_infos + .into_iter() + .map(PropertyInfo::into_property_sys), + ); + + // Use as null-terminator. `PropertyInfo::into_property_sys` will never create a `sys::GDExtensionPropertyInfo` with values like this. + // This is *important*. If we do have a value before the final one that is equal to `empty_sys()` then we would later call + // `Vec::from_raw_parts` with an incorrect capacity and trigger UB. + list.push(PropertyInfo::empty_sys()); + + // So at least in debug mode, let's check that our assumptions about `list` hold true. + if cfg!(debug_assertions) { + for prop in list.iter().take(vec_length - 1) { + assert!( + !prop.name.is_null(), + "Invalid property info found: {:?}", + prop + ); + } + assert_eq!(list.len(), vec_length); + assert_eq!(list.capacity(), vec_length); + assert!((property_count as usize) < vec_length) + } + + // SAFETY: Godot gives us exclusive ownership over `count` for the purposes of returning the length of the property list, so we can safely + // write a value of type `u32` to `count`. + unsafe { + count.write(property_count); + } + + let slice = Box::into_raw(list.into_boxed_slice()); + + // Since `list` is in a `ManuallyDrop`, this leaks the `list` and thus passes ownership of the vec to the caller (which is gonna be Godot). + (*slice).as_mut_ptr() as *const _ +} + +/// Get the length of a "null"-terminated array. +/// +/// Where "null" here is defined by `terminator_fn` returning true. +/// +/// # Panics +/// +/// The given array has more than `isize::MAX` elements. +/// +/// # Safety +/// +/// `arr` must be dereferencable to `&T`. +/// +/// Whenever `terminator_fn(&*arr.offset(i))` is false, for every i in `0..n`, then: +/// - `arr.offset(n)` must be safe, see safety docs for `offset`. +/// - `arr.offset(n)` must be dereferencable to `&T`. +unsafe fn arr_length(arr: *const T, terminator_fn: impl Fn(&T) -> bool) -> usize { + let mut list_index = 0; + loop { + // SAFETY: `terminator_fn` has not returned `true` yet, therefore we are allowed to do `arr.offset(list_index)`. + let arr_offset = unsafe { arr.offset(list_index) }; + + // SAFETY: `terminator_fn` has not returned `true` yet, therefore we can dereference `arr_offset` to `&T`. + let elem = unsafe { &*arr_offset }; + + if terminator_fn(elem) { + break; + } + + list_index = list_index + .checked_add(1) + .expect("there should not be more than `isize::MAX` elements in the array"); + } + + usize::try_from(list_index).expect("the size of the array should never be negative") + 1 +} + +pub unsafe extern "C" fn free_property_list( + _instance: sys::GDExtensionClassInstancePtr, + list: *const sys::GDExtensionPropertyInfo, +) { + // SAFETY: `list` was created in `get_property_list` from a `Vec` with some fixed length, where the final element is a + // `sys::GDExtensionPropertyInfo` with a null `name` field. So all the given safety conditions hold. + let list_length = unsafe { arr_length(list, |prop| prop.name.is_null()) }; + + // SAFETY: `list` was created in `get_property_list` from a `Vec` with length `list_length`. The length and capacity of this list + // are the same, as the vec was made using `with_capacity` to have exactly the same capacity as the amount of elements we put in the vec. + let v = unsafe { Vec::from_raw_parts(sys::force_mut_ptr(list), list_length, list_length) }; + + for prop in v.into_iter().take(list_length - 1) { + // SAFETY: All elements of `v` were created using `into_property_sys`, except for the last one. We are iterating over all elements + // except the last one, so all `prop` values were created with `into_property_sys`. + unsafe { crate::builtin::meta::PropertyInfo::drop_property_sys(prop) } + } +} + +pub unsafe extern "C" fn property_can_revert( + instance: sys::GDExtensionClassInstancePtr, + name: sys::GDExtensionConstStringNamePtr, +) -> sys::GDExtensionBool { + let storage = as_storage::(instance); + let instance = storage.get(); + + let property = StringName::from_string_sys(sys::force_mut_ptr(name)); + + std::mem::forget(property.clone()); + + T::__godot_property_can_revert(&*instance, property) as sys::GDExtensionBool +} + +pub unsafe extern "C" fn user_property_get_revert_fn( + instance: sys::GDExtensionClassInstancePtr, + name: sys::GDExtensionConstStringNamePtr, + ret: sys::GDExtensionVariantPtr, +) -> sys::GDExtensionBool { + let storage = as_storage::(instance); + let instance = storage.get(); + + let property = StringName::from_string_sys(sys::force_mut_ptr(name)); + + std::mem::forget(property.clone()); + + let value = T::__godot_property_get_revert(&*instance, property); + + match value { + Some(value) => { + // SAFETY: `ret` is a pointer Godot has given us exclusive ownership over for the purpose of writing a `Variant` to when we return + // `true` from this function. So this write is safe. + unsafe { + value.move_var_ptr(ret); + } + true as sys::GDExtensionBool + } + None => false as sys::GDExtensionBool, + } +} + pub unsafe extern "C" fn reference(instance: sys::GDExtensionClassInstancePtr) { let storage = as_storage::(instance); storage.on_inc_ref(); diff --git a/godot-core/src/registry/mod.rs b/godot-core/src/registry/mod.rs index b967cc648..89dfc4109 100644 --- a/godot-core/src/registry/mod.rs +++ b/godot-core/src/registry/mod.rs @@ -176,6 +176,35 @@ pub enum PluginItem { ) -> sys::GDExtensionBool, >, + user_get_property_list_fn: Option< + unsafe extern "C" fn( + p_instance: sys::GDExtensionClassInstancePtr, + r_count: *mut u32, + ) -> *const sys::GDExtensionPropertyInfo, + >, + + user_free_property_list_fn: Option< + unsafe extern "C" fn( + p_instance: sys::GDExtensionClassInstancePtr, + p_list: *const sys::GDExtensionPropertyInfo, + ), + >, + + user_property_can_revert_fn: Option< + unsafe extern "C" fn( + p_instance: sys::GDExtensionClassInstancePtr, + p_name: sys::GDExtensionConstStringNamePtr, + ) -> sys::GDExtensionBool, + >, + + user_property_get_revert_fn: Option< + unsafe extern "C" fn( + p_instance: sys::GDExtensionClassInstancePtr, + p_name: sys::GDExtensionConstStringNamePtr, + r_ret: sys::GDExtensionVariantPtr, + ) -> sys::GDExtensionBool, + >, + /// Callback for other virtuals. get_virtual_fn: unsafe extern "C" fn( p_userdata: *mut std::os::raw::c_void, @@ -446,6 +475,10 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) { user_on_notification_fn, user_set_fn, user_get_fn, + user_get_property_list_fn: user_get_property_list, + user_free_property_list_fn: user_free_property_list, + user_property_can_revert_fn: user_property_can_revert, + user_property_get_revert_fn: user_property_get_revert, get_virtual_fn, } => { c.user_register_fn = user_register_fn; @@ -467,6 +500,10 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) { c.godot_params.notification_func = user_on_notification_fn; c.godot_params.set_func = user_set_fn; c.godot_params.get_func = user_get_fn; + c.godot_params.get_property_list_func = user_get_property_list; + c.godot_params.free_property_list_func = user_free_property_list; + c.godot_params.property_can_revert_func = user_property_can_revert; + c.godot_params.property_get_revert_func = user_property_get_revert; c.user_virtual_fn = Some(get_virtual_fn); } } diff --git a/godot-core/src/storage/instance_storage.rs b/godot-core/src/storage/instance_storage.rs index 34d7a5db2..d8b97bdba 100644 --- a/godot-core/src/storage/instance_storage.rs +++ b/godot-core/src/storage/instance_storage.rs @@ -57,6 +57,9 @@ impl AtomicLifecycle { /// /// It must be safe to drop this storage if we have a `&mut` reference to the storage and /// [`is_bound()`](Storage::is_bound()) returns `false`. +/// +/// The pointer returned from [`store_property_list`](Storage::store_property_list) must be valid for reads until +/// [`free_property_list`](Storage::free_property_list) is called. pub unsafe trait Storage { /// The type of instances stored by this storage. type Instance: GodotClass; diff --git a/godot-macros/src/class/data_models/property.rs b/godot-macros/src/class/data_models/property.rs index 825eb8e29..0d2afcde3 100644 --- a/godot-macros/src/class/data_models/property.rs +++ b/godot-macros/src/class/data_models/property.rs @@ -157,14 +157,12 @@ pub fn make_property_impl(class_name: &Ident, fields: &Fields) -> TokenStream { let (hint, hint_string) = #hint; let usage = #usage_flags; - let property_info = ::godot::builtin::meta::PropertyInfo { - variant_type: #field_variant_type, - class_name: #field_class_name, - property_name: #field_name.into(), - hint, - hint_string, - usage, - }; + let mut property_info = ::godot::builtin::meta::PropertyInfo::new_var::<#field_type>(#field_name); + property_info.variant_type = #field_variant_type; + property_info.class_name = #field_class_name; + property_info.hint = hint; + property_info.hint_string = hint_string; + property_info.usage = usage; let getter_name = ::godot::builtin::StringName::from(#getter_name); let setter_name = ::godot::builtin::StringName::from(#setter_name); diff --git a/godot-macros/src/class/godot_api.rs b/godot-macros/src/class/godot_api.rs index 098c1bf6c..b6d5ce50d 100644 --- a/godot-macros/src/class/godot_api.rs +++ b/godot-macros/src/class/godot_api.rs @@ -611,6 +611,9 @@ fn transform_trait_impl(original_impl: venial::Impl) -> ParseResult let mut on_notification_impl = TokenStream::new(); let mut get_property_impl = TokenStream::new(); let mut set_property_impl = TokenStream::new(); + let mut get_property_list_impl = TokenStream::new(); + let mut property_can_revert_impl = TokenStream::new(); + let mut property_get_revert_impl = TokenStream::new(); let mut register_fn = None; let mut create_fn = None; @@ -619,6 +622,10 @@ fn transform_trait_impl(original_impl: venial::Impl) -> ParseResult let mut on_notification_fn = None; let mut get_property_fn = None; let mut set_property_fn = None; + let mut get_property_list_fn = None; + let mut free_property_list_fn = None; + let mut property_can_revert_fn = None; + let mut property_get_revert_fn = None; let mut virtual_methods = vec![]; let mut virtual_method_cfg_attrs = vec![]; @@ -793,6 +800,80 @@ fn transform_trait_impl(original_impl: venial::Impl) -> ParseResult }); } + "get_property_list" => { + get_property_list_impl = quote! { + #(#cfg_attrs)* + impl ::godot::obj::cap::GodotGetPropertyList for #class_name { + fn __godot_get_property_list(&self) -> Vec<::godot::builtin::meta::PropertyInfo> { + use ::godot::obj::UserClass as _; + + #[cfg(before_api = "4.3")] + if ::godot::private::is_class_inactive(Self::__config().is_tool) { + return Vec::new(); + } + + ::get_property_list(self) + } + } + }; + + get_property_list_fn = Some(quote! { + #(#cfg_attrs)* + () => Some(#prv::callbacks::get_property_list::<#class_name>), + }); + + free_property_list_fn = Some(quote! { + #(#cfg_attrs)* + () => Some(#prv::callbacks::free_property_list::<#class_name>), + }); + } + + "property_can_revert" => { + property_can_revert_impl = quote! { + #(#cfg_attrs)* + impl ::godot::obj::cap::GodotPropertyCanRevert for #class_name { + fn __godot_property_can_revert(&self, property: ::godot::builtin::StringName) -> bool { + use ::godot::obj::UserClass as _; + + #[cfg(before_api = "4.3")] + if ::godot::private::is_class_inactive(Self::__config().is_tool) { + return false; + } + + ::property_can_revert(self, property) + } + } + }; + + property_can_revert_fn = Some(quote! { + #(#cfg_attrs)* + () => Some(#prv::callbacks::property_can_revert::<#class_name>), + }); + } + + "property_get_revert" => { + property_get_revert_impl = quote! { + #(#cfg_attrs)* + impl ::godot::obj::cap::GodotPropertyGetRevert for #class_name { + fn __godot_property_get_revert(&mut self, property: ::godot::builtin::StringName) -> Option<::godot::builtin::Variant> { + use ::godot::obj::UserClass as _; + + #[cfg(before_api = "4.3")] + if ::godot::private::is_class_inactive(Self::__config().is_tool) { + return None; + } + + ::property_get_revert(self, property) + } + } + }; + + property_get_revert_fn = Some(quote! { + #(#cfg_attrs)* + () => Some(#prv::callbacks::property_get_revert::<#class_name>), + }); + } + // Other virtual methods, like ready, process etc. _ => { let method = util::reduce_to_signature(method); @@ -863,6 +944,10 @@ fn transform_trait_impl(original_impl: venial::Impl) -> ParseResult let on_notification_fn = convert_to_match_expression_or_none(on_notification_fn); let get_property_fn = convert_to_match_expression_or_none(get_property_fn); let set_property_fn = convert_to_match_expression_or_none(set_property_fn); + let get_property_list_fn = convert_to_match_expression_or_none(get_property_list_fn); + let free_property_list_fn = convert_to_match_expression_or_none(free_property_list_fn); + let property_can_revert_fn = convert_to_match_expression_or_none(property_can_revert_fn); + let property_get_revert_fn = convert_to_match_expression_or_none(property_get_revert_fn); let result = quote! { #original_impl @@ -872,6 +957,9 @@ fn transform_trait_impl(original_impl: venial::Impl) -> ParseResult #register_class_impl #get_property_impl #set_property_impl + #get_property_list_impl + #property_can_revert_impl + #property_get_revert_impl impl ::godot::private::You_forgot_the_attribute__godot_api for #class_name {} @@ -901,6 +989,10 @@ fn transform_trait_impl(original_impl: venial::Impl) -> ParseResult user_on_notification_fn: #on_notification_fn, user_set_fn: #set_property_fn, user_get_fn: #get_property_fn, + user_get_property_list_fn: #get_property_list_fn, + user_free_property_list_fn: #free_property_list_fn, + user_property_can_revert_fn: #property_can_revert_fn, + user_property_get_revert_fn: #property_get_revert_fn, get_virtual_fn: #prv::callbacks::get_virtual::<#class_name>, }, init_level: <#class_name as ::godot::obj::GodotClass>::INIT_LEVEL, diff --git a/itest/rust/src/builtin_tests/script/script_instance_tests.rs b/itest/rust/src/builtin_tests/script/script_instance_tests.rs index ba13f8d18..c7e860df4 100644 --- a/itest/rust/src/builtin_tests/script/script_instance_tests.rs +++ b/itest/rust/src/builtin_tests/script/script_instance_tests.rs @@ -48,44 +48,16 @@ impl TestScriptInstance { Self { script, script_property_b: false, - prop_list: vec![PropertyInfo { - variant_type: VariantType::Int, - property_name: StringName::from("script_property_a"), - class_name: ClassName::from_ascii_cstr("\0".as_bytes()), - hint: PropertyHint::NONE, - hint_string: GString::new(), - usage: PropertyUsageFlags::NONE, - }], + prop_list: vec![PropertyInfo::new_var::("script_property_a")], method_list: vec![MethodInfo { id: 1, method_name: StringName::from("script_method_a"), class_name: ClassName::from_ascii_cstr("TestScript\0".as_bytes()), - return_type: PropertyInfo { - variant_type: VariantType::String, - class_name: ClassName::none(), - property_name: StringName::from(""), - hint: PropertyHint::NONE, - hint_string: GString::new(), - usage: PropertyUsageFlags::NONE, - }, + return_type: PropertyInfo::new_return::(), arguments: vec![ - PropertyInfo { - variant_type: VariantType::String, - class_name: ClassName::none(), - property_name: StringName::from(""), - hint: PropertyHint::NONE, - hint_string: GString::new(), - usage: PropertyUsageFlags::NONE, - }, - PropertyInfo { - variant_type: VariantType::Int, - class_name: ClassName::none(), - property_name: StringName::from(""), - hint: PropertyHint::NONE, - hint_string: GString::new(), - usage: PropertyUsageFlags::NONE, - }, + PropertyInfo::new_arg::(""), + PropertyInfo::new_arg::(""), ], default_arguments: vec![], flags: MethodFlags::NORMAL, diff --git a/itest/rust/src/builtin_tests/string/gstring_test.rs b/itest/rust/src/builtin_tests/string/gstring_test.rs index 9f52aa934..8bdee3431 100644 --- a/itest/rust/src/builtin_tests/string/gstring_test.rs +++ b/itest/rust/src/builtin_tests/string/gstring_test.rs @@ -96,3 +96,26 @@ fn string_hash() { .collect(); assert_eq!(set.len(), 5); } + +#[itest] +fn gstring_name_into_string_sys() { + const TARGET_STRINGS: &[&'static str] = &[ + "property_number_one", + "another property here", + "wow properties", + "odakfhgjlk", + "more stuffsies", + ]; + let mut strings = Vec::new(); + + for i in 0..100 { + let string = TARGET_STRINGS[i % TARGET_STRINGS.len()]; + strings.push(GString::from(string).into_string_sys()); + } + + for (i, string_sys) in strings.iter().enumerate() { + let target = TARGET_STRINGS[i % TARGET_STRINGS.len()]; + let string = unsafe { GString::from_string_sys(*string_sys) }; + assert_eq!(string.to_string().as_str(), target, "iteration: {i}",); + } +} diff --git a/itest/rust/src/builtin_tests/string/string_name_test.rs b/itest/rust/src/builtin_tests/string/string_name_test.rs index 9d3cd0c5a..4455bb9ab 100644 --- a/itest/rust/src/builtin_tests/string/string_name_test.rs +++ b/itest/rust/src/builtin_tests/string/string_name_test.rs @@ -141,3 +141,30 @@ fn string_name_from_latin1_with_nul() { assert_eq!(a, b); } } + +#[itest] +fn string_name_into_string_sys() { + const TARGET_STRING_NAMES: &[&'static str] = &[ + "property_number_one", + "another property here", + "wow properties", + "odakfhgjlk", + "more stuffsies", + ]; + let mut string_names = Vec::new(); + + for i in 0..100 { + let string_name = TARGET_STRING_NAMES[i % TARGET_STRING_NAMES.len()]; + string_names.push(StringName::from(string_name).into_string_sys()); + } + + for (i, string_name_sys) in string_names.iter().enumerate() { + let target_name = TARGET_STRING_NAMES[i % TARGET_STRING_NAMES.len()]; + let string_name = unsafe { StringName::from_string_sys(*string_name_sys) }; + assert_eq!( + string_name.to_string().as_str(), + target_name, + "iteration: {i}", + ); + } +} diff --git a/itest/rust/src/object_tests/dyn_property_test.rs b/itest/rust/src/object_tests/dyn_property_test.rs new file mode 100644 index 000000000..20aa11606 --- /dev/null +++ b/itest/rust/src/object_tests/dyn_property_test.rs @@ -0,0 +1,88 @@ +/* + * 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/. + */ + +// Testing dynamic properties exposed mainly through `get_property_list` and other helper methods. + +use crate::framework::itest; + +use godot::builtin::meta::{PropertyInfo, ToGodot}; +use godot::builtin::{GString, StringName, VariantArray}; +use godot::engine::{INode, Node, Object}; +use godot::obj::{Gd, Inherits, NewAlloc}; +use godot::register::{godot_api, GodotClass}; + +#[derive(GodotClass)] +#[class(init, base = Node)] +struct PropertyListTest { + toggle_props: bool, +} + +#[godot_api] +impl INode for PropertyListTest { + fn get_property_list(&self) -> Vec { + let mut properties = vec![ + PropertyInfo::new_var::("some_i64_property"), + PropertyInfo::new_var::("some_gstring_property"), + PropertyInfo::new_var::("some_variantarray_property"), + ]; + + if self.toggle_props { + properties.push(PropertyInfo::new_var::>>( + "some_toggled_property", + )); + } + + properties + } +} + +fn has_property, S: Into>(gd: &Gd, property: S) -> bool { + let gd = gd.clone().upcast::(); + let property = property.into(); + let property_list = gd.get_property_list(); + + for prop in property_list.iter_shared() { + if prop.get("name") == Some(property.to_variant()) { + return true; + } + } + + false +} + +#[itest] +fn property_list_has_property() { + let mut property_list_test = PropertyListTest::new_alloc(); + + assert!(has_property(&property_list_test, "some_i64_property")); + assert!(has_property(&property_list_test, "some_gstring_property")); + assert!(has_property( + &property_list_test, + "some_variantarray_property" + )); + assert!(!has_property(&property_list_test, "some_toggled_property")); + assert!(!has_property( + &property_list_test, + "some_undefined_property" + )); + + property_list_test.bind_mut().toggle_props = true; + + assert!(has_property(&property_list_test, "some_i64_property")); + assert!(has_property(&property_list_test, "some_gstring_property")); + assert!(has_property( + &property_list_test, + "some_variantarray_property" + )); + assert!(has_property(&property_list_test, "some_toggled_property")); + assert!(!has_property( + &property_list_test, + "some_undefined_property" + )); + + property_list_test.free(); +} diff --git a/itest/rust/src/object_tests/mod.rs b/itest/rust/src/object_tests/mod.rs index 94d4c8c65..1bb2a5fbc 100644 --- a/itest/rust/src/object_tests/mod.rs +++ b/itest/rust/src/object_tests/mod.rs @@ -7,6 +7,7 @@ mod base_test; mod class_rename_test; +mod dyn_property_test; mod object_swap_test; mod object_test; mod onready_test;