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

ParadoxGameConverters / ImperatorToCK3 / 18695912505

21 Oct 2025 07:53PM UTC coverage: 49.031%. First build
18695912505

Pull #2786

github

web-flow
Merge 7cb89c979 into ee02d66f7
Pull Request #2786: Fix exception while cleaning up title history

2411 of 5777 branches covered (41.73%)

Branch coverage included in aggregate %.

0 of 3 new or added lines in 1 file covered. (0.0%)

8622 of 16725 relevant lines covered (51.55%)

4159.95 hits per line

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

28.67
/ImperatorToCK3/CK3/Titles/LandedTitles.cs
1
using commonItems;
2
using commonItems.Collections;
3
using commonItems.Colors;
4
using commonItems.Localization;
5
using commonItems.Mods;
6
using ImperatorToCK3.CK3.Characters;
7
using ImperatorToCK3.CK3.Cultures;
8
using ImperatorToCK3.CK3.Provinces;
9
using ImperatorToCK3.CK3.Religions;
10
using ImperatorToCK3.CommonUtils;
11
using ImperatorToCK3.CommonUtils.Map;
12
using ImperatorToCK3.Imperator.Countries;
13
using ImperatorToCK3.Imperator.Diplomacy;
14
using ImperatorToCK3.Imperator.Jobs;
15
using ImperatorToCK3.Mappers.CoA;
16
using ImperatorToCK3.Mappers.Culture;
17
using ImperatorToCK3.Mappers.Government;
18
using ImperatorToCK3.Mappers.Nickname;
19
using ImperatorToCK3.Mappers.Province;
20
using ImperatorToCK3.Mappers.Region;
21
using ImperatorToCK3.Mappers.Religion;
22
using ImperatorToCK3.Mappers.SuccessionLaw;
23
using ImperatorToCK3.Mappers.TagTitle;
24
using Open.Collections;
25
using System;
26
using System.Collections.Frozen;
27
using System.Collections.Generic;
28
using System.Collections.Immutable;
29
using System.IO;
30
using System.Linq;
31
using System.Threading.Tasks;
32

33
namespace ImperatorToCK3.CK3.Titles;
34

35
internal sealed partial class Title {
36
        private readonly LandedTitles parentCollection;
37

38
        // This is a recursive class that scrapes common/landed_titles looking for title colors, landlessness,
39
        // and most importantly relation between baronies and barony provinces so we can link titles to actual clay.
40
        // Since titles are nested according to hierarchy we do this recursively.
41
        internal sealed class LandedTitles : TitleCollection {
42
                public Dictionary<string, object> Variables { get; } = [];
216✔
43
        
44
                public IEnumerable<Title> Counties => this.Where(t => t.Rank == TitleRank.county);
16✔
45

46
                public void LoadTitles(ModFilesystem ck3ModFS, CK3LocDB ck3LocDB, ColorFactory colorFactory) {
1✔
47
                        Logger.Info("Loading landed titles...");
1✔
48

49
                        var parser = new Parser();
1✔
50
                        RegisterKeys(parser, colorFactory);
1✔
51
                        parser.ParseGameFolder("common/landed_titles", ck3ModFS, "txt", recursive: true, logFilePaths: true);
1✔
52
                        LogIgnoredTokens();
1✔
53

54
                        MakeSureEveryCountyHasAnAdjective(ck3LocDB);
1✔
55

56
                        CleanUpCountriesHavingCapitalEntries();
1✔
57

58
                        CleanUpTitlesHavingInvalidCapitalCounties();
1✔
59

60
                        Logger.IncrementProgress();
1✔
61
                }
1✔
62

63
                private void MakeSureEveryCountyHasAnAdjective(CK3LocDB ck3LocDB) {
1✔
64
                        // Make sure every county has an adjective.
65
                        foreach (var county in Counties) {
6✔
66
                                string adjLocKey = county.Id + "_adj";
1✔
67

68
                                // Use the name loc as the adjective loc.
69
                                if (!ck3LocDB.TryGetValue(county.Id, out var nameLoc)) {
2!
70
                                        continue;
1✔
71
                                }
72
                                foreach (var language in ConverterGlobals.SupportedLanguages) {
×
73
                                        if (ck3LocDB.HasKeyLocForLanguage(adjLocKey, language)) {
×
74
                                                continue;
×
75
                                        }
76

77
                                        ck3LocDB.AddLocForLanguage(adjLocKey, language, nameLoc[language] ?? nameLoc[ConverterGlobals.PrimaryLanguage] ?? county.Id);
×
78
                                }
×
79
                        }
×
80
                }
1✔
81

82
                private void CleanUpCountriesHavingCapitalEntries() {
1✔
83
                        // Cleanup for counties having "capital" entries (found in TFE).
84
                        foreach (var county in Counties) {
6✔
85
                                if (county.CapitalCountyId is null) {
2!
86
                                        continue;
1✔
87
                                }
88

89
                                Logger.Debug($"Removing capital entry from county {county.Id}.");
×
90
                                county.CapitalCountyId = null;
×
91
                        }
×
92
                }
1✔
93

94
                private void CleanUpTitlesHavingInvalidCapitalCounties() {
1✔
95
                        // Cleanup for titles having invalid capital counties.
96
                        var validTitleIds = this.Select(t => t.Id).ToFrozenSet();
6✔
97
                        var placeholderCountyId = validTitleIds.Order().First(t => t.StartsWith("c_"));
2✔
98
                        foreach (var title in this.Where(t => t.Rank > TitleRank.county)) {
20✔
99
                                if (title.CapitalCountyId is null && !title.Landless) {
6✔
100
                                        // For landed titles, the game will generate capitals.
101
                                        continue;
2✔
102
                                }
103
                                if (title.CapitalCountyId is not null && validTitleIds.Contains(title.CapitalCountyId)) {
4!
104
                                        continue;
2✔
105
                                }
106
                                // Try to use the first valid capital of a de jure vassal.
107
                                string? newCapitalId;
108
                                if (title.Rank >= TitleRank.kingdom) {
×
109
                                        newCapitalId = title.DeJureVassals
×
110
                                                .Select(v => v.CapitalCountyId)
×
111
                                                .FirstOrDefault(vassalCapitalId => vassalCapitalId is not null && validTitleIds.Contains(vassalCapitalId));
×
112
                                } else {
×
113
                                        newCapitalId = title.DeJureVassals
×
114
                                                .Where(v => v.Rank == TitleRank.county)
×
115
                                                .Select(c => c.Id)
×
116
                                                .FirstOrDefault();
×
117
                                }
×
118

119
                                // If not found, for landless titles try using capital of de jure liege.
120
                                if (newCapitalId is null && title.Landless) {
×
121
                                        newCapitalId = title.DeJureLiege?.CapitalCountyId;
×
122
                                }
×
123
                                if (newCapitalId is not null) {
×
124
                                        Logger.Debug($"Title {title.Id} has invalid capital county {title.CapitalCountyId ?? "NULL"}, replacing it with {newCapitalId}.");
×
125
                                        title.CapitalCountyId = newCapitalId;
×
126
                                } else {
×
127
                                        Logger.Warn($"Using placeholder county as capital for title {title.Id} with invalid capital county {title.CapitalCountyId ?? "NULL"}.");
×
128
                                        title.CapitalCountyId = placeholderCountyId;
×
129
                                }
×
130
                        }
×
131
                }
1✔
132

133
                public void LoadTitles(BufferedReader reader, ColorFactory colorFactory) {
45✔
134
                        var parser = new Parser();
45✔
135
                        RegisterKeys(parser, colorFactory);
45✔
136
                        parser.ParseStream(reader);
45✔
137

138
                        LogIgnoredTokens();
45✔
139
                }
45✔
140
                public void LoadStaticTitles(ColorFactory colorFactory) {
×
141
                        Logger.Info("Loading static landed titles...");
×
142

143
                        var parser = new Parser();
×
144
                        RegisterKeys(parser, colorFactory);
×
145

146
                        parser.ParseFile("configurables/static_landed_titles.txt");
×
147

148
                        LogIgnoredTokens();
×
149

150
                        Logger.IncrementProgress();
×
151
                }
×
152

153
                public void CarveTitles(LandedTitles overrides) {
2✔
154
                        Logger.Debug("Carving titles...");
2✔
155
                        // merge in new king and empire titles into this from overrides, overriding duplicates
156
                        foreach (var overrideTitle in overrides.Where(t => t.Rank > TitleRank.duchy)) {
24✔
157
                                // inherit vanilla vassals
158
                                TryGetValue(overrideTitle.Id, out Title? vanillaTitle);
4✔
159
                                AddOrReplace(new Title(vanillaTitle, overrideTitle, this));
4✔
160
                        }
4✔
161

162
                        // update duchies to correct de jure liege, remove de jure titles that lose all de jure vassals
163
                        foreach (var title in overrides.Where(t => t.Rank == TitleRank.duchy)) {
18✔
164
                                if (!TryGetValue(title.Id, out Title? duchy)) {
2!
165
                                        Logger.Warn($"Duchy {title.Id} not found!");
×
166
                                        continue;
×
167
                                }
168
                                if (duchy.DeJureLiege is not null) {
3✔
169
                                        if (duchy.DeJureLiege.DeJureVassals.Count <= 1) {
2✔
170
                                                duchy.DeJureLiege.DeJureLiege = null;
1✔
171
                                        }
1✔
172
                                }
1✔
173
                                duchy.DeJureLiege = title.DeJureLiege;
2✔
174
                        }
2✔
175
                }
2✔
176

177
                private void RegisterKeys(Parser parser, ColorFactory colorFactory) {
46✔
178
                        parser.RegisterRegex(CommonRegexes.Variable, (reader, variableName) => {
46✔
179
                                var variableValue = reader.GetString();
×
180
                                Variables[variableName[1..]] = variableValue;
×
181
                        });
46✔
182
                        parser.RegisterRegex(Regexes.TitleId, (reader, titleNameStr) => {
130✔
183
                                // Pull the titles beneath this one and add them to the lot.
46✔
184
                                // A title can be defined in multiple files, in that case merge the definitions.
46✔
185
                                if (TryGetValue(titleNameStr, out var titleToUpdate)) {
88✔
186
                                        titleToUpdate.LoadTitles(reader, colorFactory);
4✔
187
                                } else {
84✔
188
                                        var newTitle = Add(titleNameStr);
80✔
189
                                        newTitle.LoadTitles(reader, colorFactory);
80✔
190
                                }
80✔
191
                        });
130✔
192
                        parser.IgnoreAndLogUnregisteredItems();
46✔
193
                }
46✔
194

195
                private static void LogIgnoredTokens() {
46✔
196
                        if (Title.IgnoredTokens.Count > 0) {
58✔
197
                                Logger.Warn($"Ignored title tokens: {Title.IgnoredTokens}");
12✔
198
                        }
12✔
199
                }
46✔
200

201
                public Title Add(string id) {
453✔
202
                        if (string.IsNullOrEmpty(id)) {
453!
203
                                throw new ArgumentException("Not inserting a Title with empty id!");
×
204
                        }
205

206
                        var newTitle = new Title(this, id);
453✔
207
                        dict[newTitle.Id] = newTitle;
452✔
208
                        return newTitle;
452✔
209
                }
452✔
210

211
                internal Title Add(
212
                        Country country,
213
                        Dependency? dependency,
214
                        CountryCollection imperatorCountries,
215
                        LocDB irLocDB,
216
                        CK3LocDB ck3LocDB,
217
                        ProvinceMapper provinceMapper,
218
                        CoaMapper coaMapper,
219
                        TagTitleMapper tagTitleMapper,
220
                        GovernmentMapper governmentMapper,
221
                        SuccessionLawMapper successionLawMapper,
222
                        DefiniteFormMapper definiteFormMapper,
223
                        ReligionMapper religionMapper,
224
                        CultureMapper cultureMapper,
225
                        NicknameMapper nicknameMapper,
226
                        CharacterCollection characters,
227
                        Date conversionDate,
228
                        Configuration config,
229
                        IReadOnlyCollection<string> enabledCK3Dlcs
230
                ) {
9✔
231
                        var newTitle = new Title(this,
9✔
232
                                country,
9✔
233
                                dependency,
9✔
234
                                imperatorCountries,
9✔
235
                                irLocDB,
9✔
236
                                ck3LocDB,
9✔
237
                                provinceMapper,
9✔
238
                                coaMapper,
9✔
239
                                tagTitleMapper,
9✔
240
                                governmentMapper,
9✔
241
                                successionLawMapper,
9✔
242
                                definiteFormMapper,
9✔
243
                                religionMapper,
9✔
244
                                cultureMapper,
9✔
245
                                nicknameMapper,
9✔
246
                                characters,
9✔
247
                                conversionDate,
9✔
248
                                config,
9✔
249
                                enabledCK3Dlcs
9✔
250
                        );
9✔
251
                        dict[newTitle.Id] = newTitle;
9✔
252
                        return newTitle;
9✔
253
                }
9✔
254

255
                internal Title Add(
256
                        string id,
257
                        Governorship governorship,
258
                        Country country,
259
                        Imperator.Provinces.ProvinceCollection irProvinces,
260
                        Imperator.Characters.CharacterCollection imperatorCharacters,
261
                        bool regionHasMultipleGovernorships,
262
                        LocDB irLocDB,
263
                        CK3LocDB ck3LocDB,
264
                        ProvinceMapper provinceMapper,
265
                        DefiniteFormMapper definiteFormMapper,
266
                        ImperatorRegionMapper imperatorRegionMapper,
267
                        Configuration config
268
                ) {
2✔
269
                        var newTitle = new Title(this,
2✔
270
                                id,
2✔
271
                                governorship,
2✔
272
                                country,
2✔
273
                                irProvinces,
2✔
274
                                imperatorCharacters,
2✔
275
                                regionHasMultipleGovernorships,
2✔
276
                                irLocDB,
2✔
277
                                ck3LocDB,
2✔
278
                                provinceMapper,
2✔
279
                                definiteFormMapper,
2✔
280
                                imperatorRegionMapper,
2✔
281
                                config
2✔
282
                        );
2✔
283
                        dict[newTitle.Id] = newTitle;
2✔
284
                        return newTitle;
2✔
285
                }
2✔
286
                public override void Remove(string titleId) {
1✔
287
                        if (dict.TryGetValue(titleId, out var titleToErase)) {
2✔
288
                                RemoveTitleFromDeJureRelationships(titleToErase);
1✔
289
                                RemoveTitleFromDeFactoRelationships(titleToErase);
1✔
290

291
                                if (titleToErase.ImperatorCountry is not null) {
2✔
292
                                        titleToErase.ImperatorCountry.CK3Title = null;
1✔
293
                                }
1✔
294
                        }
1✔
295

296
                        dict.Remove(titleId);
1✔
297
                }
1✔
298

299
                private void RemoveTitleFromDeFactoRelationships(Title titleToErase) {
1✔
300
                        // For all the de facto vassals of titleToErase, make titleToErase's liege their direct liege.
301
                        var newDFLiegeIdForInitialEntries = titleToErase.GetLiegeId(date: null)?.RemQuotes() ?? "0";
1!
302
                        foreach (var title in this) {
12✔
303
                                if (title.Id == titleToErase.Id) {
4✔
304
                                        continue;
1✔
305
                                }
306

307
                                if (title.Rank >= titleToErase.Rank) {
3✔
308
                                        continue;
1✔
309
                                }
310

311
                                if (!title.History.Fields.TryGetValue("liege", out var liegeField)) {
1!
312
                                        continue;
×
313
                                }
314

315
                                for (int i = 0; i < liegeField.InitialEntries.Count; i++) {
2!
316
                                        var kvp = liegeField.InitialEntries[i];
×
317
                                        if (kvp.Value.ToString()?.RemQuotes() == titleToErase.Id) {
×
318
                                                liegeField.InitialEntries[i] = new(kvp.Key, newDFLiegeIdForInitialEntries);
×
319
                                        }
×
320
                                }
×
321

322
                                foreach (var datedEntriesBlock in liegeField.DateToEntriesDict) {
6✔
323
                                        for (int i = 0; i < datedEntriesBlock.Value.Count; i++) {
5✔
324
                                                var kvp = datedEntriesBlock.Value[i];
1✔
325
                                                if (kvp.Value.ToString()?.RemQuotes() == titleToErase.Id) {
2!
326
                                                        var newDFLiegeId = titleToErase.GetLiegeId(datedEntriesBlock.Key)?.RemQuotes() ?? "0";
1!
327
                                                        datedEntriesBlock.Value[i] = new(kvp.Key, newDFLiegeId);
1✔
328
                                                }
1✔
329
                                        }
1✔
330
                                }
1✔
331
                        }
1✔
332
                }
1✔
333

334
                private static void RemoveTitleFromDeJureRelationships(Title titleToErase) {
1✔
335
                        // For all the de jure vassals of titleToErase, make titleToErase's liege their direct liege.
336
                        if (titleToErase.DeJureLiege is not null) {
2✔
337
                                foreach (var vassal in titleToErase.DeJureVassals) {
6✔
338
                                        vassal.DeJureLiege = titleToErase.DeJureLiege;
1✔
339
                                }
1✔
340
                        }
1✔
341

342
                        // Remove two-way de jure liege-vassal link.
343
                        titleToErase.DeJureLiege = null;
1✔
344
                }
1✔
345

346
                public Title? GetCountyForProvince(ulong provinceId) {
8✔
347
                        foreach (var county in this.Where(title => title.Rank == TitleRank.county)) {
103!
348
                                if (county.CountyProvinceIds.Contains(provinceId)) {
27✔
349
                                        return county;
8✔
350
                                }
351
                        }
11✔
352
                        return null;
×
353
                }
8✔
354

355
                public Title? GetBaronyForProvince(ulong provinceId) {
9✔
356
                        var baronies = this.Where(title => title.Rank == TitleRank.barony);
54✔
357
                        return baronies.FirstOrDefault(b => provinceId == b?.ProvinceId, defaultValue: null);
34!
358
                }
9✔
359

360
                public ImmutableHashSet<string> GetHolderIdsForAllTitlesExceptNobleFamilyTitles(Date date) {
2✔
361
                        return this
2✔
362
                                .Where(t => t.NobleFamily != true)
1✔
363
                                .Select(t => t.GetHolderId(date)).ToImmutableHashSet();
3✔
364
                }
2✔
365
                public ImmutableHashSet<string> GetAllHolderIds() {
×
366
                        return this.SelectMany(t => t.GetAllHolderIds()).ToImmutableHashSet();
×
367
                }
×
368

369
                public void CleanUpHistory(CharacterCollection characters, Date ck3BookmarkDate) {
×
370
                        Logger.Debug("Cleaning up title history...");
×
371
                        
372
                        // Remove invalid holder ID entries.
373
                        var validCharacterIds = characters.Select(c => c.Id).ToImmutableHashSet();
×
374
                        Parallel.ForEach(this, title => {
×
375
                                if (!title.History.Fields.TryGetValue("holder", out var holderField)) {
×
376
                                        return;
×
377
                                }
×
378

×
379
                                holderField.RemoveAllEntries(
×
380
                                        value => value.ToString()?.RemQuotes() is string valStr && valStr != "0" && !validCharacterIds.Contains(valStr)
×
381
                                );
×
382

×
383
                                // Afterwards, remove empty date entries.
×
384
                                holderField.DateToEntriesDict.RemoveWhere(kvp => kvp.Value.Count == 0);
×
385
                        });
×
386

387
                        // Fix holder being born after receiving the title, by moving the title grant to the birth date.
388
                        Parallel.ForEach(this, title => {
×
389
                                if (!title.History.Fields.TryGetValue("holder", out var holderField)) {
×
390
                                        return;
×
391
                                }
×
392

×
393
                                foreach (var (date, entriesList) in holderField.DateToEntriesDict.ToArray()) {
×
394
                                        if (date > ck3BookmarkDate) {
×
395
                                                continue;
×
396
                                        }
×
397

×
398
                                        var lastEntry = entriesList[^1];
×
399
                                        var holderId = lastEntry.Value.ToString()?.RemQuotes();
×
400
                                        if (holderId is null || holderId == "0") {
×
401
                                                continue;
×
402
                                        }
×
403

×
404
                                        if (!characters.TryGetValue(holderId, out var holder)) {
×
405
                                                holderField.DateToEntriesDict.Remove(date);
×
406
                                                continue;
×
407
                                        }
×
408

×
409
                                        var holderBirthDate = holder.BirthDate;
×
410
                                        if (date <= holderBirthDate) {
×
411
                                                // Move the title grant to the birth date.
×
412
                                                holderField.DateToEntriesDict.Remove(date);
×
413
                                                holderField.AddEntryToHistory(holderBirthDate, lastEntry.Key, lastEntry.Value);
×
414
                                        }
×
415
                                }
×
416
                        });
×
417
                        
418
                        // For counties, remove holder = 0 entries that precede a holder = <char ID> entry
419
                        // that's before or at the bookmark date.
420
                        Parallel.ForEach(Counties, county => {
×
421
                                if (!county.History.Fields.TryGetValue("holder", out var holderField)) {
×
422
                                        return;
×
423
                                }
×
424
                                
×
425
                                var holderIdAtBookmark = county.GetHolderId(ck3BookmarkDate);
×
426
                                if (holderIdAtBookmark == "0") {
×
427
                                        return;
×
428
                                }
×
429
                                
×
430
                                // If we have a holder at the bookmark date, remove all holder = 0 entries that precede it.
×
431
                                var entryDatesToRemove = holderField.DateToEntriesDict
×
432
                                        .Where(pair => pair.Key < ck3BookmarkDate && pair.Value.Exists(v => v.Value.ToString() == "0"))
×
433
                                        .Select(pair => pair.Key)
×
434
                                        .ToArray();
×
435
                                foreach (var date in entryDatesToRemove) {
×
436
                                        holderField.DateToEntriesDict.Remove(date);
×
437
                                }
×
438
                        });
×
439

440
                        // Remove liege entries of the same rank as the title they're in.
441
                        // For example, TFE had more or less this: d_kordofan = { liege = d_kordofan }
442
                        var validRankChars = new HashSet<char> { 'e', 'k', 'd', 'c', 'b'};
×
443
                        Parallel.ForEach(this, title => {
×
444
                                if (!title.History.Fields.TryGetValue("liege", out var liegeField)) {
×
445
                                        return;
×
446
                                }
×
447

×
448
                                var titleRank = title.Rank;
×
449

×
450
                                liegeField.RemoveAllEntries(value => {
×
451
                                        string? valueStr = value.ToString()?.RemQuotes();
×
452
                                        if (valueStr is null || valueStr == "0") {
×
453
                                                return false;
×
454
                                        }
×
455

×
456
                                        char rankChar = valueStr[0];
×
457
                                        if (!validRankChars.Contains(rankChar)) {
×
458
                                                Logger.Warn($"Removing invalid rank liege entry from {title.Id}: {valueStr}");
×
459
                                                return true;
×
460
                                        }
×
461
                                        
×
462
                                        var liegeRank = TitleRankUtils.CharToTitleRank(rankChar);
×
463
                                        if (liegeRank <= titleRank) {
×
464
                                                Logger.Warn($"Removing invalid rank liege entry from {title.Id}: {valueStr}");
×
465
                                                return true;
×
466
                                        }
×
467

×
468
                                        return false;
×
469
                                });
×
470
                        });
×
471
                        
472
                        // Remove liege entries that are not valid (liege title is not held at the entry date).
473
                        foreach (var title in this) {
×
474
                                if (!title.History.Fields.TryGetValue("liege", out var liegeField)) {
×
475
                                        continue;
×
476
                                }
477

478
                                foreach (var (date, entriesList) in liegeField.DateToEntriesDict.ToArray()) {
×
479
                                        if (entriesList.Count == 0) {
×
480
                                                continue;
×
481
                                        }
482
                                        
483
                                        var lastEntry = entriesList[^1];
×
484
                                        var liegeTitleId = lastEntry.Value.ToString()?.RemQuotes();
×
485
                                        if (liegeTitleId is null || liegeTitleId == "0") {
×
486
                                                continue;
×
487
                                        }
488

489
                                        if (!TryGetValue(liegeTitleId, out var liegeTitle)) {
×
490
                                                liegeField.DateToEntriesDict.Remove(date);
×
491
                                        } else if (liegeTitle.GetHolderId(date) == "0") {
×
492
                                                // Instead of removing the liege entry, see if the liege title has a holder at a later date,
493
                                                // and move the liege entry to that date.
494
                                                liegeTitle.History.Fields.TryGetValue("holder", out var liegeHolderField);
×
495
                                                Date? laterDate = liegeHolderField?.DateToEntriesDict
×
496
                                                        .Where(kvp => kvp.Key > date && kvp.Key <= ck3BookmarkDate && kvp.Value.Count != 0 && kvp.Value[^1].Value.ToString() != "0")
×
NEW
497
                                                        .OrderBy(kvp => kvp.Key)
×
NEW
498
                                                        .Select(kvp => (Date?)kvp.Key)
×
NEW
499
                                                        .FirstOrDefault(defaultValue: null);
×
500

501
                                                if (laterDate == null) {
×
502
                                                        liegeField.DateToEntriesDict.Remove(date);
×
503
                                                } else {
×
504
                                                        var (setter, value) = liegeField.DateToEntriesDict[date][^1];
×
505
                                                        liegeField.DateToEntriesDict.Remove(date);
×
506
                                                        liegeField.AddEntryToHistory(laterDate, setter, value);
×
507
                                                }
×
508
                                        }
×
509
                                }
×
510
                        }
×
511

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

518
                                successionLawsField.InitialEntries.RemoveAll(entry => true);
×
519
                        }
×
520
                }
×
521

522
                internal void ImportImperatorCountries(
523
                        CountryCollection imperatorCountries,
524
                        IReadOnlyCollection<Dependency> dependencies,
525
                        TagTitleMapper tagTitleMapper,
526
                        LocDB irLocDB,
527
                        CK3LocDB ck3LocDB,
528
                        ProvinceMapper provinceMapper,
529
                        CoaMapper coaMapper,
530
                        GovernmentMapper governmentMapper,
531
                        SuccessionLawMapper successionLawMapper,
532
                        DefiniteFormMapper definiteFormMapper,
533
                        ReligionMapper religionMapper,
534
                        CultureMapper cultureMapper,
535
                        NicknameMapper nicknameMapper,
536
                        CharacterCollection characters,
537
                        Date conversionDate,
538
                        Configuration config,
539
                        List<KeyValuePair<Country, Dependency?>> countyLevelCountries,
540
                        IReadOnlyCollection<string> enabledCK3Dlcs
541
                ) {
7✔
542
                        Logger.Info("Importing Imperator countries...");
7✔
543

544
                        // landedTitles holds all titles imported from CK3. We'll now overwrite some and
545
                        // add new ones from Imperator tags.
546
                        int counter = 0;
7✔
547
                        
548
                        // We don't need pirates, barbarians etc.
549
                        var realCountries = imperatorCountries.Where(c => c.CountryType == CountryType.real).ToImmutableList();
15✔
550
                        
551
                        // Import independent countries first, then subjects.
552
                        var independentCountries = realCountries.Where(c => dependencies.All(d => d.SubjectId != c.Id)).ToImmutableList();
15✔
553
                        var subjects = realCountries.Except(independentCountries).ToImmutableList();
7✔
554
                        
555
                        foreach (var country in independentCountries) {
45✔
556
                                ImportImperatorCountry(
8✔
557
                                        country,
8✔
558
                                        dependency: null,
8✔
559
                                        imperatorCountries,
8✔
560
                                        tagTitleMapper,
8✔
561
                                        irLocDB,
8✔
562
                                        ck3LocDB,
8✔
563
                                        provinceMapper,
8✔
564
                                        coaMapper,
8✔
565
                                        governmentMapper,
8✔
566
                                        successionLawMapper,
8✔
567
                                        definiteFormMapper,
8✔
568
                                        religionMapper,
8✔
569
                                        cultureMapper,
8✔
570
                                        nicknameMapper,
8✔
571
                                        characters,
8✔
572
                                        conversionDate,
8✔
573
                                        config,
8✔
574
                                        countyLevelCountries,
8✔
575
                                        enabledCK3Dlcs
8✔
576
                                );
8✔
577
                                ++counter;
8✔
578
                        }
8✔
579
                        foreach (var country in subjects) {
21!
580
                                ImportImperatorCountry(
×
581
                                        country,
×
582
                                        dependency: dependencies.FirstOrDefault(d => d.SubjectId == country.Id),
×
583
                                        imperatorCountries,
×
584
                                        tagTitleMapper,
×
585
                                        irLocDB,
×
586
                                        ck3LocDB,
×
587
                                        provinceMapper,
×
588
                                        coaMapper,
×
589
                                        governmentMapper,
×
590
                                        successionLawMapper,
×
591
                                        definiteFormMapper,
×
592
                                        religionMapper,
×
593
                                        cultureMapper,
×
594
                                        nicknameMapper,
×
595
                                        characters,
×
596
                                        conversionDate,
×
597
                                        config,
×
598
                                        countyLevelCountries,
×
599
                                        enabledCK3Dlcs
×
600
                                );
×
601
                                ++counter;
×
602
                        }
×
603
                        Logger.Info($"Imported {counter} countries from I:R.");
7✔
604
                }
7✔
605

606
                private void ImportImperatorCountry(
607
                        Country country,
608
                        Dependency? dependency,
609
                        CountryCollection imperatorCountries,
610
                        TagTitleMapper tagTitleMapper,
611
                        LocDB irLocDB,
612
                        CK3LocDB ck3LocDB,
613
                        ProvinceMapper provinceMapper,
614
                        CoaMapper coaMapper,
615
                        GovernmentMapper governmentMapper,
616
                        SuccessionLawMapper successionLawMapper,
617
                        DefiniteFormMapper definiteFormMapper,
618
                        ReligionMapper religionMapper,
619
                        CultureMapper cultureMapper,
620
                        NicknameMapper nicknameMapper,
621
                        CharacterCollection characters,
622
                        Date conversionDate,
623
                        Configuration config,
624
                        List<KeyValuePair<Country, Dependency?>> countyLevelCountries,
625
                        IReadOnlyCollection<string> enabledCK3Dlcs) {
8✔
626
                        // Create a new title or update existing title.
627
                        var titleId = DetermineId(country, dependency, imperatorCountries, tagTitleMapper, irLocDB, ck3LocDB);
8✔
628

629
                        if (GetRankForId(titleId) == TitleRank.county) {
8!
630
                                countyLevelCountries.Add(new(country, dependency));
×
631
                                Logger.Debug($"Country {country.Id} can only be converted as county level.");
×
632
                                return;
×
633
                        }
634

635
                        if (TryGetValue(titleId, out var existingTitle)) {
8!
636
                                existingTitle.InitializeFromTag(
×
637
                                        country,
×
638
                                        dependency,
×
639
                                        imperatorCountries,
×
640
                                        irLocDB,
×
641
                                        ck3LocDB,
×
642
                                        provinceMapper,
×
643
                                        coaMapper,
×
644
                                        governmentMapper,
×
645
                                        successionLawMapper,
×
646
                                        definiteFormMapper,
×
647
                                        religionMapper,
×
648
                                        cultureMapper,
×
649
                                        nicknameMapper,
×
650
                                        characters,
×
651
                                        conversionDate,
×
652
                                        config,
×
653
                                        enabledCK3Dlcs
×
654
                                );
×
655
                        } else {
8✔
656
                                Add(
8✔
657
                                        country,
8✔
658
                                        dependency,
8✔
659
                                        imperatorCountries,
8✔
660
                                        irLocDB,
8✔
661
                                        ck3LocDB,
8✔
662
                                        provinceMapper,
8✔
663
                                        coaMapper,
8✔
664
                                        tagTitleMapper,
8✔
665
                                        governmentMapper,
8✔
666
                                        successionLawMapper,
8✔
667
                                        definiteFormMapper,
8✔
668
                                        religionMapper,
8✔
669
                                        cultureMapper,
8✔
670
                                        nicknameMapper,
8✔
671
                                        characters,
8✔
672
                                        conversionDate,
8✔
673
                                        config,
8✔
674
                                        enabledCK3Dlcs
8✔
675
                                );
8✔
676
                        }
8✔
677
                }
8✔
678

679
                internal void ImportImperatorGovernorships(
680
                        Imperator.World irWorld,
681
                        ProvinceCollection ck3Provinces,
682
                        TagTitleMapper tagTitleMapper,
683
                        LocDB irLocDB,
684
                        CK3LocDB ck3LocDB,
685
                        Configuration config,
686
                        ProvinceMapper provinceMapper,
687
                        DefiniteFormMapper definiteFormMapper,
688
                        ImperatorRegionMapper imperatorRegionMapper,
689
                        CoaMapper coaMapper,
690
                        List<Governorship> countyLevelGovernorships
691
                ) {
2✔
692
                        Logger.Info("Importing Imperator Governorships...");
2✔
693

694
                        var governorships = irWorld.JobsDB.Governorships;
2✔
695
                        var governorshipsPerRegion = governorships.GroupBy(g => g.Region.Id)
5✔
696
                                .ToFrozenDictionary(g => g.Key, g => g.Count());
8✔
697

698
                        // landedTitles holds all titles imported from CK3. We'll now overwrite some and
699
                        // add new ones from Imperator governorships.
700
                        var counter = 0;
2✔
701
                        foreach (var governorship in governorships) {
15✔
702
                                ImportImperatorGovernorship(
3✔
703
                                        governorship,
3✔
704
                                        this,
3✔
705
                                        ck3Provinces,
3✔
706
                                        irWorld.Provinces,
3✔
707
                                        irWorld.Characters,
3✔
708
                                        governorshipsPerRegion[governorship.Region.Id] > 1,
3✔
709
                                        tagTitleMapper,
3✔
710
                                        irLocDB,
3✔
711
                                        ck3LocDB,
3✔
712
                                        provinceMapper,
3✔
713
                                        definiteFormMapper,
3✔
714
                                        imperatorRegionMapper,
3✔
715
                                        coaMapper,
3✔
716
                                        countyLevelGovernorships,
3✔
717
                                        config
3✔
718
                                );
3✔
719
                                ++counter;
3✔
720
                        }
3✔
721
                        Logger.Info($"Imported {counter} governorships from I:R.");
2✔
722
                        Logger.IncrementProgress();
2✔
723
                }
2✔
724
                private void ImportImperatorGovernorship(
725
                        Governorship governorship,
726
                        LandedTitles titles,
727
                        ProvinceCollection ck3Provinces,
728
                        Imperator.Provinces.ProvinceCollection irProvinces,
729
                        Imperator.Characters.CharacterCollection imperatorCharacters,
730
                        bool regionHasMultipleGovernorships,
731
                        TagTitleMapper tagTitleMapper,
732
                        LocDB irLocDB,
733
                        CK3LocDB ck3LocDB,
734
                        ProvinceMapper provinceMapper,
735
                        DefiniteFormMapper definiteFormMapper,
736
                        ImperatorRegionMapper imperatorRegionMapper,
737
                        CoaMapper coaMapper,
738
                        List<Governorship> countyLevelGovernorships,
739
                        Configuration config
740
                ) {
3✔
741
                        var country = governorship.Country;
3✔
742

743
                        var id = DetermineId(governorship, titles, irProvinces, ck3Provinces, imperatorRegionMapper, tagTitleMapper, provinceMapper);
3✔
744
                        if (id is null) {
3!
745
                                Logger.Warn($"Cannot convert {governorship.Region.Id} of country {country.Id}");
×
746
                                return;
×
747
                        }
748

749
                        if (GetRankForId(id) == TitleRank.county) {
4✔
750
                                countyLevelGovernorships.Add(governorship);
1✔
751
                                return;
1✔
752
                        }
753

754
                        // Create a new title or update existing title
755
                        if (TryGetValue(id, out var existingTitle)) {
2!
756
                                existingTitle.InitializeFromGovernorship(
×
757
                                        governorship,
×
758
                                        country,
×
759
                                        irProvinces,
×
760
                                        imperatorCharacters,
×
761
                                        regionHasMultipleGovernorships,
×
762
                                        irLocDB,
×
763
                                        ck3LocDB,
×
764
                                        provinceMapper,
×
765
                                        definiteFormMapper,
×
766
                                        imperatorRegionMapper,
×
767
                                        config
×
768
                                );
×
769
                        } else {
2✔
770
                                Add(
2✔
771
                                        id,
2✔
772
                                        governorship,
2✔
773
                                        country,
2✔
774
                                        irProvinces,
2✔
775
                                        imperatorCharacters,
2✔
776
                                        regionHasMultipleGovernorships,
2✔
777
                                        irLocDB,
2✔
778
                                        ck3LocDB,
2✔
779
                                        provinceMapper,
2✔
780
                                        definiteFormMapper,
2✔
781
                                        imperatorRegionMapper,
2✔
782
                                        config
2✔
783
                                );
2✔
784
                        }
2✔
785
                }
3✔
786

787
                public void ImportImperatorHoldings(ProvinceCollection ck3Provinces, Imperator.Characters.CharacterCollection irCharacters, Date conversionDate) {
×
788
                        Logger.Info("Importing Imperator holdings...");
×
789
                        var counter = 0;
×
790
                        
791
                        var highLevelTitlesThatHaveHolders = this
×
792
                                .Where(t => t.Rank >= TitleRank.duchy && t.GetHolderId(conversionDate) != "0")
×
793
                                .ToImmutableList();
×
794
                        var highLevelTitleCapitalBaronyIds = highLevelTitlesThatHaveHolders
×
795
                                .Select(t=>t.CapitalCounty?.CapitalBaronyId ?? t.CapitalBaronyId)
×
796
                                .ToImmutableHashSet();
×
797
                        
798
                        // Dukes and above should be excluded from having their holdings converted.
799
                        // Otherwise, governors with holdings would own parts of other governorships.
800
                        var dukeAndAboveIds = highLevelTitlesThatHaveHolders
×
801
                                .Where(t => t.Rank >= TitleRank.duchy)
×
802
                                .Select(t => t.GetHolderId(conversionDate))
×
803
                                .ToImmutableHashSet();
×
804
                        
805
                        // We exclude baronies that are capitals of duchies and above.
806
                        var eligibleBaronies = this
×
807
                                .Where(t => t.Rank == TitleRank.barony)
×
808
                                .Where(b => !highLevelTitleCapitalBaronyIds.Contains(b.Id))
×
809
                                .ToArray();
×
810
                        
811
                        var countyCapitalBaronies = eligibleBaronies
×
812
                                .Where(b => b.DeJureLiege?.CapitalBaronyId == b.Id)
×
813
                                .OrderBy(b => b.Id)
×
814
                                .ToArray();
×
815
                        
816
                        var nonCapitalBaronies = eligibleBaronies.Except(countyCapitalBaronies).OrderBy(b => b.Id).ToArray();
×
817
                        
818

819
                        // In CK3, a county holder shouldn't own baronies in counties that are not their own.
820
                        // This dictionary tracks what counties are held by what characters.
821
                        Dictionary<string, HashSet<string>> countiesPerCharacter = []; // characterId -> countyIds
×
822
                        
823
                        // Evaluate all capital baronies first (we want to distribute counties first, then baronies).
824
                        foreach (var barony in countyCapitalBaronies) {
×
825
                                var ck3Province = GetBaronyProvince(barony);
×
826
                                if (ck3Province is null) {
×
827
                                        continue;
×
828
                                }
829

830
                                // Skip none holdings and temple holdings.
831
                                if (ck3Province.GetHoldingType(conversionDate) is "church_holding" or "none") {
×
832
                                        continue;
×
833
                                }
834

835
                                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
×
836
                                var ck3Owner = GetEligibleCK3OwnerForImperatorProvince(irProvince);
×
837
                                if (ck3Owner is null) {
×
838
                                        continue;
×
839
                                }
840
                                
841
                                var realm = ck3Owner.ImperatorCharacter?.HomeCountry?.CK3Title;
×
842
                                var deFactoLiege = realm;
×
843
                                if (realm is not null) {
×
844
                                        var deJureDuchy = barony.DeJureLiege?.DeJureLiege;
×
845
                                        if (deJureDuchy is not null && deJureDuchy.GetHolderId(conversionDate) != "0" && deJureDuchy.GetTopRealm(conversionDate) == realm) {
×
846
                                                deFactoLiege = deJureDuchy;
×
847
                                        } else {
×
848
                                                var deJureKingdom = deJureDuchy?.DeJureLiege;
×
849
                                                if (deJureKingdom is not null && deJureKingdom.GetHolderId(conversionDate) != "0" && deJureKingdom.GetTopRealm(conversionDate) == realm) {
×
850
                                                        deFactoLiege = deJureKingdom;
×
851
                                                }
×
852
                                        }
×
853
                                }
×
854
                                
855
                                // Barony is a county capital, so set the county holder to the holding owner.
856
                                var county = barony.DeJureLiege;
×
857
                                if (county is null) {
×
858
                                        Logger.Warn($"County capital barony {barony.Id} has no de jure county!");
×
859
                                        continue;
×
860
                                }
861
                                county.SetHolder(ck3Owner, conversionDate);
×
862
                                county.SetDeFactoLiege(deFactoLiege, conversionDate);
×
863
                                
864
                                if (!countiesPerCharacter.TryGetValue(ck3Owner.Id, out var countyIds)) {
×
865
                                        countyIds = [];
×
866
                                        countiesPerCharacter[ck3Owner.Id] = countyIds;
×
867
                                }
×
868
                                countyIds.Add(county.Id);
×
869
                                
870
                                ++counter;
×
871
                        }
×
872
                        
873
                        // In CK3, a baron that doesn't own counties can only hold a single barony.
874
                        // This dictionary IDs of such barons that already hold a barony.
875
                        HashSet<string> baronyHolderIds = [];
×
876
                        
877
                        // After all possible county capital baronies are distributed, distribute the rest of the eligible baronies.
878
                        foreach (var barony in nonCapitalBaronies) {
×
879
                                var ck3Province = GetBaronyProvince(barony);
×
880
                                if (ck3Province is null) {
×
881
                                        continue;
×
882
                                }
883

884
                                // Skip none holdings and temple holdings.
885
                                if (ck3Province.GetHoldingType(conversionDate) is "church_holding" or "none") {
×
886
                                        continue;
×
887
                                }
888

889
                                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
×
890
                                var ck3Owner = GetEligibleCK3OwnerForImperatorProvince(irProvince);
×
891
                                if (ck3Owner is null) {
×
892
                                        continue;
×
893
                                }
894
                                if (baronyHolderIds.Contains(ck3Owner.Id)) {
×
895
                                        continue;
×
896
                                }
897
                                
898
                                var county = barony.DeJureLiege;
×
899
                                if (county is null) {
×
900
                                        Logger.Warn($"Barony {barony.Id} has no de jure county!");
×
901
                                        continue;
×
902
                                }
903
                                // A non-capital barony cannot be held by a character that owns a county but not the county the barony is in.
904
                                if (countiesPerCharacter.TryGetValue(ck3Owner.Id, out var countyIds) && !countyIds.Contains(county.Id)) {
×
905
                                        continue;
×
906
                                }
907
                                        
908
                                barony.SetHolder(ck3Owner, conversionDate);
×
909
                                // No need to set de facto liege for baronies, they are tied to counties.
910
                                
911
                                baronyHolderIds.Add(ck3Owner.Id);
×
912
                                
913
                                ++counter;
×
914
                        }
×
915
                        Logger.Info($"Imported {counter} holdings from I:R.");
×
916
                        Logger.IncrementProgress();
×
917
                        return;
×
918

919
                        Province? GetBaronyProvince(Title barony) {
920
                                var ck3ProvinceId = barony.ProvinceId;
921
                                if (ck3ProvinceId is null) {
922
                                        return null;
923
                                }
924
                                if (!ck3Provinces.TryGetValue(ck3ProvinceId.Value, out var ck3Province)) {
925
                                        return null;
926
                                }
927
                                return ck3Province;
928
                        }
929

930
                        Character? GetEligibleCK3OwnerForImperatorProvince(Imperator.Provinces.Province? irProvince) {
931
                                var holdingOwnerId = irProvince?.HoldingOwnerId;
932
                                if (holdingOwnerId is null) {
933
                                        return null;
934
                                }
935

936
                                var irOwner = irCharacters[holdingOwnerId.Value];
937
                                var ck3Owner = irOwner.CK3Character;
938
                                if (ck3Owner is null) {
939
                                        return null;
940
                                }
941
                                if (dukeAndAboveIds.Contains(ck3Owner.Id)) {
942
                                        return null;
943
                                }
944
                                
945
                                return ck3Owner;
946
                        }
947
                }
×
948

949
                public void RemoveInvalidLandlessTitles(Date ck3BookmarkDate) {
×
950
                        Logger.Info("Removing invalid landless titles...");
×
951
                        var removedGeneratedTitles = new HashSet<string>();
×
952
                        var revokedVanillaTitles = new HashSet<string>();
×
953

954
                        HashSet<string> countyHoldersCache = GetCountyHolderIds(ck3BookmarkDate);
×
955

956
                        foreach (var title in this) {
×
957
                                // If duchy/kingdom/empire title holder holds no counties, revoke the title.
958
                                // In case of titles created from Imperator, completely remove them.
959
                                if (title.Rank <= TitleRank.county) {
×
960
                                        continue;
×
961
                                }
962
                                if (countyHoldersCache.Contains(title.GetHolderId(ck3BookmarkDate))) {
×
963
                                        continue;
×
964
                                }
965

966
                                // Check if the title has "landless = yes" attribute.
967
                                // If it does, it should be always kept.
968
                                var id = title.Id;
×
969
                                if (this[id].Landless) {
×
970
                                        continue;
×
971
                                }
972

973
                                if (title.IsCreatedFromImperator) {
×
974
                                        removedGeneratedTitles.Add(id);
×
975
                                        Remove(id);
×
976
                                } else {
×
977
                                        revokedVanillaTitles.Add(id);
×
978
                                        title.ClearHolderSpecificHistory();
×
979
                                        title.SetDeFactoLiege(newLiege: null, ck3BookmarkDate);
×
980
                                }
×
981
                        }
×
982
                        if (removedGeneratedTitles.Count > 0) {
×
983
                                Logger.Debug($"Found landless generated titles that can't be landless: {string.Join(", ", removedGeneratedTitles)}");
×
984
                        }
×
985
                        if (revokedVanillaTitles.Count > 0) {
×
986
                                Logger.Debug($"Found landless vanilla titles that can't be landless: {string.Join(", ", revokedVanillaTitles)}");
×
987
                        }
×
988

989
                        Logger.IncrementProgress();
×
990
                }
×
991

992
                private void SetDeJureKingdoms(CK3LocDB ck3LocDB, Date ck3BookmarkDate) {
×
993
                        Logger.Info("Setting de jure kingdoms...");
×
994

995
                        var duchies = this.Where(t => t.Rank == TitleRank.duchy).ToFrozenSet();
×
996
                        var duchiesWithDeJureVassals = duchies.Where(d => d.DeJureVassals.Count > 0).ToFrozenSet();
×
997

998
                        foreach (var duchy in duchiesWithDeJureVassals) {
×
999
                                // If capital county belongs to an empire and contains the empire's capital,
1000
                                // create a kingdom from the duchy and make the empire a de jure liege of the kingdom.
1001
                                var capitalEmpireRealm = duchy.CapitalCounty?.GetRealmOfRank(TitleRank.empire, ck3BookmarkDate);
×
1002
                                var duchyCounties = duchy.GetDeJureVassalsAndBelow("c").Values;
×
1003
                                if (capitalEmpireRealm is not null && duchyCounties.Any(c => c.Id == capitalEmpireRealm.CapitalCountyId)) {
×
1004
                                        var kingdom = Add("k_IRTOCK3_kingdom_from_" + duchy.Id);
×
1005
                                        kingdom.Color1 = duchy.Color1;
×
1006
                                        kingdom.CapitalCounty = duchy.CapitalCounty;
×
1007

1008
                                        var kingdomNameLoc = ck3LocDB.GetOrCreateLocBlock(kingdom.Id);
×
1009
                                        kingdomNameLoc.ModifyForEveryLanguage(
×
1010
                                                (orig, language) => $"${duchy.Id}$"
×
1011
                                        );
×
1012
                                        
1013
                                        var kingdomAdjLoc = ck3LocDB.GetOrCreateLocBlock(kingdom.Id + "_adj");
×
1014
                                        string duchyAdjLocKey = duchy.Id + "_adj";
×
1015
                                        kingdomAdjLoc.ModifyForEveryLanguage(
×
1016
                                                (orig, language) => {
×
1017
                                                        if (ck3LocDB.HasKeyLocForLanguage(duchyAdjLocKey, language)) {
×
1018
                                                                return $"${duchyAdjLocKey}$";
×
1019
                                                        }
×
1020
                                                        
×
1021
                                                        Logger.Debug($"Using duchy name as adjective for {kingdom.Id} in {language} because duchy adjective is missing.");
×
1022
                                                        return $"${duchy.Id}$";
×
1023
                                                }
×
1024
                                        );
×
1025
                                        
1026
                                        kingdom.DeJureLiege = capitalEmpireRealm;
×
1027
                                        duchy.DeJureLiege = kingdom;
×
1028
                                        continue;
×
1029
                                }
1030
                                
1031
                                // If capital county belongs to a kingdom, make the kingdom a de jure liege of the duchy.
1032
                                var capitalKingdomRealm = duchy.CapitalCounty?.GetRealmOfRank(TitleRank.kingdom, ck3BookmarkDate);
×
1033
                                if (capitalKingdomRealm is not null) {
×
1034
                                        duchy.DeJureLiege = capitalKingdomRealm;
×
1035
                                        continue;
×
1036
                                }
1037

1038
                                // Otherwise, use the kingdom that owns the biggest percentage of the duchy.
1039
                                var kingdomRealmShares = new Dictionary<string, int>(); // realm, number of provinces held in duchy
×
1040
                                foreach (var county in duchyCounties) {
×
1041
                                        var kingdomRealm = county.GetRealmOfRank(TitleRank.kingdom, ck3BookmarkDate);
×
1042
                                        if (kingdomRealm is null) {
×
1043
                                                continue;
×
1044
                                        }
1045
                                        kingdomRealmShares.TryGetValue(kingdomRealm.Id, out int currentCount);
×
1046
                                        kingdomRealmShares[kingdomRealm.Id] = currentCount + county.CountyProvinceIds.Count();
×
1047
                                }
×
1048

1049
                                if (kingdomRealmShares.Count > 0) {
×
1050
                                        var biggestShare = kingdomRealmShares.MaxBy(pair => pair.Value);
×
1051
                                        duchy.DeJureLiege = this[biggestShare.Key];
×
1052
                                }
×
1053
                        }
×
1054

1055
                        // Duchies without de jure vassals should not be de jure part of any kingdom.
1056
                        var duchiesWithoutDeJureVassals = duchies.Except(duchiesWithDeJureVassals);
×
1057
                        foreach (var duchy in duchiesWithoutDeJureVassals) {
×
1058
                                Logger.Debug($"Duchy {duchy.Id} has no de jure vassals. Removing de jure liege.");
×
1059
                                duchy.DeJureLiege = null;
×
1060
                        }
×
1061

1062
                        Logger.IncrementProgress();
×
1063
                }
×
1064

1065
                private void SetDeJureEmpires(CultureCollection ck3Cultures, CharacterCollection ck3Characters, MapData ck3MapData, CK3LocDB ck3LocDB, Date ck3BookmarkDate) {
×
1066
                        Logger.Info("Setting de jure empires...");
×
1067
                        var deJureKingdoms = GetDeJureKingdoms();
×
1068
                        
1069
                        // Try to assign kingdoms to existing empires.
1070
                        foreach (var kingdom in deJureKingdoms) {
×
1071
                                var empireShares = new Dictionary<string, int>();
×
1072
                                var kingdomProvincesCount = 0;
×
1073
                                foreach (var county in kingdom.GetDeJureVassalsAndBelow("c").Values) {
×
1074
                                        var countyProvincesCount = county.CountyProvinceIds.Count();
×
1075
                                        kingdomProvincesCount += countyProvincesCount;
×
1076

1077
                                        var empireRealm = county.GetRealmOfRank(TitleRank.empire, ck3BookmarkDate);
×
1078
                                        if (empireRealm is null) {
×
1079
                                                continue;
×
1080
                                        }
1081

1082
                                        empireShares.TryGetValue(empireRealm.Id, out var currentCount);
×
1083
                                        empireShares[empireRealm.Id] = currentCount + countyProvincesCount;
×
1084
                                }
×
1085

1086
                                kingdom.DeJureLiege = null;
×
1087
                                if (empireShares.Count == 0) {
×
1088
                                        continue;
×
1089
                                }
1090

1091
                                (string empireId, int share) = empireShares.MaxBy(pair => pair.Value);
×
1092
                                // The potential de jure empire must hold at least 50% of the kingdom.
1093
                                if (share < (kingdomProvincesCount * 0.50)) {
×
1094
                                        continue;
×
1095
                                }
1096

1097
                                kingdom.DeJureLiege = this[empireId];
×
1098
                        }
×
1099

1100
                        // For kingdoms that still have no de jure empire, create empires based on dominant culture of the realms
1101
                        // holding land in that de jure kingdom.
1102
                        var removableEmpireIds = new HashSet<string>();
×
1103
                        var kingdomToDominantHeritagesDict = new Dictionary<string, ImmutableArray<Pillar>>();
×
1104
                        var heritageToEmpireDict = GetHeritageIdToExistingTitleDict();
×
1105
                        CreateEmpiresBasedOnDominantHeritages(deJureKingdoms, ck3Cultures, ck3Characters, removableEmpireIds, kingdomToDominantHeritagesDict, heritageToEmpireDict, ck3LocDB, ck3BookmarkDate);
×
1106
                        
1107
                        Logger.Debug("Building kingdom adjacencies dict...");
×
1108
                        // Create a cache of province IDs per kingdom.
1109
                        var provincesPerKingdomDict = deJureKingdoms
×
1110
                                .ToFrozenDictionary(
×
1111
                                        k => k.Id,
×
1112
                                        k => k.GetDeJureVassalsAndBelow("c").Values.SelectMany(c => c.CountyProvinceIds).ToFrozenSet()
×
1113
                                );
×
1114
                        var kingdomAdjacenciesByLand = deJureKingdoms.ToFrozenDictionary(k => k.Id, _ => new ConcurrentHashSet<string>());
×
1115
                        var kingdomAdjacenciesByWaterBody = deJureKingdoms.ToFrozenDictionary(k => k.Id, _ => new ConcurrentHashSet<string>());
×
1116
                        Parallel.ForEach(deJureKingdoms, kingdom => {
×
1117
                                FindKingdomsAdjacentToKingdom(ck3MapData, deJureKingdoms, kingdom.Id, provincesPerKingdomDict, kingdomAdjacenciesByLand, kingdomAdjacenciesByWaterBody);
×
1118
                        });
×
1119
                        
1120
                        SplitDisconnectedEmpires(kingdomAdjacenciesByLand, kingdomAdjacenciesByWaterBody, removableEmpireIds, kingdomToDominantHeritagesDict, heritageToEmpireDict, ck3LocDB, ck3BookmarkDate);
×
1121
                        
1122
                        SetEmpireCapitals(ck3BookmarkDate);
×
1123
                }
×
1124

1125
                private void CreateEmpiresBasedOnDominantHeritages(
1126
                        IReadOnlyCollection<Title> deJureKingdoms,
1127
                        CultureCollection ck3Cultures,
1128
                        CharacterCollection ck3Characters,
1129
                        HashSet<string> removableEmpireIds,
1130
                        Dictionary<string, ImmutableArray<Pillar>> kingdomToDominantHeritagesDict,
1131
                        Dictionary<string, Title> heritageToEmpireDict,
1132
                        CK3LocDB ck3LocDB,
1133
                        Date ck3BookmarkDate
1134
                ) {
×
1135
                        var kingdomsWithoutEmpire = deJureKingdoms
×
1136
                                .Where(k => k.DeJureLiege is null)
×
1137
                                .ToImmutableArray();
×
1138

1139
                        foreach (var kingdom in kingdomsWithoutEmpire) {
×
1140
                                var counties = kingdom.GetDeJureVassalsAndBelow("c").Values;
×
1141
                                
1142
                                // Get list of dominant heritages in the kingdom, in descending order.
1143
                                var dominantHeritages = counties
×
1144
                                        .Select(c => new { County = c, HolderId = c.GetHolderId(ck3BookmarkDate)})
×
1145
                                        .Select(x => new { x.County, Holder = ck3Characters.TryGetValue(x.HolderId, out var holder) ? holder : null})
×
1146
                                        .Select(x => new { x.County, CultureId = x.Holder?.GetCultureId(ck3BookmarkDate) })
×
1147
                                        .Where(x => x.CultureId is not null)
×
1148
                                        .Select(x => new { x.County, Culture = ck3Cultures.TryGetValue(x.CultureId!, out var culture) ? culture : null })
×
1149
                                        .Where(x => x.Culture is not null)
×
1150
                                        .Select(x => new { x.County, x.Culture!.Heritage })
×
1151
                                        .GroupBy(x => x.Heritage)
×
1152
                                        .OrderByDescending(g => g.Count())
×
1153
                                        .Select(g => g.Key)
×
1154
                                        .ToImmutableArray();
×
1155
                                if (dominantHeritages.Length == 0) {
×
1156
                                        if (kingdom.GetDeJureVassalsAndBelow("c").Count > 0) {
×
1157
                                                Logger.Warn($"Kingdom {kingdom.Id} has no dominant heritage!");
×
1158
                                        }
×
1159
                                        continue;
×
1160
                                }
1161
                                kingdomToDominantHeritagesDict[kingdom.Id] = dominantHeritages;
×
1162

1163
                                var dominantHeritage = dominantHeritages[0];
×
1164

1165
                                if (heritageToEmpireDict.TryGetValue(dominantHeritage.Id, out var empire)) {
×
1166
                                        kingdom.DeJureLiege = empire;
×
1167
                                } else {
×
1168
                                        // Create new de jure empire based on heritage.
1169
                                        var heritageEmpire = CreateEmpireForHeritage(dominantHeritage, ck3Cultures, ck3LocDB);
×
1170
                                        removableEmpireIds.Add(heritageEmpire.Id);
×
1171
                                        
1172
                                        kingdom.DeJureLiege = heritageEmpire;
×
1173
                                        heritageToEmpireDict[dominantHeritage.Id] = heritageEmpire;
×
1174
                                }
×
1175
                        }
×
1176
                }
×
1177

1178
                private static void FindKingdomsAdjacentToKingdom(
1179
                        MapData ck3MapData,
1180
                        ImmutableArray<Title> deJureKingdoms,
1181
                        string kingdomId, FrozenDictionary<string, FrozenSet<ulong>> provincesPerKingdomDict,
1182
                        FrozenDictionary<string, ConcurrentHashSet<string>> kingdomAdjacenciesByLand,
1183
                        FrozenDictionary<string, ConcurrentHashSet<string>> kingdomAdjacenciesByWaterBody)
1184
                {
×
1185
                        foreach (var otherKingdom in deJureKingdoms) {
×
1186
                                // Since this code is parallelized, make sure we don't check the same pair twice.
1187
                                // Also make sure we don't check the same kingdom against itself.
1188
                                if (kingdomId.CompareTo(otherKingdom.Id) >= 0) {
×
1189
                                        continue;
×
1190
                                }
1191
                                
1192
                                var kingdom1Provinces = provincesPerKingdomDict[kingdomId];
×
1193
                                var kingdom2Provinces = provincesPerKingdomDict[otherKingdom.Id];
×
1194
                                if (AreTitlesAdjacentByLand(kingdom1Provinces, kingdom2Provinces, ck3MapData)) {
×
1195
                                        kingdomAdjacenciesByLand[kingdomId].Add(otherKingdom.Id);
×
1196
                                        kingdomAdjacenciesByLand[otherKingdom.Id].Add(kingdomId);
×
1197
                                } else if (AreTitlesAdjacentByWaterBody(kingdom1Provinces, kingdom2Provinces, ck3MapData)) {
×
1198
                                        kingdomAdjacenciesByWaterBody[kingdomId].Add(otherKingdom.Id);
×
1199
                                        kingdomAdjacenciesByWaterBody[otherKingdom.Id].Add(kingdomId);
×
1200
                                }
×
1201
                        }
×
1202
                }
×
1203

1204
                private Dictionary<string, Title> GetHeritageIdToExistingTitleDict() {
×
1205
                        var heritageToEmpireDict = new Dictionary<string, Title>();
×
1206

1207
                        var reader = new BufferedReader(File.ReadAllText("configurables/heritage_empires_map.txt"));
×
1208
                        foreach (var (heritageId, empireId) in reader.GetAssignments()) {
×
1209
                                if (heritageToEmpireDict.ContainsKey(heritageId)) {
×
1210
                                        continue;
×
1211
                                }
1212
                                if (!TryGetValue(empireId, out var empire)) {
×
1213
                                        continue;
×
1214
                                }
1215
                                if (empire.Rank != TitleRank.empire) {
×
1216
                                        continue;
×
1217
                                }
1218
                                
1219
                                heritageToEmpireDict[heritageId] = empire;
×
1220
                                Logger.Debug($"Mapped heritage {heritageId} to empire {empireId}.");
×
1221
                        }
×
1222
                        
1223
                        return heritageToEmpireDict;
×
1224
                }
×
1225

1226
                private Title CreateEmpireForHeritage(Pillar heritage, CultureCollection ck3Cultures, CK3LocDB ck3LocDB) {
×
1227
                        var newEmpireId = $"e_IRTOCK3_heritage_{heritage.Id}";
×
1228
                        var newEmpire = Add(newEmpireId);
×
1229
                        var nameLocBlock = ck3LocDB.GetOrCreateLocBlock(newEmpire.Id);
×
1230
                        nameLocBlock[ConverterGlobals.PrimaryLanguage] = $"${heritage.Id}_name$ Empire";
×
1231
                        var adjectiveLocBlock = ck3LocDB.GetOrCreateLocBlock($"{newEmpire.Id}_adj");
×
1232
                        adjectiveLocBlock[ConverterGlobals.PrimaryLanguage] = $"${heritage.Id}_name$";
×
1233
                        newEmpire.HasDefiniteForm = true;
×
1234

1235
                        // Use color of one of the cultures as the empire color.
1236
                        var empireColor = ck3Cultures.First(c => c.Heritage == heritage).Color;
×
1237
                        newEmpire.Color1 = empireColor;
×
1238
                        
1239
                        return newEmpire;
×
1240
                }
×
1241

1242
                private void SplitDisconnectedEmpires(
1243
                        FrozenDictionary<string, ConcurrentHashSet<string>> kingdomAdjacenciesByLand,
1244
                        FrozenDictionary<string, ConcurrentHashSet<string>> kingdomAdjacenciesByWaterBody,
1245
                        HashSet<string> removableEmpireIds,
1246
                        Dictionary<string, ImmutableArray<Pillar>> kingdomToDominantHeritagesDict,
1247
                        Dictionary<string, Title> heritageToEmpireDict,
1248
                        CK3LocDB ck3LocDB,
1249
                        Date date
1250
                ) {
×
1251
                        Logger.Debug("Splitting disconnected empires...");
×
1252
                        
1253
                        // Combine kingdom adjacencies by land and water body into a single dictionary.
1254
                        var kingdomAdjacencies = new Dictionary<string, HashSet<string>>();
×
1255
                        foreach (var (kingdomId, adjacencies) in kingdomAdjacenciesByLand) {
×
1256
                                kingdomAdjacencies[kingdomId] = [..adjacencies];
×
1257
                        }
×
1258
                        foreach (var (kingdomId, adjacencies) in kingdomAdjacenciesByWaterBody) {
×
1259
                                if (!kingdomAdjacencies.TryGetValue(kingdomId, out var set)) {
×
1260
                                        set = [];
×
1261
                                        kingdomAdjacencies[kingdomId] = set;
×
1262
                                }
×
1263
                                set.UnionWith(adjacencies);
×
1264
                        }
×
1265
                        
1266
                        // If one separated kingdom is separated from the rest of its de jure empire, try to get the second dominant heritage in the kingdom.
1267
                        // If any neighboring kingdom has that heritage as dominant one, transfer the separated kingdom to the neighboring kingdom's empire.
1268
                        var disconnectedEmpiresDict = GetDictOfDisconnectedEmpires(kingdomAdjacencies, removableEmpireIds);
×
1269
                        if (disconnectedEmpiresDict.Count == 0) {
×
1270
                                return;
×
1271
                        }
1272
                        Logger.Debug("\tTransferring stranded kingdoms to neighboring empires...");
×
1273
                        foreach (var (empire, kingdomGroups) in disconnectedEmpiresDict) {
×
1274
                                var dissolvableGroups = kingdomGroups.Where(g => g.Count == 1).ToArray();
×
1275
                                foreach (var group in dissolvableGroups) {
×
1276
                                        var kingdom = group.First();
×
1277
                                        if (!kingdomToDominantHeritagesDict.TryGetValue(kingdom.Id, out var dominantHeritages)) {
×
1278
                                                continue;
×
1279
                                        }
1280
                                        if (dominantHeritages.Length < 2) {
×
1281
                                                continue;
×
1282
                                        }
1283
                                        
1284
                                        var adjacentEmpiresByLand = kingdomAdjacenciesByLand[kingdom.Id].Select(k => this[k].DeJureLiege)
×
1285
                                                .Where(e => e is not null)
×
1286
                                                .Select(e => e!)
×
1287
                                                .ToFrozenSet();
×
1288
                                        
1289
                                        // Try to find valid neighbor by land first, to reduce the number of exclaves.
1290
                                        Title? validNeighbor = null;
×
1291
                                        foreach (var secondaryHeritage in dominantHeritages.Skip(1)) {
×
1292
                                                if (!heritageToEmpireDict.TryGetValue(secondaryHeritage.Id, out var heritageEmpire)) {
×
1293
                                                        continue;
×
1294
                                                }
1295
                                                if (!adjacentEmpiresByLand.Contains(heritageEmpire)) {
×
1296
                                                        continue;
×
1297
                                                }
1298

1299
                                                validNeighbor = heritageEmpire;
×
1300
                                                Logger.Debug($"\t\tTransferring kingdom {kingdom.Id} from empire {empire.Id} to empire {validNeighbor.Id} neighboring by land.");
×
1301
                                                break;
×
1302
                                        }
1303
                                        
1304
                                        // If no valid neighbor by land, try to find valid neighbor by water.
1305
                                        if (validNeighbor is null) {
×
1306
                                                var adjacentEmpiresByWaterBody = kingdomAdjacenciesByWaterBody[kingdom.Id].Select(k => this[k].DeJureLiege)
×
1307
                                                        .Where(e => e is not null)
×
1308
                                                        .Select(e => e!)
×
1309
                                                        .ToFrozenSet();
×
1310
                                                
1311
                                                foreach (var secondaryHeritage in dominantHeritages.Skip(1)) {
×
1312
                                                        if (!heritageToEmpireDict.TryGetValue(secondaryHeritage.Id, out var heritageEmpire)) {
×
1313
                                                                continue;
×
1314
                                                        }
1315
                                                        if (!adjacentEmpiresByWaterBody.Contains(heritageEmpire)) {
×
1316
                                                                continue;
×
1317
                                                        }
1318

1319
                                                        validNeighbor = heritageEmpire;
×
1320
                                                        Logger.Debug($"\t\tTransferring kingdom {kingdom.Id} from empire {empire.Id} to empire {validNeighbor.Id} neighboring by water body.");
×
1321
                                                        break;
×
1322
                                                }
1323
                                        }
×
1324

1325
                                        if (validNeighbor is not null) {
×
1326
                                                kingdom.DeJureLiege = validNeighbor;
×
1327
                                        }
×
1328
                                }
×
1329
                        }        
×
1330
                        
1331
                        disconnectedEmpiresDict = GetDictOfDisconnectedEmpires(kingdomAdjacencies, removableEmpireIds);
×
1332
                        if (disconnectedEmpiresDict.Count == 0) {
×
1333
                                return;
×
1334
                        }
1335
                        Logger.Debug("\tCreating new empires for disconnected groups...");
×
1336
                        foreach (var (empire, groups) in disconnectedEmpiresDict) {
×
1337
                                // Keep the largest group as is, and create new empires based on most developed counties for the rest.
1338
                                var largestGroup = groups.MaxBy(g => g.Count);
×
1339
                                foreach (var group in groups) {
×
1340
                                        if (group == largestGroup) {
×
1341
                                                continue;
×
1342
                                        }
1343
                                        
1344
                                        var mostDevelopedCounty = group
×
1345
                                                .SelectMany(k => k.GetDeJureVassalsAndBelow("c").Values)
×
1346
                                                .MaxBy(c => c.GetOwnOrInheritedDevelopmentLevel(date));
×
1347
                                        if (mostDevelopedCounty is null) {
×
1348
                                                continue;
×
1349
                                        }
1350
                                        
1351
                                        string newEmpireId = $"e_IRTOCK3_from_{mostDevelopedCounty.Id}";
×
1352
                                        var newEmpire = Add(newEmpireId);
×
1353
                                        newEmpire.Color1 = mostDevelopedCounty.Color1;
×
1354
                                        newEmpire.CapitalCounty = mostDevelopedCounty;
×
1355
                                        newEmpire.HasDefiniteForm = false;
×
1356
                                        
1357
                                        var empireNameLoc = ck3LocDB.GetOrCreateLocBlock(newEmpireId);
×
1358
                                        empireNameLoc.ModifyForEveryLanguage(
×
1359
                                                (orig, language) => $"${mostDevelopedCounty.Id}$"
×
1360
                                        );
×
1361
                                        
1362
                                        var empireAdjLoc = ck3LocDB.GetOrCreateLocBlock(newEmpireId + "_adj");
×
1363
                                        empireAdjLoc.ModifyForEveryLanguage(
×
1364
                                                (orig, language) => $"${mostDevelopedCounty.Id}_adj$"
×
1365
                                        );
×
1366

1367
                                        foreach (var kingdom in group) {
×
1368
                                                kingdom.DeJureLiege = newEmpire;
×
1369
                                        }
×
1370
                                        
1371
                                        Logger.Debug($"\t\tCreated new empire {newEmpire.Id} for group {string.Join(',', group.Select(k => k.Id))}.");
×
1372
                                }
×
1373
                        }
×
1374
                        
1375
                        disconnectedEmpiresDict = GetDictOfDisconnectedEmpires(kingdomAdjacencies, removableEmpireIds);
×
1376
                        if (disconnectedEmpiresDict.Count > 0) {
×
1377
                                Logger.Warn("Failed to split some disconnected empires: " + string.Join(", ", disconnectedEmpiresDict.Keys.Select(e => e.Id)));
×
1378
                        }
×
1379
                }
×
1380

1381
                private Dictionary<Title, List<HashSet<Title>>> GetDictOfDisconnectedEmpires(
1382
                        Dictionary<string, HashSet<string>> kingdomAdjacencies,
1383
                        IReadOnlySet<string> removableEmpireIds
1384
                ) {
×
1385
                        var dictToReturn = new Dictionary<Title, List<HashSet<Title>>>();
×
1386
                        
1387
                        foreach (var empire in this.Where(t => t.Rank == TitleRank.empire)) {
×
1388
                                IEnumerable<Title> deJureKingdoms = empire.GetDeJureVassalsAndBelow("k").Values;
×
1389

1390
                                // Unassign de jure kingdoms that have no de jure land themselves.
1391
                                var deJureKingdomsWithoutLand =
×
1392
                                        deJureKingdoms.Where(k => k.GetDeJureVassalsAndBelow("c").Count == 0).ToFrozenSet();
×
1393
                                foreach (var deJureKingdomWithLand in deJureKingdomsWithoutLand) {
×
1394
                                        deJureKingdomWithLand.DeJureLiege = null;
×
1395
                                }
×
1396

1397
                                deJureKingdoms = deJureKingdoms.Except(deJureKingdomsWithoutLand).ToArray();
×
1398

1399
                                if (!deJureKingdoms.Any()) {
×
1400
                                        if (removableEmpireIds.Contains(empire.Id)) {
×
1401
                                                Remove(empire.Id);
×
1402
                                        }
×
1403

1404
                                        continue;
×
1405
                                }
1406

1407
                                // Group the kingdoms into contiguous groups.
1408
                                var kingdomGroups = new List<HashSet<Title>>();
×
1409
                                foreach (var kingdom in deJureKingdoms) {
×
1410
                                        var added = false;
×
1411
                                        List<HashSet<Title>> connectedGroups = [];
×
1412

1413
                                        foreach (var group in kingdomGroups) {
×
1414
                                                if (group.Any(k => kingdomAdjacencies.TryGetValue(k.Id, out var adjacencies) && adjacencies.Contains(kingdom.Id))) {
×
1415
                                                        group.Add(kingdom);
×
1416
                                                        connectedGroups.Add(group);
×
1417

1418
                                                        added = true;
×
1419
                                                }
×
1420
                                        }
×
1421

1422
                                        // If the kingdom is adjacent to multiple groups, merge them.
1423
                                        if (connectedGroups.Count > 1) {
×
1424
                                                var mergedGroup = new HashSet<Title>();
×
1425
                                                foreach (var group in connectedGroups) {
×
1426
                                                        mergedGroup.UnionWith(group);
×
1427
                                                        kingdomGroups.Remove(group);
×
1428
                                                }
×
1429

1430
                                                mergedGroup.Add(kingdom);
×
1431
                                                kingdomGroups.Add(mergedGroup);
×
1432
                                        }
×
1433

1434
                                        if (!added) {
×
1435
                                                kingdomGroups.Add([kingdom]);
×
1436
                                        }
×
1437
                                }
×
1438

1439
                                if (kingdomGroups.Count <= 1) {
×
1440
                                        continue;
×
1441
                                }
1442

1443
                                Logger.Debug($"\tEmpire {empire.Id} has {kingdomGroups.Count} disconnected groups of kingdoms: {string.Join(" ; ", kingdomGroups.Select(g => string.Join(',', g.Select(k => k.Id))))}");
×
1444
                                dictToReturn[empire] = kingdomGroups;
×
1445
                        }
×
1446

1447
                        return dictToReturn;
×
1448
                }
×
1449

1450
                private static bool AreTitlesAdjacent(FrozenSet<ulong> title1ProvinceIds, FrozenSet<ulong> title2ProvinceIds, MapData mapData) {
×
1451
                        return mapData.AreProvinceGroupsAdjacent(title1ProvinceIds, title2ProvinceIds);
×
1452
                }
×
1453
                private static bool AreTitlesAdjacentByLand(FrozenSet<ulong> title1ProvinceIds, FrozenSet<ulong> title2ProvinceIds, MapData mapData) {
×
1454
                        return mapData.AreProvinceGroupsAdjacentByLand(title1ProvinceIds, title2ProvinceIds);
×
1455
                }
×
1456
                private static bool AreTitlesAdjacentByWaterBody(FrozenSet<ulong> title1ProvinceIds, FrozenSet<ulong> title2ProvinceIds, MapData mapData) {
×
1457
                        return mapData.AreProvinceGroupsConnectedByWaterBody(title1ProvinceIds, title2ProvinceIds);
×
1458
                }
×
1459

1460
                private void SetEmpireCapitals(Date ck3BookmarkDate) {
×
1461
                        // Make sure every empire's capital is within the empire's de jure land.
1462
                        Logger.Info("Setting empire capitals...");
×
1463
                        foreach (var empire in this.Where(t => t.Rank == TitleRank.empire)) {
×
1464
                                var deJureCounties = empire.GetDeJureVassalsAndBelow("c").Values;
×
1465
                                
1466
                                // If the empire already has a set capital, and it's within the de jure land, keep it.
1467
                                if (empire.CapitalCounty is not null && deJureCounties.Contains(empire.CapitalCounty)) {
×
1468
                                        continue;
×
1469
                                }
1470
                                
1471
                                // Try to use most developed county among the de jure kingdom capitals.
1472
                                var deJureKingdoms = empire.GetDeJureVassalsAndBelow("k").Values;
×
1473
                                var mostDevelopedCounty = deJureKingdoms
×
1474
                                        .Select(k => k.CapitalCounty)
×
1475
                                        .Where(c => c is not null)
×
1476
                                        .MaxBy(c => c!.GetOwnOrInheritedDevelopmentLevel(ck3BookmarkDate));
×
1477
                                if (mostDevelopedCounty is not null) {
×
1478
                                        empire.CapitalCounty = mostDevelopedCounty;
×
1479
                                        continue;
×
1480
                                }
1481
                                
1482
                                // Otherwise, use the most developed county among the de jure empire's counties.
1483
                                mostDevelopedCounty = deJureCounties
×
1484
                                        .MaxBy(c => c.GetOwnOrInheritedDevelopmentLevel(ck3BookmarkDate));
×
1485
                                if (mostDevelopedCounty is not null) {
×
1486
                                        empire.CapitalCounty = mostDevelopedCounty;
×
1487
                                }
×
1488
                        }
×
1489
                }
×
1490

1491
                public void SetDeJureKingdomsAndEmpires(Date ck3BookmarkDate, CultureCollection ck3Cultures, CharacterCollection ck3Characters, MapData ck3MapData, CK3LocDB ck3LocDB) {
×
1492
                        SetDeJureKingdoms(ck3LocDB, ck3BookmarkDate);
×
1493
                        SetDeJureEmpires(ck3Cultures, ck3Characters, ck3MapData, ck3LocDB, ck3BookmarkDate);
×
1494
                }
×
1495

1496
                private HashSet<string> GetCountyHolderIds(Date date) {
×
1497
                        var countyHoldersCache = new HashSet<string>();
×
1498
                        foreach (var county in this.Where(t => t.Rank == TitleRank.county)) {
×
1499
                                var holderId = county.GetHolderId(date);
×
1500
                                if (holderId != "0") {
×
1501
                                        countyHoldersCache.Add(holderId);
×
1502
                                }
×
1503
                        }
×
1504

1505
                        return countyHoldersCache;
×
1506
                }
×
1507

1508
                public void ImportDevelopmentFromImperator(ProvinceCollection ck3Provinces, Date date, double irCivilizationWorth) {
4✔
1509
                        static bool IsCountyOutsideImperatorMap(Title county, IReadOnlyDictionary<string, int> irProvsPerCounty) {
6✔
1510
                                return irProvsPerCounty[county.Id] == 0;
6✔
1511
                        }
6✔
1512

1513
                        double CalculateCountyDevelopment(Title county) {
5✔
1514
                                double dev = 0;
5✔
1515
                                IEnumerable<ulong> countyProvinceIds = county.CountyProvinceIds;
5✔
1516
                                int provsCount = 0;
5✔
1517
                                foreach (var ck3ProvId in countyProvinceIds) {
33✔
1518
                                        if (!ck3Provinces.TryGetValue(ck3ProvId, out var ck3Province)) {
6!
1519
                                                Logger.Warn($"CK3 province {ck3ProvId} not found!");
×
1520
                                                continue;
×
1521
                                        }
1522
                                        var sourceProvinces = ck3Province.ImperatorProvinces;
6✔
1523
                                        if (sourceProvinces.Count == 0) {
6!
1524
                                                continue;
×
1525
                                        }
1526
                                        ++provsCount;
6✔
1527
                                        
1528
                                        var devFromProvince = sourceProvinces.Average(srcProv => srcProv.CivilizationValue);
12✔
1529
                                        dev += devFromProvince;
6✔
1530
                                }
6✔
1531

1532
                                dev = Math.Max(0, dev - Math.Sqrt(dev));
5✔
1533
                                if (provsCount > 0) {
10✔
1534
                                        dev /= provsCount;
5✔
1535
                                }
5✔
1536
                                dev *= irCivilizationWorth;
5✔
1537
                                return dev;
5✔
1538
                        }
5✔
1539

1540
                        Logger.Info("Importing development from Imperator...");
4✔
1541

1542
                        var counties = this.Where(t => t.Rank == TitleRank.county).ToArray();
16✔
1543
                        var irProvsPerCounty = GetIRProvsPerCounty(ck3Provinces, counties);
4✔
1544

1545
                        foreach (var county in counties) {
30✔
1546
                                if (IsCountyOutsideImperatorMap(county, irProvsPerCounty)) {
7✔
1547
                                        // Don't change development for counties outside of Imperator map.
1548
                                        continue;
1✔
1549
                                }
1550

1551
                                double dev = CalculateCountyDevelopment(county);
5✔
1552

1553
                                county.History.Fields.Remove("development_level");
5✔
1554
                                county.History.AddFieldValue(date, "development_level", "change_development_level", (int)dev);
5✔
1555
                        }
5✔
1556
                        
1557
                        DistributeExcessDevelopment(date);
4✔
1558

1559
                        Logger.IncrementProgress();
4✔
1560
                        return;
4✔
1561

1562
                        static Dictionary<string, int> GetIRProvsPerCounty(ProvinceCollection ck3Provinces, IEnumerable<Title> counties) {
4✔
1563
                                Dictionary<string, int> irProvsPerCounty = [];
4✔
1564
                                foreach (var county in counties) {
30✔
1565
                                        HashSet<ulong> imperatorProvs = [];
6✔
1566
                                        foreach (ulong ck3ProvId in county.CountyProvinceIds) {
36✔
1567
                                                if (!ck3Provinces.TryGetValue(ck3ProvId, out var ck3Province)) {
6!
1568
                                                        Logger.Warn($"CK3 province {ck3ProvId} not found!");
×
1569
                                                        continue;
×
1570
                                                }
1571

1572
                                                var sourceProvinces = ck3Province.ImperatorProvinces;
6✔
1573
                                                foreach (var irProvince in sourceProvinces) {
36✔
1574
                                                        imperatorProvs.Add(irProvince.Id);
6✔
1575
                                                }
6✔
1576
                                        }
6✔
1577

1578
                                        irProvsPerCounty[county.Id] = imperatorProvs.Count;
6✔
1579
                                }
6✔
1580

1581
                                return irProvsPerCounty;
4✔
1582
                        }
4✔
1583
                }
4✔
1584

1585
                private void DistributeExcessDevelopment(Date date) {
4✔
1586
                        var topRealms = this
4✔
1587
                                .Where(t => t.Rank > TitleRank.county && t.GetHolderId(date) != "0" && t.GetDeFactoLiege(date) is null)
12!
1588
                                .ToArray();
4✔
1589

1590
                        // For every realm, get list of counties with over 100 development.
1591
                        // Distribute the excess development to the realm's least developed counties.
1592
                        foreach (var realm in topRealms) {
12!
1593
                                var realmCounties = realm.GetDeFactoVassalsAndBelow(date, "c").Values
×
1594
                                        .Select(c => new { County = c, Development = c.GetOwnOrInheritedDevelopmentLevel(date) })
×
1595
                                        .Where(c => c.Development.HasValue)
×
1596
                                        .Select(c => new { c.County, Development = c.Development!.Value })
×
1597
                                        .ToArray();
×
1598
                                var excessDevCounties = realmCounties
×
1599
                                        .Where(c => c.Development > 100)
×
1600
                                        .OrderByDescending(c => c.Development)
×
1601
                                        .ToArray();
×
1602
                                if (excessDevCounties.Length == 0) {
×
1603
                                        continue;
×
1604
                                }
1605

1606
                                var leastDevCounties = realmCounties
×
1607
                                        .Where(c => c.Development < 100)
×
1608
                                        .OrderBy(c => c.Development)
×
1609
                                        .Select(c => c.County)
×
1610
                                        .ToList();
×
1611
                                if (leastDevCounties.Count == 0) {
×
1612
                                        continue;
×
1613
                                }
1614
                                
1615
                                var excessDevSum = excessDevCounties.Sum(c => c.Development - 100);
×
1616
                                Logger.Debug($"Top realm {realm.Id} has {excessDevSum} excess development to distribute among {leastDevCounties.Count} counties.");
×
1617

1618
                                // Now that we've calculated the excess dev, we can cap the county dev at 100.
1619
                                foreach (var excessDevCounty in excessDevCounties) {
×
1620
                                        excessDevCounty.County.SetDevelopmentLevel(100, date);
×
1621
                                }
×
1622

1623
                                while (excessDevSum > 0 && leastDevCounties.Count > 0) {
×
1624
                                        var devPerCounty = excessDevSum / leastDevCounties.Count;
×
1625
                                        foreach (var county in leastDevCounties.ToArray()) {
×
1626
                                                var currentDev = county.GetOwnOrInheritedDevelopmentLevel(date) ?? 0;
×
1627
                                                var devToAdd = Math.Max(devPerCounty, 100 - currentDev);
×
1628
                                                var newDevValue = currentDev + devToAdd;
×
1629

1630
                                                county.SetDevelopmentLevel(newDevValue, date);
×
1631
                                                excessDevSum -= devToAdd;
×
1632
                                                if (newDevValue >= 100) {
×
1633
                                                        leastDevCounties.Remove(county);
×
1634
                                                }
×
1635
                                        }
×
1636
                                }
×
1637
                        }
×
1638
                }
4✔
1639
        
1640
                /// <summary>
1641
                /// Import Imperator officials as council members and courtiers.
1642
                /// https://imperator.paradoxwikis.com/Position
1643
                /// https://ck3.paradoxwikis.com/Council
1644
                /// https://ck3.paradoxwikis.com/Court#Court_positions
1645
                /// </summary>
1646
                public void ImportImperatorGovernmentOffices(ICollection<OfficeJob> irOfficeJobs, ReligionCollection religionCollection, Date irSaveDate) {
×
1647
                        Logger.Info("Converting government offices...");
×
1648
                        var titlesFromImperator = GetCountriesImportedFromImperator();
×
1649
                        
1650
                        var councilPositionToSourcesDict = new Dictionary<string, string[]> {
×
1651
                                ["councillor_court_chaplain"] = ["office_augur", "office_pontifex", "office_high_priest_monarchy", "office_high_priest", "office_wise_person"],
×
1652
                                ["councillor_chancellor"] = ["office_censor", "office_foreign_minister", "office_arbitrator", "office_elder"],
×
1653
                                ["councillor_steward"] = ["office_praetor", "office_magistrate", "office_steward", "office_tribune_of_the_treasury"],
×
1654
                                ["councillor_marshal"] = ["office_tribune_of_the_soldiers", "office_marshal", "office_master_of_the_guard", "office_warchief", "office_bodyguard"],
×
1655
                                ["councillor_spymaster"] = [], // No equivalents found in Imperator.
×
1656
                        };
×
1657
                        
1658
                        // Court positions.
1659
                        var courtPositionToSourcesDict = new Dictionary<string, string[]> {
×
1660
                                ["bodyguard_court_position"] = ["office_master_of_the_guard", "office_bodyguard"],
×
1661
                                ["court_physician_court_position"] = ["office_physician", "office_republic_physician", "office_apothecary"],
×
1662
                                ["court_tutor_court_position"] = ["office_royal_tutor"],
×
1663
                                ["chronicler_court_position"] = ["office_philosopher"], // From I:R wiki: "supervises libraries and the gathering and protection of knowledge"
×
1664
                                ["cave_hermit_court_position"] = ["office_wise_person"]
×
1665
                        };
×
1666

1667
                        string[] ignoredOfficeTypes = ["office_plebeian_aedile"];
×
1668

1669
                        // Log all unhandled office types.
1670
                        var irOfficeTypesFromSave = irOfficeJobs.Select(j => j.OfficeType).ToFrozenSet();
×
1671
                        var handledOfficeTypes = councilPositionToSourcesDict.Values
×
1672
                                .SelectMany(v => v)
×
1673
                                .Concat(courtPositionToSourcesDict.Values.SelectMany(v => v))
×
1674
                                .Concat(ignoredOfficeTypes)
×
1675
                                .ToFrozenSet();
×
1676
                        var unmappedOfficeTypes = irOfficeTypesFromSave
×
1677
                                .Where(officeType => !handledOfficeTypes.Contains(officeType)).ToArray();
×
1678
                        if (unmappedOfficeTypes.Length > 0) {
×
1679
                                Logger.Error($"Unmapped office types: {string.Join(", ", unmappedOfficeTypes)}");
×
1680
                        }
×
1681

1682
                        foreach (var title in titlesFromImperator) {
×
1683
                                var country = title.ImperatorCountry!;
×
1684
                                var ck3Ruler = country.Monarch?.CK3Character;
×
1685
                                if (ck3Ruler is null) {
×
1686
                                        continue;
×
1687
                                }
1688
                                
1689
                                // Make sure the ruler actually holds something in CK3.
1690
                                if (this.All(t => t.GetHolderId(irSaveDate) != ck3Ruler.Id)) {
×
1691
                                        continue;
×
1692
                                }
1693
                                
1694
                                var convertibleJobs = irOfficeJobs.Where(j => j.CountryId == country.Id).ToList();
×
1695
                                if (convertibleJobs.Count == 0) {
×
1696
                                        continue;
×
1697
                                }
1698
                                
1699
                                var alreadyEmployedCharacters = new HashSet<string>();
×
1700
                                title.AppointCouncilMembersFromImperator(religionCollection, councilPositionToSourcesDict, convertibleJobs, alreadyEmployedCharacters, ck3Ruler, irSaveDate);
×
1701
                                title.AppointCourtierPositionsFromImperator(courtPositionToSourcesDict, convertibleJobs, alreadyEmployedCharacters, ck3Ruler, irSaveDate);
×
1702
                        }
×
1703
                }
×
1704

1705
                public IEnumerable<Title> GetCountriesImportedFromImperator() {
1✔
1706
                        return this.Where(t => t.ImperatorCountry is not null);
16✔
1707
                }
1✔
1708

1709
                public IReadOnlyCollection<Title> GetDeJureDuchies() => this
25✔
1710
                        .Where(t => t is {Rank: TitleRank.duchy, DeJureVassals.Count: > 0})
131✔
1711
                        .ToImmutableArray();
25✔
1712
                
1713
                public ImmutableArray<Title> GetDeJureKingdoms() => this
×
1714
                        .Where(t => t is {Rank: TitleRank.kingdom, DeJureVassals.Count: > 0})
×
1715
                        .ToImmutableArray();
×
1716
                
1717
                private FrozenSet<Color> UsedColors => this
3✔
1718
                        .Select(t => t.Color1)
106✔
1719
                        .Where(c => c is not null)
106✔
1720
                        .Cast<Color>()
3✔
1721
                        .ToFrozenSet();
3✔
1722
                public bool IsColorUsed(Color color) {
×
1723
                        return UsedColors.Contains(color);
×
1724
                }
×
1725
                public Color GetDerivedColor(Color baseColor) {
3✔
1726
                        FrozenSet<Color> usedHueColors = UsedColors.Where(c => Math.Abs(c.H - baseColor.H) < 0.001).ToFrozenSet();
106✔
1727

1728
                        for (double v = 0.05; v <= 1; v += 0.02) {
155✔
1729
                                var newColor = new Color(baseColor.H, baseColor.S, v);
51✔
1730
                                if (usedHueColors.Contains(newColor)) {
100✔
1731
                                        continue;
49✔
1732
                                }
1733
                                return newColor;
2✔
1734
                        }
1735

1736
                        Logger.Warn($"Couldn't generate new color from base {baseColor.OutputRgb()}");
1✔
1737
                        return baseColor;
1✔
1738
                }
3✔
1739

1740
                private readonly HistoryFactory titleHistoryFactory = new HistoryFactory.HistoryFactoryBuilder()
212✔
1741
                        .WithSimpleField("holder", new OrderedSet<string> { "holder", "holder_ignore_head_of_faith_requirement" }, initialValue: null)
212✔
1742
                        .WithSimpleField("government", "government", initialValue: null)
212✔
1743
                        .WithSimpleField("liege", "liege", initialValue: null)
212✔
1744
                        .WithSimpleField("development_level", "change_development_level", initialValue: null)
212✔
1745
                        .WithSimpleField("succession_laws", "succession_laws", new SortedSet<string>())
212✔
1746
                        .Build();
212✔
1747

1748
                public void LoadHistory(Configuration config, ModFilesystem ck3ModFS) {
2✔
1749
                        var ck3BookmarkDate = config.CK3BookmarkDate;
2✔
1750

1751
                        int loadedHistoriesCount = 0;
2✔
1752

1753
                        var titlesHistoryParser = new Parser();
2✔
1754
                        titlesHistoryParser.RegisterRegex(Regexes.TitleId, (reader, titleName) => {
6✔
1755
                                var historyItem = reader.GetStringOfItem().ToString();
4✔
1756
                                if (!historyItem.Contains('{')) {
4!
1757
                                        return;
×
1758
                                }
2✔
1759

2✔
1760
                                if (!TryGetValue(titleName, out var title)) {
6✔
1761
                                        return;
2✔
1762
                                }
2✔
1763

2✔
1764
                                var tempReader = new BufferedReader(historyItem);
2✔
1765

2✔
1766
                                titleHistoryFactory.UpdateHistory(title.History, tempReader);
2✔
1767
                                ++loadedHistoriesCount;
2✔
1768
                        });
6✔
1769
                        titlesHistoryParser.RegisterRegex(CommonRegexes.Catchall, ParserHelpers.IgnoreAndLogItem);
2✔
1770

1771
                        Logger.Info("Parsing title history...");
2✔
1772
                        titlesHistoryParser.ParseGameFolder("history/titles", ck3ModFS, "txt", recursive: true, logFilePaths: true);
2✔
1773
                        Logger.Info($"Loaded {loadedHistoriesCount} title histories.");
2✔
1774

1775
                        // Add vanilla development to counties
1776
                        // Assign development level directly to each county for better reliability, then remove it from duchies and above.
1777
                        foreach (var county in Counties) {
6!
1778
                                var inheritedDev = county.GetOwnOrInheritedDevelopmentLevel(ck3BookmarkDate);
×
1779
                                county.History.Fields.Remove("development_level");
×
1780
                                county.SetDevelopmentLevel(inheritedDev ?? 0, ck3BookmarkDate);
×
1781
                        }
×
1782
                        foreach (var title in this.Where(t => t.Rank > TitleRank.county)) {
14✔
1783
                                title.History.Fields.Remove("development_level");
2✔
1784
                        }
2✔
1785

1786
                        // Remove history entries past the bookmark date.
1787
                        foreach (var title in this) {
12✔
1788
                                title.RemoveHistoryPastDate(ck3BookmarkDate);
2✔
1789
                        }
2✔
1790
                }
2✔
1791

1792
                public void LoadCulturalNamesFromConfigurables() {
×
1793
                        const string filePath = "configurables/cultural_title_names.txt";
1794
                        Logger.Info($"Loading cultural title names from \"{filePath}\"...");
×
1795

1796
                        var parser = new Parser();
×
1797
                        parser.RegisterRegex(CommonRegexes.String, (reader, titleId) => {
×
1798
                                var nameListToLocKeyDict = reader.GetAssignmentsAsDict();
×
1799

×
1800
                                if (!TryGetValue(titleId, out var title)) {
×
1801
                                        return;
×
1802
                                }
×
1803
                                if (title.CulturalNames is null) {
×
1804
                                        title.CulturalNames = nameListToLocKeyDict;
×
1805
                                } else {
×
1806
                                        foreach (var (nameList, locKey) in nameListToLocKeyDict) {
×
1807
                                                title.CulturalNames[nameList] = locKey;
×
1808
                                        }
×
1809
                                }
×
1810
                        });
×
1811
                        parser.IgnoreAndLogUnregisteredItems();
×
1812
                        parser.ParseFile(filePath);
×
1813
                }
×
1814

1815
                internal void SetCoatsOfArms(CoaMapper coaMapper) {
×
1816
                        Logger.Info("Setting coats of arms for CK3 titles...");
×
1817
                        
1818
                        int counter = 0;
×
1819
                        foreach (var title in this) {
×
1820
                                var coa = coaMapper.GetCoaForFlagName(title.Id, warnIfMissing: false);
×
1821
                                if (coa is null) {
×
1822
                                        continue;
×
1823
                                }
1824
                                
1825
                                title.CoA = coa;
×
1826
                                ++counter;
×
1827
                        }
×
1828
                        
1829
                        Logger.Debug($"Set coats of arms for {counter} CK3 titles.");
×
1830
                }
×
1831

1832
                public void RemoveLiegeEntriesFromReligiousHeadHistory(ReligionCollection religions) {
×
1833
                        var religiousHeadTitleIds = religions.Faiths
×
1834
                                .Select(f => f.ReligiousHeadTitleId)
×
1835
                                .Distinct()
×
1836
                                .Where(id => id is not null)
×
1837
                                .Select(id => id!);
×
1838
                        foreach (var religiousHeadTitleId in religiousHeadTitleIds) {
×
1839
                                if (!TryGetValue(religiousHeadTitleId, out var religiousHeadTitle)) {
×
1840
                                        continue;
×
1841
                                }
1842
                                
1843
                                religiousHeadTitle.History.Fields.Remove("liege");
×
1844
                        }
×
1845
                }
×
1846
        }
1847
}
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

© 2025 Coveralls, Inc