bevy_mod_config 0.7.1

A Bevy plugin for configuration management
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
//! A modular configuration framework for Bevy applications,
//! decoupling configuration access and change detection from
//! management utilities like persistence and UI.
//!
//! # Getting started
//! First, decide which [`Manager`]s you want.
//! Managers extend the framework with extra capabilities for your config fields,
//! such as saving and loading from a file ([`manager::Serde`])
//! or showing a live editor panel in your game ([`manager::Egui`]).
//! Multiple managers can be composed as a tuple; use `()` if you don't need any yet.
//!
//! Declare your chosen managers as a type alias —
//! you will pass it consistently to every [`init_config`](AppExt::init_config) call.
//! For example, to use both a JSON serializer and an egui editor:
//!
//! ```
//! # /*
//! pub type ManagerType = (bevy_mod_config::manager::serde::Json, bevy_mod_config::manager::Egui);
//! # */
//! ```
//!
//! Now we can define the configuration data model as required.
//! Multiple configuration roots are supported,
//! so each plugin can define its own configuration with <code>#[derive([Config])]</code>:
//!
//! ```
//! use bevy_mod_config::Config;
//!
//! #[derive(Config)]
//! struct VideoSettings {
//!     width:       u32,
//!     height:      u32,
//!     orientation: Orientation,
//! }
//!
//! #[derive(Config)]
//! #[config(expose(read))] // it is usually useful to expose the read type for enums
//! enum Orientation {
//!     Landscape,
//!     Portrait,
//! }
//! ```
//!
//! Refer ot the documentation of [`Config`] for more customization.
//!
//! Next, add it to the Bevy app during startup, referencing the `ManagerType` we just defined:
//!
//! ```
//! # use bevy_app::{App, Plugin};
//! use bevy_mod_config::AppExt;
//!
//! # #[derive(bevy_mod_config::Config)]
//! # struct VideoSettings { width: u32 }
//! # type ManagerType = ();
//! struct VideoPlugin;
//!
//! impl Plugin for VideoPlugin {
//!     fn build(&self, app: &mut App) { app.init_config::<ManagerType, VideoSettings>("video"); }
//! }
//! ```
//!
//! Now we can access the configuration data in systems with [`ReadConfig`].
//! `ReadConfig` gives us the "read" type of each field instead of the original type,
//! so we have to match the enum on the read type we exposed earlier:
//!
//! ```
//! use bevy_mod_config::ReadConfig;
//!
//! # #[derive(bevy_mod_config::Config)]
//! # struct VideoSettings { orientation: Orientation }
//! # #[derive(bevy_mod_config::Config)]
//! # #[config(expose(read))]
//! # enum Orientation { Landscape, Portrait }
//! # fn display_landscape() {}
//! # fn display_portrait() {}
//! fn display_system(settings: ReadConfig<VideoSettings>) {
//!     let settings = settings.read();
//!     match settings.orientation {
//!         OrientationRead::Landscape => display_landscape(),
//!         OrientationRead::Portrait => display_portrait(),
//!     }
//! }
//! ```
//!
//! Note that `ReadConfig` must use the same type as the one passed to `init_config`.
//!
//! Use [`ReadConfigChange`] instead to observe changes.
//! This change detection is on a per-configuration-root basis.
//!
//! ```
//! use bevy_mod_config::ReadConfigChange;
//!
//! # #[derive(bevy_mod_config::Config)]
//! # struct VideoSettings { width: u32, height: u32 }
//! # fn resize_window(_width: u32, _height: u32) {}
//! fn resize_system(mut settings: ReadConfigChange<VideoSettings>) {
//!     if settings.consume_change() {
//!         let settings = settings.read();
//!         resize_window(settings.width, settings.height);
//!     }
//! }
//! ```
//!
//! Now that we have configuration data defined,
//! we can use managers for persistence, loading and more.
//! See the documentation of each [manager] module for examples.
//!
//! # What's next
//! - See [`Manager`] for implementing your own behavior on configuration data.
//! - See [`ConfigField`] for implementing your own field types.
//!
//! A manager can only be used when all config fields in the app
//! implement [`ConfigFieldFor`] for that manager,
//! If you are writing a reusable plugin for other crates,
//! you should accept a generic manager type parameter `M`:
//!
//! ```
//! # use core::marker::PhantomData;
//! # use bevy_app::{App, Plugin};
//! # use bevy_mod_config::{AppExt, Config, ConfigFieldFor, Manager};
//! struct MyPlugin<M>(PhantomData<M>);
//! impl<M: Manager + Default> Plugin for MyPlugin<M>
//! where
//!     MyConf: ConfigFieldFor<M>,
//! {
//!     fn build(&self, app: &mut App) { app.init_config::<M, MyConf>("my_module"); }
//! }
//!
//! #[derive(Config)]
//! struct MyConf {
//!     // ...
//! }
//! ```

