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

Have you ever wondered how a feature of civ 4 actually worked? Did you ever scour the internet for a nagging question about civ 4 in your mind? Well, this mini-series might be the right thing to do.

The initial idea for this started in PB43 when Mr. Cairo wondered about and investigated some effects around collateral damage and immunity to collateral. I told him and other players from PB43 that I will look up the actual implementation. Because this might be interesting to a broader viewership and because there might be more unanswered questions, I started this thread. Feel free to ask anything about how civ4 works and I will try to investigate and answer those questions. But a warning, don't expect a quick answer.
Mods: RtR    CtH

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

Buy me a coffee
Reply

The big questions in life:

What units are eligible for collateral damage and how are they selected?
Founding a religion in a specific city
How is the city determined, in which a Great General should be born?
How exactly is bombarding calculated?
Can you trust the odds for great people creation?
Are the free wins against barbs 100% wins or is the game lying to us?
Which units are used to start a golden age
How exactly is the 'We love the King day' triggered?
How exactly does feature growth (jungles and forests) work?
How does teleporation work or where are my units teleported to on closed borders, war declaration etc.?
When and how is teleportation triggered?
What determines which type of unit is spawned for the barbarians?
How do barbarians gain new technologies?
How does inflation work in civ 4?
How are trade connections between cities formed?
How is trade commerce calculated?
How does the game determine where a watermill can be build?
How does one increase war weariness for a team?
How does one decrease war weariness for a team?
How is the teams war weariness converted to the players war weariness?
How is the players war weariness converted into unhappiness for each city?
How does religion spread on its own?
How is trade mission gold calculated?
How is the player benefiting from liberation via conquest being determined?
How is the goody from goody huts determined?
Which tiles are animals allowed to enter?
When are events triggered and how is the triggered event selected?
How does Flanking work?
How does vision and the sentry promotion work?
In which order do happen things during a turn order?

TBC
Mods: RtR    CtH

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

Buy me a coffee
Reply

The first big question would be from PB43

What units are eligible for collateral damage and how are they selected?

The important part to this question is found in the CvUnit.cpp and in there in the method collateralCombat:


