/// <summary>
        /// Creates a new empty balancing data object.
        /// </summary>
        /// <param name="genieFile">The genie file containing the base values the diffs are build upon.</param>
        /// <param name="languageFiles">The language DLL files, sorted by priority, descending. Used for proper name retrieval.</param>
        /// <param name="mappingFile">Optional. ID mapping file.</param>
        public BalancingFile(GenieLibrary.GenieFile genieFile, string[] languageFiles, MappingFile mappingFile = null)
        {
            // Remember mapping file
            _mappingFile = mappingFile;

            // Load language files for proper name display
            GenieLibrary.LanguageFileWrapper langFileWrapper = new GenieLibrary.LanguageFileWrapper(languageFiles);

            // Initialize unit list with base values
            Dictionary <short, UnitEntry> unitEntries = new Dictionary <short, UnitEntry>();

            foreach (Civ c in genieFile.Civs)
            {
                // Check for units not contained in the unit entry list
                foreach (KeyValuePair <int, Civ.Unit> unitData in c.Units)
                {
                    // Unit already contained in unit entry list?
                    if (unitEntries.ContainsKey((short)unitData.Key))
                    {
                        continue;
                    }

                    // Show only projectiles, living units and buildings
                    if (unitData.Value.Type < Civ.Unit.UnitType.Projectile)
                    {
                        continue;
                    }

                    // Create entry
                    UnitEntry ue = new UnitEntry();
                    ue.DisplayName = langFileWrapper.GetString(unitData.Value.LanguageDLLName);
                    if (string.IsNullOrEmpty(ue.DisplayName) || unitData.Value.Type == Civ.Unit.UnitType.Projectile)
                    {
                        ue.DisplayName = unitData.Value.Name1.TrimEnd('\0');
                    }

                    // Get members
                    ue.HitPoints = new DiffElement <short>(ue, unitData.Value.HitPoints);
                    ue.Speed     = new DiffElement <float>(ue, unitData.Value.Speed);
                    if (unitData.Value.DeadFish != null)
                    {
                        ue.RotationSpeed = new DiffElement <float>(ue, unitData.Value.DeadFish.RotationSpeed);
                    }
                    ue.LineOfSight = new DiffElement <float>(ue, unitData.Value.LineOfSight);
                    if (unitData.Value.Bird != null)
                    {
                        ue.SearchRadius = new DiffElement <float>(ue, unitData.Value.Bird.SearchRadius);
                    }
                    if (unitData.Value.Type50 != null)
                    {
                        ue.MinRange = new DiffElement <float>(ue, unitData.Value.Type50.MinRange);
                    }
                    if (unitData.Value.Type50 != null)
                    {
                        ue.MaxRange = new DiffElement <float>(ue, unitData.Value.Type50.MaxRange);
                    }
                    if (unitData.Value.Type50 != null)
                    {
                        ue.DisplayedRange = new DiffElement <float>(ue, unitData.Value.Type50.DisplayedRange);
                    }
                    if (unitData.Value.Type50 != null)
                    {
                        ue.ReloadTime = new DiffElement <float>(ue, unitData.Value.Type50.ReloadTime);
                    }
                    if (unitData.Value.Type50 != null)
                    {
                        ue.DisplayedReloadTime = new DiffElement <float>(ue, unitData.Value.Type50.DisplayedReloadTime);
                    }
                    if (unitData.Value.Type50 != null)
                    {
                        ue.BlastRadius = new DiffElement <float>(ue, unitData.Value.Type50.BlastRadius);
                    }
                    if (unitData.Value.Type50 != null)
                    {
                        ue.Attacks = new AttackArmorEntryListDiffElement
                                     (
                            ue,
                            new List <AttackArmorEntry>
                            (
                                unitData.Value.Type50.Attacks.Select(at => new AttackArmorEntry(at.Key, at.Value))
                            )
                                     );
                    }
                    if (unitData.Value.Type50 != null)
                    {
                        ue.DisplayedAttack = new DiffElement <short>(ue, unitData.Value.Type50.DisplayedAttack);
                    }
                    if (unitData.Value.Creatable != null)
                    {
                        ue.ProjectileCount = new DiffElement <float>(ue, unitData.Value.Creatable.ProjectileCount);
                    }
                    if (unitData.Value.Creatable != null)
                    {
                        ue.ProjectileCountOnFullGarrison = new DiffElement <byte>(ue, unitData.Value.Creatable.ProjectileCountOnFullGarrison);
                    }
                    if (unitData.Value.Type50 != null)
                    {
                        ue.ProjectileFrameDelay = new DiffElement <short>(ue, unitData.Value.Type50.ProjectileFrameDelay);
                    }
                    if (unitData.Value.Type50 != null)
                    {
                        ue.ProjectileAccuracyPercent = new DiffElement <short>(ue, unitData.Value.Type50.ProjectileAccuracyPercent);
                    }
                    if (unitData.Value.Type50 != null)
                    {
                        ue.ProjectileDispersion = new DiffElement <float>(ue, unitData.Value.Type50.ProjectileDispersion);
                    }
                    if (unitData.Value.Type50 != null)
                    {
                        ue.ProjectileGraphicDisplacementX = new DiffElement <float>(ue, unitData.Value.Type50.ProjectileGraphicDisplacement[0]);
                    }
                    if (unitData.Value.Type50 != null)
                    {
                        ue.ProjectileGraphicDisplacementY = new DiffElement <float>(ue, unitData.Value.Type50.ProjectileGraphicDisplacement[1]);
                    }
                    if (unitData.Value.Type50 != null)
                    {
                        ue.ProjectileGraphicDisplacementZ = new DiffElement <float>(ue, unitData.Value.Type50.ProjectileGraphicDisplacement[2]);
                    }
                    if (unitData.Value.Creatable != null)
                    {
                        ue.ProjectileSpawningAreaWidth = new DiffElement <float>(ue, unitData.Value.Creatable.ProjectileSpawningAreaWidth);
                    }
                    if (unitData.Value.Creatable != null)
                    {
                        ue.ProjectileSpawningAreaHeight = new DiffElement <float>(ue, unitData.Value.Creatable.ProjectileSpawningAreaHeight);
                    }
                    if (unitData.Value.Creatable != null)
                    {
                        ue.ProjectileSpawningAreaRandomness = new DiffElement <float>(ue, unitData.Value.Creatable.ProjectileSpawningAreaRandomness);
                    }
                    if (unitData.Value.Type50 != null)
                    {
                        ue.Armors = new AttackArmorEntryListDiffElement
                                    (
                            ue,
                            new List <AttackArmorEntry>
                            (
                                unitData.Value.Type50.Armors.Select(at => new AttackArmorEntry(at.Key, at.Value))
                            )
                                    );
                    }
                    if (unitData.Value.Type50 != null)
                    {
                        ue.DisplayedMeleeArmor = new DiffElement <short>(ue, unitData.Value.Type50.DisplayedMeleeArmor);
                    }
                    if (unitData.Value.Creatable != null)
                    {
                        ue.DisplayedPierceArmor = new DiffElement <short>(ue, unitData.Value.Creatable.DisplayedPierceArmor);
                    }
                    ue.GarrisonCapacity = new DiffElement <byte>(ue, unitData.Value.GarrisonCapacity);
                    if (unitData.Value.Building != null)
                    {
                        ue.GarrisonHealRateFactor = new DiffElement <float>(ue, unitData.Value.Building.GarrisonHealRateFactor);
                    }
                    if (unitData.Value.Creatable != null)
                    {
                        ue.TrainTime = new DiffElement <short>(ue, unitData.Value.Creatable.TrainTime);
                    }
                    if (unitData.Value.Creatable != null)
                    {
                        ue.Cost1 = new ResourceCostEntryDiffElement(ue, new ResourceCostEntry
                                                                    (
                                                                        unitData.Value.Creatable.ResourceCosts[0].Type,
                                                                        unitData.Value.Creatable.ResourceCosts[0].Amount,
                                                                        (byte)unitData.Value.Creatable.ResourceCosts[0].Mode
                                                                    ));
                    }
                    if (unitData.Value.Creatable != null)
                    {
                        ue.Cost2 = new ResourceCostEntryDiffElement(ue, new ResourceCostEntry
                                                                    (
                                                                        unitData.Value.Creatable.ResourceCosts[1].Type,
                                                                        unitData.Value.Creatable.ResourceCosts[1].Amount,
                                                                        (byte)unitData.Value.Creatable.ResourceCosts[1].Mode
                                                                    ));
                    }
                    if (unitData.Value.Creatable != null)
                    {
                        ue.Cost3 = new ResourceCostEntryDiffElement(ue, new ResourceCostEntry
                                                                    (
                                                                        unitData.Value.Creatable.ResourceCosts[2].Type,
                                                                        unitData.Value.Creatable.ResourceCosts[2].Amount,
                                                                        (byte)unitData.Value.Creatable.ResourceCosts[2].Mode
                                                                    ));
                    }

                    // Assign name of secondary projectile, if defined
                    if (unitData.Value.Creatable != null && c.Units.Keys.Contains(unitData.Value.Creatable.AlternativeProjectileUnit))
                    {
                        ue.SecondaryProjectileName = $"[{unitData.Value.Creatable.AlternativeProjectileUnit}] { c.Units[unitData.Value.Creatable.AlternativeProjectileUnit].Name1.TrimEnd('\0')}";
                    }

                    // Save unit entry
                    unitEntries[(short)unitData.Key] = ue;
                }
            }

            // Sort and save unit entry list
            UnitEntries = unitEntries.OrderBy(ue => ue.Value.DisplayName).ToDictionary(ue => ue.Key, ue => ue.Value);

            // Initialize research list with base values
            Dictionary <short, ResearchEntry> researchEntries = new Dictionary <short, ResearchEntry>();

            for (int rId = 0; rId < genieFile.Researches.Count; ++rId)
            {
                // Get research data
                Research researchData = genieFile.Researches[rId];

                // Create entry
                ResearchEntry re = new ResearchEntry();
                re.DisplayName = langFileWrapper.GetString(researchData.LanguageDLLName1);
                if (string.IsNullOrEmpty(re.DisplayName))
                {
                    re.DisplayName = researchData.Name.TrimEnd('\0');
                }
                if (string.IsNullOrWhiteSpace(re.DisplayName))
                {
                    continue;                     // Skip empty researches
                }
                // Get members
                re.ResearchTime = new DiffElement <short>(re, researchData.ResearchTime);
                re.Cost1        = new ResourceCostEntryDiffElement(re, new ResourceCostEntry
                                                                   (
                                                                       researchData.ResourceCosts[0].Type,
                                                                       researchData.ResourceCosts[0].Amount,
                                                                       researchData.ResourceCosts[0].Mode
                                                                   ));
                re.Cost2 = new ResourceCostEntryDiffElement(re, new ResourceCostEntry
                                                            (
                                                                researchData.ResourceCosts[1].Type,
                                                                researchData.ResourceCosts[1].Amount,
                                                                researchData.ResourceCosts[1].Mode
                                                            ));
                re.Cost3 = new ResourceCostEntryDiffElement(re, new ResourceCostEntry
                                                            (
                                                                researchData.ResourceCosts[2].Type,
                                                                researchData.ResourceCosts[2].Amount,
                                                                researchData.ResourceCosts[2].Mode
                                                            ));

                // Save research entry
                researchEntries[(short)rId] = re;
            }

            // Sort and save research entry list
            ResearchEntries = researchEntries.OrderBy(re => re.Value.DisplayName).ToDictionary(re => re.Key, re => re.Value);
        }
        /// <summary>
        /// Loads the balancing file at the given path.
        /// </summary>
        /// <param name="genieFile">The genie file containing the base values the diffs are build upon.</param>
        /// <param name="path">The path to the balancing file.</param>
        /// <param name="languageFiles">The language DLL files, sorted by priority, descending. Used for proper name retrieval.</param>
        /// <param name="mappingFile">Optional. ID mapping file.</param>
        public BalancingFile(GenieLibrary.GenieFile genieFile, string path, string[] languageFiles, MappingFile mappingFile = null)
            : this(genieFile, languageFiles)
        {
            // Load file into buffer
            IORAMHelper.RAMBuffer buffer = new IORAMHelper.RAMBuffer(path);

            // Check version
            int version = buffer.ReadInteger();

            if (version > Version)
            {
                throw new ArgumentException("The given file was created with a newer version of this program, please consider updating.");
            }

            // Check for embedded mapping file, and create ID conversion functions if necessary
            Func <short, short> ConvertUnitId     = null;
            Func <short, short> ConvertResearchId = null;
            MappingFile         embeddedMapping   = null;

            if (buffer.ReadByte() == 1)
            {
                // Read embedded file
                embeddedMapping = new MappingFile(buffer);
                if (mappingFile == null || mappingFile.Hash.SequenceEqual(embeddedMapping.Hash))
                {
                    // Use embedded file, no conversion required
                    _mappingFile      = embeddedMapping;
                    ConvertUnitId     = (id) => id;
                    ConvertResearchId = (id) => id;
                }
                else
                {
                    // Use new mapping file, create conversion functions (old DAT ID -> Editor ID -> new DAT ID)
                    _mappingFile      = mappingFile;
                    ConvertUnitId     = (id) => _mappingFile.UnitMapping.FirstOrDefault(m => m.Value == embeddedMapping.UnitMapping[id]).Key;
                    ConvertResearchId = (id) => _mappingFile.ResearchMapping.FirstOrDefault(m => m.Value == embeddedMapping.ResearchMapping[id]).Key;
                }
            }
            else if (mappingFile != null)
            {
                throw new ArgumentException("A mapping cannot be added to an existing file. Create a new balancing file instead.");
            }

            // Read unit entries
            int unitEntryCount = buffer.ReadInteger();

            for (int i = 0; i < unitEntryCount; ++i)
            {
                // Read entry and merge with existing entry
                short unitId = ConvertUnitId(buffer.ReadShort());
                UnitEntries[unitId].Read(buffer);
            }

            // Read research entries
            int researchEntryCount = buffer.ReadInteger();

            for (int i = 0; i < researchEntryCount; ++i)
            {
                // Read entry and merge with existing entry
                short researchId = ConvertResearchId(buffer.ReadShort());
                ResearchEntries[researchId].Read(buffer);
            }
        }