• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ParadoxGameConverters / ImperatorToCK3 / 12637452189

06 Jan 2025 05:41PM UTC coverage: 48.469%. First build
12637452189

Pull #2399

github

web-flow
Merge 55659aab8 into 8a031acfa
Pull Request #2399: Compatibility with The Fallen Eagle 'After the Pharaohs' Update

2000 of 4935 branches covered (40.53%)

Branch coverage included in aggregate %.

5 of 41 new or added lines in 8 files covered. (12.2%)

7655 of 14985 relevant lines covered (51.08%)

6566.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

40.97
/ImperatorToCK3/CK3/Characters/CharacterCollection.cs
1
using commonItems;
2
using commonItems.Collections;
3
using commonItems.Localization;
4
using ImperatorToCK3.CK3.Armies;
5
using ImperatorToCK3.CK3.Cultures;
6
using ImperatorToCK3.CK3.Dynasties;
7
using ImperatorToCK3.CK3.Titles;
8
using ImperatorToCK3.CommonUtils;
9
using ImperatorToCK3.CommonUtils.Map;
10
using ImperatorToCK3.Imperator.Armies;
11
using ImperatorToCK3.Imperator.Characters;
12
using ImperatorToCK3.Mappers.Culture;
13
using ImperatorToCK3.Mappers.DeathReason;
14
using ImperatorToCK3.Mappers.Nickname;
15
using ImperatorToCK3.Mappers.Province;
16
using ImperatorToCK3.Mappers.Religion;
17
using ImperatorToCK3.Mappers.Trait;
18
using ImperatorToCK3.Mappers.UnitType;
19
using Open.Collections;
20
using System;
21
using System.Collections.Concurrent;
22
using System.Collections.Generic;
23
using System.Linq;
24
using System.Text.RegularExpressions;
25
using System.Threading.Tasks;
26

27
namespace ImperatorToCK3.CK3.Characters;
28

