Modding Tutorial No. 7: A First Look At Events

Hello and welcome to our seventh modding blog! For today's, we'll be covering event modding! This is an expansive topic with many possible uses that will require some lua scripting, which we won't be covering in too much detail as we've a lot of ground to cover, but you should be able to find more detailed and advanced tutorials on lua scripting if you search online.

Event Registration
At its core, an event is a lua script that the game will try to run occasionally. When the game will try to run it depends on how you register the event; you can have it set to be checked whenever a city is captured, whenever a hostile AI unit is discovered, on a few different other occurrences, or just checked every couple of in-game days. You decide what type of event it is when you register it in any XML file using the .xml extension (NOT .xnt as this is not an entity) starting with "Event", eg EventEpizephyrianLocrisMoreLikeEpicFailingLosers.xml is a perfectly fine filename and I'm not at all bitter over the events of my recent Kroton game. As with entity .xnt files, our event registration .xml can be anywhere; a default event registration file can be found in each subfolder of the game's Resources/Objectives folder; we'll be mirroring this by creating EventEpizephyrianLocrisMoreLikeEpicFailingLosers.xml in our mod's Resources/Objectives/BurnLocrisBurn folder, but anywhere inside the mod's Resources folder would suffice.

So what does an event registration .xml file look like? Something like this:  36                resources/objectives/general/TaskRequestKillNBrigades.lua  36                resources/objectives/general/TaskRequestBurnFarms.lua It's all in one big eventgroup block with some event sub-blocks, each with "name" and "event" attributes, and "pollingfrequency" and "scriptfile" subtags. An event's "name" attribute is the unique string the game knows this event by; it isn't used in anything we'll be working with, but it's important to make it unique. The "event" attribute specifies what type of event it is: As for the sub-blocks, the "pollingfrequency" sub-block is only important to timed events, specifying how often the event is checked (so every 36 days in the above sample), and the "scriptfile" is the filepath to the lua file the event tries to run.
 * "timed" events are checked every few days
 * "capturecity" events are checked whenever a city is captured
 * "capturefort" events whenever a city camp or bridge is captured
 * "captureresource" when a resource building is captured
 * "factiondiscovered" when a new faction is discovered
 * "aispotted" is when a hostile AI unit comes into the player's point of view.

For our example mod, we'll look to history for one of the actions of King Pyrrhos of Ápeiros, wherein Locris, which had sworn to give him tribute, was known to have supported Roman soldiers against him, which led him to sack a major temple of Persephone in that treacherous city, because you don't just break a tribute and attack someone Locris I thought that was beneath you. For this, we'll want an event in it, a timed one that checks if Locris is near you and your nations are hostile enough that someone *cough*Locris*cough* has probably been committing treacherous atrocities on the no doubt pure and virtuous other faction.

So to register those two events, we'll edit EventEpizephyrianLocrisMoreLikeEpicFailingLosers.xml to have an eventgroup block with an event sub-block, a timed event, we'll name it BurnLocrisBurn.FlowerOfItaly, after a title Plato bestowed on Locris after he somehow came to like them. The typical naming convention default events follow is that the first part is the name of a larger group or category of events, followed by a period and then the name of the event itself, and though nothing forces us to follow that convention, I will anyway because civilized people follow codes of conduct. We'll set the pollingfrequency at 45, which isn't particularly often so it won't be too constantly checking and it puts it at a different cycle than most other events making it unlikely to be checked at the same time. The scriptfile we'll point at resources/objectives/burnlocrisburn/taskflowerofitaly.lua.

Basics of Lua
Now we have to create the .lua files we pointed to. A lua file is just a text file, you can create them through windows explorer and edit them with NotePad or any text editor you can edit XML with. A lua script is basically a series of statements and instructions our game will follow; an example of a simple lua script our game could run is local locriandoom = 5; locriandoom = locriandoom + 3; print(locriandoom); If the game were to run this script, it would open up the console with the number "8". The first line of this script, "local locriandoom = 5;", both declares and initializes what we call a local variable. It is a local variable because we typed the word "local"; a local variable is forgotten when the part of the script it was declared in is finished, which helps clean up some memory. A variable is a piece of information we attach a name to (in our example "locriandoom") that we can edit and pass around between parts of the script. In lua, a variable can be a number, a string, a function, a table, or a special type of data that a host program like Hegemony III creates (for example we have functions that can pass brigades or factions around as variables). If we'd stopped just at "local locriandoom", we'd have declared the variable, but it would not have had any data in it; variables without data in them are valid, lua calls them nil variables, but when we typed " = 5" we gave it an instruction to follow. The "=" sign, used as a single letter after a variable name, will put whatever information comes after the "=" sign into the variable; in this case, all we had after the "=" was 5, so that initialized locriandoom to 5. Then we indicate that that particular instruction is done before moving on to the next one. There are two ways to do this, and we happen to be using both; ending the instruction with a semicolon and going to a new line.

Now on a new instruction, we mention the variable again; since the name is already in use, it's being recognized as the local variable from before rather than a new variable. We then use another "=" to say we're putting new stuff in the variable, then we use a variable name (the same variable we're assigning the information to, even), the "+" operator, and 3. After the game notices the equals, it will try to put the data into the variable, but before it can do that, it has to complete all the operations we told it to; the "+" tells it to add the variable locriandoom (which is currently set to 5) and the number 3 together, which gives us 8. When the "+" is done, the resulting 8 will be put into locriandoom by the "=".

After that, we call a function. Functions, like variables, have a name; in this case, the function's name is "print". Functions are different from other variables in that they run a series of instructions when given the data they ask for; the print function takes whatever data you pass it, brings up the console, and puts the data in the console. You pass any data the function needs through its parameter list, which is found between round parentheses typed directly after the name. In our case, we're passing our variable locriandoom into the print function by typing print, opening a parenthesis, typing the variable's name, and typing a closing parenthesis. You don't need to pass a variable into a function; print(8) would achieve the same thing. For some functions, you don't need to pass anything in at all; getplayerfaction is a valid call to the getplayerfaction function our game uses which will return the player faction as a variable and it doesn't need any data parameters to do that. You can also pass more than one parameter into a function if you need to, to do that you seperate them by commas, for example savegame("Mysavegame.xml", true, true) will pass "Mysavegame.xml" to the first parameter (which expects a string), true to the second parameter (which expects either true or false), and true to the third parameter (which expects true or false). You can see how many parameters and what type a function expects by opening the console and entering commands("functionname"), which will describe functionname to you.

Our Events
Hopefully that crash course has helped you get something of a grip on lua scripting; if not, there should be tutorials elsewhere on the internet that describe things in more detail and cover many more features of lua than I have dealt in if you search "lua script tutorial". For now, let's go through the script resources/objectives/burnlocrisburn/taskflowerofitaly.lua. Most events have two parts, one where it checks to see if the game wants to run the event right now, and one that actually runs the event. We usually put each of those in their own function, one function named iseventready and one named doevent. We can declare iseventready by typing "local function iseventready( eventData )", hitting enter, and typing "end"; every instruction we write between those lines will count as part of the iseventready function. Any local variables declared inside of it will be forgotten outside of it, since that's what a local variable is, so we need to have an eventData parameter; this variable will start the function as an empty table which we'll use to store any information we want the later doevent function to have access to. When it's done, iseventready will return either false, meaning we don't want the event to happen, or true, meaning we will want it, in which case we'll need the doevent function. After iseventready's end, declare another local function, this one called doevent, also having an eventData parameter and with another end declared after it. Note that the script won't actually DO any of the instructions inside our functions if we don't call them! This is actually helpful because we don't know if we want the instructions inside doevent to run or not. At the moment, we've only declared our events, so nothing we put inside them will happen.

With our functions declared, we should set up our calls to them. First, we need to create the empty eventData table we intend to pass to both; after both function definitions, write "local eventData = {}". In lua, curly brackets ("{" and "}") indicate a table, a special type of variable that can store more variables inside it. exampletable={locris="evil", burnthetemples=true} is a table that stores members named locris and burnthetemples, one a string and the other a boolean value of true or false, which can be accessed as exampletable.locris or exampletable.burnthetemples, or the entire exampletable variable can be passed as one variable. We'll want to pass eventData between our functions to preserve any data iseventready thinks doevent will need; we don't need it to start out with any data, it's iseventready's job to populate it.

With eventData declared, it's time to pass it to iseventready and call it. We can call iseventready the same way we can call any other faction, by stating its name and putting parameters in parentheses, like "iseventready(eventData)". However, we won't just want to call iseventready, we'll want to use the data it gives us to decide whether or not to keep going, so we'll put that in an if statement. We do this by putting "if" in front of it and "then" after it, like "if iseventready(eventData) then", and then on the next line create an "end" to close the if block. Now the script will only run the parts between those two lines if iseventready(eventData) says to, otherwise it will skip right to "end" and continue on through the rest of the file. However, we won't have anything else in the file, so nothing more will happen if iseventready says false. If it says true, the code between the "then" and the "end" will run; there are two things we want to put there. First, we'll want the game to remember that this event is happening; it stores extensive logs of all events which helps certain events calculate whether it's appropriate to show up or not. To make such a log, we call the function logevent and pass in "Event" and a name for this event, in this case "BurnLocrisBurn.FlowerOfItaly"; the second thing we'll want to do is call the doevent function and pass the eventData (at this point having been changed by iseventready) into it. That function will take care of the rest. At this point, objectives/burnlocrisburn/taskflowerofitaly.lua looks like this: local function iseventready( eventData ) end local function doevent( eventData ) end local eventData = {} if( iseventready(eventData) ) then logevent("Event", "BurnLocrisBurn.FlowerOfItaly"); doevent(eventData) end

iseventready
At the moment, iseventready doesn't do anything worthwhile, just like Locris. We will need it to actually pull its weight for once and tell the script whether or not to do the contents of the if block, however. To do this, the function will need to return a value, in this case a boolean value which can be "true" or "false" (without quotes; with quotes, they would become strings). An if statement will always read "true" to mean to run the code after then and "false" to mean to skip it; thus we'll want to return true when we want doevent to be called. You can return true by typing "return true" and the function will return true and then stop. Nothing inside a function after a return will be run, which will come in handy here. How we're going to make iseventready work is by having a series of circumstances where we don't want the event to fire, and all of them will return false if those circumstances are in place. If they aren't, we'll reach the end of the function and have a "return true;" there.

So under what circumstances do we want this event to not happen? One thing is that we can't have it happen if it's already occurred; no matter how much you want to, you can't burn down the same building twice. This is where the logs the game keeps of events comes in handy. There's another function, numevents, which takes a type and name of event and returns how often the event has happened. So we can add numevents("Event", "BurnLocrisBurn.FlowerOfItaly"), and that will give us a number we can use mathematical operations on. In our case, we want to see if that number is more than zero, which uses the ">" operator; we could also use >= to see if it was more than or equal to zero, < to see if it was less than zero, or == to see if it was equal to zero, but those wouldn't help us any more than Locris ever did. If we were to type numevents("Event", "BurnLocrisBurn.FlowerOfItaly") > 0 , that would return true if the event happened more than zero times or false if it didn't, and since that evaluates to a boolean true or false value, we can check it with another if statement. if numevents("Event", "BurnLocrisBurn.FlowerOfItaly") > 0 then return false; end will return false if and only if numevents tells us the event has happened more than zero times, and since a function stops working when it returns something, the function will stop and return false to the if statement from before, skipping the doevent call. Otherwise, it will skip that return and move on to the next part of iseventready.

Having had the event before isn't the only circumstance we need to think about; what if the player is playing in the Etruria sandbox, and thus living blissfully in an idyllic utopia where Locris does not exist, or if the player is (for whatever reason) playing as Locris? When writing an event, you always have to consider such circumstances which make your event completely invalid. So we'll create another if-then and put our check on those circumstances in it. The function getfactionbyname takes the string name of the faction you're trying to look up and tries to return the data for the faction; if it fails to find a faction with that name, it will just return an empty nil value. So we can call getfactionbyname("Locris") and check if that's nil; if it is, we'll want to return false. Rather than check that directly, it's probably worthwhile to save off the result of getfactionbyname("Locris") since a lot of our checks from now on will involve checking what they have and what they're up to and it'd be easier to just type a variable name than call getfactionbyname every time. We can do that by creating a local variable named locris and using the "=" operator to put the results of getfactionbyname("Locris") inside of it, like so. local locris = getfactionbyname("Locris") From there, we'll want to check if locris is nil, if locris is the player faction, and if either of those is true we'll want to return false to make the event not happen. The "==" operator (always be careful not to mix it up with "="!) will return true if the data on both sides of it are the same, so "locris == nil" will return true if locris is nil but false if it is not. We can do a similar thing to check if locris is the player faction by calling the getplayerfaction function and comparing that. Thus, we add local locris = getfactionbyname("Locris"); if ( locris == nil ) then return false; end; if ( locris == getplayerfaction ) then return false; end; to our iseventready function.

Our next checks all involve calling member functions; we'll be getting the city of Locris to see that Locris faction still controls it and checking that Locris is actually close to the player. A member function is a function called from a variable that uses the data in that variable to change it's response; the getfaction function will return a different faction depending on what object you call it from. You call a member function from a variable by typing the variable's name, adding a comma, then adding the functions name and parameter list. The member variable checks we'll be calling look like this; local city = getentitybytag("locris"); if (city:getfaction ~= locris) then return false; end; if (locris:isplayerneighbour ~= true) then return false; end; The functions called are You can get a list of all member factions for a certain object type by opening the console, typing commands("Faction:"), or instead of "Faction:", using "City:", "Fort:", "Brigade:", "Building:" or any other object name, and when you hit enter a full list will come up. Note that the list might be long, so be prepared for a lot of scrolling!
 * getentitybytag, a global function (meaning it's not a member function) which will return whichever entity has the tag passed into it; all cities have a tag pre-defined, usually there name in all lower case, so this gives us the city of locris.
 * getfaction, a member function of cities, forts, brigades and buildings that returns whatever entity controls the object
 * isplayerneighbour, a new member function of factions which was just added in 3.1.0 that returns true if any of the faction's cities border the player or not

More circumstances could be added if we felt like tweaking it more, but I think just these do a good job of eliminating the most insane scenarios. We can add our "return true;" after that last if statement, finishing off our iseventready with code that will only be reached if all the ifs fail. Right now it should look like this local function iseventready( eventData ) if numevents("Event", "BurnLocrisBurn.FlowerOfItaly") > 0 then return false; end local locris = getfactionbyname("Locris"); if ( locris == nil ) then return false; end; if ( locris == getplayerfaction ) then return false; end; local city = getentitybytag("locris"); if (city:getfaction ~= locris) then return false; end; if (locris:isplayerneighbour ~= true) then return false; end; return true; end local function doevent( eventData ) end local eventData = {} if( iseventready(eventData) ) then logevent("Event", "BurnLocrisBurn.FlowerOfItaly"); doevent(eventData) end

doevent
So now we actually want to run the event; in here, we call all the script functions we want to actually happen. So what do we want this event to do? We want it to add an objective to the player to capture Locris; the objective will create a tracker telling the player to capture Locris, set up a script that waits until they do, and handle the scripts that happen after Locris finally falls. The actual files in the objective we'll have to go over some other week, as objective modding has been split off from this already-way-too-long tutorial into its own, but doevent will still need to make the game load and run those objective files.

First, doevent should call the createfactionobjective function and pass in the locris faction entity (retrieved through getfactionbyname("Locris")); what this does is create an objective group for that faction. All tasks need to be added to an objective group; most of them are added to one for the faction the task concerns, which is why the createfactionobjective function exists, to help us ensure that an objective group exists for the faction we provide.

After createfactionobjective we'll call taskload and pass in as parameters "Locris", which is the name of the objective we're adding this task to, and the name of the file we want to load into it, which here is "Resources/Objectives/BurnLocrisBurn/TaskFlowerOfItaly.xml". This will add the contents of TaskFlowerOfItaly.xml into the Locris objective, but it won't run them.

Since we did create a new task and the event logs have a category for task creation, it'd be wise to add in a new call to logevent before actually running the new task. We should therefore next call logevent and pass in "TaskNew" instead of "Event" this time, then the name of our event which is still "BurnLocrisBurn.FlowerOfItaly".

After that, one last function call will kick off the task. The function taskdiscover will handle that for us, it just asks for two parameters. "Locris.FlowerOfItaly" is the first, and it's made of two parts. First the name of the objective group, which is "Locris", then a dot and then the name of the task we're adding, which is actually defined in the objective's .xml; the .xml we're adding calls its task "Flower of Italy", so our complete task name is "Locris.FlowerOfItaly". The second parameter isn't actually necessary and can be skipped if you want; it's a boolean that controls whether or not you get "New Objective" text in the middle of your screen and in the message log. I set it to true to ensure the new objective gets player attention. If you do skip it, it will read as false. This is not true for all function parameters but a special case for taskdiscover; you can check a function's parameters by using the console to call commands("functionname"), and taskdiscover shows taskdiscover(string sTaskName, bool bPostNews = false), and you know the function doesn't need bPostNews passed in because there's an "= false" at the end, which is the parameter's default value.

So our final script looks like this. local function iseventready( eventData ) if numevents("Event", "BurnLocrisBurn.FlowerOfItaly") > 0 then return false; end local locris = getfactionbyname("Locris"); if ( locris == nil ) then return false; end; if ( locris == getplayerfaction ) then return false; end; local city = getentitybytag("locris"); if (city:getfaction ~= locris) then return false; end; if (locris:isplayerneighbour ~= true) then return false; end; return true; end local function doevent( eventData ) createfactionobjective( getfactionbyname("Locris") ); taskload("Locris", "Resources/Objectives/BurnLocrisBurn/TaskFlowerOfItaly.xml"); logevent("TaskNew", "BurnLocrisBurn.FlowerOfItaly"); taskdiscover("Locris.FlowerOfItaly", true); end local eventData = {} if( iseventready(eventData) ) then logevent("Event", "BurnLocrisBurn.FlowerOfItaly"); doevent(eventData) end

Anyway, that's our first glance at scripting mods, and we've barely covered a fraction of what can be done with it. We'll be delving more into the details of objective modding and some more complex functions in our next tutorial, but hopefully this has served as a good introduction to the basics of how our game uses lua. A lot of the details on lua in general, like how to work with tables or functions, can be found in incredible detail elsewhere on the web.