[DLL/C++] Replacing hard-coded constants with database values

whoward69

DLL Minion
Joined
May 30, 2011
Messages
8,691
Location
Near Portsmouth, UK
The aim of this tutorial is to show you how to move hard-coded values from the C++ source code into the database and also to introduce a few tricks and tips to make this process easier, but more importantly, to make future maintenance of your code less painful.

This tutorial assumes that you have
  1. 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)
  2. Downloaded the Civ 5 SDK from Steam (and located the source code within it)
  3. Installed MicroSoft Visual Studio for C++
  4. Followed the "How to compile the DLL" tutorial and (finally) got the original source to compile and link (aka "build")
  5. Created a mod to load your custom DLL and tested it in game
that is, that you have successfully built, deployed and loaded the unaltered source as a custom DLL mod.

So you've got the source at your fingertips and are all ready to start making your changes. But first STOP. Now go and read the comment to modders in CvDllVersion.h regarding the DLL GUID value.

The problem with changing the source code is that you never know when Firaxis is going to release an update which will force you to merge your changes into their new code. About the only thing you can guarantee is that this will happen, so you need to plan for it from the outset.

Let's take a simple example, changing the value the AI uses to decide if it has enough ships in a particular body of water. How you locate this code is a tutorials worth of explanation in its own right, but it is in CvAICityStrategy.cpp and is

Code:
    CvArea* pBiggestNearbyBodyOfWater = m_pCity->waterArea();
    if (pBiggestNearbyBodyOfWater)
    {
      int iWaterTiles = pBiggestNearbyBodyOfWater->getNumTiles();
      int iNumUnitsofMine = pBiggestNearbyBodyOfWater->getUnitsPerPlayer(m_pCity->getOwner());
      if (iNumUnitsofMine * 5 > iWaterTiles)
      {
        iTempWeight = 0;
      }
    }
    else // this should never happen, but...
    {
      iTempWeight = 0;
    }

specifically the line with the hard-coded value of 5

Code:
    if (iNumUnitsofMine * 5 > iWaterTiles)

What this says is that the AI considers 1 ship per 5 water tiles to be enough. I consider this to be overkill; 1 per 10 or even 1 per 15 would be more reasonable.

So we could jump right in and edit the line to be 1 per 12 - a compromise

Code:
    if (iNumUnitsofMine * 12 > iWaterTiles)

Ignoring the not insignificant matter of replacing one hard-coded value with another (which we'll return to later) we've just made some basic mistakes.

In order to make future maintenance easier we MUST note what we have changed, typically this is done with a comment like so

Code:
    // MOD - WH - changed number of water tiles per ship
    if (iNumUnitsofMine * 12 > iWaterTiles)

using a unique prefix (MOD - WH - ) makes it easier to search for our changes later.

A single line comment works for a single line change, but if we were changing a block of code it's as important to know where the changes end as they begin, and it is therefore a good habit to get into to "bracket" all changes, be they one line or many

Code:
    // MOD - WH - changed number of water tiles per ship
    if (iNumUnitsofMine * 12 > iWaterTiles)
    // MOD - WH - END

We now know what the changes are, but we have lost what the original code was. Why is this important? Well, if Firaxis decide that the number of tiles per ship is calculated very differently in the next release we need to know what their original code was that we based our changes on so we can re-evaluate our changes to see if we want to keep them or go with the new approach. Typically programmers will place the original value in the comment, or even "comment out" the entire original code (block), eg

Code:
    // MOD - WH - changed number of water tiles per ship from 5 to 12
    if (iNumUnitsofMine * 12 > iWaterTiles)
    // MOD - WH - END

or

Code:
    // MOD - WH - changed number of water tiles per ship
    // if (iNumUnitsofMine * 5 > iWaterTiles)
    if (iNumUnitsofMine * 12 > iWaterTiles)
    // MOD - WH - END

This is beginning to get messy and can also lead to errors. Let's say Firaxis change the value from 5 to 7 in the next release and we use a tool to do an automated merge of our changes into the new code base. Typically the contents of the comments will be ignore during the automated merge which will lead to our commented copy of Firaxis' old code being transferred verbatim into the new source. We now have a lie in the comments as they should read 7 not 5.

There is a better way. The C programming language came with a pre-processor and it is also available in MicroSoft C++. The bits we are interested in are #define, #if defined(), #else and #endif. #define allows us to give an arbitrary sequence of characters (ie a string) a value, typically the string is all upper case, eg

Code:
#define AI_TILES_PER_SHIP 12

in order to easily separate our defines from the standard ones, we should use a unique prefix

Code:
#define WH_AI_TILES_PER_SHIP 12

we can now use our defined value as

Code:
    // MOD - WH - changed number of water tiles per ship
    // if (iNumUnitsofMine * 5 > iWaterTiles)
    if (iNumUnitsofMine * WH_AI_TILES_PER_SHIP > iWaterTiles)
    // MOD - WH - END

But we can also use our defined value to tidy up the mess of the comments by using the #if-#else-#endif construct

Code:
#if defined(WH_AI_TILES_PER_SHIP)
    if (iNumUnitsofMine * WH_AI_TILES_PER_SHIP > iWaterTiles)
#else
    if (iNumUnitsofMine * 5 > iWaterTiles)
#endif

This modified code block meets all of our requirements for maintenance
1) we know where the changes start - at the #if
2) we know where the changes end - at the #endif
3) we know what the original code (if any) was - between the #else and the #endif

