├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── build.rs ├── examples ├── assets │ ├── fonts │ │ ├── FiraMono-LICENSE │ │ ├── FiraMono-Medium.ttf │ │ └── FiraSans-Bold.ttf │ ├── sounds │ │ └── breakout_collision.ogg │ └── tiles.png ├── breakout.rs ├── format.rs ├── heirarchy.rs ├── pipeline.rs └── saves │ ├── breakout.json │ ├── format.ron │ ├── heirarchy.json │ └── pipeline │ ├── 0.0.json │ ├── 0.1.json │ ├── 1.0.json │ └── 1.1.json ├── rustfmt.toml ├── src ├── backend.rs ├── checkpoint │ ├── mod.rs │ ├── registry.rs │ └── state.rs ├── clone.rs ├── commands.rs ├── dir.rs ├── error.rs ├── ext │ ├── app.rs │ ├── commands.rs │ ├── mod.rs │ └── world.rs ├── format.rs ├── lib.rs ├── middleware.rs ├── pipeline.rs ├── plugins.rs ├── prefab.rs ├── serde │ ├── de.rs │ ├── mod.rs │ └── ser.rs └── snapshot │ ├── applier.rs │ ├── builder.rs │ ├── mod.rs │ └── snapshot.rs └── tests ├── bevy.rs ├── format.rs ├── generations.rs └── overwrite.rs /.gitignore: -------------------------------------------------------------------------------- 1 | debug/ 2 | target/ 3 | Cargo.lock 4 | **/*.rs.bk 5 | *.pdb 6 | .vscode/ 7 | *.zst -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["."] 3 | 4 | [package] 5 | name = "bevy_save" 6 | version = "0.18.0" 7 | edition = "2021" 8 | description = "A framework for saving and loading application state in Bevy." 9 | license = "MIT OR Apache-2.0" 10 | readme = "README.md" 11 | repository = "https://github.com/hankjordan/bevy_save" 12 | rust-version = "1.82.0" 13 | 14 | [package.metadata.docs.rs] 15 | rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] 16 | all-features = true 17 | 18 | [features] 19 | default = ["bevy_asset", "bevy_render", "bevy_sprite"] 20 | bevy_asset = ["bevy/bevy_asset"] 21 | bevy_render = ["bevy/bevy_render"] 22 | bevy_sprite = ["bevy/bevy_sprite"] 23 | brotli = ["dep:brotli"] 24 | 25 | [dependencies] 26 | bevy = { version = "0.16", default-features = false, features = ["bevy_scene", "bevy_log"] } 27 | rmp-serde = "1.3" 28 | serde_json = "1.0" 29 | serde = "1.0" 30 | platform-dirs = "0.3" 31 | thiserror = "2.0" 32 | async-std = "1.13" 33 | brotli = { version = "7.0", optional = true } 34 | 35 | [target.'cfg(target_arch = "wasm32")'.dependencies] 36 | bevy = { version = "0.16", default-features = false } 37 | web-sys = { version = "0.3", default-features = false, features = [ 38 | "Storage", 39 | "Window", 40 | ] } 41 | wasm-bindgen = { version = "0.2", default-features = false } 42 | fragile = "2.0" 43 | uuid = { version = "1.16", features = ["js"] } 44 | 45 | [dev-dependencies] 46 | bevy = { version = "0.16" } 47 | bevy-inspector-egui = "0.31.0" 48 | ron = "0.9" 49 | io-adapters = "0.4" 50 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022 Henry Jordan 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Henry Jordan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bevy_save 2 | 3 | [![][img_bevy]][bevy] [![][img_version]][crates] [![][img_doc]][doc] [![][img_license]][license] [![][img_tracking]][tracking] [![][img_downloads]][crates] 4 | 5 | A framework for saving and loading application state in Bevy. 6 | 7 | 8 | 9 | ## Features 10 | 11 | ### Save file management 12 | 13 | `bevy_save` automatically uses your app's workspace name to create a unique, permanent save location in the correct place for [any platform](#platforms) it can run on. 14 | 15 | By default, [`World::save()`](https://docs.rs/bevy_save/latest/bevy_save/prelude/trait.WorldSaveableExt.html#tymethod.save) and [`World::load()`](https://docs.rs/bevy_save/latest/bevy_save/prelude/trait.WorldSaveableExt.html#tymethod.load) uses the managed save file location to save and load your application state, handling all serialization and deserialization for you. 16 | 17 | #### Save directory location 18 | 19 | With the default [`FileIO`] backend, your save directory is managed for you. 20 | 21 | [`WORKSPACE`] is the name of your project's workspace (parent folder) name. 22 | 23 | | Windows | Linux/\*BSD | MacOS | 24 | | --------------------------------------------------- | -------------------------------- | ----------------------------------------------- | 25 | | `C:\Users\%USERNAME%\AppData\Local\WORKSPACE\saves` | `~/.local/share/WORKSPACE/saves` | `~/Library/Application Support/WORKSPACE/saves` | 26 | 27 | On WASM, snapshots are saved to [`LocalStorage`], with the key `WORKSPACE.KEY`. 28 | 29 | ### Snapshots and rollback 30 | 31 | `bevy_save` is not just about save files, it is about total control over application state. 32 | 33 | This crate introduces a snapshot type which may be used directly: 34 | 35 | - [`Snapshot`] is a serializable snapshot of all saveable resources, entities, and components. 36 | 37 | Or via the [`World`] extension methods [`WorldSaveableExt`](https://docs.rs/bevy_save/latest/bevy_save/prelude/trait.WorldSaveableExt.html) and [`WorldCheckpointExt`](https://docs.rs/bevy_save/latest/bevy_save/prelude/trait.WorldCheckpointExt.html): 38 | 39 | - [`World::snapshot()`](https://docs.rs/bevy_save/latest/bevy_save/prelude/trait.WorldSaveableExt.html#tymethod.snapshot) captures a snapshot of the current application state, including resources. 40 | - [`World::checkpoint()`](https://docs.rs/bevy_save/latest/bevy_save/prelude/trait.WorldCheckpointExt.html#tymethod.checkpoint) captures a snapshot for later rollback / rollforward. 41 | - [`World::rollback()`](https://docs.rs/bevy_save/latest/bevy_save/prelude/trait.WorldCheckpointExt.html#tymethod.rollback) rolls the application state backwards or forwards through any checkpoints you have created. 42 | 43 | The [`Checkpoints`] resource also gives you fine-tuned control of the currently stored rollback checkpoints. 44 | 45 | ### Type registration 46 | 47 | No special traits or NewTypes necessary, `bevy_save` takes full advantage of Bevy's built-in reflection. 48 | As long as the type implements [`Reflect`], it can be registered and used with `bevy_save`. 49 | 50 | `bevy_save` provides extension traits for [`App`] allowing you to do so. 51 | 52 | - [`App.init_pipeline::

()`](https://docs.rs/bevy_save/latest/bevy_save/prelude/trait.AppSaveableExt.html#tymethod.init_pipeline) initializes a [`Pipeline`] for use with save / load. 53 | - [`App.allow_checkpoint::()`](https://docs.rs/bevy_save/latest/bevy_save/prelude/trait.AppCheckpointExt.html#tymethod.allow_checkpoint) allows a type to roll back. 54 | - [`App.deny_checkpoint::()`](https://docs.rs/bevy_save/latest/bevy_save/prelude/trait.AppCheckpointExt.html#tymethod.deny_checkpoint) denies a type from rolling back. 55 | 56 | ### Backend 57 | 58 | The [`Backend`] is the interface between your application and persistent storage. 59 | 60 | Some example backends may include [`FileIO`], sqlite, [`LocalStorage`], or network storage. 61 | 62 | ```rust,ignore 63 | #[derive(Default, Resource)] 64 | pub struct FileIO; 65 | 66 | impl Backend for FileIO { 67 | async fn save(&self, key: K, value: &T) -> Result<(), Error> { 68 | let path = get_save_file(format!("{key}{}", F::extension())); 69 | let dir = path.parent().expect("Invalid save directory"); 70 | create_dir_all(dir).await?; 71 | let mut buf = Vec::new(); 72 | F::serialize(&mut buf, value)?; 73 | let mut file = File::create(path).await?; 74 | Ok(file.write_all(&buf).await?) 75 | } 76 | 77 | async fn load DeserializeSeed<'de, Value = T>, T>( 78 | &self, 79 | key: K, 80 | seed: S, 81 | ) -> Result { 82 | let path = get_save_file(format!("{key}{}", F::extension())); 83 | let mut file = File::open(path).await?; 84 | let mut buf = Vec::new(); 85 | file.read_to_end(&mut buf).await?; 86 | F::deserialize(&*buf, seed) 87 | } 88 | } 89 | ``` 90 | 91 | ### Format 92 | 93 | [`Format`] is how your application serializes and deserializes your data. 94 | 95 | [`Format`]s can either be human-readable like [`JSON`] or binary like [`MessagePack`]. 96 | 97 | ```rust,ignore 98 | pub struct RONFormat; 99 | 100 | impl Format for RONFormat { 101 | fn extension() -> &'static str { 102 | ".ron" 103 | } 104 | 105 | fn serialize(writer: W, value: &T) -> Result<(), Error> { 106 | let mut ser = ron::Serializer::new( 107 | writer.write_adapter(), 108 | Some(ron::ser::PrettyConfig::default()), 109 | ) 110 | .map_err(Error::saving)?; 111 | 112 | value.serialize(&mut ser).map_err(Error::saving) 113 | } 114 | 115 | fn deserialize DeserializeSeed<'de, Value = T>, T>( 116 | reader: R, 117 | seed: S, 118 | ) -> Result { 119 | ron::options::Options::default() 120 | .from_reader_seed(reader, seed) 121 | .map_err(Error::loading) 122 | } 123 | } 124 | ``` 125 | 126 | ### Pipeline 127 | 128 | The [`Pipeline`] trait allows you to use multiple different configurations of [`Backend`] and [`Format`] in the same [`App`]. 129 | 130 | Using [`Pipeline`] also lets you re-use [`Snapshot`] appliers and builders. 131 | 132 | ```rust,ignore 133 | struct HeirarchyPipeline; 134 | 135 | impl Pipeline for HeirarchyPipeline { 136 | type Backend = DefaultDebugBackend; 137 | type Format = DefaultDebugFormat; 138 | 139 | type Key<'a> = &'a str; 140 | 141 | fn key(&self) -> Self::Key<'_> { 142 | "examples/saves/heirarchy" 143 | } 144 | 145 | fn capture(&self, builder: SnapshotBuilder) -> Snapshot { 146 | builder 147 | .extract_entities_matching(|e| e.contains::() || e.contains::()) 148 | .build() 149 | } 150 | 151 | fn apply(&self, world: &mut World, snapshot: &Snapshot) -> Result<(), bevy_save::Error> { 152 | snapshot 153 | .applier(world) 154 | .despawn::, With)>>() 155 | .apply() 156 | } 157 | } 158 | ``` 159 | 160 | ### Prefabs 161 | 162 | The [`Prefab`] trait allows you to easily spawn entities from a blueprint. 163 | 164 | ```rust,ignore 165 | #[derive(Component, Default, Reflect)] 166 | #[reflect(Component)] 167 | struct Ball; 168 | 169 | #[derive(Reflect)] 170 | struct BallPrefab { 171 | position: Vec3, 172 | } 173 | 174 | impl Prefab for BallPrefab { 175 | type Marker = Ball; 176 | 177 | fn spawn(self, target: Entity, world: &mut World) { 178 | // Some entities will need initialization from world state, such as mesh assets. 179 | // We can do that here. 180 | let mesh = world.resource_mut::>().add(Circle::default()); 181 | let material = world 182 | .resource_mut::>() 183 | .add(BALL_COLOR); 184 | 185 | world.entity_mut(target).insert(( 186 | Mesh2d(mesh), 187 | MeshMaterial2d(material), 188 | Transform::from_translation(self.position) 189 | .with_scale(Vec2::splat(BALL_DIAMETER).extend(1.)), 190 | Ball, 191 | Velocity(INITIAL_BALL_DIRECTION.normalize() * BALL_SPEED), 192 | )); 193 | } 194 | 195 | fn extract(builder: SnapshotBuilder) -> SnapshotBuilder { 196 | // We don't actually need to save all of those runtime components. 197 | // Only save the translation of the Ball. 198 | builder.extract_prefab(|entity| { 199 | Some(BallPrefab { 200 | position: entity.get::()?.translation, 201 | }) 202 | }) 203 | } 204 | } 205 | ``` 206 | 207 | ### Type Filtering and Partial Snapshots 208 | 209 | While `bevy_save` aims to make it as easy as possible to save your entire world, some applications also need to be able to save only parts of the world. 210 | 211 | [`SnapshotBuilder`] allows you to manually create snapshots like [`DynamicSceneBuilder`]: 212 | 213 | ```rust,ignore 214 | fn build_snapshot(world: &World, target: Entity, children: Query<&Children>) -> Snapshot { 215 | Snapshot::builder(world) 216 | // Extract all resources 217 | .extract_all_resources() 218 | 219 | // Extract all descendants of `target` 220 | // This will include all components not denied by the builder's filter 221 | .extract_entities(children.iter_descendants(target)) 222 | 223 | // Entities without any components will also be extracted 224 | // You can use `clear_empty` to remove them 225 | .clear_empty() 226 | 227 | // Build the `Snapshot` 228 | .build() 229 | } 230 | ``` 231 | 232 | You are also able to extract resources by type: 233 | 234 | ```rust,ignore 235 | Snapshot::builder(world) 236 | // Extract the resource by the type name 237 | // In this case, we extract the resource from the `manual` example 238 | .extract_resource::() 239 | 240 | // Build the `Snapshot` 241 | // It will only contain the one resource we extracted 242 | .build() 243 | ``` 244 | 245 | Additionally, explicit type filtering like [`SnapshotApplier`] is available when building snapshots: 246 | 247 | ```rust,ignore 248 | Snapshot::builder(world) 249 | // Exclude `Transform` from this `Snapshot` 250 | .deny::() 251 | 252 | // Extract all matching entities and resources 253 | .extract_all() 254 | 255 | // Clear all extracted entities without any components 256 | .clear_empty() 257 | 258 | // Build the `Snapshot` 259 | .build() 260 | ``` 261 | 262 | ### Entity hooks 263 | 264 | You are also able to add hooks when applying snapshots, similar to `bevy-scene-hook`. 265 | 266 | This can be used for many things, like spawning the snapshot as a child of an entity: 267 | 268 | ```rust,ignore 269 | let snapshot = Snapshot::from_world(world); 270 | 271 | snapshot 272 | .applier(world) 273 | 274 | // This will be run for every Entity in the snapshot 275 | // It runs after the Entity's Components are loaded 276 | .hook(move |entity, cmds| { 277 | // You can use the hook to add, get, or remove Components 278 | if !entity.contains::() { 279 | cmds.set_parent(parent); 280 | } 281 | }) 282 | 283 | .apply(); 284 | ``` 285 | 286 | Hooks may also despawn entities: 287 | 288 | ```rust,ignore 289 | let snapshot = Snapshot::from_world(world); 290 | 291 | snapshot 292 | .applier(world) 293 | 294 | .hook(|entity, cmds| { 295 | if entity.contains::() { 296 | cmds.despawn(); 297 | } 298 | }) 299 | ``` 300 | 301 | ### Entity mapping 302 | 303 | As Entity ids are not intended to be used as unique identifiers, `bevy_save` supports mapping Entity ids. 304 | 305 | First, you'll need to get a [`SnapshotApplier`]: 306 | 307 | - [`Snapshot::applier()`](https://docs.rs/bevy_save/latest/bevy_save/prelude/struct.Snapshot.html#method.applier) 308 | - [`SnapshotApplier::new()`](https://docs.rs/bevy_save/latest/bevy_save/prelude/struct.SnapshotApplier.html#method.new) 309 | 310 | The [`SnapshotApplier`] will then allow you to configure the entity map (and other settings) before applying: 311 | 312 | ```rust,ignore 313 | let snapshot = Snapshot::from_world(world); 314 | 315 | snapshot 316 | .applier(world) 317 | 318 | // Your entity map 319 | .entity_map(HashMap::default()) 320 | 321 | // Despawn all entities matching (With, Without) 322 | .despawn::<(With, Without)>() 323 | 324 | .apply(); 325 | ``` 326 | 327 | #### MapEntities 328 | 329 | `bevy_save` also supports [`MapEntities`](https://docs.rs/bevy/latest/bevy/ecs/entity/trait.MapEntities.html) via reflection to allow you to update entity ids within components and resources. 330 | 331 | See [Bevy's Parent Component](https://github.com/bevyengine/bevy/blob/v0.15.3/crates/bevy_hierarchy/src/components/parent.rs) for a simple example. 332 | 333 | ## Stability warning 334 | 335 | `bevy_save` does not _yet_ provide any stability guarantees for save file format between crate versions. 336 | 337 | `bevy_save` relies on serialization to create save files and as such is exposed to internal implementation details for types. 338 | Expect Bevy or other crate updates to break your save file format. 339 | It should be possible to mitigate this by overriding [`ReflectDeserialize`] for any offending types. 340 | 341 | Changing what entities have what components or how you use your entities or resources in your logic can also result in broken saves. 342 | While `bevy_save` does not _yet_ have explicit support for save file migration, you can use [`SnapshotApplier::hook`](https://docs.rs/bevy_save/latest/bevy_save/prelude/struct.SnapshotApplier.html#method.hook) to account for changes while applying a snapshot. 343 | 344 | If your application has specific migration requirements, please [open an issue](https://github.com/hankjordan/bevy_save/issues/new). 345 | 346 | ### Entity 347 | 348 | > For all intents and purposes, [`Entity`] should be treated as an opaque identifier. The internal bit representation is liable to change from release to release as are the behaviors or performance characteristics of any of its trait implementations (i.e. `Ord`, `Hash,` etc.). This means that changes in [`Entity`]’s representation, though made readable through various functions on the type, are not considered breaking changes under SemVer. 349 | > 350 | > In particular, directly serializing with `Serialize` and `Deserialize` make zero guarantee of long term wire format compatibility. Changes in behavior will cause serialized [`Entity`] values persisted to long term storage (i.e. disk, databases, etc.) will fail to deserialize upon being updated. 351 | > 352 | > — [Bevy's `Entity` documentation](https://docs.rs/bevy/latest/bevy/ecs/entity/struct.Entity.html#stability-warning) 353 | 354 | `bevy_save` serializes and deserializes entities directly. If you need to maintain compatibility across Bevy versions, consider adding a unique identifier [`Component`] to your tracked entities. 355 | 356 | ### Stabilization 357 | 358 | `bevy_save` will become a candidate for stabilization once [all stabilization tasks](https://github.com/hankjordan/bevy_save/milestone/2) have been completed. 359 | 360 | ## Compatibility 361 | 362 | ### Bevy 363 | 364 | | Bevy Version | Crate Version | 365 | | ------------------------- | --------------------------------- | 366 | | `0.16` | `0.18` | 367 | | `0.15` | `0.16` [2](#2), `0.17` | 368 | | `0.14` [1](#1) | `0.15` | 369 | | `0.13` | `0.14` | 370 | | `0.12` | `0.10`, `0.11`, `0.12`, `0.13` | 371 | | `0.11` | `0.9` | 372 | | `0.10` | `0.4`, `0.5`, `0.6`, `0.7`, `0.8` | 373 | | `0.9` | `0.1`, `0.2`, `0.3` | 374 | 375 | #### Save format changes (since `0.15`) 376 | 377 | 1. `bevy` changed [`Entity`]'s on-disk representation 378 | 2. `bevy_save` began using [`FromReflect`] when taking snapshots 379 | 380 | ### Platforms 381 | 382 | | Platform | Support | 383 | | -------- | ------- | 384 | | Windows | Yes | 385 | | MacOS | Yes | 386 | | Linux | Yes | 387 | | WASM | Yes | 388 | | Android | No | 389 | | iOS | No | 390 | 391 | ## Feature Flags 392 | 393 | | Feature flag | Description | Default? | 394 | | ------------- | --------------------------------------- | -------- | 395 | | `bevy_asset` | Enables `bevy_asset` type registration | Yes | 396 | | `bevy_render` | Enables `bevy_render` type registration | Yes | 397 | | `bevy_sprite` | Enables `bevy_sprite` type registration | Yes | 398 | | `brotli` | Enables `Brotli` compression middleware | No | 399 | 400 | ## License 401 | 402 | `bevy_save` is dual-licensed under MIT and Apache-2.0. 403 | 404 | [img_bevy]: https://img.shields.io/badge/Bevy-0.15-blue 405 | [img_version]: https://img.shields.io/crates/v/bevy_save.svg 406 | [img_doc]: https://docs.rs/bevy_save/badge.svg 407 | [img_license]: https://img.shields.io/badge/license-MIT%2FApache-blue.svg 408 | [img_downloads]: https://img.shields.io/crates/d/bevy_save.svg 409 | [img_tracking]: https://img.shields.io/badge/Bevy%20tracking-released%20version-lightblue 410 | [bevy]: https://crates.io/crates/bevy/0.15.0 411 | [crates]: https://crates.io/crates/bevy_save 412 | [doc]: https://docs.rs/bevy_save 413 | [license]: https://github.com/hankjordan/bevy_save#license 414 | [tracking]: https://github.com/bevyengine/bevy/blob/main/docs/plugins_guidelines.md#main-branch-tracking 415 | [`Snapshot`]: https://docs.rs/bevy_save/latest/bevy_save/prelude/struct.Snapshot.html 416 | [`SnapshotBuilder`]: https://docs.rs/bevy_save/latest/bevy_save/prelude/struct.SnapshotBuilder.html 417 | [`SnapshotApplier`]: https://docs.rs/bevy_save/latest/bevy_save/prelude/struct.SnapshotApplier.html 418 | [`Checkpoints`]: https://docs.rs/bevy_save/latest/bevy_save/checkpoint/struct.Checkpoints.html 419 | [`Pipeline`]: https://docs.rs/bevy_save/latest/bevy_save/prelude/trait.Pipeline.html 420 | [`Backend`]: https://docs.rs/bevy_save/latest/bevy_save/prelude/trait.Backend.html 421 | [`Format`]: https://docs.rs/bevy_save/latest/bevy_save/prelude/trait.Format.html 422 | [`FileIO`]: https://docs.rs/bevy_save/latest/bevy_save/backend/struct.FileIO.html 423 | [`JSON`]: https://docs.rs/bevy_save/latest/bevy_save/format/struct.JSONFormat.html 424 | [`MessagePack`]: https://docs.rs/bevy_save/latest/bevy_save/format/struct.RMPFormat.html 425 | [`Prefab`]: https://docs.rs/bevy_save/latest/bevy_save/prelude/trait.Prefab.html 426 | [`WORKSPACE`]: https://docs.rs/bevy_save/latest/bevy_save/dir/constant.WORKSPACE.html 427 | [`App`]: https://docs.rs/bevy/latest/bevy/prelude/struct.App.html 428 | [`Component`]: https://docs.rs/bevy/latest/bevy/prelude/trait.Component.html 429 | [`DynamicSceneBuilder`]: https://docs.rs/bevy/latest/bevy/prelude/struct.DynamicSceneBuilder.html 430 | [`Entity`]: https://docs.rs/bevy/latest/bevy/prelude/struct.Entity.html 431 | [`FromReflect`]: https://docs.rs/bevy/latest/bevy/prelude/trait.FromReflect.html 432 | [`Reflect`]: https://docs.rs/bevy/latest/bevy/prelude/trait.Reflect.html 433 | [`ReflectDeserialize`]: https://docs.rs/bevy/latest/bevy/prelude/struct.ReflectDeserialize.html 434 | [`World`]: https://docs.rs/bevy/latest/bevy/prelude/struct.World.html 435 | [`LocalStorage`]: https://docs.rs/web-sys/latest/web_sys/struct.Storage.html 436 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::uninlined_format_args)] 2 | 3 | use std::{ 4 | env, 5 | fs, 6 | path::Path, 7 | }; 8 | 9 | fn main() { 10 | let workspace = env::var("OUT_DIR") 11 | .ok() 12 | .map(|v| Path::new(&v).to_path_buf()) 13 | .and_then(|path| { 14 | for ancestor in path.ancestors() { 15 | if let Some(last) = ancestor.file_name() { 16 | if last == "target" { 17 | return ancestor 18 | .parent() 19 | .and_then(|p| p.file_name()) 20 | .and_then(|p| p.to_str()) 21 | .map(|p| p.to_owned()); 22 | } 23 | } 24 | } 25 | 26 | None 27 | }) 28 | .expect("Could not find parent workspace."); 29 | 30 | let out_dir = env::var_os("OUT_DIR").unwrap(); 31 | let dest = Path::new(&out_dir).join("workspace.rs"); 32 | 33 | fs::write( 34 | dest, 35 | format!( 36 | "/// The name of your application's workspace.\npub const WORKSPACE: &str = {:?};", 37 | workspace 38 | ), 39 | ) 40 | .unwrap(); 41 | } 42 | -------------------------------------------------------------------------------- /examples/assets/fonts/FiraMono-LICENSE: -------------------------------------------------------------------------------- 1 | Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A. 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /examples/assets/fonts/FiraMono-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hankjordan/bevy_save/43e2a1f683350335dbcde46d4144cdd20d9a6747/examples/assets/fonts/FiraMono-Medium.ttf -------------------------------------------------------------------------------- /examples/assets/fonts/FiraSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hankjordan/bevy_save/43e2a1f683350335dbcde46d4144cdd20d9a6747/examples/assets/fonts/FiraSans-Bold.ttf -------------------------------------------------------------------------------- /examples/assets/sounds/breakout_collision.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hankjordan/bevy_save/43e2a1f683350335dbcde46d4144cdd20d9a6747/examples/assets/sounds/breakout_collision.ogg -------------------------------------------------------------------------------- /examples/assets/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hankjordan/bevy_save/43e2a1f683350335dbcde46d4144cdd20d9a6747/examples/assets/tiles.png -------------------------------------------------------------------------------- /examples/breakout.rs: -------------------------------------------------------------------------------- 1 | //! A simplified implementation of the classic game "Breakout". 2 | //! Modified to demonstrate integration of `bevy_save`. 3 | 4 | use bevy::{ 5 | ecs::system::{ 6 | SystemParam, 7 | SystemState, 8 | }, 9 | math::bounding::{ 10 | Aabb2d, 11 | BoundingCircle, 12 | BoundingVolume, 13 | IntersectsVolume, 14 | }, 15 | platform::time::Instant, 16 | prelude::*, 17 | }; 18 | use bevy_inspector_egui::{ 19 | bevy_egui::EguiPlugin, 20 | quick::WorldInspectorPlugin, 21 | }; 22 | use bevy_save::prelude::*; 23 | 24 | // These constants are defined in `Transform` units. 25 | // Using the default 2D camera they correspond 1:1 with screen pixels. 26 | const PADDLE_SIZE: Vec2 = Vec2::new(120.0, 20.0); 27 | const GAP_BETWEEN_PADDLE_AND_FLOOR: f32 = 60.0; 28 | const PADDLE_SPEED: f32 = 500.0; 29 | // How close can the paddle get to the wall 30 | const PADDLE_PADDING: f32 = 10.0; 31 | const PADDLE_OFFSET: f32 = BOTTOM_WALL + GAP_BETWEEN_PADDLE_AND_FLOOR; 32 | 33 | // We set the z-value of the ball to 1 so it renders on top in the case of overlapping sprites. 34 | const BALL_STARTING_POSITION: Vec3 = Vec3::new(0.0, -50.0, 1.0); 35 | const BALL_DIAMETER: f32 = 30.; 36 | const BALL_SPEED: f32 = 400.0; 37 | const INITIAL_BALL_DIRECTION: Vec2 = Vec2::new(0.5, -0.5); 38 | 39 | const WALL_THICKNESS: f32 = 10.0; 40 | // x coordinates 41 | const LEFT_WALL: f32 = -450.; 42 | const RIGHT_WALL: f32 = 450.; 43 | // y coordinates 44 | const BOTTOM_WALL: f32 = -300.; 45 | const TOP_WALL: f32 = 300.; 46 | 47 | const BRICK_SIZE: Vec2 = Vec2::new(100., 30.); 48 | // These values are exact 49 | const GAP_BETWEEN_PADDLE_AND_BRICKS: f32 = 270.0; 50 | const GAP_BETWEEN_BRICKS: f32 = 5.0; 51 | // These values are lower bounds, as the number of bricks is computed 52 | const GAP_BETWEEN_BRICKS_AND_CEILING: f32 = 20.0; 53 | const GAP_BETWEEN_BRICKS_AND_SIDES: f32 = 20.0; 54 | 55 | const SCOREBOARD_FONT_SIZE: f32 = 33.0; 56 | const SCOREBOARD_TEXT_PADDING: Val = Val::Px(5.0); 57 | 58 | const BACKGROUND_COLOR: Color = Color::srgb(0.9, 0.9, 0.9); 59 | const PADDLE_COLOR: Color = Color::srgb(0.3, 0.3, 0.7); 60 | const BALL_COLOR: Color = Color::srgb(1.0, 0.5, 0.5); 61 | const BRICK_COLOR: Color = Color::srgb(0.5, 0.5, 1.0); 62 | const WALL_COLOR: Color = Color::srgb(0.8, 0.8, 0.8); 63 | const TEXT_COLOR: Color = Color::srgb(0.5, 0.5, 1.0); 64 | const SCORE_COLOR: Color = Color::srgb(1.0, 0.5, 0.5); 65 | 66 | fn main() { 67 | App::new() 68 | .add_plugins(DefaultPlugins.build().set(AssetPlugin { 69 | file_path: "examples/assets".to_owned(), 70 | ..default() 71 | })) 72 | .insert_resource(Score(0)) 73 | .insert_resource(ClearColor(BACKGROUND_COLOR)) 74 | .add_event::() 75 | .add_systems(Startup, setup) 76 | // Add our gameplay simulation systems to the fixed timestep schedule 77 | .add_systems( 78 | FixedUpdate, 79 | ( 80 | apply_velocity, 81 | move_paddle, 82 | check_for_collisions, 83 | play_collision_sound, 84 | ) 85 | // `chain`ing systems together runs them in order 86 | .chain(), 87 | ) 88 | .add_systems(Update, (update_scoreboard, close_on_esc)) 89 | .add_plugins(( 90 | // Inspector 91 | EguiPlugin { 92 | enable_multipass_for_primary_context: true, 93 | }, 94 | WorldInspectorPlugin::new(), 95 | // Bevy Save 96 | SavePlugins, 97 | )) 98 | // Register our types 99 | .register_type::() 100 | .register_type::() 101 | .register_type::() 102 | .register_type::() 103 | .register_type::() 104 | .register_type::() 105 | .register_type::() 106 | .register_type::() 107 | // Register prefabs 108 | .register_type::() 109 | .register_type::() 110 | .register_type::() 111 | // Setup 112 | .add_systems(Startup, (setup_help, setup_entity_count).after(setup)) 113 | // Systems 114 | .add_systems( 115 | Update, 116 | (update_entity_count, handle_save_input, update_toasts), 117 | ) 118 | .run(); 119 | } 120 | 121 | #[derive(Component, Default, Reflect)] 122 | #[reflect(Component)] 123 | struct Paddle; 124 | 125 | #[derive(Component, Default, Reflect)] 126 | #[reflect(Component)] 127 | struct Ball; 128 | 129 | #[derive(Component, Reflect, Deref, DerefMut)] 130 | #[reflect(Component)] 131 | struct Velocity(Vec2); 132 | 133 | #[derive(Component, Reflect)] 134 | #[reflect(Component)] 135 | struct Collider; 136 | 137 | #[derive(Event, Default)] 138 | struct CollisionEvent; 139 | 140 | #[derive(Component, Default, Reflect)] 141 | #[reflect(Component)] 142 | struct Brick; 143 | 144 | #[derive(Resource, Deref)] 145 | struct CollisionSound(Handle); 146 | 147 | // This bundle is a collection of the components that define a "wall" in our game 148 | #[derive(Bundle)] 149 | struct WallBundle { 150 | // You can nest bundles inside of other bundles like this 151 | // Allowing you to compose their functionality 152 | sprite: Sprite, 153 | transform: Transform, 154 | collider: Collider, 155 | } 156 | 157 | /// Which side of the arena is this wall located on? 158 | enum WallLocation { 159 | Left, 160 | Right, 161 | Bottom, 162 | Top, 163 | } 164 | 165 | impl WallLocation { 166 | /// Location of the *center* of the wall, used in `transform.translation()` 167 | fn position(&self) -> Vec2 { 168 | match self { 169 | WallLocation::Left => Vec2::new(LEFT_WALL, 0.), 170 | WallLocation::Right => Vec2::new(RIGHT_WALL, 0.), 171 | WallLocation::Bottom => Vec2::new(0., BOTTOM_WALL), 172 | WallLocation::Top => Vec2::new(0., TOP_WALL), 173 | } 174 | } 175 | 176 | /// (x, y) dimensions of the wall, used in `transform.scale()` 177 | fn size(&self) -> Vec2 { 178 | let arena_height = TOP_WALL - BOTTOM_WALL; 179 | let arena_width = RIGHT_WALL - LEFT_WALL; 180 | // Make sure we haven't messed up our constants 181 | assert!(arena_height > 0.0); 182 | assert!(arena_width > 0.0); 183 | 184 | match self { 185 | WallLocation::Left | WallLocation::Right => { 186 | Vec2::new(WALL_THICKNESS, arena_height + WALL_THICKNESS) 187 | } 188 | WallLocation::Bottom | WallLocation::Top => { 189 | Vec2::new(arena_width + WALL_THICKNESS, WALL_THICKNESS) 190 | } 191 | } 192 | } 193 | } 194 | 195 | impl WallBundle { 196 | // This "builder method" allows us to reuse logic across our wall entities, 197 | // making our code easier to read and less prone to bugs when we change the logic 198 | fn new(location: WallLocation) -> WallBundle { 199 | WallBundle { 200 | sprite: Sprite::from_color(WALL_COLOR, Vec2::ONE), 201 | transform: Transform { 202 | // We need to convert our Vec2 into a Vec3, by giving it a z-coordinate 203 | // This is used to determine the order of our sprites 204 | translation: location.position().extend(0.0), 205 | // The z-scale of 2D objects must always be 1.0, 206 | // or their ordering will be affected in surprising ways. 207 | // See https://github.com/bevyengine/bevy/issues/4149 208 | scale: location.size().extend(1.0), 209 | ..default() 210 | }, 211 | collider: Collider, 212 | } 213 | } 214 | } 215 | 216 | // This resource tracks the game's score 217 | #[derive(Resource, Reflect, Deref, DerefMut)] 218 | #[reflect(Resource)] 219 | struct Score(usize); 220 | 221 | #[derive(Component, Reflect)] 222 | #[reflect(Component)] 223 | struct ScoreboardUi; 224 | 225 | #[derive(Reflect)] 226 | struct PaddlePrefab { 227 | position: f32, 228 | } 229 | 230 | impl Prefab for PaddlePrefab { 231 | type Marker = Paddle; 232 | 233 | fn spawn(self, target: Entity, world: &mut World) { 234 | world.entity_mut(target).insert(( 235 | Sprite::from_color(PADDLE_COLOR, Vec2::ONE), 236 | Transform { 237 | translation: Vec3::new(self.position, PADDLE_OFFSET, 0.0), 238 | scale: PADDLE_SIZE.extend(1.0), 239 | ..default() 240 | }, 241 | Collider, 242 | )); 243 | } 244 | 245 | fn extract(builder: SnapshotBuilder) -> SnapshotBuilder { 246 | builder.extract_prefab(|entity| { 247 | Some(PaddlePrefab { 248 | position: entity.get::()?.translation.x, 249 | }) 250 | }) 251 | } 252 | } 253 | 254 | #[derive(Reflect)] 255 | struct BallPrefab { 256 | position: Vec3, 257 | } 258 | 259 | impl Prefab for BallPrefab { 260 | type Marker = Ball; 261 | 262 | fn spawn(self, target: Entity, world: &mut World) { 263 | let mesh = world.resource_mut::>().add(Circle::default()); 264 | let material = world 265 | .resource_mut::>() 266 | .add(BALL_COLOR); 267 | 268 | world.entity_mut(target).insert(( 269 | Mesh2d(mesh), 270 | MeshMaterial2d(material), 271 | Transform::from_translation(self.position) 272 | .with_scale(Vec2::splat(BALL_DIAMETER).extend(1.)), 273 | Ball, 274 | Velocity(INITIAL_BALL_DIRECTION.normalize() * BALL_SPEED), 275 | )); 276 | } 277 | 278 | fn extract(builder: SnapshotBuilder) -> SnapshotBuilder { 279 | builder.extract_prefab(|entity| { 280 | Some(BallPrefab { 281 | position: entity.get::()?.translation, 282 | }) 283 | }) 284 | } 285 | } 286 | 287 | #[derive(Reflect)] 288 | struct BrickPrefab { 289 | position: Vec2, 290 | } 291 | 292 | impl Prefab for BrickPrefab { 293 | type Marker = Brick; 294 | 295 | fn spawn(self, target: Entity, world: &mut World) { 296 | world.entity_mut(target).insert(( 297 | Sprite { 298 | color: BRICK_COLOR, 299 | ..default() 300 | }, 301 | Transform { 302 | translation: self.position.extend(0.0), 303 | scale: Vec3::new(BRICK_SIZE.x, BRICK_SIZE.y, 1.0), 304 | ..default() 305 | }, 306 | Collider, 307 | )); 308 | } 309 | 310 | fn extract(builder: SnapshotBuilder) -> SnapshotBuilder { 311 | builder.extract_prefab(|entity| { 312 | let position = entity.get::()?.translation; 313 | 314 | Some(BrickPrefab { 315 | position: Vec2::new(position.x, position.y), 316 | }) 317 | }) 318 | } 319 | } 320 | 321 | // Add the game's entities to our world 322 | fn setup(mut commands: Commands, asset_server: Res) { 323 | // Camera 324 | commands.spawn(Camera2d); 325 | 326 | // Sound 327 | let ball_collision_sound = asset_server.load("sounds/breakout_collision.ogg"); 328 | commands.insert_resource(CollisionSound(ball_collision_sound)); 329 | 330 | // Paddle 331 | 332 | commands.spawn_prefab(PaddlePrefab { position: 0.0 }); 333 | 334 | // Ball 335 | commands.spawn_prefab(BallPrefab { 336 | position: BALL_STARTING_POSITION, 337 | }); 338 | 339 | // Scoreboard 340 | commands 341 | .spawn(( 342 | Text::new("Score: "), 343 | TextFont { 344 | font_size: SCOREBOARD_FONT_SIZE, 345 | ..default() 346 | }, 347 | TextColor(TEXT_COLOR), 348 | ScoreboardUi, 349 | Node { 350 | position_type: PositionType::Absolute, 351 | top: SCOREBOARD_TEXT_PADDING, 352 | left: SCOREBOARD_TEXT_PADDING, 353 | ..default() 354 | }, 355 | )) 356 | .with_child(( 357 | TextSpan::default(), 358 | TextFont { 359 | font_size: SCOREBOARD_FONT_SIZE, 360 | ..default() 361 | }, 362 | TextColor(SCORE_COLOR), 363 | )); 364 | 365 | // Walls 366 | commands.spawn(WallBundle::new(WallLocation::Left)); 367 | commands.spawn(WallBundle::new(WallLocation::Right)); 368 | commands.spawn(WallBundle::new(WallLocation::Bottom)); 369 | commands.spawn(WallBundle::new(WallLocation::Top)); 370 | 371 | // Bricks 372 | let total_width_of_bricks = (RIGHT_WALL - LEFT_WALL) - 2. * GAP_BETWEEN_BRICKS_AND_SIDES; 373 | let bottom_edge_of_bricks = PADDLE_OFFSET + GAP_BETWEEN_PADDLE_AND_BRICKS; 374 | let total_height_of_bricks = TOP_WALL - bottom_edge_of_bricks - GAP_BETWEEN_BRICKS_AND_CEILING; 375 | 376 | assert!(total_width_of_bricks > 0.0); 377 | assert!(total_height_of_bricks > 0.0); 378 | 379 | // Given the space available, compute how many rows and columns of bricks we can fit 380 | let n_columns = (total_width_of_bricks / (BRICK_SIZE.x + GAP_BETWEEN_BRICKS)).floor() as usize; 381 | let n_rows = (total_height_of_bricks / (BRICK_SIZE.y + GAP_BETWEEN_BRICKS)).floor() as usize; 382 | let n_vertical_gaps = n_columns - 1; 383 | 384 | // Because we need to round the number of columns, 385 | // the space on the top and sides of the bricks only captures a lower bound, not an exact value 386 | let center_of_bricks = (LEFT_WALL + RIGHT_WALL) / 2.0; 387 | let left_edge_of_bricks = center_of_bricks 388 | // Space taken up by the bricks 389 | - (n_columns as f32 / 2.0 * BRICK_SIZE.x) 390 | // Space taken up by the gaps 391 | - n_vertical_gaps as f32 / 2.0 * GAP_BETWEEN_BRICKS; 392 | 393 | // In Bevy, the `translation` of an entity describes the center point, 394 | // not its bottom-left corner 395 | let offset_x = left_edge_of_bricks + BRICK_SIZE.x / 2.; 396 | let offset_y = bottom_edge_of_bricks + BRICK_SIZE.y / 2.; 397 | 398 | for row in 0..n_rows { 399 | for column in 0..n_columns { 400 | let position = Vec2::new( 401 | offset_x + column as f32 * (BRICK_SIZE.x + GAP_BETWEEN_BRICKS), 402 | offset_y + row as f32 * (BRICK_SIZE.y + GAP_BETWEEN_BRICKS), 403 | ); 404 | 405 | // brick 406 | commands.spawn_prefab(BrickPrefab { position }); 407 | } 408 | } 409 | } 410 | 411 | fn move_paddle( 412 | keyboard_input: Res>, 413 | mut paddle_transform: Single<&mut Transform, With>, 414 | time: Res

(pub P); 12 | 13 | impl Command for SaveCommand

{ 14 | fn apply(self, world: &mut World) { 15 | if let Err(e) = world.save(self.0) { 16 | warn!("Failed to save world: {:?}", e); 17 | } 18 | } 19 | } 20 | 21 | /// Load using the [`Pipeline`]. 22 | pub struct LoadCommand

(pub P); 23 | 24 | impl Command for LoadCommand

{ 25 | fn apply(self, world: &mut World) { 26 | if let Err(e) = world.load(self.0) { 27 | warn!("Failed to load world: {:?}", e); 28 | } 29 | } 30 | } 31 | 32 | /// Create a checkpoint using the [`Pipeline`]. 33 | pub struct CheckpointCommand

{ 34 | pipeline: P, 35 | } 36 | 37 | impl

CheckpointCommand

{ 38 | /// Create a [`CheckpointCommand`] from the [`Pipeline`]. 39 | pub fn new(pipeline: P) -> Self { 40 | Self { pipeline } 41 | } 42 | } 43 | 44 | impl Command for CheckpointCommand

{ 45 | fn apply(self, world: &mut World) { 46 | world.checkpoint(self.pipeline); 47 | } 48 | } 49 | 50 | /// Rollback the specified amount using the [`Pipeline`]. 51 | pub struct RollbackCommand

{ 52 | pipeline: P, 53 | checkpoints: isize, 54 | } 55 | 56 | impl

RollbackCommand

{ 57 | /// Create a [`RollbackCommand`] from the [`Pipeline`] and checkpoint count. 58 | pub fn new(pipeline: P, checkpoints: isize) -> Self { 59 | Self { 60 | pipeline, 61 | checkpoints, 62 | } 63 | } 64 | } 65 | 66 | impl Command for RollbackCommand

{ 67 | fn apply(self, world: &mut World) { 68 | if let Err(e) = world.rollback(self.pipeline, self.checkpoints) { 69 | warn!("Failed to rollback world: {:?}", e); 70 | } 71 | } 72 | } 73 | 74 | /// Spawn an instance of the [`Prefab`]. 75 | pub struct SpawnPrefabCommand

{ 76 | target: Entity, 77 | prefab: P, 78 | } 79 | 80 | impl

SpawnPrefabCommand

{ 81 | /// Create a [`SpawnPrefabCommand`] from the target entity and [`Prefab`]. 82 | pub fn new(target: Entity, prefab: P) -> Self { 83 | Self { target, prefab } 84 | } 85 | } 86 | 87 | impl Command for SpawnPrefabCommand

{ 88 | fn apply(self, world: &mut World) { 89 | self.prefab.spawn(self.target, world); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/dir.rs: -------------------------------------------------------------------------------- 1 | //! Save directory management, can be used independently. 2 | 3 | use std::{ 4 | path::PathBuf, 5 | sync::LazyLock, 6 | }; 7 | 8 | use platform_dirs::AppDirs; 9 | 10 | include!(concat!(env!("OUT_DIR"), "/workspace.rs")); 11 | 12 | /// The platform-specific save directory for the app. 13 | /// 14 | /// [`WORKSPACE`] is the name of your project's workspace (parent folder) name. 15 | /// 16 | /// | Windows | Linux/*BSD | MacOS | 17 | /// |-----------------------------------------------------|----------------------------------|-------------------------------------------------| 18 | /// | `C:\Users\%USERNAME%\AppData\Local\WORKSPACE\saves` | `~/.local/share/WORKSPACE/saves` | `~/Library/Application Support/WORKSPACE/saves` | 19 | pub static SAVE_DIR: LazyLock = LazyLock::new(|| { 20 | AppDirs::new(Some(WORKSPACE), true) 21 | .unwrap() 22 | .data_dir 23 | .join("saves") 24 | }); 25 | 26 | /// Returns the absolute path to a save file given its name. 27 | pub fn get_save_file(key: K) -> PathBuf { 28 | SAVE_DIR.join(format!("{key}")) 29 | } 30 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use thiserror::Error; 3 | 4 | /// An error that may occur when loading saves, snapshots, or checkpoints. 5 | #[derive(Error, Debug)] 6 | pub enum Error { 7 | /// Saving or serialization error. 8 | #[error("saving error: {0}")] 9 | Saving(Box), 10 | 11 | /// Loading or deserialization error. 12 | #[error("loading error: {0}")] 13 | Loading(Box), 14 | 15 | /// Scene spawning error. 16 | #[error("scene spawn error: {0}")] 17 | SceneSpawnError(bevy::scene::SceneSpawnError), 18 | 19 | /// IO / Filesystem error. 20 | #[error("io error: {0}")] 21 | IO(std::io::Error), 22 | 23 | /// Other error. 24 | #[error("other error: {0}")] 25 | Other(Box), 26 | 27 | /// Custom error. 28 | #[error("custom error: {0}")] 29 | Custom(String), 30 | } 31 | 32 | impl Error { 33 | /// Saving or serialization error. 34 | pub fn saving(err: impl std::error::Error + 'static) -> Self { 35 | Self::Saving(Box::new(err)) 36 | } 37 | 38 | /// Loading or deserialization error. 39 | pub fn loading(err: impl std::error::Error + 'static) -> Self { 40 | Self::Loading(Box::new(err)) 41 | } 42 | 43 | /// Other error. 44 | pub fn other(error: impl std::error::Error + 'static) -> Self { 45 | Self::Other(Box::new(error)) 46 | } 47 | 48 | /// Custom error. 49 | pub fn custom(error: impl std::fmt::Display) -> Self { 50 | Self::Custom(format!("{error}")) 51 | } 52 | } 53 | 54 | impl From for Error { 55 | fn from(value: bevy::scene::SceneSpawnError) -> Self { 56 | Self::SceneSpawnError(value) 57 | } 58 | } 59 | 60 | impl From for Error { 61 | fn from(value: std::io::Error) -> Self { 62 | Self::IO(value) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/ext/app.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | 3 | use bevy::prelude::*; 4 | 5 | use crate::{ 6 | checkpoint::CheckpointRegistry, 7 | prelude::*, 8 | }; 9 | 10 | /// Extension trait that adds save-related methods to Bevy's [`App`]. 11 | pub trait AppSaveableExt { 12 | /// Initialize a [`Pipeline`], allowing it to be used with [`WorldSaveableExt`] methods. 13 | fn init_pipeline(&mut self) -> &mut Self; 14 | } 15 | 16 | impl AppSaveableExt for App { 17 | fn init_pipeline(&mut self) -> &mut Self { 18 | P::build(self); 19 | self 20 | } 21 | } 22 | 23 | /// Extension trait that adds rollback checkpoint-related methods to Bevy's [`App`]. 24 | pub trait AppCheckpointExt { 25 | /// Set a type to allow rollback - it will be included in rollback checkpoints and affected by save/load. 26 | fn allow_checkpoint(&mut self) -> &mut Self; 27 | 28 | /// Set a type to ignore rollback - it will be included in save/load but it won't change during rollback. 29 | fn deny_checkpoint(&mut self) -> &mut Self; 30 | } 31 | 32 | impl AppCheckpointExt for App { 33 | fn allow_checkpoint(&mut self) -> &mut Self { 34 | self.world_mut() 35 | .resource_mut::() 36 | .allow::(); 37 | self 38 | } 39 | 40 | fn deny_checkpoint(&mut self) -> &mut Self { 41 | self.world_mut() 42 | .resource_mut::() 43 | .deny::(); 44 | self 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/ext/commands.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | 3 | use crate::{ 4 | commands::{ 5 | CheckpointCommand, 6 | LoadCommand, 7 | RollbackCommand, 8 | SaveCommand, 9 | SpawnPrefabCommand, 10 | }, 11 | prelude::*, 12 | }; 13 | 14 | /// Extension trait that adds save-related methods to Bevy's [`Commands`]. 15 | pub trait CommandsSaveableExt { 16 | /// Save using the [`Pipeline`]. 17 | fn save(&mut self, pipeline: P); 18 | 19 | /// Load using the [`Pipeline`]. 20 | fn load(&mut self, pipeline: P); 21 | } 22 | 23 | impl CommandsSaveableExt for Commands<'_, '_> { 24 | fn save(&mut self, pipeline: P) { 25 | self.queue(SaveCommand(pipeline)); 26 | } 27 | 28 | fn load(&mut self, pipeline: P) { 29 | self.queue(LoadCommand(pipeline)); 30 | } 31 | } 32 | 33 | /// Extension trait that adds rollback checkpoint-related methods to Bevy's [`Commands`]. 34 | pub trait CommandsCheckpointExt { 35 | /// Create a checkpoint using the [`Pipeline`]. 36 | fn checkpoint(&mut self, pipeline: P); 37 | 38 | /// Rollback the specified amount using the [`Pipeline`]. 39 | fn rollback(&mut self, pipeline: P, checkpoints: isize); 40 | } 41 | 42 | impl CommandsCheckpointExt for Commands<'_, '_> { 43 | fn checkpoint(&mut self, pipeline: P) { 44 | self.queue(CheckpointCommand::new(pipeline)); 45 | } 46 | 47 | fn rollback(&mut self, pipeline: P, checkpoints: isize) { 48 | self.queue(RollbackCommand::new(pipeline, checkpoints)); 49 | } 50 | } 51 | 52 | /// Extension trait that adds prefab-related methods to Bevy's [`Commands`]. 53 | pub trait CommandsPrefabExt { 54 | /// Spawn a [`Prefab`] entity. 55 | fn spawn_prefab(&mut self, prefab: P) -> EntityCommands; 56 | } 57 | 58 | impl CommandsPrefabExt for Commands<'_, '_> { 59 | fn spawn_prefab(&mut self, prefab: P) -> EntityCommands { 60 | let target = self.spawn(P::Marker::default()).id(); 61 | self.queue(SpawnPrefabCommand::new(target, prefab)); 62 | self.entity(target) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/ext/mod.rs: -------------------------------------------------------------------------------- 1 | //! Extension traits. 2 | 3 | mod app; 4 | mod commands; 5 | mod world; 6 | 7 | pub use self::{ 8 | app::{ 9 | AppCheckpointExt, 10 | AppSaveableExt, 11 | }, 12 | commands::{ 13 | CommandsCheckpointExt, 14 | CommandsPrefabExt, 15 | CommandsSaveableExt, 16 | }, 17 | world::{ 18 | WorldCheckpointExt, 19 | WorldSaveableExt, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /src/ext/world.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | prelude::*, 3 | reflect::TypeRegistry, 4 | tasks::block_on, 5 | }; 6 | 7 | use crate::{ 8 | checkpoint::Checkpoints, 9 | error::Error, 10 | prelude::*, 11 | serde::{ 12 | SnapshotDeserializer, 13 | SnapshotSerializer, 14 | }, 15 | }; 16 | 17 | /// Extension trait that adds save-related methods to Bevy's [`World`]. 18 | pub trait WorldSaveableExt: Sized { 19 | /// Captures a [`Snapshot`] from the current [`World`] state. 20 | fn snapshot(&self, pipeline: P) -> Snapshot; 21 | 22 | /// Saves the application state with the given [`Pipeline`]. 23 | /// 24 | /// # Errors 25 | /// - See [`Error`] 26 | fn save(&self, pipeline: P) -> Result<(), Error>; 27 | 28 | /// Saves the application state with the given [`Pipeline`] and [`TypeRegistry`]. 29 | /// 30 | /// # Errors 31 | /// - See [`Error`] 32 | fn save_with(&self, pipeline: P, registry: &TypeRegistry) -> Result<(), Error>; 33 | 34 | /// Loads the application state with the given [`Pipeline`]. 35 | /// 36 | /// # Errors 37 | /// - See [`Error`] 38 | fn load(&mut self, pipeline: P) -> Result<(), Error>; 39 | 40 | /// Loads the application state with the given [`Pipeline`] and [`TypeRegistry`]. 41 | /// 42 | /// # Errors 43 | /// - See [`Error`] 44 | fn load_with(&mut self, pipeline: P, registry: &TypeRegistry) 45 | -> Result<(), Error>; 46 | } 47 | 48 | impl WorldSaveableExt for World { 49 | fn snapshot(&self, pipeline: P) -> Snapshot { 50 | pipeline.capture(Snapshot::builder(self)) 51 | } 52 | 53 | fn save(&self, pipeline: P) -> Result<(), Error> { 54 | let registry = self.resource::().read(); 55 | self.save_with(pipeline, ®istry) 56 | } 57 | 58 | fn save_with(&self, pipeline: P, registry: &TypeRegistry) -> Result<(), Error> { 59 | let backend = self.resource::(); 60 | let snapshot = pipeline.capture(Snapshot::builder(self)); 61 | let ser = SnapshotSerializer::new(&snapshot, registry); 62 | 63 | block_on(backend.save::(pipeline.key(), &ser)) 64 | } 65 | 66 | fn load(&mut self, pipeline: P) -> Result<(), Error> { 67 | let app_type_registry = self.resource::().clone(); 68 | let type_registry = app_type_registry.read(); 69 | self.load_with(pipeline, &type_registry) 70 | } 71 | 72 | fn load_with( 73 | &mut self, 74 | pipeline: P, 75 | registry: &TypeRegistry, 76 | ) -> Result<(), Error> { 77 | let backend = self.resource::(); 78 | let de = SnapshotDeserializer { registry }; 79 | let snapshot = block_on(backend.load::(pipeline.key(), de))?; 80 | 81 | pipeline.apply(self, &snapshot) 82 | } 83 | } 84 | 85 | /// Extension trait that adds rollback checkpoint-related methods to Bevy's [`World`]. 86 | pub trait WorldCheckpointExt { 87 | /// Creates a checkpoint for rollback and stores it in [`Checkpoints`]. 88 | fn checkpoint(&mut self, pipeline: P); 89 | 90 | /// Rolls back / forward the [`World`] state. 91 | /// 92 | /// # Errors 93 | /// - See [`Error`] 94 | fn rollback(&mut self, pipeline: P, checkpoints: isize) -> Result<(), Error>; 95 | 96 | /// Rolls back / forward the [`World`] state using the given [`TypeRegistry`]. 97 | /// 98 | /// # Errors 99 | /// - See [`Error`] 100 | fn rollback_with( 101 | &mut self, 102 | pipeline: P, 103 | checkpoints: isize, 104 | registry: &TypeRegistry, 105 | ) -> Result<(), Error>; 106 | } 107 | 108 | impl WorldCheckpointExt for World { 109 | fn checkpoint(&mut self, pipeline: P) { 110 | let rollback = pipeline.capture(SnapshotBuilder::checkpoint(self)); 111 | self.resource_mut::().checkpoint(rollback); 112 | } 113 | 114 | fn rollback(&mut self, pipeline: P, checkpoints: isize) -> Result<(), Error> { 115 | let app_type_registry = self.resource::().clone(); 116 | let type_registry = app_type_registry.read(); 117 | 118 | self.rollback_with(pipeline, checkpoints, &type_registry) 119 | } 120 | 121 | fn rollback_with( 122 | &mut self, 123 | pipeline: P, 124 | checkpoints: isize, 125 | registry: &TypeRegistry, 126 | ) -> Result<(), Error> { 127 | if let Some(rollback) = self 128 | .resource_mut::() 129 | .rollback(checkpoints) 130 | .map(|r| r.clone_reflect(registry)) 131 | { 132 | pipeline.apply(self, &rollback) 133 | } else { 134 | Ok(()) 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/format.rs: -------------------------------------------------------------------------------- 1 | //! [`Format`] handles serialization and deserialization of application types. 2 | 3 | use std::io::{ 4 | Read, 5 | Write, 6 | }; 7 | 8 | use serde::{ 9 | de::DeserializeSeed, 10 | Serialize, 11 | }; 12 | 13 | use crate::error::Error; 14 | 15 | /// Handles serialization and deserialization of save data. 16 | pub trait Format { 17 | /// The file extension used by the format. 18 | /// 19 | /// Defaults to `.sav`. 20 | fn extension() -> &'static str { 21 | ".sav" 22 | } 23 | 24 | /// Serializes a value with the format. 25 | /// 26 | /// # Errors 27 | /// If serialization fails. 28 | fn serialize(writer: W, value: &T) -> Result<(), Error>; 29 | 30 | /// Deserializes a value with the format. 31 | /// 32 | /// # Errors 33 | /// If deserialization fails. 34 | fn deserialize DeserializeSeed<'de, Value = T>, T>( 35 | reader: R, 36 | seed: S, 37 | ) -> Result; 38 | } 39 | 40 | /// An implementation of [`Format`] that uses [`rmp_serde`]. 41 | pub struct RMPFormat; 42 | 43 | impl Format for RMPFormat { 44 | fn extension() -> &'static str { 45 | ".mp" 46 | } 47 | 48 | fn serialize(writer: W, value: &T) -> Result<(), Error> { 49 | let mut ser = rmp_serde::Serializer::new(writer); 50 | value.serialize(&mut ser).map_err(Error::saving) 51 | } 52 | 53 | fn deserialize DeserializeSeed<'de, Value = T>, T>( 54 | reader: R, 55 | seed: S, 56 | ) -> Result { 57 | let mut de = rmp_serde::Deserializer::new(reader); 58 | seed.deserialize(&mut de).map_err(Error::loading) 59 | } 60 | } 61 | 62 | /// An implementation of [`Format`] that uses [`serde_json`]. 63 | pub struct JSONFormat; 64 | 65 | impl Format for JSONFormat { 66 | fn extension() -> &'static str { 67 | ".json" 68 | } 69 | 70 | fn serialize(writer: W, value: &T) -> Result<(), Error> { 71 | let mut ser = serde_json::Serializer::pretty(writer); 72 | value.serialize(&mut ser).map_err(Error::saving) 73 | } 74 | 75 | fn deserialize DeserializeSeed<'de, Value = T>, T>( 76 | reader: R, 77 | seed: S, 78 | ) -> Result { 79 | let mut de = serde_json::Deserializer::from_reader(reader); 80 | seed.deserialize(&mut de).map_err(Error::loading) 81 | } 82 | } 83 | 84 | /// A reasonable default [`Format`]. 85 | pub type DefaultFormat = RMPFormat; 86 | 87 | /// A reasonable default debug [`Format`], human-readable. 88 | pub type DefaultDebugFormat = JSONFormat; 89 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 2 | #![warn(missing_docs)] 3 | #![warn(clippy::pedantic)] 4 | #![allow(clippy::module_name_repetitions)] 5 | #![allow(clippy::module_inception)] 6 | #![allow(clippy::must_use_candidate)] 7 | #![allow(clippy::redundant_closure_for_method_calls)] 8 | #![allow(clippy::return_self_not_must_use)] 9 | #![allow(clippy::too_many_lines)] 10 | #![doc = include_str!("../README.md")] 11 | 12 | pub mod backend; 13 | pub mod checkpoint; 14 | mod clone; 15 | pub mod commands; 16 | pub mod dir; 17 | mod error; 18 | pub mod ext; 19 | pub mod format; 20 | pub mod middleware; 21 | pub mod pipeline; 22 | pub mod plugins; 23 | pub mod prefab; 24 | pub mod serde; 25 | pub mod snapshot; 26 | 27 | pub use crate::{ 28 | clone::CloneReflect, 29 | error::Error, 30 | }; 31 | 32 | /// Prelude: convenient import for commonly used items provided by the crate. 33 | #[allow(unused_imports)] 34 | pub mod prelude { 35 | #[doc(inline)] 36 | pub use crate::{ 37 | backend::{ 38 | Backend, 39 | DefaultBackend, 40 | DefaultDebugBackend, 41 | }, 42 | clone::CloneReflect, 43 | dir::{ 44 | get_save_file, 45 | SAVE_DIR, 46 | WORKSPACE, 47 | }, 48 | error::Error, 49 | ext::{ 50 | AppCheckpointExt, 51 | AppSaveableExt, 52 | CommandsCheckpointExt, 53 | CommandsPrefabExt, 54 | CommandsSaveableExt, 55 | WorldCheckpointExt, 56 | WorldSaveableExt, 57 | }, 58 | format::{ 59 | DefaultDebugFormat, 60 | DefaultFormat, 61 | Format, 62 | }, 63 | middleware::*, 64 | pipeline::Pipeline, 65 | plugins::{ 66 | SavePlugin, 67 | SavePlugins, 68 | SaveablesPlugin, 69 | }, 70 | prefab::{ 71 | Prefab, 72 | WithPrefab, 73 | }, 74 | snapshot::{ 75 | BoxedHook, 76 | Hook, 77 | Snapshot, 78 | SnapshotApplier, 79 | SnapshotBuilder, 80 | }, 81 | }; 82 | } 83 | -------------------------------------------------------------------------------- /src/middleware.rs: -------------------------------------------------------------------------------- 1 | //! Middleware for [`Format`](crate::prelude::Format), allowing you to easily add features like compression or encryption. 2 | 3 | #[cfg(feature = "brotli")] 4 | mod brotli { 5 | use std::marker::PhantomData; 6 | 7 | use brotli::enc::BrotliEncoderParams; 8 | 9 | use crate::prelude::*; 10 | 11 | /// Brotli middleware for compressing your data after serializing 12 | /// 13 | /// # Example 14 | /// ```rust 15 | /// # use bevy_save::prelude::*; 16 | /// struct MyPipeline; 17 | /// 18 | /// impl Pipeline for MyPipeline { 19 | /// type Backend = DefaultBackend; 20 | /// /// This will emit brotli-compressed MessagePack 21 | /// type Format = Brotli; 22 | /// type Key<'a> = &'a str; 23 | /// 24 | /// fn key(&self) -> Self::Key<'_> { 25 | /// "my_pipeline" 26 | /// } 27 | /// } 28 | pub struct Brotli(PhantomData); 29 | 30 | impl Default for Brotli { 31 | fn default() -> Self { 32 | Self(PhantomData) 33 | } 34 | } 35 | 36 | impl Format for Brotli { 37 | fn extension() -> &'static str { 38 | // TODO: Should be `format!("{}.br", F::extension())` 39 | ".br" 40 | } 41 | 42 | fn serialize( 43 | writer: W, 44 | value: &T, 45 | ) -> Result<(), crate::Error> { 46 | let params = BrotliEncoderParams::default(); 47 | let writer = brotli::CompressorWriter::with_params(writer, 4096, ¶ms); 48 | F::serialize(writer, value) 49 | } 50 | 51 | fn deserialize< 52 | R: std::io::prelude::Read, 53 | S: for<'de> serde::de::DeserializeSeed<'de, Value = T>, 54 | T, 55 | >( 56 | reader: R, 57 | seed: S, 58 | ) -> Result { 59 | let reader = brotli::Decompressor::new(reader, 4096); 60 | F::deserialize(reader, seed) 61 | } 62 | } 63 | } 64 | 65 | #[cfg(feature = "brotli")] 66 | pub use brotli::Brotli; 67 | -------------------------------------------------------------------------------- /src/pipeline.rs: -------------------------------------------------------------------------------- 1 | //! [`Pipeline`] connects all of the pieces together, defining how your application state is captured, applied, saved, and loaded. 2 | 3 | use bevy::prelude::*; 4 | 5 | use crate::{ 6 | error::Error, 7 | prelude::*, 8 | }; 9 | 10 | /// Trait that defines how exactly your app saves and loads. 11 | pub trait Pipeline { 12 | /// The interface between the saver / loader and data storage. 13 | type Backend: for<'a> Backend> + Resource + Default; 14 | /// The format used for serializing and deserializing data. 15 | type Format: Format; 16 | 17 | /// Used to uniquely identify each saved [`Snapshot`]. 18 | type Key<'a>; 19 | 20 | /// Called when the pipeline is initialized with [`App::init_pipeline`](`AppSaveableExt::init_pipeline`). 21 | fn build(app: &mut App) { 22 | app.world_mut().insert_resource(Self::Backend::default()); 23 | } 24 | 25 | /// Retrieve the unique identifier for the [`Snapshot`] being processed by the [`Pipeline`]. 26 | fn key(&self) -> Self::Key<'_>; 27 | 28 | /// Retrieve a [`Snapshot`] from the [`World`]. 29 | /// 30 | /// This is where you would do any special filtering you might need. 31 | /// 32 | /// You must extract [`Checkpoints`](crate::checkpoint::Checkpoints) if you want this pipeline to handle checkpoints properly. 33 | fn capture(&self, builder: SnapshotBuilder) -> Snapshot; 34 | 35 | /// Apply a [`Snapshot`] to the [`World`]. 36 | /// 37 | /// Entity mapping goes here, along with your spawn hook and any other transformations you might need to perform. 38 | /// 39 | /// # Errors 40 | /// If a type included in the [`Snapshot`] has not been registered with the type registry. 41 | fn apply(&self, world: &mut World, snapshot: &Snapshot) -> Result<(), Error>; 42 | } 43 | -------------------------------------------------------------------------------- /src/plugins.rs: -------------------------------------------------------------------------------- 1 | //! Bevy plugins necessary for the crate to function. 2 | 3 | use bevy::{ 4 | app::PluginGroupBuilder, 5 | prelude::*, 6 | }; 7 | 8 | use crate::{ 9 | checkpoint::{ 10 | CheckpointRegistry, 11 | Checkpoints, 12 | }, 13 | prelude::*, 14 | }; 15 | 16 | /// Default plugins for `bevy_save`. 17 | pub struct SavePlugins; 18 | 19 | impl PluginGroup for SavePlugins { 20 | fn build(self) -> PluginGroupBuilder { 21 | PluginGroupBuilder::start::() 22 | .add(SavePlugin) 23 | .add(SaveablesPlugin) 24 | } 25 | } 26 | 27 | /// `bevy_save` core functionality. 28 | pub struct SavePlugin; 29 | 30 | impl Plugin for SavePlugin { 31 | fn build(&self, app: &mut App) { 32 | app.init_resource::() 33 | .init_resource::() 34 | .init_resource::() 35 | .init_resource::(); 36 | } 37 | } 38 | 39 | /// Saveable registrations for common types. 40 | pub struct SaveablesPlugin; 41 | 42 | impl Plugin for SaveablesPlugin { 43 | fn build(&self, app: &mut App) { 44 | #[cfg(feature = "bevy_render")] 45 | app.register_type::(); 46 | 47 | #[cfg(feature = "bevy_sprite")] 48 | app.register_type::>() 49 | .register_type::>(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/prefab.rs: -------------------------------------------------------------------------------- 1 | //! Entity blueprints, with additional tools for saving and loading. 2 | 3 | use bevy::prelude::*; 4 | 5 | use crate::prelude::*; 6 | 7 | #[allow(type_alias_bounds)] 8 | /// [`QueryFilter`](bevy::ecs::query::QueryFilter) matching [`Prefab`]. 9 | pub type WithPrefab = With; 10 | 11 | /// Abstract spawning for entity types 12 | pub trait Prefab: 'static { 13 | /// Marker component uniquely identifying the prefab entity 14 | /// 15 | /// This is automatically inserted for you when spawning the prefab. 16 | type Marker: Component + Default; 17 | 18 | /// Create a single instance of the prefab 19 | fn spawn(self, target: Entity, world: &mut World); 20 | 21 | /// Extract the prefab entities from the [`World`] 22 | fn extract(builder: SnapshotBuilder) -> SnapshotBuilder { 23 | builder.extract_entities_matching(|entity| entity.contains::()) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/serde/de.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Formatter; 2 | 3 | use bevy::{ 4 | platform::collections::HashSet, 5 | prelude::*, 6 | reflect::{ 7 | serde::{ 8 | ReflectDeserializer, 9 | TypeRegistrationDeserializer, 10 | TypedReflectDeserializer, 11 | }, 12 | TypeRegistry, 13 | }, 14 | scene::DynamicEntity, 15 | }; 16 | use serde::{ 17 | de::{ 18 | DeserializeSeed, 19 | Error, 20 | MapAccess, 21 | SeqAccess, 22 | Visitor, 23 | }, 24 | Deserialize, 25 | Deserializer, 26 | }; 27 | 28 | use crate::{ 29 | checkpoint::Checkpoints, 30 | prelude::*, 31 | serde::{ 32 | CHECKPOINTS_ACTIVE, 33 | CHECKPOINTS_SNAPSHOTS, 34 | CHECKPOINTS_STRUCT, 35 | ENTITY_COMPONENTS, 36 | ENTITY_STRUCT, 37 | SNAPSHOT_CHECKPOINTS, 38 | SNAPSHOT_ENTITIES, 39 | SNAPSHOT_RESOURCES, 40 | SNAPSHOT_STRUCT, 41 | }, 42 | }; 43 | 44 | #[derive(Deserialize)] 45 | #[serde(field_identifier, rename_all = "lowercase")] 46 | enum SnapshotField { 47 | Entities, 48 | Resources, 49 | Rollbacks, 50 | } 51 | 52 | #[derive(Deserialize)] 53 | #[serde(field_identifier, rename_all = "lowercase")] 54 | enum CheckpointsField { 55 | Checkpoints, 56 | Active, 57 | } 58 | 59 | #[derive(Deserialize)] 60 | #[serde(field_identifier, rename_all = "lowercase")] 61 | enum EntityField { 62 | Components, 63 | } 64 | 65 | /// Handles snapshot deserialization. 66 | pub struct SnapshotDeserializer<'a> { 67 | /// Type registry in which the components and resources types used in the snapshot to deserialize are registered. 68 | pub registry: &'a TypeRegistry, 69 | } 70 | 71 | impl<'de> DeserializeSeed<'de> for SnapshotDeserializer<'_> { 72 | type Value = Snapshot; 73 | 74 | fn deserialize(self, deserializer: D) -> Result 75 | where 76 | D: serde::Deserializer<'de>, 77 | { 78 | deserializer.deserialize_struct( 79 | SNAPSHOT_STRUCT, 80 | &[SNAPSHOT_ENTITIES, SNAPSHOT_RESOURCES, SNAPSHOT_CHECKPOINTS], 81 | SnapshotVisitor { 82 | registry: self.registry, 83 | }, 84 | ) 85 | } 86 | } 87 | 88 | struct SnapshotVisitor<'a> { 89 | pub registry: &'a TypeRegistry, 90 | } 91 | 92 | impl<'de> Visitor<'de> for SnapshotVisitor<'_> { 93 | type Value = Snapshot; 94 | 95 | fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { 96 | formatter.write_str("snapshot struct") 97 | } 98 | 99 | fn visit_map(self, mut map: A) -> std::result::Result 100 | where 101 | A: MapAccess<'de>, 102 | { 103 | let mut entities = None; 104 | let mut resources = None; 105 | let mut checkpoints = None; 106 | 107 | while let Some(key) = map.next_key()? { 108 | match key { 109 | SnapshotField::Entities => { 110 | if entities.is_some() { 111 | return Err(Error::duplicate_field(SNAPSHOT_ENTITIES)); 112 | } 113 | entities = Some(map.next_value_seed(EntityMapDeserializer { 114 | registry: self.registry, 115 | })?); 116 | } 117 | SnapshotField::Resources => { 118 | if resources.is_some() { 119 | return Err(Error::duplicate_field(SNAPSHOT_RESOURCES)); 120 | } 121 | resources = Some(map.next_value_seed(ReflectMapDeserializer { 122 | registry: self.registry, 123 | })?); 124 | } 125 | SnapshotField::Rollbacks => { 126 | if checkpoints.is_some() { 127 | return Err(Error::duplicate_field(SNAPSHOT_CHECKPOINTS)); 128 | } 129 | checkpoints = Some(map.next_value_seed(CheckpointsDeserializer { 130 | registry: self.registry, 131 | })?); 132 | } 133 | } 134 | } 135 | 136 | let entities = entities.ok_or_else(|| Error::missing_field(SNAPSHOT_ENTITIES))?; 137 | let resources = resources.ok_or_else(|| Error::missing_field(SNAPSHOT_RESOURCES))?; 138 | 139 | Ok(Snapshot { 140 | entities, 141 | resources, 142 | checkpoints, 143 | }) 144 | } 145 | 146 | fn visit_seq(self, mut seq: A) -> Result 147 | where 148 | A: SeqAccess<'de>, 149 | { 150 | let entities = seq 151 | .next_element_seed(EntityMapDeserializer { 152 | registry: self.registry, 153 | })? 154 | .ok_or_else(|| Error::missing_field(SNAPSHOT_ENTITIES))?; 155 | 156 | let resources = seq 157 | .next_element_seed(ReflectMapDeserializer { 158 | registry: self.registry, 159 | })? 160 | .ok_or_else(|| Error::missing_field(SNAPSHOT_RESOURCES))?; 161 | 162 | let checkpoints = seq.next_element_seed(CheckpointsDeserializer { 163 | registry: self.registry, 164 | })?; 165 | 166 | Ok(Snapshot { 167 | entities, 168 | resources, 169 | checkpoints, 170 | }) 171 | } 172 | } 173 | 174 | /// Handles checkpoints deserialization. 175 | pub struct CheckpointsDeserializer<'a> { 176 | /// Type registry in which the components and resources types used to deserialize the checkpoints are registered. 177 | pub registry: &'a TypeRegistry, 178 | } 179 | 180 | impl<'de> DeserializeSeed<'de> for CheckpointsDeserializer<'_> { 181 | type Value = Checkpoints; 182 | 183 | fn deserialize(self, deserializer: D) -> Result 184 | where 185 | D: serde::Deserializer<'de>, 186 | { 187 | deserializer.deserialize_struct( 188 | CHECKPOINTS_STRUCT, 189 | &[CHECKPOINTS_SNAPSHOTS, CHECKPOINTS_ACTIVE], 190 | CheckpointsVisitor { 191 | registry: self.registry, 192 | }, 193 | ) 194 | } 195 | } 196 | 197 | struct CheckpointsVisitor<'a> { 198 | registry: &'a TypeRegistry, 199 | } 200 | 201 | impl<'de> Visitor<'de> for CheckpointsVisitor<'_> { 202 | type Value = Checkpoints; 203 | 204 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 205 | formatter.write_str("checkpoints struct") 206 | } 207 | 208 | fn visit_map(self, mut map: A) -> std::result::Result 209 | where 210 | A: MapAccess<'de>, 211 | { 212 | let mut snapshots = None; 213 | let mut active = None; 214 | 215 | while let Some(key) = map.next_key()? { 216 | match key { 217 | CheckpointsField::Checkpoints => { 218 | if snapshots.is_some() { 219 | return Err(Error::duplicate_field(CHECKPOINTS_SNAPSHOTS)); 220 | } 221 | snapshots = Some(map.next_value_seed(SnapshotListDeserializer { 222 | registry: self.registry, 223 | })?); 224 | } 225 | CheckpointsField::Active => { 226 | if active.is_some() { 227 | return Err(Error::duplicate_field(CHECKPOINTS_ACTIVE)); 228 | } 229 | active = Some(map.next_value()?); 230 | } 231 | } 232 | } 233 | 234 | let snapshots = snapshots.ok_or_else(|| Error::missing_field(CHECKPOINTS_SNAPSHOTS))?; 235 | let active = active.ok_or_else(|| Error::missing_field(CHECKPOINTS_ACTIVE))?; 236 | 237 | Ok(Checkpoints { snapshots, active }) 238 | } 239 | 240 | fn visit_seq(self, mut seq: A) -> Result 241 | where 242 | A: SeqAccess<'de>, 243 | { 244 | let snapshots = seq 245 | .next_element_seed(SnapshotListDeserializer { 246 | registry: self.registry, 247 | })? 248 | .ok_or_else(|| Error::missing_field(CHECKPOINTS_SNAPSHOTS))?; 249 | 250 | let active = seq 251 | .next_element()? 252 | .ok_or_else(|| Error::missing_field(CHECKPOINTS_ACTIVE))?; 253 | 254 | Ok(Checkpoints { snapshots, active }) 255 | } 256 | } 257 | 258 | struct SnapshotListDeserializer<'a> { 259 | registry: &'a TypeRegistry, 260 | } 261 | 262 | impl<'de> DeserializeSeed<'de> for SnapshotListDeserializer<'_> { 263 | type Value = Vec; 264 | 265 | fn deserialize(self, deserializer: D) -> Result 266 | where 267 | D: Deserializer<'de>, 268 | { 269 | deserializer.deserialize_seq(SnapshotListVisitor { 270 | registry: self.registry, 271 | }) 272 | } 273 | } 274 | 275 | struct SnapshotListVisitor<'a> { 276 | registry: &'a TypeRegistry, 277 | } 278 | 279 | impl<'de> Visitor<'de> for SnapshotListVisitor<'_> { 280 | type Value = Vec; 281 | 282 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 283 | formatter.write_str("sequence of snapshots") 284 | } 285 | 286 | fn visit_seq(self, mut seq: A) -> Result 287 | where 288 | A: SeqAccess<'de>, 289 | { 290 | let mut result = Vec::new(); 291 | 292 | while let Some(next) = seq.next_element_seed(SnapshotDeserializer { 293 | registry: self.registry, 294 | })? { 295 | result.push(next); 296 | } 297 | 298 | Ok(result) 299 | } 300 | } 301 | 302 | struct EntityMapDeserializer<'a> { 303 | registry: &'a TypeRegistry, 304 | } 305 | 306 | impl<'de> DeserializeSeed<'de> for EntityMapDeserializer<'_> { 307 | type Value = Vec; 308 | 309 | fn deserialize(self, deserializer: D) -> Result 310 | where 311 | D: Deserializer<'de>, 312 | { 313 | deserializer.deserialize_map(EntityMapVisitor { 314 | registry: self.registry, 315 | }) 316 | } 317 | } 318 | 319 | struct EntityMapVisitor<'a> { 320 | registry: &'a TypeRegistry, 321 | } 322 | 323 | impl<'de> Visitor<'de> for EntityMapVisitor<'_> { 324 | type Value = Vec; 325 | 326 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 327 | formatter.write_str("map of entities") 328 | } 329 | 330 | fn visit_map(self, mut map: A) -> Result 331 | where 332 | A: MapAccess<'de>, 333 | { 334 | let mut entities = Vec::new(); 335 | while let Some(entity) = map.next_key::()? { 336 | let entity = map.next_value_seed(EntityDeserializer { 337 | entity, 338 | registry: self.registry, 339 | })?; 340 | entities.push(entity); 341 | } 342 | 343 | Ok(entities) 344 | } 345 | } 346 | 347 | struct EntityDeserializer<'a> { 348 | entity: Entity, 349 | registry: &'a TypeRegistry, 350 | } 351 | 352 | impl<'de> DeserializeSeed<'de> for EntityDeserializer<'_> { 353 | type Value = DynamicEntity; 354 | 355 | fn deserialize(self, deserializer: D) -> Result 356 | where 357 | D: serde::Deserializer<'de>, 358 | { 359 | deserializer.deserialize_struct(ENTITY_STRUCT, &[ENTITY_COMPONENTS], EntityVisitor { 360 | entity: self.entity, 361 | registry: self.registry, 362 | }) 363 | } 364 | } 365 | 366 | struct EntityVisitor<'a> { 367 | entity: Entity, 368 | registry: &'a TypeRegistry, 369 | } 370 | 371 | impl<'de> Visitor<'de> for EntityVisitor<'_> { 372 | type Value = DynamicEntity; 373 | 374 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 375 | formatter.write_str("entities") 376 | } 377 | 378 | fn visit_seq(self, mut seq: A) -> std::result::Result 379 | where 380 | A: SeqAccess<'de>, 381 | { 382 | let components = seq 383 | .next_element_seed(ReflectMapDeserializer { 384 | registry: self.registry, 385 | })? 386 | .ok_or_else(|| Error::missing_field(ENTITY_COMPONENTS))?; 387 | 388 | Ok(DynamicEntity { 389 | entity: self.entity, 390 | components, 391 | }) 392 | } 393 | 394 | fn visit_map(self, mut map: A) -> Result 395 | where 396 | A: MapAccess<'de>, 397 | { 398 | let mut components = None; 399 | while let Some(key) = map.next_key()? { 400 | match key { 401 | EntityField::Components => { 402 | if components.is_some() { 403 | return Err(Error::duplicate_field(ENTITY_COMPONENTS)); 404 | } 405 | 406 | components = Some(map.next_value_seed(ReflectMapDeserializer { 407 | registry: self.registry, 408 | })?); 409 | } 410 | } 411 | } 412 | 413 | let components = components 414 | .take() 415 | .ok_or_else(|| Error::missing_field(ENTITY_COMPONENTS))?; 416 | Ok(DynamicEntity { 417 | entity: self.entity, 418 | components, 419 | }) 420 | } 421 | } 422 | 423 | struct ReflectMapDeserializer<'a> { 424 | registry: &'a TypeRegistry, 425 | } 426 | 427 | impl<'de> DeserializeSeed<'de> for ReflectMapDeserializer<'_> { 428 | type Value = Vec>; 429 | 430 | fn deserialize(self, deserializer: D) -> Result 431 | where 432 | D: serde::Deserializer<'de>, 433 | { 434 | deserializer.deserialize_map(ReflectMapVisitor { 435 | registry: self.registry, 436 | }) 437 | } 438 | } 439 | 440 | struct ReflectMapVisitor<'a> { 441 | pub registry: &'a TypeRegistry, 442 | } 443 | 444 | impl<'de> Visitor<'de> for ReflectMapVisitor<'_> { 445 | type Value = Vec>; 446 | 447 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 448 | formatter.write_str("map of reflect types") 449 | } 450 | 451 | fn visit_map(self, mut map: A) -> std::result::Result 452 | where 453 | A: MapAccess<'de>, 454 | { 455 | let mut added = HashSet::new(); 456 | let mut entries = Vec::new(); 457 | while let Some(registration) = 458 | map.next_key_seed(TypeRegistrationDeserializer::new(self.registry))? 459 | { 460 | if !added.insert(registration.type_id()) { 461 | return Err(Error::custom(format_args!( 462 | "duplicate reflect type: `{}`", 463 | registration.type_info().type_path(), 464 | ))); 465 | } 466 | 467 | let value = 468 | map.next_value_seed(TypedReflectDeserializer::new(registration, self.registry))?; 469 | 470 | // Attempt to convert using FromReflect. 471 | let value = self 472 | .registry 473 | .get(registration.type_id()) 474 | .and_then(|tr| tr.data::()) 475 | .and_then(|fr| fr.from_reflect(value.as_partial_reflect())) 476 | .map(PartialReflect::into_partial_reflect) 477 | .unwrap_or(value); 478 | 479 | entries.push(value); 480 | } 481 | 482 | Ok(entries) 483 | } 484 | 485 | fn visit_seq(self, mut seq: A) -> Result 486 | where 487 | A: SeqAccess<'de>, 488 | { 489 | let mut dynamic_properties = Vec::new(); 490 | while let Some(entity) = seq.next_element_seed(ReflectDeserializer::new(self.registry))? { 491 | dynamic_properties.push(entity); 492 | } 493 | 494 | Ok(dynamic_properties) 495 | } 496 | } 497 | -------------------------------------------------------------------------------- /src/serde/mod.rs: -------------------------------------------------------------------------------- 1 | //! `serde` serialization and deserialization implementation for snapshots and checkpoints. 2 | 3 | mod de; 4 | mod ser; 5 | 6 | pub use self::{ 7 | de::{ 8 | CheckpointsDeserializer, 9 | SnapshotDeserializer, 10 | }, 11 | ser::{ 12 | CheckpointsSerializer, 13 | SnapshotSerializer, 14 | }, 15 | }; 16 | 17 | pub(super) const SNAPSHOT_STRUCT: &str = "Snapshot"; 18 | pub(super) const SNAPSHOT_ENTITIES: &str = "entities"; 19 | pub(super) const SNAPSHOT_RESOURCES: &str = "resources"; 20 | pub(super) const SNAPSHOT_CHECKPOINTS: &str = "rollbacks"; 21 | 22 | pub(super) const CHECKPOINTS_STRUCT: &str = "Rollbacks"; 23 | pub(super) const CHECKPOINTS_SNAPSHOTS: &str = "checkpoints"; 24 | pub(super) const CHECKPOINTS_ACTIVE: &str = "active"; 25 | 26 | pub(super) const ENTITY_STRUCT: &str = "Entity"; 27 | pub(super) const ENTITY_COMPONENTS: &str = "components"; 28 | -------------------------------------------------------------------------------- /src/serde/ser.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | reflect::TypeRegistry, 3 | scene::serde::{ 4 | EntitiesSerializer, 5 | SceneMapSerializer, 6 | }, 7 | }; 8 | use serde::{ 9 | ser::{ 10 | SerializeSeq, 11 | SerializeStruct, 12 | }, 13 | Serialize, 14 | Serializer, 15 | }; 16 | 17 | use crate::{ 18 | checkpoint::Checkpoints, 19 | prelude::*, 20 | serde::{ 21 | CHECKPOINTS_ACTIVE, 22 | CHECKPOINTS_SNAPSHOTS, 23 | CHECKPOINTS_STRUCT, 24 | SNAPSHOT_CHECKPOINTS, 25 | SNAPSHOT_ENTITIES, 26 | SNAPSHOT_RESOURCES, 27 | SNAPSHOT_STRUCT, 28 | }, 29 | }; 30 | 31 | /// Handles serialization of a snapshot as a struct containing its entities and resources. 32 | pub struct SnapshotSerializer<'a> { 33 | /// The snapshot to serialize. 34 | pub snapshot: &'a Snapshot, 35 | /// Type registry in which the components and resources types used in the snapshot are registered. 36 | pub registry: &'a TypeRegistry, 37 | } 38 | 39 | impl<'a> SnapshotSerializer<'a> { 40 | /// Creates a snapshot serializer. 41 | pub fn new(snapshot: &'a Snapshot, registry: &'a TypeRegistry) -> Self { 42 | SnapshotSerializer { snapshot, registry } 43 | } 44 | } 45 | 46 | impl Serialize for SnapshotSerializer<'_> { 47 | fn serialize(&self, serializer: S) -> Result 48 | where 49 | S: serde::Serializer, 50 | { 51 | let mut state = serializer.serialize_struct( 52 | SNAPSHOT_STRUCT, 53 | if self.snapshot.checkpoints.is_some() { 54 | 3 55 | } else { 56 | 2 57 | }, 58 | )?; 59 | state.serialize_field(SNAPSHOT_ENTITIES, &EntitiesSerializer { 60 | entities: &self.snapshot.entities, 61 | registry: self.registry, 62 | })?; 63 | state.serialize_field(SNAPSHOT_RESOURCES, &SceneMapSerializer { 64 | entries: &self.snapshot.resources, 65 | registry: self.registry, 66 | })?; 67 | 68 | if let Some(checkpoints) = &self.snapshot.checkpoints { 69 | state.serialize_field(SNAPSHOT_CHECKPOINTS, &CheckpointsSerializer { 70 | checkpoints, 71 | registry: self.registry, 72 | })?; 73 | } 74 | 75 | state.end() 76 | } 77 | } 78 | 79 | struct SnapshotListSerializer<'a> { 80 | snapshots: &'a [Snapshot], 81 | registry: &'a TypeRegistry, 82 | } 83 | 84 | impl Serialize for SnapshotListSerializer<'_> { 85 | fn serialize(&self, serializer: S) -> Result 86 | where 87 | S: Serializer, 88 | { 89 | let mut seq = serializer.serialize_seq(Some(self.snapshots.len()))?; 90 | 91 | for snapshot in self.snapshots { 92 | seq.serialize_element(&SnapshotSerializer { 93 | snapshot, 94 | registry: self.registry, 95 | })?; 96 | } 97 | 98 | seq.end() 99 | } 100 | } 101 | 102 | /// Handles serialization of the checkpoints store. 103 | pub struct CheckpointsSerializer<'a> { 104 | /// The checkpoints to serialize. 105 | pub checkpoints: &'a Checkpoints, 106 | /// Type registry in which the components and resources types used in the checkpoints are registered. 107 | pub registry: &'a TypeRegistry, 108 | } 109 | 110 | impl Serialize for CheckpointsSerializer<'_> { 111 | fn serialize(&self, serializer: S) -> Result 112 | where 113 | S: Serializer, 114 | { 115 | let mut state = serializer.serialize_struct(CHECKPOINTS_STRUCT, 2)?; 116 | 117 | state.serialize_field(CHECKPOINTS_SNAPSHOTS, &SnapshotListSerializer { 118 | snapshots: &self.checkpoints.snapshots, 119 | registry: self.registry, 120 | })?; 121 | state.serialize_field(CHECKPOINTS_ACTIVE, &self.checkpoints.active)?; 122 | 123 | state.end() 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/snapshot/applier.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::TypeId, 3 | marker::PhantomData, 4 | }; 5 | 6 | use bevy::{ 7 | ecs::{ 8 | entity::{ 9 | EntityHashMap, 10 | SceneEntityMapper, 11 | }, 12 | query::QueryFilter, 13 | reflect::ReflectMapEntities, 14 | system::EntityCommands, 15 | world::{ 16 | CommandQueue, 17 | EntityRef, 18 | }, 19 | }, 20 | platform::collections::HashMap, 21 | prelude::*, 22 | reflect::TypeRegistry, 23 | scene::SceneSpawnError, 24 | }; 25 | 26 | use crate::{ 27 | error::Error, 28 | prelude::*, 29 | }; 30 | 31 | /// A [`Hook`] runs on each entity when applying a snapshot. 32 | /// 33 | /// # Example 34 | /// This could be used to apply entities as children of another entity. 35 | /// ``` 36 | /// # use bevy::prelude::*; 37 | /// # use bevy_save::prelude::*; 38 | /// # let mut app = App::new(); 39 | /// # app.add_plugins(MinimalPlugins); 40 | /// # app.add_plugins(SavePlugins); 41 | /// # let world = app.world_mut(); 42 | /// # let snapshot = Snapshot::from_world(world); 43 | /// # let parent = world.spawn_empty().id(); 44 | /// snapshot 45 | /// .applier(world) 46 | /// .hook(move |entity, cmds| { 47 | /// if !entity.contains::() { 48 | /// cmds.insert(ChildOf(parent)); 49 | /// } 50 | /// }) 51 | /// .apply(); 52 | /// ``` 53 | pub trait Hook: for<'a> Fn(&'a EntityRef, &'a mut EntityCommands) + Send + Sync {} 54 | 55 | impl Hook for T where T: for<'a> Fn(&'a EntityRef, &'a mut EntityCommands) + Send + Sync {} 56 | 57 | /// A boxed [`Hook`]. 58 | pub type BoxedHook = Box; 59 | 60 | type SpawnPrefabFn = fn(Box, Entity, &mut World); 61 | 62 | /// [`SnapshotApplier`] lets you configure how a snapshot will be applied to the [`World`]. 63 | pub struct SnapshotApplier<'a, F = ()> { 64 | snapshot: &'a Snapshot, 65 | world: &'a mut World, 66 | entity_map: Option<&'a mut EntityHashMap>, 67 | type_registry: Option<&'a TypeRegistry>, 68 | despawn: Option>, 69 | hook: Option, 70 | prefabs: HashMap, 71 | } 72 | 73 | impl<'a> SnapshotApplier<'a> { 74 | /// Create a new [`SnapshotApplier`] with from the world and snapshot. 75 | pub fn new(snapshot: &'a Snapshot, world: &'a mut World) -> Self { 76 | Self { 77 | snapshot, 78 | world, 79 | entity_map: None, 80 | type_registry: None, 81 | despawn: None, 82 | hook: None, 83 | prefabs: HashMap::new(), 84 | } 85 | } 86 | } 87 | 88 | impl<'a, A> SnapshotApplier<'a, A> { 89 | /// Providing an entity map allows you to map ids of spawned entities and see what entities have been spawned. 90 | pub fn entity_map(mut self, entity_map: &'a mut EntityHashMap) -> Self { 91 | self.entity_map = Some(entity_map); 92 | self 93 | } 94 | 95 | /// Set the [`TypeRegistry`] to be used for reflection. 96 | /// 97 | /// If this is not provided, the [`AppTypeRegistry`] resource is used as a default. 98 | pub fn type_registry(mut self, type_registry: &'a TypeRegistry) -> Self { 99 | self.type_registry = Some(type_registry); 100 | self 101 | } 102 | 103 | /// Change how the snapshot affects existing entities while applying. 104 | pub fn despawn(self) -> SnapshotApplier<'a, F> { 105 | SnapshotApplier { 106 | snapshot: self.snapshot, 107 | world: self.world, 108 | entity_map: self.entity_map, 109 | type_registry: self.type_registry, 110 | despawn: Some(PhantomData), 111 | hook: self.hook, 112 | prefabs: self.prefabs, 113 | } 114 | } 115 | 116 | /// Add a [`Hook`] that will run for each entity after applying. 117 | pub fn hook(mut self, hook: F) -> Self { 118 | self.hook = Some(Box::new(hook)); 119 | self 120 | } 121 | 122 | /// Handle loading for a [`Prefab`]. 123 | #[allow(clippy::missing_panics_doc)] 124 | pub fn prefab(mut self) -> Self { 125 | self.prefabs.insert( 126 | std::any::TypeId::of::

(), 127 | |this: Box, target: Entity, world: &mut World| { 128 | world.entity_mut(target).insert(P::Marker::default()); 129 | 130 | P::spawn( 131 |

::from_reflect(&*this).unwrap(), 132 | target, 133 | world, 134 | ); 135 | }, 136 | ); 137 | self 138 | } 139 | } 140 | 141 | impl SnapshotApplier<'_, F> { 142 | /// Apply the [`Snapshot`] to the [`World`]. 143 | /// 144 | /// # Panics 145 | /// If `type_registry` is not set or the [`AppTypeRegistry`] resource does not exist. 146 | /// 147 | /// # Errors 148 | /// If a type included in the [`Snapshot`] has not been registered with the type registry. 149 | pub fn apply(self) -> Result<(), Error> { 150 | let app_type_registry_arc = self.world.get_resource::().cloned(); 151 | 152 | let app_type_registry = app_type_registry_arc.as_ref().map(|r| r.read()); 153 | 154 | let type_registry = self 155 | .type_registry 156 | .or(app_type_registry.as_deref()) 157 | .expect("Must set `type_registry` or insert `AppTypeRegistry` resource to apply."); 158 | 159 | let mut default_entity_map = EntityHashMap::default(); 160 | 161 | let entity_map = self.entity_map.unwrap_or(&mut default_entity_map); 162 | 163 | let mut prefab_entities = HashMap::new(); 164 | 165 | // Despawn entities 166 | if self.despawn.is_some() { 167 | let invalid = self 168 | .world 169 | .query_filtered::() 170 | .iter(self.world) 171 | .collect::>(); 172 | 173 | for entity in invalid { 174 | self.world.despawn(entity); 175 | } 176 | } 177 | 178 | // First ensure that every entity in the snapshot has a corresponding world 179 | // entity in the entity map. 180 | for scene_entity in &self.snapshot.entities { 181 | // Fetch the entity with the given entity id from the `entity_map` 182 | // or spawn a new entity with a transiently unique id if there is 183 | // no corresponding entry. 184 | entity_map 185 | .entry(scene_entity.entity) 186 | .or_insert_with(|| self.world.spawn_empty().id()); 187 | } 188 | 189 | for scene_entity in &self.snapshot.entities { 190 | // Fetch the entity with the given entity id from the `entity_map`. 191 | let entity = *entity_map 192 | .get(&scene_entity.entity) 193 | .expect("should have previously spawned an empty entity"); 194 | 195 | // Apply/ add each component to the given entity. 196 | for component in &scene_entity.components { 197 | let mut component = component.to_dynamic(); 198 | let type_info = component.get_represented_type_info().ok_or_else(|| { 199 | SceneSpawnError::NoRepresentedType { 200 | type_path: component.reflect_type_path().to_string(), 201 | } 202 | })?; 203 | 204 | let type_id = type_info.type_id(); 205 | if self.prefabs.contains_key(&type_id) { 206 | prefab_entities 207 | .entry(type_id) 208 | .or_insert_with(Vec::new) 209 | .push((entity, component)); 210 | 211 | continue; 212 | } 213 | 214 | let registration = type_registry.get(type_id).ok_or_else(|| { 215 | SceneSpawnError::UnregisteredButReflectedType { 216 | type_path: type_info.type_path().to_string(), 217 | } 218 | })?; 219 | let reflect_component = 220 | registration.data::().ok_or_else(|| { 221 | SceneSpawnError::UnregisteredComponent { 222 | type_path: type_info.type_path().to_string(), 223 | } 224 | })?; 225 | 226 | if let Some(map_entities) = registration.data::() { 227 | SceneEntityMapper::world_scope(entity_map, self.world, |_, mapper| { 228 | map_entities.map_entities(component.as_partial_reflect_mut(), mapper); 229 | }); 230 | } 231 | 232 | // If the entity already has the given component attached, 233 | // just apply the (possibly) new value, otherwise add the 234 | // component to the entity. 235 | reflect_component.insert( 236 | &mut self.world.entity_mut(entity), 237 | component.as_partial_reflect(), 238 | type_registry, 239 | ); 240 | } 241 | } 242 | 243 | // Insert resources after all entities have been added to the world. 244 | // This ensures the entities are available for the resources to reference during mapping. 245 | for resource in &self.snapshot.resources { 246 | let mut resource = resource.to_dynamic(); 247 | let type_info = resource.get_represented_type_info().ok_or_else(|| { 248 | SceneSpawnError::NoRepresentedType { 249 | type_path: resource.reflect_type_path().to_string(), 250 | } 251 | })?; 252 | let registration = type_registry.get(type_info.type_id()).ok_or_else(|| { 253 | SceneSpawnError::UnregisteredButReflectedType { 254 | type_path: type_info.type_path().to_string(), 255 | } 256 | })?; 257 | let reflect_resource = registration.data::().ok_or_else(|| { 258 | SceneSpawnError::UnregisteredResource { 259 | type_path: type_info.type_path().to_string(), 260 | } 261 | })?; 262 | 263 | // If this resource references entities in the scene, update 264 | // them to the entities in the world. 265 | if let Some(map_entities) = registration.data::() { 266 | SceneEntityMapper::world_scope(entity_map, self.world, |_, mapper| { 267 | map_entities.map_entities(resource.as_partial_reflect_mut(), mapper); 268 | }); 269 | } 270 | 271 | // If the world already contains an instance of the given resource 272 | // just apply the (possibly) new value, otherwise insert the resource 273 | reflect_resource.apply_or_insert( 274 | self.world, 275 | resource.as_partial_reflect(), 276 | type_registry, 277 | ); 278 | } 279 | 280 | // Prefab hooks 281 | for (type_id, entities) in prefab_entities { 282 | let Some(hook) = self.prefabs.get(&type_id) else { 283 | continue; 284 | }; 285 | 286 | for (entity, component) in entities { 287 | hook(component, entity, self.world); 288 | } 289 | } 290 | 291 | // Entity hook 292 | if let Some(hook) = &self.hook { 293 | let mut queue = CommandQueue::default(); 294 | let mut commands = Commands::new(&mut queue, self.world); 295 | 296 | for (_, entity) in entity_map { 297 | let entity_ref = self.world.entity(*entity); 298 | let mut entity_mut = commands.entity(*entity); 299 | 300 | hook(&entity_ref, &mut entity_mut); 301 | } 302 | 303 | queue.apply(self.world); 304 | } 305 | 306 | Ok(()) 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /src/snapshot/builder.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::{ 3 | Any, 4 | TypeId, 5 | }, 6 | collections::BTreeMap, 7 | }; 8 | 9 | use bevy::{ 10 | ecs::component::ComponentId, 11 | prelude::*, 12 | reflect::TypeRegistry, 13 | scene::DynamicEntity, 14 | }; 15 | 16 | use crate::{ 17 | checkpoint::{ 18 | CheckpointRegistry, 19 | Checkpoints, 20 | }, 21 | prelude::*, 22 | }; 23 | 24 | /// A snapshot builder that can extract entities, resources, and rollback [`Checkpoints`] from a [`World`]. 25 | pub struct SnapshotBuilder<'a> { 26 | world: &'a World, 27 | type_registry: Option<&'a TypeRegistry>, 28 | entities: BTreeMap, 29 | resources: BTreeMap>, 30 | filter: SceneFilter, 31 | checkpoints: Option, 32 | is_checkpoint: bool, 33 | } 34 | 35 | impl<'a> SnapshotBuilder<'a> { 36 | /// Create a new [`SnapshotBuilder`] from the [`World`]. 37 | /// 38 | /// You must call at least one of the `extract` methods or the built snapshot will be empty. 39 | /// 40 | /// # Example 41 | /// ``` 42 | /// # use bevy::prelude::*; 43 | /// # use bevy_save::prelude::*; 44 | /// # let mut app = App::new(); 45 | /// # app.add_plugins(MinimalPlugins); 46 | /// # app.add_plugins(SavePlugins); 47 | /// # let world = app.world_mut(); 48 | /// SnapshotBuilder::snapshot(world) 49 | /// // Extract all matching entities and resources 50 | /// .extract_all() 51 | /// 52 | /// // Clear all extracted entities without any components 53 | /// .clear_empty() 54 | /// 55 | /// // Build the `Snapshot` 56 | /// .build(); 57 | /// ``` 58 | pub fn snapshot(world: &'a World) -> Self { 59 | Self { 60 | world, 61 | type_registry: None, 62 | entities: BTreeMap::new(), 63 | resources: BTreeMap::new(), 64 | filter: SceneFilter::default(), 65 | checkpoints: None, 66 | is_checkpoint: false, 67 | } 68 | } 69 | 70 | /// Create a new [`SnapshotBuilder`] from the [`World`]. 71 | /// 72 | /// Types extracted by this builder will respect the [`CheckpointRegistry`](crate::checkpoint::CheckpointRegistry). 73 | /// 74 | /// You must call at least one of the `extract` methods or the built snapshot will be empty. 75 | /// 76 | /// # Example 77 | /// ``` 78 | /// # use bevy::prelude::*; 79 | /// # use bevy_save::prelude::*; 80 | /// # let mut app = App::new(); 81 | /// # app.add_plugins(MinimalPlugins); 82 | /// # app.add_plugins(SavePlugins); 83 | /// # let world = app.world_mut(); 84 | /// SnapshotBuilder::checkpoint(world) 85 | /// // Extract all matching entities and resources 86 | /// .extract_all() 87 | /// 88 | /// // Clear all extracted entities without any components 89 | /// .clear_empty() 90 | /// 91 | /// // Build the `Snapshot` 92 | /// .build(); 93 | /// ``` 94 | pub fn checkpoint(world: &'a World) -> Self { 95 | Self { 96 | world, 97 | type_registry: None, 98 | entities: BTreeMap::new(), 99 | resources: BTreeMap::new(), 100 | filter: SceneFilter::default(), 101 | checkpoints: None, 102 | is_checkpoint: true, 103 | } 104 | } 105 | } 106 | 107 | impl<'a> SnapshotBuilder<'a> { 108 | /// Retrieve the builder's reference to the [`World`]. 109 | pub fn world<'w>(&self) -> &'w World 110 | where 111 | 'a: 'w, 112 | { 113 | self.world 114 | } 115 | 116 | /// Set the [`TypeRegistry`] to be used for reflection. 117 | /// 118 | /// If this is not provided, the [`AppTypeRegistry`] resource is used as a default. 119 | pub fn type_registry(mut self, type_registry: &'a TypeRegistry) -> Self { 120 | self.type_registry = Some(type_registry); 121 | self 122 | } 123 | } 124 | 125 | impl SnapshotBuilder<'_> { 126 | /// Specify a custom [`SceneFilter`] to be used with this builder. 127 | /// 128 | /// This filter is applied to both components and resources. 129 | pub fn filter(mut self, filter: SceneFilter) -> Self { 130 | self.filter = filter; 131 | self 132 | } 133 | 134 | /// Allows the given type, `T`, to be included in the generated snapshot. 135 | /// 136 | /// This method may be called multiple times for any number of types. 137 | /// 138 | /// This is the inverse of [`deny`](Self::deny). 139 | /// If `T` has already been denied, then it will be removed from the blacklist. 140 | pub fn allow(mut self) -> Self { 141 | self.filter = self.filter.allow::(); 142 | self 143 | } 144 | 145 | /// Denies the given type, `T`, from being included in the generated snapshot. 146 | /// 147 | /// This method may be called multiple times for any number of types. 148 | /// 149 | /// This is the inverse of [`allow`](Self::allow). 150 | /// If `T` has already been allowed, then it will be removed from the whitelist. 151 | pub fn deny(mut self) -> Self { 152 | self.filter = self.filter.deny::(); 153 | self 154 | } 155 | 156 | /// Updates the filter to allow all types. 157 | /// 158 | /// This is useful for resetting the filter so that types may be selectively [denied]. 159 | /// 160 | /// [denied]: Self::deny 161 | pub fn allow_all(mut self) -> Self { 162 | self.filter = SceneFilter::allow_all(); 163 | self 164 | } 165 | 166 | /// Updates the filter to deny all types. 167 | /// 168 | /// This is useful for resetting the filter so that types may be selectively [allowed]. 169 | /// 170 | /// [allowed]: Self::allow 171 | pub fn deny_all(mut self) -> Self { 172 | self.filter = SceneFilter::deny_all(); 173 | self 174 | } 175 | } 176 | 177 | impl SnapshotBuilder<'_> { 178 | /// Extract a single entity from the builder’s [`World`]. 179 | pub fn extract_entity(self, entity: Entity) -> Self { 180 | self.extract_entities([entity].into_iter()) 181 | } 182 | 183 | /// Extract the given entities from the builder’s [`World`]. 184 | /// 185 | /// # Panics 186 | /// If `type_registry` is not set or the [`AppTypeRegistry`] resource does not exist. 187 | pub fn extract_entities(mut self, entities: impl Iterator) -> Self { 188 | let app_type_registry = self 189 | .world 190 | .get_resource::() 191 | .map(|r| r.read()); 192 | 193 | let type_registry = self 194 | .type_registry 195 | .or(app_type_registry.as_deref()) 196 | .expect("Must set `type_registry` or insert `AppTypeRegistry` resource to extract."); 197 | 198 | let checkpoints = self.world.get_resource::(); 199 | 200 | for entity in entities.filter_map(|e| self.world.get_entity(e).ok()) { 201 | let id = entity.id(); 202 | let mut entry = DynamicEntity { 203 | entity: id, 204 | components: Vec::new(), 205 | }; 206 | 207 | for component in entity.archetype().components() { 208 | let reflect = self 209 | .world 210 | .components() 211 | .get_info(component) 212 | .and_then(|info| info.type_id()) 213 | .filter(|id| self.filter.is_allowed_by_id(*id)) 214 | .filter(|id| { 215 | if self.is_checkpoint { 216 | checkpoints.is_none_or(|rb| rb.is_allowed_by_id(*id)) 217 | } else { 218 | true 219 | } 220 | }) 221 | .and_then(|id| type_registry.get(id)) 222 | .and_then(|r| { 223 | let reflect = r.data::()?.reflect(entity)?; 224 | 225 | let reflect = r 226 | .data::() 227 | .and_then(|fr| fr.from_reflect(reflect.as_partial_reflect())) 228 | .map_or_else( 229 | || reflect.to_dynamic(), 230 | PartialReflect::into_partial_reflect, 231 | ); 232 | 233 | Some(reflect) 234 | }); 235 | 236 | if let Some(reflect) = reflect { 237 | entry.components.push(reflect); 238 | } 239 | } 240 | 241 | self.entities.insert(id, entry); 242 | } 243 | 244 | self 245 | } 246 | 247 | /// Extract the entities matching the given filter from the builder’s [`World`]. 248 | pub fn extract_entities_matching bool>(self, filter: F) -> Self { 249 | // TODO: We should be using Query and caching the lookup 250 | let entities = self.world.iter_entities().filter(filter).map(|e| e.id()); 251 | self.extract_entities(entities) 252 | } 253 | 254 | /// Extract all entities from the builder’s [`World`]. 255 | pub fn extract_all_entities(self) -> Self { 256 | let entites = self.world.iter_entities().map(|e| e.id()); 257 | self.extract_entities(entites) 258 | } 259 | 260 | /// Extract all entities with a custom extraction function. 261 | pub fn extract_entities_manual(mut self, func: F) -> Self 262 | where 263 | F: Fn(&EntityRef) -> Option>>, 264 | { 265 | for entity in self.world.iter_entities() { 266 | let Some(components) = func(&entity) else { 267 | continue; 268 | }; 269 | 270 | self.entities.insert(entity.id(), DynamicEntity { 271 | entity: entity.id(), 272 | components, 273 | }); 274 | } 275 | 276 | self 277 | } 278 | 279 | /// Extract all [`Prefab`] entities with a custom extraction function. 280 | pub fn extract_prefab(mut self, func: F) -> Self 281 | where 282 | F: Fn(&EntityRef) -> Option

, 283 | P: Prefab + PartialReflect, 284 | { 285 | for entity in self.world.iter_entities() { 286 | if !entity.contains::() { 287 | continue; 288 | } 289 | 290 | let Some(prefab) = func(&entity) else { 291 | continue; 292 | }; 293 | 294 | self.entities.insert(entity.id(), DynamicEntity { 295 | entity: entity.id(), 296 | components: vec![Box::new(prefab).into_partial_reflect()], 297 | }); 298 | } 299 | 300 | self 301 | } 302 | 303 | /// Extract all spawned instances of [`Prefab`] from the builder’s [`World`]. 304 | pub fn extract_all_prefabs(self) -> Self { 305 | P::extract(self) 306 | } 307 | 308 | /// Extract a single resource from the builder's [`World`]. 309 | pub fn extract_resource(self) -> Self { 310 | let type_id = self 311 | .world 312 | .components() 313 | .resource_id::() 314 | .and_then(|i| self.world.components().get_info(i)) 315 | .and_then(|i| i.type_id()) 316 | .into_iter(); 317 | 318 | self.extract_resources_by_type_id(type_id) 319 | } 320 | 321 | /// Extract a single resource with the given type path from the builder's [`World`]. 322 | pub fn extract_resource_by_path>(self, type_path: T) -> Self { 323 | self.extract_resources_by_path([type_path].into_iter()) 324 | } 325 | 326 | /// Extract resources with the given type paths from the builder's [`World`]. 327 | /// 328 | /// # Panics 329 | /// If `type_registry` is not set or the [`AppTypeRegistry`] resource does not exist. 330 | pub fn extract_resources_by_path>( 331 | self, 332 | type_paths: impl Iterator, 333 | ) -> Self { 334 | let app_type_registry = self 335 | .world 336 | .get_resource::() 337 | .map(|r| r.read()); 338 | 339 | let type_registry = self 340 | .type_registry 341 | .or(app_type_registry.as_deref()) 342 | .expect("Must set `type_registry` or insert `AppTypeRegistry` resource to extract."); 343 | 344 | self.extract_resources_by_type_id( 345 | type_paths 346 | .filter_map(|p| type_registry.get_with_type_path(p.as_ref())) 347 | .map(|r| r.type_id()), 348 | ) 349 | } 350 | 351 | /// Extract resources with the given [`TypeId`]'s from the builder's [`World`]. 352 | /// 353 | /// # Panics 354 | /// If `type_registry` is not set or the [`AppTypeRegistry`] resource does not exist. 355 | pub fn extract_resources_by_type_id(mut self, type_ids: impl Iterator) -> Self { 356 | let app_type_registry = self 357 | .world 358 | .get_resource::() 359 | .map(|r| r.read()); 360 | 361 | let type_registry = self 362 | .type_registry 363 | .or(app_type_registry.as_deref()) 364 | .expect("Must set `type_registry` or insert `AppTypeRegistry` resource to extract."); 365 | 366 | let checkpoints = self.world.get_resource::(); 367 | 368 | type_ids 369 | .filter_map(|id| type_registry.get(id)) 370 | .filter(|r| self.filter.is_allowed_by_id((*r).type_id())) 371 | .filter(|r| { 372 | if self.is_checkpoint { 373 | checkpoints.is_none_or(|rb| rb.is_allowed_by_id((*r).type_id())) 374 | } else { 375 | true 376 | } 377 | }) 378 | .filter_map(|r| { 379 | let reflect = r.data::()?.reflect(self.world).ok()?; 380 | 381 | let reflect = r 382 | .data::() 383 | .and_then(|fr| fr.from_reflect(reflect.as_partial_reflect())) 384 | .map_or_else( 385 | || reflect.to_dynamic(), 386 | PartialReflect::into_partial_reflect, 387 | ); 388 | 389 | Some(( 390 | self.world.components().get_resource_id(r.type_id())?, 391 | reflect, 392 | )) 393 | }) 394 | .for_each(|(i, r)| { 395 | self.resources.insert(i, r); 396 | }); 397 | 398 | self 399 | } 400 | 401 | /// Extract all resources from the builder's [`World`]. 402 | pub fn extract_all_resources(self) -> Self { 403 | let resources = self 404 | .world 405 | .storages() 406 | .resources 407 | .iter() 408 | .map(|(id, _)| id) 409 | .filter_map(move |id| self.world.components().get_info(id)) 410 | .filter_map(|info| info.type_id()); 411 | 412 | self.extract_resources_by_type_id(resources) 413 | } 414 | 415 | /// Extract [`Checkpoints`] from the builder's [`World`]. 416 | /// 417 | /// # Panics 418 | /// If `type_registry` is not set or the [`AppTypeRegistry`] resource does not exist. 419 | pub fn extract_checkpoints(mut self) -> Self { 420 | let app_type_registry = self 421 | .world 422 | .get_resource::() 423 | .map(|r| r.read()); 424 | 425 | let type_registry = self 426 | .type_registry 427 | .or(app_type_registry.as_deref()) 428 | .expect("Must set `type_registry` or insert `AppTypeRegistry` resource to extract."); 429 | 430 | self.checkpoints = self 431 | .world 432 | .get_resource::() 433 | .map(|r| r.clone_reflect(type_registry)); 434 | 435 | self 436 | } 437 | 438 | /// Extract all entities, and resources from the builder's [`World`]. 439 | pub fn extract_all(self) -> Self { 440 | self.extract_all_entities().extract_all_resources() 441 | } 442 | 443 | /// Extract all entities, resources, and [`Checkpoints`] from the builder's [`World`]. 444 | pub fn extract_all_with_checkpoints(self) -> Self { 445 | self.extract_all().extract_checkpoints() 446 | } 447 | } 448 | 449 | impl SnapshotBuilder<'_> { 450 | /// Clear all extracted entities. 451 | pub fn clear_entities(mut self) -> Self { 452 | self.entities.clear(); 453 | self 454 | } 455 | 456 | /// Clear all extracted resources. 457 | pub fn clear_resources(mut self) -> Self { 458 | self.resources.clear(); 459 | self 460 | } 461 | 462 | /// Clear all extracted entities without any components. 463 | pub fn clear_empty(mut self) -> Self { 464 | self.entities.retain(|_, e| !e.components.is_empty()); 465 | self 466 | } 467 | 468 | /// Clear [`Checkpoints`] from the snapshot. 469 | pub fn clear_checkpoints(mut self) -> Self { 470 | self.checkpoints = None; 471 | self 472 | } 473 | 474 | /// Clear all extracted entities and resources. 475 | pub fn clear(self) -> Self { 476 | self.clear_entities().clear_resources() 477 | } 478 | } 479 | 480 | impl SnapshotBuilder<'_> { 481 | /// Build the extracted entities and resources into a [`Snapshot`]. 482 | pub fn build(self) -> Snapshot { 483 | Snapshot { 484 | entities: self.entities.into_values().collect(), 485 | resources: self.resources.into_values().collect(), 486 | checkpoints: self.checkpoints, 487 | } 488 | } 489 | } 490 | -------------------------------------------------------------------------------- /src/snapshot/mod.rs: -------------------------------------------------------------------------------- 1 | //! Capturing and applying [`Snapshot`]s of application state. 2 | 3 | mod applier; 4 | mod builder; 5 | mod snapshot; 6 | 7 | pub use self::{ 8 | applier::{ 9 | BoxedHook, 10 | Hook, 11 | SnapshotApplier, 12 | }, 13 | builder::SnapshotBuilder, 14 | snapshot::Snapshot, 15 | }; 16 | -------------------------------------------------------------------------------- /src/snapshot/snapshot.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | prelude::*, 3 | reflect::TypeRegistry, 4 | scene::DynamicEntity, 5 | }; 6 | 7 | use crate::{ 8 | checkpoint::Checkpoints, 9 | error::Error, 10 | prelude::*, 11 | serde::SnapshotSerializer, 12 | CloneReflect, 13 | }; 14 | 15 | /// A collection of serializable entities and resources. 16 | /// 17 | /// Can be serialized via [`SnapshotSerializer`](crate::serde::SnapshotSerializer) and deserialized via [`SnapshotDeserializer`](crate::serde::SnapshotDeserializer). 18 | pub struct Snapshot { 19 | /// Entities contained in the snapshot. 20 | pub entities: Vec, 21 | 22 | /// Resources contained in the snapshot. 23 | pub resources: Vec>, 24 | 25 | pub(crate) checkpoints: Option, 26 | } 27 | 28 | impl Snapshot { 29 | /// Returns a complete [`Snapshot`] of the current [`World`] state. 30 | /// 31 | /// Contains all saveable entities, resources, and [`Checkpoints`]. 32 | /// 33 | /// # Shortcut for 34 | /// ``` 35 | /// # use bevy::prelude::*; 36 | /// # use bevy_save::prelude::*; 37 | /// # let mut app = App::new(); 38 | /// # app.add_plugins(MinimalPlugins); 39 | /// # app.add_plugins(SavePlugins); 40 | /// # let world = app.world_mut(); 41 | /// Snapshot::builder(world) 42 | /// .extract_all_with_checkpoints() 43 | /// .build(); 44 | pub fn from_world(world: &World) -> Self { 45 | Self::builder(world).extract_all_with_checkpoints().build() 46 | } 47 | 48 | /// Create a [`SnapshotBuilder`] from the [`World`], allowing you to create partial or filtered snapshots. 49 | /// 50 | /// # Example 51 | /// ``` 52 | /// # use bevy::prelude::*; 53 | /// # use bevy_save::prelude::*; 54 | /// # let mut app = App::new(); 55 | /// # app.add_plugins(MinimalPlugins); 56 | /// # app.add_plugins(SavePlugins); 57 | /// # let world = app.world_mut(); 58 | /// Snapshot::builder(world) 59 | /// // Extract all matching entities and resources 60 | /// .extract_all() 61 | /// 62 | /// // Clear all extracted entities without any components 63 | /// .clear_empty() 64 | /// 65 | /// // Build the `Snapshot` 66 | /// .build(); 67 | /// ``` 68 | pub fn builder(world: &World) -> SnapshotBuilder { 69 | SnapshotBuilder::snapshot(world) 70 | } 71 | 72 | /// Apply the [`Snapshot`] to the [`World`], using default applier settings. 73 | /// 74 | /// # Errors 75 | /// If a type included in the [`Snapshot`] has not been registered with the type registry. 76 | pub fn apply(&self, world: &mut World) -> Result<(), Error> { 77 | self.applier(world).apply() 78 | } 79 | 80 | /// Create a [`SnapshotApplier`] from the [`Snapshot`] and the [`World`]. 81 | /// 82 | /// This allows you to specify an entity map, hook, etc. 83 | /// 84 | /// # Example 85 | /// ``` 86 | /// # use bevy::prelude::*; 87 | /// # use bevy_save::prelude::*; 88 | /// # let mut app = App::new(); 89 | /// # app.add_plugins(MinimalPlugins); 90 | /// # app.add_plugins(SavePlugins); 91 | /// # let world = app.world_mut(); 92 | /// # let parent = Entity::from_raw(0); 93 | /// let snapshot = Snapshot::from_world(world); 94 | /// 95 | /// snapshot 96 | /// .applier(world) 97 | /// .hook(move |entity, cmds| { 98 | /// // You can use the hook to add, get, or remove Components 99 | /// if !entity.contains::() { 100 | /// cmds.insert(ChildOf(parent)); 101 | /// } 102 | /// }) 103 | /// .apply(); 104 | /// ``` 105 | pub fn applier<'a>(&'a self, world: &'a mut World) -> SnapshotApplier<'a> { 106 | SnapshotApplier::new(self, world) 107 | } 108 | 109 | /// Create a [`SnapshotSerializer`] from the [`Snapshot`] and the [`TypeRegistry`]. 110 | pub fn serializer<'a>(&'a self, registry: &'a TypeRegistry) -> SnapshotSerializer<'a> { 111 | SnapshotSerializer { 112 | snapshot: self, 113 | registry, 114 | } 115 | } 116 | } 117 | 118 | impl CloneReflect for Snapshot { 119 | fn clone_reflect(&self, registry: &TypeRegistry) -> Self { 120 | Self { 121 | entities: self.entities.clone_reflect(registry), 122 | resources: self.resources.clone_reflect(registry), 123 | checkpoints: self.checkpoints.clone_reflect(registry), 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /tests/bevy.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::expect_fun_call)] 2 | 3 | use bevy::{ 4 | diagnostic::DiagnosticsPlugin, 5 | prelude::*, 6 | reflect::{ 7 | serde::{ 8 | ReflectDeserializer, 9 | ReflectSerializer, 10 | TypedReflectDeserializer, 11 | TypedReflectSerializer, 12 | }, 13 | TypeRegistry, 14 | }, 15 | render::{ 16 | settings::{ 17 | RenderCreation, 18 | WgpuSettings, 19 | }, 20 | RenderPlugin, 21 | }, 22 | winit::WinitPlugin, 23 | }; 24 | use bevy_save::{ 25 | prelude::*, 26 | serde::{ 27 | SnapshotDeserializer, 28 | SnapshotSerializer, 29 | }, 30 | }; 31 | use serde::{ 32 | de::DeserializeSeed, 33 | Deserialize, 34 | Serialize, 35 | }; 36 | 37 | fn init_app() -> (App, Vec) { 38 | let mut app = App::new(); 39 | 40 | app // 41 | .add_plugins((MinimalPlugins, SavePlugins)) 42 | .register_type::() 43 | .register_type::(); 44 | 45 | let world = app.world_mut(); 46 | 47 | let ids = vec![ 48 | world.spawn(()).id().to_bits(), 49 | world 50 | .spawn(Transform::from_xyz(1.0, 2.0, 3.0)) 51 | .id() 52 | .to_bits(), 53 | ]; 54 | 55 | (app, ids) 56 | } 57 | 58 | fn extract(world: &World) -> Snapshot { 59 | Snapshot::builder(world).extract_all_entities().build() 60 | } 61 | 62 | #[derive(Serialize, Deserialize)] 63 | struct ExampleTransform { 64 | position: Vec3, 65 | } 66 | 67 | #[test] 68 | fn test_transforms() { 69 | fn serialize(snapshot: &Snapshot, registry: &TypeRegistry) -> String { 70 | let serializer = SnapshotSerializer { snapshot, registry }; 71 | 72 | let mut buf = Vec::new(); 73 | let format = serde_json::ser::PrettyFormatter::with_indent(b" "); 74 | let mut ser = serde_json::Serializer::with_formatter(&mut buf, format); 75 | 76 | serializer.serialize(&mut ser).unwrap(); 77 | 78 | String::from_utf8(buf).unwrap() 79 | } 80 | 81 | let (mut app, _) = init_app(); 82 | let world = app.world_mut(); 83 | 84 | let registry = world.resource::().read(); 85 | let snapshot = extract(world); 86 | 87 | let output = serialize(&snapshot, ®istry); 88 | 89 | let deserializer = SnapshotDeserializer { 90 | registry: ®istry, 91 | }; 92 | 93 | let mut de = serde_json::Deserializer::from_str(&output); 94 | 95 | let _ = deserializer.deserialize(&mut de).unwrap(); 96 | } 97 | 98 | trait SerDe { 99 | type Error: std::fmt::Debug; 100 | 101 | fn ser(value: &T) -> Result 102 | where 103 | T: Serialize; 104 | 105 | fn de(seed: D, text: &str) -> Result 106 | where 107 | D: for<'de> DeserializeSeed<'de, Value = T>; 108 | } 109 | 110 | struct Json; 111 | 112 | impl SerDe for Json { 113 | type Error = serde_json::Error; 114 | 115 | fn ser(value: &T) -> Result 116 | where 117 | T: Serialize, 118 | { 119 | serde_json::to_string_pretty(value) 120 | } 121 | 122 | fn de<'a, D, T>(de: D, text: &'a str) -> Result 123 | where 124 | D: for<'de> DeserializeSeed<'de, Value = T>, 125 | { 126 | let mut deserializer = serde_json::Deserializer::from_str(text); 127 | de.deserialize(&mut deserializer) 128 | } 129 | } 130 | 131 | fn roundtrip_registered(registry: &TypeRegistry, erased: bool) 132 | where 133 | S: SerDe, 134 | { 135 | for ty in registry.iter() { 136 | if !ty.contains::() || !ty.contains::() { 137 | continue; 138 | } 139 | 140 | let type_path = ty.type_info().type_path(); 141 | 142 | let Some(reflect_default) = ty.data::() else { 143 | //println!("No default for {:?}", type_path); 144 | continue; 145 | }; 146 | 147 | let default = reflect_default.default(); 148 | let reflect = default.as_partial_reflect(); 149 | 150 | let data = if erased { 151 | let value = ReflectSerializer::new(reflect, registry); 152 | S::ser(&value) 153 | } else { 154 | let value = TypedReflectSerializer::new(reflect, registry); 155 | S::ser(&value) 156 | } 157 | .expect(&format!("Failed to serialize {:?}", type_path)); 158 | 159 | let output = if erased { 160 | let de = ReflectDeserializer::new(registry); 161 | S::de(de, &data) 162 | } else { 163 | let seed = TypedReflectDeserializer::new(ty, registry); 164 | S::de(seed, &data) 165 | } 166 | .expect(&format!( 167 | "Failed to deserialize {:?}\n{}\n", 168 | type_path, data 169 | )); 170 | 171 | //println!("{:?}: {data}", type_path); 172 | assert!(default.reflect_partial_eq(&*output).unwrap_or(true)); 173 | } 174 | } 175 | 176 | fn build_registry_app() -> App { 177 | let mut app = App::new(); 178 | 179 | app.add_plugins( 180 | DefaultPlugins 181 | .build() 182 | .disable::() 183 | .disable::() 184 | .set(RenderPlugin { 185 | render_creation: RenderCreation::Automatic(WgpuSettings { 186 | backends: None, 187 | ..default() 188 | }), 189 | ..default() 190 | }), 191 | ); 192 | 193 | app 194 | } 195 | 196 | #[test] 197 | fn test_builtin_types() { 198 | let app = build_registry_app(); 199 | 200 | let registry = app.world().resource::().read(); 201 | 202 | roundtrip_registered::(®istry, true); 203 | roundtrip_registered::(®istry, false); 204 | } 205 | 206 | const TRANSFORM_JSON: &str = r#" 207 | { 208 | "bevy_transform::components::transform::Transform": { 209 | "translation": [ 210 | 1.0, 211 | 2.0, 212 | 3.0 213 | ], 214 | "rotation": [ 215 | 0.0, 216 | 0.0, 217 | 0.0, 218 | 1.0 219 | ], 220 | "scale": [ 221 | 1.0, 222 | 1.0, 223 | 1.0 224 | ] 225 | } 226 | }"#; 227 | 228 | const TRANSFORM_TYPED_JSON: &str = r#" 229 | { 230 | "translation": [ 231 | 1.0, 232 | 2.0, 233 | 3.0 234 | ], 235 | "rotation": [ 236 | 0.0, 237 | 0.0, 238 | 0.0, 239 | 1.0 240 | ], 241 | "scale": [ 242 | 1.0, 243 | 1.0, 244 | 1.0 245 | ] 246 | }"#; 247 | 248 | const TRANSFORM_SNAPSHOT_JSON: &str = r#" 249 | { 250 | "entities": { 251 | "4294967296": { 252 | "components": { 253 | "bevy_transform::components::transform::Transform": { 254 | "translation": [ 255 | 1.0, 256 | 2.0, 257 | 3.0 258 | ], 259 | "rotation": [ 260 | 0.0, 261 | 0.0, 262 | 0.0, 263 | 1.0 264 | ], 265 | "scale": [ 266 | 1.0, 267 | 1.0, 268 | 1.0 269 | ] 270 | } 271 | } 272 | } 273 | }, 274 | "resources": {} 275 | }"#; 276 | 277 | #[test] 278 | fn transform_json() { 279 | let value = Transform::from_xyz(1.0, 2.0, 3.0); 280 | 281 | let mut app = App::new(); 282 | 283 | app.register_type::(); 284 | 285 | app.world_mut().spawn(value); 286 | 287 | let registry = app.world().resource::().read(); 288 | 289 | let ser = ReflectSerializer::new(&value, ®istry); 290 | let data_erased = serde_json::to_string_pretty(&ser).unwrap(); 291 | 292 | assert_eq!(TRANSFORM_JSON, format!("\n{data_erased}")); 293 | 294 | let ser = TypedReflectSerializer::new(&value, ®istry); 295 | let data_typed = serde_json::to_string_pretty(&ser).unwrap(); 296 | 297 | assert_eq!(TRANSFORM_TYPED_JSON, format!("\n{data_typed}")); 298 | 299 | let snapshot = Snapshot::builder(app.world()) 300 | .extract_all_entities() 301 | .build(); 302 | 303 | let ser = snapshot.serializer(®istry); 304 | 305 | let output = serde_json::to_string_pretty(&ser).unwrap(); 306 | 307 | assert_eq!(TRANSFORM_SNAPSHOT_JSON, format!("\n{output}")); 308 | } 309 | -------------------------------------------------------------------------------- /tests/format.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | prelude::*, 3 | reflect::TypeRegistry, 4 | }; 5 | use bevy_save::{ 6 | prelude::*, 7 | serde::{ 8 | SnapshotDeserializer, 9 | SnapshotSerializer, 10 | }, 11 | }; 12 | use serde::{ 13 | de::DeserializeSeed, 14 | Serialize, 15 | }; 16 | 17 | #[derive(Component, Reflect, Default)] 18 | #[reflect(Component)] 19 | struct Unit; 20 | 21 | #[derive(Component, Reflect, Default)] 22 | #[reflect(Component)] 23 | struct Basic { 24 | data: u32, 25 | } 26 | 27 | #[derive(Component, Reflect, Default)] 28 | #[reflect(Component)] 29 | struct Collect { 30 | data: Vec, 31 | } 32 | 33 | #[derive(Component, Reflect, Default)] 34 | #[reflect(Component)] 35 | struct Nullable { 36 | data: Option, 37 | } 38 | 39 | #[derive(Component, Reflect, Default)] 40 | #[reflect(Component)] 41 | struct Position { 42 | x: f32, 43 | y: f32, 44 | z: f32, 45 | } 46 | 47 | fn init_app() -> (App, Vec) { 48 | let mut app = App::new(); 49 | 50 | app // 51 | .add_plugins((MinimalPlugins, SavePlugins)) 52 | .register_type::() 53 | .register_type::() 54 | .register_type::() 55 | .register_type::>() 56 | .register_type::() 57 | .register_type::>() 58 | .register_type::(); 59 | 60 | let world = app.world_mut(); 61 | 62 | let ids = vec![ 63 | world.spawn(()).id(), 64 | world 65 | .spawn(( 66 | Position { 67 | x: 0.0, 68 | y: 1.0, 69 | z: 2.0, 70 | }, 71 | Collect { 72 | data: vec![3, 4, 5], 73 | }, 74 | Unit, 75 | )) 76 | .id(), 77 | world 78 | .spawn((Basic { data: 42 }, Nullable { data: Some(77) }, Unit)) 79 | .id(), 80 | world 81 | .spawn(( 82 | Position { 83 | x: 6.0, 84 | y: 7.0, 85 | z: 8.0, 86 | }, 87 | Unit, 88 | )) 89 | .id(), 90 | world.spawn(Nullable { data: None }).id(), 91 | ]; 92 | 93 | (app, ids) 94 | } 95 | 96 | fn extract(world: &World) -> Snapshot { 97 | Snapshot::builder(world).extract_all_entities().build() 98 | } 99 | 100 | #[test] 101 | fn test_json() { 102 | fn serialize(snapshot: &Snapshot, registry: &TypeRegistry) -> String { 103 | let serializer = SnapshotSerializer { snapshot, registry }; 104 | 105 | let mut buf = Vec::new(); 106 | let format = serde_json::ser::PrettyFormatter::with_indent(b" "); 107 | let mut ser = serde_json::Serializer::with_formatter(&mut buf, format); 108 | 109 | serializer.serialize(&mut ser).unwrap(); 110 | 111 | String::from_utf8(buf).unwrap() 112 | } 113 | 114 | let (mut app, _) = init_app(); 115 | let world = app.world_mut(); 116 | 117 | let registry = world.resource::().read(); 118 | let snapshot = extract(world); 119 | 120 | let output = serialize(&snapshot, ®istry); 121 | let expected = r#"{ 122 | "entities": { 123 | "4294967296": { 124 | "components": {} 125 | }, 126 | "4294967297": { 127 | "components": { 128 | "format::Collect": { 129 | "data": [ 130 | 3, 131 | 4, 132 | 5 133 | ] 134 | }, 135 | "format::Position": { 136 | "x": 0.0, 137 | "y": 1.0, 138 | "z": 2.0 139 | }, 140 | "format::Unit": {} 141 | } 142 | }, 143 | "4294967298": { 144 | "components": { 145 | "format::Basic": { 146 | "data": 42 147 | }, 148 | "format::Nullable": { 149 | "data": 77 150 | }, 151 | "format::Unit": {} 152 | } 153 | }, 154 | "4294967299": { 155 | "components": { 156 | "format::Position": { 157 | "x": 6.0, 158 | "y": 7.0, 159 | "z": 8.0 160 | }, 161 | "format::Unit": {} 162 | } 163 | }, 164 | "4294967300": { 165 | "components": { 166 | "format::Nullable": { 167 | "data": null 168 | } 169 | } 170 | } 171 | }, 172 | "resources": {} 173 | }"#; 174 | 175 | assert_eq!(output, expected); 176 | 177 | let deserializer = SnapshotDeserializer { 178 | registry: ®istry, 179 | }; 180 | 181 | let mut de = serde_json::Deserializer::from_str(&output); 182 | 183 | let value = deserializer.deserialize(&mut de).unwrap(); 184 | 185 | let output = serialize(&value, ®istry); 186 | 187 | assert_eq!(output, expected); 188 | } 189 | 190 | #[test] 191 | fn test_mp() { 192 | fn serialize(snapshot: &Snapshot, registry: &TypeRegistry) -> Vec { 193 | let serializer = SnapshotSerializer { snapshot, registry }; 194 | 195 | let mut buf = Vec::new(); 196 | let mut ser = rmp_serde::Serializer::new(&mut buf); 197 | 198 | serializer.serialize(&mut ser).unwrap(); 199 | 200 | buf 201 | } 202 | 203 | let (mut app, _) = init_app(); 204 | let world = app.world_mut(); 205 | 206 | let registry = world.resource::().read(); 207 | let snapshot = extract(world); 208 | 209 | let output = serialize(&snapshot, ®istry); 210 | let expected = vec![ 211 | 146, 133, 207, 0, 0, 0, 1, 0, 0, 0, 0, 145, 128, 207, 0, 0, 0, 1, 0, 0, 0, 1, 145, 131, 212 | 175, 102, 111, 114, 109, 97, 116, 58, 58, 67, 111, 108, 108, 101, 99, 116, 145, 147, 3, 4, 213 | 5, 176, 102, 111, 114, 109, 97, 116, 58, 58, 80, 111, 115, 105, 116, 105, 111, 110, 147, 214 | 202, 0, 0, 0, 0, 202, 63, 128, 0, 0, 202, 64, 0, 0, 0, 172, 102, 111, 114, 109, 97, 116, 215 | 58, 58, 85, 110, 105, 116, 144, 207, 0, 0, 0, 1, 0, 0, 0, 2, 145, 131, 173, 102, 111, 114, 216 | 109, 97, 116, 58, 58, 66, 97, 115, 105, 99, 145, 42, 176, 102, 111, 114, 109, 97, 116, 58, 217 | 58, 78, 117, 108, 108, 97, 98, 108, 101, 145, 77, 172, 102, 111, 114, 109, 97, 116, 58, 58, 218 | 85, 110, 105, 116, 144, 207, 0, 0, 0, 1, 0, 0, 0, 3, 145, 130, 176, 102, 111, 114, 109, 97, 219 | 116, 58, 58, 80, 111, 115, 105, 116, 105, 111, 110, 147, 202, 64, 192, 0, 0, 202, 64, 224, 220 | 0, 0, 202, 65, 0, 0, 0, 172, 102, 111, 114, 109, 97, 116, 58, 58, 85, 110, 105, 116, 144, 221 | 207, 0, 0, 0, 1, 0, 0, 0, 4, 145, 129, 176, 102, 111, 114, 109, 97, 116, 58, 58, 78, 117, 222 | 108, 108, 97, 98, 108, 101, 145, 192, 128, 223 | ]; 224 | 225 | assert_eq!(output, expected); 226 | 227 | let deserializer = SnapshotDeserializer { 228 | registry: ®istry, 229 | }; 230 | 231 | let mut de = rmp_serde::Deserializer::new(&*output); 232 | 233 | let value = deserializer.deserialize(&mut de).unwrap(); 234 | 235 | let output = serialize(&value, ®istry); 236 | 237 | assert_eq!(output, expected); 238 | } 239 | -------------------------------------------------------------------------------- /tests/generations.rs: -------------------------------------------------------------------------------- 1 | // TODO 2 | /* 3 | use std::collections::HashMap; 4 | 5 | use bevy::prelude::*; 6 | use bevy_save::prelude::*; 7 | 8 | #[derive(Component)] 9 | struct Selectable; 10 | 11 | #[test] 12 | fn issue21() { 13 | let mut app = App::new(); 14 | 15 | app.add_plugins(MinimalPlugins); 16 | app.add_plugins(SavePlugins); 17 | 18 | let world = &mut app.world; 19 | 20 | world.spawn((Name::from("ABC"), Selectable)); 21 | world.spawn((Name::from("DEF"), Selectable)); 22 | world.spawn(Name::from("GHI")); 23 | world.spawn(Name::from("JKL")); 24 | world.spawn(()); 25 | world.spawn(Selectable); 26 | 27 | assert!(world.despawn(Entity::from_bits(5))); // 5v0 28 | 29 | world.spawn(Selectable); 30 | 31 | assert!(world.despawn(Entity::from_bits((1 << 32) + 5))); // 5v1 32 | 33 | world.spawn(Selectable); 34 | 35 | let entities = world.iter_entities().map(|e| (e.id(), e.get::())).collect::>(); 36 | 37 | assert_eq!(entities.get(&Entity::from_raw(0)), Some(&Some(&Name::from("ABC")))); 38 | assert_eq!(entities.get(&Entity::from_raw(1)), Some(&Some(&Name::from("DEF")))); 39 | assert_eq!(entities.get(&Entity::from_raw(2)), Some(&Some(&Name::from("GHI")))); 40 | assert_eq!(entities.get(&Entity::from_raw(3)), Some(&Some(&Name::from("JKL")))); 41 | assert_eq!(entities.get(&Entity::from_raw(4)), Some(&None)); 42 | assert_eq!(entities.get(&Entity::from_bits((2 << 32) + 5)), Some(&None)); 43 | 44 | drop(entities); 45 | 46 | let snapshot = Snapshot::builder(world) 47 | .extract_all_entities() 48 | .build(); 49 | 50 | let filter = ::new::>(); 51 | 52 | snapshot 53 | .applier(world) 54 | .despawn(bevy_save::DespawnMode::MissingWith(Box::new(filter))) 55 | .apply() 56 | .unwrap(); 57 | 58 | let entities = world.iter_entities().map(|e| (e.id(), e.get::())).collect::>(); 59 | 60 | assert_eq!(entities.get(&Entity::from_raw(0)), Some(&Some(&Name::from("ABC")))); 61 | assert_eq!(entities.get(&Entity::from_raw(1)), Some(&Some(&Name::from("DEF")))); 62 | assert_eq!(entities.get(&Entity::from_raw(2)), Some(&Some(&Name::from("GHI")))); 63 | assert_eq!(entities.get(&Entity::from_raw(3)), Some(&Some(&Name::from("JKL")))); 64 | assert_eq!(entities.get(&Entity::from_raw(4)), Some(&None)); 65 | assert_eq!(entities.get(&Entity::from_bits((2 << 32) + 5)), Some(&None)); 66 | } 67 | */ 68 | -------------------------------------------------------------------------------- /tests/overwrite.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_save::prelude::*; 3 | 4 | #[derive(Component, Reflect, Default, Debug, Clone, PartialEq, Eq)] 5 | #[reflect(Component)] 6 | struct Collect { 7 | data: Vec, 8 | } 9 | 10 | #[test] 11 | fn test_collect() { 12 | let mut app = App::new(); 13 | 14 | app.add_plugins(MinimalPlugins); 15 | app.add_plugins(SavePlugins); 16 | 17 | app.register_type::(); 18 | app.register_type::>(); 19 | 20 | let world = app.world_mut(); 21 | 22 | let entity = world 23 | .spawn_empty() 24 | .insert(Collect { data: Vec::new() }) 25 | .id(); 26 | 27 | world 28 | .entity_mut(entity) 29 | .get_mut::() 30 | .unwrap() 31 | .data 32 | .push(1); 33 | 34 | assert_eq!( 35 | world.entity(entity).get::(), 36 | Some(&Collect { data: vec![1] }) 37 | ); 38 | assert_eq!(world.iter_entities().count(), 1); 39 | 40 | let snapshot = Snapshot::builder(world).extract_entity(entity).build(); 41 | 42 | world 43 | .entity_mut(entity) 44 | .get_mut::() 45 | .unwrap() 46 | .data 47 | .push(2); 48 | 49 | assert_eq!( 50 | world.entity(entity).get::(), 51 | Some(&Collect { data: vec![1, 2] }) 52 | ); 53 | 54 | snapshot 55 | .applier(world) 56 | .entity_map(&mut [(entity, entity)].into_iter().collect()) 57 | .apply() 58 | .unwrap(); 59 | 60 | assert_eq!( 61 | world.entity(entity).get::(), 62 | Some(&Collect { data: vec![1] }) 63 | ); 64 | assert_eq!(world.iter_entities().count(), 1); 65 | } 66 | 67 | #[derive(Component, Reflect, Default, Debug, Clone, PartialEq, Eq)] 68 | #[reflect(Component)] 69 | struct Basic { 70 | data: u32, 71 | } 72 | 73 | #[test] 74 | fn test_basic() { 75 | let mut app = App::new(); 76 | 77 | app.add_plugins(MinimalPlugins); 78 | app.add_plugins(SavePlugins); 79 | 80 | app.register_type::(); 81 | 82 | let world = app.world_mut(); 83 | 84 | let entity = world.spawn_empty().insert(Basic { data: 0 }).id(); 85 | 86 | assert_eq!( 87 | world.entity(entity).get::(), 88 | Some(&Basic { data: 0 }) 89 | ); 90 | assert_eq!(world.iter_entities().count(), 1); 91 | 92 | world.entity_mut(entity).get_mut::().unwrap().data = 1; 93 | 94 | let snapshot = Snapshot::builder(world).extract_entity(entity).build(); 95 | 96 | world.entity_mut(entity).get_mut::().unwrap().data = 2; 97 | 98 | assert_eq!( 99 | world.entity(entity).get::(), 100 | Some(&Basic { data: 2 }) 101 | ); 102 | 103 | snapshot 104 | .applier(world) 105 | .entity_map(&mut [(entity, entity)].into_iter().collect()) 106 | .apply() 107 | .unwrap(); 108 | 109 | assert_eq!( 110 | world.entity(entity).get::(), 111 | Some(&Basic { data: 1 }) 112 | ); 113 | assert_eq!(world.iter_entities().count(), 1); 114 | } 115 | --------------------------------------------------------------------------------