use std::sync::Arc;
use bevy::prelude::*;
use rhai::{Dynamic, Engine, EvalAltResult};
use scoped_tls_hkt::scoped_thread_local;
scoped_thread_local!(
pub(crate) static mut WORLD: for<'a> &'a mut World
);
pub(crate) fn with_world<R>(f: impl FnOnce(&mut World) -> R) -> Result<R, Box<EvalAltResult>> {
if !WORLD.is_set() {
return Err("dialogue logic evaluated outside the dialogue runtime".into());
}
Ok(WORLD.with(f))
}
type Bridge = Arc<dyn Fn(&mut World, Vec<Dynamic>) -> Result<Dynamic, String> + Send + Sync>;
struct DialogueSystem {
name: String,
arity: usize,
bridge: Bridge,
}
#[derive(Resource, Default)]
pub struct DialogueSystems(Vec<DialogueSystem>);
impl DialogueSystems {
pub(crate) fn install_into(&self, engine: &mut Engine) {
for system in &self.0 {
install(engine, system);
}
}
}
fn install(engine: &mut Engine, system: &DialogueSystem) {
let bridge = system.bridge.clone();
let called = system.name.clone();
let call = move |args: Vec<Dynamic>| -> Result<Dynamic, Box<EvalAltResult>> {
with_world(|world| bridge(world, args))?
.map_err(|error| format!("{called}: {error}").into())
};
let name = system.name.as_str();
match system.arity {
0 => {
engine.register_fn(name, move || call(Vec::new()));
}
1 => {
engine.register_fn(name, move |a: Dynamic| call(vec![a]));
}
2 => {
engine.register_fn(name, move |a: Dynamic, b: Dynamic| call(vec![a, b]));
}
3 => {
engine.register_fn(name, move |a: Dynamic, b: Dynamic, c: Dynamic| {
call(vec![a, b, c])
});
}
4 => {
engine.register_fn(
name,
move |a: Dynamic, b: Dynamic, c: Dynamic, d: Dynamic| call(vec![a, b, c, d]),
);
}
n => warn!("dialogue system `{name}` takes {n} arguments; the limit is 4"),
}
}
pub trait ScriptArg: Sized {
fn from_dynamic(value: Dynamic) -> Result<Self, String>;
}
impl ScriptArg for bool {
fn from_dynamic(value: Dynamic) -> Result<Self, String> {
value
.as_bool()
.map_err(|got| format!("expected a bool, got {got}"))
}
}
impl ScriptArg for i64 {
fn from_dynamic(value: Dynamic) -> Result<Self, String> {
value
.as_int()
.map_err(|got| format!("expected an integer, got {got}"))
}
}
impl ScriptArg for f64 {
fn from_dynamic(value: Dynamic) -> Result<Self, String> {
value
.as_float()
.or_else(|_| value.as_int().map(|n| n as f64))
.map_err(|got| format!("expected a number, got {got}"))
}
}
impl ScriptArg for f32 {
fn from_dynamic(value: Dynamic) -> Result<Self, String> {
f64::from_dynamic(value).map(|n| n as f32)
}
}
impl ScriptArg for String {
fn from_dynamic(value: Dynamic) -> Result<Self, String> {
value
.into_string()
.map_err(|got| format!("expected a string, got {got}"))
}
}
impl ScriptArg for Dynamic {
fn from_dynamic(value: Dynamic) -> Result<Self, String> {
Ok(value)
}
}
pub trait ScriptArgs: Sized {
const ARITY: usize;
fn from_args(args: Vec<Dynamic>) -> Result<Self, String>;
}
impl ScriptArgs for () {
const ARITY: usize = 0;
fn from_args(_: Vec<Dynamic>) -> Result<Self, String> {
Ok(())
}
}
macro_rules! single_script_arg {
($($ty:ty),*) => {$(
impl ScriptArgs for $ty {
const ARITY: usize = 1;
fn from_args(mut args: Vec<Dynamic>) -> Result<Self, String> {
ScriptArg::from_dynamic(args.remove(0))
}
}
)*};
}
single_script_arg!(bool, i64, f64, f32, String, Dynamic);
macro_rules! tuple_script_args {
($count:literal: $($ty:ident),*) => {
impl<$($ty: ScriptArg),*> ScriptArgs for ($($ty,)*) {
const ARITY: usize = $count;
fn from_args(args: Vec<Dynamic>) -> Result<Self, String> {
let mut args = args.into_iter();
Ok(($($ty::from_dynamic(args.next().expect("arity checked"))?,)*))
}
}
};
}
tuple_script_args!(2: A, B);
tuple_script_args!(3: A, B, C);
tuple_script_args!(4: A, B, C, D);
pub trait ScriptReturn {
fn into_dynamic(self) -> Dynamic;
}
impl ScriptReturn for () {
fn into_dynamic(self) -> Dynamic {
Dynamic::UNIT
}
}
impl ScriptReturn for bool {
fn into_dynamic(self) -> Dynamic {
Dynamic::from_bool(self)
}
}
impl ScriptReturn for i64 {
fn into_dynamic(self) -> Dynamic {
Dynamic::from_int(self)
}
}
impl ScriptReturn for f64 {
fn into_dynamic(self) -> Dynamic {
Dynamic::from_float(self)
}
}
impl ScriptReturn for f32 {
fn into_dynamic(self) -> Dynamic {
Dynamic::from_float(f64::from(self))
}
}
impl ScriptReturn for String {
fn into_dynamic(self) -> Dynamic {
self.into()
}
}
impl ScriptReturn for Dynamic {
fn into_dynamic(self) -> Dynamic {
self
}
}
pub trait AddDialogueSystem {
fn add_dialogue_system<S, I, O, M>(&mut self, name: impl Into<String>, system: S) -> &mut Self
where
S: IntoSystem<In<I>, O, M> + 'static,
I: ScriptArgs + Send + Sync + 'static,
O: ScriptReturn + Send + Sync + 'static;
}
impl AddDialogueSystem for App {
fn add_dialogue_system<S, I, O, M>(&mut self, name: impl Into<String>, system: S) -> &mut Self
where
S: IntoSystem<In<I>, O, M> + 'static,
I: ScriptArgs + Send + Sync + 'static,
O: ScriptReturn + Send + Sync + 'static,
{
let id = self.world_mut().register_system(system);
let bridge: Bridge = Arc::new(move |world, args| {
let input = I::from_args(args)?;
world
.run_system_with(id, input)
.map(ScriptReturn::into_dynamic)
.map_err(|error| error.to_string())
});
self.init_resource::<DialogueSystems>();
self.world_mut()
.resource_mut::<DialogueSystems>()
.0
.push(DialogueSystem {
name: name.into(),
arity: I::ARITY,
bridge,
});
self
}
}