29
internal sealed partial class CharacterCollection : ConcurrentIdObjectCollection<string, Character> {
30
        internal void ImportImperatorCharacters(
31
                Imperator.World impWorld,
32
                ReligionMapper religionMapper,
33
                CultureMapper cultureMapper,
34
                CultureCollection ck3Cultures,
35
                TraitMapper traitMapper,
36
                NicknameMapper nicknameMapper,
37
                ProvinceMapper provinceMapper,
38
                DeathReasonMapper deathReasonMapper,
39
                DNAFactory dnaFactory,
40
                CK3LocDB ck3LocDB,
41
                Date conversionDate,
42
                Configuration config
43
        ) {
5✔
44
                Logger.Info("Importing Imperator Characters...");
5✔
45

46
                var unlocalizedImperatorNames = new ConcurrentHashSet<string>();
5✔
47

48
                var parallelOptions = new ParallelOptions {
5✔
49
                        MaxDegreeOfParallelism = Environment.ProcessorCount - 1,
5✔
50
                };
5✔
51

52
                Parallel.ForEach(impWorld.Characters, parallelOptions, irCharacter => {
18✔
53
                        ImportImperatorCharacter(
13✔
54
                                irCharacter,
13✔
55
                                religionMapper,
13✔
56
                                cultureMapper,
13✔
57
                                traitMapper,
13✔
58
                                nicknameMapper,
13✔
59
                                impWorld.LocDB,
13✔
60
                                ck3LocDB,
13✔
61
                                impWorld.MapData,
13✔
62
                                provinceMapper,
13✔
63
                                deathReasonMapper,
13✔
64
                                dnaFactory,
13✔
65
                                conversionDate,
13✔
66
                                config,
13✔
67
                                unlocalizedImperatorNames
13✔
68
                        );
13✔
69
                });
18✔
70
        
71
                if (unlocalizedImperatorNames.Any()) {
5!
72
                        Logger.Warn("Found unlocalized Imperator names: " + string.Join(", ", unlocalizedImperatorNames));
×
73
                }
×
74
                Logger.Info($"Imported {impWorld.Characters.Count} characters.");
5✔
75

76
                LinkMothersAndFathers();
5✔
77
                LinkSpouses(conversionDate);
5✔
78
                LinkPrisoners(conversionDate);
5✔
79

80
                ImportFriendships(impWorld.Characters, conversionDate);
5✔
81
                ImportRivalries(impWorld.Characters, conversionDate);
5✔
82
                Logger.IncrementProgress();
5✔
83

84
                ImportPregnancies(impWorld.Characters, conversionDate);
5✔
85

86
                if (config.FallenEagleEnabled) {
5!
87
                        SetCharacterCastes(ck3Cultures, config.CK3BookmarkDate);
×
88
                }
×
89
        }
5✔
90

91
        private void ImportImperatorCharacter(
92
                Imperator.Characters.Character irCharacter,
93
                ReligionMapper religionMapper,
94
                CultureMapper cultureMapper,
95
                TraitMapper traitMapper,
96
                NicknameMapper nicknameMapper,
97
                LocDB irLocDB,
98
                CK3LocDB ck3LocDB,
99
                MapData irMapData,
100
                ProvinceMapper provinceMapper,
101
                DeathReasonMapper deathReasonMapper,
102
                DNAFactory dnaFactory,
103
                Date endDate,
104
                Configuration config,
105
                ISet<string> unlocalizedImperatorNames) {
13✔
106
                // Create a new CK3 character.
107
                var newCharacter = new Character(
13✔
108
                        irCharacter,
13✔
109
                        this,
13✔
110
                        religionMapper,
13✔
111
                        cultureMapper,
13✔
112
                        traitMapper,
13✔
113
                        nicknameMapper,
13✔
114
                        irLocDB,
13✔
115
                        ck3LocDB,
13✔
116
                        irMapData,
13✔
117
                        provinceMapper,
13✔
118
                        deathReasonMapper,
13✔
119
                        dnaFactory,
13✔
120
                        endDate,
13✔
121
                        config,
13✔
122
                        unlocalizedImperatorNames
13✔
123
                );
13✔
124
                irCharacter.CK3Character = newCharacter;
13✔
125
                AddOrReplace(newCharacter);
13✔
126
        }
13✔
127

128
        public override void Remove(string key) {
1✔
129
                BulkRemove([key]);
1✔
130
        }
1✔
131

132
        private void BulkRemove(ICollection<string> keys) {
5✔
133
                foreach (var key in keys) {
27✔
134
                        var characterToRemove = this[key];
4✔
135

136
                        characterToRemove.RemoveAllSpouses();
4✔
137
                        characterToRemove.RemoveAllConcubines();
4✔
138
                        characterToRemove.RemoveAllChildren();
4✔
139

140
                        var irCharacter = characterToRemove.ImperatorCharacter;
4✔
141
                        if (irCharacter is not null) {
7✔
142
                                irCharacter.CK3Character = null;
3✔
143
                        }
3✔
144

145
                        base.Remove(key);
4✔
146
                }
4✔
147

148
                RemoveCharacterReferencesFromHistory(keys);
5✔
149
        }
5✔
150

151
        private void RemoveCharacterReferencesFromHistory(ICollection<string> idsToRemove) {
5✔
152
                var idsCapturingGroup = "(" + string.Join('|', idsToRemove) + ")";
5✔
153

154
                // Effects like "break_alliance = character:ID" entries should be removed.
155
                const string commandsGroup = "(break_alliance|make_concubine)";
156
                var simpleCommandsRegex = new Regex(commandsGroup + @"\s*=\s*character:" + idsCapturingGroup + @"\s*\b");
5✔
157

158
                foreach (var character in this) {
39✔
159
                        var effectsHistoryField = character.History.Fields["effects"];
8✔
160
                        if (effectsHistoryField is not LiteralHistoryField effectsLiteralField) {
8!
161
                                Logger.Warn($"Effects history field for character {character.Id} is not a literal field!");
×
162
                                continue;
×
163
                        }
164
                        if (effectsLiteralField.EntriesCount == 0) {
16!
165
                                continue;
8✔
166
                        }
167

168
                        effectsLiteralField.RegexReplaceAllEntries(simpleCommandsRegex, string.Empty);
×
169

170
                        // Remove all empty effect blocks (effect = { }).
171
                        effectsHistoryField.RemoveAllEntries(entryValue => {
×
172
                                if (entryValue is not string valueString) {
×
173
                                        return false;
×
174
                                }
×
175

×
176
                                string trimmedBlock = valueString.Trim();
×
177
                                if (!trimmedBlock.StartsWith('{') || !trimmedBlock.EndsWith('}')) {
×
178
                                        return false;
×
179
                                }
×
180

×
181
                                return trimmedBlock[1..^1].Trim().Length == 0;
×
182
                        });
×
183
                }
×
184
        }
5✔
185

186
        private void LinkMothersAndFathers() {
5✔
187
                var motherCounter = 0;
5✔
188
                var fatherCounter = 0;
5✔
189
                foreach (var ck3Character in this) {
54✔
190
                        // make links between Imperator characters
191
                        if (ck3Character.ImperatorCharacter is null) {
13!
192
                                // imperatorRegnal characters do not have ImperatorCharacter
193
                                continue;
×
194
                        }
195
                        var irMotherCharacter = ck3Character.ImperatorCharacter.Mother;
13✔
196
                        if (irMotherCharacter is not null) {
13!
197
                                var ck3MotherCharacter = irMotherCharacter.CK3Character;
×
198
                                if (ck3MotherCharacter is not null) {
×
199
                                        ck3Character.Mother = ck3MotherCharacter;
×
200
                                        ++motherCounter;
×
201
                                } else {
×
202
                                        Logger.Warn($"Imperator mother {irMotherCharacter.Id} has no CK3 character!");
×
203
                                }
×
204
                        }
×
205

206
                        // make links between Imperator characters
207
                        var irFatherCharacter = ck3Character.ImperatorCharacter.Father;
13✔
208
                        if (irFatherCharacter is not null) {
13!
209
                                var ck3FatherCharacter = irFatherCharacter.CK3Character;
×
210
                                if (ck3FatherCharacter is not null) {
×
211
                                        ck3Character.Father = ck3FatherCharacter;
×
212
                                        ++fatherCounter;
×
213
                                } else {
×
214
                                        Logger.Warn($"Imperator father {irFatherCharacter.Id} has no CK3 character!");
×
215
                                }
×
216
                        }
×
217
                }
13✔
218
                Logger.Info($"{motherCounter} mothers and {fatherCounter} fathers linked in CK3.");
5✔
219
        }
5✔
220

221
        private void LinkSpouses(Date conversionDate) {
5✔
222
                var spouseCounter = 0;
5✔
223
                foreach (var ck3Character in this) {
54✔
224
                        if (ck3Character.Female) {
16✔
225
                                continue; // we set spouses for males to avoid doubling marriages
3✔
226
                        }
227
                        // make links between Imperator characters
228
                        if (ck3Character.ImperatorCharacter is null) {
10!
229
                                // imperatorRegnal characters do not have ImperatorCharacter
230
                                continue;
×
231
                        }
232
                        foreach (var impSpouseCharacter in ck3Character.ImperatorCharacter.Spouses.Values) {
36✔
233
                                var ck3SpouseCharacter = impSpouseCharacter.CK3Character;
2✔
234
                                if (ck3SpouseCharacter is null) {
2!
235
                                        Logger.Warn($"Imperator spouse {impSpouseCharacter.Id} has no CK3 character!");
×
236
                                        continue;
×
237
                                }
238

239
                                // Imperator saves don't seem to store marriage date
240
                                Date estimatedMarriageDate = GetEstimatedMarriageDate(ck3Character.ImperatorCharacter, impSpouseCharacter);
2✔
241

242
                                ck3Character.AddSpouse(estimatedMarriageDate, ck3SpouseCharacter);
2✔
243
                                ++spouseCounter;
2✔
244
                        }
2✔
245
                }
10✔
246
                Logger.Info($"{spouseCounter} spouses linked in CK3.");
5✔
247

248
                Date GetEstimatedMarriageDate(Imperator.Characters.Character imperatorCharacter, Imperator.Characters.Character imperatorSpouse) {
2✔
249
                        // Imperator saves don't seem to store marriage date.
250

251
                        var marriageDeathDate = GetMarriageDeathDate(imperatorCharacter, imperatorSpouse);
2✔
252
                        var birthDateOfCommonChild = GetBirthDateOfFirstCommonChild(imperatorCharacter, imperatorSpouse);
2✔
253
                        if (birthDateOfCommonChild is not null) {
4!
254
                                // We assume the child was conceived after marriage.
255
                                var estimatedConceptionDate = birthDateOfCommonChild.ChangeByDays(-280);
2✔
256
                                if (marriageDeathDate is not null && marriageDeathDate < estimatedConceptionDate) {
2!
257
                                        estimatedConceptionDate = marriageDeathDate.ChangeByDays(-1);
×
258
                                }
×
259
                                return estimatedConceptionDate;
2✔
260
                        }
261

262
                        if (marriageDeathDate is not null) {
×
263
                                return marriageDeathDate.ChangeByDays(-1); // Death is not a good moment to marry.
×
264
                        }
265

266
                        return conversionDate;
×
267
                }
2✔
268
                Date? GetBirthDateOfFirstCommonChild(Imperator.Characters.Character father, Imperator.Characters.Character mother) {
2✔
269
                        var childrenOfFather = father.Children.Values.ToHashSet();
2✔
270
                        var childrenOfMother = mother.Children.Values.ToHashSet();
2✔
271
                        var commonChildren = childrenOfFather.Intersect(childrenOfMother).OrderBy(child => child.BirthDate).ToArray();
2✔
272

273
                        Date? firstChildBirthDate = commonChildren.Length > 0 ? commonChildren.FirstOrDefault()?.BirthDate : null;
2!
274
                        if (firstChildBirthDate is not null) {
3✔
275
                                return firstChildBirthDate;
1✔
276
                        }
277

278
                        var unborns = mother.Unborns.Where(u => u.FatherId == father.Id).OrderBy(u => u.BirthDate).ToArray();
2✔
279
                        return unborns.FirstOrDefault()?.BirthDate;
1!
280
                }
2✔
281

282
                Date? GetMarriageDeathDate(Imperator.Characters.Character husband, Imperator.Characters.Character wife) {
2✔
283
                        if (husband.DeathDate is not null && wife.DeathDate is not null) {
2!
284
                                return husband.DeathDate < wife.DeathDate ? husband.DeathDate : wife.DeathDate;
×
285
                        }
286
                        return husband.DeathDate ?? wife.DeathDate;
2✔
287
                }
2✔
288
        }
5✔
289

290
        private void LinkPrisoners(Date date) {
5✔
291
                var prisonerCount = this.Count(character => character.LinkJailor(date));
18✔
292
                Logger.Info($"{prisonerCount} prisoners linked with jailors in CK3.");
5✔
293
        }
5✔
294

295
        private static void ImportFriendships(Imperator.Characters.CharacterCollection irCharacters, Date conversionDate) {
5✔
296
                Logger.Info("Importing friendships...");
5✔
297
                foreach (var irCharacter in irCharacters) {
54✔
298
                        var ck3Character = irCharacter.CK3Character;
13✔
299
                        if (ck3Character is null) {
13!
300
                                Logger.Warn($"Imperator character {irCharacter.Id} has no CK3 character!");
×
301
                                continue;
×
302
                        }
303

304
                        foreach (var irFriendId in irCharacter.FriendIds) {
39!
305
                                // Make sure to only add this relation once.
306
                                if (irCharacter.Id.CompareTo(irFriendId) > 0) {
×
307
                                        continue;
×
308
                                }
309

310
                                var irFriend = irCharacters[irFriendId];
×
311
                                var ck3Friend = irFriend.CK3Character;
×
312

313
                                if (ck3Friend is not null) {
×
314
                                        var effectStr = $"{{ set_relation_friend={{ reason=friend_generic_history target=character:{ck3Friend.Id} }} }}";
×
315
                                        ck3Character.History.AddFieldValue(conversionDate, "effects", "effect", effectStr);
×
316
                                } else {
×
317
                                        Logger.Warn($"Imperator friend {irFriendId} has no CK3 character!");
×
318
                                }
×
319
                        }
×
320
                }
13✔
321
        }
5✔
322

323
        private static void ImportRivalries(Imperator.Characters.CharacterCollection irCharacters, Date conversionDate) {
5✔
324
                Logger.Info("Importing rivalries...");
5✔
325
                foreach (var irCharacter in irCharacters) {
54✔
326
                        var ck3Character = irCharacter.CK3Character;
13✔
327
                        if (ck3Character is null) {
13!
328
                                Logger.Warn($"Imperator character {irCharacter.Id} has no CK3 character!");
×
329
                                continue;
×
330
                        }
331

332
                        foreach (var irRivalId in irCharacter.RivalIds) {
39!
333
                                // Make sure to only add this relation once.
334
                                if (irCharacter.Id.CompareTo(irRivalId) > 0) {
×
335
                                        continue;
×
336
                                }
337

338
                                var irRival = irCharacters[irRivalId];
×
339
                                var ck3Rival = irRival.CK3Character;
×
340

341
                                if (ck3Rival is not null) {
×
342
                                        var effectStr = $"{{ set_relation_rival={{ reason=rival_historical target=character:{ck3Rival.Id} }} }}";
×
343
                                        ck3Character.History.AddFieldValue(conversionDate, "effects", "effect", effectStr);
×
344
                                } else {
×
345
                                        Logger.Warn($"Imperator rival {irRivalId} has no CK3 character!");
×
346
                                }
×
347
                        }
×
348
                }
13✔
349
        }
5✔
350
        private void ImportPregnancies(Imperator.Characters.CharacterCollection imperatorCharacters, Date conversionDate) {
5✔
351
                Logger.Info("Importing pregnancies...");
5✔
352
                foreach (var female in this.Where(c => c.Female)) {
37✔
353
                        var imperatorFemale = female.ImperatorCharacter;
3✔
354
                        if (imperatorFemale is null) {
3!
355
                                continue;
×
356
                        }
357

358
                        foreach (var unborn in imperatorFemale.Unborns) {
18✔
359
                                var conceptionDate = unborn.EstimatedConceptionDate;
3✔
360

361
                                // in CK3 the make_pregnant effect used in character history is executed on game start, so
362
                                // it only makes sense to convert pregnancies that lasted around 3 months or less
363
                                // (longest recorded pregnancy was around 12 months)
364
                                var pregnancyLength = conversionDate.DiffInYears(conceptionDate);
3✔
365
                                if (pregnancyLength > 0.25) {
4✔
366
                                        continue;
1✔
367
                                }
368

369
                                if (!imperatorCharacters.TryGetValue(unborn.FatherId, out var imperatorFather)) {
2!
370
                                        continue;
×
371
                                }
372

373
                                var ck3Father = imperatorFather.CK3Character;
2✔
374
                                if (ck3Father is null) {
2!
375
                                        continue;
×
376
                                }
377

378
                                female.Pregnancies.Add(new(ck3Father.Id, female.Id, unborn.BirthDate, unborn.IsBastard));
2✔
379
                        }
2✔
380
                }
3✔
381

382
                Logger.IncrementProgress();
5✔
383
        }
5✔
384

385
        private void SetCharacterCastes(CultureCollection cultures, Date ck3BookmarkDate) {
×
386
                var casteSystemCultureIds = cultures
×
387
                        .Where(c => c.TraditionIds.Contains("tradition_caste_system"))
×
388
                        .Select(c => c.Id)
×
389
                        .ToHashSet();
×
390
                var learningEducationTraits = new[]{"education_learning_1", "education_learning_2", "education_learning_3", "education_learning_4"};
×
391

392
                foreach (var character in this.OrderBy(c => c.BirthDate)) {
×
393
                        if (character.ImperatorCharacter is null) {
×
394
                                continue;
×
395
                        }
396

397
                        var cultureId = character.GetCultureId(ck3BookmarkDate);
×
398
                        if (cultureId is null || !casteSystemCultureIds.Contains(cultureId)) {
×
399
                                continue;
×
400
                        }
401

402
                        // The caste is hereditary.
403
                        var father = character.Father;
×
404
                        if (father is not null) {
×
405
                                var foundTrait = GetCasteTraitFromParent(father);
×
406
                                if (foundTrait is not null) {
×
407
                                        character.AddBaseTrait(foundTrait);
×
408
                                        continue;
×
409
                                }
410
                        }
×
411
                        var mother = character.Mother;
×
412
                        if (mother is not null) {
×
413
                                var foundTrait = GetCasteTraitFromParent(mother);
×
414
                                if (foundTrait is not null) {
×
415
                                        character.AddBaseTrait(foundTrait);
×
416
                                        continue;
×
417
                                }
418
                        }
×
419

420
                        // Try to set caste based on character's traits.
421
                        var traitIds = character.BaseTraits.ToHashSet();
×
422
                        character.AddBaseTrait(traitIds.Intersect(learningEducationTraits).Any() ? "brahmin" : "kshatriya");
×
423
                }
×
424
                return;
×
425

426
                static string? GetCasteTraitFromParent(Character parentCharacter) {
×
427
                        var casteTraits = new[]{"brahmin", "kshatriya", "vaishya", "shudra"};
×
428
                        var parentTraitIds = parentCharacter.BaseTraits.ToHashSet();
×
429
                        return casteTraits.Intersect(parentTraitIds).FirstOrDefault();
×
430
                }
×
431
        }
×
432

433
        public void LoadCharacterIDsToPreserve(Date ck3BookmarkDate) {
×
434
                Logger.Debug("Loading IDs of CK3 characters to preserve...");
×
435

436
                string configurablePath = "configurables/ck3_characters_to_preserve.txt";
×
437
                var parser = new Parser();
×
438
                parser.RegisterRegex("keep_as_is", reader => {
×
439
                        var ids = reader.GetStrings();
×
440
                        foreach (var id in ids) {
×
441
                                if (!TryGetValue(id, out var character)) {
×
442
                                        continue;
×
443
                                }
×
444

×
445
                                character.IsNonRemovable = true;
×
446
                        }
×
447
                });
×
448
                parser.RegisterKeyword("after_bookmark_date", reader => {
×
449
                        var ids = reader.GetStrings();
×
450
                        foreach (var id in ids) {
×
451
                                if (!TryGetValue(id, out var character)) {
×
452
                                        continue;
×
453
                                }
×
454

×
455
                                character.IsNonRemovable = true;
×
456
                                character.BirthDate = ck3BookmarkDate.ChangeByDays(1);
×
457
                                character.DeathDate = ck3BookmarkDate.ChangeByDays(2);
×
458
                                // Remove all dated history entries other than birth and death.
×
459
                                foreach (var field in character.History.Fields) {
×
460
                                        if (field.Id == "birth" || field.Id == "death") {
×
461
                                                continue;
×
462
                                        }
×
463
                                        field.DateToEntriesDict.Clear();
×
464
                                }
×
465
                        }
×
466
                });
×
467
                parser.IgnoreAndLogUnregisteredItems();
×
468
                parser.ParseFile(configurablePath);
×
469
        }
×
470

471
        public void PurgeUnneededCharacters(Title.LandedTitles titles, DynastyCollection dynasties, HouseCollection houses, Date ck3BookmarkDate) {
2✔
472
                Logger.Info("Purging unneeded characters...");
2✔
473
                
474
                // Characters from CK3 that hold titles at the bookmark date should be kept.
475
                var currentTitleHolderIds = titles.GetHolderIdsForAllTitlesExceptNobleFamilyTitles(ck3BookmarkDate);
2✔
476
                var landedCharacters = this
2✔
477
                        .Where(character => currentTitleHolderIds.Contains(character.Id))
5✔
478
                        .ToArray();
2✔
479
                var charactersToCheck = this.Except(landedCharacters);
2✔
480
                
481
                // Characters from I:R that held or hold titles should be kept.
482
                var allTitleHolderIds = titles.GetAllHolderIds();
2✔
483
                var imperatorTitleHolders = this
2✔
484
                        .Where(character => character.FromImperator && allTitleHolderIds.Contains(character.Id))
5✔
485
                        .ToArray();
2✔
486
                charactersToCheck = charactersToCheck.Except(imperatorTitleHolders);
2✔
487

488
                // Don't purge animation_test characters.
489
                charactersToCheck = charactersToCheck
2✔
490
                        .Where(c => !c.Id.StartsWith("animation_test_"));
6✔
491

492
                // Keep alive Imperator characters.
493
                charactersToCheck = charactersToCheck
2✔
494
                        .Where(c => c is not {FromImperator: true, ImperatorCharacter.IsDead: false});
6✔
495

496
                // Make some exceptions for characters referenced in game's script files.
497
                charactersToCheck = charactersToCheck
2✔
498
                        .Where(character => !character.IsNonRemovable)
4✔
499
                        .ToArray();
2✔
500

501
                // I:R members of landed dynasties will be preserved, unless dead and childless.
502
                var dynastyIdsOfLandedCharacters = landedCharacters
2✔
503
                        .Select(character => character.GetDynastyId(ck3BookmarkDate))
1✔
504
                        .Distinct()
2✔
505
                        .Where(id => id is not null)
1✔
506
                        .ToHashSet();
2✔
507

508
                var i = 0;
2✔
509
                var charactersToRemove = new List<Character>();
2✔
510
                var parentIdsCache = new HashSet<string>();
2✔
511
                do {
4✔
512
                        Logger.Debug($"Beginning iteration {i} of characters purge...");
4✔
513
                        charactersToRemove.Clear();
4✔
514
                        parentIdsCache.Clear();
4✔
515
                        ++i;
4✔
516

517
                        // Build cache of all parent IDs.
518
                        foreach (var character in this) {
33✔
519
                                var motherId = character.MotherId;
7✔
520
                                if (motherId is not null) {
7!
521
                                        parentIdsCache.Add(motherId);
×
522
                                }
×
523

524
                                var fatherId = character.FatherId;
7✔
525
                                if (fatherId is not null) {
10✔
526
                                        parentIdsCache.Add(fatherId);
3✔
527
                                }
3✔
528
                        }
7✔
529

530
                        // See who can be removed.
531
                        foreach (var character in charactersToCheck) {
27✔
532
                                // Is the character from Imperator and do they belong to a dynasty that holds or held titles?
533
                                if (character.FromImperator && dynastyIdsOfLandedCharacters.Contains(character.GetDynastyId(ck3BookmarkDate))) {
8✔
534
                                        // Is the character dead and childless? Purge.
535
                                        if (!parentIdsCache.Contains(character.Id)) {
4✔
536
                                                charactersToRemove.Add(character);
1✔
537
                                        }
1✔
538

539
                                        continue;
3✔
540
                                }
541

542
                                charactersToRemove.Add(character);
2✔
543
                        }
2✔
544

545
                        BulkRemove(charactersToRemove.ConvertAll(c => c.Id));
7✔
546

547
                        Logger.Debug($"\tPurged {charactersToRemove.Count} unneeded characters in iteration {i}.");
4✔
548
                        charactersToCheck = charactersToCheck.Except(charactersToRemove).ToArray();
4✔
549
                } while(charactersToRemove.Count > 0);
8✔
550

551
                // At this point we probably have many dynasties with no characters left.
552
                // Let's purge them.
553
                houses.PurgeUnneededHouses(this, ck3BookmarkDate);
2✔
554
                dynasties.PurgeUnneededDynasties(this, houses, ck3BookmarkDate);
2✔
555
                dynasties.FlattenDynastiesWithNoFounders(this, houses, ck3BookmarkDate);
2✔
556
        }
2✔
557

558
        public void RemoveEmployerIdFromLandedCharacters(Title.LandedTitles titles, Date conversionDate) {
×
559
                Logger.Info("Removing employer id from landed characters...");
×
560
                var landedCharacterIds = titles.GetHolderIdsForAllTitlesExceptNobleFamilyTitles(conversionDate);
×
561
                foreach (var character in this.Where(character => landedCharacterIds.Contains(character.Id))) {
×
562
                        character.History.Fields["employer"].RemoveAllEntries();
×
563
                }
×
564

565
                Logger.IncrementProgress();
×
566
        }
×
567

568
        /// <summary>
569
        /// Distributes Imperator countries' gold among rulers and governors
570
        /// </summary>
571
        /// <param name="titles">Landed titles collection</param>
572
        /// <param name="config">Current configuration</param>
573
        public void DistributeCountriesGold(Title.LandedTitles titles, Configuration config) {
1✔
574
                static void AddGoldToCharacter(Character character, double gold) {
3✔
575
                        if (character.Gold is null) {
6!
576
                                character.Gold = gold;
3✔
577
                        } else {
3✔
578
                                character.Gold += gold;
×
579
                        }
×
580
                }
3✔
581

582
                Logger.Info("Distributing countries' gold...");
1✔
583

584
                var bookmarkDate = config.CK3BookmarkDate;
1✔
585
                var ck3CountriesFromImperator = titles.GetCountriesImportedFromImperator();
1✔
586
                foreach (var ck3Country in ck3CountriesFromImperator) {
6✔
587
                        var rulerId = ck3Country.GetHolderId(bookmarkDate);
1✔
588
                        if (rulerId == "0") {
1!
589
                                Logger.Debug($"Can't distribute gold in {ck3Country} because it has no holder.");
×
590
                                continue;
×
591
                        }
592

593
                        var imperatorGold = ck3Country.ImperatorCountry!.Currencies.Gold * config.ImperatorCurrencyRate;
1✔
594

595
                        var vassalCharacterIds = ck3Country.GetDeFactoVassals(bookmarkDate).Values
1✔
596
                                .Where(vassalTitle => !vassalTitle.Landless)
2✔
597
                                .Select(vassalTitle => vassalTitle.GetHolderId(bookmarkDate))
2✔
598
                                .ToHashSet();
1✔
599

600
                        var vassalCharacters = new HashSet<Character>();
1✔
601
                        foreach (var vassalCharacterId in vassalCharacterIds) {
9✔
602
                                if (TryGetValue(vassalCharacterId, out var vassalCharacter)) {
4!
603
                                        vassalCharacters.Add(vassalCharacter);
2✔
604
                                } else {
2✔
605
                                        Logger.Warn($"Character {vassalCharacterId} not found!");
×
606
                                }
×
607
                        }
2✔
608

609
                        // Ruler should also get a share, he has double weight, so we add 2 to the count.
610
                        var mouthsToFeedCount = vassalCharacters.Count + 2;
1✔
611

612
                        var goldPerVassal = imperatorGold / mouthsToFeedCount;
1✔
613
                        foreach (var vassalCharacter in vassalCharacters) {
9✔
614
                                AddGoldToCharacter(vassalCharacter, goldPerVassal);
2✔
615
                                imperatorGold -= goldPerVassal;
2✔
616
                        }
2✔
617

618
                        var ruler = this[rulerId];
1✔
619
                        AddGoldToCharacter(ruler, imperatorGold);
1✔
620
                }
1✔
621

622
                Logger.IncrementProgress();
1✔
623
        }
1✔
624

625
        internal void ImportLegions(
626
                Title.LandedTitles titles,
627
                UnitCollection imperatorUnits,
628
                Imperator.Characters.CharacterCollection imperatorCharacters,
629
                Date date,
630
                UnitTypeMapper unitTypeMapper,
631
                IdObjectCollection<string, MenAtArmsType> menAtArmsTypes,
632
                ProvinceMapper provinceMapper,
633
                CK3LocDB ck3LocDB,
634
                Configuration config
635
        ) {
×
636
                Logger.Info("Importing Imperator armies...");
×
637

638
                var ck3CountriesFromImperator = titles.GetCountriesImportedFromImperator();
×
639
                foreach (var ck3Country in ck3CountriesFromImperator) {
×
640
                        var rulerId = ck3Country.GetHolderId(date);
×
641
                        if (rulerId == "0") {
×
642
                                Logger.Debug($"Can't add armies to {ck3Country} because it has no holder.");
×
643
                                continue;
×
644
                        }
645

646
                        var imperatorCountry = ck3Country.ImperatorCountry!;
×
647
                        var countryLegions = imperatorUnits.Where(u => u.CountryId == imperatorCountry.Id && u.IsArmy && u.IsLegion).ToArray();
×
648
                        if (countryLegions.Length == 0) {
×
649
                                continue;
×
650
                        }
651

652
                        var ruler = this[rulerId];
×
653

654
                        if (config.LegionConversion == LegionConversion.MenAtArms) {
×
655
                                ruler.ImportUnitsAsMenAtArms(countryLegions, date, unitTypeMapper, menAtArmsTypes, ck3LocDB);
×
656
                        } else if (config.LegionConversion == LegionConversion.SpecialTroops) {
×
657
                                ruler.ImportUnitsAsSpecialTroops(countryLegions, imperatorCharacters, date, unitTypeMapper, provinceMapper, ck3LocDB);
×
658
                        }
×
659
                }
×
660

661
                Logger.IncrementProgress();
×
662
        }
×
663

664
        public void GenerateSuccessorsForOldCharacters(Title.LandedTitles titles, CultureCollection cultures, Date irSaveDate, Date ck3BookmarkDate, ulong randomSeed) {
×
665
                Logger.Info("Generating successors for old characters...");
×
666
                
667
                var oldCharacters = this
×
668
                        .Where(c => c.BirthDate < ck3BookmarkDate && c.DeathDate is null)
×
669
                        .Where(c => ck3BookmarkDate.DiffInYears(c.BirthDate) > 60)
×
670
                        .ToArray();
×
671

672
                var titleHolderIds = titles.GetHolderIdsForAllTitlesExceptNobleFamilyTitles(ck3BookmarkDate);
×
673
                
674
                var oldTitleHolders = oldCharacters
×
675
                        .Where(c => titleHolderIds.Contains(c.Id))
×
676
                        .ToArray();
×
677
                
678
                // For characters that don't hold any titles, just set up a death date.
679
                var randomForCharactersWithoutTitles = new Random((int)randomSeed);
×
680
                foreach (var oldCharacter in oldCharacters.Except(oldTitleHolders)) {
×
681
                        // Roll a dice to determine how much longer the character will live.
682
                        var yearsToLive = randomForCharactersWithoutTitles.Next(0, 30);
×
683
                        
684
                        // If the character is female and pregnant, make sure she doesn't die before the pregnancy ends.
685
                        if (oldCharacter is {Female: true, ImperatorCharacter: not null}) {
×
686
                                var lastPregnancy = oldCharacter.Pregnancies.OrderBy(p => p.BirthDate).LastOrDefault();
×
687
                                if (lastPregnancy is not null) {
×
688
                                        oldCharacter.DeathDate = lastPregnancy.BirthDate.ChangeByYears(yearsToLive);
×
689
                                        continue;
×
690
                                }
691
                        }
×
692
                        
693
                        oldCharacter.DeathDate = irSaveDate.ChangeByYears(yearsToLive);
×
694
                }
×
695
                
696
                ConcurrentDictionary<string, Title[]> titlesByHolderId = new(titles
×
697
                        .Select(t => new {Title = t, HolderId = t.GetHolderId(ck3BookmarkDate)})
×
698
                        .Where(t => t.HolderId != "0")
×
699
                        .GroupBy(t => t.HolderId)
×
700
                        .ToDictionary(g => g.Key, g => g.Select(t => t.Title).ToArray()));
×
701

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

705
                // For title holders, generate successors and add them to title history.
706
                Parallel.ForEach(oldTitleHolders, oldCharacter => {
×
707
                        // Get all titles held by the character.
×
708
                        var heldTitles = titlesByHolderId[oldCharacter.Id];
×
709
                        string? dynastyId = oldCharacter.GetDynastyId(ck3BookmarkDate);
×
710
                        string? dynastyHouseId = oldCharacter.GetDynastyHouseId(ck3BookmarkDate);
×
711
                        string? faithId = oldCharacter.GetFaithId(ck3BookmarkDate);
×
712
                        string? cultureId = oldCharacter.GetCultureId(ck3BookmarkDate);
×
713
                        string[] maleNames;
×
714
                        if (cultureId is not null) {
×
715
                                maleNames = cultureIdToMaleNames[cultureId];
×
716
                        } else {
×
717
                                Logger.Warn($"Failed to find male names for successors of {oldCharacter.Id}.");
×
718
                                maleNames = ["Alexander"];
×
719
                        }
×
720
                        
×
721
                        var randomSeedForCharacter = randomSeed ^ (oldCharacter.ImperatorCharacter?.Id ?? 0);
×
722
                        var random = new Random((int)randomSeedForCharacter);
×
723

×
724
                        int successorCount = 0;
×
725
                        Character currentCharacter = oldCharacter;
×
726
                        Date currentCharacterBirthDate = currentCharacter.BirthDate;
×
727
                        while (ck3BookmarkDate.DiffInYears(currentCharacterBirthDate) >= 90) {
×
728
                                // If the character has living male children, the oldest one will be the successor.
×
729
                                var successorAndBirthDate = currentCharacter.Children
×
730
                                        .Where(c => c is {Female: false, DeathDate: null})
×
731
                                        .Select(c => new { Character = c, c.BirthDate })
×
732
                                        .OrderBy(x => x.BirthDate)
×
733
                                        .FirstOrDefault();
×
734
                                
×
735
                                Character successor;
×
736
                                Date currentCharacterDeathDate;
×
737
                                Date successorBirthDate;
×
738
                                if (successorAndBirthDate is not null) {
×
739
                                        successor = successorAndBirthDate.Character;
×
740
                                        successorBirthDate = successorAndBirthDate.BirthDate;
×
741
                                        
×
742
                                        // Roll a dice to determine how much longer the character will live.
×
743
                                        // But make sure the successor is at least 16 years old when the old character dies.
×
744
                                        var successorAgeAtBookmarkDate = ck3BookmarkDate.DiffInYears(successorBirthDate);
×
745
                                        var yearsUntilSuccessorBecomesAnAdult = Math.Max(16 - successorAgeAtBookmarkDate, 0);
×
746

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

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

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

×
770
                                        successorBirthDate = currentCharacterDeathDate.ChangeByYears(-successorAge);
×
771
                                        successor = new Character(id, firstName, successorBirthDate, this) {FromImperator = true};
×
772
                                        Add(successor);
×
773
                                        if (currentCharacter.Female) {
×
774
                                                successor.Mother = currentCharacter;
×
775
                                        } else {
×
776
                                                successor.Father = currentCharacter;
×
777
                                        }
×
778
                                        if (cultureId is not null) {
×
779
                                                successor.SetCultureId(cultureId, null);
×
780
                                        }
×
781
                                        if (faithId is not null) {
×
782
                                                successor.SetFaithId(faithId, null);
×
783
                                        }
×
784
                                        if (dynastyId is not null) {
×
785
                                                successor.SetDynastyId(dynastyId, null);
×
786
                                        }
×
787
                                        if (dynastyHouseId is not null) {
×
788
                                                successor.SetDynastyHouseId(dynastyHouseId, null);
×
789
                                        }
×
790
                                }
×
791

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

×
798
                                // Move to the successor and repeat the process.
×
799
                                currentCharacter = successor;
×
800
                                currentCharacterBirthDate = successorBirthDate;
×
801
                                ++successorCount;
×
802
                        }
×
803
                        
×
804
                        // After the loop, currentCharacter should represent the successor at bookmark date.
×
805
                        // Set his DNA to avoid weird looking character on the bookmark screen in CK3.
×
806
                        currentCharacter.DNA = oldCharacter.DNA;
×
807
                        
×
808
                        // Transfer gold to the living successor.
×
809
                        currentCharacter.Gold = oldCharacter.Gold;
×
810
                        oldCharacter.Gold = null;
×
811
                });
×
812
        }
×
813
        
814
        internal void ConvertImperatorCharacterDNA(DNAFactory dnaFactory) {
×
815
                Logger.Info("Converting Imperator character DNA to CK3...");
×
816
                foreach (var character in this) {
×
817
                        if (character.ImperatorCharacter is null) {
×
818
                                continue;
×
819
                        }
820
                        
821
                        PortraitData? portraitData = character.ImperatorCharacter.PortraitData;
×
822
                        if (portraitData is not null) {
×
823
                                character.DNA = dnaFactory.GenerateDNA(character.ImperatorCharacter, portraitData);
×
824
                        }
×
825
                }
×
826
        }
×
827

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

NEW
831
                var definedTraits = traitMapper.ValidCK3TraitIDs.ToHashSet();
×
832
                
NEW
833
                foreach (var character in this) {
×
NEW
834
                        if (character.FromImperator) {
×
NEW
835
                                continue;
×
836
                        }
837
                        
NEW
838
                        var traitsField = character.History.Fields["traits"];
×
NEW
839
                        int removedCount = traitsField.RemoveAllEntries(value => !definedTraits.Contains(value.ToString() ?? string.Empty));
×
NEW
840
                        if (removedCount > 0) {
×
NEW
841
                                Logger.Debug($"Removed {removedCount} undefined traits from character {character.Id}.");
×
NEW
842
                        }
×
NEW
843
                }
×
NEW
844
        }
×
845
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc