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:
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):
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.
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
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
- 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