Add: logical verification workflow for bevy
This commit is contained in:
@@ -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<RwLock<FrontendStateSnapshot>>);
|
||||
struct PlatformResource(Arc<dyn PlatformAdapter>);
|
||||
|
||||
#[derive(Resource)]
|
||||
struct ConfigResource {
|
||||
path: PathBuf,
|
||||
config: AppConfig,
|
||||
}
|
||||
struct RuntimeCoreResource(Arc<RuntimeCore>);
|
||||
|
||||
#[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<dyn PlatformAdapter> = 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<RwLock<FrontendStateSnapshot>>,
|
||||
api_command_tx: tokio_mpsc::Sender<CommandEnvelope>,
|
||||
) {
|
||||
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<ConfigResource>,
|
||||
runtime_core: Res<RuntimeCoreResource>,
|
||||
root: Res<SpritePackRoot>,
|
||||
mut texture_layouts: ResMut<Assets<TextureAtlasLayout>>,
|
||||
mut images: ResMut<Assets<Image>>,
|
||||
@@ -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<HotkeyIngress>,
|
||||
platform: Res<PlatformResource>,
|
||||
mut config: ResMut<ConfigResource>,
|
||||
snapshot: Res<SharedSnapshot>,
|
||||
runtime_core: Res<RuntimeCoreResource>,
|
||||
mut pet_query: Query<&mut Visibility, With<PetSprite>>,
|
||||
) {
|
||||
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<AssetServer>,
|
||||
mut images: ResMut<Assets<Image>>,
|
||||
mut texture_layouts: ResMut<Assets<TextureAtlasLayout>>,
|
||||
mut config: ResMut<ConfigResource>,
|
||||
runtime_core: Res<RuntimeCoreResource>,
|
||||
snapshot: Res<SharedSnapshot>,
|
||||
mut durable_state: ResMut<DurableState>,
|
||||
mut reset: ResMut<PendingStateReset>,
|
||||
@@ -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<Time>,
|
||||
runtime_core: Res<RuntimeCoreResource>,
|
||||
mut reset: ResMut<PendingStateReset>,
|
||||
mut animation: ResMut<AnimationResource>,
|
||||
current_pack: Res<CurrentPackResource>,
|
||||
@@ -993,6 +937,10 @@ fn tick_state_reset(
|
||||
|
||||
let next_state = reset.revert_to;
|
||||
let next_animation = default_animation_for_state(next_state);
|
||||
let _ = runtime_core.0.apply_command(&FrontendCommand::SetState {
|
||||
state: next_state,
|
||||
ttl_ms: None,
|
||||
});
|
||||
set_animation(&mut animation, ¤t_pack.runtime.clips, next_animation);
|
||||
if let Ok(mut atlas) = atlas_query.get_single_mut() {
|
||||
if let Some(frame) = current_frame(&animation, ¤t_pack.runtime.clips) {
|
||||
|
||||
Reference in New Issue
Block a user