Skip to content

Commit

Permalink
AnimationsAPI (#553)
Browse files Browse the repository at this point in the history
  • Loading branch information
KingEnderBrine authored Oct 17, 2024
1 parent 6c90353 commit 78685d4
Show file tree
Hide file tree
Showing 20 changed files with 1,081 additions and 0 deletions.
190 changes: 190 additions & 0 deletions R2API.Animations/AnimationsAPI.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using AssetsTools.NET.Extra;
using BepInEx;
using R2API.AutoVersionGen;
using UnityEngine;

namespace R2API;

/// <summary>
/// API for modifying <see cref="RuntimeAnimatorController" />
/// </summary>
#pragma warning disable CS0436 // Type conflicts with imported type
[AutoVersion]
#pragma warning restore CS0436 // Type conflicts with imported type
public static partial class AnimationsAPI
{
public const string PluginGUID = R2API.PluginGUID + ".animations";
public const string PluginName = R2API.PluginName + ".Animations";

private const string bundleExtension = ".bundle";
private const string hashExtension = ".hash";
private const string dummyBundleName = "dummy_controller_bundle";
private const long dummyAnimatorControllerPathID = -27250459394986890;
private const string dummyAnimatorControllerPath = "assets/dummycontroller.controller";

private static readonly Dictionary<(string, RuntimeAnimatorController), List<AnimatorModifications>> controllerModifications = [];
private static readonly Dictionary<RuntimeAnimatorController, List<Animator>> controllerToAnimators = [];
private static readonly List<UnityEngine.Object> cache = [];

private static bool _hooksEnabled = false;
private static bool _requestsDone = false;

internal static void SetHooks()
{
if (_hooksEnabled)
{
return;
}

RoR2.RoR2Application.onLoad += OnLoad;

_hooksEnabled = true;
}

internal static void UnsetHooks()
{
RoR2.RoR2Application.onLoad -= OnLoad;

_hooksEnabled = false;
}

private static void OnLoad()
{
NativeHelpers.Init();
ApplyModifications();
}


/// <summary>
/// Add <see cref="RuntimeAnimatorController" /> modifications
/// </summary>
/// <param name="sourceBundlePath">Path to a bundle containing <see cref="RuntimeAnimatorController" />. For game bundles you can do System.IO.Path.Combine(Addressables.RuntimePath, "StandaloneWindows64", "bundle_name")</param>
/// <param name="sourceAnimatorController"><see cref="RuntimeAnimatorController" /> to which modifications would be applied</param>
/// <param name="modifications">Modifications for <see cref="RuntimeAnimatorController" /></param>
public static void AddModifications(
string sourceBundlePath,
RuntimeAnimatorController sourceAnimatorController,
AnimatorModifications modifications)
{
SetHooks();

var list = controllerModifications.GetOrAddDefault((sourceBundlePath, sourceAnimatorController), () => []);
list.Add(modifications);
}

/// <summary>
/// Mapping <see cref="RuntimeAnimatorController" /> to an <see cref="Animator"/>. After modified controller will be created it will be applied to mapped animator.
/// </summary>
/// <param name="animator"><see cref="Animator"/> component from a prefab</param>
/// <param name="controller"><see cref="RuntimeAnimatorController"/> that will have modifications applied</param>
public static void AddAnimatorController(Animator animator, RuntimeAnimatorController controller)
{
SetHooks();

var animators = controllerToAnimators.GetOrAddDefault(controller, () => []);
animators.Add(animator);
}

internal static void ApplyModifications()
{
var manager = new AssetsManager();

var dummyPath = Path.Combine(Path.GetDirectoryName(AnimationsPlugin.Instance.Info.Location), dummyBundleName);
foreach (var ((sourceBundlePath, sourceAnimatorController), modifications) in controllerModifications)
{
var sourceAnimatorControllerPathID = NativeHelpers.GetAssetPathID(sourceAnimatorController);
var modifiedBundlePath = Path.Combine(
Paths.CachePath,
"R2API.Animations",
$"{Path.GetFileName(sourceBundlePath)}_{sourceAnimatorControllerPathID}{bundleExtension}");
var hashPath = Path.Combine(
Paths.CachePath,
"R2API.Animations",
$"{Path.GetFileName(sourceBundlePath)}_{sourceAnimatorControllerPathID}{hashExtension}");

var ignoreCache = AnimationsPlugin.IgnoreCache.Value;
string hash = null;
if (ignoreCache || !CachedBundleExists(modifiedBundlePath, hashPath, sourceAnimatorControllerPathID, modifications, out hash))
{
var bundleFile = manager.LoadBundleFile(sourceBundlePath);
var assetFile = manager.LoadAssetsFileFromBundle(bundleFile, 0);

var dummyBundleFile = manager.LoadBundleFile(dummyPath);
var dummyAssetFile = manager.LoadAssetsFileFromBundle(dummyBundleFile, 0);

var creator = new ModificationsBundleCreator(
manager,
assetFile,
sourceAnimatorControllerPathID,
dummyAssetFile,
dummyBundleFile,
dummyAnimatorControllerPathID,
modifications,
modifiedBundlePath
);

creator.Run();
if (!ignoreCache)
{
File.WriteAllText(hashPath, hash);
}

manager.UnloadAssetsFile(dummyAssetFile);
manager.UnloadBundleFile(dummyBundleFile);
}

var modifiedBundle = AssetBundle.LoadFromFile(modifiedBundlePath);
var modifiedAnimatorController = modifiedBundle.LoadAsset<RuntimeAnimatorController>(dummyAnimatorControllerPath);

if (controllerToAnimators.TryGetValue(sourceAnimatorController, out var animators))
{
foreach (var animator in animators)
{
animator.runtimeAnimatorController = modifiedAnimatorController;
}
}

cache.Add(sourceAnimatorController);
cache.Add(modifiedAnimatorController);
}

manager.UnloadAll(true);
controllerModifications.Clear();
controllerToAnimators.Clear();
}

private static bool CachedBundleExists(string modifiedBundlePath, string hashPath, long sourceAnimatorControllerPathID, List<AnimatorModifications> modifications, out string hash)
{
using (var md5 = MD5.Create())
using (var stream = new MemoryStream())
using (var writer = new BinaryWriter(stream))
{
writer.Write(RoR2.RoR2Application.buildId);
writer.Write(sourceAnimatorControllerPathID);
foreach (var modification in modifications)
{
modification.WriteBinary(writer);
}
var hashBytes = md5.ComputeHash(stream);
hash = BitConverter.ToString(hashBytes);
}

if (!File.Exists(modifiedBundlePath))
{
return false;
}

if (!File.Exists(hashPath))
{
return false;
}

var cachedHash = File.ReadAllText(hashPath);

return hash == cachedHash;
}
}
25 changes: 25 additions & 0 deletions R2API.Animations/AnimationsPlugin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;

namespace R2API;

[BepInPlugin(AnimationsAPI.PluginGUID, AnimationsAPI.PluginName, AnimationsAPI.PluginVersion)]
public sealed class AnimationsPlugin : BaseUnityPlugin
{
internal static AnimationsPlugin Instance { get; private set; }
internal static new ManualLogSource Logger { get; private set; }
internal static ConfigEntry<bool> IgnoreCache { get; private set; }

private void Awake()
{
Instance = this;
Logger = base.Logger;
IgnoreCache = Config.Bind("Dev", nameof(IgnoreCache), false, "Always generate new bundles with modifications");
}

private void OnDestroy()
{
AnimationsAPI.UnsetHooks();
}
}
66 changes: 66 additions & 0 deletions R2API.Animations/AnimatorModifications.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System.Collections.Generic;
using System.IO;
using BepInEx;
using R2API.Models;
using UnityEngine;

namespace R2API;

/// <summary>
/// Modifications for a <see cref="RuntimeAnimatorController"/>
/// </summary>
public class AnimatorModifications
{
/// <summary>
/// A key for caching, calculated from plugin info.
/// </summary>
public string Key { get; }
/// <summary>
/// New states to add. The Key is a layer name to which a state will be added.
/// </summary>
public Dictionary<string, State> NewStates { get; } = [];
/// <summary>
/// New transitions to add. The key is a (layer name, state name)
/// </summary>
public Dictionary<(string, string), Transition> NewTransitions { get; } = [];
/// <summary>
/// New parameters to add.
/// </summary>
public List<Parameter> NewParameters { get; } = [];

/// <summary>
/// Modifications for a <see cref="RuntimeAnimatorController"/>
/// </summary>
/// <param name="plugin">BepInPlugin instance</param>
public AnimatorModifications(BepInPlugin plugin)
{
Key = $"{plugin.GUID};{plugin.Version}";
}

/// <summary>
/// Writing modifications into a binary writer for caching purposes.
/// </summary>
/// <param name="writer"></param>
public void WriteBinary(BinaryWriter writer)
{
writer.Write(Key);

foreach (var (layer, state) in NewStates)
{
writer.Write(layer);
state.WriteBinary(writer);
}

foreach (var ((layer, state), transition) in NewTransitions)
{
writer.Write(layer);
writer.Write(state);
transition.WriteBinary(writer);
}

foreach (var parameter in NewParameters)
{
parameter.WriteBinary(writer);
}
}
}
18 changes: 18 additions & 0 deletions R2API.Animations/Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace R2API;

internal static class Extensions
{
public static TValue GetOrAddDefault<TKey, TValue>(this Dictionary<TKey, TValue> dict, TKey key, Func<TValue> defaultValueFunc)
{
if (dict.TryGetValue(key, out var value))
{
return value;
}

return dict[key] = defaultValueFunc();
}
}
25 changes: 25 additions & 0 deletions R2API.Animations/Models/Condition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;

namespace R2API.Models;

public class Condition
{
public ConditionMode ConditionMode { get; set; }
public string ParamName { get; set; }
public float Value { get; set; }

/// <summary>
/// Writing into a binary writer for caching purposes.
/// </summary>
/// <param name="writer"></param>
public void WriteBinary(BinaryWriter writer)
{
writer.Write((int)ConditionMode);
writer.Write(ParamName ?? "");
writer.Write(Value.ToString(CultureInfo.InvariantCulture));
}
}
16 changes: 16 additions & 0 deletions R2API.Animations/Models/ConditionMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace R2API.Models;

public enum ConditionMode
{
IsTrue = 1,
IsFalse = 2,
IsGreater = 3,
IsLess = 4,
//IsExitTime = 5,
IsEqual = 6,
IsNotEqual = 7,
}
24 changes: 24 additions & 0 deletions R2API.Animations/Models/Parameter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace R2API.Models;

public class Parameter
{
public string Name { get; set; }
public ParameterType Type { get; set; }
public ParameterValue Value { get; set; }

/// <summary>
/// Writing into a binary writer for caching purposes.
/// </summary>
/// <param name="writer"></param>
public void WriteBinary(BinaryWriter writer)
{
writer.Write(Name ?? "");
writer.Write((int)Type);
Value.WriteBinary(writer);
}
}
13 changes: 13 additions & 0 deletions R2API.Animations/Models/ParameterType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace R2API.Models;

public enum ParameterType
{
Float = 1,
Int = 3,
Bool = 4,
Trigger = 9
}
Loading

0 comments on commit 78685d4

Please sign in to comment.