[SDK] How To: Add Custom Health or Happiness Modifiers

isau

Deity
Joined
Jan 15, 2007
Messages
3,071
This reference explains how to modify the SDK to account for custom Happiness and Health Modifiers. I hope you find this reference useful. Please post any questions suggestions or corrections in the thread below.


What You Need Before Beginning
Depending on what you are trying to do, it may be a good idea to read Kael's excellent tutorial on adding new XML tags before attempting this walkthrough. Basic knowledge of C++ and the CivIV SDK will also be helpful.


Background Info: How Happiness and Health Work in the Standard Game
It's important to have a basic understanding of how happiness and health work before attempting to mod them.

Each city has it's own Health and Happiness rating. These are displayed on the city screen. Various things, including rising populations, building buildings, and using Civics cause health and happiness to change.

Rolling over the happiness or health rating on the city screen produces a read-out of all the reasons the city is or is not prospering. It shows the total happiness or health along with every factor that is contributing to it.

Although most players think of Happiness and Health as "complete" concepts, what you will discover is that the happiness rating is actually stored in multiple variables. Each variable represents happiness or health derrived from a different source. There is an accessor function at the City level for every factor that contributes to the final health or happiness value. Some of these functions provide access to variables stored on the City level. Others merely pass a value from the Player, Team or Game level to the City.

Is that all clear as mud? Let's throw one more complication in: not only are there different accessor functions for each source of happiness or health, the functions are also seperated by whether they are good or bad. The game distinguishes between "good" Happiness and "bad" Happiness (i.e. anger), "good" health and "bad" health. Thus you will see lots of functions with names like changeBuildingGoodHappiness and changeBuildingBadHappiness, which reflect the best and the worst opinions of your spoiled little citizens.


The CvCity Files
In order for Happiness or Health to have any affect at all, accessor methods will need to be added at the City level. Even if all the methods are going to do is pass a value obtained from the Player (GET_PLAYER(getOwner())) you will still need them.


CvCity.h - Add a declaration for your new accessor function. Naming conventions are get<whatever>GoodHealth, get<whatever>BadHealth, get<whatever>Happiness, and get<whatever>Anger.

In this case, mine will be getNukedBadHealth and it will represent bad health incurred after a city gets nuked. I will also add a changeNukedBadHealth function that changes the value. Make both methods public so that they can be accessed by classes other than CvCity. The value will need to be saved, so I'll also need a new variable/property to hold it. We'll make that variable protected* for OOP reasons.

*(Edited May 1, 2008, original advise was to make property 'private'. Changed to 'protected' so that the variable is available to sub-classes.)

CvCity.h
Code:
public: 
     getNukedBadHealth() const;
     changeNukedBadHealth(int iChange);

protected:
     int m_iNukedBadHealth;


CvCity.cpp - Now it's time to add the definition for the function. We'll also have to set up the variable so it can be saved.

To save the variable we find the function CvCity::read, and add this line to it (making sure to add comment tags around it so we can find it later):

CvCity::read
Code:
pStream->Read(&m_iNukedBadHealth);


Next, edit CvCity::write as follows. This is the code that handles writing the value when the player saves a game.

CvCity::write
Code:
pStream->Write(m_iNukedBadHealth);

The variable's still not completely set up. Next we need to make sure it's value is always reset to zero when a new game is loaded. If we don't, the game will work okay when first loaded, but will go crazy if we load a new game.

In CvCity::reset add this code somewhere in the long stream of reset values:

CvCity::reset
Code:
m_iNukedBadHealth = 0;

Ok, the variable is done. Now it's time to add definitions for our functions. Somewhere in the CvCity.cpp (I always prefer to do this near the bottom of the file), add this:

CvCity.cpp
Code:
int CvPlayer::getNukedBadHealth() const
{
	return m_iNukedBadHealth;
}

void CvPlayer::changeNukedBadHealth(int iChange)
{
	m_iNukedBadHealth += iChange;
}


Okay, the variable/property should be totally set up. You're part of the way there.

