[DLL/C++] Adding new tables to the database

whoward69

DLL Minion
Joined
May 30, 2011
Messages
8,699
Location
Near Portsmouth, UK
The aim of this tutorial is to show you how to extend the existing database by adding new tables and the required C++ code such that they actually do something useful.

This tutorial assumes you are familiar with the techniques presented in the "Replacing hard-coded constants with database values" tutorial, specifically adding your own custom header file and the pre-processor techniques. If you haven't already done so, please read that tutorial first.

This tutorial also assumes that you have
  1. Knowledge of the Civ 5 database tables and how to update them either via XML or SQL
  2. A basic knowledge of C programming and an understanding of object-orientated programming as implemented by C++ (if you don't know the significance of :: and the difference between -> and . then this tutorial is probably not for you)
  3. Successfully built, deployed and loaded the unaltered source as a custom DLL mod
  4. Created your own custom header file and added it to the CvGameCoreDLLPCH.h file

This tutorial builds on the "Adding new columns to the database" tutorial, if you haven't already done so, please read that tutorial first.

A fairly common question on the forums is "How do I add X yield to mountains for this trait/building/belief/whatever". The simple answer is "You can't, as mountains are a plot type, not a terrain or feature type". The more complex answer is "By doing 'magic' with free buildings and/or fake natural wonders (features that can be applied to mountains) and some Lua coding". The problem with "magic" is that it can only really be used by one mod. It works well for a total conversion, but is not really suitable for multiple modular mods (probably by multiple authors), where each mod would need to know (and take account of) all the nuances of other mods adding yields to mountains. "Magic" Lua solutions are also not what this DLL/C++ tutorial is about!

So how would we extend the DLL such that we could then add yields to mountains by beliefs simply by adding appropriate entries into the database? We need the equivalent of the Belief_TerrainYieldChanges table for plot types, so something like
Code:
    <Belief_PlotYieldChanges>
        <Row>
            <BeliefType>BELIEF_HOME_OF_THE_GODS</BeliefType>
            <PlotType>PLOT_MOUNTAIN</PlotType>
            <YieldType>YIELD_FAITH</YieldType>
            <Yield>2</Yield>
        </Row>
    </Belief_PlotYieldChanges>

Our first problem is that there is no Plots table for the PlotType column to reference. This can be easily fixed by adding the following to our mod
Code:
<?xml version="1.0" encoding="utf-8"?>
<GameData>
    <Table name="Plots">
        <Column name="ID" type="integer" primarykey="true" autoincrement="true"/>
        <Column name="Type" type="text" notnull="true" unique="true"/>
    </Table>

    <Plots>
        <Row>
            <ID>0</ID>
            <Type>PLOT_MOUNTAIN</Type>
        </Row>
        <Row>
            <Type>PLOT_HILLS</Type>
        </Row>
        <Row>
            <Type>PLOT_LAND</Type>
        </Row>
        <Row>
            <Type>PLOT_OCEAN</Type>
        </Row>
    </Plots>
</GameData>
(in reality we'd add more columns to the Plots table, for example Description, Civilopedia, IconAtlas and PortraitIndex)

IMPORTANT NOTE: The order of the Types in the Plots table MUST be the same as the order in the PlotTypes enum.

This XML (or its SQL equivalent) needs to be in a separate file. That way, if two mods attempt to add the Plots table, when the second one fails it will only affect adding the table (which won't matter as it's only failing because it's already present in the database) and not the other database modifications applied by the second mod.

We also need to define the Belief_PlotYieldChanges table, again this should be in a separate file for the same reasons. (But NOT the same file as the Plots table, as the other mod may be adding plot yields for buildings and so not including the Belief_PlotYieldChanges table.)
Code:
<?xml version="1.0" encoding="utf-8"?>
<GameData>
    <Table name="Belief_PlotYieldChanges">
        <Column name="BeliefType" type="text" reference="Beliefs(Type)"/>
        <Column name="PlotType" type="text" reference="Plots(Type)"/>
        <Column name="YieldType" type="integer" reference="Yields(Type)"/>
        <Column name="Yield" type="integer" default="0"/>
    </Table>
</GameData>

We can now go ahead and define our new "Home of the Gods" pantheon belief
Code:
<?xml version="1.0" encoding="utf-8"?>
<GameData>
    <Beliefs>
        <Row>
            <Type>BELIEF_HOME_OF_THE_GODS</Type>
            <Description>TXT_KEY_BELIEF_HOME_OF_THE_GODS</Description>
            <ShortDescription>TXT_KEY_BELIEF_HOME_OF_THE_GODS_SHORT</ShortDescription>
            <Pantheon>true</Pantheon>
        </Row>
    </Beliefs>

    <Belief_PlotYieldChanges>
        <Row>
            <BeliefType>BELIEF_HOME_OF_THE_GODS</BeliefType>
            <PlotType>PLOT_MOUNTAIN</PlotType>
            <YieldType>YIELD_FAITH</YieldType>
            <Yield>2</Yield>
        </Row>
    </Belief_PlotYieldChanges>

    <Language_en_US>
        <Row Tag="TXT_KEY_BELIEF_HOME_OF_THE_GODS_SHORT">
            <Text>Home of the Gods</Text>
        </Row>
        <Row Tag="TXT_KEY_BELIEF_HOME_OF_THE_GODS">
            <Text>+2 [ICON_PEACE] Faith from Mountains</Text>
        </Row>
    </Language_en_US>
</GameData>

Now all we have to do is get the DLL to understand the purpose of the Belief_PlotYieldChanges table. But before we can do this, like adding a column, we need to get the database table entries loaded into memory and made accessible to the C++ code.

Searching the code base, "Belief_TerrainYieldChanges" appears in only the CvBeliefClasses.cpp file.
Code:
    //TerrainYieldChanges
    {
        kUtility.Initialize2DArray(m_ppaiTerrainYieldChange, "Terrains", "Yields");

        std::string strKey("Belief_TerrainYieldChanges");
        Database::Results* pResults = kUtility.GetResults(strKey);
        if(pResults == NULL)
        {
            pResults = kUtility.PrepareResults(strKey, "select Terrains.ID as TerrainID, Yields.ID as YieldID, Yield from Belief_TerrainYieldChanges inner join Terrains on Terrains.Type = TerrainType inner join Yields on Yields.Type = YieldType where BeliefType = ?");
        }

        pResults->Bind(1, szBeliefType);

        while(pResults->Step())
        {
            const int TerrainID = pResults->GetInt(0);
            const int YieldID = pResults->GetInt(1);
            const int yield = pResults->GetInt(2);

            m_ppaiTerrainYieldChange[TerrainID][YieldID] = yield;
        }
    }
This code executes a database query and caches the results into m_ppaiTerrainYieldChange - an array specific to a belief, indexed by both terrain type and yield type, to give the yield change. We will need to add similar code to load the Belief_PlotYieldChanges, and we will also need all the associated code to support the creation/destruction of the array. Searching for m_ppaiTerrainYieldChange results in three pairs of files CvBeliefClasses.h/cpp, CvBuildingClasses.h/cpp and CvCity.h/cpp - the first pair are the ones we're interested in for beliefs, the other two relate to adding yields to buildings.

In CvBeliefClasses.h
Code:
    int GetTerrainYieldChange(int i, int j) const;
#if defined(MOD_RELIGION_PLOT_YIELDS)
    int GetPlotYieldChange(int i, int j) const;
#endif
and
Code:
    int** m_ppaiTerrainYieldChange;
#if defined(MOD_RELIGION_PLOT_YIELDS)
    int** m_ppaiPlotYieldChange;
#endif

In CvBeliefClasses.cpp
Code:
    m_ppaiTerrainYieldChange(NULL),
#if defined(MOD_RELIGION_PLOT_YIELDS)
    m_ppaiPlotYieldChange(NULL),
#endif
and
Code:
    CvDatabaseUtility::SafeDelete2DArray(m_ppaiTerrainYieldChange);
#if defined(MOD_RELIGION_PLOT_YIELDS)
    CvDatabaseUtility::SafeDelete2DArray(m_ppaiPlotYieldChange);
#endif
and
Code:
/// Change to yield by terrain
int CvBeliefEntry::GetTerrainYieldChange(int i, int j) const
{
    CvAssertMsg(i < GC.getNumTerrainInfos(), "Index out of bounds");
    CvAssertMsg(i > -1, "Index out of bounds");
    CvAssertMsg(j < NUM_YIELD_TYPES, "Index out of bounds");
    CvAssertMsg(j > -1, "Index out of bounds");
    return m_ppaiTerrainYieldChange ? m_ppaiTerrainYieldChange[i][j] : -1;
}

#if defined(MOD_RELIGION_PLOT_YIELDS)
/// Change to yield by plot
int CvBeliefEntry::GetPlotYieldChange(int i, int j) const
{
    // Only uncomment the next line if you are also adding all the supporting code for the Plots table - which is beyond the scope of this tutorial
    // CvAssertMsg(i < GC.getNumPlotInfos(), "Index out of bounds");
    CvAssertMsg(i > -1, "Index out of bounds");
    CvAssertMsg(j < NUM_YIELD_TYPES, "Index out of bounds");
    CvAssertMsg(j > -1, "Index out of bounds");
    return m_ppaiPlotYieldChange ? m_ppaiPlotYieldChange[i][j] : -1;
}
#endif
and
Code:
#if defined(MOD_RELIGION_PLOT_YIELDS)
    //PlotYieldChanges
    {
        kUtility.Initialize2DArray(m_ppaiPlotYieldChange, "Plots", "Yields");

        std::string strKey("Belief_PlotYieldChanges");
        Database::Results* pResults = kUtility.GetResults(strKey);
        if(pResults == NULL)
        {
            pResults = kUtility.PrepareResults(strKey, "select Plots.ID as PlotID, Yields.ID as YieldID, Yield from Belief_PlotYieldChanges inner join Plots on Plots.Type = PlotType inner join Yields on Yields.Type = YieldType where BeliefType = ?");
        }

        pResults->Bind(1, szBeliefType);

        while(pResults->Step())
        {
            const int PlotID = pResults->GetInt(0);
            const int YieldID = pResults->GetInt(1);
            const int yield = pResults->GetInt(2);

            m_ppaiPlotYieldChange[PlotID][YieldID] = yield;
        }
    }
#endif

We now have our Belief_PlotYieldChanges entries cached in memory for each belief and accessible via the CvBeliefEntry::GetPlotYieldChange() accessor, so how do we use them. As our plot yield changes are an extension of the existing terrain yields, a good starting point is to locate all uses of the CvBeliefEntry::GetTerrainYieldChange() accessor. Searching for this we find that it's used in the CvReligionBeliefs::GetTerrainYieldChange() and CvReligionAI::ScoreBeliefAtPlot() methods. The CvReligionBeliefs class is in the CvBeliefClasses.h/cpp files so we need another addition to each of those

In CvBeliefClasses.h
Code:
    int GetTerrainYieldChange(TerrainTypes eTerrain, YieldTypes eYieldType) const;
#if defined(MOD_RELIGION_PLOT_YIELDS)
    int GetPlotYieldChange(PlotTypes ePlot, YieldTypes eYieldType) const;
#endif

In CvBeliefClasses.cpp
Code:
/// Get yield change from beliefs for a specific terrain
int CvReligionBeliefs::GetTerrainYieldChange(TerrainTypes eTerrain, YieldTypes eYieldType) const
{
    // omitted for brevity
}

#if defined(MOD_RELIGION_PLOT_YIELDS)
/// Get yield change from beliefs for a specific plot
int CvReligionBeliefs::GetPlotYieldChange(PlotTypes ePlot, YieldTypes eYieldType) const
{
    CvBeliefXMLEntries* pBeliefs = GC.GetGameBeliefs();
    int rtnValue = 0;

    for(int i = 0; i < pBeliefs->GetNumBeliefs(); i++)
    {
        if(HasBelief((BeliefTypes)i))
        {
            rtnValue += pBeliefs->GetEntry(i)->GetPlotYieldChange(ePlot, eYieldType);
        }
    }

    return rtnValue;
}
#endif

The CvReligionBeliefs::GetPlotYieldChange() accessor just sums the individual belief yields for all beliefs that are part of the religion.

And for CvReligionAI::ScoreBeliefAtPlot() we need to add to CvReligionClasses.cpp
Code:
    // Terrain
    TerrainTypes eTerrain = pPlot->getTerrainType();
    if(eTerrain != NO_TERRAIN)
    {
        iRtnValue += pEntry->GetTerrainYieldChange(eTerrain, iI);
    }

#if defined(MOD_RELIGION_PLOT_YIELDS)
    // Plot
    PlotTypes ePlot = pPlot->getPlotType();
    if(ePlot != NO_PLOT)
    {
        iRtnValue += pEntry->GetPlotYieldChange(ePlot, iI);
    }
#endif

So the AI will now take our plot yields into account when scoring a plot for its "belief value" (whatever that may be and whatever it is used for), but we're still not adding the plot yields to anything. Back to the detective work (aka searching), we need to locate where the CvReligionBeliefs::GetTerrainYieldChange() method is used. The answer to that question is in the CvPlot::calculateNatureYield() method (which also includes a call to CvBeliefEntry::GetTerrainYieldChange() which we conveniently ignored above).

The calculateNatureYield() method calculates the base yield for a plot from natural things, for example, terrain, features, resources, natural wonders (special features), etc and for some bizarre reason also beliefs (but not cities/buildings). So if we add our yields from plot type into here we should be good to go!

In the CvPlot::calculateNatureYield() method in the CvPlot.cpp file
Code:
    int iReligionChange = pReligion->m_Beliefs.GetTerrainYieldChange(getTerrainType(), eYield);
    if (eSecondaryPantheon != NO_BELIEF)
    {
        iReligionChange += GC.GetGameBeliefs()->GetEntry(eSecondaryPantheon)->GetTerrainYieldChange(getTerrainType(), eYield);
    }
            
#if defined(MOD_RELIGION_PLOT_YIELDS)
    iReligionChange += pReligion->m_Beliefs.GetPlotYieldChange(getPlotType(), eYield);
    if (eSecondaryPantheon != NO_BELIEF)
    {
        iReligionChange += GC.GetGameBeliefs()->GetEntry(eSecondaryPantheon)->GetPlotYieldChange(getPlotType(), eYield);
    }
#endif

    iYield += iReligionChange;

Build the DLL, start a game (as the Celts in a forest near a mountain), found a Pantheon, select Home of the Gods and ... what the!!! No faith yield from mountains???

Double checking all the logic, it should have worked. So there must be some "special case" code that's interfering, and this is indeed the case. The game designer(s) obviously wanted "dead" plots - mountains and ice - as modders we want to undo that design decision. But the designers didn't allow for modders actually wanting to change things, so have hard-code this "dead plot" logic. At the top of the calculateNatureYield() method we find
Code:
    if(isImpassable() || isMountain())
    {
        // No Feature, or the Feature isn't a Natural Wonder (which are impassable but allowed to be worked)
        if(getFeatureType() == NO_FEATURE || !GC.getFeatureInfo(getFeatureType())->IsNaturalWonder())
        {
            return 0;
        }
    }
so if the plot is impassable (ice) or a mountain but not a natural wonder (special case of a feature on a mountain), the plot always has a yield of 0. I so loathe special-case coding. We need to disable this code (we'll use a #if !defined() to do it)
Code:
#if !defined(MOD_RELIGION_PLOT_YIELDS)
    if(isImpassable() || isMountain())
        // Omitted for brevity
    }
#endif

Build the DLL, reload the Celts game and ... what!!! Our mountains now have 2 faith but also the yield from the underlying terrain (usually plains for +1 food, +1 hammer). And if you check an ice tile it will also have a yield. So why don't natural wonders also pick up the yields from the underlying terrain? Well they have a YieldNotAdditive=true entry, and the code that processes that is also in the calculateNatureYield() method (lower down from our changes). Now we could (and probably should) add a similar column to our Plots table and process it, but there are many other places where mountains and impassable plots are treated as special cases that it's just not worth the effort, so we'll re-instate the special case processing, except we'll do it where the terrain yield is calculated (rather than as the first thing in the method)
Code:
    CvAssertMsg(getTerrainType() != NO_TERRAIN, "TerrainType is not assigned a valid value");

#if defined(MOD_RELIGION_PLOT_YIELDS)
    // Impassable terrain and mountains have no base yield
    if(isImpassable() || isMountain()) {
        iYield = 0;
    } else
#endif
        iYield = GC.getTerrainInfo(getTerrainType())->getYield(eYield);
At least the special-case code is now in a (more) sensible place.

Build the DLL, reload the Celts game and ... yeah!!! Our mountains only have 2 faith. What's more, if we write another mod to add an enhancer belief called "Dwarven Mines" to add +1 production to mountains it will correctly stack as we're not using "magic" to coerce the game into doing something by using standard features in a non-standard way.

[END]
 
Please use this thread in the SDK / Lua sub-forum for asking questions about this tutorial, I will then update/expand this thread as necessary.

This is NOT a tutorial on basic C/C++ programming, so please do not ask questions about the syntax, semantics or usage of the C++ language - there are many excellent resources for learning C++ available (most of which can be found in your local bookshop)

Also, please do NOT ask questions about specific issues for your own mods and/or ask for someone else to write your code for you on these threads - start a new thread either in the main C&C forum or the SDK / Lua sub-forum to ask your specific question/request, as that way you'll get specific answers ;) And finally, please note that I do NOT take requests for writing code for others' mods!
 
Top Bottom