diff --git a/ImperatorToCK3.UnitTests/CK3/Characters/CharacterCollectionTests.cs b/ImperatorToCK3.UnitTests/CK3/Characters/CharacterCollectionTests.cs index f61d60068..e185b8aa2 100644 --- a/ImperatorToCK3.UnitTests/CK3/Characters/CharacterCollectionTests.cs +++ b/ImperatorToCK3.UnitTests/CK3/Characters/CharacterCollectionTests.cs @@ -54,7 +54,8 @@ static CharacterCollectionTests() { "1={} 2={} 3={} 4={} 5={} 6={} 7={} 8={} 9={} 69={}" ), states, - countries + countries, + irMapData ); AreaCollection areas = new(); areas.LoadAreas(irModFS, irProvinces); diff --git a/ImperatorToCK3.UnitTests/CK3/Titles/LandedTitlesTests.cs b/ImperatorToCK3.UnitTests/CK3/Titles/LandedTitlesTests.cs index 3ae486bd1..44cb7846a 100644 --- a/ImperatorToCK3.UnitTests/CK3/Titles/LandedTitlesTests.cs +++ b/ImperatorToCK3.UnitTests/CK3/Titles/LandedTitlesTests.cs @@ -51,7 +51,8 @@ static LandedTitlesTests() { irProvinces.LoadProvinces( new BufferedReader("1={} 2={} 3={} 4={} 5={} 6={} 7={} 8={} 9={} 69={}"), new StateCollection(), - new CountryCollection() + new CountryCollection(), + irMapData ); AreaCollection areas = new(); areas.LoadAreas(irModFS, irProvinces); diff --git a/ImperatorToCK3.UnitTests/Imperator/Provinces/ProvincesTests.cs b/ImperatorToCK3.UnitTests/Imperator/Provinces/ProvincesTests.cs index 7fdea0f6a..00e005812 100644 --- a/ImperatorToCK3.UnitTests/Imperator/Provinces/ProvincesTests.cs +++ b/ImperatorToCK3.UnitTests/Imperator/Provinces/ProvincesTests.cs @@ -1,4 +1,6 @@ using commonItems; +using commonItems.Mods; +using ImperatorToCK3.CommonUtils.Map; using ImperatorToCK3.Imperator.Countries; using ImperatorToCK3.Imperator.States; using System; @@ -12,11 +14,13 @@ namespace ImperatorToCK3.UnitTests.Imperator.Provinces; public class ProvincesTests { private readonly StateCollection states = new(); private readonly CountryCollection countries = new(); + private static readonly MapData irMapData = new(new ModFilesystem("TestFiles/ProvincesTests", [])); + [Fact] public void ProvincesDefaultToEmpty() { var reader = new BufferedReader("={}"); var provinces = new ImperatorToCK3.Imperator.Provinces.ProvinceCollection(); - provinces.LoadProvinces(reader, states, countries); + provinces.LoadProvinces(reader, states, countries, irMapData); Assert.Empty(provinces); } @@ -32,7 +36,7 @@ public void ProvincesCanBeLoaded() { """ ); var provinces = new ImperatorToCK3.Imperator.Provinces.ProvinceCollection(); - provinces.LoadProvinces(reader, states, countries); + provinces.LoadProvinces(reader, states, countries, irMapData); Assert.Equal((ulong)42, provinces[42].Id); Assert.Equal((ulong)43, provinces[43].Id); @@ -42,7 +46,7 @@ public void ProvincesCanBeLoaded() { public void PopCanBeLinked() { var reader = new BufferedReader("={42={pop=8}}\n"); var provinces = new ImperatorToCK3.Imperator.Provinces.ProvinceCollection(); - provinces.LoadProvinces(reader, states, countries); + provinces.LoadProvinces(reader, states, countries, irMapData); var reader2 = new BufferedReader( "8={type=\"citizen\" culture=\"roman\" religion=\"paradoxian\"}\n" @@ -68,7 +72,7 @@ public void MultiplePopsCanBeLinked() { "}\n" ); var provinces = new ImperatorToCK3.Imperator.Provinces.ProvinceCollection(); - provinces.LoadProvinces(reader, states, countries); + provinces.LoadProvinces(reader, states, countries, irMapData); var reader2 = new BufferedReader( "={\n" + @@ -105,7 +109,7 @@ public void BrokenLinkAttemptThrowsWarning() { "}\n" ); var provinces = new ImperatorToCK3.Imperator.Provinces.ProvinceCollection(); - provinces.LoadProvinces(reader, states, countries); + provinces.LoadProvinces(reader, states, countries, irMapData); var reader2 = new BufferedReader( "={\n" + diff --git a/ImperatorToCK3.UnitTests/Imperator/States/StateTests.cs b/ImperatorToCK3.UnitTests/Imperator/States/StateTests.cs index ae1cb07d5..26cd2734e 100644 --- a/ImperatorToCK3.UnitTests/Imperator/States/StateTests.cs +++ b/ImperatorToCK3.UnitTests/Imperator/States/StateTests.cs @@ -1,6 +1,7 @@ using commonItems; using commonItems.Mods; using FluentAssertions; +using ImperatorToCK3.CommonUtils.Map; using ImperatorToCK3.Imperator.Countries; using ImperatorToCK3.Imperator.Geography; using ImperatorToCK3.Imperator.Provinces; @@ -65,7 +66,7 @@ public void ProvincesCanBeRetrievedAfterProvincesInitialization() { 5={} """ ); - provinces.LoadProvinces(provincesReader, states, countries); + provinces.LoadProvinces(provincesReader, states, countries, new MapData(irModFS)); Assert.Equal((ulong)2, state.CapitalProvince.Id); state.Provinces.Select(p=>p.Id).Should().Equal(1, 2, 3); } diff --git a/ImperatorToCK3.UnitTests/Mappers/Region/ImperatorAreaTests.cs b/ImperatorToCK3.UnitTests/Mappers/Region/ImperatorAreaTests.cs index 2cb333efd..0d75f1b2c 100644 --- a/ImperatorToCK3.UnitTests/Mappers/Region/ImperatorAreaTests.cs +++ b/ImperatorToCK3.UnitTests/Mappers/Region/ImperatorAreaTests.cs @@ -1,4 +1,6 @@ using commonItems; +using commonItems.Mods; +using ImperatorToCK3.CommonUtils.Map; using ImperatorToCK3.Imperator.Countries; using ImperatorToCK3.Imperator.Geography; using ImperatorToCK3.Imperator.Provinces; @@ -11,7 +13,8 @@ namespace ImperatorToCK3.UnitTests.Mappers.Region; [Collection("Sequential")] [CollectionDefinition("Sequential", DisableParallelization = true)] public class ImperatorAreaTests { - private readonly ProvinceCollection provinces = new(); + private readonly ProvinceCollection provinces = []; + private static readonly MapData irMapData = new(new ModFilesystem("TestFiles/AreaTests", [])); public ImperatorAreaTests() { var states = new StateCollection(); @@ -21,7 +24,8 @@ public ImperatorAreaTests() { "1={} 2={} 3={} 4={} 5={} 6={} 7={} 8={} 9={} 69={}" ), states, - countries + countries, + irMapData ); } diff --git a/ImperatorToCK3.UnitTests/Mappers/Region/ImperatorRegionMapperTests.cs b/ImperatorToCK3.UnitTests/Mappers/Region/ImperatorRegionMapperTests.cs index 7097fde21..4f651103d 100644 --- a/ImperatorToCK3.UnitTests/Mappers/Region/ImperatorRegionMapperTests.cs +++ b/ImperatorToCK3.UnitTests/Mappers/Region/ImperatorRegionMapperTests.cs @@ -17,15 +17,15 @@ namespace ImperatorToCK3.UnitTests.Mappers.Region; [CollectionDefinition("Sequential", DisableParallelization = true)] public class ImperatorRegionMapperTests { private const string ImperatorRoot = "TestFiles/Imperator/root"; - private static readonly ModFilesystem irModFS = new(ImperatorRoot, System.Array.Empty()); + private static readonly ModFilesystem irModFS = new(ImperatorRoot, []); private static readonly MapData irMapData = new(irModFS); - private readonly ProvinceCollection provinces = new(); - private static readonly ColorFactory ColorFactory = new(); + private readonly ProvinceCollection provinces = []; + private static readonly ColorFactory colorFactory = new(); public ImperatorRegionMapperTests() { provinces.LoadProvinces(new BufferedReader( "1={} 2={} 3={} 4={} 5={} 6={} 7={} 8={} 9={} 69={}") - , new StateCollection(), new CountryCollection()); + , new StateCollection(), new CountryCollection(), irMapData); } [Fact] @@ -33,7 +33,7 @@ public void RegionMapperCanBeEnabled() { // We start humble, it's a machine. var areas = new AreaCollection(); var irRegionMapper = new ImperatorRegionMapper(areas, irMapData); - irRegionMapper.LoadRegions(irModFS, ColorFactory); + irRegionMapper.LoadRegions(irModFS, colorFactory); Assert.False(irRegionMapper.ProvinceIsInRegion(1, "test")); Assert.False(irRegionMapper.RegionNameIsValid("test")); @@ -50,7 +50,7 @@ public void LoadingBrokenAreaWillThrowException() { var areas = new AreaCollection(); var irRegionMapper = new ImperatorRegionMapper(areas, irMapData); - Assert.Throws(() => irRegionMapper.LoadRegions(imperatorModFS, ColorFactory)); + Assert.Throws(() => irRegionMapper.LoadRegions(imperatorModFS, colorFactory)); } [Fact] @@ -62,7 +62,7 @@ public void LocationServicesWork() { var areas = new AreaCollection(); areas.LoadAreas(imperatorModFS, irProvinces); var theMapper = new ImperatorRegionMapper(areas, irMapData); - theMapper.LoadRegions(imperatorModFS, ColorFactory); + theMapper.LoadRegions(imperatorModFS, colorFactory); Assert.True(theMapper.ProvinceIsInRegion(3, "test_area")); Assert.True(theMapper.ProvinceIsInRegion(3, "test_region")); @@ -77,7 +77,7 @@ public void LocationServicesCorrectlyFail() { var areas = new AreaCollection(); areas.LoadAreas(imperatorModFS, irProvinces); var theMapper = new ImperatorRegionMapper(areas, irMapData); - theMapper.LoadRegions(irModFS, ColorFactory); + theMapper.LoadRegions(irModFS, colorFactory); Assert.False(theMapper.ProvinceIsInRegion(3, "test_area2")); // province in different area Assert.False(theMapper.ProvinceIsInRegion(9, "test_region")); // province in different region @@ -93,7 +93,7 @@ public void LocationServicesFailForNonsense() { var areas = new AreaCollection(); areas.LoadAreas(imperatorModFS, irProvinces); var theMapper = new ImperatorRegionMapper(areas, irMapData); - theMapper.LoadRegions(irModFS, ColorFactory); + theMapper.LoadRegions(irModFS, colorFactory); Assert.False(theMapper.ProvinceIsInRegion(1, "nonsense")); } @@ -107,7 +107,7 @@ public void CorrectParentLocationsReported() { var areas = new AreaCollection(); areas.LoadAreas(imperatorModFS, irProvinces); var theMapper = new ImperatorRegionMapper(areas, irMapData); - theMapper.LoadRegions(imperatorModFS, ColorFactory); + theMapper.LoadRegions(imperatorModFS, colorFactory); Assert.Equal("test_area", theMapper.GetParentAreaName(2)); Assert.Equal("test_region", theMapper.GetParentRegionName(2)); @@ -124,7 +124,7 @@ public void WrongParentLocationsReturnNull() { var areas = new AreaCollection(); areas.LoadAreas(imperatorModFS, irProvinces); var theMapper = new ImperatorRegionMapper(areas, irMapData); - theMapper.LoadRegions(irModFS, ColorFactory); + theMapper.LoadRegions(irModFS, colorFactory); Assert.Null(theMapper.GetParentAreaName(5)); Assert.Null(theMapper.GetParentRegionName(5)); @@ -139,7 +139,7 @@ public void LocationNameValidationWorks() { var areas = new AreaCollection(); areas.LoadAreas(imperatorModFS, irProvinces); var theMapper = new ImperatorRegionMapper(areas, irMapData); - theMapper.LoadRegions(imperatorModFS, ColorFactory); + theMapper.LoadRegions(imperatorModFS, colorFactory); Assert.True(theMapper.RegionNameIsValid("test_area")); Assert.True(theMapper.RegionNameIsValid("test_area2")); @@ -158,7 +158,7 @@ public void ModAreasAndRegionsCanBeLoaded() { var areas = new AreaCollection(); areas.LoadAreas(imperatorModFS, irProvinces); var theMapper = new ImperatorRegionMapper(areas, irMapData); - theMapper.LoadRegions(imperatorModFS, ColorFactory); + theMapper.LoadRegions(imperatorModFS, colorFactory); Assert.False(theMapper.RegionNameIsValid("vanilla_area")); // present only in vanilla file which is overriden by mod Assert.True(theMapper.RegionNameIsValid("common_area")); diff --git a/ImperatorToCK3.UnitTests/Mappers/Region/ImperatorRegionTests.cs b/ImperatorToCK3.UnitTests/Mappers/Region/ImperatorRegionTests.cs index 067e6dfbf..fb67b1a1f 100644 --- a/ImperatorToCK3.UnitTests/Mappers/Region/ImperatorRegionTests.cs +++ b/ImperatorToCK3.UnitTests/Mappers/Region/ImperatorRegionTests.cs @@ -1,6 +1,8 @@ using commonItems; using commonItems.Collections; using commonItems.Colors; +using commonItems.Mods; +using ImperatorToCK3.CommonUtils.Map; using ImperatorToCK3.Imperator.Countries; using ImperatorToCK3.Imperator.Geography; using ImperatorToCK3.Imperator.Provinces; @@ -11,19 +13,20 @@ namespace ImperatorToCK3.UnitTests.Mappers.Region; public class ImperatorRegionTests { - private readonly ProvinceCollection provinces = new(); - private static readonly ColorFactory ColorFactory = new(); + private readonly ProvinceCollection provinces = []; + private static readonly ColorFactory colorFactory = new(); + private static readonly MapData irMapData = new(new ModFilesystem("TestFiles/RegionTests", [])); public ImperatorRegionTests() { provinces.LoadProvinces(new BufferedReader( "1={} 2={} 3={} 4={} 5={} 6={} 7={} 8={} 9={} 69={}") - , new StateCollection(), new CountryCollection()); + , new StateCollection(), new CountryCollection(), irMapData); } [Fact] public void BlankRegionLoadsWithNoAreas() { var reader = new BufferedReader(string.Empty); - var region = new ImperatorRegion("region1", reader, new AreaCollection(), ColorFactory); + var region = new ImperatorRegion("region1", reader, new AreaCollection(), colorFactory); Assert.Empty(region.Areas); } @@ -35,7 +38,7 @@ public void RegionCanBeLinkedToArea() { var areas = new IdObjectCollection { area }; var reader1 = new BufferedReader("areas = { test1 }"); - var region = new ImperatorRegion("region1", reader1, areas, ColorFactory); + var region = new ImperatorRegion("region1", reader1, areas, colorFactory); Assert.NotNull(region.Areas["test1"]); } @@ -48,7 +51,7 @@ public void RegionCanBeLinkedToArea() { var areas = new IdObjectCollection { area1, area2, area3 }; var reader = new BufferedReader("areas = { test1 test2 test3 }"); - var region = new ImperatorRegion("region1", reader, areas, ColorFactory); + var region = new ImperatorRegion("region1", reader, areas, colorFactory); Assert.Collection(region.Areas, item => Assert.Equal("test1", item.Id), @@ -64,7 +67,7 @@ public void LinkedRegionCanLocateProvince() { var areas = new IdObjectCollection { area }; var reader1 = new BufferedReader("{ areas={area1} }"); - var region = new ImperatorRegion("region1", reader1, areas, ColorFactory); + var region = new ImperatorRegion("region1", reader1, areas, colorFactory); Assert.True(region.ContainsProvince(6)); } @@ -76,7 +79,7 @@ public void LinkedRegionWillFailForProvinceMismatch() { var areas = new IdObjectCollection { area }; var reader1 = new BufferedReader("{ areas={area1} }"); - var region = new ImperatorRegion("region1", reader1, areas, ColorFactory); + var region = new ImperatorRegion("region1", reader1, areas, colorFactory); Assert.False(region.ContainsProvince(7)); } diff --git a/ImperatorToCK3/CommonUtils/Map/MapData.cs b/ImperatorToCK3/CommonUtils/Map/MapData.cs index d46bd7f61..60b8aced9 100644 --- a/ImperatorToCK3/CommonUtils/Map/MapData.cs +++ b/ImperatorToCK3/CommonUtils/Map/MapData.cs @@ -192,7 +192,7 @@ public IReadOnlySet GetNeighborProvinceIds(ulong provinceId) { return NeighborsDict.TryGetValue(provinceId, out var neighbors) ? neighbors : []; } - private bool IsColorableImpassable(ulong provinceId) => ProvinceDefinitions[provinceId].IsColorableImpassable; + public bool IsColorableImpassable(ulong provinceId) => ProvinceDefinitions.TryGetValue(provinceId, out var province) && province.IsColorableImpassable; public bool IsImpassable(ulong provinceId) => ProvinceDefinitions.TryGetValue(provinceId, out var province) && province.IsImpassable; diff --git a/ImperatorToCK3/Imperator/Provinces/ProvinceCollection.cs b/ImperatorToCK3/Imperator/Provinces/ProvinceCollection.cs index ed012b4b5..fa60bf781 100644 --- a/ImperatorToCK3/Imperator/Provinces/ProvinceCollection.cs +++ b/ImperatorToCK3/Imperator/Provinces/ProvinceCollection.cs @@ -1,14 +1,16 @@ using commonItems; using commonItems.Collections; +using ImperatorToCK3.CommonUtils.Map; using ImperatorToCK3.Imperator.Countries; using ImperatorToCK3.Imperator.Pops; using ImperatorToCK3.Imperator.States; +using System.Collections.Generic; using System.Linq; namespace ImperatorToCK3.Imperator.Provinces; internal sealed class ProvinceCollection : IdObjectCollection { - public void LoadProvinces(BufferedReader provincesReader, StateCollection states, CountryCollection countries) { + public void LoadProvinces(BufferedReader provincesReader, StateCollection states, CountryCollection countries, MapData irMapData) { var parser = new Parser(); parser.RegisterRegex(CommonRegexes.Integer, (reader, provIdStr) => { var newProvince = Province.Parse(reader, ulong.Parse(provIdStr), states, countries); @@ -16,9 +18,61 @@ public void LoadProvinces(BufferedReader provincesReader, StateCollection states }); parser.RegisterRegex(CommonRegexes.Catchall, ParserHelpers.IgnoreAndLogItem); parser.ParseStream(provincesReader); + + // After all the provinces are loaded, we can determine if there are impassables to be considered owned. + // This should match the impassables colored with a country color on the Imperator map. + DetermineImpassableOwnership(irMapData); } public void LinkPops(PopCollection pops) { var counter = this.Sum(province => province.LinkPops(pops)); Logger.Info($"{counter} pops linked to provinces."); } + + private void DetermineImpassableOwnership(MapData irMapData) { + // Store the map of province -> country to be assigned in a dict, to avoid one impassable being given an owner + // skewing the calculation for the neighboring impassables. + Dictionary impassableOwnership = []; + + foreach (var province in this) { + if (province.OwnerCountry is not null) { + continue; + } + + if (!irMapData.IsColorableImpassable(province.Id)) { + continue; + } + + Country? country = GetCountryForColorableImpassable(province.Id, irMapData); + if (country is null) { + continue; + } + + impassableOwnership[province.Id] = country; + } + + foreach (var (provinceId, country) in impassableOwnership) { + var province = this[provinceId]; + province.OwnerCountry = country; + country.RegisterProvince(province); + } + } + + private Country? GetCountryForColorableImpassable(ulong provinceId, MapData irMapData) { + var neighborProvIds = irMapData.GetNeighborProvinceIds(provinceId); + int neighborsCount = neighborProvIds.Count; + + // Group the neighboring provinces by their owner. The one with most owned neighbors may be the owner of the impassable. + var ownerCandidate = neighborProvIds + .Select(provId => this[provId].OwnerCountry) + .Where(country => country is not null) + .GroupBy(country => country) + .OrderByDescending(group => group.Count()) + .FirstOrDefault(); + + // If any country controls at least half of the neighboring provinces, the impassable should be colored. + if (ownerCandidate is not null && ownerCandidate.Count() >= (float)neighborsCount / 2) { + return ownerCandidate.Key; + } + return null; + } } \ No newline at end of file diff --git a/ImperatorToCK3/Imperator/World.cs b/ImperatorToCK3/Imperator/World.cs index 346e41649..df4e515a2 100644 --- a/ImperatorToCK3/Imperator/World.cs +++ b/ImperatorToCK3/Imperator/World.cs @@ -487,7 +487,7 @@ private SimpleDel LoadPlayerCountries(OrderedSet playerCountriesToLog) { private void LoadProvinces(BufferedReader reader) { Logger.Info("Loading provinces..."); - Provinces.LoadProvinces(reader, States, Countries); + Provinces.LoadProvinces(reader, States, Countries, MapData); Logger.Debug($"Ignored Province tokens: {Province.IgnoredTokens}"); Logger.Info($"Loaded {Provinces.Count} provinces."); @@ -638,6 +638,7 @@ private void LoadModFilesystemDependentData() { () => LoadImperatorLocalization(), () => { MapData = new MapData(ModFS); + Areas.LoadAreas(ModFS, Provinces); ImperatorRegionMapper = new ImperatorRegionMapper(Areas, MapData); }, diff --git a/ImperatorToCK3/Resources/forum/release_post_template.txt b/ImperatorToCK3/Resources/forum/release_post_template.txt new file mode 100644 index 000000000..23ff1ee54 --- /dev/null +++ b/ImperatorToCK3/Resources/forum/release_post_template.txt @@ -0,0 +1,16 @@ +[CENTER][ATTACH type="full" alt="1746694710355.png"]1292768[/ATTACH] + +[B][SIZE=18px]NEW MAJOR VERSION NOW RELEASED![/SIZE] +([URL='https://github.com/ParadoxGameConverters/ImperatorToCK3/releases/download//ImperatorToCK3-win-x64-setup.exe']DIRECT WINDOWS DOWNLOAD[/URL])[/B][/CENTER] + +[B][SIZE=18px]ImperatorToCK3 ""[/SIZE][/B] +[SIZE=15px][B]Compatible with Imperator: Rome 2.0 and Crusader Kings III .[/B][/SIZE] + +[QUOTE] +[SIZE=15px][B]Notable changes[/B][/SIZE] +[LIST] +[*]Example entry 1 +[*]Example entry 2 +[*]Example entry 3 +[/LIST] +[/QUOTE] \ No newline at end of file