first commit
This commit is contained in:
commit
aa44d3ad70
1585 changed files with 277994 additions and 0 deletions
|
@ -0,0 +1,60 @@
|
|||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
|
||||
#nullable enable
|
||||
|
||||
|
||||
//namespace BgTools.PlayerPrefsEditor
|
||||
namespace Meryel.UnityCodeAssist.Editor.Preferences
|
||||
{
|
||||
[System.Serializable]
|
||||
public class PreferenceEntryHolder : ScriptableObject
|
||||
{
|
||||
public List<PreferenceEntry>? userDefList;
|
||||
public List<PreferenceEntry>? unityDefList;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
hideFlags = HideFlags.DontSave;
|
||||
userDefList ??= new List<PreferenceEntry>();
|
||||
unityDefList ??= new List<PreferenceEntry>();
|
||||
}
|
||||
|
||||
public void ClearLists()
|
||||
{
|
||||
userDefList?.Clear();
|
||||
unityDefList?.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class PreferenceEntry
|
||||
{
|
||||
public enum PrefTypes
|
||||
{
|
||||
String = 0,
|
||||
Int = 1,
|
||||
Float = 2
|
||||
}
|
||||
|
||||
public PrefTypes m_typeSelection;
|
||||
public string? m_key;
|
||||
|
||||
// Need diffrend ones for auto type selection of serilizedProerty
|
||||
public string? m_strValue;
|
||||
public int m_intValue;
|
||||
public float m_floatValue;
|
||||
|
||||
public string? ValueAsString()
|
||||
{
|
||||
return m_typeSelection switch
|
||||
{
|
||||
PrefTypes.String => m_strValue,
|
||||
PrefTypes.Int => m_intValue.ToString(),
|
||||
PrefTypes.Float => m_floatValue.ToString(),
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 38f9da72011fd244b94cc233de93ac92
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,370 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Globalization;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEditorInternal;
|
||||
|
||||
|
||||
#nullable enable
|
||||
|
||||
|
||||
namespace Meryel.UnityCodeAssist.Editor.Preferences
|
||||
{
|
||||
public class PreferenceMonitor
|
||||
{
|
||||
private static readonly Lazy<PreferenceMonitor> _instanceOfPlayerPrefs = new Lazy<PreferenceMonitor>(() => new PreferenceMonitor(true));
|
||||
private static readonly Lazy<PreferenceMonitor> _instanceOfEditorPrefs = new Lazy<PreferenceMonitor>(() => new PreferenceMonitor(false));
|
||||
public static PreferenceMonitor InstanceOfPlayerPrefs => _instanceOfPlayerPrefs.Value;
|
||||
public static PreferenceMonitor InstanceOfEditorPrefs => _instanceOfEditorPrefs.Value;
|
||||
|
||||
//const int Limit = 128;
|
||||
const int Limit = 8192;
|
||||
|
||||
/// <summary>
|
||||
/// PlayerPrefs or EditorPrefs
|
||||
/// </summary>
|
||||
readonly bool isPlayerPrefs;
|
||||
|
||||
#region ErrorValues
|
||||
private readonly int ERROR_VALUE_INT = int.MinValue;
|
||||
private readonly string ERROR_VALUE_STR = "<UCA_ERR_2407201713>";
|
||||
#endregion //ErrorValues
|
||||
|
||||
#pragma warning disable CS0414
|
||||
private static string pathToPrefs = String.Empty;
|
||||
private static string platformPathPrefix = @"~";
|
||||
#pragma warning restore CS0414
|
||||
|
||||
//private string[] userDef;
|
||||
//private string[] unityDef;
|
||||
//private bool showSystemGroup = false;
|
||||
|
||||
private SerializedObject? serializedObject;
|
||||
private ReorderableList? userDefList;
|
||||
private ReorderableList? unityDefList;
|
||||
|
||||
private PreferenceEntryHolder? prefEntryHolder;
|
||||
|
||||
private PreferanceStorageAccessor? entryAccessor;
|
||||
|
||||
|
||||
private bool updateView = false;
|
||||
//private bool monitoring = false;
|
||||
//private bool showLoadingIndicatorOverlay = false;
|
||||
|
||||
|
||||
#if UNITY_EDITOR_LINUX
|
||||
private readonly char[] invalidFilenameChars = { '"', '\\', '*', '/', ':', '<', '>', '?', '|' };
|
||||
#elif UNITY_EDITOR_OSX
|
||||
private readonly char[] invalidFilenameChars = { '$', '%', '&', '\\', '/', ':', '<', '>', '|', '~' };
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
PreferenceMonitor(bool isPlayerPrefs)
|
||||
{
|
||||
this.isPlayerPrefs = isPlayerPrefs;
|
||||
OnEnable();
|
||||
EditorApplication.update += Update;
|
||||
}
|
||||
|
||||
~PreferenceMonitor()
|
||||
{
|
||||
OnDisable();
|
||||
}
|
||||
|
||||
public void Bump()
|
||||
{
|
||||
Serilog.Log.Debug("Bumping preference {IsPlayerPrefs}", isPlayerPrefs);
|
||||
|
||||
RetrieveAndSendKeysAndValues(false);
|
||||
}
|
||||
|
||||
private void RetrieveAndSendKeysAndValues(bool reloadKeys)
|
||||
{
|
||||
string[]? keys = GetKeys(reloadKeys);
|
||||
if (keys == null)
|
||||
return;
|
||||
string[] values = GetKeyValues(reloadKeys, keys, out var stringKeys, out var integerKeys, out var floatKeys, out var booleanKeys);
|
||||
|
||||
if (isPlayerPrefs)
|
||||
NetMQInitializer.Publisher?.SendPlayerPrefs(keys, values, stringKeys, integerKeys, floatKeys);
|
||||
else
|
||||
NetMQInitializer.Publisher?.SendEditorPrefs(keys, values, stringKeys, integerKeys, floatKeys, booleanKeys);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
#if UNITY_EDITOR_WIN
|
||||
if (isPlayerPrefs)
|
||||
pathToPrefs = @"SOFTWARE\Unity\UnityEditor\" + PlayerSettings.companyName + @"\" + PlayerSettings.productName;
|
||||
else
|
||||
pathToPrefs = @"Software\Unity Technologies\Unity Editor 5.x";
|
||||
|
||||
platformPathPrefix = @"<CurrentUser>";
|
||||
entryAccessor = new WindowsPrefStorage(pathToPrefs);
|
||||
#elif UNITY_EDITOR_OSX
|
||||
if (isPlayerPrefs)
|
||||
pathToPrefs = @"Library/Preferences/com." + MakeValidFileName(PlayerSettings.companyName) + "." + MakeValidFileName(PlayerSettings.productName) + ".plist";
|
||||
else
|
||||
pathToPrefs = @"Library/Preferences/com.unity3d.UnityEditor5.x.plist";
|
||||
|
||||
platformPathPrefix = @"~";
|
||||
entryAccessor = new MacPrefStorage(pathToPrefs);
|
||||
//entryAccessor.StartLoadingDelegate = () => { showLoadingIndicatorOverlay = true; };
|
||||
//entryAccessor.StopLoadingDelegate = () => { showLoadingIndicatorOverlay = false; };
|
||||
#elif UNITY_EDITOR_LINUX
|
||||
if (isPlayerPrefs)
|
||||
pathToPrefs = @".config/unity3d/" + MakeValidFileName(PlayerSettings.companyName) + "/" + MakeValidFileName(PlayerSettings.productName) + "/prefs";
|
||||
else
|
||||
pathToPrefs = @".local/share/unity3d/prefs";
|
||||
|
||||
platformPathPrefix = @"~";
|
||||
entryAccessor = new LinuxPrefStorage(pathToPrefs);
|
||||
#else
|
||||
Serilog.Log.Warning("Undefined Unity Editor platform");
|
||||
pathToPrefs = String.Empty;
|
||||
platformPathPrefix = @"~";
|
||||
entryAccessor = null;
|
||||
#endif
|
||||
|
||||
if (entryAccessor != null)
|
||||
{
|
||||
entryAccessor.PrefEntryChangedDelegate = () => { updateView = true; };
|
||||
entryAccessor.StartMonitoring();
|
||||
}
|
||||
}
|
||||
|
||||
// Handel view updates for monitored changes
|
||||
// Necessary to avoid main thread access issue
|
||||
private void Update()
|
||||
{
|
||||
if (updateView)
|
||||
{
|
||||
updateView = false;
|
||||
//PrepareData();
|
||||
//Repaint();
|
||||
|
||||
Serilog.Log.Debug("Updating preference {IsPlayerPrefs}", isPlayerPrefs);
|
||||
|
||||
RetrieveAndSendKeysAndValues(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
entryAccessor?.StopMonitoring();
|
||||
}
|
||||
|
||||
private void InitReorderedList()
|
||||
{
|
||||
if (prefEntryHolder == null)
|
||||
{
|
||||
var tmp = Resources.FindObjectsOfTypeAll<PreferenceEntryHolder>();
|
||||
if (tmp.Length > 0)
|
||||
{
|
||||
prefEntryHolder = tmp[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
prefEntryHolder = ScriptableObject.CreateInstance<PreferenceEntryHolder>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
serializedObject ??= new SerializedObject(prefEntryHolder);
|
||||
|
||||
userDefList = new ReorderableList(serializedObject, serializedObject.FindProperty("userDefList"), false, true, true, true);
|
||||
unityDefList = new ReorderableList(serializedObject, serializedObject.FindProperty("unityDefList"), false, true, false, false);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private string[]? GetKeys(bool reloadKeys)
|
||||
{
|
||||
if (entryAccessor == null)
|
||||
{
|
||||
Serilog.Log.Warning($"{nameof(entryAccessor)} is null");
|
||||
return null;
|
||||
}
|
||||
|
||||
string[] keys = entryAccessor.GetKeys(reloadKeys);
|
||||
|
||||
if (keys.Length > Limit)
|
||||
keys = keys.Where(k => !k.StartsWith("unity.") && !k.StartsWith("UnityGraphicsQuality")).Take(Limit).ToArray();
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
string[]? _cachedKeyValues = null;
|
||||
|
||||
string[]? _cachedStringKeys = null;
|
||||
string[]? _cachedIntegerKeys = null;
|
||||
string[]? _cachedFloatKeys = null;
|
||||
string[]? _cachedBooleanKeys = null;
|
||||
|
||||
private string[] GetKeyValues(bool reloadData, string[] keys,
|
||||
out string[] stringKeys, out string[] integerKeys, out string[] floatKeys, out string[] booleanKeys)
|
||||
{
|
||||
if (!reloadData && _cachedKeyValues != null && _cachedKeyValues.Length == keys.Length)
|
||||
{
|
||||
stringKeys = _cachedStringKeys!;
|
||||
integerKeys = _cachedIntegerKeys!;
|
||||
floatKeys = _cachedFloatKeys!;
|
||||
booleanKeys = _cachedBooleanKeys!;
|
||||
return _cachedKeyValues;
|
||||
}
|
||||
|
||||
string[] values = new string[keys.Length];
|
||||
var stringKeyList = new List<string>();
|
||||
var integerKeyList = new List<string>();
|
||||
var floatKeyList = new List<string>();
|
||||
var boolenKeyList = new List<string>();
|
||||
|
||||
for (int i = 0; i < keys.Length; i++)
|
||||
{
|
||||
var key = keys[i];
|
||||
|
||||
string stringValue;
|
||||
if (isPlayerPrefs)
|
||||
stringValue = PlayerPrefs.GetString(key, ERROR_VALUE_STR);
|
||||
else
|
||||
stringValue = EditorPrefs.GetString(key, ERROR_VALUE_STR);
|
||||
|
||||
if (stringValue != ERROR_VALUE_STR)
|
||||
{
|
||||
values[i] = stringValue;
|
||||
stringKeyList.Add(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
float floatValue;
|
||||
if (isPlayerPrefs)
|
||||
floatValue = PlayerPrefs.GetFloat(key, float.NaN);
|
||||
else
|
||||
floatValue = EditorPrefs.GetFloat(key, float.NaN);
|
||||
|
||||
if (!float.IsNaN(floatValue))
|
||||
{
|
||||
values[i] = floatValue.ToString();
|
||||
floatKeyList.Add(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
int intValue;
|
||||
if (isPlayerPrefs)
|
||||
intValue = PlayerPrefs.GetInt(key, ERROR_VALUE_INT);
|
||||
else
|
||||
intValue = EditorPrefs.GetInt(key, ERROR_VALUE_INT);
|
||||
|
||||
if (intValue != ERROR_VALUE_INT)
|
||||
{
|
||||
values[i] = intValue.ToString();
|
||||
integerKeyList.Add(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
bool boolValue = false;
|
||||
if (!isPlayerPrefs)
|
||||
{
|
||||
bool boolValueTrue = EditorPrefs.GetBool(key, true);
|
||||
bool boolValueFalse = EditorPrefs.GetBool(key, false);
|
||||
|
||||
boolValue = boolValueFalse;
|
||||
if (boolValueTrue == boolValueFalse)
|
||||
{
|
||||
values[i] = boolValueTrue.ToString();
|
||||
boolenKeyList.Add(key);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
values[i] = string.Empty;
|
||||
if (isPlayerPrefs)
|
||||
{
|
||||
// Keys with ? causing problems, just ignore them
|
||||
if (key.Contains("?"))
|
||||
Serilog.Log.Debug("Invalid {PreferenceType} KEY WITH '?', '{Key}' at {Location}, str:{StringValue}, int:{IntegerValue}, float:{FloatValue}, bool:{BooleanValue}",
|
||||
(isPlayerPrefs ? "PlayerPrefs" : "EditorPrefs"), key, nameof(GetKeyValues),
|
||||
stringValue, intValue, floatValue, boolValue);
|
||||
|
||||
else
|
||||
// EditorPrefs gives error for some keys
|
||||
Serilog.Log.Error("Invalid {PreferenceType} '{Key}' at {Location}, str:{StringValue}, int:{IntegerValue}, float:{FloatValue}, bool:{BooleanValue}",
|
||||
(isPlayerPrefs ? "PlayerPrefs" : "EditorPrefs"), key, nameof(GetKeyValues),
|
||||
stringValue, intValue, floatValue, boolValue);
|
||||
}
|
||||
}
|
||||
|
||||
stringKeys = stringKeyList.ToArray();
|
||||
integerKeys = integerKeyList.ToArray();
|
||||
floatKeys = floatKeyList.ToArray();
|
||||
booleanKeys = boolenKeyList.ToArray();
|
||||
|
||||
_cachedKeyValues = values;
|
||||
|
||||
_cachedStringKeys = stringKeys;
|
||||
_cachedIntegerKeys = integerKeys;
|
||||
_cachedFloatKeys = floatKeys;
|
||||
_cachedBooleanKeys = booleanKeys;
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
private void LoadKeys(out string[]? userDef, out string[]? unityDef, bool reloadKeys)
|
||||
{
|
||||
if(entryAccessor == null)
|
||||
{
|
||||
userDef = null;
|
||||
unityDef = null;
|
||||
return;
|
||||
}
|
||||
|
||||
string[] keys = entryAccessor.GetKeys(reloadKeys);
|
||||
|
||||
//keys.ToList().ForEach( e => { Debug.Log(e); } );
|
||||
|
||||
// Seperate keys int unity defined and user defined
|
||||
Dictionary<bool, List<string>> groups = keys
|
||||
.GroupBy((key) => key.StartsWith("unity.") || key.StartsWith("UnityGraphicsQuality"))
|
||||
.ToDictionary((g) => g.Key, (g) => g.ToList());
|
||||
|
||||
unityDef = (groups.ContainsKey(true)) ? groups[true].ToArray() : new string[0];
|
||||
userDef = (groups.ContainsKey(false)) ? groups[false].ToArray() : new string[0];
|
||||
}
|
||||
|
||||
|
||||
#if (UNITY_EDITOR_LINUX || UNITY_EDITOR_OSX)
|
||||
private string MakeValidFileName(string unsafeFileName)
|
||||
{
|
||||
string normalizedFileName = unsafeFileName.Trim().Normalize(NormalizationForm.FormD);
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
|
||||
// We need to use a TextElementEmumerator in order to support UTF16 characters that may take up more than one char(case 1169358)
|
||||
TextElementEnumerator charEnum = StringInfo.GetTextElementEnumerator(normalizedFileName);
|
||||
while (charEnum.MoveNext())
|
||||
{
|
||||
string c = charEnum.GetTextElement();
|
||||
if (c.Length == 1 && invalidFilenameChars.Contains(c[0]))
|
||||
{
|
||||
stringBuilder.Append('_');
|
||||
continue;
|
||||
}
|
||||
UnicodeCategory unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c, 0);
|
||||
if (unicodeCategory != UnicodeCategory.NonSpacingMark)
|
||||
stringBuilder.Append(c);
|
||||
}
|
||||
return stringBuilder.ToString().Normalize(NormalizationForm.FormC);
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7885cbf1aab77214c8bbcf789440022d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,287 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
|
||||
#if UNITY_EDITOR_WIN
|
||||
using Microsoft.Win32;
|
||||
using System.Text;
|
||||
#elif UNITY_EDITOR_OSX
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
#elif UNITY_EDITOR_LINUX
|
||||
using System.IO;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
#endif
|
||||
|
||||
|
||||
#nullable enable
|
||||
|
||||
|
||||
//namespace BgTools.PlayerPrefsEditor
|
||||
namespace Meryel.UnityCodeAssist.Editor.Preferences
|
||||
{
|
||||
public abstract class PreferanceStorageAccessor
|
||||
{
|
||||
protected string prefPath;
|
||||
protected string[] cachedData = new string[0];
|
||||
|
||||
protected abstract void FetchKeysFromSystem();
|
||||
|
||||
protected PreferanceStorageAccessor(string pathToPrefs)
|
||||
{
|
||||
prefPath = pathToPrefs;
|
||||
}
|
||||
|
||||
public string[] GetKeys(bool reloadData = true)
|
||||
{
|
||||
if (reloadData || cachedData.Length == 0)
|
||||
{
|
||||
FetchKeysFromSystem();
|
||||
}
|
||||
|
||||
return cachedData;
|
||||
}
|
||||
|
||||
public Action? PrefEntryChangedDelegate;
|
||||
protected bool ignoreNextChange = false;
|
||||
|
||||
public void IgnoreNextChange()
|
||||
{
|
||||
ignoreNextChange = true;
|
||||
}
|
||||
|
||||
protected virtual void OnPrefEntryChanged()
|
||||
{
|
||||
if (ignoreNextChange)
|
||||
{
|
||||
ignoreNextChange = false;
|
||||
return;
|
||||
}
|
||||
|
||||
PrefEntryChangedDelegate?.Invoke();
|
||||
}
|
||||
|
||||
public Action? StartLoadingDelegate;
|
||||
public Action? StopLoadingDelegate;
|
||||
|
||||
public abstract void StartMonitoring();
|
||||
public abstract void StopMonitoring();
|
||||
public abstract bool IsMonitoring();
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR_WIN
|
||||
|
||||
public class WindowsPrefStorage : PreferanceStorageAccessor
|
||||
{
|
||||
readonly RegistryMonitor monitor;
|
||||
|
||||
public WindowsPrefStorage(string pathToPrefs) : base(pathToPrefs)
|
||||
{
|
||||
monitor = new RegistryMonitor(RegistryHive.CurrentUser, prefPath);
|
||||
monitor.RegChanged += new EventHandler(OnRegChanged);
|
||||
}
|
||||
|
||||
private void OnRegChanged(object sender, EventArgs e)
|
||||
{
|
||||
OnPrefEntryChanged();
|
||||
}
|
||||
|
||||
protected override void FetchKeysFromSystem()
|
||||
{
|
||||
cachedData = new string[0];
|
||||
|
||||
using (RegistryKey rootKey = Registry.CurrentUser.OpenSubKey(prefPath))
|
||||
{
|
||||
if (rootKey != null)
|
||||
{
|
||||
cachedData = rootKey.GetValueNames();
|
||||
rootKey.Close();
|
||||
}
|
||||
}
|
||||
|
||||
// Clean <key>_h3320113488 nameing
|
||||
//cachedData = cachedData.Select((key) => { return key.Substring(0, key.LastIndexOf("_h", StringComparison.Ordinal)); }).ToArray();
|
||||
for (int i = 0; i < cachedData.Length; i++)
|
||||
{
|
||||
var indexOfSuffix = cachedData[i].LastIndexOf("_h", StringComparison.Ordinal);
|
||||
if (indexOfSuffix >= 0)
|
||||
cachedData[i] = cachedData[i].Substring(0, indexOfSuffix);
|
||||
}
|
||||
|
||||
EncodeAnsiInPlace();
|
||||
}
|
||||
|
||||
public override void StartMonitoring()
|
||||
{
|
||||
monitor.Start();
|
||||
}
|
||||
|
||||
public override void StopMonitoring()
|
||||
{
|
||||
monitor.Stop();
|
||||
}
|
||||
|
||||
public override bool IsMonitoring()
|
||||
{
|
||||
return monitor.IsMonitoring;
|
||||
}
|
||||
|
||||
private void EncodeAnsiInPlace()
|
||||
{
|
||||
Encoding utf8 = Encoding.UTF8;
|
||||
Encoding ansi = Encoding.GetEncoding(1252);
|
||||
|
||||
for (int i = 0; i < cachedData.Length; i++)
|
||||
{
|
||||
cachedData[i] = utf8.GetString(ansi.GetBytes(cachedData[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#elif UNITY_EDITOR_LINUX
|
||||
|
||||
public class LinuxPrefStorage : PreferanceStorageAccessor
|
||||
{
|
||||
readonly FileSystemWatcher fileWatcher;
|
||||
|
||||
public LinuxPrefStorage(string pathToPrefs) : base(Path.Combine(Environment.GetEnvironmentVariable("HOME"), pathToPrefs))
|
||||
{
|
||||
fileWatcher = new FileSystemWatcher
|
||||
{
|
||||
Path = Path.GetDirectoryName(prefPath),
|
||||
NotifyFilter = NotifyFilters.LastAccess | NotifyFilters.LastWrite,
|
||||
Filter = "prefs"
|
||||
};
|
||||
|
||||
fileWatcher.Changed += OnWatchedFileChanged;
|
||||
}
|
||||
|
||||
protected override void FetchKeysFromSystem()
|
||||
{
|
||||
cachedData = new string[0];
|
||||
|
||||
if (File.Exists(prefPath))
|
||||
{
|
||||
XmlReaderSettings settings = new XmlReaderSettings();
|
||||
XmlReader reader = XmlReader.Create(prefPath, settings);
|
||||
|
||||
XDocument doc = XDocument.Load(reader);
|
||||
|
||||
cachedData = doc.Element("unity_prefs").Elements().Select((e) => e.Attribute("name").Value).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public override void StartMonitoring()
|
||||
{
|
||||
fileWatcher.EnableRaisingEvents = true;
|
||||
}
|
||||
|
||||
public override void StopMonitoring()
|
||||
{
|
||||
fileWatcher.EnableRaisingEvents = false;
|
||||
}
|
||||
|
||||
public override bool IsMonitoring()
|
||||
{
|
||||
return fileWatcher.EnableRaisingEvents;
|
||||
}
|
||||
|
||||
private void OnWatchedFileChanged(object source, FileSystemEventArgs e)
|
||||
{
|
||||
OnPrefEntryChanged();
|
||||
}
|
||||
}
|
||||
|
||||
#elif UNITY_EDITOR_OSX
|
||||
|
||||
public class MacPrefStorage : PreferanceStorageAccessor
|
||||
{
|
||||
private readonly FileSystemWatcher fileWatcher;
|
||||
private readonly DirectoryInfo prefsDirInfo;
|
||||
private readonly String prefsFileNameWithoutExtension;
|
||||
|
||||
public MacPrefStorage(string pathToPrefs) : base(Path.Combine(Environment.GetEnvironmentVariable("HOME"), pathToPrefs))
|
||||
{
|
||||
prefsDirInfo = new DirectoryInfo(Path.GetDirectoryName(prefPath));
|
||||
prefsFileNameWithoutExtension = Path.GetFileNameWithoutExtension(prefPath);
|
||||
|
||||
fileWatcher = new FileSystemWatcher
|
||||
{
|
||||
Path = Path.GetDirectoryName(prefPath),
|
||||
NotifyFilter = NotifyFilters.LastWrite,
|
||||
Filter = Path.GetFileName(prefPath)
|
||||
};
|
||||
|
||||
// MAC delete the old and create a new file instead of updating
|
||||
fileWatcher.Created += OnWatchedFileChanged;
|
||||
}
|
||||
|
||||
protected override void FetchKeysFromSystem()
|
||||
{
|
||||
// Workaround to avoid incomplete tmp phase from MAC OS
|
||||
foreach (FileInfo info in prefsDirInfo.GetFiles())
|
||||
{
|
||||
// Check if tmp PlayerPrefs file exist
|
||||
if (info.FullName.Contains(prefsFileNameWithoutExtension) && !info.FullName.EndsWith(".plist"))
|
||||
{
|
||||
StartLoadingDelegate?.Invoke();
|
||||
return;
|
||||
}
|
||||
}
|
||||
StopLoadingDelegate?.Invoke();
|
||||
|
||||
cachedData = new string[0];
|
||||
|
||||
if (File.Exists(prefPath))
|
||||
{
|
||||
string fixedPrefsPath = prefPath.Replace("\"", "\\\"").Replace("'", "\\'").Replace("`", "\\`");
|
||||
var cmdStr = string.Format(@"-p '{0}'", fixedPrefsPath);
|
||||
|
||||
string stdOut = String.Empty;
|
||||
string errOut = String.Empty;
|
||||
|
||||
var process = new System.Diagnostics.Process();
|
||||
process.StartInfo.UseShellExecute = false;
|
||||
process.StartInfo.FileName = "plutil";
|
||||
process.StartInfo.Arguments = cmdStr;
|
||||
process.StartInfo.RedirectStandardOutput = true;
|
||||
process.StartInfo.RedirectStandardError = true;
|
||||
process.OutputDataReceived += new DataReceivedEventHandler((sender, evt) => { stdOut += evt.Data + "\n"; });
|
||||
process.ErrorDataReceived += new DataReceivedEventHandler((sender, evt) => { errOut += evt.Data + "\n"; });
|
||||
|
||||
process.Start();
|
||||
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
process.WaitForExit();
|
||||
|
||||
MatchCollection matches = Regex.Matches(stdOut, @"(?: "")(.*)(?:"" =>.*)");
|
||||
cachedData = matches.Cast<Match>().Select((e) => e.Groups[1].Value).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public override void StartMonitoring()
|
||||
{
|
||||
fileWatcher.EnableRaisingEvents = true;
|
||||
}
|
||||
|
||||
public override void StopMonitoring()
|
||||
{
|
||||
fileWatcher.EnableRaisingEvents = false;
|
||||
}
|
||||
|
||||
public override bool IsMonitoring()
|
||||
{
|
||||
return fileWatcher.EnableRaisingEvents;
|
||||
}
|
||||
|
||||
private void OnWatchedFileChanged(object source, FileSystemEventArgs e)
|
||||
{
|
||||
OnPrefEntryChanged();
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5a10351133ed64a488d6be3043f5402a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
339
Assets/Plugins/CodeAssist/Editor/Preferences/RegistryMonitor.cs
Normal file
339
Assets/Plugins/CodeAssist/Editor/Preferences/RegistryMonitor.cs
Normal file
|
@ -0,0 +1,339 @@
|
|||
|
||||
/*
|
||||
* Thanks to gr0ss for the inspiration.
|
||||
*
|
||||
* https://github.com/gr0ss/RegistryMonitor
|
||||
*
|
||||
* 11/08/2019
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Win32;
|
||||
|
||||
//namespace BgTools.PlayerPrefsEditor
|
||||
namespace Meryel.UnityCodeAssist.Editor.Preferences
|
||||
{
|
||||
public class RegistryMonitor : IDisposable
|
||||
{
|
||||
#region P/Invoke
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern int RegOpenKeyEx(IntPtr hKey, string subKey, uint options, int samDesired, out IntPtr phkResult);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
private static extern int RegNotifyChangeKeyValue(IntPtr hKey, bool bWatchSubtree, RegChangeNotifyFilter dwNotifyFilter, IntPtr hEvent, bool fAsynchronous);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
private static extern int RegCloseKey(IntPtr hKey);
|
||||
|
||||
private const int KEY_QUERY_VALUE = 0x0001;
|
||||
private const int KEY_NOTIFY = 0x0010;
|
||||
private const int STANDARD_RIGHTS_READ = 0x00020000;
|
||||
|
||||
private static readonly IntPtr HKEY_CLASSES_ROOT = new IntPtr(unchecked((int)0x80000000));
|
||||
private static readonly IntPtr HKEY_CURRENT_USER = new IntPtr(unchecked((int)0x80000001));
|
||||
private static readonly IntPtr HKEY_LOCAL_MACHINE = new IntPtr(unchecked((int)0x80000002));
|
||||
private static readonly IntPtr HKEY_USERS = new IntPtr(unchecked((int)0x80000003));
|
||||
private static readonly IntPtr HKEY_PERFORMANCE_DATA = new IntPtr(unchecked((int)0x80000004));
|
||||
private static readonly IntPtr HKEY_CURRENT_CONFIG = new IntPtr(unchecked((int)0x80000005));
|
||||
private static readonly IntPtr HKEY_DYN_DATA = new IntPtr(unchecked((int)0x80000006));
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event handling
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the specified registry key has changed.
|
||||
/// </summary>
|
||||
public event EventHandler RegChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Raises the <see cref="RegChanged"/> event.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <p>
|
||||
/// <b>OnRegChanged</b> is called when the specified registry key has changed.
|
||||
/// </p>
|
||||
/// <note type="inheritinfo">
|
||||
/// When overriding <see cref="OnRegChanged"/> in a derived class, be sure to call
|
||||
/// the base class's <see cref="OnRegChanged"/> method.
|
||||
/// </note>
|
||||
/// </remarks>
|
||||
protected virtual void OnRegChanged()
|
||||
{
|
||||
RegChanged?.Invoke(this, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the access to the registry fails.
|
||||
/// </summary>
|
||||
public event ErrorEventHandler Error;
|
||||
|
||||
/// <summary>
|
||||
/// Raises the <see cref="Error"/> event.
|
||||
/// </summary>
|
||||
/// <param name="e">The <see cref="Exception"/> which occured while watching the registry.</param>
|
||||
/// <remarks>
|
||||
/// <p>
|
||||
/// <b>OnError</b> is called when an exception occurs while watching the registry.
|
||||
/// </p>
|
||||
/// <note type="inheritinfo">
|
||||
/// When overriding <see cref="OnError"/> in a derived class, be sure to call
|
||||
/// the base class's <see cref="OnError"/> method.
|
||||
/// </note>
|
||||
/// </remarks>
|
||||
protected virtual void OnError(Exception e)
|
||||
{
|
||||
Error?.Invoke(this, new ErrorEventArgs(e));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private member variables
|
||||
|
||||
private IntPtr _registryHive;
|
||||
private string _registrySubName;
|
||||
private readonly object _threadLock = new object();
|
||||
private Thread _thread;
|
||||
private bool _disposed = false;
|
||||
private readonly ManualResetEvent _eventTerminate = new ManualResetEvent(false);
|
||||
|
||||
private RegChangeNotifyFilter _regFilter = RegChangeNotifyFilter.Key | RegChangeNotifyFilter.Attribute | RegChangeNotifyFilter.Value | RegChangeNotifyFilter.Security;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RegistryMonitor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="registryKey">The registry key to monitor.</param>
|
||||
public RegistryMonitor(RegistryKey registryKey)
|
||||
{
|
||||
InitRegistryKey(registryKey.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RegistryMonitor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="name">The name.</param>
|
||||
public RegistryMonitor(string name)
|
||||
{
|
||||
if (name == null || name.Length == 0)
|
||||
throw new ArgumentNullException("name");
|
||||
|
||||
InitRegistryKey(name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RegistryMonitor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="registryHive">The registry hive.</param>
|
||||
/// <param name="subKey">The sub key.</param>
|
||||
public RegistryMonitor(RegistryHive registryHive, string subKey)
|
||||
{
|
||||
InitRegistryKey(registryHive, subKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes this object.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
_disposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="RegChangeNotifyFilter">RegChangeNotifyFilter</see>.
|
||||
/// </summary>
|
||||
public RegChangeNotifyFilter RegChangeNotifyFilter
|
||||
{
|
||||
get { return _regFilter; }
|
||||
set
|
||||
{
|
||||
lock (_threadLock)
|
||||
{
|
||||
if (IsMonitoring)
|
||||
throw new InvalidOperationException("Monitoring thread is already running");
|
||||
|
||||
_regFilter = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Initialization
|
||||
|
||||
private void InitRegistryKey(RegistryHive hive, string name)
|
||||
{
|
||||
_registryHive = hive switch
|
||||
{
|
||||
RegistryHive.ClassesRoot => HKEY_CLASSES_ROOT,
|
||||
RegistryHive.CurrentConfig => HKEY_CURRENT_CONFIG,
|
||||
RegistryHive.CurrentUser => HKEY_CURRENT_USER,
|
||||
RegistryHive.DynData => HKEY_DYN_DATA,
|
||||
RegistryHive.LocalMachine => HKEY_LOCAL_MACHINE,
|
||||
RegistryHive.PerformanceData => HKEY_PERFORMANCE_DATA,
|
||||
RegistryHive.Users => HKEY_USERS,
|
||||
_ => throw new InvalidEnumArgumentException("hive", (int)hive, typeof(RegistryHive)),
|
||||
};
|
||||
_registrySubName = name;
|
||||
}
|
||||
|
||||
private void InitRegistryKey(string name)
|
||||
{
|
||||
string[] nameParts = name.Split('\\');
|
||||
|
||||
switch (nameParts[0])
|
||||
{
|
||||
case "HKEY_CLASSES_ROOT":
|
||||
case "HKCR":
|
||||
_registryHive = HKEY_CLASSES_ROOT;
|
||||
break;
|
||||
|
||||
case "HKEY_CURRENT_USER":
|
||||
case "HKCU":
|
||||
_registryHive = HKEY_CURRENT_USER;
|
||||
break;
|
||||
|
||||
case "HKEY_LOCAL_MACHINE":
|
||||
case "HKLM":
|
||||
_registryHive = HKEY_LOCAL_MACHINE;
|
||||
break;
|
||||
|
||||
case "HKEY_USERS":
|
||||
_registryHive = HKEY_USERS;
|
||||
break;
|
||||
|
||||
case "HKEY_CURRENT_CONFIG":
|
||||
_registryHive = HKEY_CURRENT_CONFIG;
|
||||
break;
|
||||
|
||||
default:
|
||||
_registryHive = IntPtr.Zero;
|
||||
throw new ArgumentException("The registry hive '" + nameParts[0] + "' is not supported", "value");
|
||||
}
|
||||
|
||||
_registrySubName = String.Join("\\", nameParts, 1, nameParts.Length - 1);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// <b>true</b> if this <see cref="RegistryMonitor"/> object is currently monitoring;
|
||||
/// otherwise, <b>false</b>.
|
||||
/// </summary>
|
||||
public bool IsMonitoring
|
||||
{
|
||||
get { return _thread != null; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start monitoring.
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(null, "This instance is already disposed");
|
||||
|
||||
lock (_threadLock)
|
||||
{
|
||||
if (!IsMonitoring)
|
||||
{
|
||||
_eventTerminate.Reset();
|
||||
_thread = new Thread(new ThreadStart(MonitorThread)) { IsBackground = true };
|
||||
_thread.Start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the monitoring thread.
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(null, "This instance is already disposed");
|
||||
|
||||
lock (_threadLock)
|
||||
{
|
||||
Thread thread = _thread;
|
||||
if (thread != null)
|
||||
{
|
||||
_eventTerminate.Set();
|
||||
thread.Join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void MonitorThread()
|
||||
{
|
||||
try
|
||||
{
|
||||
ThreadLoop();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
OnError(e);
|
||||
}
|
||||
_thread = null;
|
||||
}
|
||||
|
||||
private void ThreadLoop()
|
||||
{
|
||||
int result = RegOpenKeyEx(_registryHive, _registrySubName, 0, STANDARD_RIGHTS_READ | KEY_QUERY_VALUE | KEY_NOTIFY, out IntPtr registryKey);
|
||||
if (result != 0)
|
||||
{
|
||||
throw new Win32Exception(result);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
AutoResetEvent _eventNotify = new AutoResetEvent(false);
|
||||
WaitHandle[] waitHandles = new WaitHandle[] { _eventNotify, _eventTerminate };
|
||||
while (!_eventTerminate.WaitOne(0, true))
|
||||
{
|
||||
result = RegNotifyChangeKeyValue(registryKey, true, _regFilter, _eventNotify.SafeWaitHandle.DangerousGetHandle(), true);
|
||||
if (result != 0)
|
||||
{
|
||||
throw new Win32Exception(result);
|
||||
}
|
||||
|
||||
if (WaitHandle.WaitAny(waitHandles) == 0)
|
||||
{
|
||||
OnRegChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (registryKey != IntPtr.Zero)
|
||||
{
|
||||
RegCloseKey(registryKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filter for notifications reported by <see cref="RegistryMonitor"/>.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum RegChangeNotifyFilter
|
||||
{
|
||||
/// <summary>Notify the caller if a subkey is added or deleted.</summary>
|
||||
Key = 1,
|
||||
/// <summary>Notify the caller of changes to the attributes of the key,
|
||||
/// such as the security descriptor information.</summary>
|
||||
Attribute = 2,
|
||||
/// <summary>Notify the caller of changes to a value of the key. This can
|
||||
/// include adding or deleting a value, or changing an existing value.</summary>
|
||||
Value = 4,
|
||||
/// <summary>Notify the caller of changes to the security descriptor
|
||||
/// of the key.</summary>
|
||||
Security = 8,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 231ce51d1620ff843ba99ae307c881e2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
Loading…
Add table
Add a link
Reference in a new issue