Skip to content

Commit 7461251

Browse files
authored
Merge pull request #998 from Yarwin/derive-var-and-export-for-dyn-gd
Implement `Var` and `Export` for `DynGd<T, D>`
2 parents d7dfcf2 + 7de1570 commit 7461251

File tree

5 files changed

+142
-16
lines changed

5 files changed

+142
-16
lines changed

godot-core/src/obj/dyn_gd.rs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77

88
use crate::builtin::Variant;
99
use crate::meta::error::ConvertError;
10-
use crate::meta::{FromGodot, GodotConvert, ToGodot};
10+
use crate::meta::{ClassName, FromGodot, GodotConvert, PropertyHintInfo, ToGodot};
1111
use crate::obj::guards::DynGdRef;
1212
use crate::obj::{bounds, AsDyn, Bounds, DynGdMut, Gd, GodotClass, Inherits};
13-
use crate::registry::class::try_dynify_object;
13+
use crate::registry::class::{get_dyn_property_hint_string, try_dynify_object};
14+
use crate::registry::property::{Export, Var};
1415
use crate::{meta, sys};
1516
use std::{fmt, ops};
1617

@@ -478,3 +479,34 @@ where
478479
D: ?Sized + 'static,
479480
{
480481
}
482+
483+
impl<T, D> Var for DynGd<T, D>
484+
where
485+
T: GodotClass,
486+
D: ?Sized + 'static,
487+
{
488+
fn get_property(&self) -> Self::Via {
489+
self.obj.get_property()
490+
}
491+
492+
fn set_property(&mut self, value: Self::Via) {
493+
// `set_property` can't be delegated to Gd<T>, since we have to set `erased_obj` as well.
494+
*self = <Self as FromGodot>::from_godot(value);
495+
}
496+
}
497+
498+
impl<T, D> Export for DynGd<T, D>
499+
where
500+
T: GodotClass + Bounds<Exportable = bounds::Yes>,
501+
D: ?Sized + 'static,
502+
{
503+
fn export_hint() -> PropertyHintInfo {
504+
PropertyHintInfo {
505+
hint_string: get_dyn_property_hint_string::<D>(),
506+
..<Gd<T> as Export>::export_hint()
507+
}
508+
}
509+
fn as_node_class() -> Option<ClassName> {
510+
<Gd<T> as Export>::as_node_class()
511+
}
512+
}

godot-core/src/registry/class.rs

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,19 @@
55
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
66
*/
77

8+
use godot_ffi::join_with;
89
use std::collections::HashMap;
910
use std::{any, ptr};
1011

12+
use crate::builtin::GString;
1113
use crate::init::InitLevel;
1214
use crate::meta::error::{ConvertError, FromGodotError};
1315
use crate::meta::ClassName;
1416
use crate::obj::{cap, DynGd, Gd, GodotClass};
1517
use crate::private::{ClassPlugin, PluginItem};
1618
use crate::registry::callbacks;
1719
use crate::registry::plugin::{ErasedDynifyFn, ErasedRegisterFn, InherentImpl};
18-
use crate::{classes, godot_error, sys};
20+
use crate::{classes, godot_error, godot_warn, sys};
1921
use sys::{interface_fn, out, Global, GlobalGuard, GlobalLockError};
2022

2123
/// Returns a lock to a global map of loaded classes, by initialization level.
@@ -215,11 +217,30 @@ pub fn auto_register_classes(init_level: InitLevel) {
215217
fill_class_info(elem.item.clone(), class_info);
216218
});
217219

