It pains me to see luanti/minetest 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
minetest.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 see 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")
|