Skip to main content

bevy_cache/
lib.rs

1mod asset_server_ext;
2mod config;
3mod error;
4mod manifest;
5mod save_queue;
6mod system_param;
7mod systems;
8
9#[cfg(feature = "hot_reload")]
10pub mod hot_reload;
11
12pub use asset_server_ext::AssetServerCacheExt;
13pub use config::CacheConfig;
14pub use error::CacheError;
15pub use manifest::{CacheEntry, CacheManifest};
16pub use save_queue::CacheQueue;
17pub use system_param::Cache;
18
19use bevy::asset::io::{AssetSource, AssetSourceBuilder};
20use bevy::prelude::*;
21
22pub mod prelude {
23    pub use crate::AssetServerCacheExt;
24    pub use crate::{BevyCachePlugin, Cache, CacheError, CacheEntry, CacheConfig, CacheManifest, CacheQueue};
25}
26
27/// Plugin that registers the file cache system.
28///
29/// Registers a `"cache"` asset source that maps to the OS cache directory.
30/// Any Bevy asset type can be loaded from the cache using the `cache://` scheme;
31/// Bevy's built-in loaders handle the file based on its real extension.
32///
33/// **Must be added before `DefaultPlugins`** so that the asset source is
34/// available when `AssetPlugin` initialises.
35///
36/// The plugin automatically registers the cache directory with Bevy's
37/// `AssetPlugin` using `platform_default`, so if you enable file watching in
38/// `AssetPlugin` the cache directory is watched alongside the normal assets
39/// folder — no extra configuration required:
40///
41/// ```rust,ignore
42/// App::new()
43///     .add_plugins(BevyCachePlugin::new("my_game"))
44///     .add_plugins(DefaultPlugins.set(AssetPlugin {
45///         watch_for_changes_override: Some(true),
46///         ..default()
47///     }))
48///     .run();
49/// ```
50///
51/// Without a `watch_for_changes_override` the cache source is still registered
52/// and accessible via `cache://`; watching is just disabled.
53///
54/// ## Hot-reloading the manifest
55///
56/// Enable the `hot_reload` Cargo feature to additionally watch the
57/// `manifest.cache_manifest` file itself and have [`CacheManifest`] re-synced
58/// automatically when it changes on disk.
59///
60/// After adding the plugin, use [`CacheManifest`] to store and query cached
61/// assets, and [`CacheQueue`] to enqueue asset handles for deferred caching:
62///
63/// ```rust,ignore
64/// fn cache_screenshot(mut cache: Cache) {
65///     let png_data: Vec<u8> = render_my_screenshot();
66///     cache.store("scene_01", "png", std::io::Cursor::new(png_data), None)
67///         .expect("cache write failed");
68/// }
69///
70/// fn cache_asset_by_handle(
71///     mut pending: ResMut<CacheQueue>,
72///     assets: Res<Assets<MyAsset>>,
73///     handle: Res<MyAssetHandle>,
74/// ) {
75///     if let Some(asset) = assets.get(&handle.0) {
76///         // Reflect-based: serialized to RON via ReflectSerializer
77///         pending.enqueue_reflect(
78///             Box::new(asset.clone()),
79///             "my_asset_key",
80///             "ron",
81///             None,
82///         );
83///     }
84/// }
85///
86/// fn load_cached(mut cache: Cache, asset_server: Res<AssetServer>) {
87///     if let Some(path) = cache.asset_path("scene_01") {
88///         // Bevy detects ".png" and uses ImageLoader automatically.
89///         let handle: Handle<Image> = asset_server.load(path);
90///     }
91/// }
92/// ```
93#[derive(Default)]
94pub struct BevyCachePlugin {
95    pub config: CacheConfig,
96}
97
98impl BevyCachePlugin {
99    pub fn new(app_name: &str) -> Self {
100        Self {
101            config: CacheConfig::new(app_name),
102        }
103    }
104}
105
106impl Plugin for BevyCachePlugin {
107    fn build(&self, app: &mut App) {
108        let cache_dir = self.config.cache_dir.clone();
109
110        // Ensure the cache directory exists on disk *before* registering the
111        // asset source. Bevy's `get_default_watcher` skips watcher creation
112        // (returning None) when the path does not exist at the time
113        // `AssetPlugin::build()` calls the watcher factory. Pre-creating the
114        // directory here guarantees the watcher is set up correctly.
115        if let Err(e) = self.config.ensure_cache_dir() {
116            warn!("bevy_cache: could not create cache directory {:?}: {e}", cache_dir);
117        }
118
119        // Register the cache source manually with a 1 s debounce (instead of
120        // `platform_default`'s 300 ms) so that editors that write files in two
121        // OS-level steps (truncate then write, or write-to-temp then rename)
122        // don't produce two reload events for a single logical save.
123        let s = cache_dir.to_string_lossy().into_owned();
124        app.register_asset_source(
125            "cache",
126            AssetSourceBuilder::new(AssetSource::get_default_reader(s.clone()))
127                .with_writer(AssetSource::get_default_writer(s.clone()))
128                .with_watcher(AssetSource::get_default_watcher(
129                    s,
130                    std::time::Duration::from_millis(1000),
131                ))
132                .with_watch_warning(AssetSource::get_default_watch_warning()),
133        );
134
135        app.insert_resource(self.config.clone())
136            .init_resource::<save_queue::CacheQueue>()
137            .add_systems(Startup, systems::load_manifest)
138            .add_systems(Last, systems::cleanup_on_exit);
139
140        #[cfg(not(feature = "hot_reload"))]
141        app.add_systems(PostUpdate, (
142            save_queue::process_pending_saves,
143            systems::save_manifest_on_change,
144        ).chain());
145
146        #[cfg(feature = "hot_reload")]
147        app
148            .init_resource::<hot_reload::ManifestReloadState>()
149            .add_systems(
150                Startup,
151                hot_reload::startup_watch_manifest.after(systems::load_manifest),
152            )
153            .add_systems(PostUpdate, (
154                save_queue::process_pending_saves,
155                hot_reload::sync_manifest_from_asset,
156                hot_reload::save_manifest_skip_reload,
157            ).chain());
158    }
159
160    /// Called after all plugins have had `build` run — by this point
161    /// `AssetPlugin` (from `DefaultPlugins`) has initialised `AssetServer`,
162    /// so it is safe to call `init_asset` / `register_asset_loader`.
163    #[cfg(feature = "hot_reload")]
164    fn finish(&self, app: &mut App) {
165        app.init_asset::<hot_reload::CacheManifestAsset>()
166            .register_asset_loader(hot_reload::CacheManifestLoader::default());
167    }
168}