Skip to main content

bevy_convars/
save.rs

1//! Provides support for saving CVars to a TOML config file.
2
3use bevy_ecs::{
4    change_detection::MaybeLocation,
5    component::Tick,
6    reflect::{AppTypeRegistry, ReflectResource},
7    world::{Ref, World},
8};
9use bevy_reflect::{Reflect, ReflectSerialize};
10use serde::Serialize;
11use toml_edit::{DocumentMut, Item, Table, ser::ValueSerializer};
12
13use crate::{
14    CVarError, CVarFlags, CVarManagement,
15    reflect::{CVarMeta, ReflectCVar},
16};
17
18#[cfg(test)]
19mod tests;
20
21/// Provides a context for mutating a TOML document to save CVars to it.
22///
23/// # Example
24/// ```no_run
25/// # use bevy_ecs::prelude::*;
26/// # use bevy_convars::save::*;
27/// # let world = World::new();
28/// let mut context = CVarSaveContext::blank();
29///
30/// // Let's save the world config to this.
31///
32/// context.save_world(&world);
33///
34/// // And serialize out the results so we can save it.
35/// let file_contents = context.to_string();
36/// ```
37pub struct CVarSaveContext(DocumentMut);
38
39impl CVarSaveContext {
40    /// Creates a new context with an empty document.
41    pub fn blank() -> Self {
42        Self(DocumentMut::new())
43    }
44
45    /// Creates a new context with an existing document.
46    pub fn from_document(doc: DocumentMut) -> Self {
47        Self(doc)
48    }
49
50    /// Returns the document used from the context, destroying the context.
51    pub fn return_document(self) -> DocumentMut {
52        self.0
53    }
54
55    fn get_cvar_entry(&mut self, path: &str) -> Result<toml_edit::Entry<'_>, CVarError> {
56        let mut sections = path.split('.');
57        let section_count = sections.clone().count();
58        let leading_sections = sections.clone().take(section_count - 1);
59        let final_section = sections.next_back().unwrap();
60
61        let mut cur_table = self.0.as_table_mut();
62
63        for section in leading_sections {
64            cur_table = cur_table
65                .entry(section)
66                .or_insert(toml_edit::Item::Table(Table::new()))
67                .as_table_mut()
68                .ok_or(CVarError::MalformedConfigDuringWrite("Expected a table."))?;
69        }
70
71        Ok(cur_table.entry(final_section))
72    }
73
74    /// Saves an individual CVar to the document.
75    fn save_cvar_inner(&mut self, path: &str, value: &impl Serialize) -> Result<(), CVarError> {
76        let entry = self.get_cvar_entry(path)?;
77
78        *entry.or_insert(toml_edit::Item::None) =
79            Item::Value(value.serialize(ValueSerializer::new())?);
80
81        Ok(())
82    }
83
84    fn save_cvar_inner_erased(
85        &mut self,
86        path: &str,
87        value: &bevy_reflect::serde::Serializable,
88    ) -> Result<(), CVarError> {
89        let entry = self.get_cvar_entry(path)?;
90
91        *entry.or_insert(toml_edit::Item::None) =
92            Item::Value(value.serialize(ValueSerializer::new())?);
93
94        Ok(())
95    }
96
97    /// Manually save an individual CVar to the document.
98    /// # Remarks
99    /// This does not check for the presence of [CVarFlags::SAVED], and as such can be used to specially handle some CVars.
100    pub fn save_cvar<T: CVarMeta>(&mut self, cvar: &T) -> Result<(), CVarError>
101    where
102        T::Inner: Serialize,
103    {
104        self.save_cvar_inner(T::CVAR_PATH, &**cvar)
105    }
106
107    /// Manually save an individual CVar to the document, from the world.
108    /// # Remarks
109    /// This does not check for the presence of [CVarFlags::SAVED], and as such can be used to specially handle some CVars.
110    pub fn save_cvar_from_world<T: CVarMeta>(&mut self, world: &World) -> Result<(), CVarError>
111    where
112        T::Inner: Serialize,
113    {
114        self.save_cvar_inner(T::CVAR_PATH, &**world.resource::<T>())
115    }
116
117    /// Saves a world's CVars to the document.
118    /// # Remarks
119    /// This obeys [CVarFlags::SAVED] and will not attempt to save CVars without it.
120    pub fn save_world(&mut self, world: &World) -> Result<(), CVarError> {
121        let management: &CVarManagement = world.resource::<CVarManagement>();
122        let registry = world.resource::<AppTypeRegistry>().read();
123        let types = management.iterate_cvar_types();
124
125        for reg in types {
126            let cvar = reg.data::<ReflectCVar>().expect("Impossible.");
127
128            if !cvar.flags().contains(CVarFlags::SAVED) {
129                continue;
130            }
131
132            let Some(serialize) = registry.get_type_data::<ReflectSerialize>(cvar.inner_type())
133            else {
134                panic!(
135                    "Can't save a saveable cvar due to lack of ReflectSerialize implementation. CVar in question is {}",
136                    cvar.cvar_path()
137                );
138            };
139
140            let resource = reg.data::<ReflectResource>().expect("Impossible.");
141
142            let cvar_id = management.tree.get(cvar.cvar_path()).unwrap();
143
144            let change_data = world.get_resource_change_ticks_by_id(cvar_id).unwrap();
145
146            let caller = MaybeLocation::caller();
147
148            let res = resource.reflect(world)?;
149            let resource: Ref<dyn Reflect> = {
150                // Jank, Bevy is missing an API for this..
151
152                Ref::new(
153                    res,
154                    &change_data.added,
155                    &change_data.changed,
156                    Tick::new(0),
157                    Tick::new(0),
158                    caller.as_ref(),
159                )
160            };
161
162            if cvar.is_default_value(resource) {
163                continue;
164            }
165
166            self.save_cvar_inner_erased(
167                cvar.cvar_path(),
168                &serialize.get_serializable(
169                    cvar.reflect_inner(res.as_partial_reflect())?
170                        .try_as_reflect()
171                        .unwrap(),
172                ),
173            )?;
174        }
175
176        Ok(())
177    }
178}
179
180#[allow(clippy::to_string_trait_impl)]
181impl ToString for CVarSaveContext {
182    fn to_string(&self) -> String {
183        self.0.to_string()
184    }
185}