using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; using System.Globalization; using UnityEditor; using UnityEngine; using UnityEditorInternal; #pragma warning disable IDE0005 using Serilog = Meryel.Serilog; #pragma warning restore IDE0005 #nullable enable namespace Meryel.UnityCodeAssist.Editor.Preferences { public class PreferenceMonitor { private static readonly Lazy _instanceOfPlayerPrefs = new Lazy(() => new PreferenceMonitor(true)); private static readonly Lazy _instanceOfEditorPrefs = new Lazy(() => new PreferenceMonitor(false)); public static PreferenceMonitor InstanceOfPlayerPrefs => _instanceOfPlayerPrefs.Value; public static PreferenceMonitor InstanceOfEditorPrefs => _instanceOfEditorPrefs.Value; //const int Limit = 128; const int Limit = 8192; /// /// PlayerPrefs or EditorPrefs /// readonly bool isPlayerPrefs; #region ErrorValues private readonly int ERROR_VALUE_INT = int.MinValue; private readonly string ERROR_VALUE_STR = ""; #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) MQTTnetInitializer.Publisher?.SendPlayerPrefs(keys, values, stringKeys, integerKeys, floatKeys); else MQTTnetInitializer.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 = @""; 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(); if (tmp.Length > 0) { prefEntryHolder = tmp[0]; } else { prefEntryHolder = ScriptableObject.CreateInstance(); } } 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(); var integerKeyList = new List(); var floatKeyList = new List(); var boolenKeyList = new List(); 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> 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 } }