Skip to content

Commit b1bacc7

Browse files
committed
feat(Interop): introduce AssemblyInteropAttribute and enhance mod assembly handling
- Added `AssemblyInteropAttribute` to facilitate public member forwarding to CLR types resolved by assembly-qualified names. - Updated `ModInteropTypeDiscoveryContributor` to process both `ModInteropAttribute` and `AssemblyInteropAttribute`. - Implemented `BuildLoadedModAssembliesByManifestId` method for better management of loaded mod assemblies. - Enhanced interop member generation to support assembly-qualified type resolution, improving mod integration capabilities.
1 parent 2b5d532 commit b1bacc7

6 files changed

Lines changed: 163 additions & 40 deletions

File tree

Compat/Sts2ModManagerCompat.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,30 @@ internal static IEnumerable<Mod> EnumerateLoadedModsWithAssembly()
3838
return ModManager.GetLoadedMods();
3939
}
4040

41+
internal static IReadOnlyDictionary<string, Assembly> BuildLoadedModAssembliesByManifestId()
42+
{
43+
var result = new Dictionary<string, Assembly>(StringComparer.Ordinal);
44+
45+
foreach (var mod in EnumerateLoadedModsWithAssembly())
46+
try
47+
{
48+
var manifest = ReadManifest(mod);
49+
var modId = manifest == null ? null : ReadManifestId(manifest);
50+
var assembly = ReadAssembly(mod);
51+
if (string.IsNullOrWhiteSpace(modId) || assembly == null)
52+
continue;
53+
54+
result[modId] = assembly;
55+
}
56+
catch (Exception ex)
57+
{
58+
RitsuLibFramework.Logger.Warn(
59+
$"[Compat] Failed to inspect a loaded mod assembly for discovery interop: {ex.Message}");
60+
}
61+
62+
return result;
63+
}
64+
4165
/// <summary>
4266
/// All registered mods (including disabled / not loaded), for manifest name/description lookup.
4367
/// 所有已注册 mod(包括禁用/未加载的 mod),用于清单名称/描述查找。

Interop/Internal/ModInteropEmitter.cs

Lines changed: 80 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,39 @@ internal static void TryProcessType(
2323
Type t)
2424
{
2525
var modInterop = t.GetCustomAttribute<ModInteropAttribute>();
26-
if (modInterop is null)
26+
var assemblyInterop = t.GetCustomAttribute<AssemblyInteropAttribute>();
27+
if (modInterop != null && assemblyInterop != null)
28+
{
29+
RitsuLibFramework.Logger.Warn(
30+
$"[Interop] Type {t.FullName} declares both ModInterop and AssemblyInterop; skipping.");
2731
return;
32+
}
33+
34+
if (modInterop != null)
35+
{
36+
if (!loadedAssembliesByModId.TryGetValue(modInterop.ModId, out var assembly))
37+
return;
2838

29-
if (!loadedAssembliesByModId.TryGetValue(modInterop.ModId, out var assembly))
39+
RitsuLibFramework.Logger.Info($"[ModInterop] Processing type {t.FullName} -> mod {modInterop.ModId}");
40+
41+
var members = t.GetMembers(ValidMemberFlags);
42+
GenInteropMembers(members, harmony, TargetResolutionContext.ForModAssembly(assembly),
43+
modInterop.Type, true);
3044
return;
45+
}
3146

32-
RitsuLibFramework.Logger.Info($"[ModInterop] Processing type {t.FullName} -> mod {modInterop.ModId}");
47+
if (assemblyInterop == null)
48+
return;
3349

34-
var members = t.GetMembers(ValidMemberFlags);
35-
GenInteropMembers(members, harmony, assembly, modInterop.Type, true);
50+
RitsuLibFramework.Logger.Info($"[AssemblyInterop] Processing type {t.FullName}");
51+
GenInteropMembers(t.GetMembers(ValidMemberFlags), harmony,
52+
TargetResolutionContext.ForAssemblyQualifiedTypes(), assemblyInterop.Type, true);
3653
}
3754

