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

Commit

Permalink
New mod: LocalPlayerPrefs
Browse files Browse the repository at this point in the history
  • Loading branch information
knah committed Sep 21, 2020
1 parent 663eb1c commit 050e38f
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 0 deletions.
9 changes: 9 additions & 0 deletions LocalPlayerPrefs/LocalPlayerPrefs.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Version>1.0.0.0</Version>
</PropertyGroup>

</Project>
226 changes: 226 additions & 0 deletions LocalPlayerPrefs/LocalPlayerPrefsMod.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using LocalPlayerPrefs;
using MelonLoader;
using MelonLoader.TinyJSON;
using UnhollowerBaseLib;
using UnityEngine;

[assembly:MelonInfo(typeof(LocalPlayerPrefsMod), "LocalPlayerPrefs", "1.0.0", "knah", "https://github.com/knah/VRCMods")]
[assembly:MelonGame()]

namespace LocalPlayerPrefs
{
public class LocalPlayerPrefsMod : MelonMod
{
private const string FileName = "UserData/PlayerPrefs.json";

private readonly List<Delegate> myPinnedDelegates = new List<Delegate>();
private readonly ConcurrentDictionary<string, object> myPrefs = new ConcurrentDictionary<string, object>();

private bool myHadChanges = false;

[UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate bool TrySetFloatDelegate(IntPtr keyPtr, float value);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate bool TrySetIntDelegate(IntPtr keyPtr, int value);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate bool TrySetStringDelegate(IntPtr keyPtr, IntPtr valuePtr);

[UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate int GetIntDelegate(IntPtr keyPtr, int defaultValue);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate float GetFloatDelegate(IntPtr keyPtr, float defaultValue);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate IntPtr GetStringDelegate(IntPtr keyPtr, IntPtr defaultValuePtr);

[UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate bool HasKeyDelegate(IntPtr keyPtr);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void DeleteKeyDelegate(IntPtr keyPtr);

[UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void VoidDelegate();

public override void OnApplicationStart()
{
try
{
if (File.Exists(FileName))
{
var dict = (ProxyObject) JSON.Load(File.ReadAllText(FileName));
foreach (var keyValuePair in dict) myPrefs[keyValuePair.Key] = ToObject(keyValuePair.Key, keyValuePair.Value);
MelonLogger.Log($"Loaded {dict.Count} prefs from PlayerPrefs.json");
}
}
catch (Exception ex)
{
MelonLogger.LogError($"Unable to load PlayerPrefs.json: {ex}");
}

HookICall<TrySetFloatDelegate>(nameof(PlayerPrefs.TrySetFloat), TrySetFloat);
HookICall<TrySetIntDelegate>(nameof(PlayerPrefs.TrySetInt), TrySetInt);
HookICall<TrySetStringDelegate>(nameof(PlayerPrefs.TrySetSetString), TrySetString);

HookICall<GetFloatDelegate>(nameof(PlayerPrefs.GetFloat), GetFloat);
HookICall<GetIntDelegate>(nameof(PlayerPrefs.GetInt), GetInt);
HookICall<GetStringDelegate>(nameof(PlayerPrefs.GetString), GetString);

HookICall<HasKeyDelegate>(nameof(PlayerPrefs.HasKey), HasKey);
HookICall<DeleteKeyDelegate>(nameof(PlayerPrefs.DeleteKey), DeleteKey);

HookICall<VoidDelegate>(nameof(PlayerPrefs.DeleteAll), DeleteAll);
HookICall<VoidDelegate>(nameof(PlayerPrefs.Save), Save);
}

private object ToObject(string key, Variant value)
{
if (value is null) return null;

if (value is ProxyString proxyString)
return proxyString.ToString();

if (value is ProxyNumber number)
{
var numDouble = number.ToDouble(NumberFormatInfo.InvariantInfo);
if ((double) (int) numDouble == numDouble)
return (int) numDouble;

return (float) numDouble;
}

throw new ArgumentException($"Unknown value in prefs: {key} = {value?.GetType()} / {value}");
}

public override void OnLevelWasLoaded(int level)
{
Save();
MelonLogger.Log("Saved PlayerPrefs.json on level load");
}

public override void OnApplicationQuit()
{
Save();
MelonLogger.Log("Saved PlayerPrefs.json on exit");
}

private bool HasKey(IntPtr keyPtr)
{
var key = IL2CPP.Il2CppStringToManaged(keyPtr);
return myPrefs.ContainsKey(key);
}

private void DeleteKey(IntPtr keyPtr)
{
var key = IL2CPP.Il2CppStringToManaged(keyPtr);
myPrefs.TryRemove(key, out _);
}

private void DeleteAll()
{
myPrefs.Clear();
}

private readonly object mySaveLock = new object();
private void Save()
{
if (!myHadChanges)
return;

myHadChanges = false;

try
{
lock (mySaveLock)
{
File.WriteAllText(FileName, JSON.Dump(myPrefs, EncodeOptions.PrettyPrint));
}
}
catch (IOException ex)
{
MelonLogger.LogWarning($"Exception while saving PlayerPrefs: {ex}");
}
}

private float GetFloat(IntPtr keyPtr, float defaultValue)
{
var key = IL2CPP.Il2CppStringToManaged(keyPtr);
if (myPrefs.TryGetValue(key, out var result))
{
switch (result)
{
case float resultFloat:
return resultFloat;
case int resultInt:
return resultInt;
}
}

return defaultValue;
}

private int GetInt(IntPtr keyPtr, int defaultValue)
{
var key = IL2CPP.Il2CppStringToManaged(keyPtr);
if (myPrefs.TryGetValue(key, out var result))
{
switch (result)
{
case float resultFloat:
return (int) resultFloat;
case int resultInt:
return resultInt;
}
}

return defaultValue;
}

private IntPtr GetString(IntPtr keyPtr, IntPtr defaultValuePtr)
{
var key = IL2CPP.Il2CppStringToManaged(keyPtr);
if (myPrefs.TryGetValue(key, out var result))
{
if (result is string resultString)
return IL2CPP.ManagedStringToIl2Cpp(resultString);
}

return defaultValuePtr;
}

private bool TrySetFloat(IntPtr keyPtr, float value)
{
myHadChanges = true;

var key = IL2CPP.Il2CppStringToManaged(keyPtr);
myPrefs[key] = value;
return true;
}

private bool TrySetInt(IntPtr keyPtr, int value)
{
myHadChanges = true;

var key = IL2CPP.Il2CppStringToManaged(keyPtr);
myPrefs[key] = value;
return true;
}

private bool TrySetString(IntPtr keyPtr, IntPtr valuePtr)
{
myHadChanges = true;

var key = IL2CPP.Il2CppStringToManaged(keyPtr);
myPrefs[key] = IL2CPP.Il2CppStringToManaged(valuePtr);
return true;
}

private unsafe void HookICall<T>(string name, T target) where T: Delegate
{
var originalPointer = IL2CPP.il2cpp_resolve_icall("UnityEngine.PlayerPrefs::" + name);
if (originalPointer == IntPtr.Zero)
{
MelonLogger.LogWarning($"ICall {name} was not found, not patching");
return;
}

myPinnedDelegates.Add(target);
Imports.Hook((IntPtr) (&originalPointer), Marshal.GetFunctionPointerForDelegate(target));
}
}
}
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ Current features:
- Toggleable per instance type (public/friends/private)
- Can be set to highlight friends or show only friends

## LocalPlayerPrefs
This mod moves game settings storage from Windows registry to UserData folder.
This can make using multiple accounts easier by having separate installs for them.
Do note that some settings will stay in registry (the ones that Unity itself uses as opposed to game code).
There's also no import from registry, so expect to have to log in again after installing this mod.

## MirrorResolutionUnlimiter
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!
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.
Expand Down
6 changes: 6 additions & 0 deletions VRCMods.sln
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParticleAndBoneLimiterSetti
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoSteamAtAll", "NoSteamAtAll\NoSteamAtAll.csproj", "{2BE973E0-E16C-4BAF-9266-E58304C24637}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LocalPlayerPrefs", "LocalPlayerPrefs\LocalPlayerPrefs.csproj", "{0DF69680-B5E0-4E1F-A047-634160E0E0E8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -85,5 +87,9 @@ Global
{2BE973E0-E16C-4BAF-9266-E58304C24637}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2BE973E0-E16C-4BAF-9266-E58304C24637}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2BE973E0-E16C-4BAF-9266-E58304C24637}.Release|Any CPU.Build.0 = Release|Any CPU
{0DF69680-B5E0-4E1F-A047-634160E0E0E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0DF69680-B5E0-4E1F-A047-634160E0E0E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0DF69680-B5E0-4E1F-A047-634160E0E0E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0DF69680-B5E0-4E1F-A047-634160E0E0E8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

0 comments on commit 050e38f

Please sign in to comment.