use std::sync::{Arc, Mutex};
use bevy::ecs::entity::Entity;
use bevy::ecs::message::Message;
use bevy::ecs::resource::Resource;
use bevy::reflect::Reflect;
use rhai::{AST, Dynamic, Engine, EvalAltResult, ParseError, Scope};
use crate::core::PersonId;
use crate::defs::DefsError;
use crate::snapshot::SocietySnapshot;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Reflect)]
pub struct ScriptId(pub u32);
#[derive(Clone)]
pub(crate) struct CompiledScript {
pub(crate) owner: String,
pub(crate) ast: AST,
}
pub(crate) fn apply_limits(engine: &mut Engine) {
engine.set_max_operations(10_000);
engine.set_max_call_levels(16);
engine.set_max_string_size(1024);
engine.set_max_array_size(256);
engine.set_max_map_size(64);
}
pub(crate) struct ScriptCompiler {
engine: Engine,
scripts: Vec<CompiledScript>,
}
impl ScriptCompiler {
pub(crate) fn new() -> Self {
let mut engine = Engine::new_raw();
apply_limits(&mut engine);
Self {
engine,
scripts: Vec::new(),
}
}
pub(crate) fn expression(&mut self, owner: &str, source: &str) -> Result<ScriptId, DefsError> {
let ast = self
.engine
.compile_expression(source)
.map_err(|err| script_error(owner, &err))?;
self.push(owner, ast)
}
pub(crate) fn statements(&mut self, owner: &str, source: &str) -> Result<ScriptId, DefsError> {
let ast = self
.engine
.compile(source)
.map_err(|err| script_error(owner, &err))?;
self.push(owner, ast)
}
fn push(&mut self, owner: &str, ast: AST) -> Result<ScriptId, DefsError> {
let index = u32::try_from(self.scripts.len()).map_err(|_| DefsError::Invalid {
owner: owner.to_owned(),
message: "too many scripts (limit 2^32)".to_owned(),
})?;
self.scripts.push(CompiledScript {
owner: owner.to_owned(),
ast,
});
Ok(ScriptId(index))
}
pub(crate) fn into_scripts(self) -> Vec<CompiledScript> {
self.scripts
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScriptKind {
Expression,
Statements,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScriptSyntaxError {
pub line: Option<usize>,
pub column: Option<usize>,
pub message: String,
}
impl std::fmt::Display for ScriptSyntaxError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match (self.line, self.column) {
(Some(line), Some(column)) => {
write!(f, "line {line}, column {column}: {}", self.message)
}
(Some(line), None) => write!(f, "line {line}: {}", self.message),
_ => f.write_str(&self.message),
}
}
}
impl std::error::Error for ScriptSyntaxError {}
pub fn check_script(source: &str, kind: ScriptKind) -> Result<(), ScriptSyntaxError> {
let mut engine = Engine::new_raw();
apply_limits(&mut engine);
let parsed = match kind {
ScriptKind::Expression => engine.compile_expression(source),
ScriptKind::Statements => engine.compile(source),
};
parsed.map(|_| ()).map_err(|err| ScriptSyntaxError {
line: err.1.line(),
column: err.1.position(),
message: err.0.to_string(),
})
}
fn script_error(owner: &str, err: &ParseError) -> DefsError {
DefsError::Script {
owner: owner.to_owned(),
message: err.to_string(),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Bindings {
pub(crate) actor: i64,
pub(crate) target: i64,
pub(crate) subject: i64,
pub(crate) rumor: i64,
pub(crate) accepted: Option<bool>,
}
impl Default for Bindings {
fn default() -> Self {
Self {
actor: -1,
target: -1,
subject: -1,
rumor: -1,
accepted: None,
}
}
}
impl Bindings {
pub(crate) fn of_actor(id: Option<PersonId>) -> Self {
Self {
actor: id.map_or(-1, |p| i64::from(p.0)),
..Self::default()
}
}
}
#[derive(Message, Debug, Clone, PartialEq, Eq)]
pub struct ScriptError {
pub owner: String,
pub message: String,
}
#[derive(Message, Debug, Clone, PartialEq)]
pub struct ScriptEvent {
pub name: String,
pub actor: Option<Entity>,
pub target: Option<Entity>,
pub data: Vec<(String, ScriptValue)>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ScriptValue {
Bool(bool),
Int(i64),
Float(f32),
Text(String),
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum ScriptAction {
Adjust {
person: i64,
other: i64,
field: String,
delta: f32,
},
Emote {
person: i64,
kind: String,
intensity: f32,
},
Need {
person: i64,
need: String,
delta: f32,
},
Opinion {
person: i64,
topic: String,
delta: f32,
},
Join {
person: i64,
faction: String,
},
Leave {
person: i64,
faction: String,
},
Rumor {
origin: i64,
subject: i64,
deed: String,
truth: bool,
},
Interact {
actor: i64,
target: i64,
interaction: String,
},
Bark {
speaker: i64,
text: String,
},
Think {
speaker: i64,
text: String,
},
Remember {
person: i64,
other: i64,
text: String,
valence: f32,
importance: f32,
},
Converse {
npc: i64,
player: i64,
},
Emit {
name: String,
actor: i64,
target: i64,
data: Vec<(String, ScriptValue)>,
},
}
#[derive(Clone)]
pub(crate) struct Ctx(pub(crate) Arc<SocietySnapshot>);
#[derive(Clone)]
pub(crate) struct Fx {
queue: Arc<Mutex<Vec<ScriptAction>>>,
actor: i64,
target: i64,
}
fn boxed(err: String) -> Box<EvalAltResult> {
Box::new(EvalAltResult::ErrorRuntime(
Dynamic::from(err),
rhai::Position::NONE,
))
}
#[derive(Resource)]
pub struct ScriptEngine {
engine: Engine,
}
impl Default for ScriptEngine {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for ScriptEngine {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ScriptEngine").finish_non_exhaustive()
}
}
impl ScriptEngine {
pub fn new() -> Self {
let mut engine = Engine::new();
apply_limits(&mut engine);
register_ctx(&mut engine);
register_fx(&mut engine);
Self { engine }
}
pub(crate) fn eval_bool(
&self,
scripts: &[CompiledScript],
id: ScriptId,
snapshot: &Arc<SocietySnapshot>,
bindings: Bindings,
) -> Result<bool, String> {
let mut scope = scope_with(snapshot, bindings);
let value: Dynamic = self
.engine
.eval_ast_with_scope(&mut scope, ast(scripts, id)?)
.map_err(|err| err.to_string())?;
let type_name = value.type_name();
value
.try_cast::<bool>()
.ok_or_else(|| format!("a condition returned {type_name}, not a bool"))
}
pub(crate) fn eval_f32(
&self,
scripts: &[CompiledScript],
id: ScriptId,
snapshot: &Arc<SocietySnapshot>,
bindings: Bindings,
) -> Result<f32, String> {
let mut scope = scope_with(snapshot, bindings);
let value: Dynamic = self
.engine
.eval_ast_with_scope(&mut scope, ast(scripts, id)?)
.map_err(|err| err.to_string())?;
if let Some(n) = value.clone().try_cast::<f32>() {
return Ok(n);
}
if let Some(n) = value.clone().try_cast::<i64>() {
return Ok(n as f32);
}
Err(format!(
"a consideration returned {}, not a number",
value.type_name()
))
}
pub(crate) fn run(
&self,
scripts: &[CompiledScript],
id: ScriptId,
snapshot: &Arc<SocietySnapshot>,
bindings: Bindings,
) -> Result<Vec<ScriptAction>, String> {
let queue = Arc::new(Mutex::new(Vec::new()));
let mut scope = scope_with(snapshot, bindings);
scope.push(
"fx",
Fx {
queue: Arc::clone(&queue),
actor: bindings.actor,
target: bindings.target,
},
);
self.engine
.run_ast_with_scope(&mut scope, ast(scripts, id)?)
.map_err(|err| err.to_string())?;
let actions = match queue.lock() {
Ok(mut q) => std::mem::take(&mut *q),
Err(poisoned) => std::mem::take(&mut *poisoned.into_inner()),
};
Ok(actions)
}
pub(crate) fn owner(scripts: &[CompiledScript], id: ScriptId) -> &str {
scripts
.get(id.0 as usize)
.map_or("<unknown script>", |s| s.owner.as_str())
}
}
fn ast(scripts: &[CompiledScript], id: ScriptId) -> Result<&AST, String> {
scripts
.get(id.0 as usize)
.map(|s| &s.ast)
.ok_or_else(|| format!("no script with id {}", id.0))
}
fn scope_with<'a>(snapshot: &Arc<SocietySnapshot>, bindings: Bindings) -> Scope<'a> {
let mut scope = Scope::new();
scope.push_constant("actor", bindings.actor);
scope.push_constant("target", bindings.target);
scope.push_constant("subject", bindings.subject);
scope.push_constant("rumor", bindings.rumor);
if let Some(accepted) = bindings.accepted {
scope.push_constant("accepted", accepted);
}
scope.push("ctx", Ctx(Arc::clone(snapshot)));
scope
}
macro_rules! ctx_pair {
($engine:expr, $name:literal, $method:ident) => {
$engine.register_fn(
$name,
|ctx: &mut Ctx, a: i64, b: i64| -> Result<f32, Box<EvalAltResult>> {
ctx.0.$method(a, b).map_err(boxed)
},
);
};
}
macro_rules! ctx_one {
($engine:expr, $name:literal, $method:ident) => {
$engine.register_fn(
$name,
|ctx: &mut Ctx, a: i64| -> Result<f32, Box<EvalAltResult>> {
ctx.0.$method(a).map_err(boxed)
},
);
};
}
fn register_ctx(engine: &mut Engine) {
engine.register_type_with_name::<Ctx>("Ctx");
ctx_pair!(engine, "affinity", affinity);
ctx_pair!(engine, "trust", trust);
ctx_pair!(engine, "respect", respect);
ctx_pair!(engine, "familiarity", familiarity);
ctx_pair!(engine, "grudge", grudge);
ctx_one!(engine, "standing", standing);
engine.register_fn(
"label",
|ctx: &mut Ctx, a: i64, b: i64| -> Result<String, Box<EvalAltResult>> {
ctx.0.label(a, b).map_err(boxed)
},
);
engine.register_fn(
"mood",
|ctx: &mut Ctx, a: i64| -> Result<rhai::Map, Box<EvalAltResult>> {
let mood = ctx.0.mood(a).map_err(boxed)?;
let mut map = rhai::Map::new();
map.insert("p".into(), Dynamic::from(mood[0]));
map.insert("a".into(), Dynamic::from(mood[1]));
map.insert("d".into(), Dynamic::from(mood[2]));
Ok(map)
},
);
engine.register_fn(
"emotion",
|ctx: &mut Ctx, a: i64, kind: &str| -> Result<f32, Box<EvalAltResult>> {
ctx.0.emotion(a, kind).map_err(boxed)
},
);
engine.register_fn(
"need",
|ctx: &mut Ctx, a: i64, need: &str| -> Result<f32, Box<EvalAltResult>> {
ctx.0.need(a, need).map_err(boxed)
},
);
engine.register_fn(
"signal",
|ctx: &mut Ctx, a: i64, signal: &str| -> Result<f32, Box<EvalAltResult>> {
ctx.0.signal(a, signal).map_err(boxed)
},
);
engine.register_fn(
"stock",
|ctx: &mut Ctx, a: i64, good: &str| -> Result<f32, Box<EvalAltResult>> {
ctx.0.stock(a, good).map_err(boxed)
},
);
engine.register_fn(
"price",
|ctx: &mut Ctx, good: &str| -> Result<f32, Box<EvalAltResult>> {
ctx.0.price(good).map_err(boxed)
},
);
engine.register_fn(
"opinion",
|ctx: &mut Ctx, a: i64, topic: &str| -> Result<f32, Box<EvalAltResult>> {
ctx.0.opinion(a, topic).map_err(boxed)
},
);
engine.register_fn(
"trait",
|ctx: &mut Ctx, a: i64, name: &str| -> Result<f32, Box<EvalAltResult>> {
ctx.0.trait_value(a, name).map_err(boxed)
},
);
engine.register_fn(
"knows",
|ctx: &mut Ctx, a: i64, rumor: i64| -> Result<bool, Box<EvalAltResult>> {
ctx.0.knows(a, rumor).map_err(boxed)
},
);
engine.register_fn(
"tellable",
|ctx: &mut Ctx, a: i64| -> Result<bool, Box<EvalAltResult>> {
ctx.0.tellable(a).map_err(boxed)
},
);
engine.register_fn(
"member",
|ctx: &mut Ctx, a: i64, faction: &str| -> Result<bool, Box<EvalAltResult>> {
ctx.0.member(a, faction).map_err(boxed)
},
);
engine.register_fn(
"name",
|ctx: &mut Ctx, a: i64| -> Result<String, Box<EvalAltResult>> {
ctx.0.name(a).map_err(boxed)
},
);
engine.register_fn(
"doing",
|ctx: &mut Ctx, a: i64| -> Result<String, Box<EvalAltResult>> {
ctx.0.doing(a).map_err(boxed)
},
);
engine.register_fn(
"position",
|ctx: &mut Ctx, a: i64| -> Result<rhai::Map, Box<EvalAltResult>> {
let [x, y] = ctx.0.position(a).map_err(boxed)?;
let mut map = rhai::Map::new();
map.insert("x".into(), Dynamic::from(x));
map.insert("y".into(), Dynamic::from(y));
Ok(map)
},
);
ctx_pair!(engine, "distance", distance);
engine.register_fn("persons", |ctx: &mut Ctx| -> rhai::Array {
ctx.0.person_ids().into_iter().map(Dynamic::from).collect()
});
engine.register_fn(
"faction_size",
|ctx: &mut Ctx, faction: &str| -> Result<i64, Box<EvalAltResult>> {
ctx.0.faction_size(faction).map_err(boxed)
},
);
engine.register_fn("hour", |ctx: &mut Ctx| -> f32 { ctx.0.hour() });
engine.register_fn("day", |ctx: &mut Ctx| -> i64 { ctx.0.day() });
engine.register_fn("weekday", |ctx: &mut Ctx| -> i64 { ctx.0.weekday() });
engine.register_fn("rand", |ctx: &mut Ctx| -> f32 { ctx.0.unit() });
}
macro_rules! fx_push {
($engine:expr, $name:literal, |$fx:ident $(, $arg:ident : $ty:ty)*| $make:expr) => {
$engine.register_fn(
$name,
|$fx: &mut Fx $(, $arg: $ty)*| -> Result<(), Box<EvalAltResult>> {
let action = $make;
let mut queue = match $fx.queue.lock() {
Ok(queue) => queue,
Err(poisoned) => poisoned.into_inner(),
};
queue.push(action);
Ok(())
},
);
};
}
fn register_fx(engine: &mut Engine) {
engine.register_type_with_name::<Fx>("Fx");
fx_push!(
engine,
"adjust",
|fx, person: i64, other: i64, field: String, delta: f32| ScriptAction::Adjust {
person,
other,
field,
delta,
}
);
fx_push!(
engine,
"emote",
|fx, person: i64, kind: String, intensity: f32| ScriptAction::Emote {
person,
kind,
intensity,
}
);
fx_push!(engine, "need", |fx,
person: i64,
need: String,
delta: f32| {
ScriptAction::Need {
person,
need,
delta,
}
});
fx_push!(
engine,
"opinion",
|fx, person: i64, topic: String, delta: f32| ScriptAction::Opinion {
person,
topic,
delta,
}
);
fx_push!(engine, "join", |fx, person: i64, faction: String| {
ScriptAction::Join { person, faction }
});
fx_push!(engine, "leave", |fx, person: i64, faction: String| {
ScriptAction::Leave { person, faction }
});
fx_push!(
engine,
"rumor",
|fx, origin: i64, subject: i64, deed: String, truth: bool| ScriptAction::Rumor {
origin,
subject,
deed,
truth,
}
);
fx_push!(
engine,
"interact",
|fx, actor: i64, target: i64, interaction: String| ScriptAction::Interact {
actor,
target,
interaction,
}
);
fx_push!(engine, "bark", |fx, speaker: i64, text: String| {
ScriptAction::Bark { speaker, text }
});
fx_push!(engine, "think", |fx, speaker: i64, text: String| {
ScriptAction::Think { speaker, text }
});
fx_push!(
engine,
"remember",
|fx, person: i64, other: i64, text: String, valence: f32, importance: f32| {
ScriptAction::Remember {
person,
other,
text,
valence,
importance,
}
}
);
register_converse(engine);
engine.register_fn(
"emit",
|fx: &mut Fx, name: String, data: rhai::Map| -> Result<(), Box<EvalAltResult>> {
let mut values = Vec::with_capacity(data.len());
for (key, value) in data {
let value = if let Some(b) = value.clone().try_cast::<bool>() {
ScriptValue::Bool(b)
} else if let Some(n) = value.clone().try_cast::<i64>() {
ScriptValue::Int(n)
} else if let Some(n) = value.clone().try_cast::<f32>() {
ScriptValue::Float(n)
} else if let Some(text) = value.clone().try_cast::<rhai::ImmutableString>() {
ScriptValue::Text(text.to_string())
} else {
return Err(boxed(format!(
"fx.emit value '{key}' is {}, which a ScriptValue cannot carry",
value.type_name()
)));
};
values.push((key.to_string(), value));
}
let mut queue = match fx.queue.lock() {
Ok(queue) => queue,
Err(poisoned) => poisoned.into_inner(),
};
queue.push(ScriptAction::Emit {
name,
actor: fx.actor,
target: fx.target,
data: values,
});
Ok(())
},
);
}
#[cfg(feature = "dialogue")]
fn register_converse(engine: &mut Engine) {
fx_push!(engine, "converse", |fx, npc: i64, player: i64| {
ScriptAction::Converse { npc, player }
});
}
#[cfg(not(feature = "dialogue"))]
fn register_converse(engine: &mut Engine) {
engine.register_fn(
"converse",
|_fx: &mut Fx, _npc: i64, _player: i64| -> Result<(), Box<EvalAltResult>> {
Err(boxed("fx.converse needs the `dialogue` feature".to_owned()))
},
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_default_engine_has_the_whole_api() {
let engine = ScriptEngine::default();
let mut scope = Scope::new();
scope.push(
"fx",
Fx {
queue: Arc::new(Mutex::new(Vec::new())),
actor: 0,
target: -1,
},
);
let result = engine
.engine
.run_with_scope(&mut scope, "fx.emit(\"met\", #{ n: 1 })");
assert!(result.is_ok(), "{result:?}");
}
}