#![no_std]
#![warn(missing_docs, clippy::pedantic)]
#![allow(clippy::missing_panics_doc)]

extern crate alloc;

use alloc::string::String;
use alloc::vec::Vec;
use core::num::NonZeroU64;

use bevy_ecs::component::Component;
use bevy_ecs::entity::Entity;
use bevy_ecs::query::QueryData;
use bevy_ecs::world::{EntityRef, EntityWorldMut, World};

pub mod impls;
pub use impls::BareField;
mod query;
pub use query::QueryLike;
mod enum_;
pub use enum_::{
    EnumDiscriminant, EnumDiscriminantMetadata, EnumDiscriminantWrapper, EnumFieldMetadata,
};
pub mod manager;
pub use manager::Manager;
#[doc(hidden)]
pub mod __import;

mod macro_doc;
pub use macro_doc::Config;

mod app;
pub use app::{AppExt, ReadConfig, ReadConfigChange};

mod tree;
pub use tree::{
    ChildNodeList, ChildNodeOf, ConditionalRelevance, ConfigNode, RootNode, ScalarField,
};

/// Tracks the number of changes to a config field.
///
/// After each change, the new generation is greater than the previous one.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct FieldGeneration(NonZeroU64);

impl Default for FieldGeneration {
    fn default() -> Self { FieldGeneration(const { NonZeroU64::new(1).unwrap() }) }
}

impl FieldGeneration {
    /// Increments the generation by one.
    ///
    /// # Panics
    /// Panics if the generation overflows.
    #[must_use]
    pub fn next(self) -> Self {
        FieldGeneration(self.0.checked_add(1).expect("field generation overflow"))
    }
}

/// Context information of the config field from its referrers.
#[derive(Clone)]
pub struct SpawnContext {
    /// The hierarchical path from the root config field.
    ///
    /// Uniquely identifies the config field statically.
    pub path:       Vec<String>,
    /// The parent entity of the config field, if any.
    pub parent:     Option<Entity>,
    /// The [`ConditionalRelevance`] dependency of the config field, if any.
    pub dependency: Option<ConditionalRelevance>,
}

impl SpawnContext {
    /// Appends a path component to this context.
    #[must_use]
    pub fn join(
        &self,
        key: impl IntoIterator<Item = impl Into<String>>,
        parent: Option<Entity>,
    ) -> Self {
        SpawnContext {
            path: self
                .path
                .iter()
                .cloned()
                .chain(key.into_iter().map(Into::<String>::into))
                .collect(),
            parent,
            dependency: None,
        }
    }

    /// Adds a [`ConditionalRelevance`] dependency to this context.
    #[must_use]
    pub fn with_dependency(
        mut self,
        dependency: Entity,
        is_entity_relevant: fn(EntityRef) -> bool,
    ) -> Self {
        self.dependency = Some(ConditionalRelevance { dependency, is_entity_relevant });
        self
    }
}

/// The spawn handle of a config node.
pub trait SpawnHandle {
    /// The entity of the subtree root node.
    fn node(&self) -> Entity;
}

