shity basic one shot

This commit is contained in:
2026-09-18 14:01:10 +01:00
parent 5851077659
commit 42a1234e7f
81 changed files with 3484 additions and 76 deletions
+26
View File
@@ -0,0 +1,26 @@
//! Evaluate a `.rhai` script and print the generated JSON.
//!
//! ```text
//! cargo run --bin aac-dump -- rust/examples/avatar.rhai
//! ```
fn main() {
let Some(path) = std::env::args().nth(1) else {
eprintln!("usage: aac-dump <script.rhai>");
std::process::exit(2);
};
let script = match std::fs::read_to_string(&path) {
Ok(script) => script,
Err(error) => {
eprintln!("cannot read {path}: {error}");
std::process::exit(2);
}
};
match aac::evaluate_to_json(&script) {
Ok(json) => println!("{json}"),
Err(error) => {
eprintln!("{error}");
std::process::exit(1);
}
}
}
+234
View File
@@ -0,0 +1,234 @@
use crate::graph::*;
use std::sync::{Arc, Mutex, MutexGuard};
/// Shared, thread-safe handle to the graph being built. Every builder carries a clone.
#[derive(Clone)]
pub struct Aac {
pub(crate) graph: Arc<Mutex<ControllerGraph>>,
}
impl Default for Aac {
fn default() -> Self {
Self::new()
}
}
impl Aac {
pub fn new() -> Self {
Self {
graph: Arc::new(Mutex::new(ControllerGraph::default())),
}
}
/// A poisoned lock still holds a usable graph; recovering avoids panicking across the FFI boundary.
fn lock(&self) -> MutexGuard<'_, ControllerGraph> {
self.graph.lock().unwrap_or_else(|e| e.into_inner())
}
pub fn write<R>(&self, f: impl FnOnce(&mut ControllerGraph) -> R) -> R {
f(&mut self.lock())
}
}
/// Parameter handles only carry the name; conditions read nothing else from them.
#[derive(Clone)]
pub struct FloatParam {
pub(crate) name: String,
}
#[derive(Clone)]
pub struct IntParam {
pub(crate) name: String,
}
#[derive(Clone)]
pub struct BoolParam {
pub(crate) name: String,
}
#[derive(Clone)]
pub struct ControllerBuilder {
pub(crate) aac: Aac,
}
#[derive(Clone)]
pub struct LayerBuilder {
pub(crate) aac: Aac,
pub(crate) layer: usize,
}
/// A state machine. `machine` is the chain of sub-machine indices from the layer root, so the empty
/// chain is the layer's root machine and indices stay stable because machines are only ever appended.
#[derive(Clone)]
pub struct MachineRef {
pub(crate) aac: Aac,
pub(crate) layer: usize,
pub(crate) machine: Vec<usize>,
}
#[derive(Clone)]
pub struct StateRef {
pub(crate) aac: Aac,
pub(crate) layer: usize,
pub(crate) machine: Vec<usize>,
pub(crate) state: usize,
}
#[derive(Clone)]
pub struct AnyStateRef {
pub(crate) aac: Aac,
pub(crate) layer: usize,
pub(crate) machine: Vec<usize>,
}
#[derive(Clone)]
pub struct ClipBuilder {
pub(crate) aac: Aac,
pub(crate) name: String,
}
#[derive(Clone)]
pub struct BlendTreeBuilder {
pub(crate) aac: Aac,
pub(crate) name: String,
}
/// `source` is the originating state, or `None` for an any-state transition.
#[derive(Clone)]
pub struct TransitionRef {
pub(crate) aac: Aac,
pub(crate) layer: usize,
pub(crate) machine: Vec<usize>,
pub(crate) source: Option<usize>,
pub(crate) index: usize,
}
pub(crate) fn machine_mut<'a>(
graph: &'a mut ControllerGraph,
layer: usize,
path: &[usize],
) -> &'a mut StateMachine {
let mut machine = &mut graph.controller.layers[layer].state_machine;
for &index in path {
machine = &mut machine.sub_machines[index];
}
machine
}
pub(crate) fn clip_mut<'a>(graph: &'a mut ControllerGraph, name: &str) -> Option<&'a mut ClipData> {
graph.clips.iter_mut().find(|clip| clip.name == name)
}
pub(crate) fn blend_tree_mut<'a>(
graph: &'a mut ControllerGraph,
name: &str,
) -> Option<&'a mut BlendTreeData> {
graph.blend_trees.iter_mut().find(|tree| tree.name == name)
}
impl StateRef {
pub fn state_name(&self) -> String {
self.aac
.write(|graph| machine_mut(graph, self.layer, &self.machine).states[self.state].name.clone())
}
pub fn set_motion(&self, motion: MotionRef) {
self.aac.write(|graph| {
machine_mut(graph, self.layer, &self.machine).states[self.state].motion = Some(motion);
});
}
pub fn transition_to(&self, target: &StateRef) -> TransitionRef {
let to = target.state_name();
let index = self.aac.write(|graph| {
let state = &mut machine_mut(graph, self.layer, &self.machine).states[self.state];
state.transitions.push(Transition::new(to));
state.transitions.len() - 1
});
TransitionRef {
aac: self.aac.clone(),
layer: self.layer,
machine: self.machine.clone(),
source: Some(self.state),
index,
}
}
}
impl AnyStateRef {
pub fn transition_to(&self, target: &StateRef) -> TransitionRef {
let to = target.state_name();
let index = self.aac.write(|graph| {
let machine = machine_mut(graph, self.layer, &self.machine);
machine.any_state_transitions.push(Transition::new(to));
machine.any_state_transitions.len() - 1
});
TransitionRef {
aac: self.aac.clone(),
layer: self.layer,
machine: self.machine.clone(),
source: None,
index,
}
}
}
impl TransitionRef {
/// Apply a mutation to the transition this handle points at.
pub fn update(&self, f: impl FnOnce(&mut Transition)) {
self.aac.write(|graph| {
let machine = machine_mut(graph, self.layer, &self.machine);
let transitions = match self.source {
Some(state) => &mut machine.states[state].transitions,
None => &mut machine.any_state_transitions,
};
f(&mut transitions[self.index]);
});
}
pub fn add_conditions(&self, conditions: impl IntoIterator<Item = Condition>) {
self.update(|transition| transition.conditions.extend(conditions));
}
}
impl ClipBuilder {
pub fn data_mut<R>(&self, f: impl FnOnce(&mut ClipData) -> R) -> Option<R> {
let name = self.name.clone();
self.aac.write(|graph| clip_mut(graph, &name).map(f))
}
/// Append one keyframe to the curve for (path, property), creating the curve if needed.
pub fn push_key(&self, path: &str, property: &str, key: Keyframe) -> Result<(), String> {
let target = infer_target(property).ok_or_else(|| {
format!(
"cannot tell which component `{property}` belongs to; \
known properties are `m_IsActive` and `blendShape.*`"
)
})?;
self.data_mut(|clip| {
clip.curve_mut(path, target, property).keys.push(key);
})
.ok_or_else(|| format!("unknown clip `{}`", self.name))
}
}
impl BlendTreeBuilder {
pub fn data_mut<R>(&self, f: impl FnOnce(&mut BlendTreeData) -> R) -> Option<R> {
let name = self.name.clone();
self.aac.write(|graph| blend_tree_mut(graph, &name).map(f))
}
pub fn configure(&self, blend_type: BlendType, param_x: &str, param_y: Option<&str>) -> Result<(), String> {
self.data_mut(|tree| {
tree.blend_type = blend_type;
tree.param_x = param_x.to_string();
tree.param_y = param_y.map(str::to_string);
})
.ok_or_else(|| format!("unknown blend tree `{}`", self.name))
}
pub fn push_child(&self, child: BlendChild) -> Result<(), String> {
self.data_mut(|tree| tree.children.push(child))
.ok_or_else(|| format!("unknown blend tree `{}`", self.name))
}
}
+325
View File
@@ -0,0 +1,325 @@
use crate::graph::*;
use std::collections::{HashMap, HashSet};
/// Sort keyframes into time order, then validate, then serialize.
pub fn to_json(graph: &ControllerGraph) -> Result<String, String> {
let mut graph = graph.clone();
normalize(&mut graph);
validate(&graph)?;
serde_json::to_string_pretty(&graph).map_err(|e| format!("serialization failed: {e}"))
}
fn normalize(graph: &mut ControllerGraph) {
for clip in &mut graph.clips {
for curve in &mut clip.curves {
curve
.keys
.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap_or(std::cmp::Ordering::Equal));
}
}
}
fn validate(graph: &ControllerGraph) -> Result<(), String> {
let mut errors = Vec::new();
if graph.system_name.trim().is_empty() {
errors.push("system_name is not set (call `aac.system_name(\"...\")`)".to_string());
}
if graph.asset_key.trim().is_empty() {
errors.push("asset_key is not set (call `aac.asset_key(\"...\")`)".to_string());
}
let parameters = parameter_types(graph, &mut errors);
validate_clips(graph, &mut errors);
validate_blend_trees(graph, &parameters, &mut errors);
validate_layers(graph, &parameters, &mut errors);
if errors.is_empty() {
Ok(())
} else {
Err(format!(
"graph is invalid ({} problem{}):\n- {}",
errors.len(),
if errors.len() == 1 { "" } else { "s" },
errors.join("\n- ")
))
}
}
#[derive(Clone, Copy, PartialEq)]
enum ParamType {
Float,
Int,
Bool,
}
fn parameter_types(graph: &ControllerGraph, errors: &mut Vec<String>) -> HashMap<String, ParamType> {
let mut seen = HashSet::new();
let mut map = HashMap::new();
for parameter in &graph.parameters {
let name = parameter.name();
if name.trim().is_empty() {
errors.push("a parameter has an empty name".to_string());
continue;
}
if !seen.insert(name.to_string()) {
errors.push(format!("parameter `{name}` is declared more than once"));
}
let ty = match parameter {
Parameter::Float { .. } => ParamType::Float,
Parameter::Int { .. } => ParamType::Int,
Parameter::Bool { .. } => ParamType::Bool,
};
map.insert(name.to_string(), ty);
}
map
}
fn validate_clips(graph: &ControllerGraph, errors: &mut Vec<String>) {
let mut seen = HashSet::new();
for clip in &graph.clips {
if clip.name.trim().is_empty() {
errors.push("a clip has an empty name".to_string());
} else if !seen.insert(clip.name.clone()) {
errors.push(format!("clip `{}` is declared more than once", clip.name));
}
for curve in &clip.curves {
if curve.path.trim().is_empty() {
errors.push(format!("clip `{}` has a curve with an empty path", clip.name));
}
if curve.keys.is_empty() {
errors.push(format!(
"clip `{}` curve `{}` on `{}` has no keyframes",
clip.name, curve.property, curve.path
));
}
for key in &curve.keys {
if !key.time.is_finite() || !key.value.is_finite() {
errors.push(format!(
"clip `{}` curve `{}` on `{}` has a non-finite keyframe",
clip.name, curve.property, curve.path
));
}
}
}
}
}
fn validate_blend_trees(
graph: &ControllerGraph,
parameters: &HashMap<String, ParamType>,
errors: &mut Vec<String>,
) {
let mut seen = HashSet::new();
for tree in &graph.blend_trees {
if tree.name.trim().is_empty() {
errors.push("a blend tree has an empty name".to_string());
} else if !seen.insert(tree.name.clone()) {
errors.push(format!("blend tree `{}` is declared more than once", tree.name));
}
let has_y = tree.param_y.as_ref().is_some_and(|y| !y.trim().is_empty());
if tree.blend_type == BlendType::Direct {
if !tree.param_x.trim().is_empty() || tree.param_y.is_some() {
errors.push(format!(
"blend tree `{}` is Direct and must not have blend parameters",
tree.name
));
}
} else {
if tree.param_x.trim().is_empty() {
errors.push(format!("blend tree `{}` has no x parameter", tree.name));
} else {
expect_param(parameters, &tree.param_x, ParamType::Float, &tree.name, "x", errors);
}
if tree.blend_type.is_2d() && !has_y {
errors.push(format!(
"blend tree `{}` is 2D and needs a y parameter",
tree.name
));
}
if !tree.blend_type.is_2d() && has_y {
errors.push(format!(
"blend tree `{}` is {:?} and must not have a y parameter",
tree.name, tree.blend_type
));
}
if tree.blend_type.is_2d() {
if let Some(y) = &tree.param_y {
expect_param(parameters, y, ParamType::Float, &tree.name, "y", errors);
}
}
}
if tree.children.is_empty() {
errors.push(format!("blend tree `{}` has no children", tree.name));
}
for child in &tree.children {
expect_motion(&child.motion, graph, errors);
if tree.blend_type == BlendType::Direct {
match &child.direct_param {
None => errors.push(format!(
"blend tree `{}` is Direct and a child has no direct parameter",
tree.name
)),
Some(p) => expect_param(parameters, p, ParamType::Float, &tree.name, "direct", errors),
}
} else if child.direct_param.is_some() {
errors.push(format!(
"blend tree `{}` is {:?} and thresholds are used, so a child must not have a direct parameter",
tree.name, tree.blend_type
));
}
}
}
}
fn validate_layers(
graph: &ControllerGraph,
parameters: &HashMap<String, ParamType>,
errors: &mut Vec<String>,
) {
let mut layer_names = HashSet::new();
for layer in &graph.controller.layers {
let label = if layer.name.trim().is_empty() {
errors.push("a layer has an empty name".to_string());
"<unnamed>".to_string()
} else {
if !layer_names.insert(layer.name.clone()) {
errors.push(format!("layer `{}` is declared more than once", layer.name));
}
layer.name.clone()
};
// Transitions cannot cross layers, so resolution is per-layer.
let mut machines = Vec::new();
walk_machines(&layer.state_machine, &label, &mut machines);
let mut state_names = HashSet::new();
for (machine_label, machine) in &machines {
for state in &machine.states {
if state.name.trim().is_empty() {
errors.push(format!("machine `{machine_label}` has a state with an empty name"));
} else if !state_names.insert(state.name.clone()) {
errors.push(format!(
"state `{}` is declared more than once in layer `{label}`",
state.name
));
}
}
}
for (machine_label, machine) in &machines {
for state in &machine.states {
if let Some(motion) = &state.motion {
expect_motion(motion, graph, errors);
}
for transition in &state.transitions {
validate_transition(
transition,
&format!("state `{}`", state.name),
&state_names,
parameters,
errors,
);
}
}
for transition in &machine.any_state_transitions {
validate_transition(
transition,
&format!("machine `{machine_label}` any-state"),
&state_names,
parameters,
errors,
);
}
}
}
}
/// State names are collected in a first pass so that forward references resolve.
fn walk_machines<'a>(machine: &'a StateMachine, label: &str, out: &mut Vec<(String, &'a StateMachine)>) {
out.push((label.to_string(), machine));
for sub in &machine.sub_machines {
let name = sub.name.as_deref().unwrap_or("<unnamed>");
walk_machines(sub, &format!("{label}/{name}"), out);
}
}
fn validate_transition(
transition: &Transition,
origin: &str,
state_names: &HashSet<String>,
parameters: &HashMap<String, ParamType>,
errors: &mut Vec<String>,
) {
if !state_names.contains(&transition.to) {
errors.push(format!(
"{origin} transitions to `{}`, which is not a state in that layer",
transition.to
));
}
if !transition.duration.is_finite() || !transition.exit_time.is_finite() {
errors.push(format!("{origin} has a non-finite duration or exit time"));
}
if !transition.conditions.is_empty() && transition.has_exit_time {
errors.push(format!(
"{origin} has both conditions and an exit time; Unity will ignore the conditions"
));
}
for condition in &transition.conditions {
let Some(ty) = parameters.get(&condition.parameter) else {
errors.push(format!(
"{origin} uses parameter `{}`, which is not declared",
condition.parameter
));
continue;
};
if !condition.threshold.is_finite() {
errors.push(format!("{origin} has a non-finite condition threshold"));
}
let ok = match condition.mode {
CondMode::If | CondMode::IfNot => *ty == ParamType::Bool,
CondMode::Greater | CondMode::Less => *ty != ParamType::Bool,
CondMode::Equals | CondMode::NotEqual => true,
};
if !ok {
errors.push(format!(
"{origin} uses {:?} on `{}`, but that parameter cannot be compared that way",
condition.mode, condition.parameter
));
}
}
}
fn expect_motion(motion: &MotionRef, graph: &ControllerGraph, errors: &mut Vec<String>) {
let (kind, name, exists) = match motion {
MotionRef::Clip { name } => ("clip", name, graph.clips.iter().any(|c| &c.name == name)),
MotionRef::BlendTree { name } => (
"blend tree",
name,
graph.blend_trees.iter().any(|t| &t.name == name),
),
};
if !exists {
errors.push(format!("reference to {kind} `{name}`, which is not declared"));
}
}
fn expect_param(
parameters: &HashMap<String, ParamType>,
name: &str,
expected: ParamType,
owner: &str,
role: &str,
errors: &mut Vec<String>,
) {
match parameters.get(name) {
None => errors.push(format!(
"blend tree `{owner}` uses `{name}` as its {role} parameter, which is not declared"
)),
Some(actual) if *actual != expected => errors.push(format!(
"blend tree `{owner}` uses `{name}` as its {role} parameter, which is not a float parameter"
)),
Some(_) => {}
}
}
+263
View File
@@ -0,0 +1,263 @@
use serde::{Deserialize, Serialize};
/// The complete description of one animator controller, produced entirely by a Rhai script.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct ControllerGraph {
pub system_name: String,
pub asset_key: String,
pub parameters: Vec<Parameter>,
pub clips: Vec<ClipData>,
pub blend_trees: Vec<BlendTreeData>,
pub controller: Controller,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Controller {
pub layers: Vec<Layer>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Layer {
/// The layer suffix as written in the script. Unity's layer name is derived from it by the C# side.
pub name: String,
pub state_machine: StateMachine,
}
/// Grid position in Unity's animator graph. Serialized as an object so C# can bind it directly.
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
pub struct GridPos {
pub x: i32,
pub y: i32,
}
impl GridPos {
pub fn new(x: i32, y: i32) -> Self {
Self { x, y }
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct StateMachine {
/// `None` for a layer's root machine, `Some` for a sub-state machine.
pub name: Option<String>,
pub position: GridPos,
pub states: Vec<State>,
pub sub_machines: Vec<StateMachine>,
pub any_state_transitions: Vec<Transition>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct State {
pub name: String,
pub position: GridPos,
pub motion: Option<MotionRef>,
pub transitions: Vec<Transition>,
}
/// References to entries in `ControllerGraph::clips` / `ControllerGraph::blend_trees`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum MotionRef {
Clip { name: String },
BlendTree { name: String },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Transition {
pub to: String,
pub conditions: Vec<Condition>,
pub has_exit_time: bool,
pub exit_time: f32,
pub duration: f32,
pub ordered_interruption: bool,
pub source_interruption: bool,
pub can_transition_to_self: bool,
}
impl Transition {
/// Mirrors `AacDefaultsProvider.ConfigureTransition`, so unspecified fields match library defaults.
pub fn new(to: impl Into<String>) -> Self {
Self {
to: to.into(),
conditions: Vec::new(),
has_exit_time: false,
exit_time: 0.0,
duration: 0.0,
ordered_interruption: true,
source_interruption: false,
can_transition_to_self: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Condition {
pub parameter: String,
pub mode: CondMode,
pub threshold: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CondMode {
Greater,
Less,
Equals,
NotEqual,
If,
IfNot,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ClipData {
pub name: String,
pub looping: bool,
pub curves: Vec<Curve>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Curve {
pub path: String,
pub target: TargetType,
pub property: String,
pub keys: Vec<Keyframe>,
}
/// The Unity component a curve binds to. Closed set: an unknown property is an error, not a guess.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TargetType {
GameObject,
SkinnedMeshRenderer,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Keyframe {
pub time: f32,
pub value: f32,
pub in_tangent: f32,
pub out_tangent: f32,
}
impl Keyframe {
pub fn linear(time: f32, value: f32) -> Self {
Self {
time,
value,
in_tangent: 0.0,
out_tangent: 0.0,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BlendTreeData {
pub name: String,
pub blend_type: BlendType,
pub param_x: String,
pub param_y: Option<String>,
pub children: Vec<BlendChild>,
pub use_automatic_thresholds: bool,
}
/// `rename_all = "snake_case"` would turn `SimpleDirectional2D` into `simple_directional2_d`,
/// so every variant is renamed explicitly.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum BlendType {
#[serde(rename = "simple_1d")]
Simple1D,
#[serde(rename = "simple_directional_2d")]
SimpleDirectional2D,
#[serde(rename = "freeform_directional_2d")]
FreeformDirectional2D,
#[serde(rename = "freeform_cartesian_2d")]
FreeformCartesian2D,
#[serde(rename = "direct")]
Direct,
}
impl BlendType {
pub fn is_2d(self) -> bool {
matches!(
self,
BlendType::SimpleDirectional2D
| BlendType::FreeformDirectional2D
| BlendType::FreeformCartesian2D
)
}
/// Blend trees that place children by threshold rather than by a direct blend parameter.
pub fn uses_thresholds(self) -> bool {
self != BlendType::Direct
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BlendChild {
pub motion: MotionRef,
pub threshold: f32,
pub threshold_y: Option<f32>,
/// Only meaningful on `Direct` blend trees.
pub direct_param: Option<String>,
}
impl BlendChild {
pub fn motion(motion: MotionRef) -> Self {
Self {
motion,
threshold: 0.0,
threshold_y: None,
direct_param: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Parameter {
Float { name: String, default: f32 },
Int { name: String, default: i32 },
Bool { name: String, default: bool },
}
impl Parameter {
pub fn name(&self) -> &str {
match self {
Parameter::Float { name, .. } | Parameter::Int { name, .. } | Parameter::Bool { name, .. } => name,
}
}
}
/// Property-name -> component-type inference. Deliberately strict; an unmapped property is an error.
pub fn infer_target(property: &str) -> Option<TargetType> {
if property.starts_with("blendShape.") {
return Some(TargetType::SkinnedMeshRenderer);
}
match property {
"m_IsActive" => Some(TargetType::GameObject),
_ => None,
}
}
impl ClipData {
/// Find-or-create the curve for one (path, target, property) binding.
pub fn curve_mut(&mut self, path: &str, target: TargetType, property: &str) -> &mut Curve {
let index = match self
.curves
.iter()
.position(|c| c.path == path && c.target == target && c.property == property)
{
Some(index) => index,
None => {
self.curves.push(Curve {
path: path.to_string(),
target,
property: property.to_string(),
keys: Vec::new(),
});
self.curves.len() - 1
}
};
&mut self.curves[index]
}
}
+167
View File
@@ -0,0 +1,167 @@
//! Evaluates a Rhai script that describes an Animator Controller, and hands the result to C# as JSON.
//!
//! The C# side never sees Rhai: it receives a `ControllerGraph` and drives the Animator As Code
//! modification API with it.
pub mod builder;
pub mod export;
pub mod graph;
pub mod motion_api;
pub mod rhai_api;
use builder::Aac;
use rhai::{Dynamic, Engine, Scope};
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::ptr;
pub use graph::ControllerGraph;
/// Opaque to C#. Owns the graph, the Rhai engine, and the last error message.
pub struct AacContext {
aac: Aac,
engine: Engine,
last_error: Option<CString>,
}
impl AacContext {
fn set_error(&mut self, message: String) {
// Interior NUL bytes would make CString::new fail; they can only come from a script string.
self.last_error = Some(CString::new(message.replace('\0', "\\0")).unwrap_or_default());
}
}
/// Evaluate a script and serialize the resulting graph. The testable core of the FFI.
pub fn evaluate_to_json(script: &str) -> Result<String, String> {
let aac = Aac::new();
let engine = rhai_api::engine(aac.clone());
let mut scope = Scope::new();
let _ = engine
.eval_with_scope::<Dynamic>(&mut scope, script)
.map_err(|error| error.to_string())?;
let graph = aac.write(|graph| graph.clone());
export::to_json(&graph)
}
/// Create a context. The returned pointer owns everything; release it with `aac_destroy`.
#[no_mangle]
pub extern "C" fn aac_create() -> *mut AacContext {
let aac = Aac::new();
let engine = rhai_api::engine(aac.clone());
Box::into_raw(Box::new(AacContext {
aac,
engine,
last_error: None,
}))
}
/// Evaluate a Rhai script. Returns 0 on success; on failure, call `aac_last_error`.
#[no_mangle]
pub extern "C" fn aac_eval_rhai(handle: *mut AacContext, script: *const c_char) -> i32 {
if handle.is_null() || script.is_null() {
return 1;
}
// SAFETY: the caller guarantees `handle` came from `aac_create` and has not been destroyed.
let context = unsafe { &mut *handle };
let script = match unsafe { CStr::from_ptr(script) }.to_str() {
Ok(script) => script,
Err(error) => {
context.set_error(format!("script is not valid UTF-8: {error}"));
return 1;
}
};
let outcome = {
let engine = &context.engine;
catch_unwind(AssertUnwindSafe(|| {
let mut scope = Scope::new();
engine.eval_with_scope::<Dynamic>(&mut scope, script)
}))
};
match outcome {
Ok(Ok(_)) => {
context.last_error = None;
0
}
Ok(Err(error)) => {
context.set_error(error.to_string());
1
}
Err(_) => {
context.set_error("internal error: the evaluator panicked".to_string());
2
}
}
}
/// Serialize the graph to JSON. Returns null on error; release the result with `aac_free_string`.
#[no_mangle]
pub extern "C" fn aac_to_json(handle: *mut AacContext) -> *mut c_char {
if handle.is_null() {
return ptr::null_mut();
}
// SAFETY: the caller guarantees `handle` came from `aac_create` and has not been destroyed.
let context = unsafe { &mut *handle };
let outcome = catch_unwind(AssertUnwindSafe(|| {
let graph = context.aac.write(|graph| graph.clone());
export::to_json(&graph)
}));
match outcome {
Ok(Ok(json)) => match CString::new(json) {
Ok(json) => {
context.last_error = None;
json.into_raw()
}
Err(_) => {
context.set_error("internal error: serialized JSON contained a NUL byte".to_string());
ptr::null_mut()
}
},
Ok(Err(error)) => {
context.set_error(error);
ptr::null_mut()
}
Err(_) => {
context.set_error("internal error: serialization panicked".to_string());
ptr::null_mut()
}
}
}
/// The last error message, or null if the last call succeeded. Borrowed: valid until the next call
/// on this context, and must not be freed.
#[no_mangle]
pub extern "C" fn aac_last_error(handle: *mut AacContext) -> *const c_char {
if handle.is_null() {
return ptr::null();
}
// SAFETY: the caller guarantees `handle` came from `aac_create` and has not been destroyed.
let context = unsafe { &*handle };
match &context.last_error {
Some(message) => message.as_ptr(),
None => ptr::null(),
}
}
/// Free a string returned by `aac_to_json`.
#[no_mangle]
pub extern "C" fn aac_free_string(string: *mut c_char) {
if string.is_null() {
return;
}
// SAFETY: `string` must come from `aac_to_json`, which uses `CString::into_raw`.
unsafe { drop(CString::from_raw(string)) };
}
/// Destroy a context created by `aac_create`.
#[no_mangle]
pub extern "C" fn aac_destroy(handle: *mut AacContext) {
if handle.is_null() {
return;
}
// SAFETY: `handle` must come from `aac_create`, which uses `Box::into_raw`.
unsafe { drop(Box::from_raw(handle)) };
}
+188
View File
@@ -0,0 +1,188 @@
use crate::builder::*;
use crate::graph::*;
use crate::rhai_api::err;
use rhai::{Engine, EvalAltResult};
pub fn register(engine: &mut Engine) {
register_clips(engine);
register_blend_trees(engine);
register_state_motion(engine);
}
fn clip_ref(clip: &ClipBuilder) -> MotionRef {
MotionRef::Clip {
name: clip.name.clone(),
}
}
fn tree_ref(tree: &BlendTreeBuilder) -> MotionRef {
MotionRef::BlendTree {
name: tree.name.clone(),
}
}
fn register_clips(engine: &mut Engine) {
engine.register_fn("clip", |a: &mut Aac, name: &str| -> Result<ClipBuilder, Box<EvalAltResult>> {
if name.trim().is_empty() {
return Err(err("clip name is empty".to_string()));
}
if a.write(|graph| graph.clips.iter().any(|clip| clip.name == name)) {
return Err(err(format!("clip `{name}` is already declared")));
}
a.write(|graph| {
graph.clips.push(ClipData {
name: name.to_string(),
looping: false,
curves: Vec::new(),
})
});
Ok(ClipBuilder {
aac: a.clone(),
name: name.to_string(),
})
});
engine.register_fn("looping", |clip: ClipBuilder, value: bool| {
clip.data_mut(|data| data.looping = value);
clip
});
engine.register_fn(
"keyframe",
|clip: ClipBuilder, path: &str, property: &str, time: f64, value: f64| -> Result<ClipBuilder, Box<EvalAltResult>> {
clip.push_key(path, property, Keyframe::linear(time as f32, value as f32))
.map_err(err)?;
Ok(clip)
},
);
engine.register_fn("toggle", |clip: ClipBuilder, path: &str, value: bool| -> Result<ClipBuilder, Box<EvalAltResult>> {
let value = if value { 1.0 } else { 0.0 };
// A one-frame constant, matching AAC's toggling semantics.
clip.push_key(path, "m_IsActive", Keyframe::linear(0.0, value))
.map_err(err)?;
clip.push_key(path, "m_IsActive", Keyframe::linear(1.0 / 60.0, value))
.map_err(err)?;
Ok(clip)
});
engine.register_fn(
"blend_shape",
|clip: ClipBuilder, path: &str, shape: &str, time: f64, value: f64| -> Result<ClipBuilder, Box<EvalAltResult>> {
let property = format!("blendShape.{shape}");
clip.push_key(path, &property, Keyframe::linear(time as f32, value as f32))
.map_err(err)?;
Ok(clip)
},
);
}
fn register_blend_trees(engine: &mut Engine) {
engine.register_fn("blend_tree", |a: &mut Aac, name: &str| -> Result<BlendTreeBuilder, Box<EvalAltResult>> {
if name.trim().is_empty() {
return Err(err("blend tree name is empty".to_string()));
}
if a.write(|graph| graph.blend_trees.iter().any(|tree| tree.name == name)) {
return Err(err(format!("blend tree `{name}` is already declared")));
}
a.write(|graph| {
graph.blend_trees.push(BlendTreeData {
name: name.to_string(),
blend_type: BlendType::Simple1D,
param_x: String::new(),
param_y: None,
children: Vec::new(),
use_automatic_thresholds: false,
})
});
Ok(BlendTreeBuilder {
aac: a.clone(),
name: name.to_string(),
})
});
engine.register_fn("simple_1d", |tree: BlendTreeBuilder, x: FloatParam| {
tree.configure(BlendType::Simple1D, &x.name, None).map_err(err)?;
Ok::<BlendTreeBuilder, Box<EvalAltResult>>(tree)
});
engine.register_fn("simple_directional_2d", |tree: BlendTreeBuilder, x: FloatParam, y: FloatParam| {
tree.configure(BlendType::SimpleDirectional2D, &x.name, Some(&y.name))
.map_err(err)?;
Ok::<BlendTreeBuilder, Box<EvalAltResult>>(tree)
});
engine.register_fn("freeform_directional_2d", |tree: BlendTreeBuilder, x: FloatParam, y: FloatParam| {
tree.configure(BlendType::FreeformDirectional2D, &x.name, Some(&y.name))
.map_err(err)?;
Ok::<BlendTreeBuilder, Box<EvalAltResult>>(tree)
});
engine.register_fn("freeform_cartesian_2d", |tree: BlendTreeBuilder, x: FloatParam, y: FloatParam| {
tree.configure(BlendType::FreeformCartesian2D, &x.name, Some(&y.name))
.map_err(err)?;
Ok::<BlendTreeBuilder, Box<EvalAltResult>>(tree)
});
engine.register_fn("direct", |tree: BlendTreeBuilder| {
tree.configure(BlendType::Direct, "", None).map_err(err)?;
Ok::<BlendTreeBuilder, Box<EvalAltResult>>(tree)
});
engine.register_fn("automatic_thresholds", |tree: BlendTreeBuilder, value: bool| {
tree.data_mut(|data| data.use_automatic_thresholds = value);
tree
});
engine.register_fn("add_motion", |tree: BlendTreeBuilder, clip: ClipBuilder, threshold: f64| {
push_child(&tree, clip_ref(&clip), threshold, None, None)?;
Ok::<BlendTreeBuilder, Box<EvalAltResult>>(tree)
});
engine.register_fn("add_motion", |tree: BlendTreeBuilder, child: BlendTreeBuilder, threshold: f64| {
push_child(&tree, tree_ref(&child), threshold, None, None)?;
Ok::<BlendTreeBuilder, Box<EvalAltResult>>(tree)
});
engine.register_fn("add_motion", |tree: BlendTreeBuilder, clip: ClipBuilder, x: f64, y: f64| {
push_child(&tree, clip_ref(&clip), x, Some(y), None)?;
Ok::<BlendTreeBuilder, Box<EvalAltResult>>(tree)
});
engine.register_fn("add_motion", |tree: BlendTreeBuilder, child: BlendTreeBuilder, x: f64, y: f64| {
push_child(&tree, tree_ref(&child), x, Some(y), None)?;
Ok::<BlendTreeBuilder, Box<EvalAltResult>>(tree)
});
engine.register_fn("add_motion_direct", |tree: BlendTreeBuilder, clip: ClipBuilder, parameter: FloatParam| {
push_child(&tree, clip_ref(&clip), 0.0, None, Some(&parameter.name))?;
Ok::<BlendTreeBuilder, Box<EvalAltResult>>(tree)
});
engine.register_fn("add_motion_direct", |tree: BlendTreeBuilder, child: BlendTreeBuilder, parameter: FloatParam| {
push_child(&tree, tree_ref(&child), 0.0, None, Some(&parameter.name))?;
Ok::<BlendTreeBuilder, Box<EvalAltResult>>(tree)
});
}
fn register_state_motion(engine: &mut Engine) {
engine.register_fn("set_clip", |state: StateRef, clip: ClipBuilder| {
state.set_motion(clip_ref(&clip));
state
});
engine.register_fn("set_motion", |state: StateRef, clip: ClipBuilder| {
state.set_motion(clip_ref(&clip));
state
});
engine.register_fn("set_motion", |state: StateRef, tree: BlendTreeBuilder| {
state.set_motion(tree_ref(&tree));
state
});
}
fn push_child(
tree: &BlendTreeBuilder,
motion: MotionRef,
threshold: f64,
threshold_y: Option<f64>,
direct_param: Option<&str>,
) -> Result<(), Box<EvalAltResult>> {
let child = BlendChild {
motion,
threshold: threshold as f32,
threshold_y: threshold_y.map(|y| y as f32),
direct_param: direct_param.map(str::to_string),
};
tree.push_child(child).map_err(err)
}
+271
View File
@@ -0,0 +1,271 @@
use crate::builder::*;
use crate::graph::*;
use rhai::{Array, Engine, EvalAltResult, Position};
pub(crate) fn err(message: String) -> Box<EvalAltResult> {
Box::new(EvalAltResult::ErrorRuntime(message.into(), Position::NONE))
}
fn condition(parameter: &str, mode: CondMode, threshold: f32) -> Condition {
Condition {
parameter: parameter.to_string(),
mode,
threshold,
}
}
fn grid(value: i64) -> Result<i32, Box<EvalAltResult>> {
i32::try_from(value).map_err(|_| err(format!("grid position {value} does not fit in a 32-bit integer")))
}
fn add_state(aac: &Aac, layer: usize, machine: &[usize], name: &str, x: i64, y: i64) -> Result<StateRef, Box<EvalAltResult>> {
let position = GridPos::new(grid(x)?, grid(y)?);
let state = aac.write(|graph| {
let states = &mut machine_mut(graph, layer, machine).states;
states.push(State {
name: name.to_string(),
position,
..Default::default()
});
states.len() - 1
});
Ok(StateRef {
aac: aac.clone(),
layer,
machine: machine.to_vec(),
state,
})
}
fn add_sub_machine(
aac: &Aac,
layer: usize,
machine: &[usize],
name: &str,
x: i64,
y: i64,
) -> Result<MachineRef, Box<EvalAltResult>> {
let position = GridPos::new(grid(x)?, grid(y)?);
let index = aac.write(|graph| {
let sub_machines = &mut machine_mut(graph, layer, machine).sub_machines;
sub_machines.push(StateMachine {
name: Some(name.to_string()),
position,
..Default::default()
});
sub_machines.len() - 1
});
let mut path = machine.to_vec();
path.push(index);
Ok(MachineRef {
aac: aac.clone(),
layer,
machine: path,
})
}
fn assert_new_parameter(aac: &Aac, name: &str) -> Result<(), Box<EvalAltResult>> {
if name.trim().is_empty() {
return Err(err("parameter name is empty".to_string()));
}
let duplicate = aac.write(|graph| graph.parameters.iter().any(|p| p.name() == name));
if duplicate {
return Err(err(format!("parameter `{name}` is already declared")));
}
Ok(())
}
pub fn engine(aac: Aac) -> Engine {
let mut engine = Engine::new();
engine.register_type_with_name::<Aac>("Aac");
engine.register_type_with_name::<FloatParam>("FloatParam");
engine.register_type_with_name::<IntParam>("IntParam");
engine.register_type_with_name::<BoolParam>("BoolParam");
engine.register_type_with_name::<Condition>("Condition");
engine.register_type_with_name::<ControllerBuilder>("Controller");
engine.register_type_with_name::<LayerBuilder>("Layer");
engine.register_type_with_name::<MachineRef>("Machine");
engine.register_type_with_name::<StateRef>("State");
engine.register_type_with_name::<TransitionRef>("Transition");
engine.register_type_with_name::<ClipBuilder>("Clip");
engine.register_type_with_name::<BlendTreeBuilder>("BlendTree");
register_conditions(&mut engine);
register_setup(&mut engine, aac);
register_navigation(&mut engine);
register_transitions(&mut engine);
crate::motion_api::register(&mut engine);
engine
}
/// Leaf conditions come from overloaded comparison operators, because Rhai's `&&`/`||` cannot be
/// overloaded. Combine several conditions with `when_all([...])`.
fn register_conditions(engine: &mut Engine) {
engine.register_fn(">", |p: FloatParam, v: f64| condition(&p.name, CondMode::Greater, v as f32));
engine.register_fn("<", |p: FloatParam, v: f64| condition(&p.name, CondMode::Less, v as f32));
engine.register_fn("==", |p: FloatParam, v: f64| condition(&p.name, CondMode::Equals, v as f32));
engine.register_fn("!=", |p: FloatParam, v: f64| condition(&p.name, CondMode::NotEqual, v as f32));
engine.register_fn(">", |p: IntParam, v: i64| condition(&p.name, CondMode::Greater, v as f32));
engine.register_fn("<", |p: IntParam, v: i64| condition(&p.name, CondMode::Less, v as f32));
engine.register_fn("==", |p: IntParam, v: i64| condition(&p.name, CondMode::Equals, v as f32));
engine.register_fn("!=", |p: IntParam, v: i64| condition(&p.name, CondMode::NotEqual, v as f32));
engine.register_fn("==", |p: BoolParam, v: bool| {
condition(&p.name, if v { CondMode::If } else { CondMode::IfNot }, 0.0)
});
engine.register_fn("!=", |p: BoolParam, v: bool| {
condition(&p.name, if v { CondMode::IfNot } else { CondMode::If }, 0.0)
});
}
fn register_setup(engine: &mut Engine, aac: Aac) {
engine.register_fn("AnimatorAsCode", move || aac.clone());
engine.register_fn("system_name", |a: &mut Aac, name: &str| {
a.write(|graph| graph.system_name = name.to_string());
});
engine.register_fn("asset_key", |a: &mut Aac, key: &str| {
a.write(|graph| graph.asset_key = key.to_string());
});
engine.register_fn("float_param", |a: &mut Aac, name: &str, default: f64| -> Result<FloatParam, Box<EvalAltResult>> {
assert_new_parameter(a, name)?;
a.write(|graph| {
graph.parameters.push(Parameter::Float {
name: name.to_string(),
default: default as f32,
})
});
Ok(FloatParam {
name: name.to_string(),
})
});
engine.register_fn("int_param", |a: &mut Aac, name: &str, default: i64| -> Result<IntParam, Box<EvalAltResult>> {
assert_new_parameter(a, name)?;
a.write(|graph| {
graph.parameters.push(Parameter::Int {
name: name.to_string(),
default: default as i32,
})
});
Ok(IntParam {
name: name.to_string(),
})
});
engine.register_fn(
"bool_param",
|a: &mut Aac, name: &str, default: bool| -> Result<BoolParam, Box<EvalAltResult>> {
assert_new_parameter(a, name)?;
a.write(|graph| {
graph.parameters.push(Parameter::Bool {
name: name.to_string(),
default,
})
});
Ok(BoolParam {
name: name.to_string(),
})
},
);
engine.register_fn("new_controller", |a: &mut Aac| ControllerBuilder { aac: a.clone() });
}
fn register_navigation(engine: &mut Engine) {
// There is exactly one controller, so this is the only place layers are attached.
engine.register_fn("layer", |c: ControllerBuilder, name: &str| -> LayerBuilder {
let layer = c.aac.write(|graph| {
graph.controller.layers.push(Layer {
name: name.to_string(),
..Default::default()
});
graph.controller.layers.len() - 1
});
LayerBuilder {
aac: c.aac.clone(),
layer,
}
});
engine.register_fn("state", |l: LayerBuilder, name: &str, x: i64, y: i64| {
add_state(&l.aac, l.layer, &[], name, x, y)
});
engine.register_fn("state", |m: MachineRef, name: &str, x: i64, y: i64| {
add_state(&m.aac, m.layer, &m.machine, name, x, y)
});
engine.register_fn("sub_machine", |l: LayerBuilder, name: &str, x: i64, y: i64| {
add_sub_machine(&l.aac, l.layer, &[], name, x, y)
});
engine.register_fn("sub_machine", |m: MachineRef, name: &str, x: i64, y: i64| {
add_sub_machine(&m.aac, m.layer, &m.machine, name, x, y)
});
engine.register_fn("any_state", |l: LayerBuilder| AnyStateRef {
aac: l.aac.clone(),
layer: l.layer,
machine: Vec::new(),
});
engine.register_fn("any_state", |m: MachineRef| AnyStateRef {
aac: m.aac.clone(),
layer: m.layer,
machine: m.machine.clone(),
});
}
fn register_transitions(engine: &mut Engine) {
engine.register_fn("transition_to", |from: StateRef, to: StateRef| from.transition_to(&to));
engine.register_fn("transition_to", |from: AnyStateRef, to: StateRef| from.transition_to(&to));
engine.register_fn("when", |t: TransitionRef, c: Condition| {
t.add_conditions([c]);
t
});
engine.register_fn(
"when_all",
|t: TransitionRef, conditions: Array| -> Result<TransitionRef, Box<EvalAltResult>> {
let mut collected = Vec::with_capacity(conditions.len());
for value in conditions {
collected.push(
value
.try_cast::<Condition>()
.ok_or_else(|| err("when_all expects a list of conditions".to_string()))?,
);
}
t.add_conditions(collected);
Ok(t)
},
);
engine.register_fn("duration", |t: TransitionRef, seconds: f64| {
t.update(|transition| transition.duration = seconds as f32);
t
});
engine.register_fn("no_exit_time", |t: TransitionRef| {
t.update(|transition| {
transition.has_exit_time = false;
transition.exit_time = 0.0;
});
t
});
engine.register_fn("exit_time", |t: TransitionRef, normalized: f64| {
t.update(|transition| {
transition.has_exit_time = true;
transition.exit_time = normalized as f32;
});
t
});
engine.register_fn("ordered_interruption", |t: TransitionRef, value: bool| {
t.update(|transition| transition.ordered_interruption = value);
t
});
engine.register_fn("source_interruption", |t: TransitionRef| {
t.update(|transition| transition.source_interruption = true);
t
});
engine.register_fn("to_self", |t: TransitionRef| {
t.update(|transition| transition.can_transition_to_self = true);
t
});
}