It also has another major benefit. If we want to revert to the original code, all we have to do is comment out the #define (such that it is no longer defined) and all of our changes go away and the original code is restored. Not particularly useful in this simple example, but if you have many changes across many files to implement some new functionality, it is invaluable. Especially when your newly written functionality is buggy and stopping you from finishing your game. If you've identified your changes with comments you're only option is to search every file for code changes related to that new functionality and then either delete it or comment it all out while re-instating the original code - which itself is error prone and likely to introduce further bugs. However, if you've used the #if-#else-#endif technique you just need to comment out one line (the controlling #define) to remove all the offending code and at the same time re-instate the original code. The same is true if you're half way through adding new functionality and need to go back to fix a bug in something previously added - commenting out one #define will temporarily remove all your partially complete changes.

We now have our simple change, neatly packaged, but still hard-coded. What we really want to do is move the value 5 into the database, as that way we can easily change it with an XML (or SQL) mod. The obvious database table to put it in is Defines, such that we could write in a mod

Code:
<GameData>
  <Defines>
    <Row Name="AI_TILES_PER_SHIP">
      <Value>12</Value>
    </Row>
  </Defines>
</GameData>

Hunting through the source code you will find that the entries in the Defines database table are handled within the CvGlobals class. To support this new entry, we need to make changes in four places in two files. So our simple one line change is already becoming much more complex.

Looking through CvAICityStrategy.cpp for other XML based values we find CITYSTRATEGY_ARMY_UNIT_BASE_WEIGHT, so we'll use that to locate places in CvGlobals.h and CvGlobals.cpp to insert our own code.

Using our newly acquired #if-#else-endif technique, in CvGlobals.h we need

Code:
    inline int getAI_CITYSTRATEGY_ARMY_UNIT_BASE_WEIGHT()
    {
      return m_iAI_CITYSTRATEGY_ARMY_UNIT_BASE_WEIGHT;
    }
#if defined(WH_AI_TILES_PER_SHIP)
    inline int getAI_TILES_PER_SHIP()
    {
      return m_iAI_TILES_PER_SHIP;
    }
#endif
    inline int getAI_CITIZEN_VALUE_FOOD()
    {
      return m_iAI_CITIZEN_VALUE_FOOD;
    }

and

Code:
    int m_iAI_CITYSTRATEGY_ARMY_UNIT_BASE_WEIGHT;
#if defined(WH_AI_TILES_PER_SHIP)
    int m_iAI_TILES_PER_SHIP;
#endif
    int m_iAI_CITIZEN_VALUE_FOOD;

and in CvGlobals.cpp we need

Code:
    m_iAI_CITYSTRATEGY_ARMY_UNIT_BASE_WEIGHT(700),
#if defined(WH_AI_TILES_PER_SHIP)
    m_iAI_TILES_PER_SHIP(5),
#endif
    m_iAI_CITIZEN_VALUE_FOOD(12),

(Note that we are using the original Firaxis value of 5 here and not our compromise value of 12)
and

Code:
    m_iAI_CITYSTRATEGY_ARMY_UNIT_BASE_WEIGHT = getDefineINT("AI_CITYSTRATEGY_ARMY_UNIT_BASE_WEIGHT");
#if defined(WH_AI_TILES_PER_SHIP)
    m_iAI_TILES_PER_SHIP = getDefineINT("AI_TILES_PER_SHIP");
#endif
    m_iAI_CITIZEN_VALUE_FOOD = getDefineINT("AI_CITIZEN_VALUE_FOOD");

finally we need to change our code in CvAICityStrategy.cpp to

Code:
#if defined(WH_AI_TILES_PER_SHIP)
    if (iNumUnitsofMine * GC.getAI_TILES_PER_SHIP() > iWaterTiles)
#else
    if (iNumUnitsofMine * 5 > iWaterTiles)
#endif

We now have a problem in that we have three files that all refer to WH_AI_TILES_PER_SHIP but obviously we should only have one #define for it. We need a central place to keep all of our custom #defines. To do this we will create a custom header file and add it in such a way that all of our #defines will be available to all files.

In the "Solution Explorer" pane, right-click "CvGameCoreDLL_Expansion2" (or whatever DLL source you are modifying) and select "Add -> New Filter". Enter a name, eg "WhMods" and press Enter. Right-click your new filter and select "Add -> New Item ..." In the pop-up dialog select "Header File (.h)" and enter a name, eg "WhMods.h", and click the "Add" button
The file WhMods.h will open in the editor, add the following

Code:
// WhMods.h
#pragma once

#ifndef WH_MODS_H
#define WH_MODS_H

#endif

(replacing WH with whatever unique prefix you have chosen). This is some basic house-keeping you will find in all .h files to defend against the compiler including the contents more than once.

We now need to add our custom header file into the files that need our #defines. We could #include the header into every file that needs the values but this is bad practice - we should include it into the project's "pre-compiled header" (or pch) file. In the "Solution Explorer" window, expand the "Header Files" section under "CvGameCoreDLL_Expansion2", locate the file "CvGameCoreDLLPCH.h" and double-click it to open it in the editor. Scroll down to around line 95 and add your new custom header file as follows

Code:
#include <Fireworks/FFastList.h>

#include "WhMods.h"

#include "CvGameDatabase.h"

save and close the "CvGameCoreDLLPCH.h" file.

We now just need to add our #defines into our custom header file and they will be available in any file within the project.

Code:
// WhMods.h
#pragma once

#ifndef WH_MODS_H
#define WH_MODS_H

// Use the database value Defines.AI_TILES_PER_SHIP to change the
// AI's decision if it needs to build more ships for a particular body of water
#define WH_AI_TILES_PER_SHIP

#endif