220+
// First register all the loaded classes and dyn traits.
221+
// We need all the dyn classes in the registry to properly register DynGd properties;
222+
// one can do it directly inside the loop – by locking and unlocking the mutex –
223+
// but it is much slower and doesn't guarantee that all the dependent classes will be already loaded in most cases.
224+
register_classes_and_dyn_traits(&mut map, init_level);
225+
226+
// actually register all the classes
227+
for info in map.into_values() {
228+
register_class_raw(info);
229+
out!("Class {class_name} loaded.");
230+
}
231+
232+
out!("All classes for level `{init_level:?}` auto-registered.");
233+
}
234+
235+
fn register_classes_and_dyn_traits(
236+
map: &mut HashMap<ClassName, ClassRegistrationInfo>,
237+
init_level: InitLevel,
238+
) {
218239
let mut loaded_classes_by_level = global_loaded_classes_by_init_level();
219240
let mut loaded_classes_by_name = global_loaded_classes_by_name();
220241
let mut dyn_traits_by_typeid = global_dyn_traits_by_typeid();
221242

222-
for mut info in map.into_values() {
243+
for info in map.values_mut() {
223244
let class_name = info.class_name;
224245
out!("Register class: {class_name} at level `{init_level:?}`");
225246

@@ -246,13 +267,7 @@ pub fn auto_register_classes(init_level: InitLevel) {
246267
.push(loaded_class);
247268

248269
loaded_classes_by_name.insert(class_name, metadata);
249-
250-
register_class_raw(info);
251-
252-
out!("Class {class_name} loaded.");
253270
}
254-
255-
out!("All classes for level `{init_level:?}` auto-registered.");
256271
}
257272

258273
pub fn unregister_classes(init_level: InitLevel) {
@@ -334,6 +349,38 @@ pub(crate) fn try_dynify_object<T: GodotClass, D: ?Sized + 'static>(
334349
Err(error.into_error(object))
335350
}
336351

352+
/// Responsible for creating hint_string for [`DynGd<T, D>`][crate::obj::DynGd] properties which works with [`PropertyHint::NODE_TYPE`][crate::global::PropertyHint::NODE_TYPE] or [`PropertyHint::RESOURCE_TYPE`][crate::global::PropertyHint::RESOURCE_TYPE].
353+
///
354+
/// Godot offers very limited capabilities when it comes to validating properties in the editor if given class isn't a tool.
355+
/// Proper hint string combined with `PropertyHint::NODE_TYPE` or `PropertyHint::RESOURCE_TYPE` allows to limit selection only to valid classes - those registered as implementors of given `DynGd<T, D>`'s `D` trait.
356+
///
357+
/// See also [Godot docs for PropertyHint](https://docs.godotengine.org/en/stable/classes/[email protected]#enum-globalscope-propertyhint).
358+
pub(crate) fn get_dyn_property_hint_string<D>() -> GString
359+
where
360+
D: ?Sized + 'static,
361+
{
362+
let typeid = any::TypeId::of::<D>();
363+
let dyn_traits_by_typeid = global_dyn_traits_by_typeid();
364+
let Some(relations) = dyn_traits_by_typeid.get(&typeid) else {
365+
let trait_name = sys::short_type_name::<D>();
366+
godot_warn!(
367+
"godot-rust: No class has been linked to trait {trait_name} with #[godot_dyn]."
368+
);
369+
return GString::default();
370+
};
371+
assert!(
372+
!relations.is_empty(),
373+
"Trait {trait_name} has been registered as DynGd Trait \
374+
despite no class being related to it \n\
375+
**this is a bug, please report it**",
376+
trait_name = sys::short_type_name::<D>()
377+
);
378+
379+
GString::from(join_with(relations.iter(), ", ", |relation| {
380+
relation.implementing_class_name.to_cow_str()
381+
}))
382+
}
383+
337384
/// Populate `c` with all the relevant data from `component` (depending on component type).
338385
fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) {
339386
c.validate_unique(&item);

godot-ffi/src/toolbox.rs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
//! Functions and macros that are not very specific to gdext, but come in handy.
99
1010
use crate as sys;
11+
use std::fmt::{Display, Write};
1112

1213
// ----------------------------------------------------------------------------------------------------------------------------------------------
1314
// Macros
@@ -162,21 +163,25 @@ where
162163
join_with(iter, ", ", |item| format!("{item:?}"))
163164
}
164165

165-
pub fn join_with<T, I, F>(mut iter: I, sep: &str, mut format_elem: F) -> String
166+
pub fn join_with<T, I, F, S>(mut iter: I, sep: &str, mut format_elem: F) -> String
166167
where
167168
I: Iterator<Item = T>,
168-
F: FnMut(&T) -> String,
169+
F: FnMut(&T) -> S,
170+
S: Display,
169171
{
170172
let mut result = String::new();
171173

172174
if let Some(first) = iter.next() {
173-
result.push_str(&format_elem(&first));
175+
// write! propagates error only if given formatter fails.
176+
// String formatting by itself is an infallible operation.
177+
// Read more at: https://doc.rust-lang.org/stable/std/fmt/index.html#formatting-traits
178+
write!(&mut result, "{first}", first = format_elem(&first))
179+
.expect("Formatter should not fail!");
174180
for item in iter {
175-
result.push_str(sep);
176-
result.push_str(&format_elem(&item));
181+
write!(&mut result, "{sep}{item}", item = format_elem(&item))
182+
.expect("Formatter should not fail!");
177183
}
178184
}
179-
180185
result
181186
}
182187

itest/godot/ManualFfiTests.gd

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,36 @@ func test_export():
6868
obj.free()
6969
node.free()
7070

71+
func test_export_dyn_gd():
72+
var dyn_gd_exporter = RefcDynGdExporter.new()
73+
74+
# NodeHealth is valid candidate both for `empty` and `second` fields.
75+
var node = NodeHealth.new()
76+
dyn_gd_exporter.first = node
77+
assert_eq(dyn_gd_exporter.first, node)
78+
79+
dyn_gd_exporter.second = node
80+
assert_eq(dyn_gd_exporter.second, node)
81+
82+
# RefcHealth is valid candidate for `first` field.
83+
var refc = RefcHealth.new()
84+
dyn_gd_exporter.first = refc
85+
assert_eq(dyn_gd_exporter.first, refc)
86+
node.free()
87+
88+
func test_export_dyn_gd_should_fail_for_wrong_type():
89+
if runs_release():
90+
return
91+
92+
var dyn_gd_exporter = RefcDynGdExporter.new()
93+
var refc = RefcHealth.new()
94+
95+
disable_error_messages()
96+
dyn_gd_exporter.second = refc # Should fail.
97+
enable_error_messages()
98+
99+
assert_fail("`DynGdExporter.second` should only accept NodeHealth and only if it implements `InstanceIdProvider` trait")
100+
71101
class MockObjGd extends Object:
72102
var i: int = 0
73103

itest/rust/src/object_tests/dyn_gd_test.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,3 +445,15 @@ impl InstanceIdProvider for foreign::NodeHealth {
445445
self.base().instance_id()
446446
}
447447
}
448+
449+
// ----------------------------------------------------------------------------------------------------------------------------------------------
450+
// Check if DynGd can be properly exported
451+
452+
#[derive(GodotClass)]
453+
#[class(init)]
454+
struct RefcDynGdExporter {
455+
#[var]
456+
first: Option<DynGd<Object, dyn Health>>,
457+
#[export]
458+
second: Option<DynGd<foreign::NodeHealth, dyn InstanceIdProvider>>,
459+
}

0 commit comments

Comments
 (0)