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}