[SDK] Mistakes I Have Already Made for You

isau

Deity
Joined
Jan 15, 2007
Messages
3,071
Overview
Modifying the SDK can be an incredibly rewarding experience. By working with the C++ code you can rewrite portions of Civ IV. This power should not be balked at; it's a part of what makes Civ IV one of the best strategy game ever released.

But with power comes responsibility. The ability to modify the code means the ability to break the code in ways you never imagined.

Below is a list of mistakes that I made over the course of building my first mod. I've compiled it hoping I can help you overcome similar errors. This list is far from comprehensive, and I'm sure I and others will have many more things to add it as we continue to mess up... err, mod files. :D


Mistake 1: Not Keeping Backups of Your Work
This one seems so obvious, but I cannot stress it enough. If you plan to work with the SDK, you must make regular backups of your work, and label them effectively. A single missed keystroke can bring your code to its knees; lots of times code that used to work will suddenly stop, and identifying what might have changed is incredibly important. Keep backups. KEEP backups! It will seem like a waste of time until your code breaks the game. This happened to me once and it cost me 3 months of dev time.

If you modifying the SDK, there is a 100% guarantee that at some point you will break the game. KEEP BACKUPS!


Mistake 2: Including Unmodified XML in your Mod Folders
Ok so this one has nothing to do with the SDK, but it's a mistake I made early in the modding process that caused my mod to take minutes, instead of seconds, to load.

You only need to include XML in your mod if you've done something to change the file versus what would've been included in the standard game. Including an XML file forces the game to reload the XML data, and since your data is exactly the same as the core game, all you're doing is making the game take a longer time to load.

Remember that to create a new mod in Civ IV, all you have to is add a folder with the mod's name in the mod directory. Unless you add files to this folder, the mod will load and function exactly like a standard game of Civ IV. Only add XML files that are different than what's in the core game.

The one XML file type that's a little unusual is the stuff in the Text folder. Remember that unlike other XML files, the Text files follow an add-or-replace logic. What this means is that if you can create a new XML file with a unique name and place it in this folder, Civ IV will load it and add any new tags you've created to the available strings resource, and replace any pre-existing tags, regardless of what file they came from. Don't bother including entire copies of original Text XML files just to change one or two lines of text; just copy the tags into a new XML file and the game will override the original values.


Mistake 3: Adding Variables without Total Recompiles
Adding a new variable to the game requires you to edit the .h (header) file that controls the list of available properties/variables and functions/methods. Anytime you add a new variable to the game or make other significant changes to the .h files, you must force a full recompile. This is done by deleting everything from your output folder (the folder where your dll compiles, which contains, among other things, your dll and all the obj files). Full recompiles are a lengthy process, often taking as long as 20 minutes.

But you must do this when you add or delete variables. The consequences of forgetting are random, untraceable, annoying crashes to the desktop.


Mistake 4: Forgetting that CvCity::acquireCity Erases the Original City
Now we're really getting into the weeds. Remember that CvCity::acquireCity actually deletes the original city and replaces it with a new one. What this means for modders is if you don't specifically tell Civ IV to copy a value from the old city to the new, the value disappears when someone invades, culture flips, or is given the city. Also, any pointers to the city will vanish. This should all be accounted for somewhere in this method.


Mistake 5: Making Boolean Values Truly Boolean Behind the Scenes
When adding booleans, remember that just because something will ultimately result in a bool value doesn't mean the best way to store the data is to literally use a bool. Here's an example of a much more powerful way to set up bools (using a real example derrived from my mod) that took me much too long to figure out and start using:

CvPlayer.h
Code:
public:
  bool CvPlayer::getIsCanUpgradeOutsideBorders() 
  void CvPlayer::changeCanUpgradeOutsideBordersCount(int iChange)

protected: 
  int m_iCanUpgradeOutsideBordersCount;


CvPlayer.cpp
Code:
bool CvPlayer::getIsCanUpgradeOutsideBorders()
{
  // return true if the count is greater than 1
  return m_iCanUpgradeOutsideBordersCount > 0;
}

