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