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
@@ -0,0 +1,407 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AnimatorAsCode.V1;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
namespace AnimatorAsCrab.V1
{
/// <summary>
/// Turns an <see cref="AacCrabGraph"/> into an AnimatorController by driving Animator As Code V1.
/// This uses the modification workflow: the controller is cleared and rebuilt, and no layer of the
/// controller is touched beforehand.
/// </summary>
public static class AacCrabGenerator
{
/// <summary>Clears the AnimatorController and the assets of the same asset key, then rebuilds them.</summary>
public static void Generate(AacCrabGraph graph, AacConfiguration configuration, AnimatorController controller)
{
if (graph == null) throw new ArgumentNullException(nameof(graph));
if (controller == null) throw new ArgumentNullException(nameof(controller));
if (graph.Controller == null) throw new InvalidOperationException("The graph has no controller.");
var aac = AacV1.Create(configuration);
var modification = aac.Modification();
aac.ClearPreviousAssets();
var aacController = modification.ResetAnimatorController(controller);
var layers = new Dictionary<string, AacFlLayer>();
foreach (var layer in graph.Controller.Layers)
{
layers[layer.Name] = aacController.NewLayer(layer.Name);
}
var floatingParameters = CreateParameters(graph, layers, controller);
var clips = new Dictionary<string, AacFlClip>();
foreach (var clip in graph.Clips)
{
clips[clip.Name] = CreateClip(aac, clip);
}
// Blend trees are created in declaration order, so a child motion can only refer to a tree
// that was declared before its parent. This also makes cycles impossible.
var blendTrees = new Dictionary<string, AacFlBlendTree>();
foreach (var blendTree in graph.BlendTrees)
{
blendTrees[blendTree.Name] = CreateBlendTree(aac, blendTree, clips, blendTrees, floatingParameters);
}
foreach (var layer in graph.Controller.Layers)
{
BuildLayer(layers[layer.Name], layer, clips, blendTrees);
}
modification.SetDirtyAll();
EditorUtility.SetDirty(controller);
}
private static IReadOnlyDictionary<string, AacFlFloatParameter> CreateParameters(AacCrabGraph graph, IReadOnlyDictionary<string, AacFlLayer> layers, AnimatorController controller)
{
var floatingParameters = new Dictionary<string, AacFlFloatParameter>();
if (graph.Parameters.Count == 0)
{
return floatingParameters;
}
if (layers.Count == 0)
{
throw new InvalidOperationException("The controller declares parameters but no layer to hold them.");
}
// Parameters are controller-wide, so any layer can create them.
var layer = layers.First().Value;
foreach (var parameter in graph.Parameters)
{
switch (parameter.Type)
{
case AacCrabParameterType.Float:
floatingParameters[parameter.Name] = layer.FloatParameter(parameter.Name);
break;
case AacCrabParameterType.Int:
layer.IntParameter(parameter.Name);
break;
case AacCrabParameterType.Bool:
layer.BoolParameter(parameter.Name);
break;
default:
throw new InvalidOperationException($"Unknown parameter type {parameter.Type}.");
}
}
ApplyParameterDefaults(controller, graph.Parameters);
return floatingParameters;
}
// Animator As Code creates parameters with Unity's defaults; the graph's defaults are applied
// afterwards by mutating the controller's own parameter list.
private static void ApplyParameterDefaults(AnimatorController controller, IEnumerable<AacCrabParameter> parameters)
{
var wanted = parameters.ToDictionary(parameter => parameter.Name);
var current = controller.parameters;
foreach (var parameter in current)
{
if (!wanted.TryGetValue(parameter.name, out var declared))
{
continue;
}
switch (declared.Type)
{
case AacCrabParameterType.Float:
parameter.defaultFloat = declared.DefaultFloat;
break;
case AacCrabParameterType.Int:
parameter.defaultInt = declared.DefaultInt;
break;
case AacCrabParameterType.Bool:
parameter.defaultBool = declared.DefaultBool;
break;
}
}
controller.parameters = current;
}
private static AacFlClip CreateClip(AacFlBase aac, AacCrabClip graph)
{
var clip = aac.NewClip(graph.Name);
if (graph.Looping)
{
clip.Looping();
}
else
{
clip.NonLooping();
}
return clip.Animating(edit =>
{
foreach (var curve in graph.Curves)
{
var keys = curve.Keys
.Select(key => new Keyframe(key.Time, key.Value, key.InTangent, key.OutTangent))
.ToArray();
edit.Animates(curve.Path, UnityType(curve.Target), curve.Property)
.WithAnimationCurve(new AnimationCurve(keys));
}
});
}
private static Type UnityType(AacCrabTargetType target)
{
switch (target)
{
case AacCrabTargetType.GameObject:
return typeof(GameObject);
case AacCrabTargetType.SkinnedMeshRenderer:
return typeof(SkinnedMeshRenderer);
default:
throw new InvalidOperationException($"Unknown curve target {target}.");
}
}
private static AacFlBlendTree CreateBlendTree(
AacFlBase aac,
AacCrabBlendTree graph,
IReadOnlyDictionary<string, AacFlClip> clips,
IReadOnlyDictionary<string, AacFlBlendTree> trees,
IReadOnlyDictionary<string, AacFlFloatParameter> floatingParameters)
{
Motion Resolve(AacCrabMotionRef reference) => MotionOf(reference, clips, trees);
AacFlFloatParameter FloatParameter(string name)
{
if (name == null || !floatingParameters.TryGetValue(name, out var parameter))
{
throw new InvalidOperationException($"Blend tree '{graph.Name}' uses '{name}', which is not a Float parameter.");
}
return parameter;
}
var uninitialized = aac.NewBlendTree(graph.Name);
switch (graph.BlendType)
{
case AacCrabBlendType.Simple1D:
{
var tree = uninitialized.Simple1D(FloatParameter(graph.ParamX));
tree.BlendTree.useAutomaticThresholds = graph.UseAutomaticThresholds;
foreach (var child in graph.Children)
{
tree.WithAnimation(Resolve(child.Motion), child.Threshold ?? 0f);
}
return tree;
}
case AacCrabBlendType.SimpleDirectional2D:
case AacCrabBlendType.FreeformDirectional2D:
case AacCrabBlendType.FreeformCartesian2D:
{
var x = FloatParameter(graph.ParamX);
var y = FloatParameter(graph.ParamY);
var tree = graph.BlendType == AacCrabBlendType.SimpleDirectional2D
? uninitialized.SimpleDirectional2D(x, y)
: graph.BlendType == AacCrabBlendType.FreeformDirectional2D
? uninitialized.FreeformDirectional2D(x, y)
: uninitialized.FreeformCartesian2D(x, y);
foreach (var child in graph.Children)
{
tree.WithAnimation(Resolve(child.Motion), child.Threshold ?? 0f, child.ThresholdY ?? 0f);
}
return tree;
}
case AacCrabBlendType.Direct:
{
var tree = uninitialized.Direct();
foreach (var child in graph.Children)
{
tree.WithAnimation(Resolve(child.Motion), FloatParameter(child.DirectParam));
}
return tree;
}
default:
throw new InvalidOperationException($"Unknown blend type {graph.BlendType}.");
}
}
private static Motion MotionOf(
AacCrabMotionRef reference,
IReadOnlyDictionary<string, AacFlClip> clips,
IReadOnlyDictionary<string, AacFlBlendTree> trees)
{
switch (reference.Type)
{
case AacCrabMotionType.Clip:
if (!clips.TryGetValue(reference.Name, out var clip))
{
throw new InvalidOperationException($"Clip '{reference.Name}' is not declared.");
}
return clip.Clip;
case AacCrabMotionType.BlendTree:
// Blend trees are built in declaration order, so a tree can only refer to an earlier one.
if (!trees.TryGetValue(reference.Name, out var tree))
{
throw new InvalidOperationException($"Blend tree '{reference.Name}' is not declared yet; declare it before the motion that uses it.");
}
return tree.BlendTree;
default:
throw new InvalidOperationException($"Unknown motion type {reference.Type}.");
}
}
private sealed class MachineScope
{
public AacFlStateMachine Machine;
public AacCrabStateMachine Graph;
}
private static void BuildLayer(
AacFlLayer layer,
AacCrabLayer graph,
IReadOnlyDictionary<string, AacFlClip> clips,
IReadOnlyDictionary<string, AacFlBlendTree> trees)
{
if (graph.StateMachine == null)
{
throw new InvalidOperationException($"Layer '{graph.Name}' has no state machine.");
}
Motion Resolve(AacCrabMotionRef reference) => MotionOf(reference, clips, trees);
// State names are unique within a layer, so a single dictionary resolves every transition,
// including the ones that cross state machines.
var states = new Dictionary<string, AacFlState>();
var scopes = new List<MachineScope>();
AacFlStateMachine CreateMachine(AacFlStateMachine machine, AacCrabStateMachine machineGraph)
{
scopes.Add(new MachineScope { Machine = machine, Graph = machineGraph });
foreach (var state in machineGraph.States)
{
var aacState = machine.NewState(state.Name, state.Position.X, state.Position.Y);
if (state.Motion != null)
{
aacState.WithAnimation(Resolve(state.Motion));
}
states[state.Name] = aacState;
}
foreach (var subMachine in machineGraph.SubMachines)
{
CreateMachine(machine.NewSubStateMachine(subMachine.Name, subMachine.Position.X, subMachine.Position.Y), subMachine);
}
return machine;
}
// Create every state first: transitions may point forward.
CreateMachine(layer.StateMachine, graph.StateMachine);
foreach (var scope in scopes)
{
foreach (var state in scope.Graph.States)
{
foreach (var transition in state.Transitions)
{
ApplyTransition(states[state.Name].TransitionsTo(Destination(states, transition)), transition);
}
}
foreach (var transition in scope.Graph.AnyStateTransitions)
{
ApplyTransition(scope.Machine.AnyTransitionsTo(Destination(states, transition)), transition);
}
}
}
private static AacFlState Destination(IReadOnlyDictionary<string, AacFlState> states, AacCrabTransition transition)
{
if (!states.TryGetValue(transition.To, out var destination))
{
throw new InvalidOperationException($"Transition target '{transition.To}' is not a state in this layer.");
}
return destination;
}
private static void ApplyTransition(AacFlTransition transition, AacCrabTransition graph)
{
transition.WithTransitionDurationSeconds(graph.Duration);
if (graph.OrderedInterruption)
{
transition.WithOrderedInterruption();
}
else
{
transition.WithNoOrderedInterruption();
}
if (graph.SourceInterruption)
{
transition.WithSourceInterruption();
}
if (graph.CanTransitionToSelf)
{
transition.WithTransitionToSelf();
}
if (graph.HasExitTime)
{
transition.AfterAnimationIsAtLeastAtNormalized(graph.ExitTime);
}
// Conditions are applied last: Animator As Code forbids configuring a transition afterwards.
if (graph.Conditions.Count == 0)
{
return;
}
var continuation = transition.When(Condition(graph.Conditions[0]));
for (var index = 1; index < graph.Conditions.Count; index++)
{
continuation = continuation.And(Condition(graph.Conditions[index]));
}
}
// Animator As Code only exposes typed comparisons for some parameter types, so conditions are
// built the same way the library builds them internally.
private static IAacFlCondition Condition(AacCrabCondition condition)
{
var parameter = condition.Parameter;
var mode = UnityConditionMode(condition.Mode);
var threshold = condition.Threshold;
return AacFlConditionSimple.Just(appender => appender.Add(parameter, mode, threshold));
}
private static AnimatorConditionMode UnityConditionMode(AacCrabCondMode mode)
{
switch (mode)
{
case AacCrabCondMode.Greater:
return AnimatorConditionMode.Greater;
case AacCrabCondMode.Less:
return AnimatorConditionMode.Less;
case AacCrabCondMode.Equals:
return AnimatorConditionMode.Equals;
case AacCrabCondMode.NotEqual:
return AnimatorConditionMode.NotEqual;
case AacCrabCondMode.If:
return AnimatorConditionMode.If;
case AacCrabCondMode.IfNot:
return AnimatorConditionMode.IfNot;
default:
throw new InvalidOperationException($"Unknown condition mode {mode}.");
}
}
}
}
@@ -0,0 +1,192 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
namespace AnimatorAsCrab.V1
{
/// <summary>
/// The wire format produced by the Rust core. Keep in sync with rust/src/graph.rs.
/// Property names are snake_cased by the serializer settings, so C# names must only differ
/// from the wire format by casing; enum values are explicit.
/// </summary>
public static class AacCrabJson
{
public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
{
// A graph the generator does not understand is a bug, not something to ignore.
MissingMemberHandling = MissingMemberHandling.Error,
ContractResolver = new DefaultContractResolver { NamingStrategy = new SnakeCaseNamingStrategy() },
Converters = { new StringEnumConverter() },
};
public static AacCrabGraph Parse(string json)
{
return JsonConvert.DeserializeObject<AacCrabGraph>(json, Settings);
}
}
public sealed class AacCrabGraph
{
public string SystemName { get; set; }
public string AssetKey { get; set; }
public List<AacCrabParameter> Parameters { get; set; }
public List<AacCrabClip> Clips { get; set; }
public List<AacCrabBlendTree> BlendTrees { get; set; }
public AacCrabController Controller { get; set; }
}
public sealed class AacCrabParameter
{
public AacCrabParameterType Type { get; set; }
public string Name { get; set; }
public JToken Default { get; set; }
public float DefaultFloat => Default.Value<float>();
public int DefaultInt => Default.Value<int>();
public bool DefaultBool => Default.Value<bool>();
}
public enum AacCrabParameterType
{
[EnumMember(Value = "float")] Float,
[EnumMember(Value = "int")] Int,
[EnumMember(Value = "bool")] Bool,
}
public sealed class AacCrabClip
{
public string Name { get; set; }
public bool Looping { get; set; }
public List<AacCrabCurve> Curves { get; set; }
}
public sealed class AacCrabCurve
{
public string Path { get; set; }
public AacCrabTargetType Target { get; set; }
public string Property { get; set; }
public List<AacCrabKeyframe> Keys { get; set; }
}
public enum AacCrabTargetType
{
[EnumMember(Value = "game_object")] GameObject,
[EnumMember(Value = "skinned_mesh_renderer")] SkinnedMeshRenderer,
}
public sealed class AacCrabKeyframe
{
public float Time { get; set; }
public float Value { get; set; }
public float InTangent { get; set; }
public float OutTangent { get; set; }
}
public sealed class AacCrabBlendTree
{
public string Name { get; set; }
public AacCrabBlendType BlendType { get; set; }
public string ParamX { get; set; }
public string ParamY { get; set; }
public List<AacCrabBlendChild> Children { get; set; }
public bool UseAutomaticThresholds { get; set; }
}
public enum AacCrabBlendType
{
[EnumMember(Value = "simple_1d")] Simple1D,
[EnumMember(Value = "simple_directional_2d")] SimpleDirectional2D,
[EnumMember(Value = "freeform_directional_2d")] FreeformDirectional2D,
[EnumMember(Value = "freeform_cartesian_2d")] FreeformCartesian2D,
[EnumMember(Value = "direct")] Direct,
}
public sealed class AacCrabBlendChild
{
public AacCrabMotionRef Motion { get; set; }
public float? Threshold { get; set; }
public float? ThresholdY { get; set; }
public string DirectParam { get; set; }
}
/// <summary>A motion is always a reference: clips and blend trees are declared at the top level.</summary>
public sealed class AacCrabMotionRef
{
public AacCrabMotionType Type { get; set; }
public string Name { get; set; }
}
public enum AacCrabMotionType
{
[EnumMember(Value = "clip")] Clip,
[EnumMember(Value = "blend_tree")] BlendTree,
}
public sealed class AacCrabController
{
public List<AacCrabLayer> Layers { get; set; }
}
public sealed class AacCrabLayer
{
public string Name { get; set; }
public AacCrabStateMachine StateMachine { get; set; }
}
/// <summary>Name is null for the root state machine of a layer.</summary>
public sealed class AacCrabStateMachine
{
public string Name { get; set; }
public AacCrabGridPos Position { get; set; }
public List<AacCrabState> States { get; set; }
public List<AacCrabStateMachine> SubMachines { get; set; }
public List<AacCrabTransition> AnyStateTransitions { get; set; }
}
public sealed class AacCrabState
{
public string Name { get; set; }
public AacCrabGridPos Position { get; set; }
public AacCrabMotionRef Motion { get; set; }
public List<AacCrabTransition> Transitions { get; set; }
}
public sealed class AacCrabTransition
{
/// <summary>The name of the destination state, within the same layer.</summary>
public string To { get; set; }
public List<AacCrabCondition> Conditions { get; set; }
public bool HasExitTime { get; set; }
public float ExitTime { get; set; }
public float Duration { get; set; }
public bool OrderedInterruption { get; set; }
public bool SourceInterruption { get; set; }
public bool CanTransitionToSelf { get; set; }
}
public sealed class AacCrabCondition
{
public string Parameter { get; set; }
public AacCrabCondMode Mode { get; set; }
public float Threshold { get; set; }
}
public enum AacCrabCondMode
{
[EnumMember(Value = "greater")] Greater,
[EnumMember(Value = "less")] Less,
[EnumMember(Value = "equals")] Equals,
[EnumMember(Value = "not_equal")] NotEqual,
[EnumMember(Value = "if")] If,
[EnumMember(Value = "if_not")] IfNot,
}
public struct AacCrabGridPos
{
public int X { get; set; }
public int Y { get; set; }
}
}
@@ -0,0 +1,115 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace AnimatorAsCrab.V1
{
/// <summary>
/// P/Invoke bindings for `libaac` (see rust/src/lib.rs). The native library is expected to live
/// in the project's Assets/Plugins folder; place libaac.so, libaac.dll or libaac.dylib there.
/// </summary>
public static class AacCrabNative
{
private const string Library = "libaac";
[DllImport(Library, CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr aac_create();
[DllImport(Library, CallingConvention = CallingConvention.Cdecl)]
private static extern int aac_eval_rhai(IntPtr context, byte[] script);
[DllImport(Library, CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr aac_to_json(IntPtr context);
[DllImport(Library, CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr aac_last_error(IntPtr context);
[DllImport(Library, CallingConvention = CallingConvention.Cdecl)]
private static extern void aac_free_string(IntPtr value);
[DllImport(Library, CallingConvention = CallingConvention.Cdecl)]
private static extern void aac_destroy(IntPtr context);
/// <summary>
/// Evaluate a Rhai script and return the graph as JSON, or null with a message in
/// <paramref name="error"/>. The context lives only for the duration of this call.
/// </summary>
public static string Evaluate(string script, out string error)
{
IntPtr context;
try
{
context = aac_create();
}
catch (DllNotFoundException exception)
{
error = $"Could not load {Library}: {exception.Message}. Place the native library in Assets/Plugins.";
return null;
}
if (context == IntPtr.Zero)
{
error = "aac_create returned null.";
return null;
}
try
{
var status = aac_eval_rhai(context, Utf8Z(script));
if (status != 0)
{
error = ReadUtf8(aac_last_error(context)) ?? $"The script failed with status {status}.";
return null;
}
var json = aac_to_json(context);
if (json == IntPtr.Zero)
{
error = ReadUtf8(aac_last_error(context)) ?? "The graph could not be serialized.";
return null;
}
try
{
error = null;
return ReadUtf8(json);
}
finally
{
aac_free_string(json);
}
}
finally
{
aac_destroy(context);
}
}
private static byte[] Utf8Z(string value)
{
var bytes = Encoding.UTF8.GetBytes(value);
var terminated = new byte[bytes.Length + 1];
Array.Copy(bytes, terminated, bytes.Length);
return terminated;
}
private static string ReadUtf8(IntPtr pointer)
{
if (pointer == IntPtr.Zero)
{
return null;
}
// Marshal.PtrToStringUTF8 is not available on all Unity runtimes, so the bytes are copied by hand.
var length = 0;
while (Marshal.ReadByte(pointer, length) != 0)
{
length++;
}
var bytes = new byte[length];
Marshal.Copy(pointer, bytes, 0, length);
return Encoding.UTF8.GetString(bytes);
}
}
}
@@ -0,0 +1,201 @@
using System;
using System.IO;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
namespace AnimatorAsCrab.V1
{
/// <summary>
/// Evaluates a Rhai script with libaac and generates the Animator Controller through Animator As Code.
/// The Unity-side configuration is never guessed: every field below is supplied by the user, and the
/// system name and asset key are declared by the script itself.
/// </summary>
public class AacCrabWindow : EditorWindow
{
[SerializeField] private string _scriptPath = "";
[SerializeField] private bool _generateOnScriptChange = true;
[SerializeField] private AnimatorController _controller;
[SerializeField] private Transform _animatorRoot;
[SerializeField] private UnityEngine.Object _assetContainer;
[SerializeField] private AacConfiguration.Container _containerMode = AacConfiguration.Container.Everything;
[SerializeField] private bool _writeDefaults;
[SerializeField] private long _lastWriteUtcTicks;
private string _error;
private string _status;
private bool _scriptChanged;
[MenuItem("Tools/Animator As Crab")]
public static void Open()
{
var window = GetWindow<AacCrabWindow>();
window.titleContent = new GUIContent("Animator As Crab");
window.Show();
}
private void OnEnable()
{
EditorApplication.update += OnUpdate;
_scriptChanged = _lastWriteUtcTicks != 0 && _lastWriteUtcTicks != LastWriteUtcTicks();
_lastWriteUtcTicks = LastWriteUtcTicks();
}
private void OnDisable()
{
EditorApplication.update -= OnUpdate;
}
private void OnUpdate()
{
if (string.IsNullOrEmpty(_scriptPath) || !File.Exists(_scriptPath))
{
return;
}
var ticks = LastWriteUtcTicks();
if (ticks == _lastWriteUtcTicks)
{
return;
}
_lastWriteUtcTicks = ticks;
_scriptChanged = true;
if (_generateOnScriptChange)
{
Generate();
}
else
{
Repaint();
}
}
private void OnGUI()
{
EditorGUILayout.LabelField("Rhai script", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
_scriptPath = EditorGUILayout.TextField(_scriptPath);
if (GUILayout.Button("...", GUILayout.Width(28)))
{
var directory = string.IsNullOrEmpty(_scriptPath) ? null : Path.GetDirectoryName(_scriptPath);
var picked = EditorUtility.OpenFilePanel("Rhai script", directory ?? string.Empty, "rhai");
if (!string.IsNullOrEmpty(picked))
{
_scriptPath = picked;
_lastWriteUtcTicks = LastWriteUtcTicks();
_scriptChanged = false;
Generate();
}
}
EditorGUILayout.EndHorizontal();
_generateOnScriptChange = EditorGUILayout.Toggle("Generate when the script changes", _generateOnScriptChange);
EditorGUILayout.Space();
EditorGUILayout.LabelField("Unity side", EditorStyles.boldLabel);
_controller = (AnimatorController)EditorGUILayout.ObjectField("Animator Controller", _controller, typeof(AnimatorController), false);
_animatorRoot = (Transform)EditorGUILayout.ObjectField("Animator Root", _animatorRoot, typeof(Transform), true);
_assetContainer = EditorGUILayout.ObjectField("Asset Container", _assetContainer, typeof(UnityEngine.Object), false);
_containerMode = (AacConfiguration.Container)EditorGUILayout.EnumPopup("Container Mode", _containerMode);
_writeDefaults = EditorGUILayout.Toggle("Write Defaults", _writeDefaults);
EditorGUILayout.Space();
if (GUILayout.Button("Generate", GUILayout.Height(24)))
{
Generate();
}
EditorGUILayout.Space();
if (_error != null)
{
EditorGUILayout.HelpBox(_error, MessageType.Error);
}
else if (_scriptChanged)
{
EditorGUILayout.HelpBox("The script changed since the last generation.", MessageType.Warning);
}
if (_status != null)
{
EditorGUILayout.HelpBox(_status, MessageType.Info);
}
}
private void Generate()
{
_error = null;
_status = null;
_scriptChanged = false;
try
{
_status = GenerateOrThrow();
}
catch (Exception exception)
{
_error = exception.Message;
Debug.LogException(exception);
}
Repaint();
}
private string GenerateOrThrow()
{
if (string.IsNullOrEmpty(_scriptPath))
{
throw new InvalidOperationException("Select a Rhai script.");
}
if (!File.Exists(_scriptPath))
{
throw new InvalidOperationException($"'{_scriptPath}' does not exist.");
}
if (_controller == null)
{
throw new InvalidOperationException("Select the Animator Controller to generate into.");
}
if (_animatorRoot == null)
{
throw new InvalidOperationException("Select the animator root Transform.");
}
if (_assetContainer == null)
{
throw new InvalidOperationException("Select the asset container that will hold the generated clips and blend trees.");
}
var json = AacCrabNative.Evaluate(File.ReadAllText(_scriptPath), out var nativeError);
if (json == null)
{
throw new InvalidOperationException(nativeError);
}
var graph = AacCrabJson.Parse(json);
var configuration = new AacConfiguration
{
SystemName = graph.SystemName,
AssetKey = graph.AssetKey,
AnimatorRoot = _animatorRoot,
AssetContainer = _assetContainer,
ContainerMode = _containerMode,
DefaultsProvider = new AacDefaultsProvider(_writeDefaults),
};
AacCrabGenerator.Generate(graph, configuration, _controller);
AssetDatabase.SaveAssets();
return $"Generated '{graph.SystemName}' with asset key '{graph.AssetKey}': " +
$"{graph.Controller.Layers.Count} layer(s), {graph.Clips.Count} clip(s), {graph.BlendTrees.Count} blend tree(s).";
}
private long LastWriteUtcTicks()
{
return string.IsNullOrEmpty(_scriptPath) || !File.Exists(_scriptPath)
? 0
: File.GetLastWriteTimeUtc(_scriptPath).Ticks;
}
}
}