void CvPlayer::changeCanUpgradeOutsideBordersCount(int iChange)
{
  m_iCanUpgradeOutsideBordersCount += iChange;
}

When I want to grant the player the ability to upgrade outside borders:
Code:
  pPlayer->changeCanUpgradeOutsideBordersCount(1);

...and when I want to take it away:
Code:
  pPlayer->changeCanUpgradeOutsideBordersCount(-1);

The power of this logic is that you can use setCanUpgradeOutsideBordersCount to count the number of sources that are granting the ability to perform this action. So the ability to upgrade outside my borders might be coming from a Civic (+1), a Trait (+1), a special building (+1), and a Golden Age (+1) all at the same time. If I switch civics, lose the building, and the golden age ends, I still have the ability to upgrade because of the Trait. Using this logic makes complex relationships far easier to set up than trying to figure out on a case-by-case basis whether the player should be able to perform an action.


Mistake 6: Adding XML Tags that Reference Values that are Not Yet Loaded
The ability to add new XML tags to Civ IV is incredibly powerful. There's also a trap herein. Before you spend time adding new tags that will cross-reference between XML files (say, cross-referencing between Civics and Promotions, for example) you must verify the order in which the XML files load. Make sure that whatever file you will be editing to cross-reference the other file loads after the file you're attempting to reference, or the game won't work. You can find a list of the load orders on these forums.

(The details of adding new XML tags are beyond the scope of this guide. Check out Kael's excellent tutorial on these forums to get yourself started.)


Mistake 7: Looping through Players without Checking for Dead or Barbarian Players
Its often useful to loop through all of the players in the game. Just keep in mind that doing so can cause problems if you forget to check, as I often have, for Barbarians and dead/inactive player slots.

Code:
  int iI;
  for (iI = 0; iI < MAX_CIV_PLAYERS; iI++)
  {
    CvPlayer& kPlayer = GET_PLAYER((PlayerTypes)iI)
    // make sure the player is alive and not a barb before doing anything
    [COLOR="Blue"]if (kPlayer.isAlive() && !kPlayer.isBarbarian())
[/COLOR]    {
        // do something
    }
  }


Mistake 8: Forgetting that Players != Teams
A mistake I constantly make is coding something at the Player level (often a Civic or Trait), getting almost done with it, going to add the logic for it, and then realizing that what I'm trying to affect applies to Teams and not Players.

Remember that in Civ IV, everyone is a member of a team, even if it's a team of 1 (as it is in a standard game). Some things that might appear to happen on the Player level that in fact happen at the team level are:
  • War and Peace Status
  • Technology
  • Open Borders Agreements
  • Visibility
  • Victories
  • Espionage Points
  • Score


Mistake 9: Referencing a Pointer without Checking to See If It's NULL

This is the classic C++ mistake that every programming manual warns you about. If you attempt to reference a property or method on a pointer without checking to see if the pointer is NULL first you will without fail crash the game. Always build pointer-checking logic into your code.

Code:
if (pPlayer != NULL)
{
  // do something
}

Mistake 10: Hard-Coding Values
A temptation that will strike repeatedly is to hardcode values into the SDK. Civ IV has a great way to avoid this.

In the XML folder (in the main folder, not in the subfolders) there's convenient file called GlobalDefines.xml. No mod has an excuse for not using it. Place a copy of this file in your mod, and add tags that define values you will be referencing in code. Below is an example of a define I used to determine the changes of burning population resulting in a religion being removed:

Code:
	<Define>
		<DefineName>BURN_WITCH_REMOVE_RELIGION_CHANCE</DefineName>
		<!--Threshold below which religions disappear for leaders with negative MissionarySpreadModifier, bigger number = bigger chance-->
		<iDefineIntVal>25</iDefineIntVal>
	</Define>

To reference this in code, use:
Code:
GC.getDefineInt("BURN_WITCH_REMOVE_RELIGION_CHANCE");

There's also a getDefineFloat and getDefineString function with similar use.

Mistake 11: Dividing by Zero
Another classic. Trying to divide something by zero will crash the game. While none of us would ever type something like x = 3/0, divide-by-zero errors can creep in when we obtain numbers from methods or as the result of a formula. Check to make sure denominators are not equal to zero before trying to divide.

Code:
int iResult;
if (GC.getNumThingees() == 0)
{
  // dividing something by zero does not really produce zero, but it's usually the answer I wanted
  iResult = 0;
}
else
{
  iResult = 10/GC.getNumThingees();
}
 
This should be stickied in Tutorials & Refrences.
 
I could totally add to the list myself... :rolleyes:
 
Defintely hit most of the top ones. I learned early that if the game crashes, pull out winmerge, compare to your last backup ;) (definitely a necessity) and look for potential NULL pointers or division by zeros.

