Mission Tutorial

Contents

Introduction

Mission scripting is something that most people can do. Missions in Naev are written in the Lua scripting language, which is a fairly simple language to learn. For those who don't have any experience with programming or scripting, there are mission templates available for creating simple missions with little effort.

A Lua script is a plaintext file using the .lua file extension. No special tools are needed to edit a script, any text editor will do.
In Naev, mission files are stored in the dat/missions subtree. Events go into the dat/events subtree.

How missions work

A mission is offered through the player through one of several means. Most missions are started when the player talks to an NPC in a spaceport bar when landed, but it's also possible for missions to start in space. Typically, the player will be given a brief explanation of what the mission is about, and the opportunity to decline. Once the mission starts, the player may choose to cancel the mission at any time by going to the info menu, selecting the mission on the missions tab and clicking the abort button. Otherwise, the player can complete or fail the mission, depending on his performance.

The master mission index (found in dat/mission.xml) is what the game uses for reference. This file determines the in-game name of the mission, and it tells the game where the mission script is located. It also tells the game when and where to spawn the mission. Note that the path for the mission file is relative to dat/missions.

A create() function in the mission script

The mission script must contain a create() function. This function is the code entry point for the mission, no matter how it is started. Missions started from spaceport bar NPCs and the mission computer also need an accept() function, which is the function that gets called when the player approaches the NPC or accepts the computer mission.

A misn.accept() call

A mission is not considered "active" until misn.accept() has been called. This function registers the mission in the player's mission log and activates the OSD. Note: misn.accept() is NOT the same thing as accept()! Typically, misn.accept() is called in the mission's accept() function.

A misn.finish() call

An active mission can end in two ways. The first is when the player manually aborts the mission. The other way is when the mission script calls misn.finish(). This function de-registers the mission, deactivates the OSD, removes all mission related information (such as mission cargo) and stops executing the mission script. misn.finish should be the last command called in any mission script.

Events

Events are much like missions in that they use largely the same API and can make about the same things happen in the universe. The difference between events and missions is that while missions are player-accepted and can be player-aborted, the same is not true for events. An event will trigger according to certain conditions, and when it does, the player does not have the ability to prevent it. An event will continue to run until it is ended, again according to certain conditions.

The master event index (found in dat/event.xml) is what the game uses for reference. This file determined the in-game name of the event, and it tells the game where the event script is located. It also tells the game when and where to spawn the event. Note that the path for the event file is relative to dat/events.

A create() function in the event script

The event script must contain a create() function. This function is the code entry point for the event.

An evt.finish() call

An active event can end in only one way, and that is when the mission script calls evt.finish(). This function de-registers the event, removes all event related information and stops executing the mission script. evt.finish should be the last command called in any event script.

Examples

There are several annotated example mission scripts available. If you have a git clone of the project, you will find these in docs/missions. This page lists them as well.

A more complete example, explaining several aspects mission scripting, including some pitfalls that should be avoided.

Ideas to implement

The best way to learn to develop missions for Naev is by implementing them. If you can't think of an idea or want a simple concept to get started check out the Mission Ideas page that gives you general simple missions to implement. If you need help or advice you can always come onto IRC or send a mail to the Mailing List.

Tips

There are some things that you should keep in mind when scripting a mission:

Testing missions
Mission scripts are loaded when you load a game. If you make a change to your script, you usually only have to reload your game to test the changes. However, keep in mind that the mission will NOT re-run code that was previously run! Typically, this means that calls made in the create() or accept() functions will require you to restart the mission to see the changes.

If you want to force start an event while testing use naev.eventStart() and to force start missions use naev.missionStart(). Note that the event "name" mentioned here is from the relevant xml file!

Unlike mission Lua code, the XML data is only loaded at game startup. If you change anything about the mission appearance, you will need to restart Naev before the changes are applied.

If an error occurs during execution the stack trace is both shown in the Lua console and printed to stderr. You can also use print(x) to print something to the Lua console to help debug.

The abort() function
Missions (not events) may or may not include an abort() function. This function will be called when the player aborts the mission using the abort button in the game. Scripters may use this function to perform cleanup on abort, such as removing stack variables or displaying a customized abort message. A mission does not need an abort() function to work.

Tables
Lua can use anything as a table index, but most commonly tables are indexed numerically. It's important to note that Lua's indexing starts at 1, unlike other languages where it starts at 0. For example:

