whoward69
DLL Minion
The aim of this tutorial is to show you how to extend the core game objects (CvPlayer, CvUnit, CvCity, et al) to persist data across game saves.
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
Persisting data across saved games (at least in core objects) is fairly trivial, but it has some major implications. But first we need a really good reason to persist data, so we're going to fix the "Free Aqueduct" finisher for Tradition.
There are two problems with the finisher, first if a civilization has a unique replacement for the aqueduct it still receives the aqueduct and not it's unique building. Secondly if you fix the finisher such that it grants unique buildings, if you don't have Engineering (or whatever technology the unique replacement needs) when you adopt the final Tradition policy, you won't ever receive the building - unlike the "Free Culture" building policy which will give you the free building when you get the required technology.
The free food building is selected in CvCity::ChooseFreeFoodBuilding() method (don't believe the comment on the line above the method signature), and the solution to the unique building problem is in the code for the CvCity::ChooseFreeCultureBuilding() method that immediately precedes this method in the file, ie check that the civilization can actually construct the building!
That's the unique replacement bug fixed, what about handing out the free buildings when we finally get the technology and/or extra cities? While still simple, we need changes in four files. We need to duplicate the "OwedCultureBuilding" code.
In CvCity.h
and
In CvCity.cpp
and
and
In CvTeam.cpp
In CvPlayer.cpp
and
So our free food buildings now behave the same as our free culture buildings, you will get a unique replacement if appropriate and if you don't have the required technology or the total number of cities, the free food buildings will be handed out when you do. (Why the heck Firaxis didn't do it this way I'll never understand!)
Notice the problem in all that code? I start a game, I complete Tradition with only two cities - no free food buildings, but that's to be expected. I research Engineering and my two cities get their free food buildings - as expected. I found my third city - it gets its free food building as well. Looks like this works. I save the game and go to bed. Next day I reload my saved game, and shortly after found my fourth city - but no free food building. So what went wrong? The boolean that tells a city if it's owed a free food building is in memory, as long as I'm playing the same game that's OK, but the moment I save and exit it's gone. When I reload the game the flag defaults to false (from the constructor), so I don't get my outstanding free food buildings.
Now we could attempt to recalculate if cities are owed free food buildings - have we adopted a policy that grants them? do we have the required technology? have we handed out all the food buildings to the first N cities? ... but what about captured/razed/traded cities? Ouch! My head hurts! What we really need to do is store that flag into the saved game file and restore it when we reload the game (or in computer geek speak, we need to "persist" the data or even "serialize" it).
The persistence code is simple, in CvCity.cpp
and
Basically we just write it out, or read it back in, along with all the other city data. Just make sure you keep the sequencing when writing and reading the same!!!
However, we have just broken the saved game format. Any saved game you have that uses your modded DLL from before adding those two lines CANNOT be loaded by your newly updated DLL. This may not be a problem if your DLL is solely for your own personal usage, but if it's part of a mod with a user-base (eg a community based mod, or a total conversion mod with a lot of followers) your users are not going to be happy bunnies when they update to the latest and greatest version of your mod and can no longer finish their previous games - but at least they can down-grade your mod, unlike Firaxis' game breaking patches
There is a solution (based on the approach taken by Firaxis in solving the same problem), but it itself breaks the saved game format. To be effective it also has to be applied to every method that persists data - and there are 220 of those (110 reads and 110 writes) in 59 files. So, if you're not worried about being lynched by your user base you can probably stop reading now!
The basic approach is to write our own version number into the persisted data (we can't use the ones Firaxis add as we have no control over those). Every time we read the saved game data, if the version is before the value we are looking for came into existence, we use a default value, otherwise we read the value. Easier to code than describe!
and
For reasons known only to themselves, Firaxis use a unique version number - uiVersion (which has everything to do with being an Unsigned Int and nothing to do with the User Interface!!!) - for each persisted object. We'll use a single version number corresponding to the version of our DLL for every persisted object.
Unfortunately the pair
and
is required 110 times in 59 files - copy-paste typo nightmares!
We can simplify our work by using macros
and our changes above become
and
Much neater and far less error prone. We can also disable all persistence code by commenting out the single #define MOD_SERIALIZE line. Note, the CvAssert() in the MOD_SERIALIZE_WRITE macro is only there to stop numerous compiler warnings about unused variables.
Now all you have to do is decide when you are going to break the saved game format for your mod and introduce all 110 occurrences of MOD_SERIALIZE_INIT_READ() and MOD_SERIALIZE_INIT_WRITE() into those 59 files!
[END]
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
- Familiarity with #defines used as macros
- Knowledge of Lua and the Civ 5 objects, eg Player, City, Unit et al
- 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)
- The C/C++ stream operators >> and <<
- Successfully built, deployed and loaded the unaltered source as a custom DLL mod
- Created your own custom header file and added it to the CvGameCoreDLLPCH.h file
Persisting data across saved games (at least in core objects) is fairly trivial, but it has some major implications. But first we need a really good reason to persist data, so we're going to fix the "Free Aqueduct" finisher for Tradition.
There are two problems with the finisher, first if a civilization has a unique replacement for the aqueduct it still receives the aqueduct and not it's unique building. Secondly if you fix the finisher such that it grants unique buildings, if you don't have Engineering (or whatever technology the unique replacement needs) when you adopt the final Tradition policy, you won't ever receive the building - unlike the "Free Culture" building policy which will give you the free building when you get the required technology.
The free food building is selected in CvCity::ChooseFreeFoodBuilding() method (don't believe the comment on the line above the method signature), and the solution to the unique building problem is in the code for the CvCity::ChooseFreeCultureBuilding() method that immediately precedes this method in the file, ie check that the civilization can actually construct the building!
Code:
const CvBuildingClassInfo& kBuildingClassInfo = pkBuildingInfo->GetBuildingClassInfo();
if(!isWorldWonderClass(kBuildingClassInfo) && !isNationalWonderClass(kBuildingClassInfo))
{
#if defined(MOD_BUGFIX_FREE_FOOD_BUILDING)
if(getFirstBuildingOrder(eBuilding) != -1 || canConstruct(eBuilding))
{
#endif
int iFood = pkBuildingInfo->GetFoodKept();
int iCost = pkBuildingInfo->GetProductionCost();
if(iFood > 0 && iCost > 0)
{
int iWeight = iFood * 10000 / iCost;
if(iWeight > 0)
{
buildingChoices.push_back(iI, iWeight);
}
}
#if defined(MOD_BUGFIX_FREE_FOOD_BUILDING)
}
#endif
}
That's the unique replacement bug fixed, what about handing out the free buildings when we finally get the technology and/or extra cities? While still simple, we need changes in four files. We need to duplicate the "OwedCultureBuilding" code.
In CvCity.h
Code:
bool IsOwedCultureBuilding() const;
void SetOwedCultureBuilding(bool bNewValue);
#if defined(MOD_BUGFIX_FREE_FOOD_BUILDING)
bool IsOwedFoodBuilding() const;
void SetOwedFoodBuilding(bool bNewValue);
#endif
Code:
bool m_bOwedCultureBuilding;
#if defined(MOD_BUGFIX_FREE_FOOD_BUILDING)
bool m_bOwedFoodBuilding;
#endif
In CvCity.cpp
Code:
, m_bOwedCultureBuilding(false)
#if defined(MOD_BUGFIX_FREE_FOOD_BUILDING)
, m_bOwedFoodBuilding(false)
#endif
Code:
m_bOwedCultureBuilding = false;
#if defined(MOD_BUGFIX_FREE_FOOD_BUILDING)
m_bOwedFoodBuilding = false;
#endif
Code:
// --------------------------------------------------------------------------------
bool CvCity::IsOwedCultureBuilding() const
[I]// omitted for brevity[/I]
}
#if defined(MOD_BUGFIX_FREE_FOOD_BUILDING)
// --------------------------------------------------------------------------------
bool CvCity::IsOwedFoodBuilding() const
{
return m_bOwedFoodBuilding;
}
// --------------------------------------------------------------------------------
void CvCity::SetOwedFoodBuilding(bool bNewValue)
{
m_bOwedFoodBuilding = bNewValue;
}
#endif
In CvTeam.cpp
Code:
// Look at all Cities
for(pLoopCity = GET_PLAYER(eLoopPlayer).firstCity(&iLoop); pLoopCity != NULL; pLoopCity = GET_PLAYER(eLoopPlayer).nextCity(&iLoop))
{
if (pLoopCity->IsOwedCultureBuilding())
{
[I]// omitted for brevity[/I]
}
#if defined(MOD_BUGFIX_FREE_FOOD_BUILDING)
if (pLoopCity->IsOwedFoodBuilding())
{
BuildingTypes eFreeFoodBuilding = pLoopCity->ChooseFreeFoodBuilding();
if (eFreeFoodBuilding != NO_BUILDING)
{
pLoopCity->GetCityBuildings()->SetNumFreeBuilding(eFreeFoodBuilding, 1);
pLoopCity->SetOwedFoodBuilding(false);
}
}
#endif
}
In CvPlayer.cpp
Code:
int iNumFreeFoodBuildings = GetNumCitiesFreeFoodBuilding();
if(iNumFreeFoodBuildings > 0)
{
BuildingTypes eBuilding = pCity->ChooseFreeFoodBuilding();
if(eBuilding != NO_BUILDING)
{
pCity->GetCityBuildings()->SetNumFreeBuilding(eBuilding, 1);
}
#if defined(MOD_BUGFIX_FREE_FOOD_BUILDING)
else
{
pCity->SetOwedFoodBuilding(true);
}
#endif
ChangeNumCitiesFreeFoodBuilding(-1);
}
Code:
if(iNumCitiesFreeFoodBuilding > 0)
{
BuildingTypes eFoodBuilding = pLoopCity->ChooseFreeFoodBuilding();
if(eFoodBuilding != NO_BUILDING)
{
[I]// omitted for brevity[/I]
}
#if defined(MOD_BUGFIX_FREE_FOOD_BUILDING)
else
{
pLoopCity->SetOwedFoodBuilding(true);
}
#endif
// Decrement cities left to get free food building (at end of loop we'll set the remainder)
iNumCitiesFreeFoodBuilding--;
}
So our free food buildings now behave the same as our free culture buildings, you will get a unique replacement if appropriate and if you don't have the required technology or the total number of cities, the free food buildings will be handed out when you do. (Why the heck Firaxis didn't do it this way I'll never understand!)
Notice the problem in all that code? I start a game, I complete Tradition with only two cities - no free food buildings, but that's to be expected. I research Engineering and my two cities get their free food buildings - as expected. I found my third city - it gets its free food building as well. Looks like this works. I save the game and go to bed. Next day I reload my saved game, and shortly after found my fourth city - but no free food building. So what went wrong? The boolean that tells a city if it's owed a free food building is in memory, as long as I'm playing the same game that's OK, but the moment I save and exit it's gone. When I reload the game the flag defaults to false (from the constructor), so I don't get my outstanding free food buildings.
Now we could attempt to recalculate if cities are owed free food buildings - have we adopted a policy that grants them? do we have the required technology? have we handed out all the food buildings to the first N cities? ... but what about captured/razed/traded cities? Ouch! My head hurts! What we really need to do is store that flag into the saved game file and restore it when we reload the game (or in computer geek speak, we need to "persist" the data or even "serialize" it).
The persistence code is simple, in CvCity.cpp
Code:
kStream >> m_bOwedCultureBuilding;
#if defined(MOD_BUGFIX_FREE_FOOD_BUILDING)
kStream >> m_bOwedFoodBuilding;
#endif
Code:
kStream << m_bOwedCultureBuilding;
#if defined(MOD_BUGFIX_FREE_FOOD_BUILDING)
kStream << m_bOwedFoodBuilding;
#endif
Basically we just write it out, or read it back in, along with all the other city data. Just make sure you keep the sequencing when writing and reading the same!!!
However, we have just broken the saved game format. Any saved game you have that uses your modded DLL from before adding those two lines CANNOT be loaded by your newly updated DLL. This may not be a problem if your DLL is solely for your own personal usage, but if it's part of a mod with a user-base (eg a community based mod, or a total conversion mod with a lot of followers) your users are not going to be happy bunnies when they update to the latest and greatest version of your mod and can no longer finish their previous games - but at least they can down-grade your mod, unlike Firaxis' game breaking patches
There is a solution (based on the approach taken by Firaxis in solving the same problem), but it itself breaks the saved game format. To be effective it also has to be applied to every method that persists data - and there are 220 of those (110 reads and 110 writes) in 59 files. So, if you're not worried about being lynched by your user base you can probably stop reading now!
The basic approach is to write our own version number into the persisted data (we can't use the ones Firaxis add as we have no control over those). Every time we read the saved game data, if the version is before the value we are looking for came into existence, we use a default value, otherwise we read the value. Easier to code than describe!
Code:
void CvCity::read(FDataStream& kStream)
{
VALIDATE_OBJECT
// Init data before load
reset();
// Version number to maintain backwards compatibility
uint uiVersion;
kStream >> uiVersion;
#if defined(MOD_BUGFIX_FREE_FOOD_BUILDING)
uint uiDllSaveVersion;
kStream >> uiDllSaveVersion;
#endif
kStream >> m_iID;
[I]// omitted for brevity[/I]
kStream >> m_bOwedCultureBuilding;
#if defined(MOD_BUGFIX_FREE_FOOD_BUILDING)
if (uiDllSaveVersion >= 30)
{
kStream >> m_bOwedFoodBuilding;
}
else
{
m_bOwedFoodBuilding = false;
}
#endif
[I]// omitted for brevity[/I]
}
Code:
void CvCity::write(FDataStream& kStream) const
{
VALIDATE_OBJECT
// Current version number
uint uiVersion = 6;
kStream << uiVersion;
#if defined(MOD_BUGFIX_FREE_FOOD_BUILDING)
uint uiDllSaveVersion = MOD_DLL_VERSION_NUMBER;
kStream << uiDllSaveVersion;
#endif
kStream << m_iID;
[I]// omitted for brevity[/I]
kStream << m_bOwedCultureBuilding;
#if defined(MOD_BUGFIX_FREE_FOOD_BUILDING)
kStream << m_bOwedFoodBuilding;
#endif
[I]// omitted for brevity[/I]
}
For reasons known only to themselves, Firaxis use a unique version number - uiVersion (which has everything to do with being an Unsigned Int and nothing to do with the User Interface!!!) - for each persisted object. We'll use a single version number corresponding to the version of our DLL for every persisted object.
Code:
#define MOD_DLL_VERSION_NUMBER ((uint) 35)
Unfortunately the pair
Code:
uint uiDllSaveVersion;
kStream >> uiDllSaveVersion;
Code:
uint uiDllSaveVersion = MOD_DLL_VERSION_NUMBER;
kStream << uiDllSaveVersion;
We can simplify our work by using macros
Code:
// Serialization wrappers
#define MOD_SERIALIZE
#if defined(MOD_SERIALIZE)
#define MOD_SERIALIZE_INIT_READ(stream) uint uiDllSaveVersion; stream >> uiDllSaveVersion
#define MOD_SERIALIZE_READ(version, stream, member, def) if (uiDllSaveVersion >= version) { stream >> member; } else { member = def; }
#define MOD_SERIALIZE_INIT_WRITE(stream) uint uiDllSaveVersion = MOD_DLL_VERSION_NUMBER; stream << uiDllSaveVersion
#define MOD_SERIALIZE_WRITE(stream, member) CvAssert(uiDllSaveVersion == MOD_DLL_VERSION_NUMBER); stream << member
#else
#define MOD_SERIALIZE_INIT_READ(stream, member) __noop
#define MOD_SERIALIZE_READ(stream, member) __noop
#define MOD_SERIALIZE_INIT_WRITE(stream, member) __noop
#define MOD_SERIALIZE_WRITE(stream, member) __noop
#endif
Code:
void CvCity::read(FDataStream& kStream)
{
VALIDATE_OBJECT
// Init data before load
reset();
// Version number to maintain backwards compatibility
uint uiVersion;
kStream >> uiVersion;
[B]MOD_SERIALIZE_INIT_READ(kStream);[/B]
kStream >> m_iID;
[I]// omitted for brevity[/I]
kStream >> m_bOwedCultureBuilding;
#if defined(MOD_BUGFIX_FREE_FOOD_BUILDING)
[B]MOD_SERIALIZE_READ(30, kStream, m_bOwedFoodBuilding, false);[/B]
#endif
[I]// omitted for brevity[/I]
}
Code:
void CvCity::write(FDataStream& kStream) const
{
VALIDATE_OBJECT
// Current version number
uint uiVersion = 6;
kStream << uiVersion;
[B]MOD_SERIALIZE_INIT_WRITE(kStream);[/B]
kStream << m_iID;
[I]// omitted for brevity[/I]
kStream << m_bOwedCultureBuilding;
#if defined(MOD_BUGFIX_FREE_FOOD_BUILDING)
[B]MOD_SERIALIZE_WRITE(kStream, m_bOwedFoodBuilding);[/B]
#endif
[I]// omitted for brevity[/I]
}
Much neater and far less error prone. We can also disable all persistence code by commenting out the single #define MOD_SERIALIZE line. Note, the CvAssert() in the MOD_SERIALIZE_WRITE macro is only there to stop numerous compiler warnings about unused variables.
Now all you have to do is decide when you are going to break the saved game format for your mod and introduce all 110 occurrences of MOD_SERIALIZE_INIT_READ() and MOD_SERIALIZE_INIT_WRITE() into those 59 files!
[END]