Code:
void CvUnit::collateralCombat(const CvPlot* pPlot, CvUnit* pSkipUnit)
{
   CLLNode<IDInfo>* pUnitNode;
   CvUnit* pLoopUnit;
   CvUnit* pBestUnit;
   CvWString szBuffer;
   int iTheirStrength;
   int iStrengthFactor;
   int iCollateralDamage;
   int iUnitDamage;
   int iDamageCount;
   int iPossibleTargets;
   int iCount;
   int iValue;
   int iBestValue;
   std::map<CvUnit*, int> mapUnitDamage;
   std::map<CvUnit*, int>::iterator it;

   int iCollateralStrength = (getDomainType() == DOMAIN_AIR ? airBaseCombatStr() : baseCombatStr()) * collateralDamage() / 100;
   // UNOFFICIAL_PATCH Start
   // * Barrage promotions made working again on Tanks and other units with no base collateral ability
   if (iCollateralStrength == 0 && getExtraCollateralDamage() == 0)
   // UNOFFICIAL_PATCH End
   {
       return;
   }

   iPossibleTargets = std::min((pPlot->getNumVisibleEnemyDefenders(this) - 1), collateralDamageMaxUnits());

   pUnitNode = pPlot->headUnitNode();

   while (pUnitNode != NULL)
   {
       pLoopUnit = ::getUnit(pUnitNode->m_data);
       pUnitNode = pPlot->nextUnitNode(pUnitNode);

       if (pLoopUnit != pSkipUnit)
       {
           if (isEnemy(pLoopUnit->getTeam(), pPlot))
           {
               if (!(pLoopUnit->isInvisible(getTeam(), false)))
               {
                   if (pLoopUnit->canDefend())
                   {
                       iValue = (1 + GC.getGameINLINE().getSorenRandNum(10000, "Collateral Damage"));

                       iValue *= pLoopUnit->currHitPoints();

                       mapUnitDamage[pLoopUnit] = iValue;
                   }
               }
           }
       }
   }

   CvCity* pCity = NULL;
   if (getDomainType() == DOMAIN_AIR)
   {
       pCity = pPlot->getPlotCity();
   }

   iDamageCount = 0;
   iCount = 0;

   while (iCount < iPossibleTargets)
   {
       iBestValue = 0;
       pBestUnit = NULL;

       for (it = mapUnitDamage.begin(); it != mapUnitDamage.end(); it++)
       {
           if (it->second > iBestValue)
           {
               iBestValue = it->second;
               pBestUnit = it->first;
           }
       }

       if (pBestUnit != NULL)
       {
           mapUnitDamage.erase(pBestUnit);

           if (NO_UNITCOMBAT == getUnitCombatType() || !pBestUnit->getUnitInfo().getUnitCombatCollateralImmune(getUnitCombatType()))
           {
               iTheirStrength = pBestUnit->baseCombatStr();

               iStrengthFactor = ((iCollateralStrength + iTheirStrength + 1) / 2);

               iCollateralDamage = (GC.getDefineINT("COLLATERAL_COMBAT_DAMAGE") * (iCollateralStrength + iStrengthFactor)) / (iTheirStrength + iStrengthFactor);

               iCollateralDamage *= 100 + getExtraCollateralDamage();

               iCollateralDamage *= std::max(0, 100 - pBestUnit->getCollateralDamageProtection());
               iCollateralDamage /= 100;

               if (pCity != NULL)
               {
                   iCollateralDamage *= 100 + pCity->getAirModifier();
                   iCollateralDamage /= 100;
               }

               iCollateralDamage /= 100;

               iCollateralDamage = std::max(0, iCollateralDamage);

               int iMaxDamage = std::min(collateralDamageLimit(), (collateralDamageLimit() * (iCollateralStrength + iStrengthFactor)) / (iTheirStrength + iStrengthFactor));
               iUnitDamage = std::max(pBestUnit->getDamage(), std::min(pBestUnit->getDamage() + iCollateralDamage, iMaxDamage));

               if (pBestUnit->getDamage() != iUnitDamage)
               {
                   pBestUnit->setDamage(iUnitDamage, getOwnerINLINE());
                   iDamageCount++;
               }
           }

           iCount++;
       }
       else
       {
           break;
       }
   }

   if (iDamageCount > 0)
   {
       szBuffer = gDLL->getText("TXT_KEY_MISC_YOU_SUFFER_COL_DMG", iDamageCount);
       gDLL->getInterfaceIFace()->addMessage(pSkipUnit->getOwnerINLINE(), (pSkipUnit->getDomainType() != DOMAIN_AIR), GC.getEVENT_MESSAGE_TIME(), szBuffer, "AS2D_COLLATERAL", MESSAGE_TYPE_INFO, getButton(), (ColorTypes)GC.getInfoTypeForString("COLOR_RED"), pSkipUnit->getX_INLINE(), pSkipUnit->getY_INLINE(), true, true);

       szBuffer = gDLL->getText("TXT_KEY_MISC_YOU_INFLICT_COL_DMG", getNameKey(), iDamageCount);
       gDLL->getInterfaceIFace()->addMessage(getOwnerINLINE(), true, GC.getEVENT_MESSAGE_TIME(), szBuffer, "AS2D_COLLATERAL", MESSAGE_TYPE_INFO, getButton(), (ColorTypes)GC.getInfoTypeForString("COLOR_GREEN"), pSkipUnit->getX_INLINE(), pSkipUnit->getY_INLINE());
   }
}

There are two parts in the code that are important to the question. First this:

Code:
   iPossibleTargets = std::min((pPlot->getNumVisibleEnemyDefenders(this) - 1), collateralDamageMaxUnits());

   pUnitNode = pPlot->headUnitNode();

   while (pUnitNode != NULL)
   {
       pLoopUnit = ::getUnit(pUnitNode->m_data);
       pUnitNode = pPlot->nextUnitNode(pUnitNode);

       if (pLoopUnit != pSkipUnit)
       {
           if (isEnemy(pLoopUnit->getTeam(), pPlot))
           {
               if (!(pLoopUnit->isInvisible(getTeam(), false)))
               {
                   if (pLoopUnit->canDefend())
                   {
                       iValue = (1 + GC.getGameINLINE().getSorenRandNum(10000, "Collateral Damage"));

                       iValue *= pLoopUnit->currHitPoints();

                       mapUnitDamage[pLoopUnit] = iValue;
                   }
               }
           }
       }
   }

