#![deny(missing_docs)]
#![cfg_attr(all(feature = "config_loader_asset", feature = "config_loader_fs"), doc = include_str!("examples.md"))]
use bevy_app::App;
use bevy_app::prelude::*;
use bevy_ecs::component::ComponentId;
use bevy_ecs::prelude::*;
use bevy_platform_support::collections::HashMap;
use bevy_reflect::{TypeRegistration, prelude::*};
#[cfg(feature = "config_loader")]
use builtin::ConfigLoaderCVarsPlugin;
use builtin::CoreCVarsPlugin;
use builtin::LogCVarChanges;
#[cfg(feature = "parse_cvars")]
use parse::CVarOverride;
use reflect::CVarMeta;
use serde::Deserializer;
#[cfg(feature = "parse_cvars")]
use serde::de::IntoDeserializer as _;
pub mod defaults;
mod error;
mod macros;
mod types;
pub use error::*;
pub use types::*;
pub mod builtin;
#[cfg(feature = "config_loader")]
pub mod loader;
#[cfg(feature = "parse_cvars")]
pub mod parse;
#[cfg(feature = "parse_cvars")]
pub mod save;
pub mod prelude;
pub mod reflect;
#[cfg(test)]
mod tests;
#[doc(hidden)]
pub mod reexports {
pub use bevy_app;
pub use bevy_ecs;
pub use bevy_reflect;
pub mod jank {
pub use crate::reflect::ReflectCVar as ReflectCVar__MACRO_JANK;
pub use bevy_ecs::reflect::ReflectResource as ReflectResource__MACRO_JANK;
pub use bevy_reflect::prelude::ReflectDefault as ReflectDefault__MACRO_JANK;
}
}
pub struct CVarsPlugin;
#[derive(Debug)]
pub(crate) enum CVarTreeNode {
Leaf {
name: &'static str,
reg: ComponentId,
},
Branch {
descendants: HashMap<&'static str, CVarTreeNode>,
},
}
impl Default for CVarTreeNode {
fn default() -> Self {
CVarTreeNode::Branch {
descendants: Default::default(),
}
}
}
struct CVarTreeEditContext {
new_cvar: &'static str,
}
impl CVarTreeNode {
pub fn children(&self) -> Option<impl Iterator<Item = (&'_ &'static str, &'_ CVarTreeNode)>> {
match self {
CVarTreeNode::Leaf { name: _, reg: _ } => None,
CVarTreeNode::Branch { descendants } => Some(descendants.iter()),
}
}
pub fn is_leaf(&self) -> bool {
matches!(self, CVarTreeNode::Leaf { .. })
}
pub fn insert(&mut self, name: &'static str, id: ComponentId) {
let segments: Vec<&'static str> = name.split('.').collect();
let edit_ctx = CVarTreeEditContext { new_cvar: name };
let mut cur = self;
for (idx, segment) in segments.iter().enumerate() {
if idx == segments.len() - 1 {
let _ = cur.insert_leaf(segment, id, &edit_ctx);
return;
} else {
cur = cur.get_or_insert_branch(segment, &edit_ctx);
}
}
}
#[must_use]
fn get_or_insert_branch(
&mut self,
key: &'static str,
ctx: &CVarTreeEditContext,
) -> &mut CVarTreeNode {
match self {
CVarTreeNode::Leaf { name, reg: _ } => panic!(
"Tried to insert branch {name} into a terminating node. A CVar cannot be both a value and table. CVar in question is {}",
ctx.new_cvar
),
CVarTreeNode::Branch { descendants } => {
descendants.entry(key).or_insert(CVarTreeNode::Branch {
descendants: Default::default(),
})
}
}
}
#[must_use]
fn insert_leaf(
&mut self,
key: &'static str,
reg: ComponentId,
ctx: &CVarTreeEditContext,
) -> &mut CVarTreeNode {
match self {
CVarTreeNode::Leaf { name, reg: _ } => {
panic!(
"Tried to insert leaf {name} into a terminating node. Is there a duplicate or overlap? CVar in question is {}",
ctx.new_cvar
)
}
CVarTreeNode::Branch { descendants } => {
assert!(
descendants
.insert(
key,
CVarTreeNode::Leaf {
name: ctx.new_cvar,
reg
}
)
.is_none(),
"Attempted to insert a duplicate CVar. CVar in question is {}",
ctx.new_cvar
);
descendants.get_mut(key).unwrap()
}
}
}
#[must_use]
pub fn get(&self, name: &str) -> Option<ComponentId> {
let mut cur = self;
for seg in name.split('.') {
let CVarTreeNode::Branch { descendants } = cur else {
return None;
};
cur = descendants.get(seg)?;
}
let CVarTreeNode::Leaf { name: _, reg } = cur else {
return None;
};
Some(*reg)
}
}
#[derive(Default, Resource)]
pub struct CVarManagement {
pub(crate) resources: HashMap<ComponentId, TypeRegistration>,
pub(crate) tree: CVarTreeNode,
}
impl CVarManagement {
#[doc(hidden)]
pub fn register_cvar<T: Reflect + Resource + CVarMeta>(&mut self, app: &mut App) {
let registration = {
let registry = app.world().resource::<AppTypeRegistry>();
let registry = registry.read();
registry.get(::std::any::TypeId::of::<T>()).unwrap().clone()
};
let cid = app.world().resource_id::<T>().unwrap();
self.tree.insert(T::CVAR_PATH, cid);
self.resources.insert(cid, registration);
}
pub fn get_cvar_reflect<'a>(
&self,
world: &'a World,
cvar: &str,
) -> Result<&'a dyn Reflect, CVarError> {
let cid = self.tree.get(cvar).ok_or(CVarError::UnknownCVar)?;
let ty_info = self.resources.get(&cid).ok_or(CVarError::UnknownCVar)?;
let reflect_res = ty_info
.data::<ReflectResource>()
.ok_or(CVarError::BadCVarType)?;
let reflect_cvar = ty_info
.data::<reflect::ReflectCVar>()
.ok_or(CVarError::BadCVarType)?;
let res = reflect_res.reflect(world)?;
reflect_cvar
.reflect_inner(res.as_partial_reflect())
.unwrap()
.try_as_reflect()
.ok_or(CVarError::BadCVarType)
}
pub fn get_cvar_reflect_mut<'a>(
&self,
world: &'a mut World,
cvar: &str,
) -> Result<Mut<'a, dyn Reflect>, CVarError> {
let cid = self.tree.get(cvar).ok_or(CVarError::UnknownCVar)?;
let ty_info = self.resources.get(&cid).ok_or(CVarError::UnknownCVar)?;
let reflect_res = ty_info
.data::<ReflectResource>()
.ok_or(CVarError::BadCVarType)?;
let reflect_cvar = ty_info
.data::<reflect::ReflectCVar>()
.ok_or(CVarError::BadCVarType)?;
Ok(reflect_res.reflect_mut(world)?.map_unchanged(|x| {
reflect_cvar
.reflect_inner_mut(x.as_partial_reflect_mut())
.unwrap()
.try_as_reflect_mut()
.unwrap()
}))
}
pub fn set_cvar_reflect(
&self,
world: &mut World,
cvar: &str,
value: &dyn Reflect,
) -> Result<(), CVarError> {
let cid = self.tree.get(cvar).ok_or(CVarError::UnknownCVar)?;
let ty_reg = self.resources.get(&cid).ok_or(CVarError::MissingCid)?;
let reflect_cvar = ty_reg.data::<reflect::ReflectCVar>().unwrap();
let reflect_res = ty_reg.data::<ReflectResource>().unwrap();
let cvar = reflect_res.reflect_mut(world)?;
reflect_cvar.reflect_apply(
cvar.into_inner().as_partial_reflect_mut(),
value.as_partial_reflect(),
)?;
Ok(())
}
pub fn set_cvar_reflect_no_change(
&self,
world: &mut World,
cvar: &str,
value: &dyn Reflect,
) -> Result<(), CVarError> {
let cid = self.tree.get(cvar).ok_or(CVarError::UnknownCVar)?;
let ty_reg = self.resources.get(&cid).ok_or(CVarError::MissingCid)?;
let reflect_cvar = ty_reg.data::<reflect::ReflectCVar>().unwrap();
let reflect_res = ty_reg.data::<ReflectResource>().unwrap();
let mut cvar = reflect_res.reflect_mut(world)?;
reflect_cvar.reflect_apply(
cvar.bypass_change_detection().as_partial_reflect_mut(),
value.as_partial_reflect(),
)?;
Ok(())
}
pub fn set_cvar_deserialize<'w, 'a>(
&self,
world: &mut World,
cvar: &str,
value: impl Deserializer<'a>,
) -> Result<(), CVarError> {
let cid = self.tree.get(cvar).ok_or(CVarError::UnknownCVar)?;
let ty_reg = self.resources.get(&cid).ok_or(CVarError::MissingCid)?;
let reflect_cvar = ty_reg.data::<reflect::ReflectCVar>().unwrap();
let value_patch = {
let field_0 = reflect_cvar.inner_type();
let registry = world.resource::<AppTypeRegistry>().read();
let deserializer = registry
.get(field_0)
.ok_or(CVarError::BadCVarType)?
.data::<ReflectDeserialize>()
.ok_or(CVarError::CannotDeserialize)?;
deserializer
.deserialize(value)
.map_err(|e| CVarError::FailedDeserialize(format!("{e:?}")))?
};
let reflect_res = ty_reg.data::<ReflectResource>().unwrap();
let cvar = reflect_res.reflect_mut(world)?;
reflect_cvar.reflect_apply(
cvar.into_inner().as_partial_reflect_mut(),
value_patch.as_partial_reflect(),
)?;
Ok(())
}
pub fn set_cvar_deserialize_no_change<'w, 'a>(
&self,
world: &mut World,
cvar: &str,
value: impl Deserializer<'a>,
) -> Result<(), CVarError> {
let cid = self.tree.get(cvar).ok_or(CVarError::UnknownCVar)?;
let ty_reg = self.resources.get(&cid).ok_or(CVarError::MissingCid)?;
let reflect_cvar = ty_reg.data::<reflect::ReflectCVar>().unwrap();
let value_patch = {
let field_0 = reflect_cvar.inner_type();
let registry = world.resource::<AppTypeRegistry>().read();
let deserializer = registry
.get(field_0)
.ok_or(CVarError::CannotDeserialize)?
.data::<ReflectDeserialize>()
.ok_or(CVarError::CannotDeserialize)?;
deserializer
.deserialize(value)
.map_err(|e| CVarError::FailedDeserialize(format!("{e:?}")))?
};
let reflect_res = ty_reg.data::<ReflectResource>().unwrap();
let mut cvar = reflect_res.reflect_mut(world)?;
reflect_cvar.reflect_apply(
cvar.bypass_change_detection().as_partial_reflect_mut(),
value_patch.as_partial_reflect(),
)?;
Ok(())
}
pub fn iterate_cvar_types(&self) -> impl Iterator<Item = &TypeRegistration> {
self.resources.values()
}
}
pub trait WorldExtensions {
#[doc(hidden)]
fn as_world(&mut self) -> &mut World;
fn set_cvar_deserialize<'a>(
&mut self,
cvar: &str,
value: impl serde::Deserializer<'a>,
) -> Result<(), CVarError> {
let cell = self.as_world();
cell.resource_scope::<CVarManagement, _>(|w, management| {
management.set_cvar_deserialize(w, cvar, value)
})
}
fn set_cvar_deserialize_no_change<'a>(
&mut self,
cvar: &str,
value: impl serde::Deserializer<'a>,
) -> Result<(), CVarError> {
let cell = self.as_world();
cell.resource_scope::<CVarManagement, _>(|w, management| {
management.set_cvar_deserialize_no_change(w, cvar, value)
})
}
fn set_cvar_reflect(&mut self, cvar: &str, value: &dyn Reflect) -> Result<(), CVarError> {
let cell = self.as_world();
cell.resource_scope::<CVarManagement, _>(|w, management| {
management.set_cvar_reflect(w, cvar, value)
})
}
fn set_cvar_reflect_no_change(
&mut self,
cvar: &str,
value: &dyn Reflect,
) -> Result<(), CVarError> {
let cell = self.as_world();
cell.resource_scope::<CVarManagement, _>(|w, management| {
management.set_cvar_reflect_no_change(w, cvar, value)
})
}
#[cfg(feature = "parse_cvars")]
fn set_cvar_with_override(&mut self, r#override: &CVarOverride) -> Result<(), CVarError> {
let cell = self.as_world();
cell.resource_scope::<CVarManagement, _>(|w, management| {
management.set_cvar_deserialize_no_change(
w,
&r#override.0,
r#override.1.clone().into_deserializer(),
)
})
}
}
impl WorldExtensions for World {
fn as_world(&mut self) -> &mut World {
self
}
}
impl Plugin for CVarsPlugin {
fn build(&self, app: &mut bevy_app::App) {
app.register_type::<CVarFlags>();
app.insert_resource::<CVarManagement>(CVarManagement::default());
app.add_plugins(CoreCVarsPlugin);
#[cfg(feature = "config_loader")]
{
app.add_plugins(ConfigLoaderCVarsPlugin);
}
}
}
#[doc(hidden)]
pub fn cvar_modified_system<T: CVarMeta>(
r: bevy_ecs::prelude::Res<T>,
log_updates: Res<LogCVarChanges>,
) {
use bevy_ecs::prelude::DetectChanges as _;
if **log_updates && r.is_changed() {
bevy_log::info!("CVar modified: {} = {:?}", T::CVAR_PATH, **r);
}
if !r.is_changed() {
return;
}
if !T::flags().contains(CVarFlags::RUNTIME) && !r.is_added() {
if T::flags().contains(CVarFlags::SAVED) {
bevy_log::warn!("Non-runtime CVar was modified! Change will not apply until restart.");
} else {
bevy_log::error!("Non-runtime, non-saved CVar was modified! This will have NO EFFECT.");
}
}
}