359 lines
10 KiB
Rust
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(());
|
|
}
|
|
}
|