Preface
Welcome to the Naev development manual! This manual is meant to cover all aspects of Naev development, including both the engine and the lore of the Naev base scenario known as Sea of Darkness. It is currently a work in progress. The source code for the manual can be found on codeberg with pull requests and issues being welcome.
The document is split into two parts: the first part deals with the Naev engine and how to implement and work with it. The second part deals with the Lore of the Naev base scenario known as Sea of Darkness.
What is Naev?
Naev started development circa 20041 as an attempt to make an open source clone of Escape Velocity (Classic) that would run on Linux. While the engine itself was meant to be a modern clone of the Escape Velocity engine, the game scenario itself was meant to be unique. The name NAEV (later to become Naev) stood for Not Another Escape Velocity. It is now used as a proper known and has not been changed to confuse users.
Over the time and with the come and go of many contributors, Naev has grown into an advanced game engine with a complex base scenario featuring many new mechanics and features not seen in games of the same genre. While still far from done, since version 0.10.0 plugin support has been added, and initial work has begun on creating this in-depth guide to Naev development in hopes that more people join in on the exciting project.
-
This is roughly 2 years after the release of Escape Velocity Nova on Mac OS. ↩
Introduction
Naev is designed to be easily modifiable by using many plain text files, usually in the XML format, to define the data of the different components, such as missions, ships, or outfits. They are loaded as necessary by the engine when the game is started.
Although it is possible to modify the data files directly, it is highly recommended to use the plugin framework so that changes are not lost on updating, and can also be easily shared with other people,
Data Structure
Naev plugins and data use the same directory structure. It is best to open up the original data to see how everything is laid. For completeness, the main directories are described below:
ai/: contains the different AI profiles and their associated libraries.asteroids/: contains the different asteroid types and groups in different directories.commodities/: contains all the different commodity files.damagetype/: contains all the potential damage types.difficulty/: contains the different difficulty settings.effects/: contains information about effects that can affect ships.events/: contains all the events.factions/: contains all the factions and their related Lua functionality.shaders/: contains all the shaders. Some are critical for basic game functionality.gui/: contains the different GUIsmap_decorator/: contains the information of what images to render on the map.missions/: contains all the missions.outfits/: contains all the outfits.scripts/: this is an optional directory that contains all libraries and useful scripts by convention. It is directly appended to the Lua path, so you canrequirefiles in this directory directly without having to prependscripts..ships/: contains all the ships.slots/: contains information about the different ship slot types.snd/: contains all the sound and music.spfx/: contains all the special effects. Explosions are required by the engine and can not be removed.spob/: contains all the space objects (planets, stations, etc.).spob_virtual/: contains all the virtual space objects. These mainly serve to modify the presence levels of factions in different systems artificially.ssys/: contains all the star systems.tech/: contains all the tech structures.trails/: contains all the descriptions of ship trails that are available and their shaders.unidiff/: contains all the universe diffs. These are used to create modifications to the game data during a playthrough, such as adding spobs or systems.
In general, recursive iteration is used with all directories.
This means you don’t have to put all the ship XML files directly it ships/, but you can use subdirectories, such as ships/special/.
Furthermore, in order to avoid collision between plugins, it is highly recommended to use a subdirectory with the plugin name.
So if you want to define a new ship called Stardragon, you would put the XML file in ships/my_plugin/stardragon.xml.
Furthermore, the following files play a critical role:
AUTHORS: contains author information about the game.VERSION: contains version information about the game.autoequip.lua: used when the player presses autoequip in the equipment window.board.lua: used when the player boards a ship.comm.lua: used when the player hails a ship.common.lua: changes to the Lua language that are applied to all scripts.intro: the introduction text when starting a new game.loadscreen.lua: renders the loading screen.rep.lua: internal file for the console. Do not modify!!rescue.lua: script run when the game detects the player is stranded, such as they have a non-spaceworthy ship and are stuck in an uninhabited spob.save_updater.lua: used when updating saves to replace old outfits and licenses with newer ones.start.xml: determines the starting setting, time and such of the game.
Plugin Framework
Plugins are user-made collections of files that can add or change content from Naev. They can be seen as a set of files that can overwrite core Naev files and add new content such as missions, outfits, ships, etc. They are implemented with PHYSFS that allows treating the plugins and Naev data as a single “combined” virtual filesystems. Effectively, Naev will see plugin files as part of core data files, and use them appropriately.
Plugins are found at the following locations by default, and are automatically loaded if found.
| Operating System | Data Location |
|---|---|
| Linux | ~/.local/share/naev/plugins |
| Mac OS X | ~/Library/Application Support/org.naev.Naev/plugins |
| Windows | %APPDATA%\naev\plugins |
You can also see the path that Naev is looking for plugins from the plugins tab of the options menu.
Creating a Plugin
Plugins can either be a directory structure or compressed into a single zip file which allows for easier distribution.
You can create them by simply creating a directory with a plugin.toml metadata file, and it should already be usable, although it will do nothing.
To add functionality to the plugin, you have to add data files for the functionality you want.
See the information on the data structure for how it is organized, and refer to the other sections for adding or modifying particular functionality.
Plugin Repository
Since version 0.13.0, Naev has a built-in plugin explorer and installer, which uses the plugin repository by default.
This repository contains only the minimum information of the plugins necessary to be able to download and look up the rest of the information.
To add your plugin, please create a pull request on the repository adding the following information as a .toml file in plugins/:
identifier = "MyId"
name = "My Plugin Full Name"
source = { git = "https://codeberg.org/naev/my_plugin" }
metadata = "https://codeberg.org/naev/my_plugin/raw/branch/main/plugin.toml"
Where:
- identifier: has to be a unique identifier of the plugin, as plugins with the same identifier will overwrite each other. Only ASCII alphanumeric (letters from a-z both uppercase and lowercase, and numbers) are allowed, and it must not exceed 25 characters.
- name: attribute that contains the name of the plugin. This is what the player and other plugins will see when searching or installing the plugin.
- source: defines where the source is located to try to update the plugin when possible.
Valid options are
source = { git = "url" }orsource = { download = "url" }depending on whether it is a direct download or a git repository. - metadata: should be a url pointing to where the plugin’s
plugin.tomlis available. The plugin manager will use this file to see if updates are necessary.
Plugin Meta-Data plugin.toml
The plugin.toml file is specific to plugins and does not exist in the base game. A full example with only the required fields is shown below:
identifier = "MyPlugin" # Has to be unique
name = "My Awesome Plugin"
author = "Me, Myself, and I"
version = "1.0.0"
abstract = "The coolest plugin on the web."
naev_version = ">= 0.13.0"
source = { git = "https://my.awesome.website/plugin/repo.git" }
Required Fields
- identifier: has to be a unique identifier of the plugin, as plugins with the same identifier will overwrite each other. Only ASCII alphanumeric (letters from a-z both uppercase and lowercase, and numbers) are allowed, and it must not exceed 25 characters.
- name: attribute that contains the name of the plugin. This is what the player and other plugins will see when searching or installing the plugin.
- author: contains the name of the author(s) of the plugin.
- version: contains the version of the plugin. This has to be a semver string, that is usually in the form of “X.Y.Z” where X is the major version, Y is the major version, and Z is the minor version.
- abstract: contains the abstract of the plugin. This is required and must not exceed 200 characters. It is what players see when searching.
- naev_version: specifies which verisons of naev are compatible.
This is based on semver and allows comparisons with existing versions.
For example,
>= 0.13would mean a version after 0.13.0 (including betas), while something like>= 0.13, < 0.14would specify something newer than0.13.0but older than0.14.0.
Optional Fields
- source: defines where the source is located to try to update the plugin when possible.
It can be set to
source = "local"to not search online, which is the default if not specified. Other valid options aresource = { git = "url" }orsource = { download = "url" }depending on whether it is a direct download or a git repository. Note that if set to local, it will have to be updated manually! - description: contains the description of the plugin. This can be as long as necessary, and can use a limited subset of markdown. In the case it is not defined, the abstract field will be used in its place.
- license: a free-form string defining the license. Usually abbreviated license names such as “GPLv3+”, “CC0” or “CC-by-sa 3.0” are used.
- release_status: defines the status of the plugin. This can be one of “stable”, “testing”, or “development”. If omitted, it defaults to “stable”.
- tags: a list of free-form tags such as
[ "tc", "casual" ]. - image_url: the location of a representative image of the plugin.
This will be shown in the plugin manager and cached.
The url must have an explicit extension such as
.webpor.png. - depends: a list of plugin identifiers that this plugin depends on such as
[ "SomeOtherPlugin", "YetAnotherPlugin" ]. - recommends a list of plugin identifiers that this plugin recommends usage jointly with.
- priority: indicates the loading order of the plugin.
The default value is 5 and a lower value indicates higher priority.
Higher priority allows the plugin to overwrite files of lower priority plugins.
For example, if two plugins have a file
missions/test.lua, the one with the lower priority would take preference and overwrite the file of the other plugin. - total_conversion: a boolean value whether this plugin is a total conversion.
- blacklist: a list of regex strings representing files to blacklist as explained below.
- whitelist a list of regex strings representing files to whitelist as explained below.
Blacklisting and Whitelisting Base Files
Furthermore, it is also possible to use regex to hide files from the base game with the blacklist field.
For example, blacklist = [ "^ssys/.*\.xml" ] would hide all the XML files in the ssys directory.
This is especially useful when planning total conversions or plugins that modify significantly the base game, and you don’t want updates to add new files you have to hide.
By using the appropriate blacklists, you increase compatibility with future versions of Naev.
Furthermore, given the popularity of total conversion-type plugins, you can use the total_conversion = true tag to apply a large set of blacklist rules which will remove all explicit content from Naev.
This means you will have to define at least a star system, a spob, and a flyable ship for the game to run.
In additiona to the blacklist, a whitelist can also be defined with whitelist, which takes priority over the blacklist.
In other words, whitlelist stuff will ignore the blacklist.
Total conversions automatically get a few critical files such as the settings.lua event included, although they can still be overwritten.
Tips and Tricks
Tips and Tricks
This section includes some useful tricks when designing plugins.
Making Compatible Changes
While plugins can easily overwrite files, there are times you may not wish to do that, as replacing the same file as another plugin will lead to conflicts.
To avoid replacing files, it is possible to use the Universe Diff (unidiff) system (TODO add section).
Let us consider a motivating example where we want to simply add an outfit, and add it to an existing tech group, but not replace the file so it can work with other plugins.
This can be done by creating an event that automatically applies the unidiff when the player loads the game.
Assume that we have a tech group called “Base Tech Group”, and an outfit called “My Outfit”. We wish to add “My Outfit” to “Base Tech Group” without replacing the file. To do this, first we define the unidiff that adds “My Outfit” to “Base Tech Group” as below:
<?xml version="1.0" encoding="UTF-8"?>
<unidiff name="my_plugin_unidiff">
<tech name="Base Tech Group">
<item_add>My Outfit</item_add>
</tech>
</unidiff>
The above file should be saved to unidiff/my_plugin_unidiff.xml, and will simply add the “My Outfit” to “Base Tech Group”.
However, this unidiff will be disabled by default, so we’ll have to enable it with an event such as below:
--[[
<?xml version='1.0' encoding='utf8'?>
<event name="My Plugin Start">
<location>load</location>
<chance>100</chance>
</event>
--]]
function create ()
local diffname = "my_plugin_unidiff"
if not diff.isApplied(diffname) then
diff.apply(diffname)
end
evt.finish()
end
The above file should be saved somewhere in events/, such as events/my_plugin_start.lua, and will simply apply the unidiff "my_plugin_unidiff" if it is not active, thus adding “My Outfit” to “Base Tech Group” without overwriting the file.
The same technique can be done to add new systems or any other feature supported by the unidiff system (see section TODO). For example, you can add new systems normally, but then add the jumps to existing systems in a unidiff so you do not have to overwrite the original files.
Extending Naev Core Functionality
This section deals with some of the core functionality in Naev: Sea of Darkness that can be extended to support plugins without the need to be overwritten. Extending Naev functionality, in general, relies heavily on the Lua using custom bindings that can interact with the Naev engine.
A full overview of the Naev Lua API can be found at naev.org/api and is out of the scope of this document.
Adding News
News is controlled by the dat/events/news.lua script. This script looks in the dat/events/news/ for news data that can be used to create customized news feeds for the player. In general, all you have to do is created a specially formatted news file and chuck it in the directory for it to be used in-game.
When the player loads a game, the news script goes over all the news data files and loads them up. Afterwards, each time the player lands, it creates a dynamic list of potential news based on the faction and characteristics of the landed spob. Afterwards, it randomly samples from the news a number of times based on certain criteria. News is not refreshed entirely each time the player lands, instead it is slowly updated over time based on a diversity of criteria. When new news is needed, the script samples from the dynamic list to create it. Thus, it tends to slowly evolve as the player does things.
Let us take a look at how the news data files have to be formatted.
At the core, each news data file has to return a function that returns 4 values:
- The name of the faction the news should be displayed at, or
"Generic"for all factions with thegenerictag. - The headers to use for the faction. Set to nil if you don’t want to add more header options.
- The greetings to use for the faction. Set to nil if you don’t want to add more greeting options.
- A list of available articles for the faction.
Let us look at a minimal working example with all the features:
local head = {
_("Welcome to Universal News Feed.")
}
local greeting = {
_("Interesting events from around the universe."),
}
local articles = {
{
head = N_([[Naev Dev Manual Released!]]),
body = _([[The Naev Development Manual was released after a long time in development. "About time" said an impatient user.]]),
},
}
return function ()
return "Independent", head, greeting, articles
end
The above example declares 3 tables corresponding to the news header (head), news greeting (greeting), and articles (articles).
In this simple case, each table only has a single element, but it can have many more which will be chosen at random.
The script returns a function at the bottom, that returns the faction name, "Independent" in this case, and the four tables.
The function will be evaluated each time the player lands and news has to be generated, and allows you to condition what articles are generated based on all sorts of criteria.
Most of the meat of news lies in the articles. Each article is represented as a table with the following elements:
- head: Title of the news. Must be an untranslated string (wrap with
N_()) - body: Body text of the news. Can be either a function or a string. In case of being a function, it gets evaluated.
- tag (optional): Used to determine if a piece of news is repeated. Defaults to the head, but you can define multiple news with the same tag to make them mutually exclusive.
- priority (optional): Determines how high up the news is shown. Smaller values prioritize the news. Defaults to a value of 6.
As an alternative, it is also possible to bypass the news script entirely and directly add news with news.add.
This can be useful when adding news you want to appear directly as a result of in-game actions and not have it randomly appear.
However, do note that not all players read the news and it can easily be missed.
Adding Bar NPCs
TODO
Adding Derelict Events
TODO
Adding Sensor Anomalies
TODO
Adding Personalities
TODO
Missions and Events
Naev missions and events are written in the Lua Programming Language. In particular, they use version 5.1 of the Lua programming language. While both missions and events share most of the same API, they differ in the following ways:
- Missions: Always visible to the player in the info window.
The player can also abort them at any time.
Missions are saved by default.
Have exclusive access to the
misnlibrary and are found indat/missions/. - Events: Not visible or shown to the player in any way, however, their consequences can be seen by the player.
By default, they are not saved to the player savefile.
If you want the event to be saved you have to explicitly do it with
evt.save(). Have exclusive access to theevtlibrary and are found indat/events/.
The general rule of thumb when choosing which to make is that if you want the player to have control, use a mission, otherwise use an event.
Example missions include cargo deliveries, system patrols, etc.
On the other hand, most events are related to game internals and cutscenes such as the save game updater event (dat/events/updater.lua) or news generator event (dat/events/news.lua).
A full overview of the Naev Lua API can be found at naev.org/api and is out of the scope of this document.
Mission Guidelines
This following section deals with guidelines for getting missions included into the official Naev repository. These are rough guidelines and do not necessarily have to be followed exactly. Exceptions can be made depending on the context.
- Avoid stating what the player is feeling or making choices for them. The player should be in control of themselves.
- There should be no penalties for aborting missions. Let the player abort/fail and try again.
Getting Started
Missions and events share the same overall structure in which there is a large Lua comment at the top containing all sorts of meta-data, such as where it appears, requirements, etc.
Once the mission or event is started, the obligatory create function entry point is run.
Mission Header
Let us start by writing a simple mission header.
This will be enclosed by long Lua comments --[[ and --]] in the file.
Below is our simple header.
--[[
<mission name="My First Mission">
<unique />
<avail>
<chance>50</chance>
<location>Bar</location>
</avail>
</mission>
--]]
The mission is named “My First Mission” and has a 50% chance of appearing in any spaceport bar. Furthermore, it is marked unique so that once it is successfully completed, it will not appear again to the same player.
Entry Point create
Now, we can start coding the actual mission.
This all begins with the create () function.
Let us write a simple one to create an NPC at the Spaceport Bar where the mission appears:
function create ()
misn.setNPC( _("A human."),
"neutral/unique/youngbusinessman.webp",
_("A human wearing clothes.") )
end
The create function in this case is really simple, it only creates a single NPC with misn.setNPC.
Please note that only a single NPC is supported with misn.setNPC, if you want to use more NPC you would have to use misn.npcAdd which is much more flexible and not limited to mission givers.
There are two important things to note:
- All human-readable text is enclosed in
_()for translations. In principle, you should always use_()to enclose any text meant for the user to read, which will allow the translation system to automatically deal with it. - There is an image defined as a string.
In this case, this refers to an image in
gfx/portraits/. Note that Naev uses a virtual filesystem and the exact place of the file may vary depending on where it is set up.
With that set up, the mission will now spawn an NPC with 50% chance at any Spaceport Bar, but they will not do anything when approached.
This is because we have not defined an accept() function.
This function is only necessary when either using misn.npcAdd or creating mission computer missions.
So let us define that which will determine what happens when the NPC is approached as follows:
local vntk = require "vntk"
local fmt = require "format"
local reward = 50e3 -- This is equivalent to 50000, and easier to read
function accept ()
-- Make sure the player has space
if player.pilot():cargoFree() < 1 then
vntk.msg( _("Not Enough Space"),
_("You need more free space for this mission!") )
return
end
-- We get a target destination
mem.dest, mem.destsys = spob.getS( "Caladan" )
-- Ask the player if they want to do the mission
if not vntk.yesno( _("Apples?"),
fmt.f(_("Deliver apples to {spb} ({sys})?"),
{spb=mem.dest,sys=mem.destsys}) ) then
-- Player did not accept, so we finish here
vntk.msg(_("Rejected"),_("Your loss."))
misn.finish(false) -- Say the mission failed to complete
return
end
misn.accept() -- Have to accept the mission for it to be active
-- Set mission details
misn.setTitle( _("Deliver Apples") )
misn.setReward( fmt.credits( reward ) )
local desc = fmt.f(_("Take Apples to {spb} ({sys})."),
{spb=mem.dest,sys=mem.destsys}) )
misn.setDesc( desc )
-- On-screen display
misn.osdCreate( _("Apples"), { desc } )
misn.cargoAdd( "Food", 1 ) -- Add cargo
misn.markerAdd( mem.dest ) -- Show marker on the destination
-- Hook will trigger when we land
hook.land( "land" )
end
This time it’s a bit more complicated than before.
Let us try to break it down a bit.
The first line includes the vntk library, which is a small wrapper around the vn Visual Novel library.
This allows us to show simple dialogues and ask the player questions.
We also include the format library to let us format arbitrary text, and we also define the local reward to be 50,000 credits in exponential notation.
The function contains of 3 main parts:
- We first check to see if the player has enough space for the apples with
player.pilot():cargoFree()and display a message and return from the function if not. - We then ask the player if then ask the player if they want to deliver apples to Caladan and if they don’t, we give a message and return from the function.
- Finally, we accept the mission, adding it to the player’s active mission list, set the details, add the cargo to the player, and define a hook on when the player lands to run the final part of the mission.
Functions like
misn.markerAddadd markers on the spob the player has to go to, making it easier to complete the mission. The On-Screen Display (OSD) is also set with the mission details to guide the player withmisn.osdCreate.
Some important notes.
- We use
fmt.fto format the strings. In this case, the{spb}will be replaced by thespbfield in the table, which corresponds to the name of themem.destspob. This is further explained in Formatting Text. - Variables don’t get saved unless they are in the
memtable. This table gets populated again every time the save game gets loaded. More details in Memory Model - You have to pass function names as strings to the family of
hook.*functions. More details on hooks in Hooks.
Now this gives us almost the entirety of the mission, but a last crucial component is missing: we need to reward the player when they deliver the cargo to Caladan.
We do this by exploiting the hook.land that makes it so our defined land function gets called whenever the player lands.
We can define one as follows:
local neu = require "common.neutral"
function land ()
if spob.cur() ~= mem.dest then
return
end
vn.msg(_("Winner"), _("You win!"))
neu.addMiscLog( _("You helped deliver apples!") )
player.pay( reward )
misn.finish(true)
end
We can see it’s very simple.
It first does a check to make sure the landed planet spob.cur() is indeed the destination planet mem.dest.
If not, it returns, but if it is, it’ll display a message, add a message to the ship log, pay the player, and finally finish the mission with misn.finish(true).
Remember that since this is defined to be a unique mission, once the mission is done it will not appear again to the same player.
That concludes our very simple introduction to mission writing. Note that it doesn’t handle things like playing victory sounds, nor other more advanced functionality. However, please refer to the full example that covers more advanced functionality.
Basics
Basics
In this section we will discuss basic and fundamental aspects of mission and event developments that you will have to take into account in almost all cases.
Header
Headers contain all the necessary data about a mission or event to determine where and when they should be run. They are written as XML code embedded in a Lua comment at the top of each individual mission or event. In the case a Lua file does not contain a header, it is ignored and not loaded as a mission or event.
The header has to be at the top of the file starting with --[[ and ending with --]] which are long Lua comments with newlines.
A full example is shown below using all the parameters, however, some are contradictory in this case.
--[[
<?xml version='1.0' encoding='utf8'?>
<mission name="Mission Name">
<unique />
<chance>5</chance>
<location>Bar</location>
<chapter>[^0]</chapter>
<spob>Caladan</spob>
<faction>Empire</faction>
<system>Delta Pavonis</system>
<cond>player.credits() > 10e3</cond>
<done>Another Mission</done>
<priority>4</priority>
<tags>
<some_random_binary_tag />
</tags>
<notes />
</mission>
--]]
Let us go over the different parameters.
To start, either a <mission> or <event> node is necessary as the root for either missions (located in dat/missions/) or events (located in dat/events/).
The name attribute has to be set to a unique string and will be used to identify the mission.
Next it is possible to identify mission properties.
In particular, only the <unique /> property is supported, which indicates the mission can only be completed once.
It will not appear again to the same player.
The header includes all the information about mission availability. Most are optional and ignored if not provided. The following nodes can be used to control the availability:
- chance (required field): Indicates the chance that the mission appears. For values over 100, the whole part of dividing the value by 100 indicates how many instances can spawn, and the remainder is the chance of each instance. So, for example, a value of 320 indicates that 3 instances can spawn with 20% each.
- location (required field): Indicates where the mission or event can start.
It can be one of
none,land,enter,load,computer, orbar. Note that not all are supported by both missions and events. More details will be discussed later in this section. - unique: the presence of this tag indicates the mission or event is unique and will not appear again once fully completed.
- chapter: indicates what chapter it can appear in.
Note that this is regular expression-powered.
Something like
0will match chapter 0 only, while you can write[01]to match either chapter 0 or 1. All chapters except 0 would be[^0], and such. Please refer to a regular expression guide such as regexr for more information on how to write regex. - faction: must match a faction.
Multiple can be specified, and only one has to match.
In the case of
land,computer, orbarlocations it refers to the spob faction, while forenterlocations it refers to the system faction. - spob: must match a specific spob.
Only used for
land,computer, andbarlocations. Only one can be specified. - system: must match a specific system.
Only used for
enterlocation and only one can be specified. - cond: arbitrary Lua conditional code.
The Lua code must return a boolean value.
For example
player.credits() > 10e3would mean the player having more than 10,000 credits. Note that since this is XML, you have to escape<and>with<and>, respectively. Multiple expressions can be hooked withandandorlike regular Lua code. If the code does not contain anyreturnstatements,returnis prepended to the string. - done: indicates that the mission must be done. This allows to create mission strings where one starts after the next one.
- priority: indicates what priority the mission has. Lower priority makes the mission more important. Missions are processed in priority order, so lower priority increases the chance of missions being able to perform claims. If not specified, it is set to the default value of 5.
The valid location parameters are as follows:
| Location | Event | Mission | Description |
|---|---|---|---|
| none | ✔ | ✔ | Not available anywhere. |
| land | ✔ | ✔ | Run when player lands |
| enter | ✔ | ✔ | Run when the player enters a system. |
| load | ✔ | Run when the game is loaded. | |
| computer | ✔ | Available at mission computers. | |
| bar | ✔ | Available at spaceport bars. |
Note that availability differs between events and missions.
Furthermore, there are two special cases for missions: computer and bar that both support an accept function.
In the case of the mission computer, the accept function is run when the player tries to click on the accept button in the interface.
On the other hand, the spaceport bar accept function is called when the NPC is approached.
This NPC must be defined with misn.setNPC to be approachable.
Also notice that it is also possible to define arbitrary tags in the <tags> node.
This can be accessed with player.misnDoneList() and can be used for things such as handling faction standing caps automatically.
Finally, there is a <notes> section that contains optional metadata about the metadata.
This is only used by auxiliary tools to create visualizations of mission maps.
Example: Cargo Missions
Cargo missions appear at the mission computer in a multitude of different factions.
Since they are not too important, they have a lower than default priority (6).
Furthermore, they have 9 independent chances to appear, each with 60% chance.
This is written as <chance>960</chance>.
The full example is shown below (--[[ and --]] are omitted):
<?xml version='1.0' encoding='utf8'?>
<mission name="Cargo">
<priority>6</priority>
<chance>960</chance>
<location>Computer</location>
<faction>Dvaered</faction>
<faction>Empire</faction>
<faction>Frontier</faction>
<faction>Goddard</faction>
<faction>Independent</faction>
<faction>Sirius</faction>
<faction>Soromid</faction>
<faction>Za'lek</faction>
<notes>
<tier>1</tier>
</notes>
</mission>
Example: Antlejos
Terraforming antlejos missions form a chain.
Each mission requires the previous one and are available at the same planet (Antlejos V) with 100% chance.
The priority is slightly lower than default to try to ensure the claims get through.
Most missions trigger on Land (<location>Land</location>) because Antlejos V does not have a spaceport bar at the beginning.
The full example is shown below:
<?xml version='1.0' encoding='utf8'?>
<mission name="Terraforming Antlejos 3">
<unique />
<priority>4</priority>
<chance>100</chance>
<location>Land</location>
<spob>Antlejos V</spob>
<done>Terraforming Antlejos 2</done>
<notes>
<campaign>Terraforming Antlejos</campaign>
</notes>
</mission>
Example: Taiomi
Next is an example of a unique event.
The Finding Taiomi event has a 100% of appearing in the Bastion system outside of Chapter 0.
It triggers automatically when entering the system (<location>enter</location>).
<?xml version='1.0' encoding='utf8'?>
<event name="Finding Taiomi">
<location>enter</location>
<unique />
<chance>100</chance>
<cond>system.cur() == system.get("Bastion")</cond>
<chapter>[^0]</chapter>
<notes>
<campaign>Taiomi</campaign>
</notes>
</event>
Memory Model
By default, variables in Lua scripts are not saved when the player saves the game. This means that all the values you have set up will be cleared if the player saves and loads. This can lead to problems with scripts that do the following:
local dest
function create ()
dest = spob.get("Caladan")
-- ...
hook.land( "land" )
end
function land ()
if spob.cur() == dest then -- This is wrong!
-- ...
end
end
In the above script, a variable called dest is created, and when the mission is created, it gets set to spob.get("Caladan").
Afterwards, it gets used in land which is triggered by a hook when the player lands.
For this mission, the value dest will be set as long as the player doesn’t save and load.
When the player saves and loads, the value dest gets set to nil by default in the first line.
However, upon loading, the create function doesn’t get run again, while the hook is still active.
This means that when the player lands, spob.cur() will be compared with dest will not have been set, and thus always be false.
In conclusion, the player will never be able to finish the mission!
How do we fix this?
The solution is the mission/event memory model.
In particular, all mission / event instances have a table that gets set called mem.
This table has the particular property of being persistent, i.e., even if the player saves and loads the game, the contents will not change!
We can then use this table and insert values to avoid issues with saving and loading games.
Let us update the previous code to work as expected with saving and loading.
function create ()
mem.dest = spob.get("Caladan")
-- ...
hook.land( "land" )
end
function land ()
if spob.cur() == mem.dest then
-- ...
end
end
We can see the changes are minimal.
We no longer declare the dest variable, and instead of setting and accessing dest, we use mem.dest, which is the dest field of the mem persistent memory table.
With these changes, the mission is now robust to saving and loading!
It is important to note that almost everything can be stored in the mem table, and this includes other tables.
However, make sure to not create loops, or it will hang the saving of the games.
The most common use of the persistent memory table mem is variables that keep track of the mission progress, such as if the player has delivered cargo or has talked to a certain NPC.
Mission Variables
Mission variables allow storing arbitrary variables in save files.
Unlike the mem per-mission/event memory model, these are per-player and can be read and written by any Lua code.
The API is available as part of the var module.
The core of the var module is three functions:
var.peek( varname ): allows obtaining the value of a mission variable calledvarname. If it does not exist it returnsnil.var.push( varname, value ): creates a new mission variablevarnameor overwrites an existing mission variablevarnameif it exists with the valuevalue. Note that not all data types are supported, but many are.var.pop( varname): removes a mission variable.
It is common to use mission variables to store outcomes in mission strings that affect other missions or events.
Since they can also be read by any Lua code, they are useful in <cond> header statements too.
Supported variable types are number, boolean, string, and time.
If you want to pass systems and other data, you have to pass it via untranslated name :nameRaw() and then use the corresponding .get() function to convert it to the corresponding type again.
Hooks
Hooks are the basic way missions and events can interact with the game.
They are accessed via the hook.* API and basically serve the purpose of binding script functions to specific in-game events or actions. A full list of the hook API is available here and the API is always available in missions and events.
Hooks are saved and loaded automatically.
The basics to using hooks is as follows:
function create ()
-- ...
hook.land( "land" )
end
function land ()
-- ...
end
In this example, at the end of the create function, the local function land is bound to the player landing with hook.land.
Thus, whenever the player lands, the script function land will be run.
All hook functions return a hook ID that can be used to remove the hook with hook.rm.
For example, we can write a slightly more complicated example as such:
function create ()
-- ...
mem.hook_land = hook.land( "land" )
mem.hook_enter = hook.enter( "enter" )
end
function land ()
-- ...
end
function enter ()
hook.rm( mem.hook_land )
hook.rm( mem.hook_enter )
end
The above example is setting up a land hook when the player lands, and an enter hook, which activates whenever the player enters a system by either taking off or jumping. Both hooks are stored in persistent memory, and are removed when the enter function is run when the player enters a system.
Each mission or event can have an infinite number of hooks enabled.
Except for timer and safe hooks, hooks do not get removed when run.
Timer Hooks
Timer hooks are hooks that get run once when a certain amount of real in-game time has passed. Once the hook is triggered, it gets removed automatically. If you wish to repeat a function periodically, you have to create a new timer hook. A commonly used example is shown below.
function create ()
-- ...
hook.enter( "enter" )
end
function enter ()
-- ...
hook.timer( 5, "dostuff" )
end
function dostuff ()
if condition then
-- ...
return
end
-- ...
hook.timer( 5, "dostuff" )
end
In this example, an enter hook is created and triggered when the player enters a system by taking off or jumping.
Then, in the enter function, a 5-second timer hook is started that runs the dostuff function when the time is up.
The dostuff function then checks a condition to do something and end, otherwise it repeats the 5-second hook.
This system can be used to, for example, detect when the player is near a pilot or position, or display periodic messages.
Timer hooks persist even when the player lands and takes off.
If you wish to clear them, please use hook.timerClear(), which will remove all the timers created by the mission or event calling the function.
This can be useful in combination with hook.enter.
Pilot Hooks
When it comes to pilots, hooks can also be used. However, given that pilots are not saved, the hooks are not saved either. The hooks can be made to be specific to a particular pilot, or apply to any pilot. In either case, the pilot triggering the hook is passed as a parameter. An illustrative example is shown below:
function enter ()
-- ...
local p = pilot.add( "Llama", "Independent" )
hook.pilot( p, "death", "pilot_died" )
end
function pilot_died( p )
-- ...
end
In the above example, when the player enters a system with the enter function, a new pilot p is created, and a "death" hook is set on that pilot.
Thus, when the pilot p dies, the pilot_dead function will get called.
Furthermore, the pilot_died function takes the pilot that died as a parameter.
There are other hooks for a diversity of pilot actions that are documented in the official API documentation, allowing for full control of pilot actions.
Translation Support
Naev supports translation through codeberg Weblate. However, in order for translations to be used you have to mark strings as translatable. This is done with a gettext compatible interface. In particular, the following functions are provided:
_(): This function takes a string, marks it as translatable, and returns the translated version.N_(): This function takes a string, marks it as translatable, however, it returns the untranslated version of the string.n_(): Takes two strings related to a number quantity and return the translated version that matches the number quantity. This is because some languages translate number quantities differently. For example “1 apple”, but “2 apples”.p_(): This function takes two strings, the first is a context string, and the second is the string to translate. It returns the translated string. This allows to disambiguate same strings based on context such asp_( "main menu", "Close" )andp_( "some guy", "Close" ). In this case"Close"can be translated differently based on the context strings.
In general, you want to use _() and n_() to envelop all strings that are being shown to the player, which will allow for translations to work without extra effort.
For example, when defining a new mission you want to translate all the strings as shown below:
misn.setTitle( _("My Mission") )
misn.setDesc( _("You have been asked to do lots of fancy stuff for a very fancy individual. How fancy!") )
misn.setReward( _("Lots of good stuff!") )
Note that _() and friends all assume that you are inputting strings in English.
It is important to note that strings not shown to the player, e.g., strings representing faction names or ship names, do not need to be translated!
So when adding a pilot you can just use directly the correct strings for the ship and faction (e.g., "Hyena" and "Mercenary"):
pilot.add( "Hyena", "Mercenary", nil, _("Cool Dude") )
Note that the name (Cool Dude in this case) does have to be translated!
For plurals, you have to use n_() given that not all languages pluralize like in English.
For example, if you want to indicate how many pirates are left, you could do something like:
player.msg(string.format(n_( "%d pirate left!", "%d pirates left!", left ), left ))
The above example says how many pirates are left based on the value of the variable left.
In the case there is a single pirate left, the singular form should be used in English, which is the first parameter.
For other cases, the plural form is used.
The value of the variable left determines which is used based on translated language.
Although the example above uses string.format to display the number value for illustrative purposes, it is recommended to format text with the format library explained below.
Formatting Text
An important part of displaying information to the player is formatting text.
While string.format exists, it is not very good for translations, as the Lua version can not change the order of parameters unlike C.
For this purpose, we have prepared the format library, which is much more intuitive and powerful than string.format.
A small example is shown below:
local fmt = require "format"
function create ()
-- ...
local spb, sys = spob.getS( "Caladan" )
local desc = fmt.f( _("Take this cheese to {spb} ({sys}), {name}."),
{ spb=spb, sys=sys, name=player.name() } )
misn.setDesc( desc )
end
Let us break down this example.
First, we include the library as fmt.
This is the recommended way of including it.
Afterwards, we run fmt.f which is the main formatting function.
This takes two parameters: a string to be formatted, and a table of values to format with.
The string contains substrings of the form "{foo}", that is, a variable name surrounded by { and }.
Each of these substrings is replaced by the corresponding field in the table passed as the second parameter, which are converted to strings.
So, in this case, {spb} gets replaced by the value of table.spb which in this case is the variable spb that corresponds to the Spob of Caladan.
This gets converted to a string, which in this case is the translated name of the planet.
If any of the substrings are missing and not found in the table, it will raise an error.
There are additional useful functions in the format library.
In particular the following:
format.number: Converts a non-negative integer into a human-readable number as a string. Gets rounded to the nearest integer.format.credits: Displays a credit value with the credit symbol ¤.format.reward: Used for displaying mission rewards.format.tonnes: Used to convert tonne values to strings.format.list: Displays a list of values with commas and the word “and”. For examplefmt.list{"one", "two", "three"}returns"one, two, and three".format.humanize: Converts a number string to a human-readable rough string such as"1.5 billion".
More details can be found in the generated documentation.
Colouring Text
All string printing functions in Naev accept special combinations to change the colour.
This will work whenever the string is shown to the player.
In particular, the character # is used for a prefix to set the colour of text in a string.
The colour is determined by the character after #.
In particular, the following are valid values:
| Symbol | Description |
|---|---|
#0 | Resets colour to the default value. |
#r | Red colour. |
#g | Green colour. |
#b | Blue colour. |
#o | Orange colour. |
#y | Yellow colour. |
#w | White colour. |
#p | Purple colour. |
#n | Grey colour. |
#F | Colour indicating friend. |
#H | Colour indicating hostile. |
#N | Colour indicating neutral. |
#I | Colour indicating inert. |
#R | Colour indicating restricted. |
Multiple colours can be used in a string such as "It is a #ggood#0 #rmonday#0!".
In this case, the word "good" is shown in green, and "monday" is shown in red.
The rest of the text will be shown in the default colour.
While it is possible to accent and emphasize text with this, it is important to not go too overboard, as it can difficult translating.
When possible, it is also best to put the colour outside the string being translated.
For example _("#rred#0") should be written as "#r".._("red").."#0".
System Claims
One important aspect of mission and event development are system claiming. Claims serve the purpose of avoiding collisions between Lua code. For example, pilot.clear() allows removing all pilots from a system. However, say that there are two events going on in a system. They both run pilot.clear() and add some custom pilots. What will happen then, is that the second event to run will get rid of all the pilots created from the first event, likely resulting in Lua errors. This is not what we want is it? In this case, we would want both events to try to claim the system and abort if the system was already claimed.
Systems can be claimed with either misn.claim or evt.claim depending on whether they are being claimed by a mission or an event. A mission or event can claim multiple systems at once, and claims can be exclusive (default) or inclusive. Exclusive claims don’t allow any other system to claim the system, while inclusive claims can claim the same system. In general, if you use things like pilot.clear() you should use exclusive claims, while if you don’t mind if other missions / events share the system, you should use inclusive claims. You have to claim all systems that your mission uses to avoid collisions!
Let us look at the standard way to use claims in a mission or event:
function create ()
if not misn.claim( {system.get("Gamma Polaris")} ) then
misn.finish(false)
end
-- ...
end
The above mission tries to claim the system "Gamma Polaris" right away in the create function. If it fails and the function returns false, the mission then finishes unsuccessfully with misn.finish(false). This will cause the mission to only start when it can claim the "Gamma Polaris" system and silently fail otherwise. You can pass more systems to claim them, and by default they will be exclusive claims.
Say our event only adds a small derelict in the system, and we don’t mind it sharing the system with other missions and events. Then we can write the event as:
function create ()
if not evt.claim( {system.get("Gamma Polaris")}, true ) then
evt.finish(false)
end
-- ...
end
In this case, the second parameter is set to true which indicates that this event is trying to do an inclusive claim. Again, if the claiming fails, the event silently fails.
Claims can also be tested in an event/mission-neutral way with naev.claimTest. However, this can only test the claims. Only misn.claim and evt.claim can enforce claims for missions and events, respectively.
As missions and events are processed by priority, make sure to give higher priority to those that you want to be able to claim easier. Otherwise, they will have difficulties claiming systems and may never appear to the player. Minimizing the number of claims and cutting up missions and events into smaller parts is also a way to minimize the amount of claim collisions.
Mission Cargo
Cargo given to the player by missions using misn.cargoAdd is known as Mission Cargo.
This differs from normal cargo in that only the player’s ship can carry it (escorts are not allowed to), and that if the player jettisons it, the mission gets aborted.
Missions and events can still add normal cargo through pilot.cargoAdd or player.fleetCargoAdd, however, only missions can have mission cargo.
It is important to note that when the mission finishes, all associated mission cargos of the mission are also removed!
The API for mission cargo is fairly simple and relies on three functions:
misn.cargoAdd: takes a commodity or string with a commodity name, and the amount to add. It returns the id of the mission cargo. This ID can be used with the other mission cargo functions.misn.cargoRm: takes a mission cargo ID as a parameter and removes it. Returns true on success, false otherwise.misn.cargojet: same asmisn.cargoRm, but it jets the cargo into space (small visual effect).
Custom Commodities
Commodities are generally defined in dat/commodities/, however, it is a common need for a mission to have custom cargo.
Instead of bloating the commodity definitions, it is possible to create arbitrary commodities dynamically.
Once created, they are saved with the player, but will disappear when the player gets rid of them.
There are two functions to handle custom commodities:
commodity.new: takes the name of the cargo, description, and an optional set of parameters and returns a new commodity. If it already exists, it returns the commodity with the same name. It is important to note that you have to pass untranslated strings. However, in order to allow for translation, they should be used withN_().commodity.illegalto: makes a custom commodity illegal to a faction, and takes the commodity and a faction or table of factions to make the commodity illegal to as parameters. Note that this function only works with custom commodities.
An full example of adding a custom commodity to the player is as follows:
local c = commodity.new( N_("Smelly Cheese"), N_("This cheese smells really bad. It must be great!") )
c:illegalto( {"Empire", "Sirius"} )
mem.cargo_id = misn.cargoAdd( c, 1 )
-- Later it is possible to remove the cargo with misn.cargoRm( mem.cargo_id )
Ship Log
The Ship Log is a framework that allows recording in-game events so that the player can easily access them later on.
This is meant to help players that haven’t logged in for a while or have forgotten what they have done in their game.
The core API is in the shiplog module and is a core library that is always loaded without the need to require.
It consists of two functions:
shiplog.create: takes three parameters, the first specifies the ID of the log (string), the second the name of the log (string, visible to player), and the third is the logtype (string, visible to player and used to group logs).shiplog.append: takes two parameters, the first specifies the ID of the log (string), and second is the message to append. The ID should match one created byshiplog.create.
The logs have the following hierarchy: logtype → log name → message.
The logtype and log name are specified by shiplog.create and the messages are added with shiplog.append.
Since, by default, shiplog.create doesn’t overwrite existing logs, it’s very common to write a helper log function as follows:
local function addlog( msg )
local logid = "my_log_id"
shiplog.create( logid, _("Secret Stuff"), _("Neutral") )
shiplog.append( logid, msg )
end
You would use the function to quickly add log messages with addlog(_("This is a message relating to secret stuff.")).
Usually logs are added when important one-time things happen during missions or when they are completed.
Visual Novel Framework
The Visual Novel framework is based on the Love2D API and allows for displaying text, characters, and other effects to the player.
It can be thought of as a graph representing the choices and messages the player can engage with.
The core API is in the vn module.
The VN API is similar to existing frameworks such as Ren’Py, in which conversations are divided into scenes with characters. In particular, the flow of engaging the player with the VN framework consists roughly of the following:
- Reset internal state (recommended)
- Start a new scene
- Define all the characters that should appear in the scene (they can still be added and removed in the scene with
vn.appearandvn.disappear) - Run the transition to make the characters and scene appear
- Display text
- Jump to 2. as needed or end the
vn
For most purposes, all you will need is a single scene, however, you are not limited to that.
The VN is based around adding nodes which represent things like displaying text or giving the player options.
Once the conversation graph defined by the nodes is set up, vn.run() will begin execution and it won’t return until the dialogue is done.
Nodes are run in consecutive order unless vn.jump is used to jump to a label node defined with vn.label.
Let us start by looking at a simple example:
local vn = require "vn" -- Load the library
-- Below would be what you would include when you want the dialogue
vn.reset() -- Resets internal state
vn.scene() -- Start a new scene
local mychar = vn.newCharacter( _("Alex"), {image="mychar.webp"} )
vn.transition() -- Will fade in the new character
vn.na(_([[You see a character appear in front of you.]]) -- Narrator
mychar(_([[How do you do?]])
vn.menu{ -- Give a list of options the player chooses from
{_("Good."), "good"},
{_("Bad."), "bad"},
}
vn.label("good") -- Triggered when the "good" option is chosen
mychar(_("Great!"))
vn.done() -- Finish
vn.label("bad") -- Triggered on "bad" option
mychar(_("That's not good…"))
vn.run()
Above is a simple example that creates a new scene with a single character (mychar), introduces the character with the narrator (vn.na), has the character talk, and then gives two choices to the player that trigger different messages.
By default the vn.transition() will do a fading transition, but you can change the parameters to do different ones.
The narrator API is always available with vn.na, and once you create a character with vn.newCharacter, you can simple call the variable to have the character talk.
The character images are looking for in the gfx/vn/characters/ directory, and in this case it would try to use the file gfx/vn/characters/mychar.webp.
Player choices are controlled with vn.menu which receives a table where each entry consists of another table with the first entry being the string to display (e.g., _("Good.")), and the second entry being either a function to run, or a string representing a label to jump to (e.g., "good").
In the case of passing strings, vn.jump is used to jump to the label, so that in the example above the first option jumps to vn.label("good"), while the second one jumps to vn.label("bad").
By using vn.jump, vn.label, and vn.menu it is possible to create complex interactions and loops.
It is recommended to look at existing missions for examples of what can be done with the vn framework.
vntk Wrapper
The full vn framework can be a bit verbose when only displaying small messages or giving small options.
For this purpose, the vntk module can simplify the usage, as it is a wrapper around the vn framework.
Like the vn framework, you have to import the library with require, and all the functions are blocking, that is, the Lua code execution will not continue until the dialogues have closed.
Let us look at some simple examples of vntk.msg and vntk.yesno below:
local vntk = require "vntk"
-- …
vntk.msg( _("Caption"), _("Some message to show to the player.") )
-- …
if vntk.yesno( _("Cheese?"), _("Do you like cheese?") ) then
-- player likes cheese
else
-- player does not
end
The code is very simple and requires the library.
Then it will display a message, and afterwards, it will display another with a Yes and No prompt.
If the player chooses yes, the first part of the code will be executed, and if they choose no, the second part is executed.
Arbitrary Code Execution
It is also possible to create nodes in the dialogue that execute arbitrary Lua code, and can be used to do things such as pay the player money or modify mission variables.
Note that you can not write Lua code directly, or it will be executed when the vn is being set up.
To have the code run when triggered by the vn framework, you must use vn.func and pass a function to it.
A very simple example would be
-- ...
vn.label("pay_player")
vn.na(_("You got some credits!"))
vn.func( function ()
player.pay( 50e3 )
end )
-- ...
It is also to execute conditional jumps in the function code with vn.jump.
This would allow to condition the dialogue on things like the player’s free space or amount of credits as shown below:
-- ...
vn.func( function ()
if player.pilot():cargoFree() < 10 then
vn.jump("no_space")
else
vn.jump("has_space")
end
end )
vn.label("no_space")
-- ...
vn.label("has_space")
-- ...
In the code above, a different dialogue will be run depending on whether the player has less than 10 free cargo space or more than that.
As you can guess, vn.func is really powerful and opens up all sorts of behaviour.
You can also set local or global variables with it, which is very useful to detect if a player has accepted or not a mission.
Advanced
This section covers more complex implementation of features in missions and events that you may need, but are usually not necessary to master in contrast with mission and event basics.
Handling Aborting Missions
When missions are aborted, the abort function is run if it exists.
Although this function can’t stop the mission from aborting, it can be used to clean up the mission stuff, or even start events such as a penalty for quitting halfway through the mission.
A representative example is below:
local vntk = require "vntk"
...
function abort ()
vntk.msg(_("Mission Failure!"),_([[You have failed the mission, try again next time!]]))
end
Note that it is not necessary to run misn.finish() nor any other clean up functions; this is all done for you by the engine.
Cutscenes
Cutscenes are a powerful way of conveying events that the player may or may not interact with.
In order to activate cinematic mode, you must use player.cinematics function.
However, the player will still be controllable and escorts will be doing their thing.
If you want to make the player and escorts stop and be invulnerable, you can use the cinema library.
In particular, the cinema.on function enables cinema mode and cinema.off disables it.
You can also control where the camera is with camera.set().
By default, it will try to centre the camera on the player, but if you pass a position or pilot as a parameter, it will move to the position or follow the pilot, respectively.
The cornerstone of cutscenes is to use hooks to make things happen and show that to the player.
In this case, one of the most useful hooks is the hook.timer timer hook.
Let us put it all together to do a short example.
local cinema = require "cinema" -- load the cinema library
...
local someguy -- assume some pilot is stored here
-- function that starts the cutscene
function cutscene00 ()
cinema.on()
camera.set( someguy ) -- make the camera go to someguy
hook.timer( 5, "cutscene01" ) -- advance to next step in 5 seconds
end
function cutscene01 ()
someguy:broadcast(_("I like cheese!"),true) -- broadcast so the player can always see it
hook.timer( 6, "cutscene02" ) -- give 6 seconds for the player to see
end
function cutscene02 ()
cinema.off()
camera.set()
end
Breaking down the example above, the cutscene itself is made of 3 functions.
The first cutscene00 initializes the cinematic mode and sets the camera to someguy.
Afterwards, cutscene01 makes someguy say some text and shows it to the player. Finally, in cutscene02, the cinematic mode is finished and the camera is returned to the player.
While that is the basics, there is no limit to what can be done. It is possible to use shaders to create more visual effects, or the luaspfx library. Furthermore, pilots can be controlled and made to do all sorts of actions. There is no limit to what is possible!
Event-Mission Communication
In general, events and missions are to be seen as self-contained isolated entities, that is, they do not affect each other outside of mission variables.
However, it is possible to exploit the hook module API to overcome this limitation with hook.custom and naev.trigger:
hook.custom: allows defining an arbitrary hook on an arbitrary string. The function takes two parameters: the first is the string to hook (should not collide with standard names), and the second is the function to run when the hook is triggered.naev.trigger: also takes two parameters and allows triggering the hooks set byhook.custom. In particular, the first parameter is the same as the first parameter string passed tohook.custom, and the second optional parameter is data to pass to the custom hooks.
For example, you can define a mission to listen to a hook as below:
function create ()
-- ...
hook.custom( "my_custom_hook_type", "dohook" )
end
function dohook( param )
print( param )
end
In this case, "my_custom_hook_type" is the name we are using for the hook.
It is chosen to not conflict with any of the existing names.
When the hook triggers, it runs the function dohook which just prints the parameter.
Now, we can trigger this hook from anywhere simply by using the following code:
naev.trigger( "my_custom_hook_type", some_parameter )
The hook will not be triggered immediately, but the second the current running code is done to ensure that no Lua code is run in parallel. In general, the mission variables should be more than good enough for event-mission communication, however, in the few cases communication needs to be more tightly coupled, custom hooks are a perfect solution.
LÖVE API
LÖVE is an awesome framework you can use to make 2D games in Lua. It’s free, open-source, and works on Windows, Mac OS X, Linux, Android and iOS.
Naev implements a subset of the LÖVE API (also known as Love2D), allowing it to execute many Love2D games out of the box. Furthermore, it is possible to use the Naev API from inside the Love2D to have the games interact with the Naev engine. In particular, the Visual Novel Framework, minigames, etc. are implemented using the Love2D API. Many of the core game functionality, such as the boarding or communication menus make use of this API also, albeit indirectly.
The Love2D API works with a custom dialogue window that has to be started up. There are two ways to do this: create a Love2D game directory and run them, or set up the necessary functions and create the Love2D instance. Both are very similar.
The easiest way is to create a directory with a main.lua file that will be run like a normal Love2D game.
At the current time the Naev Love2D API does not support zip files.
An optional conf.lua file can control settings of the game.
Afterwards you can start the game with:
local love = require "love"
love.exec( "path/to/directory" )
If the directory is a correct Love2D game, it will create a window instead of Naev and be run in that.
You can use love.graphics.setBackgroundColor( 0, 0, 0, 0 ) to make the background transparent, and the following conf.lua function will make the virtual Love2D window use the entire screen, allowing you to draw normally on the screen.
function love.conf(t)
t.window.fullscreen = true
end
The more advanced way to set up Love2D is to directly populate the love namespace with the necessary functions, such as love.load, love.conf. love.draw, etc.
Afterwards you can use love.run() to start the Love2D game and create the virtual window.
This way is much more compact and does not require creating a separate directory structure with a main.lua.
Please note that while most of the core Love2D 11.4 API is implemented, more niche API and things that depend on external libraries like love.physics, lua-enet, or luasocket are not implemented.
If you wish to have missing API added, it is possible to open an issue for the missing API or create a pull request.
Differences with Love2D API
Some of the known differences with the Love2D API are as follows:
- You can call images or canvases to render them with
object:draw( ... )instead of onlylove.graphics.draw( obj, ... ). - Fonts default to Naev fonts.
- You can use Naev colour strings such as
"#b"inlove.graphics.printandlove.graphics.printf. audio.newSourcedefaults to second paramater"static"unless specified (older Love2D versions defaulted to"stream", and it must be set explicity in newer versions).love.graphics.setBackgroundColoruses alpha colour to set the alpha of the window, with 0 making the Love2D window not drawn.
Tips and Tricks
Tips and Tricks
This section contains some tricks and tips for better understanding how to do specific things in missions and events.
Optimizing Loading
It is important to understand how missions and events are loaded. The headers are parsed at the beginning of the game and stored in memory. Whenever a trigger (entering a system, landing on a spob, etc.) happens, the game runs through all the missions and events to check to see if they should be started. The execution is done in the following way:
- Header statements are checked (e.g., unique missions that are already done are discarded)
- Lua conditional code is compiled and run
- Lua code is compiled and run
createfunction is run
In general, since many events and missions can be checked at every frame, it is important to try to cull them as soon as possible. When you can, use location or faction filters to avoid having missions and events appear in triggers you don’t wish them to. In the case that is not possible, try to use the Lua conditional code contained in the <cond> node in the header. You can either write simple conditional statements such as player.credits() > 50e3, where return gets automatically prepended, or you can write more complex code where you have to manually call return and return a boolean value. Do note, however, that it is not possible to reuse variables or code in the <cond> node in the rest of the program. If you have to do expensive computations and wish to use the variables later on, it is best to put the conditional code in the create function and abort the mission or event with misn.finish(false) or evt.finish(false), respectively.
Furthermore, when a mission or event passes the header and conditional Lua statements, the entire code gets compiled and run. This implies that all global variables are computed. If you load many graphics, shaders, or sound files as global values, this can cause a slowdown whenever the mission is started. An early issue with the visual novel framework was that all cargo missions were loading the visual novel framework that were loading lots of sounds and shaders. Since this was repeated for every mission in the mission computer, it created noticeable slowdowns. This was solved by relying on lazy loading and caching, and not just blindly loading graphics and audio files into global variables on library load.
Global Cache
In some cases that you want to load large amount of data once and reuse it throughout different instances of events or missions, it is possible to use the global cache with naev.cache(). This function returns a table that is accessible by all the Lua code. However, this cache is cleared every time the game starts. You can not rely on elements in this cache to be persistent. It is common to wrap around the cache with the following code:
local function get_calculation ()
local nc = naev.cache()
if nc.my_calculation then
return nc.my_calculation
end
nc.my_calculation = do_expensive_calculation ()
return nc.my_calculation
end
The above code tries to access data in the cache. However, if it does not exist (by default all fields in Lua are nil), it will do the expensive calculation and store it in the cache. Thus, the first call of get_calculation() will be slow, however, all subsequent calls will be very fast as no do_expensive_calculation() gets called.
Finding Natural Pilots $\naev$
In some cases, you want a mission or event to do things with naturally spawning pilots, and not spawn new ones. Naturally spawned pilots have the natural flag set in their memory. You can access this with p:memory().natural and use this to limit boarding hooks and the likes to only naturally spawned pilots. An example would be:
function create ()
-- ...
hook.board( "my_board" )
end
function my_board( pilot_boarded )
if not pilot_boarded:memory().natural then
return
end
-- Do something with natural pilots here
end
In the above example, we can use a board hook to control when the player boards a ship, and only handle the case that naturally spawning pilots are boarded.
Making Aggressive Enemies
TODO Explain how to nudge the enemies without relying on pilot:control().
Working with Player Fleets
TODO Explain how to detect and/or limit player fleets.
Full Example
Below is a full example of a mission.
--[[
<?xml version='1.0' encoding='utf8'?>
<mission name="Mission Template (mission name goes here)">
<unique/>
<priority>4</priority>
<chance>5</chance>
<location>Bar</location>
</mission>
--]]
--[[
Mission Template (mission name goes here)
This is a Naev mission template.
This document aims to provide a structure on which to build many
Naev missions and teach how to make basic missions in Naev.
For more information on Naev, please visit: http://naev.org/
Naev missions are written in the Lua programming language: http://www.lua.org/
There is documentation on Naev's Lua API at: http://api.naev.org/
You can study the source code of missions in [path_to_Naev_folder]/dat/missions/
When creating a mission with this template, please erase the explanatory
comments (such as this one) along the way, but retain the the MISSION and
DESCRIPTION fields below, adapted to your mission.
MISSION: <NAME GOES HERE>
DESCRIPTION: <DESCRIPTION GOES HERE>
--]]
-- require statements go here. Most missions should include
-- "format", which provides the useful `number()` and
-- `credits()` functions. We use these functions to format numbers
-- as text properly in Naev. dat/scripts/common/neutral.lua provides
-- the addMiscLog function, which is typically used for non-factional
-- unique missions.
local fmt = require "format"
local neu = require "common.neutral"
local vntk = require "vntk"
--[[
Multi-paragraph dialog strings *can* go here, each with an identifiable
name. You can see here that we wrap strings that are displayed to the
player with `_()`. This is a call to gettext, which enables
localization. The _() call should be used directly on the string, as
shown here, instead of on a variable, so that the script which figures
out what all the translatable text is can find it.
When writing dialog, write it like a book (in the present-tense), with
paragraphs and quotations and all that good stuff. Leave the first
paragraph unindented, and indent every subsequent paragraph by four (4)
spaces. Use quotation marks as would be standard in a book. However, do
*not* quote the player speaking; instead, paraphrase what the player
generally says, as shown below.
In most cases, you should use double-brackets for your multi-paragraph
dialog strings, as shown below.
One thing to keep in mind: the player can be any gender, so keep all
references to the player gender-neutral. If you need to use a
third-person pronoun for the player, singular "they" is the best choice.
You may notice curly-bracketed {words} sprinkled throughout the text. These
are portions that will be filled in later by the mission via the
`fmt.f()` function.
--]]
-- Set some mission parameters.
-- For credit values in the thousands or millions, we use scientific notation (less error-prone than counting zeros).
-- There are two ways to set values usable from outside the create() function:
-- - Define them at file scope in a statement like "local credits = 250e3" (good for constants)
-- - Define them as fields of a special "mem" table: "mem.credits = 250e3" (will persist across games in the player's save file)
local misplanet, missys = spob.getS("Ulios")
local credits = 250e3
-- Here we use the `fmt.credits()` function to convert our credits
-- from a number to a string. This function both applies gettext
-- correctly for variable amounts (by using the ngettext function),
-- and formats the number in a way that is appropriate for Naev (by
-- using the numstring function). You should always use this when
-- displaying a number of credits.
local reward_text = fmt.credits( credits )
--[[
First you need to *create* the mission. This is *obligatory*.
You have to set the NPC and the description. These will show up at the
bar with the character that gives the mission and the character's
description.
--]]
function create ()
-- Naev will keep the contents of "mem" across games if the player saves and quits.
-- Track mission state there. Warning: pilot variables cannot be saved.
mem.talked = false
-- If we needed to claim a system, we would do that here with
-- something like the following commented out statement. However,
-- this mission won't be doing anything fancy with the system, so we
-- won't make a system claim for it.
-- Only one mission or event can claim a system at a time. Using claims
-- helps avoid mission and event collisions. Use claims for all systems
-- you intend to significantly mess with the behaviour of.
--if not misn.claim(missys) then misn.finish(false) end
-- Give the name of the NPC and the portrait used. You can see all
-- available portraits in dat/gfx/portraits.
misn.setNPC( _("A well-dressed man"), "neutral/unique/youngbusinessman.webp", _("This guy is wearing a nice suit.") )
end
--[[
This is an *obligatory* part which is run when the player approaches the
character.
Run misn.accept() here to internally "accept" the mission. This is
required; if you don't call misn.accept(), the mission is scrapped.
This is also where mission details are set.
--]]
function accept ()
-- Use different text if we've already talked to him before than if
-- this is our first time.
local text
if mem.talked then
-- We use `fmt.f()` here to fill in the destination and
-- reward text. (You may also see Lua's standard library used for similar purposes:
-- `s1:format(arg1, ...)` or equivalently string.format(s1, arg1, ...)`.)
-- You can tell `fmt.f()` to put a planet/system/commodity object into the text, and
-- (via the `tostring` built-in) know to write its name in the player's native language.
text = fmt.f(_([["Ah, it's you again! Have you changed your mind? Like I said, I just need transport to {pnt} in the {sys} system, and I'll pay you {rwd} when we get there. How's that sound?"]]), {pnt=misplanet, sys=missys, rwd=reward_text})
else
text = fmt.f(_([[As you approach the guy, he looks up in curiosity. You sit down and ask him how his day is. "Why, fine," he answers. "How are you?" You answer that you are fine as well and compliment him on his suit, which seems to make his eyes light up. "Why, thanks! It's my favourite suit! I had it custom tailored, you know.
"Actually, that reminds me! There was a special suit on {pnt} in the {sys} system, the last one I need to complete my collection, but I don't have a ship. You do have a ship, don't you? So I'll tell you what, give me a ride and I'll pay you {rwd} for it! What do you say?"]]),
{pnt=misplanet, sys=missys, rwd=reward_text})
mem.talked = true
end
-- This will create the typical "Yes/No" dialogue. It returns true if
-- yes was selected.
-- For more full-fledged visual novel API please see the vn module. The
-- vntk module wraps around that and provides a more simple and easy to use
-- interface, although it is much more limited.
if vntk.yesno( _("My Suit Collection"), text ) then
-- Followup text.
vntk.msg( _("My Suit Collection"), _([["Fantastic! I knew you would do it! Like I said, I'll pay you as soon as we get there. No rush! Just bring me there when you're ready.]]) )
-- Accept the mission
misn.accept()
-- Mission details:
-- You should always set mission details right after accepting the
-- mission.
misn.setTitle( _("Suits Me Fine") )
misn.setReward( reward_text )
misn.setDesc( fmt.f(_("A well-dressed man wants you to take him to {pnt} in the {sys} system so he can get some sort of special suit."), {pnt=misplanet, sys=missys}) )
-- Markers indicate a target planet (or system) on the map, it may not be
-- needed depending on the type of mission you're writing.
misn.markerAdd( misplanet, "low" )
-- The OSD shows your objectives.
local osd_desc = {}
osd_desc[1] = fmt.f(_("Fly to {pnt} in the {sys} system"), {pnt=misplanet, sys=missys} )
misn.osdCreate( _("Suits Me Fine"), osd_desc )
-- This is where we would define any other variables we need, but
-- we won't need any for this example.
-- Hooks go here. We use hooks to cause something to happen in
-- response to an event. In this case, we use a hook for when the
-- player lands on a planet.
hook.land( "land" )
end
-- If misn.accept() isn't run, the mission doesn't change and the player can
-- interact with the NPC and try to start it again.
end
-- luacheck: globals land (Hook functions passed by name)
-- ^^ That is a directive to Luacheck, telling it we're about to use a global variable for a legitimate reason.
-- (More info here: https://github.com/naev/naev/issues/1566) Typically we put these at the top of the file.
-- This is our land hook function. Once `hook.land( "land" )` is called,
-- this function will be called any time the player lands.
function land ()
-- First check to see if we're on our target planet.
if spob.cur() == misplanet then
-- Mission accomplished! Now we do an outro dialog and reward the
-- player. Rewards are usually credits, as shown here, but
-- other rewards can also be given depending on the circumstances.
vntk.msg( fmt.f(_([[As you arrive at {pnt}, your passenger reacts with glee. "I must sincerely thank you, kind stranger! Now I can finally complete my suit collection, and it's all thanks to you. Here is {reward}, as we agreed. I hope you have safe travels!"]]), {pnt=misplanet, reward=reward_text}) )
-- Reward the player. Rewards are usually credits, as shown here,
-- but other rewards can also be given depending on the
-- circumstances.
player.pay( credits )
-- Add a log entry. This should only be done for unique missions.
neu.addMiscLog( fmt.f(_([[You helped transport a well-dressed man to {pnt} so that he could buy some kind of special suit to complete his collection.]]), {pnt=misplanet} ) )
-- Finish the mission. Passing the `true` argument marks the
-- mission as complete.
misn.finish( true )
end
end
Star Systems (Ssys)
In the context of Naev, “systems” refer to star systems, the instanced locations where starship flight and combat take place. The contents of systems consist mainly of three object types: spobs (space objects) which represent planets, space stations or other bodies of interest; asteroid fields which act as commodity sources and obstacles to weapons fire; and jump points to facilitate travel to other systems. Many systems may also have persistent effects related to nebulae including visuals, sensor interference and even constant damage over time. A “total conversion” plugin must contain at least one system to have minimum viable content.
Each system can contain multiple Space Objects that can represent planets, space stations, wormholes, and other objects the player can usually interact with. They also contain jumps to other adjacent systems.
Universe Editor
Naev includes an in game editor to generate and modify both systems and their contents. The editor is accessible from the game’s main menu through the “Extras” menu.
The universe editor is far easier to use than direct editing of XML files.
You can quickly place new systems and drag them around the map, link systems by generating jump lanes and automatically generating entry and exit points, and create spobs, virtual spobs and asteroid fields within systems.
Data Format
Each system is represented by a standalone XML file within the /ssys/ directory of your main or plugin data directory.
Each XML file begins with a <ssys> root node that contains all the data, which consists of the following nodes:
<name>: Name of the system. Use this string when referencing this system in other XML files. This name will also be displayed within the game itself.<general>: Includes data defining the size and traits of the system.<radius>: Defines the physical dimensions of the system. This value is visualized in game by the scaling of the system travel map, and in the universe editor by a circle seen when editing systems. Jump points with the<autopos/>tag will be placed on this circle. System content such as spobs can be placed outside this radius but may be difficult for players to locate or access.<spacedust>: Defines the density of space dust displayed in the system.<interference>: Influences the sensors of ships in the system. A value greater than 0 will reduce the ranges at which you can detect, identify or destealth other ships. Reduction of detection, evasion, and stealth ranges is computed by the formula $\frac{1}{1+\frac{\text{interference}}{100}}$.<nebula>: Reduces visibility when within the system. A value greater than 0 will cause ships, spobs and asteroids to not appear until the player gets close. The rough visibility range is computed from the formula $(1200-\text{nebula}) \cdot \text{ewdetect} + \text{nebuvisibility}$, where ewdetect and nebuvisibility are each ships detection and nebula visibility statistics.<volatility>: Damage over time inflicted upon ships travelling in this system. Value is expressed in MJ per second, applied to shields first and armour after.
<features>: A string value defining unique characteristics of the system, such as whether it has a factional homeworld or some other anomality. This is shown in the in-game map.
<pos>: Position of the system on the universe map, expressed asxandycoordinates relative to the universe map’s origin point.<spobs>: Category which includes all spobs, including virtual spobs, which are present in this system.<spob>: Adds the spob of that name to the system. The coordinate position of the spob is defined within that spob’s .XML file.<spob_virtual>: Adds the virtual spob of that name to the system. Virtual spobs are used primarily for faction presences within the system.
<jumps>: Category which includes coordinates and tags for jump points which allow players to travel to other systems.<jump>: Defines a jump point.<target>: Name of the jump point’s destination system. The direction of travel when entering this jump point corresponds to that of the jump line shown on the universe map.<pos>: Position of the jump point within the system, expressed asxandycoordinates relative to the system’sx="0" y="0"origin point.<autopos/>: Alternative to<pos>which prompts the game to generate a position for the jump point. The point will always be placed at the system boundary (the circle defined by<radius>) on a line between the current system centre and the destination system.- `
: Prevents the player from detecting this jump point or entering it from the current system. These points are used exclusively as the destinations to jumps coming in from other systems.
- `
<hide>: Modifies the range at which your sensors can discover previously unknown jump points. A value of1is the default and indicates no change. Values greater than1increase the jump point’s detection distance. Values less than1but greater than0reduce the jump point’s detection distance. A value of0is a specific exception which labels the jump as part of a Trade Route - the jump point will automatically be discovered when the player enters the system, regardless of distance, and also have some small beacons next to it.<hidden/>: Designates the jump as a hidden point which cannot be discovered with standard sensors. In the base Naev scenario, hidden jump points are revealed to the player mainly via mission rewards, by completing certain missions or by equipping and activating a Hidden Jump Scanner outfit.
<asteroids>: Category which includes coordinates and contents of asteroid fields.<asteroid>: Defines an asteroid field.<group>: Names an .XML list from/asteroids/groupsthat defines what asteroids spawn in this field.<pos>: Centre position of the asteroid field within the system, expressed asxandycoordinates relative to the system’s centre point.<radius>: Size of the circular asteroid field, expressed in distance units from the field’s centre point as defined in the<pos>field.<density>: Affects how many asteroids are present within the asteroid field’s area.
<exclusion>: Defines an asteroid exclusion zone, creating a “negative” asteroid field. This can be used to create asteroid fields of unique shapes such as rings or crescents.<radius>and<pos>fields function identically to those under<asteroid>.
Defining Jumps
Within Naev, jump points are used to travel between systems.
Each jump point has a position within the system, defined either manually using the <pos> tag and x and y values or by using the <autopos/> tag to automatically place the point at a distance defined by the system’s <radius>.
Jump points also have an entry vector, or direction which ships must be facing to begin a jump.
This entry vector is dictated by the position of the destination system on the universe map relative to the current system - that is, a jump point will always point towards its destination system.
To create a standard two-way jump lane between two systems:
- Within current system a, create a
<jump>. Use the<target>tag to name destination system b. Use the<autopos/>tag to automatically place the jump point, or the<pos>tag to manually define its position with X and Y values. - Repeat the above in system b to create a jump point, using the
<target>tag to name destination system a.
Asteroid Fields
Asteroid fields are zones of floating objects within systems. They differ from spobs in that they are defined as circular areas rather than single points with graphics. Asteroids also interact with ship weapons fire and often generate commodity pickups when destroyed.
Asteroid data files are found in /asteroids/types/.
These files are in XML format and contain the following fields:
<scanned>: Text string shown to the player upon entering range of their asteroid scanner outfit.<gfx>: Possible graphics for this asteroid. Multiple graphics can be referenced, one per<gfx>tag, to increase the variety of visuals.<armor_min>and<armor_max>: Defines a range of armour values for asteroids to spawn with. Higher values mean more damage must be dealt to destroy an asteroid.<absorb>: Defines the asteroid’s damage reduction before applying weapons’ armour penetration stats.<commodity>: Lists which commodity pickups and quantities thereof can spawn upon destruction of the asteroid.<name>: Name of commodity.<quantity>: Maximum quantity of commodity pickups
This process will let you create an asteroid field in your <ssys> .XML file:
- Place graphics for your asteroids, in WEBP format, to
/gfx/spob/space/asteroid/; - Write asteroid data files, in XML format, to
/asteroids/types/; - Write an asteroid group list, in XML format, to
/asteroids/groups/. - In your
<ssys>XML file, use the<asteroid>field and subfields above to tell the game what asteroids the field will be made of.<pos>and<radius>define the position and size of your field.<group>and<density>define which asteroid group and how many asteroids appear in your field.
Space Objects (Spobs)
You can either create spobs manually by copying and pasting existing spobs and editing them (make sure to add them to a system!), or create and manipulate them with the in-game editor accessible from the “Extras” button in the main menu. Note that the in-game editor doesn’t support all the complex functionality, but does a large part of the job such as choosing graphics and positioning the spobs.
Spob Classification
Naev planetary classes are based on Star Trek planetary classes.
Station Classes
- Class 0: Civilian stations and small outposts
- Class 1: Major military stations and outposts
- Class 2: Pirate strongholds
- Class 3: Robotic stations
- Class 4: Artificial ecosystems such as ringworlds or discworlds
Planet Classes
- Class A: Geothermal (partially molten)
- Class B: Geomorteus (partially molten, high temperature; Mercury-like)
- Class C: Geoinactive (low temperature)
- Class D: Asteroid/Moon-like (barren with no or little atmosphere)
- Class E: Geoplastic (molten, high temperature)
- Class F: Geometallic (volcanic)
- Class G: Geocrystaline (crystalizing)
- Class H: Desert (hot and arid, little or no water)
- Class I: Gas Giant (comprised of gaseous compounds, Saturn-like)
- Class J: Gas Giant (comprised of gaseous compounds, Jupiter-like)
- Class K: Adaptable (barren, little or no water, Mars-like)
- Class L: Marginal (rocky and barren, little water)
- Class M: Terrestrial (Earth-like)
- Class N: Reducing (high temperature, Venus-like)
- Class O: Pelagic (very water-abundant)
- Class P: Glaciated (very water-abundant with ice)
- Class Q: Variable
- Class R: Rogue (temperate due to geothermal venting)
- Class S: Ultragiant (comprised of gaseous compounds)
- Class X: Demon (very hot and/or toxic, inhospitable)
- Class Y: Toxic (very hot and/or toxic, inhospitable, containing valuable minerals)
- Class Z: Shattered (formerly hospitable planet which has become hot and/or toxic and inhospitable)
Customizing Classes
TODO
Data Format
<spob>: Category which encapsulates all tag data relating to the spob.<lua>: Runs a Lua script in relation to this spob.<pos>: Position of the spob within its parent system, defined byxandycoordinates relative to the system center.<GFX>: Category relating to graphics.<space>: Defines the image, in .WEBP format, which represents the spob when travelling through the parent system. The dimensions of the graphic can also influence the area at which a ship can begin its landing sequence.<exterior>: Defines the image, in .WEBP format, displayed on the spob’s “Landing Main” tab.<presence>: Category relating to faction presence, used to generate patrol lanes within the parent system.<faction>: Defines the spob’s owning or dominant faction.<base>: Defines the base presence of the spob. The maximum base presence of all spobs of the same faction is used as the base presence of the faction in the system. For example, if there are two spobs with base 50 and 100 for a faction in a system, the system’s base presence for the faction is 100 and the 50 value is ignored.<bonus>: Defines the bonus presence of the spob. The bonus presence of all the spobs of the same faction in a system are added together and added to the presence of the system. For example, for a system with a base presence of 100, if there are two spobs with a bonus of 50 each, the total presence becomes $100+50+50=200$.<range>: The range at which the presence of the spob extends. A value of 0 indicates that the presence range only extends to the current system, while a presence of 2 would indicate that it extends to up to 2 systems away. The presence falloff is defined as $1-\frac{dist}{range+1}$, and is multiplied to both base presence and bonus presence. For example, a spob with 100 presence and a range of 3 would give 75 presesnce to 1 system away, 50 presence to 2 systems away, and 25 presence to 3 systems away.<general>: Category relating to many functions of the spob including world statistics, available services, etc.<class>: Defines the spob’s planetary or station class as listed above in the Station Classes and Planetary Classes categories above. This may be referenced by missions or scripts.<population>: Defines the spob’s habitating population.<hide>: Modifies the range at which your ship’s sensors can first discover the spob. A value of1is default range; values greater than 1 make it easier while values between 1 and 0 make it more difficult. A spob with ahidevalue of0will automatically reveal themselves to the player upon entering the system.<services>: Defines which services are available to the player while landed at the spob.<land>: Includes the Landing Main tab and allows the player to land on the spob. A spob without thelandtag cannot be landed on.<refuel>: Refuels the player’s ship on landing. A landable spob without this tag will not generate an Autosave (and will warn the player of this) to mitigate the chances of a “soft lock” where the player becomes trapped in a region of systems with no fuel sources and no autosaves prior to entering said region.<bar>: Includes the Bar tab, allowing the player to converse with generic or mission-relevant NPCs and view a news feed. Certain spob tags may alter the availability of NPCs and the news.<missions>: Includes the Mission Computer tab, where the player can accept generic missions.<commodity>: Includes the Commodities Exchange tab, where the player can buy and sell trade goods.<outfits>: Includes the Outfitter tab, allowing the player to buy and sell ship outfits. Also grants access to the Equipment tab where the player can swap outfits to and from their active ship.<shipyard>: Includes the Shipyard tab, allowing the player to purchase new ships. Grants access to the Equipment tab as above; also allows the player to swap their active and fleet ships and change the oufits on all player-owned ships.<commodities>: Declares the spob as having ready access to commodities, independent of the Commodities Exchange service.<description>: Text string presented to the player on the Landing Main tab. This text body is perhaps the primary method of presenting the spob’s lore to the player.<bar>: Text string presented to the player on the Bar tab. Compared to thedescriptiontag’s lore regarding the spob as a whole, this text describes only the Spaceport Bar and its surroundings.<tech>: Category which includes Tech Lists, used to define the items in stock at the Outfitter and Shipyard.<item>: Includes one Tech List.<tags>: Category which includes tags that describe the spob. These tags can be referenced in missions and scripts; see the Spob Tags section below for more information.
Spob Tags $\naev$
Tags are a versatile way to define the main facets of interest about a spob with respect to its faction, i.e. what differentiates it from the other spobs the player will (try and) visit.
Tags consist of binary labels which are accessible through the Lua API with spob.tags(). They are meant to give indication of the type of spob, and are meant to be used by missions and scripts to, for example, get specific spobs such as Dvaered mining worlds to send the player to get mining equipment or the likes.
Tags can be defined by the following part of XML code:
<tags>
<tag>research</tag>
</tags>
where the above example would signify the spob is focused on research.
Spob Common Tags
Below are some common tags used by many of the different scripts you can use in Sea of Darkness or with plugins. It is recommended to use as many tags as possible early in the development cycle, as it is rather tedious to go over all the spobs to add new tags afterwards.
Special Tags
These tags significantly change the functionality of the spob:
- restricted: player should not normally have access here, and normal missions shouldn’t spawn or try to come to the spob.
- nonpc: There should be no normal generic NPCs spawning at the spaceport bar.
- nonews: There is no news at the spaceport bar.
- nosightseeing: Will never be a point of interest for sightseeing missions.
- garbage: Can be used as a garbage dump for the waste desposal missions.
- wormhole: The spob is a wormhole. Doesn’t really do anything yet though.
- hypergate: The spob is a hypergate, is necessary for the spob to be a valid hypergate target.
- obelisk: Is a special obelisk that can be used to gain Sirius flow abilities.
- active: The spob is active (currently only matters for hypergates) and can be used. This allows the hypergate interface to be used and allows hypergates to connect to them.
Descriptive Tags
Below is the complete list of dominantly used descriptive tags. It should be noted that tagging is incomplete at present and it is possible that none of these tags will apply to many spobs (e.g. uninhabited, average, uninteresting or deserted spobs). Most others will only have one or two tags - they are supposed to represent important facets of the spob in its own estimation, not minor elements e.g. while the (temporary) Imperial Homeworld has many criminals and military personnel neither tag applies since its defining tags would be rich, urban and maybe tourism or trade.
These tags allow missions to randomly target spobs with specific properties, so they may have fairly localized specific effects.
- station: the spob is a space station or gas giant spaceport
- ruined: the spob is ruined (currently only matters for hypergates)
- new: recently colonised worlds / recently built stations (definitely post-Incident)
- old: long-time colonised worlds / old stations (definitely pre-Incident)
- rich: the population living on the spob is rich by the standards of the faction
- poor: the population living on the spob is poor by the standards of the faction
- urban: the spob consists of mainly heavily developed cities and urban environments
- rural: the spob consists of mainly undeveloped and virgin lands
- tourism: spob has interests and draws in tourists
- mining: mining is an important part of the spob economy
- agriculture: agriculture is an important part of the spob economy
- industrial: industry is an important part of the spob economy
- medical: medicine is an important part of the spob economy
- trade: trade is an important part of the spob economy
- shipbuilding: shipbuilding is an important part of the spob economy
- research: the spob has a strong focus in research (special research laboratories, etc…)
- immigration: the spob draws in many immigrants or is being colonised
- refuel: the spobs reason for existance is as a fueling point
- government: the spob has important government functions or hosts the central government
- military: the spob has an important factional military presence
- religious: the spob has an important religious significance or presence
- prison: the spob has important prison installations
- criminal: the spob has a large criminal element such as important pirate or mafia presence
- ringworld: Is a ring world.
- toxic: Is a toxic location, dangerous to stay too long.
- refuel: Is mainly used as a refuelling location with few or no other services.
Ships
Ships are tho cornerstone of gameplay in Naev. The player themselves is represented as a ship and so are all other NPCs found in space.
Ship Classes
Ships classes have an intrinsic size parameter accessible with the ship.size() Lua API.
This is a whole integer number from 1 to 6.
In Naev: Sea of Darkness, small ships (size 1 and 2) use small core slots and are meant to be fast and small. Medium ships (size 3 and 4) use medium core slots and are still agile, while being able to pack more of a punch. Large ships (size 5 and 6) are slow hulking giants with heavy slots meant to dominate. There is always a trade-off between agility and raw power, giving all ships a useful role in the game.
Ships are also split into two categories: civilian and military. Civilian ships are meant to focus more on utility and flexibility, while military ships focus more on combat abilities.
An overview of all the ship classes is shown below:
- Civilian
- Yacht: very small ship often with only few crew members (size 1)
- Courier: small transport ship (size 2)
- Freighter: medium transport ship (size 3)
- Amoured Transport: medium ship with some combat abilities (size 4)
- Bulk Freighter: large transport ship (size 5)
- Military
- Small
- Scout: small support ship (size 1)
- Interceptor: ultra-small attack ship (size 1)
- Fighter: small attack ship (size 2)
- Bomber: missile-based small attack ship (size 2)
- Medium
- Corvette: agile medium ship (size 3)
- Destroyer: heavy-medium ship (size 4)
- Large
- Cruiser: large ship (size 5)
- Battleship: firepower-based extremely large ship (size 6)
- Carrier: fighter bay-based extremely large ship (size 6)
- Small
Note that it is also possible to give custom class names.
For example, you can have a ship be of class Yacht, yet show the class name as Luxury Yacht in-game.
The only thing that matters from the ship class is the internal size which is used in different computations.
Ship Data Format
Each ship is represented with a stand-alone file that has to be located in ships/ in the data files or plugins.
Each ship has to be defined in a separate file and has to have a single <ship> base node.
name(attribute): Ship name, displayed in game and referenced bytechlists.points: Fleet point value. In general used by both the fleet spawning code and by player fleets.base_type: Specifies the base version of the ship, useful for factional or other situational variants. (For example, a Pirate Hyena would have the “Hyena” base type.)GFX: Name of the ship graphic in.webpformat. It is looked up atgfx/ship/DIR/NAME, whereDIRis the value ofGFXup to the first underscore, andNAMEis the value ofGFXwith a special suffix depending on the type of image. The base image will try different common file suffixes if not specified. The comm window graphic will use a suffix of_comm.webp, and the engine glow will use a suffix of_engine.webp. As an example, for a value ofGFX="hyena_pirate", the base graphic will be searched atgfx/ship/hyena/hyena_pirate.webpsize(attribute): The ship sprite’s resolution in pixels. For example,size=60refers to a 60x60 graphic.sxandsy(attributes): The number of columns and rows, respectively, in the sprite sheet.
GUI: The in-flight GUI used when flying this ship.sound: Sound effect used when accelerating during flight.class: Defines the ship’s AI when flown by escorts and NPCs.display(attribute): Overrides the displayed “class” field in the ship stats screen.
price: Credits value of the ship in its “dry” state with no outfits.time_mod(optional): Time compression factor during normal flight. A value of1means the ship will fly in “real time”, <1 speeds up the game and >1 slows down the game.trail_generator(unecessary with 3D ships): Creates a particle trail during flight.x,y(attributes): Trail origin coordinates, relative to the ship sprite in a “90 degree” heading.h(attributes): Trail coordinate y-offset, used to modify the origin point on a “perspective” camera.
fabricator: Flavour text stating the ship’s manufacturer.faction(optional): Defines the faction which produces the ship. Currently only used for the fancy gradient backgrounds.licence(optional): Licence-type outfit which must be owned to purchase the ship.cond(optional): Lua conditional expression to evaluate to see if the player can buy the ship.condstr(optional): human-readable interpretation of the Lua conditional expressioncond.description: Flavour text describing the ship and its capabilities.characteristics: Core ship characteristics that are defined as integers.crew: Number of crewmen operating the ship. Used in boarding actions.mass: Tonnage of the ship hull without any cargo or outfits.fuel_consumption: How many units of fuel the ship consumes to make a hyperspace jump.cargo: Capacity for tonnes of cargo.
slots: List of available outfit slots of the ship.weapon,utilityandstructure: Defines whether the outfit slot fits under the Weapon, Utility or Structure columns.x,y, andh(attributes) define the origin coordinates of weapon graphics such as projectiles, particles and launched fighters.size(attribute): Defines the largest size of outfit allowed in the slot. Valid values aresmall,mediumandlarge.prop(attribute): Defines the slot as accepting a particular type of outfit defined by an . XML file in theslots/directory. The Naev default scenario includessystems,engines, andhullvalues for Core Systems, Engines, and Hull outfits which must be filled (if they exist) for a ship to be spaceworthy.exclusive=1(attribute): Restricts the slot to accepting only the outfits defined by thepropfield.- Inserting an outfit’s
namewill add it to that outfit slot in the ship’s “stock” configuration. This is useful for selling a ship with prefilled core outfits to ensure its spaceworthiness immediately upon purchase.
stats(optional): Defines modifiers applied to all characteristics and outfits on the ship.- Fields here correspond to those in the
characteristicscategory and thegeneralandspecificscategories on equipped outfits.
- Fields here correspond to those in the
tags(optional): Referenced by scripts. Can be used to effect availability of missions, NPC behaviour and other elements.tag: Eachtagnode represents a binary flag which are accessible as a table withship.tags()
health: Supercategory which defines the ship’s intrinsic durability before modifiers fromstatsand equipped outfits. Note that this node and subnodes are deprecated and will likely be removed in future versions. Use ship stats instead!armour: Armour value.armour_regen: Armour regeneration in MW (MJ per second).shield: Shield value.shield_regen: Shield regeneration in MW (MJ per second).energy: Energy capacity.energy_regen: Energy regeneration in MW (MJ per second).absorb: Reduction to incoming damage.
For a full example of the $\naev$ starter ship ``Llama’’ is shown below, refer to the original file
Graphics
NOTE: This section is a bit out of date. It is now possible to use 3D ships with GLTF files and define the trails and mount points there. This is the preferred way to give ship graphics and will be properly documented in the future.
Ship graphics are defined in the <GFX> node as a string with additional attributes like number of sprites or size also defined in the XML.
Graphics for each ship are stored in a directory found in gfx/ship/, where the base graphics, engine glow graphics, and comm window graphics are placed separately with specific file names.
In particular, the GFX string name is sensitive to underscores, and the first component up to the first underscore is used as the directory name.
As an example, with <GFX>llama</GFX>, the graphics would have to be put in gfx/ship/llama/, while for <GFX>hyena_pirate</GFX>, the directory would be gfx/ship/hyena.
The specific graphics are then searched for inside the directory with the full GFX string value and a specific prefix.
Assuming GFX is the graphics name and DIR is the directory name (up to first underscore in GFX), we get:
gfx/ship/DIR/GFX.webp: ship base graphic filegfx/ship/DIR/GFX_engine.webp: ship engine glow graphics filegfx/ship/DIR/GFX_comm.webp: ship communication graphics (used in the comm window)
The base graphics are stored as a spritesheet and start facing right before spinning counter-clockwise. The top-left sprite faces to the right, and it rotates across the row first before going down to the next row. The background should be stored in RGBA with a transparent background. An example is shown below:

Example of the ship graphics for the “Llama”.
Starting from top-left position, and going right first before going down, the ship rotates counter-clockwise and starts facing right.
A black background has been added for visibility.
The engine glow graphics are similar to the base graphics, but should show engine glow of the ship. This graphic gets used instead of the normal graphic when accelerated with some interpolation to fade on and off. An example is shown below;

Example of the engine glow graphics for the “Llama”.
Notice the yellow glow of the engines.
A black background has been added for visibility.
The comm graphics should show the ship facing the player and be higher resolution. This image will be shown in large when the player communicates with them. An example is shown below:

Example of the comm graphics for the “Llama”.
Specifying Full Paths
It is also possible to avoid all the path logic in the <GFX> nodes by specifying the graphics individually using other nodes.
In particular, you can use the following nodes in the XML in place of a single <GFX> node to specify graphics:
<gfx_space>: Indicates the full path to the base graphics (gfx/is prepended). Thesxandsyattributes should be specified, or they default to 8.<gfx_engine>: Indicates the full path to the engine glow graphics (gfx/is prepended). Thesxandsyattributes should be specified, or they default to 8.<gfx_comm>: Indicates the full path to the comm graphics (gfx/is prepended).
This gives more flexibility and allows using, for example, spob station graphics for a “ship”.
Naev Sea of Darkness Tags
These are some tags mainly used by the Naev files, but can be useful for plugins that make use of existing infrastructure.
- noplayer: Does not let the player capture the script.
- noequip: Does not run the equipping script (equipopt) on pilots using the ship.
- nonaevpedia: Does not appear in the Holo-Archives even if the conditions are met.
- standard: Is a standard ship, used in several events such as the Ship Lover quizzes.
- civilian: Is a non-combat civilian ship. Used by the Pirate AI when choosing targets and gives bonuses in the gauntlet.
- transport: Mainly used to slightly tune the behaviour of the AI and in some missions.
- luxury: Used to give a bonus in the sightseeing missions.
- drone: Used to change the text to ROBOTIC for some AI and missions.
- bioship: Indicates the ship is a bioship and thus should have skill points. Also used in several missions and the equipment scripts.
- feral: Indicates a bioship is feral. Currently unused.
- FACTION NAMES (e.g., dvaered): Indicate the ship is used dominantly by the faction. Used by some missions.
Outfits
TODO
Slots
TODO
Ship Stats
TODO
Outfit Types
TODO
Modification Outfits
TODO
Outift Tags $\naev$
These are some tags mainly used by the Naev files, but can be useful for plugins that make use of existing infrastructure.
- nosteal: Can not be stolen by the player when boarding ships.
- movement: Is turned off when doing autonav or using the bioship skill “The Bite”.
- lucky: Gives the player luck, which can slightly modify outcomes and probabilities of certain missions.
- fleetcap_10: Gives some fleet capacity to the player (currently 25 despite saying 10).
- poi_scan: Indicates the player can use this to scan sensor anomalies.
- flow: Indicates a flow-related outfit.
- flow_ability: Indicates the outfit is a flow-related ability.
- flow_amplifier: Indicates a flow amplifier.
- FACTION NAMES (e.g., dvaered): Indicate the ship is used dominantly by the faction. Used by some missions.
Artificial Intelligence (AI)
The AI in Naev is mainly a hierarchical finite state machine, where tasks are a FIFO queue. New tasks get added (which can have subtasks) and when done, it goes on with the task before that.
Memory Parameters
Below are some of the important memory parameters that can affect significantly the behaviour of the AI.
These can be set by either accessing the mem table from the AI script itself, or accessing it via pilot.memory().
- natural (boolean): Represents a naturally spawning ship. These ships are cleared by missions / events as necessary, and can generally be bribed. They can also be captured when boarded. This parameter is automatically set for ships spawned with the faction spawning scripts. It has to be manually set on pilots spawned elsewhere.
- capturable (boolean): The ship is capturable despite being not necessarily natural.
- vulnerability (number): How vulnerable the ship is. A larger value means the ship is less likely to be targetted. A negative value indicates more likely to be targetted. It’s roughly equivalent to points, but different AIs will prefer to target different ships so it is not very transparent. Setting a value of
math.hugewill make a pilot usually targetted last, while-math.hugewill make it be prioritized as much as possible.
Introduction to Naev
This document refers to development details the of the Naev base setting known as Sea of Darkness. For more information on lore and details, please see the in-game Holo-Archives. Only information not present in the Holo-Archives will be shown here.
Rarity
Rarity begins at 0 for normal items. It is modified based on the following rules:
- Items that are acquired conditionally (such as reputation limits) get +1 rarity.
- Items that are sold in very specific spobs or not available more generally get +1 rarity.
- Easily unlocked or hard to get models get +2 rarity (e.g., Thurion Ships).
- Hard to unlock or very hard to get models get +3 rarity.
- Extremely hard to acquire models get +4 rarity.
- Ship Variants get +2 rarity, unless unlocked or hard to get, then it is +1.
Examples
Za'lek Stinggets a rarity of 1 as it is conditional.Koala Armouredgets a rarity of 2 as it is a normal variant.Za'lek Sting Type IIgets a rarity of 3 as it is conditional (+1) and a variant (+2).Thurion Perspicacitygets a rarity of 3 as it is unlocked (+2) and conditional (+1).Thurion Perspicacity Betagets a rarity of 4 as it is unlocked (+2), conditional (+1), and a variant (+1 as it is already unlocked).