The table will then be available when the mission gets loaded up from a savegame.

Mission NPCs versus mission-generated NPCs
A mission can add NPC characters to the spaceport bar for the player to interact with. There are, however, two cases in this: the NPC that actually gives the mission and NPCs that are used as part of the mission. In the first case, there can be only one NPC, and it uses a somewhat different command. The reason for this is that a kind of bootstrapping process is going on. While the mission is inactive no mission related data is kept, yet the mission-associated NPC still has to show up.

Here is an example showing the difference in usage between the two NPC types:

function create() -- code entry point.
misn.setDesc("This is the description text for the NPC")
misn.setNPC("Name of the NPC", "none") -- none is for the portrait. Normally you'd use something else.
end
function accept() -- Remember, function accept() automatically gets called when the player approaches the mission NPC.
misn.accept() -- Normally, you'd give the player the opportunity to decline before doing this.
misn.npcAdd("my_function", "My NPC's name 1", "none", "My NPC's description 1", 5) -- Notice how you get to specify the hooked function yourself in this case.
misn.npcAdd("my_function", "My NPC's name 2", "none", "My NPC's description 2", 5) -- Notice how you get to add as many NPCs as you like, now that the mission has started.
end

Iterating over pilots
When iterating over the pilots in a fleet, ALWAYS make sure the pilots exists before trying to do anything with them, unless you're certain that they exist! If a ship is destroyed in the game, its pilot handle will not automatically be removed (and can still be referenced). Here is an example of typical iteration code:

for i, j in ipairs(some_fleet) do -- i stores the index number, j stores the pilot
if j:exists() then -- equivalent to pilot.exists(j)
-- j does indeed exist, you may do something with it now.
end
end

Spawning one-pilot fleets
Quite often, you'll want to add a single ship to a system, rather than a whole fleet. However, even single ships belong to a fleet. When you spawn them using pilot.add, you will get a table containing all the pilots in your fleet, even if there is only one pilot. Here is a short-hand notation to get the pilot right when you spawn it:

my_pilot = pilot.add("Civilian Gawain")[1] -- Assign the first element of the table returned by pilot.add() to my_pilot.

Including code
In Lua you may include other Lua files. Doing so will give you access to the code in those Lua files as if you copy-pasted it into your own code. There are some useful, generic functions available in the scripts directory. The include path is relative to the naev binary. For example:

include "scripts/jumpdist.lua" -- include external code
my_table = getsysatdistance(system.cur(), 2, 3) -- Get a table of all systems at least 2 and at most 3 jumps away from the current system

Making timer-based cutscenes
When making events happen one after another, you're going to need timer hooks. Timer hooks trigger n milliseconds after they were created, where n is the first argument. Since Lua doesn't have a sleep function, you are going to need to set the times for the hooks so that they will trigger in succession. A good way to make the sequence more manageable is by keeping a helper variable:

local delay = 0 -- "local" means the variable only exists within the current function.
hook.timer(delay, "playerControl", true) -- First argument is the delay, second argument is the function to call when the hook triggers, third argument is the argument to pass to the function
delay = delay + 2000 -- time between last timer and next timer
hook.timer(delay, "zoomTo", joe)
delay = delay + 4000 -- time between last timer and next timer
hook.timer(delay, "showText", Jorscene[1])

Giving pilots orders in manual control
Pilots may be put under manual control, and given specific orders. These orders go in a queue, and will be executed consecutively. If you want to abort the current order and issue a new one right away, you should clear the order queue first by using pilot.taskClear() or a new pilot.control(). Note also that you may hook the pilot with hook.pilot(p, "idle"). This hook will trigger as soon as the pilot has no orders in its queue.

Passing multiple arguments to a hooked function
Hooks are very useful, but they only allow you to pass a single argument to the function they call. If you want to pass more than one argument, you can get around this by storing your arguments in a table and passing the table. Example:

hook.timer(1000, "my_function", {pilot = my_pilot, text = "Hello!"}) -- Pass a table with custom named indices
function my_function(args)
p = args.pilot -- Get pilot from the table. In this example, this will be my_pilot.
t = args.text -- Get text from the table In this example, this will be "Hello!".
pilot.broadcast(p, t) -- Make pilot p broadcast text t
end