(Note that now the value is being picked out of the database, we don't need an actual value for the #define any more, we just need it to be defined.)

Having one custom header file for all our changes also has the benefit of having one central location where we can document our efforts and act as a starting point for anyone coming along behind us.


Phew! Returning to our edits for retrieving a value from the database Defines table, that's not too bad for a few new XML values, but if we want lots it's going to get very tedious and error prone. Lots of copying and pasting code that then needs to be repetitively edited is always prone to making mistakes; typically we'll do something stupid like
Code:
    m_iAI_TILES_PER_SHIP = getDefineINT("AI_CITYSTRATEGY_ARMY_UNIT_BASE_WEIGHT");
which will compile and execute without error, but will not perform as expected and be very hard to track down!

The pre-processor #define can be used for more than constant values, and by getting creative we can simplify our code changes considerably. By adding the following to our header file we can create a suite of #defines to simplify our work. When used like this, the #defines are referred to as macros.

Code:
// GlobalDefines (GD) wrappers
#define GD_INT_DECL(name)       int m_i##name
#define GD_INT_DEF(name)        inline int get##name() { return m_i##name; }
#define GD_INT_INIT(name, def)  m_i##name(def)
#define GD_INT_CACHE(name)      m_i##name = getDefineINT(#name)
#define GD_INT_GET(name)        GC.get##name()

(Note: I like to place macros like this at the end of the file as once written/copied they rarely need to be consulted or edited.)

We don't really need to know how these work, just what they do and how to use them - much like the methods on a class. You can think of macros as automated copy and paste, for example

Code:
GD_INT_CACHE(AI_TILES_PER_SHIP);

will be expanded by the pre-processor to

Code:
m_iAI_TILES_PER_SHIP = getDefineINT("AI_TILES_PER_SHIP");

We can now change our code in CvGlobals.h to

Code:
    inline int getAI_CITYSTRATEGY_ARMY_UNIT_BASE_WEIGHT()
    {
      return m_iAI_CITYSTRATEGY_ARMY_UNIT_BASE_WEIGHT;
    }
#if defined(WH_AI_TILES_PER_SHIP)
    GD_INT_DEF(AI_TILES_PER_SHIP)
#endif
    inline int getAI_CITIZEN_VALUE_FOOD()
    {
      return m_iAI_CITIZEN_VALUE_FOOD;
    }

and

Code:
    int m_iAI_CITYSTRATEGY_ARMY_UNIT_BASE_WEIGHT;
#if defined(WH_AI_TILES_PER_SHIP)
    GD_INT_DECL(AI_TILES_PER_SHIP);
#endif
    int m_iAI_CITIZEN_VALUE_FOOD;

and in CvGlobals.cpp we need

Code:
    m_iAI_CITYSTRATEGY_ARMY_UNIT_BASE_WEIGHT(700),
#if defined(WH_AI_TILES_PER_SHIP)
    GD_INT_INIT(AI_TILES_PER_SHIP, 5),
#endif
    m_iAI_CITIZEN_VALUE_FOOD(12),

and

Code:
    m_iAI_CITYSTRATEGY_ARMY_UNIT_BASE_WEIGHT = getDefineINT("AI_CITYSTRATEGY_ARMY_UNIT_BASE_WEIGHT");
#if defined(WH_AI_TILES_PER_SHIP)
    GD_INT_CACHE(AI_TILES_PER_SHIP);
#endif
    m_iAI_CITIZEN_VALUE_FOOD = getDefineINT("AI_CITIZEN_VALUE_FOOD");

finally we need to change our code in CvAICityStrategy.cpp to

Code:
#if defined(WH_AI_TILES_PER_SHIP)
    if (iNumUnitsofMine * GD_INT_GET(AI_TILES_PER_SHIP) > iWaterTiles)
#else
    if (iNumUnitsofMine * 5 > iWaterTiles)
#endif

This has both reduced the number of lines we need to add and also removed the obvious candidate where an error is likely to occur. The benefits of these macros really show when you are adding many new entries to the <Defines> table, for example, from my own DLL mods to support CS gifts

Code:
#if defined(MOD_GLOBAL_CS_GIFTS)
	GD_INT_DEF(MINOR_CIV_FIRST_CONTACT_BONUS_FRIENDSHIP)
	GD_INT_DEF(MINOR_CIV_FIRST_CONTACT_BONUS_CULTURE)
	GD_INT_DEF(MINOR_CIV_FIRST_CONTACT_BONUS_FAITH)
	GD_INT_DEF(MINOR_CIV_FIRST_CONTACT_BONUS_GOLD)
	GD_INT_DEF(MINOR_CIV_FIRST_CONTACT_BONUS_FOOD)
	GD_INT_DEF(MINOR_CIV_FIRST_CONTACT_BONUS_UNIT)
	GD_INT_DEF(MINOR_CIV_FIRST_CONTACT_XP_PER_ERA)
	GD_INT_DEF(MINOR_CIV_FIRST_CONTACT_XP_RANDOM)
	GD_INT_DEF(MINOR_CIV_FIRST_CONTACT_PLAYER_MULTIPLIER)
	GD_INT_DEF(MINOR_CIV_FIRST_CONTACT_PLAYER_DIVISOR)
	GD_INT_DEF(MINOR_CIV_FIRST_CONTACT_SUBSEQUENT_TEAM_MULTIPLIER)
	GD_INT_DEF(MINOR_CIV_FIRST_CONTACT_SUBSEQUENT_TEAM_DIVISOR)
	GD_INT_DEF(MINOR_CIV_FIRST_CONTACT_FRIENDLY_BONUS_MULTIPLIER)
	GD_INT_DEF(MINOR_CIV_FIRST_CONTACT_FRIENDLY_BONUS_DIVISOR)
	GD_INT_DEF(MINOR_CIV_FIRST_CONTACT_FRIENDLY_UNIT_MULTIPLIER)
	GD_INT_DEF(MINOR_CIV_FIRST_CONTACT_FRIENDLY_UNIT_DIVISOR)
	GD_INT_DEF(MINOR_CIV_FIRST_CONTACT_HOSTILE_BONUS_MULTIPLIER)
	GD_INT_DEF(MINOR_CIV_FIRST_CONTACT_HOSTILE_BONUS_DIVISOR)
	GD_INT_DEF(MINOR_CIV_FIRST_CONTACT_HOSTILE_UNIT_MULTIPLIER)
	GD_INT_DEF(MINOR_CIV_FIRST_CONTACT_HOSTILE_UNIT_DIVISOR)
#endif

By using "wrappers" in this way, not only can we easily find all code for AI_TILES_PER_SHIP, but we can also easily find all the values we have pushed into the database and where they are used by searching for GD_INT_GET

Before ending this tutorial let's implement a simple yet effective logging feature. Add the following to your header file before the GlobalDefines (GD) wrappers (and change WH to whatever your unique prefix is)

Code:
// Comment out this line to switch off all custom mod logging
#define WH_MODSLOG "WhMods.log"

// Custom mod logger
#if defined(WH_MODSLOG)
#define	WH_LOG(sFmt, ...) {  \
	CvString sMsg;  \
	CvString::format(sMsg, sFmt, __VA_ARGS__);  \
	LOGFILEMGR.GetLog(WH_MODSLOG, FILogFile::kDontTimeStamp)->Msg(sMsg.c_str());  \
}
#else
#define	WH_LOG(sFmt, ...) __noop
#endif

