From 05c9a7e7b65d68e9855a4eb7cb95de3a7aaf4fe1 Mon Sep 17 00:00:00 2001 From: bd_ Date: Sun, 17 Nov 2024 16:51:48 -0800 Subject: [PATCH] feat: DependsOnContext --- CHANGELOG.md | 1 + Editor/API/Attributes/DependsOnContext.cs | 20 +++++ .../API/Attributes/DependsOnContext.cs.meta | 3 + Editor/API/IExtensionContext.cs | 53 ++++++++++- Editor/API/Model/SolverPass.cs | 23 ++++- Editor/API/Solver/PluginResolver.cs | 40 ++++++++- .../ExtensionDependenciesTest.cs | 88 +++++++++++++++++++ .../ExtensionDependenciesTest.cs.meta | 3 + 8 files changed, 225 insertions(+), 6 deletions(-) create mode 100644 Editor/API/Attributes/DependsOnContext.cs create mode 100644 Editor/API/Attributes/DependsOnContext.cs.meta create mode 100644 UnitTests~/PluginResolverTests/ExtensionDependenciesTest.cs create mode 100644 UnitTests~/PluginResolverTests/ExtensionDependenciesTest.cs.meta diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b9d9d3..8dbb2a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- [#472] Added the `DependsOnContext` attribute, for declaring dependencies between extension contexts. ### Fixed diff --git a/Editor/API/Attributes/DependsOnContext.cs b/Editor/API/Attributes/DependsOnContext.cs new file mode 100644 index 0000000..c56dfe1 --- /dev/null +++ b/Editor/API/Attributes/DependsOnContext.cs @@ -0,0 +1,20 @@ +using System; + +namespace nadena.dev.ndmf +{ + /// + /// This attribute declares a pass or an extension context to depend on another context. + /// When an extension context depends on another, it will implicitly activate the other context whenever the + /// depending context is activated. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public sealed class DependsOnContext : Attribute + { + public Type ExtensionContext { get; } + + public DependsOnContext(Type extensionContext) + { + ExtensionContext = extensionContext; + } + } +} \ No newline at end of file diff --git a/Editor/API/Attributes/DependsOnContext.cs.meta b/Editor/API/Attributes/DependsOnContext.cs.meta new file mode 100644 index 0000000..005c31e --- /dev/null +++ b/Editor/API/Attributes/DependsOnContext.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b6c419f7a20f42a8b60c4101aaba9ad1 +timeCreated: 1731887348 \ No newline at end of file diff --git a/Editor/API/IExtensionContext.cs b/Editor/API/IExtensionContext.cs index 23d67ec..1747ce9 100644 --- a/Editor/API/IExtensionContext.cs +++ b/Editor/API/IExtensionContext.cs @@ -1,4 +1,7 @@ -namespace nadena.dev.ndmf +using System; +using System.Collections.Generic; + +namespace nadena.dev.ndmf { /// /// The IExtensionContext is declared by custom extension contexts. @@ -17,4 +20,52 @@ public interface IExtensionContext /// void OnDeactivate(BuildContext context); } + + internal static class ExtensionContextUtil + { + public static IEnumerable ContextDependencies(this Type ty, bool recurse) + { + if (recurse) + { + return RecursiveContextDependencies(ty); + } + + return ContextDependencies(ty); + } + + public static IEnumerable ContextDependencies(this Type ty) + { + foreach (var attr in ty.GetCustomAttributes(typeof(DependsOnContext), true)) + { + if (attr is DependsOnContext dependsOn && dependsOn.ExtensionContext != null) + { + yield return dependsOn.ExtensionContext; + } + } + } + + private static IEnumerable RecursiveContextDependencies(Type ty) + { + HashSet enqueued = new(); + Queue queue = new(); + + queue.Enqueue(ty); + enqueued.Add(ty); + + while (queue.Count > 0) + { + var current = queue.Dequeue(); + + yield return current; + + foreach (var dep in ContextDependencies(current)) + { + if (enqueued.Add(dep)) + { + queue.Enqueue(dep); + } + } + } + } + } } \ No newline at end of file diff --git a/Editor/API/Model/SolverPass.cs b/Editor/API/Model/SolverPass.cs index 1aab2f4..14b6015 100644 --- a/Editor/API/Model/SolverPass.cs +++ b/Editor/API/Model/SolverPass.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; using nadena.dev.ndmf.preview; #endregion @@ -26,9 +27,25 @@ internal class SolverPass internal IImmutableSet CompatibleExtensions { get; set; } internal List RenderFilters { get; } = new(); - internal bool IsExtensionCompatible(Type ty) + internal bool IsExtensionCompatible(Type ty, ISet activeExtensions) { - return IsPhantom || RequiredExtensions.Contains(ty) || CompatibleExtensions.Contains(ty.FullName); + if (IsPhantom || RequiredExtensions.Contains(ty) || CompatibleExtensions.Contains(ty.FullName)) + { + return true; + } + + // See if any of the active extensions depends on the given type, and if so, if we are compatible with it. + foreach (var active in activeExtensions) + { + if (!CompatibleExtensions.Contains(active.FullName) && !RequiredExtensions.Contains(active)) + { + continue; + } + + if (active.ContextDependencies(true).Contains(ty)) return true; + } + + return false; } internal SolverPass(IPluginInternal plugin, IPass pass, BuildPhase phase, IImmutableSet compatibleExtensions, @@ -37,8 +54,8 @@ internal SolverPass(IPluginInternal plugin, IPass pass, BuildPhase phase, IImmut Plugin = plugin; Pass = pass; Phase = phase; - RequiredExtensions = requiredExtensions; CompatibleExtensions = compatibleExtensions; + RequiredExtensions = requiredExtensions.Union(pass.GetType().ContextDependencies()); } public override string ToString() diff --git a/Editor/API/Solver/PluginResolver.cs b/Editor/API/Solver/PluginResolver.cs index 85f82db..3c8a56d 100644 --- a/Editor/API/Solver/PluginResolver.cs +++ b/Editor/API/Solver/PluginResolver.cs @@ -182,7 +182,7 @@ ImmutableList ToConcretePasses(BuildPhase phase, IEnumerable(); activeExtensions.RemoveWhere(t => { - if (!pass.IsExtensionCompatible(t)) + if (!pass.IsExtensionCompatible(t, activeExtensions)) { toDeactivate.Add(t); return true; @@ -191,7 +191,7 @@ ImmutableList ToConcretePasses(BuildPhase phase, IEnumerable ToConcretePasses(BuildPhase phase, IEnumerable ResolveExtensionDependencies(IImmutableSet passRequiredExtensions) + { + var resultSet = new HashSet(); + var results = new List(); + var stack = new Stack(); + + foreach (var type in new SortedSet(passRequiredExtensions, new TypeComparer())) + { + VisitType(type); + } + + return results; + + void VisitType(Type ty) + { + if (stack.Contains(ty)) + { + throw new Exception("Circular dependency detected: " + string.Join(" -> ", stack)); + } + + if (resultSet.Contains(ty)) return; + + stack.Push(ty); + + foreach (var dep in new SortedSet(ty.ContextDependencies(), new TypeComparer())) + { + VisitType(dep); + } + + stack.Pop(); + + resultSet.Add(ty); + results.Add(ty); + } + } + internal PreviewSession PreviewSession { get diff --git a/UnitTests~/PluginResolverTests/ExtensionDependenciesTest.cs b/UnitTests~/PluginResolverTests/ExtensionDependenciesTest.cs new file mode 100644 index 0000000..14b877d --- /dev/null +++ b/UnitTests~/PluginResolverTests/ExtensionDependenciesTest.cs @@ -0,0 +1,88 @@ +using System.Linq; +using nadena.dev.ndmf; +using NUnit.Framework; + +namespace UnitTests.PluginResolverTests +{ + [DependsOnContext(typeof(Ctx3))] + [DependsOnContext(typeof(Ctx2))] + public class Ctx1 : IExtensionContext + { + public void OnActivate(BuildContext context) + { + + } + + public void OnDeactivate(BuildContext context) + { + + } + } + + public class Ctx2 : IExtensionContext + { + public void OnActivate(BuildContext context) + { + + } + + public void OnDeactivate(BuildContext context) + { + + } + } + + public class Ctx3 : IExtensionContext + { + public void OnActivate(BuildContext context) + { + + } + + public void OnDeactivate(BuildContext context) + { + + } + } + + [DependsOnContext(typeof(Ctx1))] + public class Pass1 : Pass + { + protected override void Execute(BuildContext context) + { + + } + } + + public class Plugin1 : Plugin + { + protected override void Configure() + { + InPhase(BuildPhase.Generating) + .Run(Pass1.Instance) + .Then.WithCompatibleExtension(typeof(Ctx1), seq => + { + seq.Run("test test", _ => { }); + }); + } + } + + public class ExtensionDependenciesTest + { + [Test] + public void AssertCorrectPassDependencies() + { + var resolver = new PluginResolver(new[] { typeof(Plugin1) }); + + var phase = resolver.Passes.First(p => p.Item1 == BuildPhase.Generating).Item2; + var pass1 = phase.First(pass => pass.InstantiatedPass is Pass1); + + Assert.That(pass1.ActivatePlugins, Is.EquivalentTo(new[] { typeof(Ctx2), typeof(Ctx3), typeof(Ctx1) })); + Assert.That(pass1.DeactivatePlugins, Is.Empty); + + var pass2 = phase.First(pass => pass.InstantiatedPass.DisplayName == "test test"); + Assert.That(pass2.ActivatePlugins.IsEmpty); + Assert.That(pass2.DeactivatePlugins.IsEmpty); + } + } +} \ No newline at end of file diff --git a/UnitTests~/PluginResolverTests/ExtensionDependenciesTest.cs.meta b/UnitTests~/PluginResolverTests/ExtensionDependenciesTest.cs.meta new file mode 100644 index 0000000..68e8dd9 --- /dev/null +++ b/UnitTests~/PluginResolverTests/ExtensionDependenciesTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 49943233b6b64e1087b5d47e2d039d9f +timeCreated: 1731888310 \ No newline at end of file