From 7f0e0631b5b80c5c262bae00fb842f86665150ea Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 10 Feb 2025 16:18:40 -0600 Subject: [PATCH] Import citizen types from the BIC file This allows us to know about things like entertainers, tax collectors, etc. While here I noticed that one of the tests wasn't actually doing anything, so I fixed the path issue and added an assertion to catch similar issues in the future. #541 --- C7/Text/c7-static-map-save.json | 82 +++++++++++++++++++++++++++++++++ C7GameData/CitizenType.cs | 45 ++++++++++++++++++ C7GameData/GameData.cs | 1 + C7GameData/ImportCiv3.cs | 30 ++++++++++++ C7GameData/Save/SaveGame.cs | 3 ++ C7GameDataTests/SaveTest.cs | 13 +++++- 6 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 C7GameData/CitizenType.cs diff --git a/C7/Text/c7-static-map-save.json b/C7/Text/c7-static-map-save.json index bdef05ac..3865fdbb 100644 --- a/C7/Text/c7-static-map-save.json +++ b/C7/Text/c7-static-map-save.json @@ -64533,5 +64533,87 @@ "tech-44" ] } + ], + "citizenTypes": [ + { + "id": "CitizenType-1", + "isDefaultCitizen": true, + "specialistIndex": 0, + "singularName": "Laborer", + "civilopediaEntry": "CTZN_Laborer", + "pluralName": "Laborers", + "luxuries": 0, + "research": 0, + "taxes": 0, + "corruption": 0, + "construction": 0 + }, + { + "id": "CitizenType-2", + "isDefaultCitizen": false, + "specialistIndex": 1, + "singularName": "Entertainer", + "civilopediaEntry": "CTZN_Entertainer", + "pluralName": "Entertainers", + "luxuries": 1, + "research": 0, + "taxes": 0, + "corruption": 0, + "construction": 0 + }, + { + "id": "CitizenType-3", + "isDefaultCitizen": false, + "specialistIndex": 2, + "singularName": "Tax Collector", + "civilopediaEntry": "CTZN_Tax_Collector", + "pluralName": "Tax Collectors", + "luxuries": 0, + "research": 0, + "taxes": 2, + "corruption": 0, + "construction": 0 + }, + { + "id": "CitizenType-4", + "isDefaultCitizen": false, + "specialistIndex": 3, + "singularName": "Scientist", + "civilopediaEntry": "CTZN_Scientist", + "pluralName": "Scientists", + "luxuries": 0, + "research": 3, + "taxes": 0, + "corruption": 0, + "construction": 0 + }, + { + "id": "CitizenType-5", + "isDefaultCitizen": false, + "specialistIndex": 4, + "singularName": "Policeman", + "civilopediaEntry": "CTZN_Laborer", + "pluralName": "Policemen", + "prerequisiteTech": "tech-44", + "luxuries": 0, + "research": 0, + "taxes": 0, + "corruption": 1, + "construction": 0 + }, + { + "id": "CitizenType-6", + "isDefaultCitizen": false, + "specialistIndex": 5, + "singularName": "Civil Engineer", + "civilopediaEntry": "CTZN_Laborer", + "pluralName": "Civil Engineers", + "prerequisiteTech": "tech-58", + "luxuries": 0, + "research": 0, + "taxes": 0, + "corruption": 0, + "construction": 2 + } ] } diff --git a/C7GameData/CitizenType.cs b/C7GameData/CitizenType.cs new file mode 100644 index 00000000..0e296175 --- /dev/null +++ b/C7GameData/CitizenType.cs @@ -0,0 +1,45 @@ +using System; +using System.Runtime.InteropServices; + +namespace C7GameData { + public class CitizenType { + public ID Id; + + // Should this citizen be the default? + public bool IsDefaultCitizen; + + // If !IsDefaultCitizen, the index of this specialist, for looking up in + // popHeads.pcx. 0 is the first non-laborer row. + public int SpecialistIndex; + + // Like "Laborer" or "Scientist" + public string SingularName; + + public string CivilopediaEntry; + + // Like "Laborers" or "Scientists" + public string PluralName; + + // If non-null, the tech needed to use this citizen type. + public ID PrerequisiteTech; + + // The contribution, in gold per turn, that this citizen makes towards + // luxuries/happiness. + public int Luxuries; + + // The contribution, in beakers, that this citizen makes towards teching + public int Research; + + // The contribution, in gold per turn, that this citizen makes towards + // the treasury. + public int Taxes; + + // TODO: Figure out the details of how corruption is reduced by + // policemen. + public int Corruption; + + // The contribution, in shields per turn, that this citizen makes + // towards production. + public int Construction; + } +} diff --git a/C7GameData/GameData.cs b/C7GameData/GameData.cs index 45f3f601..15c8e880 100644 --- a/C7GameData/GameData.cs +++ b/C7GameData/GameData.cs @@ -22,6 +22,7 @@ public class GameData { public List experienceLevels = new List(); public List techs = new(); + public List citizenTypes = new(); public string defaultExperienceLevelKey; public ExperienceLevel defaultExperienceLevel; diff --git a/C7GameData/ImportCiv3.cs b/C7GameData/ImportCiv3.cs index 919e8b72..8347d73b 100644 --- a/C7GameData/ImportCiv3.cs +++ b/C7GameData/ImportCiv3.cs @@ -51,6 +51,7 @@ private void ImportSharedBiqData() { // save.ScenarioSearchPath = biq?.Game[0].ScenarioSearchFolders; ImportBarbarianInfo(); ImportTechs(); + ImportCitizenTypes(); } public static SaveGame ImportSav(string savePath, string defaultBicPath, Func getPediaIconsPath) { @@ -768,6 +769,35 @@ private void ImportTechs() { } } + private void ImportCitizenTypes() { + BiqData theBiq = biq.Ctzn is null ? defaultBiq : biq; + + for (int i = 0; i < theBiq.Ctzn.Length; ++i) { + CTZN c = theBiq.Ctzn[i]; + + CitizenType ct = new() { + Id = ids.CreateID("CitizenType"), + IsDefaultCitizen = c.DefaultCitizen == 1, + SingularName = c.SingularName, + CivilopediaEntry = c.CivilopediaEntry, + PluralName = c.PluralName, + Luxuries = c.Luxuries, + Research = c.Research, + Taxes = c.Taxes, + Corruption = c.Corruption, + Construction = c.Construction + }; + if (!ct.IsDefaultCitizen) { + ct.SpecialistIndex = i; + } + if (c.Prerequisite > -1) { + ct.PrerequisiteTech = save.Techs[c.Prerequisite].id; + } + + save.CitizenTypes.Add(ct); + } + } + private static void SetWorldWrap(SavData civ3Save, SaveGame save) { if (civ3Save is not null && civ3Save.Wrld.Height > 0 && civ3Save.Wrld.Width > 0) { save.Map.wrapHorizontally = civ3Save.Wrld.XWrapping; diff --git a/C7GameData/Save/SaveGame.cs b/C7GameData/Save/SaveGame.cs index 6b644d84..f12525ee 100644 --- a/C7GameData/Save/SaveGame.cs +++ b/C7GameData/Save/SaveGame.cs @@ -72,6 +72,7 @@ public static SaveGame FromGameData(GameData data) { save.ScenarioSearchPath = data.scenarioSearchPath; save.DefaultExperienceLevel = data.defaultExperienceLevelKey; save.Techs = data.techs.ConvertAll(t => t.ToSaveTech()); + save.CitizenTypes = data.citizenTypes; return save; } @@ -94,6 +95,7 @@ public GameData ToGameData() { unitPrototypes = UnitPrototypes.ToDictionary(up => up.name), scenarioSearchPath = ScenarioSearchPath, civilizations = Civilizations, + citizenTypes = CitizenTypes, ids = new ID.Factory(this), }; // units and cities are empty @@ -198,6 +200,7 @@ public GameData ToGameData() { public List StrengthBonuses = new List(); public Dictionary HealRates = new Dictionary(); public List Techs = new(); + public List CitizenTypes = new(); public string ScenarioSearchPath; // TODO: what is this public void Save(string path) { byte[] json = JsonSerializer.SerializeToUtf8Bytes(this, JsonOptions); diff --git a/C7GameDataTests/SaveTest.cs b/C7GameDataTests/SaveTest.cs index 86f59f46..83e3fa45 100644 --- a/C7GameDataTests/SaveTest.cs +++ b/C7GameDataTests/SaveTest.cs @@ -80,14 +80,22 @@ public void SimpleSave() { [Fact] public async void LoadSampleSaves() { + // When running the tests via github actions, civ3 isn't installed so we + // can't load the default bic. + // + // See https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables + // for a full list of env vars. + string is_on_github = System.Environment.GetEnvironmentVariable("CI"); + if (is_on_github != null) { return; } + string savesPath = getDataPath("saves"); Directory.CreateDirectory(savesPath); - string sampleSavPath = Path.Combine(testDirectory, "data", "12345.SAV"); + string sampleSavPath = Path.Combine(savesPath, "12345.SAV"); if (GetMd5FileHash(sampleSavPath) != "d34dd19a76eaebe26d29d73132c2fa60") { using HttpClient client = new(); byte[] fileData = await client.GetByteArrayAsync("https://drive.usercontent.google.com/download?id=1QlIavkLtPZEIv1kHK9sO0fY2yp3o2si7&confirm=y"); - File.WriteAllBytes(Path.Combine(testDirectory, "data", "12345.SAV"), fileData); + File.WriteAllBytes(Path.Combine(savesPath, "12345.SAV"), fileData); } IEnumerable saveFiles = new DirectoryInfo(savesPath).EnumerateFiles("*.SAV"); @@ -110,6 +118,7 @@ public async void LoadSampleSaves() { game.Save(Path.Combine(testDirectory, "data", "output", $"gotm_save_{i}.json")); i++; } + Assert.True(i > 0); } [Fact]