context: /*  This file is part of Chummer5a.
   *
   *  Chummer5a is free software: you can redistribute it and/or modify
   *  it under the terms of the GNU General Public License as published by
   *  the Free Software Foundation, either version 3 of the License, or
   *  (at your option) any later version.
   *
   *  Chummer5a is distributed in the hope that it will be useful,
   *  but WITHOUT ANY WARRANTY; without even the implied warranty of
   *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   *  GNU General Public License for more details.
   *
   *  You should have received a copy of the GNU General Public License
   *  along with Chummer5a.  If not, see <http://www.gnu.org/licenses/>.
   *
   *  You can obtain the full source code for Chummer5a at
   *  https://github.com/chummer5a/chummer5a
   */
  using System;
  using System.Collections.Generic;
  using System.IO;
  using System.Linq;
  using System.Runtime.CompilerServices;
  using System.Text;
  using System.Text.RegularExpressions;
  using System.Threading.Tasks;
  using System.Xml;
  using System.Xml.XPath;
  using NLog;
  
  namespace Chummer
  {
      // ReSharper disable InconsistentNaming
      public static class XmlManager
      {
          /// <summary>
          /// Used to cache XML files so that they do not need to be loaded and translated each time an object wants the file.
          /// </summary>
          private sealed class XmlReference : IHasLockObject
          {
              /// <summary>
              /// Whether or not the XML content has been successfully checked for duplicate guids.
              /// </summary>
              public bool DuplicatesChecked
              {
                  get
                  {
                      using (EnterReadLock.Enter(LockObject))
                          return _blnDuplicatesChecked;
                  }
                  set
                  {
                      using (EnterReadLock.Enter(LockObject))
                      {
                          if (_blnDuplicatesChecked == value)
                              return;
                          using (LockObject.EnterWriteLock())
                              _blnDuplicatesChecked = value;
                      }
                  }
              }
  
              private XmlDocument _xmlContent = new XmlDocument { XmlResolver = null };
              private XPathDocument _objXPathContent;
              private bool _blnDuplicatesChecked = Utils.IsUnitTest;
              private bool _blnInitialLoadComplete;
  
              /// <summary>
              /// XmlDocument that is created by merging the base data file and data translation file. Does not include custom content since this must be loaded each time.
              /// </summary>
              public XmlDocument GetXmlContent()
              {
                  while (true)
                  {
                      bool blnLoadComplete;
                      using (EnterReadLock.Enter(LockObject))
                          blnLoadComplete = _blnInitialLoadComplete;
                      if (blnLoadComplete)
                          break;
                      Utils.SafeSleep();
                  }
                  using (EnterReadLock.Enter(LockObject))
                      return _xmlContent;
              }
  
              /// <summary>
              /// XmlDocument that is created by merging the base data file and data translation file. Does not include custom content since this must be loaded each time.
              /// </summary>
              public async ValueTask<XmlDocument> GetXmlContentAsync()
              {
                  while (true)
                  {
                      bool blnLoadComplete;
                      using (await EnterReadLock.EnterAsync(LockObject).ConfigureAwait(false))
                          blnLoadComplete = _blnInitialLoadComplete;
                      if (blnLoadComplete)
                          break;
                      await Utils.SafeSleepAsync();
                  }
                  using (await EnterReadLock.EnterAsync(LockObject).ConfigureAwait(false))
                      return _xmlContent;
              }
  
              /// <summary>
              /// Set the XmlDocument that is created by merging the base data file and data translation file. Does not include custom content since this must be loaded each time.
              /// </summary>
              public void SetXmlContent(XmlDocument objContent)
              {
                  using (EnterReadLock.Enter(LockObject))
                  {
                      if (objContent == _xmlContent)
                          return;
                      using (LockObject.EnterWriteLock())
                      {
                          _xmlContent = objContent;
                          if (objContent != null)
                          {
                              _blnInitialLoadComplete = true;
                              using (MemoryStream memStream = new MemoryStream())
                              {
                                  objContent.Save(memStream);
                                  memStream.Position = 0;
                                  using (XmlReader objXmlReader
                                         = XmlReader.Create(memStream, GlobalSettings.SafeXmlReaderSettings))
                                      _objXPathContent = new XPathDocument(objXmlReader);
                              }
                          }
                          else
                              _objXPathContent = null;
                      }
                  }
              }
  
              /// <summary>
              /// Set the XmlDocument that is created by merging the base data file and data translation file. Does not include custom content since this must be loaded each time.
              /// </summary>
              public async ValueTask SetXmlContentAsync(XmlDocument objContent)
              {
                  using (await EnterReadLock.EnterAsync(LockObject).ConfigureAwait(false))
                  {
                      if (objContent == _xmlContent)
                          return;
                      IAsyncDisposable objLocker = await LockObject.EnterWriteLockAsync().ConfigureAwait(false);
                      try
                      {
                          _xmlContent = objContent;
                          if (objContent != null)
                          {
                              _blnInitialLoadComplete = true;
                              using (MemoryStream memStream = new MemoryStream())
                              {
                                  objContent.Save(memStream);
                                  memStream.Position = 0;
                                  using (XmlReader objXmlReader
                                         = XmlReader.Create(memStream, GlobalSettings.SafeXmlReaderSettings))
                                      _objXPathContent = new XPathDocument(objXmlReader);
                              }
                          }
                          else
                              _objXPathContent = null;
                      }
                      finally
                      {
                          await objLocker.DisposeAsync().ConfigureAwait(false);
                      }
                  }
              }
  
              /// <summary>
              /// XmlContent, but in a form that is much faster to navigate
              /// XPathDocuments are usually faster than XmlDocuments, but are read-only and take longer to load if live custom data is enabled
              /// </summary>
              public XPathDocument GetXPathContent()
              {
                  while (true)
                  {
                      bool blnLoadComplete;
                      using (EnterReadLock.Enter(LockObject))
                          blnLoadComplete = _blnInitialLoadComplete;
                      if (blnLoadComplete)
                          break;
                      Utils.SafeSleep();
                  }
                  using (EnterReadLock.Enter(LockObject))
                      return _objXPathContent;
              }
  
              /// <summary>
              /// XmlContent, but in a form that is much faster to navigate
              /// XPathDocuments are usually faster than XmlDocuments, but are read-only and take longer to load if live custom data is enabled
              /// </summary>
              public async ValueTask<XPathDocument> GetXPathContentAsync()
              {
                  while (true)
                  {
                      bool blnLoadComplete;
                      using (await EnterReadLock.EnterAsync(LockObject).ConfigureAwait(false))
                          blnLoadComplete = _blnInitialLoadComplete;
                      if (blnLoadComplete)
                          break;
                      await Utils.SafeSleepAsync();
                  }
                  using (await EnterReadLock.EnterAsync(LockObject).ConfigureAwait(false))
                      return _objXPathContent;
              }
  
              /// <inheritdoc />
              public void Dispose()
              {
                  LockObject.Dispose();
              }
  
              /// <inheritdoc />
              public ValueTask DisposeAsync()
              {
                  return LockObject.DisposeAsync();
              }
  
              /// <inheritdoc />
              public AsyncFriendlyReaderWriterLock LockObject { get; } = new AsyncFriendlyReaderWriterLock();
          }
  
          private static readonly string s_StrBaseDataPath = Path.Combine(Utils.GetStartupPath, "data");
          private static readonly LockingDictionary<KeyArray<string>, XmlReference> s_DicXmlDocuments =
              new LockingDictionary<KeyArray<string>, XmlReference>(); // Key is language + array of all file paths for the complete combination of data used
          private static readonly AsyncFriendlyReaderWriterLock s_objDataDirectoriesLock = new AsyncFriendlyReaderWriterLock();
          private static readonly HashSet<string> s_SetDataDirectories = new HashSet<string>(s_StrBaseDataPath.Yield());
          private static readonly Dictionary<string, HashSet<string>> s_DicPathsWithCustomFiles = new Dictionary<string, HashSet<string>>();
  
          private static Logger Log { get; } = LogManager.GetCurrentClassLogger();
  
          #region Methods
  
          static XmlManager()
          {
              if (Utils.IsDesignerMode || Utils.IsRunningInVisualStudio)
                  return;
              foreach (string strFileName in Utils.BasicDataFileNames)
              {
                  if (!s_DicPathsWithCustomFiles.TryGetValue(strFileName, out HashSet<string> setLoop))
                  {
                      setLoop = new HashSet<string>();
                      s_DicPathsWithCustomFiles.Add(strFileName, setLoop);
                  }
                  else
                      setLoop.Clear();
                  setLoop.AddRange(CompileRelevantCustomDataPaths(strFileName, s_SetDataDirectories));
              }
          }
  
          public static void RebuildDataDirectoryInfo(IEnumerable<CustomDataDirectoryInfo> lstCustomDirectories)
          {
              using (s_objDataDirectoriesLock.EnterWriteLock())
              {
                  foreach (XmlReference objReference in s_DicXmlDocuments.Values)
                      objReference.Dispose();
                  s_DicXmlDocuments.Clear();
                  s_SetDataDirectories.Clear();
                  s_SetDataDirectories.Add(s_StrBaseDataPath);
                  foreach (CustomDataDirectoryInfo objCustomDataDirectory in lstCustomDirectories)
                  {
                      s_SetDataDirectories.Add(objCustomDataDirectory.DirectoryPath);
                  }
  
                  if (!Utils.IsDesignerMode && !Utils.IsRunningInVisualStudio)
                  {
                      foreach (string strFileName in Utils.BasicDataFileNames)
                      {
                          if (!s_DicPathsWithCustomFiles.TryGetValue(strFileName, out HashSet<string> setLoop))
                          {
                              setLoop = new HashSet<string>();
                              s_DicPathsWithCustomFiles.Add(strFileName, setLoop);
                          }
                          else
                              setLoop.Clear();
  
                          setLoop.AddRange(CompileRelevantCustomDataPaths(strFileName, s_SetDataDirectories));
                      }
                  }
              }
          }
  
          public static async ValueTask RebuildDataDirectoryInfoAsync(IEnumerable<CustomDataDirectoryInfo> lstCustomDirectories)
          {
              IAsyncDisposable objLocker = await s_objDataDirectoriesLock.EnterWriteLockAsync().ConfigureAwait(false);
              try
              {
                  await s_DicXmlDocuments.ForEachAsync(async kvpDocument =>
                                                           await kvpDocument.Value.DisposeAsync().ConfigureAwait(false))
                                         .ConfigureAwait(false);
                  await s_DicXmlDocuments.ClearAsync().ConfigureAwait(false);
                  s_SetDataDirectories.Clear();
                  s_SetDataDirectories.Add(s_StrBaseDataPath);
                  foreach (CustomDataDirectoryInfo objCustomDataDirectory in lstCustomDirectories)
                  {
                      s_SetDataDirectories.Add(objCustomDataDirectory.DirectoryPath);
                  }
  
                  if (!Utils.IsDesignerMode && !Utils.IsRunningInVisualStudio)
                  {
                      foreach (string strFileName in Utils.BasicDataFileNames)
                      {
                          if (!s_DicPathsWithCustomFiles.TryGetValue(strFileName, out HashSet<string> setLoop))
                          {
                              setLoop = new HashSet<string>();
                              s_DicPathsWithCustomFiles.Add(strFileName, setLoop);
                          }
                          else
                              setLoop.Clear();
  
                          setLoop.AddRange(CompileRelevantCustomDataPaths(strFileName, s_SetDataDirectories));
                      }
                  }
              }
              finally
              {
                  await objLocker.DisposeAsync().ConfigureAwait(false);
              }
          }
  
          /// <summary>
          /// Load the selected XML file and its associated custom files synchronously.
          /// XPathDocuments are usually faster than XmlDocuments, but are read-only and take longer to load if live custom data is enabled
          /// Returns a new XPathNavigator associated with the XPathDocument so that multiple threads each get their own navigator if they're called on the same file
          /// </summary>
          /// <param name="strFileName">Name of the XML file to load.</param>
          /// <param name="lstEnabledCustomDataPaths">List of enabled custom data directory paths in their load order</param>
          /// <param name="strLanguage">Language in which to load the data document.</param>
          /// <param name="blnLoadFile">Whether to force reloading content even if the file already exists.</param>
          [MethodImpl(MethodImplOptions.AggressiveInlining)]
          public static XPathNavigator LoadXPath(string strFileName, IReadOnlyList<string> lstEnabledCustomDataPaths = null, string strLanguage = "", bool blnLoadFile = false)
          {
              return LoadXPathCoreAsync(true, strFileName, lstEnabledCustomDataPaths, strLanguage, blnLoadFile).GetAwaiter().GetResult();
          }
  
          /// <summary>
          /// Load the selected XML file and its associated custom files asynchronously.
          /// XPathDocuments are usually faster than XmlDocuments, but are read-only and take longer to load if live custom data is enabled
          /// Returns a new XPathNavigator associated with the XPathDocument so that multiple threads each get their own navigator if they're called on the same file
          /// </summary>
          /// <param name="strFileName">Name of the XML file to load.</param>
          /// <param name="lstEnabledCustomDataPaths">List of enabled custom data directory paths in their load order</param>
          /// <param name="strLanguage">Language in which to load the data document.</param>
          /// <param name="blnLoadFile">Whether to force reloading content even if the file already exists.</param>
          public static Task<XPathNavigator> LoadXPathAsync(string strFileName, IReadOnlyList<string> lstEnabledCustomDataPaths = null, string strLanguage = "", bool blnLoadFile = false)
          {
              return LoadXPathCoreAsync(false, strFileName, lstEnabledCustomDataPaths, strLanguage, blnLoadFile);
          }
  
          /// <summary>
          /// Core of the method to load an XML file and its associated custom file.
          /// Uses flag hack method design outlined here to avoid locking:
          /// https://docs.microsoft.com/en-us/archive/msdn-magazine/2015/july/async-programming-brownfield-async-development
          /// </summary>
          /// <param name="blnSync">Flag for whether method should always use synchronous code or not.</param>
          /// <param name="strFileName">Name of the XML file to load.</param>
          /// <param name="lstEnabledCustomDataPaths">List of enabled custom data directory paths in their load order</param>
          /// <param name="strLanguage">Language in which to load the data document.</param>
          /// <param name="blnLoadFile">Whether to force reloading content even if the file already exists.</param>
          /// <returns></returns>
          private static async Task<XPathNavigator> LoadXPathCoreAsync(bool blnSync, string strFileName, IReadOnlyList<string> lstEnabledCustomDataPaths = null, string strLanguage = "", bool blnLoadFile = false)
          {
              strFileName = Path.GetFileName(strFileName);
              // Wait to make sure our data directories are loaded before proceeding
              // ReSharper disable once MethodHasAsyncOverload
              using (blnSync ? EnterReadLock.Enter(s_objDataDirectoriesLock) : await EnterReadLock.EnterAsync(s_objDataDirectoriesLock))
              {
                  if (string.IsNullOrEmpty(strLanguage))
                      strLanguage = GlobalSettings.Language;
  
                  string strPath;
                  if (Utils.BasicDataFileNames.Contains(strFileName))
                      strPath = Path.Combine(s_StrBaseDataPath, strFileName);
                  else
                  {
                      strPath = FetchBaseFileFromCustomDataPaths(strFileName, lstEnabledCustomDataPaths);
                      if (string.IsNullOrEmpty(strPath))
                      {
                          // We don't actually have such a file
                          Utils.BreakIfDebug();
                          return new XmlDocument {XmlResolver = null}.CreateNavigator();
                      }
                  }
  
                  string[] astrRelevantCustomDataPaths;
                  if (lstEnabledCustomDataPaths == null)
                      astrRelevantCustomDataPaths = Array.Empty<string>();
                  else if (s_DicPathsWithCustomFiles.TryGetValue(strFileName, out HashSet<string> setDirectoriesPossible))
                      astrRelevantCustomDataPaths = lstEnabledCustomDataPaths
                                                    .Where(x => setDirectoriesPossible.Contains(x)).ToArray();
                  else
                  {
                      astrRelevantCustomDataPaths = CompileRelevantCustomDataPaths(strFileName, lstEnabledCustomDataPaths)
                          .ToArray();
                      if (astrRelevantCustomDataPaths.Length > 0 && !Utils.IsDesignerMode
                                                                 && !Utils.IsRunningInVisualStudio)
                      {
                          IDisposable objLocker = null;
                          IAsyncDisposable objLockerAsync = null;
                          if (blnSync)
                              // ReSharper disable once MethodHasAsyncOverload
                              objLocker = s_objDataDirectoriesLock.EnterWriteLock();
                          else
                              objLockerAsync = await s_objDataDirectoriesLock.EnterWriteLockAsync().ConfigureAwait(false);
                          try
                          {
                              if (!s_DicPathsWithCustomFiles.TryGetValue(strFileName, out HashSet<string> setLoop))
                              {
                                  setLoop = new HashSet<string>();
                                  s_DicPathsWithCustomFiles.Add(strFileName, setLoop);
                              }
  
                              setLoop.AddRange(astrRelevantCustomDataPaths);
                          }
                          finally
                          {
                              if (blnSync)
                                  // ReSharper disable once MethodHasAsyncOverload
                                  objLocker.Dispose();
                              else
                                  await objLockerAsync.DisposeAsync().ConfigureAwait(false);
                          }
                      }
                  }
  
                  List<string> lstKey = new List<string>(2 + astrRelevantCustomDataPaths.Length) {strLanguage, strPath};
                  lstKey.AddRange(astrRelevantCustomDataPaths);
                  KeyArray<string> objDataKey = new KeyArray<string>(lstKey);
  
                  // Look to see if this XmlDocument is already loaded.
                  XmlDocument xmlDocumentOfReturn = null;
                  XmlReference xmlReferenceOfReturn = null;
                  bool blnDoLoad = blnLoadFile || (GlobalSettings.LiveCustomData && strFileName != "improvements.xml");
                  if (!blnDoLoad)
                  {
                      if (blnSync)
                          blnDoLoad = !s_DicXmlDocuments.TryGetValue(objDataKey, out xmlReferenceOfReturn);
                      else
                      {
                          bool blnLoadSuccess;
                          (blnLoadSuccess, xmlReferenceOfReturn) = await s_DicXmlDocuments.TryGetValueAsync(objDataKey).ConfigureAwait(false);
                          blnDoLoad = !blnLoadSuccess;
                      }
                  }
  
                  if (blnDoLoad)
                  {
                      // The file was not found in the reference list, so it must be loaded.
                      bool blnLoadSuccess;
                      if (blnSync)
                      {
                          // ReSharper disable once MethodHasAsyncOverload
                          xmlDocumentOfReturn = Load(strFileName, lstEnabledCustomDataPaths, strLanguage, blnLoadFile);
                          blnLoadSuccess = s_DicXmlDocuments.TryGetValue(objDataKey, out xmlReferenceOfReturn);
                      }
                      else
                      {
                          xmlDocumentOfReturn
                              = await LoadAsync(strFileName, lstEnabledCustomDataPaths, strLanguage, blnLoadFile).ConfigureAwait(false);
                          // Need this conditional so that we actually await the line above before we check to see if the load was successful or not
                          // ReSharper disable once ConvertIfStatementToConditionalTernaryExpression
                          if (xmlDocumentOfReturn != null)
                          {
                              (blnLoadSuccess, xmlReferenceOfReturn)
                                  = await s_DicXmlDocuments.TryGetValueAsync(objDataKey).ConfigureAwait(false);
                          }
                          else
                          {
                              xmlReferenceOfReturn = null;
                              blnLoadSuccess = false;
                          }
                      }
  
                      if (!blnLoadSuccess)
                      {
                          Utils.BreakIfDebug();
                          return null;
                      }
                  }
  
                  // Live custom data will cause the reference's document to not be the same as the actual one we need, so we'll need to remake the document returned by the Load
                  if (GlobalSettings.LiveCustomData && strFileName != "improvements.xml" && xmlDocumentOfReturn != null)
                  {
                      using (MemoryStream memStream = new MemoryStream())
                      {
                          xmlDocumentOfReturn.Save(memStream);
                          memStream.Position = 0;
                          using (XmlReader objXmlReader
                                 = XmlReader.Create(memStream, GlobalSettings.SafeXmlReaderSettings))
                              return new XPathDocument(objXmlReader).CreateNavigator();
                      }
                  }
  
                  XPathDocument objTemp = blnSync
                      // ReSharper disable once MethodHasAsyncOverload
                      ? xmlReferenceOfReturn.GetXPathContent()
                      : await xmlReferenceOfReturn.GetXPathContentAsync().ConfigureAwait(false);
  
                  return objTemp.CreateNavigator();
              }
          }
  
          /// <summary>
          /// Load the selected XML file and its associated custom file synchronously.
          /// </summary>
          /// <param name="strFileName">Name of the XML file to load.</param>
          /// <param name="lstEnabledCustomDataPaths">List of enabled custom data directory paths in their load order</param>
          /// <param name="strLanguage">Language in which to load the data document.</param>
          /// <param name="blnLoadFile">Whether to force reloading content even if the file already exists.</param>
          [Annotations.NotNull]
          [MethodImpl(MethodImplOptions.AggressiveInlining)]
          public static XmlDocument Load(string strFileName, IReadOnlyList<string> lstEnabledCustomDataPaths = null, string strLanguage = "", bool blnLoadFile = false)
          {
              return LoadCoreAsync(true, strFileName, lstEnabledCustomDataPaths, strLanguage, blnLoadFile).GetAwaiter().GetResult();
          }
  
          /// <summary>
          /// Load the selected XML file and its associated custom file asynchronously.
          /// </summary>
          /// <param name="strFileName">Name of the XML file to load.</param>
          /// <param name="lstEnabledCustomDataPaths">List of enabled custom data directory paths in their load order</param>
          /// <param name="strLanguage">Language in which to load the data document.</param>
          /// <param name="blnLoadFile">Whether to force reloading content even if the file already exists.</param>
          public static Task<XmlDocument> LoadAsync(string strFileName, IReadOnlyList<string> lstEnabledCustomDataPaths = null, string strLanguage = "", bool blnLoadFile = false)
          {
              return LoadCoreAsync(false, strFileName, lstEnabledCustomDataPaths, strLanguage, blnLoadFile);
          }
  
          /// <summary>
          /// Load the selected XML file and its associated custom file asynchronously.
          /// Uses flag hack method design outlined here to avoid locking:
          /// https://docs.microsoft.com/en-us/archive/msdn-magazine/2015/july/async-programming-brownfield-async-development
          /// </summary>
          /// <param name="blnSync">Flag for whether method should always use synchronous code or not.</param>
          /// <param name="strFileName">Name of the XML file to load.</param>
          /// <param name="lstEnabledCustomDataPaths">List of enabled custom data directory paths in their load order</param>
          /// <param name="strLanguage">Language in which to load the data document.</param>
          /// <param name="blnLoadFile">Whether to force reloading content even if the file already exists.</param>
          private static async Task<XmlDocument> LoadCoreAsync(bool blnSync, string strFileName, IReadOnlyCollection<string> lstEnabledCustomDataPaths = null, string strLanguage = "", bool blnLoadFile = false)
          {
              bool blnFileFound = false;
              string strPath = string.Empty;
              strFileName = Path.GetFileName(strFileName);
              // Wait to make sure our data directories are loaded before proceeding
              using (blnSync
                         // ReSharper disable once MethodHasAsyncOverload
                         ? EnterReadLock.Enter(s_objDataDirectoriesLock)
                         : await EnterReadLock.EnterAsync(s_objDataDirectoriesLock).ConfigureAwait(false))
              {
                  foreach (string strDirectory in s_SetDataDirectories)
                  {
                      strPath = Path.Combine(strDirectory, strFileName);
                      if (File.Exists(strPath))
                      {
                          blnFileFound = true;
                          break;
                      }
                  }
  
                  if (!blnFileFound)
                  {
                      Utils.BreakIfDebug();
                      return new XmlDocument {XmlResolver = null};
                  }
  
                  if (string.IsNullOrEmpty(strLanguage))
                      strLanguage = GlobalSettings.Language;
  
                  string[] astrRelevantCustomDataPaths;
                  if (lstEnabledCustomDataPaths == null)
                      astrRelevantCustomDataPaths = Array.Empty<string>();
                  else if (s_DicPathsWithCustomFiles.TryGetValue(strFileName, out HashSet<string> setDirectoriesPossible))
                      astrRelevantCustomDataPaths = lstEnabledCustomDataPaths
                                                    .Where(x => setDirectoriesPossible.Contains(x)).ToArray();
                  else
                      astrRelevantCustomDataPaths = CompileRelevantCustomDataPaths(strFileName, lstEnabledCustomDataPaths)
                          .ToArray();
                  bool blnHasCustomData = astrRelevantCustomDataPaths.Length > 0;
                  List<string> lstKey = new List<string>(2 + astrRelevantCustomDataPaths.Length) {strLanguage, strPath};
                  lstKey.AddRange(astrRelevantCustomDataPaths);
                  KeyArray<string> objDataKey = new KeyArray<string>(lstKey);
  
                  XmlDocument xmlReturn = null;
                  // Create a new document that everything will be merged into.
                  XmlDocument xmlScratchpad = new XmlDocument {XmlResolver = null};
                  // Look to see if this XmlDocument is already loaded.
                  XmlReference xmlReferenceOfReturn;
                  if (blnSync)
                  {
                      // ReSharper disable MethodHasAsyncOverload
                      if (!s_DicXmlDocuments.TryGetValue(objDataKey, out xmlReferenceOfReturn))
                      {
                          int intEmergencyRelease = 0;
                          xmlReferenceOfReturn = new XmlReference();
                          // We break either when we successfully add our XmlReference to the dictionary or when we end up successfully fetching an existing one.
                          for (; intEmergencyRelease <= Utils.SleepEmergencyReleaseMaxTicks; ++intEmergencyRelease)
                          {
                              // The file was not found in the reference list, so it must be loaded.
                              if (s_DicXmlDocuments.TryAdd(objDataKey, xmlReferenceOfReturn))
                              {
                                  blnLoadFile = true;
                                  break;
                              }
  
                              xmlReferenceOfReturn.Dispose();
  
                              // It somehow got added in the meantime, so let's fetch it again
                              if (s_DicXmlDocuments.TryGetValue(objDataKey, out xmlReferenceOfReturn))
                                  break;
                              // We're iterating the loop because we failed to get the reference, so we need to re-allocate our reference because it was in an out-argument above
                              xmlReferenceOfReturn = new XmlReference();
                          }
  
                          if (intEmergencyRelease >
                              Utils
                                  .SleepEmergencyReleaseMaxTicks) // Shouldn't ever happen, but just in case it does, emergency exit out of the loading function
                          {
                              Utils.BreakIfDebug();
                              xmlReferenceOfReturn.Dispose();
                              return new XmlDocument {XmlResolver = null};
                          }
                      }
                      // ReSharper restore MethodHasAsyncOverload
                  }
                  else
                  {
                      bool blnSuccess;
                      (blnSuccess, xmlReferenceOfReturn) = await s_DicXmlDocuments.TryGetValueAsync(objDataKey).ConfigureAwait(false);
                      if (!blnSuccess)
                      {
                          int intEmergencyRelease = 0;
                          xmlReferenceOfReturn = new XmlReference();
                          // We break either when we successfully add our XmlReference to the dictionary or when we end up successfully fetching an existing one.
                          for (; intEmergencyRelease <= Utils.SleepEmergencyReleaseMaxTicks; ++intEmergencyRelease)
                          {
                              // The file was not found in the reference list, so it must be loaded.
                              if (await s_DicXmlDocuments.TryAddAsync(objDataKey, xmlReferenceOfReturn).ConfigureAwait(false))
                              {
                                  blnLoadFile = true;
                                  break;
                              }
  
                              await xmlReferenceOfReturn.DisposeAsync().ConfigureAwait(false);
  
                              // It somehow got added in the meantime, so let's fetch it again
                              (blnSuccess, xmlReferenceOfReturn) = await s_DicXmlDocuments.TryGetValueAsync(objDataKey);
                              if (blnSuccess)
                                  break;
                              // We're iterating the loop because we failed to get the reference, so we need to re-allocate our reference because it was in an out-argument above
                              xmlReferenceOfReturn = new XmlReference();
                          }
  
                          if (intEmergencyRelease >
                              Utils
                                  .SleepEmergencyReleaseMaxTicks) // Shouldn't ever happen, but just in case it does, emergency exit out of the loading function
                          {
                              Utils.BreakIfDebug();
                              await xmlReferenceOfReturn.DisposeAsync().ConfigureAwait(false);
                              return new XmlDocument {XmlResolver = null};
                          }
                      }
                  }
  
                  if (blnLoadFile)
                  {
                      IDisposable objLocker = null;
                      IAsyncDisposable objLockerAsync = null;
                      if (blnSync)
                          // ReSharper disable once MethodHasAsyncOverload
                          objLocker = xmlReferenceOfReturn.LockObject.EnterWriteLock();
                      else
                          objLockerAsync = await xmlReferenceOfReturn.LockObject.EnterWriteLockAsync().ConfigureAwait(false);
                      try
                      {
                          if (blnHasCustomData)
                          {
                              // If we have any custom data, make sure the base data is already loaded so we can easily just copy it over
                              XmlDocument xmlBaseDocument = blnSync
                                  // ReSharper disable once MethodHasAsyncOverload
                                  ? Load(strFileName, null, strLanguage)
                                  : await LoadAsync(strFileName, null, strLanguage).ConfigureAwait(false);
                              xmlReturn = xmlBaseDocument.Clone() as XmlDocument;
                          }
                          else if (!strLanguage.Equals(GlobalSettings.DefaultLanguage,
                                                       StringComparison.OrdinalIgnoreCase))
                          {
                              // When loading in non-English data, just clone the English stuff instead of recreating it to hopefully save on time
                              XmlDocument xmlBaseDocument = blnSync
                                  // ReSharper disable once MethodHasAsyncOverload
                                  ? Load(strFileName, null, GlobalSettings.DefaultLanguage)
                                  : await LoadAsync(strFileName, null, GlobalSettings.DefaultLanguage)
                                      .ConfigureAwait(false);
                              xmlReturn = xmlBaseDocument.Clone() as XmlDocument;
                          }
  
                          if (xmlReturn
                              == null) // Not an else in case something goes wrong in safe cast in the line above
                          {
                              xmlReturn = new XmlDocument {XmlResolver = null};
                              // write the root chummer node.
                              xmlReturn.AppendChild(xmlReturn.CreateElement("chummer"));
                              XmlElement xmlReturnDocElement = xmlReturn.DocumentElement;
                              // Load the base file and retrieve all of the child nodes.
                              try
                              {
                                  xmlScratchpad.LoadStandard(strPath);
  
                                  if (xmlReturnDocElement != null)
                                  {
                                      using (XmlNodeList xmlNodeList = xmlScratchpad.SelectNodes("/chummer/*"))
                                      {
                                          if (xmlNodeList?.Count > 0)
                                          {
                                              foreach (XmlNode objNode in xmlNodeList)
                                              {
                                                  // Append the entire child node to the new document.
                                                  xmlReturnDocElement.AppendChild(xmlReturn.ImportNode(objNode, true));
                                              }
                                          }
                                      }
                                  }
                              }
                              catch (IOException e)
                              {
                                  Log.Info(e);
                                  Utils.BreakIfDebug();
                              }
                              catch (XmlException e)
                              {
                                  Log.Warn(e);
                                  Utils.BreakIfDebug();
                              }
                          }
  
                          // Load any override data files the user might have. Do not attempt this if we're loading the Improvements file.
                          if (blnHasCustomData)
                          {
                              foreach (string strLoopPath in astrRelevantCustomDataPaths)
                              {
                                  DoProcessCustomDataFiles(xmlScratchpad, xmlReturn, strLoopPath, strFileName);
                              }
                          }
  
                          // Load the translation file for the current base data file if the selected language is not en-us.
                          if (!strLanguage.Equals(GlobalSettings.DefaultLanguage, StringComparison.OrdinalIgnoreCase))
                          {
                              // Everything is stored in the selected language file to make translations easier, keep all of the language-specific information together, and not require users to download 27 individual files.
                              // The structure is similar to the base data file, but the root node is instead a child /chummer node with a file attribute to indicate the XML file it translates.
                              XPathDocument objDataDoc = blnSync
                                  // ReSharper disable once MethodHasAsyncOverload
                                  ? LanguageManager.GetDataDocument(strLanguage)
                                  : await LanguageManager.GetDataDocumentAsync(strLanguage).ConfigureAwait(false);
                              if (objDataDoc != null)
                              {
                                  XmlNode xmlBaseChummerNode = xmlReturn.SelectSingleNode("/chummer");
                                  foreach (XPathNavigator objType in objDataDoc.CreateNavigator()
                                                                               .Select("/chummer/chummer[@file = "
                                                                                   + strFileName.CleanXPath() + "]/*"))
                                  {
                                      AppendTranslations(xmlReturn, objType, xmlBaseChummerNode);
                                  }
                              }
                          }
  
                          // Cache the merged document and its relevant information
                          if (blnSync)
                              // ReSharper disable once MethodHasAsyncOverload
                              xmlReferenceOfReturn.SetXmlContent(xmlReturn);
                          else
                              await xmlReferenceOfReturn.SetXmlContentAsync(xmlReturn).ConfigureAwait(false);
                      }
                      finally
                      {
                          if (blnSync)
                              // ReSharper disable once MethodHasAsyncOverload
                              objLocker.Dispose();
                          else
                              await objLockerAsync.DisposeAsync().ConfigureAwait(false);
                      }
  
                      // Make sure we do not override the cached document with our live data
                      if (GlobalSettings.LiveCustomData && blnHasCustomData)
                      {
                          XmlDocument objTemp = blnSync
                              // ReSharper disable once MethodHasAsyncOverload
                              ? xmlReferenceOfReturn.GetXmlContent()
                              : await xmlReferenceOfReturn.GetXmlContentAsync().ConfigureAwait(false);
                          xmlReturn = objTemp.Clone() as XmlDocument;
                      }
                  }
                  else
                  {
                      XmlDocument objTemp = blnSync
                          // ReSharper disable once MethodHasAsyncOverload
                          ? xmlReferenceOfReturn.GetXmlContent()
                          : await xmlReferenceOfReturn.GetXmlContentAsync().ConfigureAwait(false);
                      // Make sure we do not override the cached document with our live data
                      if (GlobalSettings.LiveCustomData && blnHasCustomData)
                          xmlReturn = objTemp.Clone() as XmlDocument;
                      else
                          xmlReturn = objTemp;
                  }
  
                  xmlReturn = xmlReturn ?? new XmlDocument {XmlResolver = null};
                  if (strFileName == "improvements.xml")
                      return xmlReturn;
  
                  // Load any custom data files the user might have. Do not attempt this if we're loading the Improvements file.
                  bool blnHasLiveCustomData = false;
                  if (GlobalSettings.LiveCustomData)
                  {
                      strPath = Path.Combine(Utils.GetStartupPath, "livecustomdata");
                      if (Directory.Exists(strPath))
                      {
                          blnHasLiveCustomData = DoProcessCustomDataFiles(xmlScratchpad, xmlReturn, strPath, strFileName);
                      }
                  }
  
                  // Check for non-unique guids and non-guid formatted ids in the loaded XML file. Ignore improvements.xml since the ids are used in a different way.
                  if (!xmlReferenceOfReturn.DuplicatesChecked || blnHasLiveCustomData)
                  {
                      xmlReferenceOfReturn.DuplicatesChecked
                          = true; // Set early to make sure work isn't done multiple times in case of multiple threads
                      using (XmlNodeList xmlNodeList = xmlReturn.SelectNodes("/chummer/*"))
                      {
                          if (xmlNodeList?.Count > 0)
                          {
                              foreach (XmlNode objNode in xmlNodeList)
                              {
                                  if (objNode.HasChildNodes)
                                  {
                                      // Parsing the node into an XDocument for LINQ parsing would result in slightly slower overall code (31 samples vs. 30 samples).
                                      CheckIdNodes(objNode, strFileName);
                                  }
                              }
                          }
                      }
                  }
  
                  return xmlReturn;
              }
          }
  
          private static void CheckIdNodes(XmlNode xmlParentNode, string strFileName)
          {
              if (Utils.IsUnitTest)
                  return;
              List<string> lstItemsWithMalformedIDs = new List<string>(1);
              using (new FetchSafelyFromPool<HashSet<string>>(Utils.StringHashSetPool,
                                                              out HashSet<string> setDuplicateIDs))
              {
                  // Key is ID, Value is a list of the names of all items with that ID.
                  Dictionary<string, IList<string>> dicItemsWithIDs = new Dictionary<string, IList<string>>(1);
                  CheckIdNode(xmlParentNode, setDuplicateIDs, lstItemsWithMalformedIDs, dicItemsWithIDs);
  
                  if (setDuplicateIDs.Count > 0)
                  {
                      using (new FetchSafelyFromPool<StringBuilder>(Utils.StringBuilderPool,
                                                                    out StringBuilder sbdDuplicatesNames))
                      {
                          foreach (IList<string> lstDuplicateNames in dicItemsWithIDs
                                                                      .Where(x => setDuplicateIDs.Contains(x.Key))
                                                                      .Select(x => x.Value))
                          {
                              if (sbdDuplicatesNames.Length != 0)
                                  sbdDuplicatesNames.AppendLine();
                              sbdDuplicatesNames.AppendJoin(Environment.NewLine, lstDuplicateNames);
                          }
  
                          Program.ShowMessageBox(string.Format(GlobalSettings.CultureInfo
                              , LanguageManager.GetString(
                                  "Message_DuplicateGuidWarning")
                              , setDuplicateIDs.Count
                              , strFileName
                              , sbdDuplicatesNames.ToString()));
                      }
                  }
              }
  
              if (lstItemsWithMalformedIDs.Count > 0)
              {
                  Program.ShowMessageBox(string.Format(GlobalSettings.CultureInfo
                      , LanguageManager.GetString("Message_NonGuidIdWarning")
                      , lstItemsWithMalformedIDs.Count
                      , strFileName
                      , string.Join(Environment.NewLine, lstItemsWithMalformedIDs)));
              }
          }
  
          private static void CheckIdNode(XmlNode xmlParentNode, ICollection<string> setDuplicateIDs, ICollection<string> lstItemsWithMalformedIDs, IDictionary<string, IList<string>> dicItemsWithIDs)
          {
              using (XmlNodeList xmlChildNodeList = xmlParentNode.SelectNodes("*"))
              {
                  if (!(xmlChildNodeList?.Count > 0))
                      return;
  
                  foreach (XmlNode xmlLoopNode in xmlChildNodeList)
                  {
                      string strId = xmlLoopNode["id"]?.InnerText;
                      if (!string.IsNullOrEmpty(strId))
                      {
                          strId = strId.ToUpperInvariant();
                          if (xmlLoopNode.Name == "knowledgeskilllevel")
                              continue; //TODO: knowledgeskilllevel node in lifemodules.xml uses ids instead of name references. Find a better way to manage this!
                          string strItemName = xmlLoopNode["name"]?.InnerText
                                               ?? xmlLoopNode["stage"]?.InnerText
                                               ?? xmlLoopNode["category"]?.InnerText
                                               ?? strId;
                          if (!strId.IsGuid())
                              lstItemsWithMalformedIDs.Add(strItemName);
                          else if (dicItemsWithIDs.TryGetValue(strId, out IList<string> lstNamesList))
                          {
                              if (!setDuplicateIDs.Contains(strId))
                              {
                                  setDuplicateIDs.Add(strId);
                                  if (strItemName == strId)
                                      strItemName = string.Empty;
                              }
  
                              lstNamesList.Add(strItemName);
                          }
                          else
                              dicItemsWithIDs.Add(strId, new List<string>(2) { strItemName });
                      }
  
                      // Perform recursion so that nested elements that also have ids are also checked (e.g. Metavariants)
                      CheckIdNode(xmlLoopNode, setDuplicateIDs, lstItemsWithMalformedIDs, dicItemsWithIDs);
                  }
              }
          }
  
          private static void AppendTranslations(XmlDocument xmlDataDocument, XPathNavigator xmlTranslationListParentNode, XmlNode xmlDataParentNode)
          {
              foreach (XPathNavigator objChild in xmlTranslationListParentNode.SelectAndCacheExpression("*"))
              {
                  XmlNode xmlItem = null;
                  string strXPathPrefix = xmlTranslationListParentNode.Name + '/' + objChild.Name + '[';
                  string strChildName = objChild.SelectSingleNodeAndCacheExpression("id")?.Value;
                  if (!string.IsNullOrEmpty(strChildName))
                  {
                      xmlItem = xmlDataParentNode.SelectSingleNode(strXPathPrefix + "id = " + strChildName.CleanXPath() + ']');
                  }
                  if (xmlItem == null)
                  {
                      strChildName = objChild.SelectSingleNodeAndCacheExpression("name")?.Value.Replace("&amp;", "&");
                      if (!string.IsNullOrEmpty(strChildName))
                      {
                          xmlItem = xmlDataParentNode.SelectSingleNode(strXPathPrefix + "name = " + strChildName.CleanXPath() + ']');
                      }
                  }
                  // If this is a translatable item, find the proper node and add/update this information.
                  if (xmlItem != null)
                  {
                      XPathNavigator xmlLoopNode = objChild.SelectSingleNodeAndCacheExpression("translate");
                      if (xmlLoopNode != null)
                          xmlItem.AppendChild(xmlLoopNode.ToXmlNode(xmlDataDocument));
  
                      xmlLoopNode = objChild.SelectSingleNodeAndCacheExpression("altpage");
                      if (xmlLoopNode != null)
                          xmlItem.AppendChild(xmlLoopNode.ToXmlNode(xmlDataDocument));
  
                      xmlLoopNode = objChild.SelectSingleNodeAndCacheExpression("altcode");
                      if (xmlLoopNode != null)
                          xmlItem.AppendChild(xmlLoopNode.ToXmlNode(xmlDataDocument));
  
                      xmlLoopNode = objChild.SelectSingleNodeAndCacheExpression("altnotes");
                      if (xmlLoopNode != null)
                          xmlItem.AppendChild(xmlLoopNode.ToXmlNode(xmlDataDocument));
  
                      xmlLoopNode = objChild.SelectSingleNodeAndCacheExpression("altadvantage");
                      if (xmlLoopNode != null)
                          xmlItem.AppendChild(xmlLoopNode.ToXmlNode(xmlDataDocument));
  
                      xmlLoopNode = objChild.SelectSingleNodeAndCacheExpression("altdisadvantage");
                      if (xmlLoopNode != null)
                          xmlItem.AppendChild(xmlLoopNode.ToXmlNode(xmlDataDocument));
  
                      xmlLoopNode = objChild.SelectSingleNodeAndCacheExpression("altnameonpage");
                      if (xmlLoopNode != null)
                          xmlItem.AppendChild(xmlLoopNode.ToXmlNode(xmlDataDocument));
  
                      xmlLoopNode = objChild.SelectSingleNodeAndCacheExpression("alttexts");
                      if (xmlLoopNode != null)
                          xmlItem.AppendChild(xmlLoopNode.ToXmlNode(xmlDataDocument));
  
                      string strTranslate = objChild.SelectSingleNodeAndCacheExpression("@translate")?.InnerXml;
                      if (!string.IsNullOrEmpty(strTranslate))
                      {
                          // Handle Category name translations.
                          (xmlItem as XmlElement)?.SetAttribute("translate", strTranslate);
                      }
  
                      // Sub-children to also process with the translation
                      XPathNavigator xmlSubItemsNode = objChild.SelectSingleNodeAndCacheExpression("specs");
                      if (xmlSubItemsNode != null)
                      {
                          AppendTranslations(xmlDataDocument, xmlSubItemsNode, xmlItem);
                      }
                      xmlSubItemsNode = objChild.SelectSingleNodeAndCacheExpression("metavariants");
                      if (xmlSubItemsNode != null)
                      {
                          AppendTranslations(xmlDataDocument, xmlSubItemsNode, xmlItem);
                      }
                      xmlSubItemsNode = objChild.SelectSingleNodeAndCacheExpression("choices");
                      if (xmlSubItemsNode != null)
                      {
                          AppendTranslations(xmlDataDocument, xmlSubItemsNode, xmlItem);
                      }
                      xmlSubItemsNode = objChild.SelectSingleNodeAndCacheExpression("talents");
                      if (xmlSubItemsNode != null)
                      {
                          AppendTranslations(xmlDataDocument, xmlSubItemsNode, xmlItem);
                      }
                      xmlSubItemsNode = objChild.SelectSingleNodeAndCacheExpression("versions");
                      if (xmlSubItemsNode != null)
                      {
                          AppendTranslations(xmlDataDocument, xmlSubItemsNode, xmlItem);
                      }
                  }
                  else
                  {
                      string strTranslate = objChild.SelectSingleNodeAndCacheExpression("@translate")?.InnerXml;
                      if (!string.IsNullOrEmpty(strTranslate))
                      {
                          // Handle Category name translations.
                          XmlElement objItem = xmlDataParentNode.SelectSingleNode(strXPathPrefix + ". = " + objChild.InnerXml.Replace("&amp;", "&").CleanXPath() + ']') as XmlElement;
                          // Expected result is null if not found.
                          objItem?.SetAttribute("translate", strTranslate);
                      }
                  }
              }
          }
  
          /// <summary>
          /// Fetch the first file from a list of custom file paths.
          /// </summary>
          /// <param name="strFileName">Name of the file to fetch.</param>
          /// <param name="lstPaths">Paths to check.</param>
          /// <returns>The first full path containing <paramref name="strFileName"/> if one is found, string.Empty otherwise.</returns>
          private static string FetchBaseFileFromCustomDataPaths(string strFileName, IEnumerable<string> lstPaths)
          {
              if (strFileName != "improvements.xml" && lstPaths != null)
              {
                  foreach (string strLoopPath in lstPaths)
                  {
                      string strLoop = Directory.EnumerateFiles(strLoopPath, strFileName, SearchOption.AllDirectories)
                                                .FirstOrDefault();
                      if (!string.IsNullOrEmpty(strLoop))
                          return strLoop;
                  }
              }
              return string.Empty;
          }
  
          /// <summary>
          /// Filter through a list of data paths and return a list of ones that modify the given file. Used to recycle data files between different rule sets.
          /// </summary>
          /// <param name="strFileName">Name of the file that would be modified by custom data files.</param>
          /// <param name="lstPaths">Paths to check for custom data files relevant to <paramref name="strFileName"/>.</param>
          /// <returns>A list of paths with <paramref name="lstPaths"/> that is relevant to <paramref name="strFileName"/>, in the same order that they are in <paramref name="lstPaths"/>.</returns>
          private static IEnumerable<string> CompileRelevantCustomDataPaths(string strFileName, IEnumerable<string> lstPaths)
          {
              if (strFileName == "improvements.xml" || lstPaths == null)
                  yield break;
              foreach (string strLoopPath in lstPaths)
              {
                  if (Directory.EnumerateFiles(strLoopPath, "*_" + strFileName, SearchOption.AllDirectories)
                               .Any(x =>
                               {
                                   string strInnerFileName = Path.GetFileName(x);
                                   return strInnerFileName.StartsWith("override_") || strInnerFileName.StartsWith("custom_")
                                       || strInnerFileName.StartsWith("amend_");
                               }))
                  {
                      yield return strLoopPath;
                  }
              }
          }
  
          private static bool DoProcessCustomDataFiles(XmlDocument xmlFile, XmlDocument xmlDataDoc, string strLoopPath, string strFileName, SearchOption eSearchOption = SearchOption.AllDirectories)
          {
              bool blnReturn = false;
              XmlElement objDocElement = xmlDataDoc.DocumentElement;
              List<string> lstPossibleCustomFiles = Directory.GetFiles(strLoopPath, "*_" + strFileName, eSearchOption).ToList();
              foreach (string strFile in lstPossibleCustomFiles)
              {
                  if (!Path.GetFileName(strFile).StartsWith("override_"))
                      continue;
                  try
                  {
                      xmlFile.LoadStandard(strFile);
                  }
                  catch (IOException)
                  {
                      continue;
                  }
                  catch (XmlException)
                  {
                      continue;
                  }
  
                  using (XmlNodeList xmlNodeList = xmlFile.SelectNodes("/chummer/*"))
                  {
                      if (xmlNodeList?.Count > 0)
                      {
                          foreach (XmlNode objNode in xmlNodeList)
                          {
                              foreach (XmlNode objType in objNode.ChildNodes)
                              {
                                  string strFilter;
                                  using (new FetchSafelyFromPool<StringBuilder>(Utils.StringBuilderPool,
                                                                                out StringBuilder sbdFilter))
                                  {
                                      XmlNode xmlIdNode = objType["id"];
                                      if (xmlIdNode != null)
                                          sbdFilter.Append("id = ")
                                                   .Append(xmlIdNode.InnerText.Replace("&amp;", "&").CleanXPath());
                                      else
                                      {
                                          xmlIdNode = objType["name"];
                                          if (xmlIdNode != null)
                                              sbdFilter.Append("name = ")
                                                       .Append(xmlIdNode.InnerText.Replace("&amp;", "&").CleanXPath());
                                      }
  
                                      // Child Nodes marked with "isidnode" serve as additional identifier nodes, in case something needs modifying that uses neither a name nor an ID.
                                      using (XmlNodeList objAmendingNodeExtraIds
                                             = objType.SelectNodes("child::*[@isidnode = " + bool.TrueString.CleanXPath() + ']'))
                                      {
                                          if (objAmendingNodeExtraIds?.Count > 0)
                                          {
                                              foreach (XmlNode objExtraId in objAmendingNodeExtraIds)
                                              {
                                                  if (sbdFilter.Length > 0)
                                                      sbdFilter.Append(" and ");
                                                  sbdFilter.Append(objExtraId.Name).Append(" = ")
                                                           .Append(
                                                               objExtraId.InnerText.Replace("&amp;", "&").CleanXPath());
                                              }
                                          }
                                      }
  
                                      strFilter = sbdFilter.ToString();
                                  }
                                  if (!string.IsNullOrEmpty(strFilter))
                                  {
                                      XmlNode objItem = xmlDataDoc.SelectSingleNode(
                                          "/chummer/" + objNode.Name + '/' + objType.Name + '[' + strFilter + ']');
                                      if (objItem != null)
                                      {
                                          objItem.InnerXml = objType.InnerXml;
                                          blnReturn = true;
                                      }
                                  }
                              }
                          }
                      }
                  }
              }
  
              // Load any custom data files the user might have. Do not attempt this if we're loading the Improvements file.
              foreach (string strFile in lstPossibleCustomFiles)
              {
                  if (!Path.GetFileName(strFile).StartsWith("custom_"))
                      continue;
                  try
                  {
                      xmlFile.LoadStandard(strFile);
                  }
                  catch (IOException)
                  {
                      continue;
                  }
                  catch (XmlException)
                  {
                      continue;
                  }
  
                  using (XmlNodeList xmlNodeList = xmlFile.SelectNodes("/chummer/*"))
                  {
                      if (xmlNodeList?.Count > 0 && objDocElement != null)
                      {
                          foreach (XmlNode objNode in xmlNodeList)
                          {
                              // Look for any items with a duplicate name and pluck them from the node so we don't end up with multiple items with the same name.
                              List<XmlNode> lstDelete = new List<XmlNode>(objNode.ChildNodes.Count);
                              foreach (XmlNode objChild in objNode.ChildNodes)
                              {
                                  XmlNode objParentNode = objChild.ParentNode;
                                  if (objParentNode == null)
                                      continue;
                                  string strFilter = string.Empty;
                                  XmlNode xmlIdNode = objChild["id"];
                                  if (xmlIdNode != null)
                                      strFilter = "id = " + xmlIdNode.InnerText.Replace("&amp;", "&").CleanXPath();
                                  else
                                  {
                                      XmlNode xmlNameNode = objChild["name"];
                                      if (xmlNameNode != null)
                                      {
                                          strFilter += (string.IsNullOrEmpty(strFilter)
                                                           ? "name = "
                                                           : " and name = ") +
                                                       xmlNameNode.InnerText.Replace("&amp;", "&").CleanXPath();
                                      }
                                  }
  
                                  // Only do this if the child has the name or id field since this is what we must match on.
                                  if (!string.IsNullOrEmpty(strFilter))
                                  {
                                      string strParentNodeFilter = string.Empty;
                                      if (objParentNode.Attributes?.Count > 0)
                                      {
                                          using (new FetchSafelyFromPool<StringBuilder>(Utils.StringBuilderPool,
                                                     out StringBuilder sbdParentNodeFilter))
                                          {
                                              foreach (XmlAttribute objLoopAttribute in objParentNode.Attributes)
                                              {
                                                  sbdParentNodeFilter
                                                      .Append('@').Append(objLoopAttribute.Name).Append(" = ")
                                                      .Append(objLoopAttribute.Value.Replace("&amp;", "&")
                                                                              .CleanXPath()).Append(" and ");
                                              }
  
                                              if (sbdParentNodeFilter.Length > 0)
                                                  sbdParentNodeFilter.Length -= 5;
  
                                              strParentNodeFilter = sbdParentNodeFilter.ToString();
                                          }
                                      }
                                      XmlNode objItem = xmlDataDoc.SelectSingleNode(string.IsNullOrEmpty(strParentNodeFilter)
                                          ? "/chummer/" + objParentNode.Name + '/' + objChild.Name + '[' + strFilter + ']'
                                          : "/chummer/" + objParentNode.Name + '[' + strParentNodeFilter + "]/" + objChild.Name + '[' + strFilter + ']');
                                      if (objItem != null)
                                          lstDelete.Add(objChild);
                                  }
                              }
  
                              // Remove the offending items from the node we're about to merge in.
                              foreach (XmlNode objRemoveNode in lstDelete)
                              {
                                  objNode.RemoveChild(objRemoveNode);
                              }
  
                              XmlNode xmlExistingNode = objDocElement[objNode.Name];
                              if (xmlExistingNode != null
                                  && xmlExistingNode.Attributes?.Count == objNode.Attributes?.Count)
                              {
                                  bool blnAllMatching = true;
                                  foreach (XmlAttribute x in xmlExistingNode.Attributes)
                                  {
                                      if (objNode.Attributes.GetNamedItem(x.Name)?.Value != x.Value)
                                      {
                                          blnAllMatching = false;
                                          break;
                                      }
                                  }
  
                                  if (blnAllMatching)
                                  {
                                      /* We need to do this to avoid creating multiple copies of the root node, ie
                                         <chummer>
                                             <metatypes>
                                                 <metatype>Standard</metatype>
                                             </metatypes>
                                             <metatypes>
                                                 <metatype>Custom</metatype>
                                             </metatypes>
                                         </chummer>
                                         Otherwise xpathnavigators that to a selectsinglenode will only grab the first instance of the name. TODO: fix better?
                                     */
                                      foreach (XmlNode childNode in objNode.ChildNodes)
                                      {
                                          xmlExistingNode.AppendChild(xmlDataDoc.ImportNode(childNode, true));
                                      }
                                  }
                              }
                              else
                              {
                                  // Append the entire child node to the new document.
                                  objDocElement.AppendChild(xmlDataDoc.ImportNode(objNode, true));
                              }
  
                              blnReturn = true;
                          }
                      }
                  }
              }
  
              // Load any amending data we might have, i.e. rules that only amend items instead of replacing them. Do not attempt this if we're loading the Improvements file.
              foreach (string strFile in lstPossibleCustomFiles)
              {
                  if (!Path.GetFileName(strFile).StartsWith("amend_"))
                      continue;
                  try
                  {
                      xmlFile.LoadStandard(strFile);
                  }
                  catch (IOException)
                  {
                      continue;
                  }
                  catch (XmlException)
                  {
                      continue;
                  }
  
                  using (XmlNodeList xmlNodeList = xmlFile.SelectNodes("/chummer/*"))
                  {
                      if (xmlNodeList?.Count > 0)
                      {
                          foreach (XmlNode objNode in xmlNodeList)
                          {
                              blnReturn = AmendNodeChildren(xmlDataDoc, objNode, "/chummer") || blnReturn;
                          }
                      }
                  }
              }
  
              return blnReturn;
          }
  
          /// <summary>
          /// Deep search a document to amend with a new node.
          /// If Attributes exist for the amending node, the Attributes for the original node will all be overwritten.
          /// </summary>
          /// <param name="xmlDoc">Document element in which to operate.</param>
          /// <param name="xmlAmendingNode">The amending (new) node.</param>
          /// <param name="strXPath">The current XPath in the document element that leads to the target node(s) where the amending node would be applied.</param>
          /// <param name="lstExtraNodesToAddIfNotFound">List of extra nodes to add (with their XPaths) if the given amending node would be added if not found, with each entry's node being the parent of the next entry's node. Needed in case of recursing into nodes that don't exist.</param>
          /// <returns>True if any amends were made, False otherwise.</returns>
          private static bool AmendNodeChildren(XmlDocument xmlDoc, XmlNode xmlAmendingNode, string strXPath, IList<Tuple<XmlNode, string>> lstExtraNodesToAddIfNotFound = null)
          {
              bool blnReturn = false;
              string strFilter = string.Empty;
              string strOperation = string.Empty;
              string strRegexPattern = string.Empty;
              bool blnAddIfNotFoundAttributePresent = false;
              bool blnAddIfNotFound = false;
              XmlAttributeCollection objAmendingNodeAttribs = xmlAmendingNode.Attributes;
              if (objAmendingNodeAttribs != null)
              {
                  // This attribute is not used by the node itself, so it can be removed to speed up node importing later on.
                  objAmendingNodeAttribs.RemoveNamedItem("isidnode");
  
                  // Gets the specific operation to execute on this node.
                  XmlNode objAmendOperation = objAmendingNodeAttribs.RemoveNamedItem("amendoperation");
                  if (objAmendOperation != null)
                  {
                      strOperation = objAmendOperation.InnerText;
                  }
  
                  using (new FetchSafelyFromPool<StringBuilder>(Utils.StringBuilderPool, out StringBuilder sbdFilter))
                  {
                      // Gets the custom XPath filter defined for what children to fetch. If it exists, use that as the XPath filter for targeting nodes.
                      XmlNode objCustomXPath = objAmendingNodeAttribs.RemoveNamedItem("xpathfilter");
                      if (objCustomXPath != null)
                      {
                          sbdFilter.Append(objCustomXPath.InnerText.Replace("&amp;", "&").Replace("&quot;", "\""));
                      }
                      else
                      {
                          // Fetch the old node based on identifiers present in the amending node (id or name)
                          XmlNode objAmendingNodeId = xmlAmendingNode["id"];
                          if (objAmendingNodeId != null)
                          {
                              sbdFilter.Append("id = ")
                                       .Append(objAmendingNodeId.InnerText.Replace("&amp;", "&").CleanXPath());
                          }
                          else
                          {
                              objAmendingNodeId = xmlAmendingNode["name"];
                              if (objAmendingNodeId != null && (strOperation == "remove"
                                                                || xmlAmendingNode.SelectSingleNode(
                                                                    "child::*[not(self::name)]") != null))
                              {
                                  // A few places in the data files use just "name" as an actual entry in a list, so only default to using it as an id node
                                  // if there are other nodes present in the amending node or if a remove operation is specified (since that only requires an id node).
                                  sbdFilter.Append("name = ")
                                           .Append(objAmendingNodeId.InnerText.Replace("&amp;", "&").CleanXPath());
                              }
                          }
  
                          // Child Nodes marked with "isidnode" serve as additional identifier nodes, in case something needs modifying that uses neither a name nor an ID.
                          using (XmlNodeList xmlChildrenWithIds
                                 = xmlAmendingNode.SelectNodes("child::*[@isidnode = " + bool.TrueString.CleanXPath() + ']'))
                          {
                              if (xmlChildrenWithIds != null)
                              {
                                  foreach (XmlNode objExtraId in xmlChildrenWithIds)
                                  {
  
gt:                                     if (sbdFilter.Length > 0)