A game framework written with osu! in mind.
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}