Skip to content
This repository was archived by the owner on May 15, 2023. It is now read-only.

Commit 050e38f

Browse files
committed
New mod: LocalPlayerPrefs
1 parent 663eb1c commit 050e38f

File tree

4 files changed

+247
-0
lines changed

4 files changed

+247
-0
lines changed
+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net472</TargetFramework>
5+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
6+
<Version>1.0.0.0</Version>
7+
</PropertyGroup>
8+
9+
</Project>
+226
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
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+
}

README.md

+6
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ Current features:
3737
- Toggleable per instance type (public/friends/private)
3838
- Can be set to highlight friends or show only friends
3939

40+
## LocalPlayerPrefs
41+
This mod moves game settings storage from Windows registry to UserData folder.
42+
This can make using multiple accounts easier by having separate installs for them.
43+
Do note that some settings will stay in registry (the ones that Unity itself uses as opposed to game code).
44+
There's also no import from registry, so expect to have to log in again after installing this mod.
45+
4046
## MirrorResolutionUnlimiter
4147
Headset and display resolutions increase each year, and yet VRChat limits mirror resolution to 2048 pixels per eye. With this mod, that's not the case anymore!
4248
Set whatever limit you want, with an option to un-potatoify mirrors that world makers set to potato resolution for their insane reasons. Or you can make all mirrors blurry as a sacrifice to performance gods. It's up to you, really.

VRCMods.sln

+6
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParticleAndBoneLimiterSetti
3131
EndProject
3232
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoSteamAtAll", "NoSteamAtAll\NoSteamAtAll.csproj", "{2BE973E0-E16C-4BAF-9266-E58304C24637}"
3333
EndProject
34+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LocalPlayerPrefs", "LocalPlayerPrefs\LocalPlayerPrefs.csproj", "{0DF69680-B5E0-4E1F-A047-634160E0E0E8}"
35+
EndProject
3436
Global
3537
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3638
Debug|Any CPU = Debug|Any CPU
@@ -85,5 +87,9 @@ Global
8587
{2BE973E0-E16C-4BAF-9266-E58304C24637}.Debug|Any CPU.Build.0 = Debug|Any CPU
8688
{2BE973E0-E16C-4BAF-9266-E58304C24637}.Release|Any CPU.ActiveCfg = Release|Any CPU
8789
{2BE973E0-E16C-4BAF-9266-E58304C24637}.Release|Any CPU.Build.0 = Release|Any CPU
90+
{0DF69680-B5E0-4E1F-A047-634160E0E0E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
91+
{0DF69680-B5E0-4E1F-A047-634160E0E0E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
92+
{0DF69680-B5E0-4E1F-A047-634160E0E0E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
93+
{0DF69680-B5E0-4E1F-A047-634160E0E0E8}.Release|Any CPU.Build.0 = Release|Any CPU
8894
EndGlobalSection
8995
EndGlobal

0 commit comments

Comments
 (0)