Writing your own Mod

From Avorion Wiki
Jump to: navigation, search

In Avorion, modding should be done without modifying vanilla game files. The game looks for mod files in their respective folders, in mod load order. Mod load order is determined by mod dependencies.

Also, check out the Scripting API and Avorion Internal Architecture to get to know how Avorion works and what it offers in terms of modding and API.

Creating a new mod[edit | edit source]

You can easily start a new mod from scratch by clicking the New Mod button in the mod upload window. After entering a name, a default modinfo.lua file will be created as well as a loose folder structure that you can use for your mods.

Make sure to enable the "Dev Mode" check box on the top of the mod configuration window. This will impact game performance, but your mods and extensions will always be loaded freshly from disk when you add / remove a script to / from an entity while the game is running.

Mod Folder Structure[edit | edit source]

During development, mods should be placed in the %AppData%\Avorion\mods\ folder on Windows, or ~/.avorion/mods/ on Unix-based systems.

Mods must follow the same data folder structure as the main game. Example:

MyMod/ 
|--modinfo.lua
|--main.lua
|--thumb.png
|--data/
|----scripts/
|------items/
|--------moddeditem.lua
|------entity/
|--------stationfounder.lua
|----textures/
|------icons/
|--------modded_icon.png
|--------turret.png

In the above example

  • modinfo.lua is the required meta data description file of the mod, that determines several attributes like its name, ID, title, description, dependencies, and so on
  • main.lua is an optional file that runs in a global server environment, similar to Player, Entity or Sector scripts
  • thumb.png is the thumbnail picture that will be used by the mod when uploading it to the workshop
  • moddeditem.lua is a new file that will be loaded by the game as if it were in its data/ folder
  • stationfounder.lua is an extension that will be appended to the vanilla stationfounder.lua file. This behavior is true for all lua files that name-clash with vanilla files.
  • modded_icon.png is a new asset file that will be loaded by the game as if it were in its data/ folder
  • turret.png is a new asset file that will replace the vanilla file. This behavior is true for all asset files that name-clash with vanilla files.

Debugging[edit | edit source]

Check out the article on Debugging Scripts for information on how to find errors in your mods and what pitfalls of the API to avoid.

modinfo.lua[edit | edit source]

This file is required and will provide important information for Avorion to determine load order, dependencies and other attributes required for loading mods. Here, you must configure all important meta data settings for your mod.

Note: The modinfo.lua file will be generated freshly and up-to-date when creating a mod with the New Mod button in the mod upload window.

It could look like this (some details may differ):

