• 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.

Rylock

Field Marshal
64 Badges
Mar 10, 2008
11.618
2.427
  • Crusader Kings II: Charlemagne
  • Stellaris - Path to Destruction bundle
  • Victoria 2: Heart of Darkness
  • Victoria 2: A House Divided
  • Semper Fi
  • Victoria: Revolutions
  • Heir to the Throne
  • Hearts of Iron III
  • Europa Universalis IV: Conquest of Paradise
  • Crusader Kings II
  • Divine Wind
  • Europa Universalis III Complete
  • Europa Universalis III
  • Crusader Kings II: Legacy of Rome
  • Crusader Kings II: The Old Gods
  • Crusader Kings II: Rajas of India
  • Crusader Kings II: The Republic
  • Crusader Kings II: Sons of Abraham
  • Crusader Kings II: Sunset Invasion
  • Crusader Kings II: Sword of Islam
  • Darkest Hour
  • Europa Universalis III: Chronicles
  • Shadowrun: Hong Kong
  • Stellaris: Digital Anniversary Edition
  • Stellaris: Leviathans Story Pack
  • Hearts of Iron IV: Together for Victory
  • Crusader Kings II: Reapers Due
  • PDXCon 2017 Awards Winner
  • Hearts of Iron IV: Death or Dishonor
  • Hearts of Iron IV: No Step Back
  • Hearts of Iron IV: Expansion Pass
  • Stellaris: Humanoids Species Pack
  • Stellaris: Apocalypse
  • Stellaris: Distant Stars
  • Shadowrun Returns
  • Shadowrun: Dragonfall
  • Hearts of Iron IV: Colonel
  • Hearts of Iron IV: Cadet
  • Stellaris
  • Crusader Kings II: Conclave
  • Crusader Kings II: Horse Lords
  • Pillars of Eternity
  • Crusader Kings II: Way of Life
  • Pride of Nations
  • Europa Universalis IV: Pre-order
  • Crusader Kings II: Holy Knight (pre-order)
  • 500k Club
  • Victoria 2
  • Europa Universalis III Complete
  • Europa Universalis III Complete
Since it was asked for in another thread, and there hasn't been a post about it since this previous one (which is now quite out of date), I thought I'd make a post about writing efficient code for events and decisions. It's not something of big concern to every modder, but for a big mod like CK2Plus it's a constant battle we need to fight -- so I thought I might be able to share some of my experience to those interested.

Writing Efficient Events & Decisions

It's actually quite easy to write a single event in CK2 that, all by itself, can slow down your entire game.

Think of it like this: during every event "pulse" (which is a period of time determined by the EVENT_PROCESS_OFFSET setting in defines.lua, usually 20 days), the game is running through a list of every single character in the game. For each character, it is also running through every single event and decision, and evaluating whether they apply to that character. It does this at varying levels of efficiency, but at the base level it's going through every line of code in the trigger (for events) or the potential/allow (for decisions) and determining whether the line returns as "true" or "false". If the entire thing returns "true", then the event or decision can potentially fire.

Now imagine how many calculations that would require. Your computer can do this fairly quickly, but when we're talking about hundreds of events and decisions spread over literally thousands of characters, this is going to add up...and it can add up very quickly if you have triggers that are doing many things at once. I once innocently wrote an event that checked against any_independent_ruler in the world, but also against any_realm_character for each of those rulers...and, having not limited the event in any way, it was doing this for every single character in the game. Checking every character against every other character resulted in a noticeable period of lag every twenty-day period...for a single event.

Want to avoid doing that? Keep a few rules in mind when writing your code.


Rule #1: Use Pre-Triggers


A "pre-trigger" is a set of triggers which appears above the actual "trigger" section in character-based events (and only in those events), like so:

Code:
character_event = {
   id = XXXXX
   desc = EVTDESCXXXXX
   picture = XXXXX

   ai = no
   min_age = 16
   capable_only = yes
   prisoner = no
   only_rulers = yes
   religion_group = muslim

   trigger = {
     ...

This is separate from some other event-specific commands which also go in the same place, like is_triggered_only, hide_window, hide_from, or major. They are, essentially, triggers that are evaluated before anything else is even looked at, reducing the pool of characters the game will use to check the actual trigger section. Instead of running through a list of thousands of characters, imagine it separating out a hundred to whom the pre-triggers specifically apply...and only worrying about them when it comes to the rest of the event. The game does this relatively efficiently, so it's your first line of defense for writing efficient code.

The pre-triggers which are valid to use:

ai = [yes/no]
capable_only = [yes]*
culture = [culture]
culture_group = [culture group]
has_character_flag = [flag name]
is_part_of_plot = [yes/no]*
is_patrician = [yes]
max_age = [int]*
min_age = [int]*
only_independent = [yes]*
only_men = [yes]*
only_playable = [yes]*
only_rulers = [yes]*
only_women = [yes]*
prisoner = [yes/no]
religion = [religion]
religion_group = [religion group]

* = these are only useable in pre-triggers -- don't use them anywhere else!

Put as many in as apply to your event, and remember that you don't need to repeat them again in the trigger section. The game has already eliminated every character who doesn't match up to your pre-triggers, so it's wasteful to have the game check these a second time.

Also keep in mind that pre-triggers are inherently within an AND limiter. They all must apply in order for the rest of the event's trigger to be evaluated. You can't, for instance, put two has_character_flag pre-triggers in unless you want both to be present on the character.


Rule #2: Eliminate the Largest Groups First

What does this mean? Well, the game starts reading your triggers from the top line just as you do, and works its way sequentially down the list. As soon as it finds a trigger which returns "false", it stops reading and moves onto the next character. You want it to hit that "false" as quickly as possible, so you must make sure the triggers that will return "false" for the largest number of characters come higher in the list.

For instance, look at this:

Code:
trigger = {
   NOT = { trait = craven }
   is_ruler = yes
   culture = russian
}

That's inefficient code. The number of people who won't have the craven trait is much larger than the number of rulers in the game, which is also large than the number of characters with russian culture. You'd want to do this instead;

Code:
trigger = {
   culture = russian
   is_ruler = yes
   NOT = { trait = craven }
}

That way, when the game starts evaluating the trigger for a character, the first thing it asks is "is this character Russian?" If the answer is "no" (which it would be for the vast majority of characters, barring some Russian realm becoming enormous), the game then ignores the rest of the trigger and moves on. With the former piece of code, the first question would be "does this character not have the craven trait?", the answer being "yes" for the vast majority of characters, forcing it to move on to the next line and make another calculation.

Of course, you want to be careful with your application of Rule #2, as there is also Rule #3 below to consider.


Rule #3: Not All Triggers Are Equal

What this means is that some triggers are quicker for the game to evaluate than others. For these "slower" triggers, you want to keep them further down the list than others so the game only evaluates them when it absolutely has to. There are two types of these. One are the "large" scopes changes:

any_independent_ruler
any_playable_ruler
any_realm_character
any_realm_lord


Really, all the any_* scopes have a heavier load than regular triggers, as now you're forcing the game to evaluate other characters on top of the one it's already evaluating, and the wider that scope needs to check, the worse the efficiency. Considering you can also use scopes inside those scopes (like my original example of using any_realm_character inside of any_independent_ruler), the effect can be magnified exponentially.

The other type of "slower" triggers are a few very specific things: namely any trigger that checks for flags and modifiers (including opinion modifiers). In my experience, these aren't as bad, but the devs assure us the engine is slower at evaluating them, so you should be careful. Using has_character_flag now handily allows you to use it in a pre-trigger, but if you need to check several flags or the absence of one, that's just not going to be an option.

Now, you may ask: what's more important? Rule #2 or Rule #3? What if I have a "slower" trigger that could also eliminate a much larger group than any other one? In that case, it's a judgement call. I've made events where I simply have to check a flag or a modifier as the top trigger, because all the other triggers for that event are far too common. You need to think about each trigger in terms of how much mileage it buys you having it evaluated earlier rather than later.


Example

Let's say I wanted to write an event where I want to check if a wrathful or proud tribal ruler has any rivals in the same realm of the same gender. I want to make sure they only get this event once, so I'll be setting a flag called duel_check on them when the event fires, in addition to checking for an opinion modifier I'm calling challenged_to_duel (so they don't challenge someone who's already challenged them). I also want to make sure they're not busy or otherwise incapable.

An inefficient way to do this:

Code:
trigger = {
   war = no
   is_ruler = yes
   is_adult = yes
   prisoner = no
   is_tribal = yes
   NOR = {
     trait = incapable
     trait = in_hiding
     has_character_flag = do_not_disturb
     has_character_flag = duel_check
   }
   OR = {
     trait = wroth
     trait = proud
   }
   any_rival = {
     is_adult = yes
     prisoner = no
     NOT = { trait = incapable }
     same_sex = ROOT
     same_realm = ROOT
     NOR = {
       has_opinion_modifier = { who = ROOT modifier = challenged_to_duel }
       trait = in_hiding
       trait = incapable
     }
   }
}

Note that, without pre-triggers, this event will be evaluated on every single character -- including children and courtiers. We're going through a lot of evaluation, including some slower triggers, before we even get to some important determiners as to whether the character should be evaluated in the first place. While any_rival is, at least, not a very large scope to check for any character (presumably), it's also being ordered inefficiently.

Done a little better:

Code:
only_rulers = yes
min_age = 16
capable_only = yes
prisoner = no

trigger = {
   OR = {
     trait = wroth
     trait = proud
   }
   is_tribal = yes
   war = no
   any_rival = {
     same_realm = ROOT
     same_sex = ROOT
     is_adult = yes
     prisoner = no
     NOR = {
       trait = in_hiding
       trait = incapable
       has_opinion_modifier = { who = ROOT modifier = challenged_to_duel }
     }
   }
   NOR = {
     trait = in_hiding
     has_character_flag = do_not_disturb
     has_character_flag = duel_check
   }
}

Now only adult rulers who aren't imprisoned or incapable are even being considered. We eliminate anyone who isn't Wroth or Proud right off the bat, since traits are nice and easy to check, and narrow that down to tribal rulers before checking for the existence of rivals, and leave the flag-checking until last.

Would making these changes make for a noticeable difference? No, not in a single event. Indeed, you could probably add a bunch of events with no pre-triggers at all, all of them checking solely for the existence of flags and modifiers, and the end effect on your performance wouldn't change in any appreciable way. As your game gets later and later in date, however, the number of characters in existence is going to go up...meaning the footprint of every single event and decision is also going to go up. Efficient coding is not a magic bullet that will suddenly make everything run more smoothly -- it's a cumulative thing, the idea being to manage how many tiny little bites you're taking out of performance with each event and decision you code.


A Word on Decisions

To be honest, I'm not really certain whether decisions are more efficient than events. They're essentially events that apply to all characters and which have no pre-triggers available, but also aren't calculating the mean_time_to_happen (though, if you include a lot of factors in the ai_will_do, I imagine the calculations could almost be as intense). You should consider the potential and allow as being together the same as an event's trigger section -- the only difference between them is that, if everything in the potential returns "true", a player will see the decision in their list even if they can't enact it. It takes the entire "allow" returning true in order for them to be able to enact it. That's it. That's all they do.

The targetted_decisions (and it pains me every time to spell it like that) are where decisions can really impact your performance, and where you want to carefully manage how you're using them. Now you suddenly have a decision which is inherently not only checking against the character potentially enacting it, but also potentially evaluating that character against every other character in the game. Fortunately, the devs have included a couple of tools to help you manage the footprint of these decisions, as well.

The first thing is the ai_target_filter. Think of this as a pre-trigger, the only one you have available, and it allows you to restrict the pool of third-party characters being evaluated right off the bat. Paradox included the list of available filters in the way_of_life_decisions file, but I'll also include them here:

court: all characters in the AI's host court, including prisoners, and characters currently away (wards, prisoners, etc)
home_court: all characters in the AI's home court, including prisoners, and characters currently away (wards, prisoners, etc)
vassals: direct vassal rulers of the AI's employer
sub_realm: all characters below the AI's employer
realm: all characters in the same top realm as the AI
dynasty: all members of the same dynasty
rivals: your rivals plus any character you have an opinion modifier with 'crime = yes' set (the same set of characters the 'is_foe' trigger evaluates)
spouse: all spouses of the AI
all: all living characters (Avoid if possible. VERY CPU-HEAVY!)

So a "court" filter becomes, in effect, the same as an any_courtier scope, the "vassals" filter is the same as an any_vassal scope, the "realm" filter is the same as scoping to the top_liege and then any_realm_character, and so on. This is good. Think carefully about what you want your target pool to be, and if you decide that it just has to be "all" then make sure you code the rest of the decision as efficiently as you possibly can.

The next step is your from_potential. This determines who the game will check all those third-party characters against, and the wider your ai_target_filter is, the narrower you should aim for your from_potential to be. You might remember a comment the devs made regarding how the castration and blinding decisions were -- all by themselves -- eating up a huge amount of processing power in the late game. That was before they had targetted_decisions (back them it was vassal_decisions, but they worked the same way -- simply without the option of ai_target_filter), and they'd made the from_potential solely check for greek culture...that's it. So every greek character was checking against every other character, until the decision discovered somewhere in the potential that most of these characters couldn't even have prisoners...but it was still performing all those calculations. Presumably, the bigger the Byzantium Empire got, the worse the footprint of these decisions became. A simple "is_ruler = yes" in the from_potential would have saved a lot of grief and heartache.

The only thing to remember about targetted_decisions? Unlike every other event or decision, ROOT is not the character enacting the decision. That's FROM, while ROOT is the target. If you fire an event from a targetted_decision, using FROM in the event scopes back to the target (even if they were who you fired the event upon), and you need FROMFROM to scope back to the decision enactor. Not a code efficiency issue, sure, but probably the easiest mistake to make, and one even the devs themselves have done more than once.


A Word on Province Events

One thing you might notice from the above is that, when it comes to events, I'm talking almost exclusively about characters and not provinces (for province events). That's for a good reason: none of the pre-triggers I mentioned apply to province events...at all. There is no way to reduce the pool of provinces that province events will be evaluated against. None.

That said, there are a set number of provinces in the game, and it's far less than the number of characters. Your average province event in vanilla has about the same footprint as a character event that's using the only_playable pre-trigger...which is not too bad.

Even so, Rule #2 and Rule #3 still apply to province events. Order your triggers appropriately, as that's the only way you can keep province events from having too big an impact on your performance, particularly if you're using a lot of them.


And that's it. Hopefully it's helpful for those who are curious.
 
Last edited:
  • 24
Reactions:
There is also an undocumented, but used in vanilla, spouse filter.
You might also consider adding a section on cb_types and on minor_titles.
 
A lot of this also applies to the oft overlooked buildings; the trigger and potential conditions in buildings are evaluated every second (or more, like targeted and non-targeted decisions) as opposed to every 20 days, having extensive building chains with poorly structured potential/triggers can lead to severe slowdown. In some cases much more noticeably than events.
 
  • 2
Reactions:
As far as I know, all pretriggers scope to ROOT, so no, character oriented pretriggers should not be used in province_events. You'll need to scope to owner in the triggers instead before applying those triggers.
 
  • 1
Reactions:
There is also an undocumented, but used in vanilla, spouse filter.

Ah, right. Forgot about that one. I'll add that to the list, thanks.

You might also consider adding a section on cb_types and on minor_titles.

Well...true, though the topic is events & decisions. :)
 
Most of the pre-triggers are aimed at characters... if I make a province event will they work? Will they automatically aim the province owner?

As richvh mentions, no -- pre-triggers don't exist for province events at all.

Thanks for reminding me, however. I'll add a note on province events at the end, since I was talking almost exclusively about character events.
 
  • 1
Reactions:
Wouldn't the religion pretriggers work in province_events? And culture, forget if culture is a valid pretrigger.
 
Wouldn't the religion pretriggers work in province_events? And culture, forget if culture is a valid pretrigger.
geographical regions maybe as well
 
Wouldn't the religion pretriggers work in province_events? And culture, forget if culture is a valid pretrigger.

Possibly. Those are new pre-triggers, so I've yet to try them on province events. I'll test it and see.
 
filter is applied to the player, ai_target_filter to the AI taking the decision. You can make them different so the player has a wider choice of targets.
 
filter is applied to the player, ai_target_filter to the AI taking the decision. You can make them different so the player has a wider choice of targets.

When I tested it, I found that "filter" itself actually did nothing -- the targeted decisions always applied to everyone, for the player -- but maybe I just need to do more testing.
 
Thanks for this great guide. A question:

If you'd take a look at this trigger qickly:

trigger = {

any_demesne_title = {
location = {
OR = {

has_building = ca_geb_terr_2_1
has_building = ct_geb_terr_2_1
has_building = trib_geb_terr_2_1

}
}
}

NOT = {

has_character_flag = terr_geb_2_1

}


}

Wouldn't it be more efficient then to have the NOT statement checking the flag at the beginning of the trigger according to your second rule?
 
Probably, but the building check is messed up. It should be any_demesne_title = { tier = baron OR = { has_building = ... } } since buildings are in holding (barony title) scope, not province scope.
 
With regards to rule 2, I thought the game engine automatically compiles the script so more efficient triggers are evaluated first?

I know it was the case when CK2 first came out that triggers were evaluated in the order they were scripted, but I'm fairly sure it was changed in a subsequent patch. Although I dont have the changelog to hand to check...
 
Probably, but the building check is messed up. It should be any_demesne_title = { tier = baron OR = { has_building = ... } } since buildings are in holding (barony title) scope, not province scope.

Thanks for your answer. The event works as intended though (regardless of optimization possibilities).
Do you think the alternative you are giving works more efficient then? (as it is funny that the "messed up" building check works as intended isn't it?
 
Well, yes, it would be more effecient, as it's excluding higher titles before checking for buildings.