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

Invention mappings for The Fallen Eagle mod and improved compatibility for its 'After the Pharaohs' update #2399

Merged
merged 15 commits into from
Jan 7, 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
18 changes: 18 additions & 0 deletions ImperatorToCK3/CK3/Characters/CharacterCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -661,156 +661,156 @@
Logger.IncrementProgress();
}

public void GenerateSuccessorsForOldCharacters(Title.LandedTitles titles, CultureCollection cultures, Date irSaveDate, Date ck3BookmarkDate, ulong randomSeed) {
Logger.Info("Generating successors for old characters...");

var oldCharacters = this
.Where(c => c.BirthDate < ck3BookmarkDate && c.DeathDate is null)
.Where(c => ck3BookmarkDate.DiffInYears(c.BirthDate) > 60)
.ToArray();

var titleHolderIds = titles.GetHolderIdsForAllTitlesExceptNobleFamilyTitles(ck3BookmarkDate);

var oldTitleHolders = oldCharacters
.Where(c => titleHolderIds.Contains(c.Id))
.ToArray();

// For characters that don't hold any titles, just set up a death date.
var randomForCharactersWithoutTitles = new Random((int)randomSeed);
foreach (var oldCharacter in oldCharacters.Except(oldTitleHolders)) {
// Roll a dice to determine how much longer the character will live.
var yearsToLive = randomForCharactersWithoutTitles.Next(0, 30);

// If the character is female and pregnant, make sure she doesn't die before the pregnancy ends.
if (oldCharacter is {Female: true, ImperatorCharacter: not null}) {
var lastPregnancy = oldCharacter.Pregnancies.OrderBy(p => p.BirthDate).LastOrDefault();
if (lastPregnancy is not null) {
oldCharacter.DeathDate = lastPregnancy.BirthDate.ChangeByYears(yearsToLive);
continue;
}
}

oldCharacter.DeathDate = irSaveDate.ChangeByYears(yearsToLive);
}

ConcurrentDictionary<string, Title[]> titlesByHolderId = new(titles
.Select(t => new {Title = t, HolderId = t.GetHolderId(ck3BookmarkDate)})
.Where(t => t.HolderId != "0")
.GroupBy(t => t.HolderId)
.ToDictionary(g => g.Key, g => g.Select(t => t.Title).ToArray()));

ConcurrentDictionary<string, string[]> cultureIdToMaleNames = new(cultures
.ToDictionary(c => c.Id, c => c.MaleNames.ToArray()));

// For title holders, generate successors and add them to title history.
Parallel.ForEach(oldTitleHolders, oldCharacter => {
// Get all titles held by the character.
var heldTitles = titlesByHolderId[oldCharacter.Id];
string? dynastyId = oldCharacter.GetDynastyId(ck3BookmarkDate);
string? dynastyHouseId = oldCharacter.GetDynastyHouseId(ck3BookmarkDate);
string? faithId = oldCharacter.GetFaithId(ck3BookmarkDate);
string? cultureId = oldCharacter.GetCultureId(ck3BookmarkDate);
string[] maleNames;
if (cultureId is not null) {
maleNames = cultureIdToMaleNames[cultureId];
} else {
Logger.Warn($"Failed to find male names for successors of {oldCharacter.Id}.");
maleNames = ["Alexander"];
}

var randomSeedForCharacter = randomSeed ^ (oldCharacter.ImperatorCharacter?.Id ?? 0);
var random = new Random((int)randomSeedForCharacter);

int successorCount = 0;
Character currentCharacter = oldCharacter;
Date currentCharacterBirthDate = currentCharacter.BirthDate;
while (ck3BookmarkDate.DiffInYears(currentCharacterBirthDate) >= 90) {
// If the character has living male children, the oldest one will be the successor.
var successorAndBirthDate = currentCharacter.Children
.Where(c => c is {Female: false, DeathDate: null})
.Select(c => new { Character = c, c.BirthDate })
.OrderBy(x => x.BirthDate)
.FirstOrDefault();

Character successor;
Date currentCharacterDeathDate;
Date successorBirthDate;
if (successorAndBirthDate is not null) {
successor = successorAndBirthDate.Character;
successorBirthDate = successorAndBirthDate.BirthDate;

// Roll a dice to determine how much longer the character will live.
// But make sure the successor is at least 16 years old when the old character dies.
var successorAgeAtBookmarkDate = ck3BookmarkDate.DiffInYears(successorBirthDate);
var yearsUntilSuccessorBecomesAnAdult = Math.Max(16 - successorAgeAtBookmarkDate, 0);

var yearsToLive = random.Next((int)Math.Ceiling(yearsUntilSuccessorBecomesAnAdult), 25);
int currentCharacterAge = random.Next(30 + yearsToLive, 80);
currentCharacterDeathDate = currentCharacterBirthDate.ChangeByYears(currentCharacterAge);
// Needs to be after the save date.
if (currentCharacterDeathDate <= irSaveDate) {
currentCharacterDeathDate = irSaveDate.ChangeByDays(1);
}
} else {
// We don't want all the generated successors on the map to have the same birth date.
var yearsUntilHeir = random.Next(1, 5);

// Make the old character live until the heir is at least 16 years old.
var successorAge = random.Next(yearsUntilHeir + 16, 30);
int currentCharacterAge = random.Next(30 + successorAge, 80);
currentCharacterDeathDate = currentCharacterBirthDate.ChangeByYears(currentCharacterAge);
if (currentCharacterDeathDate <= irSaveDate) {
currentCharacterDeathDate = irSaveDate.ChangeByDays(1);
}

// Generate a new successor.
string id = $"irtock3_{oldCharacter.Id}_successor_{successorCount}";
string firstName = maleNames[random.Next(0, maleNames.Length)];

successorBirthDate = currentCharacterDeathDate.ChangeByYears(-successorAge);
successor = new Character(id, firstName, successorBirthDate, this) {FromImperator = true};
Add(successor);
if (currentCharacter.Female) {
successor.Mother = currentCharacter;
} else {
successor.Father = currentCharacter;
}
if (cultureId is not null) {
successor.SetCultureId(cultureId, null);
}
if (faithId is not null) {
successor.SetFaithId(faithId, null);
}
if (dynastyId is not null) {
successor.SetDynastyId(dynastyId, null);
}
if (dynastyHouseId is not null) {
successor.SetDynastyHouseId(dynastyHouseId, null);
}
}

currentCharacter.DeathDate = currentCharacterDeathDate;
// On the old character death date, the successor should inherit all titles.
foreach (var heldTitle in heldTitles) {
heldTitle.SetHolder(successor, currentCharacterDeathDate);
}

// Move to the successor and repeat the process.
currentCharacter = successor;
currentCharacterBirthDate = successorBirthDate;
++successorCount;
}

// After the loop, currentCharacter should represent the successor at bookmark date.
// Set his DNA to avoid weird looking character on the bookmark screen in CK3.
currentCharacter.DNA = oldCharacter.DNA;

// Transfer gold to the living successor.
currentCharacter.Gold = oldCharacter.Gold;
oldCharacter.Gold = null;
});
}

Check notice on line 813 in ImperatorToCK3/CK3/Characters/CharacterCollection.cs

View check run for this annotation

codefactor.io / CodeFactor

ImperatorToCK3/CK3/Characters/CharacterCollection.cs#L664-L813

Complex Method
internal void ConvertImperatorCharacterDNA(DNAFactory dnaFactory) {
Logger.Info("Converting Imperator character DNA to CK3...");
foreach (var character in this) {
Expand All @@ -824,4 +824,22 @@
}
}
}

public void RemoveUndefinedTraits(TraitMapper traitMapper) {
Logger.Info("Removing undefined traits from CK3 character history...");

var definedTraits = traitMapper.ValidCK3TraitIDs.ToHashSet();

foreach (var character in this) {
if (character.FromImperator) {
continue;
}

var traitsField = character.History.Fields["traits"];
int removedCount = traitsField.RemoveAllEntries(value => !definedTraits.Contains(value.ToString()?.RemQuotes() ?? string.Empty));
if (removedCount > 0) {
Logger.Debug($"Removed {removedCount} undefined traits from character {character.Id}.");
}
}
}
}
2 changes: 1 addition & 1 deletion ImperatorToCK3/CK3/Characters/CharactersLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public void LoadCK3Characters(ModFilesystem ck3ModFS, Date bookmarkDate) {
"set_relation_ward", "set_relation_mentor",
"add_opinion", "make_concubine",
];
string[] fieldsToClear = ["friends", "best_friends", "lovers", "rivals", "nemesis", "primary_title"];
string[] fieldsToClear = ["friends", "best_friends", "lovers", "rivals", "nemesis", "primary_title", "dna"];

foreach (var character in loadedCharacters) {
// Remove post-bookmark history except for births and deaths.
Expand Down
4 changes: 2 additions & 2 deletions ImperatorToCK3/CK3/Cultures/CultureCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,11 +215,11 @@
return cultureMapper.Match(irCulture, ck3ProvinceId, irProvinceId, country.HistoricalTag);
}