Take care with the \ at the end of four of the lines, they are very important!

We can now write a message to the logs\WhMods.log file with

Code:
    WH_LOG("Hello my own log file");

We can also include variables into the messages as

Code:
    WH_LOG("My id is %i and my name is %s", kPlayer.GetID(), kPlayer.getName());

Take care with methods that return CvString, as they look look like C++ strings (char*) but arn't - use their c_str() method to get a real C++ string (or you will get nonsense values in the messages in the log!)

Code:
    WH_LOG("The next city for player %s is %s", kPlayer.getName(), kPlayer.getNewCityName().c_str());

An immediate use for the log file would be to add logging as our custom values are loaded from the database - useful for confirming both their loaded value and that they actually got loaded. We could add a WH_LOG() line after each value has been loaded into the cache via the GD_INT_CACHE() macro in CvGlobal.cpp, or we could just expand the GD_INT_CACHE macro as follows

Code:
#define GD_INT_CACHE(name)      m_i##name = getDefineINT(#name); WH_LOG("GlobalDefine: %s=%i", #name, m_i##name)

Finally, note the comment above where WH_MODSLOG is defined. If you want to disable all your custom logging (say for a final release version of your custom DLL mod) you don't need to remove every WH_LOG() line, you just need to comment out the #define WH_MODSLOG line.

[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