Skip to main content

bevy_cache/
lib.rs

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