Skip to content

Commit b6bae81

Browse files
authored
Fix for stack overflow when checking for circular culture parents (#2612) #patch
Sentry event ID: 0ebe38c3b4b044eba5ab8406481e650f
1 parent 65cceae commit b6bae81

File tree

2 files changed

+68
-13
lines changed

2 files changed

+68
-13
lines changed

ImperatorToCK3.UnitTests/CK3/Cultures/CultureCollectionTests.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
using ImperatorToCK3.UnitTests.TestHelpers;
77
using System;
88
using System.Collections.Generic;
9+
using System.IO;
910
using Xunit;
1011

1112
namespace ImperatorToCK3.UnitTests.CK3.Cultures;
1213

14+
[Collection("Sequential")]
1315
public class CultureCollectionTests {
1416
private static readonly ModFilesystem ck3ModFS = new("TestFiles/CK3/game", Array.Empty<Mod>());
1517
private static readonly PillarCollection pillars;
@@ -78,4 +80,55 @@ public void ConverterLanguageCanBeMergedIntoExistingLanguage() {
7880
Assert.Equal("language_illyrian", cultures["albanian"].Language.Id);
7981
Assert.Equal("language_illyrian", cultures["dalmatian"].Language.Id);
8082
}
83+
84+
[Fact]
85+
public void WarnAboutCircularParentsLogsCorrectWarningsForAPairOfCultures() {
86+
var cultures = new TestCK3CultureCollection();
87+
88+
// Create a circular dependency by making "french" a child of "roman"
89+
// and "roman" a child of "french".
90+
cultures.GenerateTestCulture("roman", "heritage_latin");
91+
cultures.GenerateTestCulture("french", "heritage_latin");
92+
cultures["french"].ParentCultureIds.Add("roman");
93+
cultures["roman"].ParentCultureIds.Add("french");
94+
95+
var output = new StringWriter();
96+
Console.SetOut(output);
97+
cultures.WarnAboutCircularParents();
98+
var outputString = output.ToString();
99+
100+
Assert.Contains("[ERROR] Culture french is set as its own direct or indirect parent!", outputString);
101+
Assert.Contains("[ERROR] Culture roman is set as its own direct or indirect parent!", outputString);
102+
}
103+
104+
[Fact]
105+
public void WarnAboutCircularParentsLogsCorrectWarningForCultureBeingItsOwnParent() {
106+
var cultures = new TestCK3CultureCollection();
107+
// Create a culture that is its own parent.
108+
cultures.GenerateTestCulture("roman", "heritage_latin");
109+
cultures["roman"].ParentCultureIds.Add("roman");
110+
111+
var output = new StringWriter();
112+
Console.SetOut(output);
113+
cultures.WarnAboutCircularParents();
114+
var outputString = output.ToString();
115+
116+
Assert.Contains("[ERROR] Culture roman is set as its own direct or indirect parent!", outputString);
117+
}
118+
119+
[Fact]
120+
public void WarnAboutCircularParentsDoesNotLogAnythingForValidCultures() {
121+
var cultures = new TestCK3CultureCollection();
122+
// Just French being a child of Roman, no circular dependency.
123+
cultures.GenerateTestCulture("roman", "heritage_latin");
124+
cultures.GenerateTestCulture("french", "heritage_latin");
125+
cultures["french"].ParentCultureIds.Add("roman");
126+
127+
var output = new StringWriter();
128+
Console.SetOut(output);
129+
cultures.WarnAboutCircularParents();
130+
var outputString = output.ToString();
131+
132+
Assert.DoesNotContain("[ERROR]", outputString);
133+
}
81134
}

ImperatorToCK3/CK3/Cultures/CultureCollection.cs

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -258,33 +258,35 @@ internal void WarnAboutCircularParents() {
258258
// For every culture, check if it isn't set as its own immediate or distant parent.
259259
Logger.Debug("Checking for circular culture parents...");
260260
foreach (var culture in this) {
261-
var allParents = GetAncestorsOfCulture(culture);
261+
var allParents = GetAncestorsOfCulture(culture, alreadyChecked: []);
262262
if (allParents.Contains(culture.Id)) {
263-
Logger.Error($"Culture {culture.Id} is set as its own parent!");
263+
Logger.Error($"Culture {culture.Id} is set as its own direct or indirect parent!");
264264
}
265265
}
266266
}
267267

268-
private HashSet<string> GetAncestorsOfCulture(Culture cultureToCheck, HashSet<string>? alreadyChecked = null) {
268+
private HashSet<string> GetAncestorsOfCulture(Culture cultureToCheck, HashSet<string> alreadyChecked) {
269269
HashSet<string> allParents = [];
270270

271271
// Get immediate parents.
272272
foreach (var parentCultureId in cultureToCheck.ParentCultureIds) {
273-
// Avoid infinite recursion.
274-
if (alreadyChecked?.Contains(parentCultureId) == true) {
273+
allParents.Add(parentCultureId);
274+
}
275+
276+
// Prevent infinite recursion.
277+
alreadyChecked.Add(cultureToCheck.Id);
278+
279+
// For parents that haven't been checked yet, get their parents.
280+
foreach (var parentCultureId in cultureToCheck.ParentCultureIds) {
281+
if (alreadyChecked.Contains(parentCultureId)) {
275282
continue;
276283
}
277-
278-
allParents.Add(parentCultureId);
279-
280284
if (!TryGetValue(parentCultureId, out var parentCulture)) {
281-
Logger.Warn($"Parent culture {parentCultureId} not found for culture {cultureToCheck.Id}.");
285+
Logger.Warn($"Parent culture {parentCultureId} not found for culture {cultureToCheck.Id}!");
282286
continue;
283287
}
284-
285-
// Add the parent's parents.
286-
var parentParents = GetAncestorsOfCulture(parentCulture, allParents);
287-
allParents.UnionWith(parentParents);
288+
var parentAncestors = GetAncestorsOfCulture(parentCulture, alreadyChecked);
289+
allParents.UnionWith(parentAncestors);
288290
}
289291

290292
return allParents;

0 commit comments

Comments
 (0)