Skip to content

Commit 9b04259

Browse files
committed
Skip the I:R CoA export when continue_game.json can not be modified
1 parent 54e4885 commit 9b04259

2 files changed

Lines changed: 133 additions & 29 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using System;
2+
using System.IO;
3+
using System.Reflection;
4+
using ImperatorToCK3.Imperator;
5+
using ImperatorToCK3.UnitTests.TestHelpers;
6+
using Xunit;
7+
8+
namespace ImperatorToCK3.UnitTests.Imperator;
9+
10+
public sealed class WorldTests : IDisposable {
11+
private readonly string tempRoot;
12+
private readonly Configuration config;
13+
private readonly TestImperatorWorld world;
14+
15+
public WorldTests() {
16+
tempRoot = Path.Combine(Path.GetTempPath(), "IRToCK3Tests", nameof(WorldTests), Guid.NewGuid().ToString("N"));
17+
Directory.CreateDirectory(tempRoot);
18+
19+
config = new Configuration {
20+
ImperatorPath = "TestFiles/Imperator",
21+
ImperatorDocPath = Path.Combine(tempRoot, "ImperatorDocuments"),
22+
SaveGamePath = Path.Combine(tempRoot, "test-save.rome")
23+
};
24+
Directory.CreateDirectory(config.ImperatorDocPath);
25+
File.WriteAllText(config.SaveGamePath, string.Empty);
26+
27+
world = new TestImperatorWorld(config);
28+
}
29+
30+
public void Dispose() {
31+
try {
32+
Directory.Delete(tempRoot, recursive: true);
33+
} catch {
34+
// best effort cleanup
35+
}
36+
}
37+
38+
[Fact]
39+
public void OutputContinueGameJson_canOverwriteReadOnlyFile() {
40+
var continueGamePath = Path.Combine(config.ImperatorDocPath, "continue_game.json");
41+
File.WriteAllText(continueGamePath, "old content");
42+
File.SetAttributes(continueGamePath, FileAttributes.ReadOnly);
43+
44+
var result = (bool)InvokeWorldMethod("OutputContinueGameJson", config)!;
45+
46+
Assert.True(result);
47+
Assert.Contains("\"title\": \"test-save\"", File.ReadAllText(continueGamePath), StringComparison.Ordinal);
48+
Assert.False(File.GetAttributes(continueGamePath).HasFlag(FileAttributes.ReadOnly));
49+
}
50+
51+
[Fact]
52+
public void LaunchImperatorToExportCountryFlags_skipsLaunchWhenContinueGameJsonCannotBeWritten() {
53+
Directory.CreateDirectory(Path.Combine(config.ImperatorDocPath, "continue_game.json"));
54+
55+
var exception = Record.Exception(() => InvokeWorldMethod("LaunchImperatorToExportCountryFlags", config));
56+
57+
Assert.Null(exception);
58+
Assert.False(File.Exists(Path.Combine(config.ImperatorDocPath, "dlc_load.json")));
59+
}
60+
61+
private object? InvokeWorldMethod(string methodName, params object[] args) {
62+
var method = typeof(World).GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic);
63+
Assert.NotNull(method);
64+
return method!.Invoke(world, args);
65+
}
66+
}

ImperatorToCK3/Imperator/World.cs

Lines changed: 67 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,14 @@ protected World(Configuration config) {
8787
ImperatorRegionMapper = new ImperatorRegionMapper(Areas, MapData);
8888
}
8989

90-
internal static void OutputGuiContainer(ModFilesystem modFS, IEnumerable<string> tagsNeedingFlags, Configuration config) {
90+
internal static bool OutputGuiContainer(ModFilesystem modFS, IEnumerable<string> tagsNeedingFlags, Configuration config) {
9191
Logger.Debug("Modifying gui for exporting CoAs...");
9292

9393
const string relativeTopBarGuiPath = "gui/ingame_topbar.gui";
9494
var topBarGuiPath = modFS.GetActualFileLocation(relativeTopBarGuiPath);
9595
if (topBarGuiPath is null) {
9696
Logger.Warn($"{relativeTopBarGuiPath} not found, can't write CoA export commands!");
97-
return;
97+
return false;
9898
}
9999

100100
// build the GUI snippet we want to insert
@@ -128,7 +128,7 @@ internal static void OutputGuiContainer(ModFilesystem modFS, IEnumerable<string>
128128
} catch (Exception e) {
129129
Logger.Warn($"Failed to output modified GUI: {e.Message}");
130130
// bail out but don't crash the whole conversion
131-
return;
131+
return false;
132132
}
133133

134134
// Create a .mod file for the temporary mod.
@@ -138,17 +138,40 @@ internal static void OutputGuiContainer(ModFilesystem modFS, IEnumerable<string>
138138
name = "IRToCK3 CoA export mod"
139139
path = "mod/coa_export_mod"
140140
""";
141-
File.WriteAllText(Path.Combine(config.ImperatorDocPath, "mod/coa_export_mod/descriptor.mod"), modFileContents);
141+
if (!TryWriteTextFile(Path.Combine(config.ImperatorDocPath, "mod/coa_export_mod/descriptor.mod"), modFileContents)) {
142+
return false;
143+
}
142144

143145
var absoluteModPath = Path.Combine(config.ImperatorDocPath, "mod/coa_export_mod").Replace('\\', '/');
144146
modFileContents = modFileContents.Replace("path = \"mod/coa_export_mod\"", $"path = \"{absoluteModPath}\"");
145-
File.WriteAllText(Path.Combine(config.ImperatorDocPath, "mod/coa_export_mod.mod"), modFileContents);
147+
return TryWriteTextFile(Path.Combine(config.ImperatorDocPath, "mod/coa_export_mod.mod"), modFileContents);
148+
}
149+
150+
private static bool TryWriteTextFile(string filePath, string contents) {
151+
try {
152+
var directoryPath = Path.GetDirectoryName(filePath);
153+
if (directoryPath is not null) {
154+
FileHelper.EnsureDirectoryExists(directoryPath);
155+
}
156+
157+
if (File.Exists(filePath)) {
158+
File.SetAttributes(filePath, FileAttributes.Normal);
159+
}
160+
161+
using var writer = FileHelper.OpenWriteWithRetries(filePath, Encoding.UTF8);
162+
writer.Write(contents);
163+
return true;
164+
} catch (Exception e) when (e is UnauthorizedAccessException or IOException or UserErrorException) {
165+
Logger.Warn($"Failed to write \"{filePath}\": {e.Message}");
166+
Logger.Debug(e.ToString());
167+
return false;
168+
}
146169
}
147170

148-
private void OutputContinueGameJson(Configuration config) {
171+
private bool OutputContinueGameJson(Configuration config) {
149172
// Set the current save to be used when launching the game with the continuelastsave option.
150173
Logger.Debug("Modifying continue_game.json...");
151-
File.WriteAllText(Path.Join(config.ImperatorDocPath, "continue_game.json"),
174+
return TryWriteTextFile(Path.Join(config.ImperatorDocPath, "continue_game.json"),
152175
contents: $$"""
153176
{
154177
"title": "{{Path.GetFileNameWithoutExtension(config.SaveGamePath)}}",
@@ -158,7 +181,7 @@ private void OutputContinueGameJson(Configuration config) {
158181
""");
159182
}
160183

161-
private void OutputDlcLoadJson(Configuration config) {
184+
private bool OutputDlcLoadJson(Configuration config) {
162185
Logger.Debug("Outputting dlc_load.json...");
163186
var dlcLoadBuilder = new StringBuilder();
164187
dlcLoadBuilder.AppendLine("{");
@@ -169,19 +192,21 @@ private void OutputDlcLoadJson(Configuration config) {
169192
dlcLoadBuilder.AppendLine("],");
170193
dlcLoadBuilder.AppendLine(@"""disabled_dlcs"":[]");
171194
dlcLoadBuilder.AppendLine("}");
172-
File.WriteAllText(Path.Join(config.ImperatorDocPath, "dlc_load.json"), dlcLoadBuilder.ToString());
195+
return TryWriteTextFile(Path.Join(config.ImperatorDocPath, "dlc_load.json"), dlcLoadBuilder.ToString());
173196
}
174197

175-
private void LaunchImperatorToExportCountryFlags(Configuration config) {
198+
private bool LaunchImperatorToExportCountryFlags(Configuration config) {
176199
Logger.Info("Retrieving random CoAs from Imperator...");
177-
OutputContinueGameJson(config);
178-
OutputDlcLoadJson(config);
200+
if (!OutputContinueGameJson(config) || !OutputDlcLoadJson(config)) {
201+
Logger.Warn("Skipping Imperator launch because the launcher files couldn't be written.");
202+
return false;
203+
}
179204

180205
string imperatorBinaryName = OperatingSystem.IsWindows() ? "imperator.exe" : "imperator";
181206
var imperatorBinaryPath = Path.Combine(config.ImperatorPath, "binaries", imperatorBinaryName);
182207
if (!File.Exists(imperatorBinaryPath)) {
183208
Logger.Warn("Imperator binary not found! Aborting the random CoA extraction!");
184-
return;
209+
return false;
185210
}
186211

187212
string dataTypesLogPath = Path.Combine(config.ImperatorDocPath, "logs/data_types.log");
@@ -202,7 +227,7 @@ private void LaunchImperatorToExportCountryFlags(Configuration config) {
202227
var imperatorProcess = Process.Start(processStartInfo);
203228
if (imperatorProcess is null) {
204229
Logger.Warn("Failed to start Imperator process! Aborting!");
205-
return;
230+
return false;
206231
}
207232

208233
imperatorProcess.Exited += HandleImperatorProcessExit(config, imperatorProcess);
@@ -220,6 +245,8 @@ private void LaunchImperatorToExportCountryFlags(Configuration config) {
220245
Logger.Debug("Killing Imperator process...");
221246
imperatorProcess.Kill();
222247
}
248+
249+
return true;
223250
}
224251

225252
private void WaitForImperatorDataTypesLog(Process imperatorProcess, string dataTypesLogPath) {
@@ -279,25 +306,36 @@ private void ReadCoatsOfArmsFromGameLog(string imperatorDocPath) {
279306
}
280307

281308
private void ExtractDynamicCoatsOfArms(Configuration config) {
282-
var countryFlags = Countries.Select(country => country.Flag).ToArray();
283-
var missingFlags = CoaMapper.GetAllMissingFlagKeys(countryFlags);
284-
if (missingFlags.Count == 0) {
285-
return;
286-
}
309+
try {
310+
var countryFlags = Countries.Select(country => country.Flag).ToArray();
311+
var missingFlags = CoaMapper.GetAllMissingFlagKeys(countryFlags);
312+
if (missingFlags.Count == 0) {
313+
return;
314+
}
287315

288-
Logger.Debug("Missing country flag definitions: " + string.Join(", ", missingFlags));
316+
Logger.Debug("Missing country flag definitions: " + string.Join(", ", missingFlags));
289317

290-
var tagsWithMissingFlags = Countries
291-
.Where(country => missingFlags.Contains(country.Flag))
292-
.Select(country => country.Tag);
318+
var tagsWithMissingFlags = Countries
319+
.Where(country => missingFlags.Contains(country.Flag))
320+
.Select(country => country.Tag);
293321

294-
OutputGuiContainer(ModFS, tagsWithMissingFlags, config);
295-
LaunchImperatorToExportCountryFlags(config);
296-
ReadCoatsOfArmsFromGameLog(config.ImperatorDocPath);
322+
if (!OutputGuiContainer(ModFS, tagsWithMissingFlags, config)) {
323+
Logger.Warn("Skipping Imperator launch because the temporary CoA export mod couldn't be prepared.");
324+
return;
325+
}
326+
327+
if (!LaunchImperatorToExportCountryFlags(config)) {
328+
return;
329+
}
330+
ReadCoatsOfArmsFromGameLog(config.ImperatorDocPath);
297331

298-
var missingFlagsAfterExtraction = CoaMapper.GetAllMissingFlagKeys(countryFlags);
299-
if (missingFlagsAfterExtraction.Count > 0) {
300-
Logger.Warn("Failed to export the following country flags: " + string.Join(", ", missingFlagsAfterExtraction));
332+
var missingFlagsAfterExtraction = CoaMapper.GetAllMissingFlagKeys(countryFlags);
333+
if (missingFlagsAfterExtraction.Count > 0) {
334+
Logger.Warn("Failed to export the following country flags: " + string.Join(", ", missingFlagsAfterExtraction));
335+
}
336+
} catch (Exception e) {
337+
Logger.Warn($"Failed to extract dynamic coats of arms: {e.Message}");
338+
Logger.Debug(e.ToString());
301339
}
302340
}
303341

0 commit comments

Comments
 (0)