Let's dissect this. The first line with "iPossibleTargets" is rather easy. This is the maximum number of units that can be damaged by collateral and it's what you already know. It's the limit set by the attacking unit or the amount of visible enemy units minus the original defender, which ever number is smaller.
Next we go through all the defending units. There are some checks that remove units from the pool:
  • if (pLoopUnit != pSkipUnit): pSkipUnit is the actually defending unit. It is not eligible for collateral
  • if (isEnemy(pLoopUnit->getTeam(), pPlot)): The unit must be an enemy unit, meaning we are at war with the owner of the unit
  • if (!(pLoopUnit->isInvisible(getTeam(), false))): Only visible units are allowed.
  • if (pLoopUnit->canDefend()): Among others, this canDefend checks if the defending units combat value is > 0. That way units like settler and worker are not eligible for collateral
After all these checks every unit is assigned a random value between 1 and 10001. This value is then multiplied by their current hitpoints. That way damaged units are still eligible, but it's less likely they are selected.

So now we know which units are eligible and we now need to pick the units and do damage to them. This happens here:

Code:
while (iCount < iPossibleTargets)
   {
       iBestValue = 0;
       pBestUnit = NULL;

       for (it = mapUnitDamage.begin(); it != mapUnitDamage.end(); it++)
       {
           if (it->second > iBestValue)
           {
               iBestValue = it->second;
               pBestUnit = it->first;
           }
       }

       if (pBestUnit != NULL)
       {
           mapUnitDamage.erase(pBestUnit);

           if (NO_UNITCOMBAT == getUnitCombatType() || !pBestUnit->getUnitInfo().getUnitCombatCollateralImmune(getUnitCombatType()))
           {
               do the actual damage
           }

           iCount++;
       }
       else
       {
           break;
       }
   }

Here we just select the unit with the highest random value and try to do damage to it. We do this to as many units as determined by iPossibleTargets. But there are some important things before we do the actual damage:
  • We guarantee that every unit is only considered once. This is the mapUnitDamage.erase(pBestUnit); part.
  • We do this check before if (NO_UNITCOMBAT == getUnitCombatType() || !pBestUnit->getUnitInfo().getUnitCombatCollateralImmune(getUnitCombatType()))
Now that last part is important. The "||" is an "or" in C++. That first check before the or is a simple safety check for the unit type. As far as I know, no unit has the UnitType NO_UNITCOMBAT, but if it would have this, would not receive damage. It's the last part that is important. Here we check if the selected unit is immune. If true it does not receive damage. Most importantly the counter iCount is counted up outside of this check. This means that units that are NO_UNITCOMBAT or immune, are considered targets without receiving damage.

Lastly there is the code for the actual damage:


Code:
iTheirStrength = pBestUnit->baseCombatStr();

iStrengthFactor = ((iCollateralStrength + iTheirStrength + 1) / 2);

iCollateralDamage = (GC.getDefineINT("COLLATERAL_COMBAT_DAMAGE") * (iCollateralStrength + iStrengthFactor)) / (iTheirStrength + iStrengthFactor);

iCollateralDamage *= 100 + getExtraCollateralDamage();

iCollateralDamage *= std::max(0, 100 - pBestUnit->getCollateralDamageProtection());
iCollateralDamage /= 100;

if (pCity != NULL)
{
    iCollateralDamage *= 100 + pCity->getAirModifier();
    iCollateralDamage /= 100;
}

iCollateralDamage /= 100;

iCollateralDamage = std::max(0, iCollateralDamage);

int iMaxDamage = std::min(collateralDamageLimit(), (collateralDamageLimit() * (iCollateralStrength + iStrengthFactor)) / (iTheirStrength + iStrengthFactor));
iUnitDamage = std::max(pBestUnit->getDamage(), std::min(pBestUnit->getDamage() + iCollateralDamage, iMaxDamage));

if (pBestUnit->getDamage() != iUnitDamage)
{
    pBestUnit->setDamage(iUnitDamage, getOwnerINLINE());
    iDamageCount++;
}

The only important part here is that units that are already damaged and under the damage limit, are counted as targets, but receive no damage.


Summary

So to summarize, units that fullfill these criteria are eligible for collateral damage:
  • Not the actually defending unit
  • Defending units of players, whom we are at war with
  • Visible units
  • Combat strength > 0
