A game framework written with osu! in mind.
at master 14 kB view raw
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2// See the LICENCE file in the repository root for full licence text. 3 4using System; 5using System.Collections.Generic; 6using System.Drawing; 7using System.Threading; 8using System.Threading.Tasks; 9using osu.Framework.Bindables; 10using osu.Framework.Configuration.Tracking; 11 12namespace osu.Framework.Configuration 13{ 14 public abstract class ConfigManager<TLookup> : ConfigManager, ITrackableConfigManager 15 where TLookup : struct, Enum 16 { 17 /// <summary> 18 /// Whether user specified configuration elements should be set even though a default was never specified. 19 /// </summary> 20 protected virtual bool AddMissingEntries => true; 21 22 private readonly IDictionary<TLookup, object> defaultOverrides; 23 24 protected readonly Dictionary<TLookup, IBindable> ConfigStore = new Dictionary<TLookup, IBindable>(); 25 26 /// <summary> 27 /// Initialise a new <see cref="ConfigManager{TLookup}"/> 28 /// </summary> 29 /// <param name="defaultOverrides">Dictionary of overrides which should take precedence over defaults specified by the <see cref="ConfigManager{TLookup}"/> implementation.</param> 30 protected ConfigManager(IDictionary<TLookup, object> defaultOverrides = null) 31 { 32 this.defaultOverrides = defaultOverrides; 33 } 34 35 /// <summary> 36 /// Set all required default values via Set() calls. 37 /// Note that defaults set here may be overridden by <see cref="defaultOverrides"/> provided in the constructor. 38 /// </summary> 39 protected virtual void InitialiseDefaults() 40 { 41 } 42 43 /// <summary> 44 /// Sets a configuration's value. 45 /// </summary> 46 /// <param name="lookup">The lookup key.</param> 47 /// <param name="value">The value. Will also become the default value if one has not already been initialised.</param> 48 /// <typeparam name="TValue">The type of value.</typeparam> 49 public void SetValue<TValue>(TLookup lookup, TValue value) 50 { 51 var bindable = GetOriginalBindable<TValue>(lookup); 52 53 if (bindable == null) 54 SetDefault(lookup, value); 55 else 56 bindable.Value = value; 57 } 58 59 [Obsolete("In derived classes, use SetDefault() to set the default value. In public contexts, use SetValue() to set the value.")] // Can be removed 20210915 60 public BindableDouble Set(TLookup lookup, double value, double? min = null, double? max = null, double? precision = null) => SetDefault(lookup, value, min, max, precision); 61 62 [Obsolete("In derived classes, use SetDefault() to set the default value. In public contexts, use SetValue() to set the value.")] // Can be removed 20210915 63 public BindableFloat Set(TLookup lookup, float value, float? min = null, float? max = null, float? precision = null) => SetDefault(lookup, value, min, max, precision); 64 65 [Obsolete("In derived classes, use SetDefault() to set the default value. In public contexts, use SetValue() to set the value.")] // Can be removed 20210915 66 public BindableInt Set(TLookup lookup, int value, int? min = null, int? max = null) => SetDefault(lookup, value, min, max); 67 68 [Obsolete("In derived classes, use SetDefault() to set the default value. In public contexts, use SetValue() to set the value.")] // Can be removed 20210915 69 public BindableBool Set(TLookup lookup, bool value) => SetDefault(lookup, value); 70 71 [Obsolete("In derived classes, use SetDefault() to set the default value. In public contexts, use SetValue() to set the value.")] // Can be removed 20210915 72 public BindableSize Set(TLookup lookup, Size value, Size? min = null, Size? max = null) => SetDefault(lookup, value, min, max); 73 74 [Obsolete("In derived classes, use SetDefault() to set the default value. In public contexts, use SetValue() to set the value.")] // Can be removed 20210915 75 public Bindable<TValue> Set<TValue>(TLookup lookup, TValue value) => SetDefault(lookup, value); 76 77 /// <summary> 78 /// Sets a configuration's default value. 79 /// </summary> 80 /// <param name="lookup">The lookup key.</param> 81 /// <param name="value">The default value.</param> 82 /// <param name="min">The minimum value.</param> 83 /// <param name="max">The maximum value.</param> 84 /// <param name="precision">The value precision.</param> 85 /// <returns>The original bindable (not a bound copy).</returns> 86 protected BindableDouble SetDefault(TLookup lookup, double value, double? min = null, double? max = null, double? precision = null) 87 { 88 value = getDefault(lookup, value); 89 90 if (!(GetOriginalBindable<double>(lookup) is BindableDouble bindable)) 91 { 92 bindable = new BindableDouble(value); 93 AddBindable(lookup, bindable); 94 } 95 else 96 { 97 bindable.Value = value; 98 } 99 100 bindable.Default = value; 101 if (min.HasValue) bindable.MinValue = min.Value; 102 if (max.HasValue) bindable.MaxValue = max.Value; 103 if (precision.HasValue) bindable.Precision = precision.Value; 104 105 return bindable; 106 } 107 108 /// <summary> 109 /// Sets a configuration's default value. 110 /// </summary> 111 /// <param name="lookup">The lookup key.</param> 112 /// <param name="value">The default value.</param> 113 /// <param name="min">The minimum value.</param> 114 /// <param name="max">The maximum value.</param> 115 /// <param name="precision">The value precision.</param> 116 /// <returns>The original bindable (not a bound copy).</returns> 117 protected BindableFloat SetDefault(TLookup lookup, float value, float? min = null, float? max = null, float? precision = null) 118 { 119 value = getDefault(lookup, value); 120 121 if (!(GetOriginalBindable<float>(lookup) is BindableFloat bindable)) 122 { 123 bindable = new BindableFloat(value); 124 AddBindable(lookup, bindable); 125 } 126 else 127 { 128 bindable.Value = value; 129 } 130 131 bindable.Default = value; 132 if (min.HasValue) bindable.MinValue = min.Value; 133 if (max.HasValue) bindable.MaxValue = max.Value; 134 if (precision.HasValue) bindable.Precision = precision.Value; 135 136 return bindable; 137 } 138 139 /// <summary> 140 /// Sets a configuration's default value. 141 /// </summary> 142 /// <param name="lookup">The lookup key.</param> 143 /// <param name="value">The default value.</param> 144 /// <param name="min">The minimum value.</param> 145 /// <param name="max">The maximum value.</param> 146 /// <returns>The original bindable (not a bound copy).</returns> 147 protected BindableInt SetDefault(TLookup lookup, int value, int? min = null, int? max = null) 148 { 149 value = getDefault(lookup, value); 150 151 if (!(GetOriginalBindable<int>(lookup) is BindableInt bindable)) 152 { 153 bindable = new BindableInt(value); 154 AddBindable(lookup, bindable); 155 } 156 else 157 { 158 bindable.Value = value; 159 } 160 161 bindable.Default = value; 162 if (min.HasValue) bindable.MinValue = min.Value; 163 if (max.HasValue) bindable.MaxValue = max.Value; 164 165 return bindable; 166 } 167 168 /// <summary> 169 /// Sets a configuration's default value. 170 /// </summary> 171 /// <param name="lookup">The lookup key.</param> 172 /// <param name="value">The default value.</param> 173 /// <returns>The original bindable (not a bound copy).</returns> 174 protected BindableBool SetDefault(TLookup lookup, bool value) 175 { 176 value = getDefault(lookup, value); 177 178 if (!(GetOriginalBindable<bool>(lookup) is BindableBool bindable)) 179 { 180 bindable = new BindableBool(value); 181 AddBindable(lookup, bindable); 182 } 183 else 184 { 185 bindable.Value = value; 186 } 187 188 bindable.Default = value; 189 190 return bindable; 191 } 192 193 /// <summary> 194 /// Sets a configuration's default value. 195 /// </summary> 196 /// <param name="lookup">The lookup key.</param> 197 /// <param name="value">The default value.</param> 198 /// <param name="min">The minimum value.</param> 199 /// <param name="max">The maximum value.</param> 200 /// <returns>The original bindable (not a bound copy).</returns> 201 protected BindableSize SetDefault(TLookup lookup, Size value, Size? min = null, Size? max = null) 202 { 203 value = getDefault(lookup, value); 204 205 if (!(GetOriginalBindable<Size>(lookup) is BindableSize bindable)) 206 { 207 bindable = new BindableSize(value); 208 AddBindable(lookup, bindable); 209 } 210 else 211 { 212 bindable.Value = value; 213 } 214 215 bindable.Default = value; 216 if (min.HasValue) bindable.MinValue = min.Value; 217 if (max.HasValue) bindable.MaxValue = max.Value; 218 219 return bindable; 220 } 221 222 /// <summary> 223 /// Sets a configuration's default value. 224 /// </summary> 225 /// <param name="lookup">The lookup key.</param> 226 /// <param name="value">The default value.</param> 227 /// <returns>The original bindable (not a bound copy).</returns> 228 protected Bindable<TValue> SetDefault<TValue>(TLookup lookup, TValue value) 229 { 230 value = getDefault(lookup, value); 231 232 Bindable<TValue> bindable = GetOriginalBindable<TValue>(lookup); 233 234 if (bindable == null) 235 bindable = set(lookup, value); 236 else 237 bindable.Value = value; 238 239 bindable.Default = value; 240 241 return bindable; 242 } 243 244 protected virtual void AddBindable<TBindable>(TLookup lookup, Bindable<TBindable> bindable) 245 { 246 ConfigStore[lookup] = bindable; 247 bindable.ValueChanged += _ => QueueBackgroundSave(); 248 } 249 250 private TValue getDefault<TValue>(TLookup lookup, TValue fallback) 251 { 252 if (defaultOverrides != null && defaultOverrides.TryGetValue(lookup, out object found)) 253 return (TValue)found; 254 255 return fallback; 256 } 257 258 private Bindable<TValue> set<TValue>(TLookup lookup, TValue value) 259 { 260 Bindable<TValue> bindable = new Bindable<TValue>(value); 261 AddBindable(lookup, bindable); 262 return bindable; 263 } 264 265 public TValue Get<TValue>(TLookup lookup) => GetOriginalBindable<TValue>(lookup).Value; 266 267 protected Bindable<TValue> GetOriginalBindable<TValue>(TLookup lookup) 268 { 269 if (ConfigStore.TryGetValue(lookup, out IBindable obj)) 270 { 271 if (!(obj is Bindable<TValue>)) 272 throw new InvalidCastException($"Cannot convert bindable of type {obj.GetType()} retrieved from {nameof(ConfigManager<TLookup>)} to {typeof(Bindable<TValue>)}."); 273 274 return (Bindable<TValue>)obj; 275 } 276 277 return null; 278 } 279 280 /// <summary> 281 /// Retrieve a bindable. This will be a new instance weakly bound to the configuration backing. 282 /// If you are further binding to events of a bindable retrieved using this method, ensure to hold 283 /// a local reference. 284 /// </summary> 285 /// <returns>A weakly bound copy of the specified bindable.</returns> 286 public Bindable<TValue> GetBindable<TValue>(TLookup lookup) => GetOriginalBindable<TValue>(lookup)?.GetBoundCopy(); 287 288 /// <summary> 289 /// Binds a local bindable with a configuration-backed bindable. 290 /// </summary> 291 public void BindWith<TValue>(TLookup lookup, Bindable<TValue> bindable) => bindable.BindTo(GetOriginalBindable<TValue>(lookup)); 292 293 public virtual TrackedSettings CreateTrackedSettings() => null; 294 295 public void LoadInto(TrackedSettings settings) => settings.LoadFrom(this); 296 297 public class TrackedSetting<TValue> : Tracking.TrackedSetting<TValue> 298 { 299 /// <summary> 300 /// Constructs a new <see cref="TrackedSetting{TValue}"/>. 301 /// </summary> 302 /// <param name="setting">The config setting to be tracked.</param> 303 /// <param name="generateDescription">A function that generates the description for the setting, invoked every time the value changes.</param> 304 public TrackedSetting(TLookup setting, Func<TValue, SettingDescription> generateDescription) 305 : base(setting, generateDescription) 306 { 307 } 308 } 309 } 310 311 public abstract class ConfigManager : IDisposable 312 { 313 private bool hasLoaded; 314 315 public void Load() 316 { 317 PerformLoad(); 318 hasLoaded = true; 319 } 320 321 private int lastSave; 322 323 /// <summary> 324 /// Queue a background save operation with debounce. 325 /// </summary> 326 protected void QueueBackgroundSave() 327 { 328 var current = Interlocked.Increment(ref lastSave); 329 330 Task.Delay(100).ContinueWith(task => 331 { 332 if (current == lastSave) Save(); 333 }); 334 } 335 336 private readonly object saveLock = new object(); 337 338 public bool Save() 339 { 340 if (!hasLoaded) return false; 341 342 lock (saveLock) 343 { 344 Interlocked.Increment(ref lastSave); 345 return PerformSave(); 346 } 347 } 348 349 protected abstract void PerformLoad(); 350 351 protected abstract bool PerformSave(); 352 353 #region IDisposable Support 354 355 private bool isDisposed; 356 357 protected virtual void Dispose(bool disposing) 358 { 359 if (!isDisposed) 360 { 361 Save(); 362 isDisposed = true; 363 } 364 } 365 366 public void Dispose() 367 { 368 Dispose(true); 369 GC.SuppressFinalize(this); 370 } 371 372 #endregion 373 } 374}