bevy-settings-lib 0.1.0

A flexible settings management library for Bevy with async saving, multiple formats, and built‑in validation
Documentation

bevy-settings-lib

Crates.io Documentation Bevy version License: MIT

A flexible settings management library for Bevy with async saving, multiple formats, and built‑in validation.

This library provides a convenient way to save, load, and reload settings in Bevy applications. It supports text formats (TOML, JSON) and binary (postcard) with atomic write‑then‑rename to prevent file corruption, with a clean, event‑driven API.

Features

  • Any number of configurations – each configuration has its own data type and file name.
  • File names can be explicit or derived from the struct name (automatically converted to snake_case).
  • Asynchronous saving with atomic write‑then‑rename – files are never left in a corrupted state.
  • Format support: TOML (default), JSON, binary (postcard).
  • Load from OS‑standard directories (via directories crate) or from the game's local folder.
  • Events: PersistSetting<S>, PersistAllSettings, ReloadSetting<S>, SettingsSaveError<S>.
  • Partial loading – if a file does not exist, S::default() is used.
  • Validation – every settings type must implement ValidatedSetting to normalize values after loading and before saving.
  • Thread‑safe – background threads handle file I/O without blocking the main thread.

Installation

Add to your Cargo.toml:

[dependencies]

bevy-settings-lib = "0.1.0"

Quick Start

use bevy::prelude::*;
use bevy_settings_lib::{SettingsPlugin, PersistSetting, SettingsPluginConfig, FormatKind, ValidatedSetting, SettingsStorage};
use serde::{Serialize, Deserialize};

#[derive(Resource, Default, Serialize, Deserialize, Clone, Debug, PartialEq)]
struct MySettings {
    volume: f32,
    fullscreen: bool,
}

// Mandatory validation implementation (can be empty if no validation needed)
impl ValidatedSetting for MySettings {
    fn validate(&mut self) {
        self.volume = self.volume.clamp(0.0, 1.0);
    }
}

fn main() {
    // Use the system configuration directory (AppData, ~/.config, etc.)
    let config = SettingsPluginConfig {
        format: FormatKind::Toml,
        company: "MyCompany".into(),
        project: "MyGame".into(),
        file_name: None, // auto‑name → "my_settings"
        storage: SettingsStorage::SystemConfigDir,
        ..Default::default()
    };
    App::new()
        .add_plugins(SettingsPlugin::<MySettings>::from_config(config))
        .add_systems(Update, save_on_keypress)
        .run();
}

fn save_on_keypress(mut commands: Commands, keyboard: Res<ButtonInput<KeyCode>>) {
    if keyboard.just_pressed(KeyCode::KeyS) {
        commands.trigger(PersistSetting::<MySettings> { value: None });
    }
}

Guide

1. Defining a Settings Type

Your settings type must be a Bevy Resource and implement Serialize, Deserialize, Clone, Debug, Default, and PartialEq. It also must implement the ValidatedSetting trait, which is called after loading and before saving to normalize values.

#[derive(Resource, Default, Serialize, Deserialize, Clone, Debug, PartialEq)]
struct GameConfig {
    render_scale: f32,
    vsync: bool,
    resolution: (u32, u32),
}

impl ValidatedSetting for GameConfig {
    fn validate(&mut self) {
        self.render_scale = self.render_scale.clamp(0.5, 2.0);
        if self.resolution.0 == 0 || self.resolution.1 == 0 {
            self.resolution = (1920, 1080);
        }
    }
}

2. Plugin Configuration

Use SettingsPluginConfig to control where and how settings are stored.

Field Description Default
domain Top‑level domain for OS‑specific paths (e.g., "com", "org"). "com"
company Company/organization name (required for SystemConfigDir). ""
project Project name (required for SystemConfigDir). ""
format Serialization format: FormatKind::Toml, Json, or Binary. Toml
file_name Explicit file name (without extension). If None, derived from the struct name. None
storage Where to store files: SettingsStorage::SystemConfigDir (OS‑standard) or GameLocalDir (next to executable). SystemConfigDir

Example with custom configuration:

let config = SettingsPluginConfig {
format: FormatKind::Json,
company: "MyCompany".into(),
project: "MyGame".into(),
file_name: Some("user_prefs".into()),
storage: SettingsStorage::GameLocalDir, // save next to the .exe
..Default::default ()
};
app.add_plugins(SettingsPlugin::<MySettings>::from_config(config));

3. Storage Locations

  • SystemConfigDir – uses the OS‑standard configuration directory:

    • Windows: %APPDATA%\Company\Project\config\
    • macOS: ~/Library/Application Support/company/project/config/
    • Linux: ~/.config/company/project/config/
  • GameLocalDir – uses the directory containing the executable (ideal for portable installations).

4. Saving Settings

Trigger saving by sending a PersistSetting<S> event. The event can optionally carry a new value to replace the current settings before saving.

// Save current settings
commands.trigger(PersistSetting::<MySettings> { value: None });

// Save with a new value
commands.trigger(PersistSetting::<MySettings> {
value: Some(MySettings { volume: 0.8, fullscreen: true }),
});

To save all settings types at once (if you have multiple plugins), use PersistAllSettings:

commands.trigger(PersistAllSettings);

5. Reloading Settings

Reload settings from disk with a ReloadSetting<S> event:

commands.trigger(ReloadSetting::<MySettings> {
_phantom: std::marker::PhantomData,
});

6. Handling Errors

If a background save fails, a SettingsSaveError<S> event is emitted. You can observe it to notify the user or log the error.

fn handle_save_errors(
    mut errors: MessageReader<SettingsSaveError<MySettings>>,
) {
    for error in errors.read() {
        error!("Failed to save settings: {}", error.error);
    }
}

7. Multiple Settings Types

You can have as many independent settings types as you need – just add a separate SettingsPlugin for each.

app.add_plugins(SettingsPlugin::<GameConfig>::from_config(game_config))
.add_plugins(SettingsPlugin::<UserPrefs>::from_config(user_prefs));

Each will be stored in its own file and can be saved/reloaded independently.

Important Notes

  • Company and project names must not contain invalid characters for ProjectDirs and cannot be empty when using SystemConfigDir – the library will panic. With GameLocalDir these fields are optional (may be empty).
  • No auto‑save – the developer decides when to trigger saving.
  • Validation is mandatory – even if you don't need validation, you must provide an empty validate implementation.

Examples

#TODO

Run an example with:

cargo run --example basic

API Reference

Full API documentation is available on docs.rs.

Contributing

Contributions are welcome! Please open an issue or pull request on GitHub.

See CHANGELOG.md for a history of changes.

License

Licensed under the MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT).