Skip to content

Various micro-optimizations #2616

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 2 commits into from
May 25, 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
15 changes: 11 additions & 4 deletions ImperatorToCK3/CK3/CK3LocDB.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
using Murmur;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using ZLinq;

namespace ImperatorToCK3.CK3;

Expand All @@ -23,7 +23,7 @@
modFSLocDB.ScrapeLocalizations(ck3ModFS);
ImportLocFromLocDB(modFSLocDB);

// Read loc from ImperatorToCK3 congifurables.
// Read loc from ImperatorToCK3 configurables.
// It will only be outputted for keys localized in neither ModFSLocDB nor ConverterGeneratedLocDB.
LoadOptionalLoc(activeModFlags);
}
Expand Down Expand Up @@ -54,7 +54,8 @@
if (!Directory.Exists(modLocDir)) {
continue;
}
optionalLocFilePaths = optionalLocFilePaths.Concat(Directory.GetFiles(modLocDir, "*.yml", SearchOption.AllDirectories)).ToArray();
optionalLocFilePaths = optionalLocFilePaths.AsValueEnumerable()
.Concat(Directory.GetFiles(modLocDir, "*.yml", SearchOption.AllDirectories)).ToArray();
}

var optionalConverterLocDB = new LocDB(ConverterGlobals.PrimaryLanguage, ConverterGlobals.SecondaryLanguages);
Expand Down Expand Up @@ -99,7 +100,7 @@
}
}

// TODO: add unit test for combining loc from all the sources into one locblock

Check warning on line 103 in ImperatorToCK3/CK3/CK3LocDB.cs

View workflow job for this annotation

GitHub Actions / Upload development build (linux-x64)

TODO add unit test for combining loc from all the sources into one locblock (https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0026.md)

Check warning on line 103 in ImperatorToCK3/CK3/CK3LocDB.cs

View workflow job for this annotation

GitHub Actions / build (macos-14)

TODO add unit test for combining loc from all the sources into one locblock (https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0026.md)

Check warning on line 103 in ImperatorToCK3/CK3/CK3LocDB.cs

View workflow job for this annotation

GitHub Actions / test_and_check_coverage

TODO add unit test for combining loc from all the sources into one locblock (https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0026.md)

Check warning on line 103 in ImperatorToCK3/CK3/CK3LocDB.cs

View workflow job for this annotation

GitHub Actions / test (macos-14)

TODO add unit test for combining loc from all the sources into one locblock (https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0026.md)

Check warning on line 103 in ImperatorToCK3/CK3/CK3LocDB.cs

View workflow job for this annotation

GitHub Actions / build (self-hosted, linux)

TODO add unit test for combining loc from all the sources into one locblock (https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0026.md)

Check warning on line 103 in ImperatorToCK3/CK3/CK3LocDB.cs

View workflow job for this annotation

GitHub Actions / Upload development build (win-x64)

TODO add unit test for combining loc from all the sources into one locblock (https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0026.md)


