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