[REFERENCE] How BTS Decides to Trigger Events

Merkava120

Oberleutnant
Joined
Feb 2, 2013
Messages
450
Location
Socially distant
Solver's tutorial gives you all the ups and downs of event modding in XML and Python, and it's way, way worth reading if you want to get into event modding. But the actual machinery behind event triggers is a bit opaque. For one thing, there is no single "can this event be triggered?" method, and the prerequisites for the trigger are spread across several different methods, which can be pretty confusing.

So, here's what I've pieced together by sorting through the SDK.

The main function to care about is CvPlayer::doEvents(). This is called in doTurn() which occurs shortly after you press the "END TURN" button. Unfortunately, it is not as simple as "see if an event can be triggered and roll a die to see if it is actually triggered this turn" - there are a whole bunch of methods that doEvents() uses to trigger things:
  1. CvPlayer::getEventTriggerWeight(EventTrigger)
  2. CvPlayer::initTriggeredData(EventTrigger, bFire = false, + a whole bunch of other optional variables)
  3. CvPlayer::addEventTriggered()
  4. CvPythonCaller::doEventTrigger()
  5. CvPlayer::trigger(EventTrigger)
  6. CvPlayer::trigger(EventTriggeredData) - this is not the same as #5
  7. CvPlayer::addPopup(CvPopupInfo, bFront)
  8. CvPlayerAI::AI_chooseEvent(EventTriggeredData ID)
But, before messing with any of this at all, doEvents
  1. Makes sure you have not disabled events, and if you have, dies
  2. Makes sure you are not barbarian or minor, and if you are, dies
  3. Checks events that have expiring effects (in vanilla this means quests, I think) and expires them if time ran out
  4. Apparently there is a delay at the beginning of the game before events can happen at all, and that is checked next
  5. Then doEvents rolls a die to see if events will happen this turn. The probability is set in Global Defines, EVENT_PROBABILITY_ROLL_SIDES.
And now we get to the good stuff.

1. getEventTriggerWeight

doEvents checks this next. The goal here is to put all the different possible triggers in a pile and choose one randomly, but the XML allows you to "weight" different triggers to be more likely in different situations. So that's the goal here.

But. That word possible means that the system has to check the many prerequisites set in the XML file to see if the trigger should be included at all. Some (but not all) of these prereqs are handled here, namely:

Spoiler Prereqs :
Minimum difficulty, single player vs. multiplayer, whether or not the event trigger has actually been included in this particular game (XML for that is PercentGamesActive), obsolete techs, whether it is previously fired and is NOT a recurring event, prerequisite techs, prerequisite events, required civics, the player's treasury size, the number of continents on the map, and "getMinOurLandmass" and "getMaxOurLandmass" which ask about how many islands/continents the player themself has settled on.


If any of these fail, the method returns 0, which as we will shortly see, means the trigger is excluded from the pile.

Then if the event has a weight less than 0, this method simply returns that weight, which means setting iWeight to -1 in the XML file speeds things up quite a bit.

Otherwise, the function finally gets around to the actual weighting. Weights are iWeight * (iNumUnits) * (iNumBuildings), where
  • iWeight is the tag from the XML
  • iNumUnits is not considered unless the XML tag "isProbabilityUnitMultiply" is set to 1; if it is 1, then iNumUnits is the number of units owned by the player who pass through CvUnit::getTriggerValue() without failing the following prereqs:
    • The trigger does not require 0 or less units
    • The unit is not dead
    • The python associated with the trigger does not say "no"
    • The unit is one specified by the trigger, if any are
    • The unit is on a plot which can trigger according to criteria listed below in the spoiler marked "Things", for the "Plots" section, IF the trigger has bUnitsOnPlot set to 1;
    • The unit is damaged, if the trigger weights units by damage
    • (If the last three do not apply, and the unit passes the first three, it is included in the total, and this function returns a weight value)
  • iNumBuildings is not considered unless the XML tag "isProbabilityBuildingMultiply" is set to 1; if it is 1, then iNumBuildings is the number of buildings owned by the player which are listed in the trigger xml.
Note that the CvUnit::getTriggerValue() function returns a weighted value: Damage * DamageWeight + Experience * ExperienceWeight + Distance (from the trigger plot, I believe) * DistanceWeight. However, that does not affect this part of triggering at all.