public CK3LocBlock? GetLocBlockForKey(string key) {
Expand Down Expand Up @@ -161,7 +162,13 @@
private static string GetHashStrForKey(string key) {
var keyBytes = System.Text.Encoding.UTF8.GetBytes(key);
var hash = murmur3A.ComputeHash(keyBytes);
return string.Concat(hash.Select(b => b.ToString("X2")));

var sb = new System.Text.StringBuilder(hash.Length * 2);
foreach (byte t in hash) {
sb.Append(t.ToString("X2"));
}

return sb.ToString();
}

private readonly Dictionary<string, string> hashToKeyDict = []; // stores MurmurHash3A hash to key mapping
Expand Down
36 changes: 18 additions & 18 deletions ImperatorToCK3/CK3/Characters/Character.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,10 @@
using ImperatorToCK3.Mappers.UnitType;
using Open.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using ZLinq;

namespace ImperatorToCK3.CK3.Characters;
namespace ImperatorToCK3.CK3.Characters;

internal sealed class Character : IIdentifiable<string> {
public string Id { get; }
Expand All @@ -37,7 +36,7 @@
return false;
}

var value = entries.LastOrDefault().Value;
var value = entries.AsValueEnumerable().LastOrDefault().Value;
if (value is string str) {
return str == "yes";
}
Expand Down Expand Up @@ -66,11 +65,12 @@
public string? GetNickname(Date date) {
return History.GetFieldValue("give_nickname", date)?.ToString();
}
public IEnumerable<string> BaseTraits => History.Fields["traits"].InitialEntries

public List<string> BaseTraits => History.Fields["traits"].InitialEntries.AsValueEnumerable()
.Where(kvp => kvp.Key == "trait")
.Select(kvp => kvp.Value)
.Cast<string>();
.Cast<string>()
.ToList();

public void AddBaseTrait(string traitId) {
History.Fields["traits"].InitialEntries.Add(new KeyValuePair<string, object>("trait", traitId));
Expand All @@ -94,7 +94,7 @@
}

public Date BirthDate {
get => History.Fields["birth"].DateToEntriesDict.First().Key;
get => History.Fields["birth"].DateToEntriesDict.AsValueEnumerable().First().Key;
set {
var field = History.Fields["birth"];
field.RemoveAllEntries();
Expand All @@ -105,7 +105,7 @@
public Date? DeathDate {
get {
var entriesDict = History.Fields["death"].DateToEntriesDict;
return entriesDict.Count == 0 ? null : entriesDict.First().Key;
return entriesDict.Count == 0 ? null : entriesDict.AsValueEnumerable().First().Key;
}
set {
var field = History.Fields["death"];
Expand All @@ -121,7 +121,7 @@
if (entriesDict.Count == 0) {
return null;
}
var deathObj = entriesDict.First().Value[^1].Value;
var deathObj = entriesDict.AsValueEnumerable().First().Value[^1].Value;
if (deathObj is not StringOfItem deathStrOfItem || !deathStrOfItem.IsArrayOrObject()) {
return null;
}
Expand All @@ -142,7 +142,7 @@
}

// Modify the last entry in the history to include the death reason.
var entriesList = entriesDict.First().Value;
var entriesList = entriesDict.AsValueEnumerable().First().Value;
var lastEntry = entriesList[^1];
// No reason provided.
var deathStr = value is null ? "yes" : $"{{ death_reason = {value} }}";
Expand Down Expand Up @@ -226,7 +226,7 @@

public void InitSpousesCache() {
var spousesHistoryField = History.Fields["spouses"];
foreach (var spouseId in spousesHistoryField.InitialEntries.Select(kvp => kvp.Value.ToString())) {
foreach (var spouseId in spousesHistoryField.InitialEntries.AsValueEnumerable().Select(kvp => kvp.Value.ToString())) {
if (spouseId is null) {
continue;
}
Expand All @@ -251,7 +251,7 @@

public void InitConcubinesCache() {
var concubinesHistoryField = History.Fields["concubines"];
foreach (var concubineId in concubinesHistoryField.InitialEntries.Select(kvp => kvp.Value.ToString())) {
foreach (var concubineId in concubinesHistoryField.InitialEntries.AsValueEnumerable().Select(kvp => kvp.Value.ToString())) {
if (concubineId is null) {
continue;
}
Expand Down Expand Up @@ -287,246 +287,246 @@
SetName(name, null);
BirthDate = birthDate;
}
public Character(
RulerTerm.PreImperatorRulerInfo preImperatorRuler,
Date rulerTermStart,
Country imperatorCountry,
CharacterCollection characters,
LocDB irLocDB,
CK3LocDB ck3LocDB,
ReligionMapper religionMapper,
CultureMapper cultureMapper,
NicknameMapper nicknameMapper,
ProvinceMapper provinceMapper,
Configuration config
) {
this.characters = characters;

Id = $"imperatorRegnal{imperatorCountry.Tag}{preImperatorRuler.Name}{rulerTermStart.ToString()[1..]}BC".Replace('.', '_');
FromImperator = true;
var name = preImperatorRuler.Name ?? Id;
SetName(name, null);
if (!string.IsNullOrEmpty(name)) {
var impNameLoc = irLocDB.GetLocBlockForKey(name);
CK3LocBlock ck3NameLoc = ck3LocDB.GetOrCreateLocBlock(name);
if (impNameLoc is not null) {
ck3NameLoc.CopyFrom(impNameLoc);
} else { // fallback: use unlocalized name as displayed name
ck3NameLoc[ConverterGlobals.PrimaryLanguage] = name;
}
}

BirthDate = preImperatorRuler.BirthDate!;
DeathDate = preImperatorRuler.DeathDate!;

// determine culture and religion
ulong ck3Province = 0;
ulong irProvince = 0;
var srcReligion = preImperatorRuler.Religion ?? imperatorCountry.Religion;
var srcCulture = preImperatorRuler.Culture ?? imperatorCountry.PrimaryCulture;
if (imperatorCountry.CapitalProvinceId is not null) {
irProvince = imperatorCountry.CapitalProvinceId.Value;
var ck3Provinces = provinceMapper.GetCK3ProvinceNumbers(irProvince);
if (ck3Provinces.Count > 0) {
ck3Province = ck3Provinces[0];
}
}

if (srcCulture is not null) {
var cultureMatch = cultureMapper.Match(srcCulture, ck3Province, irProvince, imperatorCountry.HistoricalTag);
if (cultureMatch is not null) {
SetCultureId(cultureMatch, null);
}
}

if (srcReligion is not null) {
var faithMatch = religionMapper.Match(
srcReligion,
GetCultureId(config.CK3BookmarkDate),
ck3Province,
irProvince,
imperatorCountry.HistoricalTag,
config
);
if (faithMatch is not null) {
SetFaithId(faithMatch, null);
}
}

var nickname = nicknameMapper.GetCK3NicknameForImperatorNickname(preImperatorRuler.Nickname);
if (nickname is not null) {
SetNickname(nickname, DeathDate);
}
}

Check notice on line 361 in ImperatorToCK3/CK3/Characters/Character.cs

View check run for this annotation

codefactor.io / CodeFactor

ImperatorToCK3/CK3/Characters/Character.cs#L290-L361

Complex Method
internal Character(
Imperator.Characters.Character impCharacter,
CharacterCollection characters,
ReligionMapper religionMapper,
CultureMapper cultureMapper,
TraitMapper traitMapper,
NicknameMapper nicknameMapper,
LocDB irLocDB,
CK3LocDB ck3LocDB,
MapData irMapData,
ProvinceMapper provinceMapper, // used to determine ck3 province for religion mapper
DeathReasonMapper deathReasonMapper,
DNAFactory dnaFactory,
Date dateOnConversion,
Configuration config,
ISet<string> unlocalizedImperatorNames
) {
this.characters = characters;

ImperatorCharacter = impCharacter;
ImperatorCharacter.CK3Character = this;
Id = "imperator" + ImperatorCharacter.Id;
FromImperator = true;

if (!string.IsNullOrEmpty(ImperatorCharacter.CustomName)) {
var loc = ImperatorCharacter.CustomName;
var locKey = CommonFunctions.NormalizeUTF8Path(loc.FoldToASCII().Replace(' ', '_'));
var name = $"IRTOCK3_CUSTOM_NAME_{locKey}";
SetName(name, null);

var ck3NameLocBlock = ck3LocDB.GetOrCreateLocBlock(name);
foreach (var language in ConverterGlobals.SupportedLanguages) {
ck3NameLocBlock[language] = loc;
}
} else {
var nameLoc = ImperatorCharacter.Name;
var name = nameLoc.Replace(' ', '_');
SetName(name, null);
if (!string.IsNullOrEmpty(name)) {
var ck3NameLocBlock = ck3LocDB.GetOrCreateLocBlock(name);
var matchedLocBlock = irLocDB.GetLocBlockForKey(name);
if (matchedLocBlock is not null) {
ck3NameLocBlock.CopyFrom(matchedLocBlock);
} else { // fallback: use unlocalized name as displayed name
unlocalizedImperatorNames.Add(name);
ck3NameLocBlock[ConverterGlobals.PrimaryLanguage] = nameLoc;
}
}
}

Female = ImperatorCharacter.Female;

// Determine valid (not dropped in province mappings) "source I:R province" and "source CK3 province"
// to be used by religion mapper. Don't give up without a fight.
ulong? irProvinceId = ImperatorCharacter.GetSourceLandProvince(irMapData);

var irProvIdForProvMapper = irProvinceId;
if (IsImperatorProvIdInvalidForCharacterSource(irProvIdForProvMapper, provinceMapper) && ImperatorCharacter.Father is not null) {
irProvIdForProvMapper = ImperatorCharacter.Father.ProvinceId;
}
if (IsImperatorProvIdInvalidForCharacterSource(irProvIdForProvMapper, provinceMapper) && ImperatorCharacter.Mother is not null) {
irProvIdForProvMapper = ImperatorCharacter.Mother.ProvinceId;
}
if (IsImperatorProvIdInvalidForCharacterSource(irProvIdForProvMapper, provinceMapper) && ImperatorCharacter.Spouses.Count > 0) {
var firstSpouse = ImperatorCharacter.Spouses.First().Value;
var firstSpouse = ImperatorCharacter.Spouses.AsValueEnumerable().First().Value;
irProvIdForProvMapper = firstSpouse.ProvinceId;
}

var ck3ProvinceNumbers = irProvIdForProvMapper.HasValue ? provinceMapper.GetCK3ProvinceNumbers(irProvIdForProvMapper.Value) : [];
ulong? ck3ProvinceId = ck3ProvinceNumbers.Count > 0 ? ck3ProvinceNumbers[0] : null;

var cultureMatch = cultureMapper.Match(
ImperatorCharacter.Culture,
ck3ProvinceId,
irProvinceId,
ImperatorCharacter.Country?.HistoricalTag
);
if (cultureMatch is null) {
Logger.Warn($"Could not determine CK3 culture for Imperator character {ImperatorCharacter.Id}" +
$" with culture {ImperatorCharacter.Culture}!");
} else {
SetCultureId(cultureMatch, null);
}

var faithMatch = religionMapper.Match(
ImperatorCharacter.Religion,
GetCultureId(dateOnConversion),
ck3ProvinceId,
irProvinceId,
ImperatorCharacter.HomeCountry?.HistoricalTag,
config
);
if (faithMatch is not null) {
SetFaithId(faithMatch, null);
}

// Determine character attributes.
History.AddFieldValue(null, "diplomacy", "diplomacy", ImperatorCharacter.Attributes.Charisma);
History.AddFieldValue(null, "martial", "martial", ImperatorCharacter.Attributes.Martial);
History.AddFieldValue(null, "stewardship", "stewardship", ImperatorCharacter.Attributes.Finesse);
var intrigue = (ImperatorCharacter.Attributes.Finesse + ImperatorCharacter.Attributes.Charisma) / 2;
History.AddFieldValue(null, "intrigue", "intrigue", intrigue);
History.AddFieldValue(null, "learning", "learning", ImperatorCharacter.Attributes.Zeal);

if (impCharacter.Fertility.HasValue) {
History.AddFieldValue(null, "fertility", "fertility", impCharacter.Fertility.Value);
}

if (impCharacter.Health is not null) {
// In I:R, health is a value between 0 and 100, with 100 being the best.
// In CK3, 0 means near death, ≥ 7 means excellent health.
// https://imperator.paradoxwikis.com/Characters#Secondary
// https://ck3.paradoxwikis.com/Attributes#Health
var ck3Health = impCharacter.Health.Value / 10;
History.AddFieldValue(null, "health", "health", ck3Health);
}

foreach (var traitId in traitMapper.GetCK3TraitsForImperatorTraits(ImperatorCharacter.Traits)) {
AddBaseTrait(traitId);
}

BirthDate = ImperatorCharacter.BirthDate;
DeathDate = ImperatorCharacter.DeathDate;
var impDeathReason = ImperatorCharacter.DeathReason;
if (impDeathReason is not null) {
DeathReason = deathReasonMapper.GetCK3ReasonForImperatorReason(impDeathReason);
}

var nicknameMatch = nicknameMapper.GetCK3NicknameForImperatorNickname(ImperatorCharacter.Nickname);
if (nicknameMatch is not null) {
var nicknameDate = ImperatorCharacter.DeathDate ?? dateOnConversion;
SetNickname(nicknameMatch, nicknameDate);
}

if (ImperatorCharacter.Wealth != 0) {
Gold = ImperatorCharacter.Wealth * config.ImperatorCurrencyRate;
}

// If character is imprisoned, set jailor.
SetJailor();
SetEmployerFromImperator();

void SetJailor() {
if (ImperatorCharacter.PrisonerHome is null) {
return;
}

var prisonCountry = ImperatorCharacter.Country;
if (prisonCountry is null) {
Logger.Warn($"Imperator character {ImperatorCharacter.Id} is imprisoned but has no country!");
} else if (prisonCountry.CK3Title is null) {
Logger.Debug($"Imperator character {ImperatorCharacter.Id}'s prison country does not exist in CK3!");
} else {
jailorId = prisonCountry.CK3Title.GetHolderId(dateOnConversion);
}
}

void SetEmployerFromImperator() {
var prisonerHome = ImperatorCharacter.PrisonerHome;
var homeCountry = ImperatorCharacter.HomeCountry;
if (prisonerHome?.CK3Title is not null) { // is imprisoned
SetEmployerId(prisonerHome.CK3Title.GetHolderId(dateOnConversion), null);
} else if (homeCountry?.CK3Title is not null) {
SetEmployerId(homeCountry.CK3Title.GetHolderId(dateOnConversion), null);
}
}
}

Check notice on line 529 in ImperatorToCK3/CK3/Characters/Character.cs

View check run for this annotation

codefactor.io / CodeFactor

ImperatorToCK3/CK3/Characters/Character.cs#L362-L529

Complex Method
private static bool IsImperatorProvIdInvalidForCharacterSource(ulong? impProvForProvinceMapper, ProvinceMapper provinceMapper) {
return !impProvForProvinceMapper.HasValue || provinceMapper.GetCK3ProvinceNumbers(impProvForProvinceMapper.Value).Count == 0;
}
Expand Down Expand Up @@ -579,11 +579,11 @@

public void RemoveAllChildren() {
if (Female) {
foreach (var child in childrenCache.Where(c => c.MotherId == Id)) {
foreach (var child in childrenCache.AsValueEnumerable().Where(c => c.MotherId == Id)) {
child.Mother = null;
}
} else {
foreach (var child in childrenCache.Where(c => c.FatherId == Id)) {
foreach (var child in childrenCache.AsValueEnumerable().Where(c => c.FatherId == Id)) {
child.Father = null;
}
}
Expand Down Expand Up @@ -690,7 +690,7 @@
}
}

public IReadOnlyCollection<Character> Children => characters
public IReadOnlyCollection<Character> Children => characters.AsValueEnumerable()
.Where(c => c.FatherId == Id || c.MotherId == Id)
.ToImmutableList();

Expand Down Expand Up @@ -802,7 +802,7 @@
sb.AppendLine($"\t\t\tmen_at_arms={{type={type} men={men}}}");
}

var ck3Location = provinceMapper.GetCK3ProvinceNumbers(unit.Location)
var ck3Location = provinceMapper.GetCK3ProvinceNumbers(unit.Location).AsValueEnumerable()
.Cast<ulong?>()
.FirstOrDefault(defaultValue: null);
if (ck3Location is not null) {
Expand Down
19 changes: 10 additions & 9 deletions ImperatorToCK3/CK3/Characters/CharacterCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using Open.Collections;
using System;
using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
Expand Down Expand Up @@ -266,8 +267,8 @@
return conversionDate;
}
Date? GetBirthDateOfFirstCommonChild(Imperator.Characters.Character father, Imperator.Characters.Character mother) {
var childrenOfFather = father.Children.Values.ToHashSet();
var childrenOfMother = mother.Children.Values.ToHashSet();
var childrenOfFather = father.Children.Values.ToFrozenSet();
var childrenOfMother = mother.Children.Values.ToFrozenSet();
var commonChildren = childrenOfFather.Intersect(childrenOfMother).OrderBy(child => child.BirthDate).ToArray();

Date? firstChildBirthDate = commonChildren.Length > 0 ? commonChildren.FirstOrDefault()?.BirthDate : null;
Expand Down Expand Up @@ -382,54 +383,54 @@
Logger.IncrementProgress();
}

private void SetCharacterCastes(CultureCollection cultures, Date ck3BookmarkDate) {
var casteSystemCultureIds = cultures
.Where(c => c.TraditionIds.Contains("tradition_caste_system"))
.Select(c => c.Id)
.ToHashSet();
.ToFrozenSet();
var learningEducationTraits = new[]{"education_learning_1", "education_learning_2", "education_learning_3", "education_learning_4"};

foreach (var character in this.OrderBy(c => c.BirthDate)) {
if (character.ImperatorCharacter is null) {
continue;
}

var cultureId = character.GetCultureId(ck3BookmarkDate);
if (cultureId is null || !casteSystemCultureIds.Contains(cultureId)) {
continue;
}

// The caste is hereditary.
var father = character.Father;
if (father is not null) {
var foundTrait = GetCasteTraitFromParent(father);
if (foundTrait is not null) {
character.AddBaseTrait(foundTrait);
continue;
}
}
var mother = character.Mother;
if (mother is not null) {
var foundTrait = GetCasteTraitFromParent(mother);
if (foundTrait is not null) {
character.AddBaseTrait(foundTrait);
continue;
}
}

// Try to set caste based on character's traits.
var traitIds = character.BaseTraits.ToHashSet();
var traitIds = character.BaseTraits.ToFrozenSet();
character.AddBaseTrait(traitIds.Intersect(learningEducationTraits).Any() ? "brahmin" : "kshatriya");
}
return;

static string? GetCasteTraitFromParent(Character parentCharacter) {
var casteTraits = new[]{"brahmin", "kshatriya", "vaishya", "shudra"};
var parentTraitIds = parentCharacter.BaseTraits.ToHashSet();
var parentTraitIds = parentCharacter.BaseTraits.ToFrozenSet();
return casteTraits.Intersect(parentTraitIds).FirstOrDefault();
}
}

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

View check run for this annotation

codefactor.io / CodeFactor

ImperatorToCK3/CK3/Characters/CharacterCollection.cs#L386-L433

Complex Method
public void LoadCharacterIDsToPreserve(Date ck3BookmarkDate) {
Logger.Debug("Loading IDs of CK3 characters to preserve...");

Expand Down Expand Up @@ -468,93 +469,93 @@
parser.ParseFile(configurablePath);
}

public void PurgeUnneededCharacters(Title.LandedTitles titles, DynastyCollection dynasties, HouseCollection houses, Date ck3BookmarkDate) {
Logger.Info("Purging unneeded characters...");

// Characters from CK3 that hold titles at the bookmark date should be kept.
var currentTitleHolderIds = titles.GetHolderIdsForAllTitlesExceptNobleFamilyTitles(ck3BookmarkDate);
var landedCharacters = this
.Where(character => currentTitleHolderIds.Contains(character.Id))
.ToArray();
var charactersToCheck = this.Except(landedCharacters);

// Characters from I:R that held or hold titles should be kept.
var allTitleHolderIds = titles.GetAllHolderIds();
var imperatorTitleHolders = this
.Where(character => character.FromImperator && allTitleHolderIds.Contains(character.Id))
.ToArray();
charactersToCheck = charactersToCheck.Except(imperatorTitleHolders);

// Don't purge animation_test characters.
charactersToCheck = charactersToCheck
.Where(c => !c.Id.StartsWith("animation_test_"));

// Keep alive Imperator characters.
charactersToCheck = charactersToCheck
.Where(c => c is not {FromImperator: true, ImperatorCharacter.IsDead: false});

// Make some exceptions for characters referenced in game's script files.
charactersToCheck = charactersToCheck
.Where(character => !character.IsNonRemovable)
.ToArray();

// I:R members of landed dynasties will be preserved, unless dead and childless.
var dynastyIdsOfLandedCharacters = landedCharacters
.Select(character => character.GetDynastyId(ck3BookmarkDate))
.Distinct()
.Where(id => id is not null)
.ToHashSet();
.ToFrozenSet();

var i = 0;
var charactersToRemove = new List<Character>();
var parentIdsCache = new HashSet<string>();
do {
Logger.Debug($"Beginning iteration {i} of characters purge...");
charactersToRemove.Clear();
parentIdsCache.Clear();
++i;

// Build cache of all parent IDs.
foreach (var character in this) {
var motherId = character.MotherId;
if (motherId is not null) {
parentIdsCache.Add(motherId);
}

var fatherId = character.FatherId;
if (fatherId is not null) {
parentIdsCache.Add(fatherId);
}
}

// See who can be removed.
foreach (var character in charactersToCheck) {
// Is the character from Imperator and do they belong to a dynasty that holds or held titles?
if (character.FromImperator && dynastyIdsOfLandedCharacters.Contains(character.GetDynastyId(ck3BookmarkDate))) {
// Is the character dead and childless? Purge.
if (!parentIdsCache.Contains(character.Id)) {
charactersToRemove.Add(character);
}

continue;
}

charactersToRemove.Add(character);
}

BulkRemove(charactersToRemove.ConvertAll(c => c.Id));

Logger.Debug($"\tPurged {charactersToRemove.Count} unneeded characters in iteration {i}.");
charactersToCheck = charactersToCheck.Except(charactersToRemove).ToArray();
} while(charactersToRemove.Count > 0);

// At this point we probably have many dynasties with no characters left.
// Let's purge them.
houses.PurgeUnneededHouses(this, ck3BookmarkDate);
dynasties.PurgeUnneededDynasties(this, houses, ck3BookmarkDate);
dynasties.FlattenDynastiesWithNoFounders(this, houses, ck3BookmarkDate);
}

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

View check run for this annotation

codefactor.io / CodeFactor

ImperatorToCK3/CK3/Characters/CharacterCollection.cs#L472-L558

Complex Method
public void RemoveEmployerIdFromLandedCharacters(Title.LandedTitles titles, Date conversionDate) {
Logger.Info("Removing employer id from landed characters...");
var landedCharacterIds = titles.GetHolderIdsForAllTitlesExceptNobleFamilyTitles(conversionDate);
Expand Down Expand Up @@ -595,7 +596,7 @@
var vassalCharacterIds = ck3Country.GetDeFactoVassals(bookmarkDate).Values
.Where(vassalTitle => !vassalTitle.Landless)
.Select(vassalTitle => vassalTitle.GetHolderId(bookmarkDate))
.ToHashSet();
.ToFrozenSet();

var vassalCharacters = new HashSet<Character>();
foreach (var vassalCharacterId in vassalCharacterIds) {
Expand Down Expand Up @@ -661,154 +662,154 @@
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 && 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 812 in ImperatorToCK3/CK3/Characters/CharacterCollection.cs

View check run for this annotation

codefactor.io / CodeFactor

ImperatorToCK3/CK3/Characters/CharacterCollection.cs#L665-L812

Complex Method
internal void ConvertImperatorCharacterDNA(DNAFactory dnaFactory) {
Logger.Info("Converting Imperator character DNA to CK3...");
foreach (var character in this) {
Expand All @@ -830,7 +831,7 @@
public void RemoveUndefinedTraits(TraitMapper traitMapper) {
Logger.Info("Removing undefined traits from CK3 character history...");

var definedTraits = traitMapper.ValidCK3TraitIDs.ToHashSet();
var definedTraits = traitMapper.ValidCK3TraitIDs.ToFrozenSet();

foreach (var character in this) {
if (character.FromImperator) {
Expand All @@ -849,7 +850,7 @@
Logger.Info("Removing invalid dynasties from CK3 character history...");

var ck3Characters = this.Where(c => !c.FromImperator).ToArray();
var validDynastyIds = dynasties.Select(d => d.Id).ToHashSet();
var validDynastyIds = dynasties.Select(d => d.Id).ToFrozenSet();

foreach (var character in ck3Characters) {
if (!character.History.Fields.TryGetValue("dynasty", out var dynastyField)) {
Expand Down
14 changes: 9 additions & 5 deletions ImperatorToCK3/CK3/Characters/CharactersLoader.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
using commonItems;
using commonItems.Mods;
using Open.Collections.Synchronized;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Linq;
using ZLinq;

namespace ImperatorToCK3.CK3.Characters;

Expand Down Expand Up @@ -51,8 +52,10 @@ public void LoadCK3Characters(ModFilesystem ck3ModFS, Date bookmarkDate) {
"claims",
];

var femaleCharacterIds = loadedCharacters.Where(c => c.Female).Select(c => c.Id).ToHashSet();
var maleCharacterIds = loadedCharacters.Select(c => c.Id).Except(femaleCharacterIds).ToHashSet();
var femaleCharacterIds = loadedCharacters.AsValueEnumerable()
.Where(c => c.Female).Select(c => c.Id).ToFrozenSet();
var maleCharacterIds = loadedCharacters.AsValueEnumerable()
.Select(c => c.Id).Except(femaleCharacterIds).ToFrozenSet();

foreach (var character in loadedCharacters) {
// Clear some fields we don't need.
Expand Down Expand Up @@ -91,7 +94,8 @@ public void LoadCK3Characters(ModFilesystem ck3ModFS, Date bookmarkDate) {

// Remove effects that set relations. They don't matter a lot in our alternate timeline.
character.History.Fields["effects"].RemoveAllEntries(
entry => irrelevantEffects.Any(effect => entry.ToString()?.Contains(effect) ?? false));
entry => irrelevantEffects.AsValueEnumerable()
.Any(effect => entry.ToString()?.Contains(effect) ?? false));

// Fix characters being set as their own fathers/mothers.
if (character.FatherId == character.Id) {
Expand All @@ -111,7 +115,7 @@ public void LoadCK3Characters(ModFilesystem ck3ModFS, Date bookmarkDate) {
Logger.Info("Loaded CK3 characters.");
}

private static void RemoveInvalidMotherAndFatherEntries(Character character, HashSet<string> femaleCharacterIds, HashSet<string> maleCharacterIds) {
private static void RemoveInvalidMotherAndFatherEntries(Character character, FrozenSet<string> femaleCharacterIds, FrozenSet<string> maleCharacterIds) {
// Remove wrong sex mother and father references (male mothers, female fathers).
var motherField = character.History.Fields["mother"];
motherField.RemoveAllEntries(value => {
Expand Down
3 changes: 2 additions & 1 deletion ImperatorToCK3/CK3/Cultures/Culture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using commonItems.Serialization;
using ImperatorToCK3.Mappers.Technology;
using Open.Collections;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -103,7 +104,7 @@ public async Task OutputHistory(string outputModPath, Date date) {
await historyWriter.WriteAsync(historyStrBuilder.ToString());
}

public void ImportInnovationsFromImperator(ISet<string> irInventions, InnovationMapper innovationMapper) {
public void ImportInnovationsFromImperator(FrozenSet<string> irInventions, InnovationMapper innovationMapper) {
innovationsFromImperator.AddRange(innovationMapper.GetInnovations(irInventions));

var progresses = innovationMapper.GetInnovationProgresses(irInventions);
Expand Down
3 changes: 2 additions & 1 deletion ImperatorToCK3/CK3/Cultures/CultureCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using ImperatorToCK3.Mappers.Province;
using ImperatorToCK3.Mappers.Technology;
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Linq;

Expand Down Expand Up @@ -226,7 +227,7 @@
return cultureMapper.Match(irCulture, ck3ProvinceId, irProvinceId, country.HistoricalTag);
}

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 230 in ImperatorToCK3/CK3/Cultures/CultureCollection.cs

View workflow job for this annotation

GitHub Actions / Upload development build (linux-x64)

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

View workflow job for this annotation

GitHub Actions / build (macos-14)

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

View workflow job for this annotation

GitHub Actions / test_and_check_coverage

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

View workflow job for this annotation

GitHub Actions / test (macos-14)

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

View workflow job for this annotation

GitHub Actions / build (self-hosted, linux)

Check warning on line 230 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();
Expand All @@ -249,7 +250,7 @@

var irInventions = grouping
.SelectMany(c => c.Country.GetActiveInventionIds(inventionsDB))
.ToHashSet();
.ToFrozenSet();
culture.ImportInnovationsFromImperator(irInventions, innovationMapper);
}
}
Expand Down
Loading
Loading