IMPORTANT: What you should do at this point is test to make sure you haven't missed anything. Since you've added a new property to the game, you should clear out your output folder--literally go to the folder where your dll outputs and delete everything in it. This will force the compiler to do a full rebuild. If you don't do this, the game will crash the first time a city is built or restored. After the full build, the game should load and be playable again. It doesn't do anything special yet, but verifying that it still works is gold.


----

Once you've tested to make sure your game is playable, go back through the code and double check it. Make sure all your read, write, and accessor functions are coded exactly as they should be. A VERY common mistake is to have accessors that either don't save, save but aren't restored, or (the worst!) accidentally read or write data from or to the wrong places. If you made a typo it will save you hours of time if you catch the problem at this stage.

Once you're confident that your code is correct, you can get started with making the property actually do something.

We'll start by adding our modifications to the calculation of total city health. That function is CvCity::badHealth(). (If we were adding good health, it would be CvCity::goodHealth()). Near the bottom of the function add this:

CvCity::badHealth
Code:
iHealth = getNukedBadHealth();
	if (iHealth > 0)
	{
		iTotalHealth += iHealth;
	}

iTotalHealth is the return value of this function. We are checking to see if the the nukedBadHealth is higher than zero. If so, we add that (un)health to the total. That takes care of adjusting the health downwards. It would seem that we are done...

...except that we've introduced a major problem! Right now the city screen is going to display the wrong value. According to the underlying city value, the health will be one value, but the health rate reported on the city screen will show a different number.

The problem is fixed by updating CvGameTextMgr with an explanation of this new health hazard. Find the method called void CvGameTextMgr::setBadHealthHelp(CvWStringBuffer &szBuffer, CvCity& city). Somewhere in this section, add the following:

CvGameTextMgr::setBadHealth
Code:
		iHealth = (city.getNukedBadHealth());
		if (iHealth > 0)
		{
			szBuffer.append(gDLL->getText("TXT_KEY_MISC_NUKED_BAD_HEALTH", iHealth));
			szBuffer.append(NEWLINE);
		}

This code does 2 things. It adjusts the health value displayed on the city screen down. It also adds an explanation of why it's been adjusted.

Now it's time for a little XML. We need to add text explaining what TXT_KEY_MISC_NUKED_BAD_HEALTH translates to. (If you're not sure how to do this, check out some of the other tutorials posted on this site.) The XML should be something like this:

XML
Code:
	<TEXT>
		<Tag>TXT_KEY_MISC_NUKED_BAD_HEALTH</Tag>
		<English>+%d1_Change[ICON_UNHEALTHY] Nuclear fallout sucks!</English>
		<French>+%d1_Change[ICON_UNHEALTHY] Nuclear fallout sucks!</French>
		<German>+%d1_Change[ICON_UNHEALTHY] Nuclear fallout sucks!</German>
		<Italian>+%d1_ChangeICON_UNHEALTHY] Nuclear fallout sucks!</Italian>
		<Spanish>+%d1_ChangeICON_UNHEALTHY] Nuclear fallout sucks!</Spanish>
	</TEXT>

The %d1 will be replaced by a number passed by your DLL. [ICON_UNHEALTHY] displays as :yuck:.


You're almost there! The only thing left to do is add code that actually does the dirty deed of adjusting the values of the city. Since mine involves nuking, I'll do it in CvUnit::nuke. Here's what I added (somewhere under the missile interception code--want to make sure to only do a health hit if the missile actually hits!)

CvUnit::nuke
Code:
CvCity* pCity = pPlot->getPlotCity();
if (pCity != NULL)
{
	pCity->changeNukedBadHealth(2);
}

