Get Started With Lua Events Lesson 6: Splitting Code into Multiple Files: Difference between revisions

From Scenario League Wiki
Jump to navigationJump to search
Line 132: Line 132:
If you plan to write some functionality for your scenario that others might find useful, it is worth spending a little extra effort to see if you can write that functionality in a form that will allow others to use it with minimal effort.  If you can, then a little extra work on your part might save someone else the trouble of re-inventing the wheel, and allow them to complete their project sooner, or to work on some new feature for you to enjoy.  Unfortunately, the Lua scenarios that have been completed at the time of writing haven't been that good at creating re-usable code, at least in part since we were all getting used to Lua.  This will hopefully be improved in the future.
If you plan to write some functionality for your scenario that others might find useful, it is worth spending a little extra effort to see if you can write that functionality in a form that will allow others to use it with minimal effort.  If you can, then a little extra work on your part might save someone else the trouble of re-inventing the wheel, and allow them to complete their project sooner, or to work on some new feature for you to enjoy.  Unfortunately, the Lua scenarios that have been completed at the time of writing haven't been that good at creating re-usable code, at least in part since we were all getting used to Lua.  This will hopefully be improved in the future.


For now, we're going to look at a library which is called the General Library (unless someone comes up with a better name) and which can be found [[here]].  The purpose of this module is to provide relatively simple functionality that probably shouldn't be implemented every time it is needed.  This is useful for our purposes, since we can look at relatively self contained and simple functions for our first examples.  We'll create a library for generating munitions later.
For now, we're going to look at a library which is called the General Library (unless someone comes up with a better name) and which can be found [https://forums.civfanatics.com/threads/totpp-the-general-library.647583/ here].  The purpose of this module is to provide relatively simple functionality that probably shouldn't be implemented every time it is needed.  This is useful for our purposes, since we can look at relatively self contained and simple functions for our first examples.  We'll create a library for generating munitions later.


This library would be used by invoking the line  
This library would be used by invoking the line  
Line 193: Line 193:
Let us begin by looking at the function <code>toTile</code>.  The purpose of this function is to allow other functions to accept tile coordinates in a table instead of an actual tile object.  The function tries to be reasonably permissive in the coordinates it will accept, allowing both integer indices and lowercase string indices, and by assuming that a missing z value corresponds to map 0.  The function also tries to give reasonably helpful errors.
Let us begin by looking at the function <code>toTile</code>.  The purpose of this function is to allow other functions to accept tile coordinates in a table instead of an actual tile object.  The function tries to be reasonably permissive in the coordinates it will accept, allowing both integer indices and lowercase string indices, and by assuming that a missing z value corresponds to map 0.  The function also tries to give reasonably helpful errors.


This brings up something that should be considered when making reusable code: what to do if you don't get values you were expecting.  One option is to throw an error and bring up the Lua console.  This is good for scenario creators, so they know something is wrong, but not so great for end users because the error stops the rest of the event from happening, should mistake slip through playtesting.
This brings up something that should be considered when making reusable code: what to do if you don't get values you were expecting.  One option is to throw an error and bring up the Lua console.  This is good for scenario creators, so they know something is wrong, but not so great for end users because the error stops the rest of the event from happening, should the mistake slip through playtesting.


The other option is for the code to fail silently, and try to do what it can.  This is the way the old Test of Time macro system often fails.  If Civ II doesn't "understand" the event, it just doesn't happen, and doesn't bother the player with the error.  This means that the scenario will still mostly work, but it is harder to catch issues during playtesting.  Sometimes, this means doing something "sensible" if the input isn't of the expected type, and might make the code easier to write, since you don't have to handle inputs differently.
The other option is for the code to fail silently, and try to do what it can.  This is the way the old Test of Time macro system often fails.  If Civ II doesn't "understand" the event, it just doesn't happen, and doesn't bother the player with the error.  This means that the scenario will still mostly work, but it is harder to catch issues during playtesting.  Sometimes, this means doing something "sensible" if the input isn't of the expected type, and might make the code easier to write, since you don't have to handle inputs differently.
Line 224: Line 224:
     return
     return
  end
  end
