It pains me to see luanti mods that have some common issues:
- Un-DRY (Don’t Repeat Yourself, DRY)
- Never using a global table
- Lots of tables
- Unorganized (everything in
init.lua
)
A good example is when we make a mod that’s organized into multiple .lua
files.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| -- Ugly Un-DRY code
dofile(core.get_modpath("my_mod") .. "/something1.lua")
dofile(core.get_modpath("my_mod") .. "/something2.lua")
dofile(core.get_modpath("my_mod") .. "/something3.lua")
dofile(core.get_modpath("my_mod") .. "/something4.lua")
-- Same code, but DRY
local runfile = function(file)
local mod_path = core.get_modpath("my_mod")
-- dofile(mod_path .. "/" .. file)
-- There's also a Lua global called DIR_DELIM (Directory Delimeter, Unix/Linix "/", Windows "\")
dofile(mod_path .. DIR_DELIM .. file .. ".lua") -- automatically add .lua (dofile only works with Lua)
end
runfile("something1")
runfile("something2")
runfile("something3")
runfile("something4")
|
By us making it a function runfile
, we now can also add it to the mod’s global table, so when we run a file we can keep access to this function too.
Another good example is using Luanti/Minetest’s logging.
1
2
3
4
5
6
7
8
9
10
11
12
13
| my_mod = {} -- It's highly recommended to add a log function into the mod's global table
my_mod.log = function(msg)
-- If the msg is a table, we'll have minetest serialize it
if type(msg) == "table" then
-- serialize and deserialize make the given into a lua table,
-- it will return it as a string, so we can then safely log it out
msg = core.serialize(msg)
-- i.e. a position {x=1, y=2, z=3}
-- "return {"x": 1, "y": 2, "z": 3}"
end
core.log("action", "[my_mod] " .. tostring(msg))
end
|
Modspace global table Top
A global table for the mod (typcally the mod’s name) helps publicly provide an API, while also permitting the mod to be assembled in an organized fashion.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| my_mod = {
version = "1.0-dev",
game = "???",
}
-- I typically prefix underscore for a "private" API (note there is no local, as we want it accessable in the mod's other files)
-- (this also means it is accessable from another mod)
_my_mod = {
secret = "Ta da!"
}
-- We'll detect what game is installed (in this case we could detect upto 3 different games/gamemodes)
if core.registered_nodes["default:stone"] ~= nil then
my_mod.game = "minetest_game"
elseif core.registered_nodes["mcl_core:stone"] ~= nil then
my_mod.game = "voxelibre"
elseif core.registered_nodes["nc_terrain:stone"] ~= nil then
my_mod.game = "node_core"
end
my_mod.log = function(msg)
if type(msg) == "table" then
msg = core.serialize(msg)
end
core.log("action", "[my_mod] " .. tostring(msg))
end
my_mod.runfile = function(file)
local mod_path = core.get_modpath("my_mod")
dofile(mod_path .. DIR_DELIM .. file .. ".lua")
end
my_mod.log("Version: " .. my_mod.version)
my_mod.log("Game: " .. my_mod.game)
my_mod.log(" " .. _VERSION) -- Display's what version of Lua we have (5.1 for example)
my_mod.runfile("crafting")
|
Now in a individual file crafting.lua
(for example)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| -- 3 materials that will change based on the game (defaulting to minetest_game)
local iron = "default:steel_ingot"
local gold = "default:gold_ingot"
local diamond = "default:diamond"
-- VoxeLibre support (This would support Voxelibre, Mineclone2, Mineclone5, and Mineclonia)
if my_mod.game == "voxelibre" then
iron = "mcl_core:iron_ingot"
gold = "mcl_core:gold_ingot"
diamond = "mcl_core:diamond"
end
-- (DRY) Now instead of multiple register_craft calls with the different ingredients
core.register_craft({
type = "shapeless",
output = diamond,
recipe = {
iron,
gold,
iron,
gold
}
})
|
Too many tables Top
An issue that has also come up.
1
2
3
4
5
6
7
| local def = {
description = "My Mod Item",
inventory_image = "my_mod_item.png",
stack_max = 1,
groups = {not_in_creative_inventory=1},
}
core.register_craftitem("my_mod:item", def)
|
Unless you are going to be making multiple items with the same definition (or minor programmatic changes), don’t use a table.
1
2
3
4
5
6
| core.register_craftitem("my_mod:item", {
description = "My Mod Item",
inventory_image = "my_mod_item.png",
stack_max = 1,
groups = {not_in_creative_inventory=1},
})
|
Unorganized (Everything in init.lua
) Top
An issue that has come up a couple of times.
I’ve seen from 400 to even 1200+ lines in the init.lua
file, with no other .lua
files present.
This is what dofile is for, which is also why my first thing I showed was the runfile
function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| my_mod = {
version = "1.0-dev",
}
_my_mod = {
game = "???",
runfile = function(file)
local mod_path = core.get_modpath("my_mod")
dofile(mod_path .. DIR_DELIM .. file .. ".lua")
end,
log = function(msg)
if type(msg) == "table" then
msg = core.serialize(msg)
end
core.log("action", "[my_mod] " .. tostring(msg))
end,
}
if core.registered_nodes["default:stone"] ~= nil then
_my_mod.game = "minetest_game"
elseif core.registered_nodes["mcl_core:stone"] ~= nil then
_my_mod.game = "mineclone"
elseif core.registered_nodes["nc_terrain:stone"] ~= nil then
_my_mod.game = "node_core"
end
_my_mod.runfile("api")
_my_mod.runfile("crafting")
_my_mod.runfile("craftitems")
_my_mod.runfile("nodes")
|