Also note that failing the above unit/building requirements for all units in a civ effectively fails the trigger by multiplying its weight by 0; so this is a consideration of unit and building number prereqs too.

Now that we have the weight, back in doEvents(), we check if the weight is -1, and if it was, trigger the event immediately using function #5 listed above (and described below).

But if the weight is greater than 0, it's time to move on.

2. initTriggeredData

This is where the event trigger is set up to fire. (Even though it hasn't been chosen yet.) And, lo and behold, there are more prereqs to check! This time they're bundled together with figuring out the data for the trigger.

But first, it should be noted that this method is used every time a trigger is fired, whether through Python, or as a response to a different event, or whatever. So there are a whole bunch of variables that can be passed into initTriggeredData to specify other players, particular buildings, particular units, or whatever else that this particular trigger should target. None of these are used by doEvents, which determines these things based on the XML and good ol' die rolling.

Anyway, the way this method handles prereqs is a little odd, since it's also trying to set up the triggering data at the same time. So it switches back and forth between choosing Things to target that satisfy criteria and seeing whether or not the player satisfies certain...other criteria. Details in the spoiler.

Spoiler Things :

  • Cities, if the trigger has pickTriggerCity set to 1, and (via CvPlayer::pickTriggerCity, which calls CvCity::getTriggerValue to test the following)
    • if python tied to the event does not say "no" for this city,
    • the city has buildings specified by the trigger, if any are,
    • the city has a number of religions specified by the trigger (this can be 0),
    • the city has specific religions required by the player, UNLESS the trigger is asking about the state religion, in which case it's enough for the player to have the specified religion as their state religion regardless of this city,
    • the city is the holy city of any specified religion if the trigger requires the city to be a holy city,
    • repeat the above three bullets, but replace 'religion' with 'corporation',
    • the city has a population between the trigger's min and max,
    • the city has an anger level between the trigger's min and max,
    • the city has an unhealthy level between the trigger's min and max,
    • the city is tied to an event that previously occurred and which is required by this trigger,
    • the city has food, if the trigger requires food greater than 0;
    • then cities are weighted based on their food and the trigger's Food Weight, and the city with the most food is chosen, or a random city if the trigger does not care about food.
  • Plots belonging to the city chosen above, if the trigger has pickPlot set to 1, and if (via CvPlot::canTrigger)
    • the plot is owned by us, if the trigger requires that,
    • the plot is a specific plot type, if specified by the trigger (land, sea, hills, or peaks),
    • the plot has one of the features specified by the trigger, if any are,
    • the plot has one of the terrains specified by the trigger, if any are,
    • the plot has one of the improvements specified by the trigger, if any are,
    • the plot has one of the bonuses specified by the trigger, if any are,
    • the plot has one of the routes specified by the trigger, if any are,
    • the plot has a unit specified by the trigger (and the trigger has isUnitsOnPlot set to 1) which is owned by us, and the unit passes the same criteria listed for NumUnits in the weighting formula above (but we still ignore the specific weights),
    • the plot belongs to a city which has had an event occur which is a prereq for this trigger, if any are, AND the plot is the specific plot the previous event occurred on;
  • Then it checks buildings, and sees if the player has the specified number of SPECIFIED buildings,
  • Then it checks religions, and sees if the player has the specified number of cities with SPECIFIED religions OR a specified number of cities with ANY religions if no religion is specified,
  • Same for corporations,
  • And then we check that the player's population is in the correct range (the player's whole civ, not just a city),
  • And then if the trigger needs to pick a plot but does NOT require picking a city, we go through all the plots on the WHOLE MAP and apply all the criteria listed for Plots above, and add them to a list, which we do nothing with just yet;
  • Then if the trigger needs to pick a religion, we either pick the state religion, if the trigger requires state religions and the current state religion passes CvPlayer::isValidTriggerReligion:
    • Which is basically anything mentioned about religions up to this point, including the holy city stuff, and I don't know why they are not using this method every single time,
  • ...or, if the trigger does not require a state religion, we pick a random one from all the religions that pass that same function,
  • and then we do all the same crap for corporations, which have their own method too (CvPlayer::isValidTriggerCorporation),
  • And now we finally take that plot list mentioned above and choose a random plot from it, or just return the city's plot if we picked a city above and none of the plots we ran through met the criteria,
  • And also we check buildings, again, but this time stick the ones that are mentioned in the trigger into a list, and then choose one for the trigger to fire on;
  • Now, units! via CvPlayer::pickTriggerUnit. This is finally where the value determined by CvUnit::getTriggerValue is considered; for details see iNumUnits explanation above. Basically, all living, python-passing units are considered unless the trigger specifies that units have to be of a certain type or on the plot or damaged, and units are weighted by Damage and Experience and Distance from the trigger plot if the trigger cares about those things.
  • Aaaand now we do the same exact thing done above, but for ALLLL of the units belonging to EVERY player, and add them into a grand total to compare to the trigger's NumUnitsGlobal.
  • ...also buildings
  • And then if PickPlayer is 1, we do all the city stuff again (via CvPlayer::pickTriggerCity) for valid players (as determined by CvPlayer::canTrigger) to select a city if PickOtherPlayerCity is 1, or just choose from among the valid players if the trigger doesn't PickOtherPlayerCity.


Whew. Talk about redundancy. I mean, I'm no C++ expert, and I have serious respect for the Firaxis programmers. But I feel like even I could shorten this by 50% just by doing each type of check...once. Instead of 3+ times.

Anyway, at this point the triggered data is finally ready to be set up, with: Trigger, Player, Turn, City, Plot X and Y, Other Player City, Religion, Corporation, Unit, and Building. (No objects are passed to trigger data, just ints, so 'unit' and 'player' and the cities use an ID).

3. Triggered data is set up by adding a 'row' (~basically) to the Triggered Events Array, via TriggeredData = CvPlayer::addEventTriggered(). In an enormous contrast to the previous method, addEventTriggered is three lines long.

...and we're not done with #2. Next, initTriggeredData calls

4. CvPythonCaller::doEventTrigger(),

which hands the triggered data to a python method specified by the trigger, if there is one. This is set up so that the player can check more things (anything they want) and return false to kill this trigger. So, after all that effort, the player might decide that your civ has had too much cake already, and kill the trigger (despite possibly having looped through every single tile and every single unit once each, plus a bunch of other stuff).

Then, back to #2. In initTriggeredData the text of the trigger is set up and added to the triggered data. And, the world news text too.

Then, important last bit. If initTriggeredData was called with bFire = true, then we call #6 of the methods listed at the beginning. doEvents() does not set bFire to true when calling initTriggeredData, but if you want to trigger an event yourself, you probably should.

Lastly, initTriggeredData returns the triggered data you set up. Which was a lot of work! It's important to remember that this triggered data is also inherently being added to the 'events triggered' list.

back in doEvents()...

So far we have checked a lot of prereqs and returned a weight and triggered data for the trigger. So now we add this weight to the pile (which is actually a variable called iTotalWeight, if you cared, which is tied to each triggerdata and put in a list of PossibleEventTriggerWeights), and repeat the above two methods for every single trigger in the game.

The rest is fairly straightforward: choose a random number less than iTotalWeight, run through the list of triggers / weights until we get a value less than the trigger weight (I assume the list ends up sorted from 'highest' to 'lowest', so the first trigger weight < RandomNumber will be the one chosen), and call method #6 from the above list.

All the others are deleted, so that the game has to set up their triggered data again next turn. (This is necessary because of the weighting system - since any number of things can change the weight of a trigger, keeping the triggers in a list would require updating their weights every turn anyway.)


5. Finally, fire the trigger with CvPlayer::trigger(EventTrigger)...right?

nope. Actually, this is a method which is passed an XML EventTriggerInfo, and it's called in doEvents() if the weight is -1. The entire function is:

Code:
initTriggeredData(eTrigger, true);

So, basically, this is a really roundabout way of saying "just set up this triggered data directly without considering the weighting stuff, and proceed directly to #6."

6. Now fire the trigger, with CvPlayer::trigger(EventTriggeredData).

This is where parts #2 and #5 point to. What does it actually mean to fire a trigger? Why, display a popup, of course! Go to #7.

>>But only if the player is human. For AIs, we just ask them which event they would like to choose from the ones displayed by the trigger, via #8.

7. CvPlayer::addPopup(popup)

#6 hands off the ID of the triggered data to a popup, which it sends here, and that continues on its merry way outside the scope of this reference. (As far as when the popup is actually displayed, best I can tell, that happens deep in the UI part of the DLL, and basically all that matters is that there's a list of popups called m_listPopups in CvPlayer, and this method pushes the popup onto that list.)

8. CvPlayerAI::AI_chooseEvent(EventTriggeredData id)

