Get Started With Lua Events Lesson 7: Saving Data in the State Table

From Scenario League Wiki

Jump to: navigation, search

Back to Get Started With Lua Events

Contents

Introduction

Thus far in these lessons we have used Lua to alter the state of the active civilization game. That is, we can create or delete units via events, grant or remove technologies, make adjustments to cities and so forth. However, what we have not done is to keep a record of previous events in order to change how events execute. In this respect, the old macro events system can still do things that we can't, by using functionality such as flags and the justonce modifier. This lesson will remedy the deficiency.

You can get the events for the start of lesson 7 here. (I provided them at the end of lesson 6.)

The 'state' Table

The Lua Events system allows us to store extra information in a saved game and retrieve it later. The information that we intend to store and retrieve is kept in a table, which is, by convention, named state, and usually referred to as the "state table." The state table got its name from an example events.lua file provided by The Nameless One, but you can change the name if you really want to. It is probably best to follow convention, however, especially if you are using code written by others.

The code to save the state table information to a file is given by:

civ.scen.onSave(function () return civlua.serialize(state) end) 

civlua.serialize(state) turns the state table into a string, and the event trigger civ.scen.onSave appends that string to the end of the saved game file. To get that information back,

civ.scen.onLoad(function (buffer) state = civlua.unserialize(buffer) end)

GetStartedWithLuaLesson7-01-SaveAndLoad.jpg

is used to turn the "buffer" back into a table, named state, when a game is loaded.

Counting Unit Kills

In the old macro system, counting things is rather laborious. The following is macro code from an American Civil War scenario that requires 5 Confederate CSA Infantry units to be killed in order to issue the Emancipation Proclamation.

@IF
TURN
turn=12
@THEN
Flag
Continuous
Who=Union
State=On
Flag=12 ; Activates Antietam Unit KIA Counter
JUSTONCE
TEXT
LINCOLN REQUIRES MILTIARY SUCCESS ON THE BATTLEFIELD TO AVERT EUROPEAN INTERVENTION!
^
As the Commander-in-chief, you need to demonstrate to the European powers that you are
fighting for more then the preservation of the Union but for a higher moral cause, i.e. the 
abolition of slavery on the continent through the Emancipation Proclamation.
^
But you cannot emit the Proclamation without gaining a victory on the battlefield and 
thereby demonstrating that your ability to emancipate the slaves is backed by more than 
words...
ENDTEXT
@ENDIF

@IF
TURN
turn=12
@THEN
TEXT
... As such you have till the end of September 1862 to defeat 5 CSA Infantry units. Failure to 
do so will prevent you from emitting the Proclamation in a timely manner and bring France and 
England on the side of the Confederacy. 
ENDTEXT
@ENDIF

@IF
UNITKILLED
unit=CSA Infantry
attacker=Union
defender=Confederates
@AND
CheckFlag
Who=Union
Flag=12 ; 
State=On
@THEN
Flag
Continuous
Who=Union
State=On
Flag=13 ; 
JUSTONCE
TEXT
1st CSA Infantry killed!
ENDTEXT
@ENDIF

@IF
UNITKILLED
unit=CSA Infantry
attacker=Union
defender=Confederates
@AND
CheckFlag
Who=Union
Flag=13 ; 1st CSA Infantry Killed
State=On
@THEN
Flag
Continuous
Who=Union
State=On
Flag=14 ; 
JUSTONCE
TEXT
2nd CSA Infantry killed!
ENDTEXT
@ENDIF

@IF
UNITKILLED
unit=CSA Infantry
attacker=Union
defender=Confederates
@AND
CheckFlag
Who=Union
Flag=14 ; 2nd CSA Infantry Killed
State=On
@THEN
Flag
Continuous
Who=Union
State=On
Flag=15 ; 
JUSTONCE
TEXT
3rd CSA Infantry killed!
ENDTEXT
@ENDIF

@IF
UNITKILLED
unit=CSA Infantry
attacker=Union
defender=Confederates
@AND
CheckFlag
Who=Union
Flag=15 ; 3rd CSA Infantry Killed
State=On
@THEN
Flag
Continuous
Who=Union
State=On
Flag=16 ; 
JUSTONCE
TEXT
4th CSA Infantry killed!
ENDTEXT
@ENDIF

@IF
UNITKILLED
unit=CSA Infantry
attacker=Union
defender=Confederates
@AND
CheckFlag
Who=Union
Flag=16 ; 4th CSA Infantry Killed
State=On
@THEN
Flag
Continuous
Who=Union
State=On
Flag=17 ; Emancipation Proclamation Given
JUSTONCE
TEXT
5th CSA Infantry killed!
ENDTEXT
@ENDIF

@IF
CheckFlag
Who=Union
Flag=17 ; Union kills 5th CSA Infantry starting January 1862
State=On
@THEN
GIVETECHNOLOGY
technology=82
receiver=Union
JUSTONCE
TEXT
EMANCIPATION PROCLAMATION ISSUED!
^
President Abraham Lincoln issues the Emancipation Proclamation, which declares the 
freedom of all slaves in any state of the Confederate States of America.
ENDTEXT
@ENDIF

With Lua, we can do much better. Let us write an event that requires the Romans to kill some Celtic chariots, will display a message with the current tally when one is killed, and will display a message when an adequate number are killed, after which no more messages will be displayed.

A state table has already been in our events.lua file since the beginning of these lessons, along with a justOnce function (which we'll discuss later).

GetStartedWithLuaLesson7-02-StateJustOnce.jpg

We'll store the count of Celtic chariots killed in the key "celticChariotsKilledByRomans". We should initialize this value to 0 by using

state.celticChariotsKilledByRomans = state.celticChariotsKilledByRomans or 0

This means that if there is no value associated with the key "celticChariotsKilledByRomans" then the key is created and the value set to 0. If there already is a value, then that value is preserved.

We need to make this initialization in 2 places. The first is in the main body of our events.lua script, which is to say not in any particular function or table in the script. Just after the state table is created is a good idea.

The second place is in the function run by civ.scen.onLoad, after the state table is created from the buffer.

GetStartedWithLuaLesson7-03-initializeChariotCounter.jpg

It is important to make the initialization at both places. If the game has never been saved (or not saved with the state table), then the only way to have a state table is to initialize it in the main body of the events.lua. If, however, the game has been saved, onLoad (which will run after the code in events.lua is run) will replace the state table created by events.lua, and so render invalid any initializations done in the body of the events. This means that if you change the state initialization after having saved the game (such as if you add a new state entry during construction of your scenario), that entry will be erased when the state table is unserialized from the data stored in the saved game, so you will have to re-initialize it at least once. Just make the initialization in both places, and you won't have to worry about the meaning of this paragraph.

Let us make a parameter "chariotsToKill" and set it to 5. That means, we'll do something after the fifth chariot is killed.

In doWhenUnitKilled, when a Celtic chariot is killed by a Roman unit, we'll show a message with the number of chariots killed, and a different message when the threshold is reached.

local function doWhenUnitKilled(loser,winner)
    --[[loser.owner.money = math.max(loser.owner.money -campaignCost,0)
    winner.owner.money = math.max(winner.owner.money-campaignCost,0)]]
	if loser.type == unitAliases.chariot and loser.owner == tribeAliases.celts 
		and winner.owner == tribeAliases.romans 
		and state.celticChariotsKilledByRomans < parameters.chariotsToKill then
		state.celticChariotsKilledByRomans = 1 + state.celticChariotsKilledByRomans
		if state.celticChariotsKilledByRomans < parameters.chariotsToKill then
			civ.ui.text("The "..tribeAliases.romans.name.." have now defeated "..
				tostring(state.celticChariotsKilledByRomans).." "..tribeAliases.celts.adjective
				.." "..unitAliases.chariot.name.."s.")
		elseif state.celticChariotsKilledByRomans == parameters.chariotsToKill then
			civ.ui.text("The "..tribeAliases.romans.name.." have now defeated "..
				tostring(parameters.chariotsToKill).." "..tribeAliases.celts.adjective
				.." "..unitAliases.chariot.name.."s.  Something good happens.")
		end
	end
-- more code
end

GetStartedWithLuaLesson7-04-ChariotCounterCode.jpg

There isn't much to say about this code. The "outer" if statement checks if the unit that was killed was a chariot, if the unit was owned by the Celts, if the winning unit was owned by the Romans, and if the number of chariots killed is less than 5 (or whatever the parameter is set to).

If so, then we add 1 to the total of chariots killed. If the total is still less than 5, we display a message with the current number of chariots killed. If the last chariot killed brought the total to 5, a different message is displayed. We could get fancier and have a separate message for the first chariot killed, or whatever.

Note that by using the name and adjective of the tribes and unit types, we can make changes to the names of things without having to go back to our events and rewrite all the text.

Test this code, and make sure it works, even after saving and loading.

Just Once

Now let us take a look at justOnce(key,f).

Fundamentally, justOnce works by checking if key is already in the state table. If it is not, then the function f is executed (with no arguments), and key is added to the state table with the value true. If key is already in the state table, then f is not executed.

What this means is that you have to choose your keys so that they don't conflict with any other key in the state table (either for other justOnce instances or for other things in the state table, such as counter values), unless you specifically want them to. For example, you might have event A and event B, and you want only one of them to happen, and to happen only once. By giving them the same key for justOnce, you will ensure that only one of them happens.

Let us now take a look at how justOnce is implemented. In the civlua module, we have this code

-- Wraps getting and setting of key `n` in table `t`.
local function property(t, n)
  return {get = function () return t[n] end,
          set = function (v) t[n] = v end}
end

-- When property `guard` is false, runs `f` and sets `guard` to true, otherwise does nothing. 
local function justOnce(guard, f)
  if not guard.get() then
    f()
    guard.set(true)
  end
end

And in events.lua we have:

local justOnce = function (key, f) civlua.justOnce(civlua.property(state, key), f) end

GetStartedWithLuaLesson7-05-JustOnce.jpg

The function civlua.property(t,n) returns two functions, one that gets the value of t[n] and the other that sets t[n]=v for a value v given as an argument to the function.

The function civlua.justOnce(guard,f) takes a table {get=function,set=function(v)} and a function f. If guard.get is nil or false, then the function f is executed and guard.set(true) is run. Basically, the property function returns an appropriate table for civlua.justOnce to use as the guard entry.

Finally, the justOnce function defined in events.lua "wraps" civlua.justOnce. At every instance of justOnce, we need to specify the key and function f, but we always want to use the same table, the state table. So, civlua.property(state,key) returns an appropriate guard value for civlua.justOnce every time, and then civlua.justOnce can be run with the correct guard and function.

I will not provide an example of the usage of justOnce. There should be enough information here for you to figure out how to use it, and you won't always find documentation with examples on how to use code. If you have trouble, ask in the forums.

Flags and Accessing State from Modules

When we counted chariot kills, we saw how to define and use our own counter in the state table. We could have made a "flag" by setting true or false values instead. While we certainly can write our events by directly manipulating and accessing the state table to use flags and counters, it will be more convenient to define some functions to do this for us. The scenario Over The Reich has the functions for flags and counters defined directly in the events.lua file, if you want to have a look. For this lesson, however, we're going to define a flags module that can be used from any file.

This raises a question: how can a function in our flag module make changes to the state table, which is local to events.lua? One solution is to make state global (by removing the 'local' when it is defined). That way, any module can access the state table by simply writing state. This has a couple downsides. One is that this forces "state" to be the only name for the state table. This is probably not a huge problem. The other reason is that by giving every module direct access to the state table, you run into the problem that multiple modules might try to use the same key values without realizing it. Of course, it might be useful to make state global if you've split your code into multiple files anyway, setting aside modules written by others.

So, what can we do? The first thing to realize is that tables are fundamentally global. Any part of the program can access a table as long as it has the "name" of the table. If I write local myTable={}, what is stored in the variable myTable is the "name" of the table, not all the key-value pairs of the table. The variable myTable just tells the program where to find the table in memory. If we can provide the value in myTable to another part of the program, then that part of the program can access myTable, even though it was defined locally.

Of course, if we simply write

local state= state or {}
globalState=state

then all we have done is to allow the writer of events.lua to use a different name for the state table, without any other benefit. However, we can do something else:

local state= state or {}
state.moduleOneState = state.moduleOneState or {}
globalModuleOneState = state.moduleOneState

What this does is create a new table in the state table at the key "moduleOneState", unless something already exists at that key, and that table is provided to moduleOne via the global variable globalModuleOneState. Then, to prevent conflicts, the scenario designer only has to make sure that the key "moduleOneState" is not used for any other purpose in the state table, and that globalModuleOneState is not used anywhere other than in ModuleOne.

However, if we like, we can be even cleverer. Consider this module, flags:

local flagState = "flagState not linked"
-- the module name for flags if moduleName is not specified
-- moduleName allows the use of flags in other modules or libraries
-- without worrying about name conflicts
local defaultModuleName = "events.lua"

-- a table of flag keys and their initialized values
local initializeFlagList = {}

-- table of flag functions to be returned with the module
local flag = {}

local function linkState(tableInStateTable)
    if type(tableInStateTable) == "table" then
        flagState = tableInStateTable
    else
        error("linkState: linkState takes a table as an argument.")
    end
end

flag.linkState = linkState

-- Specifies a key for a flag, and that flag's initial value.
-- optional parameter moduleName prevents name conflicts if
-- flags are used in a module or library
local function define(key,initialValue,moduleName)
    if type(initialValue) ~= "boolean" then
        error("A flag in the flag module must have an initial value of true or false.")
    end
    if type(key) ~= "string" then
        error("The flag module only allows strings for flag names.  Consider using the function tostring to make the conversion if necessary.")
    end
    -- the moduleName is prefixed to prevent conflicts in different modules
    -- define is only meant to be used in actual scenarios, not in library modules
    moduleName = moduleName or defaultModuleName
    -- if this key hasn't been used for a flag yet, then
    if initializeFlagList[moduleName..key] == nil then
        initializeFlagList[moduleName..key] = initialValue
    else
        error("The key "..tostring(key).." has already been used for a flag.")
    end
end
flag.define = define

-- makes sure all flags are in the state table
-- if a flag is not in the state table, it is inserted
-- and initialized to the correct value
local function initializeFlags()
    if type(flagState) == "string" and flagState == "flagState not linked" then
        error("Use the function linkState before trying to initialize flags")
    end
    for key,initialValue in pairs(initializeFlagList) do
        if flagState[key] == nil then
            flagState[key] = initialValue
        end
    end
end
flag.initializeFlags = initializeFlags

-- returns the current state (i.e. true or false) of the flag with the key
-- optional parameter moduleName prevents name conflicts if
-- flags are used in a module or library
local function value(key,moduleName)
    moduleName = moduleName or defaultModuleName
    if flagState[moduleName..key] == nil then
        error("The key "..key.." has not been initialized.  Check your flag definitions and "..
        "that the initializeFlags function has been executed.")
    end
    return flagState[moduleName..key]
end
flag.value = value

-- sets the flag's value to true
-- optional parameter moduleName prevents name conflicts if
-- flags are used in a module or library
local function setTrue(key,moduleName)
    moduleName=moduleName or defaultModuleName
    if flagState[moduleName..key] == nil then
        error("The key "..key.." has not been initialized.  Check your flag definitions and "..
        "that the initializeFlags function has been executed.")
    else
        flagState[moduleName..key] = true
    end
end
flag.setTrue = setTrue

-- sets the flag's value to false
-- optional parameter moduleName prevents name conflicts if
-- flags are used in a module or library
local function setFalse(key,moduleName)
    moduleName=moduleName or defaultModuleName
    if flagState[moduleName..key] == nil then
        error("The key "..key.." has not been initialized.  Check your flag definitions and "..
        "that the initializeFlags function has been executed.")
    else
        flagState[moduleName..key] = false
    end
end
flag.setFalse = setFalse

-- changes the flag's value from true to false or false to true
-- optional parameter moduleName prevents name conflicts if
-- flags are used in a module or library
local function toggle(key,moduleName)
    moduleName=moduleName or defaultModuleName
    if flagState[moduleName..key] == nil then
        error("The key "..key.." has not been initialized.  Check your flag definitions and "..
        "that the initializeFlags function has been executed.")
    else
        flagState[moduleName..key] = not(flagState[moduleName..key])
    end
end
flag.toggle = toggle

return flag


Let's first look at this part

local flagState = "flagState not linked"
-- the module name for flags if moduleName is not specified
-- moduleName allows the use of flags in other modules or libraries
-- without worrying about name conflicts
local defaultModuleName = "events.lua"

-- a table of flag keys and their initialized values
local initializeFlagList = {}

-- table of flag functions to be returned with the module
local flag = {}

GetStartedWithLuaLesson7-06-Flags1.jpg

The variables flag is a table to hold the functions this module will provide, and is not very interesting. Default module name is just a substitute for the string "events.lua", so that it is only defined in one place (on the off chance that we might want to change it).

The variables flagState and initializeFlagList are more interesting to us. Although they are local variables within this module, the functions within flags.lua will treat them as global variables. And these are not just constants that we've defined in one place for consistency and ease of making changes; we're actually going to change them as the program runs.

Recall the following code in lesson 2

function buggy(input)
    j = 3
    print(input)
end
print(j)
j = 7
buggy(8)
print(j) 

This code prints

nil
8
3

Since the variable j is changed to 3 whenever buggy is run. We are going to do that on purpose to the variable flagState.

When the module is first loaded, flagState is set to the string "flagState not linked". This is simply so that we have an easy error check.

Now, consider the next function that was written

local function linkState(tableInStateTable)
    if type(tableInStateTable) == "table" then
        flagState = tableInStateTable
    else
        error("linkState: linkState takes a table as an argument.")
    end
end

flag.linkState = linkState

GetStartedWithLuaLesson7-07-Flags2.jpg

This function takes the "name" of a table as input, and stores that name in the flagState variable, so that the table can be accessed by other functions in this module. If the state table is passed to this function, or, preferably, a table stored as a value in the state table, then all the functions in this module can write to the state table, or at least the part of the state table passed as an argument to linkState. Hence, we have achieved access to the state table without defining a global variable or by requiring that the state table be passed as an argument for every single function provided.

Now, look at

-- Specifies a key for a flag, and that flag's initial value.
-- optional parameter moduleName prevents name conflicts if
-- flags are used in a module or library
local function define(key,initialValue,moduleName)
    if type(initialValue) ~= "boolean" then
        error("A flag in the flag module must have an initial value of true or false.")
    end
    if type(key) ~= "string" then
        error("The flag module only allows strings for flag names.  Consider using the function tostring to make the conversion if necessary.")
    end
    -- the moduleName is prefixed to prevent conflicts in different modules
    -- define is only meant to be used in actual scenarios, not in library modules
    moduleName = moduleName or defaultModuleName
    -- if this key hasn't been used for a flag yet, then
    if initializeFlagList[moduleName..key] == nil then
        initializeFlagList[moduleName..key] = initialValue
    else
        error("The key "..tostring(key).." has already been used for a flag.")
    end
end
flag.define = define

GetStartedWithLuaLesson7-08-Flags3.jpg

Much of this function is error checking, to make sure that appropriate values are given. The name of the flag (or the key) must be a string, and the initial value for the flag must be a boolean.

The moduleName variable is optional. If it is nil (which is the same as not being provided at all), then defaultModuleName is used. Basically, this allows a someone writing a module to use the flag library without worrying about writing flag names that will conflict with the flag names written by some scenario designer. Since the "true" key is the moduleName prefixed to the actual key, as long as moduleName is different in each module (and none are "events.lua"), there will be no conflict. This way, the scenario designer doesn't have to worry about the name at all, and module writers just have to add an extra argument to their function calls, and everything will work well "under the hood".

The line

    if initializeFlagList[moduleName..key] == nil then

checks to make sure that the flag key in question has not already been used elsewhere. If it has, then its value will not be nil, and so an error can be given. Note that since false is a valid initialization, we must specify whether the value is nil or not.

If the key is not already in the table, it is added, with the initial value specified as the value. Each use of define permanently changes initializeFlagList, by adding a value to the table.

-- makes sure all flags are in the state table
-- if a flag is not in the state table, it is inserted
-- and initialized to the correct value
local function initializeFlags()
    if type(flagState) == "string" and flagState == "flagState not linked" then
        error("Use the function linkState before trying to initialize flags")
    end
    for key,initialValue in pairs(initializeFlagList) do
        if flagState[key] == nil then
            flagState[key] = initialValue
        end
    end
end
flag.initializeFlags = initializeFlags

GetStartedWithLuaLesson7-09-Flags4.jpg

Here, we first check that flagState has actually been replaced with a table. Next, we go through all the key-value pairs in initializeFlagList. If the key is not already in flagState, the key is added along with the initial value.

-- returns the current state (i.e. true or false) of the flag with the key
-- optional parameter moduleName prevents name conflicts if
-- flags are used in a module or library
local function value(key,moduleName)
    moduleName = moduleName or defaultModuleName
    if flagState[moduleName..key] == nil then
        error("The key "..key.." has not been initialized.  Check your flag definitions and "..
        "that the initializeFlags function has been executed.")
    end
    return flagState[moduleName..key]
end
flag.value = value

-- sets the flag's value to true
-- optional parameter moduleName prevents name conflicts if
-- flags are used in a module or library
local function setTrue(key,moduleName)
    moduleName=moduleName or defaultModuleName
    if flagState[moduleName..key] == nil then
        error("The key "..key.." has not been initialized.  Check your flag definitions and "..
        "that the initializeFlags function has been executed.")
    else
        flagState[moduleName..key] = true
    end
end
flag.setTrue = setTrue

-- sets the flag's value to false
-- optional parameter moduleName prevents name conflicts if
-- flags are used in a module or library
local function setFalse(key,moduleName)
    moduleName=moduleName or defaultModuleName
    if flagState[moduleName..key] == nil then
        error("The key "..key.." has not been initialized.  Check your flag definitions and "..
        "that the initializeFlags function has been executed.")
    else
        flagState[moduleName..key] = false
    end
end
flag.setFalse = setFalse

-- changes the flag's value from true to false or false to true
-- optional parameter moduleName prevents name conflicts if
-- flags are used in a module or library
local function toggle(key,moduleName)
    moduleName=moduleName or defaultModuleName
    if flagState[moduleName..key] == nil then
        error("The key "..key.." has not been initialized.  Check your flag definitions and "..
        "that the initializeFlags function has been executed.")
    else
        flagState[moduleName..key] = not(flagState[moduleName..key])
    end
end
flag.toggle = toggle

GetStartedWithLuaLesson7-10-Flags5.jpg

All of these functions simply provide access to the flagState table, adding the moduleName and producing an error if the flag doesn't exist.

Now, let us do some tests.

local flag = require("flags")
flag.define("warriorKilled",false)
flag.define("phalanxKilled",false)
state.flagState=state.flagState or {}
flag.linkState(state.flagState)
flag.initializeFlags()

And in onLoad

state.flagState=state.flagState or {}
flag.linkState(state.flagState)
flag.initializeFlags()

GetStartedWithLuaLesson7-11-FlagTest1.jpg

Add a warrior alias to classicRomeAliases, and the following code to doWhenUnitKilled

	if loser.type == unitAliases.warrior and not flag.value("warriorKilled") then
		civ.ui.text("First Warrior Killed")
		flag.setTrue("warriorKilled")
	end
	if loser.type == unitAliases.phalanx and not flag.value("phalanxKilled") then
		civ.ui.text("First Phalanx Killed")
		flag.setTrue("phalanxKilled")
	end

GetStartedWithLuaLesson7-12-FlagTest2.jpg

Test this by killing warriors and phalanxes (suicide attacks are effective here). Only the first one killed should show a message.

We'll make a little addendum to the plunder module, to test flags in separate modules.

flag = require("flags")
flag.define("warriorKilled",false,"plunder.lua")
local function firstLegion(loser)
	if loser.type == unitAliases.legion and 
	not flag.value("warriorKilled","plunder.lua") then
		civ.ui.text("First Legion Killed.")
		flag.setTrue("warriorKilled","plunder.lua")
	end
end
return {
firstLegion=firstLegion,

GetStartedWithLuaLesson7-13-FlagTest3.jpg

Add plunder.firstLegion(loser) to the doWhenUnitKilled function.

It should have the same effect as the warrior and phalanx events, but for legions. This is true even though we reused the "warriorKilled" string for a flag name. Note that to use flags in a module, the flag module must also be set up in events.lua.

After Production Events

With flag functionality, we can now introduce a new event trigger, the "afterProduction" trigger. This event trigger takes place after city production is complete for the turn, but before unit movement begins. This is not a built in event trigger from the civ.scen library, however. Instead, what we're going to do is use the civ.scen.onActivateUnit trigger to achieve it.

We'll make this trigger by running our "doAfterProduction" function the first time a unit is activated for a player during a turn. So that doAfterProduction is only run once per tribe per turn, we'll have flags that tell if the afterProduction event has been run. If it has, the onActivateUnit code will ignore it. Then, the flags are reset each turn.

We'll make a simple after production event

local function doAfterProduction(turn,tribe)
    civ.ui.text(tribe.name.." is the active tribe.")
    if tribe.isHuman then
        civ.ui.text(tribe.leader.name.." is a human.")
    end
end

In our flag definitions, we add the following lines:

for i=0,7 do
	flag.define("afterProductionForTribe"..tostring(i).."NotDone",true)
end

In our doOnActivation function, we add the lines

	if flag.value("afterProductionForTribe"..tostring(civ.getCurrentTribe().id).."NotDone") then
		doAfterProduction(civ.getTurn(),civ.getCurrentTribe())
		flag.setFalse("afterProductionForTribe"..tostring(civ.getCurrentTribe().id).."NotDone")
	end

Finally, in doThisOnTurn, we add the loop

	for i=0,7 do
		flag.setTrue("afterProductionForTribe"..tostring(i).."NotDone")
	end

This loop will reset the flags to true each turn, so that after production will run every turn.

GetStartedWithLuaLesson7-14-AfterProduction1.jpg GetStartedWithLuaLesson7-15-AfterProduction2.jpg

Now we try it out. One message should appear for each tribe every turn, but the other message should appear only for the human owned tribe.

Note that the after production event won't run if a player has no active units after city production. If this is a likely possibility for your scenario, you will have to account for that. For example, during the after production event, you could create a unit for the next tribe in a remote corner of the map, and delete the guaranteeing unit for the current tribe.

Barracks and Veteran Status

NOTE: It was discovered that you can simply create a new unit and delete the produced unit in order to avoid bestowing veteran status from a barracks. This will be much easier than saving stuff in tables. However, this example is still useful.

Back in Lesson 4, I mentioned that we couldn't remove veteran status from newly created units (bestowed by a barracks) because the veteran status was not applied until after the on production event was completed. However, now that we have the afterProduction functionality, we can make the change.

What we'll do is keep a list of all (ground) units produced in cities with barracks, and remove veteran status for those units after production. We won't bother checking if Sun Tzu is built or anything else.

The code in doOnCityProduciton is

	if civ.isUnit(prod) then
		if civ.hasImprovement(city,improvementAliases.barracks) and prod.type.domain == 0 then
			state.vetStatusToRemove = state.vetStatusToRemove or {}
			state.vetStatusToRemove[#state.vetStatusToRemove+1] = prod
		end

The code in doAfterProduction is

	state.vetStatusToRemove=state.vetStatusToRemove or {}
	for index,unit in pairs(state.vetStatusToRemove) do
		unit.veteran = false
		state.vetStatusToRemove[index] = nil
	end

GetStartedWithLuaLesson7-16-NoBarracksVet.jpg

Something to note is that before referring to state.vetStatusToRemove, we make sure that it exists, and create it if it doesn't. This ensures that there will not be any errors trying to index a nil value. We might want to make functions to add, check, and remove things from the state table (and, more particularly, any subtables in state) so that we don't have to write the checks that all the tables exist manually each time.

Try this code. It should work perfectly fine (except that it will remove the effect of Sun Tzu's from cities with barracks). Be sure to test that units produced in previous turns that have subsequently gained veteran status keep it, and test that sea units produced in a city with a port facility and barracks still retain the port facility advantage.

civ.scen.onLoad vs. civ.scen.onScenarioLoaded

Civ.scen.onLoad and civ.scen.onScenarioLoaded appear at first glance to do the same thing. When a game is loaded, they execute their code. So what's the difference? Well, the first difference is that the "buffer" that is attached to the end of a save game file (and which is used to re-create the state), is available to civ.scen.onLoad. The other difference is timing. civ.scen.onScenarioLoaded happens after the rules.txt information is accessed by the game, while onLoad happens before that. That means, if you are interested in changing some feature of the game based on the season (such as the road multiplier), you will want to do that in civ.scen.onScenarioLoaded. The code in events.lua (which is not part of a function definition) is executed before civ.scen.onLoad. This is how the various tables and other data are built for the scenario.

I mentioned the road multiplier in the last paragraph. When you did testing in the Classic Rome scenario, you may have noticed that roads granted unlimited movement. This is because when I converted the Rome scenario to Test of Time, I failed to include an @COSMIC2 line in the rules.txt, and due to a bug, TOTPP doesn't look at the @COSMIC value instead. During my live creation of the munition library, I inserted a line totpp.movementMultipliers.road=2, which counteracts the bug. As I mentioned last paragraph, if I wanted to override rules.txt (rather than replace an omission), I would have had to put the line in the function supplied to civ.scen.onScenarioLoaded.

Conclusion

This concludes the last fundamental lesson for writing Civilization II Scenario Events with Lua. I may, at some point, add some more material, but that will be to either re-visit stuff I mentioned in earlier lessons but then forgot about, or to add tips on how to do certain things or use certain functionality. For the latter case, I may simply decide to write an independent document instead.

Thank you for following these lessons and getting to the end. If there are any improvements you think could be made to these lessons, please either let me know or just make them yourself. My goal is to make it easy for others to use the tools lua provides for Civ II, and if a change would improve this product, then I'd like to make it. Feel free to redistribute and modify these lessons. I would appreciate credit (and a notice that changes were made to the text if applicable), and maybe a PM in the civfanatics forums of what you did, but these aren't really essential. After all, these lessons are only good for teaching Lua with Civilization II, so just about any use of them is likely to further that goal.

Good luck with your projects. Remember that you will gain confidence with practice and experience. Ask for help if and when you need it.

Personal tools