Terraria ModLoader  0.11.5
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.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 && Main.netMode == 1)
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  string json = File.Exists(path) ? File.ReadAllText(path) : "{}";
110  JsonConvert.PopulateObject(json, config, serializerSettings);
111  }
112 
113  internal static void Reset(ModConfig pendingConfig)
114  {
115  string json = "{}";
116  JsonConvert.PopulateObject(json, pendingConfig, serializerSettings);
117  }
118 
119  internal static void Save(ModConfig config)
120  {
121  Directory.CreateDirectory(ModConfigPath);
122  string filename = config.mod.Name + "_" + config.Name + ".json";
123  string path = Path.Combine(ModConfigPath, filename);
124  string json = JsonConvert.SerializeObject(config, serializerSettings);
125  File.WriteAllText(path, json);
126  }
127 
128  internal static void Unload()
129  {
130  serializerSettings.ContractResolver = new ReferenceDefaultsPreservingResolver();
131  serializerSettingsCompact.ContractResolver = serializerSettings.ContractResolver;
132 
133  Configs.SelectMany(configList => configList.Value).ToList().ForEach(config => {
134  FieldInfo instance = config.GetType().GetField("Instance", BindingFlags.Static | BindingFlags.Public);
135  if (instance != null) {
136  instance.SetValue(null, null);
137  }
138  });
139  Configs.Clear();
140  LoadTimeConfigs.Clear();
141 
142  Interface.modConfig.Unload();
143  Interface.modConfigList.Unload();
144  }
145 
146  internal static bool AnyModNeedsReload() => ModLoader.Mods.Any(ModNeedsReload);
147 
148  internal static bool ModNeedsReload(Mod mod)
149  {
150  if (Configs.ContainsKey(mod))
151  {
152  var configs = Configs[mod];
153  var loadTimeConfigs = LoadTimeConfigs[mod];
154  for (int i = 0; i < configs.Count; i++)
155  {
156  if (loadTimeConfigs[i].NeedsReload(configs[i]))
157  {
158  return true;
159  }
160  }
161  }
162  return false;
163  }
164 
165  // GetConfig...returns the config instance
166  internal static ModConfig GetConfig(ModNet.NetConfig netConfig) => ConfigManager.GetConfig(ModLoader.GetMod(netConfig.modname), netConfig.configname);
167  internal static ModConfig GetConfig(Mod mod, string config)
168  {
169  List<ModConfig> configs;
170  if (Configs.TryGetValue(mod, out configs))
171  {
172  return configs.Single(x => x.Name == config);
173  }
174  throw new MissingResourceException("Missing config named " + config + " in mod " + mod.Name);
175  }
176 
177  internal static ModConfig GetLoadTimeConfig(Mod mod, string config) {
178  List<ModConfig> configs;
179  if (LoadTimeConfigs.TryGetValue(mod, out configs)) {
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 void HandleInGameChangeConfigPacket(BinaryReader reader, int whoAmI)
186  {
187  if (Main.netMode == NetmodeID.MultiplayerClient)
188  {
189  bool success = reader.ReadBoolean();
190  string message = reader.ReadString();
191  if (success)
192  {
193  string modname = reader.ReadString();
194  string configname = reader.ReadString();
195  string json = reader.ReadString();
196  ModConfig activeConfig = GetConfig(ModLoader.GetMod(modname), configname);
197  JsonConvert.PopulateObject(json, activeConfig, serializerSettingsCompact);
198  activeConfig.OnChanged();
199 
200  Main.NewText($"Shared config changed: Message: {message}, Mod: {modname}, Config: {configname}");
201  if (Main.InGameUI.CurrentState == Interface.modConfig)
202  {
203  Main.InGameUI.SetState(Interface.modConfig);
204  Interface.modConfig.SetMessage("Server response: " + message, Color.Green);
205  }
206  }
207  else
208  {
209  // rejection only sent back to requester.
210  // Update UI with message
211 
212  Main.NewText("Changes Rejected: " + message);
213  if (Main.InGameUI.CurrentState == Interface.modConfig)
214  {
215  Interface.modConfig.SetMessage("Server rejected changes: " + message, Color.Red);
216  //Main.InGameUI.SetState(Interface.modConfig);
217  }
218 
219  }
220  }
221  else
222  {
223  // no bool in request.
224  string modname = reader.ReadString();
225  string configname = reader.ReadString();
226  string json = reader.ReadString();
227  ModConfig config = GetConfig(ModLoader.GetMod(modname), configname);
228  ModConfig loadTimeConfig = GetLoadTimeConfig(ModLoader.GetMod(modname), configname);
229  ModConfig pendingConfig = GeneratePopulatedClone(config);
230  JsonConvert.PopulateObject(json, pendingConfig, serializerSettingsCompact);
231  bool success = true;
232  string message = "Accepted";
233  if (loadTimeConfig.NeedsReload(pendingConfig))
234  {
235  success = false;
236  message = "Can't save because changes would require a reload.";
237  }
238  if (!config.AcceptClientChanges(pendingConfig, whoAmI, ref message))
239  {
240  success = false;
241  }
242  if (success)
243  {
244  // Apply to Servers Config
245  ConfigManager.Save(pendingConfig);
246  JsonConvert.PopulateObject(json, config, ConfigManager.serializerSettingsCompact);
247  config.OnChanged();
248  // Send new config to all clients
249  var p = new ModPacket(MessageID.InGameChangeConfig);
250  p.Write(true);
251  p.Write(message);
252  p.Write(modname);
253  p.Write(configname);
254  p.Write(json);
255  p.Send();
256  }
257  else
258  {
259  // Send rejections message back to client who requested change
260  var p = new ModPacket(MessageID.InGameChangeConfig);
261  p.Write(false);
262  p.Write(message);
263  p.Send(whoAmI);
264  }
265 
266  }
267  return;
268  }
269 
270  public static IEnumerable<PropertyFieldWrapper> GetFieldsAndProperties(object item)
271  {
272  PropertyInfo[] properties = item.GetType().GetProperties(
273  //BindingFlags.DeclaredOnly |
274  BindingFlags.Public |
275  BindingFlags.Instance);
276 
277  FieldInfo[] fields = item.GetType().GetFields(
278  //BindingFlags.DeclaredOnly |
279  BindingFlags.Public |
280  BindingFlags.Instance);
281 
282  return fields.Select(x => new PropertyFieldWrapper(x)).Concat(properties.Select(x => new PropertyFieldWrapper(x)));
283  }
284 
285  public static ModConfig GeneratePopulatedClone(ModConfig original) {
286  string json = JsonConvert.SerializeObject(original, ConfigManager.serializerSettings);
287  ModConfig properClone = original.Clone();
288  JsonConvert.PopulateObject(json, properClone, ConfigManager.serializerSettings);
289  return properClone;
290  }
291 
292  public static object AlternateCreateInstance(Type type)
293  {
294  if (type == typeof(string))
295  return "";
296  return Activator.CreateInstance(type);
297  }
298 
299  // Gets an Attribute from a property or field. Attribute defined on Member has highest priority,
300  // followed by the containing data structure, followed by attribute defined on the Class.
301  public static T GetCustomAttribute<T>(PropertyFieldWrapper memberInfo, object item, object array) where T : Attribute {
302  // Class
303  T attribute = (T)Attribute.GetCustomAttribute(memberInfo.Type, typeof(T), true);
304  if (array != null)
305  {
306  // item null?
307  // attribute = (T)Attribute.GetCustomAttribute(item.GetType(), typeof(T), true) ?? attribute; // TODO: is this wrong?
308  }
309  // Member
310  attribute = (T)Attribute.GetCustomAttribute(memberInfo.MemberInfo, typeof(T)) ?? attribute;
311  return attribute;
312  // TODO: allow for inheriting from parent's parent? (could get attribute from parent ConfigElement)
313  }
314 
315  public static T GetCustomAttribute<T>(PropertyFieldWrapper memberInfo, Type type) where T : Attribute {
316  // Class
317  T attribute = (T)Attribute.GetCustomAttribute(memberInfo.Type, typeof(T), true);
318 
319  attribute = (T)Attribute.GetCustomAttribute(type, typeof(T), true) ?? attribute;
320 
321  // Member
322  attribute = (T)Attribute.GetCustomAttribute(memberInfo.MemberInfo, typeof(T)) ?? attribute;
323  return attribute;
324  }
325 
326  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)
327  {
328  // public api for modders.
329  return UIModConfig.WrapIt(parent, ref top, memberInfo, item, order, list, arrayType, index);
330  }
331 
332  public static void SetPendingChanges(bool changes = true) {
333  // public api for modders.
334  Interface.modConfig.SetPendingChanges(changes);
335  }
336 
337  // TODO: better home?
338  public static bool ObjectEquals(object a, object b) {
339  if (ReferenceEquals(a, b)) return true;
340  if (a == null || b == null) return false;
341  if (a is IEnumerable && b is IEnumerable && !(a is string) && !(b is string))
342  return EnumerableEquals((IEnumerable)a, (IEnumerable)b);
343  return a.Equals(b);
344  }
345 
346  public static bool EnumerableEquals(IEnumerable a, IEnumerable b) {
347  IEnumerator enumeratorA = a.GetEnumerator();
348  IEnumerator enumeratorB = b.GetEnumerator();
349  bool hasNextA = enumeratorA.MoveNext();
350  bool hasNextB = enumeratorB.MoveNext();
351  while (hasNextA && hasNextB) {
352  if (!ObjectEquals(enumeratorA.Current, enumeratorB.Current)) return false;
353  hasNextA = enumeratorA.MoveNext();
354  hasNextB = enumeratorB.MoveNext();
355  }
356  return !hasNextA && !hasNextB;
357  }
358  }
359 
366  {
367  // This approach largely based on https://stackoverflow.com/a/52684798.
368  public abstract class ValueProviderDecorator : IValueProvider
369  {
371 
372  public ValueProviderDecorator(IValueProvider baseProvider) {
373  if (baseProvider == null)
374  throw new ArgumentNullException();
375  this.baseProvider = baseProvider;
376  }
377 
378  public virtual object GetValue(object target) { return baseProvider.GetValue(target); }
379 
380  public virtual void SetValue(object target, object value) { baseProvider.SetValue(target, value); }
381  }
383  {
384  //readonly object defaultValue;
385  readonly Func<object> defaultValueGenerator;
386 
387  //public NullToDefaultValueProvider(IValueProvider baseProvider, object defaultValue) : base(baseProvider) {
388  // this.defaultValue = defaultValue;
389  //}
390 
391  public NullToDefaultValueProvider(IValueProvider baseProvider, Func<object> defaultValueGenerator) : base(baseProvider) {
392  this.defaultValueGenerator = defaultValueGenerator;
393  }
394 
395  public override void SetValue(object target, object value) {
396  base.SetValue(target, value ?? defaultValueGenerator.Invoke());
397  //base.SetValue(target, value ?? defaultValue);
398  }
399  }
400 
401  protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization) {
402  IList<JsonProperty> props = base.CreateProperties(type, memberSerialization);
403  if (type.IsClass) {
404  ConstructorInfo ctor = type.GetConstructor(Type.EmptyTypes);
405  if (ctor != null) {
406  object referenceInstance = ctor.Invoke(null);
407  foreach (JsonProperty prop in props.Where(p => p.Readable)) {
408  if (!prop.PropertyType.IsValueType) {
409  var a = type.GetMember(prop.PropertyName);
410  if (prop.Writable) {
411  if (prop.PropertyType.GetConstructor(Type.EmptyTypes) != null) {
412  // defaultValueCreator will create new instance, then get the value from a field in that object. Prevents deserialized nulls from sharing with other instances.
413  Func<object> defaultValueCreator = () => prop.ValueProvider.GetValue(ctor.Invoke(null));
414  prop.ValueProvider = new NullToDefaultValueProvider(prop.ValueProvider, defaultValueCreator);
415  }
416  else if (prop.PropertyType.IsArray) {
417  Func<object> defaultValueCreator = () => (prop.ValueProvider.GetValue(referenceInstance) as Array).Clone();
418  prop.ValueProvider = new NullToDefaultValueProvider(prop.ValueProvider, defaultValueCreator);
419  }
420  }
421  if (prop.ShouldSerialize == null)
422  prop.ShouldSerialize = instance =>
423  {
424  object val = prop.ValueProvider.GetValue(instance);
425  object refVal = prop.ValueProvider.GetValue(referenceInstance);
426  return !ConfigManager.ObjectEquals(val, refVal);
427  };
428  }
429  }
430  }
431  }
432  return props;
433  }
434  }
435 }
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:26
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:86
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)