Skip to content

Commit a594b51

Browse files
authored
Performance optimizations (#3124)
1 parent 59fa960 commit a594b51

18 files changed

Lines changed: 531 additions & 171 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using commonItems;
2+
using commonItems.Localization;
3+
using AwesomeAssertions;
4+
using ImperatorToCK3.CK3.Characters;
5+
using ImperatorToCK3.Imperator;
6+
using ImperatorToCK3.Imperator.Armies;
7+
using System.Linq;
8+
using Xunit;
9+
10+
namespace ImperatorToCK3.UnitTests.CK3.Characters;
11+
12+
public class CharacterCollectionLegionTests {
13+
[Fact]
14+
public void LegionGroupingIndexesOnlyArmyLegionsByCountry() {
15+
var unitCollection = new UnitCollection();
16+
var locDB = new LocDB("english");
17+
var defines = new ImperatorDefines();
18+
19+
unitCollection.AddOrReplace(new Unit(1, new BufferedReader("country=1 legion={} is_army=yes"), unitCollection, locDB, defines));
20+
unitCollection.AddOrReplace(new Unit(2, new BufferedReader("country=1 is_army=yes"), unitCollection, locDB, defines));
21+
unitCollection.AddOrReplace(new Unit(3, new BufferedReader("country=2 legion={} is_army=yes"), unitCollection, locDB, defines));
22+
unitCollection.AddOrReplace(new Unit(4, new BufferedReader("country=2 legion={} is_army=no"), unitCollection, locDB, defines));
23+
24+
var legionsByCountry = CharacterCollection.GetLegionsByCountry(unitCollection);
25+
26+
legionsByCountry.Keys.Should().BeEquivalentTo(new ulong[] {1, 2});
27+
legionsByCountry[1].Select(unit => unit.Id).Should().BeEquivalentTo(new ulong[] {1});
28+
legionsByCountry[2].Select(unit => unit.Id).Should().BeEquivalentTo(new ulong[] {3});
29+
}
30+
}

ImperatorToCK3.UnitTests/CK3/Titles/LandedTitlesTests.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
using System;
3131
using System.Collections.Generic;
3232
using System.IO;
33+
using System.Linq;
3334
using Xunit;
3435
using ProvinceCollection = ImperatorToCK3.CK3.Provinces.ProvinceCollection;
3536

@@ -737,5 +738,28 @@ public void RemoveBreaksAllLinks() {
737738
// Check if I:R country - CK3 title link is broken.
738739
Assert.Null(country.CK3Title);
739740
}
741+
742+
[Fact]
743+
public void SubjectDependencyIndexKeepsFirstMatchAndSplitsCountriesCorrectly() {
744+
var dependencies = new List<Dependency> {
745+
new(overlordId: 10, subjectId: 2, startDate: new Date(1, 1, 1), subjectType: "tributary"),
746+
new(overlordId: 11, subjectId: 2, startDate: new Date(2, 1, 1), subjectType: "vassal"),
747+
new(overlordId: 12, subjectId: 4, startDate: new Date(3, 1, 1), subjectType: "client_state")
748+
};
749+
var countries = new[] {
750+
new Country(1),
751+
new Country(2),
752+
new Country(3),
753+
new Country(4)
754+
};
755+
756+
var dependenciesBySubjectId = Title.LandedTitles.GetFirstDependenciesBySubjectId(dependencies);
757+
var (independentCountries, subjects) = Title.LandedTitles.SplitRealCountriesBySubjectDependencies(countries, dependenciesBySubjectId);
758+
759+
dependenciesBySubjectId.Should().HaveCount(2);
760+
dependenciesBySubjectId[2].OverlordId.Should().Be(10UL);
761+
independentCountries.Select(c => c.Id).Should().Equal([1UL, 3UL]);
762+
subjects.Select(c => c.Id).Should().Equal([2UL, 4UL]);
763+
}
740764
}
741765

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using AwesomeAssertions;
2+
using ImperatorToCK3.CK3;
3+
using ImperatorToCK3.Imperator.Countries;
4+
using ImperatorToCK3.Imperator.Diplomacy;
5+
using System.Collections.Generic;
6+
using Xunit;
7+
8+
namespace ImperatorToCK3.UnitTests.CK3;
9+
10+
public sealed class WorldLookupTests {
11+
[Fact]
12+
public void FirstValueIndexKeepsFirstCountyLevelEntryPerCountry() {
13+
var country1 = new Country(1);
14+
var country2 = new Country(2);
15+
var firstDependency = new Dependency(10, 1, new commonItems.Date(1, 1, 1), "tributary");
16+
var secondDependency = new Dependency(11, 1, new commonItems.Date(2, 1, 1), "vassal");
17+
var countyLevelCountries = new List<KeyValuePair<Country, Dependency?>> {
18+
new(country1, firstDependency),
19+
new(country1, secondDependency),
20+
new(country2, null)
21+
};
22+
23+
var indexed = World.GetFirstValuesByKey(countyLevelCountries, entry => entry.Key.Id);
24+
25+
indexed.Should().HaveCount(2);
26+
indexed[1].Value.Should().BeSameAs(firstDependency);
27+
indexed[2].Value.Should().BeNull();
28+
}
29+
}

