[DLL/C++] Adding GameEvents

whoward69

DLL Minion
Joined
May 30, 2011
Messages
8,699
Location
Near Portsmouth, UK
The aim of this tutorial is to show you how to add your own GameEvents to enable the C++ DLL to notify Lua of actions or to defer decisions for the modder to implement in Lua.

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. Knowledge of Lua and how to use (some of) the standard Civ 5 GameEvents - specifically how to implement and add a listener function
  2. 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)
  3. Successfully built, deployed and loaded the unaltered source as a custom DLL mod
  4. Created your own custom header file and added it to the CvGameCoreDLLPCH.h file

There are four types of GameEvent that we can add - Hook, TestAll, TestAny and Accumulator - and this tutorial gives examples of how to add and use all four types.

But before we start, a word of warning. If you consider C++ to be the jet-engined Gulfstream, then Lua is the turbo-prop Cessna. Both will get you there, but one will do it considerably quicker than the other. Just like changing aircraft, switching from C++ to Lua and vice-versa takes time, it doesn't really matter which plane you're on, you want to stay on it rather than change. In programming terms, you do not want to liberally sprinkle GameEvents into C++ code just for the sake of it, and you certainly don't want to do it in the middle of loops or code that is frequently executed per turn (a prime example is the path finding code - events in here will be triggered hundreds, possibly thousands, of times per player per turn).

As an example of a "Hook" GameEvent (one which is sent from C++ to inform Lua that something has happened) we'll add code to notify when a Major Civ's friendship status with a City State changes. The code is in CvMinorCivAI:: DoFriendshipChangeEffects()

Code:
    // Add Friends Bonus
    if(!bWasAboveFriendsThreshold && bNowAboveFriendsThreshold)
    {
        bAdd = true;
        bFriends = true;
        // Send a GameEvent Hook here to tell any interested Lua code that we have a new friend
    }
    // Remove Friends bonus
    else if(bWasAboveFriendsThreshold && !bNowAboveFriendsThreshold)
    {
        bAdd = false;
        bFriends = true;
        // Send a GameEvent Hook here to tell any interested Lua code that we have lost a friend
    }

Now we could send two different events "MinorFriendsGained" and "MinorFriendsLost" or we could just use the same one "MinorFriendsChanged" with different parameters. We'll take the latter approach; both to keep the number of named events down and also the modder may not care what the change was, just that it changed. Now we need to decide on what information (parameters) we're going to send to Lua. This is a fine line to walk between not enough information and too much information - not enough and we'll spend a lot of time in the Lua code calculating values or retrieving them from C++ via the Lua API. Too much information and we'll waste time passing junk to Lua that'll rarely be needed. (Back at the airport ... we're going to get to the beach a lot faster if we only have to pick up one bag, unless we forgot to pack swim-wear and sun-glasses, in which case we'll be spending our first day shopping!) Obviously we need to pass the major civ and the city state for whom the relationship has changed. However, as we can't pass C++ objects to Lua, this information has to be as their integer player IDs. We'll also need a flag (boolean) to indicate if we've gained or lost this relationship, and, as we have the data to hand in the C++ method and it may be useful to the modder, we'll also pass the old and new friendship values. So we have five values to pass, but in what order? Looking at the standard GameEvents, Firaxis always pass the major player ID (if required) first. Other information is then typically passed with other ids (city/unit/minors) before any other data and then that is passed in the order it is most likely to be needed (as this allows us to write the Lua function with an incomplete parameter list, only including those parameters we will actually be using). So the most sensible order (to me, others may disagree, but hey I'm adding this event) is iPlayer, iCS, bIsfriend, iOldFriendship, iNewFriendship.

Design decisions made, now for the code. Firaxis use a standard pattern for calling game events, so we'll stick to it. Looking at any other Hook event we can see the pattern is