This method looks at all the events available to the trigger, which the player can do (CvPlayerAI::canDoEvent), and calculates their value (CvPlayerAI::AI_eventValue), and chooses the best one. The details are not really relevant here, but you can fairly easily influence these values through iAIValue in Civ4EventInfos.xml.

So, to sum up, here's the process:

  • doTurn
    • doEvents
      • Loop through triggers, getEventTriggerWeight (and quietly test a bunch of prereqs within that function)
        • if it's -1, go to branch A below; if it's 0, skip it
        • Otherwise, initTriggeredData() to set up a triggered data with text and everything (subtly testing some more prereqs) and add the weight to a pile
        • Choose a number from the weights in the pile and fire that trigger on branch B below
      • Clean up some stuff - I did not mention this above, but this seems to be where events with countdowns that lead to other events are actually applied.
  • A. trigger
    • initTriggeredData() to set up a triggered data, then proceed directly to branch B
  • B. trigger
    • Set up a popup (for humans) or have the player choose the event with the best value (for AIs).
In structure, pretty simple; in practice, as convoluted as the cables under my computer desk were before I discovered velcro.
--------

SO

What does all of this mean for modders?

In short, messing with the trigger system is tricky. Here's exactly what you can do (as far as I have figured out) and how to do it.

How to test if a trigger can be fired based on its XML setup

(
I did not spell out the function calls exactly here, search them up in the SDK if you want to use them)
Spoiler Checks :

  • For general game prereqs, techs, civics, continents, treasury size, prereq events, and numbers of units (on tile/of a type/in general) and buildings (of a type): hit CvPlayer::getEventTriggerWeight(), and if it's greater than 0 you're good. The units/buildings ones will only be considered if the trigger is set to be weighted by numbers of units/buildings, so keep that in mind.
  • For any more specific unit stuff, you can hit CvUnit::getTriggerValue(), which returns MIN_INT if the unit does not meet the trigger requirements, and 0 or a number greater than 0 (weighted by damage/experience if applicable) if it DOES pass. This also checks the plot the unit is on. You can also run pickTriggerUnit to see if there are any units owned by the player that pass the trigger (that one gets the trigger value of every unit and picks the best one from a list that pass all the criteria).
  • For specific plot stuff, you can use CvPlot::canTrigger(); this checks plot ownership, plot type, features, terrains, improvements, bonuses, routes, units (using the above), prereq events.
  • For cities, you can run CvPlayer::pickTriggerCity() to see if the method finds a city; if it returns a list you're good, if NULL, then it's a fail. Cities also have a specific CvCity::getTriggerValue() which tests python, buildings in the city, religions in the city, corporations in the city, population in the city, anger/unhealthiness/prereq events in the city, and food (if CityFoodWeight of the trigger xml is set to 1) and returns MIN_INT if the city fails these tests. pickTriggerCity tests these for all cities, so that's a roundabout way to find out if a certain religion/corporation is present in all cities (using triggers alone).
  • Testing religions and corporations is a little harder, but you can loop through all of them and check CvPlayer::isValidTriggerReligion (or corporation). Or, you can test for specific corporations being present in a civ through the above city thing. State religions are only checked in the city method above, but holy cities are checked in both.
  • To test if a trigger can fire for a specific "Other Player" as defined by prereqs in the trigger XML, you'll want to check CvPlayer::canTrigger(). (which sounds like it should do all of this stuff, but, does not.)
If you are trying to test if a trigger can be triggered without knowing any of the XML that will be used for it, here's what I'd do:
  1. Get the trigger weight first (getEventTriggerWeight) because returning 0 means this trigger should not fire at all, and you can skip everything else.
  2. Then I would just loop through all the plots of the player and check canTrigger for each one, and if it's a city, also check if the city's getTriggerValue. This will implicitly check units on the plots, religions and state religions and corporations (through the city thing), and anything else related to the plot or city.
The following things, copied from the "Things" spoiler above, are ONLY checked within initTriggeredData itself:
Spoiler :

  • Then it checks buildings, and sees if the player has the specified number of SPECIFIED buildings,
  • Then it checks religions, and sees if the player has the specified number of cities with SPECIFIED religions OR a specified number of cities with ANY religions if no religion is specified,
  • Same for corporations,
  • And then we check that the player's population is in the correct range (the player's whole civ, not just a city),
  • [check eTriggerValue] for ALLLL of the units belonging to EVERY player, and add them into a grand total to compare to the trigger's NumUnitsGlobal.



