use std::collections::{HashMap, HashSet};
use rhai::{AST, Array, Dynamic, Engine, Map, Scope};
use nightshade::ecs::event::{Event, events};
use nightshade::ecs::script::components::ScriptSource;
use nightshade::ecs::world::SCRIPT;
use nightshade::prelude::{Entity, KeyCode, MouseState, Vec3, World, tracing};
use crate::command::{Command, CommandReply, submit_commands};
const TAG_ID_KEY: &str = "$id";
const TAG_COMMAND_KEY: &str = "$cmd";
#[derive(Clone, Default)]
pub enum CommandPolicy {
#[default]
AllowAll,
Allow(HashSet<String>),
}
impl CommandPolicy {
fn allows(&self, name: &str) -> bool {
match self {
CommandPolicy::AllowAll => true,
CommandPolicy::Allow(allowed) => allowed.contains(name),
}
}
}
pub struct ScriptRuntime {
pub enabled: bool,
pub policy: CommandPolicy,
engine: Engine,
compiled: HashMap<u64, AST>,
scopes: HashMap<Entity, Scope<'static>>,
global_scopes: HashMap<String, Scope<'static>>,
started_globals: HashSet<String>,
started_entities: HashSet<Entity>,
next_replies: Map,
assets: HashMap<String, Vec<u8>>,
random_state: u64,
}
const RANDOM_SEED: u64 = 0x853c_49e6_748f_ea9b;
fn new_engine() -> Engine {
let mut engine = Engine::new();
engine.set_max_operations(200_000);
engine.set_max_call_levels(64);
engine.set_max_string_size(8 * 1024);
engine.set_max_array_size(4096);
engine.set_max_expr_depths(0, 0);
register_api(&mut engine);
engine
}
impl Default for ScriptRuntime {
fn default() -> Self {
let engine = new_engine();
Self {
enabled: false,
policy: CommandPolicy::AllowAll,
engine,
compiled: HashMap::new(),
scopes: HashMap::new(),
global_scopes: HashMap::new(),
started_globals: HashSet::new(),
started_entities: HashSet::new(),
next_replies: Map::new(),
assets: HashMap::new(),
random_state: RANDOM_SEED,
}
}
}
#[derive(Default)]
pub struct ScriptReport {
pub commands: Vec<Command>,
pub errors: Vec<String>,
}
thread_local! {
static SCRIPT_RNG: std::cell::Cell<u64> = const { std::cell::Cell::new(RANDOM_SEED) };
}
fn next_unit_random() -> f64 {
SCRIPT_RNG.with(|cell| {
let mut state = cell.get();
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
cell.set(state);
((state >> 11) as f64) / ((1u64 << 53) as f64)
})
}
fn register_api(engine: &mut Engine) {
engine.register_fn("log", |message: &str| tracing::info!("[script] {message}"));
engine.register_fn("random", next_unit_random);
engine.register_fn("random_range", |low: f64, high: f64| {
low + next_unit_random() * (high - low)
});
engine.register_fn("random_int", |low: i64, high: i64| {
if high <= low {
low
} else {
low + (next_unit_random() * ((high - low + 1) as f64)) as i64
}
});
register_mixed_number_ops(engine);
register_color_helpers(engine);
engine.register_fn("entity_ref", |packed: i64| {
let entity = script_to_entity(packed);
let mut map = Map::new();
map.insert("Existing".into(), Dynamic::from(entity.id as i64));
Dynamic::from_map(map)
});
engine.register_fn("result", |index: i64| {
let mut map = Map::new();
map.insert("Result".into(), Dynamic::from(index));
Dynamic::from_map(map)
});
engine.register_fn("last", |commands: &mut Array| {
let index = commands.len().saturating_sub(1) as i64;
let mut map = Map::new();
map.insert("Result".into(), Dynamic::from(index));
Dynamic::from_map(map)
});
engine.register_fn("tag", |commands: &mut Array, id: rhai::ImmutableString| {
if let Some(last) = commands.last_mut() {
let command = std::mem::take(last);
let mut request = Map::new();
request.insert(TAG_ID_KEY.into(), Dynamic::from(id));
request.insert(TAG_COMMAND_KEY.into(), command);
*last = Dynamic::from_map(request);
}
});
crate::command::register_command_methods(engine);
}
fn register_mixed_number_ops(engine: &mut Engine) {
engine.register_fn("+", |a: i64, b: f64| a as f64 + b);
engine.register_fn("+", |a: f64, b: i64| a + b as f64);
engine.register_fn("-", |a: i64, b: f64| a as f64 - b);
engine.register_fn("-", |a: f64, b: i64| a - b as f64);
engine.register_fn("*", |a: i64, b: f64| a as f64 * b);
engine.register_fn("*", |a: f64, b: i64| a * b as f64);
engine.register_fn("/", |a: i64, b: f64| a as f64 / b);
engine.register_fn("/", |a: f64, b: i64| a / b as f64);
engine.register_fn("%", |a: i64, b: f64| a as f64 % b);
engine.register_fn("%", |a: f64, b: i64| a % b as f64);
engine.register_fn("<", |a: i64, b: f64| (a as f64) < b);
engine.register_fn("<", |a: f64, b: i64| a < b as f64);
engine.register_fn(">", |a: i64, b: f64| a as f64 > b);
engine.register_fn(">", |a: f64, b: i64| a > b as f64);
engine.register_fn("<=", |a: i64, b: f64| a as f64 <= b);
engine.register_fn("<=", |a: f64, b: i64| a <= b as f64);
engine.register_fn(">=", |a: i64, b: f64| a as f64 >= b);
engine.register_fn(">=", |a: f64, b: i64| a >= b as f64);
}
fn register_color_helpers(engine: &mut Engine) {
engine.register_fn("rgb", |r: f64, g: f64, b: f64| -> Array {
vec![
Dynamic::from(r),
Dynamic::from(g),
Dynamic::from(b),
Dynamic::from(1.0_f64),
]
});
engine.register_fn("rgba", |r: f64, g: f64, b: f64, a: f64| -> Array {
vec![
Dynamic::from(r),
Dynamic::from(g),
Dynamic::from(b),
Dynamic::from(a),
]
});
engine.register_fn("hsv", |h: f64, s: f64, v: f64| -> Array {
let (r, g, b) = hsv_to_rgb(h, s, v);
vec![
Dynamic::from(r),
Dynamic::from(g),
Dynamic::from(b),
Dynamic::from(1.0_f64),
]
});
}
fn hsv_to_rgb(hue: f64, saturation: f64, value: f64) -> (f64, f64, f64) {
let sector = hue.rem_euclid(1.0) * 6.0;
let chroma = value * saturation;
let secondary = chroma * (1.0 - (sector % 2.0 - 1.0).abs());
let base = value - chroma;
let (red, green, blue) = match sector as i64 {
0 => (chroma, secondary, 0.0),
1 => (secondary, chroma, 0.0),
2 => (0.0, chroma, secondary),
3 => (0.0, secondary, chroma),
4 => (secondary, 0.0, chroma),
_ => (chroma, 0.0, secondary),
};
(red + base, green + base, blue + base)
}
fn ast_defines_hook(ast: &AST, name: &str) -> bool {
ast.iter_functions()
.any(|function| function.name == name && function.params.is_empty())
}
fn reply_to_dynamic(reply: &CommandReply) -> Dynamic {
match reply {
CommandReply::None => Dynamic::UNIT,
CommandReply::Entity(entity) => Dynamic::from(entity_to_script(*entity)),
CommandReply::Bool(value) => Dynamic::from(*value),
CommandReply::Float(value) => Dynamic::from(*value as f64),
CommandReply::Int(value) => Dynamic::from(*value),
CommandReply::Text(value) => Dynamic::from(value.clone()),
CommandReply::Vector(value) => {
Dynamic::from_array(array3(Vec3::new(value[0], value[1], value[2])))
}
CommandReply::Entities(entities) => Dynamic::from_array(
entities
.iter()
.map(|entity| Dynamic::from(entity_to_script(*entity)))
.collect(),
),
CommandReply::Strings(strings) => Dynamic::from_array(
strings
.iter()
.map(|value| Dynamic::from(value.clone()))
.collect(),
),
CommandReply::Bytes(bytes) => Dynamic::from_blob(bytes.clone()),
CommandReply::Json(value) => rhai::serde::to_dynamic(value).unwrap_or(Dynamic::UNIT),
CommandReply::Error(message) => {
let mut map = Map::new();
map.insert("error".into(), Dynamic::from(message.clone()));
Dynamic::from_map(map)
}
}
}
fn entity_to_script(entity: Entity) -> i64 {
(((entity.id as u64) << 32) | entity.generation as u64) as i64
}
fn script_to_entity(value: i64) -> Entity {
let bits = value as u64;
Entity {
id: (bits >> 32) as u32,
generation: bits as u32,
}
}
fn hash_source(source: &str) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
source.hash(&mut hasher);
hasher.finish()
}
fn compiled_ast<'a>(
runtime: &'a mut ScriptRuntime,
source: &str,
errors: &mut Vec<String>,
) -> Option<&'a AST> {
let key = hash_source(source);
if !runtime.compiled.contains_key(&key) {
match runtime.engine.compile(source) {
Ok(ast) => {
runtime.compiled.insert(key, ast);
}
Err(error) => {
tracing::error!("script compile error: {error}");
errors.push(format!("compile error: {error}"));
return None;
}
}
}
runtime.compiled.get(&key)
}
pub fn script_runtime_forget_entity(runtime: &mut ScriptRuntime, entity: Entity) {
runtime.scopes.remove(&entity);
runtime.started_entities.remove(&entity);
}
pub fn script_check(source: &str) -> Result<(), String> {
new_engine()
.compile(source)
.map(|_| ())
.map_err(|error| error.to_string())
}
pub fn script_runtime_set_max_operations(runtime: &mut ScriptRuntime, max: u64) {
runtime.engine.set_max_operations(max);
}
pub fn script_runtime_set_max_array_size(runtime: &mut ScriptRuntime, max: usize) {
runtime.engine.set_max_array_size(max);
}
pub fn script_runtime_set_asset(runtime: &mut ScriptRuntime, name: &str, bytes: Vec<u8>) {
runtime.assets.insert(name.to_string(), bytes);
}
pub fn script_runtime_reset(runtime: &mut ScriptRuntime) {
runtime.compiled.clear();
runtime.scopes.clear();
runtime.global_scopes.clear();
runtime.started_globals.clear();
runtime.started_entities.clear();
runtime.next_replies.clear();
runtime.random_state = RANDOM_SEED;
}
pub fn run_scripts(world: &mut World, runtime: &mut ScriptRuntime) -> ScriptReport {
SCRIPT_RNG.with(|cell| cell.set(runtime.random_state));
let frame_events: Vec<Event> = events(world).to_vec();
for event in &frame_events {
if let Event::Despawned { entity } = event {
script_runtime_forget_entity(runtime, *entity);
}
}
let policy = runtime.policy.clone();
let dt = world.resources.window.timing.delta_time as f64;
let time = world.resources.window.timing.uptime_milliseconds as f64 / 1000.0;
let keyboard = &world.resources.input.keyboard;
let move_left =
keyboard.is_key_pressed(KeyCode::ArrowLeft) || keyboard.is_key_pressed(KeyCode::KeyA);
let move_right =
keyboard.is_key_pressed(KeyCode::ArrowRight) || keyboard.is_key_pressed(KeyCode::KeyD);
let launch = keyboard.is_key_pressed(KeyCode::Space);
let mut keys = Map::new();
for keycode in keyboard.keystates.keys() {
if keyboard.is_key_pressed(*keycode) {
keys.insert(format!("{keycode:?}").into(), Dynamic::from(true));
}
}
let mut pressed = Map::new();
for keycode in &keyboard.just_pressed_keys {
pressed.insert(format!("{keycode:?}").into(), Dynamic::from(true));
}
let mouse = &world.resources.input.mouse;
let mut mouse_map = Map::new();
mouse_map.insert("x".into(), Dynamic::from(mouse.position.x as f64));
mouse_map.insert("y".into(), Dynamic::from(mouse.position.y as f64));
mouse_map.insert(
"left".into(),
Dynamic::from(mouse.state.contains(MouseState::LEFT_CLICKED)),
);
mouse_map.insert(
"right".into(),
Dynamic::from(mouse.state.contains(MouseState::RIGHT_CLICKED)),
);
mouse_map.insert(
"middle".into(),
Dynamic::from(mouse.state.contains(MouseState::MIDDLE_CLICKED)),
);
mouse_map.insert(
"left_pressed".into(),
Dynamic::from(mouse.state.contains(MouseState::LEFT_JUST_PRESSED)),
);
mouse_map.insert(
"right_pressed".into(),
Dynamic::from(mouse.state.contains(MouseState::RIGHT_JUST_PRESSED)),
);
mouse_map.insert("scroll".into(), Dynamic::from(mouse.wheel_delta.y as f64));
#[cfg(feature = "picking")]
let ground_hit = nightshade::prelude::get_ground_position_from_screen(
world,
world.resources.input.mouse.position,
0.0,
);
#[cfg(not(feature = "picking"))]
let ground_hit: Option<Vec3> = None;
let ground = match ground_hit {
Some(point) => array3(point),
None => Array::new(),
};
let pointer_over_ui = world
.resources
.retained_ui
.interaction
.hovered_entity
.is_some();
let mut named = Map::new();
let mut positions = Map::new();
let mut velocities = Map::new();
for (name, entity) in &world.resources.entities.names {
named.insert(
name.as_str().into(),
Dynamic::from(entity_to_script(*entity)),
);
let (position, velocity) = entity_pos_vel(world, *entity);
positions.insert(name.as_str().into(), Dynamic::from(position));
velocities.insert(name.as_str().into(), Dynamic::from(velocity));
}
let mut tag_lists: HashMap<String, Array> = HashMap::new();
for (entity, tags) in &world.resources.entities.tags {
let (position, velocity) = entity_pos_vel(world, *entity);
let mut record = Map::new();
record.insert("entity".into(), Dynamic::from(entity_to_script(*entity)));
record.insert("pos".into(), Dynamic::from(position));
record.insert("vel".into(), Dynamic::from(velocity));
let record = Dynamic::from_map(record);
for tag in tags {
tag_lists
.entry(tag.clone())
.or_default()
.push(record.clone());
}
}
let mut tagged = Map::new();
for (tag, list) in tag_lists {
tagged.insert(tag.as_str().into(), Dynamic::from(list));
}
let mut produced: Vec<Command> = Vec::new();
let mut result_tags: Vec<(usize, String)> = Vec::new();
let mut errors: Vec<String> = Vec::new();
let replies = std::mem::take(&mut runtime.next_replies);
let global_scripts: Vec<(String, String)> = world
.resources
.global_scripts
.entries
.iter()
.filter(|script| script.enabled && !script.source.trim().is_empty())
.map(|script| (script.name.clone(), script.source.clone()))
.collect();
for (name, source) in &global_scripts {
let Some(ast) = compiled_ast(runtime, source, &mut errors).cloned() else {
continue;
};
let run_start =
!runtime.started_globals.contains(name) && ast_defines_hook(&ast, "on_start");
runtime.started_globals.insert(name.clone());
let scope = runtime.global_scopes.entry(name.clone()).or_default();
if !scope.contains("state") {
scope.set_value("state", Map::new());
}
if !scope.contains("assets") {
let mut assets = Map::new();
for (name, bytes) in &runtime.assets {
assets.insert(name.as_str().into(), Dynamic::from_blob(bytes.clone()));
}
scope.set_value("assets", assets);
}
scope.set_value("dt", dt);
scope.set_value("time", time);
scope.set_value("tau", std::f64::consts::TAU);
scope.set_value("pi", std::f64::consts::PI);
scope.set_value("replies", replies.clone());
scope.set_value("move_left", move_left);
scope.set_value("move_right", move_right);
scope.set_value("launch", launch);
scope.set_value("keys", keys.clone());
scope.set_value("pressed", pressed.clone());
scope.set_value("mouse", mouse_map.clone());
scope.set_value("named", named.clone());
scope.set_value("positions", positions.clone());
scope.set_value("velocities", velocities.clone());
scope.set_value("tagged", tagged.clone());
scope.set_value("ground", ground.clone());
scope.set_value("pointer_over_ui", pointer_over_ui);
scope.set_value("events", events_array(&frame_events));
scope.set_value("commands", Array::new());
if run_start {
if let Err(error) = runtime.engine.call_fn::<()>(scope, &ast, "on_start", ()) {
tracing::warn!("global script '{name}' on_start error: {error}");
errors.push(format!("on_start error: {error}"));
} else {
collect_commands(scope, &mut produced, &mut result_tags, &mut errors, &policy);
}
scope.set_value("commands", Array::new());
}
if ast_defines_hook(&ast, "on_tick") {
if let Err(error) = runtime.engine.call_fn::<()>(scope, &ast, "on_tick", ()) {
tracing::warn!("global script '{name}' on_tick error: {error}");
errors.push(format!("on_tick error: {error}"));
continue;
}
collect_commands(scope, &mut produced, &mut result_tags, &mut errors, &policy);
}
}
let mut scripted: Vec<(Entity, String)> = Vec::new();
for entity in world.core.query_entities(SCRIPT).collect::<Vec<_>>() {
if let Some(script) = world.core.get_script(entity)
&& script.enabled
&& let ScriptSource::Embedded { source } = &script.source
{
scripted.push((entity, source.clone()));
}
}
for (entity, source) in &scripted {
let position = world
.core
.get_local_transform(*entity)
.map(|transform| transform.translation)
.unwrap_or_else(Vec3::zeros);
let velocity = nightshade::ecs::physics::resources::physics_world_linear_velocity(
&world.resources.physics,
*entity,
)
.unwrap_or_else(Vec3::zeros);
let entity_events = relevant_events(&frame_events, *entity);
let Some(ast) = compiled_ast(runtime, source, &mut errors).cloned() else {
continue;
};
let run_start =
!runtime.started_entities.contains(entity) && ast_defines_hook(&ast, "on_start");
runtime.started_entities.insert(*entity);
let scope = runtime.scopes.entry(*entity).or_default();
if !scope.contains("state") {
scope.set_value("state", Map::new());
}
if !scope.contains("assets") {
let mut assets = Map::new();
for (name, bytes) in &runtime.assets {
assets.insert(name.as_str().into(), Dynamic::from_blob(bytes.clone()));
}
scope.set_value("assets", assets);
}
scope.set_value("self", entity_to_script(*entity));
scope.set_value("dt", dt);
scope.set_value("time", time);
scope.set_value("tau", std::f64::consts::TAU);
scope.set_value("pi", std::f64::consts::PI);
scope.set_value("replies", replies.clone());
scope.set_value("move_left", move_left);
scope.set_value("move_right", move_right);
scope.set_value("launch", launch);
scope.set_value("keys", keys.clone());
scope.set_value("pressed", pressed.clone());
scope.set_value("mouse", mouse_map.clone());
scope.set_value("pos", array3(position));
scope.set_value("vel", array3(velocity));
scope.set_value("named", named.clone());
scope.set_value("positions", positions.clone());
scope.set_value("velocities", velocities.clone());
scope.set_value("tagged", tagged.clone());
scope.set_value("ground", ground.clone());
scope.set_value("pointer_over_ui", pointer_over_ui);
scope.set_value("events", events_array(&entity_events));
scope.set_value("commands", Array::new());
if run_start {
if let Err(error) = runtime.engine.call_fn::<()>(scope, &ast, "on_start", ()) {
tracing::warn!("script on_start error: {error}");
errors.push(format!("on_start error: {error}"));
} else {
collect_commands(scope, &mut produced, &mut result_tags, &mut errors, &policy);
}
scope.set_value("commands", Array::new());
}
if ast_defines_hook(&ast, "on_tick") {
if let Err(error) = runtime.engine.call_fn::<()>(scope, &ast, "on_tick", ()) {
tracing::warn!("script on_tick error: {error}");
errors.push(format!("on_tick error: {error}"));
continue;
}
collect_commands(scope, &mut produced, &mut result_tags, &mut errors, &policy);
}
}
let replies_out = submit_commands(world, &produced);
let mut next_replies = Map::new();
for (index, id) in &result_tags {
if let Some(reply) = replies_out.get(*index) {
next_replies.insert(id.as_str().into(), reply_to_dynamic(reply));
}
}
runtime.next_replies = next_replies;
runtime.random_state = SCRIPT_RNG.with(|cell| cell.get());
ScriptReport {
commands: produced,
errors,
}
}
fn collect_commands(
scope: &Scope<'static>,
produced: &mut Vec<Command>,
tags: &mut Vec<(usize, String)>,
errors: &mut Vec<String>,
policy: &CommandPolicy,
) {
let Some(commands) = scope
.get("commands")
.and_then(|value| value.read_lock::<Array>())
else {
return;
};
for entry in commands.iter() {
let is_tagged = entry
.read_lock::<Map>()
.is_some_and(|map| map.contains_key(TAG_COMMAND_KEY));
let parsed = if is_tagged {
let request = entry.read_lock::<Map>().unwrap();
let id = request
.get(TAG_ID_KEY)
.and_then(|value| value.clone().into_string().ok());
match request.get(TAG_COMMAND_KEY) {
Some(command) => {
crate::dynamic_de::from_dynamic::<Command>(command).map(|c| (c, id))
}
None => continue,
}
} else {
crate::dynamic_de::from_dynamic::<Command>(entry).map(|c| (c, None))
};
match parsed {
Ok((command, id)) if policy.allows(command.name()) => {
if let Some(id) = id {
tags.push((produced.len(), id));
}
produced.push(command);
}
Ok((command, _)) => {
tracing::warn!(
"script command '{}' blocked by write policy",
command.name()
);
errors.push(format!(
"command '{}' blocked by write policy",
command.name()
));
}
Err(error) => {
tracing::warn!("dropped malformed script command: {error}");
errors.push(format!("dropped command: {error}"));
}
}
}
}
fn entity_pos_vel(world: &World, entity: Entity) -> (Array, Array) {
let position = world
.core
.get_local_transform(entity)
.map(|transform| transform.translation)
.unwrap_or_else(Vec3::zeros);
let velocity = nightshade::ecs::physics::resources::physics_world_linear_velocity(
&world.resources.physics,
entity,
)
.unwrap_or_else(Vec3::zeros);
(array3(position), array3(velocity))
}
fn array3(value: Vec3) -> Array {
vec![
Dynamic::from(value.x as f64),
Dynamic::from(value.y as f64),
Dynamic::from(value.z as f64),
]
}
fn relevant_events(frame_events: &[Event], entity: Entity) -> Vec<Event> {
frame_events
.iter()
.filter(|event| match event {
Event::Collision { a, b, .. } => *a == entity || *b == entity,
Event::Despawned { entity: target }
| Event::AnimationFinished { entity: target }
| Event::AnimationEvent { entity: target, .. }
| Event::NavigationArrived { entity: target } => *target == entity,
})
.cloned()
.collect()
}
fn events_array(frame_events: &[Event]) -> Array {
frame_events.iter().map(event_to_map).collect()
}
fn event_to_map(event: &Event) -> Dynamic {
let mut map = Map::new();
match event {
Event::Collision {
a,
b,
sensor,
started,
} => {
map.insert("kind".into(), Dynamic::from("collision".to_string()));
map.insert("a".into(), Dynamic::from(entity_to_script(*a)));
map.insert("b".into(), Dynamic::from(entity_to_script(*b)));
map.insert("sensor".into(), Dynamic::from(*sensor));
map.insert("started".into(), Dynamic::from(*started));
}
Event::Despawned { entity } => {
map.insert("kind".into(), Dynamic::from("despawned".to_string()));
map.insert("entity".into(), Dynamic::from(entity_to_script(*entity)));
}
Event::AnimationFinished { entity } => {
map.insert(
"kind".into(),
Dynamic::from("animation_finished".to_string()),
);
map.insert("entity".into(), Dynamic::from(entity_to_script(*entity)));
}
Event::AnimationEvent { entity, name } => {
map.insert("kind".into(), Dynamic::from("animation_event".to_string()));
map.insert("entity".into(), Dynamic::from(entity_to_script(*entity)));
map.insert("name".into(), Dynamic::from(name.clone()));
}
Event::NavigationArrived { entity } => {
map.insert(
"kind".into(),
Dynamic::from("navigation_arrived".to_string()),
);
map.insert("entity".into(), Dynamic::from(entity_to_script(*entity)));
}
}
Dynamic::from_map(map)
}
#[cfg(test)]
mod tests {
use super::new_engine;
#[test]
fn mixed_integer_and_float_arithmetic_works() {
let engine = new_engine();
assert_eq!(engine.eval::<f64>("3 * 1.5").unwrap(), 4.5);
assert_eq!(engine.eval::<f64>("2.0 + 3").unwrap(), 5.0);
assert_eq!(engine.eval::<f64>("7 / 2.0").unwrap(), 3.5);
assert!(engine.eval::<bool>("2 < 2.5").unwrap());
assert!(engine.eval::<bool>("3.0 >= 3").unwrap());
}
#[test]
fn same_type_integer_division_stays_integer() {
let engine = new_engine();
assert_eq!(engine.eval::<i64>("7 / 2").unwrap(), 3);
}
#[test]
fn closures_capture_and_mutate_outer_state() {
let engine = new_engine();
let result = engine
.eval::<i64>(
r#"
let state = #{ n: 0 };
let bump = || { state.n += 1; };
bump.call();
bump.call();
state.n
"#,
)
.unwrap();
assert_eq!(result, 2);
}
#[test]
fn color_helpers_build_four_channel_arrays() {
let engine = new_engine();
assert_eq!(
engine
.eval::<rhai::Array>("rgb(0.5, 0.25, 0.1)")
.unwrap()
.len(),
4
);
assert_eq!(
engine
.eval::<rhai::Array>("rgba(0.1, 0.2, 0.3, 0.4)")
.unwrap()
.len(),
4
);
assert_eq!(
engine
.eval::<rhai::Array>("hsv(0.0, 1.0, 1.0)")
.unwrap()
.len(),
4
);
}
}