diff --git a/ImperatorToCK3/CK3/Characters/Character.cs b/ImperatorToCK3/CK3/Characters/Character.cs index cab8b5f21..c042bab25 100644 --- a/ImperatorToCK3/CK3/Characters/Character.cs +++ b/ImperatorToCK3/CK3/Characters/Character.cs @@ -698,6 +698,9 @@ public void ClearDynastyHouse() { public string? GetDynastyHouseId(Date date) { return History.GetFieldValue("dynasty_house", date)?.ToString(); } + public void SetDynastyHouseId(string dynastyHouseId, Date? date) { + History.AddFieldValue(date, "dynasty_house", "dynasty_house", dynastyHouseId); + } private string? jailorId; public void SetEmployer(Character employer, Date? date) { diff --git a/ImperatorToCK3/CK3/Characters/CharacterCollection.cs b/ImperatorToCK3/CK3/Characters/CharacterCollection.cs index a10516557..9daedab56 100644 --- a/ImperatorToCK3/CK3/Characters/CharacterCollection.cs +++ b/ImperatorToCK3/CK3/Characters/CharacterCollection.cs @@ -18,6 +18,7 @@ using ImperatorToCK3.Mappers.UnitType; using Open.Collections; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; @@ -668,6 +669,142 @@ Configuration config Logger.IncrementProgress(); } + + public void GenerateSuccessorsForOldCharacters(Title.LandedTitles titles, CultureCollection cultures, Date irSaveDate, Date ck3BookmarkDate, ulong randomSeed) { + Logger.Info("Generating successors for old characters..."); + + var oldCharacters = this + .Where(c => c.BirthDate < ck3BookmarkDate && c.DeathDate is null) + .Where(c => ck3BookmarkDate.DiffInYears(c.BirthDate) > 60) + .ToArray(); + + var titleHolderIds = titles.GetHolderIdsForAllTitlesExceptNobleFamilyTitles(ck3BookmarkDate); + + var oldTitleHolders = oldCharacters + .Where(c => titleHolderIds.Contains(c.Id)) + .ToArray(); + + // For characters that don't hold any titles, just set up a death date. + var randomForCharactersWithoutTitles = new Random((int)randomSeed); + foreach (var oldCharacter in oldCharacters.Except(oldTitleHolders)) { + // Roll a dice to determine how much longer the character will live. + var yearsToLive = randomForCharactersWithoutTitles.Next(0, 30); + oldCharacter.DeathDate = irSaveDate.ChangeByYears(yearsToLive); + } + + ConcurrentDictionary titlesByHolderId = new(titles + .Select(t => new {Title = t, HolderId = t.GetHolderId(ck3BookmarkDate)}) + .Where(t => t.HolderId != "0") + .GroupBy(t => t.HolderId) + .ToDictionary(g => g.Key, g => g.Select(t => t.Title).ToArray())); + + ConcurrentDictionary cultureIdToMaleNames = new(cultures + .ToDictionary(c => c.Id, c => c.MaleNames.ToArray())); + + // For title holders, generate successors and add them to title history. + Parallel.ForEach(oldTitleHolders, oldCharacter => { + // Get all titles held by the character. + var heldTitles = titlesByHolderId[oldCharacter.Id]; + string? dynastyId = oldCharacter.GetDynastyId(ck3BookmarkDate); + string? dynastyHouseId = oldCharacter.GetDynastyHouseId(ck3BookmarkDate); + string? faithId = oldCharacter.GetFaithId(ck3BookmarkDate); + string? cultureId = oldCharacter.GetCultureId(ck3BookmarkDate); + string[] maleNames; + if (cultureId is not null) { + maleNames = cultureIdToMaleNames[cultureId]; + } else { + Logger.Warn($"Failed to find male names for successors of {oldCharacter.Id}."); + maleNames = ["Alexander"]; + } + + var randomSeedForCharacter = randomSeed ^ (oldCharacter.ImperatorCharacter?.Id ?? 0); + var random = new Random((int)randomSeedForCharacter); + + int successorCount = 0; + Character currentCharacter = oldCharacter; + Date currentCharacterBirthDate = currentCharacter.BirthDate; + while (ck3BookmarkDate.DiffInYears(currentCharacterBirthDate) >= 90) { + // If the character has living male children, the oldest one will be the successor. + var successorAndBirthDate = currentCharacter.Children + .Where(c => c is {Female: false, DeathDate: null}) + .Select(c => new { Character = c, c.BirthDate }) + .OrderBy(x => x.BirthDate) + .FirstOrDefault(); + + Character successor; + Date currentCharacterDeathDate; + Date successorBirthDate; + if (successorAndBirthDate is not null) { + successor = successorAndBirthDate.Character; + successorBirthDate = successorAndBirthDate.BirthDate; + + // Roll a dice to determine how much longer the character will live. + // But make sure the successor is at least 16 years old when the old character dies. + var successorAgeAtBookmarkDate = ck3BookmarkDate.DiffInYears(successorBirthDate); + var yearsUntilSuccessorBecomesAnAdult = Math.Max(16 - successorAgeAtBookmarkDate, 0); + + var yearsToLive = random.Next((int)Math.Ceiling(yearsUntilSuccessorBecomesAnAdult), 25); + int currentCharacterAge = random.Next(30 + yearsToLive, 80); + currentCharacterDeathDate = currentCharacterBirthDate.ChangeByYears(currentCharacterAge); + // Needs to be after the save date. + if (currentCharacterDeathDate <= irSaveDate) { + currentCharacterDeathDate = irSaveDate.ChangeByDays(1); + } + } else { + // We don't want all the generated successors on the map to have the same birth date. + var yearsUntilHeir = random.Next(1, 5); + + // Make the old character live until the heir is at least 16 years old. + var successorAge = random.Next(yearsUntilHeir + 16, 30); + int currentCharacterAge = random.Next(30 + successorAge, 80); + currentCharacterDeathDate = currentCharacterBirthDate.ChangeByYears(currentCharacterAge); + if (currentCharacterDeathDate <= irSaveDate) { + currentCharacterDeathDate = irSaveDate.ChangeByDays(1); + } + + // Generate a new successor. + string id = $"irtock3_{oldCharacter.Id}_successor_{successorCount}"; + string firstName = maleNames[random.Next(0, maleNames.Length)]; + + successorBirthDate = currentCharacterDeathDate.ChangeByYears(-successorAge); + successor = new Character(id, firstName, successorBirthDate, this) {FromImperator = true}; + Add(successor); + if (currentCharacter.Female) { + successor.Mother = currentCharacter; + } else { + successor.Father = currentCharacter; + } + if (cultureId is not null) { + successor.SetCultureId(cultureId, null); + } + if (faithId is not null) { + successor.SetFaithId(faithId, null); + } + if (dynastyId is not null) { + successor.SetDynastyId(dynastyId, null); + } + if (dynastyHouseId is not null) { + successor.SetDynastyHouseId(dynastyHouseId, null); + } + } + + currentCharacter.DeathDate = currentCharacterDeathDate; + // On the old character death date, the successor should inherit all titles. + foreach (var heldTitle in heldTitles) { + heldTitle.SetHolder(successor, currentCharacterDeathDate); + } + + // Move to the successor and repeat the process. + currentCharacter = successor; + currentCharacterBirthDate = successorBirthDate; + ++successorCount; + } + + // After the loop, currentCharacter should represent the successor at bookmark date. + // Set his DNA to avoid weird looking character on the bookmark screen in CK3. + currentCharacter.DNA = oldCharacter.DNA; + }); + } public void ConvertImperatorCharacterDNA(DNAFactory dnaFactory) { Logger.Info("Converting Imperator character DNA to CK3..."); diff --git a/ImperatorToCK3/CK3/World.cs b/ImperatorToCK3/CK3/World.cs index 77a1ad6e0..ebf0440ca 100644 --- a/ImperatorToCK3/CK3/World.cs +++ b/ImperatorToCK3/CK3/World.cs @@ -31,6 +31,7 @@ using ImperatorToCK3.Mappers.UnitType; using ImperatorToCK3.Outputter; using log4net.Core; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; @@ -357,6 +358,12 @@ public World(Imperator.World impWorld, Configuration config, Thread? irCoaExtrac RemoveIslam(config); } Logger.IncrementProgress(); + + // If there's a gap between the I:R save date and the CK3 bookmark date, + // generate successors for old I:R characters instead of making them live for centuries. + if (config.CK3BookmarkDate.DiffInYears(impWorld.EndDate) > 1) { + Characters.GenerateSuccessorsForOldCharacters(LandedTitles, Cultures, impWorld.EndDate, config.CK3BookmarkDate, impWorld.RandomSeed); + } Parallel.Invoke( () => ImportImperatorWars(impWorld, config.CK3BookmarkDate), diff --git a/ImperatorToCK3/Imperator/World.cs b/ImperatorToCK3/Imperator/World.cs index 5fedde098..58a635afa 100644 --- a/ImperatorToCK3/Imperator/World.cs +++ b/ImperatorToCK3/Imperator/World.cs @@ -71,6 +71,8 @@ private enum SaveType { Invalid, Plaintext, CompressedEncoded } private SaveType saveType = SaveType.Invalid; private string metaPlayerName = string.Empty; + public ulong RandomSeed { get; private set; } + protected World(Configuration config) { ModFS = new ModFilesystem(Path.Combine(config.ImperatorPath, "game"), Array.Empty()); MapData = new MapData(ModFS); @@ -360,7 +362,7 @@ private void ParseSave(Configuration config, ConverterVersion converterVersion, parser.RegisterKeyword("deity_manager", reader => Religions.LoadHolySiteDatabase(reader)); parser.RegisterKeyword("meta_player_name", reader => metaPlayerName = reader.GetString()); parser.RegisterKeyword("speed", ParserHelpers.IgnoreItem); - parser.RegisterKeyword("random_seed", ParserHelpers.IgnoreItem); + parser.RegisterKeyword("random_seed", reader => RandomSeed = reader.GetULong()); parser.RegisterKeyword("tutorial_disable", ParserHelpers.IgnoreItem); var playerCountriesToLog = new OrderedSet(); parser.RegisterKeyword("played_country", LoadPlayerCountries(playerCountriesToLog)); diff --git a/ImperatorToCK3/Outputter/CharacterOutputter.cs b/ImperatorToCK3/Outputter/CharacterOutputter.cs index af1144d67..6b072f9b8 100644 --- a/ImperatorToCK3/Outputter/CharacterOutputter.cs +++ b/ImperatorToCK3/Outputter/CharacterOutputter.cs @@ -11,17 +11,11 @@ public static void WriteCharacter(StringBuilder sb, Character character, Date co sb.AppendLine($"{character.Id}={{"); if (character.DeathDate is not null && character.DeathDate <= ck3BookmarkDate) { - // Don't output traits and attributes of dead characters (not needed). - var fieldsToRemove = new[] {"traits", "employer", "diplomacy", "martial", "stewardship", "intrigue", "learning"}; + // Don't output attributes of dead characters (not needed). + var fieldsToRemove = new[] {"employer", "diplomacy", "martial", "stewardship", "intrigue", "learning"}; foreach (var field in fieldsToRemove) { character.History.Fields.Remove(field); } - - // Disallow random traits for adult dead characters. - // Don't disallow for children, because the game complains if they have no childhood traits. - if (character.GetAge(ck3BookmarkDate) >= 16) { - character.History.AddFieldValue(date: null, "disallow_random_traits", "disallow_random_traits", "yes"); - } } // Add DNA to history.