diff --git a/Cargo.lock b/Cargo.lock index 4687ace..35d5317 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6039,6 +6039,7 @@ dependencies = [ "sprimo-config", "sprimo-platform", "sprimo-protocol", + "sprimo-runtime-core", "sprimo-sprite", "thiserror 2.0.18", "tokio", diff --git a/crates/sprimo-app/Cargo.toml b/crates/sprimo-app/Cargo.toml index 251096f..226fc7e 100644 --- a/crates/sprimo-app/Cargo.toml +++ b/crates/sprimo-app/Cargo.toml @@ -15,6 +15,7 @@ sprimo-api = { path = "../sprimo-api" } sprimo-config = { path = "../sprimo-config" } sprimo-platform = { path = "../sprimo-platform" } sprimo-protocol = { path = "../sprimo-protocol" } +sprimo-runtime-core = { path = "../sprimo-runtime-core" } sprimo-sprite = { path = "../sprimo-sprite" } thiserror.workspace = true tokio.workspace = true diff --git a/crates/sprimo-app/src/main.rs b/crates/sprimo-app/src/main.rs index d5eb0f5..7ad9db9 100644 --- a/crates/sprimo-app/src/main.rs +++ b/crates/sprimo-app/src/main.rs @@ -6,10 +6,9 @@ use bevy::render::render_resource::{Extent3d, TextureDimension, TextureFormat}; use bevy::window::{PrimaryWindow, WindowLevel, WindowMode, WindowPlugin, WindowPosition}; use image::{DynamicImage, GenericImageView, Rgba}; use raw_window_handle::{HasWindowHandle, RawWindowHandle}; -use sprimo_api::{ApiConfig, ApiState}; -use sprimo_config::{save, AppConfig, ConfigError}; use sprimo_platform::{create_adapter, PlatformAdapter}; use sprimo_protocol::v1::{CommandEnvelope, FrontendCommand, FrontendState, FrontendStateSnapshot}; +use sprimo_runtime_core::{RuntimeCore, RuntimeCoreError}; use sprimo_sprite::{ load_manifest, resolve_pack_path, AnimationDefinition, SpriteError, SpritePackManifest, }; @@ -20,8 +19,7 @@ use std::sync::{Arc, RwLock}; use std::time::Duration; use thiserror::Error; use tokio::runtime::Runtime; -use tokio::sync::mpsc as tokio_mpsc; -use tracing::{error, info, warn}; +use tracing::{info, warn}; const APP_NAME: &str = "sprimo"; const DEFAULT_PACK: &str = "default"; @@ -35,7 +33,7 @@ const WINDOWS_COLOR_KEY: [u8; 3] = [255, 0, 255]; #[derive(Debug, Error)] enum AppError { #[error("{0}")] - Config(#[from] ConfigError), + RuntimeCore(#[from] RuntimeCoreError), } #[derive(Resource, Clone)] @@ -45,10 +43,7 @@ struct SharedSnapshot(Arc>); struct PlatformResource(Arc); #[derive(Resource)] -struct ConfigResource { - path: PathBuf, - config: AppConfig, -} +struct RuntimeCoreResource(Arc); #[derive(Resource, Copy, Clone)] struct DurableState { @@ -181,35 +176,30 @@ fn main() -> Result<(), AppError> { .compact() .init(); - let (config_path, mut config) = sprimo_config::load_or_create(APP_NAME)?; - if config.window.click_through { - config.window.click_through = false; - if let Err(err) = save(&config_path, &config) { - warn!(%err, "failed to persist click-through disable at startup"); - } - } let platform: Arc = create_adapter().into(); - let capabilities = platform.capabilities(); - - let mut snapshot = FrontendStateSnapshot::idle(capabilities); - snapshot.x = config.window.x; - snapshot.y = config.window.y; - snapshot.scale = config.window.scale; - snapshot.flags.click_through = false; - snapshot.flags.always_on_top = config.window.always_on_top; - snapshot.flags.visible = config.window.visible; - snapshot.active_sprite_pack = config.sprite.selected_pack.clone(); - let shared_snapshot = Arc::new(RwLock::new(snapshot)); + let runtime_core = Arc::new(RuntimeCore::new(APP_NAME, platform.capabilities())?); + let shared_snapshot = runtime_core.snapshot(); + let config = runtime_core + .config() + .read() + .expect("runtime core config lock poisoned") + .clone(); let runtime = Runtime::new().expect("tokio runtime"); - let (api_command_tx, api_command_rx) = tokio_mpsc::channel(1_024); let (bevy_command_tx, bevy_command_rx) = mpsc::channel(); let (hotkey_tx, hotkey_rx) = mpsc::channel(); - spawn_api(&runtime, &config, Arc::clone(&shared_snapshot), api_command_tx); + runtime_core.spawn_api(&runtime); + let command_rx = runtime_core.command_receiver(); runtime.spawn(async move { - let mut rx = api_command_rx; - while let Some(command) = rx.recv().await { + loop { + let next = { + let mut receiver = command_rx.lock().await; + receiver.recv().await + }; + let Some(command) = next else { + break; + }; if bevy_command_tx.send(command).is_err() { break; } @@ -245,11 +235,8 @@ fn main() -> Result<(), AppError> { app.insert_resource(ClearColor(window_clear_color())); app.insert_resource(SharedSnapshot(shared_snapshot)); + app.insert_resource(RuntimeCoreResource(Arc::clone(&runtime_core))); app.insert_resource(PlatformResource(Arc::clone(&platform))); - app.insert_resource(ConfigResource { - path: config_path, - config, - }); app.insert_resource(DurableState { state: FrontendState::Idle, }); @@ -281,23 +268,6 @@ fn main() -> Result<(), AppError> { Ok(()) } -fn spawn_api( - runtime: &Runtime, - config: &AppConfig, - shared_snapshot: Arc>, - api_command_tx: tokio_mpsc::Sender, -) { - let mut api_config = ApiConfig::default_with_token(config.api.auth_token.clone()); - api_config.bind_addr = ([127, 0, 0, 1], config.api.port).into(); - let api_state = Arc::new(ApiState::new(api_config.clone(), shared_snapshot, api_command_tx)); - - runtime.spawn(async move { - if let Err(err) = sprimo_api::run_server(api_config, api_state).await { - error!(%err, "api server exited"); - } - }); -} - fn default_asset_root() -> PathBuf { std::env::current_dir() .unwrap_or_else(|_| PathBuf::from(".")) @@ -307,7 +277,7 @@ fn default_asset_root() -> PathBuf { fn setup_scene( mut commands: Commands, - config: Res, + runtime_core: Res, root: Res, mut texture_layouts: ResMut>, mut images: ResMut>, @@ -323,7 +293,14 @@ fn setup_scene( ..default() }); - let selected = config.config.sprite.selected_pack.clone(); + let selected = runtime_core + .0 + .config() + .read() + .expect("runtime core config lock poisoned") + .sprite + .selected_pack + .clone(); let runtime = match load_pack_runtime( &root.asset_root, &selected, @@ -749,8 +726,7 @@ fn attach_window_handle_once( fn poll_hotkey_recovery( ingress: NonSendMut, platform: Res, - mut config: ResMut, - snapshot: Res, + runtime_core: Res, mut pet_query: Query<&mut Visibility, With>, ) { while ingress.0.try_recv().is_ok() { @@ -761,18 +737,12 @@ fn poll_hotkey_recovery( if let Ok(mut visibility) = pet_query.get_single_mut() { *visibility = Visibility::Visible; } - - config.config.window.always_on_top = true; - config.config.window.visible = true; - if let Err(err) = save(&config.path, &config.config) { - warn!(%err, "failed to persist config after hotkey recovery"); - } - - if let Ok(mut guard) = snapshot.0.write() { - guard.flags.click_through = false; - guard.flags.always_on_top = true; - guard.flags.visible = true; - guard.last_error = None; + if let Err(err) = runtime_core.0.apply_command(&FrontendCommand::SetFlags { + click_through: None, + always_on_top: Some(true), + visible: Some(true), + }) { + warn!(%err, "failed to persist recovery flag config"); } } } @@ -784,7 +754,7 @@ fn poll_backend_commands( asset_server: Res, mut images: ResMut>, mut texture_layouts: ResMut>, - mut config: ResMut, + runtime_core: Res, snapshot: Res, mut durable_state: ResMut, mut reset: ResMut, @@ -800,6 +770,7 @@ fn poll_backend_commands( let Ok(envelope) = ingress.0.try_recv() else { break; }; + let command = envelope.command; let Ok((mut transform, mut visibility, mut atlas, mut image_handle)) = pet_query.get_single_mut() @@ -807,7 +778,14 @@ fn poll_backend_commands( continue; }; - match envelope.command { + if !matches!(command, FrontendCommand::SetSpritePack { .. }) { + if let Err(err) = runtime_core.0.apply_command(&command) { + warn!(%err, "failed to apply command in runtime core"); + continue; + } + } + + match command { FrontendCommand::SetState { state, ttl_ms } => { if !matches!(state, FrontendState::Success | FrontendState::Error) { durable_state.state = state; @@ -878,8 +856,11 @@ fn poll_backend_commands( window.resolution.set(size.x, size.y); } - config.config.sprite.selected_pack = requested; - if let Err(err) = save(&config.path, &config.config) { + if let Err(err) = runtime_core.0.apply_command( + &FrontendCommand::SetSpritePack { + pack_id_or_path: requested.clone(), + }, + ) { warn!(%err, "failed to persist sprite pack selection"); } @@ -899,37 +880,17 @@ fn poll_backend_commands( } } FrontendCommand::SetTransform { x, y, scale, .. } => { - if let Some(value) = x { - config.config.window.x = value; - } - if let Some(value) = y { - config.config.window.y = value; - } if let Some(value) = scale { transform.scale = Vec3::splat(value); - config.config.window.scale = value; if let Ok(mut window) = window_query.get_single_mut() { let size = window_size_for_pack(¤t_pack.runtime, value); window.resolution.set(size.x, size.y); } } - let _ = platform - .0 - .set_window_position(config.config.window.x, config.config.window.y); - if let Err(err) = save(&config.path, &config.config) { - warn!(%err, "failed to persist transform config"); - } - if let Ok(mut guard) = snapshot.0.write() { - if let Some(value) = x { - guard.x = value; + if x.is_some() || y.is_some() { + if let Ok(guard) = snapshot.0.read() { + let _ = platform.0.set_window_position(guard.x, guard.y); } - if let Some(value) = y { - guard.y = value; - } - if let Some(value) = scale { - guard.scale = value; - } - guard.last_error = None; } } FrontendCommand::SetFlags { @@ -937,36 +898,18 @@ fn poll_backend_commands( always_on_top, visible, } => { - if click_through.is_some() { - config.config.window.click_through = false; - } + let _ = click_through; if let Some(value) = always_on_top { let _ = platform.0.set_always_on_top(value); - config.config.window.always_on_top = value; } if let Some(value) = visible { let _ = platform.0.set_visible(value); - config.config.window.visible = value; *visibility = if value { Visibility::Visible } else { Visibility::Hidden }; } - - if let Err(err) = save(&config.path, &config.config) { - warn!(%err, "failed to persist flag config"); - } - if let Ok(mut guard) = snapshot.0.write() { - guard.flags.click_through = false; - if let Some(value) = always_on_top { - guard.flags.always_on_top = value; - } - if let Some(value) = visible { - guard.flags.visible = value; - } - guard.last_error = None; - } } FrontendCommand::Toast { text, .. } => { info!(toast = text, "toast command received"); @@ -977,6 +920,7 @@ fn poll_backend_commands( fn tick_state_reset( time: Res