From 00e16ba229ced7c7c095ef397b681511edf93af2 Mon Sep 17 00:00:00 2001 From: ada Date: Tue, 10 Feb 2026 21:39:41 -0600 Subject: [PATCH] feat: auto-create assets if not found on disk This is a temporary solution. For now, it only creates the assets in the server, but does not write to disk. Would like to write to disk but want to ensure WASM compatibility, so waiting for 0.19's AssetSaver abstraction. --- crates/app/src/service/dev/console/ui.rs | 4 +- crates/cmd_prompt/examples/minimal.rs | 2 +- crates/cmd_prompt/examples/styled.rs | 2 +- .../src/actions/actions/basic_input.rs | 6 +- .../cmd_prompt/src/actions/actions/history.rs | 4 +- crates/cmd_prompt/src/assets.rs | 248 +++++++++++++----- crates/cmd_prompt/src/test_harness.rs | 2 +- crates/cmd_prompt/src/ui/console.rs | 39 +-- 8 files changed, 192 insertions(+), 115 deletions(-) diff --git a/crates/app/src/service/dev/console/ui.rs b/crates/app/src/service/dev/console/ui.rs index 30e30f7..f6e6229 100644 --- a/crates/app/src/service/dev/console/ui.rs +++ b/crates/app/src/service/dev/console/ui.rs @@ -87,8 +87,10 @@ pub fn plugin(app: &mut App) { children![( Name::new("Console"), Visibility::Hidden, + ConsolePrompt("\n\n> ".into()), Console, - ConsolePrompt("\n\n> ".into()) + ConsoleAssetHandle::::new("dev/console.history".to_string()), + ConsoleAssetHandle::::new("dev/console.env".to_string()), )], )], )); diff --git a/crates/cmd_prompt/examples/minimal.rs b/crates/cmd_prompt/examples/minimal.rs index f352429..84049d6 100644 --- a/crates/cmd_prompt/examples/minimal.rs +++ b/crates/cmd_prompt/examples/minimal.rs @@ -22,7 +22,7 @@ pub fn main() { height: Val::Vh(100.), ..Default::default() }, - children![Console::default()], + children![Console], )); }); app.run(); diff --git a/crates/cmd_prompt/examples/styled.rs b/crates/cmd_prompt/examples/styled.rs index 33cbc91..c01e142 100644 --- a/crates/cmd_prompt/examples/styled.rs +++ b/crates/cmd_prompt/examples/styled.rs @@ -18,7 +18,7 @@ pub fn main() { ..Default::default() }, children![ - Console::default(), + Console, ConsolePrompt("<=================>\n=>".into()), ConsoleUiSettings { font_color: tailwind::AMBER_700.into(), diff --git a/crates/cmd_prompt/src/actions/actions/basic_input.rs b/crates/cmd_prompt/src/actions/actions/basic_input.rs index c5ac2af..e924846 100644 --- a/crates/cmd_prompt/src/actions/actions/basic_input.rs +++ b/crates/cmd_prompt/src/actions/actions/basic_input.rs @@ -1,4 +1,4 @@ -use bevy::input::keyboard::Key; +use bevy::{asset::AsAssetId, input::keyboard::Key}; use crate::prelude::*; @@ -73,13 +73,13 @@ pub fn submit( mut query: Query<( &mut ConsoleBuffer, &mut ConsoleInputText, - &ConsoleHistoryHandle, + &ConsoleAssetHandle, )>, mut assets: ResMut>, mut commands: Commands, ) { if let Ok((mut buffer, mut input_text, history_handle)) = query.get_mut(input.console_id) { - let history = assets.get_mut(history_handle.id()); + let history = assets.get_mut(history_handle.as_asset_id()); if history.is_none() { error!("Failed to get console history!"); return; diff --git a/crates/cmd_prompt/src/actions/actions/history.rs b/crates/cmd_prompt/src/actions/actions/history.rs index 644d200..1744146 100644 --- a/crates/cmd_prompt/src/actions/actions/history.rs +++ b/crates/cmd_prompt/src/actions/actions/history.rs @@ -4,7 +4,7 @@ use crate::prelude::*; pub fn set_from_history( input: In, - mut q_console: Query<(&mut ConsoleInputText, &ConsoleHistoryHandle)>, + mut q_console: Query<(&mut ConsoleInputText, &ConsoleAssetHandle)>, mut assets: ResMut>, mut history_idx: Local, mut filtered_history: Local>>, @@ -132,7 +132,7 @@ mod test { } app.add_step(3, |world: &mut World| { let handle = world - .query::<&ConsoleHistoryHandle>() + .query::<&ConsoleAssetHandle>() .single_mut(world) .cloned() .unwrap(); diff --git a/crates/cmd_prompt/src/assets.rs b/crates/cmd_prompt/src/assets.rs index 31394e7..9b1ec00 100644 --- a/crates/cmd_prompt/src/assets.rs +++ b/crates/cmd_prompt/src/assets.rs @@ -1,95 +1,201 @@ use bevy::{ - asset::{AssetLoader, AsyncReadExt}, + asset::{AssetLoadError, AssetLoader, AsyncReadExt, io::embedded::GetAssetServer}, + ecs::{component::Mutable, lifecycle::HookContext, world::DeferredWorld}, platform::collections::HashMap, }; use crate::prelude::*; -/// Environment variables for this console. Saved as '.env' files on disk. -#[derive(Asset, Default, Component, Debug, Deref, DerefMut, Reflect)] -pub struct ConsoleEnvVars(HashMap); - -/// Wrapper for the [Handle] of the [ConsoleEnvVarsAsset] for this [Console] -#[derive(Component, Debug, Deref, DerefMut, Reflect)] -pub struct ConsoleEnvVarsHandle(pub Handle); - -/// Loader for the [ConsoleEnvVarsAsset] -#[derive(Reflect, Default, Debug)] -pub struct ConsoleEnvVarsLoader; -impl AssetLoader for ConsoleEnvVarsLoader { - type Asset = ConsoleEnvVars; - type Settings = (); - type Error = BevyError; - - async fn load( - &self, - reader: &mut dyn bevy::asset::io::Reader, - _settings: &(), - load_context: &mut bevy::asset::LoadContext<'_>, - ) -> Result { - let mut buf = String::new(); - reader.read_to_string(&mut buf).await?; - let map = buf - .split('\n') - .filter_map(|s| { - let mut split = s.split('=').map(|s| s.to_string()).collect::>(); - if split.len() == 2 { - Some((std::mem::take(&mut split[0]), std::mem::take(&mut split[1]))) +mod assets_impl { + use super::*; + + /// Environment variables for this console. Saved as '.env' files on disk. + /// Follows conventional '.env' format. + #[derive(Asset, Default, Component, Debug, Deref, DerefMut, Reflect, Clone)] + pub struct ConsoleEnvVars(pub HashMap); + + /// Command history of this [Console]. Saved as '.history' files on disk. + /// Simple line-separated list of executed commands. + #[derive(Default, Asset, Debug, Deref, DerefMut, Reflect, Clone)] + pub struct ConsoleHistory(pub Vec); +} +pub use assets_impl::*; + +mod wrappers { + use bevy::asset::{AsAssetId, AssetLoadFailedEvent}; + + use super::*; + + /// A handle to some asset. In particular, this is used with [ConsoleEnvVars] and [ConsoleHistory] + #[derive(Component, Debug, Reflect, Default, Clone)] + #[component(on_insert=Self::on_insert)] + pub struct ConsoleAssetHandle { + path: Option, + handle: Handle, + } + impl ConsoleAssetHandle { + pub fn new(path: String) -> Self { + Self { + path: Some(path), + handle: Handle::default(), + } + } + + pub fn from_handle(handle: Handle) -> Self { + Self { path: None, handle } + } + + pub fn path(&self) -> Option<&String> { + self.path.as_ref() + } + + pub fn handle(&self) -> &Handle { + &self.handle + } + + fn on_insert(mut world: DeferredWorld, ctx: HookContext) { + let handle = { + let server = world.get_asset_server(); + let this = world.get::(ctx.entity).unwrap(); + if let Some(path) = this.path() { + server.load(path.clone()) } else { - warn!( - "Got invalid line while reading {}:\n'{}'", - load_context.path(), - s - ); - None + server.add(A::default()) } - }) - .collect::>(); - Ok(ConsoleEnvVars(map)) + }; + let mut this = world.get_mut::(ctx.entity).unwrap(); + this.handle = handle; + } } + impl AutoCreateAsset for ConsoleAssetHandle { + type Target = A; + fn set_handle(&mut self, handle: Handle) { + self.handle = handle + } + fn id(&self) -> AssetId { + self.handle.id() + } + } + impl AsAssetId for ConsoleAssetHandle { + type Asset = A; + fn as_asset_id(&self) -> AssetId { + self.handle.id() + } + } + + /// This trait will automatically create an asset on disk if it does not exist at load time. + pub trait AutoCreateAsset: Component + Sized { + type Target: Asset + Default; - fn extensions(&self) -> &[&str] { - &["env"] + fn set_handle(&mut self, handle: Handle); + fn id(&self) -> AssetId; + + fn check_assets( + mut reader: MessageReader>, + mut these: Query<&mut Self>, + server: Res, + ) { + for val in reader.read() { + if let AssetLoadError::AssetReaderError(_) = val.error { + // TODO: try writing an empty version to the path. + warn!( + "Failed to load path {:?} + NOTE: Eventually, this will result in the asset being automatically created on disk or in the server. + However, this relies on the AssetSaver struct, which is slated to release in Bevy 0.19. + For now, the asset is added with its default value, but _not_ saved to disk.", val.path + ); + if let Some(mut this) = these.iter_mut().find(|this| this.id() == val.id) { + this.set_handle(server.add(Self::Target::default())); + } + } + } + } } } +pub use wrappers::*; + +mod loaders { + use super::*; + + /// Loader for [ConsoleEnvVars] + #[derive(Reflect, Default, Debug)] + pub struct ConsoleEnvVarsLoader; + impl AssetLoader for ConsoleEnvVarsLoader { + type Asset = ConsoleEnvVars; + type Settings = (); + type Error = BevyError; -/// Command history of this [Console]. -#[derive(Default, Asset, Debug, Deref, DerefMut, Reflect)] -pub struct ConsoleHistory(Vec); - -/// Wrapper around the [Handle] for the [ConsoleHistoryAsset] for this [Console] -#[derive(Component, Debug, Deref, DerefMut, Reflect, Clone)] -pub struct ConsoleHistoryHandle(pub Handle); - -/// Loader for the [ConsoleHistoryAsset] -#[derive(Reflect, Default, Debug)] -pub struct ConsoleHistoryLoader; -impl AssetLoader for ConsoleHistoryLoader { - type Asset = ConsoleHistory; - type Settings = (); - type Error = BevyError; - - async fn load( - &self, - reader: &mut dyn bevy::asset::io::Reader, - _settings: &(), - _load_context: &mut bevy::asset::LoadContext<'_>, - ) -> Result { - let mut buf = String::new(); - reader.read_to_string(&mut buf).await?; - // todo: not memory efficient - let vec = buf.split('\n').map(|s| s.to_owned()).collect::>(); - Ok(ConsoleHistory(vec)) + async fn load( + &self, + reader: &mut dyn bevy::asset::io::Reader, + _settings: &(), + load_context: &mut bevy::asset::LoadContext<'_>, + ) -> Result { + let mut buf = String::new(); + reader.read_to_string(&mut buf).await?; + let map = buf + .split('\n') + .filter_map(|s| { + let mut split = s.split('=').map(|s| s.to_string()).collect::>(); + if split.len() == 2 { + Some((std::mem::take(&mut split[0]), std::mem::take(&mut split[1]))) + } else { + warn!( + "Got invalid line while reading {}:\n'{}'", + load_context.path(), + s + ); + None + } + }) + .collect::>(); + Ok(ConsoleEnvVars(map)) + } + + fn extensions(&self) -> &[&str] { + &["env"] + } } - fn extensions(&self) -> &[&str] { - &["hist"] + /// Loader for [ConsoleHistory] + #[derive(Reflect, Default, Debug)] + pub struct ConsoleHistoryLoader; + impl AssetLoader for ConsoleHistoryLoader { + type Asset = ConsoleHistory; + type Settings = (); + type Error = BevyError; + + async fn load( + &self, + reader: &mut dyn bevy::asset::io::Reader, + _settings: &(), + _load_context: &mut bevy::asset::LoadContext<'_>, + ) -> Result { + let mut buf = String::new(); + reader.read_to_string(&mut buf).await?; + // todo: not memory efficient + let vec = buf.split('\n').map(|s| s.to_owned()).collect::>(); + Ok(ConsoleHistory(vec)) + } + + fn extensions(&self) -> &[&str] { + &["hist"] + } } } +pub use loaders::*; pub fn plugin(app: &mut App) { app.register_asset_loader(ConsoleHistoryLoader); app.init_asset::(); app.register_asset_loader(ConsoleEnvVarsLoader); app.init_asset::(); + app.add_systems( + PreUpdate, + ConsoleAssetHandle::::check_assets, + ); + app.add_systems( + PreUpdate, + ConsoleAssetHandle::::check_assets, + ); } diff --git a/crates/cmd_prompt/src/test_harness.rs b/crates/cmd_prompt/src/test_harness.rs index d0dc9eb..20e0b64 100644 --- a/crates/cmd_prompt/src/test_harness.rs +++ b/crates/cmd_prompt/src/test_harness.rs @@ -28,6 +28,6 @@ pub fn plugin(app: &mut App) { } fn setup(mut commands: Commands, mut focus: ResMut) { - let id = commands.spawn(Console::new(None, None)).id(); + let id = commands.spawn(Console).id(); focus.0 = Some(id); } diff --git a/crates/cmd_prompt/src/ui/console.rs b/crates/cmd_prompt/src/ui/console.rs index d72b676..3bf3bb8 100644 --- a/crates/cmd_prompt/src/ui/console.rs +++ b/crates/cmd_prompt/src/ui/console.rs @@ -1,6 +1,5 @@ use crate::prelude::*; use bevy::{ - asset::io::embedded::GetAssetServer, ecs::{lifecycle::HookContext, world::DeferredWorld}, input_focus::InputFocus, }; @@ -14,46 +13,16 @@ use bevy::{ ConsoleBufferFlags, ConsolePrompt, ConsoleInputText, - TextFont + TextFont, + ConsoleAssetHandle, + ConsoleAssetHandle, )] #[component(on_add=Self::on_add)] -pub struct Console { - /// Path to the history file. If unset, will not serialize. - history_path: Option, - /// Asset ID of the console history. - history: Handle, - /// Path to the environment variables file. If unset, will not serialize. - vars_path: Option, - /// Asset ID of the environment variables asset. - vars: Handle, -} +pub struct Console; impl Console { - pub fn new(history_path: Option, vars_path: Option) -> Self { - Self { - history_path, - vars_path, - history: Handle::default(), - vars: Handle::default(), - } - } pub(crate) fn on_add<'w>(mut world: DeferredWorld<'w>, ctx: HookContext) { - // load assets - let this = world.get::(ctx.entity).unwrap(); - let server = world.get_asset_server(); - let history = if let Some(path) = this.history_path.as_ref() { - server.load::(path) - } else { - server.add(ConsoleHistory::default()) - }; - let vars = if let Some(path) = this.vars_path.as_ref() { - server.load::(path) - } else { - server.add(ConsoleEnvVars::default()) - }; let bundle = ( Name::new("Console"), - ConsoleHistoryHandle(history), - ConsoleEnvVarsHandle(vars), Node { display: Display::Flex, flex_direction: FlexDirection::ColumnReverse, -- 2.51.2