godot_core/registry/
property.rs

1/*
2 * Copyright (c) godot-rust; Bromeon and contributors.
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
6 */
7
8//! Registration support for property types.
9
10use crate::classes;
11use crate::global::PropertyHint;
12use godot_ffi as sys;
13use godot_ffi::{GodotNullableFfi, VariantType};
14use std::fmt::Display;
15
16use crate::meta::{ClassName, FromGodot, GodotConvert, GodotType, PropertyHintInfo, ToGodot};
17use crate::obj::{EngineEnum, GodotClass};
18
19// ----------------------------------------------------------------------------------------------------------------------------------------------
20// Trait definitions
21
22// Note: HTML link for #[var] works if this symbol is inside prelude, but not in register::property.
23/// Trait implemented for types that can be used as [`#[var]`](../register/derive.GodotClass.html#properties-and-exports) fields.
24///
25/// This creates a copy of the value, according to copy semantics provided by `Clone`. For example, `Array`, `Dictionary` and `Gd` are
26/// returned by shared reference instead of copying the actual data.
27///
28/// This does not require [`FromGodot`] or [`ToGodot`], so that something can be used as a property even if it can't be used in function
29/// arguments/return types.
30///
31/// See also [`Export`], a specialization of this trait for properties exported to the editor UI.
32///
33/// For enums, this trait can be derived using the [`#[derive(Var)]`](../derive.Var.html) macro.
34#[doc(alias = "property")]
35//
36// on_unimplemented: we also mention #[export] here, because we can't control the order of error messages.
37// Missing Export often also means missing Var trait, and so the Var error message appears first.
38#[diagnostic::on_unimplemented(
39    message = "`#[var]` properties require `Var` trait; #[export] ones require `Export` trait",
40    label = "type cannot be used as a property",
41    note = "see also: https://godot-rust.github.io/book/register/properties.html"
42)]
43pub trait Var: GodotConvert {
44    fn get_property(&self) -> Self::Via;
45
46    fn set_property(&mut self, value: Self::Via);
47
48    /// Specific property hints, only override if they deviate from [`GodotType::property_info`], e.g. for enums/newtypes.
49    fn var_hint() -> PropertyHintInfo {
50        Self::Via::property_hint_info()
51    }
52}
53
54// Note: HTML link for #[export] works if this symbol is inside prelude, but not in register::property.
55/// Trait implemented for types that can be used as [`#[export]`](../register/derive.GodotClass.html#properties-and-exports) fields.
56///
57/// To export objects, see the [_Exporting_ section of `Gd<T>`](../obj/struct.Gd.html#exporting).
58///
59/// For enums, this trait can be derived using the [`#[derive(Export)]`](../derive.Export.html) macro.
60#[doc(alias = "property")]
61//
62// on_unimplemented: mentioning both Var + Export; see above.
63#[diagnostic::on_unimplemented(
64    message = "`#[var]` properties require `Var` trait; #[export] ones require `Export` trait",
65    label = "type cannot be used as a property",
66    note = "see also: https://godot-rust.github.io/book/register/properties.html"
67)]
68pub trait Export: Var {
69    /// The export info to use for an exported field of this type, if no other export info is specified.
70    fn export_hint() -> PropertyHintInfo {
71        <Self as Var>::var_hint()
72    }
73
74    /// If this is a class inheriting `Node`, returns the `ClassName`; otherwise `None`.
75    ///
76    /// Only overridden for `Gd<T>`, to detect erroneous exports of `Node` inside a `Resource` class.
77    #[allow(clippy::wrong_self_convention)]
78    #[doc(hidden)]
79    fn as_node_class() -> Option<ClassName> {
80        None
81    }
82}
83
84/// Marker trait to identify `GodotType`s that can be directly used with an `#[export]`.
85///
86/// Implemented pretty much for all the [`GodotTypes`][GodotType] that are not [`GodotClass`].
87/// Provides a few blanket implementations and, by itself, has no implications
88/// for the [`Var`] or [`Export`] traits.
89///
90/// Types which don't implement the `BuiltinExport` trait can't be used directly as an `#[export]`
91/// and must be handled using associated algebraic types, such as:
92/// * [`Option<T>`], which represents optional value that can be null when used.
93/// * [`OnEditor<T>`][crate::obj::OnEditor], which represents value that must not be null when used.
94// Some Godot Types which are inherently non-nullable (e.g., `Gd<T>`),
95// might have their value set to null by the editor. Additionally, Godot must generate
96// initial, default value for such properties, causing memory leaks.
97// Such `GodotType`s don't implement `BuiltinExport`.
98//
99// Note: This marker trait is required to create a blanket implementation
100// for `OnEditor<T>` where `T` is anything other than `GodotClass`.
101// An alternative approach would involve introducing an extra associated type
102// to `GodotType` trait. However, this would not be ideal — `GodotType` is used
103// in contexts unrelated to `#[export]`, and adding unnecessary complexity
104// should be avoided. Since Rust does not yet support specialization (i.e. negative trait bounds),
105// this `MarkerTrait` serves as the intended solution to recognize aforementioned types.
106pub trait BuiltinExport {}
107
108/// This function only exists as a place to add doc-tests for the `Export` trait.
109///
110/// Test with export of exportable type should succeed:
111/// ```no_run
112/// use godot::prelude::*;
113///
114/// #[derive(GodotClass)]
115/// #[class(init)]
116/// struct Foo {
117///     #[export]
118///     obj: Option<Gd<Resource>>,
119///     #[export]
120///     array: Array<Gd<Resource>>,
121/// }
122/// ```
123///
124/// Tests with export of non-exportable type should fail:
125/// ```compile_fail
126/// use godot::prelude::*;
127///
128/// #[derive(GodotClass)]
129/// #[class(init)]
130/// struct Foo {
131///     #[export]
132///     obj: Option<Gd<Object>>,
133/// }
134/// ```
135///
136/// Neither `Gd<T>` nor `DynGd<T, D>` can be used with an `#[export]` directly:
137///
138/// ```compile_fail
139///  use godot::prelude::*;
140///
141/// #[derive(GodotClass)]
142/// #[class(init, base = Node)]
143/// struct MyClass {
144///     #[export]
145///     editor_property: Gd<Resource>,
146/// }
147/// ```
148///
149/// ```compile_fail
150///  use godot::prelude::*;
151///
152/// #[derive(GodotClass)]
153/// #[class(init, base = Node)]
154/// struct MyClass {
155///     #[export]
156///     editor_property: DynGd<Node, dyn Display>,
157/// }
158/// ```
159///
160/// ```compile_fail
161/// use godot::prelude::*;
162///
163/// #[derive(GodotClass)]
164/// #[class(init)]
165/// struct Foo {
166///     #[export]
167///     array: Array<Gd<Object>>,
168/// }
169/// ```
170#[allow(dead_code)]
171fn export_doctests() {}
172
173// ----------------------------------------------------------------------------------------------------------------------------------------------
174// Blanket impls for Option<T>
175
176impl<T> Var for Option<T>
177where
178    T: Var + FromGodot,
179    Option<T>: GodotConvert<Via = Option<T::Via>>,
180{
181    fn get_property(&self) -> Self::Via {
182        self.as_ref().map(Var::get_property)
183    }
184
185    fn set_property(&mut self, value: Self::Via) {
186        match value {
187            Some(value) => {
188                if let Some(current_value) = self {
189                    current_value.set_property(value)
190                } else {
191                    *self = Some(FromGodot::from_godot(value))
192                }
193            }
194            None => *self = None,
195        }
196    }
197}
198
199impl<T> Export for Option<T>
200where
201    T: Export,
202    Option<T>: Var,
203{
204    fn export_hint() -> PropertyHintInfo {
205        T::export_hint()
206    }
207}
208
209impl<T> BuiltinExport for Option<T>
210where
211    T: GodotType,
212    T::Ffi: GodotNullableFfi,
213{
214}
215
216// ----------------------------------------------------------------------------------------------------------------------------------------------
217// Export machinery
218
219/// Functions used to translate user-provided arguments into export hints.
220///
221/// You are not supposed to use these functions directly. They are used by the `#[export]` macro to generate the correct export hint.
222///
223/// Each function is named the same as the equivalent Godot annotation.  
224/// For instance, `@export_range` in Godot is `fn export_range` here.
225pub mod export_info_functions {
226    use crate::builtin::GString;
227    use crate::global::PropertyHint;
228    use crate::meta::{GodotType, PropertyHintInfo, PropertyInfo};
229    use crate::obj::EngineEnum;
230    use crate::registry::property::Export;
231    use godot_ffi::VariantType;
232
233    /// Turn a list of variables into a comma separated string containing only the identifiers corresponding
234    /// to a true boolean variable.
235    macro_rules! comma_separate_boolean_idents {
236        ($( $ident:ident),* $(,)?) => {
237            {
238                let mut strings = Vec::new();
239
240                $(
241                    if $ident {
242                        strings.push(stringify!($ident));
243                    }
244                )*
245
246                strings.join(",")
247            }
248        };
249    }
250
251    // We want this to match the options available on `@export_range(..)`
252    /// Mark an exported numerical value to use the editor's range UI.
253    ///
254    /// You'll never call this function itself, but will instead use the macro `#[export(range=(...))]`, as below.  The syntax is
255    /// very similar to Godot's [`@export_range`](https://docs.godotengine.org/en/stable/classes/class_%40gdscript.html#class-gdscript-annotation-export-range).
256    /// `min`, `max`, and `step` are `f32` positional arguments, with `step` being optional and defaulting to `1.0`.  The rest of
257    /// the arguments can be written in any order.  The symbols of type `bool` just need to have those symbols written, and those of type `Option<T>` will be written as `{KEY}={VALUE}`, e.g. `suffix="px"`.
258    ///
259    /// ```
260    /// # use godot::prelude::*;
261    /// #[derive(GodotClass)]
262    /// #[class(init, base=Node)]
263    /// struct MyClassWithRangedValues {
264    ///     #[export(range=(0.0, 400.0, 1.0, or_greater, suffix="px"))]
265    ///     icon_width: i32,
266    ///     #[export(range=(-180.0, 180.0, degrees))]
267    ///     angle: f32,
268    /// }
269    /// ```
270    #[allow(clippy::too_many_arguments)]
271    pub fn export_range(
272        min: f64,
273        max: f64,
274        step: Option<f64>,
275        or_greater: bool,
276        or_less: bool,
277        exp: bool,
278        radians_as_degrees: bool,
279        degrees: bool,
280        hide_slider: bool,
281        suffix: Option<String>,
282    ) -> PropertyHintInfo {
283        // From Godot 4.4, GDScript uses `.0` for integral floats, see https://github.com/godotengine/godot/pull/47502.
284        // We still register them the old way, to test compatibility. See also property_template_test.rs.
285
286        let hint_beginning = if let Some(step) = step {
287            format!("{min},{max},{step}")
288        } else {
289            format!("{min},{max}")
290        };
291
292        let rest = comma_separate_boolean_idents!(
293            or_greater,
294            or_less,
295            exp,
296            radians_as_degrees,
297            degrees,
298            hide_slider
299        );
300
301        let mut hint_string = hint_beginning;
302        if !rest.is_empty() {
303            hint_string.push_str(&format!(",{rest}"));
304        }
305        if let Some(suffix) = suffix {
306            hint_string.push_str(&format!(",suffix:{suffix}"));
307        }
308
309        PropertyHintInfo {
310            hint: PropertyHint::RANGE,
311            hint_string: hint_string.into(),
312        }
313    }
314
315    #[doc(hidden)]
316    pub struct ExportValueWithKey<T> {
317        variant: String,
318        key: Option<T>,
319    }
320
321    impl<T: std::fmt::Display> ExportValueWithKey<T> {
322        fn as_hint_string(&self) -> String {
323            let Self { variant, key } = self;
324
325            match key {
326                Some(key) => format!("{variant}:{key}"),
327                None => variant.clone(),
328            }
329        }
330
331        fn slice_as_hint_string<V>(values: &[V]) -> String
332        where
333            for<'a> &'a V: Into<Self>,
334        {
335            let values = values
336                .iter()
337                .map(|v| v.into().as_hint_string())
338                .collect::<Vec<_>>();
339
340            values.join(",")
341        }
342    }
343
344    impl<T, S> From<&(S, Option<T>)> for ExportValueWithKey<T>
345    where
346        T: Clone,
347        S: AsRef<str>,
348    {
349        fn from((variant, key): &(S, Option<T>)) -> Self {
350            Self {
351                variant: variant.as_ref().into(),
352                key: key.clone(),
353            }
354        }
355    }
356
357    type EnumVariant = ExportValueWithKey<i64>;
358
359    /// Equivalent to `@export_enum` in Godot.
360    ///
361    /// A name without a key would be represented as `(name, None)`, and a name with a key as `(name, Some(key))`.
362    ///
363    /// # Examples
364    ///
365    /// ```no_run
366    /// # use godot::register::property::export_info_functions::export_enum;
367    /// export_enum(&[("a", None), ("b", Some(10))]);
368    /// ```
369    pub fn export_enum<T>(variants: &[T]) -> PropertyHintInfo
370    where
371        for<'a> &'a T: Into<EnumVariant>,
372    {
373        let hint_string: String = EnumVariant::slice_as_hint_string(variants);
374
375        PropertyHintInfo {
376            hint: PropertyHint::ENUM,
377            hint_string: hint_string.into(),
378        }
379    }
380
381    pub fn export_exp_easing(attenuation: bool, positive_only: bool) -> PropertyHintInfo {
382        let hint_string = comma_separate_boolean_idents!(attenuation, positive_only);
383
384        PropertyHintInfo {
385            hint: PropertyHint::EXP_EASING,
386            hint_string: hint_string.into(),
387        }
388    }
389
390    type BitFlag = ExportValueWithKey<u32>;
391
392    /// Equivalent to `@export_flags` in Godot.
393    ///
394    /// A flag without a key would be represented as `(flag, None)`, and a flag with a key as `(flag, Some(key))`.
395    ///
396    /// # Examples
397    ///
398    /// ```no_run
399    /// # use godot::register::property::export_info_functions::export_flags;
400    /// export_flags(&[("a", None), ("b", Some(10))]);
401    /// ```
402    pub fn export_flags<T>(bits: &[T]) -> PropertyHintInfo
403    where
404        for<'a> &'a T: Into<BitFlag>,
405    {
406        let hint_string = BitFlag::slice_as_hint_string(bits);
407
408        PropertyHintInfo {
409            hint: PropertyHint::FLAGS,
410            hint_string: hint_string.into(),
411        }
412    }
413
414    /// Handles `@export_file`, `@export_global_file`, `@export_dir` and `@export_global_dir`.
415    pub fn export_file_or_dir<T: Export>(
416        is_file: bool,
417        is_global: bool,
418        filter: impl AsRef<str>,
419    ) -> PropertyHintInfo {
420        let field_ty = T::Via::property_info("");
421        let filter = filter.as_ref();
422        debug_assert!(is_file || filter.is_empty()); // Dir never has filter.
423
424        export_file_or_dir_inner(&field_ty, is_file, is_global, filter)
425    }
426
427    pub fn export_file_or_dir_inner(
428        field_ty: &PropertyInfo,
429        is_file: bool,
430        is_global: bool,
431        filter: &str,
432    ) -> PropertyHintInfo {
433        let hint = match (is_file, is_global) {
434            (true, true) => PropertyHint::GLOBAL_FILE,
435            (true, false) => PropertyHint::FILE,
436            (false, true) => PropertyHint::GLOBAL_DIR,
437            (false, false) => PropertyHint::DIR,
438        };
439
440        // Returned value depends on field type.
441        match field_ty.variant_type {
442            // GString field:
443            // { "type": 4, "hint": 13, "hint_string": "*.png" }
444            VariantType::STRING => PropertyHintInfo {
445                hint,
446                hint_string: GString::from(filter),
447            },
448
449            // Array<GString> or PackedStringArray field:
450            // { "type": 28, "hint": 23, "hint_string": "4/13:*.png" }
451            #[cfg(since_api = "4.3")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.3")))]
452            VariantType::PACKED_STRING_ARRAY => to_string_array_hint(hint, filter),
453            #[cfg(since_api = "4.3")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.3")))]
454            VariantType::ARRAY if field_ty.is_array_of_elem::<GString>() => {
455                to_string_array_hint(hint, filter)
456            }
457
458            _ => {
459                // E.g. `global_file`.
460                let attribute_name = hint.as_str().to_lowercase();
461
462                // TODO nicer error handling.
463                // Compile time may be difficult (at least without extra traits... maybe const fn?). But at least more context info, field name etc.
464                #[cfg(since_api = "4.3")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.3")))]
465                panic!(
466                    "#[export({attribute_name})] only supports GString, Array<String> or PackedStringArray field types\n\
467                    encountered: {field_ty:?}"
468                );
469
470                #[cfg(before_api = "4.3")] #[cfg_attr(published_docs, doc(cfg(before_api = "4.3")))]
471                panic!(
472                    "#[export({attribute_name})] only supports GString type prior to Godot 4.3\n\
473                    encountered: {field_ty:?}"
474                );
475            }
476        }
477    }
478
479    /// For `Array<GString>` and `PackedStringArray` fields using one of the `@export[_global]_{file|dir}` annotations.
480    ///
481    /// Formats: `"4/13:"`, `"4/15:*.png"`, ...
482    fn to_string_array_hint(hint: PropertyHint, filter: &str) -> PropertyHintInfo {
483        let variant_ord = VariantType::STRING.ord(); // "4"
484        let hint_ord = hint.ord();
485        let hint_string = format!("{variant_ord}/{hint_ord}");
486
487        PropertyHintInfo {
488            hint: PropertyHint::TYPE_STRING,
489            hint_string: format!("{hint_string}:{filter}").into(),
490        }
491    }
492
493    pub fn export_placeholder<S: AsRef<str>>(placeholder: S) -> PropertyHintInfo {
494        PropertyHintInfo {
495            hint: PropertyHint::PLACEHOLDER_TEXT,
496            hint_string: GString::from(placeholder.as_ref()),
497        }
498    }
499
500    macro_rules! default_export_funcs {
501        (
502            $( $function_name:ident => $property_hint:ident, )*
503        ) => {
504            $(
505                pub fn $function_name() -> PropertyHintInfo {
506                    PropertyHintInfo {
507                        hint: PropertyHint::$property_hint,
508                        hint_string: GString::new()
509                    }
510                }
511            )*
512        };
513    }
514
515    // The left side of these declarations follows the export annotation provided by GDScript, whereas the
516    // right side are the corresponding property hint. Godot is not always consistent between the two, such
517    // as `export_multiline` being `PROPERTY_HINT_MULTILINE_TEXT`.
518    default_export_funcs!(
519        export_storage => NONE, // Storage exports don't display in the editor.
520        export_flags_2d_physics => LAYERS_2D_PHYSICS,
521        export_flags_2d_render => LAYERS_2D_RENDER,
522        export_flags_2d_navigation => LAYERS_2D_NAVIGATION,
523        export_flags_3d_physics => LAYERS_3D_PHYSICS,
524        export_flags_3d_render => LAYERS_3D_RENDER,
525        export_flags_3d_navigation => LAYERS_3D_NAVIGATION,
526        export_multiline => MULTILINE_TEXT,
527        export_color_no_alpha => COLOR_NO_ALPHA,
528    );
529}
530
531mod export_impls {
532    use super::*;
533    use crate::builtin::*;
534
535    macro_rules! impl_property_by_godot_convert {
536        ($Ty:ty, no_export) => {
537            impl_property_by_godot_convert!(@property $Ty);
538        };
539
540        ($Ty:ty) => {
541            impl_property_by_godot_convert!(@property $Ty);
542            impl_property_by_godot_convert!(@export $Ty);
543            impl_property_by_godot_convert!(@builtin $Ty);
544        };
545
546        (@property $Ty:ty) => {
547            impl Var for $Ty {
548                fn get_property(&self) -> Self::Via {
549                    self.to_godot()
550                }
551
552                fn set_property(&mut self, value: Self::Via) {
553                    *self = FromGodot::from_godot(value);
554                }
555            }
556        };
557
558        (@export $Ty:ty) => {
559            impl Export for $Ty {
560                fn export_hint() -> PropertyHintInfo {
561                    PropertyHintInfo::type_name::<$Ty>()
562                }
563            }
564        };
565
566        (@builtin $Ty:ty) => {
567            impl BuiltinExport for $Ty {}
568        }
569    }
570
571    // Bounding Boxes
572    impl_property_by_godot_convert!(Aabb);
573    impl_property_by_godot_convert!(Rect2);
574    impl_property_by_godot_convert!(Rect2i);
575
576    // Matrices
577    impl_property_by_godot_convert!(Basis);
578    impl_property_by_godot_convert!(Transform2D);
579    impl_property_by_godot_convert!(Transform3D);
580    impl_property_by_godot_convert!(Projection);
581
582    // Vectors
583    impl_property_by_godot_convert!(Vector2);
584    impl_property_by_godot_convert!(Vector2i);
585    impl_property_by_godot_convert!(Vector3);
586    impl_property_by_godot_convert!(Vector3i);
587    impl_property_by_godot_convert!(Vector4);
588    impl_property_by_godot_convert!(Vector4i);
589
590    // Misc Math
591    impl_property_by_godot_convert!(Quaternion);
592    impl_property_by_godot_convert!(Plane);
593
594    // Stringy Types
595    impl_property_by_godot_convert!(GString);
596    impl_property_by_godot_convert!(StringName);
597    impl_property_by_godot_convert!(NodePath);
598
599    impl_property_by_godot_convert!(Color);
600
601    // Dictionary: will need to be done manually once they become typed.
602    impl_property_by_godot_convert!(Dictionary);
603    impl_property_by_godot_convert!(Variant);
604
605    // Packed arrays: we manually implement `Export`.
606    impl_property_by_godot_convert!(PackedByteArray, no_export);
607    impl_property_by_godot_convert!(PackedInt32Array, no_export);
608    impl_property_by_godot_convert!(PackedInt64Array, no_export);
609    impl_property_by_godot_convert!(PackedFloat32Array, no_export);
610    impl_property_by_godot_convert!(PackedFloat64Array, no_export);
611    impl_property_by_godot_convert!(PackedStringArray, no_export);
612    impl_property_by_godot_convert!(PackedVector2Array, no_export);
613    impl_property_by_godot_convert!(PackedVector3Array, no_export);
614    #[cfg(since_api = "4.3")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.3")))]
615    impl_property_by_godot_convert!(PackedVector4Array, no_export);
616    impl_property_by_godot_convert!(PackedColorArray, no_export);
617
618    // Primitives
619    impl_property_by_godot_convert!(f64);
620    impl_property_by_godot_convert!(i64);
621    impl_property_by_godot_convert!(bool);
622
623    // Godot uses f64 internally for floats, and if Godot tries to pass an invalid f32 into a rust property
624    // then the property will just round the value or become inf.
625    impl_property_by_godot_convert!(f32);
626
627    // Godot uses i64 internally for integers, and if Godot tries to pass an invalid integer into a property
628    // accepting one of the below values then rust will panic. In the editor this will appear as the property
629    // failing to be set to a value and an error printed in the console. During runtime this will crash the
630    // program and print the panic from rust stating that the property cannot store the value.
631    impl_property_by_godot_convert!(i32);
632    impl_property_by_godot_convert!(i16);
633    impl_property_by_godot_convert!(i8);
634    impl_property_by_godot_convert!(u32);
635    impl_property_by_godot_convert!(u16);
636    impl_property_by_godot_convert!(u8);
637
638    // Callables and Signals are useless when exported to the editor, so we only need to make them available as
639    // properties.
640    impl_property_by_godot_convert!(Callable, no_export);
641    impl_property_by_godot_convert!(Signal, no_export);
642
643    // RIDs when exported act slightly weird. They are largely read-only, however you can reset them to their
644    // default value. This seems to me very unintuitive. Since if we are storing an RID we would likely not
645    // want that RID to be spuriously resettable. And if used for debugging purposes we can use another
646    // mechanism than exporting the RID to the editor. Such as exporting a string containing the RID.
647    //
648    // Additionally, RIDs aren't persistent, and can sometimes behave a bit weirdly when passed from the
649    // editor to the runtime.
650    impl_property_by_godot_convert!(Rid, no_export);
651
652    // impl_property_by_godot_convert!(Signal);
653}
654
655// ----------------------------------------------------------------------------------------------------------------------------------------------
656// Crate-local utilities
657
658pub(crate) fn builtin_type_string<T: GodotType>() -> String {
659    use sys::GodotFfi as _;
660
661    let variant_type = T::Ffi::VARIANT_TYPE.variant_as_nil();
662
663    // Godot 4.3 changed representation for type hints, see https://github.com/godotengine/godot/pull/90716.
664    if sys::GdextBuild::since_api("4.3") {
665        format!("{}:", variant_type.ord())
666    } else {
667        format!("{}:{}", variant_type.ord(), T::godot_type_name())
668    }
669}
670
671/// Creates `hint_string` to be used for given `GodotClass` when used as an `ArrayElement`.
672pub(crate) fn object_export_element_type_string<T>(class_hint: impl Display) -> String
673where
674    T: GodotClass,
675{
676    let hint = if T::inherits::<classes::Resource>() {
677        Some(PropertyHint::RESOURCE_TYPE)
678    } else if T::inherits::<classes::Node>() {
679        Some(PropertyHint::NODE_TYPE)
680    } else {
681        None
682    };
683
684    // Exportable classes (Resource/Node based) include the {RESOURCE|NODE}_TYPE hint + the class name.
685    if let Some(export_hint) = hint {
686        format!(
687            "{variant}/{hint}:{class}",
688            variant = VariantType::OBJECT.ord(),
689            hint = export_hint.ord(),
690            class = class_hint
691        )
692    } else {
693        // Previous impl: format!("{variant}:", variant = VariantType::OBJECT.ord())
694        unreachable!("element_type_string() should only be invoked for exportable classes")
695    }
696}