1 #region Copyright & License Information
2 /*
3  * Copyright 2007-2020 The OpenRA Developers (see AUTHORS)
4  * This file is part of OpenRA, which is free software. It is made
5  * available to you under the terms of the GNU General Public License
6  * as published by the Free Software Foundation, either version 3 of
7  * the License, or (at your option) any later version. For more
8  * information, see COPYING.
9  */
10 #endregion
11 
12 using System;
13 using System.Collections.Generic;
14 using System.ComponentModel;
15 using System.Globalization;
16 using System.IO;
17 using System.Linq;
18 using System.Reflection;
19 using System.Runtime.Serialization;
20 using System.Text.RegularExpressions;
21 using OpenRA.Graphics;
22 using OpenRA.Primitives;
23 using OpenRA.Support;
24 
25 namespace OpenRA
26 {
27 	public static class FieldLoader
28 	{
29 		[Serializable]
30 		public class MissingFieldsException : YamlException
31 		{
32 			public readonly string[] Missing;
33 			public readonly string Header;
34 			public override string Message
35 			{
36 				get
37 				{
38 					return (string.IsNullOrEmpty(Header) ? "" : Header + ": ") + Missing[0]
39 						+ string.Concat(Missing.Skip(1).Select(m => ", " + m));
40 				}
41 			}
42 
MissingFieldsException(string[] missing, string header = null, string headerSingle = null)43 			public MissingFieldsException(string[] missing, string header = null, string headerSingle = null)
44 				: base(null)
45 			{
46 				Header = missing.Length > 1 ? header : headerSingle ?? header;
47 				Missing = missing;
48 			}
49 
GetObjectData(SerializationInfo info, StreamingContext context)50 			public override void GetObjectData(SerializationInfo info, StreamingContext context)
51 			{
52 				base.GetObjectData(info, context);
53 				info.AddValue("Missing", Missing);
54 				info.AddValue("Header", Header);
55 			}
56 		}
57 
58 		public static Func<string, Type, string, object> InvalidValueAction = (s, t, f) =>
59 		{
60 			throw new YamlException("FieldLoader: Cannot parse `{0}` into `{1}.{2}` ".F(s, f, t));
61 		};
62 
63 		public static Action<string, Type> UnknownFieldAction = (s, f) =>
64 		{
65 			throw new NotImplementedException("FieldLoader: Missing field `{0}` on `{1}`".F(s, f.Name));
66 		};
67 
68 		static readonly ConcurrentCache<Type, FieldLoadInfo[]> TypeLoadInfo =
69 			new ConcurrentCache<Type, FieldLoadInfo[]>(BuildTypeLoadInfo);
70 		static readonly ConcurrentCache<MemberInfo, bool> MemberHasTranslateAttribute =
71 			new ConcurrentCache<MemberInfo, bool>(member => member.HasAttribute<TranslateAttribute>());
72 
73 		static readonly ConcurrentCache<string, BooleanExpression> BooleanExpressionCache =
74 			new ConcurrentCache<string, BooleanExpression>(expression => new BooleanExpression(expression));
75 		static readonly ConcurrentCache<string, IntegerExpression> IntegerExpressionCache =
76 			new ConcurrentCache<string, IntegerExpression>(expression => new IntegerExpression(expression));
77 
78 		static readonly object TranslationsLock = new object();
79 		static Dictionary<string, string> translations;
80 
Load(object self, MiniYaml my)81 		public static void Load(object self, MiniYaml my)
82 		{
83 			var loadInfo = TypeLoadInfo[self.GetType()];
84 			var missing = new List<string>();
85 
86 			Dictionary<string, MiniYaml> md = null;
87 
88 			foreach (var fli in loadInfo)
89 			{
90 				object val;
91 
92 				if (md == null)
93 					md = my.ToDictionary();
94 				if (fli.Loader != null)
95 				{
96 					if (!fli.Attribute.Required || md.ContainsKey(fli.YamlName))
97 						val = fli.Loader(my);
98 					else
99 					{
100 						missing.Add(fli.YamlName);
101 						continue;
102 					}
103 				}
104 				else
105 				{
106 					if (!TryGetValueFromYaml(fli.YamlName, fli.Field, md, out val))
107 					{
108 						if (fli.Attribute.Required)
109 							missing.Add(fli.YamlName);
110 						continue;
111 					}
112 				}
113 
114 				fli.Field.SetValue(self, val);
115 			}
116 
117 			if (missing.Any())
118 				throw new MissingFieldsException(missing.ToArray());
119 		}
120 
TryGetValueFromYaml(string yamlName, FieldInfo field, Dictionary<string, MiniYaml> md, out object ret)121 		static bool TryGetValueFromYaml(string yamlName, FieldInfo field, Dictionary<string, MiniYaml> md, out object ret)
122 		{
123 			ret = null;
124 
125 			MiniYaml yaml;
126 			if (!md.TryGetValue(yamlName, out yaml))
127 				return false;
128 
129 			ret = GetValue(field.Name, field.FieldType, yaml, field);
130 			return true;
131 		}
132 
133 		public static T Load<T>(MiniYaml y) where T : new()
134 		{
135 			var t = new T();
136 			Load(t, y);
137 			return t;
138 		}
139 
LoadField(object target, string key, string value)140 		public static void LoadField(object target, string key, string value)
141 		{
142 			const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
143 
144 			key = key.Trim();
145 
146 			var field = target.GetType().GetField(key, Flags);
147 			if (field != null)
148 			{
149 				var sa = field.GetCustomAttributes<SerializeAttribute>(false).DefaultIfEmpty(SerializeAttribute.Default).First();
150 				if (!sa.FromYamlKey)
151 					field.SetValue(target, GetValue(field.Name, field.FieldType, value, field));
152 				return;
153 			}
154 
155 			var prop = target.GetType().GetProperty(key, Flags);
156 			if (prop != null)
157 			{
158 				var sa = prop.GetCustomAttributes<SerializeAttribute>(false).DefaultIfEmpty(SerializeAttribute.Default).First();
159 				if (!sa.FromYamlKey)
160 					prop.SetValue(target, GetValue(prop.Name, prop.PropertyType, value, prop), null);
161 				return;
162 			}
163 
164 			UnknownFieldAction(key, target.GetType());
165 		}
166 
GetValue(string field, string value)167 		public static T GetValue<T>(string field, string value)
168 		{
169 			return (T)GetValue(field, typeof(T), value, null);
170 		}
171 
GetValue(string fieldName, Type fieldType, string value)172 		public static object GetValue(string fieldName, Type fieldType, string value)
173 		{
174 			return GetValue(fieldName, fieldType, value, null);
175 		}
176 
GetValue(string fieldName, Type fieldType, string value, MemberInfo field)177 		public static object GetValue(string fieldName, Type fieldType, string value, MemberInfo field)
178 		{
179 			return GetValue(fieldName, fieldType, new MiniYaml(value), field);
180 		}
181 
GetValue(string fieldName, Type fieldType, MiniYaml yaml, MemberInfo field)182 		public static object GetValue(string fieldName, Type fieldType, MiniYaml yaml, MemberInfo field)
183 		{
184 			var value = yaml.Value;
185 			if (value != null) value = value.Trim();
186 
187 			if (fieldType == typeof(int))
188 			{
189 				int res;
190 				if (Exts.TryParseIntegerInvariant(value, out res))
191 					return res;
192 				return InvalidValueAction(value, fieldType, fieldName);
193 			}
194 			else if (fieldType == typeof(ushort))
195 			{
196 				ushort res;
197 				if (ushort.TryParse(value, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out res))
198 					return res;
199 				return InvalidValueAction(value, fieldType, fieldName);
200 			}
201 
202 			if (fieldType == typeof(long))
203 			{
204 				long res;
205 				if (long.TryParse(value, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out res))
206 					return res;
207 				return InvalidValueAction(value, fieldType, fieldName);
208 			}
209 			else if (fieldType == typeof(float))
210 			{
211 				float res;
212 				if (value != null && float.TryParse(value.Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out res))
213 					return res * (value.Contains('%') ? 0.01f : 1f);
214 				return InvalidValueAction(value, fieldType, fieldName);
215 			}
216 			else if (fieldType == typeof(decimal))
217 			{
218 				decimal res;
219 				if (value != null && decimal.TryParse(value.Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out res))
220 					return res * (value.Contains('%') ? 0.01m : 1m);
221 				return InvalidValueAction(value, fieldType, fieldName);
222 			}
223 			else if (fieldType == typeof(string))
224 			{
225 				if (field != null && MemberHasTranslateAttribute[field] && value != null)
226 					return Regex.Replace(value, "@[^@]+@", m => Translate(m.Value.Substring(1, m.Value.Length - 2)), RegexOptions.Compiled);
227 				return value;
228 			}
229 			else if (fieldType == typeof(Color))
230 			{
231 				Color color;
232 				if (value != null && Color.TryParse(value, out color))
233 					return color;
234 
235 				return InvalidValueAction(value, fieldType, fieldName);
236 			}
237 			else if (fieldType == typeof(Hotkey))
238 			{
239 				Hotkey res;
240 				if (Hotkey.TryParse(value, out res))
241 					return res;
242 
243 				return InvalidValueAction(value, fieldType, fieldName);
244 			}
245 			else if (fieldType == typeof(HotkeyReference))
246 			{
247 				return Game.ModData.Hotkeys[value];
248 			}
249 			else if (fieldType == typeof(WDist))
250 			{
251 				WDist res;
252 				if (WDist.TryParse(value, out res))
253 					return res;
254 
255 				return InvalidValueAction(value, fieldType, fieldName);
256 			}
257 			else if (fieldType == typeof(WVec))
258 			{
259 				if (value != null)
260 				{
261 					var parts = value.Split(',');
262 					if (parts.Length == 3)
263 					{
264 						WDist rx, ry, rz;
265 						if (WDist.TryParse(parts[0], out rx) && WDist.TryParse(parts[1], out ry) && WDist.TryParse(parts[2], out rz))
266 							return new WVec(rx, ry, rz);
267 					}
268 				}
269 
270 				return InvalidValueAction(value, fieldType, fieldName);
271 			}
272 			else if (fieldType == typeof(WVec[]))
273 			{
274 				if (value != null)
275 				{
276 					var parts = value.Split(',');
277 
278 					if (parts.Length % 3 != 0)
279 						return InvalidValueAction(value, fieldType, fieldName);
280 
281 					var vecs = new WVec[parts.Length / 3];
282 
283 					for (var i = 0; i < vecs.Length; ++i)
284 					{
285 						WDist rx, ry, rz;
286 						if (WDist.TryParse(parts[3 * i], out rx) && WDist.TryParse(parts[3 * i + 1], out ry) && WDist.TryParse(parts[3 * i + 2], out rz))
287 							vecs[i] = new WVec(rx, ry, rz);
288 					}
289 
290 					return vecs;
291 				}
292 
293 				return InvalidValueAction(value, fieldType, fieldName);
294 			}
295 			else if (fieldType == typeof(WPos))
296 			{
297 				if (value != null)
298 				{
299 					var parts = value.Split(',');
300 					if (parts.Length == 3)
301 					{
302 						WDist rx, ry, rz;
303 						if (WDist.TryParse(parts[0], out rx) && WDist.TryParse(parts[1], out ry) && WDist.TryParse(parts[2], out rz))
304 							return new WPos(rx, ry, rz);
305 					}
306 				}
307 
308 				return InvalidValueAction(value, fieldType, fieldName);
309 			}
310 			else if (fieldType == typeof(WAngle))
311 			{
312 				int res;
313 				if (Exts.TryParseIntegerInvariant(value, out res))
314 					return new WAngle(res);
315 				return InvalidValueAction(value, fieldType, fieldName);
316 			}
317 			else if (fieldType == typeof(WRot))
318 			{
319 				if (value != null)
320 				{
321 					var parts = value.Split(',');
322 					if (parts.Length == 3)
323 					{
324 						int rr, rp, ry;
325 						if (Exts.TryParseIntegerInvariant(parts[0], out rr) && Exts.TryParseIntegerInvariant(parts[1], out rp) && Exts.TryParseIntegerInvariant(parts[2], out ry))
326 							return new WRot(new WAngle(rr), new WAngle(rp), new WAngle(ry));
327 					}
328 				}
329 
330 				return InvalidValueAction(value, fieldType, fieldName);
331 			}
332 			else if (fieldType == typeof(CPos))
333 			{
334 				if (value != null)
335 				{
336 					var parts = value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
337 					return new CPos(Exts.ParseIntegerInvariant(parts[0]), Exts.ParseIntegerInvariant(parts[1]));
338 				}
339 
340 				return InvalidValueAction(value, fieldType, fieldName);
341 			}
342 			else if (fieldType == typeof(CVec))
343 			{
344 				if (value != null)
345 				{
346 					var parts = value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
347 					return new CVec(Exts.ParseIntegerInvariant(parts[0]), Exts.ParseIntegerInvariant(parts[1]));
348 				}
349 
350 				return InvalidValueAction(value, fieldType, fieldName);
351 			}
352 			else if (fieldType == typeof(CVec[]))
353 			{
354 				if (value != null)
355 				{
356 					var parts = value.Split(',');
357 
358 					if (parts.Length % 2 != 0)
359 						return InvalidValueAction(value, fieldType, fieldName);
360 
361 					var vecs = new CVec[parts.Length / 2];
362 					for (var i = 0; i < vecs.Length; i++)
363 					{
364 						int rx, ry;
365 						if (int.TryParse(parts[2 * i], out rx) && int.TryParse(parts[2 * i + 1], out ry))
366 							vecs[i] = new CVec(rx, ry);
367 					}
368 
369 					return vecs;
370 				}
371 
372 				return InvalidValueAction(value, fieldType, fieldName);
373 			}
374 			else if (fieldType == typeof(BooleanExpression))
375 			{
376 				if (value != null)
377 				{
378 					try
379 					{
380 						return BooleanExpressionCache[value];
381 					}
382 					catch (InvalidDataException e)
383 					{
384 						throw new YamlException(e.Message);
385 					}
386 				}
387 
388 				return InvalidValueAction(value, fieldType, fieldName);
389 			}
390 			else if (fieldType == typeof(IntegerExpression))
391 			{
392 				if (value != null)
393 				{
394 					try
395 					{
396 						return IntegerExpressionCache[value];
397 					}
398 					catch (InvalidDataException e)
399 					{
400 						throw new YamlException(e.Message);
401 					}
402 				}
403 
404 				return InvalidValueAction(value, fieldType, fieldName);
405 			}
406 			else if (fieldType.IsEnum)
407 			{
408 				try
409 				{
410 					return Enum.Parse(fieldType, value, true);
411 				}
412 				catch (ArgumentException)
413 				{
414 					return InvalidValueAction(value, fieldType, fieldName);
415 				}
416 			}
417 			else if (fieldType == typeof(bool))
418 			{
419 				bool result;
420 				if (bool.TryParse(value.ToLowerInvariant(), out result))
421 					return result;
422 
423 				return InvalidValueAction(value, fieldType, fieldName);
424 			}
425 			else if (fieldType == typeof(int2[]))
426 			{
427 				if (value != null)
428 				{
429 					var parts = value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
430 					if (parts.Length % 2 != 0)
431 						return InvalidValueAction(value, fieldType, fieldName);
432 
433 					var ints = new int2[parts.Length / 2];
434 					for (var i = 0; i < ints.Length; i++)
435 						ints[i] = new int2(Exts.ParseIntegerInvariant(parts[2 * i]), Exts.ParseIntegerInvariant(parts[2 * i + 1]));
436 
437 					return ints;
438 				}
439 
440 				return InvalidValueAction(value, fieldType, fieldName);
441 			}
442 			else if (fieldType.IsArray && fieldType.GetArrayRank() == 1)
443 			{
444 				if (value == null)
445 					return Array.CreateInstance(fieldType.GetElementType(), 0);
446 
447 				var options = field != null && field.HasAttribute<AllowEmptyEntriesAttribute>() ?
448 					StringSplitOptions.None : StringSplitOptions.RemoveEmptyEntries;
449 				var parts = value.Split(new char[] { ',' }, options);
450 
451 				var ret = Array.CreateInstance(fieldType.GetElementType(), parts.Length);
452 				for (var i = 0; i < parts.Length; i++)
453 					ret.SetValue(GetValue(fieldName, fieldType.GetElementType(), parts[i].Trim(), field), i);
454 				return ret;
455 			}
456 			else if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(HashSet<>))
457 			{
458 				var set = Activator.CreateInstance(fieldType);
459 				if (value == null)
460 					return set;
461 
462 				var parts = value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
463 				var addMethod = fieldType.GetMethod("Add", fieldType.GetGenericArguments());
464 				for (var i = 0; i < parts.Length; i++)
465 					addMethod.Invoke(set, new[] { GetValue(fieldName, fieldType.GetGenericArguments()[0], parts[i].Trim(), field) });
466 				return set;
467 			}
468 			else if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
469 			{
470 				var dict = Activator.CreateInstance(fieldType);
471 				var arguments = fieldType.GetGenericArguments();
472 				var addMethod = fieldType.GetMethod("Add", arguments);
473 
474 				foreach (var node in yaml.Nodes)
475 				{
476 					var key = GetValue(fieldName, arguments[0], node.Key, field);
477 					var val = GetValue(fieldName, arguments[1], node.Value, field);
478 					addMethod.Invoke(dict, new[] { key, val });
479 				}
480 
481 				return dict;
482 			}
483 			else if (fieldType == typeof(Size))
484 			{
485 				if (value != null)
486 				{
487 					var parts = value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
488 					return new Size(Exts.ParseIntegerInvariant(parts[0]), Exts.ParseIntegerInvariant(parts[1]));
489 				}
490 
491 				return InvalidValueAction(value, fieldType, fieldName);
492 			}
493 			else if (fieldType == typeof(int2))
494 			{
495 				if (value != null)
496 				{
497 					var parts = value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
498 					if (parts.Length != 2)
499 						return InvalidValueAction(value, fieldType, fieldName);
500 
501 					return new int2(Exts.ParseIntegerInvariant(parts[0]), Exts.ParseIntegerInvariant(parts[1]));
502 				}
503 
504 				return InvalidValueAction(value, fieldType, fieldName);
505 			}
506 			else if (fieldType == typeof(float2))
507 			{
508 				if (value != null)
509 				{
510 					var parts = value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
511 					float xx = 0;
512 					float yy = 0;
513 					float res;
514 					if (float.TryParse(parts[0].Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out res))
515 						xx = res * (parts[0].Contains('%') ? 0.01f : 1f);
516 					if (float.TryParse(parts[1].Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out res))
517 						yy = res * (parts[1].Contains('%') ? 0.01f : 1f);
518 					return new float2(xx, yy);
519 				}
520 
521 				return InvalidValueAction(value, fieldType, fieldName);
522 			}
523 			else if (fieldType == typeof(float3))
524 			{
525 				if (value != null)
526 				{
527 					var parts = value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
528 					float x = 0;
529 					float y = 0;
530 					float z = 0;
531 					float.TryParse(parts[0], NumberStyles.Float, NumberFormatInfo.InvariantInfo, out x);
532 					float.TryParse(parts[1], NumberStyles.Float, NumberFormatInfo.InvariantInfo, out y);
533 
534 					// z component is optional for compatibility with older float2 definitions
535 					if (parts.Length > 2)
536 						float.TryParse(parts[2], NumberStyles.Float, NumberFormatInfo.InvariantInfo, out z);
537 
538 					return new float3(x, y, z);
539 				}
540 
541 				return InvalidValueAction(value, fieldType, fieldName);
542 			}
543 			else if (fieldType == typeof(Rectangle))
544 			{
545 				if (value != null)
546 				{
547 					var parts = value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
548 					return new Rectangle(
549 						Exts.ParseIntegerInvariant(parts[0]),
550 						Exts.ParseIntegerInvariant(parts[1]),
551 						Exts.ParseIntegerInvariant(parts[2]),
552 						Exts.ParseIntegerInvariant(parts[3]));
553 				}
554 
555 				return InvalidValueAction(value, fieldType, fieldName);
556 			}
557 			else if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(BitSet<>))
558 			{
559 				if (value != null)
560 				{
561 					var parts = value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
562 					var ctor = fieldType.GetConstructor(new[] { typeof(string[]) });
563 					return ctor.Invoke(new object[] { parts.Select(p => p.Trim()).ToArray() });
564 				}
565 
566 				return InvalidValueAction(value, fieldType, fieldName);
567 			}
568 			else if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(Nullable<>))
569 			{
570 				var innerType = fieldType.GetGenericArguments().First();
571 				var innerValue = GetValue("Nullable<T>", innerType, value, field);
572 				return fieldType.GetConstructor(new[] { innerType }).Invoke(new[] { innerValue });
573 			}
574 			else if (fieldType == typeof(DateTime))
575 			{
576 				DateTime dt;
577 				if (DateTime.TryParseExact(value, "yyyy-MM-dd HH-mm-ss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out dt))
578 					return dt;
579 				return InvalidValueAction(value, fieldType, fieldName);
580 			}
581 			else
582 			{
583 				var conv = TypeDescriptor.GetConverter(fieldType);
584 				if (conv.CanConvertFrom(typeof(string)))
585 				{
586 					try
587 					{
588 						return conv.ConvertFromInvariantString(value);
589 					}
590 					catch
591 					{
592 						return InvalidValueAction(value, fieldType, fieldName);
593 					}
594 				}
595 			}
596 
597 			UnknownFieldAction("[Type] {0}".F(value), fieldType);
598 			return null;
599 		}
600 
601 		public sealed class FieldLoadInfo
602 		{
603 			public readonly FieldInfo Field;
604 			public readonly SerializeAttribute Attribute;
605 			public readonly string YamlName;
606 			public readonly Func<MiniYaml, object> Loader;
607 
FieldLoadInfo(FieldInfo field, SerializeAttribute attr, string yamlName, Func<MiniYaml, object> loader = null)608 			internal FieldLoadInfo(FieldInfo field, SerializeAttribute attr, string yamlName, Func<MiniYaml, object> loader = null)
609 			{
610 				Field = field;
611 				Attribute = attr;
612 				YamlName = yamlName;
613 				Loader = loader;
614 			}
615 		}
616 
GetTypeLoadInfo(Type type, bool includePrivateByDefault = false)617 		public static IEnumerable<FieldLoadInfo> GetTypeLoadInfo(Type type, bool includePrivateByDefault = false)
618 		{
619 			return TypeLoadInfo[type].Where(fli => includePrivateByDefault || fli.Field.IsPublic || (fli.Attribute.Serialize && !fli.Attribute.IsDefault));
620 		}
621 
BuildTypeLoadInfo(Type type)622 		static FieldLoadInfo[] BuildTypeLoadInfo(Type type)
623 		{
624 			var ret = new List<FieldLoadInfo>();
625 
626 			foreach (var ff in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
627 			{
628 				var field = ff;
629 
630 				var sa = field.GetCustomAttributes<SerializeAttribute>(false).DefaultIfEmpty(SerializeAttribute.Default).First();
631 				if (!sa.Serialize)
632 					continue;
633 
634 				var yamlName = string.IsNullOrEmpty(sa.YamlName) ? field.Name : sa.YamlName;
635 
636 				var loader = sa.GetLoader(type);
637 				if (loader == null && sa.FromYamlKey)
638 					loader = yaml => GetValue(yamlName, field.FieldType, yaml, field);
639 
640 				var fli = new FieldLoadInfo(field, sa, yamlName, loader);
641 				ret.Add(fli);
642 			}
643 
644 			return ret.ToArray();
645 		}
646 
647 		[AttributeUsage(AttributeTargets.Field)]
648 		public sealed class IgnoreAttribute : SerializeAttribute
649 		{
IgnoreAttribute()650 			public IgnoreAttribute()
651 				: base(false) { }
652 		}
653 
654 		[AttributeUsage(AttributeTargets.Field)]
655 		public sealed class RequireAttribute : SerializeAttribute
656 		{
RequireAttribute()657 			public RequireAttribute()
658 				: base(true, true) { }
659 		}
660 
661 		[AttributeUsage(AttributeTargets.Field)]
662 		public sealed class AllowEmptyEntriesAttribute : SerializeAttribute
663 		{
AllowEmptyEntriesAttribute()664 			public AllowEmptyEntriesAttribute()
665 				: base(allowEmptyEntries: true) { }
666 		}
667 
668 		[AttributeUsage(AttributeTargets.Field)]
669 		public sealed class LoadUsingAttribute : SerializeAttribute
670 		{
LoadUsingAttribute(string loader, bool required = false)671 			public LoadUsingAttribute(string loader, bool required = false)
672 			{
673 				Loader = loader;
674 				Required = required;
675 			}
676 		}
677 
678 		[AttributeUsage(AttributeTargets.Field)]
679 		public class SerializeAttribute : Attribute
680 		{
681 			public static readonly SerializeAttribute Default = new SerializeAttribute(true);
682 
683 			public bool IsDefault { get { return this == Default; } }
684 
685 			public readonly bool Serialize;
686 			public string YamlName;
687 			public string Loader;
688 			public bool FromYamlKey;
689 			public bool DictionaryFromYamlKey;
690 			public bool Required;
691 			public bool AllowEmptyEntries;
692 
SerializeAttribute(bool serialize = true, bool required = false, bool allowEmptyEntries = false)693 			public SerializeAttribute(bool serialize = true, bool required = false, bool allowEmptyEntries = false)
694 			{
695 				Serialize = serialize;
696 				Required = required;
697 				AllowEmptyEntries = allowEmptyEntries;
698 			}
699 
GetLoader(Type type)700 			internal Func<MiniYaml, object> GetLoader(Type type)
701 			{
702 				const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.FlattenHierarchy;
703 
704 				if (!string.IsNullOrEmpty(Loader))
705 				{
706 					var method = type.GetMethod(Loader, Flags);
707 					if (method == null)
708 						throw new InvalidOperationException("{0} does not specify a loader function '{1}'".F(type.Name, Loader));
709 
710 					return (Func<MiniYaml, object>)Delegate.CreateDelegate(typeof(Func<MiniYaml, object>), method);
711 				}
712 
713 				return null;
714 			}
715 		}
716 
Translate(string key)717 		public static string Translate(string key)
718 		{
719 			if (string.IsNullOrEmpty(key))
720 				return key;
721 
722 			lock (TranslationsLock)
723 			{
724 				if (translations == null)
725 					return key;
726 
727 				string value;
728 				if (!translations.TryGetValue(key, out value))
729 					return key;
730 
731 				return value;
732 			}
733 		}
734 
SetTranslations(IDictionary<string, string> translations)735 		public static void SetTranslations(IDictionary<string, string> translations)
736 		{
737 			lock (TranslationsLock)
738 				FieldLoader.translations = new Dictionary<string, string>(translations);
739 		}
740 	}
741 
742 	[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
743 	public sealed class TranslateAttribute : Attribute { }
744 
745 	[AttributeUsage(AttributeTargets.Field)]
746 	public sealed class FieldFromYamlKeyAttribute : FieldLoader.SerializeAttribute
747 	{
FieldFromYamlKeyAttribute()748 		public FieldFromYamlKeyAttribute()
749 		{
750 			FromYamlKey = true;
751 		}
752 	}
753 
754 	// Special-cases FieldFromYamlKeyAttribute for use with Dictionary<K,V>.
755 	[AttributeUsage(AttributeTargets.Field)]
756 	public sealed class DictionaryFromYamlKeyAttribute : FieldLoader.SerializeAttribute
757 	{
DictionaryFromYamlKeyAttribute()758 		public DictionaryFromYamlKeyAttribute()
759 		{
760 			FromYamlKey = true;
761 			DictionaryFromYamlKey = true;
762 		}
763 	}
764 
765 	// Mirrors DescriptionAttribute from System.ComponentModel but we don't want to have to use that everywhere.
766 	[AttributeUsage(AttributeTargets.All)]
767 	public sealed class DescAttribute : Attribute
768 	{
769 		public readonly string[] Lines;
DescAttribute(params string[] lines)770 		public DescAttribute(params string[] lines) { Lines = lines; }
771 	}
772 }
773