Skip to content

Commit bceb884

Browse files
authored
feat: add support for declaring ProvidesParametersFor via base classes and interfaces (#198)
Closes: #194
1 parent a2126f5 commit bceb884

12 files changed

+242
-14
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [unreleased]
99

1010
### Added
11+
- Added support for declaring ProvidesParametersFor via base classes and interfaces (#198)
1112

1213
### Fixed
1314
- Apply on play isn't suppressed when Av3mu is present (#200)

Editor/EnhancerDatabase.cs

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
#region
22

33
using System;
4+
using System.Collections.Generic;
45
using System.Collections.Immutable;
6+
using System.Linq;
57
using System.Linq.Expressions;
68
using System.Reflection;
79
using System.Threading.Tasks;
@@ -11,14 +13,23 @@
1113

1214
namespace nadena.dev.ndmf
1315
{
14-
internal static class EnhancerDatabase<T, Interface> where T : Attribute
16+
internal class EnhancerDatabase<T, Interface> where T : Attribute
1517
{
1618
internal delegate Interface Creator(Component c);
1719

1820
private static readonly PropertyInfo forTypeProp = typeof(T).GetProperty("ForType");
19-
private static readonly Task<IImmutableDictionary<Type, Creator>> TaskDatabase = Task.Run(Init);
21+
private static readonly Task<EnhancerDatabase<T, Interface>> TaskDatabase = Task.Run(() => new EnhancerDatabase<T, Interface>());
2022

21-
private static IImmutableDictionary<Type, Creator> Init()
23+
private readonly IImmutableDictionary<Type, Creator> _attributes = FindAttributes();
24+
private Dictionary<Type, Creator> _resolved;
25+
26+
private EnhancerDatabase()
27+
{
28+
_resolved = new Dictionary<Type, EnhancerDatabase<T, Interface>.Creator>();
29+
}
30+
31+
32+
private static IImmutableDictionary<Type, Creator> FindAttributes()
2233
{
2334
var builder = ImmutableDictionary.CreateBuilder<Type, Creator>();
2435
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
@@ -66,26 +77,70 @@ private static void TryConfigureAttribute(ref ImmutableDictionary<Type, Creator>
6677
builder.Add(forType, lambda.Compile());
6778
}
6879

69-
public static IImmutableDictionary<Type, Creator> Mappings
80+
public static bool Query(Component c, out Interface iface)
81+
{
82+
iface = default;
83+
if (!TaskDatabase.Result.DoQuery(c, out var creator)) return false;
84+
85+
iface = creator(c);
86+
87+
return true;
88+
}
89+
90+
private bool DoQuery(Component c, out Creator creator)
7091
{
71-
get
92+
if (_resolved.TryGetValue(c.GetType(), out creator))
7293
{
73-
TaskDatabase.Wait();
74-
return TaskDatabase.Result;
94+
return true;
7595
}
76-
}
7796

78-
public static bool Query(Component c, out Interface iface)
79-
{
80-
if (!Mappings.TryGetValue(c.GetType(), out var creator))
97+
var tmp = WalkTypeTree(c.GetType())
98+
.Where((kvp) => _attributes.ContainsKey(kvp.Item1)).ToList();
99+
100+
// Perform breadth-first search on base classes and interfaces, prioritizing more specific declarations.
101+
using (var it = WalkTypeTree(c.GetType())
102+
.Where((kvp) => _attributes.ContainsKey(kvp.Item1))
103+
.OrderBy((kvp) => kvp.Item2)
104+
.Take(2)
105+
.GetEnumerator())
81106
{
82-
iface = default;
83-
return false;
107+
108+
if (!it.MoveNext()) return false;
109+
var first = it.Current;
110+
if (it.MoveNext() && it.Current.Item2 == first.Item2)
111+
{
112+
Debug.LogError("Multiple candidate " + typeof(T) +
113+
" attributes found for base types and interfaces of " + c.GetType());
114+
return false;
115+
}
116+
117+
creator = _attributes[first.Item1];
118+
_resolved[c.GetType()] = creator; // cache resolved type
84119
}
120+
85121

86-
iface = creator(c);
87122
return true;
88123
}
124+
125+
private IEnumerable<(Type, int)> WalkTypeTree(Type type)
126+
{
127+
int depth = 0;
128+
while (type != null)
129+
{
130+
yield return (type, depth);
131+
132+
foreach (var i in type.GetInterfaces())
133+
{
134+
yield return (i, depth + 1);
135+
}
136+
137+
if (type.BaseType == type) break;
138+
139+
type = type.BaseType;
140+
depth++;
141+
}
142+
}
143+
89144

90145
public static void AsyncInit()
91146
{
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using UnityEngine;
2+
3+
namespace nadena.dev.ndmf.UnitTestSupport
4+
{
5+
interface ITestInterface2
6+
{
7+
}
8+
9+
[AddComponentMenu("")]
10+
internal class PTCConflictComponent : MonoBehaviour, ITestInterface1, ITestInterface2
11+
{
12+
13+
}
14+
}

Runtime/TestSupport/PTCConflictComponent.cs.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace nadena.dev.ndmf.UnitTestSupport
2+
{
3+
internal class PTCDepthResolutionComponent : PTCDepthResolutionComponentBase, ITestInterface2
4+
{
5+
6+
}
7+
}

Runtime/TestSupport/PTCDepthResolutionComponent.cs.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using UnityEngine;
2+
3+
namespace nadena.dev.ndmf.UnitTestSupport
4+
{
5+
internal abstract class PTCDepthResolutionComponentBase : PTCDepthResolutionComponentBase2
6+
{
7+
}
8+
9+
internal abstract class PTCDepthResolutionComponentBase2 : MonoBehaviour
10+
{ }
11+
}

Runtime/TestSupport/PTCDepthResolutionComponentBase.cs.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using UnityEngine;
2+
3+
namespace nadena.dev.ndmf.UnitTestSupport
4+
{
5+
interface ITestInterface1
6+
{
7+
}
8+
9+
[AddComponentMenu("")]
10+
internal class PTCInheritanceComponent : MonoBehaviour, ITestInterface1
11+
{
12+
13+
}
14+
}

Runtime/TestSupport/PTCInheritanceComponent.cs.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Collections.Immutable;
4+
using System.Text.RegularExpressions;
5+
using nadena.dev.ndmf;
6+
using nadena.dev.ndmf.UnitTestSupport;
7+
using NUnit.Framework;
8+
using UnityEngine;
9+
using UnityEngine.TestTools;
10+
11+
#if NDMF_VRCSDK3_AVATARS
12+
13+
namespace UnitTests.Parameters
14+
{
15+
[ParameterProviderFor(typeof(ITestInterface1))]
16+
internal class TestInterface1Provider : IParameterProvider
17+
{
18+
public TestInterface1Provider(ITestInterface1 _)
19+
{
20+
21+
}
22+
23+
public IEnumerable<ProvidedParameter> GetSuppliedParameters(BuildContext context)
24+
{
25+
return Array.Empty<ProvidedParameter>();
26+
}
27+
28+
public void RemapParameters(ref ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> nameMap, BuildContext context)
29+
{
30+
}
31+
}
32+
33+
[ParameterProviderFor(typeof(ITestInterface2))]
34+
internal class TestInterface2Provider : IParameterProvider
35+
{
36+
public TestInterface2Provider(ITestInterface2 _)
37+
{
38+
39+
}
40+
41+
public IEnumerable<ProvidedParameter> GetSuppliedParameters(BuildContext context)
42+
{
43+
return Array.Empty<ProvidedParameter>();
44+
}
45+
46+
public void RemapParameters(ref ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> nameMap, BuildContext context)
47+
{
48+
}
49+
}
50+
51+
[ParameterProviderFor(typeof(PTCDepthResolutionComponentBase2))]
52+
internal class DepthResolutionProvider : IParameterProvider
53+
{
54+
public DepthResolutionProvider(PTCDepthResolutionComponentBase2 _)
55+
{
56+
57+
}
58+
59+
public IEnumerable<ProvidedParameter> GetSuppliedParameters(BuildContext context)
60+
{
61+
return Array.Empty<ProvidedParameter>();
62+
}
63+
64+
public void RemapParameters(ref ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> nameMap, BuildContext context)
65+
{
66+
}
67+
}
68+
69+
public class InheritanceTest : TestBase
70+
{
71+
[Test]
72+
public void ResolvesInterface()
73+
{
74+
var root = CreateRoot("root");
75+
var obj = root.AddComponent<PTCInheritanceComponent>();
76+
77+
Assert.IsTrue(EnhancerDatabase<ParameterProviderFor, IParameterProvider>.Query(
78+
obj, out var provider
79+
));
80+
81+
Assert.IsTrue(provider is TestInterface1Provider);
82+
}
83+
84+
[Test]
85+
public void DoesNotResolveAmbiguous()
86+
{
87+
var root = CreateRoot("root");
88+
var obj = root.AddComponent<PTCConflictComponent>();
89+
90+
Assert.IsFalse(EnhancerDatabase<ParameterProviderFor, IParameterProvider>.Query(
91+
obj, out var provider
92+
));
93+
LogAssert.Expect(LogType.Error, new Regex("Multiple candidate .*ParameterProviderFor attributes"));
94+
}
95+
96+
[Test]
97+
public void ResolvesByDepth()
98+
{
99+
var root = CreateRoot("root");
100+
var obj = root.AddComponent<PTCDepthResolutionComponent>();
101+
102+
Assert.IsTrue(EnhancerDatabase<ParameterProviderFor, IParameterProvider>.Query(
103+
obj, out var provider
104+
));
105+
106+
Assert.IsTrue(provider is TestInterface2Provider);
107+
}
108+
}
109+
}
110+
111+
#endif

UnitTests~/ParameterIntrospection/InheritanceTests.cs.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)