<code>
</code>


If the tile has a city, return so nothing else is done in the function.
If the tile has a city, return so nothing else is done in the function.
Line 239: Line 239:


The functions in this library don't have to be complex, and in fact, most probably shouldn't be.  They just have to make life easier for the end user.  If you reach this lesson before the General Library is considered "complete," your assignment now is to submit some work for it.  If you don't feel comfortable doing bitwise operations, you can test the work of others or submit functions that simplify other tasks.  However, there are also some helper functions in the library to facilitate changing bits, so you might also consider working on the unit type flags functions.  Remember, if you want to make functions available to the console for testing purposes, they must be global variables.
The functions in this library don't have to be complex, and in fact, most probably shouldn't be.  They just have to make life easier for the end user.  If you reach this lesson before the General Library is considered "complete," your assignment now is to submit some work for it.  If you don't feel comfortable doing bitwise operations, you can test the work of others or submit functions that simplify other tasks.  However, there are also some helper functions in the library to facilitate changing bits, so you might also consider working on the unit type flags functions.  Remember, if you want to make functions available to the console for testing purposes, they must be global variables.


=== The Threshold Table and First Look at Metatables===
=== The Threshold Table and First Look at Metatables===

Revision as of 04:26, 23 July 2019

Back to Get Started With Lua Events

This Lesson is under construction.

Introduction

In this lesson, we will take a look at how to split the code for a scenario's events into multiple files. There are several reasons to consider doing this.

The first reason is simply to organize your work. If you have a series of complicated events, the events.lua file can become very large very quickly. Once your events.lua file has thousands of lines of code in it, it may not be very obvious where the best place is to put that next helper function, or where to implement the function that runs a complicated event. Sometimes even finding the piece of code that has to be changed can be a bit of a hassle. In Over the Reich, I sometimes search for the associated civ.scen line, and then search for the exact name of the function I need. Occasionally, there may even be more searching to find what I actually have to change. With multiple files, it becomes easier to find things.

Another aspect of using multiple files is that you can make certain changes without worrying about breaking things elsewhere. If myBigComplicatedFunction relies on aModestHelperFunction for a certain part of its functionality, and a change to myBigComplicatedFunction also requires a change to aModestHelperFunction, you have to make sure that nothing else in your code also uses aModestHelperFunction, or, if something else does depend on it, that aModestHelperFunction either still provides the original functionality, or that all other functions that use aModestHelperFunction are suitably modified. If myBigComplicatedFunction is defined in its own lua module, then you don't have to worry about forgetting that aModestHelperFunction was used elsewhere, since it can't be accessed outside the module unless you specifically allow it to be.

A second reason to split events code into multiple files is to facilitate collaboration. If all of the events are in a single file, then only one person can make changes to the file at a time. If one partner starts changing an event, but has to do something else before finishing, everyone waits on one person's delay. However, if most aspects of the scenario are separated into individual files, everyone can work on his or her own piece of the puzzle without depriving others of the opportunity to work at that time.

A third reason to split events into multiple modules is to facilitate reusing code. If everything required to implement an event is in its own file (or files), it becomes much easier for other creators to use your functionality in their scenario. Since everything they need is already in one place, they don't have to find every last helper function, or concern themselves with naming conflicts.

Two Simple Examples

Let us look at the top of the events.lua file. You can get the version that I started lesson 6 with here.

GetStartedWithLuaLesson6-01-DefaultRequire.jpg

We get code from other files by using the command require along with a string specifying the file name (except for the .lua extension). At the moment, we can only get code stored in the lua folder in the Test of Time directory. This is inconvenient if we're trying to distribute a scenario, since installing a scenario would require moving certain files to the lua directory, and those file names might conflict. Instead, we need to tell the Lua interpreter to search in the same folder that the events.lua folder is in.

To do this, we need the following code:

local eventsPath = string.gsub(debug.getinfo(1).source, "@", "")
local scenarioFolderPath = string.gsub(eventsPath, "events.lua", "?.lua")
if string.find(package.path, scenarioFolderPath, 1, true) == nil then
   package.path = package.path .. ";" .. scenarioFolderPath
end

You can probably cut and paste this code before the require lines in your events.lua file and not worry about how it works. However, I'll provide a brief explanation for reference.

The first line stores the location of the of the current file as a string in eventsPath.

The second line replaces events.lua in the eventsPath with ?.lua, which will tell the Lua interpreter to look for .lua files in the current directory (that is to say, the directory with the events.lua file).

The if statement adds scenarioFolderPath to the places for the Lua interpreter to search for code, if that directory is not already in the search path. Since the search path is kept active by the Lua interpreter, it will not be necessary to repeat these lines of code in every file that performs a require action as long as you are running the file as part of events. If you are writing some other kind of code, you will need to substitute "events.lua" with the name of the file with the code you are running. Note that for some reason, events.lua is always provided in lower case. However, for use in other situations, debug.getinfo(1).source uses the correct case for event files. I have only briefly tested this, so you may have to do some testing yourself if this situation comes up (or ask for help in the forums).

Now that we've told Lua where to look for our extra code, we can begin moving code to other files. Think of require as telling Lua to run the entire contents of another file as a function. We can then assign a value to whatever that file returns, which will usually be a table. A couple of examples will clarify.

For our first example, we will move our scenario's parameters to a separate file, and access it via require. This may not seem worthwhile in this example, but tables like this can get large in a hurry. At the time of writing, Over the Reich has over 100 lines of parameters ("specialNumbers"), and parameters are something that a designer might like to play with while collaborators make other implementations. Also, parameters will have to be referenced by other parts of the code, and a file that is required by events.lua cannot, in turn, require events.lua in order to get information. That means that we'll have to keep our parameters in a separate file anyway.

Cut the parameters table definition and paste it into a new file, classicRomeParameters.lua. For the last line, write

return parameters

Now, Near the top of events.lua, where the existing require lines are, add the eventsPath code from above and also the line

local parameters = require("classicRomeParameters")

GetStartedWithLuaLesson6-02-RequireParameters.jpg

Open the classicRome scenario and test out a couple of events to make sure everything is the same.

We might find it convenient to put a few tables into the same file. For this, we will simply return a table containing these tables, and give each table a name in the events.lua file.

Create a file classicRomeAliases.lua and move all the "aliases" tables to that file. Also move the getCityAt function.

At the bottom of the file, put the following lines:

return {
unitAliases=unitAliases,
tribeAliases=tribeAliases,
tileAliases=tileAliases,
cityAliases=cityAliases,
textAliases=textAliases,
terrainAliases=terrainAliases,
improvementAliases=improvementAliases,
}

Then, add the following lines to events.lua (in the require section)

local aliases = require("classicRomeAliases")
local unitAliases,tribeAliases,tileAliases,cityAliases,textAliases,
		terrainAliases,improvementAliases=aliases.unitAliases,aliases.tribeAliases,
		aliases.tileAliases,aliases.cityAliases, aliases.textAliases,aliases.terrainAliases,
		aliases.improvementAliases

Here, our classicRomeAliases module is returning a table with our "alias" tables. We then assign the entries in this returned table to local variables with the desired names. Here, I use multiple variable assignment, which I don't believe I've used before (even though Lua allows it), but these could just as simply have been assigned line by line.

GetStartedWithLuaLesson6-03-RequireAliases.jpg

Now, let us move the legion plundering code into its own file, which we'll call plunder.lua. First, copy over the legionPlunder function, and paste it near the bottom of the new file.

Now, let us go through the code of this function, and figure out what else we need in our plunder.lua module.

The very first part of the function is a check against unitAliases.legion. This means we need the unitAliases table, and so we copy the aliases require code to the top of plunder.lua. We don't need the path information, since that will be added in events.lua.

