From acc7d2a6c7d28ce894b5d942c1176219b80f4b59 Mon Sep 17 00:00:00 2001 From: Seweryn Presnal Date: Mon, 25 Mar 2024 20:34:48 +0100 Subject: [PATCH 1/9] WIP I:R launching to extract coats of arms --- ImperatorToCK3/Imperator/World.cs | 153 ++++++++++++++++++ .../Outputter/MenAtArmsOutputter.cs | 2 + 2 files changed, 155 insertions(+) diff --git a/ImperatorToCK3/Imperator/World.cs b/ImperatorToCK3/Imperator/World.cs index 24b9c387b..1d22eb040 100644 --- a/ImperatorToCK3/Imperator/World.cs +++ b/ImperatorToCK3/Imperator/World.cs @@ -22,8 +22,12 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.IO; using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; using Mods = System.Collections.Generic.List; using Parser = commonItems.Parser; @@ -68,9 +72,155 @@ protected World(Configuration config) { Religions = new ReligionCollection(new ScriptValueCollection()); ImperatorRegionMapper = new ImperatorRegionMapper(Areas, MapData); } + + + private static void OutputGuiContainer(Configuration config, ModFilesystem modFS, List tagsNeedingFlags) { + // Create scripted GUI. + var scriptedGuisLocation = modFS.GetActualFolderLocation("common/scripted_guis"); + if (scriptedGuisLocation is null) { + Logger.Warn("Failed to find Imperator common/scripted_guis folder, can't write CoA export commands!"); + return; + } + + string sguiPath = Path.Combine(scriptedGuisLocation, "IRToCK3_dump_coas.txt"); + Logger.Debug($"Writing scripted GUI to \"{sguiPath}\"..."); + try { + File.WriteAllText(sguiPath, """ + IRToCK3_dump_coas_sgui = { + scope = character + + is_valid = { always = yes } + is_shown = { always = yes } + } + """, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true)); + } catch (Exception e) { + Logger.Warn($"Failed to write scripted GUI to {sguiPath}: {e.Message}"); + return; + } + + const string relativeTopBarGuiPath = "gui/ingame_topbar.gui"; + var topBarGuiPath = modFS.GetActualFileLocation(relativeTopBarGuiPath); + if (topBarGuiPath is null) { + Logger.Warn($"{relativeTopBarGuiPath} not found, can't write CoA export commands!"); + return; + } + + string originalGuiText = File.ReadAllText(topBarGuiPath); + + string modifiedGuiText = originalGuiText.TrimEnd().TrimEnd('}'); + modifiedGuiText += "\tcontainer={\n"; + modifiedGuiText += "\t\tname=\"IRToCK3_dump_coas_container\"\n"; + modifiedGuiText += "\t\tdatacontext=\"[GetScriptedGui('IRToCK3_dump_coas_sgui')]\"\n"; + modifiedGuiText += "\t\tvisible=yes\n"; + const float duration = 0.01f; + int state = 0; + string commandsString = string.Join(';', tagsNeedingFlags.Select(tag => $"coat_of_arms {tag}")); + modifiedGuiText += $"\t\tstate = {{ name=_show next=state{state} on_finish=\"[ExecuteConsoleCommands('{commandsString}')]\" duration={duration} }}\n"; + modifiedGuiText += "\t}\n"; + modifiedGuiText += "}\n"; + + Logger.Debug($"Writing modified GUI to \"{topBarGuiPath}\"..."); + try { + File.WriteAllText(topBarGuiPath, modifiedGuiText); + } catch (Exception e) { + Logger.Warn($"Failed to write GUI to {topBarGuiPath}: {e.Message}"); + return; + } + + + + // TODO: MOVE THE I:R LAUNCHING CODE TO A SEPARATE FUNCTION + + // TODO: SET THE CURRENT SAVE TO BE USED IN continuelastsave + + var imperatorBinaryPath = Path.Combine(config.ImperatorPath, "binaries/imperator.exe"); + if (!File.Exists(imperatorBinaryPath)) { + Logger.Error("Imperator binary not found! Aborting!"); + } + + Logger.Notice("The converter will now launch Imperator to export country flags. Please don't touch the Imperator window until it closes..."); + Thread.Sleep(5000); + + var processStartInfo = new ProcessStartInfo { + FileName = imperatorBinaryPath, + Arguments = "-continuelastsave -debug_mode", + CreateNoWindow = true, + RedirectStandardOutput = true + }; + var imperatorProcess = Process.Start(processStartInfo); + if (imperatorProcess is null) { + Logger.Error("Failed to start Imperator process! Aborting!"); + return; + } + imperatorProcess.Exited += (sender, args) => { + Logger.Error("Imperator process exited unexpectedly! Aborting!"); + }; + + + // WAIT UNTIL THE IMPERATOR finishes loading. + Logger.Debug("Waiting for Imperator process to load savegame..."); + while (true) { + var line = imperatorProcess.StandardOutput.ReadLine(); + if (line is null) { + continue; + } + if (line.Contains("Updating cached data done")) { + break; + } + Logger.Debug(line); + } + + Logger.Notice("Imperator process loaded savegame."); + + + // TODO: IN THE END, RESTORE THE ORIGINAL GUI TEXT + } + + public World(Configuration config, ConverterVersion converterVersion) { Logger.Info("*** Hello Imperator, Roma Invicta! ***"); + + // var imperatorBinaryPath = Path.Combine(config.ImperatorPath, "binaries/imperator.exe"); + // if (!File.Exists(imperatorBinaryPath)) { + // Logger.Error("Imperator binary not found! Aborting!"); + // } + + // Logger.Notice("The converter will now launch Imperator to export country flags. Please don't touch the Imperator window until it closes..."); + // Thread.Sleep(5000); + + // var processStartInfo = new ProcessStartInfo { + // FileName = imperatorBinaryPath, + // Arguments = "-continuelastsave -debug_mode", + // CreateNoWindow = true, + // RedirectStandardOutput = true + // }; + // var imperatorProcess = Process.Start(processStartInfo); + // if (imperatorProcess is null) { + // Logger.Error("Failed to start Imperator process! Aborting!"); + // return; + // } + // imperatorProcess.Exited += (sender, args) => { + // Logger.Error("Imperator process exited unexpectedly! Aborting!"); + // }; + + + // // WAIT UNTIL THE IMPERATOR finishes loading. + // Logger.Debug("Waiting for Imperator process to load savegame..."); + // while (true) { + // var line = imperatorProcess.StandardOutput.ReadLine(); + // if (line is null) { + // continue; + // } + // if (line.Contains("Updating cached data done")) { + // break; + // } + // Logger.Debug(line); + // } + + // Logger.Notice("Imperator process loaded savegame."); + + // throw new NotImplementedException("TODO: REMOVE ME"); var imperatorRoot = Path.Combine(config.ImperatorPath, "game"); @@ -124,6 +274,9 @@ public World(Configuration config, ConverterVersion converterVersion) { modLoader.LoadMods(config.ImperatorDocPath, incomingMods); ModFS = new ModFilesystem(imperatorRoot, modLoader.UsableMods); + OutputGuiContainer(config, ModFS, ["ROM", "CAR", "EGY"]); // TODO: REMOVE THIS FROM HERE + throw new NotImplementedException("TODO: REMOVE ME"); + // Now that we have the list of mods used, we can load data from Imperator mod filesystem LoadModFilesystemDependentData(); }); diff --git a/ImperatorToCK3/Outputter/MenAtArmsOutputter.cs b/ImperatorToCK3/Outputter/MenAtArmsOutputter.cs index 44e5febf6..65cf3feda 100644 --- a/ImperatorToCK3/Outputter/MenAtArmsOutputter.cs +++ b/ImperatorToCK3/Outputter/MenAtArmsOutputter.cs @@ -78,6 +78,8 @@ private static void OutputGuiContainer(string outputModName, ModFilesystem modFS foreach (var character in charactersWithMaa) { foreach (var (maaType, stacks) in character.MenAtArmsStacksPerType) { for (int i = 0; i < stacks; ++i) { + // TODO: Use ExecuteConsoleCommands instead of using ExecuteConsoleCommand in a loop + // TODO: use on_finish instead of on_start, on_start may execute twice according to a CK3 mod coop output.WriteLine( "\t\tstate = { " + $"name=state{state++} " + From 3f87fd3a22cb2ac38327c47cd3c98bce174a598b Mon Sep 17 00:00:00 2001 From: Seweryn Presnal Date: Fri, 3 May 2024 19:29:08 +0200 Subject: [PATCH 2/9] post-merge fix --- ImperatorToCK3/Imperator/World.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ImperatorToCK3/Imperator/World.cs b/ImperatorToCK3/Imperator/World.cs index 3545c1316..3cb409333 100644 --- a/ImperatorToCK3/Imperator/World.cs +++ b/ImperatorToCK3/Imperator/World.cs @@ -278,6 +278,9 @@ private void ParseSave(Configuration config, ConverterVersion converterVersion) modLoader.LoadMods(config.ImperatorDocPath, incomingMods); ModFS = new ModFilesystem(imperatorRoot, modLoader.UsableMods); + OutputGuiContainer(config, ModFS, ["ROM", "CAR", "EGY"]); // TODO: REMOVE THIS FROM HERE + throw new NotImplementedException("TODO: REMOVE ME"); + // Now that we have the list of mods used, we can load data from Imperator mod filesystem LoadModFilesystemDependentData(); }); From eb5dd58a1640354ad6e2687ca5b5b184f46e665f Mon Sep 17 00:00:00 2001 From: Seweryn Presnal Date: Sat, 18 May 2024 20:19:23 +0200 Subject: [PATCH 3/9] Extract ExtractDynamicCoatsOfArms, load meta player --- ImperatorToCK3/Imperator/World.cs | 46 +++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/ImperatorToCK3/Imperator/World.cs b/ImperatorToCK3/Imperator/World.cs index 3cb409333..3a3bdb600 100644 --- a/ImperatorToCK3/Imperator/World.cs +++ b/ImperatorToCK3/Imperator/World.cs @@ -26,7 +26,6 @@ using System.Diagnostics; using System.IO; using System.Linq; -using System.Runtime.InteropServices; using System.Text; using System.Threading; using Mods = System.Collections.Generic.List; @@ -65,6 +64,7 @@ public class World { private enum SaveType { Invalid, Plaintext, CompressedEncoded } private SaveType saveType = SaveType.Invalid; + private string metaPlayerName = string.Empty; protected World(Configuration config) { ModFS = new ModFilesystem(Path.Combine(config.ImperatorPath, "game"), Array.Empty()); @@ -75,7 +75,7 @@ protected World(Configuration config) { } - private static void OutputGuiContainer(Configuration config, ModFilesystem modFS, List tagsNeedingFlags) { + private static void OutputGuiContainer(ModFilesystem modFS, List tagsNeedingFlags) { // Create scripted GUI. var scriptedGuisLocation = modFS.GetActualFolderLocation("common/scripted_guis"); if (scriptedGuisLocation is null) { @@ -127,13 +127,22 @@ private static void OutputGuiContainer(Configuration config, ModFilesystem modFS Logger.Warn($"Failed to write GUI to {topBarGuiPath}: {e.Message}"); return; } + } - - - // TODO: MOVE THE I:R LAUNCHING CODE TO A SEPARATE FUNCTION - - // TODO: SET THE CURRENT SAVE TO BE USED IN continuelastsave - + private void LaunchImperatorToExportCountryFlags(Configuration config) { + // Set the current save to be used when launching the game with the continuelastsave option. + Logger.Debug("Modifying continue_game.json..."); + string continueGameJsonPath = Path.Join(config.ImperatorDocPath, "continue_game.json"); + + File.WriteAllText(continueGameJsonPath, + contents: $$""" + { + "title": "{{Path.GetFileNameWithoutExtension(config.SaveGamePath)}}", + "desc": "Playing as ${{metaPlayerName}} - ${{EndDate}} AD", + "date": "{{DateTime.Now:yyyy-MM-dd HH:mm:ss}}" + } + """); + var imperatorBinaryPath = Path.Combine(config.ImperatorPath, "binaries/imperator.exe"); if (!File.Exists(imperatorBinaryPath)) { Logger.Error("Imperator binary not found! Aborting!"); @@ -154,7 +163,11 @@ private static void OutputGuiContainer(Configuration config, ModFilesystem modFS return; } imperatorProcess.Exited += (sender, args) => { - Logger.Error("Imperator process exited unexpectedly! Aborting!"); + if (imperatorProcess.ExitCode != 0) { + Logger.Error("Imperator process exited unexpectedly! Aborting!"); + } else { + Logger.Notice("Imperator process finished exporting country flags."); + } }; @@ -172,12 +185,18 @@ private static void OutputGuiContainer(Configuration config, ModFilesystem modFS } Logger.Notice("Imperator process loaded savegame."); + } + + private void ExtractDynamicCoatsOfArms(Configuration config) { + OutputGuiContainer(ModFS, ["ROM", "CAR", "EGY"]); + LaunchImperatorToExportCountryFlags(config); // TODO: IN THE END, RESTORE THE ORIGINAL GUI TEXT + + throw new NotImplementedException("TODO: REMOVE ME"); // TODO: REMOVE THIS } - public World(Configuration config, ConverterVersion converterVersion) { Logger.Info("*** Hello Imperator, Roma Invicta! ***"); @@ -244,6 +263,8 @@ public World(Configuration config, ConverterVersion converterVersion) { } Logger.Info("*** Building World ***"); + + ExtractDynamicCoatsOfArms(config); // Link all the intertwining references Logger.Info("Linking Characters with Families..."); @@ -278,9 +299,6 @@ private void ParseSave(Configuration config, ConverterVersion converterVersion) modLoader.LoadMods(config.ImperatorDocPath, incomingMods); ModFS = new ModFilesystem(imperatorRoot, modLoader.UsableMods); - OutputGuiContainer(config, ModFS, ["ROM", "CAR", "EGY"]); // TODO: REMOVE THIS FROM HERE - throw new NotImplementedException("TODO: REMOVE ME"); - // Now that we have the list of mods used, we can load data from Imperator mod filesystem LoadModFilesystemDependentData(); }); @@ -295,7 +313,7 @@ private void ParseSave(Configuration config, ConverterVersion converterVersion) parser.RegisterKeyword("diplomacy", LoadDiplomacy); parser.RegisterKeyword("jobs", LoadJobs); parser.RegisterKeyword("deity_manager", reader => Religions.LoadHolySiteDatabase(reader)); - parser.RegisterKeyword("meta_player_name", ParserHelpers.IgnoreItem); + parser.RegisterKeyword("meta_player_name", reader => metaPlayerName = reader.GetString()); parser.RegisterKeyword("speed", ParserHelpers.IgnoreItem); parser.RegisterKeyword("random_seed", ParserHelpers.IgnoreItem); parser.RegisterKeyword("tutorial_disable", ParserHelpers.IgnoreItem); From 6fd0fa3b8d397f5d86df8a784411c24d2a78f57c Mon Sep 17 00:00:00 2001 From: Seweryn Presnal Date: Sun, 2 Jun 2024 19:36:58 +0200 Subject: [PATCH 4/9] fix desc in continue_game.json --- ImperatorToCK3/Imperator/World.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ImperatorToCK3/Imperator/World.cs b/ImperatorToCK3/Imperator/World.cs index 3a3bdb600..ac819e0e7 100644 --- a/ImperatorToCK3/Imperator/World.cs +++ b/ImperatorToCK3/Imperator/World.cs @@ -132,17 +132,19 @@ private static void OutputGuiContainer(ModFilesystem modFS, List tagsNee private void LaunchImperatorToExportCountryFlags(Configuration config) { // Set the current save to be used when launching the game with the continuelastsave option. Logger.Debug("Modifying continue_game.json..."); - string continueGameJsonPath = Path.Join(config.ImperatorDocPath, "continue_game.json"); - - File.WriteAllText(continueGameJsonPath, + File.WriteAllText(Path.Join(config.ImperatorDocPath, "continue_game.json"), contents: $$""" { "title": "{{Path.GetFileNameWithoutExtension(config.SaveGamePath)}}", - "desc": "Playing as ${{metaPlayerName}} - ${{EndDate}} AD", + "desc": "Playing as {{metaPlayerName}} - {{EndDate}} AD", "date": "{{DateTime.Now:yyyy-MM-dd HH:mm:ss}}" } """); + Logger.Debug("Activating the playset used in the save..."); + // TODO: create temporary playset with all the mods used in the save + throw new NotImplementedException("TODO: IMPLEMENT ACTIVATING THE IMPERATOR PLAYLIST USED IN THE SAVE"); + var imperatorBinaryPath = Path.Combine(config.ImperatorPath, "binaries/imperator.exe"); if (!File.Exists(imperatorBinaryPath)) { Logger.Error("Imperator binary not found! Aborting!"); From 1bc04812fb2a7337d94f53862ac56394bc0bc271 Mon Sep 17 00:00:00 2001 From: Seweryn Presnal Date: Sat, 29 Jun 2024 12:12:05 +0200 Subject: [PATCH 5/9] ReadCoatsOfArmsFromGameLog --- ImperatorToCK3/CK3/World.cs | 8 +- ImperatorToCK3/Imperator/World.cs | 275 ++++++++++++++---------- ImperatorToCK3/Mappers/CoA/CoaMapper.cs | 25 ++- 3 files changed, 180 insertions(+), 128 deletions(-) diff --git a/ImperatorToCK3/CK3/World.cs b/ImperatorToCK3/CK3/World.cs index b01995bcb..4d0f79825 100644 --- a/ImperatorToCK3/CK3/World.cs +++ b/ImperatorToCK3/CK3/World.cs @@ -127,9 +127,6 @@ public World(Imperator.World impWorld, Configuration config) { Logger.Info("Loading map data..."); MapData = new MapData(ModFS); - // Load Imperator CoAs to use them for generated CK3 titles - coaMapper = new CoaMapper(impWorld.ModFS); - // Load vanilla CK3 landed titles and their history LandedTitles.LoadTitles(ModFS); @@ -228,7 +225,7 @@ public World(Imperator.World impWorld, Configuration config) { tagTitleMapper, impWorld.LocDB, provinceMapper, - coaMapper, + impWorld.CoaMapper, governmentMapper, successionLawMapper, definiteFormMapper, @@ -259,7 +256,7 @@ public World(Imperator.World impWorld, Configuration config) { provinceMapper, definiteFormMapper, imperatorRegionMapper, - coaMapper, + impWorld.CoaMapper, countyLevelGovernorships ); @@ -930,7 +927,6 @@ private void GenerateFillerHoldersForUnownedLands(CultureCollection cultures, Co } } - private readonly CoaMapper coaMapper; private readonly DeathReasonMapper deathReasonMapper = new(); private readonly DefiniteFormMapper definiteFormMapper = new(Path.Combine("configurables", "definite_form_names.txt")); private readonly NicknameMapper nicknameMapper = new(Path.Combine("configurables", "nickname_map.txt")); diff --git a/ImperatorToCK3/Imperator/World.cs b/ImperatorToCK3/Imperator/World.cs index 8c9c14cd2..a61098557 100644 --- a/ImperatorToCK3/Imperator/World.cs +++ b/ImperatorToCK3/Imperator/World.cs @@ -19,7 +19,9 @@ using ImperatorToCK3.Imperator.Provinces; using ImperatorToCK3.Imperator.Religions; using ImperatorToCK3.Imperator.States; +using ImperatorToCK3.Mappers.CoA; using ImperatorToCK3.Mappers.Region; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -27,6 +29,7 @@ using System.IO; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Threading; using Mods = System.Collections.Generic.List; using Parser = commonItems.Parser; @@ -35,6 +38,7 @@ namespace ImperatorToCK3.Imperator; public class World { public Date EndDate { get; private set; } = new Date("727.2.17", AUC: true); + private readonly IList incomingModPaths = []; // List of all mods used in the save. public ModFilesystem ModFS { get; private set; } private readonly SortedSet dlcs = new(); public IReadOnlySet GlobalFlags { get; private set; } = ImmutableHashSet.Empty; @@ -48,6 +52,7 @@ public class World { private PopCollection pops = new(); public ProvinceCollection Provinces { get; } = new(); public CountryCollection Countries { get; } = new(); + public CoaMapper CoaMapper { get; private set; } = new(); public MapData MapData { get; private set; } public AreaCollection Areas { get; } = new(); public ImperatorRegionMapper ImperatorRegionMapper { get; private set; } @@ -73,31 +78,9 @@ protected World(Configuration config) { Religions = new ReligionCollection(new ScriptValueCollection()); ImperatorRegionMapper = new ImperatorRegionMapper(Areas, MapData); } - - private static void OutputGuiContainer(ModFilesystem modFS, List tagsNeedingFlags) { - // Create scripted GUI. - var scriptedGuisLocation = modFS.GetActualFolderLocation("common/scripted_guis"); - if (scriptedGuisLocation is null) { - Logger.Warn("Failed to find Imperator common/scripted_guis folder, can't write CoA export commands!"); - return; - } - - string sguiPath = Path.Combine(scriptedGuisLocation, "IRToCK3_dump_coas.txt"); - Logger.Debug($"Writing scripted GUI to \"{sguiPath}\"..."); - try { - File.WriteAllText(sguiPath, """ - IRToCK3_dump_coas_sgui = { - scope = character - - is_valid = { always = yes } - is_shown = { always = yes } - } - """, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true)); - } catch (Exception e) { - Logger.Warn($"Failed to write scripted GUI to {sguiPath}: {e.Message}"); - return; - } + private static void OutputGuiContainer(ModFilesystem modFS, IEnumerable tagsNeedingFlags, Configuration config) { + Logger.Debug("Modifying gui for exporting CoAs..."); const string relativeTopBarGuiPath = "gui/ingame_topbar.gui"; var topBarGuiPath = modFS.GetActualFileLocation(relativeTopBarGuiPath); @@ -106,30 +89,43 @@ private static void OutputGuiContainer(ModFilesystem modFS, List tagsNee return; } - string originalGuiText = File.ReadAllText(topBarGuiPath); - - string modifiedGuiText = originalGuiText.TrimEnd().TrimEnd('}'); - modifiedGuiText += "\tcontainer={\n"; - modifiedGuiText += "\t\tname=\"IRToCK3_dump_coas_container\"\n"; - modifiedGuiText += "\t\tdatacontext=\"[GetScriptedGui('IRToCK3_dump_coas_sgui')]\"\n"; - modifiedGuiText += "\t\tvisible=yes\n"; - const float duration = 0.01f; - int state = 0; + var guiTextBuilder = new StringBuilder(); + guiTextBuilder.AppendLine("\tstate = {"); + guiTextBuilder.AppendLine("\t\tname = _show"); string commandsString = string.Join(';', tagsNeedingFlags.Select(tag => $"coat_of_arms {tag}")); - modifiedGuiText += $"\t\tstate = {{ name=_show next=state{state} on_finish=\"[ExecuteConsoleCommands('{commandsString}')]\" duration={duration} }}\n"; - modifiedGuiText += "\t}\n"; - modifiedGuiText += "}\n"; + commandsString += ";dumpdatatypes"; // This will let us know when the commands finished executing. + guiTextBuilder.AppendLine($"\t\ton_start=\"[ExecuteConsoleCommandsForced('{commandsString}')]\""); + guiTextBuilder.AppendLine("\t}"); + + List lines = File.ReadAllLines(topBarGuiPath).ToList(); + int index = lines.FindIndex(line => line.Contains("name = \"ingame_topbar\"")); + if (index != -1) { + lines.Insert(index + 1, guiTextBuilder.ToString()); + } - Logger.Debug($"Writing modified GUI to \"{topBarGuiPath}\"..."); - try { - File.WriteAllText(topBarGuiPath, modifiedGuiText); - } catch (Exception e) { - Logger.Warn($"Failed to write GUI to {topBarGuiPath}: {e.Message}"); - return; + var topBarOutputPath = Path.Combine(config.ImperatorDocPath, "mod/coa_export_mod", relativeTopBarGuiPath); + Logger.Debug($"Writing modified GUI to \"{topBarOutputPath}\"..."); + var topBarOutputDir = Path.GetDirectoryName(topBarOutputPath); + if (topBarOutputDir is not null) { + Directory.CreateDirectory(topBarOutputDir); } + File.WriteAllLines(topBarOutputPath, lines); + + // Create a .mod file for the temporary mod. + Logger.Debug("Creating temporary mod file..."); + string modFileContents = + """ + name = "IRToCK3 CoA export mod" + path = "mod/coa_export_mod" + """; + File.WriteAllText(Path.Combine(config.ImperatorDocPath, "mod/coa_export_mod/descriptor.mod"), modFileContents); + + var absoluteModPath = Path.Combine(config.ImperatorDocPath, "mod/coa_export_mod").Replace('\\', '/'); + modFileContents = modFileContents.Replace("path = \"mod/coa_export_mod\"", $"path = \"{absoluteModPath}\""); + File.WriteAllText(Path.Combine(config.ImperatorDocPath, "mod/coa_export_mod.mod"), modFileContents); } - - private void LaunchImperatorToExportCountryFlags(Configuration config) { + + private void OutputContinueGameJson(Configuration config) { // Set the current save to be used when launching the game with the continuelastsave option. Logger.Debug("Modifying continue_game.json..."); File.WriteAllText(Path.Join(config.ImperatorDocPath, "continue_game.json"), @@ -140,109 +136,143 @@ private void LaunchImperatorToExportCountryFlags(Configuration config) { "date": "{{DateTime.Now:yyyy-MM-dd HH:mm:ss}}" } """); + } + + private void OutputDlcLoadJson(Configuration config) { + Logger.Debug("Outputting dlc_load.json..."); + var dlcLoadBuilder = new StringBuilder(); + dlcLoadBuilder.AppendLine("{"); + dlcLoadBuilder.Append(@"""enabled_mods"": ["); + dlcLoadBuilder.AppendJoin(", ", incomingModPaths.Select(modPath => $"\"{modPath}\"")); + dlcLoadBuilder.AppendLine(","); + dlcLoadBuilder.AppendLine("\"mod/coa_export_mod.mod\""); + dlcLoadBuilder.AppendLine("],"); + dlcLoadBuilder.AppendLine(@"""disabled_dlcs"":[]"); + dlcLoadBuilder.AppendLine("}"); + File.WriteAllText(Path.Join(config.ImperatorDocPath, "dlc_load.json"), dlcLoadBuilder.ToString()); + } + + private void LaunchImperatorToExportCountryFlags(Configuration config) { + OutputContinueGameJson(config); + OutputDlcLoadJson(config); - Logger.Debug("Activating the playset used in the save..."); - // TODO: create temporary playset with all the mods used in the save - throw new NotImplementedException("TODO: IMPLEMENT ACTIVATING THE IMPERATOR PLAYLIST USED IN THE SAVE"); - - var imperatorBinaryPath = Path.Combine(config.ImperatorPath, "binaries/imperator.exe"); + string imperatorBinaryName = OperatingSystem.IsWindows() ? "imperator.exe" : "imperator"; + var imperatorBinaryPath = Path.Combine(config.ImperatorPath, "binaries", imperatorBinaryName); if (!File.Exists(imperatorBinaryPath)) { Logger.Error("Imperator binary not found! Aborting!"); } - Logger.Notice("The converter will now launch Imperator to export country flags. Please don't touch the Imperator window until it closes..."); - Thread.Sleep(5000); + string dataTypesLogPath = Path.Combine(config.ImperatorDocPath, "logs/data_types.log"); + if (File.Exists(dataTypesLogPath)) { + File.Delete(dataTypesLogPath); + } + + Logger.Info("Launching Imperator to extract coats of arms..."); var processStartInfo = new ProcessStartInfo { FileName = imperatorBinaryPath, Arguments = "-continuelastsave -debug_mode", CreateNoWindow = true, - RedirectStandardOutput = true + RedirectStandardOutput = true, + WindowStyle = ProcessWindowStyle.Hidden }; var imperatorProcess = Process.Start(processStartInfo); if (imperatorProcess is null) { - Logger.Error("Failed to start Imperator process! Aborting!"); + Logger.Warn("Failed to start Imperator process! Aborting!"); return; } - imperatorProcess.Exited += (sender, args) => { - if (imperatorProcess.ExitCode != 0) { - Logger.Error("Imperator process exited unexpectedly! Aborting!"); - } else { - Logger.Notice("Imperator process finished exporting country flags."); - } - }; + imperatorProcess.Exited += HandleImperatorProcessExit(config, imperatorProcess); - // WAIT UNTIL THE IMPERATOR finishes loading. - Logger.Debug("Waiting for Imperator process to load savegame..."); - while (true) { - var line = imperatorProcess.StandardOutput.ReadLine(); - if (line is null) { - continue; + // Make sure that if converter is closed, Imperator is closed as well. + AppDomain.CurrentDomain.ProcessExit += (sender, args) => { + if (!imperatorProcess.HasExited) { + imperatorProcess.Kill(); } - if (line.Contains("Updating cached data done")) { + }; + + // Wait until data_types.log exists (it will be created by the dumpdatatypes command). + var stopwatch = new Stopwatch(); + stopwatch.Start(); + while (!imperatorProcess.HasExited && !File.Exists(dataTypesLogPath)) { + if (stopwatch.Elapsed > TimeSpan.FromMinutes(5)) { + Logger.Warn("Imperator process took too long to execute console commands! Aborting!"); + imperatorProcess.Kill(); break; } - Logger.Debug(line); + + if (imperatorProcess.StandardOutput.ReadLine()?.Contains("Updating cached data done") == true) { + Logger.Debug("Imperator finished loading. Waiting for console commands to execute..."); + } + + Thread.Sleep(100); } - Logger.Notice("Imperator process loaded savegame."); + if (!imperatorProcess.HasExited) { + Logger.Debug("Killing Imperator process..."); + imperatorProcess.Kill(); + } } - private void ExtractDynamicCoatsOfArms(Configuration config) { - OutputGuiContainer(ModFS, ["ROM", "CAR", "EGY"]); - LaunchImperatorToExportCountryFlags(config); + private static EventHandler HandleImperatorProcessExit(Configuration config, Process imperatorProcess) { + return (sender, args) => { + Logger.Debug($"Imperator process exited with code {imperatorProcess.ExitCode}. Removing temporary mod files..."); + try { + File.Delete(Path.Combine(config.ImperatorDocPath, "mod/coa_export_mod.mod")); + Directory.Delete(Path.Combine(config.ImperatorDocPath, "mod/coa_export_mod"), recursive: true); + } catch (Exception e) { + Logger.Warn($"Failed to remove temporary mod files: {e.Message}"); + } + }; + } + private void ReadCoatsOfArmsFromGameLog(string imperatorDocPath) { + Logger.Info("Reading CoAs from game log..."); + string inputFilePath = Path.Combine(imperatorDocPath, "logs/game.log"); - // TODO: IN THE END, RESTORE THE ORIGINAL GUI TEXT + using var saveStream = File.Open(inputFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + using var reader = new StreamReader(saveStream); + string content = reader.ReadToEnd(); - throw new NotImplementedException("TODO: REMOVE ME"); // TODO: REMOVE THIS - } + // Remove everything prior to the first line contatining "Coat of arms:" + int startIndex = content.IndexOf("Coat of arms:", StringComparison.Ordinal); + if (startIndex == -1) { + Logger.Warn("No CoAs found in game log."); + return; + } + content = content.Substring(startIndex); - - public World(Configuration config, ConverterVersion converterVersion) { - Logger.Info("*** Hello Imperator, Roma Invicta! ***"); + string pattern = @"\S+=\s*\{[^}]*\}"; + MatchCollection matches = Regex.Matches(content, pattern); + + CoaMapper.ParseCoAs(matches.Select(match => match.Value)); + } - // var imperatorBinaryPath = Path.Combine(config.ImperatorPath, "binaries/imperator.exe"); - // if (!File.Exists(imperatorBinaryPath)) { - // Logger.Error("Imperator binary not found! Aborting!"); - // } + private void ExtractDynamicCoatsOfArms(Configuration config) { + var countryFlags = Countries.Select(country => country.Flag).ToList(); + var missingFlags = CoaMapper.GetAllMissingFlagKeys(countryFlags); + if (missingFlags.Count == 0) { + return; + } - // Logger.Notice("The converter will now launch Imperator to export country flags. Please don't touch the Imperator window until it closes..."); - // Thread.Sleep(5000); - - // var processStartInfo = new ProcessStartInfo { - // FileName = imperatorBinaryPath, - // Arguments = "-continuelastsave -debug_mode", - // CreateNoWindow = true, - // RedirectStandardOutput = true - // }; - // var imperatorProcess = Process.Start(processStartInfo); - // if (imperatorProcess is null) { - // Logger.Error("Failed to start Imperator process! Aborting!"); - // return; - // } - // imperatorProcess.Exited += (sender, args) => { - // Logger.Error("Imperator process exited unexpectedly! Aborting!"); - // }; + Logger.Debug("Missing country flag definitions: " + string.Join(", ", missingFlags)); + var tagsWithMissingFlags = Countries + .Where(country => missingFlags.Contains(country.Flag)) + .Select(country => country.Tag); - // // WAIT UNTIL THE IMPERATOR finishes loading. - // Logger.Debug("Waiting for Imperator process to load savegame..."); - // while (true) { - // var line = imperatorProcess.StandardOutput.ReadLine(); - // if (line is null) { - // continue; - // } - // if (line.Contains("Updating cached data done")) { - // break; - // } - // Logger.Debug(line); - // } - - // Logger.Notice("Imperator process loaded savegame."); + OutputGuiContainer(ModFS, tagsWithMissingFlags, config); + LaunchImperatorToExportCountryFlags(config); + ReadCoatsOfArmsFromGameLog(config.ImperatorDocPath); - // throw new NotImplementedException("TODO: REMOVE ME"); + var missingFlagsAfterExtraction = CoaMapper.GetAllMissingFlagKeys(countryFlags); + if (missingFlagsAfterExtraction.Count > 0) { + Logger.Warn("Failed to export the following country flags: " + string.Join(", ", missingFlagsAfterExtraction)); + } + } + + public World(Configuration config, ConverterVersion converterVersion) { + Logger.Info("*** Hello Imperator, Roma Invicta! ***"); Logger.Info("Verifying Imperator save..."); VerifySave(config.SaveGamePath); @@ -330,12 +360,18 @@ private void ParseSave(Configuration config, ConverterVersion converterVersion) Logger.IncrementProgress(); } - private static Mods DetectUsedMods(BufferedReader reader) { + private Mods DetectUsedMods(BufferedReader reader) { Logger.Info("Detecting used mods..."); - var modsList = reader.GetStrings(); - Logger.Info($"Save game claims {modsList.Count} mods used:"); - Mods incomingMods = new(); - foreach (var modPath in modsList) { + foreach (var modPath in reader.GetStrings()) { + incomingModPaths.Add(modPath); + } + if (incomingModPaths.Count == 0) { + Logger.Warn("Save game claims no mods used."); + } else { + Logger.Info($"Save game claims {incomingModPaths.Count} mods used:"); + } + Mods incomingMods = []; + foreach (var modPath in incomingModPaths) { Logger.Info($"Used mod: {modPath}"); incomingMods.Add(new Mod(string.Empty, modPath)); } @@ -589,6 +625,7 @@ private void LoadModFilesystemDependentData() { ImperatorRegionMapper.LoadRegions(ModFS, ColorFactory); Country.LoadGovernments(ModFS); + CoaMapper = new CoaMapper(ModFS); CulturesDB.Load(ModFS); diff --git a/ImperatorToCK3/Mappers/CoA/CoaMapper.cs b/ImperatorToCK3/Mappers/CoA/CoaMapper.cs index 90bd7b51e..ec704bb5d 100644 --- a/ImperatorToCK3/Mappers/CoA/CoaMapper.cs +++ b/ImperatorToCK3/Mappers/CoA/CoaMapper.cs @@ -1,6 +1,7 @@ using commonItems; using commonItems.Mods; using System.Collections.Generic; +using System.Linq; namespace ImperatorToCK3.Mappers.CoA; @@ -22,9 +23,27 @@ private void RegisterKeys(Parser parser) { parser.RegisterRegex(CommonRegexes.Catchall, (reader, flagName) => coasMap[flagName] = reader.GetStringOfItem().ToString()); } - public string? GetCoaForFlagName(string impFlagName) { - bool contains = coasMap.TryGetValue(impFlagName, out string? value); - return contains ? value : null; + public void ParseCoAs(IEnumerable coaDefinitionStrings) { + var parser = new Parser(); + RegisterKeys(parser); + foreach (var coaDefinitionString in coaDefinitionStrings) { + parser.ParseStream(new BufferedReader(coaDefinitionString)); + } + } + + public string? GetCoaForFlagName(string irFlagName) { + if (!coasMap.TryGetValue(irFlagName, out string? value)) { + Logger.Warn($"No CoA defined for flag name {irFlagName}."); + return null; + } + + return value; + } + + /// For a given collection of flag names, returns ones that don't have a defined CoA. + public ISet GetAllMissingFlagKeys(IEnumerable flagKeys) { + var existingFlagKeys = coasMap.Keys.ToHashSet(); + return flagKeys.Where(flagKey => !existingFlagKeys.Contains(flagKey)).ToHashSet(); } private readonly Dictionary coasMap = new(); From fb51af59c5cc0657a0640ac2068e7710859f2ba6 Mon Sep 17 00:00:00 2001 From: Seweryn Presnal Date: Sat, 29 Jun 2024 13:54:06 +0200 Subject: [PATCH 6/9] Fix regex for reading CoAs from game.log --- ImperatorToCK3/Imperator/World.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ImperatorToCK3/Imperator/World.cs b/ImperatorToCK3/Imperator/World.cs index a61098557..4b46aaa46 100644 --- a/ImperatorToCK3/Imperator/World.cs +++ b/ImperatorToCK3/Imperator/World.cs @@ -242,8 +242,8 @@ private void ReadCoatsOfArmsFromGameLog(string imperatorDocPath) { } content = content.Substring(startIndex); - string pattern = @"\S+=\s*\{[^}]*\}"; - MatchCollection matches = Regex.Matches(content, pattern); + string pattern = @"^\S+=\s*\{[\s\S]*?^\}"; + MatchCollection matches = Regex.Matches(content, pattern, RegexOptions.Multiline); CoaMapper.ParseCoAs(matches.Select(match => match.Value)); } From f797f0bb1331eba4628993ef3abb9eb60a657dd4 Mon Sep 17 00:00:00 2001 From: Seweryn Presnal Date: Sat, 29 Jun 2024 13:59:00 +0200 Subject: [PATCH 7/9] remove unused using --- ImperatorToCK3/Imperator/World.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/ImperatorToCK3/Imperator/World.cs b/ImperatorToCK3/Imperator/World.cs index 4b46aaa46..0024f9759 100644 --- a/ImperatorToCK3/Imperator/World.cs +++ b/ImperatorToCK3/Imperator/World.cs @@ -21,7 +21,6 @@ using ImperatorToCK3.Imperator.States; using ImperatorToCK3.Mappers.CoA; using ImperatorToCK3.Mappers.Region; -using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Collections.Immutable; From 246dcdfb18eabfdd0c5889dc5969d7bc3d386023 Mon Sep 17 00:00:00 2001 From: Seweryn Presnal Date: Sat, 29 Jun 2024 14:35:16 +0200 Subject: [PATCH 8/9] Tweak for Codacy --- ImperatorToCK3/Imperator/World.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ImperatorToCK3/Imperator/World.cs b/ImperatorToCK3/Imperator/World.cs index 0024f9759..4c0e8e47f 100644 --- a/ImperatorToCK3/Imperator/World.cs +++ b/ImperatorToCK3/Imperator/World.cs @@ -184,7 +184,7 @@ private void LaunchImperatorToExportCountryFlags(Configuration config) { imperatorProcess.Exited += HandleImperatorProcessExit(config, imperatorProcess); // Make sure that if converter is closed, Imperator is closed as well. - AppDomain.CurrentDomain.ProcessExit += (sender, args) => { + AppDomain.CurrentDomain.ProcessExit += (_, args) => { if (!imperatorProcess.HasExited) { imperatorProcess.Kill(); } @@ -214,7 +214,7 @@ private void LaunchImperatorToExportCountryFlags(Configuration config) { } private static EventHandler HandleImperatorProcessExit(Configuration config, Process imperatorProcess) { - return (sender, args) => { + return (_, args) => { Logger.Debug($"Imperator process exited with code {imperatorProcess.ExitCode}. Removing temporary mod files..."); try { File.Delete(Path.Combine(config.ImperatorDocPath, "mod/coa_export_mod.mod")); From 5997d4277270528ebf355fb3f3ce6970df964f31 Mon Sep 17 00:00:00 2001 From: Seweryn Presnal Date: Sat, 29 Jun 2024 14:40:17 +0200 Subject: [PATCH 9/9] Tweak for Codacy --- ImperatorToCK3/Imperator/World.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ImperatorToCK3/Imperator/World.cs b/ImperatorToCK3/Imperator/World.cs index 4c0e8e47f..2821a1136 100644 --- a/ImperatorToCK3/Imperator/World.cs +++ b/ImperatorToCK3/Imperator/World.cs @@ -184,7 +184,7 @@ private void LaunchImperatorToExportCountryFlags(Configuration config) { imperatorProcess.Exited += HandleImperatorProcessExit(config, imperatorProcess); // Make sure that if converter is closed, Imperator is closed as well. - AppDomain.CurrentDomain.ProcessExit += (_, args) => { + AppDomain.CurrentDomain.ProcessExit += (_, _) => { if (!imperatorProcess.HasExited) { imperatorProcess.Kill(); } @@ -214,7 +214,7 @@ private void LaunchImperatorToExportCountryFlags(Configuration config) { } private static EventHandler HandleImperatorProcessExit(Configuration config, Process imperatorProcess) { - return (_, args) => { + return (_, _) => { Logger.Debug($"Imperator process exited with code {imperatorProcess.ExitCode}. Removing temporary mod files..."); try { File.Delete(Path.Combine(config.ImperatorDocPath, "mod/coa_export_mod.mod"));