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}