io/
save_project.rs

1//! Contains the events for saving projects and their handling systems.
2use crate::document::Document;
3use anyhow::Context;
4use bevy::prelude::{
5    BevyError, Children, Entity, Event, EventReader, Name, Query, Transform, With,
6};
7use bevy::prelude::{Commands, default};
8use data::{Element, Layer, Level, Project};
9use serialization::serialize_to;
10use std::{fs::File, path::PathBuf};
11use utils::{AsyncComponent, report_progress};
12
13/// When this event is sent, the associated `project` will be fetched and saved.
14/// As a reaction to this event, a system will build a [`bevy::prelude::Query`] that attempts to
15/// fetch all [`data::Project`]'s [`data::Level`]s (and their descendant [`data::Layer`]s along
16/// all their descendants) and then (attempts) to persist them to disk.
17///
18/// The [`data::Project`] fetched is queried by the [`SaveProjectEvent::project`] [`Entity`],
19/// the user is responsible for only emitting "valid" entities, as this crate will assume they are,
20/// and according to Bevy's own documentation, this can lead to undefined behaviour if not respected.
21#[derive(Event, Debug)]
22pub struct SaveProjectEvent {
23    /// The [`Entity`] of the [`data::Project`] to save.
24    pub(crate) project: Entity,
25    /// The output path of the savefile that will be created.
26    pub(crate) output: PathBuf,
27}
28
29/// This event indicates that the work of a [`SaveProjectEvent`] has completed.
30#[derive(Event, Debug)]
31pub struct SaveProjectCompleteEvent {
32    /// The [`Entity`] of the [`data::Project`] that was saved.
33    pub project: Entity,
34    /// The output path of the savefile that was created.
35    #[allow(
36        dead_code,
37        reason = "Temporarily until editor and status reporting is implemented"
38    )]
39    pub output: PathBuf, // TODO: remove dead_code
40}
41
42impl SaveProjectEvent {
43    /// Generate a new [`SaveProjectEvent`] that can be dispatched.
44    #[must_use = "This event does nothing unless you dispatch it"]
45    pub fn new(project: Entity, output: PathBuf) -> Self {
46        Self { project, output }
47    }
48}
49
50/// Bevy system that handles [`SaveProjectEvent`] events.
51#[utils::bevy_system]
52pub fn handle_save_project(
53    mut commands: Commands,
54    mut events: EventReader<SaveProjectEvent>,
55    project_query: Query<(&Name, &Children), With<Project>>,
56    level_query: Query<(&Level, &Name, &Children)>,
57    layer_query: Query<(&Layer, &Name, &Transform, &Children)>,
58    object_query: Query<(&Element, &Name, &Transform)>,
59) -> Result<(), BevyError> {
60    let Some(event) = events.read().next() else {
61        return Ok(());
62    };
63
64    let project = project_query.get(event.project)?;
65
66    let entity = event.project;
67    let output = event.output.clone();
68    let document = Document::new(project, level_query, layer_query, object_query);
69    commands.spawn(AsyncComponent::new_io(
70        async move |sender| {
71            let file = File::create(output.clone()).with_context(|| {
72                format!("Failed to open {} for writing savefile", output.display())
73            })?;
74            serialize_to(&document, &default(), file)?;
75
76            // Report completion
77            report_progress(
78                &sender,
79                SaveProjectCompleteEvent {
80                    project: entity,
81                    output,
82                },
83            )?;
84            Ok(())
85        },
86        |_, _| {
87            // TODO: handle errors.
88        },
89    ));
90
91    Ok(())
92}