Skip to content

Commit 3448fa3

Browse files
authored
Add small chapter for registering ResourceFormatLoaders/Savers. (#65)
1 parent ca277b4 commit 3448fa3

File tree

3 files changed

+228
-0
lines changed

3 files changed

+228
-0
lines changed

src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
- [Editor plugins](recipes/editor-plugin/index.md)
3636
- [Inspector plugins](recipes/editor-plugin/inspector-plugins.md)
3737
- [Engine singletons](recipes/engine-singleton.md)
38+
- [`Resource` savers and loaders](recipes/resource-saver-loader.md)
3839
- [Custom node icons](recipes/custom-icons.md)
3940
- [Ecosystem](ecosystem/index.md)
4041
- [Contributing to gdext](contribute/index.md)

src/recipes/index.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ An Engine Singleton is a class instance that is always globally available (follo
2525
it cannot access the `SceneTree` through any reliable means.
2626

2727

28+
## `ResourceFormatSaver` and `ResourceFormatLoader`
29+
30+
Provide custom logic for saving and loading your `Resource` derived classes.
31+
32+
2833
## Custom icons
2934

3035
Adding custom icons to your classes is actually fairly simple!

src/recipes/resource-saver-loader.md

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
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+
# `Resource` savers and loaders
9+
10+
The [`ResourceFormatSaver`][godot-saver] and [`ResourceFormatLoader`][godot-loader] classes allow you to serialize and deserialize your Rust
11+
`Resource`-derived classes with a custom procedure, as well as define new recognized file extensions. This is mostly useful if you have resources
12+
that contain _pure Rust state_. "Pure" in this context refers to members of your struct that don’t have any `#[var]` or similar annotations, i.e.
13+
Godot isn't aware of them. This can easily be the case when you work with Rust libraries.
14+
15+
The following example gives you a starting point to copy-and-paste. For advanced use cases, consult the Godot documentation for these classes.
16+
17+
First of all, you need to call the provided functions in your library entry point at the `InitLevel::Scene`. This ensures proper initialization
18+
and cleanup of your loader/saver.
19+
20+
```rust
21+
// These imports will be needed across the following code samples.
22+
use godot::classes::{
23+
Engine, IResourceFormatLoader, IResourceFormatSaver, ResourceFormatLoader,
24+
ResourceFormatSaver, ResourceLoader, ResourceSaver,
25+
};
26+
use godot::prelude::*;
27+
28+
#[gdextension]
29+
unsafe impl ExtensionLibrary for MyGDExtension {
30+
// Register the singleton when the extension is loading.
31+
fn on_level_init(level: InitLevel) {
32+
if level == InitLevel::Scene {
33+
Engine::singleton().register_singleton(
34+
"MyAssetSingleton",
35+
&MyAssetSingleton::new_alloc(),
36+
);
37+
}
38+
}
39+
40+
// Unregisters the singleton when the extension exits.
41+
fn on_level_init(level: InitLevel) {
42+
if level == InitLevel::Scene {
43+
Engine::singleton().unregister_singleton("MyAssetSingleton");
44+
my_singleton.free();
45+
}
46+
}
47+
}
48+
```
49+
50+
Define the singleton to keep track of your Loaders and Savers.
51+
52+
```rust
53+
// The definition of the singleton with all your loader/savers as members,
54+
// to keep the object references for destruction later.
55+
#[derive(GodotClass)]
56+
#[class(base=Object, tool)]
57+
struct MyAssetSingleton {
58+
base: Base<Object>,
59+
loader: Gd<MyAssetLoader>,
60+
saver: Gd<MyAssetSaver>,
61+
}
62+
63+
#[godot_api]
64+
impl IObject for MyAssetSingleton {
65+
fn init(base: Base<Object>) -> Self {
66+
let saver = MyAssetSaver::new_gd();
67+
let loader = MyAssetLoader::new_gd();
68+
69+
// Register the loader and saver in Godot.
70+
//
71+
// If you want your default extension to be the one defined by your loader,
72+
// set the `at_front` parameter to true. Otherwise you can also remove the
73+
// builder. Godot currently doesn't provide a way to completely deactivate
74+
// the built-in loaders.
75+
// WARNING: The built-in loaders won't work if you have _pure Rust state_.
76+
ResourceSaver::singleton().add_resource_format_saver_ex(&saver)
77+
.at_front(false)
78+
.done();
79+
ResourceLoader::singleton().add_resource_format_loader(&loader);
80+
81+
Self { base, loader, saver }
82+
}
83+
}
84+
```
85+
86+
```admonish warning title="at_front behavior"
87+
The ordering of `at_front` may currently not work as expected in Godot. For more information, see PR [godot#101543] or
88+
book discussion [#65][book#65].
89+
```
90+
91+
92+
The minimal code for a **saver**, with all required virtual methods defined:
93+
94+
```rust
95+
#[derive(GodotClass)]
96+
#[class(base=ResourceFormatSaver, init, tool)]
97+
struct MyAssetSaver {
98+
base: Base<ResourceFormatSaver>,
99+
}
100+
101+
#[godot_api]
102+
impl IResourceFormatSaver for MyAssetSaver {
103+
// If you want a custom extension name (e.g., resource.myextension),
104+
// then override this.
105+
fn get_recognized_extensions(
106+
&self,
107+
res: Option<Gd<Resource>>
108+
) -> PackedStringArray {
109+
let mut array = PackedStringArray::new();
110+
111+
// Even though the Godot docs state that you don't need this check, it is
112+
// in fact necessary.
113+
if Self::is_recognized_resource(res) {
114+
// It is also possible to add multiple extensions per Saver.
115+
array.push("myextension");
116+
}
117+
118+
array
119+
}
120+
121+
// All resource types that this saver should handle must return true.
122+
fn is_recognized_resource(res: Option<Gd<Resource>>) -> bool {
123+
// It is also possible to add multible resource types per Saver.
124+
res.expect("Godot called this without an input resource?")
125+
.is_class("MyResourceType")
126+
}
127+
128+
// This defines your logic for actually saving your resource.
129+
fn save(
130+
&mut self,
131+
// The resource that is currently getting saved.
132+
resource: Option<Gd<Resource>>,
133+
// The path that the resource is getting saved at.
134+
path: GString,
135+
// Flags for saving (see link below).
136+
flags: u32,
137+
) -> godot::global::Error {
138+
// TODO: Put your saving logic in here, with the `GFile` API (see link below).
139+
140+
godot::global::Error::OK
141+
}
142+
}
143+
```
144+
145+
Here are direct doc links to `SaverFlags` ([Godot][godot-saverflags], [Rust][api-saverflags]) and [`GFile`][api-gfile].
146+
147+
148+
The minimal code for a **loader**, with all required virtual methods defined:
149+
150+
```rust
151+
#[derive(GodotClass)]
152+
#[class(init, tool, base=ResourceFormatLoader)]
153+
struct MyAssetLoader {
154+
base: Base<ResourceFormatLoader>,
155+
}
156+
157+
#[godot_api]
158+
impl IResourceFormatLoader for MyAssetLoader {
159+
// All file extensions you want to be redirected to your loader
160+
// should be added here.
161+
fn get_recognized_extensions(&self) -> PackedStringArray {
162+
let mut arr = PackedStringArray::new();
163+
arr.push("myextension");
164+
arr
165+
}
166+
167+
// All resource types that this loader handles.
168+
fn handles_type(&self, ty: StringName) -> bool {
169+
ty == "MyResourceType".into()
170+
}
171+
172+
// The stringified name of your resource should be returned.
173+
fn get_resource_type(&self, path: GString) -> GString {
174+
// The extension arg always comes with a `.` in Godot, so don't forget it ;)
175+
if path.get_extension().to_lower() == ".myextension".into() {
176+
"MyResourceType".into()
177+
} else {
178+
// In case of not handling the given resource, this function must
179+
// return an empty string.
180+
GString::new()
181+
}
182+
}
183+
184+
// The actual loading and parsing of your data.
185+
fn load(
186+
&self,
187+
// The path that should be openend to load the resource.
188+
path: GString,
189+
// If the resource was part of a import step you can access the original file
190+
// with this. Otherwise this path is equal to the normal path.
191+
original_path: GString,
192+
// This parameter is true when the resource is loaded with
193+
// load_threaded_request().
194+
// Internal implementations in Godot also ignore this parameter.
195+
_use_sub_threads: bool,
196+
// If you want to provide custom caching this parameter is the CacheMode enum.
197+
// You can look into the ResourceLoader docs to learn about the values.
198+
// When calling the default load() method, cache_mode is CacheMode::REUSE.
199+
cache_mode: i32,
200+
) -> Variant {
201+
// TODO: Put your saving logic in here, with the `GFile` API (see link below).
202+
203+
// If your loading operation failed and you want to handle errors,
204+
// you can return a godot::global::Error and cast it to a Variant.
205+
}
206+
}
207+
```
208+
209+
Direct link to `CacheMode` ([Godot][godot-cachemode], [Rust][api-cachemode]) and [`GFile`][api-gfile].
210+
211+
[godot-cachemode]: https://docs.godotengine.org/en/stable/classes/class_resourceformatloader.html#enum-resourceformatloader-cachemode
212+
[api-cachemode]: https://godot-rust.github.io/docs/gdext/master/godot/classes/resource_loader/enum.CacheMode.html
213+
214+
[godot-saverflags]: https://docs.godotengine.org/en/stable/classes/class_resourcesaver.html#enum-resourcesaver-saverflags
215+
[api-saverflags]: https://godot-rust.github.io/docs/gdext/master/godot/classes/resource_saver/struct.SaverFlags.html
216+
[api-gfile]: https://godot-rust.github.io/docs/gdext/master/godot/prelude/struct.GFile.html
217+
218+
[godot-saver]: https://docs.godotengine.org/en/stable/classes/class_resourceformatsaver.html
219+
[godot-loader]: https://docs.godotengine.org/en/stable/classes/class_resourceformatloader.html
220+
221+
[godot#101543]: https://github.com/godotengine/godot/pull/101543
222+
[book#65]: https://github.com/godot-rust/book/pull/65#issuecomment-2585403123

0 commit comments

Comments
 (0)