Galactica/crates/render/src/ui/executor.rs
Mark 847542a8cb
Reworked subelements
Cleaned up argument passing
Added persistent UI script variables
2024-02-08 20:40:12 -08:00

359 lines
10 KiB
Rust

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<RefCell<UiState>>,
last_player_state: u32,
last_scene: Option<ImmutableString>,
}
impl UiScriptExecutor {
pub fn new(ct: Arc<Content>, 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<RenderInput>,
event: InputEvent,
) -> Result<PlayerDirective> {
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<RenderInput>,
arg: Dynamic,
) -> Result<Option<PlayerDirective>> {
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::<Dynamic, _>(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::<PlayerDirective>() {
return Ok(Some(d.cast::<PlayerDirective>()));
} 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<RenderInput>) -> 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<RenderInput>) -> 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(());
}
}