meta =
{
    -- ID of your mod; Make sure this is unique!
    -- Will be used for identifying the mod in dependency lists
    -- Will be changed to workshop ID (ensuring uniqueness) when you upload the mod to the workshop
    id = "ChipOfCreditsUseableItemExample",

    -- Name of your mod; You may want this to be unique, but it's not absolutely necessary.
    -- This is an additional helper attribute for you to easily identify your mod in the Mods() list
    name = "ChipOfCreditsUseableItemExample",

    -- Title of your mod that will be displayed to players
    title = "Example: Chip Of Credits Inventory Item",

    -- Description of your mod that will be displayed to players
    description = "This mod adds a new inventory item to the game, a chip full of credits, where the amount of credits is based on the rarity of the item.",

    -- Insert all authors into this list
    authors = {"koonschi"},

    -- Version of your mod, should be in format 1.0.0 (major.minor.patch) or 1.0 (major.minor)
    -- This will be used to check for unmet dependencies or incompatibilities
    version = "1.0",

    -- If your mod requires dependencies, enter them here. The game will check that all dependencies given here are met.
    -- Possible attributes:
    -- id: The ID of the other mod as stated in its modinfo.lua
    -- min, max, exact: version strings that will determine minimum, maximum or exact version required (exact is only syntactic sugar for min == max)
    -- optional: set to true if this mod is only an optional dependency (will only influence load order, not requirement checks)
    -- incompatible: set to true if your mod is incompatible with the other one
    -- Example:
    -- dependencies = {
    --      {id = "Avorion", min = "0.17", max = "0.21"}, -- we can only work with Avorion between versions 0.17 and 0.21
    --      {id = "SomeModLoader", min = "1.0", max = "2.0"}, -- we require SomeModLoader, and we need its version to be between 1.0 and 2.0
    --      {id = "AnotherMod", max = "2.0"}, -- we require AnotherMod, and we need its version to be 2.0 or lower
    --      {id = "IncompatibleMod", incompatible = true}, -- we're incompatible with IncompatibleMod, regardless of its version
    --      {id = "IncompatibleModB", exact = "2.0", incompatible = true}, -- we're incompatible with IncompatibleModB, but only exactly version 2.0
    --      {id = "OptionalMod", min = "0.2", optional = true}, -- we support OptionalMod optionally, starting at version 0.2
    -- },
    dependencies = {
        {id = "Avorion", max = "0.21.2"}
    },

    -- Set to true if the mod only has to run on the server. Clients will get notified that the mod is running on the server, but they won't download it to themselves
    serverSideOnly = false,

    -- Set to true if the mod only has to run on the client, such as UI mods
    clientSideOnly = false,

    -- Set to true if the mod changes the savegame in a potentially breaking way, as in it adds scripts or mechanics that get saved into database and no longer work once the mod gets disabled
    -- logically, if a mod is client-side only, it can't alter savegames, but Avorion doesn't check for that at the moment
    saveGameAltering = true,

    -- Contact info for other users to reach you in case they have questions
    contact = "mail@provider.com",
}

Dependencies[edit | edit source]

Dependencies to other mods always have to reference the id of the mod. So make sure that ids are unique.

Dependencies to other Mods[edit | edit source]

You can just put the other mod's id in the dependency to reference a mod as a dependency. You will have to check in that mod's modinfo.lua to get its ID, if it's not a workshop mod.

Dependencies to Workshop Mods[edit | edit source]

When uploading a mod to the workshop, the mod's id is replaced by its workshop ID, ensuring uniqueness between workshop mods. In order to get the id of a workshop mod, go to its workshop site, and look at the last number of its URL. That's your mod's id.

So for https://steamcommunity.com/sharedfiles/filedetails/?id=1682700568, the ID would be 1682700568.

You can also get the ID of a mod by subscribing to it and looking at its modinfo.lua.

Scripting API[edit | edit source]

Together with your Avorion installation comes a Scripting API containing all information about all predefined Avorion scripting objects, functions, enums, callbacks and so on. Check it out. Now.

Examples[edit | edit source]

Avorion Scripts[edit | edit source]

You can find the entire scripting code of Avorion open and available in your installation folder in data/scripts/. Go there, read some scripts and learn how Avorion does things. It's open for a reason!

Mods[edit | edit source]

There are several examples for mods in the workshop.

You should subscribe to those mods and look at their structure to gain a basic understanding about how mods are done in Avorion.

Example Scripts[edit | edit source]

In your local Scripting API (that you should 100% look at because it contains 99% of the information you'll need to write mods) you can find example script templates for all predefined scripts that can be added to Avorion.

Predefined Functions[edit | edit source]

Every script in Avorion has a set of functions that can be defined that will be called at various points in time by the game. You can find extensive documentation on which scripts can define which functions and when they're called, together with several example scripts, in the Predefined Functions section in the Scripting API.

Modifying Modules and Libraries[edit | edit source]

There are several issues that come with appending code to files. To solve those, if you're not interested in technical backgrounds, just follow these guidelines:

  • When extending library files with a return at the end, your code will be injected before the return statement.
  • Use include() instead of require(), it behaves the same way, but loads mods correctly.

In case you're interested in what's going on, keep reading (recommended).

In case of files that are supposed to be required by other files, just appending extensions doesn't always suffice because they return a value and the appended code would never be executed.

Take this example:

MyMod/ 
|--modinfo.lua
|--thumb.png
|--data/
|----scripts/
|------lib/
|--------upgradegenerator.lua

Vanilla data/scripts/lib/upgradegenerator.lua (some parts were left out for easier reading)

local UpgradeGenerator = {}
...
function UpgradeGenerator.initialize(seed)
    ...
end 
...
-- << Injection Point >>
return UpgradeGenerator
-- Appending here is bad

Any code appended to the above file would never be executed and would even cause errors. So what Avorion does, in order to give you full freedom, is that it injects extensions before the last return instead of appending code to those files. So if you want to have a file upgradegenerator.lua in your mod, it may look like this:

Modded MyMod/data/scripts/lib/upgradegenerator.lua (some parts were left out for easier reading)

function UpgradeGenerator.generateWithAnotherAlgorithm(...)
    -- we changed something in the generator code
end

And the final loaded virtual file, after the injection will look like this:

local UpgradeGenerator = {}
...
function UpgradeGenerator.initialize(seed)
    ...
end 
...
function UpgradeGenerator.generateWithAnotherAlgorithm(...)
    -- we changed something in the generator code
end
-- << New Injection Point for following extensions >> 
return UpgradeGenerator

Using include()[edit | edit source]

require() cannot be used to load library files any more, since it can only load a single file. With the above example, this code:

-- can only ever load vanilla upgradegenerator.lua
require("upgradegenerator.lua")

would load only the vanilla file, and ignore the modification made by the upgradegenerator.lua of MyMod, basically ignoring any changes that mod was supposed to introduce.

But in order for the mod to work, we need the vanilla file and all of its extensions instead. To solve this, use include(): The include() function behaves exactly the way require() does, but it loads files including their respective mod extensions. The extensions are appended in mod load order and are loaded as if they were a single file.

-- loads upgradegenerator.lua and all of its included (no pun intended) modded extensions
include("upgradegenerator.lua")

Adding Scripts to Existing Objects[edit | edit source]

init.lua[edit | edit source]

In order to add your own scripts to existing scriptable objects, there is a init.lua file for every object that you can attach scripts to. init.lua is run once on creation of an object, but will not get attached to the object itself. The init.lua file can be extended just like any other script, so you can, for example, do this:

MyMod/data/scripts/entity/init.lua

local entity = Entity() -- gain access to the entity that's being initialized
if entity.playerOwned then
    entity:addScriptOnce("entity/mynewentityscript.lua") -- add our own script to the entity
end

The above example will check for every entity, on initialization, if the entity is owned by a player, and will then attach a script that we wrote to that entity. Keep in mind that init.lua is always executed whenever an entity is constructed, so it will also be called when loading a sector from disk. This is why, in the above example, we're using the addScriptOnce() function, so our script doesn't get added if it's already there.

Internal Architecture[edit | edit source]

In order to understand what exactly you're doing and how you should be doing it, you should read up on the Avorion Internal Architecture. Avorion is a complex multithreaded environment and allows modders a lot of freedom. Sadly this freedom also comes with high complexity, so you should read up on how Avorion is structured internally to get a better gist of everything.

Script Loading[edit | edit source]

Load Order[edit | edit source]

A load order is established between mods, depending on the dependencies configured in each modinfo.lua file. When a dependency is added to a mod, the dependency is loaded before that mod. If two mods have the same load order position, then a lexicographic comparison between their IDs determines which one gets loaded first.

Loading Scripts[edit | edit source]

When a script is loaded from disk, then the game looks for it in its installation data folder, and then in all subsequent data folders of all enabled mods. So if you have 2 mods MyMod and OtherMod enabled, and MyMod is loaded before OtherMod, then the game will look through its installation folder, then in MyMods data folder, and then OtherMods data folder.

After all those files were found they are combined into a single virtual file, in load order, that is then loaded all at once, thus allowing subsequently loaded script fragments access to local variables of previously loaded script fragments.

Original File, stored in [InstallationFolder]/data/scripts/entity/backup.lua:

package.path = package.path .. ";data/scripts/lib/?.lua"

local timeUntilBackup = 0
local damageUntilBackup = 0
local damageTaken = 0

-- Don't remove or alter the following comment, it tells the game the namespace this script lives in. If you remove it, the script will break.
-- namespace Backup
Backup = {}

-- this function gets called on creation of the entity the script is attached to, on client and server
function Backup.initialize()
    if onServer() then
        local entity = Entity()
        entity:registerCallback("onDamaged" , "onDamaged")

        damageUntilBackup = entity.maxDurability * 0.15
    end
end

Additional Mod File, stored in %AppData%/mods/MyMod/data/scripts/entity/backup.lua:

local oldBackup_initialize = Backup.initialize
-- this shadows the previous Backup.initialize()
function Backup.initialize(...)
    oldBackup_initialize(...) -- we want to call the old function still!
    if onServer() then
        local entity = Entity()
        damageUntilBackup = entity.maxDurability * 0.25 -- we have access to local 'damageUntilBackup'
    end
end

Full Loaded Virtual File, as loaded into the game

package.path = package.path .. ";data/scripts/lib/?.lua"

local timeUntilBackup = 0
local damageUntilBackup = 0
local damageTaken = 0

-- Don't remove or alter the following comment, it tells the game the namespace this script lives in. If you remove it, the script will break.
-- namespace Backup
Backup = {}

-- this function gets called on creation of the entity the script is attached to, on client and server
function Backup.initialize()
    if onServer() then
        local entity = Entity()
        entity:registerCallback("onDamaged" , "onDamaged")

        damageUntilBackup = entity.maxDurability * 0.15
    end
end

local oldBackup_initialize = Backup.initialize
-- this shadows the previous Backup.initialize()
function Backup.initialize(...)
    oldBackup_initialize(...) -- we want to call the old function still!
    if onServer() then
        local entity = Entity()
        damageUntilBackup = entity.maxDurability * 0.25 -- we have access to local 'damageUntilBackup'
    end
end

Loading Assets[edit | edit source]

When another asset is loaded by the game, such as a texture or a sound effect etc. then the game looks for the asset similar to the script loader above, but in reverse mod load order. It then just uses the first asset found, so mod assets effectively shadow vanilla assets.

Localization[edit | edit source]

Avorion uses a gettext inspired system to do its translation work. It uses .po files like gettext, but marking strings that should be translated is done a little differently to make coding and reading code more comfortable.

Marking Texts[edit | edit source]

In order to translate texts in Avorion, you have to first mark them as a translatable text in the game files. You can do this with the %_t and %_T suffix:

package.path = package.path .. ";data/scripts/lib/?.lua"
include ("stringutility")

function MyScript.updateClient(timeStep)
    -- this will be found by the generator, and translated with the % operator by passing _t
    print ("This is my translated message!"%_t)    
    -- this will be found by the generator, but NOT translated with the % operator because we're passing _T
    print ("This is my untranslated, but marked message!"%_T) 
    -- this will be found and translated, and afterwards ${foo} and ${bar} are replaced by "That" and "other"
    print ("${foo} is my translated ${bar} message!"%_t % {foo = "That", bar = "other"})
end

The % operator is a custom operator that is defined in scriptutility.lua. It can be used to format strings with ${var} variables and to translate strings when passing _t.

In our case, it does two things: It marks the text as a translatable text in the code and it translates the text when being used with the % operator.

  • Note: To mark a string as translatable, you have to make sure that there are no spaces around the % operator in your "My Text"%_t expression. A text like "My Text" % _t or "My Text"% _t won't be detected as a translatable text.
  • Note: Use %_T to mark texts you want to be translatable, but not to be translated immediately to save performance (for example error chat messages sent by the server). Avorion has several places where it auto-translates texts if it finds them, for example when displaying chat messages.

Generating Translation Files[edit | edit source]

After marking the texts, you can use the Mod Upload window to generate localization files for a mod. (You can also write them yourself, but why do that when such a tedious task can just be done by a machine?) Press the button above the file listing to generate or regenerate translations. The generator looks for strings that end with %_t or %_T and puts them into the translation .po file. New texts will be added to an existing file, without removing existing texts.

-- file: deutsch.po
msgid "This is my translated message!"
msgstr ""

msgid "This is my untranslated, but marked message!"
msgstr ""

msgid "${foo} is my translated ${bar} message!"
msgstr ""

Note: Texts that already exist in the vanilla localization files won't appear in your mod localization file.

Usage in the game[edit | edit source]

After your localization files are generated, you can fill in the translated texts.

Mod localization files are loaded in addition to the vanilla files.

-- file: deutsch.po
msgid "This is my translated message!"
msgstr "Das ist meine übersetzte Nachricht!"

msgid "This is my untranslated, but marked message!"
msgstr "Das ist meine nicht übersetzte, aber markierte Nachricht!"

msgid "${foo} is my translated ${bar} message!"
msgstr "${foo} ist meine übersetzte ${bar} Nachricht!"

With the above translations filled in and German language selected, the output of the above code would look like this:

Das ist meine übersetzte Nachricht!
This is my untranslated, but marked message!
That ist meine übersetzte other Nachricht!

Note that the That and other weren't translated, because we didn't mark them as to-be-translated.

When a translation for a text is empty, then Avorion will use the english text.

Contexts[edit | edit source]

Sometimes you have to add some context info to a text to make it easier to translate and to avoid silly translation errors. For example, the text "Show" could refer to the verb "to show" (as in "show a UI window", "show yourself") or the substantive "a show" (as in a presentation).

To avoid translation errors, you can also add some context that will be removed on translation. You can add context to a string by using C comment notation:

print ("Show /* as in: show a window */"%_t)

will result in this entry in the .po file:

msgctxt "as in: show a window"
msgid "Show"
msgstr "Zeigen"

Note that the context comment will be removed from the string on translation, even when there is no translation for the text, so

print ("Show /* as in: show a window */"%_t)

prints Zeigen when a translation was found, and Show when no translation was found.

Plural[edit | edit source]

Use plural_t() and plural_with_context_t() to do plural translations (see the Scripting API for more details):

local a = 5
print (plural_t("1 minute", "${i} minutes", a))
local a = 1
print (plural_t("1 minute", "${i} minutes", a))

prints

5 minutes
1 minute

and creates these .po file entries:

msgid "1 minute"
msgid_plural "${i} minutes"
msgstr[0] "1 Minute"
msgstr[1] "${i} Minuten"

Depending on the plural rules specified in the .po file of the selected language, those entries can vary.

Testing your Mod[edit | edit source]

To test your mod, put your mod into the %AppData%/Avorion/mods/ (~/.avorion/mods/ on Unix) folder. Make sure that the modinfo.lua file exists in that folder and that it doesn't have any syntax errors.

In the main menu, go to Settings -> Mods. Your mod should be listed there, provided your modinfo.lua doesn't have any errors. If it isn't listed there, then there was an error while loading the modinfo.lua file. To find those errors, you can either open the in-game console to look for errors, or you can close the game and check the latest clientlog*.txt file in your %AppData%/Avorion/ (~/.avorion/ on Unix) folder.

You can then enable the mod in the list, and it will be enabled on your next game start.

Advanced Topics[edit | edit source]

Internal Architecture[edit | edit source]

Now, that you know the basics on how to structure your mod to seamlessly fit into the game, it is time to read up on the Avorion Internal Architecture for more information on...

  • what kinds of scripts there are
  • how to communicate between client and server
  • how & why to do namespacing in scripts
  • how to communicate between objects, scripts and sectors

Pitfalls[edit | edit source]

Sadly, the Avorion Scripting API and its environment comes with a few pitfalls that you simply have to be aware of to avoid them.

You can read more about Modding Pitfalls here.

Performance Optimization[edit | edit source]

A single badly written mod can completely destroy the performance of Avorion. Make sure it's not your mod!

Check out the article on optimizing performance of your scripts and mods.

Best Practices[edit | edit source]

Check out the article on Best Practices for more info on this!

Uploading to the Workshop[edit | edit source]

Once you've finished your mod, you can upload it to the workshop for everybody else to use!

See Also[edit | edit source]