public void ImportTechnology(CountryCollection countries, CultureMapper cultureMapper, ProvinceMapper provinceMapper, InventionsDB inventionsDB, LocDB irLocDB) { // TODO: add tests for this
public void ImportTechnology(CountryCollection countries, CultureMapper cultureMapper, ProvinceMapper provinceMapper, InventionsDB inventionsDB, LocDB irLocDB, OrderedDictionary<string, bool> ck3ModFlags) { // TODO: add tests for this

Check warning on line 218 in ImperatorToCK3/CK3/Cultures/CultureCollection.cs

View workflow job for this annotation

GitHub Actions / build (macos-14)

Check warning on line 218 in ImperatorToCK3/CK3/Cultures/CultureCollection.cs

View workflow job for this annotation

GitHub Actions / build (self-hosted, linux)

Check warning on line 218 in ImperatorToCK3/CK3/Cultures/CultureCollection.cs

View workflow job for this annotation

GitHub Actions / test (macos-14)

Check warning on line 218 in ImperatorToCK3/CK3/Cultures/CultureCollection.cs

View workflow job for this annotation

GitHub Actions / test_and_check_coverage

Check warning on line 218 in ImperatorToCK3/CK3/Cultures/CultureCollection.cs

View workflow job for this annotation

GitHub Actions / Upload development build (linux-x64)

Check warning on line 218 in ImperatorToCK3/CK3/Cultures/CultureCollection.cs

View workflow job for this annotation

GitHub Actions / Upload development build (win-x64)

Logger.Info("Converting Imperator inventions to CK3 innovations...");

var innovationMapper = new InnovationMapper();
innovationMapper.LoadLinksAndBonuses("configurables/inventions_to_innovations_map.txt");
innovationMapper.LoadLinksAndBonuses("configurables/inventions_to_innovations_map.liquid", ck3ModFlags);
innovationMapper.LogUnmappedInventions(inventionsDB, irLocDB);
innovationMapper.RemoveMappingsWithInvalidInnovations(InnovationIds);

Expand Down
23 changes: 20 additions & 3 deletions ImperatorToCK3/CK3/Provinces/ProvinceCollection.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using commonItems;
using commonItems.Collections;
using commonItems.Mods;
using ImperatorToCK3.CK3.Cultures;
using ImperatorToCK3.CK3.Religions;
using ImperatorToCK3.CK3.Titles;
using ImperatorToCK3.CommonUtils.Map;
using ImperatorToCK3.Exceptions;
Expand All @@ -9,8 +11,6 @@
using ImperatorToCK3.Mappers.Religion;
using Microsoft.VisualBasic.FileIO;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -72,7 +72,7 @@ private void LoadProvincesHistory(ModFilesystem ck3ModFs) {
parser.ParseGameFolder("history/provinces", ck3ModFs, "txt", recursive: true);
}

public void ImportVanillaProvinces(ModFilesystem ck3ModFs) {
public void ImportVanillaProvinces(ModFilesystem ck3ModFs, ReligionCollection religions, CultureCollection cultures) {
var existingProvinceDefinitionsCount = Count;
Logger.Info("Importing vanilla provinces...");

Expand All @@ -83,6 +83,23 @@ public void ImportVanillaProvinces(ModFilesystem ck3ModFs) {
LoadProvincesHistory(ck3ModFs);
Logger.IncrementProgress();

// Cleanup: remove invalid faith and culture entries from province history
var validFaithIds = religions.Faiths.Select(f => f.Id).ToHashSet();
var validCultureIds = cultures.Select(c => c.Id).ToHashSet();
foreach (var province in this) {
var faithField = province.History.Fields["faith"];
int removedCount = faithField.RemoveAllEntries(value => !validFaithIds.Contains(value.ToString()?.RemQuotes() ?? string.Empty));
if (removedCount > 0) {
Logger.Debug($"Removed {removedCount} invalid faith entries from province {province.Id}.");
}

var cultureField = province.History.Fields["culture"];
removedCount = cultureField.RemoveAllEntries(value => !validCultureIds.Contains(value.ToString()?.RemQuotes() ?? string.Empty));
if (removedCount > 0) {
Logger.Debug($"Removed {removedCount} invalid culture entries from province {province.Id}.");
}
}

// Now load the provinces that don't have unique entries in history/provinces.
// They instead use history/province_mapping.
foreach (var (newProvinceId, baseProvinceId) in new ProvinceMappings(ck3ModFs)) {
Expand Down
7 changes: 7 additions & 0 deletions ImperatorToCK3/CK3/Religions/Faith.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ public Faith(string id, FaithData faithData, Religion religion) {
DoctrineIds = faithData.DoctrineIds.ToOrderedSet();
holySiteIds = faithData.HolySiteIds.ToOrderedSet();
attributes = [.. faithData.Attributes];

// Fixup for issue found in TFE: add reformed_icon if faith has unreformed_faith_doctrine.
if (DoctrineIds.Contains("unreformed_faith_doctrine") && !attributes.Any(pair => pair.Key == "reformed_icon")) {
// Use the icon attribute.
var icon = attributes.FirstOrDefault(pair => pair.Key == "icon");
attributes = [.. attributes, new KeyValuePair<string, StringOfItem>("reformed_icon", icon.Value)];
}
}

private readonly OrderedSet<string> holySiteIds;
Expand Down
9 changes: 9 additions & 0 deletions ImperatorToCK3/CK3/Titles/LandedTitles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -265,81 +265,90 @@
return this.SelectMany(t => t.GetAllHolderIds()).ToImmutableHashSet();
}

public void CleanUpHistory(CharacterCollection characters, Date ck3BookmarkDate) {
Logger.Debug("Removing invalid holders from history...");

var validIds = characters.Select(c => c.Id).ToImmutableHashSet();
foreach (var title in this) {
if (!title.History.Fields.TryGetValue("holder", out var holderField)) {
continue;
}

holderField.RemoveAllEntries(
value => value.ToString() is string valStr && valStr != "0" && !validIds.Contains(valStr)
);
}

// For counties, remove holder = 0 entries that precede a holder = <char ID> entry
// that's before or at the bookmark date.
foreach (var county in Counties) {
if (!county.History.Fields.TryGetValue("holder", out var holderField)) {
continue;
}

var holderIdAtBookmark = county.GetHolderId(ck3BookmarkDate);
if (holderIdAtBookmark == "0") {
continue;
}

// If we have a holder at the bookmark date, remove all holder = 0 entries that precede it.
var entryDatesToRemove = holderField.DateToEntriesDict
.Where(pair => pair.Key < ck3BookmarkDate && pair.Value.Any(v => v.Value.ToString() == "0"))
.Select(pair => pair.Key)
.ToArray();
foreach (var date in entryDatesToRemove) {
holderField.DateToEntriesDict.Remove(date);
}
}

// Remove liege entries that are not valid (liege title is not held at the entry date).
foreach (var title in this) {
if (!title.History.Fields.TryGetValue("liege", out var liegeField)) {
continue;
}

foreach (var (date, entriesList) in liegeField.DateToEntriesDict.ToArray()) {
if (entriesList.Count == 0) {
continue;
}

var lastEntry = entriesList.Last();
var liegeTitleId = lastEntry.Value.ToString();
if (liegeTitleId is null || liegeTitleId == "0") {
continue;
}

if (!TryGetValue(liegeTitleId, out var liegeTitle)) {
liegeField.DateToEntriesDict.Remove(date);
} else if (liegeTitle.GetHolderId(date) == "0") {
// Instead of removing the liege entry, see if the liege title has a holder at a later date,
// and move the liege entry to that date.
liegeTitle.History.Fields.TryGetValue("holder", out var liegeHolderField);
Date? laterDate = liegeHolderField?.DateToEntriesDict.Keys
.Where(d => d > date && d <= ck3BookmarkDate)
.Min();

if (laterDate == null) {
liegeField.DateToEntriesDict.Remove(date);
} else {
var (setter, value) = liegeField.DateToEntriesDict[date].Last();
liegeField.DateToEntriesDict.Remove(date);
liegeField.AddEntryToHistory(laterDate, setter, value);
}
}
}
}

// Remove undated succession_laws entries; the game doesn't seem to like them.
foreach (var title in this) {
if (!title.History.Fields.TryGetValue("succession_laws", out var successionLawsField)) {
continue;
}

successionLawsField.InitialEntries.RemoveAll(entry => true);
}
}

Check notice on line 351 in ImperatorToCK3/CK3/Titles/LandedTitles.cs

View check run for this annotation

codefactor.io / CodeFactor

ImperatorToCK3/CK3/Titles/LandedTitles.cs#L268-L351

Complex Method
internal void ImportImperatorCountries(
CountryCollection imperatorCountries,
IReadOnlyCollection<Dependency> dependencies,
Expand Down Expand Up @@ -606,168 +615,168 @@
}
}

public void ImportImperatorHoldings(ProvinceCollection ck3Provinces, Imperator.Characters.CharacterCollection irCharacters, Date conversionDate) {
Logger.Info("Importing Imperator holdings...");
var counter = 0;

var highLevelTitlesThatHaveHolders = this
.Where(t => t.Rank >= TitleRank.duchy && t.GetHolderId(conversionDate) != "0")
.ToImmutableList();
var highLevelTitleCapitalBaronyIds = highLevelTitlesThatHaveHolders
.Select(t=>t.CapitalCounty?.CapitalBaronyId ?? t.CapitalBaronyId)
.ToImmutableHashSet();

// Dukes and above should be excluded from having their holdings converted.
// Otherwise, governors with holdings would own parts of other governorships.
var dukeAndAboveIds = highLevelTitlesThatHaveHolders
.Where(t => t.Rank >= TitleRank.duchy)
.Select(t => t.GetHolderId(conversionDate))
.ToImmutableHashSet();

// We exclude baronies that are capitals of duchies and above.
var eligibleBaronies = this
.Where(t => t.Rank == TitleRank.barony)
.Where(b => !highLevelTitleCapitalBaronyIds.Contains(b.Id))
.ToArray();

var countyCapitalBaronies = eligibleBaronies
.Where(b => b.DeJureLiege?.CapitalBaronyId == b.Id)
.OrderBy(b => b.Id)
.ToArray();

var nonCapitalBaronies = eligibleBaronies.Except(countyCapitalBaronies).OrderBy(b => b.Id).ToArray();


// In CK3, a county holder shouldn't own baronies in counties that are not their own.
// This dictionary tracks what counties are held by what characters.
Dictionary<string, HashSet<string>> countiesPerCharacter = []; // characterId -> countyIds

// Evaluate all capital baronies first (we want to distribute counties first, then baronies).
foreach (var barony in countyCapitalBaronies) {
var ck3Province = GetBaronyProvince(barony);
if (ck3Province is null) {
continue;
}

// Skip none holdings and temple holdings.
if (ck3Province.GetHoldingType(conversionDate) is "church_holding" or "none") {
continue;
}

var irProvince = ck3Province.PrimaryImperatorProvince; // TODO: when the holding owner of the primary I:R province is not able to hold the CK3 equivalent, also check the holding owners from secondary source provinces
var ck3Owner = GetEligibleCK3OwnerForImperatorProvince(irProvince);
if (ck3Owner is null) {
continue;
}

var realm = ck3Owner.ImperatorCharacter?.HomeCountry?.CK3Title;
var deFactoLiege = realm;
if (realm is not null) {
var deJureDuchy = barony.DeJureLiege?.DeJureLiege;
if (deJureDuchy is not null && deJureDuchy.GetHolderId(conversionDate) != "0" && deJureDuchy.GetTopRealm(conversionDate) == realm) {
deFactoLiege = deJureDuchy;
} else {
var deJureKingdom = deJureDuchy?.DeJureLiege;
if (deJureKingdom is not null && deJureKingdom.GetHolderId(conversionDate) != "0" && deJureKingdom.GetTopRealm(conversionDate) == realm) {
deFactoLiege = deJureKingdom;
}
}
}

// Barony is a county capital, so set the county holder to the holding owner.
var county = barony.DeJureLiege;
if (county is null) {
Logger.Warn($"County capital barony {barony.Id} has no de jure county!");
continue;
}
county.SetHolder(ck3Owner, conversionDate);
county.SetDeFactoLiege(deFactoLiege, conversionDate);

if (!countiesPerCharacter.TryGetValue(ck3Owner.Id, out var countyIds)) {
countyIds = [];
countiesPerCharacter[ck3Owner.Id] = countyIds;
}
countyIds.Add(county.Id);

++counter;
}

// In CK3, a baron that doesn't own counties can only hold a single barony.
// This dictionary IDs of such barons that already hold a barony.
HashSet<string> baronyHolderIds = [];

// After all possible county capital baronies are distributed, distribute the rest of the eligible baronies.
foreach (var barony in nonCapitalBaronies) {
var ck3Province = GetBaronyProvince(barony);
if (ck3Province is null) {
continue;
}

// Skip none holdings and temple holdings.
if (ck3Province.GetHoldingType(conversionDate) is "church_holding" or "none") {
continue;
}

var irProvince = ck3Province.PrimaryImperatorProvince; // TODO: when the holding owner of the primary I:R province is not able to hold the CK3 equivalent, also check the holding owners from secondary source provinces
var ck3Owner = GetEligibleCK3OwnerForImperatorProvince(irProvince);
if (ck3Owner is null) {
continue;
}
if (baronyHolderIds.Contains(ck3Owner.Id)) {
continue;
}

var county = barony.DeJureLiege;
if (county is null) {
Logger.Warn($"Barony {barony.Id} has no de jure county!");
continue;
}
// A non-capital barony cannot be held by a character that owns a county but not the county the barony is in.
if (countiesPerCharacter.TryGetValue(ck3Owner.Id, out var countyIds) && !countyIds.Contains(county.Id)) {
continue;
}

barony.SetHolder(ck3Owner, conversionDate);
// No need to set de facto liege for baronies, they are tied to counties.

baronyHolderIds.Add(ck3Owner.Id);

++counter;
}
Logger.Info($"Imported {counter} holdings from I:R.");
Logger.IncrementProgress();
return;

Province? GetBaronyProvince(Title barony) {
var ck3ProvinceId = barony.ProvinceId;
if (ck3ProvinceId is null) {
return null;
}
if (!ck3Provinces.TryGetValue(ck3ProvinceId.Value, out var ck3Province)) {
return null;
}
return ck3Province;
}

Character? GetEligibleCK3OwnerForImperatorProvince(Imperator.Provinces.Province? irProvince) {
var holdingOwnerId = irProvince?.HoldingOwnerId;
if (holdingOwnerId is null) {
return null;
}

var irOwner = irCharacters[holdingOwnerId.Value];
var ck3Owner = irOwner.CK3Character;
if (ck3Owner is null) {
return null;
}
if (dukeAndAboveIds.Contains(ck3Owner.Id)) {
return null;
}

return ck3Owner;
}
}

Check notice on line 779 in ImperatorToCK3/CK3/Titles/LandedTitles.cs

View check run for this annotation

codefactor.io / CodeFactor

ImperatorToCK3/CK3/Titles/LandedTitles.cs#L618-L779

Complex Method
public void RemoveInvalidLandlessTitles(Date ck3BookmarkDate) {
Logger.Info("Removing invalid landless titles...");
var removedGeneratedTitles = new HashSet<string>();
Expand Down Expand Up @@ -1061,145 +1070,145 @@
return newEmpire;
}

private void SplitDisconnectedEmpires(
IDictionary<string, ConcurrentHashSet<string>> kingdomAdjacenciesByLand,
IDictionary<string, ConcurrentHashSet<string>> kingdomAdjacenciesByWaterBody,
HashSet<string> removableEmpireIds,
Dictionary<string, ImmutableArray<Pillar>> kingdomToDominantHeritagesDict,
Dictionary<string, Title> heritageToEmpireDict,
CK3LocDB ck3LocDB,
Date date
) {
Logger.Debug("Splitting disconnected empires...");

// Combine kingdom adjacencies by land and water body into a single dictionary.
var kingdomAdjacencies = new Dictionary<string, HashSet<string>>();
foreach (var (kingdomId, adjacencies) in kingdomAdjacenciesByLand) {
kingdomAdjacencies[kingdomId] = [..adjacencies];
}
foreach (var (kingdomId, adjacencies) in kingdomAdjacenciesByWaterBody) {
if (!kingdomAdjacencies.TryGetValue(kingdomId, out var set)) {
set = [];
kingdomAdjacencies[kingdomId] = set;
}
set.UnionWith(adjacencies);
}

// If one separated kingdom is separated from the rest of its de jure empire, try to get the second dominant heritage in the kingdom.
// If any neighboring kingdom has that heritage as dominant one, transfer the separated kingdom to the neighboring kingdom's empire.
var disconnectedEmpiresDict = GetDictOfDisconnectedEmpires(kingdomAdjacencies, removableEmpireIds);
if (disconnectedEmpiresDict.Count == 0) {
return;
}
Logger.Debug("\tTransferring stranded kingdoms to neighboring empires...");
foreach (var (empire, kingdomGroups) in disconnectedEmpiresDict) {
var dissolvableGroups = kingdomGroups.Where(g => g.Count == 1).ToArray();
foreach (var group in dissolvableGroups) {
var kingdom = group.First();
if (!kingdomToDominantHeritagesDict.TryGetValue(kingdom.Id, out var dominantHeritages)) {
continue;
}
if (dominantHeritages.Length < 2) {
continue;
}

var adjacentEmpiresByLand = kingdomAdjacenciesByLand[kingdom.Id].Select(k => this[k].DeJureLiege)
.Where(e => e is not null)
.Select(e => e!)
.ToHashSet();

// Try to find valid neighbor by land first, to reduce the number of exclaves.
Title? validNeighbor = null;
foreach (var secondaryHeritage in dominantHeritages.Skip(1)) {
if (!heritageToEmpireDict.TryGetValue(secondaryHeritage.Id, out var heritageEmpire)) {
continue;
}
if (!adjacentEmpiresByLand.Contains(heritageEmpire)) {
continue;
}

validNeighbor = heritageEmpire;
Logger.Debug($"\t\tTransferring kingdom {kingdom.Id} from empire {empire.Id} to empire {validNeighbor.Id} neighboring by land.");
break;
}

// If no valid neighbor by land, try to find valid neighbor by water.
if (validNeighbor is null) {
var adjacentEmpiresByWaterBody = kingdomAdjacenciesByWaterBody[kingdom.Id].Select(k => this[k].DeJureLiege)
.Where(e => e is not null)
.Select(e => e!)
.ToHashSet();

foreach (var secondaryHeritage in dominantHeritages.Skip(1)) {
if (!heritageToEmpireDict.TryGetValue(secondaryHeritage.Id, out var heritageEmpire)) {
continue;
}
if (!adjacentEmpiresByWaterBody.Contains(heritageEmpire)) {
continue;
}

validNeighbor = heritageEmpire;
Logger.Debug($"\t\tTransferring kingdom {kingdom.Id} from empire {empire.Id} to empire {validNeighbor.Id} neighboring by water body.");
break;
}
}

if (validNeighbor is not null) {
kingdom.DeJureLiege = validNeighbor;
}
}
}

disconnectedEmpiresDict = GetDictOfDisconnectedEmpires(kingdomAdjacencies, removableEmpireIds);
if (disconnectedEmpiresDict.Count == 0) {
return;
}
Logger.Debug("\tCreating new empires for disconnected groups...");
foreach (var (empire, groups) in disconnectedEmpiresDict) {
// Keep the largest group as is, and create new empires based on most developed counties for the rest.
var largestGroup = groups.MaxBy(g => g.Count);
foreach (var group in groups) {
if (group == largestGroup) {
continue;
}

var mostDevelopedCounty = group
.SelectMany(k => k.GetDeJureVassalsAndBelow("c").Values)
.MaxBy(c => c.GetOwnOrInheritedDevelopmentLevel(date));
if (mostDevelopedCounty is null) {
continue;
}

string newEmpireId = $"e_IRTOCK3_from_{mostDevelopedCounty.Id}";
var newEmpire = Add(newEmpireId);
newEmpire.Color1 = mostDevelopedCounty.Color1;
newEmpire.CapitalCounty = mostDevelopedCounty;
newEmpire.HasDefiniteForm = false;

var empireNameLoc = ck3LocDB.GetOrCreateLocBlock(newEmpireId);
empireNameLoc.ModifyForEveryLanguage(
(orig, language) => $"${mostDevelopedCounty.Id}$"
);

var empireAdjLoc = ck3LocDB.GetOrCreateLocBlock(newEmpireId + "_adj");
empireAdjLoc.ModifyForEveryLanguage(
(orig, language) => $"${mostDevelopedCounty.Id}_adj$"
);

foreach (var kingdom in group) {
kingdom.DeJureLiege = newEmpire;
}

Logger.Debug($"\t\tCreated new empire {newEmpire.Id} for group {string.Join(',', group.Select(k => k.Id))}.");
}
}

disconnectedEmpiresDict = GetDictOfDisconnectedEmpires(kingdomAdjacencies, removableEmpireIds);
if (disconnectedEmpiresDict.Count > 0) {
Logger.Warn("Failed to split some disconnected empires: " + string.Join(", ", disconnectedEmpiresDict.Keys.Select(e => e.Id)));
}
}

Check notice on line 1211 in ImperatorToCK3/CK3/Titles/LandedTitles.cs

View check run for this annotation

codefactor.io / CodeFactor

ImperatorToCK3/CK3/Titles/LandedTitles.cs#L1073-L1211

Complex Method
private Dictionary<Title, List<HashSet<Title>>> GetDictOfDisconnectedEmpires(
Dictionary<string, HashSet<string>> kingdomAdjacencies,
IReadOnlySet<string> removableEmpireIds
Expand Down
13 changes: 9 additions & 4 deletions ImperatorToCK3/CK3/World.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,15 +134,17 @@ public World(Imperator.World impWorld, Configuration config, Thread? irCoaExtrac
() => CK3CoaMapper = new(ModFS)
);

OrderedDictionary<string, bool> ck3ModFlags = config.GetCK3ModFlags();

Parallel.Invoke(
() => { // depends on ck3ColorFactory and CulturalPillars
// Load CK3 cultures from CK3 mod filesystem.
Logger.Info("Loading cultural pillars...");
CulturalPillars = new(ck3ColorFactory, config.GetCK3ModFlags());
CulturalPillars = new(ck3ColorFactory, ck3ModFlags);
CulturalPillars.LoadPillars(ModFS);
Logger.Info("Loading converter cultural pillars...");
CulturalPillars.LoadConverterPillars("configurables/cultural_pillars");
Cultures = new CultureCollection(ck3ColorFactory, CulturalPillars, config.GetCK3ModFlags());
Cultures = new CultureCollection(ck3ColorFactory, CulturalPillars, ck3ModFlags);
Cultures.LoadNameLists(ModFS);
Cultures.LoadInnovationIds(ModFS);
Cultures.LoadCultures(ModFS);
Expand Down Expand Up @@ -213,7 +215,7 @@ public World(Imperator.World impWorld, Configuration config, Thread? irCoaExtrac
var religionMapper = new ReligionMapper(Religions, imperatorRegionMapper, ck3RegionMapper);

Parallel.Invoke(
() => Cultures.ImportTechnology(impWorld.Countries, cultureMapper, provinceMapper, impWorld.InventionsDB, impWorld.LocDB),
() => Cultures.ImportTechnology(impWorld.Countries, cultureMapper, provinceMapper, impWorld.InventionsDB, impWorld.LocDB, ck3ModFlags),

() => { // depends on religionMapper
// Check if all I:R religions have a base mapping.
Expand Down Expand Up @@ -243,6 +245,9 @@ public World(Imperator.World impWorld, Configuration config, Thread? irCoaExtrac
Logger.Warn($"No base mapping found for I:R culture {cultureStr}!");
}
}
},
() => { // depends on TraitMapper and CK3 characters being loaded
Characters.RemoveUndefinedTraits(traitMapper);
}
);

Expand Down Expand Up @@ -308,7 +313,7 @@ public World(Imperator.World impWorld, Configuration config, Thread? irCoaExtrac

// Now we can deal with provinces since we know to whom to assign them. We first import vanilla province data.
// Some of it will be overwritten, but not all.
Provinces.ImportVanillaProvinces(ModFS);
Provinces.ImportVanillaProvinces(ModFS, Religions, Cultures);

// Next we import Imperator provinces and translate them ontop a significant part of all imported provinces.
Provinces.ImportImperatorProvinces(impWorld, MapData, LandedTitles, cultureMapper, religionMapper, provinceMapper, CorrectedDate, config);
Expand Down
9 changes: 6 additions & 3 deletions ImperatorToCK3/CommonUtils/IHistoryField.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@ public void RemoveAllEntries() {
/// Removes all entries with values matching the predicate
/// </summary>
/// <param name="predicate"></param>
public void RemoveAllEntries(Func<object, bool> predicate) {
InitialEntries.RemoveAll(kvp => predicate(kvp.Value));
public int RemoveAllEntries(Func<object, bool> predicate) {
int removed = 0;
removed += InitialEntries.RemoveAll(kvp => predicate(kvp.Value));
foreach (var datedEntriesBlock in DateToEntriesDict) {
datedEntriesBlock.Value.RemoveAll(kvp => predicate(kvp.Value));
removed += datedEntriesBlock.Value.RemoveAll(kvp => predicate(kvp.Value));
}

return removed;
}

public void RegisterKeywords(Parser parser, Date date);
Expand Down
5 changes: 4 additions & 1 deletion ImperatorToCK3/Data_Files/configurables/culture_map.txt
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,9 @@ link = { ck3=butr ir=gyzantian ir=psyllic ir=nasamonian ir=perorsian ir=phazani
link = { ck3=numidian ir=capariensian ir=lotophagoi ir=abannaean }

## Persian and Iranian
# TFE
link = { ck3 = median ir = median }
# vanilla CK3
link = { ck3 = persian ir = persian } # https://steamcommunity.com/sharedfiles/filedetails/?id=3098496649
link = { ck3 = daylamite ir = daylamite } # https://steamcommunity.com/sharedfiles/filedetails/?id=3098496649
link = { ck3 = baloch ir = baloch } # https://steamcommunity.com/sharedfiles/filedetails/?id=3098496649
Expand All @@ -591,7 +594,7 @@ link = { ck3 = daylamite ir = hyrcanian }
link = { ck3 = parthian ir = parthian }
link = {
ck3 = kurdish
ir = median
ir = median # TODO: add a separate Median culture to the converter instead of mapping to Kurdish
ir = cardukoi # added by Invictus https://en.wikipedia.org/wiki/Carduchii, TODO: create a separate CK3 culture for them
}
link = { ck3 = afghan ir = pactyan ir = sattagydian }
Expand Down
Loading
Loading