And most importantly units that fullfill these criteria are considered targets, but don't receive damage, therefore reducing the effectiveness of collateral damage, just like Mr. Cairo already discovered
  • Units with Type NO_COMBATUNIT (there is none in the game)
  • Collateral immune units
  • Damaged units that are under the damage limit. (but it's less likely that they are selected because of their reduced HP)
Mods: RtR    CtH

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

Buy me a coffee
Reply

Nice thread and great writeup on that.

I remember doing something similar years ago on the details of granary mechanics. If you grow while building a settler or worker (only possible if the food box shrinks, say by whipping), the 2 food the new citizen consumes are subtracted out of the hammers applied to the settler/worker that same turn. The city screen doesn't account for that ahead of time so you end up with 2 hammers less than it showed. This drove someone crazy in some early reporting thread until I traced through the code line by line to figure it out.
Reply

(April 25th, 2020, 00:00)Charriu Wrote: After all these checks every unit is assigned a random value between 1 and 10001. This value is then multiplied by their current hitpoints. That way damaged units are still eligible, but it's less likely they are selected.
So, does it mean that units that are immune to collateral damage will become more likely to be selected with each subsequent collateral attack? Because all other units would get damaged but immune units would remain at full health? I actually didn't know that, so thanks!
Reply

You are absolutely right about that.
Mods: RtR    CtH

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

Buy me a coffee
Reply

Founding a religion in a specific city

In my reasoned game (PB52, don't worry spoiler-free) I planned to found a religion and wondered about these two questions:

1. How exactly does the game choose which city?
2. Does the game deceide on the holy city before or after that city grew?

I start with the second question as it's quicker to answer. The game first decides on the holy city and after that decision the city grows. That's because the holy city decision is done during the tech progress steps of the game and those are done before iterating through all cities.

With that out of the way back to the first question. There's already good knowledge about it and it involves random numbers, the cities population and if the palace is present. First here is the code:


Code:
    for (pLoopCity = firstCity(&iLoop); pLoopCity != NULL; pLoopCity = nextCity(&iLoop))
    {
        if (!bStarting || !(pLoopCity->isHolyCity()))
        {
            iValue = 10;
            iValue += pLoopCity->getPopulation();
            iValue += GC.getGameINLINE().getSorenRandNum(GC.getDefineINT("FOUND_RELIGION_CITY_RAND"), "Found Religion");

            iValue /= (pLoopCity->getReligionCount() + 1);

            if (pLoopCity->isCapital())
            {
                iValue /= 8;
            }

            iValue = std::max(1, iValue);

            if (iValue > iBestValue)
            {
                iBestValue = iValue;
                pBestCity = pLoopCity;
            }
        }
    }

So what does this mean in plain old english. We iterate through all cities in order of founding. Then we do the following

1. The city starts with a base value of 10
2. We add the cities population to that value
3. We add a random number between 0 and 9
4. We divide that number through the amount of religions present in the city + 1 (that last one so that we don't divide by zero)
5. If this city is the capital we divide the number by 8
6. We guarantee that this number is at least 1
7. We compare this value with the last best value if it's higher, then this cities value is the new best value and the city is more likely to be choosen.

Summary

So what do we learn from this:

1. The algorithm favors earlier founded cities, simply because they can set a higher best value earlier
2. Population difference isn't really that big of a game changer, especially if the difference is very small.
Mods: RtR    CtH

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

Buy me a coffee
Reply

So it is basically impossible to get it in the capital even if you get the lucky 10 roll, and has say 4 pop.
"I know that Kilpatrick is a hell of a damned fool, but I want just that sort of man to command my cavalry on this expedition."
- William Tecumseh Sherman

Reply

Even with 20 pop it's impossible. The only way is to have only one city.
Mods: RtR    CtH

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

Buy me a coffee
Reply

Wow, I always misunderstood that.  I thought the numbers were weights, so it could still be the capital, just 1/8 as often.  It's highest-city-wins rather than weights, and so dividing the capital by 8 means it will never beat any city's base value of 10 (it would need ~80 population.)  I wonder if Firaxis (Soren) themselves understood what they were doing here, that the capital is shut out rather than 1/8 likely; that 8 value really has no effective meaning.

Your last point really means "earlier city wins a tie" after adding the random(10) to each.  That alone is significant, 55%-45% in favor of the earlier city among two with the same population.  And population does have a significant effect, even if sometimes overstated.  Just one more population for the earlier city shifts it to 64%-36%, almost two-to-one.  Two more pop becomes 72%-28%, almost three-to-one.  And the earlier city will be likely to have grown to more population.
Reply



Forum Jump: