|
| 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