From 21d88d24dca1fd018771db0d992e83664f6a4db9 Mon Sep 17 00:00:00 2001 From: Paul Cento Date: Wed, 20 Sep 2023 00:05:17 -0400 Subject: [PATCH 1/7] validate round-trip json --- C7Engine/SaveManager.cs | 30 ++++---- C7GameData/GameData.cs | 1 - C7GameData/Save/SaveGame.cs | 69 ++++++++++++++---- C7GameData/SaveFormat.cs | 140 ------------------------------------ C7GameDataTests/SaveTest.cs | 32 ++++++--- 5 files changed, 92 insertions(+), 180 deletions(-) delete mode 100644 C7GameData/SaveFormat.cs diff --git a/C7Engine/SaveManager.cs b/C7Engine/SaveManager.cs index f87b7fc8..a0c9372b 100644 --- a/C7Engine/SaveManager.cs +++ b/C7Engine/SaveManager.cs @@ -1,47 +1,47 @@ -namespace C7Engine -{ - using System.IO; - using C7GameData; - using C7GameData.Save; +using System.IO; +using C7GameData; +using C7GameData.Save; +using QueryCiv3.Sav; + +namespace C7Engine { enum SaveFileFormat { Sav, Biq, C7, + C7Zip, Invalid, } // The engine performs all save file creating, reading, and updating // via the SaveManager - public static class SaveManager - { - private static SaveFileFormat getFileFormat(string path) - { + public static class SaveManager { + private static SaveFileFormat getFileFormat(string path) { return Path.GetExtension(path).ToUpper() switch { ".SAV" => SaveFileFormat.Sav, ".BIQ" => SaveFileFormat.Biq, ".JSON" => SaveFileFormat.C7, - ".ZIP" => SaveFileFormat.C7, + ".ZIP" => SaveFileFormat.C7Zip, _ => SaveFileFormat.Invalid, }; } // Load and initialize a save - public static SaveGame LoadSave(string path, string bicPath) - { + public static SaveGame LoadSave(string path, string bicPath) { SaveGame save = getFileFormat(path) switch { SaveFileFormat.Sav => ImportCiv3.ImportSav(path, bicPath), SaveFileFormat.Biq => ImportCiv3.ImportBiq(path, bicPath), - SaveFileFormat.C7 => SaveGame.Load(path), + SaveFileFormat.C7 => SaveGame.Load(path, SaveCompression.None), + SaveFileFormat.C7Zip => SaveGame.Load(path, SaveCompression.Zip), _ => throw new FileLoadException("invalid save format"), }; return save; } - public static void Save(string path) { + public static void Save(string path, SaveCompression compression) { GameData gameData = EngineStorage.gameData; SaveGame save = SaveGame.FromGameData(gameData); - save.Save(path); + save.Save(path, compression); } } diff --git a/C7GameData/GameData.cs b/C7GameData/GameData.cs index 34e1493b..296e7773 100644 --- a/C7GameData/GameData.cs +++ b/C7GameData/GameData.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Text.Json.Serialization; using Serilog; namespace C7GameData diff --git a/C7GameData/Save/SaveGame.cs b/C7GameData/Save/SaveGame.cs index d0bacd48..ec7a0bd2 100644 --- a/C7GameData/Save/SaveGame.cs +++ b/C7GameData/Save/SaveGame.cs @@ -1,6 +1,7 @@ using System.Collections; using System.Collections.Generic; using System.IO; +using System.IO.Compression; using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; @@ -23,9 +24,12 @@ public static void IgnoreDefaultValues(JsonTypeInfo jsonTypeInfo) { } } } + public enum SaveCompression { + None, + Zip, + } public class SaveGame { - private static JsonSerializerOptions JsonOptions { get => new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, @@ -59,18 +63,22 @@ public static SaveGame FromGameData(GameData data) { Players = data.players.ConvertAll(player => new SavePlayer(player)), Cities = data.cities.ConvertAll(city => new SaveCity(city)), ExperienceLevels = data.experienceLevels, + StrengthBonuses = new(){ + data.fortificationBonus, + data.riverCrossingBonus, + data.cityLevel1DefenseBonus, + data.cityLevel2DefenseBonus, + data.cityLevel3DefenseBonus, + }, + HealRates = new(){ + {"friendly_field", data.healRateInFriendlyField}, + {"neutral_field", data.healRateInNeutralField}, + {"hostile_field", data.healRateInHostileField}, + {"city", data.healRateInCity}, + }, + ScenarioSearchPath = data.scenarioSearchPath, + DefaultExperienceLevel = data.defaultExperienceLevelKey, }; - save.StrengthBonuses.Add(data.fortificationBonus); - save.StrengthBonuses.Add(data.riverCrossingBonus); - save.StrengthBonuses.Add(data.cityLevel1DefenseBonus); - save.StrengthBonuses.Add(data.cityLevel2DefenseBonus); - save.StrengthBonuses.Add(data.cityLevel3DefenseBonus); - save.HealRates["friendly_field"] = data.healRateInFriendlyField; - save.HealRates["neutral_field"] = data.healRateInNeutralField; - save.HealRates["hostile_field"] = data.healRateInHostileField; - save.HealRates["city"] = data.healRateInCity; - save.ScenarioSearchPath = data.scenarioSearchPath; - save.DefaultExperienceLevel = data.defaultExperienceLevelKey; return save; } @@ -177,13 +185,44 @@ public GameData ToGameData() { public List StrengthBonuses = new List(); public Dictionary HealRates = new Dictionary(); public string ScenarioSearchPath; // TODO: what is this - public void Save(string path) { - byte[] json = JsonSerializer.SerializeToUtf8Bytes(this, JsonOptions); + + private void SaveJson(SaveGame save, string path) { + byte[] json = JsonSerializer.SerializeToUtf8Bytes(save, JsonOptions); File.WriteAllBytes(path, json); } - public static SaveGame Load(string path) { + private void SaveZip(SaveGame save, string path) { + byte[] json = JsonSerializer.SerializeToUtf8Bytes(save, JsonOptions); + using MemoryStream zipStream = new(); + using ZipArchive archive = new(zipStream, ZipArchiveMode.Create); + ZipArchiveEntry entry = archive.CreateEntry("save"); + using Stream stream = entry.Open(); + stream.Write(json, 0, json.Length); + archive.Dispose(); + File.WriteAllBytes(path, zipStream.ToArray()); + } + + public void Save(string path, SaveCompression compression) { + if (compression == SaveCompression.None) { + SaveJson(this, path); + } else { + SaveZip(this, path); + } + } + + private static SaveGame LoadJson(string path) { return JsonSerializer.Deserialize(File.ReadAllText(path), JsonOptions); } + + private static SaveGame LoadZip(string path) { + using ZipArchive archive = new(new FileStream(path, FileMode.Open), ZipArchiveMode.Read); + ZipArchiveEntry entry = archive.GetEntry("save"); + using Stream stream = entry.Open(); + return JsonSerializer.Deserialize(stream, JsonOptions); + } + + public static SaveGame Load(string path, SaveCompression compression) { + return compression == SaveCompression.None ? LoadJson(path) : LoadZip(path); + } } } diff --git a/C7GameData/SaveFormat.cs b/C7GameData/SaveFormat.cs deleted file mode 100644 index e16c4f14..00000000 --- a/C7GameData/SaveFormat.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System.Linq; - -namespace C7GameData -/* - The save format is intended to be serialized to JSON upon saving - and deserialized from JSON upon loading. - - The names are capitalized per C#, but I intend to use JsonSerializer - settings to use camel case instead, unless there is reason not to. -*/ -{ - using System; - using System.Collections.Generic; - using System.IO; - using System.IO.Compression; - using System.Text.Json; - - public enum SaveCompression { - None, - Zip, - Invalid, - } - - public class C7SaveFormat { - public string Version = "v0.0early-prototype"; - - // This naming is probably bad form, but it makes sense to me to name it as such here - public GameData GameData; - - public C7SaveFormat() { - GameData = new GameData(); - } - - public C7SaveFormat(GameData gameData) { - GameData = gameData; - } - - public bool PostLoadProcess() { - GameData.PerformPostLoadActions(); - - return true; - } - - static SaveCompression getCompression(string path) { - var ext = Path.GetExtension(path); - if (ext.Equals(".JSON", StringComparison.CurrentCultureIgnoreCase)) { - return SaveCompression.None; - } else if (ext.Equals(".ZIP", StringComparison.CurrentCultureIgnoreCase)) { - return SaveCompression.Zip; - } - return SaveCompression.Invalid; - } - - public static C7SaveFormat Load(string path) { - SaveCompression format = getCompression(path); - C7SaveFormat save = null; - if (format == SaveCompression.None) { - save = JsonSerializer.Deserialize(File.ReadAllText(path), JsonOptions); - } else { - using (var archive = new ZipArchive(new FileStream(path, FileMode.Open), ZipArchiveMode.Read)) { - ZipArchiveEntry entry = archive.GetEntry("save"); - using (Stream stream = entry.Open()) { - save = JsonSerializer.Deserialize(stream, JsonOptions); - } - } - } - - // Inflate things that are stored by reference, first tiles - foreach (Tile tile in save.GameData.map.tiles) { - if (tile.ResourceKey == "NONE") { - tile.Resource = Resource.NONE; - } else { - tile.Resource = save.GameData.Resources.Find(r => r.Key == tile.ResourceKey); - } - tile.baseTerrainType = save.GameData.terrainTypes.Find(t => t.Key == tile.baseTerrainTypeKey); - tile.overlayTerrainType = save.GameData.terrainTypes.Find(t => t.Key == tile.overlayTerrainTypeKey); - } - - // Inflate experience levels - Dictionary levelsByKey = new Dictionary(); - foreach (ExperienceLevel eL in save.GameData.experienceLevels) { - levelsByKey.Add(eL.key, eL); - } - save.GameData.defaultExperienceLevel = levelsByKey[save.GameData.defaultExperienceLevelKey]; - foreach (MapUnit unit in save.GameData.mapUnits) { - unit.experienceLevel = levelsByKey[unit.experienceLevelKey]; - } - - //Inflate barbarian info - List prototypes = save.GameData.unitPrototypes.Values.ToList(); - save.GameData.barbarianInfo.basicBarbarian = - prototypes[save.GameData.barbarianInfo.basicBarbarianIndex]; - save.GameData.barbarianInfo.advancedBarbarian = - prototypes[save.GameData.barbarianInfo.advancedBarbarianIndex]; - save.GameData.barbarianInfo.barbarianSeaUnit = - prototypes[save.GameData.barbarianInfo.barbarianSeaUnitIndex]; - - return save; - } - - public static void Save(C7SaveFormat save, string path) { - SaveCompression format = getCompression(path); - byte[] json = JsonSerializer.SerializeToUtf8Bytes(save, JsonOptions); - if (format == SaveCompression.Zip) { - using (var zipStream = new MemoryStream()) { - var archive = new ZipArchive(zipStream, ZipArchiveMode.Create); - ZipArchiveEntry entry = archive.CreateEntry("save"); - using (Stream stream = entry.Open()) { - stream.Write(json, 0, json.Length); - } - // ZipArchive needs to be disposed in order for its content - // to be written to the MemoryStream - // https://stackoverflow.com/questions/12347775/ziparchive-creates-invalid-zip-file#12350106 - archive.Dispose(); - File.WriteAllBytes(path, zipStream.ToArray()); - } - } else { - File.WriteAllBytes(path, json); - } - } - - public static JsonSerializerOptions JsonOptions { - get => new JsonSerializerOptions { - // Lower-case the first letter in JSON because JSON naming standards - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - // Pretty print during development; may change this for production - WriteIndented = true, - // By default it only serializes getters, this makes it serialize fields, too - IncludeFields = true, - - Converters = { - // Serialize 2D array types - new Json2DArrayConverter(), - new IDJsonConverter(), - }, - }; - } - - } -} diff --git a/C7GameDataTests/SaveTest.cs b/C7GameDataTests/SaveTest.cs index 09719db1..f36a0c1b 100644 --- a/C7GameDataTests/SaveTest.cs +++ b/C7GameDataTests/SaveTest.cs @@ -42,12 +42,12 @@ public void SimpleSave() string developerSave = getBasePath("../C7/Text/c7-static-map-save.json"); - SaveGame saveNeverGameData = SaveGame.Load(developerSave); + SaveGame saveNeverGameData = SaveGame.Load(developerSave, SaveCompression.None); - saveNeverGameData.Save(outputNeverGameDataPath); + saveNeverGameData.Save(outputNeverGameDataPath, SaveCompression.None); GameData gameData = saveNeverGameData.ToGameData(); SaveGame saveWasGameData = SaveGame.FromGameData(gameData); - saveWasGameData.Save(outputWasGameDataPath); + saveWasGameData.Save(outputWasGameDataPath, SaveCompression.None); byte[] original = File.ReadAllBytes(developerSave); byte[] savedNeverGameData = File.ReadAllBytes(outputNeverGameDataPath); @@ -60,13 +60,27 @@ public void SimpleSave() // saved files should be the same as the original Assert.Equal(original, savedNeverGameData); - // TODO: Currently the order of the properties in the json is different, - // so this fails. For testing, it would be convenient to sort SaveGame - // fields alphabetically before serializing to json. - Assert.Equal(original, savedWasGameData); } + [Fact] + public void RoundTripZip() { + string zipOutput = getDataPath("output/round-trip-zip.zip"); + string decompressedOutput = getDataPath("output/round-trip-zip.json"); + string developerSave = getBasePath("../C7/Text/c7-static-map-save.json"); + SaveGame saveGame = SaveGame.Load(developerSave, SaveCompression.None); + saveGame.Save(zipOutput, SaveCompression.Zip); + SaveGame loadedFromZip = SaveGame.Load(zipOutput, SaveCompression.Zip); + loadedFromZip.Save(decompressedOutput, SaveCompression.None); + + byte[] original = File.ReadAllBytes(developerSave); + byte[] decompressed = File.ReadAllBytes(decompressedOutput); + + Assert.NotEmpty(original); + Assert.NotEmpty(decompressed); + Assert.Equal(original, decompressed); + } + [Fact] public void LoadGOTMWinners() { string path = getDataPath("gotm"); @@ -90,7 +104,7 @@ public void LoadGOTMWinners() { Assert.Null(ex); Assert.NotNull(game); Assert.NotNull(gd); - game.Save(Path.Combine(testDirectory, "data", "output", $"gotm_save_{i}.json")); + game.Save(Path.Combine(testDirectory, "data", "output", $"gotm_save_{i}.json"), SaveCompression.None); i++; } } @@ -122,7 +136,7 @@ public void LoadAllConquests() { Assert.Null(ex); Assert.NotNull(game); Assert.NotNull(gd); - game.Save(Path.Combine(testDirectory, "data", "output", $"conquest_{name[0]}.json")); + game.Save(Path.Combine(testDirectory, "data", "output", $"conquest_{name[0]}.json"), SaveCompression.None); } } } From da84de1f753e1e8560a7482f9aaa2e2b3d280ca2 Mon Sep 17 00:00:00 2001 From: Paul Cento Date: Sun, 24 Sep 2023 21:27:32 -0400 Subject: [PATCH 2/7] ctrl + s saves game --- .gitignore | 2 ++ C7/Game.cs | 30 +++++++++++++++---------- C7/project.godot | 5 +++++ C7Engine/EntryPoints/CreateGame.cs | 2 +- C7Engine/EntryPoints/MessageToEngine.cs | 17 +++++++++++--- C7Engine/EntryPoints/MessageToUI.cs | 2 ++ C7Engine/SaveManager.cs | 1 - C7GameData/Actions.cs | 1 + _Console/BuildDevSave/Program.cs | 2 +- 9 files changed, 44 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index 3635d19d..548b3fbd 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,8 @@ project.lock.json C7.ini log.txt *.csproj.old* +# testing save.json save file +C7/Text/save.json # Build results [Dd]ebug/ diff --git a/C7/Game.cs b/C7/Game.cs index b9c90b26..f837d80d 100644 --- a/C7/Game.cs +++ b/C7/Game.cs @@ -417,6 +417,10 @@ private void processActions() { this.OnPlayerEndTurn(); } + if (Input.IsActionJustPressed(C7Action.SaveGame)) { + OnSaveGame("./Text/save.json"); + } + if (this.HasCurrentlySelectedUnit()) { // TODO: replace bool with an invalid TileDirection enum TileDirection dir = TileDirection.NORTH; @@ -472,10 +476,9 @@ private void processActions() { } if (Input.IsActionJustPressed(C7Action.UnitWait)) { - using (var gameDataAccess = new UIGameDataAccess()) { - UnitInteractions.waitUnit(gameDataAccess.gameData, CurrentlySelectedUnit.id); - GetNextAutoselectedUnit(gameDataAccess.gameData); - } + using UIGameDataAccess gameDataAccess = new(); + UnitInteractions.waitUnit(gameDataAccess.gameData, CurrentlySelectedUnit.id); + GetNextAutoselectedUnit(gameDataAccess.gameData); } if (Input.IsActionJustPressed(C7Action.UnitFortify)) { @@ -507,14 +510,12 @@ private void processActions() { } if (Input.IsActionJustPressed(C7Action.UnitBuildCity) && CurrentlySelectedUnit.canBuildCity()) { - using (var gameDataAccess = new UIGameDataAccess()) { - MapUnit currentUnit = gameDataAccess.gameData.GetUnit(CurrentlySelectedUnit.id); - log.Debug(currentUnit.Describe()); - if (currentUnit.canBuildCity()) { - PopupOverlay popupOverlay = GetNode(PopupOverlay.NodePath); - popupOverlay.ShowPopup(new BuildCityDialog(controller.GetNextCityName()), - PopupOverlay.PopupCategory.Advisor); - } + using UIGameDataAccess gameDataAccess = new(); + MapUnit currentUnit = gameDataAccess.gameData.GetUnit(CurrentlySelectedUnit.id); + log.Debug(currentUnit.Describe()); + if (currentUnit.canBuildCity()) { + PopupOverlay popupOverlay = GetNode(PopupOverlay.NodePath); + popupOverlay.ShowPopup(new BuildCityDialog(controller.GetNextCityName()), PopupOverlay.PopupCategory.Advisor); } } @@ -557,4 +558,9 @@ public override void _Notification(int what) { private void OnBuildCity(string name) { new MsgBuildCity(CurrentlySelectedUnit.id, name).send(); } + + private void OnSaveGame(string path) { + log.Debug($"Saving game to {path}"); + new MsgSaveGame(path).send(); + } } diff --git a/C7/project.godot b/C7/project.godot index 023bff70..40e8ed11 100644 --- a/C7/project.godot +++ b/C7/project.godot @@ -183,6 +183,11 @@ unit_build_road={ "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":82,"key_label":0,"unicode":114,"echo":false,"script":null) ] } +save_game={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":true,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":0,"echo":false,"script":null) +] +} [mono] diff --git a/C7Engine/EntryPoints/CreateGame.cs b/C7Engine/EntryPoints/CreateGame.cs index de8ac4c5..f54e6ed0 100644 --- a/C7Engine/EntryPoints/CreateGame.cs +++ b/C7Engine/EntryPoints/CreateGame.cs @@ -19,7 +19,7 @@ public static Player createGame(string loadFilePath, string defaultBicPath) EngineStorage.createThread(); EngineStorage.gameDataMutex.WaitOne(); - SaveGame save = SaveGame.Load(loadFilePath); + SaveGame save = SaveGame.Load(loadFilePath, SaveCompression.None); GameData gameData = save.ToGameData(); EngineStorage.gameData = gameData; diff --git a/C7Engine/EntryPoints/MessageToEngine.cs b/C7Engine/EntryPoints/MessageToEngine.cs index 2cb7f1c4..b07999da 100644 --- a/C7Engine/EntryPoints/MessageToEngine.cs +++ b/C7Engine/EntryPoints/MessageToEngine.cs @@ -1,10 +1,9 @@ using Serilog; +using C7GameData; +using C7GameData.Save; namespace C7Engine { - using System; - using C7GameData; - public abstract class MessageToEngine { public abstract void process(); @@ -206,4 +205,16 @@ public override void process() EngineStorage.animationsEnabled = enabled; } } + + public class MsgSaveGame : MessageToEngine { + private string path; + + public MsgSaveGame(string path) { + this.path = path; + } + + public override void process() { + SaveManager.Save(this.path, SaveCompression.None); + } + } } diff --git a/C7Engine/EntryPoints/MessageToUI.cs b/C7Engine/EntryPoints/MessageToUI.cs index 9805f13c..6ef3e7c2 100644 --- a/C7Engine/EntryPoints/MessageToUI.cs +++ b/C7Engine/EntryPoints/MessageToUI.cs @@ -59,4 +59,6 @@ public MsgTileDiscovered(Tile tile) { } } + public class MsgFinishSave : MessageToUI {} + } diff --git a/C7Engine/SaveManager.cs b/C7Engine/SaveManager.cs index a0c9372b..185468f5 100644 --- a/C7Engine/SaveManager.cs +++ b/C7Engine/SaveManager.cs @@ -1,7 +1,6 @@ using System.IO; using C7GameData; using C7GameData.Save; -using QueryCiv3.Sav; namespace C7Engine { diff --git a/C7GameData/Actions.cs b/C7GameData/Actions.cs index 939d8bff..b5a884ba 100644 --- a/C7GameData/Actions.cs +++ b/C7GameData/Actions.cs @@ -10,6 +10,7 @@ public static class C7Action { public static readonly string MoveUnitNorthwest = "move_unit_northwest"; public static readonly string MoveUnitNorth = "move_unit_north"; public static readonly string MoveUnitNortheast = "move_unit_northeast"; + public static readonly string SaveGame = "save_game"; public static readonly string ToggleAnimations = "toggle_animations"; public static readonly string ToggleGrid = "toggle_grid"; public static readonly string ToggleZoom = "toggle_zoom"; diff --git a/_Console/BuildDevSave/Program.cs b/_Console/BuildDevSave/Program.cs index ef8d51c5..837a2944 100644 --- a/_Console/BuildDevSave/Program.cs +++ b/_Console/BuildDevSave/Program.cs @@ -28,7 +28,7 @@ static void Main(string[] args) { string fullSavePath = args[0]; string outputPath = Path.Combine(C7DefaultSaveDir, "c7-static-map-save.json"); SaveGame output = ImportCiv3.ImportSav(fullSavePath, GetCiv3Path + @"/Conquests/conquests.biq"); - output.Save(outputPath); + output.Save(outputPath, SaveCompression.None); Info(outputPath, output); } } From 014a4b8a6d9d622d672e8968495aa722a1228ccf Mon Sep 17 00:00:00 2001 From: Paul Cento Date: Sun, 24 Sep 2023 21:50:57 -0400 Subject: [PATCH 3/7] load arbitrary save files --- C7/MainMenu.cs | 10 +-- C7/Util.cs | 105 +++++++++++++++-------------- C7Engine/EntryPoints/CreateGame.cs | 2 +- 3 files changed, 60 insertions(+), 57 deletions(-) diff --git a/C7/MainMenu.cs b/C7/MainMenu.cs index 2b209c32..1a213724 100644 --- a/C7/MainMenu.cs +++ b/C7/MainMenu.cs @@ -30,11 +30,13 @@ public override void _Ready() // To pass data between scenes, putting path string in a global singleton and reading it later in createGame Global = GetNode("/root/GlobalSingleton"); Global.ResetLoadGamePath(); - LoadDialog = new Util.Civ3FileDialog(); - LoadDialog.RelPath = @"Conquests/Saves"; + LoadDialog = new Util.Civ3FileDialog { + RelPath = @"Conquests/Saves" + }; LoadDialog.Connect("file_selected",new Callable(this,nameof(_on_FileDialog_file_selected))); - LoadScenarioDialog = new Util.Civ3FileDialog(); - LoadScenarioDialog.RelPath = @"Conquests/Scenarios"; + LoadScenarioDialog = new Util.Civ3FileDialog { + RelPath = @"Conquests/Scenarios" + }; LoadScenarioDialog.Connect("file_selected",new Callable(this,nameof(_on_FileDialog_file_selected))); GetNode("CanvasLayer").AddChild(LoadDialog); SetCiv3Home = GetNode