//! Tauri-Bevy integration plugin //! //! This module provides the main integration between Tauri and Bevy, handling //! the main application loop and event bridging. use borders_core::app::{App, Plugin, Update}; use borders_core::time::Time; use std::cell::RefCell; use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::time::Duration; use tauri::{Manager, RunEvent}; #[cfg(not(target_arch = "wasm32"))] use std::time::Instant; #[cfg(target_arch = "wasm32")] use web_time::Instant; use crate::render_bridge::{TauriRenderBridgeTransport, cache_leaderboard_snapshot_system}; const TARGET_FPS: f64 = 60.0; pub fn generate_tauri_context() -> tauri::Context { tauri::generate_context!() } fn setup_tauri_integration(app: &mut App, tauri_app: &tauri::AppHandle, shared_render_state: Arc>>, shared_leaderboard_state: Arc>>) { tracing::debug!("Setup tauri integration"); // Register state for render bridge commands tauri_app.manage(Arc::new(Mutex::new(None::))); tauri_app.manage(Arc::new(Mutex::new(None::))); // InputState - shared between Tauri commands and ECS systems let input_state_shared = Arc::new(Mutex::new(borders_core::ui::input::InputState::new())); tauri_app.manage(input_state_shared.clone()); app.insert_non_send_resource(input_state_shared); // Register shared state with Tauri (for get_game_state command) tauri_app.manage(shared_render_state.clone()); tauri_app.manage(shared_leaderboard_state.clone()); // Get the message queue from the transport (already added as plugin) let transport = app.world().get_resource::>().expect("RenderBridge should be added by plugin"); let message_queue = transport.transport.inbound_messages(); tauri_app.manage(message_queue); // Store shared states in world app.insert_non_send_resource(shared_leaderboard_state); } pub struct TauriPlugin { setup: Box tauri::App + Send + Sync>, } impl TauriPlugin { pub fn new(setup: F) -> Self where F: Fn() -> tauri::App + Send + Sync + 'static, { Self { setup: Box::new(setup) } } } impl TauriPlugin { pub fn build_and_run(self, mut app: App) -> ! { let tauri_app = (self.setup)(); // Create shared state for game state recovery let shared_render_state = Arc::new(Mutex::new(None::)); let shared_leaderboard_state = Arc::new(Mutex::new(None::)); // Create transport for Tauri frontend (handles both render and UI communication) let transport = TauriRenderBridgeTransport::new(tauri_app.handle().clone(), shared_render_state.clone()); // Add the render bridge plugin to handle all frontend communication borders_core::ui::FrontendPlugin::new(transport).build(&mut app); // Set up Tauri integration directly (no startup system needed) setup_tauri_integration(&mut app, tauri_app.handle(), shared_render_state, shared_leaderboard_state); // Add the leaderboard caching system app.add_systems(Update, cache_leaderboard_snapshot_system); // Run the app run_tauri_app(app, tauri_app); std::process::exit(0) } } pub fn run_tauri_app(app: App, tauri_app: tauri::App) { let app_rc = Rc::new(RefCell::new(app)); let mut tauri_app = tauri_app; let mut is_initialized = false; let mut last_frame_time = Instant::now(); let target_frame_duration = Duration::from_secs_f64(1.0 / TARGET_FPS); loop { let frame_start = Instant::now(); #[allow(deprecated)] tauri_app.run_iteration(move |_app_handle, event: RunEvent| { match event { tauri::RunEvent::Ready => { // Event acknowledged, actual setup happens below } tauri::RunEvent::ExitRequested { .. } => { // Track session end and flush analytics before exit if borders_core::telemetry::client().is_some() { tracing::debug!("ExitRequested: tracking session end and flushing analytics"); // Create a minimal runtime for blocking operations let runtime = tokio::runtime::Builder::new_current_thread().enable_time().enable_io().build().expect("Failed to create tokio runtime for flush"); runtime.block_on(async { // Track session end event borders_core::telemetry::track_session_end().await; // Flush all pending events (the batch-triggered send is now synchronous) if let Some(client) = borders_core::telemetry::client() { let timeout = std::time::Duration::from_millis(500); match tokio::time::timeout(timeout, client.flush()).await { Ok(_) => { tracing::debug!("Analytics flushed successfully before exit") } Err(_) => { tracing::warn!("Analytics flush timed out after 500ms") } } } }); } } _ => (), } }); if tauri_app.webview_windows().is_empty() { tauri_app.cleanup_before_exit(); break; } // Initialize game plugin on first iteration after Tauri is ready if !is_initialized { let mut app = app_rc.borrow_mut(); // Add core game plugin borders_core::GamePlugin::new(borders_core::plugin::NetworkMode::Local).build(&mut app); app.run_startup(); app.finish(); app.cleanup(); is_initialized = true; last_frame_time = Instant::now(); // Reset timer after initialization tracing::info!("Game initialized"); } // Update time resource with delta from PREVIOUS frame let mut app = app_rc.borrow_mut(); let delta = frame_start.duration_since(last_frame_time); if let Some(mut time) = app.world_mut().get_resource_mut::