diff --git a/S1API.Tests/Internal/Utils/ReflectionUtilsTests.cs b/S1API.Tests/Internal/Utils/ReflectionUtilsTests.cs
index 3dfc4a81..0d8afed6 100644
--- a/S1API.Tests/Internal/Utils/ReflectionUtilsTests.cs
+++ b/S1API.Tests/Internal/Utils/ReflectionUtilsTests.cs
@@ -121,6 +121,57 @@ public void DerivedTypeScanDoesNotFollowSameNameAssembliesWithDifferentIdentitie
AppDomain.CurrentDomain.GetAssemblies()));
}
+ [Theory]
+ [InlineData("S1API, Version=3.0.1.0, Culture=neutral, PublicKeyToken=null")]
+ [InlineData("S1API, Version=2.9.2.0, Culture=neutral, PublicKeyToken=null")]
+ [InlineData("S1API, Version=3.2.1.0, Culture=neutral, PublicKeyToken=null")]
+ [InlineData("s1api, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null")]
+ public void AReferenceToAnyVersionOfTheBaseAssemblyBindsToIt(string reference)
+ {
+ var loaded = new AssemblyName("S1API, Version=3.2.1.0, Culture=neutral, PublicKeyToken=null");
+
+ Assert.True(ReflectionUtils.ReferenceBindsToDefinition(new AssemblyName(reference), loaded));
+ }
+
+ [Fact]
+ public void AReferenceToADifferentAssemblyNameDoesNotBind()
+ {
+ var loaded = new AssemblyName("S1API, Version=3.2.1.0, Culture=neutral, PublicKeyToken=null");
+
+ Assert.False(ReflectionUtils.ReferenceBindsToDefinition(
+ new AssemblyName("S1APILoader, Version=3.2.1.0, Culture=neutral, PublicKeyToken=null"),
+ loaded));
+ }
+
+ [Fact]
+ public void AReferenceWithADifferentPublicKeyTokenDoesNotBind()
+ {
+ var signed = new AssemblyName("Example, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089");
+ var unsigned = new AssemblyName("Example, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null");
+
+ Assert.False(ReflectionUtils.ReferenceBindsToDefinition(signed, unsigned));
+ Assert.False(ReflectionUtils.ReferenceBindsToDefinition(unsigned, signed));
+ Assert.True(ReflectionUtils.ReferenceBindsToDefinition(signed, signed));
+ }
+
+ [Fact]
+ public void ALoadedAssemblyOfADifferentVersionIsFollowedOnlyWhenItIsTheOnlyOneOfThatName()
+ {
+ var reference = new AssemblyName("Library, Version=1.5.0.0, Culture=neutral, PublicKeyToken=null");
+ var newer = new AssemblyName("Library, Version=1.6.0.0, Culture=neutral, PublicKeyToken=null");
+
+ Assert.True(ReflectionUtils.ShouldFollowLoadedReference(newer, reference, loadedAssembliesWithThatName: 1));
+ Assert.False(ReflectionUtils.ShouldFollowLoadedReference(newer, reference, loadedAssembliesWithThatName: 2));
+ }
+
+ [Fact]
+ public void ALoadedAssemblyWithTheExactIdentityIsAlwaysFollowed()
+ {
+ var reference = new AssemblyName("Library, Version=1.5.0.0, Culture=neutral, PublicKeyToken=null");
+
+ Assert.True(ReflectionUtils.ShouldFollowLoadedReference(reference, reference, loadedAssembliesWithThatName: 2));
+ }
+
private static AssemblyBuilder CreateDynamicAssembly(string name, Version version)
{
var assemblyName = new AssemblyName(name)
diff --git a/S1API/Internal/Utils/ReflectionUtils.cs b/S1API/Internal/Utils/ReflectionUtils.cs
index 853db58b..a2ec62db 100644
--- a/S1API/Internal/Utils/ReflectionUtils.cs
+++ b/S1API/Internal/Utils/ReflectionUtils.cs
@@ -137,7 +137,7 @@ private static bool ReferencesAssemblyTransitively(
foreach (AssemblyName referencedAssembly in referencedAssemblies)
{
- if (AssemblyIdentityMatches(referencedAssembly, baseAssemblyName))
+ if (ReferenceBindsToDefinition(referencedAssembly, baseAssemblyName))
return true;
string referencedName = referencedAssembly.Name ?? string.Empty;
@@ -147,7 +147,10 @@ private static bool ReferencesAssemblyTransitively(
foreach (Assembly loadedReference in loadedReferences)
{
- if (!AssemblyIdentityMatches(loadedReference.GetName(), referencedAssembly))
+ if (!ShouldFollowLoadedReference(
+ loadedReference.GetName(),
+ referencedAssembly,
+ loadedReferences.Length))
continue;
if (ReferencesAssemblyTransitively(
@@ -164,6 +167,45 @@ private static bool ReferencesAssemblyTransitively(
return false;
}
+ ///
+ /// INTERNAL: Does this reference bind to that assembly? The simple name and public key token must match; the
+ /// version need not, because the runtime binds a reference to whichever assembly of that name is loaded. A mod
+ /// built against an older S1API still runs against the loaded one, so it must still be scanned for the types
+ /// that derive from it.
+ ///
+ internal static bool ReferenceBindsToDefinition(
+ AssemblyName referenceAssemblyName,
+ AssemblyName definitionAssemblyName)
+ {
+ if (!string.Equals(
+ referenceAssemblyName.Name,
+ definitionAssemblyName.Name,
+ StringComparison.OrdinalIgnoreCase))
+ return false;
+
+ byte[] referenceToken = referenceAssemblyName.GetPublicKeyToken() ?? Array.Empty();
+ byte[] definitionToken = definitionAssemblyName.GetPublicKeyToken() ?? Array.Empty();
+ return referenceToken.SequenceEqual(definitionToken);
+ }
+
+ ///
+ /// INTERNAL: Should the scan follow a reference into this loaded assembly? An exact identity always; a different
+ /// version only when it is the one assembly of that name loaded, since then the reference can only bind to it.
+ /// With several same-named assemblies loaded it is unknown which one the reference means, so only an exact
+ /// match is followed.
+ ///
+ internal static bool ShouldFollowLoadedReference(
+ AssemblyName loadedAssemblyName,
+ AssemblyName referenceAssemblyName,
+ int loadedAssembliesWithThatName)
+ {
+ if (AssemblyIdentityMatches(loadedAssemblyName, referenceAssemblyName))
+ return true;
+
+ return loadedAssembliesWithThatName == 1
+ && ReferenceBindsToDefinition(referenceAssemblyName, loadedAssemblyName);
+ }
+
private static bool AssemblyIdentityMatches(
AssemblyName referenceAssemblyName,
AssemblyName definitionAssemblyName) =>