From f9889a6824100ee94ce5554827062f4e20ac1b32 Mon Sep 17 00:00:00 2001 From: IhateTrains Date: Sun, 14 Apr 2024 14:58:19 +0200 Subject: [PATCH] Technology conversion configurable through configurables/inventions_to_innovations_map.txt (#1878) part of #163 --- ImperatorToCK3/CK3/Cultures/Culture.cs | 36 ++++++++++++- .../CK3/Cultures/CultureCollection.cs | 47 +++++++++++++++++ ImperatorToCK3/CK3/World.cs | 4 +- ImperatorToCK3/Imperator/Countries/Country.cs | 6 +++ .../Imperator/Countries/CountryFactory.cs | 6 ++- .../Imperator/Inventions/InventionsDB.cs | 38 ++++++++++++++ ImperatorToCK3/Imperator/World.cs | 4 ++ ImperatorToCK3/ImperatorToCK3.csproj | 4 +- .../Mappers/Technology/InnovationBonus.cs | 35 +++++++++++++ .../Mappers/Technology/InnovationLink.cs | 23 +++++++++ .../Mappers/Technology/InnovationMapper.cs | 50 +++++++++++++++++++ ImperatorToCK3/Outputter/CulturesOutputter.cs | 15 +++++- ImperatorToCK3/Outputter/WorldOutputter.cs | 3 +- 13 files changed, 263 insertions(+), 8 deletions(-) create mode 100644 ImperatorToCK3/Imperator/Inventions/InventionsDB.cs create mode 100644 ImperatorToCK3/Mappers/Technology/InnovationBonus.cs create mode 100644 ImperatorToCK3/Mappers/Technology/InnovationLink.cs create mode 100644 ImperatorToCK3/Mappers/Technology/InnovationMapper.cs diff --git a/ImperatorToCK3/CK3/Cultures/Culture.cs b/ImperatorToCK3/CK3/Cultures/Culture.cs index 69e4adffd..7e4a3bce1 100644 --- a/ImperatorToCK3/CK3/Cultures/Culture.cs +++ b/ImperatorToCK3/CK3/Cultures/Culture.cs @@ -2,7 +2,10 @@ using commonItems.Collections; using commonItems.Colors; using commonItems.Serialization; +using ImperatorToCK3.Mappers.Technology; +using Open.Collections; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; @@ -11,7 +14,7 @@ namespace ImperatorToCK3.CK3.Cultures; public sealed class Culture : IIdentifiable, IPDXSerializable { public string Id { get; } public Color Color { get; } - public OrderedSet ParentCultureIds { get; set; } = new(); + public OrderedSet ParentCultureIds { get; set; } public Pillar Heritage { get; } public Pillar Language { get; } private readonly OrderedSet traditionIds; @@ -20,6 +23,9 @@ public sealed class Culture : IIdentifiable, IPDXSerializable { public IReadOnlyCollection NameLists => nameLists; private readonly List> attributes; public IReadOnlyCollection> Attributes => attributes; + + private readonly List innovationsFromImperator = []; + private readonly Dictionary innovationProgressesFromImperator = []; public Culture(string id, CultureData cultureData) { Id = id; @@ -63,6 +69,34 @@ public string Serialize(string indent, bool withBraces) { return sb.ToString(); } + public void OutputHistory(string outputModPath, Date date) { + if (innovationsFromImperator.Count == 0 && innovationProgressesFromImperator.Count == 0) { + // Nothing to output. + return; + } + + var historyPath = Path.Combine(outputModPath, "history/cultures", Id + ".txt"); + using var historyWriter = File.CreateText(historyPath); + historyWriter.WriteLine("# This file was generated by the IRToCK3 converter."); + + historyWriter.WriteLine($"{date} = {{"); + foreach (var innovationId in innovationsFromImperator) { + historyWriter.WriteLine($"\tdiscover_innovation = {innovationId}"); + } + foreach (var (innovationId, progress) in innovationProgressesFromImperator) { + historyWriter.WriteLine("\tadd_innovation_progress = {"); + historyWriter.WriteLine($"\t\tculture_innovation = {innovationId}"); + historyWriter.WriteLine($"\t\tprogress = {progress}"); + historyWriter.WriteLine("\t}"); + } + historyWriter.WriteLine("}"); + } + + public void ImportInnovationsFromImperator(ISet irInventions, InnovationMapper innovationMapper) { + innovationsFromImperator.AddRange(innovationMapper.GetInnovations(irInventions)); + innovationProgressesFromImperator.AddRange(innovationMapper.GetInnovationProgresses(irInventions)); + } + public IEnumerable MaleNames => NameLists.SelectMany(l => l.MaleNames); public IEnumerable FemaleNames => NameLists.SelectMany(l => l.FemaleNames); } \ No newline at end of file diff --git a/ImperatorToCK3/CK3/Cultures/CultureCollection.cs b/ImperatorToCK3/CK3/Cultures/CultureCollection.cs index 32cc09955..b69836d19 100644 --- a/ImperatorToCK3/CK3/Cultures/CultureCollection.cs +++ b/ImperatorToCK3/CK3/Cultures/CultureCollection.cs @@ -4,6 +4,11 @@ using commonItems.Mods; using Fernandezja.ColorHashSharp; using ImperatorToCK3.CommonUtils; +using ImperatorToCK3.Imperator.Countries; +using ImperatorToCK3.Imperator.Inventions; +using ImperatorToCK3.Mappers.Culture; +using ImperatorToCK3.Mappers.Province; +using ImperatorToCK3.Mappers.Technology; using System; using System.Collections.Generic; using System.Linq; @@ -151,6 +156,48 @@ public void LoadNameLists(ModFilesystem ck3ModFS) { parser.ParseGameFolder("common/culture/name_lists", ck3ModFS, "txt", recursive: true, logFilePaths: true); } + private string? GetCK3CultureIdForImperatorCountry(Country country, CultureMapper cultureMapper, ProvinceMapper provinceMapper) { + var irCulture = country.PrimaryCulture ?? country.Monarch?.Culture; + if (irCulture is null) { + Logger.Warn($"Failed to get primary or monarch culture for Imperator country {country.Tag}!"); + return null; + } + + ulong? irProvinceId = country.CapitalProvinceId ?? country.Monarch?.ProvinceId; + ulong? ck3ProvinceId = null; + if (irProvinceId.HasValue) { + ck3ProvinceId = provinceMapper.GetCK3ProvinceNumbers(irProvinceId.Value).FirstOrDefault(); + } + + return cultureMapper.Match(irCulture, ck3ProvinceId, irProvinceId, country.HistoricalTag); + } + + public void ImportTechnology(CountryCollection countries, CultureMapper cultureMapper, ProvinceMapper provinceMapper, InventionsDB inventionsDB) { // TODO: test this + Logger.Info("Converting Imperator inventions to CK3 innovations..."); + + var innovationMapper = new InnovationMapper(); + innovationMapper.LoadLinksAndBonuses("configurables/inventions_to_innovations_map.txt"); + + // Group I:R countries by corresponding CK3 culture. + var countriesByCulture = countries.Select(c => new { + Country = c, CK3CultureId = GetCK3CultureIdForImperatorCountry(c, cultureMapper, provinceMapper), + }) + .Where(c => c.CK3CultureId is not null) + .GroupBy(c => c.CK3CultureId); + + foreach (var grouping in countriesByCulture) { + if (!TryGetValue(grouping.Key!, out var culture)) { + Logger.Warn($"Can't import technology for culture {grouping.Key}: culture not found in CK3 cultures!"); + continue; + } + + var irInventions = grouping + .SelectMany(c => c.Country.GetActiveInventionIds(inventionsDB)) + .ToHashSet(); + culture.ImportInnovationsFromImperator(irInventions, innovationMapper); + } + } + private readonly IDictionary cultureReplacements = new Dictionary(); // replaced culture -> replacing culture protected readonly PillarCollection PillarCollection; diff --git a/ImperatorToCK3/CK3/World.cs b/ImperatorToCK3/CK3/World.cs index fd45f82e3..37db493fe 100644 --- a/ImperatorToCK3/CK3/World.cs +++ b/ImperatorToCK3/CK3/World.cs @@ -180,8 +180,10 @@ public World(Imperator.World impWorld, Configuration config) { Logger.Warn($"No base mapping found for I:R culture {irCultureId}!"); } } + + Cultures.ImportTechnology(impWorld.Countries, cultureMapper, provinceMapper, impWorld.InventionsDB); - var traitMapper = new TraitMapper(Path.Combine("configurables", "trait_map.txt"), ModFS); + var traitMapper = new TraitMapper("configurables/trait_map.txt", ModFS); Logger.Info("Initializing DNA factory..."); var dnaFactory = new DNAFactory(impWorld.ModFS, ModFS); diff --git a/ImperatorToCK3/Imperator/Countries/Country.cs b/ImperatorToCK3/Imperator/Countries/Country.cs index 336345f02..3917f99e2 100644 --- a/ImperatorToCK3/Imperator/Countries/Country.cs +++ b/ImperatorToCK3/Imperator/Countries/Country.cs @@ -2,6 +2,7 @@ using commonItems.Colors; using ImperatorToCK3.Imperator.Characters; using ImperatorToCK3.Imperator.Families; +using ImperatorToCK3.Imperator.Inventions; using ImperatorToCK3.Imperator.Provinces; using System.Collections.Generic; @@ -46,6 +47,7 @@ public string HistoricalTag { private readonly HashSet parsedFamilyIds = []; public IDictionary Families { get; private set; } = new Dictionary(); private readonly HashSet ownedProvinces = []; + private readonly List inventionBooleans = []; public CK3.Titles.Title? CK3Title { get; set; } @@ -103,4 +105,8 @@ public void LinkOriginCountry(CountryCollection countries) { OriginCountry = originCountry; } } + + public IEnumerable GetActiveInventionIds(InventionsDB inventionsDB) { + return inventionsDB.GetActiveInventionIds(inventionBooleans); + } } \ No newline at end of file diff --git a/ImperatorToCK3/Imperator/Countries/CountryFactory.cs b/ImperatorToCK3/Imperator/Countries/CountryFactory.cs index 5f630b55d..3ccaf11f5 100644 --- a/ImperatorToCK3/Imperator/Countries/CountryFactory.cs +++ b/ImperatorToCK3/Imperator/Countries/CountryFactory.cs @@ -1,4 +1,4 @@ -using commonItems; +using commonItems; using commonItems.Colors; using commonItems.Mods; using ImperatorToCK3.CommonUtils; @@ -86,6 +86,10 @@ static Country() { parser.RegisterKeyword("family", reader => parsedCountry.parsedFamilyIds.Add(reader.GetULong())); parser.RegisterKeyword("minor_family", reader => parsedCountry.parsedFamilyIds.Add(reader.GetULong())); parser.RegisterKeyword("monarch", reader => parsedCountry.monarchId = reader.GetULong()); + parser.RegisterKeyword("active_inventions", reader => { + parsedCountry.inventionBooleans.AddRange(reader.GetInts().Select(i => i != 0)); + }); + parser.RegisterKeyword("mark_invention", ParserHelpers.IgnoreItem); parser.RegisterKeyword("ruler_term", reader => parsedCountry.RulerTerms.Add(RulerTerm.Parse(reader))); parser.RegisterRegex(monarchyLawRegexStr, reader => parsedCountry.monarchyLaws.Add(reader.GetString())); parser.RegisterRegex(republicLawRegexStr, reader => parsedCountry.republicLaws.Add(reader.GetString())); diff --git a/ImperatorToCK3/Imperator/Inventions/InventionsDB.cs b/ImperatorToCK3/Imperator/Inventions/InventionsDB.cs new file mode 100644 index 000000000..59dd24e55 --- /dev/null +++ b/ImperatorToCK3/Imperator/Inventions/InventionsDB.cs @@ -0,0 +1,38 @@ +using commonItems; +using commonItems.Collections; +using commonItems.Mods; +using System.Collections.Generic; +using System.Linq; + +namespace ImperatorToCK3.Imperator.Inventions; + +public class InventionsDB { + private readonly OrderedSet inventions = []; + + public void LoadInventions(ModFilesystem irModFS) { + var inventionsParser = new Parser(); + inventionsParser.RegisterKeyword("technology", ParserHelpers.IgnoreItem); + inventionsParser.RegisterKeyword("color", ParserHelpers.IgnoreItem); + inventionsParser.RegisterRegex(CommonRegexes.String, (reader, inventionId) => { + inventions.Add(inventionId); + ParserHelpers.IgnoreItem(reader); + }); + inventionsParser.IgnoreAndLogUnregisteredItems(); + + var inventionGroupsParser = new Parser(); + inventionGroupsParser.RegisterRegex(CommonRegexes.String, reader => inventionsParser.ParseStream(reader)); + inventionGroupsParser.IgnoreAndLogUnregisteredItems(); + + Logger.Info("Loading Imperator inventions..."); + inventionGroupsParser.ParseGameFolder("common/inventions", irModFS, "txt", recursive: true); + } + + public IEnumerable GetActiveInventionIds(IList booleans) { + // Enumerate over the inventions and return the ones that are active (bool is true). + foreach (var item in inventions.Select((inventionId, i) => new { i, inventionId })) { + if (booleans[item.i]) { + yield return item.inventionId; + } + } + } +} \ No newline at end of file diff --git a/ImperatorToCK3/Imperator/World.cs b/ImperatorToCK3/Imperator/World.cs index 24b9c387b..a6ac8cb34 100644 --- a/ImperatorToCK3/Imperator/World.cs +++ b/ImperatorToCK3/Imperator/World.cs @@ -14,6 +14,7 @@ using ImperatorToCK3.Imperator.Cultures; using ImperatorToCK3.Imperator.Families; using ImperatorToCK3.Imperator.Geography; +using ImperatorToCK3.Imperator.Inventions; using ImperatorToCK3.Imperator.Pops; using ImperatorToCK3.Imperator.Provinces; using ImperatorToCK3.Imperator.Religions; @@ -56,6 +57,7 @@ public class World : Parser { public CulturesDB CulturesDB { get; } = new(); public ReligionCollection Religions { get; private set; } private GenesDB genesDB = new(); + public InventionsDB InventionsDB { get; } = new(); public ColorFactory ColorFactory { get; } = new(); private enum SaveType { Invalid, Plaintext, CompressedEncoded } @@ -359,6 +361,8 @@ private void LoadModFilesystemDependentData() { Logger.IncrementProgress(); Defines.LoadDefines(ModFS); + + InventionsDB.LoadInventions(ModFS); Logger.Info("Loading named colors..."); NamedColors.LoadNamedColors("common/named_colors", ModFS); diff --git a/ImperatorToCK3/ImperatorToCK3.csproj b/ImperatorToCK3/ImperatorToCK3.csproj index 0f6317e33..475077184 100644 --- a/ImperatorToCK3/ImperatorToCK3.csproj +++ b/ImperatorToCK3/ImperatorToCK3.csproj @@ -34,11 +34,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/ImperatorToCK3/Mappers/Technology/InnovationBonus.cs b/ImperatorToCK3/Mappers/Technology/InnovationBonus.cs new file mode 100644 index 000000000..c233b3f12 --- /dev/null +++ b/ImperatorToCK3/Mappers/Technology/InnovationBonus.cs @@ -0,0 +1,35 @@ +using commonItems; +using System.Collections.Generic; +using System.Linq; + +namespace ImperatorToCK3.Mappers.Technology; + +public sealed class InnovationBonus { // TODO: add tests + private readonly HashSet imperatorInventions = []; + private string? ck3Innovation; + + public InnovationBonus(BufferedReader bonusReader) { + var parser = new Parser(); + parser.RegisterKeyword("ir", reader => imperatorInventions.Add(reader.GetString())); + parser.RegisterKeyword("ck3", reader => ck3Innovation = reader.GetString()); + parser.IgnoreAndLogUnregisteredItems(); + parser.ParseStream(bonusReader); + + // A bonus should have at most 3 inventions. + if (imperatorInventions.Count > 3) { + Logger.Warn($"Innovation bonus for {ck3Innovation} has more than 3 inventions: {string.Join(", ", imperatorInventions)}"); + } + } + + public KeyValuePair? GetProgress(IEnumerable activeInventions) { + if (ck3Innovation is null) { + return null; + } + + // For each matching invention, add 25 to the progress. + int progress = activeInventions + .Where(invention => imperatorInventions.Contains(invention)) + .Sum(invention => 25); + return new(ck3Innovation, (ushort)progress); + } +} \ No newline at end of file diff --git a/ImperatorToCK3/Mappers/Technology/InnovationLink.cs b/ImperatorToCK3/Mappers/Technology/InnovationLink.cs new file mode 100644 index 000000000..7168d5f6f --- /dev/null +++ b/ImperatorToCK3/Mappers/Technology/InnovationLink.cs @@ -0,0 +1,23 @@ +using commonItems; + +namespace ImperatorToCK3.Mappers.Technology; + +public sealed class InnovationLink { // TODO: ADD TESTS + private string? imperatorInvention; + private string? ck3Innovation; + + public InnovationLink(BufferedReader linkReader) { + var parser = new Parser(); + parser.RegisterKeyword("ir", reader => imperatorInvention = reader.GetString()); + parser.RegisterKeyword("ck3", reader => ck3Innovation = reader.GetString()); + parser.IgnoreAndLogUnregisteredItems(); + parser.ParseStream(linkReader); + } + + public string? Match(string irInvention) { + if (imperatorInvention is null) { + return null; + } + return imperatorInvention == irInvention ? ck3Innovation : null; + } +} \ No newline at end of file diff --git a/ImperatorToCK3/Mappers/Technology/InnovationMapper.cs b/ImperatorToCK3/Mappers/Technology/InnovationMapper.cs new file mode 100644 index 000000000..c1f9bd681 --- /dev/null +++ b/ImperatorToCK3/Mappers/Technology/InnovationMapper.cs @@ -0,0 +1,50 @@ +using commonItems; +using System.Collections.Generic; + +namespace ImperatorToCK3.Mappers.Technology; + +public class InnovationMapper { + private readonly List innovationLinks = []; + private readonly List innovationBonuses = []; + + public void LoadLinksAndBonuses(string configurablePath) { + var parser = new Parser(); + parser.RegisterKeyword("link", reader => innovationLinks.Add(new InnovationLink(reader))); + parser.RegisterKeyword("bonus", reader => innovationBonuses.Add(new InnovationBonus(reader))); + parser.IgnoreAndLogUnregisteredItems(); + parser.ParseFile(configurablePath); + } + + public IList GetInnovations(IEnumerable irInventions) { + var ck3Innovations = new List(); + foreach (var irInvention in irInventions) { + foreach (var link in innovationLinks) { + var match = link.Match(irInvention); + if (match is not null) { + ck3Innovations.Add(match); + } + } + } + return ck3Innovations; + } + + public IDictionary GetInnovationProgresses(ICollection irInventions) { + Dictionary progressesToReturn = []; + foreach (var bonus in innovationBonuses) { + var innovationProgress = bonus.GetProgress(irInventions); + if (!innovationProgress.HasValue) { + continue; + } + + if (progressesToReturn.TryGetValue(innovationProgress.Value.Key, out ushort currentValue)) { + // Only the highest progress should be kept. + if (currentValue < innovationProgress.Value.Value) { + progressesToReturn[innovationProgress.Value.Key] = innovationProgress.Value.Value; + } + } else { + progressesToReturn[innovationProgress.Value.Key] = innovationProgress.Value.Value; + } + } + return progressesToReturn; + } +} diff --git a/ImperatorToCK3/Outputter/CulturesOutputter.cs b/ImperatorToCK3/Outputter/CulturesOutputter.cs index 48e4d3c03..01a86b21c 100644 --- a/ImperatorToCK3/Outputter/CulturesOutputter.cs +++ b/ImperatorToCK3/Outputter/CulturesOutputter.cs @@ -7,13 +7,24 @@ namespace ImperatorToCK3.Outputter; public static class CulturesOutputter { - public static void OutputCultures(string outputModName, CultureCollection cultures) { + public static void OutputCultures(string outputModName, CultureCollection cultures, Date date) { Logger.Info("Outputting cultures..."); - var outputPath = Path.Combine("output", outputModName, "common/culture/cultures/IRtoCK3_all_cultures.txt"); + var outputModPath = Path.Combine("output", outputModName); + var outputPath = Path.Combine(outputModPath, "common/culture/cultures/IRtoCK3_all_cultures.txt"); using var output = FileOpeningHelper.OpenWriteWithRetries(outputPath, System.Text.Encoding.UTF8); foreach (var culture in cultures) { output.WriteLine($"{culture.Id}={PDXSerializer.Serialize(culture)}"); } + + OutputCultureHistory(outputModPath, cultures, date); + } + + private static void OutputCultureHistory(string outputModPath, CultureCollection cultures, Date date) { + Logger.Info("Outputting cultures history..."); + + foreach (var culture in cultures) { + culture.OutputHistory(outputModPath, date); + } } } \ No newline at end of file diff --git a/ImperatorToCK3/Outputter/WorldOutputter.cs b/ImperatorToCK3/Outputter/WorldOutputter.cs index bd139147b..6b55dfdb0 100644 --- a/ImperatorToCK3/Outputter/WorldOutputter.cs +++ b/ImperatorToCK3/Outputter/WorldOutputter.cs @@ -42,7 +42,7 @@ public static void OutputWorld(World ck3World, Imperator.World imperatorWorld, C Logger.IncrementProgress(); PillarOutputter.OutputPillars(outputName, ck3World.CulturalPillars); - CulturesOutputter.OutputCultures(outputName, ck3World.Cultures); + CulturesOutputter.OutputCultures(outputName, ck3World.Cultures, ck3World.CorrectedDate); ReligionsOutputter.OutputHolySites(outputName, ck3World.Religions); ReligionsOutputter.OutputReligions(outputName, ck3World.Religions); @@ -192,6 +192,7 @@ private static void CreateFolders(string outputName) { SystemUtils.TryCreateFolder(Path.Combine(outputPath, "history", "titles")); SystemUtils.TryCreateFolder(Path.Combine(outputPath, "history", "characters")); + SystemUtils.TryCreateFolder(Path.Combine(outputPath, "history", "cultures")); SystemUtils.TryCreateFolder(Path.Combine(outputPath, "history", "provinces")); SystemUtils.TryCreateFolder(Path.Combine(outputPath, "history", "province_mapping")); SystemUtils.TryCreateFolder(Path.Combine(outputPath, "history", "struggles"));