A better way to avoid division by zero in most cases (and how it's generally handled in Firaxis code) is to use:

Code:
Var1 /= std::max(1, Var2)

Of course this is used expecting that the divisor should not ever be zero, it's just there to make sure if it is (by some mistake in the code, unexpected input values, etc.), it doesn't crash the game. If you actually expect a case where a zero divisor should occur and you want it to return zero (I've done a lot of code and don't think I've ever needed such a case, but it could happen), then your code would be the correct way to handle.

Also, I highly suggest checking out the DannyDaemonic's makefile here. It may take a little time to make sure everything is set up right the first time you switch to it, but it really does full compiles in half the time, and eliminates having to manually force full recompiles (it automatically will do so if you change header files). Definitely worth it.
 
You can also use my updated makefile (which is based on DannyDaemonic's). It's very similar but with a few improvements (no need to copy the boost/python folders everywhere).

And as for backups - it's much easier and safer to use version control.
 
You can also use my updated makefile (which is based on DannyDaemonic's). It's very similar but with a few improvements (no need to copy the boost/python folders everywhere).

Even better!

And as for backups - it's much easier and safer to use version control.

Agreed (though I'm currently doing it the old fashioned way :mischief:). Version control makes finding problem code easier, and keeps everything backed up where a hard drive crash won't ruin your day.
 
Generally agree, esp. point 9, which was the reason for most of my "mysterious" CTDs and a few hours wasted with debugger.

Ad. 3
I'll just echo Asaf - since I switched to that makefile, compiling takes 10-20 seconds and I never did a full rebuild since.

Ad. 8
For mods/scenarios playable through pre-made maps only, if you set Players and Teams in WB, they are equal at all times, which is very, very convenient both in C++ and Python.

Ad. 10
No "real programmer" will agree, but especially when it comes to hobby, laziness is often better than good practice if you want to finish the mod in a year, not three. Here's one excuse: Global defines (and XML in general) can't really store complex data like large multi-dimensional arrays.
 
This is a quite useful list and I would like to add a few comments, which will hopefully be helpful to somebody.

Mistake 1: Not Keeping Backups of Your Work
User svn or git and add proper commit messages. You need a proper log when you least expect it.

Mistake 3: Adding Variables without Total Recompiles
Fastdep.exe solves this issue. Check the makefile link in my signature for details. That link also contains info on faster compilation, profiling and other useful stuff.

Mistake 6: Adding XML Tags that Reference Values that are Not Yet Loaded
I got so tired of XML loading order issues and in the end I redesigned the XML loading system.
I changed it into looping though all XML files and only call CvInfoBase::read(). This will read all types and add them to the list in the DLL. After that XML files can be read normally and a file can refer to one, which has yet to be read because it already read the base for all.

There is just one issue with this approach. Some files lacks a type and those files can't handle being read twice. Hints is one of them and if you read it twice, it will list each hint twice.

See Medieval Conquest source code for more details on this solution.


Mistake 9+11 are common programming errors and not really MOD specific. I would advice reading up on common C++ mistakes if needed.
 
Top Bottom