1+ using BaseX ;
12using FrooxEngine ;
23using HarmonyLib ;
34using System ;
45using System . Collections . Generic ;
6+ using System . ComponentModel ;
57using System . IO ;
8+ using System . Linq ;
69using System . Reflection ;
710using System . Reflection . Emit ;
811using System . Runtime . CompilerServices ;
@@ -12,65 +15,159 @@ namespace NeosModLoader
1215{
1316 internal class NeosVersionReset
1417 {
18+ // used when AdvertiseVersion == true
19+ private const string NEOS_MOD_LOADER = "NeosModLoader.dll" ;
20+
1521 internal static void Initialize ( )
1622 {
1723 ModLoaderConfiguration config = ModLoaderConfiguration . Get ( ) ;
1824 Engine engine = Engine . Current ;
1925
2026 List < string > extraAssemblies = Engine . ExtraAssemblies ;
2127 string assemblyFilename = Path . GetFileName ( Assembly . GetExecutingAssembly ( ) . Location ) ;
22- bool nmlPresent = extraAssemblies . Contains ( assemblyFilename ) ;
28+ bool nmlPresent = extraAssemblies . Remove ( assemblyFilename ) ;
2329
2430 if ( ! nmlPresent )
2531 {
2632 throw new Exception ( $ "Assertion failed: Engine.ExtraAssemblies did not contain \" { assemblyFilename } \" ") ;
2733 }
2834
29- bool otherPluginsPresent = extraAssemblies . Count > 1 ;
30- bool shouldSpoofCompatibility = ! otherPluginsPresent || config . Unsafe ;
31- bool shouldSpoofVersion = ! config . AdvertiseVersion && shouldSpoofCompatibility ;
35+ // get all PostX'd assemblies. This is useful, as plugins can't NOT be PostX'd.
36+ Assembly [ ] postxedAssemblies = AppDomain . CurrentDomain . GetAssemblies ( )
37+ . Where ( IsPostXProcessed )
38+ . ToArray ( ) ;
3239
33- if ( shouldSpoofVersion )
34- {
35- // we intentionally attempt to set the version string first, so if it fails the compatibilty hash is left on the original value
36- // this is to prevent the case where a player simply doesn't know their version string is wrong
37- extraAssemblies . Clear ( ) ;
38- if ( ! SpoofVersionString ( engine ) )
40+ string potentialPlugins = postxedAssemblies
41+ . Select ( a => Path . GetFileName ( a . Location ) )
42+ . Join ( delimiter : ", " ) ;
43+
44+ Logger . DebugFuncInternal ( ( ) => $ "Found { postxedAssemblies . Length } potential plugins: { potentialPlugins } ") ;
45+
46+ HashSet < Assembly > expectedPostXAssemblies = GetExpectedPostXAssemblies ( ) ;
47+
48+ // attempt to map the PostX'd assemblies to Neos's plugin list
49+ Dictionary < string , Assembly > plugins = new Dictionary < string , Assembly > ( postxedAssemblies . Length ) ;
50+ Assembly [ ] unmatchedAssemblies = postxedAssemblies
51+ . Where ( assembly =>
3952 {
40- Logger . WarnInternal ( "Version string spoofing failed" ) ;
41- return ;
42- }
53+ string filename = Path . GetFileName ( assembly . Location ) ;
54+ if ( extraAssemblies . Contains ( filename ) )
55+ {
56+ // okay, the assembly's filename is in the plugin list. It's probably a plugin.
57+ plugins . Add ( filename , assembly ) ;
58+ return false ;
59+ }
60+ else
61+ {
62+ // remove certain expected assemblies from the "unmatchedAssemblies" naughty list
63+ return ! expectedPostXAssemblies . Contains ( assembly ) ;
64+ }
65+ } )
66+ . ToArray ( ) ;
67+
68+ string actualPlugins = plugins . Keys . Join ( delimiter : ", " ) ;
69+ Logger . DebugFuncInternal ( ( ) => $ "Found { plugins . Count } actual plugins: { actualPlugins } ") ;
70+
71+ // warn about the assemblies we couldn't map to plugins
72+ foreach ( Assembly assembly in unmatchedAssemblies )
73+ {
74+ Logger . WarnInternal ( $ "Unexpected PostX'd assembly: \" { assembly . Location } \" . If this is a plugin, then my plugin-detection code is faulty.") ;
4375 }
44- else
76+
77+ // warn about the plugins we couldn't map to assemblies
78+ HashSet < string > unmatchedPlugins = new ( extraAssemblies ) ;
79+ unmatchedPlugins . ExceptWith ( plugins . Keys ) ; // remove all matched plugins
80+ foreach ( string plugin in unmatchedPlugins )
4581 {
46- Logger . MsgInternal ( "Version string not being spoofed due to config .") ;
82+ Logger . ErrorInternal ( $ "Unmatched plugin: \" { plugin } \" . NML could not find the assembly for this plugin, therefore NML cannot properly calculate the compatibility hash .") ;
4783 }
4884
49- if ( shouldSpoofCompatibility )
85+ // flags used later to determine how to spoof
86+ bool includePluginsInHash = true ;
87+
88+ // if unsafe is true, we should pretend there are no plugins and spoof everything
89+ if ( config . Unsafe )
5090 {
51- if ( ! SpoofCompatibilityHash ( engine ) )
91+ if ( ! config . AdvertiseVersion )
5292 {
53- Logger . WarnInternal ( "Compatibility hash spoofing failed" ) ;
54- return ;
93+ extraAssemblies . Clear ( ) ;
5594 }
95+ includePluginsInHash = false ;
96+ Logger . WarnInternal ( "Unsafe mode is enabled! Not that you had a warranty, but now it's DOUBLE void!" ) ;
5697 }
57- else
98+ // else if unmatched plugins are present, we should not spoof anything
99+ else if ( unmatchedPlugins . Count != 0 )
58100 {
59- Logger . WarnInternal ( "Version spoofing was not performed due to another plugin being present! Either remove unknown plugins or enable NeosModLoader's unsafe mode." ) ;
101+ Logger . ErrorInternal ( "Version spoofing was not performed due to some plugins having missing assemblies." ) ;
102+ return ;
103+ }
104+ // else we should spoof normally
105+
106+
107+ // get plugin assemblies sorted in the same order Neos sorted them.
108+ List < Assembly > sortedPlugins = extraAssemblies
109+ . Select ( path => plugins [ path ] )
110+ . ToList ( ) ;
111+
112+ if ( config . AdvertiseVersion )
113+ {
114+ // put NML back in the version string
115+ Logger . MsgInternal ( $ "Adding { NEOS_MOD_LOADER } to version string because you have AdvertiseVersion set to true.") ;
116+ extraAssemblies . Insert ( 0 , NEOS_MOD_LOADER ) ;
117+ }
118+
119+ // we intentionally attempt to set the version string first, so if it fails the compatibilty hash is left on the original value
120+ // this is to prevent the case where a player simply doesn't know their version string is wrong
121+ if ( ! SpoofVersionString ( engine ) )
122+ {
123+ Logger . WarnInternal ( "Version string spoofing failed" ) ;
124+ return ;
125+ }
126+
127+ if ( ! SpoofCompatibilityHash ( engine , sortedPlugins , includePluginsInHash ) )
128+ {
129+ Logger . WarnInternal ( "Compatibility hash spoofing failed" ) ;
60130 return ;
61131 }
62132
63133 Logger . MsgInternal ( "Compatibility hash spoofing succeeded" ) ;
64134 }
65135
66- private static bool SpoofCompatibilityHash ( Engine engine )
136+ private static bool IsPostXProcessed ( Assembly assembly )
137+ {
138+ return assembly . Modules // in practice there will only be one module, and it will have the dll's name
139+ . SelectMany ( module => module . GetCustomAttributes < DescriptionAttribute > ( ) )
140+ . Where ( IsPostXProcessedAttribute )
141+ . Any ( ) ;
142+ }
143+
144+ private static bool IsPostXProcessedAttribute ( DescriptionAttribute descriptionAttribute )
145+ {
146+ return descriptionAttribute . Description == "POSTX_PROCESSED" ;
147+ }
148+
149+ // get all the non-plugin PostX'd assemblies we expect to exist
150+ private static HashSet < Assembly > GetExpectedPostXAssemblies ( )
151+ {
152+ List < Assembly ? > list = new ( )
153+ {
154+ Type . GetType ( "FrooxEngine.IComponent, FrooxEngine" ) ? . Assembly ,
155+ Type . GetType ( "BusinessX.NeosClassroom, BusinessX" ) ? . Assembly ,
156+ Assembly . GetExecutingAssembly ( ) ,
157+ } ;
158+ return list
159+ . Where ( assembly => assembly != null )
160+ . ToHashSet ( ) ! ;
161+ }
162+
163+ private static bool SpoofCompatibilityHash ( Engine engine , List < Assembly > plugins , bool includePluginsInHash )
67164 {
68165 string vanillaCompatibilityHash ;
69166 int ? vanillaProtocolVersionMaybe = GetVanillaProtocolVersion ( ) ;
70167 if ( vanillaProtocolVersionMaybe is int vanillaProtocolVersion )
71168 {
72169 Logger . DebugFuncInternal ( ( ) => $ "Vanilla protocol version is { vanillaProtocolVersion } ") ;
73- vanillaCompatibilityHash = CalculateCompatibilityHash ( vanillaProtocolVersion ) ;
170+ vanillaCompatibilityHash = CalculateCompatibilityHash ( vanillaProtocolVersion , plugins , includePluginsInHash ) ;
74171 return SetCompatibilityHash ( engine , vanillaCompatibilityHash ) ;
75172 }
76173 else
@@ -80,10 +177,21 @@ private static bool SpoofCompatibilityHash(Engine engine)
80177 }
81178 }
82179
83- private static string CalculateCompatibilityHash ( int ProtocolVersion )
180+ private static string CalculateCompatibilityHash ( int ProtocolVersion , List < Assembly > plugins , bool includePluginsInHash )
84181 {
85182 using MD5CryptoServiceProvider cryptoServiceProvider = new ( ) ;
86- byte [ ] hash = cryptoServiceProvider . ComputeHash ( new MemoryStream ( BitConverter . GetBytes ( ProtocolVersion ) ) ) ;
183+ ConcatenatedStream inputStream = new ( ) ;
184+ inputStream . EnqueueStream ( new MemoryStream ( BitConverter . GetBytes ( ProtocolVersion ) ) ) ;
185+ if ( includePluginsInHash )
186+ {
187+ foreach ( Assembly plugin in plugins )
188+ {
189+ FileStream fileStream = File . OpenRead ( plugin . Location ) ;
190+ fileStream . Seek ( 375L , SeekOrigin . Current ) ;
191+ inputStream . EnqueueStream ( fileStream ) ;
192+ }
193+ }
194+ byte [ ] hash = cryptoServiceProvider . ComputeHash ( inputStream ) ;
87195 return Convert . ToBase64String ( hash ) ;
88196 }
89197
@@ -109,20 +217,18 @@ private static bool SetCompatibilityHash(Engine engine, string Target)
109217
110218 private static bool SpoofVersionString ( Engine engine )
111219 {
112- // calculate correct version string
113- string target = Engine . VersionNumber ;
220+ string cachedVersion = engine . VersionString ;
114221
115- if ( ! engine . VersionString . Equals ( target ) )
222+ FieldInfo field = AccessTools . DeclaredField ( engine . GetType ( ) , "_versionString" ) ;
223+ if ( field == null )
116224 {
117- FieldInfo field = AccessTools . DeclaredField ( engine . GetType ( ) , "_versionString" ) ;
118- if ( field == null )
119- {
120- Logger . WarnInternal ( "Unable to write Engine._versionString" ) ;
121- return false ;
122- }
123- Logger . DebugFuncInternal ( ( ) => $ "Changing version string from { engine . VersionString } to { target } ") ;
124- field . SetValue ( engine , target ) ;
225+ Logger . WarnInternal ( "Unable to write Engine._versionString" ) ;
226+ return false ;
125227 }
228+ // null the cached value
229+ field . SetValue ( engine , null ) ;
230+
231+ Logger . DebugFuncInternal ( ( ) => $ "Changing version string from { cachedVersion } to { engine . VersionString } ") ;
126232 return true ;
127233 }
128234
0 commit comments