using System; using System.IO; using UnityEngine; namespace UnityEditor.SettingsManagement { /// <summary> /// Represents a settings repository that stores data serialized to a JSON file. /// </summary> [Serializable] public class FileSettingsRepository : ISettingsRepository { /// <summary> /// Location of where the package settings are saved under the `ProjectSettings` directory. /// </summary> /// <returns>The folder where package settings are saved under the `ProjectSettings` directory.</returns> protected const string k_PackageSettingsDirectory = "ProjectSettings/Packages"; /// <summary> /// Location of where the package settings are saved under the `UserSettings` directory. /// </summary> /// <returns>Per-project user settings directory. </returns> protected const string k_UserProjectSettingsDirectory = "UserSettings/Packages"; const bool k_PrettyPrintJson = true; bool m_Initialized; string m_Path; [SerializeField] SettingsDictionary m_Dictionary = new SettingsDictionary(); Hash128 m_JsonHash; /// <summary> /// Initializes and returns an instance of the FileSettingsRepository /// with the serialized data location set to the specified path. /// </summary> /// <param name="path">The project-relative path to save settings to.</param> public FileSettingsRepository(string path) { m_Path = path; m_Initialized = false; AssemblyReloadEvents.beforeAssemblyReload += Save; EditorApplication.quitting += Save; } void Init() { if (m_Initialized) return; m_Initialized = true; if (TryLoadSavedJson(out string json)) { m_Dictionary = null; m_JsonHash = Hash128.Compute(json); EditorJsonUtility.FromJsonOverwrite(json, this); } if (m_Dictionary == null) m_Dictionary = new SettingsDictionary(); } /// <summary> /// Sets the <see cref="SettingsScope"/> this repository applies to. /// </summary> /// <remarks> /// By default, this repository implementation is relevant to the Project scope, but any implementations /// that override this method can choose to store this serialized data at a user scope instead. /// </remarks> /// <value> /// <see cref="SettingsScope.Project"/>, meaning that this setting applies to project settings (the default); /// or <see cref="SettingsScope.User"/>, meaning that this setting applies to user preferences. /// </value> /// <seealso cref="ISettingsRepository.scope"/> public virtual SettingsScope scope => SettingsScope.Project; /// <summary> /// Gets the full path to the file containing the serialized settings data. /// </summary> /// <value>The location stored for this repository.</value> /// <seealso cref="ISettingsRepository.path"/> public string path { get { return m_Path; } } /// <summary> /// Sets the name of file containing the serialized settings data. /// </summary> /// <value>The bare filename of the settings file.</value> public string name => Path.GetFileNameWithoutExtension(path); /// <summary> /// Loads the JSON file that stores the values for this settings object. /// </summary> /// <param name="json">The full path to the JSON file to load.</param> /// <returns>True if the file exists; false if it doesn't.</returns> public bool TryLoadSavedJson(out string json) { json = string.Empty; if (!File.Exists(path)) return false; json = File.ReadAllText(path); return true; } /// <summary> /// Saves all settings to their serialized state. /// </summary> /// <seealso cref="ISettingsRepository.Save"/> public void Save() { Init(); if (!File.Exists(path)) { var directory = Path.GetDirectoryName(path); if (string.IsNullOrEmpty(directory)) { Debug.LogError( $"Settings file {name} is saved to an invalid path: {path}. Settings will not be saved."); return; } Directory.CreateDirectory(directory); } string json = EditorJsonUtility.ToJson(this, k_PrettyPrintJson); // While unlikely, a hash collision is possible. Always test the actual saved contents before early exit. if (m_JsonHash == Hash128.Compute(json) && TryLoadSavedJson(out string existing) && existing.Equals(json)) return; #if UNITY_2019_3_OR_NEWER // AssetDatabase.IsOpenForEdit can be a very slow synchronous blocking call when Unity is connected to // Perforce Version Control. Especially if it's called repeatedly with every EditorGUI redraw. if (File.Exists(path) && !AssetDatabase.IsOpenForEdit(path)) { if (!AssetDatabase.MakeEditable(path)) { Debug.LogWarning($"Could not save package settings to {path}"); return; } } #endif try { m_JsonHash = Hash128.Compute(json); File.WriteAllText(path, json); } catch (UnauthorizedAccessException) { Debug.LogWarning($"Could not save package settings to {path}"); } } /// <summary> /// Sets a value for a settings entry with a matching key and type `T`. /// </summary> /// <param name="key">The key used to identify the settings entry.</param> /// <param name="value">The value to set. This value must be serializable.</param> /// <typeparam name="T">The type of value that this key points to.</typeparam> /// <seealso cref="ISettingsRepository.Set{T}"/> public void Set<T>(string key, T value) { Init(); m_Dictionary.Set<T>(key, value); } /// <summary> /// Returns a value with key of type `T`, or the fallback value if no matching key is found. /// </summary> /// <param name="key">The key used to identify the settings entry.</param> /// <param name="fallback">Specify the value of type `T` to return if the entry can't be found.</param> /// <typeparam name="T">The type of value that this key points to.</typeparam> /// <returns>The settings value if a match is found; otherwise, it returns the default (fallback) value.</returns> /// <seealso cref="ISettingsRepository.Get{T}"/> public T Get<T>(string key, T fallback = default(T)) { Init(); return m_Dictionary.Get<T>(key, fallback); } /// <summary> /// Determines whether this repository contains a settings entry that matches the specified key and is of type `T`. /// </summary> /// <param name="key">The key used to identify the settings entry.</param> /// <typeparam name="T">The type of value that this key points to.</typeparam> /// <returns>True if a match is found for both key and type; false if no entry is found.</returns> /// <seealso cref="ISettingsRepository.ContainsKey{T}"/> public bool ContainsKey<T>(string key) { Init(); return m_Dictionary.ContainsKey<T>(key); } /// <summary> /// Removes a key-value pair from the settings repository. This method identifies the settings entry to remove /// by matching the specified key for a value of type `T`. /// </summary> /// <param name="key">The key used to identify the settings entry.</param> /// <typeparam name="T">The type of value that this key points to.</typeparam> /// <seealso cref="ISettingsRepository.Remove{T}"/> public void Remove<T>(string key) { Init(); m_Dictionary.Remove<T>(key); } } }