Modding

Libraries and modules

Split your mod into files or reuse functions from another mod. The examples target Evospace 0.22.2; the API reference documents version 0.22.2.

require

A path starts with ./, ../ or @. You can load modules from your own mod folder or from a declared dependency.

PathReads
require('./ore_washer') A module beside the requiring file.
require('../panels') A module one folder up.
require('@self/Blocks/all') A module of the mod itself, from its init.lua.
require('@VanillaLib') What that mod's init.lua returned.
require('@MyLib/tiers') A module of that mod.
  • A module is name.luau or name.lua, or a folder with init.luau or init.lua in it. If the path matches more than one module, require reports an error.
  • A module returns one value. It runs the first time it is required, and every require after that gets the same value.
  • @ModName is the mod's own name or one of its dependencies; names are case-insensitive. Trying to import an undeclared dependency fails and writes an error to the log:
log
require('@VanillaLib/...') in mod DrillingRigSpeed: that mod is not among its dependencies

Using a library

Add the library mod to dependencies so it loads before your mod, then import it with require. The game includes VanillaLib, which provides the helper functions used by Vanilla to set up machines.

info.json json
{
    "name": "DayAndNightSolar",
    "version": "1.1",
    "title": "Day & Night Solar",
    "author": "your name",
    "evospace_version": "0.22.2",
    "dependencies": ["Vanilla>=0.22.0", "VanillaLib>=0.22.0"],
    "description": "Solar panels produce at full output throughout the day and night."
}
init.lua lua
local Vlib = require('@VanillaLib')

local DayAndNightSolar = {}

local function day_and_night(name)
    local base = StaticBlock.find(name).logic_init
    return {
        logic_init = function(self)
            base(self)
            EnvironmentCrafter.cast(self).follows_sun = false
        end,
    }
end

function DayAndNightSolar.post_init()
    Vlib.FillBlock("SmallSolarPanel", day_and_night)
    Vlib.FillBlock("SolarPanel", day_and_night)
end

db:mod(DayAndNightSolar)

Vlib.FillBlock takes a block name without its material prefix and a function. It walks the material tiers, calls the function for every prototype it finds — StoneSolarPanel, CopperSolarPanel and the rest — and writes the fields of the returned table onto the prototype. Here that field is logic_init, wrapped around the one vanilla set.

Modules of your own

A mod reads its own files the same way. Vanilla keeps one file per machine and loads them through a single module:

folder
Vanilla/
    info.json
    init.lua
    Blocks/
        all.lua
        ore_washer.lua
        macerator.lua
init.lua lua
local vanilla_mod = {}

function vanilla_mod.init()
    require('@self/Blocks/all')()
end

db:mod(vanilla_mod)
Blocks/all.lua lua
local Vlib = require('@VanillaLib')

return function()
    Vlib.FillBlock("OreWasher", require('./ore_washer'))
    Vlib.FillBlock("Macerator", require('./macerator'))
end

@self in init.lua is the mod folder, so it reaches any file under it. ./ and ../ are read from the file that requires them, which keeps a folder of modules together.

Writing a library

This example exports helper functions without registering new prototypes. Its init.lua creates a table of functions, registers the mod with db:mod and returns the table. Other mods receive that table when they call require('@MyLib').

MyLib/info.json json
{
    "name": "MyLib",
    "version": "1.0",
    "title": "My Lib",
    "author": "your name",
    "evospace_version": "0.22.2",
    "dependencies": [],
    "description": "Helpers for my mods."
}
MyLib/init.lua lua
local MyLib = {}

--- Every recipe of a dictionary takes the same number of ticks.
--- @param dictionary string
--- @param ticks integer
function MyLib.set_ticks(dictionary, ticks)
    for _, recipe in pairs(RecipeDictionary.get(dictionary):recipes()) do
        recipe.ticks = ticks
    end
end

db:mod({})

return MyLib

A mod that depends on MyLib reads it by name. The library's init.lua has run by then, and it runs once however many mods require it.

FastSmelting/init.lua lua
local MyLib = require('@MyLib')

local FastSmelting = {}

function FastSmelting.post_init()
    MyLib.set_ticks('SmelterRecipeDictionary', 30)
end

db:mod(FastSmelting)

Globals

Each mod has its own global environment. Assigning a global variable affects only that mod. When a name is not defined there, Lua looks it up in the game's global environment. To share values between mods, return them in a library's table.

Beside require, the API globals and the classes of the reference, a mod sees _G, _VERSION, assert, error, ipairs, pairs, next, pcall, xpcall, select, tonumber, tostring, type, typeof, unpack, rawget, rawset, rawequal, rawlen, setmetatable, getmetatable, print, print_info, print_warn, print_err, bit32, buffer, integer, utf8, math, string, table, coroutine.

Editor

The API ships as type definitions in Evospace/lua-api: evospace.d.luau for luau-lsp, which reads Luau itself, and a library folder of LuaCATS stubs for LuaLS. A .luaurc in the folder that holds the mods gives luau-lsp the mod names, so it resolves @VanillaLib the way the game does.