This code creates a pointer to the plot where the nuke landed. If there is a city on the plot, it gets 2 bad health points for getting nuked. (The 2 is hardcoded. You could greatly improve this code by using an XML-defined integer in a modded GlobalDefines.xml file, and adding something like GC.getDefineINT("NUKE_HEALTH_DAMAGE"). That way if you decide the health hit is over or under powered, you can adjust it without recompiling.


Possible Improvements
This code works, but there is some potential for improvement. Suppose I wanted the bad health to heal over time. It won't the way I've written it here--that nuke hit is permanent!

To make the damage heal over time, I could take this approach (not that to save time I am modifying methods and properties we already created rather than creating new ones like I would actually probably do in real life):

CvPlayer.h - Change the names of some of the variables to match what they're really going to do (items in blue are changes):

CvCity.h
Code:
public: 
     getNukedBadHealth() const;
     changeNukedBadHealth[COLOR="Blue"]Turns[/COLOR](int iChange);

private:
     int m_iNukedBadHealth[COLOR="Blue"]Turns[/COLOR];

Make sure to change the read, write, and reset functions as well as anywhere else that the changed function names will have an affect.

Now we need a slight change to the logic:

CvCity.cpp
Code:
int CvPlayer::getNukedBadHealth() const
{
	return m_iNukedBadHealthTurns / 30;
}

void CvPlayer::changeNukedBadHealthTurns(int iChange)
{
	m_iNukedBadHealthTurns += iChange;
        m_iNukedBadHealthTurns = std::max(0, m_iNukedBadHealthTurns); // don't let it be less than 0
}

(Again you could improve this by removing the hard-coded 30 in getNukedBadHealth and replacing it with a XML defined int, or, even better, a variable from the GameSpeedInfo class.)


To make the value heal over time, you need to modify CvCity::doTurn() with this additional line of code: This will be called every turn.

CvCity::doTurn
Code:
changeNukedBadHealthTurns(-1);


And now we need to edit the part where the nuke actually occurs in CvUnit::nuke to this:

CvUnit::nuke
Code:
CvCity* pCity = pPlot->getPlotCity();
if (pCity != NULL)
{
	pCity->changeNukedBadHealthTurns(90);
}

That will add 90 turns of nuked-ness to the city. Every turn, we subtract one from the number of turns of nuked-ness remaining. The amount of health effect is equal to number of turns remaining /30. This rounds down, so the effect disappears after 60 turns, with a final cooldown period of 30 turns where there is no health hit but still some record that the player was hit being maintained behind the scenes.

Happy nuking!

-isau
 
I could say this is a great tut. It is better than that. It is exceptional and the kind of thing that makes us wannabe modders try harder. Many thanks!

I have a question about clearing the output folder. Is that the Assets folder containing files?
CvGameCoreDll.dll
CvGameCoreDll.exp
CvGameCoreDll.lib
CvGameCoreDll.pdb

I'm guessing it is, I copied those files over to a junk folder and recompiled. Magically all the files were replaced but I'm concerned about what it did 'cause it only took 14 seconds.
 
If you use makefile and VC++ 2005. Then Final_release is the folder where you have to delete files.

The compiler builds these files back.

Actually i made some tests. I deleted only modified *.obj files and the

CvGameCoreDll.dll
CvGameCoreDll.exp
CvGameCoreDll.lib
CvGameCoreDll.pdb
CvGameCoreDLL.obj
CvGameCoreUtils.obj

The game seems to work as planned. Total rebuild takes 20 to 30 minutes for my comuter. It is too long.
 
Glad you guys found the tutorial helpful! Let me know if you have requests for others. I'm not an expert on everything, but any of the features in my mod are fair game.

FYI I did change one piece of advice in the column. Properties should be set to protected, not private. This makes sure they're available to sub-classes (in the case of CvPlayer, a subclass would be CvPlayerAI, which controls AI behavior).
 
Looks like for Visual C++ 2003, it is the /Assets folder of the SDK. When I actually changed a file before compiling, it took like 12 mins.
 
Just wanted to say how much this tut has helped me. For a long time I wanted colonies and even bought BtS for that reason. Just about the time my copy of BtS arrived I got a link to Jeckel's culture mod and it was for BtS. Well everything was all set except for the fact I had a whole bunch of stuff already incorporated into vanilla Civ and there was really nothing I wanted from BtS except the culture mod.

Knowing you have to break a few eggs to get an omelet, I tried incorporating the culture mod into vanilla Civ. After about a week I got it working - almost. It was fine as long as I never saved a game and exited. Whenever I reloaded a saved game, the culture would go away after the first turn. No doubt I would never have found the trouble except for this tut. Here is the bit of code that fixed it:
Code:
    m_iCultureBorderRange = 0,
    m_iSameTileCultureChange = 0,
    m_iAdjacentTileCultureChange = 0,
    m_iMinBuildDistance = 0;
    {
        reset();
    }

Thanks again. As far as I'm concerned there are no bad tuts - anyone who takes the time to write one deserves a great deal of appreciation but some just don't hit where I live. They are either too easy or too hard. This one was just right. :)
 
