Create Mods in Minetest Guide
Create Mods in Minetest Guide
Introduction
Understanding the basic structure of a mod's folder is an essential skill
when creating mods.
Both mods contained in a game and third-party mods use the same API.
This book will cover the main parts of the Minetest API and is applicable
for both game developers and modders.
For a given world/save game, three mod locations are checked. They are
in order:
1. Game mods. These are the mods that make up the game the world
is running. For example: minetest/games/minetest_game/mods/ ,
/usr/share/minetest/games/minetest/
2. Global mods, the location where mods are almost always installed. If
in doubt, put them here. Eg: minetest/mods/
3. World Mods, the location to store mods that are specific to a
particular world. Eg: minetest/worlds/world/worldmods/
minetest is the user data directory. You can find the location of the user
data directory by opening Minetest and clicking "Open user data directory"
in the Credits tab.
When loading mods, Minetest will check each of the above locations in
order. If you find a mod with a name the same as the one found previously,
the later mod will be loaded instead of the previous mod. This means that
you can override game mods by placing a mod with the same name in the
global mod location.
Mod Directory
A mod name is used to refer to a mod. Each mod must have a unique
name. Mod names can include letters, numbers, and underscores. A good
name should describe what the mod does, and the directory containing a
mod's components should have the same name as the mod name. To find
out if a mod name is available, try searching for it on content.minetest.net .
mymod
dependencies.
Only the init.lua file is required in a mod to run on game load; however,
mod.conf is recommended and other components may be needed
depending on the functionality of the mod.
mod.conf
This file is used for the mod's metadata, including the name, description,
and other information about the mod.
For example:
name = mymod
Dependencies
optional_depends = modthree
Mod Packs
Mods can be grouped into mod packs, which allow multiple mods to be
packaged and moved together. They are useful if you want to provide
multiple mods to a player, but don't want them to download each one
individually.
modpack1
├── mod1
Please note that a modpack is not a game . Games have their own
organizational structure that will be explained in the Games chapter.
Example
Here's an example that brings all this together:
Mod Folder
mymod
├── textures
└── mod.conf
init.lua
print ( "This file will be run at load time!" )
minetest.register_node( "mymod:node" , {
tiles = { "mymod_node.png" },
groups = {cracky = 1 }
})
minetest.register_craft({
type = "shapeless" ,
})
mod.conf
name = mymod
depends = default
This mod has the name "mymod". It has two text files: init.lua and
mod.conf. The script prints a message and then registers a node and a
craft recipe; these will be explained later. There is a single dependency,
the default mod , which is usually found on Minetest Game. There is also a
texture in /textures for the node.
Lua Scripting
Introduction
In this chapter we will talk about creating scripts in Lua, the tools needed
to help with this, and some techniques that you may find useful.
Code editors
Coding in Lua
o Program flow
o Variable types
o Arithmetic operators
o Selection
o Logical operators
Programming
Local and global reach
o Premises should be used as much as possible
Including other Lua scripts
Code editors
A code editor with code highlighting is sufficient for writing scripts in Lua.
Code highlighting uses different colors for words and characters
depending on what they represent. This allows you to easily notice errors
and inconsistencies.
For example:
return false
end
ctf.team(team).log = {}
end
table.insert (ctf.team(team).log, 1 ,msg)
ctf.save()
return true
end
The keywords in this example stand out, including if , then , end , and
return . Functions that come with Lua by default, such as table.insert ,
are also highlighted.
Coding in Lua
Program flow
Programs are a series of commands that are executed one after another.
We call these commands "statements." Program flow is how these
statements are executed, and different flow types allow you to skip or skip
sets of commands.
In this example, a , b , and result are the variables . Local variables are
declared using the local keyword and then given an initial value. Local will
be discussed later, because it is part of a very important concept called
scope .
The = sign means assignment , so result = a + b means setting the
value of result to the value of a + b . Variable names can have more than
one character, as seen with the result variable. It's also worth noting that
like most languages, Lua is case-sensitive ; A is a different variable than
a .
Variable types
A variable will be only one of the following types and can change type after
an assignment. It is good practice to ensure that a variable is only null or
of a unique non-null type.
A-B Subtraction 2 - 10 = -8
A*B Multiplication 2 * 2 = 4
Please note that this is not an exhaustive list; It does not contain all
possible operators.
Selection
100.
print ( "Woohoo!" )
else
print ( "No!" )
end
print ( "Yay!" )
end
Logical and arithmetic operators work the same way; both accept input
and return a value that can be stored. For example:
local A = 5
local is_equal = (A == 5 )
if is_equal then
end
Programming
Programming is the action of solving a problem, such as arranging a list of
items and converting them into steps that a computer can understand.
local one = 1
function myfunc ()
end
end
function one ()
foo = "bar"
end
function two ()
end
one()
two()
init.lua:2
end
function two ()
end
one()
two()
Remember that nil means uninitialized . The variable has not yet been
assigned a value, does not exist, or has not been initialized (that is, set to
nil).
Functions are variables of a special type, but they must also be made
local, because other mods might have functions with the same names.
return bar * 2
end
To allow mods to call your functions, you must create a table with the
same name as the mod and add your function to it. This table is often
called an API table or namespace.
mymod = {}
end
mymod.foo( "foobar" )
Including other Lua scripts
The recommended way to include other Lua scripts in a mod is to use
dofile .
A script can return a value, which is useful for sharing private premises:
-- script.lua
-- init.lua
"/script.lua" )
Later chapters will discuss the best way to split code for a mod.
Introduction
Registering new nodes and crafting items, and creating crafting recipes,
are basic requirements for many mods.
A node is an item that can be placed or found in the world. Each position
in the world must be occupied with a single node; apparently blank
positions are usually air nodes.
A tool has the ability to wear out and typically has non-predetermined
digging capabilities. In the future, crafting items and tools will likely be
merged into one item type, as the distinction between them is rather
artificial.
Article Registration
Element definitions consist of an element name and a definition table . The
definition table contains attributes that affect the behavior of the item.
minetest.register_craftitem( "modname:itemname" , {
inventory_image = "modname_itemname.png"
})
Item names
Each element has an element name used to refer to it, which must be in
the following format:
modname:itemname
The mod name is the name of the mod the item is registered to, and the
item name is the name of the item itself. The item name must be relevant
to what the item is and cannot be registered.
Element aliases
Elements can also have aliases that point to their name. An alias is a
pseudo-element name that results in the engine treating any occurrence of
the alias as if it were the element name. There are two main common uses
of this:
Mods should make sure to resolve aliases before dealing directly with item
names, as the engine will not do this. However, this is pretty simple:
Textures
Textures should be placed in the textures/ folder with names in the format
modname_itemname.png .
JPEG textures are supported, but they do not support transparency and
are generally poor quality at low resolutions. It is often better to use the
PNG format.
tiles = { "mymod_diamond.png" },
is_ground_content = true ,
})
The tiles property is a table of texture names that the node will use.
When there is only one texture, this texture is used on all sides. To give a
different texture per side, provide the names of 6 textures in this order:
up (+Y), down (-Y), right (+X), left (-X), back (+Z), front (-Z).
minetest.register_node( "mymod:diamond" , {
tiles = {
"mymod_diamond_up.png" , -- and+
"mymod_diamond_down.png" , -- and-
"mymod_diamond_right.png" , -- x+
"mymod_diamond_left.png" , -- x-
"mymod_diamond_back.png" , -- z+
"mymod_diamond_front.png" , --z-
},
is_ground_content = true ,
groups = {cracky = 3 },
drop = "mymod:diamond_fragments"
-- ^ Rather than dropping diamond, drop
mymod:diamond_fragments
})
on_use
minetest.register_craftitem( "mymod:mudpie" , {
inventory_image = "myfood_mudpie.png" ,
on_use = minetest.item_eat( 20 ),
})
minetest.register_craftitem( "mymod:mudpie" , {
description = "Alien Mud Pie" ,
inventory_image = "myfood_mudpie.png" ,
end ,
})
Elaboration
There are several types of crafting recipes available, indicated by the type
property.
Craft recipes are not items, so they do not use item names to uniquely
identify themselves.
Conformed
Shaped recipes are when the ingredients must be the correct shape or
pattern for them to work. In the example below, the fragments must be in a
chair-like pattern for the craft to work.
minetest.register_craft({
type = "shaped" ,
recipe = {
{ "mymod:diamond_fragments" , "" , "" },
})
One thing to note is the blank column on the right side. This means that
there must be an empty column to the right of the form; otherwise this
won't work. If this empty column should not be needed, then the empty
strings can be left out as follows:
minetest.register_craft({
recipe = {
{ "mymod:diamond_fragments" , "" },
{ "mymod:diamond_fragments" , "mymod:diamond_fragments" },
{ "mymod:diamond_fragments" , "mymod:diamond_fragments" }
})
The type field is not really necessary for shaped crafts, since shape is the
default craft type.
Report
Formless recipes are a type of recipe used when it doesn't matter where
the ingredients are placed, just that they are there.
minetest.register_craft({
type = "shapeless" ,
recipe = {
"mymod:diamond_fragments" ,
"mymod:diamond_fragments" ,
"mymod:diamond_fragments" ,
},
})
Recipes with the "cook" type are not crafted in the crafting grid, but are
instead cooked in ovens or other cooking tools that can be found in mods.
minetest.register_craft({
type = "cooking" ,
output = "mymod:diamond_fragments" ,
recipe = "default:coalblock" ,
cooktime = 10 ,
})
The only real difference in the code is that the recipe is just an element,
compared to being in a table (in curly braces). They also have an optional
"cook time" parameter that defines how long the item takes to cook. If not
set, the default value is 3.
The above recipe works when the charcoal block is in the entry slot, with
some type of fuel underneath. Create diamond shards after 10 seconds!
minetest.register_craft({
type = "fuel" ,
recipe = "mymod:diamond" ,
burntime = 300 ,
})
They don't have an output like other recipes, but they do have a burn time
that defines how long they will last as fuel in seconds. So, diamond is good
as fuel for 300 seconds!
Groups
Elements can be members of many groups, and groups can have many
members. Groups are defined using the groups property on the definition
table and have an associated value.
groups = {cracky = 3 , wood = 1 }
There are several reasons why you use groups. First, groups are used to
describe properties such as excavation types and flammability. Secondly,
groups can be used in a craft recipe in place of an item name to allow any
item in the group to be used.
minetest.register_craft({
type = "shapeless" ,
})
Each tool has a tool capacity. A capability includes a list of supported dig
types and associated properties for each type, such as dig times and
amount of wear. Tools can also have a maximum supported hardness for
each type, allowing weaker tools to be prevented from digging into harder
nodes. It is very common for tools to include all types of excavation in their
capabilities, and the least suitable ones have very inefficient properties. If
the item a player is currently wielding does not have an explicit tool ability,
then the current hand ability is used.
minetest.register_tool( "mymod:tool" , {
inventory_image = "mymod_tool.png" ,
tool_capabilities = {
full_punch_interval = 1 . 5 ,
max_drop_level = 1 ,
groupcaps = {
crumbly = {
maxlevel = 2 ,
uses = 20 ,
times = { [ 1 ] = 1 . 60 , [ 2 ] = 1 . 20 , [ 3 ]
= 0 . 80 }
},
},
damage_groups = {fleshy = 2 },
},
})
Groupcaps is the list of supported dig types for dig nodes. Damage groups
are used to control how tools damage objects, which will be discussed
later in the Objects, Players, and Entities chapter.
Introduction
The method by which a node is drawn is called drawing type. There are
many types of drawing available. The behavior of a drawing type can be
controlled by providing properties in the node type definition. These
properties are fixed for all instances of this node. It is possible to control
some properties per node using something called param2 .
In the previous chapter, the concept of nodes and elements was
introduced, but a complete definition of a node was not provided. The
Minetest world is a 3D grid of positions. Each position is called a node and
consists of the node type (name) and two parameters (param1 and
param2). The minetest.register_node function is a bit tricky in that it
doesn't actually register a node, it registers a new type of node.
Node parameters are used to control how an individual node is rendered.
param1 is used to store the lighting of a node, and the meaning of param2
depends on the paramtype2 property of the node type definition.
Cubic nodes: normal and all faces
Crystal nodes
o Glasslike_Framed
Aeronautical nodes
Lighting and sunlight propagation
Liquid nodes
node boxes
o Wall Mounted Node Boxes
mesh nodes
Signlike nodes
Plant nodes
Fire-like nodes
More types of drawing
In contrast, the allfaces drawing type will still represent the inner side when
it is against a solid node. This is good for nodes with partially transparent
sides, such as leaf nodes. You can use the allfaces_optional drawing type
to allow users to opt out of the slower drawing, in which case it will act as a
normal node.
minetest.register_node( "mymod:diamond" , {
tiles = { "mymod_diamond.png" },
groups = {cracky = 3 },
})
minetest.register_node( "default:leaves" , {
description = "Leaves" ,
drawtype = "allfaces_optional" ,
tiles = { "default_leaves.png" }
})
Note: Normal drawing type is the default drawing type, so you do not need
to specify it explicitly.
Crystal nodes
The difference between glass-like and normal nodes is that placing a
glass-like node next to a normal node will not cause the side of the normal
node to be hidden. This is useful because glass-like nodes tend to be
transparent, so using a normal drawing type would result in the ability to
see through the world.
Glasslike
edges
minetest.register_node( "default:obsidian_glass" , {
drawtype = "glasslike" ,
tiles = { "default_obsidian_glass.png" },
paramtype = "light" ,
is_ground_content = false ,
sunlight_propagates = true ,
sounds = default.node_sound_glass_defaults(),
})
Glasslike_Framed
This makes the node border surround the entire thing with a 3D effect,
rather than individual nodes, like the following:
Glasslike_Fra
med Edges
You can use the glasslike_framed_optional drawing type to allow the user
to opt for the framed appearance.
minetest.register_node( "default:glass" , {
description = "Glass" ,
drawtype = "glasslike_framed" ,
),
paramtype = "light" ,
sounds = default.node_sound_glass_defaults()
})
Aeronautical nodes
These nodes are not rendered and therefore have no textures.
minetest.register_node( "myair:air" , {
drawtype = "airlike" ,
paramtype = "light" ,
sunlight_propagates = true ,
walkable = false , -- Would make the player collide with the air
node
air node
air_equivalent = true ,
drop = "" ,
groups = {not_in_creative_inventory = 1 }
})
By default, a node type will not allow light to be stored in any node
instance. It is generally desirable that some nodes, such as glass and air,
can let light pass through. To do this, there are two properties that must be
defined:
paramtype = "light" ,
sunlight_propagates = true ,
The first line means that param1 does, in fact, store the light level. The
second line means that sunlight must pass through this node without
decreasing its value.
Liquid nodes
minetest.register_node( "default:water_source" , {
drawtype = "liquid" ,
paramtype = "light" ,
),
animated
tiles = {
name = "default_water_source_animated.png" ,
animation = {
type = "vertical_frames" ,
aspect_w = 16 ,
aspect_h = 16 ,
length = 2 . 0
},
special_tiles = {
name = "default_water_source_animated.png" ,
animation = { type = "vertical_frames" , aspect_w =
16 ,
aspect_h = 16 , length = 2 . 0 },
backface_culling = false ,
},
--
-- Behavior
--
alpha = 160 ,
--
--Liquid Properties
--
drowning = 1 ,
liquidtype = "source" ,
liquid_alternative_flowing = "default:water_flowing" ,
liquid_alternative_source = "default:water_source" ,
liquid_viscosity = WATER_VISC,
-- ^ how fast
liquid_range = 8 ,
-- ^ how far
})
Flowing nodes have a similar definition, but with a different name and
animation. See default: water_flowing in the default mod in minetest_game
for a complete example.
node boxes
Node boxes allow you to create a node that is not cubic, but is made of as
many cuboids as you want.
minetest.register_node( "stairs:stair_stone" , {
drawtype = "nodebox" ,
paramtype = "light" ,
node_box = {
type = "fixed" ,
fixed = {
},
}
})
Each row is a cuboid that joins together to form a single node. The first
three numbers are the coordinates, from -0.5 to 0.5 inclusive, of the lower
front left corner, the last three numbers are the opposite corner. They are
in the form X, Y, Z, where Y is at the top.
You can use NodeBoxEditor to create node boxes by dragging the edges,
it's more visual than doing it by hand.
Sometimes you want different node boxes for when placed on the floor,
wall, or ceiling, such as with torches.
minetest.register_node( "default:sign_wall" , {
drawtype = "nodebox" ,
node_box = {
type = "wallmounted" ,
-- Ceiling
wall_top = {
},
--Floor
wall_bottom = {
},
-- Wall
wall_side = {
},
})
mesh nodes
While node boxes are generally easier to make, they are limited because
they can only consist of cuboids. Node boxes are also not optimized;
Internal faces will continue to be rendered even when completely hidden.
A face is a flat surface on a mesh. An inside face occurs when the faces of
two different node boxes overlap, causing parts of the node box model to
be invisible but still rendered.
minetest.register_node( "mymod:meshy" , {
drawtype = "mesh" ,
tiles = {
"mymod_meshy.png"
},
mesh = "mymod_meshy.b3d" ,
})
Make sure the mesh is available in a models directory. Most of the time,
the mesh should be in your mod's folder, however, it's okay to share a
mesh provided by another mod you depend on. For example, a mod that
adds more types of furniture may want to share the model provided by a
basic furniture mod.
Signlike nodes
Sign nodes are flat nodes that can be mounted on the sides of other
nodes.
Despite the name of this type of drawing, signs do not actually tend to use
the sign type, but instead use the nodebox type drawing to provide a 3D
effect. The signlike drawtype is, however, commonly used for stairs.
minetest.register_node( "default:ladder_wood" , {
drawtype = "signlike" ,
tiles = { "default_ladder_wood.png" },
paramtype2 = "wallmounted" ,
selection_box = {
type = "wallmounted" ,
},
})
Plant nodes
drawtype = "plantlike" ,
tiles = { "default_papyrus.png" },
selection_box = {
type = "fixed" ,
/ 16 },
},
})
Fire-like nodes
Firelike is similar to plantlike, except that it is designed to "stick" to walls
and ceilings.
fire nodes
minetest.register_node( "mymod:clingere" , {
drawtype = "firelike" ,
--Only one texture used
tiles = { "mymod:clinger" },
})
Fencelike
Rooted plant - for underwater plants
Raillike - for cart rails
Torch: for 2D wall/floor/ceiling nodes. Torches in Minetest Game
actually use two different node definitions of mesh nodes (default:
torch and default: torch_wall).
As always, read the Lua API documentation for the full list
Introduction
In this chapter, you will learn how to use and manipulate inventories,
whether it is a player inventory, a node inventory, or a separate inventory.
ItemStacks
ItemStacks has four components: name, count, wear, and metadata.
The item name can be the registered item name, an alias, or an unknown
item name. Unknown items are common when users uninstall mods or
when mods remove items without precautions, such as registering aliases.
print (stack:get_name())
stack:set_name( "default:dirt" )
end
print (stack:get_count())
stack:set_count( 10 )
Item metadata is an unlimited key-value store for data about the item. Key-
value means that you use a name (called key) to access the data (called
value). Some keys have a special meaning, such as description which is
used to have a per-stack element description. This will be covered in more
detail in the Metadata and Storage chapter.
Inventory Locations
An inventory location is where and how inventory is stored. There are
three types of inventory location: player, node, and separate. An inventory
is directly linked to one and only one location; Updating the inventory will
cause it to be updated immediately.
, y = 2 , z = 3 } })
"player1" })
-- or
local inv = player:get_inventory()
Unlike the other types of inventory, you must first create a separate
inventory before accessing it:
minetest.create_detached_inventory( "inventory_name" )
minetest.create_detached_inventory( "inventory_name" , {
end ,
end ,
end ,
minetest.chat_send_all(player:get_player_name() ..
"gave" .. stack:to_string() ..
minetest.pos_to_string(player:get_pos()))
end ,
})
Allow callbacks, that is, those starting with allow_ , return the number of
items to transfer, with 0 to prevent the transfer altogether.
In contrast, action callbacks, starting with on_ , do not have a return value.
Lisa
Inventory lists are a concept used to allow multiple grids to be stored
within a single location. This is especially useful for the player as there are
a number of common lists that all games have, such as the main inventory
and slot machines .
Lists have a size, which is the total number of cells in the grid, and a width,
which is only used within the engine. The list width is not used when
drawing inventory in a window, because the code behind the window
determines the width to use.
inv:set_width( "main" , 8 )
else
end
set_size will fail and return false if the list name or size is invalid. For
example, the new size may be too small to fit all of the current items in
inventory.
Content Check
is_empty can be used to see if a list contains any elements:
if inv:is_empty( "main" ) then
end
end
Add to a list
add_item adds elements to a list (in this case "main" ). In the following
example, the maximum stack size is also respected:
local stack = ItemStack( "default:stone 99" )
end
Taking items
Battery handling
-- ^ will be 51
-- ^ will be 80
-- min(50+100, stack_max) - 19 = 80
-- where stack_max = 99
add_item will add items to an ItemStack and return those that could not be
added. take_item will take up to the number of items but can take less,
and returns the taken stack.
inv:set_stack(listname, 0 , stack)
Wear
Tools may have wear; Wear shows a progress bar and causes the tool to
break when completely worn. Wear is a number of 65535; The higher it is,
the more worn the tool is.
local max_uses = 10
-- This is done automatically when you use a tool that digs things
When excavating a node, the amount of wear on a tool may depend on the
node being excavated. So max_uses varies depending on what is being
excavated.
Lua tables
ItemStacks and Inventories can be converted to and from tables. This is
useful for copying and performing bulk operations.
-- Entire inventory
inv2:set_lists(data)
-- One list
list_one = {
ItemStack,
ItemStack,
ItemStack,
ItemStack,
-- inv:get_size("list_one") elements
},
list_two = {
ItemStack,
ItemStack,
ItemStack,
ItemStack,
-- inv:get_size("list_two") elements
One important thing to note is that the methods stated above do not
change the size of the lists. This means that you can delete a list by
setting it to an empty table and it will not decrease in size:
Introduction
In this chapter, you will learn how to perform basic actions on the map.
Map structure
Read
o Reading nodes
o Find nodes
Writing
o Write nodes
o Delete nodes
Loading blocks
Delete blocks
Map structure
The Minetest map is divided into MapBlocks, each MapBlocks is a cube of size 16. As
players travel around the map, MapBlocks are created, loaded, activated, and downloaded.
Areas of the map that are not yet loaded are full of ignored nodes, an impassable and
unselectable placeholder node. Empty space is filled with air nodes, an invisible node that
you can walk through.
Loaded map blocks are often called active blocks . Mods or players can read or write to
Active Blocks, and have active entities. The engine also performs operations on the map,
such as performing liquid physics.
MapBlocks can be loaded from the global database or generated. MapBlocks will be
generated up to the map generation limit ( mapgen_limit ) which is set to its maximum
value, 31000, by default. However, existing MapBlocks can be loaded from the world
database outside the generation limit.
Read
Reading nodes
If the position is a decimal, it will be rounded to the containing node. The function will
always return a table containing the node information:
name - The name of the node, which will be ignored when the area is unloaded.
param1 - View node definition. This will normally be light.
param2 - View node definition.
It's worth noting that the function will not load the containing block if the block is inactive,
but will instead return a table with name being ignored .
Instead, you can use minetest.get_node_or_nil , which will return nil instead of a
table with the name ignore . However, it still won't load the block. This can still return
ignore if a block actually contains ignore. This will happen near the edge of the map as
defined by the map generation limit ( mapgen_limit ).
Find nodes
Minetest offers a number of auxiliary functions to speed up common map actions. The most
used are to find nodes.
For example, let's say we want to make a certain type of plant that grows best near me; You
should look for nearby mese nodes and adapt the growth rate accordingly.
minetest.find_node_near will return the first node found in a certain radius that
matches the given node or group names. In the following example, we search for a mese
node within 5 nodes of the position:
local grow_speed = 1
"default:mese" })
if node_pos then
grow_speed = 2
end
Say, for example, the growth rate increases the more there is nearby. Then you need to use
a function that can find multiple nodes in the area:
local pos_list =
local pos_list =
local grow_speed = 1
for i = 1 , # pos_list do
5 * 5 then
grow_speed = grow_speed + 1
end
end
The code will now correctly increase in grow_speed based on the mese nodes in the range.
Notice how we compare the squared distance from the position, instead of rooting it
squared to get the actual distance. This is because computers consider square roots to be
computationally expensive, so they should avoid them as much as possible.
There are more variations of the above two functions, such as find_nodes_with_meta
and find_nodes_in_area_under_air , which work similarly and are useful in other
circumstances.
Writing
Write nodes
You can use set_node to write to the map. Each call to set_node will cause lighting to be
recalculated and node callbacks to be executed, which means set_node is quite slow for a
large number of nodes.
minetest.set_node({ x = 1 , y = 3 , z = 4 }, { name =
"default:mese" })
set_node will remove any associated metadata or inventory from that position. This is not
desirable in all circumstances, especially if you are using multiple node definitions to
represent a conceptual node. An example of this is the furnace node: although conceptually
you think of it as one node, there are actually two.
You can configure a node without deleting metadata or inventory like this:
minetest.swap_node({ x = 1 , y = 3 , z = 4 }, { name =
"default:mese" })
Delete nodes
There must always be a node. To remove a node, set the position to air .
The following two lines will delete a node and they are both identical:
minetest.remove_node(pos)
Loading blocks
You can use minetest.emerge_area to load map blocks. The emergency area is
asynchronous, meaning that blocks will not be loaded instantly. Instead, they will be loaded
soon in the future and the callback will be called every time.
-- Load to 20x20x20 area
local halfsize = { x = 10 , y = 10 , z = 10 }
num_calls_remaining, context)
context.total_blocks = num_calls_remaining + 1
context.loaded_blocks = 0
end
context.loaded_blocks = context.loaded_blocks + 1
-- Send progress message
else
context.total_blocks
minetest.chat_send_all(msg)
end
end
Eliminar bloques
Puede usar delete_blocks para eliminar un rango de bloques de mapa:
minetest.delete_area(pos1, pos2)
Esto eliminará todos los bloques de mapas en esa área, inclusive . Esto significa que
algunos nodos se eliminarán fuera del área, ya que estarán en un bloque de mapa que se
superpone a los límites del área.
Ejecutar periódicamente una función en ciertos nodos es una tarea
común. Minetest proporciona dos métodos para hacer esto: modificadores
de bloques activos (ABM) y temporizadores de nodo.
Los ABM escanean todos los MapBlocks cargados en busca de nodos que
coincidan con un criterio. Son los más adecuados para los nodos que se
encuentran con frecuencia en el mundo, como la hierba. Tienen una
sobrecarga de CPU alta, pero poca sobrecarga de memoria y
almacenamiento.
Para los nodos que son poco comunes o que ya usan metadatos, como
hornos y máquinas, se deben usar temporizadores de nodo en su
lugar. Los temporizadores de nodo funcionan al realizar un seguimiento de
los temporizadores pendientes en cada MapBlock y luego ejecutarlos
cuando caducan. Esto significa que los temporizadores no necesitan
buscar todos los nodos cargados para encontrar coincidencias, sino que
requieren un poco más de memoria y almacenamiento para el seguimiento
de los temporizadores pendientes.
Temporizadores de nodo
Modificadores de bloque activos
Tu turno
Temporizadores de nodo
Los temporizadores de nodo están directamente vinculados a un solo
nodo. Puede administrar los temporizadores de nodo obteniendo un objeto
NodeTimerRef.
timer:start(10.5) -- in seconds
minetest.register_node("autodoors:door_open", {
on_timer = function(pos)
end
})
Es posible que haya notado una limitación con los temporizadores: por
razones de optimización, solo es posible tener un tipo de temporizador por
tipo de nodo y solo un temporizador en ejecución por nodo.
minetest.register_node("aliens:grass", {
tiles = {"aliens_grass.png"},
groups = {choppy=1},
on_use = minetest.item_eat(20)
})
minetest.register_abm({
nodenames = {"default:dirt_with_grass"},
neighbors = {"default:water_source",
"default:water_flowing"},
end
})
Este ABM se ejecuta cada diez segundos y, para cada nodo coincidente,
existe una probabilidad de 1 entre 50 de que se ejecute. Si el ABM se
ejecuta en un nodo, se coloca un nodo de hierba extraterrestre encima de
él. Tenga en cuenta que esto eliminará cualquier nodo ubicado
anteriormente en esa posición. Para evitar esto, debe incluir una
verificación usando minetest.get_node para asegurarse de que haya
espacio para el césped.
Tu turno
Toque de Midas: haz que el agua se convierta en bloques de oro
con una probabilidad de 1 en 100, cada 5 segundos.
Descomposición: hace que la madera se convierta en tierra cuando
el agua es vecina.
Burnin ': Haz que todos los nodos aéreos se incendien. (Consejo:
"aire" y "fuego: llama_básica"). Advertencia: espere que el juego se
bloquee.
Almacenamiento y metadatos
Introducción
En este capítulo, aprenderá cómo almacenar datos.
Metadatos
o ¿Qué son los metadatos?
o Obtener un objeto de metadatos
o Leyendo y escribiendo
o Llaves especiales
o Almacenamiento de tablas
o Metadatos privados
o Mesas Lua
Almacenamiento Mod
Bases de datos
Decidir cuál usar
Tu turno
Metadatos
Los metadatos son datos sobre datos. Los datos en sí, como el tipo de un
nodo o el recuento de una pila, no son metadatos.
Leyendo y escribiendo
En la mayoría de los casos, se utilizarán
métodos get_<type>() y set_<type>() para leer y escribir en meta. Los
metadatos almacenan cadenas, por lo que los métodos de cadena se
establecerán directamente y obtendrán el valor.
print(meta:get_string("foo")) --> ""
meta:set_string("foo", "bar")
print(meta:get_int("count")) --> 0
meta:set_int("count", 3)
print(meta:get_int("count")) --> 3
Llaves especiales
infotext se utiliza en Metadatos de nodo para mostrar información sobre
herramientas cuando se pasa el cursor por encima de un nodo. Esto es
útil cuando se muestra la propiedad o el estado de un nodo.
description se utiliza en ItemStack Metadata para anular la descripción al
pasar el cursor sobre la pila en un inventario. Puede utilizar colores
codificándolos con minetest.colorize() .
owner es una clave común que se utiliza para almacenar el nombre de
usuario del jugador propietario del elemento o nodo.
Almacenamiento de tablas
El método Lua tiende a ser mucho más rápido y coincide con el formato
que usa Lua para las tablas, mientras que JSON es un formato más
estándar, está mejor estructurado y es adecuado para cuando necesita
intercambiar información con otro programa.
meta:set_string("foo", minetest.serialize(data))
data = minetest.deserialize(minetest:get_string("foo"))
Metadatos privados
meta:set_string("secret", "asd34dn")
meta:mark_as_private("secret")
Mesas Lua
Puede convertir desde y hacia tablas Lua usando to_table y from_table :
local tmp = meta:to_table()
tmp.foo = "bar"
meta:from_table(tmp)
Almacenamiento Mod
El almacenamiento de modificaciones utiliza exactamente la misma API
que los metadatos, aunque técnicamente no son metadatos. El
almacenamiento de mod es por mod, y solo se puede obtener durante el
tiempo de carga para saber qué mod lo está solicitando.
storage:set_string("foo", "bar")
Bases de datos
Si es probable que el mod se use en un servidor y almacene muchos
datos, es una buena idea ofrecer un método de almacenamiento de base
de datos. Debe hacer que esto sea opcional separando cómo se
almacenan los datos y dónde se utilizan.
local backend
if use_database then
backend =
dofile(minetest.get_modpath("mymod") ..
"/backend_sqlite.lua")
else
backend =
dofile(minetest.get_modpath("mymod") ..
"/backend_storage.lua")
end
backend.get_foo("a")
backend.set_foo("a", { score = 3 })
storage:set_string(key, minetest.serialize(value))
end
function backend.get_foo(key)
return minetest.deserialize(storage:get_string(key))
end
return backend
local ie = minetest.request_insecure_environment()
settings")
if sqlite3 then
sqlite3 = nil
end
Enseñar sobre SQL o cómo usar la biblioteca lsqlite3 está fuera del
alcance de este libro.
Decidir cuál usar
El tipo de método que use depende de qué se tratan los datos, cómo
están formateados y qué tan grandes son. Como pauta, los datos
pequeños son de hasta 10K, los datos medianos son de hasta 10 MB y los
datos grandes son de cualquier tamaño por encima de eso.
Los metadatos de nodo son una buena opción cuando necesita almacenar
datos relacionados con el nodo. El almacenamiento de datos medianos es
bastante eficiente si lo hace privado.
Tu turno
Crea un nodo que desaparezca después de haber sido golpeado
cinco veces. (Úselo on_punch en la definición de nodo
y minetest.set_node .)
Introducción
En este capítulo, aprenderá a manipular objetos y a definir los suyos
propios.
¿Qué son objetos, jugadores y entidades?
Posición y velocidad
Propiedades del objeto
Entidades
Salud y daño
Archivos adjuntos
Tu turno
Posición y velocidad
get_pos y set_pos existen para permitirle obtener y establecer la posición
de una entidad.
local object = minetest.get_player_by_name("bob")
object:set_properties({
visual = "mesh",
mesh = "character.b3d",
textures = {"character_texture.png"},
})
local MyEntity = {
initial_properties = {
hp_max = 1,
physical = true,
collide_with_objects = false,
visual = "wielditem",
textures = {""},
spritediv = {x = 1, y = 1},
initial_sprite_basepos = {x = 0, y = 0},
},
function MyEntity:set_message(msg)
self.message = msg
end
print("entity is at " ..
minetest.pos_to_string(object:get_pos()))
function MyEntity:on_step(dtime)
local delta
delta = vector.new(0, 0, 1)
else
delta = vector.new(0, 1, 0)
end
self.object:move_to(vector.add(pos, delta))
end
function MyEntity:on_punch(hitter)
minetest.chat_send_player(hitter:get_player_name(),
self.message)
end
Ahora, si tuviera que generar y usar esta entidad, notaría que el mensaje
se olvidaría cuando la entidad se vuelve inactiva y luego activa
nuevamente. Esto se debe a que el mensaje no se guarda. En lugar de
guardar todo en la tabla de entidades, Minetest le da control sobre cómo
guardar cosas. Staticdata es una cadena que contiene toda la información
personalizada que debe almacenarse.
function MyEntity:get_staticdata()
return minetest.write_json({
message = self.message,
})
end
self:set_message(data.message)
end
end
local pos = { x = 1, y = 2, z = 3 }
obj:get_luaentity():set_message("hello!")
/spawnentity mymod:entity
Salud y daño
object:set_hp(hp + 3)
El daño total se calcula multiplicando los grupos de daño del golpe con las
vulnerabilidades del objetivo. Luego, esto se limita dependiendo de qué
tan reciente fue el último golpe. Veremos un ejemplo de este cálculo en un
momento.
})
full_punch_interval = 0.8,
max_drop_level=1,
groupcaps={
maxlevel=2},
},
local time_since_last_punch =
tool_capabilities.full_punch_interval
Ahora, averigüemos cuál será el daño. Los grupos de daño del golpe
son fleshy=5 y choppy=10 , y target recibirán un 90% de daño de carnoso y
un 0% de entrecortado.
Primero, multiplicamos los grupos de daño por la vulnerabilidad y
sumamos el resultado. Luego multiplicamos por un número entre 0 o 1
dependiendo del time_since_last_punch .
= (5*90/100 + 10*0/100) * limit(time_since_last_punch /
full_punch_interval, 0, 1)
= (5*90/100 + 10*0/100) * 1
= 4.5
Archivos adjuntos
Los objetos adjuntos se moverán cuando el padre, el objeto al que están
adjuntos, se mueva. Se dice que un objeto adjunto es hijo del padre. Un
objeto puede tener un número ilimitado de hijos, pero como máximo uno
de los padres.
Grados y radianes
La rotación de los adjuntos se establece en grados, mientras que la
rotación del objeto está en radianes. Asegúrese de convertir a la medida
de ángulo correcta.
obj:set_attach(player,
"Arm_Right", -- default bone
Tu turno
Haz un molino de viento combinando nodos y una entidad.
Haga una mafia de su elección (usando solo la API de entidad y sin
usar ninguna otra modificación).
Privilegios
Introducción
Los privilegios, a menudo llamados privs para abreviar, dan a los
jugadores la capacidad de realizar ciertas acciones. Los propietarios de
servidores pueden otorgar y revocar privilegios para controlar qué
habilidades tiene cada jugador.
interactuar
gritar
no hay video
volar
patear
prohibición
votar
Modificar
area_admin - las funciones de administración de un mod están bien
Malos privilegios:
moderador
administración
duende
enano
Declaración de privilegios
Úselo register_privilege para declarar un nuevo privilegio:
minetest.register_privilege("vote", {
give_to_singleplayer = true
})
Comprobación de privilegios
Para comprobar rápidamente si un jugador tiene todos los privilegios
necesarios:
interact = true,
vote = true })
interact = true,
vote = true })
if has then
else
end
end
print(dump(privs))
privs.vote = true
minetest.set_player_privs(name, privs)
fly = true,
interact = true,
shout = true
Chat y comandos
Introducción
Los mods pueden interactuar con el chat del jugador, incluido el envío de
mensajes, la interceptación de mensajes y el registro de comandos de
chat.
El mensaje aparece en una línea separada para distinguirlo del chat del
jugador en el juego.
player1")
Comandos de chat
Para registrar un comando de chat, por ejemplo /foo ,
use register_chatcommand :
minetest.register_chatcommand("foo", {
privs = {
interact = true,
},
end,
})
Subcomandos complejos
A menudo se requiere realizar comandos de chat complejos, como:
También hay una biblioteca escrita por el autor de este libro que se puede
usar para crear comandos de chat complejos sin patrones llamada Chat
Command Builder .
Interceptar mensajes
Para interceptar un mensaje, use register_on_chat_message:
minetest.register_on_chat_message(function(name, message)
return false
end)
Al devolver false, permite que el controlador predeterminado envíe el
mensaje de chat. En realidad, puede eliminar la línea return false y
seguirá funcionando igual, porque nil se devuelve implícitamente y se
trata como falsa.
⚠
then
else
end
return false
end)
Física del jugador
Introducción
La física del jugador se puede modificar usando anulaciones de física. Las
anulaciones físicas pueden establecer la velocidad al caminar, la
velocidad del salto y las constantes de gravedad. Las anulaciones de
física se establecen jugador por jugador y son multiplicadores. Por
ejemplo, un valor de 2 para la gravedad haría que la gravedad sea dos
veces más fuerte.
Ejemplo básico
Anulaciones disponibles
o Comportamiento de movimiento antiguo
Incompatibilidad de mod
Tu turno
Ejemplo básico
A continuación, se muestra un ejemplo de cómo agregar un comando de
antigravedad, que pone a la persona que llama en G bajo:
minetest.register_chatcommand("antigravity", {
player:set_physics_override({
value
-- (0.1 * 9.81)
})
end,
})
Anulaciones disponibles
player:set_physics_override() se le da una tabla de anulaciones.
Según lua_api.txt , estos pueden ser:
Incompatibilidad de mod
Tenga en cuenta que las modificaciones que anulan el mismo valor físico
de un jugador tienden a ser incompatibles entre sí. Al configurar una
anulación, sobrescribe todas las anulaciones que se hayan establecido
antes. Esto significa que si múltiples anulaciones establecen la velocidad
de un jugador, solo el último en ejecutarse estará en efecto.
Tu turno
Sonic : establece el multiplicador de velocidad en un valor alto (al
menos 6) cuando un jugador se une al juego.
Super rebote : aumenta el valor del salto para que el jugador pueda
saltar 20 metros (1 metro es 1 nodo).
Espacio : haz que la gravedad disminuya a medida que el jugador
sube.
Introducción
Anatomía de un Formspec
Elementos
type[param1;param2]
Se declara el tipo de elemento y luego los parámetros se dan entre
corchetes. Se pueden unir varios elementos o colocarlos en varias líneas,
así:
foo[param1]bar[param1]
bo[param1]
Encabezamiento
size[2,2]
size[2,2]
position[0,0.5]
anchor[0,0.5]
Juego de adivinanzas
guessing = {}
function guessing.get_formspec(name)
local formspec = {
"formspec_version[4]",
"size[6,3.476]",
"field[0.375,1.25;5.25,0.8;number;Number;]",
"button[1.5,2.3;3,0.8;guess;Guess]"
end
minetest.show_formspec(name, "guessing:game",
guessing.get_formspec(name))
end
minetest.register_chatcommand("game", {
func = function(name)
guessing.show_to(name)
end,
})
Acolchado y espaciado
El juego de adivinanzas formpec.
formname, fields)
return
end
if fields.guess then
fields.number)
end
end)
La función dada minetest.register_on_player_receive_fields se llama
cada vez que un usuario envía un formulario. La mayoría de las
devoluciones de llamada necesitarán verificar el nombre de formulario
dado a la función y salir si no es el formulario correcto; sin embargo, es
posible que algunas devoluciones de llamada deban funcionar en varios
formularios o en todos los formularios.
El fields parámetro de la función es una tabla de los valores enviados por
el usuario, indexados por cadenas. Los elementos con nombre aparecerán
en el campo con su propio nombre, pero solo si son relevantes para el
evento que provocó el envío. Por ejemplo, un elemento de botón solo
aparecerá en los campos si se presionó ese botón en particular.
⚠
Contextos
local _contexts = {}
return context
end
minetest.register_on_leaveplayer(function(player)
_contexts[player:get_player_name()] = nil
end)
function guessing.show_to(name)
end
local text
else
if fields.guess then
context.guess = tonumber(fields.number)
guessing.show_to(name)
end
Fuentes de Formspec
Hay tres formas diferentes en que se puede entregar una especificación
de formulario al cliente:
tiles = {"mymod_rightclick.png"},
meta:set_string("formspec",
"formspec_version[4]" ..
"size[5,5]" ..
"field[1,2;2,1;x;x;]")
end,
if fields.quit then
return
end
print(fields.x)
end
})
Tu turno
Posicionamiento
o Posición y compensación
o Alineación
o Marcador
Elementos de texto
o Parámetros
o Nuestro Ejemplo
Elementos de imagen
o Parámetros
o Escala
Cambiar un elemento
Almacenamiento de ID
Otros elementos
Posicionamiento
Posición y compensación
Alineación
La alineación es donde el resultado de la posición y el desplazamiento
está en el elemento; por ejemplo, {x = -1.0, y = 0.0} hará que el
resultado de la posición y el desplazamiento apunte a la izquierda de los
límites del elemento. Esto es particularmente útil cuando desea alinear un
elemento de texto a la izquierda, al centro o a la derecha.
Marcador
Elementos de texto
Puede crear un elemento HUD una vez que tenga una copia del objeto del
jugador:
hud_elem_type = "text",
offset = {x = 0, y = 0},
})
Parámetros
scale son los límites máximos de texto; texto fuera de estos límites se
recorta, por ejemplo: {x=100, y=100} .
number esel color del texto, y está en formato hexadecimal , por
ejemplo: 0xFF0000 .
Nuestro Ejemplo
player:hud_add({
hud_elem_type = "text",
position = {x = 1, y = 0.5},
text = "Stats",
alignment = 0,
number = 0xFFFFFF,
})
player:hud_add({
hud_elem_type = "text",
position = {x = 1, y = 0.5},
text = digs_text,
alignment = -1,
number = 0xFFFFFF,
})
player:hud_add({
hud_elem_type = "text",
position = {x = 1, y = 0.5},
text = places_text,
alignment = -1,
number = 0xFFFFFF,
})
player:hud_add({
hud_elem_type = "image",
position = {x = 1, y = 0.5},
text = "score_background.png",
scale = { x = 1, y = 1},
alignment = { x = 1, y = 0 },
})
Parámetros
Escala
player:hud_add({
hud_elem_type = "image",
position = {x = 1, y = 0.5},
text = "score_bar_empty.png",
scale = { x = 1, y = 1},
alignment = { x = 1, y = 0 },
})
player:hud_add({
hud_elem_type = "image",
position = {x = 1, y = 0.5},
text = "score_bar_full.png",
alignment = { x = 1, y = 0 },
})
Cambiar un elemento
Puede utilizar el ID devuelto por el hud_add método para actualizarlo o
eliminarlo más tarde.
hud_elem_type = "text",
})
player:hud_remove(idx)
hud_elem_type = "text",
})
Almacenamiento de ID
score = {}
local saved_huds = {}
function score.update_hud(player)
meta:get_int("score:places")
if ids then
player:hud_change(ids["bar_foreground"],
"scale", { x = percent, y = 1 })
else
ids = {}
saved_huds[player_name] = ids
end
end
minetest.register_on_joinplayer(score.update_hud)
minetest.register_on_leaveplayer(function(player)
saved_huds[player:get_player_name()] = nil
end)
Otros elementos
Lea lua_api.txt para obtener una lista completa de los elementos de HUD.
SFINV: Especificación de formulario de inventario
Introducción
Simple Fast Inventory (SFINV) es un mod que se encuentra en Minetest
Game que se usa para crear la especificación de formulario de inventario
del jugador . SFINV viene con una API que le permite agregar y
administrar las páginas que se muestran.
Si bien SFINV por defecto muestra las páginas como pestañas, las
páginas se llaman páginas porque es muy posible que un mod o juego
decida mostrarlas en algún otro formato. Por ejemplo, se pueden mostrar
varias páginas en una especificación de formulario.
title = "Hello!",
end
})
sfinv.register_page("myadmin:myadmin", {
title = "Tab",
local players = {}
context.myadmin_players = players
faster
local formspec = {
"textlist[0.1,0.1;7.8,3;playerlist;"
list
do
players[#players + 1] = player_name
formspec[#formspec + 1] = ","
end
formspec[#formspec + 1] =
minetest.formspec_escape(player_name)
is_first = false
end
formspec[#formspec + 1] = "]"
-- Add buttons
formspec[#formspec + 1] = "button[0.1,3.3;2,1;kick;Kick]"
formspec[#formspec + 1] = "button[2.1,3.3;2,1;ban;Kick +
Ban]"
end,
})
Recibir eventos
Puede recibir eventos de formpec agregando
una on_player_receive_fields función a una definición de sfinv.
on_player_receive_fields = function(self, player, context,
fields)
end,
fields)
selection changed
if fields.playerlist then
local event =
minetest.explode_textlist_event(fields.playerlist)
context.myadmin_selected_idx = event.index
end
local player_name =
context.myadmin_players[context.myadmin_selected_idx]
if player_name then
minetest.chat_send_player(player:get_player_name(),
minetest.kick_player(player_name)
end
local player_name =
context.myadmin_players[context.myadmin_selected_idx]
if player_name then
minetest.chat_send_player(player:get_player_name(),
minetest.ban_player(player_name)
minetest.kick_player(player_name, "Banned")
end
end
end,
local privs =
minetest.get_player_privs(player:get_player_name())
end,
return
end
return
end
return
end
sfinv.set_player_inventory_formspec(player, context)
end
minetest.register_on_priv_grant(on_grant_revoke)
minetest.register_on_priv_revoke(on_grant_revoke)
end,
end,
sfinv.override_page("sfinv:crafting", {
else
-- Backwards compatibility
return ret
end
})
Biomas y Decoraciones
Introducción
La capacidad de registrar biomas y decoraciones es vital cuando se busca
crear un entorno de juego interesante y variado. Este capítulo le enseña
cómo registrar biomas, cómo controlar la distribución de biomas y cómo
colocar decoraciones en biomas.
Otros tipos de nodos también pueden variar entre biomas, lo que brinda la
oportunidad de crear entornos muy diferentes dentro del mismo juego.
Calor y Humedad
Ajustar los valores de calor y humedad para los biomas es más fácil si
puede visualizar la relación entre los biomas que está utilizando. Esto es
más importante si está creando un conjunto completo de sus propios
biomas, pero también puede ser útil si está agregando un bioma a un
conjunto existente.
Registro de un bioma
El siguiente código registra un bioma simple llamado bioma de pastizales:
minetest.register_biome({
name = "grasslands",
node_top = "default:dirt_with_grass",
depth_top = 1,
node_filler = "default:dirt",
depth_filler = 3,
y_max = 1000,
y_min = -3,
heat_point = 50,
humidity_point = 50,
})
Este bioma tiene una capa de nodos Dirt with Grass en la superficie y tres
capas de nodos Dirt debajo de esta. No especifica un nodo de piedra, por
lo que el nodo definido en el registro de alias de
mapgen mapgen_stone estará presente debajo de la suciedad.
No es necesario definir todas las opciones para cada bioma que cree,
pero en algunos casos, si no se define una opción específica o un alias de
mapgen adecuado, se pueden producir errores en la generación de
mapas.
Por ejemplo:
minetest.register_decoration({
deco_type = "simple",
place_on = {"base:dirt_with_grass"},
sidelen = 16,
fill_ratio = 0.1,
biomes = {"grassy_plains"},
y_max = 200,
y_min = 1,
decoration = "plants:grass",
})
minetest.register_decoration({
deco_type = "schematic",
place_on = {"base:desert_sand"},
sidelen = 16,
fill_ratio = 0.0001,
biomes = {"desert"},
y_max = 200,
y_min = 1,
schematic = minetest.get_modpath("plants") ..
"/schematics/cactus.mts",
rotation = "random",
})
Alias de Mapgen
Los juegos existentes ya deberían incluir alias de mapgen adecuados, por
lo que solo debe considerar registrar sus propios alias de mapgen si está
creando su propio juego.
minetest.register_alias("mapgen_stone", "base:smoke_stone")
mapgen_stone
mapgen_water_source
mapgen_river_water_source
mapgen_lava_source
Manipuladores Lua Voxel
Introducción
Las funciones descritas en el capítulo Operaciones básicas de mapas son
convenientes y fáciles de usar, pero para áreas grandes son
ineficaces. Cada vez que llamas set_node o get_node , tu mod necesita
comunicarse con el motor. Esto da como resultado operaciones de copia
individuales constantes entre el motor y su mod, lo que es lento y
disminuirá rápidamente el rendimiento de su juego. Usar un manipulador
de vóxeles Lua (LVM) puede ser una mejor alternativa.
Conceptos
Leer en el LVM
Nodos de lectura
Nodos de escritura
Ejemplo
Tu turno
Conceptos
Un LVM te permite cargar grandes áreas del mapa en la memoria de tu
mod. Luego puede leer y escribir estos datos sin más interacción con el
motor y sin ejecutar ninguna devolución de llamada, lo que significa que
estas operaciones son muy rápidas. Una vez hecho esto, puede volver a
escribir el área en el motor y ejecutar los cálculos de iluminación.
Leer en el LVM
Solo puede cargar un área cúbica en un LVM, por lo que debe calcular las
posiciones mínima y máxima que necesita modificar. Luego puede crear y
leer en un LVM. Por ejemplo:
local vm = minetest.get_voxel_manip()
Por razones de rendimiento, un LVM casi nunca leerá el área exacta que
le indique. En cambio, probablemente leerá un área más grande. El área
más grande viene dada por emin y emax , que significan
posición mínima emergida y posición máxima emergida . Un LVM cargará
el área que contiene por usted, ya sea que implique cargar desde la
memoria, desde el disco o llamar al generador de mapas.
⚠
LVM y Mapgen
No lo use minetest.get_voxel_manip() con mapgen, ya que puede causar
fallas. Úselo en su minetest.get_mapgen_object("voxelmanip") lugar.
Nodos de lectura
Para leer los tipos de nodos en posiciones particulares, necesitará
usar get_data() . Esto devuelve una matriz plana donde cada entrada
representa el tipo de un nodo en particular.
local data = vm:get_data()
MinEdge = emin,
MaxEdge = emax
-- Read node
print(data[idx])
print("is stone!")
end
local vi = a:index(x, y, z)
print("is stone!")
end
end
end
end
Nodos de escritura
Primero, debe configurar el nuevo ID de contenido en la matriz de datos:
local vi = a:index(x, y, z)
data[vi] = c_air
end
end
end
end
vm:set_data(data)
vm:write_to_map(true)
Ejemplo
local function grass_to_dirt(pos1, pos2)
local c_grass =
minetest.get_content_id("default:dirt_with_grass")
local vm = minetest.get_voxel_manip()
local a = VoxelArea:new{
MinEdge = emin,
MaxEdge = emax
-- Modify data
local vi = a:index(x, y, z)
data[vi] = c_dirt
end
end
end
end
-- Write data
vm:set_data(data)
vm:write_to_map(true)
end
Tu turno
Crear replace_in_area(from, to, pos1, pos2) , que reemplaza
todas las instancias de from con to en el área dada,
donde from y to son nombres de nodo.
Realice una función que gire todos los nodos del pecho en 90 °.
Cree una función que utilice un LVM para hacer que el adoquín
cubierto de musgo se extienda a los nodos de piedra y adoquines
cercanos. ¿Su implementación hace que el adoquín cubierto de
musgo se extienda más de una distancia de un nodo cada vez? Si
es así, ¿cómo podría detener esto?
Crear juegos
Introducción
El poder de Minetest es la capacidad de desarrollar juegos fácilmente sin
la necesidad de crear sus propios gráficos voxel, algoritmos voxel o
código de red sofisticado.
¿Qué es un juego?
Directorio del juego
Compatibilidad entre juegos
o Compatibilidad API
o Grupos y alias
Tu turno
¿Qué es un juego?
Los juegos son una colección de modificaciones que funcionan juntas
para crear un juego cohesivo. Un buen juego tiene un tema subyacente y
una dirección consistentes, por ejemplo, podría ser un minero artesano
clásico con elementos de supervivencia difíciles, o podría ser un juego de
simulación espacial con una estética de automatización steampunk.
├── game.conf
├── menu
│ ├── header.png
│ ├── background.png
│ └── icon.png
├── minetest.conf
├── mods
├── README.txt
└── settingtypes.txt
Compatibilidad API
Es una buena idea tratar de mantener tanta compatibilidad de API con
Minetest Game como sea conveniente, ya que facilitará la migración de
mods a otro juego.
Las pequeñas roturas no son tan malas, como no tener una función de
utilidad aleatoria que solo se usara internamente, pero las roturas más
grandes relacionadas con las funciones principales son muy malas.
Grupos y alias
Tu turno
Crea un juego simple en el que el jugador gane puntos excavando
bloques especiales.
Errores comunes
Introducción
Este capítulo detalla los errores comunes y cómo evitarlos.
if obj:get_pos() then
-- is valid!
end
No confíe en las presentaciones de Formspec
Los clientes malintencionados pueden enviar formpecs cuando lo deseen,
con el contenido que deseen.
Por ejemplo, el siguiente código tiene una vulnerabilidad que permite a los
jugadores otorgarse privilegios de moderador:
then
return false
end
minetest.show_formspec(name, "modman:modman", [[
size[3,2]
field[0,0;3,1;target;Name;]
button_exit[0,1;3,1;sub;Promote]
]])
return true
})
minetest.register_on_player_receive_fields(function(player,
formname, fields)
privs.kick = true
privs.ban = true
minetest.set_player_privs(fields.target, privs)
return true
end)
Agregue una verificación de privilegios para resolver esto:
minetest.register_on_player_receive_fields(function(player,
formname, fields)
then
return false
end
-- code
end)
inv:set_stack("main", 1, stack)
replace_with_item,
itemstack:get_meta():set_string("description", "Partially
eaten")
end)
minetest.register_on_item_eat(function(hp_change,
replace_with_item,
itemstack:get_meta():set_string("description", "Partially
eaten")
user:get_inventory():set_stack("main",
user:get_wield_index(),
itemstack)
end)
Introducción
En este capítulo, aprenderá a usar una herramienta llamada LuaCheck
para escanear automáticamente su mod en busca de errores. Esta
herramienta se puede utilizar en combinación con su editor para
proporcionar alertas sobre cualquier error.
Instalación de LuaCheck
o Ventanas
o Linux
Ejecutando LuaCheck
Configuración de LuaCheck
o Solución de problemas
Usando con editor
Comprobación de confirmaciones con Travis
Instalación de LuaCheck
Ventanas
Linux
luacheck -v
Ejecutando LuaCheck
La primera vez que ejecute LuaCheck, probablemente detectará muchos
errores falsos. Esto se debe a que aún debe configurarse.
Configuración de LuaCheck
Cree un archivo llamado .luacheckrc en la raíz de su proyecto. Esta podría
ser la raíz de tu juego, modpack o mod.
unused_args = false
allow_defined_top = true
globals = {
"minetest",
read_globals = {
"vector", "ItemStack",
-- MTG
Solución de problemas
Átomo - linter-luacheck .
VSCode - Ctrl + P, luego pegue: ext install dwenegar.vscode-
luacheck
Sublime : instale usando el control de
paquetes: SublimeLinter , SublimeLinter-luacheck .
language: generic
sudo: false
addons:
apt:
packages:
- luarocks
before_install:
script:
- $HOME/.luarocks/bin/luacheck .
notifications:
email: false
Seguridad
Introducción
La seguridad es muy importante para asegurarse de que su mod no haga
que el propietario del servidor pierda datos o control.
Conceptos básicos
Formularios
o Nunca confíes en las presentaciones
o El tiempo de verificación no es el tiempo de uso
Ambientes (inseguros)
Conceptos básicos
El concepto más importante en seguridad es nunca confiar en el
usuario . Todo lo que envíe el usuario debe tratarse como malicioso. Esto
significa que siempre debe verificar que la información que ingrese sea
válida, que el usuario tenga los permisos correctos y que, de lo contrario,
se le permita realizar esa acción (es decir, dentro del rango o propietario).
Formularios
minetest.register_on_player_receive_fields(function(player,
formname, fields)
"goto_([%d-]+)_([%d-]+)_([%d-]+)")
z=tonumber(z) })
return true
end
end
end
Ambientes (inseguros)
Minetest permite que los mods soliciten un entorno sin zona de pruebas,
lo que les da acceso a la API completa de Lua.
local ie = minetest.request_insecure_environment()
ie.os.execute(("path/to/prog %d"):format(3))
end
El mod podría pasar algo mucho más malicioso que abrir un sitio web,
como darle a un usuario remoto el control de la máquina.
Introducción
Una vez que su mod alcance un tamaño respetable, le resultará cada vez
más difícil mantener el código limpio y libre de errores. Este es un
problema especialmente grande cuando se usa un lenguaje escrito
dinámicamente como Lua, dado que el compilador le brinda muy poca
ayuda en tiempo de compilación cuando se trata de cosas como
asegurarse de que los tipos se usen correctamente.
–CAR Hoare
Observador
Una forma sencilla de separar diferentes áreas de código es utilizar el
patrón Observer.
mymobs.registered_on_death = {}
function mymobs.register_on_death(func)
table.insert(mymobs.registered_on_death, func)
end
mymobs.registered_on_death[i](entity, reason)
end
mymobs.register_on_death(function(mob, reason)
reason.object:is_player() then
awards.notify_mob_kill(reason.object, mob.name)
end
end)
Modelo-Vista-Controlador
En el próximo capítulo, discutiremos cómo probar automáticamente su
código y uno de los problemas que tendremos es cómo separar su lógica
(cálculos, qué se debe hacer) de las llamadas a la API ( minetest.* , otras
modificaciones) tanto como sea posible.
Una forma de hacer esto es pensar en:
-- Data
land.lands[area_name] = {
name = area_name,
owner = name,
-- more stuff
end
function land.get_by_name(area_name)
return land.lands[area_name]
end
Tus acciones también deben ser puras, pero llamar a otras funciones es
más aceptable que en lo anterior.
-- Controller
-- process stuff
land.create(name, area_name)
end
function land.handle_creation_request(name)
land.show_create_formspec(name)
end
-- View
function land.show_create_formspec(name)
return [[
size[4,3]
label[1,0;This is an example]
field[0,1;3,1;area_name;]
button_exit[0,2;1,1;exit;Exit]
]]
end
minetest.register_chatcommand("/land", {
func = function(name)
land.handle_creation_request(name)
end,
})
minetest.register_on_player_receive_fields(function(player,
formname, fields)
land.handle_create_submit(player:get_player_name(),
fields.area_name)
end)
Vista de API
Separar el mod de esta manera significa que puede probar muy fácilmente
la parte de la API, ya que no usa ninguna API de Minetest, como se
muestra en el siguiente capítulo y se ve en el mod de creación.
Conclusión
Un buen diseño de código es subjetivo y depende en gran medida del
proyecto que esté realizando. Como regla general, trate de mantener la
cohesión alta y el acoplamiento bajo. Expresado de manera diferente,
mantenga el código relacionado junto y el código no relacionado
separado, y mantenga las dependencias simples.
Recomiendo encarecidamente leer el libro Patrones de programación de
juegos . Está disponible gratuitamente para leer en línea y entra en
muchos más detalles sobre los patrones de programación comunes
relevantes para los juegos.
Introducción
Las pruebas unitarias son una herramienta esencial para demostrar y
asegurarse de que su código es correcto. Este capítulo le mostrará cómo
escribir pruebas para mods y juegos Minetest usando Busted. Escribir
pruebas unitarias para funciones en las que llama funciones Minetest es
bastante difícil, pero afortunadamente en el capítulo anterior , discutimos
cómo estructurar su código para evitar esto.
Instalación de Busted
Tu primera prueba
o init.lua
o api.lua
o tests / api_spec.lua
Burlarse: usar funciones externas
Comprobación de confirmaciones con Travis
Conclusión
Instalación de Busted
Primero, necesitará instalar LuaRocks.
busted --version
Tu primera prueba
Busted es el marco de prueba unitario líder de Lua. Busted busca archivos
Lua con nombres terminados en _spec y luego los ejecuta en un entorno
Lua independiente.
mymod/
├── init.lua
├── api.lua
└── tests
└── api_spec.lua
init.lua
mymod = {}
dofile(minetest.get_modpath("mymod") .. "/api.lua")
api.lua
function mymod.add(x, y)
return x + y
end
tests / api_spec.lua
-- Look for required things in
require("api")
-- Tests
describe("add", function()
it("adds", function()
end)
end)
end)
Otra cosa a tener en cuenta es que cualquier archivo que esté probando
debe evitar llamadas a funciones que no estén dentro de él. Suele escribir
pruebas para un solo archivo a la vez.
_G.minetest = {}
local chat_send_all_calls = {}
message })
end
-- Tests
describe("list_areas", function()
mymod.list_areas_to_chat("singleplayer", "singleplayer")
assert.equals(2, #chat_send_all_calls)
end)
mymod.list_areas_to_chat("singleplayer", "singleplayer")
end
end)
mymod.list_areas_to_chat("singleplayer", "singleplayer")
local expected = {
(2,43,63)" },
(43,45,63)" },
assert.same(expected, chat_send_all_calls)
end)
end)
language: generic
sudo: false
addons:
apt:
packages:
- luarocks
before_install:
busted
script:
- $HOME/.luarocks/bin/luacheck .
- $HOME/.luarocks/bin/busted .
notifications:
email: false
Conclusión
Las pruebas unitarias aumentarán en gran medida la calidad y
confiabilidad de su proyecto si se usan bien, pero requieren que estructura
su código de una manera diferente a la habitual.
Introducción
Las pruebas unitarias son una herramienta esencial para demostrar y
asegurarse de que su código es correcto. Este capítulo le mostrará cómo
escribir pruebas para mods y juegos Minetest usando Busted. Escribir
pruebas unitarias para funciones en las que llama funciones Minetest es
bastante difícil, pero afortunadamente en el capítulo anterior , discutimos
cómo estructurar su código para evitar esto.
Instalación de Busted
Tu primera prueba
o init.lua
o api.lua
o tests / api_spec.lua
Burlarse: usar funciones externas
Comprobación de confirmaciones con Travis
Conclusión
Instalación de Busted
Primero, necesitará instalar LuaRocks.
busted --version
Tu primera prueba
Busted es el marco de prueba unitario líder de Lua. Busted busca archivos
Lua con nombres terminados en _spec y luego los ejecuta en un entorno
Lua independiente.
mymod/
├── init.lua
├── api.lua
└── tests
└── api_spec.lua
init.lua
mymod = {}
dofile(minetest.get_modpath("mymod") .. "/api.lua")
api.lua
function mymod.add(x, y)
return x + y
end
tests / api_spec.lua
-- Look for required things in
_G.mymod = {} --_
require("api")
-- Tests
describe("add", function()
it("adds", function()
end)
end)
end)
Ahora puede ejecutar las pruebas abriendo una terminal en el directorio
del mod y ejecutando busted .
Otra cosa a tener en cuenta es que cualquier archivo que esté probando
debe evitar llamadas a funciones que no estén dentro de él. Suele escribir
pruebas para un solo archivo a la vez.
_G.minetest = {}
local chat_send_all_calls = {}
message })
end
-- Tests
describe("list_areas", function()
mymod.list_areas_to_chat("singleplayer", "singleplayer")
assert.equals(2, #chat_send_all_calls)
end)
mymod.list_areas_to_chat("singleplayer", "singleplayer")
assert.equals("singleplayer", call.name)
end
end)
mymod.list_areas_to_chat("singleplayer", "singleplayer")
local expected = {
(2,43,63)" },
{ name = "singleplayer", message = "Airport
(43,45,63)" },
assert.same(expected, chat_send_all_calls)
end)
end)
language: generic
sudo: false
addons:
apt:
packages:
- luarocks
before_install:
busted
script:
- $HOME/.luarocks/bin/luacheck .
- $HOME/.luarocks/bin/busted .
notifications:
email: false
Conclusión
Las pruebas unitarias aumentarán en gran medida la calidad y
confiabilidad de su proyecto si se usan bien, pero requieren que estructura
su código de una manera diferente a la habitual.
Lanzamiento de un mod
Introducción
Lanzar o publicar un mod permite que otras personas lo utilicen. Una vez
que se ha lanzado un mod, se puede usar en juegos para un solo jugador
o en servidores, incluidos los servidores públicos.
LGPL y CC-BY-SA
CC0
Esta licencia se puede utilizar tanto para código como para arte, y permite
que cualquiera haga lo que quiera con su trabajo. Esto significa que
pueden modificar, redistribuir, vender u omitir la atribución.
MIT
Esta es una licencia común para código. La única restricción que impone
a los usuarios de su código es que deben incluir el mismo aviso de
derechos de autor y licencia en cualquier copia del código o de partes
sustanciales del código.
embalaje
Hay algunos archivos que se recomienda incluir en su mod o juego antes
de lanzarlo.
README.txt
mod.conf / game.conf
Buen ejemplo:
Evite esto:
description = The food mod for Minetest. (<-- BAD! It's vague)
screenshot.png
Subiendo
Para que un usuario potencial pueda descargar su mod, debe cargarlo en
un lugar de acceso público. Hay varias formas de hacer esto, pero debe
utilizar el enfoque que funcione mejor para usted, siempre que cumpla con
estos requisitos, y cualquier otro que puedan agregar los moderadores del
foro:
Usar git puede ser difícil al principio. Si necesita ayuda con esto, consulte:
Lanzamiento en ContentDB
ContentDB es el lugar oficial para encontrar y distribuir contenido como
mods, juegos y paquetes de texturas. Los usuarios pueden encontrar
contenido usando el sitio web, o descargarlo e instalarlo usando la
integración incorporada en el menú principal de Minetest.
El tema del foro debe tener un contenido similar al README, pero debe
ser más promocional y también debe incluir un enlace para descargar el
mod. Es una buena idea incluir capturas de pantalla de su mod en acción,
si es posible.
Por ejemplo: