Skip to content

Commit

Permalink
Generate a line of successors for very old characters (#2232) #minor
Browse files Browse the repository at this point in the history
closes #2211
  • Loading branch information
IhateTrains authored Sep 26, 2024
1 parent 6fe2c86 commit 1cb6faf
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 9 deletions.
3 changes: 3 additions & 0 deletions ImperatorToCK3/CK3/Characters/Character.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
137 changes: 137 additions & 0 deletions ImperatorToCK3/CK3/Characters/CharacterCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<string, Title[]> 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<string, string[]> 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...");
Expand Down
7 changes: 7 additions & 0 deletions ImperatorToCK3/CK3/World.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand Down
4 changes: 3 additions & 1 deletion ImperatorToCK3/Imperator/World.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Mod>());
MapData = new MapData(ModFS);
Expand Down Expand Up @@ -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<string>();
parser.RegisterKeyword("played_country", LoadPlayerCountries(playerCountriesToLog));
Expand Down
10 changes: 2 additions & 8 deletions ImperatorToCK3/Outputter/CharacterOutputter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 1cb6faf

Please sign in to comment.