As a French person I feel like it's my duty to explain strikes to you. - AdrienIer

Create an account  

 
Curious Civplayer - Mysteries of the DLL

How does vision and the sentry promotion work?

Beware this will contain math.  yikes We start our journey in CvPlot.cpp and in there in the method changeAdjacentSight. This method is called on all occasions that change the visibile, fogged and not visible tiles.

Code:
void CvPlot::changeAdjacentSight(TeamTypes eTeam, int iRange, bool bIncrement, CvUnit* pUnit, bool bUpdatePlotGroups)
{
    bool bAerial = (pUnit != NULL && pUnit->getDomainType() == DOMAIN_AIR);

    DirectionTypes eFacingDirection = NO_DIRECTION;
    if (!bAerial && NULL != pUnit)
    {
        eFacingDirection = pUnit->getFacingDirection(true);
    }

    //fill invisible types
    std::vector<InvisibleTypes> aSeeInvisibleTypes;
    if (NULL != pUnit)
    {
        for(int i=0;i<pUnit->getNumSeeInvisibleTypes();i++)
        {
            aSeeInvisibleTypes.push_back(pUnit->getSeeInvisibleType(i));
        }
    }

    if(aSeeInvisibleTypes.size() == 0)
    {
        aSeeInvisibleTypes.push_back(NO_INVISIBLE);
    }

    //check one extra outer ring
    if (!bAerial)
    {
        iRange++;
    }

    for(int i=0;i<(int)aSeeInvisibleTypes.size();i++)
    {
        for (int dx = -iRange; dx <= iRange; dx++)
        {
            for (int dy = -iRange; dy <= iRange; dy++)
            {
                //check if in facing direction
                if (bAerial || shouldProcessDisplacementPlot(dx, dy, iRange - 1, eFacingDirection))
                {
                    bool outerRing = false;
                    if ((abs(dx) == iRange) || (abs(dy) == iRange))
                    {
                        outerRing = true;
                    }

                    //check if anything blocking the plot
                    if (bAerial || canSeeDisplacementPlot(eTeam, dx, dy, dx, dy, true, outerRing))
                    {
                        CvPlot* pPlot = plotXY(getX_INLINE(), getY_INLINE(), dx, dy);
                        if (NULL != pPlot)
                        {
                            pPlot->changeVisibilityCount(eTeam, ((bIncrement) ? 1 : -1), aSeeInvisibleTypes[i], bUpdatePlotGroups);
                        }
                    }
                }
                
                if (eFacingDirection != NO_DIRECTION)
                {
                    if((abs(dx) <= 1) && (abs(dy) <= 1)) //always reveal adjacent plots when using line of sight
                    {
                        CvPlot* pPlot = plotXY(getX_INLINE(), getY_INLINE(), dx, dy);
                        if (NULL != pPlot)
                        {
                            pPlot->changeVisibilityCount(eTeam, 1, aSeeInvisibleTypes[i], bUpdatePlotGroups);
                            pPlot->changeVisibilityCount(eTeam, -1, aSeeInvisibleTypes[i], bUpdatePlotGroups);
                        }
                    }
                }
            }
        }
    }
}

So let's start with the simple things in the beginning.

1. We check if the unit, if any, is an air unit. We know those have different vision rules and therefore we remember this in the variable bAerial
2. We save the direction the ground unit is facing, if any.
3. We fill an array with all the different units according to their visibility. We make sure that this array has at least the normal units.
4. If we have a ground unit, city, basically vision from the ground, we increase the iRange by 1. This important so that we see tall tiles like peaks that are normally outside of our vision.

This last one the iRange is as you have already guessed the vision range of the unit and this value is the value increased by 1 with the sentry promotion. Next up the actually interesting part in the code opens up:

Code:
for(int i=0;i<(int)aSeeInvisibleTypes.size();i++)
    {
        for (int dx = -iRange; dx <= iRange; dx++)
        {
            for (int dy = -iRange; dy <= iRange; dy++)
            {
As you can see we first iterate through the different invisibility types. Yes the game goes through all the tiles multiple times for each invisibility type. For the standard case though we can ignore this as we are only interested in the general vision type of the majority of units. Next up we open more for loops. Basically we are iterating over all the tiles that could be in vision at all starting from the bottom left going right until we arrive at the top right. So keep in mind that the following code is run over each of those tiles:

Code:
//check if in facing direction
if (bAerial || shouldProcessDisplacementPlot(dx, dy, iRange - 1, eFacingDirection))
{
    bool outerRing = false;
    if ((abs(dx) == iRange) || (abs(dy) == iRange))
    {
        outerRing = true;
    }

    //check if anything blocking the plot
    if (bAerial || canSeeDisplacementPlot(eTeam, dx, dy, dx, dy, true, outerRing))
    {
        CvPlot* pPlot = plotXY(getX_INLINE(), getY_INLINE(), dx, dy);
        if (NULL != pPlot)
        {
            pPlot->changeVisibilityCount(eTeam, ((bIncrement) ? 1 : -1), aSeeInvisibleTypes[i], bUpdatePlotGroups);
        }
    }
}

if (eFacingDirection != NO_DIRECTION)
{
    if((abs(dx) <= 1) && (abs(dy) <= 1)) //always reveal adjacent plots when using line of sight
    {
        CvPlot* pPlot = plotXY(getX_INLINE(), getY_INLINE(), dx, dy);
        if (NULL != pPlot)
        {
            pPlot->changeVisibilityCount(eTeam, 1, aSeeInvisibleTypes[i], bUpdatePlotGroups);
            pPlot->changeVisibilityCount(eTeam, -1, aSeeInvisibleTypes[i], bUpdatePlotGroups);
        }
    }
}

As you can see we are doing to bigger if-clauses. As you can see we only go into the first clause if we are processing an air unit or if this omnious shouldProcessDisplacementPlot returns true. I've looked at that method and I'm non the wiser. I spare you the code. Basically what is happening here is that the game calculates something of a vision cone and tiles not in that cone won't be processed, but doing tests on my own this method must return true always ensuring that the well know perfect square of tiles around your unit is processed. If you want to have a go at that method I put it in spoilers:

Code:
bool CvPlot::shouldProcessDisplacementPlot(int dx, int dy, int range, DirectionTypes eFacingDirection) const
{
    if(eFacingDirection == NO_DIRECTION)
    {
        return true;
    }
    else if((dx == 0) && (dy == 0)) //always process this plot
    {
        return true;
    }
    else
    {
        //                          N       NE      E       SE          S       SW      W           NW
        int displacements[8][2] = {{0, 1}, {1, 1}, {1, 0}, {1, -1}, {0, -1}, {-1, -1}, {-1, 0}, {-1, 1}};

        int directionX = displacements[eFacingDirection][0];
        int directionY = displacements[eFacingDirection][1];
        
        //compute angle off of direction
        int crossProduct = directionX * dy - directionY * dx; //cross product
        int dotProduct = directionX * dx + directionY * dy; //dot product

        float theta = atan2((float) crossProduct, (float) dotProduct);
        float spread = 60 * (float) M_PI / 180;
        if((abs(dx) <= 1) && (abs(dy) <= 1)) //close plots use wider spread
        {
            spread = 90 * (float) M_PI / 180;
        }

        if((theta >= -spread / 2) && (theta <= spread / 2))
        {
            return true;
        }
        else
        {
            return false;
        }
        
        /*
        DirectionTypes leftDirection = GC.getTurnLeftDirection(eFacingDirection);
        DirectionTypes rightDirection = GC.getTurnRightDirection(eFacingDirection);

        //test which sides of the line equation (cross product)
        int leftSide = displacements[leftDirection][0] * dy - displacements[leftDirection][1] * dx;
        int rightSide = displacements[rightDirection][0] * dy - displacements[rightDirection][1] * dx;
        if((leftSide <= 0) && (rightSide >= 0))
            return true;
        else
            return false;
        */
    }
}

Inside this first if-clause we first check if the tile in question is in the outermost ring that we added for ground vision and remember this in a variable outerRing. We are almost done with this bigger if-clause. The next part will turn this tile visibile either if the unit is an air unit or if this new method canSeeDisplacementPlot returns true. In this method the actual calculation concering height differences happens and we will have a look at it after we are done here.

We are almost done with this method only looking at the second big if-clause

Code:
if (eFacingDirection != NO_DIRECTION)
{
    if((abs(dx) <= 1) && (abs(dy) <= 1)) //always reveal adjacent plots when using line of sight
    {
        CvPlot* pPlot = plotXY(getX_INLINE(), getY_INLINE(), dx, dy);
        if (NULL != pPlot)
        {
            pPlot->changeVisibilityCount(eTeam, 1, aSeeInvisibleTypes[i], bUpdatePlotGroups);
            pPlot->changeVisibilityCount(eTeam, -1, aSeeInvisibleTypes[i], bUpdatePlotGroups);
        }
    }
}

The developers were merciful here and the comment in there already describes perfectly what is happening here. We ensure that the first ring of tiles is always visible for units.

So the next and last destination on our tour through the vision code is the method canSeeDisplacementPlot and here it is:

Code:
bool CvPlot::canSeeDisplacementPlot(TeamTypes eTeam, int dx, int dy, int originalDX, int originalDY, bool firstPlot, bool outerRing) const
{
    CvPlot *pPlot = plotXY(getX_INLINE(), getY_INLINE(), dx, dy);
    if (pPlot != NULL)
    {
        //base case is current plot
        if((dx == 0) && (dy == 0))
        {
            return true;
        }

        //find closest of three points (1, 2, 3) to original line from Start (S) to End (E)
        //The diagonal is computed first as that guarantees a change in position
        // -------------
        // |   | 2 | S |
        // -------------
        // | E | 1 | 3 |
        // -------------

        int displacements[3][2] = {{dx - getSign(dx), dy - getSign(dy)}, {dx - getSign(dx), dy}, {dx, dy - getSign(dy)}};
        int allClosest[3];
        int closest = -1;
        for (int i=0;i<3;i++)
        {
            //int tempClosest = abs(displacements[i][0] * originalDX - displacements[i][1] * originalDY); //more accurate, but less structured on a grid
            allClosest[i] = abs(displacements[i][0] * dy - displacements[i][1] * dx); //cross product
            if((closest == -1) || (allClosest[i] < closest))
            {
                closest = allClosest[i];
            }
        }

        //iterate through all minimum plots to see if any of them are passable
        for(int i=0;i<3;i++)
        {
            int nextDX = displacements[i][0];
            int nextDY = displacements[i][1];
            if((nextDX != dx) || (nextDY != dy)) //make sure we change plots
            {
                if(allClosest[i] == closest)
                {
                    if(canSeeDisplacementPlot(eTeam, nextDX, nextDY, originalDX, originalDY, false, false))
                    {
                        int fromLevel = seeFromLevel(eTeam);
                        int throughLevel = pPlot->seeThroughLevel();
                        if(outerRing) //check strictly higher level
                        {
                            CvPlot *passThroughPlot = plotXY(getX_INLINE(), getY_INLINE(), nextDX, nextDY);
                            int passThroughLevel = passThroughPlot->seeThroughLevel();
                            if (fromLevel >= passThroughLevel)
                            {
                                if((fromLevel > passThroughLevel) || (pPlot->seeFromLevel(eTeam) > fromLevel)) //either we can see through to it or it is high enough to see from far
                                {
                                    return true;
                                }
                            }
                        }
                        else
                        {
                            if(fromLevel >= throughLevel) //we can clearly see this level
                            {
                                return true;
                            }
                            else if(firstPlot) //we can also see it if it is the first plot that is too tall
                            {
                                return true;
                            }
                        }
                    }
                }
            }
        }
    }
    
    return false;
}

A short advise for the parameters of this method. If you remember our previous method then you may know that we put the same x and y values into
int dx and int originalDX/ int dy and int originalDY. We also set bool firstPlot to true.

We start simple again with a basic null reference check for the plot and after that we check if the plot is the tile we are actually standing on. In that case we return true immediately as this tile is always visible. We now turn to some more complicated code, which includes math. If you ever wondered why you need math in those upper math classes in school here is one example for why you need it.

Code:
//find closest of three points (1, 2, 3) to original line from Start (S) to End (E)
        //The diagonal is computed first as that guarantees a change in position
        // -------------
        // |   | 2 | S |
        // -------------
        // | E | 1 | 3 |
        // -------------

        int displacements[3][2] = {{dx - getSign(dx), dy - getSign(dy)}, {dx - getSign(dx), dy}, {dx, dy - getSign(dy)}};
        int allClosest[3];
        int closest = -1;
        for (int i=0;i<3;i++)
        {
            //int tempClosest = abs(displacements[i][0] * originalDX - displacements[i][1] * originalDY); //more accurate, but less structured on a grid
            allClosest[i] = abs(displacements[i][0] * dy - displacements[i][1] * dx); //cross product
            if((closest == -1) || (allClosest[i] < closest))
            {
                closest = allClosest[i];
            }
        }

        //iterate through all minimum plots to see if any of them are passable
        for(int i=0;i<3;i++)
        {
            int nextDX = displacements[i][0];
            int nextDY = displacements[i][1];
            if((nextDX != dx) || (nextDY != dy)) //make sure we change plots
            {
                if(allClosest[i] == closest)
                {
                    if(canSeeDisplacementPlot(eTeam, nextDX, nextDY, originalDX, originalDY, false, false))
                    {

This is a lot of math for something actually very simple to understand. In the end it all boils down to us looking for the shortest path between our tile in question and the tile we are standing on. The little piktogram provided by the developers gives you a good idea of where we are going with this. The one important thing to remember is that you can have multiple shortest path if two tiles have the same distance. You can also see that this method we are looking at is called for each tile on the path again to ensure we can see the tile on the path.

Next we go through the inner part:

Code:
int fromLevel = seeFromLevel(eTeam);
                        int throughLevel = pPlot->seeThroughLevel();

So the first two important things are the new terms seeFromLevel and seeThroughLevel. The first one is basically the height of the tile and the second one, which height is required to look through this tile to the next. But beware there are some unintuitive things in their definition. These values are defined in the CIV4TerrainInfos.xml. In there all of grassland, plains, desert, tundra and snow have 1 for both values and coast, ocean, peak and hill all have 0 for both values. Now this sounds strange with hills and peaks having the same value as water tiles. Those two always have one of the other types as a base value, so they are essentially also 1. But this is still not right with peaks and hills having the same values as flatland tiles. Well there is another set of values defined in GlobalDefines.xml containing SEE_FROM_CHANGE and SEE_THROUGH_CHANGE in their name. These are added to the other values. With those in mind we have the following values for seeFromLevel as well as seeThroughLevel:

Watertile = 0
Flatland = 1
Hill = 2
Peak = 3

One special case though are the watertiles. With the right technology (Optics) they get added 1 to their seeFromLevel value.

With those values out of the way let's turn to the code at hand.

Code:
int fromLevel = seeFromLevel(eTeam);
int throughLevel = pPlot->seeThroughLevel();
if(outerRing) //check strictly higher level
{
    CvPlot *passThroughPlot = plotXY(getX_INLINE(), getY_INLINE(), nextDX, nextDY);
    int passThroughLevel = passThroughPlot->seeThroughLevel();
    if (fromLevel >= passThroughLevel)
    {
        if((fromLevel > passThroughLevel) || (pPlot->seeFromLevel(eTeam) > fromLevel)) //either we can see through to it or it is high enough to see from far
        {
            return true;
        }
    }
}
else
{
    if(fromLevel >= throughLevel) //we can clearly see this level
    {
        return true;
    }
    else if(firstPlot) //we can also see it if it is the first plot that is too tall
    {
        return true;
    }
}

First we get the seeFromLevel from the tile we are standing on and then the seeThroughLevel for the tile we are currently checking. We then start first a check for the outerRing tiles:

Code:
CvPlot *passThroughPlot = plotXY(getX_INLINE(), getY_INLINE(), nextDX, nextDY);
    int passThroughLevel = passThroughPlot->seeThroughLevel();
    if (fromLevel >= passThroughLevel)
    {
        if((fromLevel > passThroughLevel) || (pPlot->seeFromLevel(eTeam) > fromLevel)) //either we can see through to it or it is high enough to see from far
        {
            return true;
        }
    }

We get the next tile on the shortest path and keep it in passThroughPlot; we also get its seeThroughLevel. If we are standing higher or on the same level as the passedThrough tile we continue. Next up we return true, meaning the tile can be seen, only if either we are standing higher then the passedThrough tile or if the tile we are checking is higher then our current position. This ensures that only tall structures can be seen in this extra outer ring.

For all the tiles not in this extra outer ring we do the following:

Code:
if(fromLevel >= throughLevel) //we can clearly see this level
    {
        return true;
    }
    else if(firstPlot) //we can also see it if it is the first plot that is too tall
    {
        return true;
    }

We return true, meaning the tile can be seen, if we are standing higher or on the same level as the tile in question. We also return true if we arrive here with the initial tile being checked, which called this method with firstPlot=true. Now very important this does not mean that every tile in your vision range is turned visible. This is only true for the tile directly adjacent to you. All other tiles rely on the shortest path tiles and if those return false the tile in question is also not visible.

Summary

This consistent of a lot of code so let me try to summarize. First all units have a vision range, most of them just 1 meaning the tiles around them. This range can be increase with the sentry promotion by 1. But this does not mean that the tiles are immediately visible rather it means that all those tiles in your vision range are processed for a vision check. For ground units we also check and additional ring around them for tall structures. Air units are the exception they just reveal all the tiles in their vision.
For the actual vision calculation we reveal all the tiles directly adjacent to you. All tiles farther away calculate the shortest path to your point. If any tiles on that path block your vision the tile in question is not visible. For that all tiles have a height value:

Watertile = 0
Flatland = 1
Hill = 2
Peak = 3

Special care is taken for those outer ring tiles I mentioned. Those can only be visible if not blocked by the shortest path like all other tiles and in addtion are only visible if:

1. Either they are higher then your point.
2. The first tile in your vision ring between them and you has a lower height as you.
Mods: RtR    CtH

Pitboss: PB39, PB40PB52, PB59 Useful Collections: Pickmethods, Mapmaking, Curious Civplayer

Buy me a coffee
Reply

@Charriu: What is the exact trigger for unlocking the Heroic Epic? Do I have to promote a unit 3 times (what about AGG/PRO promotions)? Is it enough to have 10Exp on a unit or does it need to be promoted to unlock the HE? If the latter do I have to own a triple-promoted unit at the start/end of a turn or is is sufficient if I promote the unit and the it immediatly dies?
Reply

For the Heroic Epic you need:

- A barracks in the city
- A unit promoted 3 times.

For the unit you can achieve this requirement via two ways:

1. Promote a unit 3 times during the cause of the whole game. Just 10 XP is not enough the unit needs to be promoted 3 times via XP. The leader promotion given by great generals does not count for these. Same goes for promotions from traits (AGG, PRO) or buildings/events like dun. It doesn't matter when you promote. The unit can even die as long as you promoted it 3 times. Auto-promoting 3 times is also enough.

2. Somebody gifts you a unit promoted like in #1. You can even gift that unit away afterwards.
Mods: RtR    CtH

Pitboss: PB39, PB40PB52, PB59 Useful Collections: Pickmethods, Mapmaking, Curious Civplayer

Buy me a coffee
Reply

That's right. I remember looking into this some time ago. The requirement is literally what it says, "have a level 4 unit", which means that it must have actually promoted three times, just having the experience isn't enough. And once you ever satisfy that once, including via a gifted unit, you never lose that qualification.
Reply

I have kind of a big one if you would be willing:

How do AI weights work in civ 4 and how can I manipulate them? Particularly for tech choices?
Reply

That's definitely a big one and I'm not sure AI weights work the same way for each game mechanic. I would also split this into it's own thing like a "Autopsy of a Civ AI", because there are a lot more things one can analyze with the AI and it's not strictly a game mechanic.
When you say "how can I manipulate them", do you mean during the game or as a modder?
Mods: RtR    CtH

Pitboss: PB39, PB40PB52, PB59 Useful Collections: Pickmethods, Mapmaking, Curious Civplayer

Buy me a coffee
Reply

(March 21st, 2023, 02:52)Charriu Wrote: That's definitely a big one and I'm not sure AI weights work the same way for each game mechanic. I would also split this into it's own thing like a "Autopsy of a Civ AI", because there are a lot more things one can analyze with the AI and it's not strictly a game mechanic.
When you say "how can I manipulate them", do you mean during the game or as a modder?

Uhh, that would be great.
I would really like to manipulate the AI as a modder. For example, make the Ljosalfar AI in FfH (extra modmod to be more precise) not go for Calendar before they got their essential techs, if they don't have a calendar resource.
Reply

In which order do happen things during a turn order?

A question frequently asked for different topics. Now I won't give you code examples this time as those would be to much. I will also exclude anything that is uninteresting for the player (debug messages, logfiles etc.) as well as anything that only affects the AI. Think of it more like a general list of things that happen. Everything starts in CvGame.cpp and in there in the method update. I can tell you when this method is called as that lies outside of the SDK, but we already know that the turn roll happens for the different game modes at the following times.

- Singleplayer: When you hit the done button
- PB sequential: When you hit the done button
- PB simultaneous: When the last player hits the done button, the server calls the turn roll.
- PBEM: When you hit the done button

In all these instances the method update is called. In it the following important things happen:

  1. The Autosave is generated
  2. doTurn is called
  3. The score is updated
  4. In an always war game it is made sure everybody is at war
  5. In a simultaneous PB the order of players is shuffled
  6. Automatic movements are processed (movements you assigned for the following turns). This also includes barbarians.
  7. Timers are updated. This apparently manages combat from automatic movement. This is the one part that would need more investigation.
  8. Update turn timer
  9. For each human player the automatically worked tiles are reassigned
  10. Verfiy if players are alive and set their state accordingly
  11. Set game over state if appropiate
Now the next interesting step is the second doTurn. In here the following things happen.

  1. The score is updated
  2. Deals are verified and cancelled if necessary
  3. Duration of peace deals are updated
  4. For each alive team their doTurn is called
  5. The doTurn of the map is called.
  6. Barbarian cities are created
  7. Barbarian units are created
  8. Global Warming happens
  9. doHolyCity() is called. This is only relevant for late era starts. This manages religion founding, holy city allocation and random religious spreads
  10. doHeadquarters() is called. This is only relevant for late era starts. It randomly founds corporations headquarters.
  11. Results of any diplo votes (AP or UN) are processed
  12. Votes from AP or UN are triggered if timer has passed.
  13. Increment game turn
  14. Increment elapsed game turns
  15. In a simultaneous PB the order of players is shuffled and in the new order each players turn is set to active.
  16. In a team game the next teams turn is set to active. This in turn sets the turn of the teams players to active in player order
  17. In all other modes the next players turn is set to active.
  18. Test for victory conditions fulfilled
  19. The game engines doTurn happens.
A lot of more doTurns are called here. Let's do them in order. First the CvTeam.cpp doTurn is called for each team.

  1. doWarCounter() is called. This is an addition by us. It just countes the turns the team is at war
  2. Barbarians gain their passive tech progress
  3. The visibility of each plot on the map is updated for each team. Keep in mind this happens during each teams doTurn call so the visibility of all teams is updated multiple times.
  4. The counter espionage turns left are updated for each team. Keep in mind this happens during each teams doTurn call so the visibility of all teams is updated multiple times.
  5. The counter espionage modifiers are updated for each team. Keep in mind this happens during each teams doTurn call so the visibility of all teams is updated multiple times.
  6. If Tech brokering is off all Techs are set to tradable. This will be touched later again
  7. War weariness is updated
  8. Test if circumnavigation happened
The next mystery doTurn is that of CvMap. In there doTurn of each plot (tile) of the map is called so we immediately look at CvPlot's doTurn.

  1. The ForceUnownedTimer is decreased by 1. This timer is set when you for example conquer a city and do not immediately own the surrounding tiles.
  2. The duration you owned a tile is incremented by 1. This is used for score calculation.
  3. The duration the improvement existed is incremented by 1. To my knowledge this is only used for AI.
  4. Growth of features is process aka forest and jungle growth
  5. Revolts due to culture are processed
  6. verifyUnitValidPlot() is called. This in general checks units position and teleports them accordingly.
And with that we turn to the biggest step in the CvPlayer.cpp setTurnActive method. For the most part there is "boring" stuff that only handles server communication etc. but there is also a call to our beloved doTurn method again, but this time the CvPlayers version and a call to a new doTurnUnits. We look first at doTurn. Keep in mind this do doTurn happens for all players in a simultaneous turn and the end when all players finished their turns. In every other mode this happens when you press the turn done button.

  1. Deals are checked if they are still valid
  2. Revolution timer is decremented. This timer is for civic changes not for revolutions of cities.
  3. The timer for religious conversions is decremented this
  4. The conscription limit is reset.
  5. For each human player the automatically worked tiles are reassigned
  6. The slider is updated if you can't pay the maintenance
  7. Your current gold is updated. If you cannot pay strike hits.
  8. Beakers are applied to your current research project
  9. You can espionage points towards other players, if you have met someone.
  10. Now every cities doTurn is called from oldest to youngest city.
  11. The golden age timer is decremented and the golden age ends potentially
  12. The anarchy timer is decremented
  13. Civics are verified and you may get kicked out of certain civics think The Pyramids being razed
  14. All trade routes are recalculated
  15. Assign unhappiness from war weariness
  16. Events are triggered for the player
  17. Update all the values for the graphs
To get this out of the way immediately. We also check the doTurn of CvCity.cpp

  1. The city heals its HP from bombardment
  2. Cities defenses are recalculated after healing
  3. Reset bombarded, plundered, drafted and airlifted state
  4. Fail gold is calculated and processed
  5. Units in production are upgraded (think Horse Archers when you research guilds)
  6. Food is gained or lost
  7. Food is gained or lost for the food storage (think granary)
  8. Population is increased or decreased with enough food, food kept is updated in the event of growth
  9. Cities culture is increased
  10. Borders pop if necessary
  11. Every plot in the cities culture radius gains culture
  12. Ownership of plots is recalculated
  13. Decay of hammers is processed. Do not that this is a fix by me. In BtS this and the next step are swapped.
  14. Hammers are invested into the current build queue object
  15. Random religion spread is processed
  16. Great people points are distributed and a great person is eventually born. This also means great people pop in order of oldest to youngest city.
  17. Process meltdown event, by the way it's really just a 1/2000 chance each turn
  18. Update espionage visibility
  19. New resources on worked tiles are discovered event
  20. Improvement upgrades are processed (think cottages)
  21. Culture revolution timer is decremented
  22. Occupation timer is decremented
  23. Hurry anger timer is decremented
  24. Conscription anger timer is decremented
  25. Defy resolution timer is decremented
  26. Extra happiness from events timer is decremented
  27. Espionage health event timer is decremented
  28. Espionage happiness event timer is decremented
  29. We love the King event is processed
This leaves only one thing untouched the doTurnUnits method in CvPlayer I touched above. This methods goes through all your unit groups and in turn calls doTurn for each of your units. It also wakes up your units which is somewhat uninteresting in most cases. Keep in mind this only happens when the players turn becomes active. This is for everybody in a simultaneous game at the end of everybodys turn and in every other mode this happens when your turn is up.

  1. If enough XP is available grant promotions. Keep in mind for the Heroic Epic and others you actually have to use up the promotion aka level up the unit
  2. Collect gold from plundering
  3. Test the unit intercepts a spy
  4. Apply damage from features
  5. Heal if possible
  6. Fortify if possible
  7. Decrease immobile timer
  8. Reset values

Summary



Having written all this let me just assemble a complete list of things that happen during turn roll.

1. The Autosave is generated
2. The score is updated
3. Deals are verified and cancelled if necessary
4. Duration of peace deals are updated
5. The following steps are done for each alive team
5.1. doWarCounter() is called. This is an addition by us. It just countes the turns the team is at war
5.2. Barbarians gain their passive tech progress
5.3. The visibility of each plot on the map is updated for each team. Keep in mind this happens during each teams doTurn call so the visibility of all teams is updated multiple times.
5.4. The counter espionage turns left are updated for each team. Keep in mind this happens during each teams doTurn call so the visibility of all teams is updated multiple times.
5.5. The counter espionage modifiers are updated for each team. Keep in mind this happens during each teams doTurn call so the visibility of all teams is updated multiple times.
5.6. If Tech brokering is off all Techs are set to tradable. This will be touched later again
5.7. War weariness is updated
5.8. Test if circumnavigation happened
6. The following steps are done for each plot of the map
6.1. The ForceUnownedTimer is decreased by 1. This timer is set when you for example conquer a city and do not immediately own the surrounding tiles.
6.2. The duration you owned a tile is incremented by 1. This is used for score calculation.
6.3. The duration the improvement existed is incremented by 1. To my knowledge this is only used for AI.
6.4. Growth of features is process aka forest and jungle growth
6.5. Revolts due to culture are processed
6.6. verifyUnitValidPlot() is called. This in general checks units position and teleports them accordingly.
7. Barbarian cities are created
8. Barbarian units are created
9. Global Warming happens
10. doHolyCity() is called. This is only relevant for late era starts. This manages religion founding, holy city allocation and random religious spreads
11. doHeadquarters() is called. This is only relevant for late era starts. It randomly founds corporations headquarters.
12. Results of any diplo votes (AP or UN) are processed
13. Votes from AP or UN are triggered if timer has passed.
14. Increment game turn
15. Increment elapsed game turns
16. In a simultaneous PB the order of players is shuffled and in the new order each players turn is set to active. Jump to 19 for the next instructions
17. In a team game the next teams turn is set to active. This in turn sets the turn of the teams players to active in player order.
18. In all other modes the next players turn is set to active.
19. The following steps are processed for players at the end of the simultaneous turn or when you end your turn in every other mode.
19.1. Deals are checked if they are still valid
19.2. Revolution timer is decremented. This timer is for civic changes not for revolutions of cities.
19.3. The timer for religious conversions is decremented this
19.4. The conscription limit is reset.
19.5. For each human player the automatically worked tiles are reassigned
19.6. The slider is updated if you can't pay the maintenance
19.7. Your current gold is updated. If you cannot pay strike hits.
19.8. Beakers are applied to your current research project
19.9. You can espionage points towards other players, if you have met someone.
19.10. The following steps happen for every city from oldest to youngest city.
19.10.1. The city heals its HP from bombardment
19.10.2. Cities defenses are recalculated after healing
19.10.3. Reset bombarded, plundered, drafted and airlifted state
19.10.4. Fail gold is calculated and processed
19.10.5. Units in production are upgraded (think Horse Archers when you research guilds)
19.10.6. Food is gained or lost
19.10.7. Food is gained or lost for the food storage (think granary)
19.10.8. Population is increased or decreased with enough food, food kept is updated in the event of growth
19.10.9. Cities culture is increased
19.10.10. Borders pop if necessary
19.10.11. Every plot in the cities culture radius gains culture
19.10.12. Ownership of plots is recalculated
19.10.13. Decay of hammers is processed. Do not that this is a fix by me. In BtS this and the next step are swapped.
19.10.14. Hammers are invested into the current build queue object
19.10.15. Random religion spread is processed
19.10.16. Great people points are distributed and a great person is eventually born. This also means great people pop in order of oldest to youngest city.
19.10.17. Process meltdown event, by the way it's really just a 1/2000 chance each turn
19.10.18. Update espionage visibility
19.10.19. New resources on worked tiles are discovered event
19.10.20. Improvement upgrades are processed (think cottages)
19.10.21. Culture revolution timer is decremented
19.10.22. Occupation timer is decremented
19.10.23. Hurry anger timer is decremented
19.10.24. Conscription anger timer is decremented
19.10.25. Defy resolution timer is decremented
19.10.26. Extra happiness from events timer is decremented
19.10.27. Espionage health event timer is decremented
19.10.28. Espionage happiness event timer is decremented
19.10.29. We love the King event is processed
19.11. The golden age timer is decremented and the golden age ends potentially
19.12. The anarchy timer is decremented
19.13. Civics are verified and you may get kicked out of certain civics think The Pyramids being razed
19.14. All trade routes are recalculated
19.15. Assign unhappiness from war weariness
19.16. Events are triggered for the player
19.17. Update all the values for the graphs
19.18. In a simultaneous game you now also need to go through the extra step doTurnUnits down below
20. Test for victory conditions fulfilled
21. The game engines doTurn happens.
22. The score is updated
23. In an always war game it is made sure everybody is at war
24. In a simultaneous PB the order of players is shuffled
25. Automatic movements are processed (movements you assigned for the following turns). This also includes barbarians.
26. Timers are updated. This apparently manages combat from automatic movement. This is the one part that would need more investigation.
27. Update turn timer
28. For each human player the automatically worked tiles are reassigned
29. Verfiy if players are alive and set their state accordingly
30. Set game over state if appropiate

There is one extra step doTurnUnits. This one is a bit special. It happens for every player in a simultaneous game at the end of the turn and in all other modes when your turn is up to play.

  1. If enough XP is available grant promotions. Keep in mind for the Heroic Epic and others you actually have to use up the promotion aka level up the unit
  2. Collect gold from plundering
  3. Test the unit intercepts a spy
  4. Apply damage from features
  5. Heal if possible
  6. Fortify if possible
  7. Decrease immobile timer
  8. Reset values
Mods: RtR    CtH

Pitboss: PB39, PB40PB52, PB59 Useful Collections: Pickmethods, Mapmaking, Curious Civplayer

Buy me a coffee
Reply

When and how is research and tech progress calculated?

Nvm, found it.
Reply

(April 23rd, 2023, 07:49)Charriu Wrote: The ForceUnownedTimer is decreased by 1. This timer is set when you for example conquer a city and do not immediately own the surrounding tiles.

...wait....

Quote:6.1. The ForceUnownedTimer is decreased by 1. This timer is set when you for example conquer a city and do not immediately own the surrounding tiles.

...what??? Would you be willing to describe what happens in this step in a little more depth? Is this where the completely-incomprehensible-to-me-to-date behavior of cultural borders following city capture is determined somehow?
Reply



Forum Jump: