Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #133: check for duplicate identifiers in PresetParser #357

124 changes: 117 additions & 7 deletions PresetParser/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public class Program
private static readonly BuildingBlockProvider _buildingBlockProvider;
private static readonly IIfoFileProvider _ifoFileProvider;
private static readonly LocalizationHelper _localizationHelper;
private static readonly IFileSystem _fileSystem;

#region Initalisizing Exclude IdentifierNames, FactionNames and TemplateNames for presets.json file

Expand Down Expand Up @@ -132,7 +133,8 @@ static Program()
_ifoFileProvider = new IfoFileProvider();
_buildingBlockProvider = new BuildingBlockProvider(_ifoFileProvider);

_localizationHelper = new LocalizationHelper(new FileSystem());
_fileSystem = new FileSystem();
_localizationHelper = new LocalizationHelper(_fileSystem);

VersionSpecificPaths = new Dictionary<string, Dictionary<string, PathRef[]>>();
}
Expand All @@ -151,6 +153,7 @@ public static void Main(string[] args)
{
Environment.Exit(0);
}

if (annoVersion == Constants.ANNO_VERSION_1404 || annoVersion == Constants.ANNO_VERSION_2070 || annoVersion == Constants.ANNO_VERSION_2205 || annoVersion == Constants.ANNO_VERSION_1800 || annoVersion == "-ALL")
{
validVersion = true;
Expand All @@ -165,12 +168,52 @@ public static void Main(string[] args)
testVersion = true;
}
}
else if (annoVersion == "-validate")
{
Console.Write("Please enter path to file for validation: ");
var filePathToValidate = Console.ReadLine();

//get rid of quotes in the filepath (could contain spaces)
if (!string.IsNullOrWhiteSpace(filePathToValidate))
{
filePathToValidate = filePathToValidate.Trim('"');
}

if (string.IsNullOrWhiteSpace(filePathToValidate) || !_fileSystem.File.Exists(filePathToValidate))
{
var oldColor = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("The path to the file was not valid!");
Console.ForegroundColor = oldColor;
Console.ReadKey();
Environment.Exit(0);
}

try
{
var loadedPresets = SerializationHelper.LoadFromFile<BuildingPresets>(filePathToValidate);

ValidateBuildings(loadedPresets.Buildings.Cast<IBuildingInfo>().ToList());
Console.ReadLine();
Environment.Exit(0);
}
catch (Exception ex)
{
var oldColor = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("There was an error validating the file:");
Console.WriteLine(ex);
Console.ForegroundColor = oldColor;
Environment.Exit(0);
}
}
else
{
Console.WriteLine();
Console.WriteLine("Invalid input, please try again or enter 'quit to exit.");
}
}

if (annoVersion != "-ALL")
{
///Add a trailing backslash if one is not present.
Expand All @@ -183,6 +226,7 @@ public static void Main(string[] args)
BASE_PATH_2205 = GetBASE_PATH(Constants.ANNO_VERSION_2205);
BASE_PATH_1800 = GetBASE_PATH(Constants.ANNO_VERSION_1800);
}

if (!testVersion)
{
Console.WriteLine("Extracting and parsing RDA data from {0} for anno version {1}.", BASE_PATH, annoVersion);
Expand All @@ -195,6 +239,7 @@ public static void Main(string[] args)
{
Console.WriteLine("Extracting and parsing RDA data for all Anno versions");
}

#endregion

#region Anno Verion Data Paths
Expand Down Expand Up @@ -355,6 +400,12 @@ public static void Main(string[] args)
}
#endregion

#region Validate list of buildings

ValidateBuildings(buildings);

#endregion

#region Write preset.json and icon.json files
BuildingPresets presets = new BuildingPresets() { Version = BUILDING_PRESETS_VERSION, Buildings = buildings.Cast<BuildingInfo>().ToList() };

Expand All @@ -380,6 +431,50 @@ public static void Main(string[] args)
#endregion //End Prepare JSON Files
}

private static void ValidateBuildings(List<IBuildingInfo> buildingsToCheck)
{
// This list contains identifiers which are duplicated on purpose (on various places inside the preset tree) and known to not cause any errors (e.g. translation or statistics).
var knownDuplicates = new List<string> { "Logistic_02 (Warehouse I)" };

var validator = new Validator();
(bool isValid, List<string> duplicateIdentifiers) = validator.CheckForUniqueIdentifiers(buildingsToCheck, knownDuplicates);
var oldColor = Console.ForegroundColor;
if (!isValid)
{
try
{
Console.ForegroundColor = ConsoleColor.DarkYellow;

Console.WriteLine();
Console.WriteLine($"### There are duplicate identifiers ({duplicateIdentifiers.Count}) ###");
foreach (var curDuplicateIndentifier in duplicateIdentifiers)
{
Console.WriteLine(curDuplicateIndentifier);
}
Console.WriteLine();
}
finally
{
Console.ForegroundColor = oldColor;
}
}
else
{
try
{
Console.ForegroundColor = ConsoleColor.Green;

Console.WriteLine();
Console.WriteLine("There are no duplicate Indentifiers.");
Console.WriteLine();
}
finally
{
Console.ForegroundColor = oldColor;
}
}
}

