Skip to content

Commit a846c0b

Browse files
authored
Consider an I:R colorable impassable to be owned by a country when at least half of its neighboring provinces belong to that country (#2601) #minor
Results of an example conversion: - Before: ![obraz](https://github.com/user-attachments/assets/23250ec6-2e65-419b-8234-a8ca758e07ab) - After: ![obraz](https://github.com/user-attachments/assets/768d571e-e6ad-495b-886b-5840ecca0b1d)
1 parent 020441a commit a846c0b

File tree

11 files changed

+119
-34
lines changed

11 files changed

+119
-34
lines changed

ImperatorToCK3.UnitTests/CK3/Characters/CharacterCollectionTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ static CharacterCollectionTests() {
5454
"1={} 2={} 3={} 4={} 5={} 6={} 7={} 8={} 9={} 69={}"
5555
),
5656
states,
57-
countries
57+
countries,
58+
irMapData
5859
);
5960
AreaCollection areas = new();
6061
areas.LoadAreas(irModFS, irProvinces);

ImperatorToCK3.UnitTests/CK3/Titles/LandedTitlesTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ static LandedTitlesTests() {
5151
irProvinces.LoadProvinces(
5252
new BufferedReader("1={} 2={} 3={} 4={} 5={} 6={} 7={} 8={} 9={} 69={}"),
5353
new StateCollection(),
54-
new CountryCollection()
54+
new CountryCollection(),
55+
irMapData
5556
);
5657
AreaCollection areas = new();
5758
areas.LoadAreas(irModFS, irProvinces);

ImperatorToCK3.UnitTests/Imperator/Provinces/ProvincesTests.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using commonItems;
2+
using commonItems.Mods;
3+
using ImperatorToCK3.CommonUtils.Map;
24
using ImperatorToCK3.Imperator.Countries;
35
using ImperatorToCK3.Imperator.States;
46
using System;
@@ -12,11 +14,13 @@ namespace ImperatorToCK3.UnitTests.Imperator.Provinces;
1214
public class ProvincesTests {
1315
private readonly StateCollection states = new();
1416
private readonly CountryCollection countries = new();
17+
private static readonly MapData irMapData = new(new ModFilesystem("TestFiles/ProvincesTests", []));
18+
1519
[Fact]
1620
public void ProvincesDefaultToEmpty() {
1721
var reader = new BufferedReader("={}");
1822
var provinces = new ImperatorToCK3.Imperator.Provinces.ProvinceCollection();
19-
provinces.LoadProvinces(reader, states, countries);
23+
provinces.LoadProvinces(reader, states, countries, irMapData);
2024

2125
Assert.Empty(provinces);
2226
}
@@ -32,7 +36,7 @@ public void ProvincesCanBeLoaded() {
3236
"""
3337
);
3438
var provinces = new ImperatorToCK3.Imperator.Provinces.ProvinceCollection();
35-
provinces.LoadProvinces(reader, states, countries);
39+
provinces.LoadProvinces(reader, states, countries, irMapData);
3640

3741
Assert.Equal((ulong)42, provinces[42].Id);
3842
Assert.Equal((ulong)43, provinces[43].Id);
@@ -42,7 +46,7 @@ public void ProvincesCanBeLoaded() {
4246
public void PopCanBeLinked() {
4347
var reader = new BufferedReader("={42={pop=8}}\n");
4448
var provinces = new ImperatorToCK3.Imperator.Provinces.ProvinceCollection();
45-
provinces.LoadProvinces(reader, states, countries);
49+
provinces.LoadProvinces(reader, states, countries, irMapData);
4650

4751
var reader2 = new BufferedReader(
4852
"8={type=\"citizen\" culture=\"roman\" religion=\"paradoxian\"}\n"
@@ -68,7 +72,7 @@ public void MultiplePopsCanBeLinked() {
6872
"}\n"
6973
);
7074
var provinces = new ImperatorToCK3.Imperator.Provinces.ProvinceCollection();
71-
provinces.LoadProvinces(reader, states, countries);
75+
provinces.LoadProvinces(reader, states, countries, irMapData);
7276

7377
var reader2 = new BufferedReader(
7478
"={\n" +
@@ -105,7 +109,7 @@ public void BrokenLinkAttemptThrowsWarning() {
105109
"}\n"
106110
);
107111
var provinces = new ImperatorToCK3.Imperator.Provinces.ProvinceCollection();
108-
provinces.LoadProvinces(reader, states, countries);
112+
provinces.LoadProvinces(reader, states, countries, irMapData);
109113

110114
var reader2 = new BufferedReader(
111115
"={\n" +

ImperatorToCK3.UnitTests/Imperator/States/StateTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using commonItems;
22
using commonItems.Mods;
33
using FluentAssertions;
4+
using ImperatorToCK3.CommonUtils.Map;
45
using ImperatorToCK3.Imperator.Countries;
56
using ImperatorToCK3.Imperator.Geography;
67
using ImperatorToCK3.Imperator.Provinces;
@@ -65,7 +66,7 @@ public void ProvincesCanBeRetrievedAfterProvincesInitialization() {
6566
5={}
6667
"""
6768
);
68-
provinces.LoadProvinces(provincesReader, states, countries);
69+
provinces.LoadProvinces(provincesReader, states, countries, new MapData(irModFS));
6970
Assert.Equal((ulong)2, state.CapitalProvince.Id);
7071
state.Provinces.Select(p=>p.Id).Should().Equal(1, 2, 3);
7172
}

ImperatorToCK3.UnitTests/Mappers/Region/ImperatorAreaTests.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using commonItems;
2+
using commonItems.Mods;
3+
using ImperatorToCK3.CommonUtils.Map;
24
using ImperatorToCK3.Imperator.Countries;
35
using ImperatorToCK3.Imperator.Geography;
46
using ImperatorToCK3.Imperator.Provinces;
@@ -11,7 +13,8 @@ namespace ImperatorToCK3.UnitTests.Mappers.Region;
1113
[Collection("Sequential")]
1214
[CollectionDefinition("Sequential", DisableParallelization = true)]
1315
public class ImperatorAreaTests {
14-
private readonly ProvinceCollection provinces = new();
16+
private readonly ProvinceCollection provinces = [];
17+
private static readonly MapData irMapData = new(new ModFilesystem("TestFiles/AreaTests", []));
1518

1619
public ImperatorAreaTests() {
1720
var states = new StateCollection();
@@ -21,7 +24,8 @@ public ImperatorAreaTests() {
2124
"1={} 2={} 3={} 4={} 5={} 6={} 7={} 8={} 9={} 69={}"
2225
),
2326
states,
24-
countries
27+
countries,
28+
irMapData
2529
);
2630
}
2731

ImperatorToCK3.UnitTests/Mappers/Region/ImperatorRegionMapperTests.cs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,23 @@ namespace ImperatorToCK3.UnitTests.Mappers.Region;
1717
[CollectionDefinition("Sequential", DisableParallelization = true)]
1818
public class ImperatorRegionMapperTests {
1919
private const string ImperatorRoot = "TestFiles/Imperator/root";
20-
private static readonly ModFilesystem irModFS = new(ImperatorRoot, System.Array.Empty<Mod>());
20+
private static readonly ModFilesystem irModFS = new(ImperatorRoot, []);
2121
private static readonly MapData irMapData = new(irModFS);
22-
private readonly ProvinceCollection provinces = new();
23-
private static readonly ColorFactory ColorFactory = new();
22+
private readonly ProvinceCollection provinces = [];
23+
private static readonly ColorFactory colorFactory = new();
2424

2525
public ImperatorRegionMapperTests() {
2626
provinces.LoadProvinces(new BufferedReader(
2727
"1={} 2={} 3={} 4={} 5={} 6={} 7={} 8={} 9={} 69={}")
28-
, new StateCollection(), new CountryCollection());
28+
, new StateCollection(), new CountryCollection(), irMapData);
2929
}
3030

3131
[Fact]
3232
public void RegionMapperCanBeEnabled() {
3333
// We start humble, it's a machine.
3434
var areas = new AreaCollection();
3535
var irRegionMapper = new ImperatorRegionMapper(areas, irMapData);
36-
irRegionMapper.LoadRegions(irModFS, ColorFactory);
36+
irRegionMapper.LoadRegions(irModFS, colorFactory);
3737

3838
Assert.False(irRegionMapper.ProvinceIsInRegion(1, "test"));
3939
Assert.False(irRegionMapper.RegionNameIsValid("test"));
@@ -50,7 +50,7 @@ public void LoadingBrokenAreaWillThrowException() {
5050
var areas = new AreaCollection();
5151

5252
var irRegionMapper = new ImperatorRegionMapper(areas, irMapData);
53-
Assert.Throws<KeyNotFoundException>(() => irRegionMapper.LoadRegions(imperatorModFS, ColorFactory));
53+
Assert.Throws<KeyNotFoundException>(() => irRegionMapper.LoadRegions(imperatorModFS, colorFactory));
5454
}
5555

5656
[Fact]
@@ -62,7 +62,7 @@ public void LocationServicesWork() {
6262
var areas = new AreaCollection();
6363
areas.LoadAreas(imperatorModFS, irProvinces);
6464
var theMapper = new ImperatorRegionMapper(areas, irMapData);
65-
theMapper.LoadRegions(imperatorModFS, ColorFactory);
65+
theMapper.LoadRegions(imperatorModFS, colorFactory);
6666

6767
Assert.True(theMapper.ProvinceIsInRegion(3, "test_area"));
6868
Assert.True(theMapper.ProvinceIsInRegion(3, "test_region"));
@@ -77,7 +77,7 @@ public void LocationServicesCorrectlyFail() {
7777
var areas = new AreaCollection();
7878
areas.LoadAreas(imperatorModFS, irProvinces);
7979
var theMapper = new ImperatorRegionMapper(areas, irMapData);
80-
theMapper.LoadRegions(irModFS, ColorFactory);
80+
theMapper.LoadRegions(irModFS, colorFactory);
8181

8282
Assert.False(theMapper.ProvinceIsInRegion(3, "test_area2")); // province in different area
8383
Assert.False(theMapper.ProvinceIsInRegion(9, "test_region")); // province in different region
@@ -93,7 +93,7 @@ public void LocationServicesFailForNonsense() {
9393
var areas = new AreaCollection();
9494
areas.LoadAreas(imperatorModFS, irProvinces);
9595
var theMapper = new ImperatorRegionMapper(areas, irMapData);
96-
theMapper.LoadRegions(irModFS, ColorFactory);
96+
theMapper.LoadRegions(irModFS, colorFactory);
9797

9898
Assert.False(theMapper.ProvinceIsInRegion(1, "nonsense"));
9999
}
@@ -107,7 +107,7 @@ public void CorrectParentLocationsReported() {
107107
var areas = new AreaCollection();
108108
areas.LoadAreas(imperatorModFS, irProvinces);
109109
var theMapper = new ImperatorRegionMapper(areas, irMapData);
110-
theMapper.LoadRegions(imperatorModFS, ColorFactory);
110+
theMapper.LoadRegions(imperatorModFS, colorFactory);
111111

112112
Assert.Equal("test_area", theMapper.GetParentAreaName(2));
113113
Assert.Equal("test_region", theMapper.GetParentRegionName(2));
@@ -124,7 +124,7 @@ public void WrongParentLocationsReturnNull() {
124124
var areas = new AreaCollection();
125125
areas.LoadAreas(imperatorModFS, irProvinces);
126126
var theMapper = new ImperatorRegionMapper(areas, irMapData);
127-
theMapper.LoadRegions(irModFS, ColorFactory);
127+
theMapper.LoadRegions(irModFS, colorFactory);
128128

129129
Assert.Null(theMapper.GetParentAreaName(5));
130130
Assert.Null(theMapper.GetParentRegionName(5));
@@ -139,7 +139,7 @@ public void LocationNameValidationWorks() {
139139
var areas = new AreaCollection();
140140
areas.LoadAreas(imperatorModFS, irProvinces);
141141
var theMapper = new ImperatorRegionMapper(areas, irMapData);
142-
theMapper.LoadRegions(imperatorModFS, ColorFactory);
142+
theMapper.LoadRegions(imperatorModFS, colorFactory);
143143

144144
Assert.True(theMapper.RegionNameIsValid("test_area"));
145145
Assert.True(theMapper.RegionNameIsValid("test_area2"));
@@ -158,7 +158,7 @@ public void ModAreasAndRegionsCanBeLoaded() {
158158
var areas = new AreaCollection();
159159
areas.LoadAreas(imperatorModFS, irProvinces);
160160
var theMapper = new ImperatorRegionMapper(areas, irMapData);
161-
theMapper.LoadRegions(imperatorModFS, ColorFactory);
161+
theMapper.LoadRegions(imperatorModFS, colorFactory);
162162

163163
Assert.False(theMapper.RegionNameIsValid("vanilla_area")); // present only in vanilla file which is overriden by mod
164164
Assert.True(theMapper.RegionNameIsValid("common_area"));

ImperatorToCK3.UnitTests/Mappers/Region/ImperatorRegionTests.cs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using commonItems;
22
using commonItems.Collections;
33
using commonItems.Colors;
4+
using commonItems.Mods;
5+
using ImperatorToCK3.CommonUtils.Map;
46
using ImperatorToCK3.Imperator.Countries;
57
using ImperatorToCK3.Imperator.Geography;
68
using ImperatorToCK3.Imperator.Provinces;
@@ -11,19 +13,20 @@
1113
namespace ImperatorToCK3.UnitTests.Mappers.Region;
1214

1315
public class ImperatorRegionTests {
14-
private readonly ProvinceCollection provinces = new();
15-
private static readonly ColorFactory ColorFactory = new();
16+
private readonly ProvinceCollection provinces = [];
17+
private static readonly ColorFactory colorFactory = new();
18+
private static readonly MapData irMapData = new(new ModFilesystem("TestFiles/RegionTests", []));
1619

1720
public ImperatorRegionTests() {
1821
provinces.LoadProvinces(new BufferedReader(
1922
"1={} 2={} 3={} 4={} 5={} 6={} 7={} 8={} 9={} 69={}")
20-
, new StateCollection(), new CountryCollection());
23+
, new StateCollection(), new CountryCollection(), irMapData);
2124
}
2225

2326
[Fact]
2427
public void BlankRegionLoadsWithNoAreas() {
2528
var reader = new BufferedReader(string.Empty);
26-
var region = new ImperatorRegion("region1", reader, new AreaCollection(), ColorFactory);
29+
var region = new ImperatorRegion("region1", reader, new AreaCollection(), colorFactory);
2730

2831
Assert.Empty(region.Areas);
2932
}
@@ -35,7 +38,7 @@ public void RegionCanBeLinkedToArea() {
3538
var areas = new IdObjectCollection<string, Area> { area };
3639

3740
var reader1 = new BufferedReader("areas = { test1 }");
38-
var region = new ImperatorRegion("region1", reader1, areas, ColorFactory);
41+
var region = new ImperatorRegion("region1", reader1, areas, colorFactory);
3942

4043
Assert.NotNull(region.Areas["test1"]);
4144
}
@@ -48,7 +51,7 @@ public void RegionCanBeLinkedToArea() {
4851
var areas = new IdObjectCollection<string, Area> { area1, area2, area3 };
4952

5053
var reader = new BufferedReader("areas = { test1 test2 test3 }");
51-
var region = new ImperatorRegion("region1", reader, areas, ColorFactory);
54+
var region = new ImperatorRegion("region1", reader, areas, colorFactory);
5255

5356
Assert.Collection(region.Areas,
5457
item => Assert.Equal("test1", item.Id),
@@ -64,7 +67,7 @@ public void LinkedRegionCanLocateProvince() {
6467
var areas = new IdObjectCollection<string, Area> { area };
6568

6669
var reader1 = new BufferedReader("{ areas={area1} }");
67-
var region = new ImperatorRegion("region1", reader1, areas, ColorFactory);
70+
var region = new ImperatorRegion("region1", reader1, areas, colorFactory);
6871

6972
Assert.True(region.ContainsProvince(6));
7073
}
@@ -76,7 +79,7 @@ public void LinkedRegionWillFailForProvinceMismatch() {
7679
var areas = new IdObjectCollection<string, Area> { area };
7780

7881
var reader1 = new BufferedReader("{ areas={area1} }");
79-
var region = new ImperatorRegion("region1", reader1, areas, ColorFactory);
82+
var region = new ImperatorRegion("region1", reader1, areas, colorFactory);
8083

8184
Assert.False(region.ContainsProvince(7));
8285
}

ImperatorToCK3/CommonUtils/Map/MapData.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ public IReadOnlySet<ulong> GetNeighborProvinceIds(ulong provinceId) {
192192
return NeighborsDict.TryGetValue(provinceId, out var neighbors) ? neighbors : [];
193193
}
194194

195-
private bool IsColorableImpassable(ulong provinceId) => ProvinceDefinitions[provinceId].IsColorableImpassable;
195+
public bool IsColorableImpassable(ulong provinceId) => ProvinceDefinitions.TryGetValue(provinceId, out var province) && province.IsColorableImpassable;
196196

197197
public bool IsImpassable(ulong provinceId) => ProvinceDefinitions.TryGetValue(provinceId, out var province) && province.IsImpassable;
198198

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,78 @@
11
using commonItems;
22
using commonItems.Collections;
3+
using ImperatorToCK3.CommonUtils.Map;
34
using ImperatorToCK3.Imperator.Countries;
45
using ImperatorToCK3.Imperator.Pops;
56
using ImperatorToCK3.Imperator.States;
7+
using System.Collections.Generic;
68
using System.Linq;
79

810
namespace ImperatorToCK3.Imperator.Provinces;
911

1012
internal sealed class ProvinceCollection : IdObjectCollection<ulong, Province> {
11-
public void LoadProvinces(BufferedReader provincesReader, StateCollection states, CountryCollection countries) {
13+
public void LoadProvinces(BufferedReader provincesReader, StateCollection states, CountryCollection countries, MapData irMapData) {
1214
var parser = new Parser();
1315
parser.RegisterRegex(CommonRegexes.Integer, (reader, provIdStr) => {
1416
var newProvince = Province.Parse(reader, ulong.Parse(provIdStr), states, countries);
1517
Add(newProvince);
1618
});
1719
parser.RegisterRegex(CommonRegexes.Catchall, ParserHelpers.IgnoreAndLogItem);
1820
parser.ParseStream(provincesReader);
21+
22+
// After all the provinces are loaded, we can determine if there are impassables to be considered owned.
23+
// This should match the impassables colored with a country color on the Imperator map.
24+
DetermineImpassableOwnership(irMapData);
1925
}
2026
public void LinkPops(PopCollection pops) {
2127
var counter = this.Sum(province => province.LinkPops(pops));
2228
Logger.Info($"{counter} pops linked to provinces.");
2329
}
30+
31+
private void DetermineImpassableOwnership(MapData irMapData) {
32+
// Store the map of province -> country to be assigned in a dict, to avoid one impassable being given an owner
33+
// skewing the calculation for the neighboring impassables.
34+
Dictionary<ulong, Country> impassableOwnership = [];
35+
36+
foreach (var province in this) {
37+
if (province.OwnerCountry is not null) {
38+
continue;
39+
}
40+
41+
if (!irMapData.IsColorableImpassable(province.Id)) {
42+
continue;
43+
}
44+
45+
Country? country = GetCountryForColorableImpassable(province.Id, irMapData);
46+
if (country is null) {
47+
continue;
48+
}
49+
50+
impassableOwnership[province.Id] = country;
51+
}
52+
53+
foreach (var (provinceId, country) in impassableOwnership) {
54+
var province = this[provinceId];
55+
province.OwnerCountry = country;
56+
country.RegisterProvince(province);
57+
}
58+
}
59+
60+
private Country? GetCountryForColorableImpassable(ulong provinceId, MapData irMapData) {
61+
var neighborProvIds = irMapData.GetNeighborProvinceIds(provinceId);
62+
int neighborsCount = neighborProvIds.Count;
63+
64+
// Group the neighboring provinces by their owner. The one with most owned neighbors may be the owner of the impassable.
65+
var ownerCandidate = neighborProvIds
66+
.Select(provId => this[provId].OwnerCountry)
67+
.Where(country => country is not null)
68+
.GroupBy(country => country)
69+
.OrderByDescending(group => group.Count())
70+
.FirstOrDefault();
71+
72+
// If any country controls at least half of the neighboring provinces, the impassable should be colored.
73+
if (ownerCandidate is not null && ownerCandidate.Count() >= (float)neighborsCount / 2) {
74+
return ownerCandidate.Key;
75+
}
76+
return null;
77+
}
2478
}

ImperatorToCK3/Imperator/World.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,7 @@ private SimpleDel LoadPlayerCountries(OrderedSet<string> playerCountriesToLog) {
487487

488488
private void LoadProvinces(BufferedReader reader) {
489489
Logger.Info("Loading provinces...");
490-
Provinces.LoadProvinces(reader, States, Countries);
490+
Provinces.LoadProvinces(reader, States, Countries, MapData);
491491
Logger.Debug($"Ignored Province tokens: {Province.IgnoredTokens}");
492492
Logger.Info($"Loaded {Provinces.Count} provinces.");
493493

@@ -638,6 +638,7 @@ private void LoadModFilesystemDependentData() {
638638
() => LoadImperatorLocalization(),
639639
() => {
640640
MapData = new MapData(ModFS);
641+
641642
Areas.LoadAreas(ModFS, Provinces);
642643
ImperatorRegionMapper = new ImperatorRegionMapper(Areas, MapData);
643644
},

0 commit comments

Comments
 (0)