use anyhow::{Context, Result}; use galactica_content::Content; use galactica_system::{phys::PhysSimShipHandle, PlayerDirective}; use galactica_util::rhai_error_to_anyhow; use log::{debug, error}; use rhai::{CallFnOptions, Dynamic, Engine, ImmutableString, Scope}; use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc}; use winit::event::VirtualKeyCode; use super::{ api::{self, KeyboardEvent, PlayerShipStateEvent, ScrollEvent}, UiConfig, UiElement, UiState, }; use crate::{ui::api::State, InputEvent, RenderInput, RenderState}; pub(crate) struct UiScriptExecutor { engine: Engine, scope: Scope<'static>, pub state: Rc>, last_player_state: u32, last_scene: Option, } impl UiScriptExecutor { pub fn new(ct: Arc, state: &mut RenderState) -> Self { let scope = Scope::new(); let elements = Rc::new(RefCell::new(UiState::new(ct.clone(), state))); // TODO: document all functions rhai provides let mut engine = Engine::new(); engine.set_max_expr_depths(0, 0); // Enables custom operators engine.set_fast_operators(false); api::register_into_engine( &mut engine, ct.clone(), state.text_font_system.clone(), elements.clone(), ); Self { engine, scope, state: elements, last_scene: None, last_player_state: 0, } } pub fn get_config(&self) -> UiConfig { (*self.state).borrow().config.clone() } pub fn process_input( &mut self, state: &mut RenderState, input: &Arc, event: InputEvent, ) -> Result { let current_scene = (*self.state).borrow().get_scene().clone(); if current_scene.is_none() { return Ok(PlayerDirective::None); } // First, check if this event is captured by any ui elements. let len = (*self.state).borrow().len(); // Iterate front to back // (this also ensures that sub-elements are handled BEFORE their container.) for i in (0..len).rev() { let mut ui_state = self.state.borrow_mut(); let arg = match ui_state.get_mut_by_idx(i).unwrap() { UiElement::Sprite(sprite) => sprite.handle_event(&input, state, &event), UiElement::Scrollbox(sbox) => sbox.handle_event(&input, state, &event), UiElement::RadialBar(_) | UiElement::Text(..) => None, UiElement::SubElement { parent, element } => { // Be very careful here, to avoid // borrowing mutably twice... let e_name = element.get_name(); let p_name = parent.clone(); let parent = ui_state.get_by_name(&p_name).unwrap(); match parent { UiElement::Scrollbox(sbox) => { let offset = sbox.offset; let element = ui_state.get_mut_by_name(&e_name).unwrap(); match element { UiElement::SubElement { element, .. } => match &mut **element { UiElement::Sprite(sprite) => sprite .handle_event_with_offset(&input, state, &event, offset), UiElement::Scrollbox(sbox) => { sbox.handle_event_with_offset(&input, state, &event, offset) } UiElement::RadialBar(_) | UiElement::Text(..) => None, UiElement::SubElement { .. } => unreachable!(), }, _ => unreachable!(), } } _ => unreachable!(), } } }; drop(ui_state); // Return if event hook returns any PlayerDirective (including None), // continue to the next element if the hook returns (). if let Some(arg) = arg { let result = self.run_event_callback(state, &input, arg)?; if let Some(result) = result { return Ok(result); } } } // If nothing was caught, check global events let arg = match event { InputEvent::Scroll(val) => Some(Dynamic::from(ScrollEvent { val })), InputEvent::Keyboard { down, key } => { let str = match key { VirtualKeyCode::A => Some("A"), VirtualKeyCode::B => Some("B"), VirtualKeyCode::C => Some("C"), VirtualKeyCode::D => Some("D"), VirtualKeyCode::E => Some("E"), VirtualKeyCode::F => Some("F"), VirtualKeyCode::G => Some("G"), VirtualKeyCode::H => Some("H"), VirtualKeyCode::I => Some("I"), VirtualKeyCode::J => Some("J"), VirtualKeyCode::K => Some("K"), VirtualKeyCode::L => Some("L"), VirtualKeyCode::M => Some("M"), VirtualKeyCode::N => Some("N"), VirtualKeyCode::O => Some("O"), VirtualKeyCode::P => Some("P"), VirtualKeyCode::Q => Some("Q"), VirtualKeyCode::R => Some("R"), VirtualKeyCode::S => Some("S"), VirtualKeyCode::T => Some("T"), VirtualKeyCode::U => Some("U"), VirtualKeyCode::V => Some("V"), VirtualKeyCode::W => Some("W"), VirtualKeyCode::X => Some("X"), VirtualKeyCode::Y => Some("Y"), VirtualKeyCode::Z => Some("Z"), VirtualKeyCode::Up => Some("up"), VirtualKeyCode::Down => Some("down"), VirtualKeyCode::Left => Some("left"), VirtualKeyCode::Right => Some("right"), VirtualKeyCode::Space => Some("space"), _ => None, }; if let Some(str) = str { Some(Dynamic::from(KeyboardEvent { down, key: ImmutableString::from(str), })) } else { None } } _ => None, }; if let Some(arg) = arg { Ok(self .run_event_callback(state, &input, arg)? .unwrap_or(PlayerDirective::None)) } else { return Ok(PlayerDirective::None); } } fn run_event_callback( &mut self, state: &mut RenderState, input: &Arc, arg: Dynamic, ) -> Result> { let current_scene = (*self.state).borrow().get_scene().clone(); if current_scene.is_none() { return Ok(None); } let current_scene = current_scene.unwrap(); let ct = (*self.state).borrow().ct.clone(); let ast = ct.config.ui_scenes.get(current_scene.as_str()).unwrap(); let d: Dynamic = rhai_error_to_anyhow::(self.engine.call_fn_with_options( CallFnOptions::new().rewind_scope(true), &mut self.scope, ast, "event", (State::new(state, input), arg.clone()), )) .with_context(|| format!("while calling `event()`")) .with_context(|| format!("in UI scene `{}`", current_scene.as_str()))?; if d.is::() { return Ok(Some(d.cast::())); } else if !(d.is_unit()) { error!( "`event()` in UI scene `{}` returned invalid type `{}`", d, current_scene.as_str() ) } return Ok(None); } /// Change the current scene pub fn set_scene(&mut self, state: &RenderState, input: &Arc) -> Result<()> { let current_scene = (*self.state).borrow().get_scene().clone(); if self.last_scene == current_scene { return Ok(()); } self.last_scene = current_scene.clone(); if current_scene.is_none() { return Ok(()); } debug!( "switched to {}, running `init()`", current_scene.as_ref().unwrap() ); let ct = (*self.state).borrow().ct.clone(); let ast = ct .config .ui_scenes .get(current_scene.as_ref().unwrap().as_str()) .unwrap(); // Clear previous state self.scope.clear(); self.state.borrow_mut().clear(); rhai_error_to_anyhow(self.engine.call_fn_with_options( CallFnOptions::new().rewind_scope(false), &mut self.scope, ast, "init", (State::new(state, input),), )) .with_context(|| format!("while running `init()`")) .with_context(|| format!("in ui scene `{}`", current_scene.as_ref().unwrap()))?; // Make sure input isn't borrowed (which would happen if the UI script adds a state class to the scope) // At this point, the only reference to input should be the one in main.rs. // Note how we always pass input as &Arc, not Arc. if Arc::strong_count(&input) != 1 { error!( "State has been captured in the scope of UI scene `{}`", current_scene.as_ref().unwrap() ); error!("Clearing scope to prevent a panic, this may break the UI script."); self.scope.clear(); }; return Ok(()); } /// Draw all ui elements pub fn draw(&mut self, state: &mut RenderState, input: &Arc) -> Result<()> { let ct = (*self.state).borrow().ct.clone(); // Initialize start scene if we haven't yet if (*self.state).borrow().get_scene().is_none() { (*self.state) .borrow_mut() .set_scene(ImmutableString::from(&ct.config.start_ui_scene)); } self.set_scene(state, input)?; let current_scene = (*self.state).borrow().get_scene().clone(); (*self.state).borrow_mut().step(state, input); // Run step() (if it is defined) let ast = ct .config .ui_scenes .get(current_scene.as_ref().unwrap().as_str()) .unwrap(); if ast.iter_functions().any(|x| x.name == "step") { rhai_error_to_anyhow(self.engine.call_fn_with_options( CallFnOptions::new().rewind_scope(true), &mut self.scope, ast, "step", (State::new(state, input),), )) .with_context(|| format!("while calling `step()`")) .with_context(|| format!("in ui scene `{}`", current_scene.as_ref().unwrap()))?; } // Send player state change events if { let player = input.player.ship; if let Some(player) = player { let ship = input.phys_img.get_ship(&PhysSimShipHandle(player)).unwrap(); if self.last_player_state == 0 || NonZeroU32::new(self.last_player_state).unwrap() != ship.ship.get_data().get_state().as_int() { self.last_player_state = ship.ship.get_data().get_state().as_int().into(); true } else { false } } else { self.last_player_state = 0; true } } { self.run_event_callback(state, &input, Dynamic::from(PlayerShipStateEvent {}))?; } let len = (*self.state).borrow().len(); for i in 0..len { match self.state.borrow().get_by_idx(i).unwrap() { UiElement::Sprite(sprite) => { sprite.push_to_buffer(&input, state); } UiElement::RadialBar(x) => { x.push_to_buffer(&input, state); } UiElement::Scrollbox(_) => {} UiElement::Text(..) => {} UiElement::SubElement { element, parent } => { match self.state.borrow().get_by_name(parent).unwrap() { UiElement::Scrollbox(sbox) => match &**element { UiElement::Sprite(sprite) => { sprite.push_to_buffer_with_offset(input, state, sbox.offset) } UiElement::RadialBar(..) => {} UiElement::Text(..) => {} UiElement::Scrollbox(..) => {} UiElement::SubElement { .. } => {} }, _ => {} } } } } //self.scope.rewind(0); return Ok(()); } }