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
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
Next, edit CvCity::write as follows. This is the code that handles writing the value when the player saves a game.
CvCity::write
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
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
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
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
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
The %d1 will be replaced by a number passed by your DLL. [ICON_UNHEALTHY] displays as .
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
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
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
(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
And now we need to edit the part where the nuke actually occurs in CvUnit::nuke to this:
CvUnit::nuke
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
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 .
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