Skip to content

Commit 1a902c2

Browse files
authored
Fix development for CK3 bookmark dates other than the save's date (#2740) #patch
closes #2684
1 parent 23020ec commit 1a902c2

File tree

8 files changed

+163
-28
lines changed

8 files changed

+163
-28
lines changed

ImperatorToCK3.UnitTests/CK3/Titles/LandedTitlesTests.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -558,17 +558,15 @@ public void HistoryIsLoadedFromDatedBlocks() {
558558
var date = new Date(867, 1, 1);
559559
var config = new Configuration {
560560
CK3BookmarkDate = date,
561-
CK3Path = "TestFiles/LandedTitlesTests/CK3"
562561
};
563-
var ck3ModFS = new ModFilesystem(Path.Combine(config.CK3Path, "game"), new List<Mod>());
564562

565563
var titles = new Title.LandedTitles();
566564
var title = titles.Add("k_greece");
567565

568566
titles.LoadHistory(config, ck3ModFS);
569567

570568
Assert.Equal("420", title.GetHolderId(date));
571-
Assert.Equal(20, title.GetDevelopmentLevel(date));
569+
Assert.Equal("e_persia", title.GetLiegeId(date));
572570
}
573571

574572
[Fact]

ImperatorToCK3.UnitTests/CK3/Titles/TitleTests.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,11 @@ public void DevelopmentLevelCanBeInherited() {
237237

238238
county.SetDevelopmentLevel(4, date);
239239
Assert.Equal(4, county.GetOwnOrInheritedDevelopmentLevel(date));
240+
241+
// Development level set for de jure liege at a later date overrides the county's previously set level.
242+
Date laterDate = date.ChangeByYears(1);
243+
empire.SetDevelopmentLevel(12, laterDate);
244+
Assert.Equal(12, county.GetOwnOrInheritedDevelopmentLevel(laterDate));
240245
}
241246

242247
[Fact]
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
using commonItems;
2+
using commonItems.Collections;
3+
using ImperatorToCK3.CommonUtils;
4+
using System.Collections.Generic;
5+
using Xunit;
6+
7+
namespace ImperatorToCK3.UnitTests.CommonUtils;
8+
9+
public class IHistoryFieldTests {
10+
[Fact]
11+
public void ReturnsLatestEntryOnOrBeforeGivenDate() {
12+
var field = CreateField();
13+
14+
field.AddEntryToHistory(null, "setter", "initValue");
15+
field.AddEntryToHistory(new Date(100, 1, 1), "setter", "first");
16+
field.AddEntryToHistory(new Date(200, 1, 1), "setter", "second");
17+
field.AddEntryToHistory(new Date(200, 1, 1), "setter", "second-late");
18+
19+
var result = field.GetLastEntryWithDate(new Date(200, 1, 1));
20+
21+
Assert.Equal(new Date(200, 1, 1), result.Key);
22+
Assert.Equal("second-late", result.Value);
23+
}
24+
25+
[Fact]
26+
public void ReturnsEntryFromMostRecentEarlierDateWhenExactDateMissing() {
27+
var field = CreateField();
28+
29+
field.AddEntryToHistory(new Date(100, 1, 1), "setter", "first");
30+
field.AddEntryToHistory(new Date(150, 1, 1), "setter", "second");
31+
32+
var result = field.GetLastEntryWithDate(new Date(140, 1, 1));
33+
34+
Assert.Equal(new Date(100, 1, 1), result.Key);
35+
Assert.Equal("first", result.Value);
36+
}
37+
38+
[Fact]
39+
public void FallsBackToLastInitialEntryWhenNoEarlierDatedEntriesExist() {
40+
var field = CreateField();
41+
42+
field.AddEntryToHistory(null, "setter", "initial1");
43+
field.AddEntryToHistory(null, "setter", "initial2");
44+
field.AddEntryToHistory(new Date(200, 1, 1), "setter", "future");
45+
46+
var result = field.GetLastEntryWithDate(new Date(150, 1, 1));
47+
48+
Assert.Null(result.Key);
49+
var entry = Assert.IsType<KeyValuePair<string, object>>(result.Value);
50+
Assert.Equal("setter", entry.Key);
51+
Assert.Equal("initial2", entry.Value);
52+
}
53+
54+
[Fact]
55+
public void ReturnsNullPairWhenHistoryEmpty() {
56+
var field = CreateField();
57+
58+
var result = field.GetLastEntryWithDate(new Date(50, 1, 1));
59+
60+
Assert.Null(result.Key);
61+
Assert.Null(result.Value);
62+
}
63+
64+
[Fact]
65+
public void ReturnsLastInitialEntryWhenDateIsNull() {
66+
var field = CreateField();
67+
68+
field.AddEntryToHistory(null, "setter", "initial1");
69+
field.AddEntryToHistory(null, "setter", "initial2");
70+
field.AddEntryToHistory(new Date(100, 1, 1), "setter", "dated");
71+
72+
var result = field.GetLastEntryWithDate(null);
73+
74+
Assert.Null(result.Key);
75+
var entry = Assert.IsType<KeyValuePair<string, object>>(result.Value);
76+
Assert.Equal("setter", entry.Key);
77+
Assert.Equal("initial2", entry.Value);
78+
}
79+
80+
private static IHistoryField CreateField() => new SimpleHistoryField(
81+
fieldName: "field",
82+
setterKeywords: new OrderedSet<string> { "setter" },
83+
initialValue: null
84+
);
85+
}

ImperatorToCK3.UnitTests/Imperator/Families/FamilyTests.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using commonItems;
1+
using AwesomeAssertions;
2+
using commonItems;
23
using ImperatorToCK3.Imperator.Families;
34
using System;
45
using System.Collections.Generic;
@@ -59,6 +60,8 @@ public void LinkingNullMemberIsLogged() {
5960

6061
[Fact]
6162
public void IgnoredTokensAreSaved() {
63+
Family.IgnoredTokens.Clear(); // Ensure no bleed-over from other tests.
64+
6265
var reader1 = new BufferedReader("= { culture=paradoxian ignoredKeyword1=something ignoredKeyword2={} }");
6366
var reader2 = new BufferedReader("= { ignoredKeyword1=stuff ignoredKeyword3=stuff }");
6467
_ = Family.Parse(reader1, 1);
@@ -67,6 +70,6 @@ public void IgnoredTokensAreSaved() {
6770
var expectedIgnoredTokens = new HashSet<string> {
6871
"ignoredKeyword1", "ignoredKeyword2", "ignoredKeyword3"
6972
};
70-
Assert.True(Family.IgnoredTokens.SetEquals(expectedIgnoredTokens));
73+
Family.IgnoredTokens.Should().BeEquivalentTo(expectedIgnoredTokens);
7174
}
7275
}

ImperatorToCK3/CK3/Titles/LandedTitles.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1767,10 +1767,14 @@ public void LoadHistory(Configuration config, ModFilesystem ck3ModFS) {
17671767
Logger.Info($"Loaded {loadedHistoriesCount} title histories.");
17681768

17691769
// Add vanilla development to counties
1770-
// For counties that inherit development level from de jure lieges, assign it to them directly for better reliability.
1771-
foreach (var title in this.Where(t => t.Rank == TitleRank.county && t.GetDevelopmentLevel(ck3BookmarkDate) is null)) {
1772-
var inheritedDev = title.GetOwnOrInheritedDevelopmentLevel(ck3BookmarkDate);
1773-
title.SetDevelopmentLevel(inheritedDev ?? 0, ck3BookmarkDate);
1770+
// Assign development level directly to each county for better reliability, then remove it from duchies and above.
1771+
foreach (var county in Counties) {
1772+
var inheritedDev = county.GetOwnOrInheritedDevelopmentLevel(ck3BookmarkDate);
1773+
county.History.Fields.Remove("development_level");
1774+
county.SetDevelopmentLevel(inheritedDev ?? 0, ck3BookmarkDate);
1775+
}
1776+
foreach (var title in this.Where(t => t.Rank > TitleRank.county)) {
1777+
title.History.Fields.Remove("development_level");
17741778
}
17751779

17761780
// Remove history entries past the bookmark date.

ImperatorToCK3/CK3/Titles/Title.cs

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,14 +1081,33 @@ public Dictionary<string, Title> GetDeFactoVassalsAndBelow(Date date, string ran
10811081
[SerializedName("cultural_names")] public Dictionary<string, string>? CulturalNames { get; private set; }
10821082

10831083
public int? GetOwnOrInheritedDevelopmentLevel(Date date) {
1084-
var ownDev = GetDevelopmentLevel(date);
1085-
if (ownDev is not null) { // if development level is already set, just return it
1086-
return ownDev;
1087-
}
1088-
if (deJureLiege is not null) { // if de jure liege exists, return their level
1089-
return deJureLiege.GetOwnOrInheritedDevelopmentLevel(date);
1084+
// Latest date (<= date) takes precedence.
1085+
// If multiple titles have the same date, lowest rank takes precedence.
1086+
// Consider all de jure lieges up to empire level.
1087+
var titlesToConsider = new List<Title> { this };
1088+
var currentLiege = DeJureLiege;
1089+
while (currentLiege is not null && currentLiege.Rank <= TitleRank.empire) {
1090+
titlesToConsider.Add(currentLiege);
1091+
currentLiege = currentLiege.DeJureLiege;
1092+
}
1093+
Date? bestDate = null;
1094+
Title? bestTitle = null;
1095+
foreach (var title in titlesToConsider) {
1096+
if (!title.History.Fields.TryGetValue("development_level", out var devField)) {
1097+
continue;
1098+
}
1099+
(Date? entryDate, object? value) = devField.GetLastEntryWithDate(date);
1100+
if (value is null) {
1101+
continue;
1102+
}
1103+
1104+
if (bestDate is null || (entryDate is not null && entryDate > bestDate) || (entryDate == bestDate && title.Rank < bestTitle?.Rank)) {
1105+
bestDate = entryDate;
1106+
bestTitle = title;
1107+
}
10901108
}
1091-
return null;
1109+
1110+
return bestTitle?.GetDevelopmentLevel(bestDate ?? date);
10921111
}
10931112

10941113
public ICollection<string> GetSuccessionLaws(Date date) {

ImperatorToCK3/CommonUtils/IHistoryField.cs

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,52 @@
22
using commonItems.Collections;
33
using System;
44
using System.Collections.Generic;
5+
using System.Linq;
56
using ZLinq;
67

78
namespace ImperatorToCK3.CommonUtils;
89

910
internal interface IHistoryField : IIdentifiable<string> {
10-
public List<KeyValuePair<string, object>> InitialEntries { get; }
11-
public SortedDictionary<Date, List<KeyValuePair<string, object>>> DateToEntriesDict { get; }
11+
internal List<KeyValuePair<string, object>> InitialEntries { get; }
12+
internal SortedDictionary<Date, List<KeyValuePair<string, object>>> DateToEntriesDict { get; }
1213

13-
public object? GetValue(Date? date);
14+
internal object? GetValue(Date? date);
15+
internal KeyValuePair<Date?, object?> GetLastEntryWithDate(Date? date) { // TODO: add tests for this
16+
if (date is not null) {
17+
var pairsWithEarlierOrSameDate = DateToEntriesDict.TakeWhile(d => d.Key <= date);
1418

15-
public void RemoveHistoryPastDate(Date date) {
19+
foreach (var (d, entries) in pairsWithEarlierOrSameDate.Reverse()) {
20+
foreach (var entry in Enumerable.Reverse(entries)) {
21+
return new(d, entry.Value);
22+
}
23+
}
24+
}
25+
26+
var lastInitialEntry = InitialEntries.LastOrNull();
27+
return lastInitialEntry is not null
28+
? new(key: null, lastInitialEntry.Value)
29+
: new KeyValuePair<Date?, object?>(key: null, value: null);
30+
}
31+
32+
internal void RemoveHistoryPastDate(Date date) {
1633
foreach (var item in DateToEntriesDict.AsValueEnumerable().Where(kv => kv.Key > date).ToArray()) {
1734
DateToEntriesDict.Remove(item.Key);
1835
}
1936
}
20-
public void AddEntryToHistory(Date? date, string keyword, object value);
37+
internal void AddEntryToHistory(Date? date, string keyword, object value);
2138

2239
/// <summary>
2340
/// Removes all entries
2441
/// </summary>
25-
public void RemoveAllEntries() {
42+
internal void RemoveAllEntries() {
2643
RemoveAllEntries(_ => true);
2744
}
2845

2946
/// <summary>
3047
/// Removes all entries with values matching the predicate
3148
/// </summary>
3249
/// <param name="predicate"></param>
33-
public int RemoveAllEntries(Func<object, bool> predicate) {
50+
internal int RemoveAllEntries(Func<object, bool> predicate) {
3451
int removed = 0;
3552
removed += InitialEntries.RemoveAll(kvp => predicate(kvp.Value));
3653
foreach (var datedEntriesBlock in DateToEntriesDict) {
@@ -40,9 +57,9 @@ public int RemoveAllEntries(Func<object, bool> predicate) {
4057
return removed;
4158
}
4259

43-
public void RegisterKeywords(Parser parser, Date date);
60+
internal void RegisterKeywords(Parser parser, Date date);
4461

45-
public IEnumerable<KeyValuePair<string, object>> InitialEntriesForSerialization => InitialEntries;
62+
internal IEnumerable<KeyValuePair<string, object>> InitialEntriesForSerialization => InitialEntries;
4663

47-
public IHistoryField Clone();
64+
internal IHistoryField Clone();
4865
}

ImperatorToCK3/Outputter/TitlesOutputter.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,13 @@ private static async Task OutputTitlesHistory(string outputModPath, Title.Landed
2626
alreadyOutputtedTitles.Add(title.Id);
2727

2828
// output the kingdom's de jure vassals' history
29-
foreach (var (deJureVassalName, deJureVassal) in title.GetDeJureVassalsAndBelow()) {
29+
// Order the outputted titles first by rank (duchies before counties), then by title ID.
30+
var deJureVassalsAndBelow = title.GetDeJureVassalsAndBelow()
31+
.OrderByDescending(t => t.Value.Rank)
32+
.ThenBy(t => t.Key);
33+
foreach (var (deJureVassalTitleId, deJureVassal) in deJureVassalsAndBelow) {
3034
await deJureVassal.OutputHistory(historyOutput);
31-
alreadyOutputtedTitles.Add(deJureVassalName);
35+
alreadyOutputtedTitles.Add(deJureVassalTitleId);
3236
}
3337
}
3438

0 commit comments

Comments
 (0)