Skip to content

Commit 7096bac

Browse files
committed
Add OnReady::from_loaded() + #[init(load = …)]
1 parent 00de032 commit 7096bac

File tree

5 files changed

+148
-57
lines changed

5 files changed

+148
-57
lines changed

godot-core/src/obj/on_ready.rs

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

8-
use crate::builtin::NodePath;
9-
use crate::classes::Node;
8+
use crate::builtin::{GString, NodePath};
9+
use crate::classes::{Node, Resource};
1010
use crate::meta::{arg_into_owned, AsArg, GodotConvert};
11-
use crate::obj::{Gd, GodotClass, Inherits};
11+
use crate::obj::{Gd, Inherits};
1212
use crate::registry::property::Var;
1313
use std::fmt::{self, Debug, Formatter};
1414
use std::mem;
@@ -21,10 +21,10 @@ use std::mem;
2121
///
2222
/// `OnReady<T>` should always be used as a field. There are two modes to use it:
2323
///
24-
/// 1. **Automatic mode, using [`new()`](OnReady::new), [`from_base_fn()`](OnReady::from_base_fn) or
25-
/// [`node()`](OnReady::<Gd<T>>::from_node).**<br>
24+
/// 1. **Automatic mode, using [`new()`](OnReady::new), [`from_base_fn()`](OnReady::from_base_fn),
25+
/// [`from_node()`][Self::from_node] or [`from_loaded()`][Self::from_loaded].**<br>
2626
/// Before `ready()` is called, all `OnReady` fields constructed with the above methods are automatically initialized,
27-
/// in the order of declaration. This means that you can safely access them in `ready()`.<br><br>
27+
/// in the order of declaration. This means that you can safely access them in `ready()`.<br>
2828
/// 2. **Manual mode, using [`manual()`](Self::manual).**<br>
2929
/// These fields are left uninitialized until you call [`init()`][Self::init] on them. This is useful if you need more complex
3030
/// initialization scenarios than a closure allows. If you forget initialization, a panic will occur on first access.
@@ -111,16 +111,18 @@ pub struct OnReady<T> {
111111
state: InitState<T>,
112112
}
113113

114-
impl<T: GodotClass + Inherits<Node>> OnReady<Gd<T>> {
114+
impl<T: Inherits<Node>> OnReady<Gd<T>> {
115115
/// Variant of [`OnReady::new()`], fetching the node located at `path` before `ready()`.
116116
///
117-
/// This is the functional equivalent of the GDScript pattern `@onready var node = $NodePath`.
117+
/// This is the functional equivalent of the GDScript pattern `@onready var node = $NODE_PATH`.
118118
///
119-
/// # Panics
120-
/// - If `path` does not point to a valid node.
119+
/// When used with `#[class(init)]`, the field can be annotated with `#[init(node = "NODE_PATH")]` to call this constructor.
120+
///
121+
/// # Panics (deferred)
122+
/// - If `path` does not point to a valid node, or its type is not a `T` or a subclass.
121123
///
122124
/// Note that the panic will only happen if and when the node enters the SceneTree for the first time
123-
/// (i.e.: it receives the `READY` notification).
125+
/// (i.e. it receives the `READY` notification).
124126
pub fn from_node(path: impl AsArg<NodePath>) -> Self {
125127
arg_into_owned!(path);
126128

@@ -133,6 +135,25 @@ impl<T: GodotClass + Inherits<Node>> OnReady<Gd<T>> {
133135
}
134136
}
135137

138+
impl<T: Inherits<Resource>> OnReady<Gd<T>> {
139+
/// Variant of [`OnReady::new()`], loading the resource stored at `path` before `ready()`.
140+
///
141+
/// This is the functional equivalent of the GDScript pattern `@onready var res = load(...)`.
142+
///
143+
/// When used with `#[class(init)]`, the field can be annotated with `#[init(load = "FILE_PATH")]` to call this constructor.
144+
///
145+
/// # Panics (deferred)
146+
/// - If the resource does not exist at `path`, cannot be loaded or is not compatible with type `T`.
147+
///
148+
/// Note that the panic will only happen if and when the node enters the SceneTree for the first time
149+
/// (i.e. it receives the `READY` notification).
150+
pub fn from_loaded(path: impl AsArg<GString>) -> Self {
151+
arg_into_owned!(path);
152+
153+
Self::new(move || crate::tools::load(&path))
154+
}
155+
}
156+
136157
impl<T> OnReady<T> {
137158
/// Schedule automatic initialization before `ready()`.
138159
///
@@ -175,7 +196,7 @@ impl<T> OnReady<T> {
175196
///
176197
/// # Panics
177198
/// - If `init()` was called before.
178-
/// - If this object was already provided with a closure during construction, in [`Self::new()`].
199+
/// - If this object was already provided with a closure during construction, in [`Self::new()`] or any other automatic constructor.
179200
pub fn init(&mut self, value: T) {
180201
match &self.state {
181202
InitState::ManualUninitialized { .. } => {

godot-macros/src/class/data_models/field.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
use crate::class::{FieldExport, FieldVar};
9+
use crate::util::error;
910
use proc_macro2::{Ident, Span, TokenStream};
1011
use quote::ToTokens;
1112

@@ -37,6 +38,48 @@ impl Field {
3738
span: field.span(),
3839
}
3940
}
41+
42+
#[must_use]
43+
pub fn ensure_preconditions(
44+
&self,
45+
cond: Option<FieldCond>,
46+
span: Span,
47+
errors: &mut Vec<venial::Error>,
48+
) -> bool {
49+
let prev_size = errors.len();
50+
51+
if self.default_val.is_some() {
52+
errors.push(error!(
53+
span,
54+
"#[init] can have at most one key among `val|node|load`"
55+
));
56+
}
57+
58+
match cond {
59+
Some(FieldCond::IsOnReady) if !self.is_onready => {
60+
errors.push(error!(
61+
span,
62+
"used #[init(…)] pattern requires field type `OnReady<T>`"
63+
));
64+
}
65+
66+
Some(FieldCond::IsOnEditor) if !self.is_oneditor => {
67+
errors.push(error!(
68+
span,
69+
"used #[init(…)] pattern requires field type `OnEditor<T>`"
70+
));
71+
}
72+
73+
_ => {}
74+
}
75+
76+
errors.len() == prev_size
77+
}
78+
}
79+
80+
pub enum FieldCond {
81+
IsOnReady,
82+
IsOnEditor,
4083
}
4184

4285
pub struct Fields {

godot-macros/src/class/derive_godot_class.rs

Lines changed: 39 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ use proc_macro2::{Ident, Punct, TokenStream};
99
use quote::{format_ident, quote, quote_spanned};
1010

1111
use crate::class::{
12-
make_property_impl, make_virtual_callback, BeforeKind, Field, FieldDefault, FieldExport,
13-
FieldVar, Fields, SignatureInfo,
12+
make_property_impl, make_virtual_callback, BeforeKind, Field, FieldCond, FieldDefault,
13+
FieldExport, FieldVar, Fields, SignatureInfo,
1414
};
1515
use crate::util::{
1616
bail, error, format_funcs_collection_struct, ident, path_ends_with_complex,
@@ -530,7 +530,7 @@ fn parse_fields(
530530
);
531531
}
532532

533-
// #[init(val = expr)]
533+
// #[init(val = EXPR)]
534534
if let Some(default) = parser.handle_expr("val")? {
535535
field.default_val = Some(FieldDefault {
536536
default_val: default,
@@ -555,30 +555,13 @@ fn parse_fields(
555555
})
556556
}
557557

558-
// #[init(node = "NodePath")]
558+
// #[init(node = "PATH")]
559559
if let Some(node_path) = parser.handle_expr("node")? {
560-
let mut is_well_formed = true;
561-
if !field.is_onready {
562-
is_well_formed = false;
563-
errors.push(error!(
564-
parser.span(),
565-
"The key `node` in attribute #[init] requires field of type `OnReady<T>`\n\
566-
Hint: the syntax #[init(node = \"NodePath\")] is equivalent to \
567-
#[init(val = OnReady::from_node(\"NodePath\"))], \
568-
which can only be assigned to fields of type `OnReady<T>`"
569-
));
570-
}
571-
572-
if field.default_val.is_some() {
573-
is_well_formed = false;
574-
errors.push(error!(
575-
parser.span(),
576-
"The key `node` in attribute #[init] is mutually exclusive with the keys `default` and `val`\n\
577-
Hint: the syntax #[init(node = \"NodePath\")] is equivalent to \
578-
#[init(val = OnReady::from_node(\"NodePath\"))], \
579-
both aren't allowed since they would override each other"
580-
));
581-
}
560+
let is_well_formed = field.ensure_preconditions(
561+
Some(FieldCond::IsOnReady),
562+
parser.span(),
563+
&mut errors,
564+
);
582565

583566
let default_val = if is_well_formed {
584567
quote! { OnReady::from_node(#node_path) }
@@ -592,27 +575,36 @@ fn parse_fields(
592575
});
593576
}
594577

595-
// #[init(sentinel = val)]
596-
if let Some(sentinel_representation) = parser.handle_expr("sentinel")? {
597-
let mut is_well_formed = true;
598-
if !field.is_oneditor {
599-
is_well_formed = false;
600-
errors.push(error!(
601-
parser.span(),
602-
"The key `sentinel` in attribute #[init] requires field of type `OnEditor<T>`"
603-
));
604-
}
578+
// #[init(load = "PATH")]
579+
if let Some(resource_path) = parser.handle_expr("load")? {
580+
let is_well_formed = field.ensure_preconditions(
581+
Some(FieldCond::IsOnReady),
582+
parser.span(),
583+
&mut errors,
584+
);
605585

606-
if field.default_val.is_some() {
607-
is_well_formed = false;
608-
errors.push(error!(
609-
parser.span(),
610-
"The key `sentinel` in attribute #[init] is mutually exclusive with the key `val`"
611-
));
612-
}
586+
let default_val = if is_well_formed {
587+
quote! { OnReady::from_loaded(#resource_path) }
588+
} else {
589+
quote! { todo!() }
590+
};
591+
592+
field.default_val = Some(FieldDefault {
593+
default_val,
594+
span: parser.span(),
595+
});
596+
}
597+
598+
// #[init(sentinel = EXPR)]
599+
if let Some(sentinel_value) = parser.handle_expr("sentinel")? {
600+
let is_well_formed = field.ensure_preconditions(
601+
Some(FieldCond::IsOnEditor),
602+
parser.span(),
603+
&mut errors,
604+
);
613605

614606
let default_val = if is_well_formed {
615-
quote! { OnEditor::from_sentinel( #sentinel_representation ) }
607+
quote! { OnEditor::from_sentinel(#sentinel_value) }
616608
} else {
617609
quote! { todo!() }
618610
};
@@ -649,6 +641,9 @@ fn parse_fields(
649641
if let Some(override_onready) = handle_opposite_keys(&mut parser, "onready", "hint")? {
650642
field.is_onready = override_onready;
651643
}
644+
645+
// Not yet implemented for OnEditor.
646+
652647
parser.finish()?;
653648
}
654649

itest/rust/src/engine_tests/save_load_test.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
66
*/
77

8-
use godot::obj::NewGd;
8+
use godot::classes;
9+
use godot::classes::notify::NodeNotification;
10+
use godot::obj::{Base, Gd, NewAlloc, NewGd, OnReady};
911
use godot::register::GodotClass;
1012
use godot::tools::{load, save, try_load, try_save};
1113

@@ -25,6 +27,17 @@ struct SavedGame {
2527
level: u32,
2628
}
2729

30+
// Needed to test OnReady integration of load.
31+
#[derive(GodotClass)]
32+
#[class(base=Node, init)]
33+
struct GameLoader {
34+
// Test also more complex expressions.
35+
#[init(load = &format!("res://{}", RESOURCE_NAME))]
36+
game: OnReady<Gd<SavedGame>>,
37+
38+
_base: Base<classes::Node>,
39+
}
40+
2841
const RESOURCE_NAME: &str = "test_resource.tres";
2942
const FAULTY_PATH: &str = "no_such_path";
3043

@@ -69,3 +82,20 @@ fn load_test() {
6982

7083
remove_test_file(RESOURCE_NAME);
7184
}
85+
86+
#[itest]
87+
fn load_with_onready() {
88+
let res_path = format!("res://{}", RESOURCE_NAME);
89+
90+
let mut resource = SavedGame::new_gd();
91+
resource.bind_mut().set_level(555);
92+
93+
save(&resource, &res_path);
94+
95+
let mut loader = GameLoader::new_alloc();
96+
loader.notify(NodeNotification::READY);
97+
assert_eq!(loader.bind().game.bind().get_level(), 555);
98+
loader.free();
99+
100+
remove_test_file(RESOURCE_NAME);
101+
}

itest/rust/src/object_tests/onready_test.rs

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

8+
// Integration of OnReady with #[init(load = "PATH")] is tested in save_load_test.rs.
9+
810
use crate::framework::{expect_panic, itest};
911
use godot::classes::notify::NodeNotification;
1012
use godot::classes::{INode, Node};

0 commit comments

Comments
 (0)