When are events triggered and how is the triggered event selected?
To make understanding the whole code a bit better, I first need to mention what xml files there are and how the interact with each other. Basically there are two files important for the events:
CIV4EventInfos.xml and CIV4EventTriggerInfos.xml
The EventInfos contains all the stuff related to the actual event. The EventTrigger everything that determines when an Event is triggered. Both files make extensive interaction with python files to interact with the game most found in CvRandomEventInterface.py. As you can imagine the whole event feature can become hideously complicated quickly. To make it even worse quests are also technically events, but do make the whole event feature even more complicated. So if you ever want to divide deep into that topic, beware.
For our purpose we look into CvPlayer.cpp and in there in the method doEvents. This method is called every turn for each player.
I know what you are thinking and yes it is a lot to take into. Let's start simple with the beginning:
Of course with the "No Events" Option you won't encounter events and of course if you are a Barbarian or a Minor Civ you don't encounter those too. Let's look at the next block:
What this code does is checking the python method referenced in PythonExpireCheck inside the EventInfos.xml. This is exclusively used for the quests and ensures quests are expired when their expire conditions are met. But this code otherwise is not relevant for the question above.
The first part to our question can be found in this next code section. When bNewEventEligible remains true an event will be triggered. So when does this become false. First we look for FIRST_EVENT_DELAY_TURNS in the GlobalDefines.XML. This is set to 20 and our first check uses this. During the first 20 turns no events are triggered.
The next check is a bit more complicated. We once need GlobalDefines.XML again and here EVENT_PROBABILITY_ROLL_SIDES which is set to 100. Basically the game rolls a d100 and the result of that is checked against iEventChancePerTurn inside the EraInfos.xml the respective values are:
Ancient = 1
Classical = 2
Medieval = 4
Renaissance = 4
Industrial = 6
Modern = 8
Future = 10
If the game rolls above or equal to these numbers no event is triggered. With the way the game rolls random numbers the numbers above are basically the chance you have for an event each turn.
We get some answers to the second part of our question in this block. If bNewEventEligible = true we iterate across all the events in EventTriggerInfo and add up all the iWeight bigger then 0 found in the EventTriggerInfo to get the total amount of weights. Now it's important to know that initTriggeredData does check if the conditions for the trigger are met even the ones done by python. So only triggerable events go into the total. By the way initTriggeredData is a 500+ lines monster of a method, which I don't want to delve into with this segment.
Now you may wonder what the "iWeight == -1" part is about and why these events are triggered without any check for EventTriggerInfo and disregarding the previous event chance per era. This is another part of the quests. You will notice that quests have a trigger and an event for the start of the quest. But the end of a quest with the results is also a trigger + event. All of these have the weight -1 and this code ensures that the quests are completed. I don't want to delve any deeper into that part, because it doesn't contribute to the answer of the question, but you can see how the event feature gets complicated thanks to the tagged on quests.
Now what we will do with the TotalWeight will be looked at in the next code segment:
So if iTotalWeight is bigger then 0 we roll a random number between 0 and and iTotalWeight - 1 and this number selects the actual trigger and event being fired. For this the game collected all triggers in the previous code segment and arranged them in a specific order. Let's do a quick example:
3 triggers are consideres with the weights 100, 300 and 200. The game adds up all the weights = 600 and stacks the triggers so that a number between 0 - 100 uses the first trigger, a number between 101 and 400 uses the second and a number between 401 and 600 uses the last.
Now you may have noticed that a lot more code comes after this code segment, but that code is no longer relevant for the question. It deals with special events that have follow up events like the healing plant event.
Summary
A lot of code boils down to a simple answer. The chance for an event to happen increases with each era with the following numbers.
Ancient = 1%
Classical = 2%
Medieval = 4%
Renaissance = 4%
Industrial = 6%
Modern = 8%
Future = 10%
The game then chooses an event trigger based on a standard weighted distribution with the iWeight as long as that trigger condition is fullfilled. If no trigger condition is fulfilled no event is fired.
To make understanding the whole code a bit better, I first need to mention what xml files there are and how the interact with each other. Basically there are two files important for the events:
CIV4EventInfos.xml and CIV4EventTriggerInfos.xml
The EventInfos contains all the stuff related to the actual event. The EventTrigger everything that determines when an Event is triggered. Both files make extensive interaction with python files to interact with the game most found in CvRandomEventInterface.py. As you can imagine the whole event feature can become hideously complicated quickly. To make it even worse quests are also technically events, but do make the whole event feature even more complicated. So if you ever want to divide deep into that topic, beware.
For our purpose we look into CvPlayer.cpp and in there in the method doEvents. This method is called every turn for each player.
Code:
void CvPlayer::doEvents()
{
if (GC.getGameINLINE().isOption(GAMEOPTION_NO_EVENTS))
{
return;
}
if (isBarbarian() || isMinorCiv())
{
return;
}
CvEventMap::iterator it = m_mapEventsOccured.begin();
while (it != m_mapEventsOccured.end())
{
if (checkExpireEvent(it->first, it->second))
{
expireEvent(it->first, it->second, true);
it = m_mapEventsOccured.erase(it);
}
else
{
++it;
}
}
bool bNewEventEligible = true;
if (GC.getGameINLINE().getElapsedGameTurns() < GC.getDefineINT("FIRST_EVENT_DELAY_TURNS"))
{
bNewEventEligible = false;
}
if (bNewEventEligible)
{
if (GC.getGameINLINE().getSorenRandNum(GC.getDefineINT("EVENT_PROBABILITY_ROLL_SIDES"), "Global event check") >= GC.getEraInfo(getCurrentEra()).getEventChancePerTurn())
{
bNewEventEligible = false;
}
}
std::vector< std::pair<EventTriggeredData*, int> > aePossibleEventTriggerWeights;
int iTotalWeight = 0;
for (int i = 0; i < GC.getNumEventTriggerInfos(); ++i)
{
int iWeight = getEventTriggerWeight((EventTriggerTypes)i);
if (iWeight == -1)
{
trigger((EventTriggerTypes)i);
}
else if (iWeight > 0 && bNewEventEligible)
{
EventTriggeredData* pTriggerData = initTriggeredData((EventTriggerTypes)i);
if (NULL != pTriggerData)
{
iTotalWeight += iWeight;
aePossibleEventTriggerWeights.push_back(std::make_pair(pTriggerData, iTotalWeight));
}
}
}
if (iTotalWeight > 0)
{
bool bFired = false;
int iValue = GC.getGameINLINE().getSorenRandNum(iTotalWeight, "Event trigger");
for (std::vector< std::pair<EventTriggeredData*, int> >::iterator it = aePossibleEventTriggerWeights.begin(); it != aePossibleEventTriggerWeights.end(); ++it)
{
EventTriggeredData* pTriggerData = (*it).first;
if (NULL != pTriggerData)
{
if (iValue < (*it).second && !bFired)
{
trigger(*pTriggerData);
bFired = true;
}
else
{
deleteEventTriggered(pTriggerData->getID());
}
}
}
}
std::vector<int> aCleanup;
for (int i = 0; i < GC.getNumEventInfos(); ++i)
{
const EventTriggeredData* pTriggeredData = getEventCountdown((EventTypes)i);
if (NULL != pTriggeredData)
{
if (GC.getGameINLINE().getGameTurn() >= pTriggeredData->m_iTurn)
{
applyEvent((EventTypes)i, pTriggeredData->m_iId);
resetEventCountdown((EventTypes)i);
aCleanup.push_back(pTriggeredData->m_iId);
}
}
}
for (std::vector<int>::iterator it = aCleanup.begin(); it != aCleanup.end(); ++it)
{
bool bDelete = true;
for (int i = 0; i < GC.getNumEventInfos(); ++i)
{
const EventTriggeredData* pTriggeredData = getEventCountdown((EventTypes)i);
if (NULL != pTriggeredData)
{
if (pTriggeredData->m_iId == *it)
{
bDelete = false;
break;
}
}
}
if (bDelete)
{
deleteEventTriggered(*it);
}
}
}
I know what you are thinking and yes it is a lot to take into. Let's start simple with the beginning:
Code:
if (GC.getGameINLINE().isOption(GAMEOPTION_NO_EVENTS))
{
return;
}
if (isBarbarian() || isMinorCiv())
{
return;
}
Of course with the "No Events" Option you won't encounter events and of course if you are a Barbarian or a Minor Civ you don't encounter those too. Let's look at the next block:
Code:
CvEventMap::iterator it = m_mapEventsOccured.begin();
while (it != m_mapEventsOccured.end())
{
if (checkExpireEvent(it->first, it->second))
{
expireEvent(it->first, it->second, true);
it = m_mapEventsOccured.erase(it);
}
else
{
++it;
}
}
What this code does is checking the python method referenced in PythonExpireCheck inside the EventInfos.xml. This is exclusively used for the quests and ensures quests are expired when their expire conditions are met. But this code otherwise is not relevant for the question above.
Code:
bool bNewEventEligible = true;
if (GC.getGameINLINE().getElapsedGameTurns() < GC.getDefineINT("FIRST_EVENT_DELAY_TURNS"))
{
bNewEventEligible = false;
}
if (bNewEventEligible)
{
if (GC.getGameINLINE().getSorenRandNum(GC.getDefineINT("EVENT_PROBABILITY_ROLL_SIDES"), "Global event check") >= GC.getEraInfo(getCurrentEra()).getEventChancePerTurn())
{
bNewEventEligible = false;
}
}
The first part to our question can be found in this next code section. When bNewEventEligible remains true an event will be triggered. So when does this become false. First we look for FIRST_EVENT_DELAY_TURNS in the GlobalDefines.XML. This is set to 20 and our first check uses this. During the first 20 turns no events are triggered.
The next check is a bit more complicated. We once need GlobalDefines.XML again and here EVENT_PROBABILITY_ROLL_SIDES which is set to 100. Basically the game rolls a d100 and the result of that is checked against iEventChancePerTurn inside the EraInfos.xml the respective values are:
Ancient = 1
Classical = 2
Medieval = 4
Renaissance = 4
Industrial = 6
Modern = 8
Future = 10
If the game rolls above or equal to these numbers no event is triggered. With the way the game rolls random numbers the numbers above are basically the chance you have for an event each turn.
Code:
std::vector< std::pair<EventTriggeredData*, int> > aePossibleEventTriggerWeights;
int iTotalWeight = 0;
for (int i = 0; i < GC.getNumEventTriggerInfos(); ++i)
{
int iWeight = getEventTriggerWeight((EventTriggerTypes)i);
if (iWeight == -1)
{
trigger((EventTriggerTypes)i);
}
else if (iWeight > 0 && bNewEventEligible)
{
EventTriggeredData* pTriggerData = initTriggeredData((EventTriggerTypes)i);
if (NULL != pTriggerData)
{
iTotalWeight += iWeight;
aePossibleEventTriggerWeights.push_back(std::make_pair(pTriggerData, iTotalWeight));
}
}
}
We get some answers to the second part of our question in this block. If bNewEventEligible = true we iterate across all the events in EventTriggerInfo and add up all the iWeight bigger then 0 found in the EventTriggerInfo to get the total amount of weights. Now it's important to know that initTriggeredData does check if the conditions for the trigger are met even the ones done by python. So only triggerable events go into the total. By the way initTriggeredData is a 500+ lines monster of a method, which I don't want to delve into with this segment.
Now you may wonder what the "iWeight == -1" part is about and why these events are triggered without any check for EventTriggerInfo and disregarding the previous event chance per era. This is another part of the quests. You will notice that quests have a trigger and an event for the start of the quest. But the end of a quest with the results is also a trigger + event. All of these have the weight -1 and this code ensures that the quests are completed. I don't want to delve any deeper into that part, because it doesn't contribute to the answer of the question, but you can see how the event feature gets complicated thanks to the tagged on quests.
Now what we will do with the TotalWeight will be looked at in the next code segment:
Code:
if (iTotalWeight > 0)
{
bool bFired = false;
int iValue = GC.getGameINLINE().getSorenRandNum(iTotalWeight, "Event trigger");
for (std::vector< std::pair<EventTriggeredData*, int> >::iterator it = aePossibleEventTriggerWeights.begin(); it != aePossibleEventTriggerWeights.end(); ++it)
{
EventTriggeredData* pTriggerData = (*it).first;
if (NULL != pTriggerData)
{
if (iValue < (*it).second && !bFired)
{
trigger(*pTriggerData);
bFired = true;
}
else
{
deleteEventTriggered(pTriggerData->getID());
}
}
}
}
So if iTotalWeight is bigger then 0 we roll a random number between 0 and and iTotalWeight - 1 and this number selects the actual trigger and event being fired. For this the game collected all triggers in the previous code segment and arranged them in a specific order. Let's do a quick example:
3 triggers are consideres with the weights 100, 300 and 200. The game adds up all the weights = 600 and stacks the triggers so that a number between 0 - 100 uses the first trigger, a number between 101 and 400 uses the second and a number between 401 and 600 uses the last.
Now you may have noticed that a lot more code comes after this code segment, but that code is no longer relevant for the question. It deals with special events that have follow up events like the healing plant event.
Summary
A lot of code boils down to a simple answer. The chance for an event to happen increases with each era with the following numbers.
Ancient = 1%
Classical = 2%
Medieval = 4%
Renaissance = 4%
Industrial = 6%
Modern = 8%
Future = 10%
The game then chooses an event trigger based on a standard weighted distribution with the iWeight as long as that trigger condition is fullfilled. If no trigger condition is fulfilled no event is fired.
Mods: RtR CtH
Pitboss: PB39, PB40, PB52, PB59 Useful Collections: Pickmethods, Mapmaking, Curious Civplayer
Buy me a coffee
Pitboss: PB39, PB40, PB52, PB59 Useful Collections: Pickmethods, Mapmaking, Curious Civplayer
Buy me a coffee