impl SpawnHandle for Entity {
    fn node(&self) -> Entity { *self }
}

/// Field types that can be used in a [`Config`] struct/enum.
pub trait ConfigField: 'static {
    /// Remembers where the config data are stored in the world after spawning.
    type SpawnHandle: SpawnHandle + 'static + Send + Sync;

    /// The type returned when reading the config data from the world.
    ///
    /// `'a` is the lifetime of the receiver in [`ReadConfig::read`].
    type Reader<'a>: Copy;
    /// The minimal components required to read the typed config fields under this field.
    ///
    /// For scalar fields, this is always `Option<&ScalarData<Self>>`.
    type ReadQueryData: QueryData;

    /// Type-specific metadata specified by the referrer.
    ///
    /// By convention, `Metadata` types should be a simple struct with public fields
    /// such that users can assign to them directly in [`#[derive(Config)]`](crate::Config) fields.
    type Metadata: Default + 'static + Send + Sync;

    /// Type returned by [`ConfigField::changed`].
    ///
    /// The return type of this function is often opaque, but guarantees that:
    /// - It can be safely persisted in the world due to thread safety and static lifetime.
    /// - It can be [cloned](Clone) at a cheaper cost (than the original data, on average).
    /// - It can be compared for [equality](Eq) with the previous value
    ///   to determine whether the config data has changed.
    type Changed: Clone + Eq + 'static + Send + Sync;
    /// The minimal components required to compute whether the config data has changed.
    ///
    /// This is `()` for most types,
    /// but may contain enum discriminants for enum fields
    /// to determine which variant should be compared.
    type ChangedQueryData: QueryData;

    /// Reads config data for user consumption from a query of config data entities.
    fn read_world<'a, 's>(
        query: impl QueryLike<
            Item = <<Self::ReadQueryData as QueryData>::ReadOnly as QueryData>::Item<'a, 's>,
        >,
        spawn_handle: &Self::SpawnHandle,
    ) -> Self::Reader<'a>;

    /// Computes an [equivalence class](Eq) that represents whether the config data has changed.
    ///
    /// If the config data has been changed, the result returned by this function
    /// will be [unequal](PartialEq::ne) to the result obtained before the change.
    fn changed<'a, 's>(
        query: impl QueryLike<
            Item = (
                &'a ConfigNode,
                <<Self::ChangedQueryData as QueryData>::ReadOnly as QueryData>::Item<'a, 's>,
            ),
        >,
        spawn_handle: &Self::SpawnHandle,
    ) -> Self::Changed;
}

/// Determines how a [`ConfigField`] implementor interacts with a [`Manager`] type.
///
/// `T: ConfigField<M>` means that `T` can be used in applications
/// using a [`Manager`] `M`.
/// If `T` contains a scalar type `U`, the implementation should be written as
///
/// ```text
/// impl<M: manager::Supports<U>> ConfigField<M> for T { ... }
/// ```
pub trait ConfigFieldFor<M>: ConfigField {
    /// Spawns entities in the world to store config data.
    ///
    /// Each spawned entity MUST have a [`ConfigNode`] component
    /// AND attach the component bundle requested from [`Manager::new_entity`].
    fn spawn_world(
        world: &mut World,
        ctx: SpawnContext,
        metadata: Self::Metadata,
    ) -> Self::SpawnHandle;
}

/// Stores the typed data of a scalar config field.
///
/// In addition to direct use in [`ConfigField`] implementations,
/// this is also the conventional type used by [`Manager`]s to interact with the actual data
/// which they are monomorphized for in [`manager::Supports::new_entity_for_type`].
/// Managers generally only interact with scalar fields directly.
#[derive(Component)]
pub struct ScalarData<T>(pub T);

/// Stores the metadata of a scalar config field.
#[derive(Component)]
pub struct ScalarMetadata<T: ConfigField>(pub T::Metadata);

