use bevy_ecs::{
change_detection::MaybeLocation,
component::Tick,
reflect::{AppTypeRegistry, ReflectResource},
world::{Ref, World},
};
use bevy_reflect::{Reflect, ReflectSerialize};
use serde::Serialize;
use toml_edit::{DocumentMut, Item, Table, ser::ValueSerializer};
use crate::{
CVarError, CVarFlags, CVarManagement,
reflect::{CVarMeta, ReflectCVar},
};
#[cfg(test)]
mod tests;
pub struct CVarSaveContext(DocumentMut);
impl CVarSaveContext {
pub fn blank() -> Self {
Self(DocumentMut::new())
}
pub fn from_document(doc: DocumentMut) -> Self {
Self(doc)
}
pub fn return_document(self) -> DocumentMut {
self.0
}
fn get_cvar_entry(&mut self, path: &str) -> Result<toml_edit::Entry<'_>, CVarError> {
let sections = path.split('.');
let section_count = sections.clone().count();
let leading_sections = sections.clone().take(section_count - 1);
let final_section = sections.last().unwrap();
let mut cur_table = self.0.as_table_mut();
for section in leading_sections {
cur_table = cur_table
.entry(section)
.or_insert(toml_edit::Item::Table(Table::new()))
.as_table_mut()
.ok_or(CVarError::MalformedConfigDuringWrite("Expected a table."))?;
}
Ok(cur_table.entry(final_section))
}
fn save_cvar_inner(&mut self, path: &str, value: &impl Serialize) -> Result<(), CVarError> {
let entry = self.get_cvar_entry(path)?;
*entry.or_insert(toml_edit::Item::None) =
Item::Value(value.serialize(ValueSerializer::new())?);
Ok(())
}
fn save_cvar_inner_erased(
&mut self,
path: &str,
value: &bevy_reflect::serde::Serializable,
) -> Result<(), CVarError> {
let entry = self.get_cvar_entry(path)?;
*entry.or_insert(toml_edit::Item::None) =
Item::Value(value.serialize(ValueSerializer::new())?);
Ok(())
}
pub fn save_cvar<T: CVarMeta>(&mut self, cvar: &T) -> Result<(), CVarError>
where
T::Inner: Serialize,
{
self.save_cvar_inner(T::CVAR_PATH, &**cvar)
}
pub fn save_cvar_from_world<T: CVarMeta>(&mut self, world: &World) -> Result<(), CVarError>
where
T::Inner: Serialize,
{
self.save_cvar_inner(T::CVAR_PATH, &**world.resource::<T>())
}
pub fn save_world(&mut self, world: &World) -> Result<(), CVarError> {
let management: &CVarManagement = world.resource::<CVarManagement>();
let registry = world.resource::<AppTypeRegistry>().read();
let types = management.iterate_cvar_types();
for reg in types {
let cvar = reg.data::<ReflectCVar>().expect("Impossible.");
if !cvar.flags().contains(CVarFlags::SAVED) {
continue;
}
let Some(serialize) = registry.get_type_data::<ReflectSerialize>(cvar.inner_type())
else {
panic!(
"Can't save a saveable cvar due to lack of ReflectSerialize implementation. CVar in question is {}",
cvar.cvar_path()
);
};
let resource = reg.data::<ReflectResource>().expect("Impossible.");
let cvar_id = management.tree.get(cvar.cvar_path()).unwrap();
let change_data = world.get_resource_change_ticks_by_id(cvar_id).unwrap();
let caller = MaybeLocation::caller();
let res = resource.reflect(world)?;
let resource: Ref<dyn Reflect> = {
Ref::new(
res,
&change_data.added,
&change_data.changed,
Tick::new(0),
Tick::new(0),
caller.as_ref(),
)
};
if cvar.is_default_value(resource) {
continue;
}
self.save_cvar_inner_erased(
cvar.cvar_path(),
&serialize.get_serializable(
cvar.reflect_inner(res.as_partial_reflect())?
.try_as_reflect()
.unwrap(),
),
)?;
}
Ok(())
}
}
#[allow(clippy::to_string_trait_impl)]
impl ToString for CVarSaveContext {
fn to_string(&self) -> String {
self.0.to_string()
}
}