Client LuaCsForBarotrauma
XMLExtensions.cs
1 using Microsoft.Xna.Framework;
2 using System;
3 using System.Collections.Generic;
4 using System.Collections.Immutable;
5 using System.Diagnostics;
6 using System.Diagnostics.CodeAnalysis;
7 using System.Globalization;
8 using System.Linq;
9 using System.Reflection;
10 using System.Runtime.CompilerServices;
11 using System.Text;
12 using System.Xml;
13 using System.Xml.Linq;
15 using File = Barotrauma.IO.File;
16 using FileStream = Barotrauma.IO.FileStream;
17 using Path = Barotrauma.IO.Path;
18 
19 namespace Barotrauma
20 {
21  public static class XMLExtensions
22  {
23  private readonly static ImmutableDictionary<Type, Func<string, object, object>> Converters
24  = new Dictionary<Type, Func<string, object, object>>()
25  {
26  { typeof(string), (str, defVal) => str },
27  { typeof(int), (str, defVal) => int.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out int result) ? result : defVal },
28  { typeof(uint), (str, defVal) => uint.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out uint result) ? result : defVal },
29  { typeof(UInt64), (str, defVal) => UInt64.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out UInt64 result) ? result : defVal },
30  { typeof(float), (str, defVal) => float.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out float result) ? result : defVal },
31  { typeof(bool), (str, defVal) => bool.TryParse(str, out bool result) ? result : defVal },
32  { typeof(Color), (str, defVal) => ParseColor(str) },
33  { typeof(Vector2), (str, defVal) => ParseVector2(str) },
34  { typeof(Vector3), (str, defVal) => ParseVector3(str) },
35  { typeof(Vector4), (str, defVal) => ParseVector4(str) },
36  { typeof(Rectangle), (str, defVal) => ParseRect(str, true) }
37  }.ToImmutableDictionary();
38 
39  public static string ParseContentPathFromUri(this XObject element)
40  => !string.IsNullOrWhiteSpace(element.BaseUri)
41  ? System.IO.Path.GetRelativePath(Environment.CurrentDirectory, element.BaseUri.CleanUpPath())
42  : "";
43 
44  public static readonly XmlReaderSettings ReaderSettings = new XmlReaderSettings
45  {
46  DtdProcessing = DtdProcessing.Prohibit,
47  XmlResolver = null,
48  IgnoreWhitespace = true,
49  };
50 
51  public static XmlReader CreateReader(System.IO.Stream stream, string baseUri = "")
52  => XmlReader.Create(stream, ReaderSettings, baseUri);
53 
54  public static XDocument TryLoadXml(System.IO.Stream stream)
55  {
56  XDocument doc;
57  try
58  {
59  using XmlReader reader = CreateReader(stream);
60  doc = XDocument.Load(reader);
61  }
62  catch (Exception e)
63  {
64  DebugConsole.ThrowError($"Couldn't load xml document from stream!", e);
65  return null;
66  }
67  if (doc?.Root == null)
68  {
69  DebugConsole.ThrowError("XML could not be loaded from stream: Document or the root element is invalid!");
70  return null;
71  }
72  return doc;
73  }
74 
75  public static XDocument TryLoadXml(ContentPath path) => TryLoadXml(path.Value);
76 
77  public static XDocument TryLoadXml(string filePath)
78  {
79  var doc = TryLoadXml(filePath, out var exception);
80  if (exception != null)
81  {
82  DebugConsole.ThrowError($"Couldn't load xml document \"{filePath}\"!", exception);
83  }
84  else if (doc is null)
85  {
86  DebugConsole.ThrowError($"File \"{filePath}\" could not be loaded: Document or the root element is invalid!");
87  }
88  return doc;
89  }
90 
91  public static XDocument TryLoadXml(string filePath, out Exception exception)
92  {
93  exception = null;
94  XDocument doc;
95  try
96  {
97  ToolBox.IsProperFilenameCase(filePath);
98  using FileStream stream = File.Open(filePath, System.IO.FileMode.Open, System.IO.FileAccess.Read, catchUnauthorizedAccessExceptions: false);
99  using XmlReader reader = CreateReader(stream, Path.GetFullPath(filePath));
100  doc = XDocument.Load(reader, LoadOptions.SetBaseUri);
101  }
102  catch (Exception e)
103  {
104  exception = e;
105  return null;
106  }
107  if (doc?.Root == null)
108  {
109  return null;
110  }
111  return doc;
112  }
113 
114  public static object GetAttributeObject(XAttribute attribute)
115  {
116  if (attribute == null) { return null; }
117 
118  return ParseToObject(attribute.Value.ToString());
119  }
120 
121  public static object ParseToObject(string value)
122  {
123  if (value.Contains(".") && Single.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out float floatVal))
124  {
125  return floatVal;
126  }
127  if (Int32.TryParse(value, out int intVal))
128  {
129  return intVal;
130  }
131 
132  string lowerTrimmedVal = value.ToLowerInvariant().Trim();
133  if (lowerTrimmedVal == "true")
134  {
135  return true;
136  }
137  if (lowerTrimmedVal == "false")
138  {
139  return false;
140  }
141 
142  return value;
143  }
144 
145 
146  public static string GetAttributeString(this XElement element, string name, string defaultValue)
147  {
148  var attribute = element?.GetAttribute(name);
149  if (attribute == null) { return defaultValue; }
150  string str = GetAttributeString(attribute, defaultValue);
151 #if DEBUG
152  if (!str.IsNullOrEmpty() &&
153  (str.Contains("%ModDir", StringComparison.OrdinalIgnoreCase)
154  || str.CleanUpPathCrossPlatform(correctFilenameCase: false).StartsWith("Content/", StringComparison.OrdinalIgnoreCase)))
155  {
156  DebugConsole.ThrowError($"Use {nameof(GetAttributeContentPath)} instead of {nameof(GetAttributeString)}\n{Environment.StackTrace.CleanupStackTrace()}");
157  if (Debugger.IsAttached) { Debugger.Break(); }
158  }
159 #endif
160  return str;
161  }
162 
163  public static string GetAttributeStringUnrestricted(this XElement element, string name, string defaultValue)
164  {
165  #warning TODO: remove?
166  var attribute = element?.GetAttribute(name);
167  if (attribute == null) { return defaultValue; }
168  return GetAttributeString(attribute, defaultValue);
169  }
170 
171  public static bool DoesAttributeReferenceFileNameAlone(this XElement element, string name)
172  {
173  string texName = element.GetAttributeStringUnrestricted(name, "");
174  return !texName.IsNullOrEmpty() & !texName.Contains("/") && !texName.Contains("%ModDir", StringComparison.OrdinalIgnoreCase);
175  }
176 
177  public static ContentPath GetAttributeContentPath(this XElement element, string name, ContentPackage contentPackage)
178  {
179  var attribute = element?.GetAttribute(name);
180  if (attribute == null) { return null; }
181  return ContentPath.FromRaw(contentPackage, GetAttributeString(attribute, null));
182  }
183 
184  public static Identifier GetAttributeIdentifier(this XElement element, string name, string defaultValue)
185  {
186  return element.GetAttributeString(name, defaultValue).ToIdentifier();
187  }
188 
189  public static Identifier GetAttributeIdentifier(this XElement element, string name, Identifier defaultValue)
190  {
191  return element.GetAttributeIdentifier(name, defaultValue.Value);
192  }
193 
194  private static string GetAttributeString(XAttribute attribute, string defaultValue)
195  {
196  string value = attribute.Value;
197  return string.IsNullOrEmpty(value) ? defaultValue : value;
198  }
199 
200  public static string[] GetAttributeStringArray(this XElement element, string name, string[] defaultValue, bool trim = true, bool convertToLowerInvariant = false)
201  {
202  var attribute = element?.GetAttribute(name);
203  if (attribute == null) { return defaultValue; }
204 
205  string stringValue = attribute.Value;
206  if (string.IsNullOrEmpty(stringValue)) { return defaultValue; }
207 
208  string[] splitValue = stringValue.Split(',', ',');
209 
210  for (int i = 0; i < splitValue.Length; i++)
211  {
212  if (convertToLowerInvariant) { splitValue[i] = splitValue[i].ToLowerInvariant(); }
213  if (trim) { splitValue[i] = splitValue[i].Trim(); }
214  }
215 
216  return splitValue;
217  }
218 
219  public static Identifier[] GetAttributeIdentifierArray(this XElement element, Identifier[] defaultValue, params string[] matchingAttributeName)
220  {
221  if (element == null) { return defaultValue; }
222  foreach (string name in matchingAttributeName)
223  {
224  var value = element.GetAttributeIdentifierArray(name, defaultValue);
225  if (value != defaultValue) { return value; }
226  }
227  return defaultValue;
228  }
229 
230  public static Identifier[] GetAttributeIdentifierArray(this XElement element, string name, Identifier[] defaultValue, bool trim = true)
231  {
232  return element.GetAttributeStringArray(name, null, trim: trim, convertToLowerInvariant: false)
233  ?.ToIdentifiers()
234  ?? defaultValue;
235  }
236 
237  public static ImmutableHashSet<Identifier> GetAttributeIdentifierImmutableHashSet(this XElement element, string key, ImmutableHashSet<Identifier> defaultValue, bool trim = true)
238  {
239  return element.GetAttributeIdentifierArray(key, null, trim)?.ToImmutableHashSet()
240  ?? defaultValue;
241  }
242 
243  public static float GetAttributeFloat(this XElement element, float defaultValue, params string[] matchingAttributeName)
244  {
245  if (element == null) { return defaultValue; }
246 
247  foreach (string name in matchingAttributeName)
248  {
249  var attribute = element.GetAttribute(name);
250  if (attribute == null) { continue; }
251  return GetAttributeFloat(attribute, defaultValue);
252  }
253 
254  return defaultValue;
255  }
256 
257  public static float GetAttributeFloat(this XElement element, string name, float defaultValue) => GetAttributeFloat(element?.GetAttribute(name), defaultValue);
258 
259  public static float GetAttributeFloat(this XAttribute attribute, float defaultValue)
260  {
261  if (attribute == null) { return defaultValue; }
262 
263  float val = defaultValue;
264 
265  try
266  {
267  string strVal = attribute.Value;
268  if (strVal.LastOrDefault() == 'f')
269  {
270  strVal = strVal.Substring(0, strVal.Length - 1);
271  }
272  val = float.Parse(strVal, CultureInfo.InvariantCulture);
273  }
274  catch (Exception e)
275  {
276  DebugConsole.ThrowError("Error in " + attribute + "! ", e);
277  }
278 
279  return val;
280  }
281 
282  public static double GetAttributeDouble(this XElement element, string name, double defaultValue) => GetAttributeDouble(element?.GetAttribute(name), defaultValue);
283 
284  public static double GetAttributeDouble(this XAttribute attribute, double defaultValue)
285  {
286  if (attribute == null) { return defaultValue; }
287 
288  double val = defaultValue;
289  try
290  {
291  string strVal = attribute.Value;
292  if (strVal.LastOrDefault() == 'f')
293  {
294  strVal = strVal.Substring(0, strVal.Length - 1);
295  }
296  val = double.Parse(strVal, CultureInfo.InvariantCulture);
297  }
298  catch (Exception e)
299  {
300  DebugConsole.ThrowError("Error in " + attribute + "!", e);
301  }
302 
303  return val;
304  }
305 
306 
307  public static float[] GetAttributeFloatArray(this XElement element, string name, float[] defaultValue)
308  {
309  var attribute = element?.GetAttribute(name);
310  if (attribute == null) { return defaultValue; }
311 
312  string stringValue = attribute.Value;
313  if (string.IsNullOrEmpty(stringValue)) { return defaultValue; }
314 
315  string[] splitValue = stringValue.Split(',');
316  float[] floatValue = new float[splitValue.Length];
317  for (int i = 0; i < splitValue.Length; i++)
318  {
319  try
320  {
321  string strVal = splitValue[i];
322  if (strVal.LastOrDefault() == 'f')
323  {
324  strVal = strVal.Substring(0, strVal.Length - 1);
325  }
326  floatValue[i] = float.Parse(strVal, CultureInfo.InvariantCulture);
327  }
328  catch (Exception e)
329  {
330  LogAttributeError(attribute, element, e);
331  }
332  }
333 
334  return floatValue;
335  }
336 
337  public static bool TryGetAttributeInt(this XElement element, string name, out int result)
338  {
339  var attribute = element?.GetAttribute(name);
340  result = default;
341  if (attribute == null) { return false; }
342 
343  if (int.TryParse(attribute.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var intVal))
344  {
345  result = intVal;
346  return true;
347  }
348  if (float.TryParse(attribute.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var floatVal))
349  {
350  result = (int)floatVal;
351  return true;
352  }
353  return false;
354  }
355 
356  public static int GetAttributeInt(this XElement element, string name, int defaultValue) => GetAttributeInt(element?.GetAttribute(name), defaultValue);
357 
358  public static int GetAttributeInt(this XAttribute attribute, int defaultValue)
359  {
360  if (attribute == null) { return defaultValue; }
361 
362  int val = defaultValue;
363 
364  try
365  {
366  if (!Int32.TryParse(attribute.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out val))
367  {
368  val = (int)float.Parse(attribute.Value, CultureInfo.InvariantCulture);
369  }
370  }
371  catch (Exception e)
372  {
373  LogAttributeError(attribute, attribute.Parent, e);
374  }
375 
376  return val;
377  }
378 
379  public static uint GetAttributeUInt(this XElement element, string name, uint defaultValue)
380  {
381  var attribute = element?.GetAttribute(name);
382  if (attribute == null) { return defaultValue; }
383 
384  uint val = defaultValue;
385 
386  try
387  {
388  val = UInt32.Parse(attribute.Value);
389  }
390  catch (Exception e)
391  {
392  LogAttributeError(attribute, element, e);
393  }
394 
395  return val;
396  }
397 
398  public static ushort GetAttributeUInt16(this XElement element, string name, ushort defaultValue)
399  {
400  var attribute = element?.GetAttribute(name);
401  if (attribute == null) { return defaultValue; }
402 
403  ushort val = defaultValue;
404 
405  try
406  {
407  val = ushort.Parse(attribute.Value);
408  }
409  catch (Exception e)
410  {
411  LogAttributeError(attribute, element, e);
412  }
413 
414  return val;
415  }
416 
417  public static UInt64 GetAttributeUInt64(this XElement element, string name, UInt64 defaultValue)
418  {
419  var attribute = element?.GetAttribute(name);
420  if (attribute == null) { return defaultValue; }
421 
422  UInt64 val = defaultValue;
423 
424  try
425  {
426  val = UInt64.Parse(attribute.Value, NumberStyles.Any, CultureInfo.InvariantCulture);
427  }
428  catch (Exception e)
429  {
430  LogAttributeError(attribute, element, e);
431  }
432 
433  return val;
434  }
435 
436  public static Option<SerializableDateTime> GetAttributeDateTime(
437  this XElement element, string name)
438  {
439  var attribute = element?.GetAttribute(name);
440  if (attribute == null) { return Option<SerializableDateTime>.None(); }
441 
442  string attrVal = attribute.Value;
443  return SerializableDateTime.Parse(attrVal);
444  }
445 
446  public static Version GetAttributeVersion(this XElement element, string name, Version defaultValue)
447  {
448  var attribute = element?.GetAttribute(name);
449  if (attribute == null) { return defaultValue; }
450 
451  Version val = defaultValue;
452 
453  try
454  {
455  val = Version.Parse(attribute.Value);
456  }
457  catch (Exception e)
458  {
459  LogAttributeError(attribute, element, e);
460  }
461 
462  return val;
463  }
464 
465  public static int[] GetAttributeIntArray(this XElement element, string name, int[] defaultValue)
466  {
467  var attribute = element?.GetAttribute(name);
468  if (attribute == null) { return defaultValue; }
469 
470  string stringValue = attribute.Value;
471  if (string.IsNullOrEmpty(stringValue)) { return defaultValue; }
472 
473  string[] splitValue = stringValue.Split(',');
474  int[] intValue = new int[splitValue.Length];
475  for (int i = 0; i < splitValue.Length; i++)
476  {
477  try
478  {
479  int val = Int32.Parse(splitValue[i]);
480  intValue[i] = val;
481  }
482  catch (Exception e)
483  {
484  LogAttributeError(attribute, element, e);
485  }
486  }
487 
488  return intValue;
489  }
490  public static ushort[] GetAttributeUshortArray(this XElement element, string name, ushort[] defaultValue)
491  {
492  var attribute = element?.GetAttribute(name);
493  if (attribute == null) { return defaultValue; }
494 
495  string stringValue = attribute.Value;
496  if (string.IsNullOrEmpty(stringValue)) { return defaultValue; }
497 
498  string[] splitValue = stringValue.Split(',');
499  ushort[] ushortValue = new ushort[splitValue.Length];
500  for (int i = 0; i < splitValue.Length; i++)
501  {
502  try
503  {
504  ushort val = ushort.Parse(splitValue[i]);
505  ushortValue[i] = val;
506  }
507  catch (Exception e)
508  {
509  LogAttributeError(attribute, element, e);
510  }
511  }
512 
513  return ushortValue;
514  }
515 
516  private static T ParseEnumValue<T>(string value, T defaultValue, XAttribute attribute) where T : struct, Enum
517  {
518  if (Enum.TryParse(value, true, out T result))
519  {
520  return result;
521  }
522  else if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out int resultInt))
523  {
524  return Unsafe.As<int, T>(ref resultInt);
525  }
526  else
527  {
528  DebugConsole.ThrowError($"Error in {attribute}! \"{value}\" is not a valid {typeof(T).Name} value");
529  return defaultValue;
530  }
531  }
532 
533  public static T GetAttributeEnum<T>(this XElement element, string name, T defaultValue) where T : struct, Enum
534  {
535  var attr = element?.GetAttribute(name);
536  if (attr == null) { return defaultValue; }
537  return ParseEnumValue<T>(attr.Value, defaultValue, attr);
538  }
539 
540  [return: NotNullIfNotNull("defaultValue")]
541  public static T[] GetAttributeEnumArray<T>(this XElement element, string name, T[] defaultValue) where T : struct, Enum
542  {
543  string[] stringArray = element.GetAttributeStringArray(name, null);
544  if (stringArray == null) { return defaultValue; }
545  else if (stringArray.Length == 0) { return new T[0]; }
546 
547  T[] enumArray = new T[stringArray.Length];
548  var attribute = element.GetAttribute(name);
549 
550  for (int i = 0; i < stringArray.Length; i++)
551  {
552  try
553  {
554  enumArray[i] = ParseEnumValue<T>(stringArray[i].Trim(), default(T), attribute);
555  }
556  catch (Exception e)
557  {
558  LogAttributeError(attribute, element, e);
559  }
560  }
561 
562  return enumArray;
563  }
564 
565  public static bool GetAttributeBool(this XElement element, string name, bool defaultValue)
566  {
567  var attribute = element?.GetAttribute(name);
568  if (attribute == null) { return defaultValue; }
569  return attribute.GetAttributeBool(defaultValue);
570  }
571 
572  public static bool GetAttributeBool(this XAttribute attribute, bool defaultValue)
573  {
574  if (attribute == null) { return defaultValue; }
575 
576  string val = attribute.Value.ToLowerInvariant().Trim();
577  if (val == "true")
578  {
579  return true;
580  }
581  if (val == "false")
582  {
583  return false;
584  }
585 
586  DebugConsole.ThrowError("Error in " + attribute.Value.ToString() + "! \"" + val + "\" is not a valid boolean value");
587  return false;
588  }
589 
590  public static Point GetAttributePoint(this XElement element, string name, Point defaultValue)
591  {
592  var attribute = element?.GetAttribute(name);
593  if (attribute == null) { return defaultValue; }
594  return ParsePoint(attribute.Value);
595  }
596 
597  public static Vector2 GetAttributeVector2(this XElement element, string name, Vector2 defaultValue)
598  {
599  var attribute = element?.GetAttribute(name);
600  if (attribute == null) { return defaultValue; }
601  return ParseVector2(attribute.Value);
602  }
603 
604  public static Vector3 GetAttributeVector3(this XElement element, string name, Vector3 defaultValue)
605  {
606  var attribute = element?.GetAttribute(name);
607  if (attribute == null) { return defaultValue; }
608  return ParseVector3(attribute.Value);
609  }
610 
611  public static Vector4 GetAttributeVector4(this XElement element, string name, Vector4 defaultValue)
612  {
613  var attribute = element?.GetAttribute(name);
614  if (attribute == null) { return defaultValue; }
615  return ParseVector4(attribute.Value);
616  }
617 
618  public static Color GetAttributeColor(this XElement element, string name, Color defaultValue)
619  {
620  var attribute = element?.GetAttribute(name);
621  if (attribute == null) { return defaultValue; }
622  return ParseColor(attribute.Value);
623  }
624 
625  public static Color? GetAttributeColor(this XElement element, string name)
626  {
627  var attribute = element?.GetAttribute(name);
628  if (attribute == null) { return null; }
629  return ParseColor(attribute.Value);
630  }
631 
632  public static Color[] GetAttributeColorArray(this XElement element, string name, Color[] defaultValue)
633  {
634  var attribute = element?.GetAttribute(name);
635  if (attribute == null) { return defaultValue; }
636 
637  string stringValue = attribute.Value;
638  if (string.IsNullOrEmpty(stringValue)) { return defaultValue; }
639 
640  string[] splitValue = stringValue.Split(';');
641  Color[] colorValue = new Color[splitValue.Length];
642  for (int i = 0; i < splitValue.Length; i++)
643  {
644  try
645  {
646  Color val = ParseColor(splitValue[i], true);
647  colorValue[i] = val;
648  }
649  catch (Exception e)
650  {
651  LogAttributeError(attribute, element, e);
652  }
653  }
654 
655  return colorValue;
656  }
657 
658  private static void LogAttributeError(XAttribute attribute, XElement element, Exception e)
659  {
660  string elementStr = element.ToString();
661  if (elementStr.Length > 500)
662  {
663  DebugConsole.ThrowError($"Error when reading attribute \"{attribute}\"!", e);
664  }
665  else
666  {
667  DebugConsole.ThrowError($"Error when reading attribute \"{attribute.Name}\" from {elementStr}!", e);
668  }
669  }
670 
671 #if CLIENT
672  public static KeyOrMouse GetAttributeKeyOrMouse(this XElement element, string name, KeyOrMouse defaultValue)
673  {
674  string strValue = element.GetAttributeString(name, defaultValue?.ToString() ?? "");
675  if (Enum.TryParse(strValue, true, out Microsoft.Xna.Framework.Input.Keys key))
676  {
677  return key;
678  }
679  else if (Enum.TryParse(strValue, out MouseButton mouseButton))
680  {
681  return mouseButton;
682  }
683  else if (int.TryParse(strValue, NumberStyles.Any, CultureInfo.InvariantCulture, out int mouseButtonInt) &&
684  Enum.GetValues<MouseButton>().Contains((MouseButton)mouseButtonInt))
685  {
686  return (MouseButton)mouseButtonInt;
687  }
688  else if (string.Equals(strValue, "LeftMouse", StringComparison.OrdinalIgnoreCase))
689  {
690  return !PlayerInput.MouseButtonsSwapped() ? MouseButton.PrimaryMouse : MouseButton.SecondaryMouse;
691  }
692  else if (string.Equals(strValue, "RightMouse", StringComparison.OrdinalIgnoreCase))
693  {
694  return !PlayerInput.MouseButtonsSwapped() ? MouseButton.SecondaryMouse : MouseButton.PrimaryMouse;
695  }
696  return defaultValue;
697  }
698 #endif
699 
700  public static Rectangle GetAttributeRect(this XElement element, string name, Rectangle defaultValue)
701  {
702  var attribute = element?.GetAttribute(name);
703  if (attribute == null) { return defaultValue; }
704  return ParseRect(attribute.Value, false);
705  }
706 
707  //TODO: nested tuples and and n-uples where n!=2 are unsupported
708  public static (T1, T2) GetAttributeTuple<T1, T2>(this XElement element, string name, (T1, T2) defaultValue)
709  {
710  string strValue = element.GetAttributeString(name, $"({defaultValue.Item1}, {defaultValue.Item2})").Trim();
711 
712  return ParseTuple(strValue, defaultValue);
713  }
714 
715  public static (T1, T2)[] GetAttributeTupleArray<T1, T2>(this XElement element, string name,
716  (T1, T2)[] defaultValue)
717  {
718  var attribute = element?.GetAttribute(name);
719  if (attribute == null) { return defaultValue; }
720 
721  string stringValue = attribute.Value;
722  if (string.IsNullOrEmpty(stringValue)) { return defaultValue; }
723 
724  return stringValue.Split(';').Select(s => ParseTuple<T1, T2>(s, default)).ToArray();
725  }
726 
727  public static Range<int> GetAttributeRange(this XElement element, string name, Range<int> defaultValue)
728  {
729  var attribute = element?.GetAttribute(name);
730  if (attribute is null) { return defaultValue; }
731 
732  string stringValue = attribute.Value;
733  return string.IsNullOrEmpty(stringValue) ? defaultValue : ParseRange(stringValue);
734  }
735 
736  public static string ElementInnerText(this XElement el)
737  {
738  StringBuilder str = new StringBuilder();
739  foreach (XNode element in el.DescendantNodes().Where(x => x.NodeType == XmlNodeType.Text))
740  {
741  str.Append(element.ToString());
742  }
743  return str.ToString();
744  }
745 
746  public static string PointToString(Point point)
747  {
748  return point.X.ToString() + "," + point.Y.ToString();
749  }
750 
751  public static string Vector2ToString(Vector2 vector)
752  {
753  return vector.X.ToString("G", CultureInfo.InvariantCulture) + "," + vector.Y.ToString("G", CultureInfo.InvariantCulture);
754  }
755 
756  public static string Vector3ToString(Vector3 vector, string format = "G")
757  {
758  return vector.X.ToString(format, CultureInfo.InvariantCulture) + "," +
759  vector.Y.ToString(format, CultureInfo.InvariantCulture) + "," +
760  vector.Z.ToString(format, CultureInfo.InvariantCulture);
761  }
762 
763  public static string Vector4ToString(Vector4 vector, string format = "G")
764  {
765  return vector.X.ToString(format, CultureInfo.InvariantCulture) + "," +
766  vector.Y.ToString(format, CultureInfo.InvariantCulture) + "," +
767  vector.Z.ToString(format, CultureInfo.InvariantCulture) + "," +
768  vector.W.ToString(format, CultureInfo.InvariantCulture);
769  }
770 
771  [Obsolete("Prefer XMLExtensions.ToStringHex")]
772  public static string ColorToString(Color color)
773  => $"{color.R},{color.G},{color.B},{color.A}";
774 
775  public static string ToStringHex(this Color color)
776  => $"#{color.R:X2}{color.G:X2}{color.B:X2}"
777  + ((color.A < 255) ? $"{color.A:X2}" : "");
778 
779  public static string RectToString(Rectangle rect)
780  {
781  return rect.X + "," + rect.Y + "," + rect.Width + "," + rect.Height;
782  }
783 
784  public static (T1, T2) ParseTuple<T1, T2>(string strValue, (T1, T2) defaultValue)
785  {
786  strValue = strValue.Trim();
787  //require parentheses
788  if (strValue[0] != '(' || strValue[^1] != ')') { return defaultValue; }
789  //remove parentheses
790  strValue = strValue[1..^1];
791 
792  string[] elems = strValue.Split(',');
793  if (elems.Length != 2) { return defaultValue; }
794 
795  return ((T1)Converters[typeof(T1)].Invoke(elems[0], defaultValue.Item1),
796  (T2)Converters[typeof(T2)].Invoke(elems[1], defaultValue.Item2));
797  }
798 
799  public static Point ParsePoint(string stringPoint, bool errorMessages = true)
800  {
801  string[] components = stringPoint.Split(',');
802  Point point = Point.Zero;
803 
804  if (components.Length != 2)
805  {
806  if (!errorMessages) { return point; }
807  DebugConsole.ThrowError("Failed to parse the string \"" + stringPoint + "\" to Vector2");
808  return point;
809  }
810 
811  int.TryParse(components[0], NumberStyles.Any, CultureInfo.InvariantCulture, out point.X);
812  int.TryParse(components[1], NumberStyles.Any, CultureInfo.InvariantCulture, out point.Y);
813  return point;
814  }
815 
816  public static Vector2 ParseVector2(string stringVector2, bool errorMessages = true)
817  {
818  string[] components = stringVector2.Split(',');
819 
820  Vector2 vector = Vector2.Zero;
821 
822  if (components.Length != 2)
823  {
824  if (!errorMessages) { return vector; }
825  DebugConsole.ThrowError("Failed to parse the string \"" + stringVector2 + "\" to Vector2");
826  return vector;
827  }
828 
829  float.TryParse(components[0], NumberStyles.Any, CultureInfo.InvariantCulture, out vector.X);
830  float.TryParse(components[1], NumberStyles.Any, CultureInfo.InvariantCulture, out vector.Y);
831 
832  return vector;
833  }
834 
835  public static Vector3 ParseVector3(string stringVector3, bool errorMessages = true)
836  {
837  string[] components = stringVector3.Split(',');
838 
839  Vector3 vector = Vector3.Zero;
840 
841  if (components.Length != 3)
842  {
843  if (!errorMessages) { return vector; }
844  DebugConsole.ThrowError("Failed to parse the string \"" + stringVector3 + "\" to Vector3");
845  return vector;
846  }
847 
848  Single.TryParse(components[0], NumberStyles.Any, CultureInfo.InvariantCulture, out vector.X);
849  Single.TryParse(components[1], NumberStyles.Any, CultureInfo.InvariantCulture, out vector.Y);
850  Single.TryParse(components[2], NumberStyles.Any, CultureInfo.InvariantCulture, out vector.Z);
851 
852  return vector;
853  }
854 
855  public static Vector4 ParseVector4(string stringVector4, bool errorMessages = true)
856  {
857  string[] components = stringVector4.Split(',');
858 
859  Vector4 vector = Vector4.Zero;
860 
861  if (components.Length < 3)
862  {
863  if (errorMessages) { DebugConsole.ThrowError("Failed to parse the string \"" + stringVector4 + "\" to Vector4"); }
864  return vector;
865  }
866 
867  Single.TryParse(components[0], NumberStyles.Float, CultureInfo.InvariantCulture, out vector.X);
868  Single.TryParse(components[1], NumberStyles.Float, CultureInfo.InvariantCulture, out vector.Y);
869  Single.TryParse(components[2], NumberStyles.Float, CultureInfo.InvariantCulture, out vector.Z);
870  if (components.Length > 3)
871  {
872  Single.TryParse(components[3], NumberStyles.Float, CultureInfo.InvariantCulture, out vector.W);
873  }
874 
875  return vector;
876  }
877 
878  private static readonly ImmutableDictionary<Identifier, Color> monoGameColors =
879  typeof(Color)
880  .GetProperties(BindingFlags.Static | BindingFlags.Public)
881  .Where(p => p.PropertyType == typeof(Color))
882  .Select(p => (p.Name.ToIdentifier(), p.GetValueFromStaticProperty<Color>()))
883  .ToImmutableDictionary();
884 
885  public static Color ParseColor(string stringColor, bool errorMessages = true)
886  {
887  if (stringColor.StartsWith("gui.", StringComparison.OrdinalIgnoreCase))
888  {
889 #if CLIENT
890  Identifier colorName = stringColor.Substring(4).ToIdentifier();
891  if (GUIStyle.Colors.TryGetValue(colorName, out GUIColor guiColor))
892  {
893  return guiColor.Value;
894  }
895 #endif
896  return Color.White;
897  }
898  if (stringColor.StartsWith("faction.", StringComparison.OrdinalIgnoreCase))
899  {
900  Identifier factionId = stringColor.Substring(8).ToIdentifier();
901  if (FactionPrefab.Prefabs.TryGet(factionId, out var faction))
902  {
903  return faction.IconColor;
904  }
905  return Color.White;
906  }
907 
908  if (monoGameColors.TryGetValue(stringColor.ToIdentifier(), out var monoGameColor))
909  {
910  return monoGameColor;
911  }
912 
913  string[] strComponents = stringColor.Split(',');
914 
915  Color color = Color.White;
916 
917  float[] components = new float[4] { 1.0f, 1.0f, 1.0f, 1.0f };
918 
919  if (strComponents.Length == 1)
920  {
921  bool altParseFailed = true;
922  stringColor = stringColor.Trim();
923  if (stringColor.Length > 0 && stringColor[0] == '#')
924  {
925  stringColor = stringColor.Substring(1);
926 
927  if (int.TryParse(stringColor, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int colorInt))
928  {
929  if (stringColor.Length == 6)
930  {
931  colorInt = (colorInt << 8) | 0xff;
932  }
933  components[0] = ((float)((colorInt & 0xff000000) >> 24)) / 255.0f;
934  components[1] = ((float)((colorInt & 0x00ff0000) >> 16)) / 255.0f;
935  components[2] = ((float)((colorInt & 0x0000ff00) >> 8)) / 255.0f;
936  components[3] = ((float)(colorInt & 0x000000ff)) / 255.0f;
937 
938  altParseFailed = false;
939  }
940  }
941  else if (stringColor.Length > 0 && stringColor[0] == '{')
942  {
943  stringColor = stringColor.Substring(1, stringColor.Length-2);
944 
945  string[] mgComponents = stringColor.Split(' ');
946  if (mgComponents.Length == 4)
947  {
948  altParseFailed = false;
949 
950  string[] expectedPrefixes = {"R:", "G:", "B:", "A:"};
951  for (int i = 0; i < 4; i++)
952  {
953  if (mgComponents[i].StartsWith(expectedPrefixes[i], StringComparison.OrdinalIgnoreCase))
954  {
955  string strToParse = mgComponents[i]
956  .Remove(expectedPrefixes[i], StringComparison.OrdinalIgnoreCase)
957  .Trim();
958  int val = 0;
959  altParseFailed |= !int.TryParse(strToParse, out val);
960  components[i] = ((float) val) / 255f;
961  }
962  else
963  {
964  altParseFailed = true;
965  break;
966  }
967  }
968  }
969  }
970 
971  if (altParseFailed)
972  {
973  if (errorMessages) { DebugConsole.ThrowError("Failed to parse the string \"" + stringColor + "\" to Color"); }
974  return Color.White;
975  }
976  }
977  else
978  {
979  for (int i = 0; i < 4 && i < strComponents.Length; i++)
980  {
981  float.TryParse(strComponents[i], NumberStyles.Float, CultureInfo.InvariantCulture, out components[i]);
982  }
983 
984  if (components.Any(c => c > 1.0f))
985  {
986  for (int i = 0; i < 4; i++)
987  {
988  components[i] = components[i] / 255.0f;
989  }
990  //alpha defaults to 1.0 if not given
991  if (strComponents.Length < 4) components[3] = 1.0f;
992  }
993  }
994 
995  return new Color(components[0], components[1], components[2], components[3]);
996  }
997 
998  public static Rectangle ParseRect(string stringRect, bool requireSize, bool errorMessages = true)
999  {
1000  string[] strComponents = stringRect.Split(',');
1001  if ((strComponents.Length < 3 && requireSize) || strComponents.Length < 2)
1002  {
1003  if (errorMessages) { DebugConsole.ThrowError("Failed to parse the string \"" + stringRect + "\" to Rectangle"); }
1004  return new Rectangle(0, 0, 0, 0);
1005  }
1006 
1007  int[] components = new int[4] { 0, 0, 0, 0 };
1008  for (int i = 0; i < 4 && i < strComponents.Length; i++)
1009  {
1010  int.TryParse(strComponents[i], out components[i]);
1011  }
1012 
1013  return new Rectangle(components[0], components[1], components[2], components[3]);
1014  }
1015 
1016  public static float[] ParseFloatArray(string[] stringArray)
1017  {
1018  if (stringArray == null || stringArray.Length == 0) return null;
1019 
1020  float[] floatArray = new float[stringArray.Length];
1021  for (int i = 0; i < floatArray.Length; i++)
1022  {
1023  floatArray[i] = 0.0f;
1024  Single.TryParse(stringArray[i], NumberStyles.Float, CultureInfo.InvariantCulture, out floatArray[i]);
1025  }
1026 
1027  return floatArray;
1028  }
1029 
1030  // parse a range string, e.g "1-3" or "3"
1031  public static Range<int> ParseRange(string rangeString)
1032  {
1033  if (string.IsNullOrWhiteSpace(rangeString)) { return GetDefault(rangeString); }
1034 
1035  string[] split = rangeString.Split('-');
1036  return split.Length switch
1037  {
1038  1 when TryParseInt(split[0], out int value) => new Range<int>(value, value),
1039  2 when TryParseInt(split[0], out int min) && TryParseInt(split[1], out int max) && min < max => new Range<int>(min, max),
1040  _ => GetDefault(rangeString)
1041  };
1042 
1043  static bool TryParseInt(string value, out int result)
1044  {
1045  if (!string.IsNullOrWhiteSpace(value))
1046  {
1047  return int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out result);
1048  }
1049 
1050  result = default;
1051  return false;
1052  }
1053 
1054  static Range<int> GetDefault(string rangeString)
1055  {
1056  DebugConsole.ThrowError($"Error parsing range: \"{rangeString}\" (using default value 0-99)");
1057  return new Range<int>(0, 99);
1058  }
1059  }
1060 
1061  public static Identifier VariantOf(this XElement element) =>
1062  element.GetAttributeIdentifier("inherit", element.GetAttributeIdentifier("variantof", ""));
1063 
1064  public static bool IsOverride(this XElement element) => element.NameAsIdentifier() == "override";
1065 
1070  public static XElement GetRootExcludingOverride(this XDocument doc) => doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root;
1071 
1072  public static XElement FirstElement(this XElement element) => element.Elements().FirstOrDefault();
1073 
1074  public static XAttribute GetAttribute(this XElement element, string name, StringComparison comparisonMethod = StringComparison.OrdinalIgnoreCase) => element.GetAttribute(a => a.Name.ToString().Equals(name, comparisonMethod));
1075 
1076  public static bool TrySetAttributeValue(this XElement element, string name, object value, StringComparison comparisonMethod = StringComparison.OrdinalIgnoreCase)
1077  {
1078  var attribute = GetAttribute(element, name, comparisonMethod);
1079  if (attribute == null) { return false; }
1080  attribute.SetValue(value);
1081  return true;
1082  }
1083 
1084  public static void SetAttribute(this XElement element, string name, object value)
1085  {
1086  if (!TrySetAttributeValue(element, name, value))
1087  {
1088  element.SetAttributeValue(name, value);
1089  }
1090  }
1091 
1092  public static XAttribute GetAttribute(this XElement element, Identifier name) => element.GetAttribute(name.Value, StringComparison.OrdinalIgnoreCase);
1093 
1094  public static XAttribute GetAttribute(this XElement element, Func<XAttribute, bool> predicate) => element.Attributes().FirstOrDefault(predicate);
1095 
1099  public static XElement GetChildElement(this XContainer container, string name, StringComparison comparisonMethod = StringComparison.OrdinalIgnoreCase) => container.Elements().FirstOrDefault(e => e.Name.ToString().Equals(name, comparisonMethod));
1100 
1104  public static IEnumerable<XElement> GetChildElements(this XContainer container, string name, StringComparison comparisonMethod = StringComparison.OrdinalIgnoreCase) => container.Elements().Where(e => e.Name.ToString().Equals(name, comparisonMethod));
1105 
1106  public static IEnumerable<XElement> GetChildElements(this XContainer container, params string[] names)
1107  {
1108  return names.SelectMany(name => container.GetChildElements(name));
1109  }
1110 
1111  public static bool ComesAfter(this XElement element, XElement other)
1112  {
1113  if (element.Parent != other.Parent) { return false; }
1114  foreach (var child in element.Parent.Elements())
1115  {
1116  if (child == element) { return false; }
1117  if (child == other) { return true; }
1118  }
1119  return false;
1120  }
1121 
1122  public static Identifier NameAsIdentifier(this XElement elem)
1123  {
1124  return elem.Name.LocalName.ToIdentifier();
1125  }
1126 
1127  public static Identifier NameAsIdentifier(this XAttribute attr)
1128  {
1129  return attr.Name.LocalName.ToIdentifier();
1130  }
1131  }
1132 }