[DLL/C++] Persisting data with a saved game

whoward69

DLL Minion
Joined
May 30, 2011
Messages
8,713
Location
Near Portsmouth, UK
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
  1. Familiarity with #defines used as macros
  2. Knowledge of Lua and the Civ 5 objects, eg Player, City, Unit et al
  3. 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)
  4. The C/C++ stream operators >> and <<
  5. Successfully built, deployed and loaded the unaltered source as a custom DLL mod
  6. 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
and
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
and
Code:
    m_bOwedCultureBuilding = false;
#if defined(MOD_BUGFIX_FREE_FOOD_BUILDING)
    m_bOwedFoodBuilding = false;
#endif
and
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);
    }
and
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
and
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]
}
and
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;
and
Code:
    uint uiDllSaveVersion = MOD_DLL_VERSION_NUMBER;
    kStream << uiDllSaveVersion;
is required 110 times in 59 files - copy-paste typo nightmares!

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
and our changes above become

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]
}
and
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]
 
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