Terraria ModLoader  0.11.7.8
A mod to make and play Terraria mods
ConfigManager.cs
Go to the documentation of this file.
1 using Microsoft.Xna.Framework;
2 using Newtonsoft.Json;
3 using Newtonsoft.Json.Serialization;
4 using System;
5 using System.Collections;
6 using System.Collections.Generic;
7 using System.IO;
8 using System.Linq;
9 using System.Reflection;
10 using Terraria.ID;
11 using Terraria.ModLoader.Config.UI;
13 using Terraria.ModLoader.UI;
14 using Terraria.UI;
15 
16 namespace Terraria.ModLoader.Config
17 {
18  public static class ConfigManager
19  {
20  // These are THE active configs.
21  internal static readonly IDictionary<Mod, List<ModConfig>> Configs = new Dictionary<Mod, List<ModConfig>>();
22  // Configs should never violate reload required.
23  // Menu save should force reload
24 
25  // This copy of Configs stores instances present during load. Its only use in detecting if a reload is needed.
26  private static readonly IDictionary<Mod, List<ModConfig>> LoadTimeConfigs = new Dictionary<Mod, List<ModConfig>>();
27 
28  public static readonly JsonSerializerSettings serializerSettings = new JsonSerializerSettings
29  {
30  Formatting = Formatting.Indented,
31  DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate,
32  ObjectCreationHandling = ObjectCreationHandling.Replace,
33  NullValueHandling = NullValueHandling.Ignore,
34  Converters = converters,
35  ContractResolver = new ReferenceDefaultsPreservingResolver()
36  };
37 
38  internal static readonly JsonSerializerSettings serializerSettingsCompact = new JsonSerializerSettings
39  {
40  Formatting = Formatting.None,
41  DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate,
42  ObjectCreationHandling = ObjectCreationHandling.Replace,
43  NullValueHandling = NullValueHandling.Ignore,
44  Converters = converters,
45  ContractResolver = serializerSettings.ContractResolver
46  };
47 
48  private static readonly IList<JsonConverter> converters = new List<JsonConverter>() {
49  new Newtonsoft.Json.Converters.VersionConverter(),
50  //new ColorJsonConverter(),
51  };
52 
53  public static readonly string ModConfigPath = Path.Combine(Main.SavePath, "Mod Configs");
54  public static readonly string ServerModConfigPath = Path.Combine(Main.SavePath, "Mod Configs", "Server");
55 
56  internal static void Add(ModConfig config)
57  {
58  ConfigManager.Load(config);
59 
60  List<ModConfig> configList;
61  if (!Configs.TryGetValue(config.mod, out configList))
62  Configs.Add(config.mod, configList = new List<ModConfig>());
63  configList.Add(config);
64 
65  FieldInfo instance = config.GetType().GetField("Instance", BindingFlags.Static | BindingFlags.Public);
66  if (instance != null) {
67  instance.SetValue(null, config);
68  }
69  config.OnLoaded();
70  config.OnChanged();
71 
72  // Maintain a backup of LoadTime Configs.
73  List<ModConfig> configList2;
74  if (!LoadTimeConfigs.TryGetValue(config.mod, out configList2))
75  LoadTimeConfigs.Add(config.mod, configList2 = new List<ModConfig>());
76  configList2.Add(GeneratePopulatedClone(config));
77  }
78 
79  // This method for refreshing configs (ServerSide mostly) after events that could change configs: Multiplayer play.
80  internal static void LoadAll()
81  {
82  foreach (var activeConfigs in ConfigManager.Configs)
83  {
84  foreach (var activeConfig in activeConfigs.Value)
85  {
86  Load(activeConfig);
87  }
88  }
89  }
90 
91  internal static void OnChangedAll() {
92  foreach (var activeConfigs in ConfigManager.Configs) {
93  foreach (var activeConfig in activeConfigs.Value) {
94  activeConfig.OnChanged();
95  }
96  }
97  }
98 
99  internal static void Load(ModConfig config)
100  {
101  string filename = config.mod.Name + "_" + config.Name + ".json";
102  string path = Path.Combine(ModConfigPath, filename);
103  if (config.Mode == ConfigScope.ServerSide && ModNet.NetReloadActive) // #999: Main.netMode isn't 1 at this point due to #770 fix.
104  {
105  string netJson = ModNet.pendingConfigs.Single(x => x.modname == config.mod.Name && x.configname == config.Name).json;
106  JsonConvert.PopulateObject(netJson, config, serializerSettingsCompact);
107  return;
108  }
109  bool jsonFileExists = File.Exists(path);
110  string json = jsonFileExists ? File.ReadAllText(path) : "{}";
111  try {
112  JsonConvert.PopulateObject(json, config, serializerSettings);
113  }
114  catch (Exception e) when (jsonFileExists && (e is JsonReaderException || e is JsonSerializationException)) {
115  Logging.tML.Warn($"Then config file {config.Name} from the mod {config.mod.Name} located at {path} failed to load. The file was likely corrupted somehow, so the defaults will be loaded and the file deleted.");
116  File.Delete(path);
117  JsonConvert.PopulateObject("{}", config, serializerSettings);
118  }
119  }
120 
121  internal static void Reset(ModConfig pendingConfig)
122  {
123  string json = "{}";
124  JsonConvert.PopulateObject(json, pendingConfig, serializerSettings);
125  }
126 
127  internal static void Save(ModConfig config)
128  {
129  Directory.CreateDirectory(ModConfigPath);
130  string filename = config.mod.Name + "_" + config.Name + ".json";
131  string path = Path.Combine(ModConfigPath, filename);
132  string json = JsonConvert.SerializeObject(config, serializerSettings);
133  File.WriteAllText(path, json);
134  }
135 
136  internal static void Unload()
137  {
138  serializerSettings.ContractResolver = new ReferenceDefaultsPreservingResolver();
139  serializerSettingsCompact.ContractResolver = serializerSettings.ContractResolver;
140 
141  Configs.SelectMany(configList => configList.Value).ToList().ForEach(config => {
142  FieldInfo instance = config.GetType().GetField("Instance", BindingFlags.Static | BindingFlags.Public);
143  if (instance != null) {
144  instance.SetValue(null, null);
145  }
146  });
147  Configs.Clear();
148  LoadTimeConfigs.Clear();
149 
150  Interface.modConfig.Unload();
151  Interface.modConfigList.Unload();
152  }
153 
154  internal static bool AnyModNeedsReload() => ModLoader.Mods.Any(ModNeedsReload);
155 
156  internal static bool ModNeedsReload(Mod mod)
157  {
158  if (Configs.ContainsKey(mod))
159  {
160  var configs = Configs[mod];
161  var loadTimeConfigs = LoadTimeConfigs[mod];
162  for (int i = 0; i < configs.Count; i++)
163  {
164  if (loadTimeConfigs[i].NeedsReload(configs[i]))
165  {
166  return true;
167  }
168  }
169  }
170  return false;
171  }
172 
173  // GetConfig...returns the config instance
174  internal static ModConfig GetConfig(ModNet.NetConfig netConfig) => ConfigManager.GetConfig(ModLoader.GetMod(netConfig.modname), netConfig.configname);
175  internal static ModConfig GetConfig(Mod mod, string config)
176  {
177  List<ModConfig> configs;
178  if (Configs.TryGetValue(mod, out configs))
179  {
180  return configs.Single(x => x.Name == config);
181  }
182  throw new MissingResourceException("Missing config named " + config + " in mod " + mod.Name);
183  }
184 
185  internal static ModConfig GetLoadTimeConfig(Mod mod, string config) {
186  List<ModConfig> configs;
187  if (LoadTimeConfigs.TryGetValue(mod, out configs)) {
188  return configs.Single(x => x.Name == config);
189  }
190  throw new MissingResourceException("Missing config named " + config + " in mod " + mod.Name);
191  }
192 
193  internal static void HandleInGameChangeConfigPacket(BinaryReader reader, int whoAmI)
194  {
195  if (Main.netMode == NetmodeID.MultiplayerClient)
196  {
197  bool success = reader.ReadBoolean();
198  string message = reader.ReadString();
199  if (success)
200  {
201  string modname = reader.ReadString();
202  string configname = reader.ReadString();
203  string json = reader.ReadString();
204  ModConfig activeConfig = GetConfig(ModLoader.GetMod(modname), configname);
205  JsonConvert.PopulateObject(json, activeConfig, serializerSettingsCompact);
206  activeConfig.OnChanged();
207 
208  Main.NewText($"Shared config changed: Message: {message}, Mod: {modname}, Config: {configname}");
209  if (Main.InGameUI.CurrentState == Interface.modConfig)
210  {
211  Main.InGameUI.SetState(Interface.modConfig);
212  Interface.modConfig.SetMessage("Server response: " + message, Color.Green);
213  }
214  }
215  else
216  {
217  // rejection only sent back to requester.
218  // Update UI with message
219 
220  Main.NewText("Changes Rejected: " + message);
221  if (Main.InGameUI.CurrentState == Interface.modConfig)
222  {
223  Interface.modConfig.SetMessage("Server rejected changes: " + message, Color.Red);
224  //Main.InGameUI.SetState(Interface.modConfig);
225  }
226 
227  }
228  }
229  else
230  {
231  // no bool in request.
232  string modname = reader.ReadString();
233  string configname = reader.ReadString();
234  string json = reader.ReadString();
235  ModConfig config = GetConfig(ModLoader.GetMod(modname), configname);
236  ModConfig loadTimeConfig = GetLoadTimeConfig(ModLoader.GetMod(modname), configname);
237  ModConfig pendingConfig = GeneratePopulatedClone(config);
238  JsonConvert.PopulateObject(json, pendingConfig, serializerSettingsCompact);
239  bool success = true;
240  string message = "Accepted";
241  if (loadTimeConfig.NeedsReload(pendingConfig))
242  {
243  success = false;
244  message = "Can't save because changes would require a reload.";
245  }
246  if (!config.AcceptClientChanges(pendingConfig, whoAmI, ref message))
247  {
248  success = false;
249  }
250  if (success)
251  {
252  // Apply to Servers Config
253  ConfigManager.Save(pendingConfig);
254  JsonConvert.PopulateObject(json, config, ConfigManager.serializerSettingsCompact);
255  config.OnChanged();
256  // Send new config to all clients
257  var p = new ModPacket(MessageID.InGameChangeConfig);
258  p.Write(true);
259  p.Write(message);
260  p.Write(modname);
261  p.Write(configname);
262  p.Write(json);
263  p.Send();
264  }
265  else
266  {
267  // Send rejections message back to client who requested change
268  var p = new ModPacket(MessageID.InGameChangeConfig);
269  p.Write(false);
270  p.Write(message);
271  p.Send(whoAmI);
272  }
273 
274  }
275  return;
276  }
277 
278  public static IEnumerable<PropertyFieldWrapper> GetFieldsAndProperties(object item)
279  {
280  PropertyInfo[] properties = item.GetType().GetProperties(
281  //BindingFlags.DeclaredOnly |
282  BindingFlags.Public |
283  BindingFlags.Instance);
284 
285  FieldInfo[] fields = item.GetType().GetFields(
286  //BindingFlags.DeclaredOnly |
287  BindingFlags.Public |
288  BindingFlags.Instance);
289 
290  return fields.Select(x => new PropertyFieldWrapper(x)).Concat(properties.Select(x => new PropertyFieldWrapper(x)));
291  }
292 
293  public static ModConfig GeneratePopulatedClone(ModConfig original) {
294  string json = JsonConvert.SerializeObject(original, ConfigManager.serializerSettings);
295  ModConfig properClone = original.Clone();
296  JsonConvert.PopulateObject(json, properClone, ConfigManager.serializerSettings);
297  return properClone;
298  }
299 
300  public static object AlternateCreateInstance(Type type)
301  {
302  if (type == typeof(string))
303  return "";
304  return Activator.CreateInstance(type);
305  }
306 
307  // Gets an Attribute from a property or field. Attribute defined on Member has highest priority,
308  // followed by the containing data structure, followed by attribute defined on the Class.
309  public static T GetCustomAttribute<T>(PropertyFieldWrapper memberInfo, object item, object array) where T : Attribute {
310  // Class
311  T attribute = (T)Attribute.GetCustomAttribute(memberInfo.Type, typeof(T), true);
312  if (array != null)
313  {
314  // item null?
315  // attribute = (T)Attribute.GetCustomAttribute(item.GetType(), typeof(T), true) ?? attribute; // TODO: is this wrong?
316  }
317  // Member
318  attribute = (T)Attribute.GetCustomAttribute(memberInfo.MemberInfo, typeof(T)) ?? attribute;
319  return attribute;
320  // TODO: allow for inheriting from parent's parent? (could get attribute from parent ConfigElement)
321  }
322 
323  public static T GetCustomAttribute<T>(PropertyFieldWrapper memberInfo, Type type) where T : Attribute {
324  // Class
325  T attribute = (T)Attribute.GetCustomAttribute(memberInfo.Type, typeof(T), true);
326 
327  attribute = (T)Attribute.GetCustomAttribute(type, typeof(T), true) ?? attribute;
328 
329  // Member
330  attribute = (T)Attribute.GetCustomAttribute(memberInfo.MemberInfo, typeof(T)) ?? attribute;
331  return attribute;
332  }
333 
334  public static Tuple<UIElement, UIElement> WrapIt(UIElement parent, ref int top, PropertyFieldWrapper memberInfo, object item, int order, object list = null, Type arrayType = null, int index = -1)
335  {
336  // public api for modders.
337  return UIModConfig.WrapIt(parent, ref top, memberInfo, item, order, list, arrayType, index);
338  }
339 
340  public static void SetPendingChanges(bool changes = true) {
341  // public api for modders.
342  Interface.modConfig.SetPendingChanges(changes);
343  }
344 
345  // TODO: better home?
346  public static bool ObjectEquals(object a, object b) {
347  if (ReferenceEquals(a, b)) return true;
348  if (a == null || b == null) return false;
349  if (a is IEnumerable && b is IEnumerable && !(a is string) && !(b is string))
350  return EnumerableEquals((IEnumerable)a, (IEnumerable)b);
351  return a.Equals(b);
352  }
353 
354  public static bool EnumerableEquals(IEnumerable a, IEnumerable b) {
355  IEnumerator enumeratorA = a.GetEnumerator();
356  IEnumerator enumeratorB = b.GetEnumerator();
357  bool hasNextA = enumeratorA.MoveNext();
358  bool hasNextB = enumeratorB.MoveNext();
359  while (hasNextA && hasNextB) {
360  if (!ObjectEquals(enumeratorA.Current, enumeratorB.Current)) return false;
361  hasNextA = enumeratorA.MoveNext();
362  hasNextB = enumeratorB.MoveNext();
363  }
364  return !hasNextA && !hasNextB;
365  }
366  }
367 
374  {
375  // This approach largely based on https://stackoverflow.com/a/52684798.
376  public abstract class ValueProviderDecorator : IValueProvider
377  {
379 
380  public ValueProviderDecorator(IValueProvider baseProvider) {
381  if (baseProvider == null)
382  throw new ArgumentNullException();
383  this.baseProvider = baseProvider;
384  }
385 
386  public virtual object GetValue(object target) { return baseProvider.GetValue(target); }
387 
388  public virtual void SetValue(object target, object value) { baseProvider.SetValue(target, value); }
389  }
391  {
392  //readonly object defaultValue;
393  readonly Func<object> defaultValueGenerator;
394 
395  //public NullToDefaultValueProvider(IValueProvider baseProvider, object defaultValue) : base(baseProvider) {
396  // this.defaultValue = defaultValue;
397  //}
398 
399  public NullToDefaultValueProvider(IValueProvider baseProvider, Func<object> defaultValueGenerator) : base(baseProvider) {
400  this.defaultValueGenerator = defaultValueGenerator;
401  }
402 
403  public override void SetValue(object target, object value) {
404  base.SetValue(target, value ?? defaultValueGenerator.Invoke());
405  //base.SetValue(target, value ?? defaultValue);
406  }
407  }
408 
409  protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization) {
410  IList<JsonProperty> props = base.CreateProperties(type, memberSerialization);
411  if (type.IsClass) {
412  ConstructorInfo ctor = type.GetConstructor(Type.EmptyTypes);
413  if (ctor != null) {
414  object referenceInstance = ctor.Invoke(null);
415  foreach (JsonProperty prop in props.Where(p => p.Readable)) {
416  if (!prop.PropertyType.IsValueType) {
417  var a = type.GetMember(prop.PropertyName);
418  if (prop.Writable) {
419  if (prop.PropertyType.GetConstructor(Type.EmptyTypes) != null) {
420  // defaultValueCreator will create new instance, then get the value from a field in that object. Prevents deserialized nulls from sharing with other instances.
421  Func<object> defaultValueCreator = () => prop.ValueProvider.GetValue(ctor.Invoke(null));
422  prop.ValueProvider = new NullToDefaultValueProvider(prop.ValueProvider, defaultValueCreator);
423  }
424  else if (prop.PropertyType.IsArray) {
425  Func<object> defaultValueCreator = () => (prop.ValueProvider.GetValue(referenceInstance) as Array).Clone();
426  prop.ValueProvider = new NullToDefaultValueProvider(prop.ValueProvider, defaultValueCreator);
427  }
428  }
429  if (prop.ShouldSerialize == null)
430  prop.ShouldSerialize = instance =>
431  {
432  object val = prop.ValueProvider.GetValue(instance);
433  object refVal = prop.ValueProvider.GetValue(referenceInstance);
434  return !ConfigManager.ObjectEquals(val, refVal);
435  };
436  }
437  }
438  }
439  }
440  return props;
441  }
442  }
443 }
static bool ObjectEquals(object a, object b)
override IList< JsonProperty > CreateProperties(Type type, MemberSerialization memberSerialization)
virtual void OnLoaded()
This method is called when the ModConfig has been loaded for the first time. This happens before regu...
Definition: ModConfig.cs:36
virtual ModConfig Clone()
tModLoader will call Clone on ModConfig to facilitate proper implementation of the ModConfig user int...
virtual bool AcceptClientChanges(ModConfig pendingConfig, int whoAmI, ref string message)
Called on the Server for ServerSide configs to determine if the changes asked for by the Client will ...
Definition: ModConfig.cs:54
This serves as the central class which loads mods. It contains many static fields and methods related...
Definition: ModLoader.cs:28
ModConfig provides a way for mods to be configurable. ModConfigs can either be Client specific or Ser...
Definition: ModConfig.cs:18
This class inherits from BinaryWriter. This means that you can use all of its writing functions to se...
Definition: ModPacket.cs:13
static Tuple< UIElement, UIElement > WrapIt(UIElement parent, ref int top, PropertyFieldWrapper memberInfo, object item, int order, object list=null, Type arrayType=null, int index=-1)
static bool EnumerableEquals(IEnumerable a, IEnumerable b)
static IEnumerable< PropertyFieldWrapper > GetFieldsAndProperties(object item)
abstract ConfigScope Mode
Definition: ModConfig.cs:27
virtual void OnChanged()
This hook is called anytime new config values have been set and are ready to take effect...
Definition: ModConfig.cs:43
static ModConfig GeneratePopulatedClone(ModConfig original)
Custom ContractResolver for facilitating refernce type defaults. The ShouldSerialize code enables unc...
virtual string Name
Stores the name of the mod. This name serves as the mod&#39;s identification, and also helps with saving ...
Definition: Mod.cs:42
static void SetPendingChanges(bool changes=true)
Mod is an abstract class that you will override. It serves as a central place from which the mod&#39;s co...
Definition: Mod.cs:24
static object AlternateCreateInstance(Type type)
static readonly JsonSerializerSettings serializerSettings
static Mod GetMod(string name)
Gets the instance of the Mod with the specified name.
Definition: ModLoader.cs:90
ConfigScope
Each ModConfig class has a different scope. Failure to use the correct mode will lead to bugs...
Definition: ModConfig.cs:91
NullToDefaultValueProvider(IValueProvider baseProvider, Func< object > defaultValueGenerator)