// Get the BASE_PATH for the given Anno
#region Asking Directory path for the choiced Anno versions
public static string GetBASE_PATH(string annoVersion)
Expand Down Expand Up @@ -1295,7 +1390,7 @@ private static void ParseBuilding1800(List<IBuildingInfo> buildings, XmlNode bui
}

// Place all High Life Malls in the right Tree Menu
if (groupName=="Mall")
if (groupName == "Mall")
{
factionName = "(18) High Life";
}
Expand All @@ -1310,7 +1405,7 @@ private static void ParseBuilding1800(List<IBuildingInfo> buildings, XmlNode bui
// Place the rest of the buildings in the right Faction > Group menu
#region Order the Buildings to the right tiers and factions as in the game

var groupInfo = NewFactionAndGroup1800.GetNewFactionAndGroup1800(identifierName, factionName, groupName, templateName);
var groupInfo = NewFactionAndGroup1800.GetNewFactionAndGroup1800(identifierName, factionName, groupName, templateName);
factionName = groupInfo.Faction;
groupName = groupInfo.Group;
templateName = groupInfo.Template;
Expand Down Expand Up @@ -1361,7 +1456,8 @@ private static void ParseBuilding1800(List<IBuildingInfo> buildings, XmlNode bui

//Set right group to the City Lights DLC (just need a Faction and Group change by starting identifiername) (10-01-2021)
//if (templateName == "OrnamentalBuilding" && factionName == "Not Placed Yet -Moderate") {
if (templateName == "OrnamentalBuilding") {
if (templateName == "OrnamentalBuilding")
{
if (identifierName.Contains("CityOrnament "))
{
factionName = "Ornaments"; groupName = "20 City Lights";
Expand Down Expand Up @@ -1508,7 +1604,7 @@ private static void ParseBuilding1800(List<IBuildingInfo> buildings, XmlNode bui
case "102131": { icon = replaceName + "park_props_1x1_17.png"; break; } //Cypress corecting Icon
case "101284": { icon = replaceName + "community_lodge.png"; break; } //corecting Arctic Lodge Icon
}
switch(b.Identifier)
switch (b.Identifier)
{
case "AmusementPark CottonCandy": { icon = replaceName + "cotton_candy.png"; break; } // faulty naming fix icn_ instead of icon_
case "Coastal_colony02_01 (Salt Coast Building)": b.IconFileName = replaceName + "salt_africa.png"; break;
Expand Down Expand Up @@ -1634,7 +1730,7 @@ private static void ParseBuilding1800(List<IBuildingInfo> buildings, XmlNode bui
if (b.Template == "CityInstitutionBuilding")
{
b.InfluenceRange = 26; //Police - Fire stations and Hospiitals
if (b.Identifier== "Institution_arctic_01 (Ranger Station)")
if (b.Identifier == "Institution_arctic_01 (Ranger Station)")
{
b.InfluenceRange = 50; //fix Ranger Station InfluencRange as this is separated from normal ones (10-01-2021)
}
Expand All @@ -1655,7 +1751,8 @@ private static void ParseBuilding1800(List<IBuildingInfo> buildings, XmlNode bui

// Get/Set Influence Radius and Influence Range (Dual on 1 building : Busstop)
//Bussttop (has an other range name)
if (b.Template == "Busstop") {
if (b.Template == "Busstop")
{
b.InfluenceRadius = Convert.ToInt32(values?["BusStop"]?["ActivationRadius"]?.InnerText);
b.InfluenceRange = Convert.ToInt32(values["BusStop"]["StreetConnectionRange"].InnerText);
}
Expand Down Expand Up @@ -1909,6 +2006,19 @@ private static void ParseBuilding1800(List<IBuildingInfo> buildings, XmlNode bui

#endregion

#region Rename some duplicate indentifiers to avoid double identifiers on the hand of Icon Files

switch (b.IconFileName)
{
case "A7_col_park_props_system_1x1_24_back.png": b.Identifier = "Park_1x1_bush_02"; break;
case "A7_park_props_1x1_26.png": b.Identifier = "Park_1x1_bush_03"; break;
case "A7_col_park_props_system_2x2_03_back.png": b.Identifier = "Park_2x2_garden_02"; break;
case "A7_col_park_props_system_3x3_02_front.png": b.Identifier = "Park_3x3_fountain_02"; break;
case "A7_col_park_props_system_3x3_03_back.png": b.Identifier = "Park_3x3_gazebo_02"; break;
}

#endregion

// Remove CultureModules Menu that Appeared
if (b.Header == "(A7) Anno 1800" && b.Faction == "All Worlds" && b.Group == "CultureModule") { return; }

Expand Down
2 changes: 2 additions & 0 deletions PresetParser/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@
// by using the '*' as shown below:
[assembly: AssemblyVersion("9.1.0.0")]
[assembly: AssemblyFileVersion("9.1.0.0")]

[assembly: InternalsVisibleTo("PresetParser.Tests")]
28 changes: 28 additions & 0 deletions PresetParser/Validator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AnnoDesigner.Core.Presets.Models;

namespace PresetParser
{
internal class Validator
{
public (bool isValid, List<string> duplicateIdentifiers) CheckForUniqueIdentifiers(List<IBuildingInfo> buildingsToCheck, List<string> knownDuplicates)
{
var result = (true, new List<string>());

knownDuplicates ??= new List<string>();

var duplicates = buildingsToCheck.GroupBy(x => x.Identifier).Where(x => x.Count() > 1).Select(x => x.Key).ToList();
//remove known duplicates from result
duplicates = duplicates.Except(knownDuplicates, StringComparer.Ordinal).ToList();

if (duplicates.Count > 0)
{
result = (false, duplicates);
}

return result;
}
}
}
2 changes: 1 addition & 1 deletion Presets/presets.json

Large diffs are not rendered by default.

116 changes: 116 additions & 0 deletions Tests/PresetParser.Tests/ValidatorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
using System.Collections.Generic;
using AnnoDesigner.Core.Presets.Models;
using Xunit;
using Xunit.Abstractions;

namespace PresetParser.Tests
{
public class ValidatorTests
{
private readonly ITestOutputHelper _out;

private readonly Validator _validator;
private readonly List<IBuildingInfo> _testData_valid_buildingInfo;
private readonly List<IBuildingInfo> _testData_invalid_buildingInfo;

public ValidatorTests(ITestOutputHelper outputHelperToUse)
{
_out = outputHelperToUse;
_validator = new Validator();

_testData_valid_buildingInfo = new List<IBuildingInfo>
{
new BuildingInfo { Identifier = "A4_building 1" },
new BuildingInfo { Identifier = "A4_building 2" },
new BuildingInfo { Identifier = "A4_building 3" },
new BuildingInfo { Identifier = "A7_building 3" },
};

_testData_invalid_buildingInfo = new List<IBuildingInfo>
{
new BuildingInfo { Identifier = "A4_building 1" },
new BuildingInfo { Identifier = "A4_building 2" },
new BuildingInfo { Identifier = "A4_building 1" },
new BuildingInfo { Identifier = "A4_building 3" },
};
}

[Fact]
public void CheckForUniqueIdentifiers_KnownDuplicatesIsNull_ShouldNotThrow()
{
// Arrange/Act
var result = _validator.CheckForUniqueIdentifiers(_testData_valid_buildingInfo, null);

// Assert
Assert.True(result.isValid);
Assert.Empty(result.duplicateIdentifiers);
}

[Fact]
public void CheckForUniqueIdentifiers_KnownDuplicatesIsEmpty_ShouldNotThrow()
{
// Arrange/Act
var result = _validator.CheckForUniqueIdentifiers(_testData_valid_buildingInfo, new List<string>());

// Assert
Assert.True(result.isValid);
Assert.Empty(result.duplicateIdentifiers);
}

[Fact]
public void CheckForUniqueIdentifiers_NoDuplicateIdentifiers_ShouldReturnIsValid()
{
// Arrange/Act
var result = _validator.CheckForUniqueIdentifiers(_testData_valid_buildingInfo, new List<string>());

// Assert
Assert.True(result.isValid);
Assert.Empty(result.duplicateIdentifiers);
}

[Fact]
public void CheckForUniqueIdentifiers_DuplicateIdentifiers_ShouldReturnNotValidAndListOfIdentifiers()
{
// Arrange/Act
var result = _validator.CheckForUniqueIdentifiers(_testData_invalid_buildingInfo, new List<string>());

// Assert
Assert.False(result.isValid);
Assert.NotEmpty(result.duplicateIdentifiers);
_out.WriteLine(string.Join(" ,", result.duplicateIdentifiers));
}

[Fact]
public void CheckForUniqueIdentifiers_DuplicateIdentifiersAndKnownDuplicates_ShouldReturnNotValidAndListOfIdentifiers()
{
// Arrange
var knownDuplicate = "A99_known duplicate";
_testData_invalid_buildingInfo.Add(new BuildingInfo { Identifier = knownDuplicate });
_testData_invalid_buildingInfo.Insert(0, new BuildingInfo { Identifier = knownDuplicate });

// Arrange
var result = _validator.CheckForUniqueIdentifiers(_testData_invalid_buildingInfo, new List<string> { knownDuplicate });

// Assert
Assert.False(result.isValid);
Assert.NotEmpty(result.duplicateIdentifiers);
_out.WriteLine(string.Join(" ,", result.duplicateIdentifiers));
}

[Fact]
public void CheckForUniqueIdentifiers_KnownDuplicatesFound_ShouldReturnIsValid()
{
// Arrange
var knownDuplicate = "A99_known duplicate";
_testData_valid_buildingInfo.Add(new BuildingInfo { Identifier = knownDuplicate });
_testData_valid_buildingInfo.Insert(0, new BuildingInfo { Identifier = knownDuplicate });

// Act
var result = _validator.CheckForUniqueIdentifiers(_testData_valid_buildingInfo, new List<string> { knownDuplicate });

// Assert
Assert.True(result.isValid);
Assert.Empty(result.duplicateIdentifiers);
}
}
}