ImperatorToCK3.UnitTests/Imperator/Armies/UnitTests.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,23 @@ public void UnitStrengthIsCorrectlyCalculated() {
5555

5656
Assert.Equal(750, unit.MenPerUnitType["archers"]); // 250 + 500
5757
}
58+
59+
[Fact]
60+
public void MissingCohortsAreIgnoredAndTypesAreGroupedCorrectly() {
61+
var subunitsReader = new BufferedReader(@"
62+
1 = { strength = 0.5 type=""archers"" }
63+
2 = { strength = 1 type=""heavy_infantry"" }
64+
3 = { strength = 0.25 type=""archers"" }
65+
");
66+
67+
var unitCollection = new UnitCollection();
68+
unitCollection.LoadSubunits(subunitsReader);
69+
70+
var unitReader = new BufferedReader("cohort=1 cohort=2 cohort=3 cohort=999");
71+
var unit = new Unit(1, unitReader, unitCollection, new LocDB("english"), new ImperatorDefines());
72+
73+
Assert.Equal(375, unit.MenPerUnitType["archers"]);
74+
Assert.Equal(500, unit.MenPerUnitType["heavy_infantry"]);
75+
Assert.Equal(2, unit.MenPerUnitType.Count);
76+
}
5877
}

ImperatorToCK3.UnitTests/Imperator/Jobs/GovernorshipTests.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using ImperatorToCK3.Imperator.Countries;
66
using ImperatorToCK3.Imperator.Geography;
77
using ImperatorToCK3.Imperator.Jobs;
8+
using ImperatorToCK3.Imperator.Provinces;
89
using ImperatorToCK3.Mappers.Region;
910
using System;
1011
using Xunit;
@@ -63,4 +64,28 @@ public void FormatExceptionIsThrownWhenRegionIdIsMissing() {
6364
);
6465
Assert.Throws<FormatException>(() => new Governorship(reader, countryCollection, irRegionMapper));
6566
}
67+
68+
[Fact]
69+
public void ProvinceCountTracksOwnerChangesWithinRegion() {
70+
var provinces = new ProvinceCollection {
71+
new Province(1),
72+
new Province(2),
73+
new Province(3)
74+
};
75+
var areas = new AreaCollection {
76+
new Area("test_area", new BufferedReader("provinces={ 1 2 }"), provinces)
77+
};
78+
var region = new ImperatorRegion("test_region", new BufferedReader("areas={ test_area }"), areas, new ColorFactory());
79+
var country = new Country(1);
80+
var governorship = new Governorship(country, characterId: 2, startDate: new Date(1, 1, 1), region);
81+
82+
Assert.Equal(0, governorship.GetIRProvinceCount(provinces));
83+
84+
provinces[1].OwnerCountry = country;
85+
Assert.Equal(1, governorship.GetIRProvinceCount(provinces));
86+
87+
provinces[2].OwnerCountry = country;
88+
provinces[3].OwnerCountry = country;
89+
Assert.Equal(2, governorship.GetIRProvinceCount(provinces));
90+
}
6691
}