/// Implements [`ConfigField`] for a scalar (non-composite) type.
///
/// - `$ty`: the scalar type to implement [`ConfigField`] for.
///   This is the actual owned value to be persisted in the world.
///   Managers will see this type as a component [`ScalarData<$ty>`].
/// - `$metadata`: the metadata type for the scalar field.
/// - `$default_from_metadata`: a function to produce a default value of `$ty` from metadata.
///   Must implement `Fn($metadata) -> $ty`.
/// - `$lt`: an arbitrary lifetime parameter that may be used in `$mapped_ty`.
///   Just put an arbitrary lifetime parameter here, such as `'a`,
///   even if `$mapped_ty` does not use it.
/// - `$mapped_ty`: the type returned by [`ConfigField::read_world`].
///   This is the most user-friendly type used in readers,
///   e.g. `&str` for `String`, or the owned value for [`Copy`] types.
/// - `$map_fn`: a function that maps the scalar data to `$mapped_ty`.
#[macro_export]
macro_rules! impl_scalar_config_field {
    ($ty:ty, $metadata:ty, $default_from_metadata:expr, $lt:lifetime => $mapped_ty:ty, $map_fn:expr $(,)?) => {
        impl $crate::ConfigField for $ty {
            type SpawnHandle = $crate::__import::Entity;
            type Reader<$lt> = $mapped_ty;
            type ReadQueryData = $crate::__import::Option<&'static $crate::ScalarData<Self>>;
            type Metadata = $metadata;
            type Changed = $crate::FieldGeneration;
            type ChangedQueryData = ();

            fn read_world<'a, 's>(
                query: impl $crate::QueryLike<Item = <<Self::ReadQueryData as $crate::__import::QueryData>::ReadOnly as $crate::__import::QueryData>::Item<'a, 's>>,
                &spawn_handle: &$crate::__import::Entity,
            ) -> Self::Reader<'a> {
                let data = query.get(spawn_handle).expect(
                    "entity managed by config field must remain active as long as the config \
                     handle is used",
                );
                $map_fn(&data.as_ref().expect("scalar data component must remain valid with Self type").0)
            }

            fn changed<'a, 's>(
                query: impl $crate::QueryLike<Item = (&'a $crate::ConfigNode, <<Self::ChangedQueryData as $crate::__import::QueryData>::ReadOnly as $crate::__import::QueryData>::Item<'a, 's>)>,
                &spawn_handle: &$crate::__import::Entity,
            ) -> Self::Changed {
                let entity = query.get(spawn_handle).expect(
                    "entity managed by config field must remain active as long as the config \
                     handle is used",
                );
                entity.0.generation
            }
        }

        impl<M: $crate::manager::Supports<$ty>> $crate::ConfigFieldFor<M> for $ty {
            fn spawn_world(
                world: &mut $crate::__import::World,
                ctx: $crate::SpawnContext,
                metadata: Self::Metadata,
            ) -> $crate::__import::Entity {
                let manager_comps =
                    world.resource_mut::<$crate::manager::Instance<M>>().new_entity::<$ty>();
                let mut entity = world.spawn((
                        $crate::__import::BevyName::new("Scalar config field"),
                        $crate::ScalarData::<Self>($default_from_metadata(&metadata)),
                        $crate::ScalarMetadata::<Self>(metadata),
                        manager_comps,
                ));
                $crate::init_config_node(&mut entity, ctx);
                entity.id()
            }
        }
    };
}
use impl_scalar_config_field as impl_scalar_config_field_;

/// Initializes a newly spawned config node entity with the required components from the context.
pub fn init_config_node(entity: &mut EntityWorldMut, ctx: SpawnContext) {
    entity.insert(ConfigNode { path: ctx.path, generation: FieldGeneration::default() });
    if let Some(parent) = ctx.parent {
        entity.insert(ChildNodeOf(parent));
    }
    if let Some(dependency) = ctx.dependency {
        entity.insert(dependency);
    }
}

/// Metadata type for [`ConfigField`] implementors derived from [`Config`].
#[derive(Default, Clone)]
pub struct StructMetadata;