• We have updated our Community Code of Conduct. Please read through the new rules for the forum that are an integral part of Paradox Interactive’s User Agreement.

Meneth

Crusader Kings 3 Programmer
153 Badges
Feb 9, 2011
10.056
5.388
www.paradoxwikis.com
  • Europa Universalis IV: Common Sense
  • Crusader Kings II: Holy Knight (pre-order)
  • Crusader Kings II
  • Crusader Kings II: Sword of Islam
  • Crusader Kings II: Legacy of Rome
  • Crusader Kings II: Sunset Invasion
  • Crusader Kings II: The Republic
  • Hearts of Iron IV: Expansion Pass
  • Steel Division: Normand 44 Sign-up
  • Stellaris: Digital Anniversary Edition
  • Crusader Kings II: Way of Life
  • Mount & Blade: With Fire and Sword
  • Mount & Blade: Warband
  • Magicka: Wizard Wars Founder Wizard
  • Hearts of Iron IV: Death or Dishonor
  • Europa Universalis IV: El Dorado
  • Hearts of Iron IV: Colonel
  • Hearts of Iron IV: Field Marshal
  • Surviving Mars: Digital Deluxe Edition
  • BATTLETECH: Flashpoint
  • Crusader Kings II: Conclave
  • Surviving Mars
  • Cities: Skylines Industries
  • Stellaris: Galaxy Edition
  • BATTLETECH
  • Hearts of Iron IV Sign-up
  • Stellaris Sign-up
  • Hearts of Iron IV: Cadet
  • Stellaris: Humanoids Species Pack
  • Prison Architect
  • Crusader Kings II: The Old Gods
  • Cities: Skylines - Campus
  • Hearts of Iron IV: No Step Back
  • BATTLETECH - Digital Deluxe Edition
  • Crusader Kings Complete
  • Cities: Skylines - Parklife
  • Europa Universalis IV
  • Age of Wonders III
  • Hearts of Iron IV: Expansion Pass
  • Europa Universalis IV: Cradle of Civilization
  • 500k Club
  • Stellaris: Leviathans Story Pack
  • Crusader Kings II: Reapers Due
  • Europa Universalis IV: Mandate of Heaven
  • Europa Universalis III Complete
  • Cities: Skylines - Mass Transit
  • Europa Universalis III Complete
  • Cities: Skylines - Green Cities
  • Teleglitch: Die More Edition
  • Europa Universalis IV: Rule Britannia
Good afternoon. I’m interning as a programmer on CKII, and part of what I’ve been doing is optimizing the game (which you can read more about in the dev diary), and adding more modding functionality.

As part of the optimization work, I’ve come over numerous things modders can do to make their mods faster, and introduced a few new ones. Since many larger mods suffer from occasional slowdowns, I thought it’d make sense to share these things with the modding community at large. Some you’ll likely know already, but hopefully there’ll be something new for everyone.

MTTH vs. is_triggered_only
One of the biggest sources of performance issues is the overuse of MTTH (mean time to happen) events. Every MTTH event is evaluated every 20 (changeable in defines.lua, and the exact day varies from character to character) days for every character (with some exceptions; see the next section) in the world.
is_triggered_only events on the other hand are only ever evaluated when they’re explicitly called, either by an event/decision/etc., or by an on_action. They’re therefore far cheaper.
Effectively, an event in the yearly on_action is almost 20 times cheaper than an MTTH event, since it is only checked every 365 days rather than every 20.

So when possible, you should use triggered events rather than MTTH events.

Event pre-triggers
If you’ve done event modding, you’ve almost certainly used event pre-triggers, but from the script files alone it isn’t entirely clear exactly how they work. Having worked with the code, I can give some insight.

Most pre-triggers are simply checked at the start of event evaluation. This is slightly quicker than checking them in the trigger itself. The main benefit is that it is guaranteed to be checked first, and that they’re all cheap checks. So you should use them as much as possible, but it isn’t worth contorting the logic of your events to make it work. Every condition that can be moved to a pre-trigger instead should be moved, and there’s no limit to how many pre-triggers you can have in an event, other than there only being one of each.

