1
+ using System ;
2
+ using System . Collections . Concurrent ;
3
+ using System . Collections . Generic ;
4
+ using System . Globalization ;
5
+ using System . IO ;
6
+ using System . Runtime . InteropServices ;
7
+ using LocalPlayerPrefs ;
8
+ using MelonLoader ;
9
+ using MelonLoader . TinyJSON ;
10
+ using UnhollowerBaseLib ;
11
+ using UnityEngine ;
12
+
13
+ [ assembly: MelonInfo ( typeof ( LocalPlayerPrefsMod ) , "LocalPlayerPrefs" , "1.0.0" , "knah" , "https://github.com/knah/VRCMods" ) ]
14
+ [ assembly: MelonGame ( ) ]
15
+
16
+ namespace LocalPlayerPrefs
17
+ {
18
+ public class LocalPlayerPrefsMod : MelonMod
19
+ {
20
+ private const string FileName = "UserData/PlayerPrefs.json" ;
21
+
22
+ private readonly List < Delegate > myPinnedDelegates = new List < Delegate > ( ) ;
23
+ private readonly ConcurrentDictionary < string , object > myPrefs = new ConcurrentDictionary < string , object > ( ) ;
24
+
25
+ private bool myHadChanges = false ;
26
+
27
+ [ UnmanagedFunctionPointer ( CallingConvention . Cdecl ) ] private delegate bool TrySetFloatDelegate ( IntPtr keyPtr , float value ) ;
28
+ [ UnmanagedFunctionPointer ( CallingConvention . Cdecl ) ] private delegate bool TrySetIntDelegate ( IntPtr keyPtr , int value ) ;
29
+ [ UnmanagedFunctionPointer ( CallingConvention . Cdecl ) ] private delegate bool TrySetStringDelegate ( IntPtr keyPtr , IntPtr valuePtr ) ;
30
+
31
+ [ UnmanagedFunctionPointer ( CallingConvention . Cdecl ) ] private delegate int GetIntDelegate ( IntPtr keyPtr , int defaultValue ) ;
32
+ [ UnmanagedFunctionPointer ( CallingConvention . Cdecl ) ] private delegate float GetFloatDelegate ( IntPtr keyPtr , float defaultValue ) ;
33
+ [ UnmanagedFunctionPointer ( CallingConvention . Cdecl ) ] private delegate IntPtr GetStringDelegate ( IntPtr keyPtr , IntPtr defaultValuePtr ) ;
34
+
35
+ [ UnmanagedFunctionPointer ( CallingConvention . Cdecl ) ] private delegate bool HasKeyDelegate ( IntPtr keyPtr ) ;
36
+ [ UnmanagedFunctionPointer ( CallingConvention . Cdecl ) ] private delegate void DeleteKeyDelegate ( IntPtr keyPtr ) ;
37
+
38
+ [ UnmanagedFunctionPointer ( CallingConvention . Cdecl ) ] private delegate void VoidDelegate ( ) ;
39
+
40
+ public override void OnApplicationStart ( )
41
+ {
42
+ try
43
+ {
44
+ if ( File . Exists ( FileName ) )
45
+ {
46
+ var dict = ( ProxyObject ) JSON . Load ( File . ReadAllText ( FileName ) ) ;
47
+ foreach ( var keyValuePair in dict ) myPrefs [ keyValuePair . Key ] = ToObject ( keyValuePair . Key , keyValuePair . Value ) ;
48
+ MelonLogger . Log ( $ "Loaded { dict . Count } prefs from PlayerPrefs.json") ;
49
+ }
50
+ }
51
+ catch ( Exception ex )
52
+ {
53
+ MelonLogger . LogError ( $ "Unable to load PlayerPrefs.json: { ex } ") ;
54
+ }
55
+
56
+ HookICall < TrySetFloatDelegate > ( nameof ( PlayerPrefs . TrySetFloat ) , TrySetFloat ) ;
57
+ HookICall < TrySetIntDelegate > ( nameof ( PlayerPrefs . TrySetInt ) , TrySetInt ) ;
58
+ HookICall < TrySetStringDelegate > ( nameof ( PlayerPrefs . TrySetSetString ) , TrySetString ) ;
59
+
60
+ HookICall < GetFloatDelegate > ( nameof ( PlayerPrefs . GetFloat ) , GetFloat ) ;
61
+ HookICall < GetIntDelegate > ( nameof ( PlayerPrefs . GetInt ) , GetInt ) ;
62
+ HookICall < GetStringDelegate > ( nameof ( PlayerPrefs . GetString ) , GetString ) ;
63
+
64
+ HookICall < HasKeyDelegate > ( nameof ( PlayerPrefs . HasKey ) , HasKey ) ;
65
+ HookICall < DeleteKeyDelegate > ( nameof ( PlayerPrefs . DeleteKey ) , DeleteKey ) ;
66
+
67
+ HookICall < VoidDelegate > ( nameof ( PlayerPrefs . DeleteAll ) , DeleteAll ) ;
68
+ HookICall < VoidDelegate > ( nameof ( PlayerPrefs . Save ) , Save ) ;
69
+ }
70
+
71
+ private object ToObject ( string key , Variant value )
72
+ {
73
+ if ( value is null ) return null ;
74
+
75
+ if ( value is ProxyString proxyString )
76
+ return proxyString . ToString ( ) ;
77
+
78
+ if ( value is ProxyNumber number )
79
+ {
80
+ var numDouble = number . ToDouble ( NumberFormatInfo . InvariantInfo ) ;
81
+ if ( ( double ) ( int ) numDouble == numDouble )
82
+ return ( int ) numDouble ;
83
+
84
+ return ( float ) numDouble ;
85
+ }
86
+
87
+ throw new ArgumentException ( $ "Unknown value in prefs: { key } = { value ? . GetType ( ) } / { value } ") ;
88
+ }
89
+
90
+ public override void OnLevelWasLoaded ( int level )
91
+ {
92
+ Save ( ) ;
93
+ MelonLogger . Log ( "Saved PlayerPrefs.json on level load" ) ;
94
+ }
95
+
96
+ public override void OnApplicationQuit ( )
97
+ {
98
+ Save ( ) ;
99
+ MelonLogger . Log ( "Saved PlayerPrefs.json on exit" ) ;
100
+ }
101
+
102
+ private bool HasKey ( IntPtr keyPtr )
103
+ {
104
+ var key = IL2CPP . Il2CppStringToManaged ( keyPtr ) ;
105
+ return myPrefs . ContainsKey ( key ) ;
106
+ }
107
+
108
+ private void DeleteKey ( IntPtr keyPtr )
109
+ {
110
+ var key = IL2CPP . Il2CppStringToManaged ( keyPtr ) ;
111
+ myPrefs . TryRemove ( key , out _ ) ;
112
+ }
113
+
114
+ private void DeleteAll ( )
115
+ {
116
+ myPrefs . Clear ( ) ;
117
+ }
118
+
119
+ private readonly object mySaveLock = new object ( ) ;
120
+ private void Save ( )
121
+ {
122
+ if ( ! myHadChanges )
123
+ return ;
124
+
125
+ myHadChanges = false ;
126
+
127
+ try
128
+ {
129
+ lock ( mySaveLock )
130
+ {
131
+ File . WriteAllText ( FileName , JSON . Dump ( myPrefs , EncodeOptions . PrettyPrint ) ) ;
132
+ }
133
+ }
134
+ catch ( IOException ex )
135
+ {
136
+ MelonLogger . LogWarning ( $ "Exception while saving PlayerPrefs: { ex } ") ;
137
+ }
138
+ }
139
+
140
+ private float GetFloat ( IntPtr keyPtr , float defaultValue )
141
+ {
142
+ var key = IL2CPP . Il2CppStringToManaged ( keyPtr ) ;
143
+ if ( myPrefs . TryGetValue ( key , out var result ) )
144
+ {
145
+ switch ( result )
146
+ {
147
+ case float resultFloat :
148
+ return resultFloat ;
149
+ case int resultInt :
150
+ return resultInt ;
151
+ }
152
+ }
153
+
154
+ return defaultValue ;
155
+ }
156
+
157
+ private int GetInt ( IntPtr keyPtr , int defaultValue )
158
+ {
159
+ var key = IL2CPP . Il2CppStringToManaged ( keyPtr ) ;
160
+ if ( myPrefs . TryGetValue ( key , out var result ) )
161
+ {
162
+ switch ( result )
163
+ {
164
+ case float resultFloat :
165
+ return ( int ) resultFloat ;
166
+ case int resultInt :
167
+ return resultInt ;
168
+ }
169
+ }
170
+
171
+ return defaultValue ;
172
+ }
173
+
174
+ private IntPtr GetString ( IntPtr keyPtr , IntPtr defaultValuePtr )
175
+ {
176
+ var key = IL2CPP . Il2CppStringToManaged ( keyPtr ) ;
177
+ if ( myPrefs . TryGetValue ( key , out var result ) )
178
+ {
179
+ if ( result is string resultString )
180
+ return IL2CPP . ManagedStringToIl2Cpp ( resultString ) ;
181
+ }
182
+
183
+ return defaultValuePtr ;
184
+ }
185
+
186
+ private bool TrySetFloat ( IntPtr keyPtr , float value )
187
+ {
188
+ myHadChanges = true ;
189
+
190
+ var key = IL2CPP . Il2CppStringToManaged ( keyPtr ) ;
191
+ myPrefs [ key ] = value ;
192
+ return true ;
193
+ }
194
+
195
+ private bool TrySetInt ( IntPtr keyPtr , int value )
196
+ {
197
+ myHadChanges = true ;
198
+
199
+ var key = IL2CPP . Il2CppStringToManaged ( keyPtr ) ;
200
+ myPrefs [ key ] = value ;
201
+ return true ;
202
+ }
203
+
204
+ private bool TrySetString ( IntPtr keyPtr , IntPtr valuePtr )
205
+ {
206
+ myHadChanges = true ;
207
+
208
+ var key = IL2CPP . Il2CppStringToManaged ( keyPtr ) ;
209
+ myPrefs [ key ] = IL2CPP . Il2CppStringToManaged ( valuePtr ) ;
210
+ return true ;
211
+ }
212
+
213
+ private unsafe void HookICall < T > ( string name , T target ) where T : Delegate
214
+ {
215
+ var originalPointer = IL2CPP . il2cpp_resolve_icall ( "UnityEngine.PlayerPrefs::" + name ) ;
216
+ if ( originalPointer == IntPtr . Zero )
217
+ {
218
+ MelonLogger . LogWarning ( $ "ICall { name } was not found, not patching") ;
219
+ return ;
220
+ }
221
+
222
+ myPinnedDelegates . Add ( target ) ;
223
+ Imports . Hook ( ( IntPtr ) ( & originalPointer ) , Marshal . GetFunctionPointerForDelegate ( target ) ) ;
224
+ }
225
+ }
226
+ }
0 commit comments