Getting started guide

Last modified by Daniel Turner on 2023/09/07 15:19

Introduction

To create your addon, the normal addon process applies (workshop tool etc. See: http://forum.egosoft.com/viewtopic.php?t=376579).

The entry point for any UI mod is the ui.xml file. In your folder in the extensions directory, simply create such a file. Examples can be taken from the ui.xml files in ui/addons/ego_xxxx. The structure is documented via the XSD-file under ui/core/addon.xsd.

Please note that Lua files in the root-folder of the extension is unsupported. We are not giving any guarantee that this doesn't cause problems or will work in future versions. It's best practice to create a ui-folder and put the Lua files there.

Creating the ui.xml file

sample ui.xml file
<?xml version="1.0" encoding="UTF-8"?>
<addon name="foo" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../ui/core/addon.xsd">
 <environment type="detailmonitor">
   <file name="ui/file1.lua" />
   <file name="ui/file2.lua" />
   <dependency name="foo_dependency" />
   <savedvariable name="foo_MyData" storage="userdata" />
 </environment>
</addon>

line 1: The file being in the XML-format, this is the XML-header.
line 2: The addon node is the root-node of the ui.xml file and describes the UI addon.

  • name: Gives the addon a unique name. It's suggested to name the addon according to the folder name of your addon. This ensures that if everybody sticks with that rule, players will never end up with two addons using the same name. (note that the addon-name must not start with ego_)
  • xmlns:xsi: specified for XML-standard compliance
  • xsi:noNamespaceSchemaLocation: references the corresponding XSD in the ui-directory. This allows your editor to highlight errors and display documentation/tooltips directly in the file, if supported by your editor.

line 3: Specifies the "display environment" of the addon. At the time of writing this, X Rebirth contains three distinct environments: "detailmonitor", "fullscreen", "fullscreen2". At any time there can be only one frame being displayed in each of these environments. Detailmonitor represents the environment for the cockpit detailmonitor, the other two are displayed fullscreen.

line 4+5: You can specify multiple Lua scripts which the game loads in the given sequence when loading the addon. Note that Lua scripts must have the Lua-extension and MUST NOT contain bytecode. If a Lua file contains bytecode, the file will not be loaded and you will get an error message. Furthermore, the Lua file MUST NOT be located in the extension's root directory.
line 6: You can specify dependencies for the addon. This ensures that any dependent addon is loaded prior to your addon. For instance if you plan to modify the ego_detailmonitor addon, you would specify the "ego_detailmonitor" dependency here. Note that only addons in the same environment can be specified as dependent. If an addon is running in multiple environments and you want to depend on the addon in all its environments, the dependency to that addon must be specified in each environment separately.
line 7: You can specify multiple variables which are to be saved either as userdata or in a savegame.

  • name: specifies the Lua variable name which will be saved. It's suggested to prefix this with your addonname, so that the variable name doesn't collide with other addons (which might specify the same variable name). Note that the variable name must not start with CORE__.
  • storage: defines where the data will be stored. This is either "userdata" or "savegame". In case of savegame storage, the data will be stored in the savegame and loaded alongside the savegame. Userdata storage is independent from savegames and will be loaded as soon as the game has been started.

Lua language support

X Rebirth's Lua support is fully upwards-compatible with Lua 5.1 with the following restrictions:

  • math.mod() and string.gfind() are not supported (use the Lua 5.2 replacements instead - see below)
  • debug/os/io libraries are not available

In addition to Lua 5.1 language support the following subset of Lua 5.2 features are supported as well:

  • goto and ::labels::
  • break can be placed anywhere
  • empty statements (;emoticon_wink
  • hex escapes '\x3F' and '\*' escape in strings
  • load(string|reader [, chunkname [,mode [,env]]])
  • loadstring() is an alias to load()
  • coroutine.running() returns two results
  • table.pack() / table.unpack()
  • math.log(x [,base])
  • string.rep[s, n [,sep])
  • string.format(): %q reversible - %a and %A supported
  • string matching pattern %g supported

In addition to these, there a couple of further extensions available:

  • bitOp library (see: http://bitop.luajit.org/)
  • UTF8-library (see: https://github.com/starwing/luautf8)
  • Lua source code parser supports non-ascii characters (for instance for variable names or strings)
  • xpcall(f, err, [, args...])
    passes any argument after the error function to the function call f
  • fully resumable VM
    In contrast to Lua 5.1, you can yield from a coroutine even across contexts where this would not be possible with the standard Lua 5.1 VM (for instance you can yield across cancelpcall()).
  • numeric literals with the case-insensitive suffixes LL or ULL are treated as signed/unsigned 64 bit integers (for instance 42LL or 0x2aULL)
  • the imaginary part of complex numbers can be specified by suffixing number literals with i (for instance: 12.5i)

Further references:
 Lua homepage: http://www.lua.org/home.html
Lua 5.1 reference manual: http://www.lua.org/manual/5.1/
Lua 5.2 reference manual: http://www.lua.org/manual/5.2/

FFI and Lua interface

If you take a look at certain Lua files (for instance ego_detailmonitor/menu_map.lua or ego_detailmonitor/menu_missionbriefing.lua, you will find sections on top a file looking like this:

 

FFI-definition
-- ffi setup
local ffi = require("ffi")
local C = ffi.C
ffi.cdef[[
    typedef uint64_t UniverseID;
    typedef struct {
        const char* factionName;
        const char* factionIcon;
    } OwnerDetails;
    UniverseID AddHoloMap(const char* texturename, float x0, float x1, float y0, float y1);
    void ClearHighlightMapComponent(UniverseID holomapid);
    const char* GetBuildSourceSequence(UniverseID componentid);
    const char* GetComponentClass(UniverseID componentid);
    const char* GetComponentName(UniverseID componentid);
    uint64_t GetContextByClass(UniverseID componentid, const char* classname, bool includeself);
    uint64_t GetMapComponentBelowCamera(UniverseID holomapid);
    const char* GetMapShortName(UniverseID componentid);
    OwnerDetails GetOwnerDetails(UniverseID componentid);
    uint64_t GetParentComponent(UniverseID componentid);
    uint64_t GetPickedMapComponent(UniverseID holomapid);
    bool IsComponentOperational(UniverseID componentid);
    bool IsInfoUnlockedForPlayer(UniverseID componentid, const char* infostring);
    bool IsSellOffer(UniverseID tradeofferdockid);
    void RemoveHoloMap(UniverseID holomapid);
    void SetHighlightMapComponent(UniverseID holomapid, UniverseID componentid, bool resetplayerpan);
    void ShowUniverseMap(UniverseID holomapid, UniverseID componentid, bool resetplayerzoom, int overridezoom);
    void StartPanMap(UniverseID holomapid);
    void StartRotateMap(UniverseID holomapid);
    void StopPanMap(UniverseID holomapid);
    void StopRotateMap(UniverseID holomapid);
    void ZoomMap(UniverseID holomapid, float zoomstep);
]]

This is the so called FFI (fast function interface). It has certain advantages over simple Lua functions, especially with regards to performance and stability. Therefore we aim to deprecate the Lua interface in the long run and replace everything with corresponding FFI-functions.

That said, we highly recommend that you use FFI-functions, whenever possible. However, especially shortly after introducing the UI modding support, the majority of the functions is still written as plain Lua functions. Therefore, you will most likely still work with the old interface.

To be able to call FFI-functions, the following header should be placed at the top of the Lua script:

local ffi = require("ffi")
local C = ffi.C
ffi.cdef[[
   -- add FFI-functions here
]]

Inside ffi.cdef copy/paste the declaration of the function/struct you want to use/access in this file. See the list below for all available FFI-functions/-structs.

A call to the actual function then looks like this

local component = C.GetPickedMapComponent(myholomap) -- note the "C." prefix here which refers to the local C = ffi.C specified in the header
[...]
local details = C.GetOwnerDetails(componentID)
local faction = ffi.string(details.factionName)
local icon = ffi.string(details.factionIcon

This code also demonstrates how to work with FFI-structs and as a special case how to convert const char* values to strings. Note that whenever you retrieve a const char* you must convert it using ffi.string(). Otherwise the behavior is undefined and you might run into issues like garbled text.

Special care is to be taken with UniverseIDs. UniverseIDs are identifiers which represent any kind of object in the game universe. In most cases these are components. In case a universeID represents a component, it's also called componentID (other examples are holomapID which are universeIDs representing a holomap object, etc.).

UniverseIDs are fully interchangeable between FFI and Lua functions. In some cases, conversion-functions are needed when passing FFI/Lua UniverseIDs, however. The following table provides an overview.

 

pass to an FFI function

pass to a Lua function

pass to a script value (AI/MD-scripts)

FFI ID

use ConvertStringTo64Bit(tostring(ffiUniverseID))

use ConvertStringToLuaID(tostring(ffiUniverseID))1)

Lua ID

use ConvertIDTo64Bit(luaUniverseID)

indicates cases where you can pass the variable directly
1) Universe IDs must be passed as Lua IDs to MD/AI scripts, so the script system treats them correctly as components. Not doing so will cause problems, for instance when loading a savegame that contains such values.
A few examples where Lua values are passed to the MD/AI script system:

  • Helper.closeMenuAndReturn()
  • Lua function: SignalObject()
  • Lua function: SetNPCBlackboard()

As presented in this table, there are some conversion functions available which are required when passing Lua/FFI-IDs to FFI/Lua functions and/or scripts.

  • ConvertStringTo64Bit() - takes a string-representation and converts it to a plain 64-bit integer (ID) - these are compatible with all FFI-functions and some non-FFI-functions
  • tostring() - converts either an ID retrieved via an FFI- or non-FFI-function to a string representation - this is supposed to be used for cases, where you cannot determine whether the ID is a non-FFI-ID or an FFI-ID. It is mostly used in combination with ConvertStringTo64Bit().
  • ConvertIDTo64Bit() - takes an ID in non-FFI-format and converts it to a plain 64-bit integer (ID) - this is faster than calling a combination of ConverStringTo64Bit() and tostring() but only works, for non-FFI-IDs
  • ConvertStringToLuaID() - takes a string-representation and converts it to a Lua UniverseID - these are compatible with AI/MD-scripts and all Lua functions - however this conversion is less performant than ConvertStringTo64-bit

In some cases you don't know what kind of ID you are being passed. To determine whether a variable is an FFI-ID, you can use the follow code

 

-- ffi setup
local ffi = require("ffi")
ffi.cdef[[
    typedef uint64_t UniverseID;
]]


[...]
local myUniverseID = [...]
[...]
local ffiIDType = ffi.typeof("UniverseID")
local isFFIID = ffi.istype(ffiIDType, myUniverseID)

-- isFFIID will be true, if myUniverseID is an FFIID, otherwise this will be false

 

In exceptional cases you might also have to create FFI datatypes directly in the Lua script. Normally this is not necessary, since passing certain Lua values to FFI will be converted to the appropriate type directly. However, in cases where you have to construct objects of an FFI type, you can do so using ffi.new

 

local myFFIObject = ffi.new("UniverseID", 123)

 

The code above will create an object of type UniverseID and initialize it with the value of 123.