However, there’s a few pre-triggers that are significantly more effective, as they’re in separate event lists that are only ever evaluated if a character meets their condition. These can be considered “filtering” pre-triggers since they can completely eliminate events from even preliminary evaluation. They are, in rough order of ranking:
  • only_playable - In script, “playable” does not actually mean that the player can play the character. What it means is instead: “count-tier or above, or a patrician, and is not a landless rebel”. Using this pre-trigger completely eliminates evaluation of the event for anyone who does not meet the criteria, meaning that the event is checked for the ~1000 playable characters in the game, rather than all ~20k
  • only_rulers - This is essentially a lesser version of only_playable. It works like only_playable, but instead means holding absolutely any title
  • religion/religion_group - If you specify a religion or religion group, the event will only be evaluated for members of the religion group (even when specifying the religion; it'll check the individual religion within the group the same way regular pre-triggers work). It is worth noting that this is mutually exclusive with the other filtering pre-triggers, so in some cases it might actually be better to just use “religion_group = SOME_RARE_GROUP” and leave out “only_rulers = yes”. The quick-triggers take precedence in the order listed here, so only_playable and only_rulers both override religion/religion_group
  • is_part_of_plot - Events with this pre-trigger will only be evaluated by characters that are backing or leading a plot. Depending on how common plots are in your mod, this can be more efficient than only_rulers. In 2.6, this is moved to take precedence over everything except only_playable, as the # of characters involved in plots is generally lower than the # of rulers. Currently it does not take precedence over any of the pre-triggers
It is worth noting that currently, only_independent, is_patrician, and “ai = no” do not filter in this manner. In 2.6 however, both will imply “only_playable”, but right now it is better to use “only_playable” in addition.

In 2.6, two more filtering pre-triggers will also be added: lacks_dlc and has_dlc. If you add ‘lacks_dlc = “Way of Life”’ to an event for example, it will never ever get evaluated if Way of Life is enabled by the player (or in the case of multiplayer, the host). These two pre-triggers are also unique in it being possible to apply them more than once per event. E.G., filtering out both Conclave and Way of Life.
As a practical example, our in-development version of the game has roughly twice as many only_playable + only_rulers MTTH events as unfiltered MTTH events, yet the unfiltered MTTH events take twice as much performance combined. That’s an average of four times as much performance per event.

In practice, this means that it can make sense to have courtier-specific events originate from their liege rather than the courtiers themselves. The main drawback of this is that it is then difficult to have twice the qualifying courtiers mean half the MTTH, but if that’s not a concern it’s a good way to improve the performance.

Province events
Province events are checked as often as character events.
Since the number of provinces is quite limited, province events rarely affect performance much.
The number of provinces is generally very close to the number of “playable” characters, so province events can be considered equivalent in performance to only_playable events.
It is worth noting that event pre-triggers do not work for province events. However, in 2.6, a few will. Namely, the new has_dlc and lacks_dlc pre-triggers, as well as the new flag and global_flag pre-triggers.

Decisions
Decisions are in many ways similar to events in their effect on performance, but they’ve got some differences.
While events are evaluated every 20 days, decisions are evaluated once a month (the day of the month varying from character to character), making their baseline effect on performance slightly lower.
However, that’s complicated by a few things:

Targeted decisions
Targeted decisions are evaluated for every possible target, as defined by the ai_filter. So you will always want to use the narrowest ai_filter that still achieves what you’re trying to do. The filter used for the player on the other hand matters far less since there’s far far fewer players than AI characters in the game.
It is worth noting that the from_potential is evaluated first; if the originating character can’t take the decision, no possible targets will be evaluated. You should therefore eliminate as many decision takers as possible here.

Other “targeted” decisions
Beyond filterable targeted decisions, there’s also two more decision types: vassal_decisions and dynasty_decisions. The two are only ever evaluated for AI characters that are count level or above, making them somewhat cheaper than regular targeted decisions.

Lack of pre-triggers
Decisions currently do not have pre-triggers, making optimizing them more difficult than events.
The closest the game has to a pre-trigger is the ai_will_do. If the base factor is set to 0, AI characters will never evaluate the decision beyond checking the ai_will_do, which can save significant time. So make sure to explicitly set it to 0 for any decision the AI should never use.

Upcoming improvements
In 2.6 we’ll make it a bit easier to optimize decisions.
First of all, we’re adding the following pre-triggers: only_independent, only_playable, and only_rulers.
While this won’t outright filter the decisions like it does for events, the AI will very quickly exit evaluation of a decision if it doesn’t meet the pre-trigger. It is worth noting that these pre-triggers will not affect the player in any way, just the AI.

We’re also changing how plot_decisions work slightly. In 2.5.2 they’re functionally identical to regular decisions, so we decided to restrict them based on how vanilla uses them: in 2.6 they’ll only ever be evaluated by characters who lead a plot or a faction. So for decisions only relevant to such characters, significant time can be saved by making the decision a plot_decision instead of a regular one.

Casus bellis
Casus bellis can be pretty heavy on the performance if caution isn’t used.
The biggest, and easiest, way to optimize CBs is to have them only be available via script.
This is done by setting “is_permanent = no”. CBs with this set cannot be declared via the the regular CB interface; they’re instead declared via events, decisions, etc. This can of course not be done for all CBs, but is applicable for many CBs in both vanilla and mods. Currently about ⅓ of the CBs in vanilla are non-permanent.
It is worth noting that the “permanence” only refers to being able to declare them via the regular CB interface. It has no other effect.
Non-permanent CBs are not considered when the AI is figuring out who to declare war on, nor more importantly when figuring out who is a threat to them.

There’s also a couple modifiers to CBs that can significantly worsen their performance impact (again, only really applicable if is_permanent = yes):
  • de_jure_tier - This makes the CB check every single dejure tier of the appropriate level within the potential target realm for the can_use_title. However, this is only evaluated if the can_use was fulfilled, so eliminate as many characters as possible there
  • check_all_titles - Like de_jure_tier, this incurs a massive number of title checks
On the other hand, in 2.6 there’ll be a couple of modifiers that’ll reduce the performance used:
  • major_revolt - This will now ensure that the CB is never considered if the character is an independent ruler. As the game only evaluates other independent rulers as threats, this eliminates the CB from consideration in the process where CBs have the biggest impact
  • is_independence - Same effect as major_revolt

Triggers
Slow triggers
There’s a handful of conditions that are especially slow:
any_landed_title, any_character, any_province, any_playable_ruler, any_independent_ruler, and completely_controls/completely_controls_region
The first two are the worst offenders, evaluating a truly massive number of titles or characters. any_province, any_playable_ruler, any_independent_ruler isn’t quite as bad since the number of provinces/rulers is limited, but should still be avoided when possible.
completely_controls however is also quite slow, as it needs to check every single dejure title within the target title, so it should be used sparingly.

Trigger optimizer
For events, the game automatically sorts triggers so as to evaluate the cheapest ones first. This however is only applied to events, so for other triggers you’ll want to sort them yourself.

Overhead
There’s also a small overhead to NOR, NOT, NAND, etc. As such when possible it can be a good idea to do things like using a single NOR rather than several NOTs. However, this does bunch the conditions together when being evaluated by the trigger optimizer, so it can in some cases cause sub-optimal evaluation order. They’ll still be sorted within the NOR, but the NOR as a whole is likely to end up at the very end of the trigger.

Recursive events
Some modders like to use events that call themselves since the game currently has no support for loops, so it is the best way to have something happen a given number of times, or until some condition is met.
So as to let modders do this in a more performant way, we’ve in 2.6 added a new “while” effect. This will apply an effect repeatedly until some condition is met, avoiding the overhead of repeatedly sending events.

Various
Finally, a few odds and ends.

The number of rulers and characters in the game matters far more than the number of provinces, though the two are of course closely related.
It is worth noting that courts are only generated for characters above baron level (except for patricians), so adding more barons has considerably less of an effect on performance than adding more counts.

Triggered modifiers are extremely slow, and should be avoided like the plague. They’re evaluated every single day for every single “playable” character. If it is at all possible to implement what you want to do in a different manner, you probably should. If it is important that an effect disappear the moment some condition is met, it is likely better to have a hidden event that repeats every single day than to use a triggered modifier, as then at least you’re only evaluating it for people who’ve already got the modifier, and not everyone who doesn’t have it too.

Other musings
This isn’t the first time we’ve improved the performance, though it is likely to be the most noticeable one.
Many modders however with the release of Rajas of India noticed that their mods were suddenly much faster. This was likely mostly due to the evaluation of events and decisions being multi-threaded, meaning that several CPU cores can evaluate events at the same time rather than it all happening on one core.
It can be worth noting that this does mean that you can have contradictory events happen. That is, if the execution of event A invalidates the trigger of event B, event B can still happen as the trigger was evaluated before event A’s execution. If this causes a problem, conditions within events (“if”) can be used to make sure the triggers are still met.

I hope this post has been helpful, and I wish you all the best of luck in your modding endeavours.
If anyone has any mod performance related questions, I’ll try to answer them to the best of my ability.
 
Last edited:
  • 48
  • 3
Reactions:
Meneth said:
  • religion/religion_group - If you specify a religion or religion group, the event will only be evaluated for members of the group. It is worth noting that this is mutually exclusive with the other filtering pre-triggers, so in some cases it might actually be better to just use “religion_group = SOME_RARE_GROUP” and leave out “only_rulers = yes”. The quick-triggers take precedence in the order listed here, so only_playable and only_rulers both override religion/religion_group
Let me see if I understand this right...
Code:
character_event = {
   id = somenamespace.1
   only_rulers = yes
   religion_group = christian
...
}
will be evaluated for all rulers, not all christian rulers (filtering out non-christian non-rulers)?
 
  • 1
Reactions:
Let me see if I understand this right...
Code:
character_event = {
   id = somenamespace.1
   only_rulers = yes
   religion_group = christian
...
}
will be evaluated for all rulers, not all christian rulers (filtering out non-christian non-rulers)?
Correct.
Though it will reasonably quickly see that the ruler isn't Christian, and thus stop, for non-rulers it won't evaluate at all.
If the religion was a somewhat rarer one, E.G., Zoroastrianism, it'd likely be more effective to go religion_group = zoroastrian rather than "only_rulers = yes religion_group = zoroastrian".
 
  • 2
Reactions:
Again, to see if I understand right:
Code:
character_event = {
   id = somenamespace.1
   only_rulers = yes
   religion_group = christian
...
}
is functionally equivalent to:
Code:
character_event = {
   id = somenamespace.1
   only_rulers = yes
   trigger = {
      religion_group = christian
   }
}
both in regards to filtering characters out, and execution speed?
And for the case of rare groups:
Code:
character_event = {
   id = somenamespace.2
   religion = zoroastrian
   trigger = {
      is_ruler = yes
   }
}
is optimal?
 
  • 1
Reactions:
At which point would a group be considered to be "rare", in relative comparison to only_rulers? A few provinces? Is Romuva in vanilla a "rare" group?
 
  • 1
Reactions:
Again, to see if I understand right:
Code:
character_event = {
   id = somenamespace.1
   only_rulers = yes
   religion_group = christian
...
}
is functionally equivalent to:
Code:
character_event = {
   id = somenamespace.1
   only_rulers = yes
   trigger = {
      religion_group = christian
   }
}
both in regards to filtering characters out, and execution speed?
And for the case of rare groups:
Code:
character_event = {
   id = somenamespace.2
   religion = zoroastrian
   trigger = {
      is_ruler = yes
   }
}
is optimal?
#1 is a bit faster than #2, since the religion group will simply function as a regular pre-trigger, which is a bit quicker than putting it in the actual trigger.
#3 is optimal for rare groups.
At which point would a group be considered to be "rare", in relative comparison to only_rulers? A few provinces? Is Romuva in vanilla a "rare" group?
When there's fewer people of the religion group than there are rulers.
Romuva is probably rare enough, yes. But "is_playable = yes" would likely beat Romuva.
 
That is very handy, nice work. I didn't know triggered_modifiers were so intensive, I intend on purging them from AGOT immediately :D
 
  • 7
Reactions:
This is great info! Thank you very much

One question on mtth: Will a mtth be evaluated every 20 days independently of how high or low is the mtth?

Regarding decision evaluation, one quick question: You mention that ai_will_do = 0 means the decision isn't at all evaluated for the ai.But aside that, I asume the ai_will_do block is evaluated only after the potential and allow, right? Also,is ai_will_do similar to mtth? I mean, will halving the ai_will do mean that the decision is taken twice as often?
 
I have noticed a substantial slow down that alternates between the 25th and 18th of each month. My mod has an usually large number of provinces, could it be province events being evaluated then?
 
This is great info! Thank you very much

One question on mtth: Will a mtth be evaluated every 20 days independently of how high or low is the mtth?

Regarding decision evaluation, one quick question: You mention that ai_will_do = 0 means the decision isn't at all evaluated for the ai.But aside that, I asume the ai_will_do block is evaluated only after the potential and allow, right? Also,is ai_will_do similar to mtth? I mean, will halving the ai_will do mean that the decision is taken twice as often?
MTTH events are always evaluated every 20 days, yes (with the exception of those filtered out by filtering pre-triggers). Even if the MTTH is 1 day or 50 years.

The modifiers in the ai_will_do are evaluated after potential and allow, yes.

Doubling the ai_will_do doubles the chance the AI takes it.

I have noticed a substantial slow down that alternates between the 25th and 18th of each month. My mod has an usually large number of provinces, could it be province events being evaluated then?
The different provinces get events at different times, based on their ID. So province 20, 40, 60, etc. will get events at the same time if you're using the vanilla event offset of 20.
This won't happen on a consistent day each month (it'd go Jan 1st, Jan 21st, Feb 10th, March 2nd, etc.), so events are unlikely to be the culprit.
 
