This repository has been archived by the owner on Feb 28, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 22
/
ModConfiguration.cs
730 lines (648 loc) · 24.5 KB
/
ModConfiguration.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
using FrooxEngine;
using HarmonyLib;
using NeosModLoader.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace NeosModLoader
{
public interface IModConfigurationDefinition
{
/// <summary>
/// Mod that owns this configuration definition
/// </summary>
NeosModBase Owner { get; }
/// <summary>
/// Semantic version for this configuration definition. This is used to check if the defined and saved configs are compatible
/// </summary>
Version Version { get; }
/// <summary>
/// The set of coniguration keys defined in this configuration definition
/// </summary>
ISet<ModConfigurationKey> ConfigurationItemDefinitions { get; }
}
/// <summary>
/// Defines a mod configuration. This should be defined by a NeosMod using the NeosMod.DefineConfiguration() method.
/// </summary>
public class ModConfigurationDefinition : IModConfigurationDefinition
{
/// <summary>
/// Mod that owns this configuration definition
/// </summary>
public NeosModBase Owner { get; private set; }
/// <summary>
/// Semantic version for this configuration definition. This is used to check if the defined and saved configs are compatible
/// </summary>
public Version Version { get; private set; }
internal bool AutoSave;
// this is a ridiculous hack because HashSet.TryGetValue doesn't exist in .NET 4.6.2
private Dictionary<ModConfigurationKey, ModConfigurationKey> configurationItemDefinitionsSelfMap;
/// <summary>
/// The set of coniguration keys defined in this configuration definition
/// </summary>
public ISet<ModConfigurationKey> ConfigurationItemDefinitions
{
// clone the collection because I don't trust giving public API users shallow copies one bit
get => new HashSet<ModConfigurationKey>(configurationItemDefinitionsSelfMap.Keys);
}
internal bool TryGetDefiningKey(ModConfigurationKey key, out ModConfigurationKey? definingKey)
{
if (key.DefiningKey != null)
{
// we've already cached the defining key
definingKey = key.DefiningKey;
return true;
}
// first time we've seen this key instance: we need to hit the map
if (configurationItemDefinitionsSelfMap.TryGetValue(key, out definingKey))
{
// initialize the cache for this key
key.DefiningKey = definingKey;
return true;
}
else
{
// not a real key
definingKey = null;
return false;
}
}
internal ModConfigurationDefinition(NeosModBase owner, Version version, HashSet<ModConfigurationKey> configurationItemDefinitions, bool autoSave)
{
Owner = owner;
Version = version;
AutoSave = autoSave;
configurationItemDefinitionsSelfMap = new Dictionary<ModConfigurationKey, ModConfigurationKey>(configurationItemDefinitions.Count);
foreach (ModConfigurationKey key in configurationItemDefinitions)
{
key.DefiningKey = key; // early init this property for the defining key itself
configurationItemDefinitionsSelfMap.Add(key, key);
}
}
}
/// <summary>
/// The configuration for a mod. Each mod has zero or one configuration. The configuration object will never be reassigned once initialized.
/// </summary>
public class ModConfiguration : IModConfigurationDefinition
{
private readonly ModConfigurationDefinition Definition;
internal LoadedNeosMod LoadedNeosMod { get; private set; }
private static readonly string ConfigDirectory = Path.Combine(Directory.GetCurrentDirectory(), "nml_config");
private static readonly string VERSION_JSON_KEY = "version";
private static readonly string VALUES_JSON_KEY = "values";
/// <summary>
/// Mod that owns this configuration definition
/// </summary>
public NeosModBase Owner => Definition.Owner;
/// <summary>
/// Semantic version for this configuration definition. This is used to check if the defined and saved configs are compatible
/// </summary>
public Version Version => Definition.Version;
/// <summary>
/// The set of coniguration keys defined in this configuration definition
/// </summary>
public ISet<ModConfigurationKey> ConfigurationItemDefinitions => Definition.ConfigurationItemDefinitions;
private bool AutoSave => Definition.AutoSave;
/// <summary>
/// The delegate that is called for configuration change events.
/// </summary>
/// <param name="configurationChangedEvent">The event containing details about the configuration change</param>
public delegate void ConfigurationChangedEventHandler(ConfigurationChangedEvent configurationChangedEvent);
/// <summary>
/// Called if any config value for any mod changed.
/// </summary>
public static event ConfigurationChangedEventHandler? OnAnyConfigurationChanged;
/// <summary>
/// Called if one of the values in this mod's config changed.
/// </summary>
public event ConfigurationChangedEventHandler? OnThisConfigurationChanged;
// used to track how frequenly Save() is being called
private Stopwatch saveTimer = new Stopwatch();
// time that save must not be called for a save to actually go through
private int debounceMilliseconds = 3000;
// used to keep track of mods that spam Save():
// any mod that calls Save() for the ModConfiguration within debounceMilliseconds of the previous call to the same ModConfiguration
// will be put into Ultimate Punishment Mode, and ALL their Save() calls, regardless of ModConfiguration, will be debounced.
// The naughty list is global, while the actual debouncing is per-configuration.
private static ISet<string> naughtySavers = new HashSet<string>();
// used to keep track of the debouncers for this configuration.
private Dictionary<string, Action<bool>> saveActionForCallee = new();
private static readonly JsonSerializer jsonSerializer = CreateJsonSerializer();
private static JsonSerializer CreateJsonSerializer()
{
JsonSerializerSettings settings = new()
{
MaxDepth = 32,
ReferenceLoopHandling = ReferenceLoopHandling.Error,
DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate
};
List<JsonConverter> converters = new();
IList<JsonConverter> defaultConverters = settings.Converters;
if (defaultConverters != null && defaultConverters.Count() != 0)
{
Logger.DebugFuncInternal(() => $"Using {defaultConverters.Count()} default json converters");
converters.AddRange(defaultConverters);
}
converters.Add(new EnumConverter());
converters.Add(new NeosPrimitiveConverter());
settings.Converters = converters;
return JsonSerializer.Create(settings);
}
private ModConfiguration(LoadedNeosMod loadedNeosMod, ModConfigurationDefinition definition)
{
LoadedNeosMod = loadedNeosMod;
Definition = definition;
}
internal static void EnsureDirectoryExists()
{
Directory.CreateDirectory(ConfigDirectory);
}
private static string GetModConfigPath(LoadedNeosMod mod)
{
string filename = Path.ChangeExtension(Path.GetFileName(mod.ModAssembly.File), ".json");
return Path.Combine(ConfigDirectory, filename);
}
private static bool AreVersionsCompatible(Version serializedVersion, Version currentVersion)
{
if (serializedVersion.Major != currentVersion.Major)
{
// major version differences are hard incompatible
return false;
}
if (serializedVersion.Minor > currentVersion.Minor)
{
// if serialized config has a newer minor version than us
// in other words, someone downgraded the mod but not the config
// then we cannot load the config
return false;
}
// none of the checks failed!
return true;
}
/// <summary>
/// Check if a key is defined in this config
/// </summary>
/// <param name="key">the key to check</param>
/// <returns>true if the key is defined</returns>
public bool IsKeyDefined(ModConfigurationKey key)
{
// if a key has a non-null defining key it's guaranteed a real key. Lets check for that.
ModConfigurationKey? definingKey = key.DefiningKey;
if (definingKey != null)
{
return true;
}
// okay, the defining key was null, so lets try to get the defining key from the hashtable instead
if (Definition.TryGetDefiningKey(key, out definingKey))
{
// we might as well set this now that we have the real defining key
key.DefiningKey = definingKey;
return true;
}
// there was no definition
return false;
}
/// <summary>
/// Check if a key is the defining key
/// </summary>
/// <param name="key">the key to check</param>
/// <returns>true if the key is the defining key</returns>
internal bool IsKeyDefiningKey(ModConfigurationKey key)
{
// a key is the defining key if and only if its DefiningKey property references itself
return ReferenceEquals(key, key.DefiningKey); // this is safe because we'll throw a NRE if key is null
}
/// <summary>
/// Get a value, throwing an exception if the key is not found
/// </summary>
/// <param name="key">The key to find</param>
/// <returns>The found value</returns>
/// <exception cref="KeyNotFoundException">key does not exist in the collection</exception>
public object GetValue(ModConfigurationKey key)
{
if (TryGetValue(key, out object? value))
{
return value!;
}
else
{
throw new KeyNotFoundException($"{key.Name} not found in {LoadedNeosMod.NeosMod.Name} configuration");
}
}
/// <summary>
/// Get a value, throwing an exception if the key is not found
/// </summary>
/// <typeparam name="T">The value's type</typeparam>
/// <param name="key">The key to find</param>
/// <returns>The found value</returns>
/// <exception cref="KeyNotFoundException">key does not exist in the collection</exception>
public T? GetValue<T>(ModConfigurationKey<T> key)
{
if (TryGetValue(key, out T? value))
{
return value;
}
else
{
throw new KeyNotFoundException($"{key.Name} not found in {LoadedNeosMod.NeosMod.Name} configuration");
}
}
/// <summary>
/// Try to read a configuration value
/// </summary>
/// <param name="key">The key</param>
/// <param name="value">The value if we succeeded, or null if we failed.</param>
/// <returns>true if the value was read successfully</returns>
public bool TryGetValue(ModConfigurationKey key, out object? value)
{
if (!Definition.TryGetDefiningKey(key, out ModConfigurationKey? definingKey))
{
// not in definition
value = null;
return false;
}
if (definingKey!.TryGetValue(out object? valueObject))
{
value = valueObject;
return true;
}
else if (definingKey.TryComputeDefault(out value))
{
return true;
}
else
{
value = null;
return false;
}
}
/// <summary>
/// Try to read a typed configuration value
/// </summary>
/// <typeparam name="T">The value type</typeparam>
/// <param name="key">The key</param>
/// <param name="value">The value if we succeeded, or default(T) if we failed.</param>
/// <returns>true if the value was read successfully</returns>
public bool TryGetValue<T>(ModConfigurationKey<T> key, out T? value)
{
if (TryGetValue(key, out object? valueObject))
{
value = (T)valueObject!;
return true;
}
else
{
value = default;
return false;
}
}
/// <summary>
/// Set a configuration value
/// </summary>
/// <param name="key">The key</param>
/// <param name="value">The new value</param>
/// <param name="eventLabel">A custom label you may assign to this event</param>
public void Set(ModConfigurationKey key, object? value, string? eventLabel = null)
{
if (!Definition.TryGetDefiningKey(key, out ModConfigurationKey? definingKey))
{
throw new KeyNotFoundException($"{key.Name} is not defined in the config definition for {LoadedNeosMod.NeosMod.Name}");
}
if (value == null)
{
if (Util.CannotBeNull(definingKey!.ValueType()))
{
throw new ArgumentException($"null cannot be assigned to {definingKey.ValueType()}");
}
}
else if (!definingKey!.ValueType().IsAssignableFrom(value.GetType()))
{
throw new ArgumentException($"{value.GetType()} cannot be assigned to {definingKey.ValueType()}");
}
if (!definingKey!.Validate(value))
{
throw new ArgumentException($"\"{value}\" is not a valid value for \"{Owner.Name}{definingKey.Name}\"");
}
definingKey.Set(value);
FireConfigurationChangedEvent(definingKey, eventLabel);
}
/// <summary>
/// Set a typed configuration value
/// </summary>
/// <typeparam name="T">The value type</typeparam>
/// <param name="key">The key</param>
/// <param name="value">The new value</param>
/// <param name="eventLabel">A custom label you may assign to this event</param>
public void Set<T>(ModConfigurationKey<T> key, T value, string? eventLabel = null)
{
// the reason we don't fall back to untyped Set() here is so we can skip the type check
if (!Definition.TryGetDefiningKey(key, out ModConfigurationKey? definingKey))
{
throw new KeyNotFoundException($"{key.Name} is not defined in the config definition for {LoadedNeosMod.NeosMod.Name}");
}
if (!definingKey!.Validate(value))
{
throw new ArgumentException($"\"{value}\" is not a valid value for \"{Owner.Name}{definingKey.Name}\"");
}
definingKey.Set(value);
FireConfigurationChangedEvent(definingKey, eventLabel);
}
/// <summary>
/// Removes a configuration value, if set
/// </summary>
/// <param name="key"></param>
/// <returns>true if a value was successfully found and removed, false if there was no value to remove</returns>
public bool Unset(ModConfigurationKey key)
{
if (Definition.TryGetDefiningKey(key, out ModConfigurationKey? definingKey))
{
return definingKey!.Unset();
}
else
{
throw new KeyNotFoundException($"{key.Name} is not defined in the config definition for {LoadedNeosMod.NeosMod.Name}");
}
}
private bool AnyValuesSet()
{
return ConfigurationItemDefinitions
.Where(key => key.HasValue)
.Any();
}
internal static ModConfiguration? LoadConfigForMod(LoadedNeosMod mod)
{
ModConfigurationDefinition? definition = mod.NeosMod.BuildConfigurationDefinition();
if (definition == null)
{
// if there's no definition, then there's nothing for us to do here
return null;
}
string configFile = GetModConfigPath(mod);
try
{
using StreamReader file = File.OpenText(configFile);
using JsonTextReader reader = new(file);
JObject json = JObject.Load(reader);
Version version = new(json[VERSION_JSON_KEY]!.ToObject<string>(jsonSerializer));
if (!AreVersionsCompatible(version, definition.Version))
{
var handlingMode = mod.NeosMod.HandleIncompatibleConfigurationVersions(definition.Version, version);
switch (handlingMode)
{
case IncompatibleConfigurationHandlingOption.CLOBBER:
Logger.WarnInternal($"{mod.NeosMod.Name} saved config version is {version} which is incompatible with mod's definition version {definition.Version}. Clobbering old config and starting fresh.");
return new ModConfiguration(mod, definition);
case IncompatibleConfigurationHandlingOption.FORCE_LOAD:
// continue processing
break;
case IncompatibleConfigurationHandlingOption.ERROR: // fall through to default
default:
mod.AllowSavingConfiguration = false;
throw new ModConfigurationException($"{mod.NeosMod.Name} saved config version is {version} which is incompatible with mod's definition version {definition.Version}");
}
}
foreach (ModConfigurationKey key in definition.ConfigurationItemDefinitions)
{
string keyName = key.Name;
try
{
JToken? token = json[VALUES_JSON_KEY]?[keyName];
if (token != null)
{
object? value = token.ToObject(key.ValueType(), jsonSerializer);
key.Set(value);
}
}
catch (Exception e)
{
// I know not what exceptions the JSON library will throw, but they must be contained
mod.AllowSavingConfiguration = false;
throw new ModConfigurationException($"Error loading {key.ValueType()} config key \"{keyName}\" for {mod.NeosMod.Name}", e);
}
}
}
catch (FileNotFoundException)
{
// return early
return new ModConfiguration(mod, definition);
}
catch (Exception e)
{
// I know not what exceptions the JSON library will throw, but they must be contained
mod.AllowSavingConfiguration = false;
throw new ModConfigurationException($"Error loading config for {mod.NeosMod.Name}", e);
}
return new ModConfiguration(mod, definition);
}
/// <summary>
/// Persist this configuration to disk. This method is not called automatically. Default values are not automatically saved.
/// </summary>
public void Save() // this overload is needed for binary compatibility (REMOVE IN NEXT MAJOR VERSION)
{
Save(false, false);
}
/// <summary>
/// Persist this configuration to disk. This method is not called automatically.
/// </summary>
/// <param name="saveDefaultValues">If true, default values will also be persisted</param>
public void Save(bool saveDefaultValues = false) // this overload is needed for binary compatibility (REMOVE IN NEXT MAJOR VERSION)
{
Save(saveDefaultValues, false);
}
/// <summary>
/// Asynchronously persist this configuration to disk.
/// </summary>
/// <param name="saveDefaultValues">If true, default values will also be persisted</param>
/// <param name="immediate">Skip the debouncing and save immediately</param>
internal void Save(bool saveDefaultValues = false, bool immediate = false)
{
Thread thread = Thread.CurrentThread;
NeosMod? callee = Util.ExecutingMod(new(1));
Action<bool>? saveAction = null;
// get saved state for this callee
if (callee != null && naughtySavers.Contains(callee.Name) && !saveActionForCallee.TryGetValue(callee.Name, out saveAction))
{
// handle case where the callee was marked as naughty from a different ModConfiguration being spammed
saveAction = Util.Debounce<bool>(SaveInternal, debounceMilliseconds);
saveActionForCallee.Add(callee.Name, saveAction);
}
if (saveTimer.IsRunning)
{
float elapsedMillis = saveTimer.ElapsedMilliseconds;
saveTimer.Restart();
if (elapsedMillis < debounceMilliseconds)
{
Logger.WarnInternal($"ModConfiguration.Save({saveDefaultValues}) called for \"{LoadedNeosMod.NeosMod.Name}\" by \"{callee?.Name}\" from thread with id=\"{thread.ManagedThreadId}\", name=\"{thread.Name}\", bg=\"{thread.IsBackground}\", pool=\"{thread.IsThreadPoolThread}\". Last called {elapsedMillis / 1000f}s ago. This is very recent! Do not spam calls to ModConfiguration.Save()! All Save() calls by this mod are now subject to a {debounceMilliseconds}ms debouncing delay.");
if (saveAction == null && callee != null)
{
// congrats, you've switched into Ultimate Punishment Mode where now I don't trust you and your Save() calls get debounced
saveAction = Util.Debounce<bool>(SaveInternal, debounceMilliseconds);
saveActionForCallee.Add(callee.Name, saveAction);
naughtySavers.Add(callee.Name);
}
}
else
{
Logger.DebugFuncInternal(() => $"ModConfiguration.Save({saveDefaultValues}) called for \"{LoadedNeosMod.NeosMod.Name}\" by \"{callee?.Name}\" from thread with id=\"{thread.ManagedThreadId}\", name=\"{thread.Name}\", bg=\"{thread.IsBackground}\", pool=\"{thread.IsThreadPoolThread}\". Last called {elapsedMillis / 1000f}s ago.");
}
}
else
{
saveTimer.Start();
Logger.DebugFuncInternal(() => $"ModConfiguration.Save({saveDefaultValues}) called for \"{LoadedNeosMod.NeosMod.Name}\" by \"{callee?.Name}\" from thread with id=\"{thread.ManagedThreadId}\", name=\"{thread.Name}\", bg=\"{thread.IsBackground}\", pool=\"{thread.IsThreadPoolThread}\"");
}
// prevent saving if we've determined something is amiss with the configuration
if (!LoadedNeosMod.AllowSavingConfiguration)
{
Logger.WarnInternal($"ModConfiguration for {LoadedNeosMod.NeosMod.Name} will NOT be saved due to a safety check failing. This is probably due to you downgrading a mod.");
return;
}
if (immediate || saveAction == null)
{
// infrequent callers get to save immediately
Task.Run(() => SaveInternal(saveDefaultValues));
}
else
{
// bad callers get debounced
saveAction(saveDefaultValues);
}
}
/// <summary>
/// performs the actual, synchronous save
/// </summary>
/// <param name="saveDefaultValues">If true, default values will also be persisted</param>
private void SaveInternal(bool saveDefaultValues = false)
{
Stopwatch stopwatch = Stopwatch.StartNew();
JObject json = new()
{
[VERSION_JSON_KEY] = JToken.FromObject(Definition.Version.ToString(), jsonSerializer)
};
JObject valueMap = new();
foreach (ModConfigurationKey key in ConfigurationItemDefinitions)
{
if (key.TryGetValue(out object? value))
{
// I don't need to typecheck this as there's no way to sneak a bad type past my Set() API
valueMap[key.Name] = value == null ? null : JToken.FromObject(value, jsonSerializer);
}
else if (saveDefaultValues && key.TryComputeDefault(out object? defaultValue))
{
// I don't need to typecheck this as there's no way to sneak a bad type past my computeDefault API
// like and say defaultValue can't be null because the Json.Net
valueMap[key.Name] = defaultValue == null ? null : JToken.FromObject(defaultValue, jsonSerializer);
}
}
json[VALUES_JSON_KEY] = valueMap;
string configFile = GetModConfigPath(LoadedNeosMod);
using FileStream file = File.OpenWrite(configFile);
using StreamWriter streamWriter = new(file);
using JsonTextWriter jsonTextWriter = new(streamWriter);
json.WriteTo(jsonTextWriter);
// I actually cannot believe I have to truncate the file myself
file.SetLength(file.Position);
jsonTextWriter.Flush();
Logger.DebugFuncInternal(() => $"Saved ModConfiguration for \"{LoadedNeosMod.NeosMod.Name}\" in {stopwatch.ElapsedMilliseconds}ms");
}
private void FireConfigurationChangedEvent(ModConfigurationKey key, string? label)
{
try
{
OnAnyConfigurationChanged?.SafeInvoke(new ConfigurationChangedEvent(this, key, label));
}
catch (Exception e)
{
Logger.ErrorInternal($"An OnAnyConfigurationChanged event subscriber threw an exception:\n{e}");
}
try
{
OnThisConfigurationChanged?.SafeInvoke(new ConfigurationChangedEvent(this, key, label));
}
catch (Exception e)
{
Logger.ErrorInternal($"An OnThisConfigurationChanged event subscriber threw an exception:\n{e}");
}
}
internal static void RegisterShutdownHook(Harmony harmony)
{
try
{
MethodInfo shutdown = AccessTools.DeclaredMethod(typeof(Engine), nameof(Engine.Shutdown));
if (shutdown == null)
{
Logger.ErrorInternal("Could not find method Engine.Shutdown(). Will not be able to autosave configs on close!");
return;
}
MethodInfo patch = AccessTools.DeclaredMethod(typeof(ModConfiguration), nameof(ShutdownHook));
if (patch == null)
{
Logger.ErrorInternal("Could not find method ModConfiguration.ShutdownHook(). Will not be able to autosave configs on close!");
return;
}
harmony.Patch(shutdown, prefix: new HarmonyMethod(patch));
}
catch (Exception e)
{
Logger.ErrorInternal($"Unexpected exception applying shutdown hook!\n{e}");
}
}
private static void ShutdownHook()
{
int count = 0;
ModLoader.Mods()
.Select(mod => mod.GetConfiguration())
.Where(config => config != null)
.Where(config => config!.AutoSave)
.Where(config => config!.AnyValuesSet())
.Do(config =>
{
try
{
// synchronously save the config
config!.SaveInternal();
}
catch (Exception e)
{
Logger.ErrorInternal($"Error saving configuration for {config!.Owner.Name}:\n{e}");
}
count += 1;
});
Logger.MsgInternal($"Configs saved for {count} mods.");
}
}
public class ModConfigurationException : Exception
{
internal ModConfigurationException(string message) : base(message)
{
}
internal ModConfigurationException(string message, Exception innerException) : base(message, innerException)
{
}
}
/// <summary>
/// Defines handling of incompatible configuration versions
/// </summary>
public enum IncompatibleConfigurationHandlingOption
{
/// <summary>
/// Fail to read the config, and block saving over the config on disk.
/// </summary>
ERROR,
/// <summary>
/// Destroy the saved config and start over from scratch.
/// </summary>
CLOBBER,
/// <summary>
/// Ignore the version number and attempt to load the config from disk
/// </summary>
FORCE_LOAD,
}
}