3855
private static bool GenInteropMembers(
3956
MemberInfo[] members,
4057
Harmony harmony,
41-
Assembly assembly,
58+
TargetResolutionContext targetContext,
4259
string? contextTargetType,
4360
bool requireStatic)
4461
{
@@ -48,21 +65,21 @@ private static bool GenInteropMembers(
4865
case PropertyInfo property:
4966
if (requireStatic && !(property.SetMethod?.IsStatic ?? true))
5067
continue;
51-
if (!GenInteropPropertyOrField(harmony, assembly, contextTargetType, property))
68+
if (!GenInteropPropertyOrField(harmony, targetContext, contextTargetType, property))
5269
return false;
5370
break;
5471
case MethodInfo method:
5572
if (requireStatic && !method.IsStatic)
5673
continue;
5774
if (method.IsConstructor || method.GetCustomAttribute<CompilerGeneratedAttribute>() != null)
5875
continue;
59-
if (!GenInteropMethod(harmony, assembly, contextTargetType, method))
76+
if (!GenInteropMethod(harmony, targetContext, contextTargetType, method))
6077
return false;
6178
break;
6279
case TypeInfo nested:
6380
if (!nested.IsAssignableTo(typeof(InteropClassWrapper)))
6481
continue;
65-
if (!GenInteropType(harmony, assembly, contextTargetType, nested))
82+
if (!GenInteropType(harmony, targetContext, contextTargetType, nested))
6683
return false;
6784
break;
6885
}
@@ -72,7 +89,7 @@ private static bool GenInteropMembers(
7289

7390
private static bool GenInteropType(
7491
Harmony harmony,
75-
Assembly targetAssembly,
92+
TargetResolutionContext targetContext,
7693
string? contextTargetType,
7794
TypeInfo type)
7895
{
@@ -86,9 +103,7 @@ private static bool GenInteropType(
86103

87104
try
88105
{
89-
var targetType = Type.GetType($"{targetName}, {targetAssembly}")
90-
?? throw new InvalidOperationException(
91-
$"Type {targetName} not found in assembly {targetAssembly}");
106+
var targetType = ResolveTargetType(targetName, targetContext);
92107

93108
foreach (var constructor in constructors)
94109
{
@@ -111,7 +126,7 @@ private static bool GenInteropType(
111126
}
112127

113128
RitsuLibFramework.Logger.Info($"[ModInterop] Generated interop type {type.FullName}");
114-
return GenInteropMembers(type.GetMembers(ValidMemberFlags), harmony, targetAssembly, targetName, false);
129+
return GenInteropMembers(type.GetMembers(ValidMemberFlags), harmony, targetContext, targetName, false);
115130
}
116131
catch (Exception e)
117132
{
@@ -122,7 +137,7 @@ private static bool GenInteropType(
122137

123138
private static bool GenInteropMethod(
124139
Harmony harmony,
125-
Assembly targetAssembly,
140+
TargetResolutionContext targetContext,
126141
string? contextTargetType,
127142
MethodInfo method)
128143
{
@@ -134,9 +149,7 @@ private static bool GenInteropMethod(
134149

135150
try
136151
{
137-
var targetType = Type.GetType($"{typeName}, {targetAssembly}")
138-
?? throw new InvalidOperationException(
139-
$"Type {typeName} not found in assembly {targetAssembly}");
152+
var targetType = ResolveTargetType(typeName, targetContext);
140153

141154
var methodParams = method.GetParameters().Select(p => p.ParameterType).ToArray();
142155
var nonStaticParams = method.IsStatic ? methodParams.Skip(1).ToArray() : methodParams;
@@ -210,7 +223,7 @@ private static bool GenInteropMethod(
210223

211224
private static bool GenInteropPropertyOrField(
212225
Harmony harmony,
213-
Assembly targetAssembly,
226+
TargetResolutionContext targetContext,
214227
string? contextTargetType,
215228
PropertyInfo property)
216229
{
@@ -221,9 +234,7 @@ private static bool GenInteropPropertyOrField(
221234

222235
try
223236
{
224-
var targetType = Type.GetType($"{typeName}, {targetAssembly}")
225-
?? throw new InvalidOperationException(
226-
$"Type {typeName} not found in assembly {targetAssembly}");
237+
var targetType = ResolveTargetType(typeName, targetContext);
227238

228239
var targetProperty = AccessTools.DeclaredProperty(targetType, name);
229240
if (targetProperty is not null && targetProperty.PropertyType == property.PropertyType)
@@ -357,6 +368,40 @@ private static bool GenInteropPropertyOrField(
357368
}
358369
}
359370

371+
private static Type ResolveTargetType(string targetName, TargetResolutionContext targetContext)
372+
{
373+
if (targetContext.AssemblyQualifiedOnly)
374+
{
375+
_ = TryResolveAssemblyQualifiedType(targetName, out var resolved);
376+
return resolved ?? throw new InvalidOperationException(
377+
$"AssemblyInterop target type '{targetName}' must be an assembly-qualified CLR type name that can be resolved.");
378+
}
379+
380+
if (IsAssemblyQualifiedTypeName(targetName))
381+
throw new InvalidOperationException(
382+
$"ModInterop target type '{targetName}' must be a full type name inside the target mod assembly. Use AssemblyInterop for assembly-qualified CLR type names.");
383+
384+
return targetContext.ModAssembly!.GetType(targetName, false)
385+
?? Type.GetType($"{targetName}, {targetContext.ModAssembly.FullName}", false)
386+
?? throw new InvalidOperationException(
387+
$"Type {targetName} not found in assembly {targetContext.ModAssembly.FullName}");
388+
}
389+
390+
private static bool IsAssemblyQualifiedTypeName(string? targetName)
391+
{
392+
return !string.IsNullOrWhiteSpace(targetName) && targetName.Contains(',', StringComparison.Ordinal);
393+
}
394+
395+
private static bool TryResolveAssemblyQualifiedType(string? targetName, out Type? type)
396+
{
397+
type = null;
398+
if (!IsAssemblyQualifiedTypeName(targetName))
399+
return false;
400+
401+
type = Type.GetType(targetName!, false);
402+
return type != null;
403+
}
404+
360405
private static bool CheckParamMatch(ParameterInfo[] targetParams, Type[] checkParams)
361406
{
362407
if (targetParams.Length != checkParams.Length)
@@ -375,5 +420,18 @@ private static string FormatConstructor(ConstructorInfo c)
375420
return
376421
$"{c.DeclaringType?.FullName}.ctor({string.Join(", ", c.GetParameters().Select(p => p.ParameterType.Name))})";
377422
}
423+
424+
private readonly record struct TargetResolutionContext(Assembly? ModAssembly, bool AssemblyQualifiedOnly)
425+
{
426+
public static TargetResolutionContext ForModAssembly(Assembly assembly)
427+
{
428+
return new(assembly, false);
429+
}
430+
431+
public static TargetResolutionContext ForAssemblyQualifiedTypes()
432+
{
433+
return new(null, true);
434+
}
435+
}
378436
}
379437
}

Interop/InteropClassWrapper.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ namespace STS2RitsuLib.Interop
22
{
33
/// <summary>
44
/// Base type for interop types whose instance methods forward to a wrapped runtime object
5-
/// (see <see cref="ModInteropAttribute" />).
5+
/// (see <see cref="ModInteropAttribute" /> and <see cref="AssemblyInteropAttribute" />).
66
/// interop 类型的基类;这些类型的实例方法会转发到包装的运行时对象
7-
/// (参见 <see cref="ModInteropAttribute" />)。
7+
/// (参见 <see cref="ModInteropAttribute" /> 和 <see cref="AssemblyInteropAttribute" />)。
88
/// </summary>
99
public abstract class InteropClassWrapper
1010
{

Interop/ModInteropAttributes.cs

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,32 @@ public sealed class ModInteropAttribute(string modId, string? type = null) : Att
3131
}
3232

3333
/// <summary>
34-
/// Optional per-member override for the target type or member name in the remote mod.
35-
/// 针对远端 mod 中目标类型或成员名的可选逐成员覆盖。
34+
/// Marks a class whose public members forward to a CLR type resolved by an assembly-qualified type name
35+
/// such as <c>Namespace.Type, AssemblyName</c>.
36+
/// 标记一个类:其 public 成员会转发到用 assembly-qualified type name
37+
/// (如 <c>Namespace.Type, AssemblyName</c>)解析的 CLR 类型。
38+
/// </summary>
39+
/// <param name="type">
40+
/// Default assembly-qualified CLR type name for members without <see cref="InteropTargetAttribute" />.
41+
/// 没有 <see cref="InteropTargetAttribute" /> 的成员所使用的默认 assembly-qualified CLR 类型名。
42+
/// </param>
43+
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
44+
public sealed class AssemblyInteropAttribute(string? type = null) : Attribute
45+
{
46+
/// <summary>
47+
/// Default assembly-qualified CLR type name for members without <see cref="InteropTargetAttribute" />.
48+
/// 没有 <see cref="InteropTargetAttribute" /> 的成员所使用的默认 assembly-qualified CLR 类型名。
49+
/// </summary>
50+
public string? Type { get; } = type;
51+
}
52+
53+
/// <summary>
54+
/// Optional per-member override for the target type or member name.
55+
/// With <see cref="ModInteropAttribute" />, type is resolved inside the target mod assembly.
56+
/// With <see cref="AssemblyInteropAttribute" />, type must be an assembly-qualified CLR type name.
57+
/// 针对目标类型或成员名的可选逐成员覆盖。
58+
/// 配合 <see cref="ModInteropAttribute" /> 时,type 在目标 mod assembly 内解析。
59+
/// 配合 <see cref="AssemblyInteropAttribute" /> 时,type 必须是 assembly-qualified CLR type name。
3660
/// </summary>
3761
[AttributeUsage(
3862
AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Class | AttributeTargets.Method,
@@ -44,8 +68,10 @@ public sealed class InteropTargetAttribute : Attribute
4468
/// 覆盖远端类型,并可选覆盖成员名。
4569
/// </summary>
4670
/// <param name="type">
47-
/// Fully qualified or assembly-qualified type name in the remote mod.
48-
/// 远端 mod 中的完全限定或 assembly-qualified 类型名。
71+
/// Target type name. Use a full type name for <see cref="ModInteropAttribute" />, or an assembly-qualified
72+
/// type name for <see cref="AssemblyInteropAttribute" />.
73+
/// 目标类型名。配合 <see cref="ModInteropAttribute" /> 使用完整类型名;配合
74+
/// <see cref="AssemblyInteropAttribute" /> 使用 assembly-qualified 类型名。
4975
/// </param>
5076
/// <param name="name">
5177
/// Remote member name when different from the stub.
@@ -58,10 +84,8 @@ public InteropTargetAttribute(string type, string? name = null)
5884
}
5985

6086
/// <summary>
61-
/// Overrides only the remote member name (type comes from <see cref="ModInteropAttribute.Type" /> or enclosing
62-
/// context).
63-
/// 仅覆盖远端成员名(类型来自 <see cref="ModInteropAttribute.Type" /> 或外层
64-
/// 上下文)。
87+
/// Overrides only the remote member name (type comes from the enclosing interop attribute or context).
88+
/// 仅覆盖远端成员名(类型来自外层 interop attribute 或上下文)。
6589
/// </summary>
6690
/// <param name="name">
6791
/// Remote member name when different from the stub.
@@ -73,8 +97,8 @@ public InteropTargetAttribute(string? name = null)
7397
}
7498

7599
/// <summary>
76-
/// Remote type name when specified; otherwise inferred from <see cref="ModInteropAttribute" />.
77-
/// 显式指定时的远端类型名;否则从 <see cref="ModInteropAttribute" /> 推断。
100+
/// Target type name when specified; otherwise inferred from the enclosing interop attribute.
101+
/// 显式指定时的目标类型名;否则从外层 interop attribute 推断。
78102
/// </summary>
79103
public string? Type { get; }
80104

Interop/ModInteropTypeDiscoveryContributor.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
namespace STS2RitsuLib.Interop
66
{
77
/// <summary>
8-
/// Built-in contributor: processes <see cref="ModInteropAttribute" /> stubs.
9-
/// 内置 contributor:处理 <see cref="ModInteropAttribute" /> stub。
8+
/// Built-in contributor: processes <see cref="ModInteropAttribute" /> and
9+
/// <see cref="AssemblyInteropAttribute" /> stubs.
10+
/// 内置 contributor:处理 <see cref="ModInteropAttribute" /> 和
11+
/// <see cref="AssemblyInteropAttribute" /> stub。
1012
/// </summary>
1113
public sealed class ModInteropTypeDiscoveryContributor : IModTypeDiscoveryContributor
1214
{

Interop/ModTypeDiscoveryHub.cs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Reflection;
22
using HarmonyLib;
3+
using STS2RitsuLib.Compat;
34
using STS2RitsuLib.Interop.AutoRegistration;
45
using STS2RitsuLib.Interop.Patches;
56

@@ -67,15 +68,16 @@ internal static void EnsureBuiltInContributorsRegistered()
6768

6869
internal static void RunOnce(Harmony harmony)
6970
{
70-
Dictionary<string, Assembly> map;
71+
Dictionary<string, Assembly> scanMap;
7172
IModTypeDiscoveryContributor[] snapshot;
7273
lock (Gate)
7374
{
74-
map = new(RegisteredAssembliesByModId, StringComparer.Ordinal);
75+
scanMap = new(RegisteredAssembliesByModId, StringComparer.Ordinal);
7576
snapshot = Contributors.ToArray();
7677
}
7778

78-
var orderedAssemblies = map
79+
var targetMap = BuildTargetAssemblyMap(scanMap);
80+
var orderedAssemblies = scanMap
7981
.OrderBy(static kv => kv.Key, StringComparer.Ordinal)
8082
.Select(static kv => kv.Value)
8183
.Distinct()
@@ -89,8 +91,21 @@ internal static void RunOnce(Harmony harmony)
8991

9092
foreach (var modType in modTypes)
9193
foreach (var contributor in snapshot)
92-
contributor.Contribute(harmony, map, modType);
94+
contributor.Contribute(harmony, targetMap, modType);
9395
}
9496
}
97+
98+
private static IReadOnlyDictionary<string, Assembly> BuildTargetAssemblyMap(
99+
IReadOnlyDictionary<string, Assembly> registeredAssembliesByModId)
100+
{
101+
var result = new Dictionary<string, Assembly>(
102+
Sts2ModManagerCompat.BuildLoadedModAssembliesByManifestId(),
103+
StringComparer.Ordinal);
104+
105+
foreach (var (modId, assembly) in registeredAssembliesByModId)
106+
result[modId] = assembly;
107+
108+
return result;
109+
}
95110
}
96111
}

0 commit comments

Comments
 (0)