OK, as much as I hate to admit I jumped the gun here, the truth is I did. I had thought it was fixed because my savegames do show the colony culture but as soon as I hit the next turn button the culture goes away.

I've come to the conclusion the culture is not being written to the savegames after reading all I could find on savegames, the most definitive being some code Matze talked about in his menu mod. The thing is I don't know if these culture variables can be written to CvGame or if they are in some sort of array or, well, you get the picture.
 
The mod is apparently finally working. The couple of changes I made looks like it had nothing to do with the version or expansion or the way the savegame works. I made lots of changes but I think there were only two which really caused the mod to work. It was in CvPlot.cpp. One bit of code was commented out and one reinstated as follows:
Code:
    // < JImprovementCultureBorders Mod Start > dlo commenting out
    //m_eImprovementOwner = NO_PLAYER;
    //m_iCultureBorderRange = -1;
    //m_iCultureBorderCount = 0;
    //m_iSameTileCultureChange = 0;
    //m_iAdjacentTileCultureChange = 0;
    // < JImprovementCultureBorders Mod End >

Code:
	// dlo reinstating below block of code
    if (eBestPlayer == NO_PLAYER)
	{
	    if (getImprovementType() != NO_IMPROVEMENT)
	    {
            if (getImprovementOwner() != NO_PLAYER)
            {
                eBestPlayer = getImprovementOwner();
            }
	    }
	}
	// dlo end change
 
I have a question for you. I'm messing with Grave's HiTM Mod. I want to make the unit dynamics more like an operational wargame, basically: units die much less, but take longer to heal. Add on to this, it would be neat if terrain features, and city Pop size modify healing rates. So a Town tile with a railroad should heal a bit faster than one with a road or one with only a Hamlet, etc.

I've managed to adjust the gross healing rates in the GlobalDefines XML file by resetting some things as follows

<Define>
<DefineName>ENEMY_HEAL_RATE</DefineName>
<iDefineIntVal>3</iDefineIntVal>
</Define>
<Define>
<DefineName>NEUTRAL_HEAL_RATE</DefineName>
<iDefineIntVal>5</iDefineIntVal>
</Define>
<Define>
<DefineName>FRIENDLY_HEAL_RATE</DefineName>
<iDefineIntVal>8</iDefineIntVal>
</Define>
<Define>
<DefineName>CITY_HEAL_RATE</DefineName>
<iDefineIntVal>11</iDefineIntVal>
</Define>
<Define>
<DefineName>ROAD_HEAL_RATE_MODIFIER</DefineName>
<iDefineIntVal>100</iDefineIntVal>
</Define>

The original values for these were

<DefineName>ENEMY_HEAL_RATE</DefineName>
<iDefineIntVal>5</iDefineIntVal>
</Define>
<Define>
<DefineName>NEUTRAL_HEAL_RATE</DefineName>
<iDefineIntVal>10</iDefineIntVal>
</Define>
<Define>
<DefineName>FRIENDLY_HEAL_RATE</DefineName>
<iDefineIntVal>15</iDefineIntVal>
</Define>
<Define>
<DefineName>CITY_HEAL_RATE</DefineName>
<iDefineIntVal>20</iDefineIntVal>
</Define>

And the line about road_heal_rate_modifier is totally my addition. Even with the road lines added in, my application is running fine, and the healing rates do indeed seem to be slowed down!

However, I don't see any sign that healing rates on tiles with roads and without are having any impact. Can you offer any help or suggestions on this?
 
Top Bottom