1+ using System . Reflection ;
2+ using MegaCrit . Sts2 . Core . Localization ;
13using MegaCrit . Sts2 . Core . Modding ;
24
35namespace STS2RitsuLib . Compat
@@ -8,6 +10,29 @@ namespace STS2RitsuLib.Compat
810 /// </summary>
911 internal static class Sts2ModManagerCompat
1012 {
13+ private const BindingFlags InstanceMemberFlags =
14+ BindingFlags . Instance |
15+ BindingFlags . Public |
16+ BindingFlags . NonPublic ;
17+
18+ private static readonly Func < Mod , ModManifest ? > ReadManifest = CreateModManifestAccessor ( ) ;
19+ private static readonly Func < Mod , Assembly ? > ReadAssembly = CreateModAssemblyAccessor ( ) ;
20+ private static readonly Func < Mod , IReadOnlyList < LocString > > ReadErrors = CreateModErrorsAccessor ( ) ;
21+ private static readonly Func < Mod , string > ReadSource = CreateModSourceAccessor ( ) ;
22+ private static readonly Func < Mod , int , string > ReadLoadState = CreateLoadStateAccessor ( ) ;
23+
24+ private static readonly Func < ModManifest , string ? > ReadManifestId =
25+ CreateManifestStringAccessor ( "id" , static manifest => manifest . id ) ;
26+
27+ private static readonly Func < ModManifest , string ? > ReadManifestName =
28+ CreateManifestStringAccessor ( "name" , static manifest => manifest . name ) ;
29+
30+ private static readonly Func < ModManifest , string ? > ReadManifestVersion =
31+ CreateManifestStringAccessor ( "version" , static manifest => manifest . version ) ;
32+
33+ private static readonly Func < ModManifest , bool > ReadManifestAffectsGameplay =
34+ CreateManifestBoolAccessor ( "affectsGameplay" , static manifest => manifest . affectsGameplay , true ) ;
35+
1136 internal static IEnumerable < Mod > EnumerateLoadedModsWithAssembly ( )
1237 {
1338 return ModManager . GetLoadedMods ( ) ;
@@ -21,5 +46,206 @@ internal static IEnumerable<Mod> EnumerateModsForManifestLookup()
2146 {
2247 return ModManager . Mods ;
2348 }
49+
50+ internal static IReadOnlyList < Sts2ModInventoryEntry > BuildModInventoryEntries ( )
51+ {
52+ return EnumerateModsForManifestLookup ( )
53+ . Select ( TryBuildModInventoryEntry )
54+ . Where ( entry => entry != null )
55+ . Select ( entry => entry ! )
56+ . OrderBy ( entry => entry . Id , StringComparer . OrdinalIgnoreCase )
57+ . ThenBy ( entry => entry . AssemblyName ?? "" , StringComparer . OrdinalIgnoreCase )
58+ . ToArray ( ) ;
59+ }
60+
61+ private static Sts2ModInventoryEntry ? TryBuildModInventoryEntry ( Mod mod )
62+ {
63+ try
64+ {
65+ var manifest = ReadManifest ( mod ) ;
66+ var assembly = ReadAssembly ( mod ) ;
67+ var assemblyName = ResolveAssemblyName ( assembly ) ;
68+ var errors = ReadErrors ( mod ) ;
69+ var fallbackName = assemblyName ? . Name ?? "<unknown>" ;
70+ return new (
71+ manifest == null ? fallbackName : ReadManifestId ( manifest ) ?? fallbackName ,
72+ manifest == null ? fallbackName : ReadManifestName ( manifest ) ?? fallbackName ,
73+ manifest == null ? null : ReadManifestVersion ( manifest ) ,
74+ ReadLoadState ( mod , errors . Count ) ,
75+ ReadSource ( mod ) ,
76+ manifest == null || ReadManifestAffectsGameplay ( manifest ) ,
77+ assemblyName ? . Name ,
78+ assemblyName ? . Version ? . ToString ( ) ,
79+ errors ) ;
80+ }
81+ catch ( Exception ex )
82+ {
83+ RitsuLibFramework . Logger . Warn (
84+ $ "[Compat] Failed to describe a registered mod for inventory telemetry: { ex . Message } ") ;
85+ return null ;
86+ }
87+ }
88+
89+ private static Func < Mod , ModManifest ? > CreateModManifestAccessor ( )
90+ {
91+ if ( typeof ( Mod ) . GetField ( "manifest" , InstanceMemberFlags ) != null )
92+ return static mod => mod . manifest ;
93+
94+ var getter = CreateUntypedMemberGetter ( typeof ( Mod ) , "manifest" ) ;
95+ return mod => getter ? . Invoke ( mod ) as ModManifest ;
96+ }
97+
98+ private static Func < Mod , Assembly ? > CreateModAssemblyAccessor ( )
99+ {
100+ if ( typeof ( Mod ) . GetField ( "assembly" , InstanceMemberFlags ) != null )
101+ return static mod => mod . assembly ;
102+
103+ var getter = CreateUntypedMemberGetter ( typeof ( Mod ) , "assembly" ) ;
104+ return mod => getter ? . Invoke ( mod ) as Assembly ;
105+ }
106+
107+ private static Func < Mod , IReadOnlyList < LocString > > CreateModErrorsAccessor ( )
108+ {
109+ if ( typeof ( Mod ) . GetField ( "errors" , InstanceMemberFlags ) != null )
110+ return static mod => NormalizeErrors ( mod . errors ) ;
111+
112+ var getter = CreateUntypedMemberGetter ( typeof ( Mod ) , "errors" ) ;
113+ return mod => NormalizeErrors ( getter ? . Invoke ( mod ) as IEnumerable < LocString > ) ;
114+ }
115+
116+ private static Func < Mod , string > CreateModSourceAccessor ( )
117+ {
118+ if ( typeof ( Mod ) . GetField ( "modSource" , InstanceMemberFlags ) != null )
119+ return static mod => mod . modSource . ToString ( ) ;
120+
121+ var getter = CreateUntypedMemberGetter ( typeof ( Mod ) , "modSource" ) ;
122+ return mod => getter ? . Invoke ( mod ) ? . ToString ( ) ?? "None" ;
123+ }
124+
125+ private static Func < Mod , int , string > CreateLoadStateAccessor ( )
126+ {
127+ if ( typeof ( Mod ) . GetField ( "state" , InstanceMemberFlags ) != null )
128+ return static ( mod , _ ) => mod . state . ToString ( ) ;
129+
130+ var stateGetter = CreateUntypedMemberGetter ( typeof ( Mod ) , "state" ) ;
131+ var wasLoadedGetter = CreateUntypedMemberGetter ( typeof ( Mod ) , "wasLoaded" ) ;
132+ var assemblyLoadedSuccessfullyGetter =
133+ CreateUntypedMemberGetter ( typeof ( Mod ) , "assemblyLoadedSuccessfully" ) ;
134+ return ( mod , errorCount ) =>
135+ {
136+ if ( stateGetter ? . Invoke ( mod ) is { } stateValue )
137+ return stateValue . ToString ( ) ?? "None" ;
138+
139+ if ( ReadBool ( wasLoadedGetter , mod ) == true )
140+ return "Loaded" ;
141+
142+ if ( ReadBool ( assemblyLoadedSuccessfullyGetter , mod ) == false || errorCount > 0 )
143+ return "Failed" ;
144+
145+ return "None" ;
146+ } ;
147+ }
148+
149+ private static Func < ModManifest , string ? > CreateManifestStringAccessor (
150+ string memberName ,
151+ Func < ModManifest , string ? > directAccessor )
152+ {
153+ if ( typeof ( ModManifest ) . GetField ( memberName , InstanceMemberFlags ) != null )
154+ return directAccessor ;
155+
156+ var getter = CreateUntypedMemberGetter ( typeof ( ModManifest ) , memberName ) ;
157+ return manifest => getter ? . Invoke ( manifest ) as string ;
158+ }
159+
160+ private static Func < ModManifest , bool > CreateManifestBoolAccessor (
161+ string memberName ,
162+ Func < ModManifest , bool > directAccessor ,
163+ bool defaultValue )
164+ {
165+ if ( typeof ( ModManifest ) . GetField ( memberName , InstanceMemberFlags ) != null )
166+ return directAccessor ;
167+
168+ var getter = CreateUntypedMemberGetter ( typeof ( ModManifest ) , memberName ) ;
169+ return manifest => ReadBool ( getter , manifest ) ?? defaultValue ;
170+ }
171+
172+ private static AssemblyName ? ResolveAssemblyName ( Assembly ? assembly )
173+ {
174+ if ( assembly == null )
175+ return null ;
176+
177+ try
178+ {
179+ return assembly . GetName ( ) ;
180+ }
181+ catch
182+ {
183+ return null ;
184+ }
185+ }
186+
187+ private static IReadOnlyList < LocString > NormalizeErrors ( IEnumerable < LocString > ? errors )
188+ {
189+ return errors ? . Where ( error => error != null ) . ToArray ( ) ?? [ ] ;
190+ }
191+
192+ private static bool ? ReadBool ( Func < object , object ? > ? getter , object target )
193+ {
194+ return getter ? . Invoke ( target ) is bool value ? value : null ;
195+ }
196+
197+ private static Func < object , object ? > ? CreateUntypedMemberGetter ( Type type , string memberName )
198+ {
199+ var field = type . GetField ( memberName , InstanceMemberFlags ) ;
200+ if ( field != null )
201+ return field . GetValue ;
202+
203+ var property = type . GetProperty ( memberName , InstanceMemberFlags ) ;
204+ if ( property == null )
205+ return null ;
206+
207+ var getter = property . GetGetMethod ( true ) ;
208+ if ( getter == null )
209+ return property . GetValue ;
210+
211+ try
212+ {
213+ return CreateUntypedPropertyGetter ( type , property . PropertyType , getter ) ;
214+ }
215+ catch
216+ {
217+ return target => getter . Invoke ( target , null ) ;
218+ }
219+ }
220+
221+ private static Func < object , object ? > CreateUntypedPropertyGetter (
222+ Type declaringType ,
223+ Type valueType ,
224+ MethodInfo getter )
225+ {
226+ var method = typeof ( Sts2ModManagerCompat )
227+ . GetMethod ( nameof ( CreateUntypedPropertyGetterCore ) , BindingFlags . Static | BindingFlags . NonPublic ) !
228+ . MakeGenericMethod ( declaringType , valueType ) ;
229+ return ( Func < object , object ? > ) method . Invoke ( null , [ getter ] ) ! ;
230+ }
231+
232+ private static Func < object , object ? > CreateUntypedPropertyGetterCore < TDeclaring , TValue > (
233+ MethodInfo getter )
234+ {
235+ var typedGetter = getter . CreateDelegate < Func < TDeclaring , TValue > > ( ) ;
236+
237+ return target => typedGetter ( ( TDeclaring ) target ) ;
238+ }
24239 }
240+
241+ internal sealed record Sts2ModInventoryEntry (
242+ string Id ,
243+ string Name ,
244+ string ? Version ,
245+ string State ,
246+ string Source ,
247+ bool AffectsGameplay ,
248+ string ? AssemblyName ,
249+ string ? AssemblyVersion ,
250+ IReadOnlyList < LocString > Errors ) ;
25251}
0 commit comments