tModLoader v0.11.8.9
A mod to make and play Terraria mods
ConfigManager.cs
Go to the documentation of this file.
1using Microsoft.Xna.Framework;
2using Newtonsoft.Json;
3using Newtonsoft.Json.Serialization;
4using System;
5using System.Collections;
6using System.Collections.Generic;
7using System.IO;
8using System.Linq;
9using System.Reflection;
10using Terraria.ID;
11using Terraria.ModLoader.Config.UI;
13using Terraria.ModLoader.UI;
14using Terraria.UI;
15
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
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
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))
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.
377 {
379
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
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 object AlternateCreateInstance(Type type)
static readonly string ServerModConfigPath
static ModConfig GeneratePopulatedClone(ModConfig original)
static void SetPendingChanges(bool changes=true)
static readonly IDictionary< Mod, List< ModConfig > > LoadTimeConfigs
static readonly string ModConfigPath
static bool ObjectEquals(object a, object b)
static bool EnumerableEquals(IEnumerable a, IEnumerable b)
static IEnumerable< PropertyFieldWrapper > GetFieldsAndProperties(object item)
static readonly JsonSerializerSettings serializerSettings
static readonly IList< JsonConverter > converters
static T GetCustomAttribute< T >(PropertyFieldWrapper memberInfo, object item, object array)
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)
ModConfig provides a way for mods to be configurable. ModConfigs can either be Client specific or Ser...
Definition: ModConfig.cs:19
virtual ModConfig Clone()
tModLoader will call Clone on ModConfig to facilitate proper implementation of the ModConfig user int...
virtual void OnChanged()
This hook is called anytime new config values have been set and are ready to take effect....
Definition: ModConfig.cs:43
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
NullToDefaultValueProvider(IValueProvider baseProvider, Func< object > defaultValueGenerator)
Custom ContractResolver for facilitating refernce type defaults. The ShouldSerialize code enables unc...
override IList< JsonProperty > CreateProperties(Type type, MemberSerialization memberSerialization)
Mod is an abstract class that you will override. It serves as a central place from which the mod's co...
Definition: Mod.cs:25
virtual string Name
Stores the name of the mod. This name serves as the mod's identification, and also helps with saving ...
Definition: Mod.cs:42
This serves as the central class which loads mods. It contains many static fields and methods related...
Definition: ModLoader.cs:29
static Mod GetMod(string name)
Gets the instance of the Mod with the specified name.
Definition: ModLoader.cs:90
This class inherits from BinaryWriter. This means that you can use all of its writing functions to se...
Definition: ModPacket.cs:14
ConfigScope
Each ModConfig class has a different scope. Failure to use the correct mode will lead to bugs.
Definition: ModConfig.cs:92