For those you'll have to write your own method, or just go ahead and initTriggeredData() as a third step. But be warned, there is a LOT of looping going on there - so writing your own method may be easier.


How to safely trigger specific events

The easiest way to do this is simply by YourTriggerData = initTriggeredData(EventTrigger, true) - the "true" means the trigger will be fired as soon as the data is set up. There is actually no point to using trigger(EventTrigger) because that just goes directly to initTriggeredData(EventTrigger, true). However, if you already have trigger data or want to set it up yourself, you can use trigger(EventTriggerData) to bypass initTriggeredData.

If you do use initTriggeredData, you need to keep in mind that it adds events to a list stored for the player. From there on, triggers are passed around between functions via an ID that is stored with the trigger in that list; so if you just fire the trigger and move on, it will stay in the list forever. Good memory management requires this line afterward:

Code:
deleteEventTriggered(pTriggerData->getID());

Also, note that you can effectively set up a triggered data through the optional variables passed to initTriggeredData. The full list is:

Code:
initTriggeredData(EventTriggerTypes eEventTrigger,
    bool bFire, int iCityId, int iPlotX, int iPlotY, PlayerTypes eOtherPlayer,
    int iOtherPlayerCityId, ReligionTypes eReligion, CorporationTypes eCorporation,
    int iUnitId, BuildingTypes eBuilding)

If you don't use any of these variables, initTriggeredData will use its methods to pick random stuff that fits the trigger you fired. But if you do, it actually skips all those methods entirely, even if your trigger wasn't supposed to pick them in the first place. Then when your data is passed to the popup, it will pick the stuff, even though your trigger may not have been set up to do that.


The TLDR

If you're going to fire triggers on your own, or based on your own criteria, (for example skipping the die-rolling and weighting), but you also want to make use of the XML prereqs/events, here's what to do:
  1. CvPlayer::getEventTriggerWeight, and if it's 0, skip everything else
  2. Loop through the player's plots; if there is a city, see if CvCity::getTriggerValue is MIN_INT; for the plot, check CvPlot::canTrigger, and see if it returns false; then, do pickTriggerUnit to catch if any units outside the player's borders can use the trigger (it will return NULL if it can't find any). (You could also just loop through all plots and skip the pickTriggerUnit if you think that's easier.) If ALL of this was false or NULL or MIN_INT, then you can be pretty sure the trigger should not fire. (Religions/etc. are accounted for in the city method.)
  3. If one of those returns true that doesn't guarantee your trigger will pass ALL the prereqs, but at that point it's probably easiest to just do this: EventTriggeredData TriggerData = CvPlayer::initTriggeredData(Trigger, true). If the triggered data comes back NULL, then some other requirement was not met. Make sure to include the "true" so your trigger is fired if it's not null, since that saves you about five steps.
  4. Then delete the trigger: deleteEventTriggered(TriggerData->getID()). The ID will be set up in the TriggerData you initialized in step 3, so if this is on the next line just reference that data and everything will work smoothly.
You do not actually have to worry about deleting events with timers or cleaning up countdowns or anything; at that point, events are all handled by doEvents. However, these will not be applied to events forcefully triggered for barbarians, minor civs, or with "Disable Random Events" checked! So be careful - you may end up with permanent events if you use timers in those situations. (To see how these are cleaned up, if you want to do that yourself, look at the top and very bottom of doEvents().)


Appendix. A final thought.

Honestly, I feel like this whole system is just...a mess. It's really unfortunate, given the really awesome simplicity of some of the other mechanics I've dived into (like the spotting stuff).

It's tricky to do random events that target specific things in a game with this many objects to choose from, because rolling a die 1500 times basically guarantees an event will happen. I think this is why they chose to use the roll a die > then if ANY events should trigger figure out which ones can method rather than run through the events or all the tiles or all the players > roll dice to trigger events based on their conditions. But in effect, the rolling/weighting system ends up transferring the weight of checking everything to a "weights" method, which has to first make sure the trigger CAN fire (by looping through basically everything possible), and then has to choose WHERE it should fire (by doing it all over again, but even more!).

In my opinion, a conditional system built on prereqs first, then worry about odds/chances if and where prereqs are met would be much better than either option, and feel much less chaotic for the players.
 
Top Bottom