Skip to main content

bevy_poly_level/
save.rs

1use bevy::asset::AssetLoader;
2use bevy::{ecs::system::SystemId, prelude::*};
3use bevy_prototype_lyon::shapes;
4use futures_lite::AsyncReadExt;
5use serde::*;
6use serde_json::{self, from_slice};
7use std::collections::hash_map::DefaultHasher;
8use std::fs::OpenOptions;
9use std::hash::{Hash, Hasher};
10use std::io::Read;
11use std::io::Write;
12use std::path::PathBuf;
13
14use crate::core::{EditorObject, EditorWorldObject};
15use crate::core::{LevelEditProperties, LEVEL_ENT_Z_ORDER};
16use crate::geometry::{GeometryBundle, GeometryComponent, Polygon};
17use crate::map_entity::{MapEntity, MapEntityBundle, MapEntityListing, MapEntityListings};
18use crate::user_data::{
19    HashIdLink, MapEntityUserDataConverter, TypedUserData, TypedUserDataFiller, UserData,
20    UserDataComponent, UserDataListing, UserDataListingHandle, UserDataListings,
21};
22
23// Type Definitions: -------------------------------------------------------------------------------
24
25pub struct SavePlugin;
26
27pub struct LevelAssetLoader;
28
29#[derive(Resource)]
30pub struct SaveSystemIds {
31    /// the save file syste id, run this system to save the current level editor state to the
32    /// current file path
33    pub save_file_id: SystemId,
34
35    /// the load file id, run this system to load the current file path and build the level editor
36    /// state from it. the current level editor state will remain, clear_level system should
37    /// probably be run before this system, unless you are trying to load multiple levels additively
38    pub load_file_id: SystemId,
39
40    /// clear the current level editor state, remove all level related entities
41    pub clear_level_id: SystemId,
42}
43
44#[derive(Asset, TypePath, Serialize, Deserialize, Clone)]
45pub struct LevelData {
46    pub user_data: Vec<UserDataListingData>,
47    pub poly_data: PolygonCollectionData,
48    pub entity_data: EntityCollectionData,
49}
50
51#[derive(TypePath, Serialize, Deserialize, Clone)]
52pub struct PolygonCollectionData {
53    pub polys: Vec<PolygonData>,
54}
55
56#[derive(TypePath, Serialize, Deserialize, Clone)]
57pub struct PolygonData {
58    pub translation: Vec2,
59    pub verts: Vec<Vec2>,
60    pub user_data: Option<CustomData>,
61    pub id_hash: u64,
62}
63
64#[derive(TypePath, Serialize, Deserialize, Default, Clone)]
65pub struct EntityCollectionData {
66    pub listings: Vec<MapEntityTypeListingData>,
67    pub entities: Vec<EntityData>,
68}
69
70#[derive(TypePath, Serialize, Deserialize, Clone)]
71pub struct EntityData {
72    pub translation: Vec2,
73    pub entity_type_id: usize,
74    pub user_data: Option<CustomData>,
75    pub id_hash: u64,
76}
77
78#[derive(TypePath, Serialize, Deserialize, Clone)]
79pub struct CustomData {
80    pub listing_id: Option<usize>,
81    pub data: UserData,
82}
83
84#[derive(Reflect, Serialize, Deserialize, Clone)]
85pub struct MapEntityTypeListingData {
86    pub label: String,
87    pub icon_color: Color,
88    pub icon_image: Option<PathBuf>,
89    pub scale: Vec2,
90    pub id: usize,
91    pub usr_data: Option<usize>,
92}
93
94#[derive(TypePath, Serialize, Deserialize, Clone)]
95pub struct UserDataListingData {
96    pub label: String,
97    pub data: TypedUserData,
98}
99
100// Implementations: --------------------------------------------------------------------------------
101
102impl Plugin for SavePlugin {
103    fn build(&self, app: &mut App) {
104        let sys_ids = SaveSystemIds {
105            save_file_id: app.world.register_system(save_file),
106            load_file_id: app.world.register_system(load_file),
107            clear_level_id: app.world.register_system(clear_level),
108        };
109        app.register_asset_loader(LevelAssetLoader)
110            .init_asset::<LevelData>()
111            .insert_resource(sys_ids);
112    }
113}
114
115impl AssetLoader for LevelAssetLoader {
116    type Asset = LevelData;
117    type Settings = ();
118    type Error = std::io::Error;
119    fn load<'a>(
120        &'a self,
121        reader: &'a mut bevy::asset::io::Reader,
122        _settings: &'a Self::Settings,
123        _load_context: &'a mut bevy::asset::LoadContext,
124    ) -> bevy::utils::BoxedFuture<'a, Result<Self::Asset, Self::Error>> {
125        Box::pin(async move {
126            let mut bytes = Vec::new();
127            reader.read_to_end(&mut bytes).await?;
128            let asset = from_slice::<LevelData>(&bytes).expect("unable to parse level");
129            Ok(asset)
130        })
131    }
132    fn extensions(&self) -> &[&str] {
133        &["lvl.json"]
134    }
135}
136
137impl PolygonData {
138    fn from_polygon(
139        translation: Vec2,
140        poly: &Polygon,
141        usr_dat: Option<&UserDataComponent>,
142        ent: Entity,
143    ) -> Self {
144        let verts = poly.verts().clone();
145        let mut hasher = DefaultHasher::new();
146        ent.hash(&mut hasher);
147        let usr_dat = if let Some(dat) = usr_dat {
148            Some(CustomData {
149                listing_id: dat.listing.get_index(),
150                data: dat.data.to_map_ent_usr_dat(),
151            })
152        } else {
153            None
154        };
155        Self {
156            translation,
157            verts,
158            user_data: usr_dat.clone(),
159            id_hash: hasher.finish(),
160        }
161    }
162}
163
164impl MapEntityTypeListingData {
165    pub fn from_map_ent_listing(listing: &MapEntityListing) -> Self {
166        Self {
167            label: listing.label.clone(),
168            id: listing.id.clone(),
169            scale: listing.scale.clone(),
170            icon_color: listing.icon.color.clone(),
171            icon_image: if listing.icon_image == Handle::<Image>::default() {
172                None
173            } else {
174                todo!("implement image asset path")
175            },
176            usr_data: listing.user_data.get_index(),
177        }
178    }
179
180    pub fn to_map_ent_listing(&self) -> MapEntityListing {
181        MapEntityListing {
182            label: self.label.clone(),
183            id: self.id.clone(),
184            scale: self.scale.clone(),
185            icon: Sprite {
186                color: self.icon_color.clone(),
187                ..Default::default()
188            },
189            icon_image: default(),
190            user_data: if let Some(dat_ind) = self.usr_data {
191                UserDataListingHandle::from_index(dat_ind)
192            } else {
193                UserDataListingHandle::INVALID
194            },
195        }
196    }
197}
198
199impl UserDataListingData {
200    pub fn from_listing(listing: &UserDataListing) -> Self {
201        Self {
202            label: listing.label.clone(),
203            data: listing.data.clone(),
204        }
205    }
206
207    pub fn to_listing(&self) -> UserDataListing {
208        UserDataListing {
209            label: self.label.clone(),
210            data: self.data.clone(),
211        }
212    }
213}
214
215// System Definitions: -----------------------------------------------------------------------------
216
217/// run this system to save the level data to the current specified file path
218fn save_file(
219    editor_props: Res<LevelEditProperties>,
220    ent_listings: Res<MapEntityListings>,
221    usr_dat_listings: Res<UserDataListings>,
222    geom_query: Query<(
223        Entity,
224        &GlobalTransform,
225        &GeometryComponent,
226        Option<&UserDataComponent>,
227    )>,
228    entity_query: Query<(
229        Entity,
230        &GlobalTransform,
231        &MapEntity,
232        Option<&UserDataComponent>,
233    )>,
234) {
235    if editor_props.cur_file_path.is_none() {
236        warn!("File path not specified");
237        return;
238    }
239
240    // attempt to create or open the file at the specified path
241    let file_path = editor_props.cur_file_path.as_ref().unwrap();
242    let file = OpenOptions::new()
243        .write(true)
244        .truncate(true)
245        .create(true)
246        .open(file_path);
247
248    if let Ok(mut file) = file {
249        // attempt to build and serialize the level
250        let level_data =
251            marshal_level_data(&ent_listings, &usr_dat_listings, &geom_query, &entity_query);
252        let serialized_data = serde_json::to_string_pretty(&level_data);
253
254        if let Ok(data) = serialized_data {
255            // file was successfully created/opened and data was successfully serialized
256            let write = file.write_all(data.as_bytes());
257
258            // file was successfully written
259            if write.is_ok() {
260                println!("File Saved!");
261
262            // write error
263            } else if let Err(err) = write {
264                warn!("{}", err);
265            }
266
267        // serialization error
268        } else if let Err(err) = serialized_data {
269            warn!("{}", err);
270        }
271    // file error
272    } else if let Err(err) = file {
273        warn!("{}", err);
274    }
275}
276
277/// run this system to load the current file path and build the level editor
278/// state from it. the current level editor state will remain, clear_level system should
279/// probably be run before this system, unless you are trying to load multiple levels additively
280fn load_file(mut commands: Commands, editor_props: Res<LevelEditProperties>) {
281    if editor_props.cur_file_path.is_none() {
282        warn!("File path not specified");
283        return;
284    }
285
286    let file_path = editor_props.cur_file_path.as_ref().unwrap();
287    let file = OpenOptions::new().read(true).open(file_path);
288
289    if let Ok(mut file) = file {
290        // attempt to read the data
291        let mut file_data = Vec::<u8>::new();
292        let success = file.read_to_end(&mut file_data);
293
294        if success.is_ok() {
295            // attemt to deserialize level data from the json file data and build the level from it
296            let level_data = serde_json::from_slice::<LevelData>(file_data.as_slice());
297            if let Ok(level_data) = level_data {
298                build_level_from_data(&mut commands, &level_data);
299
300            // deserialization error
301            } else if let Err(err) = level_data {
302                warn!("{}", err);
303            }
304
305        // read error
306        } else {
307            warn!("{}", success.unwrap_err());
308        }
309
310    // file error
311    } else if let Err(err) = file {
312        warn!("{}", err);
313    }
314}
315
316/// load the level file at the specified data path and attempt parse level data from it. if the
317/// file is successfully loaded and parsed, the level data is returned wrapped in an Option.
318pub fn load_file_at(file_path: PathBuf) -> Option<LevelData> {
319    // attempt to open the file
320    let file = OpenOptions::new().read(true).open(file_path);
321    if let Err(err) = file {
322        warn!("{}", err);
323        return None;
324    }
325    let mut file = file.unwrap();
326
327    // attempt read the data
328    let mut file_data = Vec::<u8>::new();
329    if let Err(err) = file.read_to_end(&mut file_data) {
330        warn!("{}", err);
331        return None;
332    }
333
334    // attemt to deserialize data from the json file data and build level data from it
335    let data = serde_json::from_slice::<LevelData>(file_data.as_slice());
336    if let Err(err) = data {
337        warn!("{}", err);
338        return None;
339    }
340
341    // return the level data if success
342    Some(data.unwrap())
343}
344
345/// clears all level related entities from the level editor, so that you have an empty level
346fn clear_level(
347    mut commands: Commands,
348    query: Query<Entity, Or<(With<GeometryComponent>, With<MapEntity>)>>,
349) {
350    // destroy all level objects
351    for ent in &query {
352        commands.entity(ent).despawn();
353    }
354}
355
356// Utility: ----------------------------------------------------------------------------------------
357
358/// creates the level data structure from the supplied data
359pub fn marshal_level_data(
360    ent_listings: &Res<MapEntityListings>,
361    usr_dat_listings: &Res<UserDataListings>,
362    geom_query: &Query<(
363        Entity,
364        &GlobalTransform,
365        &GeometryComponent,
366        Option<&UserDataComponent>,
367    )>,
368    entity_query: &Query<(
369        Entity,
370        &GlobalTransform,
371        &MapEntity,
372        Option<&UserDataComponent>,
373    )>,
374) -> LevelData {
375    LevelData {
376        user_data: marshal_user_data_listings(&usr_dat_listings),
377        poly_data: marshal_poly_collection_data(&geom_query),
378        entity_data: marshal_entity_collection_data(&ent_listings, &entity_query),
379    }
380}
381
382/// creates the polygon data structure form the supplied data
383pub fn marshal_poly_collection_data(
384    geom_query: &Query<(
385        Entity,
386        &GlobalTransform,
387        &GeometryComponent,
388        Option<&UserDataComponent>,
389    )>,
390) -> PolygonCollectionData {
391    let mut polys = Vec::<PolygonData>::new();
392    for (ent, glob_trans, poly, usr_dat) in geom_query {
393        polys.push(marshal_poly_data(glob_trans, poly, usr_dat, ent));
394    }
395    PolygonCollectionData { polys }
396}
397
398/// create a polygon data structure from a geometry component
399pub fn marshal_poly_data(
400    glob_trans: &GlobalTransform,
401    geom: &GeometryComponent,
402    usr_dat: Option<&UserDataComponent>,
403    ent: Entity,
404) -> PolygonData {
405    PolygonData::from_polygon(
406        glob_trans.translation().truncate(),
407        geom.poly(),
408        usr_dat,
409        ent,
410    )
411}
412
413/// create entity collection structure from world data
414pub fn marshal_entity_collection_data(
415    ent_listings: &Res<MapEntityListings>,
416    ent_query: &Query<(
417        Entity,
418        &GlobalTransform,
419        &MapEntity,
420        Option<&UserDataComponent>,
421    )>,
422) -> EntityCollectionData {
423    let mut ents = Vec::<EntityData>::new();
424    for (ent, glob_trans, ent_dat, usr_dat) in ent_query {
425        ents.push(marshal_entity_data(glob_trans, ent_dat, usr_dat, ent));
426    }
427    EntityCollectionData {
428        listings: marshal_entity_listing_data(ent_listings),
429        entities: ents,
430    }
431}
432
433/// create entity listing data from world resources
434pub fn marshal_entity_listing_data(
435    ent_listings: &Res<MapEntityListings>,
436) -> Vec<MapEntityTypeListingData> {
437    let mut listing_data = Vec::<MapEntityTypeListingData>::new();
438    for listing in &ent_listings.entity_types {
439        listing_data.push(MapEntityTypeListingData::from_map_ent_listing(listing));
440    }
441    listing_data
442}
443
444/// create data about user data listings from world resources
445pub fn marshal_user_data_listings(
446    usr_dat_listings: &Res<UserDataListings>,
447) -> Vec<UserDataListingData> {
448    let mut listing_data = Vec::<UserDataListingData>::new();
449    for listing in &usr_dat_listings.data_listings {
450        listing_data.push(UserDataListingData::from_listing(listing));
451    }
452    listing_data
453}
454
455/// create entity data structure from world entity components
456pub fn marshal_entity_data(
457    glob_trans: &GlobalTransform,
458    entity: &MapEntity,
459    user_data: Option<&UserDataComponent>,
460    ent: Entity,
461) -> EntityData {
462    let id = entity.listing_id;
463    let mut hasher = DefaultHasher::new();
464    ent.hash(&mut hasher);
465    EntityData {
466        entity_type_id: id,
467        translation: glob_trans.translation().truncate(),
468        user_data: if let Some(typed_data) = user_data {
469            Some(CustomData {
470                listing_id: typed_data.listing.get_index(),
471                data: typed_data.data.to_map_ent_usr_dat(),
472            })
473        } else {
474            None
475        },
476        id_hash: hasher.finish(),
477    }
478}
479
480/// builds a complete level editor state from a supplied level data structure
481pub fn build_level_from_data(commands: &mut Commands, data: &LevelData) {
482    build_usr_dat_from_data(commands, &data.user_data);
483    build_geometries_from_data(commands, &data.poly_data);
484    build_entities_from_data(commands, &data.entity_data);
485}
486
487/// builds the level geometry from the supplied polygon collection data
488pub fn build_geometries_from_data(commands: &mut Commands, data: &PolygonCollectionData) {
489    for poly in &data.polys {
490        build_geometry_from_data(commands, poly);
491    }
492}
493
494/// build an entity with a geometry bundle based on the supplied polygon data
495pub fn build_geometry_from_data(commands: &mut Commands, data: &PolygonData) -> Entity {
496    let shape = shapes::Polygon {
497        closed: true,
498        points: data.verts.clone(),
499    };
500    let mut ent_cmd = commands.spawn((
501        EditorObject,
502        EditorWorldObject,
503        GeometryBundle::from_shape(data.translation.extend(0.0), shape),
504        HashIdLink {
505            hash_id: data.id_hash,
506        },
507    ));
508    if let Some(usr_dat) = &data.user_data {
509        if let Some(listing_index) = usr_dat.listing_id {
510            ent_cmd.insert((
511                UserDataComponent {
512                    listing: UserDataListingHandle::from_index(listing_index),
513                    data: default(),
514                },
515                TypedUserDataFiller {
516                    data_untyped: usr_dat.data.clone(),
517                },
518            ));
519        }
520    }
521    ent_cmd.id()
522}
523
524/// build user listings resource from data
525pub fn build_usr_dat_from_data(commands: &mut Commands, data: &Vec<UserDataListingData>) {
526    let mut listings: Vec<UserDataListing> = Vec::new();
527    for list_dat in data {
528        listings.push(list_dat.to_listing());
529    }
530    commands.insert_resource(UserDataListings {
531        data_listings: listings,
532    });
533}
534
535/// build level entities and components into world from data
536pub fn build_entities_from_data(commands: &mut Commands, data: &EntityCollectionData) {
537    // build listings
538    let mut listings: Vec<MapEntityListing> = Vec::new();
539    for listing_dat in &data.listings {
540        listings.push(listing_dat.to_map_ent_listing());
541    }
542    // build entities
543    for ent_dat in &data.entities {
544        build_entity_from_data(commands, &ent_dat, &listings[ent_dat.entity_type_id]);
545    }
546    // insert listings
547    commands.insert_resource(MapEntityListings {
548        entity_types: listings,
549    });
550}
551
552/// build level entity and bundle into world from supplied entity data and listing
553pub fn build_entity_from_data(
554    commands: &mut Commands,
555    data: &EntityData,
556    listing: &MapEntityListing,
557) -> Entity {
558    let mut ent_cmd = commands.spawn((
559        MapEntityBundle {
560            name: Name::new(listing.label.clone()),
561            level_ent_dat: MapEntity {
562                listing_id: data.entity_type_id,
563            },
564            sprite_bundle: SpriteBundle {
565                sprite: listing.icon.clone(),
566                texture: listing.icon_image.clone(),
567                transform: Transform {
568                    translation: data.translation.extend(LEVEL_ENT_Z_ORDER),
569                    scale: listing.scale.extend(1.0),
570                    ..Default::default()
571                },
572                ..Default::default()
573            },
574            ..Default::default()
575        },
576        HashIdLink {
577            hash_id: data.id_hash,
578        },
579    ));
580    if listing.user_data.is_valid() {
581        if let Some(usr_dat) = &data.user_data {
582            ent_cmd.insert((
583                UserDataComponent {
584                    listing: listing.user_data.clone(),
585                    data: default(),
586                },
587                TypedUserDataFiller {
588                    data_untyped: usr_dat.data.clone(),
589                },
590            ));
591        }
592    }
593    ent_cmd.id()
594}