ImperatorToCK3.UnitTests/Mappers/TagTitle/TagTitleMapperTests.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
using System.Collections.Generic;
2727
using System.IO;
2828
using Xunit;
29+
using CK3Province = ImperatorToCK3.CK3.Provinces.Province;
2930

3031
namespace ImperatorToCK3.UnitTests.Mappers.TagTitle;
3132

@@ -367,6 +368,49 @@ public void GetCK3TitleRankReturnsCorrectRank() {
367368
Assert.Equal('h', mapper.GetTitleForTag(tag12, "Test Hegemony", maxTitleRank: TitleRank.hegemony, ck3LocDB)![0]);
368369
}
369370

371+
[Fact]
372+
public void BuildCountyByRegionIndexGroupsCountiesByRegion() {
373+
// Set up titles: two counties in region_a, one in region_b.
374+
var titles = new Title.LandedTitles();
375+
titles.LoadTitles(new BufferedReader(
376+
"c_county1 = { b_barony1 = { province = 101 } }\n" +
377+
"c_county2 = { b_barony2 = { province = 102 } }\n" +
378+
"c_county_b = { b_barony3 = { province = 201 } }"),
379+
ColorFactory);
380+
381+
// Set up CK3 provinces mapped to IR provinces.
382+
var irProv11 = new ImperatorToCK3.Imperator.Provinces.Province(11);
383+
var irProv12 = new ImperatorToCK3.Imperator.Provinces.Province(12);
384+
var irProv21 = new ImperatorToCK3.Imperator.Provinces.Province(21);
385+
var ck3Provinces = new ProvinceCollection();
386+
var ck3Prov101 = new CK3Province(101) { PrimaryImperatorProvince = irProv11 };
387+
var ck3Prov102 = new CK3Province(102) { PrimaryImperatorProvince = irProv12 };
388+
var ck3Prov201 = new CK3Province(201) { PrimaryImperatorProvince = irProv21 };
389+
ck3Provinces.Add(ck3Prov101);
390+
ck3Provinces.Add(ck3Prov102);
391+
ck3Provinces.Add(ck3Prov201);
392+
393+
// Set up IR region mapper: region_a has IR provinces 11 and 12; region_b has 21.
394+
var irProvinces = new ImperatorToCK3.Imperator.Provinces.ProvinceCollection();
395+
var testAreas = new AreaCollection();
396+
testAreas.Add(new Area("area_a", new BufferedReader("provinces = { 11 12 }"), irProvinces));
397+
testAreas.Add(new Area("area_b", new BufferedReader("provinces = { 21 }"), irProvinces));
398+
var testRegionMapper = new ImperatorRegionMapper(testAreas, irMapData);
399+
testRegionMapper.Regions.Add(new ImperatorRegion("region_a", new BufferedReader("areas = { area_a }"), testAreas, ColorFactory));
400+
testRegionMapper.Regions.Add(new ImperatorRegion("region_b", new BufferedReader("areas = { area_b }"), testAreas, ColorFactory));
401+
402+
var index = TagTitleMapper.BuildCountyByRegionIndex(titles, ck3Provinces, testRegionMapper);
403+
404+
Assert.Equal(2, index.Count);
405+
Assert.True(index.ContainsKey("region_a"));
406+
Assert.True(index.ContainsKey("region_b"));
407+
Assert.Equal(2, index["region_a"].Count); // c_county1 and c_county2
408+
Assert.Single(index["region_b"]);
409+
Assert.Contains(index["region_a"], t => t.Id == "c_county1");
410+
Assert.Contains(index["region_a"], t => t.Id == "c_county2");
411+
Assert.Equal("c_county_b", index["region_b"][0].Id);
412+
}
413+
370414
[Fact]
371415
public void GetTitleForTagPreventsLocKeyHashConflict() {
372416
// k_IRTOCK3_ATV_adj and building_nishapur_mines_02 have a conflicting Murmur3A hash.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using ImperatorToCK3.Outputter;
2+
using ImperatorToCK3.UnitTests.TestHelpers;
3+
using Xunit;
4+
5+
namespace ImperatorToCK3.UnitTests.Outputter;
6+
7+
public class LocalizationOutputterTests {
8+
[Fact]
9+
public void FallbackLocIsGeneratedOnlyForSecondaryLanguagesMissingPrimaryKeyLoc() {
10+
var ck3LocDB = new TestCK3LocDB();
11+
ck3LocDB.AddLocForLanguage("key1", "english", "English loc 1");
12+
ck3LocDB.AddLocForLanguage("key1", "french", "French loc 1");
13+
ck3LocDB.AddLocForLanguage("key2", "english", "English loc 2");
14+
ck3LocDB.AddLocForLanguage("key3", "german", "German loc 3");
15+
16+
var fallbackLocByLanguage = LocalizationOutputter.GetFallbackLocLinesByLanguage(ck3LocDB);
17+
18+
Assert.DoesNotContain(" key1: \"English loc 1\"", fallbackLocByLanguage["french"]);
19+
Assert.Contains(" key1: \"English loc 1\"", fallbackLocByLanguage["german"]);
20+
Assert.Contains(" key2: \"English loc 2\"", fallbackLocByLanguage["french"]);
21+
Assert.Contains(" key2: \"English loc 2\"", fallbackLocByLanguage["german"]);
22+
Assert.DoesNotContain(" key3: \"\"", fallbackLocByLanguage["french"]);
23+
Assert.DoesNotContain(" key3: \"\"", fallbackLocByLanguage["german"]);
24+
}
25+
}

ImperatorToCK3/CK3/Characters/Character.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -769,7 +769,7 @@ public bool LinkJailor(Date date) {
769769
}
770770

771771
internal void ImportUnitsAsMenAtArms(
772-
Unit[] countryUnits,
772+
IEnumerable<Unit> countryUnits,
773773
Date date,
774774
UnitTypeMapper unitTypeMapper,
775775
IdObjectCollection<string, MenAtArmsType> menAtArmsTypes,
@@ -806,7 +806,7 @@ CK3LocDB ck3LocDB
806806
History.AddFieldValue(date, "effects", "effect", new StringOfItem(sb.ToString()));
807807
}
808808
internal void ImportUnitsAsSpecialTroops(
809-
Unit[] countryUnits,
809+
IEnumerable<Unit> countryUnits,
810810
Imperator.Characters.CharacterCollection irCharacters,
811811
CountryCollection irCountries,
812812
Date date,

ImperatorToCK3/CK3/Characters/CharacterCollection.cs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,7 @@ Configuration config
753753
Logger.Info("Importing Imperator armies...");
754754

755755
var ck3CountriesFromImperator = titles.GetCountriesImportedFromImperator();
756+
var legionsByCountry = GetLegionsByCountry(imperatorUnits);
756757
foreach (var ck3Country in ck3CountriesFromImperator) {
757758
var rulerId = ck3Country.GetHolderId(date);
758759
if (rulerId == "0") {
@@ -761,8 +762,7 @@ Configuration config
761762
}
762763

763764
var imperatorCountry = ck3Country.ImperatorCountry!;
764-
var countryLegions = imperatorUnits.Where(u => u.CountryId == imperatorCountry.Id && u.IsArmy && u.IsLegion).ToArray();
765-
if (countryLegions.Length == 0) {
765+
if (!legionsByCountry.TryGetValue(imperatorCountry.Id, out var countryLegions)) {
766766
continue;
767767
}
768768

@@ -778,6 +778,23 @@ Configuration config
778778
Logger.IncrementProgress();
779779
}
780780

781+
internal static Dictionary<ulong, List<Unit>> GetLegionsByCountry(UnitCollection imperatorUnits) {
782+
var legionsByCountry = new Dictionary<ulong, List<Unit>>();
783+
foreach (var unit in imperatorUnits) {
784+
if (!unit.IsArmy || !unit.IsLegion) {
785+
continue;
786+
}
787+
788+
if (!legionsByCountry.TryGetValue(unit.CountryId, out var countryLegions)) {
789+
countryLegions = [];
790+
legionsByCountry[unit.CountryId] = countryLegions;
791+
}
792+
countryLegions.Add(unit);
793+
}
794+
795+
return legionsByCountry;
796+
}
797+
781798
internal void GenerateSuccessorsForOldCharacters(Title.LandedTitles titles, CultureCollection cultures, Date irSaveDate, Date ck3BookmarkDate, ulong randomSeed) {
782799
Logger.Info("Generating successors for old characters...");
783800

ImperatorToCK3/CK3/Religions/ReligionCollection.cs

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -535,30 +535,35 @@ Date date
535535

536536
private List<Title> GetDynamicHolySiteBaroniesForFaith(Faith faith, Dictionary<string, HashSet<Province>> provincesByFaith) {
537537
// Collect all Imperator territories that are mapped to this faith.
538-
HashSet<Province> faithTerritories;
539-
if (provincesByFaith.TryGetValue(faith.Id, out var set)) {
540-
faithTerritories = set;
541-
} else {
542-
faithTerritories = [];
538+
if (!provincesByFaith.TryGetValue(faith.Id, out var faithTerritories)) {
539+
return [];
543540
}
544541

545-
// Split the territories into 2 sets: territories that have a holy site and territories that do not.
546-
// Order both sets in descending order by population.
547-
var provincesWithHolySite = faithTerritories
548-
.Where(p => p.ImperatorProvinces.Any(irProv => irProv.IsHolySite))
549-
.OrderByDescending(p => p.PrimaryImperatorProvince!.GetPopCount())
550-
.ToArray();
551-
var provincesWithoutHolySite = faithTerritories.Except(provincesWithHolySite)
552-
.OrderByDescending(p => p.PrimaryImperatorProvince!.GetPopCount())
553-
.ToArray();
542+
// Single-pass partition: split territories into holy-site and non-holy-site lists.
543+
List<Province> withHolySite = [];
544+
Province? bestWithout = null;
545+
int bestWithoutPop = -1;
546+
foreach (var p in faithTerritories) {
547+
if (p.ImperatorProvinces.Any(irProv => irProv.IsHolySite)) {
548+
withHolySite.Add(p);
549+
} else {
550+
var pop = p.PrimaryImperatorProvince!.GetPopCount();
551+
if (pop > bestWithoutPop) {
552+
bestWithoutPop = pop;
553+
bestWithout = p;
554+
}
555+
}
556+
}
554557

555-
// Take the top 4 territories with a holy site.
556-
var selectedDynamicSites = provincesWithHolySite.Take(4).ToList();
558+
// Take the top 4 territories with a holy site (sort only the holy-site subset).
559+
var selectedDynamicSites = withHolySite
560+
.OrderByDescending(p => p.PrimaryImperatorProvince!.GetPopCount())
561+
.Take(4)
562+
.ToList();
557563

558564
// Take the most populated territory without a holy site.
559-
var mostPopulatedProvinceWithoutHolySite = provincesWithoutHolySite.FirstOrDefault(defaultValue: null);
560-
if (mostPopulatedProvinceWithoutHolySite is not null) {
561-
selectedDynamicSites.Add(mostPopulatedProvinceWithoutHolySite);
565+
if (bestWithout is not null) {
566+
selectedDynamicSites.Add(bestWithout);
562567
}
563568

564569
return selectedDynamicSites

0 commit comments

Comments
 (0)