diff --git a/godot-codegen/src/generator/virtual_traits.rs b/godot-codegen/src/generator/virtual_traits.rs index d94f8c835..25c8c4a9d 100644 --- a/godot-codegen/src/generator/virtual_traits.rs +++ b/godot-codegen/src/generator/virtual_traits.rs @@ -119,6 +119,18 @@ fn make_special_virtual_methods(notification_enum_name: &Ident) -> TokenStream { unimplemented!() } + /// Called whenever Godot retrieves value of property. Allows to customize existing properties. + /// Every property info goes through this method, except properties **added** with `get_property_list()`. + /// + /// Exposed `property` here is a shared mutable reference obtained (and returned to) from Godot. + /// + /// See also in the Godot docs: + /// * [`Object::_validate_property`](https://docs.godotengine.org/en/stable/classes/class_object.html#class-object-private-method-validate-property) + #[cfg(since_api = "4.2")] + fn validate_property(&self, property: &mut crate::meta::PropertyInfo) { + unimplemented!() + } + /// Called by Godot to tell if a property has a custom revert or not. /// /// Return `None` for no custom revert, and return `Some(value)` to specify the custom revert. diff --git a/godot-core/src/builtin/string/gstring.rs b/godot-core/src/builtin/string/gstring.rs index aa4325e58..4bda7bec5 100644 --- a/godot-core/src/builtin/string/gstring.rs +++ b/godot-core/src/builtin/string/gstring.rs @@ -148,6 +148,17 @@ impl GString { *boxed } + /// Convert a `GString` sys pointer to a mutable reference with unbounded lifetime. + /// + /// # Safety + /// + /// - `ptr` must point to a live `GString` for the duration of `'a`. + /// - Must be exclusive - no other reference to given `GString` instance can exist for the duration of `'a`. + pub(crate) unsafe fn borrow_string_sys_mut<'a>(ptr: sys::GDExtensionStringPtr) -> &'a mut Self { + sys::static_assert_eq_size_align!(StringName, sys::types::OpaqueString); + &mut *(ptr.cast::()) + } + /// Moves this string into a string sys pointer. This is the same as using [`GodotFfi::move_return_ptr`]. /// /// # Safety diff --git a/godot-core/src/builtin/string/string_name.rs b/godot-core/src/builtin/string/string_name.rs index 48e920689..ceadd1b61 100644 --- a/godot-core/src/builtin/string/string_name.rs +++ b/godot-core/src/builtin/string/string_name.rs @@ -155,6 +155,19 @@ impl StringName { &*(ptr.cast::()) } + /// Convert a `StringName` sys pointer to a mutable reference with unbounded lifetime. + /// + /// # Safety + /// + /// - `ptr` must point to a live `StringName` for the duration of `'a`. + /// - Must be exclusive - no other reference to given `StringName` instance can exist for the duration of `'a`. + pub(crate) unsafe fn borrow_string_sys_mut<'a>( + ptr: sys::GDExtensionStringNamePtr, + ) -> &'a mut StringName { + sys::static_assert_eq_size_align!(StringName, sys::types::OpaqueStringName); + &mut *(ptr.cast::()) + } + #[doc(hidden)] pub fn as_inner(&self) -> inner::InnerStringName { inner::InnerStringName::from_outer(self) diff --git a/godot-core/src/meta/property_info.rs b/godot-core/src/meta/property_info.rs index f14e63b7d..51a96f058 100644 --- a/godot-core/src/meta/property_info.rs +++ b/godot-core/src/meta/property_info.rs @@ -10,6 +10,7 @@ use crate::global::{PropertyHint, PropertyUsageFlags}; use crate::meta::{ element_godot_type_name, ArrayElement, ClassName, GodotType, PackedArrayElement, }; +use crate::obj::{EngineBitfield, EngineEnum}; use crate::registry::property::{Export, Var}; use crate::sys; use godot_ffi::VariantType; @@ -194,6 +195,52 @@ impl PropertyInfo { let _hint_string = GString::from_owned_string_sys(info.hint_string); } } + + /// Moves its values into given `GDExtensionPropertyInfo`, dropping previous values if necessary. + /// + /// # Safety + /// + /// * `property_info_ptr` must be valid. + /// + pub(crate) unsafe fn move_into_property_info_ptr( + self, + property_info_ptr: *mut sys::GDExtensionPropertyInfo, + ) { + let ptr = &mut *property_info_ptr; + + ptr.usage = u32::try_from(self.usage.ord()).expect("usage.ord()"); + ptr.hint = u32::try_from(self.hint_info.hint.ord()).expect("hint.ord()"); + ptr.type_ = self.variant_type.sys(); + + *StringName::borrow_string_sys_mut(ptr.name) = self.property_name; + *GString::borrow_string_sys_mut(ptr.hint_string) = self.hint_info.hint_string; + + if self.class_name != ClassName::none() { + *StringName::borrow_string_sys_mut(ptr.class_name) = self.class_name.to_string_name(); + } + } + + /// Creates copy of given `sys::GDExtensionPropertyInfo`. + /// + /// # Safety + /// + /// * `property_info_ptr` must be valid. + pub(crate) unsafe fn new_from_sys( + property_info_ptr: *mut sys::GDExtensionPropertyInfo, + ) -> Self { + let ptr = *property_info_ptr; + + Self { + variant_type: VariantType::from_sys(ptr.type_), + class_name: ClassName::none(), + property_name: StringName::new_from_string_sys(ptr.name), + hint_info: PropertyHintInfo { + hint: PropertyHint::from_ord(ptr.hint.to_owned() as i32), + hint_string: GString::new_from_string_sys(ptr.hint_string), + }, + usage: PropertyUsageFlags::from_ord(ptr.usage as u64), + } + } } // ---------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/godot-core/src/obj/traits.rs b/godot-core/src/obj/traits.rs index d9be74d5a..1713fa248 100644 --- a/godot-core/src/obj/traits.rs +++ b/godot-core/src/obj/traits.rs @@ -472,6 +472,7 @@ where pub mod cap { use super::*; use crate::builtin::{StringName, Variant}; + use crate::meta::PropertyInfo; use crate::obj::{Base, Bounds, Gd}; use std::any::Any; @@ -571,6 +572,13 @@ pub mod cap { fn __godot_property_get_revert(&self, property: StringName) -> Option; } + #[doc(hidden)] + #[cfg(since_api = "4.2")] + pub trait GodotValidateProperty: GodotClass { + #[doc(hidden)] + fn __godot_validate_property(&self, property: &mut PropertyInfo); + } + /// 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 d8b19254c..23234dba3 100644 --- a/godot-core/src/registry/callbacks.rs +++ b/godot-core/src/registry/callbacks.rs @@ -13,6 +13,7 @@ use crate::builder::ClassBuilder; use crate::builtin::{StringName, Variant}; use crate::classes::Object; +use crate::meta::PropertyInfo; use crate::obj::{bounds, cap, AsDyn, Base, Bounds, Gd, GodotClass, Inherits, UserClass}; use crate::registry::plugin::ErasedDynGd; use crate::storage::{as_storage, InstanceStorage, Storage, StorageRefCounted}; @@ -357,6 +358,35 @@ pub unsafe extern "C" fn property_get_revert( sys::conv::SYS_TRUE } + +/// Callback for `validate_property`. +/// +/// Exposes `PropertyInfo` created out of `*mut GDExtensionPropertyInfo` ptr to user and moves edited values back to the pointer. +/// +/// # Safety +/// +/// - Must only be called by Godot as a callback for `validate_property` for a rust-defined class of type `T`. +/// - `property_info_ptr` must be valid for the whole duration of this function call (i.e. - can't be freed nor consumed). +/// +#[deny(unsafe_op_in_unsafe_fn)] +#[cfg(since_api = "4.2")] +pub unsafe extern "C" fn validate_property( + instance: sys::GDExtensionClassInstancePtr, + property_info_ptr: *mut sys::GDExtensionPropertyInfo, +) -> sys::GDExtensionBool { + // SAFETY: `instance` is a valid `T` instance pointer for the duration of this function call. + let storage = unsafe { as_storage::(instance) }; + let instance = storage.get(); + + // SAFETY: property_info_ptr must be valid. + let mut property_info = unsafe { PropertyInfo::new_from_sys(property_info_ptr) }; + T::__godot_validate_property(&*instance, &mut property_info); + + // SAFETY: property_info_ptr remains valid & unchanged. + unsafe { property_info.move_into_property_info_ptr(property_info_ptr) }; + + sys::conv::SYS_TRUE +} // ---------------------------------------------------------------------------------------------------------------------------------------------- // Safe, higher-level methods diff --git a/godot-core/src/registry/class.rs b/godot-core/src/registry/class.rs index 9fca57b0c..54ff08b8a 100644 --- a/godot-core/src/registry/class.rs +++ b/godot-core/src/registry/class.rs @@ -452,6 +452,8 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) { user_property_get_revert_fn, #[cfg(all(since_api = "4.3", feature = "register-docs"))] virtual_method_docs: _, + #[cfg(since_api = "4.2")] + validate_property_fn, }) => { c.user_register_fn = user_register_fn; @@ -477,6 +479,10 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) { c.godot_params.property_can_revert_func = user_property_can_revert_fn; c.godot_params.property_get_revert_func = user_property_get_revert_fn; c.user_virtual_fn = get_virtual_fn; + #[cfg(since_api = "4.2")] + { + c.godot_params.validate_property_func = validate_property_fn; + } } PluginItem::DynTraitImpl(dyn_trait_impl) => { let type_id = dyn_trait_impl.dyn_trait_typeid(); diff --git a/godot-core/src/registry/plugin.rs b/godot-core/src/registry/plugin.rs index e4734dfa8..af4f4ea69 100644 --- a/godot-core/src/registry/plugin.rs +++ b/godot-core/src/registry/plugin.rs @@ -404,6 +404,13 @@ pub struct ITraitImpl { r_ret: sys::GDExtensionVariantPtr, ) -> sys::GDExtensionBool, >, + #[cfg(since_api = "4.2")] + pub(crate) validate_property_fn: Option< + unsafe extern "C" fn( + p_instance: sys::GDExtensionClassInstancePtr, + p_property: *mut sys::GDExtensionPropertyInfo, + ) -> sys::GDExtensionBool, + >, } impl ITraitImpl { @@ -485,6 +492,15 @@ impl ITraitImpl { ); self } + + #[cfg(since_api = "4.2")] + pub fn with_validate_property(mut self) -> Self { + set( + &mut self.validate_property_fn, + callbacks::validate_property::, + ); + self + } } /// Representation of a `#[godot_dyn]` invocation. diff --git a/godot-macros/src/class/data_models/interface_trait_impl.rs b/godot-macros/src/class/data_models/interface_trait_impl.rs index 124da6bfb..ffd5c7efc 100644 --- a/godot-macros/src/class/data_models/interface_trait_impl.rs +++ b/godot-macros/src/class/data_models/interface_trait_impl.rs @@ -25,6 +25,7 @@ pub fn transform_trait_impl(original_impl: venial::Impl) -> ParseResult ParseResult { + let inactive_class_early_return = make_inactive_class_check(TokenStream::new()); + validate_property_impl = quote! { + #(#cfg_attrs)* + impl ::godot::obj::cap::GodotValidateProperty for #class_name { + fn __godot_validate_property(&self, property: &mut ::godot::meta::PropertyInfo) { + use ::godot::obj::UserClass as _; + + #inactive_class_early_return + + ::validate_property(self, property); + } + } + }; + modifiers.push((cfg_attrs, ident("with_validate_property"))); + } + // Other virtual methods, like ready, process etc. method_name_str => { #[cfg(since_api = "4.4")] @@ -317,6 +336,7 @@ pub fn transform_trait_impl(original_impl: venial::Impl) -> ParseResult::class_name(); + } + } +} + +#[itest] +fn validate_property_test() { + let obj = ValidatePropertyTest::new_alloc(); + let properties: Array = obj.get_property_list(); + + let property = properties + .iter_shared() + .find(|dict| { + dict.get("name") + .is_some_and(|v| v.to_string() == "SuperNewTestPropertyName") + }) + .expect("Test failed – unable to find validated property."); + + let hint_string = property + .get("hint_string") + .expect("validated property dict should contain a `hint_string` entry.") + .to::(); + assert_eq!(hint_string, GString::from("SomePropertyHint")); + + let class = property + .get("class_name") + .expect("Validated property dict should contain a class_name entry.") + .to::(); + assert_eq!(class, StringName::from("ValidatePropertyTest")); + + let usage = property + .get("usage") + .expect("Validated property dict should contain an usage entry.") + .to::(); + assert_eq!(usage, PropertyUsageFlags::NO_EDITOR); + + let hint = property + .get("hint") + .expect("Validated property dict should contain a hint entry.") + .to::(); + assert_eq!(hint, PropertyHint::TYPE_STRING); + + obj.free(); +}