326 lines
12 KiB
Rust
326 lines
12 KiB
Rust
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, ¶meters, &mut errors);
|
|
validate_layers(graph, ¶meters, &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(_) => {}
|
|
}
|
|
}
|