Skip to content

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

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

Merged
merged 5 commits into from
May 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion ImperatorToCK3.UnitTests/CK3/Titles/LandedTitlesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
14 changes: 9 additions & 5 deletions ImperatorToCK3.UnitTests/Imperator/Provinces/ProvincesTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using commonItems;
using commonItems.Mods;
using ImperatorToCK3.CommonUtils.Map;
using ImperatorToCK3.Imperator.Countries;
using ImperatorToCK3.Imperator.States;
using System;
Expand All @@ -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);
}
Expand All @@ -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);
Expand All @@ -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"
Expand All @@ -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" +
Expand Down Expand Up @@ -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" +
Expand Down
3 changes: 2 additions & 1 deletion ImperatorToCK3.UnitTests/Imperator/States/StateTests.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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();
Expand All @@ -21,7 +24,8 @@ public ImperatorAreaTests() {
"1={} 2={} 3={} 4={} 5={} 6={} 7={} 8={} 9={} 69={}"
),
states,
countries
countries,
irMapData
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,23 @@ 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<Mod>());
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]
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"));
Expand All @@ -50,7 +50,7 @@ public void LoadingBrokenAreaWillThrowException() {
var areas = new AreaCollection();

var irRegionMapper = new ImperatorRegionMapper(areas, irMapData);
Assert.Throws<KeyNotFoundException>(() => irRegionMapper.LoadRegions(imperatorModFS, ColorFactory));
Assert.Throws<KeyNotFoundException>(() => irRegionMapper.LoadRegions(imperatorModFS, colorFactory));
}

[Fact]
Expand All @@ -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"));
Expand All @@ -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
Expand All @@ -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"));
}
Expand All @@ -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));
Expand All @@ -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));
Expand All @@ -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"));
Expand All @@ -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"));
Expand Down
19 changes: 11 additions & 8 deletions ImperatorToCK3.UnitTests/Mappers/Region/ImperatorRegionTests.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
}
Expand All @@ -35,7 +38,7 @@ public void RegionCanBeLinkedToArea() {
var areas = new IdObjectCollection<string, Area> { 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"]);
}
Expand All @@ -48,7 +51,7 @@ public void RegionCanBeLinkedToArea() {
var areas = new IdObjectCollection<string, Area> { 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),
Expand All @@ -64,7 +67,7 @@ public void LinkedRegionCanLocateProvince() {
var areas = new IdObjectCollection<string, Area> { 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));
}
Expand All @@ -76,7 +79,7 @@ public void LinkedRegionWillFailForProvinceMismatch() {
var areas = new IdObjectCollection<string, Area> { 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));
}
Expand Down
2 changes: 1 addition & 1 deletion ImperatorToCK3/CommonUtils/Map/MapData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ public IReadOnlySet<ulong> 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;

Expand Down
56 changes: 55 additions & 1 deletion ImperatorToCK3/Imperator/Provinces/ProvinceCollection.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,78 @@
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<ulong, Province> {
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);
Add(newProvince);
});
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<ulong, Country> 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;
}
}
3 changes: 2 additions & 1 deletion ImperatorToCK3/Imperator/World.cs
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,7 @@ private SimpleDel LoadPlayerCountries(OrderedSet<string> 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.");

Expand Down Expand Up @@ -638,6 +638,7 @@ private void LoadModFilesystemDependentData() {
() => LoadImperatorLocalization(),
() => {
MapData = new MapData(ModFS);

Areas.LoadAreas(ModFS, Provinces);
ImperatorRegionMapper = new ImperatorRegionMapper(Areas, MapData);
},
Expand Down
Loading
Loading