Death information is now customisable, so will there be more details coming on how it works? DD #16 just mentions execution methods as fully moddable while the changelog mentions death reasons being customizeable. Will it be as simple as the following?

Code:
#deathlocailisation.csv
my_death_reason;My death reason ;;;;;;x # Output is My death reason on [date].

#characterfile.txt
...
death = {
    death_reason = my_death_reason
}
 
Death information is now customisable, so will there be more details coming on how it works? DD #16 just mentions execution methods as fully moddable while the changelog mentions death reasons being customizeable. Will it be as simple as the following?

Code:
#deathlocailisation.csv
my_death_reason;My death reason ;;;;;;x # Output is My death reason on [date].

#characterfile.txt
...
death = {
    death_reason = my_death_reason
}
A bit OT. If nothing else, ask in the DD thread which actually mentions what you're asking.
 
While on the topic of pre-triggers... What do the is_friendly and is_hostile pre-triggers filter? I haven't found any documentation for those.

Character attitude towards the player? That would be my first guess, but if so, I don't see how that'd work in multi-player. And what are thresholds for 'friendly' and 'hostile', in numbers?
 
Last edited:
While on the topic of pre-triggers... What do the is_friendly and is_hostile pre-triggers filter? I haven't found any documentation for those.

Character attitude towards the player? That would be my first guess, but if so, I don't see how that'd work in multi-player. And what are thresholds for 'friendly' and 'hostile', in numbers?
It isn't a pre-trigger. It changes the description of diplomatic response events. If I'm understanding the code correctly, it means that is_hostile = yes would mean that it'd include something like "your misdeeds are known from blah to blah" and such.
 
  • 1
Reactions:
It isn't a pre-trigger. It changes the description of diplomatic response events. If I'm understanding the code correctly, it means that is_hostile = yes would mean that it'd include something like "your misdeeds are known from blah to blah" and such.
Oh. It's listed among pre-triggers in the wiki, so I assumed that whoever put it there knew what they're doing. Obviously not. Fixed now.
 
Huh... mutually exclusive?

So you're saying that between

Code:
only_rulers = yes
religion = norse_pagan
and
Code:
religion = norse_pagan
trigger = {
    only_rulers = yes
}

#2 is better
#1 won't work properly
#1 and #2 work the same

I got into optimising CK2Plus pretriggers, about a week ago, good results so far :)
 
Last edited:
The _filtering_ of religion[_group] vs. rulers is mutually exclusive. Since there are a lot more pagan_group characters than rulers, it'd definitely make sense to use the only_rulers=yes pre-trigger. Using them both as pre-triggers would then be optimal, because the only_rulers=yes will automatically take precedence as the usually-better filter, and the religion pre-trigger will prevent having to evaluate the event's trigger at all for a large portion of those rulers upon which the event is evaluated (simply acting as a fast trigger rather than a filter in that case).
 
  • 1
Reactions: