Mission.lua and Structuring Your Code
There are many ways in which you can structure your code. No method is right or wrong, and it really comes down to a matter of personal preference. This section is intended to share with you, the experience gained from over 25 years of software architecture and development, to help you formulate your own approach, while hopefully being able to identify and avoid some obvious pitfalls that are most likely heading your way. While this section is specific to Lua Script and DCS Mission Building, even beyond AGHTM, many of the concepts will be general enough to apply to any programming language or environment.
We will not be covering generic Lua Script syntax like statements, conditionals, looping statements, creating functions, etc. The assumption is that you are already familiar with Lua Script fundamentals. DCS uses Lua v5.2 in its implementation. If you would like to do some additional research into these concepts, you can check out the Lua 5.2 Reference Manual. We will, however, cover the way we develop our missions, and share with you, our methodology for splitting functionality into different script files, and how to add those script files to a mission.
Aside from the code in some of the examples, we will also not be discussing techniques for performing specific tasks in Lua. This section is about how you structure your code project, how to split your code logically, and provide some groundwork for you to begin building your own missions with custom Lua code, and being consistent in doing so. If you would like more information on the scriptable objects available to you in DCS, you will definitely want to check out Simulator Scripting Engine Documentation over on Hoggitworld, as it is THE definitive source on the matter. Tip: Bookmark this page as you WILL be here often if you decide DCS Mission Scripting is something you enjoy.
Understanding How Lua Script Works in DCS
Lua Script is what we call a scripting language. Scripting languages are not full blown programming languages, but they can be very powerful. The main difference between scripting languages like Lua Script and Javascript and formal programming languages like C, C++, C#, and Visual Basic is that scripting languages do not need to be compiled into an executable standalone program (exe or dll). Scripting languages use plain text that contains a set of commands for a compiled program to execute (Lua.dll running inside DCS in this case).
For those familiar with HTML and web page design, this section will make a lot of sense, because we use that as an analogy for much of the conversation. For those who are not web developers, it will still make sense when we are done. DCS creates what is called a Lua container. A container in programmer-ese is exactly what it sounds like. It is an isolated virtual bucket where any loaded Lua scripts are executed. There is only one container where all of your Lua Script runs. This is very similar to an empty HTML web page being displayed in a web browser. That web page has a container where javascript can be executed to interact with elements of the page, similar to how DCS works, where your scripts can interact with the DCS environment when the code is run, referred to as runtime.
Whether we are talking about web design or DCS Mission Building, when scripts are loaded, they are added to the bottom of the single container. Essentially, all of the scripts that are loaded are merged into a single script, commonly referred to as a scope or context. This means you can access objects or variables in one script from another script as they are both in the same scope once loaded. This also means we need to be mindful of what objects and functions may already be loaded into the scope when you write your code. If you declare a variable or function, that has already been loaded into the scope, you will be overwriting that existing object with whatever new object you replace it with in the scope. This could have drastic repercussions, and is generally very difficult to debug. In the sections below, we discuss a way to create a nested scope inside of our main Lua container to prevent these types of conflicts, called collisions, from occurring.
Script 1.lua:
agContainer = {}
agContainer.missionName = "Sample Mission"
agContainer.missionAuthor = "Koder"
function displayValues(formatString)
trigger.action.outText(string.format(formatString, agContainer.missionName, agContainer.missionAuthor), 10, true)
end
function agContainer:doSomething(formatString)
displayValues(formatString)
end
Script 2.lua:
function startMission()
agContainer:doSomething("Mission: %s\nCreated By: %s")
end
timer.scheduleFunction(startMission, {}, timer.getTime() + 10)
Merged Scope Inside DCS:
agContainer = {}
agContainer.missionName = "Sample Mission"
agContainer.missionAuthor = "Koder"
function displayValues(formatString)
trigger.action.outText(string.format(formatString, agContainer.missionName, agContainer.missionAuthor), 10, true)
end
function agContainer:doSomething(formatString)
displayValues(formatString)
end
function startMission()
agContainer:doSomething("Mission: %s\nCreated By: %s")
end
timer.scheduleFunction(startMission, {}, timer.getTime() + 10)
Understanding Code Dependencies
Code Dependency is just a fancy way of saying that in order for our code to run, it requires other code to already be loaded. This is a basic concept, but not necessarily something that is apparent when beginning to write DCS scripts or working in any programming language really. When you think about it it makes sense, since it would be illogical to write a line of code that calls a function, when we have not yet told DCS what that function is. When adding multiple code files to your mission, you need to be aware of any dependencies that exist in your code, and you should always load scripts in an order such that you load any dependency scripts into your scope before the scripts that depend on them.
Understanding scope can be a little tricky sometimes. Code in script files can either be executed immediately when the script is loaded, or it can be a function or variable used for storing information or functionality for later use. Think of functions and variables as placeholders we intend to use later. Code contained inside of a function is not executed until that function is called by some other code. Inside of this function declaration, you can safely reference variables and functions you know will be loaded by the time the function is eventually called. This does not apply to code that runs when the script is loaded, which includes variable declarations as that is really just code that runs when the script is loaded to define those variables. This code is said to have a dependency on the other code.
Setting Up Your Project
It is always good to be consistent in the way you write your mission code. This consistency from project to project, or mission to mission in this case, is called a development methodology. In this section, we share the methodology we use consistently when building missions. This is not the right way, or the wrong way; just a way, our way. Feel free to adapt whatever you can to formulate your own methodology.
Every programming language has what is called an entry point. An entry point is the point at which your code starts to execute. When a DCS mission runs, the DCS entry point is entered, which causes its code to run. One of the things it does is starts running any scripts that are loaded. The execution of your scripts can be thought of almost as a standalone program that is running inside of DCS when the mission runs.
Whenever we build our missions, like the AGHTM Mission Bundles, for example, we add a file called Mission.lua to our mission. This file serves as our entry point into the script "program" that is running. This file, like all code files that we load, is created outside of the mission editor in a text editor like Notepad or Notepad++, and is then added or embedded into a mission using a trigger. Mission.lua contains all of our code for the mission we are building. This is the code that will be dependent on all of your other code files. Therefore, it should always be the last script file you load. This is the file that is responsible for running our mission versus simply providing functionality that can be consumed. Think of this as the consumer of all of your other code.
Our methodology when we start on a new mission, is to first create a folder for all of our mission assets, including the mission file itself. Everything related to our mission is all grouped together in one place and easy to find in the future. We create this folder right in our missions folder in the DCS Saved Games folder giving it a meaningful name. We always give that folder the same name as our mission file. Inside this folder we create a folder called Assets, and inside that folder we create another called Scripts. In the Scripts folder, we create a new empty text file named Mission.lua. This file will contain all of our mission code (for now).
Below you can see a sample AGHTM project that has been set up and which also contains our mission's agHTMConfig.lua file. For more information on the agHTMConfig.lua file, see the Customizing Your Mission with agHTMConfig.lua section. You would only include this file if you are building an AGHTM mission.
Any additional script files we need to add, go into the Scripts folder. Anything else we need to add, like audio files, images, etc. go into the Assets folder. Usually we create folders in Assets for Audio, images, Voiceover Actor Scripts, etc for these additional assets. We try to be consistent across missions in how we name these folders. Once we have our mission project set up, we are ready to begin writing our code.
Adding Mission.lua to Your Mission
In order for your script to be used in a DCS mission, you will need to embed the script file into the mission file, using the Mission Editor. Typically, scripts are loaded using a trigger. We will need one trigger for each script we intend to load. Scripts are typically loaded on 1 second intervals, since that is the shortest increment of time we can use. We add a new trigger, set the type to Once
and optionally give it a name. For the condition, we will use a Time More
condition, and set the interval to 1 second for the first script, 2 seconds for the second script, etc. until all of our scripts are loaded. For the action, we choose a Do Script File
action and use the picker to find our Mission.lua file, or any other additional scripts we need to load. This selection of the script file is what embeds it in the mission file. Be aware this is a copy that gets embedded and no connection to the original file in your project folder is preserved. Mission.lua should always be the last script you load, to ensure all of your dependency scripts have already been loaded when the code in Mission.lua starts executing.
Writing Your Mission Code
As mentioned, Lua Script is a scripting language that is written using any plain text editor, such as Notepad or Notepad++. We highly recommend tha latter as it offers Lua syntax highlighting to make your code far more readable. The basic workflow is to make your changes in the text editor, reload the script back into your mission using a trigger, and debugging your mission by running it. For information on testing and debugging your code, see the Debugging Your AGHTM Missions section of this documentation.
The very first thing we do when creating a new mission is to edit Mission.lua, adding a container that our mission will use to avoid any naming collisions, as mentioned above. Then we add some basic properties as needed to that container. In the example below, you can see we have created a new container named agMissionInfo
, and have added some basic properties that our mission will use. Our missions always use agMissionInfo as our mission scope. This way we know exactly where to find mission scope level data while writing our code.
Example:
agMissionInfo = {}
agMissionInfo.missionName = "Sample Mission"
agMissionInfo.authorInfo = {
author = "Koder",
company = "Amoeba Games",
companyWebsite = "www.amoeba-games.com"
}
Depending on your methodology, there may be certain data or functionality you always include in your mission scripts. For example, when we build our missions, we almost always use the agCore script from the agScript for DCS Library. One of the functions that library offers, is the ability to register scripts with itself to display a list of loaded scripts at runtime so you can see what scripts have been loaded. The call to agCore:registerScript(coreInfo)
has a standardized data object that gets passed to it with information about the script. Because all of our missions use this we always include a variation of it in our Mission.lua files. Once you figure out what kinds of things you typically want to include in your missions, we suggest you make a Mission.lua template. Below is an example of the Mission.lua template we include in all of our AGHTM missions, but if you remove the call to agHTM:init(...)
it applies to every mission we build. Notice it also contains some helper functions and variable shortcuts that we have found throughout our learning process, are almost always useful, so they are included in the template as well. When we create a new mission, we simply go through and update the placeholders in the template.
Example:
-- AG Core Mission Initialization Script
-- This should always be loaded after all other AG Scripts
-- Init file is for Amoeba Games Helicopter Transport Missions (AGHTM) - MISSIONNAME
local blue = coalition.side.BLUE
local red = coalition.side.RED
if debugMode == nil then
debugMode = false
end
agMissionInfo = {}
agMissionInfo.coreInfo = {
name = "AGHTM - MAPNAME - MISSIONNAME",
filename = "Mission.lua",
version = "1.0.0.0",
logFilename = "AGHTM_LOGNAME",
logPrefix = "AGHTM_LOGPREFIX"
}
agCore:registerScript(agMissionInfo.coreInfo)
--------------------------------------------------------------------------------
-- Utility ---------------------------------------------------------------------
--------------------------------------------------------------------------------
function showMessageToAll(msg)
agCore:showCoalitionMessage(blue, msg)
end
function showMessageToGroup(groupName, msg, delay, clear)
agCore:showGroupMessage(groupName, msg, delay, clear)
end
--------------------------------------------------------------------------------
-- Runtime ---------------------------------------------------------------------
--------------------------------------------------------------------------------
agHTM:init(agMissionInfo.coreInfo.logFilename, agMissionInfo.coreInfo.logPrefix)
if debugMode then
agCore:showRegisteredScripts()
end
After you make any changes to Mission.lua, or any other script file, for that matter, you will need to embed the script file again. In the trigger editor, find the trigger that loads the script you modified. This is where the suggestion to name your triggers applies, or perhaps you could color code all triggers that load scripts as the same color, for example. The preference is yours. In the selected trigger, simply choose the Do Script File
action, and reselect the same file you initially loaded, and that has now changed. This will update the embedded copy to your new version. Now you can run the mission and see the changes in your code applied.
Splitting Your Code Into Separate Code Files
In many cases, you could build your entire mission and include all of your code in Mission.lua, since that is its intended purpose. However, you may find your code file begins to get large and hard to navigate as you build out your mission, and you may wish to separate that code into separate files that represent a specific type of functionality or object, perhaps even for reuse.
You should always think about and try to write your code in terms of reusability. If you ever find yourself writing the same or very similar chunks of code, you should consider making a function to perform the same task in a repeatably predictable and consistent way. If you have snippets of code, or large functional components, that you may use across multiple missions, you should isolate all of that functionality into its own library. A library, like AGHTM and agScript for DCS, contains all of the code files and related assets like images or audio clips necessary to provide the same functionality over and over, in our case, across multiple missions.
Whenever you start to move things from one file to another, or into a new file, be sure you are mindful of your code dependencies. Always work in small chunks so when (not if) something breaks, you are digging through a lot less code to figure out what happened. Make sure if you move code into new files, that you remember to include those new script files in your mission by way of a trigger. Based on any dependencies you may introduce, be sure your code is loaded in the correct order. This may require you to change the values in your Time More
conditions to ensure they load at the right time.
For example, if I load four scripts at 1, 2, 3 and 4 seconds via different triggers, and later need something else to load at 2 seconds, I will need to update the triggers for everything that loads at or after 2 seconds. My new script will load at 2 seconds, and the other scripts will need to be loaded 1 second later than their current time. That being said, it is a good idea to sort the triggers that load your scripts in the Trigger Editor, in the order they load. This way you insert the new one, and update everything below it (thats the same color since all of your triggers that load scripts were color coded above, right?... Riiiight? [Nudge nudge, wink, wink])
Creating a Custom Library
If you have code in your mission that you realize you can reuse in other missions, or even if you start off by creating a code file intended for reuse from the get go, you will want to treat that code like a library. The library should have no dependencies on your mission code. It should be a dependency for your mission code. That being said, be mindful of the scope of your code objects when moving code into a library. You may have to adjust accordingly. If you are starting off with code meant to be a library from the start, this should most likely not be an issue.
When creating a library, you want to approach it as a separate project. Create a new folder somewhere to contain all of your libraries, and a folder inside that for the library you are creating. Plan your library, including your naming container out in advance. The naming container should be unique to your library. In the examples on this page, you can see we use names like agHTM, agCore, agMissionInfo, etc to ensure there is no collision. Include the same Scripts, Images, Audio, etc. folders as applicable in the library folder, like you would for a mission. When you want to consume this library, you simply add the scripts, and any other relevant assets into your mission. For an example of a complex library and how it might be structured, see the AGHTM Library Contents section of this documentation.
Be sure to include version information in your library. When you have 20 missions in the future, all built using your library in its many different variations, you will want to know which version of the script it uses. This will also make troubleshooting easier if you decide to distribute and support your library to others, like we did with AGHTM. If your library gets complex, be sure to keep and/or include documentation, if possible.