A few more lines down, we find that the function cityRadius is also used in legionPlunder. Therefore, we move that over as well. We also need the plunderValue function, so copy that over as well.

Next, we check if cityRadius relies on anything, but it doesn't.

Proceeding to plunderValue, we find that it relies on the parameters table, so require that as well. The function plunderValue does not appear to depend on anything else, so we're done moving functionality. If we missed something, the Lua interpreter will complain at some point anyway.

Next, we require the plunder module in events.lua, with the following line:

local plunder = require("plunder")

Next, for each of the functions in plunder.lua, we check if it is used somewhere in events.lua. We can do this by searching. plunderValue isn't used anywhere, so we're done with that. City radius is used. At the bottom of plunder.lua, we begin our return table, and have

return {
cityRadius=cityRadius,
}

Then, in events.lua, we replace the ocurance of cityRadius with plunder.cityRadius. Alternatively, we could have put

local cityRadius=plunder.cityRadius

after the line requiring plunder.

Similarly, for legionPlunder, we add legionPlunder=legionPlunder to the table that plunder.lua returns, and pre-append plunder. to legionPlunder in the one location where it occurs.

Now, we test the legion plunder function and the press 4 generate archers function to make sure everything works.

GetStartedWithLuaLesson6-04-RequirePlunder.jpg

Working With a Library

In programming terms, a "library" is a collection of code that can provide some specified functionality to a programmer without that programmer having to modify the code in the library. Thus far, you have been using the "civ" library to interact with the game, and there has been no need modify it in order to use it in our own events. (Unfortunately, in the case of the civ library, we don't actually know how to modify it.) This is in contrast to much of the code we have written thus far. For example, the code we wrote last lesson to make Selucid elephants attack Egypt is highly specific. You couldn't just cut and paste that code, change the argument of a single function, and get your own goto event. It is still a good example for how to write a goto event, but a user would still have to actually write the event.

If you plan to write some functionality for your scenario that others might find useful, it is worth spending a little extra effort to see if you can write that functionality in a form that will allow others to use it with minimal effort. If you can, then a little extra work on your part might save someone else the trouble of re-inventing the wheel, and allow them to complete their project sooner, or to work on some new feature for you to enjoy. Unfortunately, the Lua scenarios that have been completed at the time of writing haven't been that good at creating re-usable code, at least in part since we were all getting used to Lua. This will hopefully be improved in the future.

For now, we're going to look at a library which is called the General Library (unless someone comes up with a better name) and which can be found here. The purpose of this module is to provide relatively simple functionality that probably shouldn't be implemented every time it is needed. This is useful for our purposes, since we can look at relatively self contained and simple functions for our first examples. We'll create a library for generating munitions later.

This library would be used by invoking the line

local gen = require("generalLibrary")

If you wanted to make these functions available to the console for testing purposes, you would simply omit the local, thereby making gen a global variable.

Let us now take a look at a function provided by the General Library gen.hasIrrigation(tile)-->boolean. The very first line references the helper function toTile(tile or table)-->tile, so let us include that code, and look at it first.

-- toTile(tile or table)-->tile
-- gen.toTile(tile or table)-->tile
-- If given a tile object, returns the tile
-- If given coordinates for a tile, returns the tile
-- Causes error otherwise
-- Helper Function (provided to this library as toTile and gen.toTile)
local function toTile(input)
    if civ.isTile(input) then
        return input
    elseif type(input) == "table" then
        local xVal = input[1] or input["x"]
        local yVal = input[2] or input["y"]
        local zVal = input[3] or input["z"] or 0
        if type(xVal)=="number" and type(yVal)=="number" and type(zVal)=="number" then
            if civ.getTile(xVal,yVal,zVal) then
                return civ.getTile(xVal,yVal,zVal)
            else
                error("Table with values {"..tostring(xVal)..","..tostring(yVal)..
                        ","..tostring(zVal).."} does not correspond to a valid tile.")
            end
        else
            error("Table did not correspond to tile coordinates")
        end
    else
        error("Did not receive a tile object or table of coordinates.")
    end
end
gen.toTile = toTile

-- gen.hasIrrigation(tile)-->boolean
-- returns true if tile has irrigation but no farm
-- returns false otherwise
function gen.hasIrrigation(tile)
    tile = toTile(tile)
    local improvements = tile.improvements
    -- irrigation, but no mining, so not farmland
    if improvements & 0x04 == 0x04 and improvements & 0x08 == 0 then
        return true
    else
        return false
    end
end

GetStartedWithLuaLesson6-05-HasIrrigation.jpg

Let us begin by looking at the function toTile. The purpose of this function is to allow other functions to accept tile coordinates in a table instead of an actual tile object. The function tries to be reasonably permissive in the coordinates it will accept, allowing both integer indices and lowercase string indices, and by assuming that a missing z value corresponds to map 0. The function also tries to give reasonably helpful errors.

This brings up something that should be considered when making reusable code: what to do if you don't get values you were expecting. One option is to throw an error and bring up the Lua console. This is good for scenario creators, so they know something is wrong, but not so great for end users because the error stops the rest of the event from happening, should the mistake slip through playtesting.

The other option is for the code to fail silently, and try to do what it can. This is the way the old Test of Time macro system often fails. If Civ II doesn't "understand" the event, it just doesn't happen, and doesn't bother the player with the error. This means that the scenario will still mostly work, but it is harder to catch issues during playtesting. Sometimes, this means doing something "sensible" if the input isn't of the expected type, and might make the code easier to write, since you don't have to handle inputs differently.

A third option is to write your code such that both things can be done. At the top of your code, you put in a variable named "debugMode" or something, and make all the functions throw errors when true, and fail silently when false. Then you playtest with debugMode set to true, and release with debug mode set to false. However, this means extra work in writing your code and you also have to test everything with debug mode off.

For toTile, I went with the fail with error option, since that will allow every function that uses toTile to assume it is receiving proper data. It also seems unlikely that people will be regularly trying to check tiles that don't exist or have tables with tiles and units and improvement objects all in one. And if they do, "guarding" is fairly straightforward anyway (as we've done in previous lessons).

Although toTile is mainly meant to be a helper function for the General Library, it is also put into the gen table to be made available to end users, since it offers useful functionality. Hence, gen.toTile=toTile. Note that we could have given toTile a different name in the gen table that this module will return, such as gen.tableToTile=toTile. In fact, we could refer to the same function with multiple keys in the gen table, if we want to.

Next, we will look at gen.hasIrrigation.

The first thing to notice is that local is not being used to define this function. That is because we're putting the function for gen.hasIrrigation directly into the gen table that this module will return. We could define hasIrrigation as a local variable and then copy it into the gen table. However, there is a decent chance that the General Library will have more than 200 functions by the time everything is complete, since it is meant to provide a lot of building block functionality and keep it in one place, and we will run into trouble if we assign 200 local variables in a single function (which a required lua file acts as).

Perhaps the most striking thing is that we begin by changing the value of tile, the argument of the function. It looks like we're making a global variable named tile, but we're not. When you specify the names of the arguments for a function (in this case tile), you create local variables for that function, which can be given new values just as if you had explicitly used the local keyword.

Beyond that, we extract the integer representing the tile improvements into a local variable (which upon later reflection isn't really necessary), and check that the irrigation bit is set to 1 and the mining bit is set to 0. Refer to lesson 5 for why this works. In fact, we could have simply returned the value of the if statement, rather than specifying return true and return false, although it is very slightly clearer what is going on in the expanded version of the code.

Now, let us write a function to place irrigation on a square. It seems pretty clear that we should do nothing if the tile has a city, but we also have to decide what to do if there is mining or farmland on the tile, since those interfere with placing irrigation. We'll just remove them, on the basis that if we're trying to "place irrigation," then we want irrigation to be there when we are finished.

function gen.placeIrrigation(tile)
   tile = toTile(tile)

These lines open the function and convert a table of coordinates to a tile if necessary.

if tile.city then
   return
end

If the tile has a city, return so nothing else is done in the function.

    -- Set irrigation bit to 1
    tile.improvements = tile.improvements | 0x04
    -- Set mining bit to 0
    tile.improvements = tile.improvements & ~0x08
end

Set the irrigation bit to 1 and the mining bit to 0.

The functions in this library don't have to be complex, and in fact, most probably shouldn't be. They just have to make life easier for the end user. If you reach this lesson before the General Library is considered "complete," your assignment now is to submit some work for it. If you don't feel comfortable doing bitwise operations, you can test the work of others or submit functions that simplify other tasks. However, there are also some helper functions in the library to facilitate changing bits, so you might also consider working on the unit type flags functions. Remember, if you want to make functions available to the console for testing purposes, they must be global variables.

The Threshold Table and First Look at Metatables

Consider a scenario design situation that occurred in Over the Reich: a bomber is generating some bombs via a key press, and the number of bombs it should generate is dependant on its current remaining hitpoints. The more hitpoints the bomber has, the more bombs it should generate. What we want is to specify that if a bomber has between x and y hitpoints, it should produce 2 bombs. That is, for a given type of bomber, we want to have, for example,

hp >= 16 --> 4 bombs 16 > hp >= 12 --> 3 bombs 12 > hp >= 9 --> 2 bombs 9 > hp >= 4 --> 1 bomb 4 > hp >= 0 --> 0 bombs

Unfortunately, Lua tables do not naturally encode this kind of association by default. However, Lua does have a feature called metatables, which allows us to change the behaviour of tables. (This will be a much better way to make the specification than I ended up writing in Over the Reich.)

A metatable is a table that we can "attach" to another table, and then the metatable changes the behaviour of that table. We'll only look at metatables briefly here, but more information can be found at [1].

What we want is a data structure that can accept numerical "key values" that are between the key values explicitly specified, and return the appropriate value for those keys. We will call this data structure a threshold table, since we are specifying threshold values as keys. We can do this by assigning the following metatable:

local mt = { __index = function(thresholdTable,key)
                if type(key) ~= "number" then
                    return rawget(thresholdTable,key)
                else
                    local bestIndexSoFar = -math.huge
                    local bestValueSoFar = false
                    for index,value in pairs(thresholdTable) do
                        if type(index) == "number" and key >= index and index >= bestIndexSoFar then
                            bestIndexSoFar = index
                            bestValueSoFar = value
                        end
                    end
                    return bestValueSoFar
                end
            end,}

If we run setmetatable(myTable,mt), then whenever we ask for an index from myTable and that index doesn't currently exist in myTable, the function assigned to __index will run. Now let us examine the function.

If the type of the key is not a number, then we return whatever the table value is for that key. We use the function rawget, because the standard thresholdTable[key] will invoke this function again if the key doesn't exist, and so never return an answer (when we want it to return nil).

If the key type is a number, then we look for the largest numerical index that is also less than or equal to the key value. We return a default value of false if the key submitted is smaller than all numerical values. False is chosen over nil, so that a maximum value can be set by setting the value of a numerical index to false. Using the key [-math.huge] allows a return value to be set for all numbers (since all numbers are greater than -math.huge).

Since we don't want scenario creators to have to re-create threshold tables every time they need them, we'll add this functionality to the General Library

local thresholdTableMetatable = { __index = function(thresholdTable,key)
                if type(key) ~= "number" then
                    return rawget(thresholdTable,key)
                else
                    local bestIndexSoFar = -math.huge
                    local bestValueSoFar = false
                    for index,value in pairs(thresholdTable) do
                        if type(index) == "number" and key >= index and index >= bestIndexSoFar then
                            bestIndexSoFar = index
                            bestValueSoFar = value
                        end
                    end
                    return bestValueSoFar
                end
            end,}
-- gen.makeThresholdTable(table or nil)-->thresholdTable
function gen.makeThresholdTable(inputTable)
    inputTable = inputTable or {}
    return setmetatable(inputTable,thresholdTableMetatable)
end

GetStartedWithLuaLesson6-06-ThresholdTable.jpg


A Munition Generation Module

Now, let us write some re-usable code for a human player to generate units by pressing a key while an appropriate unit is active. This sort of code could be used for ranged attacks (generating a "munition") and also for recruiting mercenaries by paying gold.

The first thing we will want to do is to decide how our code will be used by the end user. This will, perhaps, change by the time we finish writing the code, but it will at least give us a starting point.

spawnUnit(generatingUnit, specificationTable)--> table of unitCreated

Our goal is that when a player attempts to generate a unit using another unit, they will provide the unit spawning the new unit, and a table of information specifying if a unit (or units) can be spawned and anything else that must happen while spawning (like paying money, expending movement points, etc.).

We have an option here. We could have the scenario creator provide the specification table as a required file for our module, or the creator can provide it at the time spawnUnit is called. I choose the latter for a couple of reasons. The first is that it allows the player to have more than one specification table, either because more than one key can spawn a unit, or because the specification can change over time. The other is reason is so that the creator can make the table in the events.lua file if desired (since our module can't require information from events.lua).

Having decided that our generateUnit events are going to be specified by a table, we have to decide what the contents of that table should be. It seems pretty reasonable to index the specificationTable with the index of the unit type of each unit that can generate using the information in the table. If a unit type id number is missing, that unit doesn't generate a munition (or recruit a unit).

The value for each unit type id in the specification table will be a table that specifies how and if that unit generates other units.

--specificationTable[unitType.id]={ -- -- goldCost = integer or nil -- amount of gold it costs to generate a unit -- absent means 0 -- minTreasury = integer or nil -- minimum amount of gold in treasury to generate a unit -- absent means refer to gold cost -- (tribe will generate and be set to 0 if treasury is less than goldCost) -- treasuryFailMessage = string or nil -- A message to be displayed if the unit fails to spawn a unit due to an -- insufficient treasury -- nil means no message

-- There are three ways to specify move costs, in full movement points, -- in "atomic" movement points, and as a fraction of total movement points for the -- unit type. Use only one kind per unit type

-- moveCost = integer or nil -- movement points to be expended generating the unit -- "full" movement points -- absent means 0 -- minMove = integer or nil -- minimum remaining movement points to be allowed to generate a unit -- "full" movement points -- absent means any movement points for land/sea, 2 "atomic" for air units -- postGenMinMove = integer or nil -- a unit will be left with at least this many movement points after -- the generation function -- absent means 0 for land/sea, 1 "atomic" for air units -- moveCostAtomic = integer or nil -- movement points to be expended generating the unit -- refers to the unit.moveSpent movement points -- absent means 0 -- minMoveAtomic = integer or nil -- minumum remaining movement points to be allowed to generate a unit -- referes to the unit.moveSpent movement points -- absent means any movement points for land, 2 "atomic" for air units -- postGenMinMoveAtomic = integer or nil -- a unit will be left with at least this many movement points after -- the generation function -- absent means 0 for land/sea, 1 "atomic" for air units -- moveCostFraction = number in [0,1] or nil -- fraction of unit's total movement points expended generating the unit -- round up to nearest "atomic" movement point -- absent means 0 -- minMoveFraction = number in [0,1] or nil -- fraction of unit's total movement points that must remain to be allowed -- to generate a unit -- absent means any movement points for land, 2 "atomic" for air units -- round up to nearest "atomic" movement point -- postGenMinMoveFraction = number in [0,1] or nil -- a unit will be left with at least this many movement points after -- the generation function, round up to nearest "atomic" move point -- absent means 0 for land/sea, 1 "atomic" for air units -- roundFractionFull = bool or nil -- fractional movement cost and minimum are rounded up to full movement point -- instead of atomic movement point -- nil/false means don't -- roundFractionFullDown = bool or nil -- fractional cost and minimum are rounded down to full move point -- nil/false means don't -- minMoveFailMessage = string or nil -- a message to be displayed if a unit is not generated due to insufficient -- movement points. -- nil means no message

-- allowedTerrainTypes = table of integers or nil -- a unit may only be generated if the tile it is standing on -- corresponds to one of numbers in the table -- nil means the unit can be generated on any terrain type -- terrainTypeFailMessage = string or nil -- message to be displayed if a unit is not generated due to standing -- on the incorrect terrain

-- requiredTech = tech object or nil -- the generating civ must have this technology in order to generate -- the unit -- nil means no requirement -- techFailMessage = string or nil -- message to be displayed if a unit is not generated due to not having -- the correct technology

-- payload = boolean or string -- if true, unit must have a home city in order to generate munitions -- and generating munitions sets the home city to NONE -- payloadFailMessage = string or nil -- message to be displayed if a unit is not generated due to the -- payload restriction -- payloadRestrictionCheck = nil or function(unit)-->boolean -- If function returns false, the home city is not valid for giving the -- unit a payload. This will be checked when the unit is activated, when -- the unit is given a new home city with the 'h' key and when the unit -- tries to generate a munition -- nil means no restriction -- payloadRestrictionMessage = nil or string -- message to show if a unit fails the payloadRestrictionCheck

-- canGenerateFunction = nil or function(unit)-->boolean -- This function applied to the generating unit must return true in order -- for a unit to be spawned. All other conditions still apply. -- any failure message should be part of canGenerateFunction -- absent means no extra conditions

-- generateUnitFunction = nil or function(unit)-->table of unit -- This function creates the unit or units to be generated -- and returns a table containing those units -- Ignore any specifications prefixed with * if this is used -- nil means use other specifications

--*generatedUnitType = unitType -- The type of unit to be generated -- can't be nil unless generateUnitFunction is used instead

--*giveVeteran = bool or nil -- generated unit(s) given veteran status if true -- nil or false means don't give vet status -- if true, overrides copyVeteranStatus

--*copyVeteranStatus = bool or nil -- generated unit(s) copy veteran status of generating unit if true -- nil or false means don't give vet status

--*setHomeCityFunction = nil or function(generatingUnit)-->cityObject -- determines what home city the spawned unit should have -- nil means a home city of NONE

--*numberToGenerate = nil or number or thresholdTable or function(generatingUnit)-->number -- if nil, generate 1 unit in all circumstances -- if integer, generate that many units (generate 0 if number less than 0) -- if number not integer, generate floor(number), and 1 more with -- probability number-floor(number) -- if thresholdTable, use remaining hp as the key, to get the number to create -- if function, get the number as the returned value of the function

-- activate = bool or nil -- Activates one of the generated units if true. If generateUnitFunction was used, -- the unit at index 1 is activated, if index 1 has a value. (if not, any unit in -- the list might be chosen)

The very last entry brings forward a change that must be made to our unit generation specification. Lua's activate unit command doesn't run the unit activation trigger, even though the active unit is changed. Therefore, we must do that manually. Hence,

spawnUnit(generatingUnit, specificationTable)--> table of unitCreated

becomes

spawnUnit(generatingUnit,specificationTable,unitActivationCode)-->table of unitCreated

Perhaps we will change it again later.

I've decided to try a different instructional technique for this section. Instead of writing the code and then going through it section by section, I've recorded a video of my coding and testing this module, and I attempt to explain some of the stuff that I do along the way.

I'm not sure of the value of the video. If you find that there is little useful content relative to the time it takes to watch, then feel free to skip it and continue the lesson. I don't teach anything "new" in it, so you won't miss anything crucial. However, it should be pretty close to how programming Lua can actually happen in the "real world." While I did give some thought to how to write the module before I started, I do attempt to solve problems in real time, and make mistakes that are not discovered for a while (including one where I try to explain how I solve a problem and then not actually do a crucial part).

You can find the code here. Even if you decide not to watch the video, download the code and try testing some of the features based on the specification above.

The link to the video is

https://youtu.be/XxPyNknGlAo