Code:
    ICvEngineScriptSystem1* pkScriptSystem = gDLL->GetScriptSystem();
    if (pkScriptSystem)
    {
        CvLuaArgsHandle args;
        // Push parameters here

        bool bResult;
        LuaSupport::CallHook(pkScriptSystem, "" /* GameEvent name here */, args.get(), bResult);
    }

So our code becomes

Code:
    // Add Friends Bonus
    if(!bWasAboveFriendsThreshold && bNowAboveFriendsThreshold)
    {
        bAdd = true;
        bFriends = true;

#if defined(WH_EVENT_MINOR_FRIENDS_CHANGED)
        ICvEngineScriptSystem1* pkScriptSystem = gDLL->GetScriptSystem();
        if (pkScriptSystem)
        {
            CvLuaArgsHandle args;
            args->Push(m_pPlayer->GetID());
            args->Push(ePlayer);
            args->Push(true);  // TRUE as we gained the friendship
            args->Push(iOldFriendship);
            args->Push(iNewFriendship);

            bool bResult;
            LuaSupport::CallHook(pkScriptSystem, "MinorFriendsChanged", args.get(), bResult);
        }
#endif
    }
    // Remove Friends bonus
    else if(bWasAboveFriendsThreshold && !bNowAboveFriendsThreshold)
    {
        bAdd = false;
        bFriends = true;
        
#if defined(WH_EVENT_MINOR_FRIENDS_CHANGED)
        ICvEngineScriptSystem1* pkScriptSystem = gDLL->GetScriptSystem();
        if (pkScriptSystem)
        {
            CvLuaArgsHandle args;
            args->Push(m_pPlayer->GetID());
            args->Push(ePlayer);
            args->Push(false);  // FALSE as we lost the friendship
            args->Push(iOldFriendship);
            args->Push(iNewFriendship);

            bool bResult;
            LuaSupport::CallHook(pkScriptSystem, "MinorFriendsChanged", args.get(), bResult);
        }
#endif
    }

we can now write a Lua listener of the form

Code:
function OnMinorFriendsChanged(iPlayer, iCS, bIsFriends, iOldFriendship, iNewFriendship)
    print(string.format("Player %i %s City State %i as a friend (old=%i, new=%i)", iPlayer, iCS, (bIsFriends and "gained" or "lost"), iOldFriendship, iNewFriendship))
end
GameEvents.MinorFriendsChanged.Add(OnMinorFriendsChanged)

An obvious extension of the above code (left as an exercise for the reader) is to extend/complement the event to include change of ally status.


As an example of a TestAll GameEvent we'll allow Lua to veto a city's ability to acquire/buy a plot, for example to stop cities expanding into ocean tiles

The method we need to change is CvCity::GetBuyablePlotList(), specifically within the two X-Y loops. Ummm. An event within loops, is this a good idea? Cities don't expand that often and we'll only going to be checking the plots it could acquire/buy. At the start of the game that'll be quite a few, but we won't be doing much else, as the game progresses we'll have fewer to choose from. Sounds like an acceptable performance "hit" so we'll add the event. We'll call it "CityCanAcquirePlot" to stick with the standard naming convention for CityCanTrain, CityCanConstruct et al events, and like those we'll pass iPlayer and iCity as the first two parameters, as well as iPlotX and iPlotY.

That's the design, what about the standard Firaxis code pattern - pretty similar to Hook events - the only real difference is actually using the returned value

Code:
    ICvEngineScriptSystem1* pkScriptSystem = gDLL->GetScriptSystem();
    if (pkScriptSystem)
    {
        CvLuaArgsHandle args;
        // Push parameters here

        bool bResult = false;
        if (LuaSupport::CallTestAll(pkScriptSystem, "" /* GameEvent name here */, args.get(), bResult))
        {
            if (bResult == false)
            {
                return false;
            }
        }
    }

So our code is

Code:
    for (iDX = -iMaxRange; iDX <= iMaxRange; iDX++)
    {
        for (iDY = -iMaxRange; iDY <= iMaxRange; iDY++)
        {
            pLoopPlot = plotXYWithRangeCheck(getX(), getY(), iDX, iDY, iMaxRange);
            if (pLoopPlot != NULL)
            {
                if (pLoopPlot->getOwner() != NO_PLAYER)
                {
                    continue;
                }

#if defined(WH_EVENT_CITY_CAN_ACQUIRE_PLOT)
                  ICvEngineScriptSystem1* pkScriptSystem = gDLL->GetScriptSystem();
                if (pkScriptSystem)
                {
                    CvLuaArgsHandle args;
                    args->Push(getOwner());
                    args->Push(GetID());
                    args->Push(pLoopPlot->getX());
                    args->Push(pLoopPlot->getY());

                    bool bResult = false;
                    if (LuaSupport::CallTestAll(pkScriptSystem, "CityCanAcquirePlot", args.get(), bResult))
                    {
                        if (bResult == false)
                        {
                            continue;
                        }
                    }
                }
#endif
                
                [I]// code omitted for brevity[/I]
            }
        }
    }

Note the variation from the standard pattern as we need to continue the loop and not return from the method if we can't acquire the plot! Note also that we place the event after the simple tests (does anyone else own this plot) but before we start doing complex calculations based on plot distance from the city and what resources/features it contains.

We can now write a Lua listener to implement a 12-mile limit

Code:
function OnCityCanAcquirePlot(iPlayer, iCity, iPlotX, iPlotY)
    // We don't care which city, only if the plot is water not adjacent to land
    local pPlot = Map.GetPlot(iPlotX, iPlotY)
  
    if (pPlot:IsWater() and not pPlot:IsAdjacentToLand()) then
        return false
    end
  
    return true
end
GameEvents.CityCanAcquirePlot.Add(OnCityCanAcquirePlot)

So what's the difference between a TestAll and a TestAny event? At the technical level TestAll is an AND while TestAny is an OR - but what's that mean for modders and their mods? For a TestAll event, the default state is usually "yes we can do that", for example, can a city construct a building or train a unit, and we then only need one mod to say "No" to stop it happening. For a TestAny event, the default state is usually "no you can't do that", for example, can a special city be razed, and then we only need one mod to say "Yes" for it to be possible. And that's the example we are going to use - razing capital and holy cities. (And in the process I'll explain how Firaxis botched their implementation of the event in the latest patch!)

The code controlling if a player can raze a city is in CvPlayer::canRaze() and we need to insert a TestAny event just after the CanRazeOverride event. We'll call our event "PlayerCanRaze" following the naming convention of the PlayerCanTrain, PlayerCanConstruct, et al series of events, and we need to pass iPlayer and iCity to the event. There is no standard pattern for a TestAny event (as Firaxis don't use them) but it would be similar to the TestAll event except for the actual LuaSupport method called and the checking of bResult being true (not false), so our code will be

Code:
#if defined(WH_EVENT_PLAYER_CAN_RAZE)
    ICvEngineScriptSystem1* pkScriptSystem = gDLL->GetScriptSystem();
    if (pkScriptSystem)
    {
        CvLuaArgsHandle args;
        args->Push(pCity->getOwner());
        args->Push(pCity->GetID());

        bool bResult = false;
        if (LuaSupport::CallTestAny(pkScriptSystem, "PlayerCanRaze", args.get(), bResult))
        {
            if (bResult == true)
            {
                return true;
            }
        }
    }
#endif

We can now write a Lua listener to permit any city to be razed

Code:
function OnPlayerCanRaze(iPlayer, iCity)
    // We don't care what the city is, let the player raze it if they want to
    return true
end
GameEvents.PlayerCanRaze.Add(OnPlayerCanRaze)

So what's the problem with the Firaxis CanRazeOverride event. If you're only considering events being hooked by one mod (as is the case with all DLC scenarios) then nothing as TestAll and TestAny behave identically in that specific case. However if you are considering the possibility of multiple mods providing functionality then the PlayerCanRaze event only works if every mod replicates every other enabled mods functionality.

Consider the following two listeners

Code:
function OnPlayerCanRaze(iPlayer, iCity)
    local pCity = Players[iPlayer]:GetCityByID(iCity)
    
    // We'll let the player raze any city (including capitals) except holy cities
    return (pCity:IsHolyCityAnyReligion() == false)
end
GameEvents.PlayerCanRaze.Add(OnPlayerCanRaze)

Code:
function OnPlayerCanRaze(iPlayer, iCity)
    local pCity = Players[iPlayer]:GetCityByID(iCity)
    
    // We'll let players raze holy cites if they could otherwise raze the city
    return (pCity:IsHolyCityAnyReligion() == true)
end
GameEvents.PlayerCanRaze.Add(OnPlayerCanRaze)

The combination of these two mods should be that we can raze any city. The standard code allows us to raze any non-capital, non-holy city. The first listener allows us to raze any capital that isn't also a holy city, while the second listener allows us to raze any holy city even if it's a capital. However, if you hook these up to a TestAll, they will cancel each other out (basically pCity:IsHolyCityAnyReligion() can never return both true and false for the same city), whereas if you hook them up to a TestAny, they will co-operate as expected. QED the Firaxis implementation is botched for anything except their specific requirements for scenarios.

Whereas TestAll and TestAny permit the C++ code to get a boolean from Lua, the Accumulator game event enables Lua to return any numeric value - integer or real (but the C++ code does need to know in advance what to expect). Well use the integer version to permit Lua to over-ride the next tech AI players will choose to research.

The code we need to modify is in CvPlayerAI::AI_chooseFreeTech() and also in CvPlayerAI::AI_chooseResearch(), we'll use the same event ("AiOverrideChooseNextTech") and in addition to the iPlayer value we'll pass a boolean (bFree) to indicate the context - presumably if it's a free tech we'll just go for the most expensive one whereas if we're picking what to research next we'll take unlocked units, wonders, improvements, etc into consideration. Or perhaps we have some "bee-lining" code so will just pick on a pre-determined path and not care about the context.

The standard code pattern for an (integer) Accumulator event is

Code:
    ICvEngineScriptSystem1* pkScriptSystem = gDLL->GetScriptSystem();
    if (pkScriptSystem)
    {
        CvLuaArgsHandle args;
        // Push parameters here

        int iValue = 0;
        if (LuaSupport::CallAccumulator(pkScriptSystem, "" /* GameEvent name here */, args.get(), iValue))
        {
            // Do something with iValue here;
        }
    }

Now, we could just assume that the Lua code is going to return a sensible value, but if it doesn't there's a good chance the DLL will crash, so we MUST validate the value before using it. Our code is

Code:
#if defined(WH_)
    ICvEngineScriptSystem1* pkScriptSystem = gDLL->GetScriptSystem();
    if (pkScriptSystem)
    {
        CvLuaArgsHandle args;
        args->Push(GetID());
        args->Push(bFree);

        int iValue = 0;
        if (LuaSupport::CallAccumulator(pkScriptSystem, "AiOverrideChooseNextTech", args.get(), iValue))
        {
            // Defend against modder stupidity!
            // MUST be a valid tech ID and for a tech we've not already researched
            if (iValue >= 0 && iValue < GC.getNumTechInfos() && !GET_TEAM(getTeam()).GetTeamTechs()->HasTech((TechTypes) iValue)) {
                eBestTech = (TechTypes)iValue;
            }
        }
    }
#endif

If you're wondering where you need to insert this code block (twice), just search for the Firaxis comments "//todo: script override" (you'll need to set bFree as appropriate for each block)

[END]
 
Before writing your own GameEvents, check to see if they're already in my DLL - source code for which can be obtained from GitHub. Search for GAMEEVENT_ in the CustomMods.h file to locate them all.

Related tutorials (in order of complexity)
 
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