0% found this document useful (0 votes)
28 views154 pages

Create Mods in Minetest Guide

This document explains the basic structure of a mod folder for Minetest. Each mod has its own folder containing files like init.lua and mod.conf. The init.lua file runs when the game loads and can register nodes, recipes, and other content, while mod.conf provides metadata about the mod such as its name and dependencies.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
28 views154 pages

Create Mods in Minetest Guide

This document explains the basic structure of a mod folder for Minetest. Each mod has its own folder containing files like init.lua and mod.conf. The init.lua file runs when the game loads and can register nodes, recipes, and other content, while mod.conf provides metadata about the mod such as its name and dependencies.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 154

Starting

Introduction
Understanding the basic structure of a mod's folder is an essential skill
when creating mods.

 What are games and mods?


 Where are the modifications stored?
 Mod Directory
 mod.conf
o Dependencies
 Mod Packs
 Example
o Mod Folder
o init.lua
o mod.conf

What are games and mods?


The power of Minetest is the ability to easily develop games without the
need to create your own voxel graphics, voxel algorithms, or sophisticated
networking code.

In Minetest, a game is a collection of modules that work together to


provide the content and behavior of a game. A module, commonly known
as a mod, is a collection of scripts and resources. It is possible to make a
game using only one mod, but this is rarely done because it reduces the
ease with which parts of the game can be adjusted and replaced
independently of each other.

It is also possible to distribute mods outside of a game, in which case they


are also mods in the more traditional sense: mods. These mods adjust or
expand the features of a game.

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.

Where are the modifications stored?


Each mod has its own directory where its Lua code, textures, models and
sounds are placed. Minetest looks for mods in several different locations.
These locations are commonly called mod cargo routes .

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

├── init.lua (required) - Runs when the game loads.

├── mod.conf (recommended) - Contains description and

dependencies.

├── textures (optional)

│ └── ... any textures or images

├── sounds (optional)

│ └── ... any sounds

└── ... any other files or directories

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

description = Adds foo, bar, and bo.

depends = modone, modtwo

Dependencies

A dependency occurs when a mod requires another mod to be loaded


before itself. A mod may require code, items, or other resources from
another mod to be available for use.

There are two types of dependencies: fixed and optional dependencies.


Both require the mod to be loaded first. If the mod being depended on is
not available, a hard dependency will cause the mod to not load, while an
optional dependency could lead to fewer features being enabled.
An optional dependency is useful if you want to optionally support another
mod; You can enable additional content if the user wants to use both mods
at the same time.

Dependencies are specified in a comma-separated list in mod.conf.

depends = modone, modtwo

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

├── modpack.conf (required) - signals that this is a mod pack

├── mod1

│ └── ... mod files

└── mymod (optional)

└── ... mod files

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

│ └── mymod_node.png files


├── init.lua

└── mod.conf

init.lua
print ( "This file will be run at load time!" )

minetest.register_node( "mymod:node" , {

description = "This is a node" ,

tiles = { "mymod_node.png" },

groups = {cracky = 1 }

})

minetest.register_craft({

type = "shapeless" ,

output = "mymod:node 3" ,

recipe = { "default:dirt" , "default:stone" },

})

mod.conf
name = mymod

descriptions = Adds a node

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:

function ctf . post (team, msg)

if not ctf.team(team) then

return false

end

if not ctf.team(team).log then

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.

Commonly used editors that are suitable for Lua include:

 VSCode – Open source (like Code-OSS or VSCodium), popular and


has plugins for Minetest modification .
 Notepad++ – Windows only
 Atom

Other suitable editors are also available.

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.

There are three main types of flow:

 Sequence: executes one statement after another, without jumps.


 Selection: Skip sequences based on conditions.
 Iteration: repeats the same statements until a condition is met.

So what do declarations look like in Lua?

local a = 2 -- Set 'a' to 2

local b = 2 -- Set 'b' to 2

local result = a + b -- Set 'result' to a + b, which is 4


a = a + 10

print ( "Sum is" .. result)

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.

Guy Description Example

Not initialized. The variable is empty,


Null local A , D = nil
it has no value.

Number An integer or decimal number. local A = 4

local D = "one two


Rope A fragment of text.
three"

local is_true = false ,


Boolean True or false.
local E = (1 == 1)

Table Liza. Explained below.

Being able to run. It may require local result = func(1,


Function
input and may return a value. 2, 3)
Arithmetic operators

Operators in Lua include:

Symbol Purpose Example

A+B Addition 2+2=4

A-B Subtraction 2 - 10 = -8

A*B Multiplication 2 * 2 = 4

A/B Division 100/50 = 2

A^B Powers 2^2=22=4

TO .. b Join chains "Foo".. "bar" = "foobar"

Please note that this is not an exhaustive list; It does not contain all
possible operators.

Selection

The most basic selection method is the if statement. For example:

local random_number = math.random ( 1 , 100 ) -- Between 1 and

100.

if random_number > 50 then

print ( "Woohoo!" )

else

print ( "No!" )

end

This generates a random number between 1 and 100. Then print


"Woohoo!" if that number is greater than 50, and otherwise, “No!” is
printed.
Logical operators

Logical operators in Lua include:

Symbol Purpose Example

A == B Equal 1 == 1 (true), 1 == 2 (false)

A~=B It is not the same 1 ~ = 1 (false), 1 ~ = 2 (true)

5 > 2 (true), 1 > 2 (false), 1 > 1


A>B Larger than
(false)

1 <3 (true), 3 <1 (false), 1 <1


A<B Less than
(false)

5>=5 (true), 5>=3 (true), 5>=6


A>=B Greater than or equal
(false)

A <= B less than or equal 3 <= 6 (true), 3 <= 3 (true)

(2 > 1) and (1 == 1) (true), (2 > 3)


A and B AND (both must be correct)
and (1 == 1) (false)

Either or. One or both must be (2 > 1) or (1 == 2) (true), (2 > 4)


A or B
true. or (1 == 3) (false)

not (1 == 2) (true), not (1 == 1)


Not a is not true
(false)

Note that this does not contain all possible operators.

It is also possible to combine operators. For example:

if not A and B then

print ( "Yay!" )
end

This prints "Yay!" if A is false and B is true.

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

print ( "Is equal!" )

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.

Teaching you the logical process of programming is beyond the scope of


this book; However, the following websites are quite helpful in developing
this:

 Codecademy is one of the best resources to learn how to write code.


Provides an interactive tutorial experience.
 Scratch is a good resource to start from the absolute basics and
learn the problem-solving techniques necessary for programming.
Scratch is designed to teach children how to program and is not a
serious programming language.

Local and global reach


Whether a variable is local or global determines where it can be written to
or read from. A local variable is only accessible from where it is defined.
Here are some examples:

-- Accessible from within this script file

local one = 1
function myfunc ()

-- Accessible from within this function

local two = one + one

if two == one then

-- Accessible from within this if statement

local three = one + two

end

end

In contrast, global variables can be accessed from anywhere in the script


file and from any other mod.

function one ()

foo = "bar"

end

function two ()

print (dump(foo)) -- Output: "bar"

end

one()

two()

Wherever possible, local variables should be used. Mods should only


create one global at most, with the same name as the mod. Creating other
globals is sloppy coding, and Minetest will warn about this:

Assignment to undeclared global 'foo' inside function at

init.lua:2

To fix this, use "local":


function one ()

local foo = "bar"

end

function two ()

print (dump(foo)) -- Output: nil

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.

local function foo (bar)

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 = {}

function mymod . foo (bar)

return "foo" .. bar

end

-- In another mod, or script:

mymod.foo( "foobar" )
Including other Lua scripts
The recommended way to include other Lua scripts in a mod is to use
dofile .

dofile (minetest.get_modpath( "modname" ) .. "/script.lua" )

A script can return a value, which is useful for sharing private premises:

-- script.lua

return "Hello world!"

-- init.lua

local ret = dofile (minetest.get_modpath( "modname" ) ..

"/script.lua" )

print (ret) -- Hello world!

Later chapters will discuss the best way to split code for a mod.

Nodes, objects and crafts

Introduction
Registering new nodes and crafting items, and creating crafting recipes,
are basic requirements for many mods.

 What are nodes and elements?


 Article Registration
o Item names
o Element aliases
o Textures
 Registering a basic node
 Actions and callbacks
o on_use
 Elaboration
o Conformed
o Report
o Cooker and fuel
 Groups
 Tools, capacities and types of excavation

What are nodes and elements?


Nodes, crafting items, and tools are all items. An item is something that
can be found in an inventory, even if it is not possible through normal
gameplay.

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 crafting item cannot be placed and is only found in inventories or as a


dropped item in the world.

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" , {

description = "My Special Item" ,

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:

 Rename the deleted items to something else. There may be


unknown nodes in the world and in inventories if an item is removed
from a mod without any corrective code.
 Adding a shortcut. /giveme dirt is easier than /giveme
default:dirt .

Registering an alias is quite simple. A good way to remember the order of


arguments is from → to where starting is the alias and that is the target.
minetest.register_alias( "dirt" , "default:dirt" )

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:

itemname = minetest.registered_aliases[itemname] or itemname

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.

Textures in Minetest are usually 16 by 16 pixels. They can have any


resolution, but it is recommended that they be of the order of 2, for
example, 16, 32, 64 or 128. This is because other resolutions may not be
supported correctly on older devices, reducing performance.
Registering a basic node
minetest.register_node( "mymod:diamond" , {

description = "Alien Diamond" ,

tiles = { "mymod_diamond.png" },

is_ground_content = true ,

groups = {cracky = 3 , stone = 1 }

})

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).

(+Y, -Y, +X, -X, +Z, -Z)

Remember that +Y is facing up in Minetest, as is the convention with 3D


computer graphics.

minetest.register_node( "mymod:diamond" , {

description = "Alien 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

})

The is_ground_content attribute allows you to generate caves on the


stone. This is essential for any nodes that may be placed underground
during map generation. Caves are cut off from the world after all other
nodes in an area have been generated.

Actions and callbacks


Minetest largely uses a callback-based mod design. Callbacks can be
placed in the element definition table to allow response to several different
user events.

on_use

By default, the usage callback is triggered when a player left-clicks an


item. Having a use callback prevents the element from being used to dig
nodes. A common use of the usage callback is for food:

minetest.register_craftitem( "mymod:mudpie" , {

description = "Alien Mud Pie" ,

inventory_image = "myfood_mudpie.png" ,

on_use = minetest.item_eat( 20 ),

})

The number provided to the minetest.item_eat function is the number of hit


points that are healed when this food is consumed. Each heart icon the
player has is worth two life points. Typically, a player can have up to 10
hearts, which is equivalent to 20 life points. Hit points do not have to be
whole numbers (integers); They can be decimal.

minetest.item_eat() is a function that returns a function, setting it as the


on_use callback. This means that the code above is more or less similar to
this:

minetest.register_craftitem( "mymod:mudpie" , {
description = "Alien Mud Pie" ,

inventory_image = "myfood_mudpie.png" ,

on_use = function ( ... )

return minetest.do_item_eat( 20 , nil , ... )

end ,

})

By understanding how item_eat works by simply returning a function, it is


possible to modify it to perform more complex behavior, such as playing a
custom sound.

Elaboration
There are several types of crafting recipes available, indicated by the type
property.

 Shape: The ingredients must be in the correct position.


 formless: it doesn't matter where the ingredients are, just that there
is the right amount.
 cooking - Recipes to use in the oven.
 fuel: defines the elements that can be burned in ovens.
 tool_repair - Defines the items that can be repaired with the tool.

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" ,

output = "mymod:diamond_chair 99" ,

recipe = {
{ "mymod:diamond_fragments" , "" , "" },

{ "mymod:diamond_fragments" , "mymod:diamond_fragments" , "" },

{ "mymod:diamond_fragments" , "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({

output = "mymod:diamond_chair 99" ,

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" ,

output = "mymod:diamond 3" ,

recipe = {

"mymod:diamond_fragments" ,
"mymod:diamond_fragments" ,

"mymod:diamond_fragments" ,

},

})

Cooker and fuel

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!

This type is an accompaniment to the cooking type, as it defines what can


be burned in ovens and other cooking tools of the mods.

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" ,

output = "mymod:diamond_thing 3" ,

recipe = { "group:wood" , "mymod:diamond" }

})

Tools, capacities and types of excavation


Dig types are groups used to define how strong a node is when excavated
with different tools. A dig type group with a higher associated value means
that the node is easier and faster to cut. It is possible to combine multiple
types of excavation to allow the most efficient use of multiple types of
tools. A node without excavation types cannot be excavated with any tools.

Cluster Best tool Description

crumbly shovel Sand earth

Hard (but brittle) things like


crazy beak
stone
Cluster Best tool Description

Can be cut with fine tools;


fast some for example, leaves, small
plants, wire, metal sheets

It can be cut with sharp force;


chopped axe
e.g. trees, wooden planks

Living beings such as animals


and the player.
fleshy sword
This could involve some blood
effects when hitting.

blow ? Especially prone to explosions.

Torches and such, very quick to


oddly_breakable_by_hand some
dig.

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" , {

description = "My 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.

Node drawing types

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

Cubic nodes: normal and all faces

Normal drawing type

The normal drawing type is generally used to represent a cubic node. If a


normal node's side is against a solid side, that side will not be rendered,
resulting in a large performance gain.

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" , {

description = "Alien 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" , {

description = "Obsidian Glass" ,

drawtype = "glasslike" ,

tiles = { "default_obsidian_glass.png" },

paramtype = "light" ,

is_ground_content = false ,

sunlight_propagates = true ,

sounds = default.node_sound_glass_defaults(),

groups = {cracky = 3 ,oddly_breakable_by_hand = 3 },

})

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" ,

tiles = { "default_glass.png" , "default_glass_detail.png" },

inventory_image = minetest.inventorycube( "default_glass.png"

),

paramtype = "light" ,

sunlight_propagates = true , -- Sunlight can shine through block

groups = {cracky = 3 , oddly_breakable_by_hand = 3 },

sounds = default.node_sound_glass_defaults()

})

Aeronautical nodes
These nodes are not rendered and therefore have no textures.

minetest.register_node( "myair:air" , {

description = "MyAir (you hacker you!)" ,

drawtype = "airlike" ,

paramtype = "light" ,

sunlight_propagates = true ,

walkable = false , -- Would make the player collide with the air

node

pointable = false , -- You can't select the node

diggable = false , -- You can't dig the node

buildable_to = true , -- Nodes can be replace this node.


-- (you can place a node and remove the

air node

-- that used to be there)

air_equivalent = true ,

drop = "" ,

groups = {not_in_creative_inventory = 1 }

})

Lighting and sunlight propagation


The lighting of a node is stored in param1. To figure out how to shade the
side of a node, the light value of the neighboring node is used. Because of
this, solid nodes have no light values because they block light.

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

Liquid drainage type


Each liquid type requires two node definitions: one for the liquid source
and one for the flowing liquid.

-- Some properties have been removed as they are beyond

-- the scope of this chapter.

minetest.register_node( "default:water_source" , {

drawtype = "liquid" ,

paramtype = "light" ,

inventory_image = minetest.inventorycube( "default_water.png"

),

-- ^ this is required to stop the inventory image from being

animated

tiles = {

name = "default_water_source_animated.png" ,

animation = {

type = "vertical_frames" ,

aspect_w = 16 ,

aspect_h = 16 ,

length = 2 . 0

},

special_tiles = {

-- New-style water source material (mostly unused)

name = "default_water_source_animated.png" ,
animation = { type = "vertical_frames" , aspect_w =

16 ,

aspect_h = 16 , length = 2 . 0 },

backface_culling = false ,

},

--

-- Behavior

--

walkable = false , -- The player falls through

pointable = false , -- The player can't highlight it

diggable = false , -- The player can't dig it

buildable_to = true , -- Nodes can be replace this node

alpha = 160 ,

--

--Liquid Properties

--

drowning = 1 ,

liquidtype = "source" ,

liquid_alternative_flowing = "default:water_flowing" ,

-- ^ when the liquid is flowing

liquid_alternative_source = "default:water_source" ,

-- ^ when the liquid is a source

liquid_viscosity = WATER_VISC,

-- ^ how fast
liquid_range = 8 ,

-- ^ how far

post_effect_color = {a = 64 , r = 100 , g = 100 , b = 200 },

-- ^ color of screen when the player is submerged

})

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

Nodebox drawing type

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 = {

{-0.5, -0.5, -0.5, 0.5, 0, 0.5},

{-0.5, 0, 0, 0.5, 0.5, 0.5},

},
}

})

The most important part is the node box table:

{-0.5, -0.5, -0.5, 0.5, 0, 0.5},

{-0.5, 0, 0, 0.5, 0.5, 0.5}

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.

Wall Mounted Node Boxes

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 = {

{-0.4375, 0.4375, -0.3125, 0.4375, 0.5, 0.3125}

},

--Floor

wall_bottom = {

{-0.4375, -0.5, -0.3125, 0.4375, -0.4375, 0.3125}

},
-- Wall

wall_side = {

{-0.5, -0.3125, -0.4375, -0.4375, 0.3125, 0.4375}

},

})

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.

You can register a mesh node as follows:

minetest.register_node( "mymod:meshy" , {

drawtype = "mesh" ,

-- Holds the texture for each "material"

tiles = {

"mymod_meshy.png"

},

-- Path to the mesh

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" },

-- Required: store the rotation in param2

paramtype2 = "wallmounted" ,

selection_box = {

type = "wallmounted" ,

},

})

Plant nodes

Plant-like drawing type

Plant-shaped nodes draw their tokens in an X pattern.


minetest.register_node( "default:papyrus" , {

drawtype = "plantlike" ,

--Only one texture used

tiles = { "default_papyrus.png" },

selection_box = {

type = "fixed" ,

fixed = { - 6 / 16 , - 0 . 5 , - 6/16 , 6/16 , 0 . 5 , 6

/ 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" },

})

More types of drawing


This is not a complete list, there are more types including:

 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

ItemStacks and inventories

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.

 What are stockpiles and inventories?


 ItemStacks
 Inventory Locations
 Lisa
o Size and width
o Content Check
 Modification of inventories and item stacks
o Add to a list
o Taking items
o Battery handling
 Wear
 Lua tables
What are stockpiles and inventories?
An ItemStack is the data behind a single cell in an inventory.

An inventory is a collection of inventory lists , each of which is a 2D grid of


ItemStacks. Inventory lists are simply called lists in the context of
inventories. The point of an inventory is to allow multiple grids when
players and nodes only have at most one inventory on them.

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" )

if not stack:is_known() then

print ( "Is an unknown item!" )

end

The count will always be 0 or greater. Throughout normal gameplay, the


count must not exceed the item's maximum stack size - stack_max .
However, buggy administrator commands and modifications can cause
stacks to exceed the maximum size.
print (stack:get_stack_max())

An ItemStack can be empty, in which case the count will be 0.

print (stack:get_count())

stack:set_count( 10 )

ItemStacks can be built in several ways using the ItemStack function.

ItemStack() -- name="", count=0


ItemStack( "default:pick_stone" ) -- count=1

ItemStack( "default:stone 30" )

ItemStack({ name = "default:wood" , 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.

Node inventories are related to the position of a specific node, such as a


chest. The node must be loaded because it is stored in node metadata .

local inv = minetest.get_inventory({ type = "node" , pos = {x = 1

, y = 2 , z = 3 } })

The above obtains an inventory reference , commonly known as InvRef .


Inventory references are used to manipulate an inventory. Reference
means that the data is not actually stored within that object, but rather the
object directly updates the data in place.

The location of an inventory reference can be found like this:

local location = inv:get_location()

Player inventories can be obtained in a similar manner or by using a player


reference. The player must be online to access their inventory.

local inv = minetest.get_inventory({ type = "player" , name =

"player1" })

-- or
local inv = player:get_inventory()

A separate inventory is one that is independent of players or nodes.


Separate inventories are also not saved on reboot.

local inv = minetest.get_inventory({

type = "detached" , name = "inventory_name" })

Unlike the other types of inventory, you must first create a separate
inventory before accessing it:

minetest.create_detached_inventory( "inventory_name" )

The create_detached_inventory function accepts 3 arguments, where only


the first one, the inventory name, is required. The second argument takes
a table of callbacks, which can be used to control how players interact with
the inventory:

--Input only detached inventory

minetest.create_detached_inventory( "inventory_name" , {

allow_move = function (inv, from_list, from_index, to_list,

to_index, count, player)

return count -- allow moving

end ,

allow_put = function (inv, listname, index, stack, player)

return stack:get_count() -- allow putting

end ,

allow_take = function (inv, listname, index, stack, player)

return 0 -- don't allow taking

end ,

on_put = function (inv, listname, index, stack, player)

minetest.chat_send_all(player:get_player_name() ..
"gave" .. stack:to_string() ..

"to the donation chest from" ..

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 .

Size and width

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.

if inv:set_size( "main" , 32 ) then

inv:set_width( "main" , 8 )

print ( "size: " .. inv:get_size( "main" ))

print ( "width: " .. inv:get_width( "main" ))

else

print ( "Error! Invalid itemname or size to set_size()" )

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

print ( "The list is empty!" )

end

contains_item can be used to see if a list contains a specific item:


if inv:contains_item( "main" , "default:stone" ) then

print ( "I've found some stone!" )

end

Modification of inventories and item stacks

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" )

local leftover = inv:add_item( "main" , stack)

if leftover:get_count() > 0 then

print ( "Inventory is full! " ..

leftover:get_count() .. "items weren't added" )

end

Taking items

To remove items from a list:

local taken = inv:remove_item( "main" , stack)

print ( "Took" .. taken:get_count())

Battery handling

You can modify individual stacks by getting them first:


local stack = inv:get_stack(listname, 0 )

Then modify them by setting properties or using methods that respect


stack_size :
local stack = ItemStack( "default:stone 50" )

local to_add = ItemStack( "default:stone 100" )

local leftover = stack:add_item(to_add)

local taken = stack:take_item( 19 )

print ( "Could not add" ..leftover:get_count() .. "of the items."

-- ^ will be 51

print ( "Have" .. stack:get_count() .. "items" )

-- ^ 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.

Finally, configure the element 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.

Wear can be manipulated with add_wear() , get_wear() and


set_wear(wear) .
local stack = ItemStack( "default:pick_mese" )

local max_uses = 10
-- This is done automatically when you use a tool that digs things

-- It increases the wear of an item by one use.

stack:add_wear( 65535 / (max_uses - 1 ))

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

local data = inv1:get_lists()

inv2:set_lists(data)

-- One list

local listdata = inv1:get_list( "main" )

inv2:set_list( "main" , listdata)

The list table returned by get_lists() will have this format:


{

list_one = {

ItemStack,

ItemStack,

ItemStack,

ItemStack,

-- inv:get_size("list_one") elements

},

list_two = {

ItemStack,
ItemStack,

ItemStack,

ItemStack,

-- inv:get_size("list_two") elements

get_list() will return a single list as just a list of ItemStacks.

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:

inv:set_list( "main" , {})

Basic map operations

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.

An active MapBlock is one that is loaded and has updates running.

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

You can read on the map once you have a position:

local node = minetest.get_node({ x = 1 , y = 3 , z = 4 })

print (dump(node)) --> { name=.., param1=.., param2=.. }

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

local node_pos = minetest.find_node_near(pos, 5 , {

"default:mese" })

if node_pos then

minetest.chat_send_all( "Node found at: " .. dump(node_pos))

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 pos1 = vector.subtract(pos, { x = 5 , y = 5 , z = 5 })

local pos2 = vector.add(pos, { x = 5 , y = 5 , z = 5 })

local pos_list =

minetest.find_nodes_in_area(pos1, pos2, { "default:mese" })

local grow_speed = 1 + # pos_list


The above code finds the number of nodes in a cuboid volume . This is different from
find_node_near , which uses the distance to the position (i.e. a sphere ). To fix this, we'll
need to manually check the range ourselves:
local pos1 = vector.subtract(pos, { x = 5 , y = 5 , z = 5 })

local pos2 = vector.add(pos, { x = 5 , y = 5 , z = 5 })

local pos_list =

minetest.find_nodes_in_area(pos1, pos2, { "default:mese" })

local grow_speed = 1

for i = 1 , # pos_list do

local delta = vector.subtract(pos_list[i], pos)


if delta.x * delta.x + delta.y * delta.y + delta.z * delta.z <=

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" })

local node = minetest.get_node({ x = 1 , y = 3 , z = 4 })

print (node.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)

minetest.set_node(pos, { name = "air" })


In fact, remove_node is just a helper function that calls set_node with "air" .

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 }

local pos1 = vector.subtract(pos, halfsize)

local pos2 = vector.add(pos, halfsize)

local context = {} -- persist data between callback calls

minetest.emerge_area(pos1, pos2, emerge_callback, context)


Minetest will call emerge_callback every time it loads a block, with some progress
information.
local function emerge_callback (pos, action,

num_calls_remaining, context)

-- On first call, record number of blocks

if not context.total_blocks then

context.total_blocks = num_calls_remaining + 1

context.loaded_blocks = 0

end

-- Increase number of blocks loaded

context.loaded_blocks = context.loaded_blocks + 1
-- Send progress message

if context.total_blocks == context.loaded_blocks then

minetest.chat_send_all("Finished loading blocks!")

else

local perc = 100 * context.loaded_blocks /

context.total_blocks

local msg = string.format("Loading blocks %d/%d (%.2f%%)",

context.loaded_blocks, context.total_blocks, perc)

minetest.chat_send_all(msg)

end

end

Esta no es la única forma de cargar bloques; El uso de un manipulador de vóxeles Lua


(LVM) también hará que los bloques abarcados se carguen sincrónicamente.

Eliminar bloques
Puede usar delete_blocks para eliminar un rango de bloques de mapa:

-- Delete a 20x20x20 area

local halfsize = { x = 10, y = 10, z = 10 }

local pos1 = vector.subtract(pos, halfsize)

local pos2 = vector.add (pos, halfsize)

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.

local timer = minetest.get_node_timer(pos)

timer:start(10.5) -- in seconds

Cuando un temporizador de nodo está activo, on_timer se


llamará al método en la tabla de definición del nodo. El método solo toma
un único parámetro, la posición del nodo:

minetest.register_node("autodoors:door_open", {

on_timer = function(pos)

minetest.set_node(pos, { name = "autodoors:door" })


return false

end

})

Devolver verdadero en on_timer hará que el temporizador vuelva a


funcionar durante el mismo intervalo. También es posible
usar get_node_timer(pos) dentro de on_timer , solo asegúrese de devolver
falso para evitar conflictos.

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.

Modificadores de bloque activos


La hierba alienígena, a los efectos de este capítulo, es un tipo de hierba
que tiene la posibilidad de aparecer cerca del agua.

minetest.register_node("aliens:grass", {

description = "Alien Grass",

light_source = 3, -- The node radiates light. Min 0, max 14

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"},

interval = 10.0, -- Run every 10 seconds

chance = 50, -- Select every 1 in 50 nodes

action = function(pos, node, active_object_count,


active_object_count_wider)

local pos = {x = pos.x, y = pos.y + 1, z = pos.z}

minetest.set_node(pos, {name = "aliens:grass"})

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.

Especificar un vecino es opcional. Si especifica varios vecinos, solo uno


de ellos debe estar presente para cumplir con los requisitos.

Especificar el azar también es opcional. Si no especifica la posibilidad, el


ABM siempre se ejecutará cuando se cumplan las demás condiciones.

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

¿Qué son los metadatos?

En Minetest, los metadatos son un almacén de valores clave que se utiliza


para adjuntar datos personalizados a algo. Puede utilizar metadatos para
almacenar información en un nodo, reproductor o pila de elementos.

Cada tipo de metadatos utiliza exactamente la misma API. Los metadatos


almacenan valores como cadenas, pero existen varios métodos para
convertir y almacenar otros tipos primitivos.

Algunas claves de los metadatos pueden tener un significado


especial. Por ejemplo, infotext en el nodo, los metadatos se utilizan para
almacenar la información sobre herramientas que se muestra al pasar el
cursor sobre el nodo con la cruz. Para evitar conflictos con otros mods, se
debe utilizar el espacio de nombres convención estándar para las
teclas: modname:keyname . La excepción es para datos convencionales
como el nombre del propietario que se almacena como owner .

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.

Obtener un objeto de metadatos

Si conoce la posición de un nodo, puede recuperar sus metadatos:


local meta = minetest.get_meta({ x = 1, y = 2, z = 3 })

Los metadatos de Player y ItemStack se obtienen mediante get_meta() :


local pmeta = player:get_meta()

local imeta = stack:get_meta()

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_string("foo")) --> "bar"

Todos los captadores escritos devolverán un valor predeterminado neutral


si la clave no existe, como "" o 0 . Puede usar get() para devolver una
cadena o nil.

Como los metadatos son una referencia, los cambios se actualizarán


automáticamente en la fuente. Sin embargo, ItemStacks no son
referencias, por lo que deberá actualizar el itemstack en el inventario.

Los captadores y definidores no tipados se convertirán ay desde cadenas:

print(meta:get_int("count")) --> 0

meta:set_int("count", 3)

print(meta:get_int("count")) --> 3

print(meta:get_string("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

Las tablas se deben convertir en cadenas antes de poder


almacenarlas. Minetest ofrece dos formatos para hacer esto: Lua y JSON.

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.

local data = { username = "player1", score = 1234 }

meta:set_string("foo", minetest.serialize(data))

data = minetest.deserialize(minetest:get_string("foo"))

Metadatos privados

De forma predeterminada, todos los metadatos de los nodos se envían al


cliente. Puede marcar las claves como privadas para evitar esto.

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.

local storage = minetest.get_mod_storage()

Ahora puede manipular el almacenamiento como si fueran metadatos:

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 })

El archivo backend_storage.lua debe incluir una implementación de


almacenamiento mod:

local storage = minetest.get_mod_storage()


local backend = {}

function backend.set_foo(key, value)

storage:set_string(key, minetest.serialize(value))

end

function backend.get_foo(key)

return minetest.deserialize(storage:get_string(key))

end

return backend

El backend_sqlite haría algo similar, pero usaría la biblioteca Lua lsqlite3


en lugar del almacenamiento de mods.

El uso de una base de datos como SQLite requiere el uso de un entorno


inseguro. Un entorno inseguro es una tabla que solo está disponible para
los mods incluidos explícitamente en la lista blanca del usuario, y contiene
una copia menos restringida de la API de Lua que podría abusarse si está
disponible para mods maliciosos. Los entornos inseguros se tratarán con
más detalle en el capítulo Seguridad .

local ie = minetest.request_insecure_environment()

assert(ie, "Please add mymod to secure.trusted_mods in the

settings")

local _sql = ie.require("lsqlite3")

-- Prevent other mods from using the global sqlite3 library

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.

Los metadatos de elementos no deben usarse para almacenar nada más


que pequeñas cantidades de datos, ya que no es posible evitar enviarlos
al cliente. Los datos también se copiarán cada vez que se mueva la pila o
se acceda a ella desde Lua.

El almacenamiento de mods es bueno para datos medianos, pero escribir


datos grandes puede ser ineficaz. Es mejor usar una base de datos para
datos grandes para evitar tener que escribir todos los datos en cada
guardado.

Las bases de datos solo son viables para servidores debido a la


necesidad de incluir el mod en la lista blanca para acceder a un entorno
inseguro. Son adecuados para grandes conjuntos de datos.

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 .)

Objetos, jugadores y entidades

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

¿Qué son objetos, jugadores y entidades?


Los jugadores y las entidades son ambos tipos de objetos. Un objeto es
algo que puede moverse independientemente de la cuadrícula de nodos y
tiene propiedades como la velocidad y la escala. Los objetos no son
artículos y tienen su propio sistema de registro independiente.

Hay algunas diferencias entre jugadores y entidades. El más importante


es que los jugadores están controlados por el jugador, mientras que las
entidades están controladas por mod. Esto significa que los mods no
pueden establecer la velocidad de un jugador: los jugadores están en el
lado del cliente y las entidades en el lado del servidor. Otra diferencia es
que los jugadores harán que se carguen los bloques del mapa, mientras
que las entidades simplemente se guardarán y quedarán inactivas.

Esta distinción se enturbia por el hecho de que las Entidades se controlan


mediante una tabla a la que se hace referencia como entidad Lua, como
se explica más adelante.

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")

local pos = object:get_pos()

object:set_pos({ x = pos.x, y = pos.y + 1, z = pos.z })

set_pos immediately sets the position, with no animation. If you’d like to


smoothly animate an object to the new position, you should use move_to .
This, unfortunately, only works for entities.
object:move_to({ x = pos.x, y = pos.y + 1, z = pos.z })
Una cosa importante en la que pensar cuando se trata de entidades es la
latencia de la red. En un mundo ideal, los mensajes sobre movimientos de
entidades llegarían de inmediato, en el orden correcto y con un intervalo
similar al de cómo los envió. Sin embargo, a menos que estés en un
jugador, este no es un mundo ideal. Los mensajes tardarán un poco en
llegar. Los mensajes de posición pueden llegar desordenados, lo que
provoca que set_pos se omitan algunas llamadas, ya que no tiene sentido
ir a una posición anterior a la posición conocida actual. Es posible que los
movimientos no estén espaciados de manera similar, lo que dificulta su
uso para la animación. Todo esto da como resultado que el cliente vea
cosas diferentes al servidor, que es algo de lo que debe estar consciente.

Propiedades del objeto


Las propiedades del objeto se utilizan para decirle al cliente cómo
renderizar y tratar con un objeto. No es posible definir propiedades
personalizadas, porque las propiedades son para que las utilice el motor,
por definición.

A diferencia de los nodos, los objetos tienen una apariencia dinámica en


lugar de fija. Puede cambiar el aspecto de un objeto, entre otras cosas, en
cualquier momento actualizando sus propiedades.

object:set_properties({

visual = "mesh",

mesh = "character.b3d",

textures = {"character_texture.png"},

visual_size = {x=1, y=1},

})

Las propiedades actualizadas se enviarán a todos los jugadores dentro


del alcance. Esto es muy útil para obtener una gran cantidad de variedad
a muy bajo costo, como tener diferentes máscaras por jugador.

Como se muestra en la siguiente sección, las entidades pueden tener


propiedades iniciales proporcionadas en su definición. Sin embargo, las
propiedades predeterminadas del reproductor están definidas en el motor,
por lo que deberá usar set_properties() in on_joinplayer para establecer
las propiedades de los jugadores recién incorporados.
Entidades
Una entidad tiene una tabla de definición que se asemeja a una tabla de
definición de artículo. Esta tabla puede contener métodos de devolución
de llamada, propiedades del objeto inicial y miembros personalizados.

local MyEntity = {

initial_properties = {

hp_max = 1,

physical = true,

collide_with_objects = false,

collisionbox = {-0.3, -0.3, -0.3, 0.3, 0.3, 0.3},

visual = "wielditem",

visual_size = {x = 0.4, y = 0.4},

textures = {""},

spritediv = {x = 1, y = 1},

initial_sprite_basepos = {x = 0, y = 0},

},

message = "Default message",

function MyEntity:set_message(msg)

self.message = msg

end

Las definiciones de entidad difieren en una forma muy importante de las


definiciones de elementos. Cuando surge una entidad (es decir, cargada o
creada), se crea una nueva tabla para esa entidad que hereda de la tabla
de definición.

Tanto una ObjectRef como una tabla de entidad proporcionan formas de


obtener la contraparte:
local entity = object:get_luaentity()

local object = entity.object

print("entity is at " ..

minetest.pos_to_string(object:get_pos()))

Hay una serie de devoluciones de llamada disponibles para su uso con


entidades. Se puede encontrar una lista completa en lua_api.txt .

function MyEntity:on_step(dtime)

local pos = self.object:get_pos()

local pos_down = vector.subtract(pos, vector.new(0, 1, 0))

local delta

if minetest.get_node(pos_down).name == "air" then

delta = vector.new(0, -1, 0)

elseif minetest.get_node(pos).name == "air" then

delta = vector.new(0, 0, 1)

else

delta = vector.new(0, 1, 0)

end

delta = vector.multiply(delta, dtime)

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

function MyEntity:on_activate(staticdata, dtime_s)

if staticdata ~= "" and staticdata ~= nil then

local data = minetest.parse_json(staticdata) or {}

self:set_message(data.message)

end

end

Minetest puede llamar get_staticdata() tantas veces como quiera y en


cualquier momento. Esto se debe a que Minetest no espera a que un
MapBlock se vuelva inactivo para guardarlo, ya que esto daría lugar a la
pérdida de datos. Los MapBlocks se guardan aproximadamente cada 18
segundos, por lo que debería notar un intervalo similar
para get_staticdata() ser llamado.
on_activate() , por otro lado, solo se llamará cuando una entidad se
active, ya sea desde que MapBlock se active o desde que la entidad se
genera. Esto significa que los datos estáticos pueden estar vacíos.
Finalmente, debe registrar la tabla de tipos utilizando el nombre
apropiado register_entity .
minetest.register_entity("mymod:entity", MyEntity)

The entity can be spawned by a mod like so:

local pos = { x = 1, y = 2, z = 3 }

local obj = minetest.add_entity(pos, "mymod:entity", nil)


El tercer parámetro son los datos estáticos iniciales. Para configurar el
mensaje, puede utilizar el método de tabla de entidad:

obj:get_luaentity():set_message("hello!")

Los jugadores con el privilegio de dar pueden usar un comando de


chat para generar entidades:

/spawnentity mymod:entity

Salud y daño

Puntos de vida (HP)


Cada objeto tiene un número de puntos de salud (HP), que representa la
salud actual. Los jugadores tienen un conjunto de hp máximo usando
la hp_max propiedad del objeto. Un objeto morirá si su HP llega a 0.
local hp = object:get_hp()

object:set_hp(hp + 3)

Puñetazos, grupos de daño y grupos de armaduras

El daño es la reducción del HP de un objeto. Un objeto puede golpear


a otro para infligir daño. Un puñetazo no es necesariamente un puñetazo
real, puede ser una explosión, un corte de espada o algo más.

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.

Al igual que los grupos de excavación de nodos , estos grupos pueden


tomar cualquier nombre y no necesitan estar registrados. Sin embargo, es
común usar los mismos nombres de grupo que con la excavación de
nodos.

La vulnerabilidad de un objeto a determinados tipos de daño depende de


la armor_groups propiedad del objeto . A pesar de su nombre
engañoso, armor_groups especifique el porcentaje de daño recibido de
grupos de daño particulares, no la resistencia. Si un grupo de daño no
está incluido en los grupos de armadura de un objeto, ese objeto es
completamente invulnerable a él.
target:set_properties({

armor_groups = { fleshy = 90, crumbly = 50 },

})

En el ejemplo anterior, el objeto recibirá el 90% del fleshy daño y el 50%


del crumbly daño

Cuando un jugador golpea un objeto, los grupos de daño provienen del


elemento que está empuñando actualmente. En otros casos, los mods
deciden qué grupos de daño se utilizan.

Ejemplo de cálculo de daños


Golpeemos el target objeto:
local tool_capabilities = {

full_punch_interval = 0.8,

damage_groups = { fleshy = 5, choppy = 10 },

-- This is only used for digging nodes, but is still required

max_drop_level=1,

groupcaps={

fleshy={times={[1]=2.5, [2]=1.20, [3]=0.35}, uses=30,

maxlevel=2},

},

local time_since_last_punch =

tool_capabilities.full_punch_interval

target:punch(object, time_since_last_punch, tool_capabilities)

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

Como HP es un número entero, el daño se redondea a 5 puntos.

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.

child:set_attach(parent, bone, position, rotation)

Un objeto get_pos() siempre devolverá la posición global del objeto, sin


importar si está adjunto o no. set_attach toma una posición relativa, pero
no como cabría esperar. La posición del archivo adjunto es relativa al
origen del padre y se amplió 10 veces. Entonces, 0,5,0 sería medio nodo
por encima del origen del padre.

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.

Para modelos 3D con animaciones, el argumento de hueso se usa para


unir la entidad a un hueso. Las animaciones 3D se basan en esqueletos:
una red de huesos en el modelo donde se puede dar a cada hueso una
posición y rotación para cambiar el modelo, por ejemplo, para mover el
brazo. Unirse a un hueso es útil si quieres que un personaje sostenga
algo:

obj:set_attach(player,
"Arm_Right", -- default bone

{x=0.2, y=6.5, z=3}, -- default position

{x=-100, y=225, z=90}) -- default rotation

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.

 Cuándo usar privilegios


 Declaración de privilegios
 Comprobación de privilegios
 Obtener y establecer privilegios
 Agregar privilegios a basic_privs

Cuándo usar privilegios


Un privilegio debería dar a un jugador la capacidad de hacer algo. Los
privilegios no son para indicar clase o estado.
Buenos privilegios:

 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", {

description = "Can vote on issues",

give_to_singleplayer = true

})

give_to_singleplayer por defecto es verdadero cuando no se especifica,


por lo que en realidad no es necesario en la definición anterior.

Comprobación de privilegios
Para comprobar rápidamente si un jugador tiene todos los privilegios
necesarios:

local has, missing = minetest.check_player_privs(player_or_name,

interact = true,
vote = true })

En este ejemplo, has es cierto si el jugador tiene todos los privilegios


necesarios. Si has es falso, missing contendrá una tabla de valores-clave
de los privilegios que faltan.
local has, missing = minetest.check_player_privs(name, {

interact = true,

vote = true })

if has then

print("Player has all privs!")

else

print("Player is missing privs: " .. dump(missing))

end

Si no necesita verificar los privilegios que faltan, puede


colocarlos check_player_privs directamente en la instrucción if.
if not minetest.check_player_privs(name, { interact=true }) then

return false, "You need interact for this!"

end

Obtener y establecer privilegios


Se puede acceder o modificar los privilegios del jugador
independientemente de que el jugador esté en línea.

local privs = minetest.get_player_privs(name)

print(dump(privs))

privs.vote = true

minetest.set_player_privs(name, privs)

Los privilegios siempre se especifican como una tabla clave-valor, siendo


la clave el nombre del privilegio y el valor un booleano.
{

fly = true,

interact = true,

shout = true

Agregar privilegios a basic_privs


Los jugadores con el basic_privs privilegio pueden otorgar y revocar un
conjunto limitado de privilegios. Es común otorgar este privilegio a los
moderadores para que puedan otorgar y revocar interact y shout , pero no
pueden otorgarse a sí mismos ni a otros jugadores privilegios con mayor
potencial de abuso como give y server .
Para agregar un privilegio basic_privs y ajustar los privilegios que sus
moderadores pueden otorgar y revocar a otros jugadores, debe cambiar
la basic_privs configuración.
Por defecto, basic_privs tiene el siguiente valor:
basic_privs = interact, shout

Para agregar vote , actualice esto a:


basic_privs = interact, shout, vote

Esto permitirá a los jugadores basic_privs otorgar y revocar


el vote privilegio.

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.

 Envío de mensajes a todos los jugadores


 Envío de mensajes a jugadores específicos
 Comandos de chat
 Subcomandos complejos
 Interceptar mensajes

Envío de mensajes a todos los jugadores


Para enviar un mensaje a todos los jugadores del juego, llama a la función
chat_send_all.

minetest.chat_send_all("This is a chat message to all players")

Aquí hay un ejemplo de cómo aparece esto en el juego:

<player1> Look at this entrance

This is a chat message to all players

<player2> What about it?

El mensaje aparece en una línea separada para distinguirlo del chat del
jugador en el juego.

Envío de mensajes a jugadores específicos


Para enviar un mensaje a un jugador específico, llame a la función
chat_send_player:

minetest.chat_send_player("player1", "This is a chat message for

player1")

Este mensaje se muestra de la misma manera que los mensajes para


todos los jugadores, pero solo es visible para el jugador nombrado, en
este caso, player1.

Comandos de chat
Para registrar un comando de chat, por ejemplo /foo ,
use register_chatcommand :
minetest.register_chatcommand("foo", {
privs = {

interact = true,

},

func = function(name, param)

return true, "You said " .. param .. "!"

end,

})

En el fragmento anterior, interact aparece como


un privilegio obligatorio, lo que significa que solo los jugadores con
el interact privilegio pueden ejecutar el comando.

Los comandos de chat pueden devolver hasta dos valores, el primero es


un booleano que indica éxito y el segundo es un mensaje para enviar al
usuario.

Los jugadores sin conexión pueden ejecutar comandos


Se pasa un nombre de jugador en lugar de un objeto de jugador porque
los mods pueden ejecutar comandos en nombre de jugadores fuera de
línea. Por ejemplo, el puente IRC permite a los jugadores ejecutar
comandos sin unirse al juego.

Así que asegúrese de no asumir que el reproductor está en línea. Puede


comprobarlo viendo si
minetest.get_player_by_name
devuelve un jugador.

Subcomandos complejos
A menudo se requiere realizar comandos de chat complejos, como:

 /msg <to> <message>


 /team join <teamname>
 /team leave <teamname>
 /team list
Esto generalmente se hace usando patrones Lua . Los patrones son una
forma de extraer cosas del texto usando reglas.

local to, msg = string.match(param, "^([%a%d_-]+) (*+)$")

El código anterior implementa /msg <to> <message> . Pasemos de


izquierda a derecha:

 ^ significa coincidir con el comienzo de la cadena.


 () es un grupo coincidente: todo lo que coincida con las cosas aquí
se devolverá desde string.match.
 [] significa aceptar caracteres en esta lista.
 %a significa aceptar cualquier letra y %d significa aceptar cualquier
dígito.
 [%a%d_-] significa aceptar cualquier letra o dígito o _ o - .
 + significa hacer coincidir la cosa antes una o más veces.
 * significa coincidir con cualquier personaje en este contexto.
 $ significa coincidir con el final de la cadena.

En pocas palabras, el patrón coincide con el nombre (una palabra con


solo letras / números / - / _), luego un espacio, luego el mensaje (uno o
más de cualquier carácter). El nombre y el mensaje se devuelven porque
están entre paréntesis.

Así es como la mayoría de los mods implementan comandos de chat


complejos. Una mejor guía para los patrones de Lua probablemente sería
el tutorial de lua-users.org o la documentación de PIL .

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)

print(name .. " said " .. 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.

Privilegios y comandos de chat


El privilegio de gritar no es necesario para que un jugador active esta
devolución de llamada. Esto se debe a que los comandos de chat se
implementan en Lua y son solo mensajes de chat que comienzan con /.
Debe asegurarse de tener en cuenta que puede ser un comando de chat o
que el usuario no lo haya hecho shout .
minetest.register_on_chat_message(function(name, message)

if message:sub(1, 1) == "/" then

print(name .. " ran chat command")

elseif minetest.check_player_privs(name, { shout = true })

then

print(name .. " said " .. message)

else

print(name .. " tried to say " .. message ..

" but doesn't have shout")

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", {

func = function(name, param)

local player = minetest.get_player_by_name(name)

player:set_physics_override({

gravity = 0.1, -- set gravity to 10% of its original

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:

 velocidad: multiplicador del valor de velocidad de marcha


predeterminado (predeterminado: 1)
 salto: multiplicador al valor de salto predeterminado
(predeterminado: 1)
 gravedad: multiplicador al valor de gravedad predeterminado
(predeterminado: 1)
 furtivo: si el jugador puede escabullirse (predeterminado: verdadero)

Comportamiento de movimiento antiguo

El movimiento del jugador antes de la versión 0.4.16 incluía el error de


furtividad, que permite varios fallos de movimiento, incluida la capacidad
de subir un 'elevador' hecho desde una cierta ubicación de nodos al
escabullirse (presionando mayúsculas) y presionando espacio para
ascender. Aunque el comportamiento no fue intencionado, se ha
conservado en anulaciones debido a su uso en muchos servidores.

Se necesitan dos anulaciones para restaurar completamente el


comportamiento de movimiento anterior:

 new_move: si el jugador usa un nuevo movimiento (predeterminado:


verdadero)
 sneak_glitch: si el jugador puede usar 'ascensores furtivos'
(predeterminado: falso)

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.

GUI (especificaciones de formularios)

Introducción

Captura de pantalla de formpec del horno,


etiquetada.

En este capítulo aprenderemos cómo crear una especificación de


formulario y mostrársela al usuario. Un formpec es el código de
especificación de un formulario. En Minetest, los formularios son ventanas
como el inventario del jugador y pueden contener una variedad de
elementos, como etiquetas, botones y campos.
Tenga en cuenta que si no necesita obtener información del usuario, por
ejemplo, cuando solo necesita proporcionar información al jugador, debe
considerar el uso de elementos de Heads Up Display (HUD) en lugar de
formularios, porque las ventanas inesperadas tienden a interrumpir el
juego.

 Coordenadas reales o heredadas


 Anatomía de un Formspec
o Elementos
o Encabezamiento
 Juego de adivinanzas
o Acolchado y espaciado
o Recibir presentaciones de Formspec
o Contextos
 Fuentes de Formspec
o Meta Formspecs de nodo
o Formularios de inventario del jugador
o Tu turno

Coordenadas reales o heredadas


En versiones anteriores de Minetest, las especificaciones de formularios
eran inconsistentes. La forma en que se colocaron los diferentes
elementos varió de formas inesperadas; era difícil predecir la ubicación de
los elementos y alinearlos. Minetest 5.1.0 contiene una característica
llamada coordenadas reales que tiene como objetivo rectificar esto
mediante la introducción de un sistema de coordenadas consistente. Se
recomienda encarecidamente el uso de coordenadas reales, por lo que
este capítulo las utilizará exclusivamente.

El uso de formpec_version de 2 o superior habilitará coordenadas reales.

Anatomía de un Formspec

Elementos

Formspec es un lenguaje específico de dominio con un formato


inusual. Consta de una serie de elementos con la siguiente forma:

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]

Los elementos son elementos como cuadros de texto o botones, o pueden


ser metadatos como el tamaño o el fondo. Debe consultar lua_api.txt para
obtener una lista de todos los elementos posibles.

Encabezamiento

El encabezado de una especificación de formulario contiene información


que debe aparecer primero. Esto incluye el tamaño de la especificación de
formulario, la posición, el ancla y si se debe aplicar el tema de todo el
juego.

Los elementos del encabezado deben definirse en un orden específico; de


lo contrario, verá un error. Este orden se da en el párrafo anterior y, como
siempre, se documenta en la referencia de la API de Lua.

El tamaño está en las ranuras de formpec, una unidad de medida que es


aproximadamente de alrededor de 64 píxeles, pero varía según la
densidad de pantalla y la configuración de escala del cliente. Aquí hay una
especificación de formulario que es 2,2 de tamaño:
formspec_version[4]

size[2,2]

Observe cómo definimos explícitamente la versión de lenguaje de


formpec. Sin esto, en su lugar, se utilizará el sistema heredado, lo que
evitará el uso de un posicionamiento de elementos consistente y otras
características nuevas.

Los elementos de posición y anclaje se utilizan para colocar la


especificación de formulario en la pantalla. La posición establece en qué
lugar de la pantalla estará la especificación de formulario y, por defecto,
es el centro ( 0.5,0.5 ). El ancla establece dónde está la posición en la
especificación del formulario, lo que le permite alinear la especificación del
formulario con el borde de la pantalla. La especificación de formulario se
puede colocar a la izquierda de la pantalla así:
formspec_version[4]

size[2,2]

position[0,0.5]

anchor[0,0.5]

Esto establece el ancla en el borde medio izquierdo del cuadro de


formpec, y luego la posición de ese ancla a la izquierda de la pantalla.

Juego de adivinanzas

El juego de adivinanzas formpec.

La mejor manera de aprender es hacer algo, así que hagamos un juego


de adivinanzas. El principio es simple: el mod decide un número, luego el
jugador hace conjeturas sobre el número. El mod luego dice si la
suposición es mayor o menor que el número real.

Primero, hagamos una función para crear el código formpec. Es una


buena práctica hacer esto, ya que facilita su reutilización en otros lugares.

guessing = {}

function guessing.get_formspec(name)

-- TODO: display whether the last guess was higher or lower

local text = "I'm thinking of a number... Make a guess!"

local formspec = {

"formspec_version[4]",
"size[6,3.476]",

"label[0.375,0.5;", minetest.formspec_escape(text), "]",

"field[0.375,1.25;5.25,0.8;number;Number;]",

"button[1.5,2.3;3,0.8;guess;Guess]"

-- table.concat is faster than string concatenation - `..`

return table.concat(formspec, "")

end

En el código anterior, colocamos un campo, una etiqueta y un botón. Un


campo permite la entrada de texto y se utiliza un botón para enviar el
formulario. Notará que los elementos están colocados con cuidado para
agregar relleno y espaciado, esto se explicará más adelante.

A continuación, queremos permitir que el jugador muestre la


especificación de formulario. La forma principal de hacer esto es
usando show_formspec :
function guessing.show_to(name)

minetest.show_formspec(name, "guessing:game",

guessing.get_formspec(name))

end

minetest.register_chatcommand("game", {

func = function(name)

guessing.show_to(name)

end,

})

La show_formspec función acepta un nombre de jugador, el nombre de la


especificación de formulario y la propia especificación de formulario. El
nombre de la especificación de formulario debe ser un nombre de
elemento válido, es decir: en el formato modname:itemname .

Acolchado y espaciado
El juego de adivinanzas formpec.

El relleno es el espacio entre el borde del formulario y su contenido, o


entre elementos no relacionados, que se muestran en rojo. El espaciado
es el espacio entre los elementos relacionados, que se muestra en azul.

Es bastante estándar tener un relleno 0.375 y un espaciado de 0.25 .

Recibir presentaciones de Formspec


Cuando show_formspec se llama, la especificación del formulario se envía
al cliente para que se muestre. Para que las especificaciones de
formularios sean útiles, la información debe devolverse del cliente al
servidor. El método para esto se llama envío de campo de formpec
y show_formspec , para ese envío, se recibe mediante una devolución de
llamada global:
minetest.register_on_player_receive_fields(function(player,

formname, fields)

if formname ~= "guessing:game" then

return

end

if fields.guess then

local pname = player:get_player_name()

minetest.chat_send_all(pname .. " guessed " ..

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.

Los clientes malintencionados pueden enviar cualquier cosa en


cualquier momento
Nunca debe confiar en un envío de formpec. Un cliente malintencionado
puede enviar lo que quiera en cualquier momento, incluso si nunca le
mostró la especificación de formulario. Esto significa que debe verificar los
privilegios y asegurarse de que se les permita realizar la acción.

Entonces, ahora la especificación del formulario se envía al cliente y el


cliente envía la información de vuelta. El siguiente paso es generar y
recordar de alguna manera el valor objetivo, y actualizar la especificación
de formulario basándose en conjeturas. La forma de hacerlo es utilizando
un concepto llamado "contextos".

Contextos

En muchos casos, desea que minetest.show_formspec proporcione


información a la devolución de llamada que no desea enviar al
cliente. Esto podría incluir cómo se llamó a un comando de chat o de qué
se trata el diálogo. En este caso, el valor objetivo que debe recordarse.

Un contexto es una tabla por jugador para almacenar información, y los


contextos para todos los jugadores en línea se almacenan en una variable
local de archivo:

local _contexts = {}

local function get_context(name)

local context = _contexts[name] or {}


_contexts[name] = context

return context

end

minetest.register_on_leaveplayer(function(player)

_contexts[player:get_player_name()] = nil

end)

A continuación, necesitamos modificar el código de presentación para


actualizar el contexto antes de mostrar la especificación de formulario:

function guessing.show_to(name)

local context = get_context(name)

context.target = context.target or math.random(1, 10)

local fs = guessing.get_formspec(name, context)

minetest.show_formspec(name, "guessing:game", fs)

end

También necesitamos modificar el código de generación de formpec para


usar el contexto:

function guessing.get_formspec(name, context)

local text

if not context.guess then

text = "I'm thinking of a number... Make a guess!"

elseif context.guess == context.target then

text = "Hurray, you got it!"

elseif context.guess > context.target then

text = "Too high!"

else

text = "Too low!"


end

Tenga en cuenta que es una buena práctica get_formspec leer solo el


contexto y no actualizarlo en absoluto. Esto puede hacer que la función
sea más simple y también más fácil de probar.

Y finalmente, necesitamos actualizar el controlador para actualizar el


contexto con la suposición:

if fields.guess then

local name = player:get_player_name()

local context = get_context(name)

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:

 show_formspec : el método utilizado anteriormente, los campos son


recibidos por register_on_player_receive_fields .
 Nodo Meta Formspecs : el nodo contiene una formpec en sus
metadatos, y el cliente la muestra inmediatamente cuando el
jugador hace clic derecho. Los campos se reciben mediante un
método en la definición de nodo llamado on_receive_fields .
 Especificaciones del formulario de inventario del jugador : la
especificación del formulario se envía al cliente en algún momento y
luego se muestra inmediatamente cuando el jugador presiona i . Los
campos son recibidos por register_on_player_receive_fields .

Meta Formspecs de nodo


minetest.show_formspec no es la única forma de mostrar una
especificación de formulario; también puede agregar formpecs a los
metadatos de un nodo . Por ejemplo, esto se usa con cofres para permitir
tiempos de apertura más rápidos; no es necesario esperar a que el
servidor envíe al jugador la especificación del formulario del cofre.
minetest.register_node("mymod:rightclick", {

description = "Rightclick me!",

tiles = {"mymod_rightclick.png"},

groups = {cracky = 1},

after_place_node = function(pos, placer)

-- This function is run when the chest node is placed.

-- The following code sets the formspec for chest.

-- Meta is a way of storing data onto a node.

local meta = minetest.get_meta(pos)

meta:set_string("formspec",

"formspec_version[4]" ..

"size[5,5]" ..

"label[1,1;This is shown on right click]" ..

"field[1,2;2,1;x;x;]")

end,

on_receive_fields = function(pos, formname, fields, player)

if fields.quit then

return

end

print(fields.x)

end

})

Las especificaciones de formulario establecidas de esta manera no


activan la misma devolución de llamada. Para recibir la entrada de
formulario para metaformularios, debe incluir
una on_receive_fields entrada al registrar el nodo.
Este estilo de devolución de llamada se activa cuando presiona Intro en
un campo, lo cual es imposible con minetest.show_formspec ; sin embargo,
este tipo de formulario solo se puede mostrar haciendo clic con el botón
derecho en un nodo. No se puede activar mediante programación.

Formularios de inventario del jugador


La especificación de formulario del inventario del jugador es la que se
muestra cuando el jugador presiona i. La devolución de llamada global se
usa para recibir eventos de esta especificación de formulario, y el nombre
del formulario es "" .

Hay una serie de modificaciones diferentes que permiten que varias


modificaciones personalicen el inventario del jugador. El mod
recomendado oficialmente es Simple Fast Inventory (sfinv) y está incluido
en Minetest Game.

Tu turno

 Amplíe el Juego de adivinanzas para realizar un seguimiento de la


puntuación máxima de cada jugador, donde la puntuación máxima
es la cantidad de conjeturas que tomó.
 Cree un nodo llamado "Bandeja de entrada" donde los usuarios
puedan abrir una especificación de formulario y dejar
mensajes. Este nodo debe almacenar el nombre de los colocadores
como owner en el meta, y debe usarse show_formspec para mostrar
diferentes especificaciones de formulario a diferentes jugadores.

Los elementos Heads Up Display (HUD) le permiten mostrar texto,


imágenes y otros elementos gráficos.

El HUD no acepta la entrada del usuario; para eso, debe usar


un formpec .

 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

Las pantallas vienen en una variedad de diferentes tamaños físicos y


resoluciones, y el HUD debe funcionar bien en todos los tipos de pantalla.

La solución de Minetest a esto es especificar la ubicación de un elemento


usando tanto una posición porcentual como un desplazamiento. La
posición porcentual es relativa al tamaño de la pantalla, por lo que para
colocar un elemento en el centro de la pantalla, necesitaría proporcionar
una posición porcentual de la mitad de la pantalla, por ejemplo (50%,
50%), y una compensación de (0 , 0).

Luego, el desplazamiento se usa para mover un elemento en relación con


la posición de porcentaje.

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.

El diagrama de arriba muestra 3 ventanas (azul), cada una con un solo


elemento HUD (amarillo) y una alineación diferente cada vez. La flecha es
el resultado del cálculo de posición y desplazamiento.

Marcador

A los efectos de este capítulo, aprenderá a colocar y actualizar un panel


de puntuación de la siguiente manera:
En la captura de pantalla anterior, todos los elementos tienen la misma
posición porcentual (100%, 50%), pero diferentes compensaciones. Esto
permite que todo se ancle a la derecha de la ventana, pero que cambie de
tamaño sin romperse.

Elementos de texto
Puede crear un elemento HUD una vez que tenga una copia del objeto del
jugador:

local player = minetest.get_player_by_name("username")

local idx = player:hud_add({

hud_elem_type = "text",

position = {x = 0.5, y = 0.5},

offset = {x = 0, y = 0},

text = "Hello world!",

alignment = {x = 0, y = 0}, -- center aligned

scale = {x = 100, y = 100}, -- covered later

})

La hud_add función devuelve un ID de elemento; esto se puede usar más


tarde para modificar o eliminar un elemento HUD.

Parámetros

El tipo del elemento se da usando la hud_elem_type propiedad en la tabla


de definición. El significado de otras propiedades varía según este tipo.

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

Sigamos adelante y coloquemos todo el texto en nuestro panel de


puntuación:

-- Get the dig and place count from storage, or default to 0

local meta = player:get_meta()

local digs_text = "Digs: " .. meta:get_int("score:digs")

local places_text = "Places: " .. meta:get_int("score:places")

player:hud_add({

hud_elem_type = "text",

position = {x = 1, y = 0.5},

offset = {x = -120, y = -25},

text = "Stats",

alignment = 0,

scale = { x = 100, y = 30},

number = 0xFFFFFF,

})

player:hud_add({

hud_elem_type = "text",

position = {x = 1, y = 0.5},

offset = {x = -180, y = 0},

text = digs_text,

alignment = -1,

scale = { x = 50, y = 10},

number = 0xFFFFFF,
})

player:hud_add({

hud_elem_type = "text",

position = {x = 1, y = 0.5},

offset = {x = -70, y = 0},

text = places_text,

alignment = -1,

scale = { x = 50, y = 10},

number = 0xFFFFFF,

})

Esto da como resultado lo siguiente:


Elementos de imagen
Los elementos de imagen se crean de forma muy similar a los elementos
de texto:

player:hud_add({

hud_elem_type = "image",

position = {x = 1, y = 0.5},

offset = {x = -220, y = 0},

text = "score_background.png",

scale = { x = 1, y = 1},

alignment = { x = 1, y = 0 },

})

Ahora tendrás esto:

Parámetros

El text campo se utiliza para proporcionar el nombre de la imagen.

Si una coordenada es positiva, entonces es un factor de escala donde 1


es el tamaño de la imagen original, 2 es el doble del tamaño, y así
sucesivamente. Sin embargo, si una coordenada es negativa, es un
porcentaje del tamaño de la pantalla. Por ejemplo, x=-100 es el 100% del
ancho.

Escala

Hagamos la barra de progreso para nuestro panel de puntuación como


ejemplo de escala:
local percent = tonumber(meta:get("score:score") or 0.2)

player:hud_add({

hud_elem_type = "image",

position = {x = 1, y = 0.5},

offset = {x = -215, y = 23},

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},

offset = {x = -215, y = 23},

text = "score_bar_full.png",

scale = { x = percent, y = 1},

alignment = { x = 1, y = 0 },

})

¡Ahora tenemos un HUD que se parece al de la primera publicación! Sin


embargo, hay un problema, no se actualizará cuando cambien las
estadísticas.

Cambiar un elemento
Puede utilizar el ID devuelto por el hud_add método para actualizarlo o
eliminarlo más tarde.

local idx = player:hud_add({

hud_elem_type = "text",

text = "Hello world!",


-- parameters removed for brevity

})

player:hud_change(idx, "text", "New Text")

player:hud_remove(idx)

El hud_change método toma el ID del elemento, la propiedad a cambiar y el


nuevo valor. La llamada anterior cambia la text propiedad de "Hola
mundo" a "Nuevo texto".

Esto significa que hacer lo hud_change inmediatamente después


de hud_add es funcionalmente equivalente a lo siguiente, de una manera
bastante ineficiente:

local idx = player:hud_add({

hud_elem_type = "text",

text = "New Text",

})

Almacenamiento de ID
score = {}

local saved_huds = {}

function score.update_hud(player)

local player_name = player:get_player_name()

-- Get the dig and place count from storage, or default to 0

local meta = player:get_meta()

local digs_text = "Digs: " .. meta:get_int("score:digs")

local places_text = "Places: " ..

meta:get_int("score:places")

local percent = tonumber(meta:get("score:score") or 0.2)


local ids = saved_huds[player_name]

if ids then

player:hud_change(ids["places"], "text", places_text)

player:hud_change(ids["digs"], "text", digs_text)

player:hud_change(ids["bar_foreground"],

"scale", { x = percent, y = 1 })

else

ids = {}

saved_huds[player_name] = ids

-- create HUD elements and set ids into `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.

 Registro de una página


 Recibir eventos
 Mostrar condicionalmente a los jugadores
 devoluciones de llamada on_enter y on_leave
 Agregar a una página existente

Registro de una página


SFINV proporciona la sfinv.register_page función con el nombre
adecuado para crear páginas. Simplemente llame a la función con el
nombre de la página y su definición:
sfinv.register_page("mymod:hello", {

title = "Hello!",

get = function(self, player, context)

return sfinv.make_formspec(player, context,

"label[0.1,0.1;Hello world!]", true)

end

})

La make_formspec función rodea su especificación de formulario con el


código de especificación de formulario de SFINV. El cuarto parámetro,
actualmente configurado como true , determina si se muestra el inventario
del jugador.
Hagamos las cosas más emocionantes; aquí está el código para la parte
de generación de formpec de una pestaña de administración de
jugador. Esta pestaña permitirá a los administradores expulsar o prohibir
jugadores seleccionándolos en una lista y haciendo clic en un botón.

sfinv.register_page("myadmin:myadmin", {

title = "Tab",

get = function(self, player, context)

local players = {}

context.myadmin_players = players

-- Using an array to build a formspec is considerably

faster

local formspec = {

"textlist[0.1,0.1;7.8,3;playerlist;"

-- Add all players to the text list, and to the players

list

local is_first = true

for _ , player in pairs(minetest.get_connected_players())

do

local player_name = player:get_player_name()

players[#players + 1] = player_name

if not is_first then

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]"

-- Wrap the formspec in sfinv's layout

-- (ie: adds the tabs and background)

return sfinv.make_formspec(player, context,

table.concat(formspec, ""), false)

end,

})

No hay nada nuevo en el código anterior; todos los conceptos se tratan


arriba y en capítulos anteriores.

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)

-- TODO: implement this

end,

on_player_receive_fields funciona igual


que minetest.register_on_player_receive_fields , excepto
que context se proporciona en lugar de formname . Tenga en cuenta que
SFINV consumirá eventos relevantes para sí mismo, como los eventos de
la pestaña de navegación, por lo que no los recibirá en esta devolución de
llamada.
Ahora implementemos el on_player_receive_fields para nuestro mod de
administrador:
on_player_receive_fields = function(self, player, context,

fields)

-- text list event, check event type and set index if

selection changed

if fields.playerlist then

local event =

minetest.explode_textlist_event(fields.playerlist)

if event.type == "CHG" then

context.myadmin_selected_idx = event.index

end

-- Kick button was pressed

elseif fields.kick then

local player_name =

context.myadmin_players[context.myadmin_selected_idx]

if player_name then

minetest.chat_send_player(player:get_player_name(),

"Kicked " .. player_name)

minetest.kick_player(player_name)
end

-- Ban button was pressed

elseif fields.ban then

local player_name =

context.myadmin_players[context.myadmin_selected_idx]

if player_name then

minetest.chat_send_player(player:get_player_name(),

"Banned " .. player_name)

minetest.ban_player(player_name)

minetest.kick_player(player_name, "Banned")

end

end

end,

Sin embargo, hay un problema bastante grande con esto. ¡Cualquiera


puede expulsar o prohibir a los jugadores! Necesita una forma de mostrar
esto solo a los jugadores con privilegios de expulsión o
prohibición. ¡Afortunadamente, SFINV te permite hacer esto!

Mostrar condicionalmente a los jugadores


Puede agregar una is_in_nav función a la definición de su página si desea
controlar cuándo se muestra la página:
is_in_nav = function(self, player, context)

local privs =

minetest.get_player_privs(player:get_player_name())

return privs.kick or privs.ban

end,

Si solo necesita marcar un priv o desea realizar un 'y', debe usar


en minetest.check_player_privs() lugar de get_player_privs .
Tenga en cuenta que is_in_nav solo se llama cuando se genera la
especificación de formulario de inventario del jugador. Esto sucede
cuando un jugador se une al juego, cambia de pestaña o un mod solicita
que SFINV se regenere.
Esto significa que debe solicitar manualmente que SFINV regenere la
especificación de formulario de inventario en cualquier evento que pueda
cambiar is_in_nav el resultado. En nuestro caso, debemos hacerlo
siempre que se conceda o revoque una expulsión o prohibición a un
jugador:
local function on_grant_revoke(grantee, granter, priv)

if priv ~= "kick" and priv ~= "ban" then

return

end

local player = minetest.get_player_by_name(grantee)

if not player then

return

end

local context = sfinv.get_or_create_context(player)

if context.page ~= "myadmin:myadmin" then

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)

devoluciones de llamada on_enter y on_leave


Un jugador ingresa a una pestaña cuando la pestaña está seleccionada
y sale de una pestaña cuando otra pestaña está a punto de ser
seleccionada. Es posible seleccionar varias páginas si se utiliza un tema
personalizado.

Tenga en cuenta que es posible que el jugador no active estos


eventos. Es posible que el jugador ni siquiera tenga abierta la
especificación de formulario en ese momento. Por ejemplo, se llama a
on_enter para la página de inicio cuando un jugador se une al juego
incluso antes de abrir su inventario.

No es posible cancelar un cambio de página, ya que eso podría confundir


al jugador.

on_enter = function(self, player, context)

end,

on_leave = function(self, player, context)

end,

Agregar a una página existente


Para agregar contenido a una página existente, deberá anular la página y
modificar la especificación de formulario devuelta.

local old_func = sfinv.registered_pages["sfinv:crafting"].get

sfinv.override_page("sfinv:crafting", {

get = function(self, player, context, ...)

local ret = old_func(self, player, context, ...)

if type(ret) == "table" then

ret.formspec = ret.formspec .. "label[0,0;Hello]"

else

-- Backwards compatibility

ret = ret .. "label[0,0;Hello]"


end

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.

 ¿Qué son los biomas?


 Colocación del bioma
o Calor y Humedad
o Visualización de límites usando diagramas de Voronoi
o Creando un diagrama de Voronoi usando Geogebra
 Registro de un bioma
 ¿Qué son las decoraciones?
 Registro de una decoración sencilla
 Registro de una decoración esquemática
 Alias de Mapgen

¿Qué son los biomas?


Un bioma Minetest es un entorno específico del juego. Al registrar biomas,
puede determinar los tipos de nodos que aparecen en ellos durante la
generación de mapas. Algunos de los tipos más comunes de nodos que
pueden variar entre biomas incluyen:

 Nodo superior: este es el nodo que se encuentra con mayor


frecuencia en la superficie. Un ejemplo muy conocido sería "Dirt with
Grass" de Minetest Game.
 Nodo de relleno: esta es la capa inmediatamente debajo del nodo
superior. En biomas con pasto, a menudo será tierra.
 Nodo de piedra: este es el nodo que se ve más comúnmente bajo
tierra.
 Nodo de agua: este suele ser un líquido y será el nodo que aparece
donde esperarías cuerpos de agua.

Otros tipos de nodos también pueden variar entre biomas, lo que brinda la
oportunidad de crear entornos muy diferentes dentro del mismo juego.

Colocación del bioma

Calor y Humedad

No es suficiente simplemente registrar un bioma; también debes decidir


dónde puede ocurrir en el juego. Esto se hace asignando un valor de calor
y humedad a cada bioma.

Debe pensar detenidamente sobre estos valores; determinan qué biomas


pueden ser vecinos entre sí. Las malas decisiones podrían resultar en lo
que se supone que es un desierto caliente que comparte una frontera con
un glaciar y otras combinaciones improbables que quizás prefiera evitar.

En el juego, los valores de calor y humedad en cualquier punto del mapa


generalmente estarán entre 0 y 100. Los valores cambian gradualmente,
aumentando o disminuyendo a medida que te mueves por el mapa. El
bioma en cualquier punto dado será determinado por cuál de los biomas
registrados tiene valores de calor y humedad más cercanos a los de esa
posición en el mapa.

Debido a que los cambios en el calor y la humedad son graduales, es una


buena práctica asignar valores de calor y humedad a los biomas en
función de expectativas razonables sobre el entorno de ese bioma. Por
ejemplo:

 Un desierto puede tener mucho calor y poca humedad.


 Un bosque nevado puede tener un calor bajo y un valor de humedad
medio.
 Un bioma de pantano generalmente tiene mucha humedad. * En la
práctica, esto significa que, siempre que tenga una gama diversa de
biomas, es probable que encuentre que los biomas que limitan entre
sí forman una progresión lógica.
Visualización de límites usando diagramas de Voronoi

Diagrama de Voronoi, que muestra el punto


más cercano.Por Balu Ertl , CC BY-SA 4.0.

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.

La forma más sencilla de visualizar qué biomas pueden compartir


fronteras es crear un diagrama de Voronoi, que se puede utilizar para
mostrar qué punto de un diagrama bidimensional está más cerca de
cualquier posición dada.

Un diagrama de Voronoi puede revelar dónde no están los biomas que


deberían lindar entre sí, y dónde lo hacen los biomas que no deberían
lindar entre sí. También puede dar una idea general de cómo serán los
biomas comunes en el juego, siendo más comunes los biomas más
grandes y centrales que los biomas más pequeños o los biomas que se
encuentran en el borde exterior del diagrama.

Esto se hace marcando un punto para cada bioma en función de los


valores de calor y humedad, donde el eje x es el calor y el eje y es la
humedad. Luego, el diagrama se divide en áreas, de modo que cada
posición en un área determinada esté más cerca del punto dentro de esa
área que de cualquier otro punto del diagrama.
Cada área representa un bioma. Si dos áreas comparten un borde, los
biomas que representan en el juego se pueden ubicar uno al lado del
otro. La longitud del borde compartido entre dos áreas, en comparación
con la longitud compartida con otras áreas, le dirá con qué frecuencia es
probable que se encuentren dos biomas uno al lado del otro.

Creando un diagrama de Voronoi usando Geogebra

Además de dibujarlos a mano, también puede crear diagramas de Voronoi


utilizando programas como Geogebra .

1. Cree puntos seleccionando la herramienta de puntos en la barra de


herramientas (el icono es un punto con 'A') y luego haciendo clic en
el gráfico. Puede arrastrar puntos o establecer explícitamente su
posición en la barra lateral izquierda. También debes ponerle una
etiqueta a cada punto, para aclarar las cosas.
2. A continuación, cree el voronoi ingresando la siguiente función en el
cuadro de entrada en la barra lateral izquierda:
3.Voronoi({ A, B, C, D, E })

Donde cada punto está dentro de los corchetes, separados por


comas. Deberías ahora

4. ¡Lucro! Ahora debería tener un diagrama de voronoi con todos los


puntos que se pueden arrastrar.

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.

Hay muchas opciones al registrar un bioma, y estas están documentadas


en Minetest Lua API Reference , como siempre.

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.

¿Qué son las decoraciones?


Las decoraciones son nodos o esquemas que se pueden colocar en el
mapa en mapgen. Algunos ejemplos comunes incluyen flores, arbustos y
árboles. Otros usos más creativos pueden incluir carámbanos colgantes o
estalagmitas en cuevas, formaciones de cristal subterráneas o incluso la
colocación de pequeños edificios.

Las decoraciones se pueden restringir a biomas específicos, por altura o


por los nodos en los que se pueden colocar. A menudo se utilizan para
desarrollar el entorno de un bioma asegurándose de que tenga plantas,
árboles u otras características específicas.

Registro de una decoración sencilla


Se utilizan decoraciones simples para colocar decoraciones de un solo
nodo en el mapa durante la generación del mapa. Debe especificar el
nodo que se colocará como decoración, detalles de dónde se puede
colocar y con qué frecuencia ocurre.

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",

})

En este ejemplo, el nodo nombrado plants:grass se colocará en el bioma


llamado grassy_plains en la parte superior de
los base:dirt_with_grass nodos, entre las alturas de y = 1 y y = 200 .

El valor de fill_ratio determina la frecuencia con la que aparece la


decoración, con valores más altos hasta 1, lo que da como resultado que
se coloque una gran cantidad de decoraciones. En su lugar, es posible
utilizar parámetros de ruido para determinar la ubicación.

Registro de una decoración esquemática


Las decoraciones esquemáticas son muy similares a la decoración simple,
pero implican la colocación de un esquema en lugar de la colocación de
un solo nodo. Por ejemplo:

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",

flags = "place_center_x, place_center_z",

rotation = "random",

})

En este ejemplo, el esquema cactus.mts se coloca en biomas


desérticos. Debe proporcionar una ruta a un esquema, que en este caso
se almacena en un directorio de esquemas dedicado dentro del mod.

Este ejemplo también establece banderas para centrar la ubicación del


esquema y la rotación se establece en aleatoria. La rotación aleatoria de
los esquemas cuando se colocan como decoraciones ayuda a introducir
más variaciones cuando se utilizan esquemas asimétricos.

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.

Los alias de Mapgen proporcionan información al mapgen principal y se


pueden registrar en el formulario:

minetest.register_alias("mapgen_stone", "base:smoke_stone")

Como mínimo debe registrarse:

 mapgen_stone
 mapgen_water_source
 mapgen_river_water_source

Si no está definiendo los nodos líquidos de la cueva para todos los


biomas, también debe registrarse:

 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()

local emin, emax = vm:read_from_map(pos1, pos2)

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()

Puede obtener datos de iluminación y param2 utilizando los


métodos get_light_data() y get_param2_data() .
Necesitará usar emin y emax averiguar dónde está un nodo en las matrices
planas dadas por los métodos anteriores. Hay una clase auxiliar
llamada VoxelArea que maneja el cálculo por usted.
local a = VoxelArea:new{

MinEdge = emin,

MaxEdge = emax

-- Get node's index

local idx = a:index(x, y, z)

-- Read node

print(data[idx])

Cuando ejecute esto, notará que data[vi] es un número entero. Esto se


debe a que el motor no almacena nodos mediante cadenas, por motivos
de rendimiento. En su lugar, el motor usa un número entero llamado ID de
contenido. Puede averiguar el ID de contenido para un tipo particular de
nodo con get_content_id() . Por ejemplo:
local c_stone = minetest.get_content_id("default:stone")

Luego puede verificar si el nodo es de piedra:

local idx = a:index(x, y, z)

if data[idx] == c_stone then

print("is stone!")

end

Los ID de contenido de un tipo de nodo pueden cambiar durante el tiempo


de carga, por lo que se recomienda que no intente obtenerlos durante este
tiempo.

Los nodos en una matriz de datos LVM se almacenan en orden de


coordenadas inverso, por lo que siempre debe iterar en el orden z, y,
x . Por ejemplo:
for z = min.z, max.z do

for y = min.y, max.y do

for x = min.x, max.x do

-- vi, voxel index, is a common variable name here

local vi = a:index(x, y, z)

if data[vi] == c_stone then

print("is stone!")

end

end

end

end

La razón de esto toca el tema de la arquitectura de computadoras. Leer


desde la RAM es bastante costoso, por lo que las CPU tienen varios
niveles de almacenamiento en caché. Si los datos que solicita un proceso
están en la caché, puede recuperarlos muy rápidamente. Si los datos no
están en la caché, se produce una falla en la caché y obtendrá los datos
que necesita de la RAM. Todos los datos que rodean a los datos
solicitados también se obtienen y luego reemplazan los datos en la
caché. Esto se debe a que es bastante probable que el proceso solicite
datos cerca de esa ubicación nuevamente. Esto significa que una buena
regla de optimización es iterar de tal manera que lea los datos uno tras
otro y evite la destrucción de caché .

Nodos de escritura
Primero, debe configurar el nuevo ID de contenido en la matriz de datos:

for z = min.z, max.z do

for y = min.y, max.y do

for x = min.x, max.x do

local vi = a:index(x, y, z)

if data[vi] == c_stone then

data[vi] = c_air

end

end

end

end

Cuando termine de configurar los nodos en el LVM, deberá cargar la


matriz de datos en el motor:

vm:set_data(data)

vm:write_to_map(true)

Para configurar los datos de iluminación y param2, utilice


los métodos set_light_data() y con el nombre
apropiado set_param2_data() .
write_to_map() toma un booleano que es verdadero si desea que se
calcule la iluminación. Si pasa falso, debe volver a calcular la iluminación
en el futuro utilizando minetest.fix_light .

Ejemplo
local function grass_to_dirt(pos1, pos2)

local c_dirt = minetest.get_content_id("default:dirt")

local c_grass =

minetest.get_content_id("default:dirt_with_grass")

-- Read data into LVM

local vm = minetest.get_voxel_manip()

local emin, emax = vm:read_from_map(pos1, pos2)

local a = VoxelArea:new{

MinEdge = emin,

MaxEdge = emax

local data = vm:get_data()

-- Modify data

for z = pos1.z, pos2.z do

for y = pos1.y, pos2.y do

for x = pos1.x, pos2.x do

local vi = a:index(x, y, z)

if data[vi] == c_grass then

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.

El diseño de juegos es un tema complejo y en realidad es todo un campo


de especialización. Está más allá del alcance del libro tocarlo más que
brevemente.

Directorio del juego


La estructura y la ubicación de un juego parecerán bastante familiares
después de trabajar con modificaciones. Los juegos se encuentran en una
ubicación de juego, como minetest/games/foo_game .
foo_game

├── game.conf

├── menu

│ ├── header.png

│ ├── background.png

│ └── icon.png

├── minetest.conf

├── mods

│ └── ... mods

├── README.txt

└── settingtypes.txt

Lo único que se requiere es una carpeta mods,


pero game.conf y menu/icon.png se recomiendan.

Compatibilidad entre juegos

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.

La mejor manera de mantener la compatibilidad con otro juego es


mantener la compatibilidad de la API con cualquier mod que tenga el
mismo nombre. Es decir, si un mod usa el mismo nombre que otro mod,
incluso si es de un tercero, debería tener una API compatible. Por
ejemplo, si un juego incluye un mod llamado doors , entonces debería
tener la misma API que doors en Minetest Game.

La compatibilidad de API para un mod es la suma de las siguientes cosas:

 Tabla de API de Lua: todas las funciones documentadas /


anunciadas en la tabla global que comparte el mismo nombre. Por
ejemplo mobs.register_mob ,.
 Nodos / elementos registrados: la presencia de elementos.

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.

Es difícil mantener la compatibilidad de la API con un desagradable mega


Dios-mod como predeterminado en Minetest Game, en cuyo caso el juego
no debería incluir un mod llamado predeterminado.

La compatibilidad de API también se aplica a otros juegos y mods de


terceros, así que intenta asegurarte de que los nuevos mods tengan un
nombre de mod único. Para comprobar si se ha tomado un nombre de
mod, búsquelo en content.minetest.net .

Grupos y alias

Los grupos y los alias son herramientas útiles para mantener la


compatibilidad entre juegos, ya que permiten que los nombres de los
elementos sean diferentes entre los diferentes juegos. Los nodos
comunes como la piedra y la madera deben tener grupos para indicar el
material. También es una buena idea proporcionar alias de los nodos
predeterminados a los reemplazos directos.

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.

 Tenga cuidado al almacenar ObjectRefs (es decir, jugadores o


entidades)
 No confíe en las presentaciones de Formspec
 Establecer ItemStacks después de cambiarlos

Tenga cuidado al almacenar ObjectRefs (es decir,


jugadores o entidades)
Un ObjectRef se invalida cuando el jugador o la entidad que representa
abandona el juego. Esto puede suceder cuando el jugador se desconecta
o la entidad se descarga o se elimina.

Los métodos de ObjectRefs siempre devolverán nil cuando no sean


válidos, desde Minetest 5.2. Básicamente, se ignorará cualquier llamada.

Debe evitar almacenar ObjectRefs siempre que sea posible. Si lo hace


para almacenar un ObjectRef, debe hacer que lo verifique antes de usarlo,
así:

-- This only works in Minetest 5.2+

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:

local function show_formspec(name)

if not minetest.check_player_privs(name, { privs = true })

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)

-- BAD! Missing privilege check here!

local privs = minetest.get_player_privs(fields.target)

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)

if not minetest.check_player_privs(name, { privs = true })

then

return false

end

-- code

end)

Establecer ItemStacks después de cambiarlos


¿Ha notado que ItemStack en la API simplemente se llama an , no
an ItemStackRef , similar a InvRef ? Esto se debe a ItemStack que no es
una referencia, es una copia. Las pilas funcionan en una copia de los
datos en lugar de en la pila del inventario. Esto significa que modificar una
pila no modificará esa pila en el inventario.

Por ejemplo, no hagas esto:

local inv = player:get_inventory()

local stack = inv:get_stack("main", 1)

stack:get_meta():set_string("description", "Partially eaten")

-- BAD! Modification will be lost

Haz esto en su lugar:

local inv = player:get_inventory()

local stack = inv:get_stack("main", 1)

stack:get_meta():set_string("description", "Partially eaten")

inv:set_stack("main", 1, stack)

-- Correct! Item stack is set


El comportamiento de las devoluciones de llamada es un poco más
complicado. La modificación de una ItemStack que se le proporcione
también la cambiará para la persona que llama y las devoluciones de
llamada posteriores. Sin embargo, solo se guardará en el motor si el
llamador de devolución de llamada lo configura.
minetest.register_on_item_eat(function(hp_change,

replace_with_item,

itemstack, user, pointed_thing)

itemstack:get_meta():set_string("description", "Partially

eaten")

-- Almost correct! Data will be lost if another

-- callback cancels the behaviour

end)

Si ninguna devolución de llamada cancela esto, la pila se establecerá y la


descripción se actualizará, pero si una devolución de llamada cancela
esto, es posible que la actualización se pierda.

Es mejor hacer esto en su lugar:

minetest.register_on_item_eat(function(hp_change,

replace_with_item,

itemstack, user, pointed_thing)

itemstack:get_meta():set_string("description", "Partially

eaten")

user:get_inventory():set_stack("main",

user:get_wield_index(),

itemstack)

-- Correct, description will always be set!

end)

Si las devoluciones de llamada se cancelan o el corredor de devolución de


llamada no configura la pila, la actualización aún se establecerá. Si las
devoluciones de llamada o el corredor de devolución de llamada
configuran la pila, entonces el uso de set_stack no importa.

Comprobación automática de errores

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

Simplemente descargue luacheck.exe desde la página de versiones de


Github .

Linux

Primero, necesitará instalar LuaRocks:


sudo apt install luarocks

Luego puede instalar LuaCheck globalmente:

sudo luarocks install luacheck

Comprueba que esté instalado con el siguiente comando:

luacheck -v

Ejecutando LuaCheck
La primera vez que ejecute LuaCheck, probablemente detectará muchos
errores falsos. Esto se debe a que aún debe configurarse.

En Windows, abra powershell o bash en la carpeta raíz de su proyecto y


ejecute path\to\luacheck.exe .
En Linux, ejecútelo luacheck . desde la carpeta raíz de su proyecto.

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.

Pon el siguiente contenido en él:

unused_args = false

allow_defined_top = true

globals = {

"minetest",

read_globals = {

string = {fields = {"split"}},

table = {fields = {"copy", "getn"}},


-- Builtin

"vector", "ItemStack",

"dump", "DIR_DELIM", "VoxelArea", "Settings",

-- MTG

"default", "sfinv", "creative",

A continuación, deberá probar que funciona ejecutando LuaCheck. Esta


vez debería obtener muchos menos errores. A partir del primer error que
reciba, modifique el código para eliminar el problema o modifique la
configuración si el código es correcto. Vea la lista a continuación.

Solución de problemas

 acceder a la variable indefinida foobar : si foobar está destinado a


ser global, agréguelo a read_globals . De lo contrario, agregue
los local s faltantes al mod.
 establecer foobar variable global no estándar : si foobar está
destinado a ser global, agréguelo a globals . Retirar
de read_globals si está presente. De lo contrario, agregue
los local s faltantes al mod.
 variable global de solo lectura mutante 'foobar' :
muévase foobar de read_globals a globals o deje de escribir en
foobar.

Usando con editor


Se recomienda encarecidamente que encuentre e instale un complemento
para su editor de elección para mostrarle errores sin ejecutar un
comando. Es probable que la mayoría de los editores tengan un
complemento disponible.

 Átomo - linter-luacheck .
 VSCode - Ctrl + P, luego pegue: ext install dwenegar.vscode-
luacheck
 Sublime : instale usando el control de
paquetes: SublimeLinter , SublimeLinter-luacheck .

Comprobación de confirmaciones con Travis


Si su proyecto es público y está en Github, puede usar TravisCI, un
servicio gratuito para ejecutar trabajos en confirmaciones para
verificarlos. Esto significa que cada confirmación que presione se
comparará con LuaCheck, y se mostrará una marca verde o una cruz roja
junto a ellos, dependiendo de si LuaCheck encuentra algún error. Esto es
especialmente útil cuando su proyecto recibe una solicitud de extracción:
podrá ver la salida de LuaCheck sin descargar el código.

Primero, debe visitar travis-ci.org e iniciar sesión con su cuenta de


Github. Luego, busque el repositorio de su proyecto en su perfil de Travis
y habilite Travis presionando el interruptor.

A continuación, cree un archivo llamado .travis.yml con el siguiente


contenido:

language: generic

sudo: false

addons:

apt:

packages:

- luarocks

before_install:

- luarocks install --local luacheck

script:

- $HOME/.luarocks/bin/luacheck .

notifications:

email: false

Si su proyecto es un juego en lugar de un mod o paquete de mod, cambie


la línea después script: a:
- $HOME/.luarocks/bin/luacheck mods/
Ahora confíe y empuje a Github. Vaya a la página de su proyecto en
Github y haga clic en 'confirmar'. Debería ver un disco naranja junto a la
confirmación que acaba de realizar. Después de un tiempo, debería
cambiar a una marca verde o una cruz roja, según el resultado de
LuaCheck. En cualquier caso, puede hacer clic en el icono para ver los
registros de compilación y la salida de LuaCheck.

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).

Una acción maliciosa no es necesariamente la modificación o destrucción


de datos, pero puede ser acceder a datos confidenciales, como hashes de
contraseñas o mensajes privados. Esto es especialmente malo si el
servidor almacena información como correos electrónicos o edades, lo
que algunos pueden hacer con fines de verificación.

Formularios

Nunca confíes en las presentaciones

Cualquier usuario puede enviar casi cualquier especificación de formulario


con cualquier valor en cualquier momento.

Aquí hay un código real que se encuentra en un mod:

minetest.register_on_player_receive_fields(function(player,

formname, fields)

for key, field in pairs(fields) do

local x,y,z = string.match(key,

"goto_([%d-]+)_([%d-]+)_([%d-]+)")

if x and y and z then

player:set_pos({ x=tonumber(x), y=tonumber(y),

z=tonumber(z) })

return true

end

end

end

¿Puedes detectar el problema? Un usuario malintencionado podría enviar


una especificación de formulario que contenga sus propios valores de
posición, lo que le permitirá teletransportarse a cualquier lugar que
desee. Esto incluso podría automatizarse utilizando modificaciones del
cliente para replicar esencialmente el /teleport comando sin necesidad de
un privilegio.

La solución para este tipo de problema es utilizar un contexto , como se


mostró anteriormente en el capítulo de Formspecs.
El tiempo de verificación no es el tiempo de uso

Cualquier usuario puede enviar cualquier especificación de formulario con


cualquier valor en cualquier momento, excepto cuando el motor lo
prohíba:

 El envío de una especificación de formulario de nodo se bloqueará


si el usuario está demasiado lejos.
 A partir de 5.0, las especificaciones de formularios con nombre se
bloquearán si aún no se han mostrado.

Esto significa que debe verificar en el controlador que el usuario cumple


con las condiciones para mostrar la especificación de formulario en primer
lugar, así como las acciones correspondientes.

La vulnerabilidad causada por la comprobación de permisos en la


especificación del formulario de la demostración pero no en la
especificación del formulario del identificador se llama Tiempo de
comprobación no es tiempo de uso (TOCTOU).

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.

¿Puedes detectar la vulnerabilidad en lo siguiente?

local ie = minetest.request_insecure_environment()

ie.os.execute(("path/to/prog %d"):format(3))

string.format esuna función en la tabla compartida global string . Un


mod malicioso podría anular esta función y pasar cosas a os.execute:
string.format = function()

return "xdg-open 'http://example.com'"

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.

Algunas reglas para usar un entorno inseguro:


 Guárdelo siempre en un local y nunca lo pase a una función.
 Asegúrese de que puede confiar en cualquier entrada
proporcionada a una función insegura, para evitar el problema
anterior. Esto significa evitar funciones redefinibles globalmente.

Introducción a arquitecturas limpias

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.

Este capítulo cubre conceptos importantes necesarios para mantener su


código limpio y patrones de diseño comunes para lograrlo. Tenga en
cuenta que este capítulo no está destinado a ser prescriptivo, sino a darle
una idea de las posibilidades. No existe una buena forma de diseñar un
mod, y un buen diseño de mod es muy subjetivo.

 Cohesión, acoplamiento y separación de preocupaciones


 Observador
 Modelo-Vista-Controlador
o Vista de API
 Conclusión

Cohesión, acoplamiento y separación de


preocupaciones
Sin ninguna planificación, un proyecto de programación tenderá a
descender gradualmente al código espagueti. El código espagueti se
caracteriza por una falta de estructura: todo el código se incluye sin límites
claros. Esto, en última instancia, hace que un proyecto sea
completamente insostenible y termina en su abandono.

Lo opuesto a esto es diseñar su proyecto como una colección de


programas o áreas de código más pequeños que interactúan.

Dentro de cada programa grande, hay un programa pequeño que intenta


salir.

–CAR Hoare

Esto debe hacerse de tal manera que logre la Separación de


preocupaciones: cada área debe ser distinta y abordar una necesidad o
inquietud por separado.

Estos programas / áreas deben tener las siguientes dos propiedades:

 Alta cohesión : el área debe estar estrechamente relacionada.


 Acoplamiento bajo : mantenga las dependencias entre áreas lo
más bajas posible y evite depender de implementaciones
internas. Es una muy buena idea asegurarse de tener una cantidad
baja de acoplamiento, ya que esto significa que cambiar las API de
ciertas áreas será más factible.

Tenga en cuenta que estos se aplican tanto al pensar en la relación entre


mods como en la relación entre áreas dentro de un mod.

Observador
Una forma sencilla de separar diferentes áreas de código es utilizar el
patrón Observer.

Tomemos el ejemplo de desbloquear un logro cuando un jugador mata por


primera vez a un animal raro. El enfoque ingenuo sería tener un código de
logro en la función de matar a la mafia, verificar el nombre de la mafia y
desbloquear el premio si coincide. Sin embargo, esta es una mala idea, ya
que hace que el mod de mobs se acople al código de logros. Si seguía
haciendo esto, por ejemplo, agregando XP al código de muerte de la
mafia, podría terminar con muchas dependencias desordenadas.
Ingrese el patrón Observer. En lugar de que el mod mymobs se preocupe
por los premios, el mod mymobs expone una forma para que otras áreas
del código registren su interés en un evento y reciban datos sobre el
evento.

mymobs.registered_on_death = {}

function mymobs.register_on_death(func)

table.insert(mymobs.registered_on_death, func)

end

-- in mob death code

for i=1, #mymobs.registered_on_death do

mymobs.registered_on_death[i](entity, reason)

end

Luego, el otro código registra su interés:

mymobs.register_on_death(function(mob, reason)

if reason.type == "punch" and reason.object and

reason.object:is_player() then

awards.notify_mob_kill(reason.object, mob.name)

end

end)

Puede estar pensando: espere un segundo, esto le resulta muy


familiar. ¡Y tienes razón! La API de Minetest se basa en gran medida en
Observer para evitar que el motor tenga que preocuparse por lo que está
escuchando algo.

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:

 Qué datos tienes.


 Qué acciones puede realizar con estos datos.
 Cómo los eventos (es decir, formpec, puñetazos, etc.)
desencadenan estas acciones y cómo estas acciones hacen que
sucedan cosas en el motor.

Tomemos un ejemplo de un mod de protección de la tierra. Los datos que


tiene son las áreas y los metadatos asociados. Acciones que puede tomar
son create , edit o delete . Los eventos que desencadenan estas acciones
son los comandos de chat y los campos de recepción de formpec. Estas
son 3 áreas que generalmente se pueden separar bastante bien.

En sus pruebas, podrá asegurarse de que una acción cuando se activa


hace lo correcto con los datos. No necesitará probar que un evento llama
a una acción (ya que esto requeriría el uso de la API Minetest, y esta área
de código debe hacerse lo más pequeña posible de todos modos).

Debe escribir su representación de datos usando Pure Lua. "Puro" en este


contexto significa que las funciones podrían ejecutarse fuera de Minetest;
no se llama a ninguna de las funciones del motor.

-- Data

function land.create(name, area_name)

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

function land.handle_create_submit(name, area_name)

-- process stuff

-- (ie: check for overlaps, check quotas, check permissions)

land.create(name, area_name)

end

function land.handle_creation_request(name)

-- This is a bad example, as explained later

land.show_create_formspec(name)

end

Sus controladores de eventos deberán interactuar con la API de


Minetest. Debe mantener el número de cálculos al mínimo, ya que no
podrá probar esta área con mucha facilidad.

-- View

function land.show_create_formspec(name)

-- Note how there's no complex calculations here!

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", {

privs = { land = true },

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)

El anterior es el patrón Modelo-Vista-Controlador. El modelo es una


colección de datos con funciones mínimas. La vista es una colección de
funciones que escuchan eventos y los pasan al controlador, y también
recibe llamadas del controlador para hacer algo con la API Minetest. El
controlador es donde se toman las decisiones y la mayoría de los
cálculos.

El controlador no debe tener conocimiento sobre la API de Minetest;


observe que no hay llamadas de Minetest ni funciones de vista que se
parezcan a ellas. NO debería tener una función
como view.hud_add(player, def) . En cambio, la vista define algunas
acciones que el controlador puede decirle a la vista que haga,
como view.add_hud(info) dónde la información es un valor o una tabla
que no se relaciona en absoluto con la API de Minetest.
Es importante que cada área solo se comunique con sus vecinos directos,
como se muestra arriba, para reducir cuánto necesita cambiar si modifica
las partes internas o externas de un área. Por ejemplo, para cambiar la
especificación de formulario, solo necesitaría editar la vista. Para cambiar
la API de vista, solo necesitaría cambiar la vista y el controlador, pero no
el modelo en absoluto.

En la práctica, este diseño rara vez se usa debido a la mayor complejidad


y porque no brinda muchos beneficios para la mayoría de los tipos de
modificaciones. En cambio, normalmente verá un tipo de diseño menos
formal y estricto: variantes de API-View.

Vista de API

En un mundo ideal, tendrías las 3 áreas anteriores perfectamente


separadas con todos los eventos yendo al controlador antes de volver a la
vista normal. Pero este no es el mundo real. Un buen compromiso es
reducir el mod en dos partes:

 API : este era el modelo y el controlador anteriores. No debería


haber ningún uso de minetest. aquí.
 Vista : esta también era la vista anterior. Es una buena idea
estructurar esto en archivos separados para cada tipo de evento.

El mod de elaboración de Rubenwardy sigue aproximadamente este


diseño. api.lua Casi todas las funciones puras de Lua manejan el
almacenamiento de datos y los cálculos de estilo controlador. gui.lua es la
vista para el envío de formpecs y formpec, y async_crafter.lua es la vista
y el controlador para un nodo formpec y temporizadores de nodo.

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.

rueba unitaria automática

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.

 Windows: siga las instrucciones de instalación en la wiki de


LuaRock .
 Debian / Ubuntu Linux: sudo apt install luarocks

A continuación, debe instalar Busted globalmente:


sudo luarocks install busted

Finalmente, verifique que esté instalado:

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

package.path = "../?.lua;" .. package.path

-- Set mymod global for API to write into


_G.mymod = {} --_

-- Run api.lua file

require("api")

-- Tests

describe("add", function()

it("adds", function()

assert.equals(2, mymod.add(1, 1))

end)

it("supports negatives", function()

assert.equals(0, mymod.add(-1, 1))

assert.equals(-2, mymod.add(-1, -1))

end)

end)

Ahora puede ejecutar las pruebas abriendo una terminal en el directorio


del mod y ejecutando busted .

Es importante que el archivo API no cree la tabla en sí, ya que los


globales en Busted funcionan de manera diferente. Cualquier variable que
sea global en Minetest es, en cambio, un archivo local en reventado. Esta
hubiera sido una mejor manera para que Minetest hiciera las cosas, pero
ahora es demasiado tarde para eso.

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.

Burlarse: usar funciones externas


Burlarse es la práctica de reemplazar funciones de las que depende la
cosa que estás probando. Esto puede tener dos propósitos; uno, es
posible que la función no esté disponible en el entorno de prueba, y dos,
es posible que desee capturar las llamadas a la función y los argumentos
pasados.
Si sigue los consejos del capítulo Arquitecturas limpias , ya tendrá un
archivo bastante limpio para probar. Sin embargo, aún tendrá que burlarse
de las cosas que no están en su área; por ejemplo, tendrá que burlarse de
la vista cuando pruebe el controlador / API. Si no siguió el consejo,
entonces las cosas son un poco más difíciles, ya que es posible que tenga
que burlarse de la API de Minetest.

-- As above, make a table

_G.minetest = {}

-- Define the mock function

local chat_send_all_calls = {}

function minetest.chat_send_all(name, message)

table.insert(chat_send_all_calls, { name = name, message =

message })

end

-- Tests

describe("list_areas", function()

it("returns a line for each area", function()

chat_send_all_calls = {} -- reset table

mymod.list_areas_to_chat("singleplayer", "singleplayer")

assert.equals(2, #chat_send_all_calls)

end)

it("sends to right player", function()

chat_send_all_calls = {} -- reset table

mymod.list_areas_to_chat("singleplayer", "singleplayer")

for _, call in pairs(chat_send_all_calls) do --_


assert.equals("singleplayer", call.name)

end

end)

-- The above two tests are actually pointless,

-- as this one tests both things

it("returns correct thing", function()

chat_send_all_calls = {} -- reset table

mymod.list_areas_to_chat("singleplayer", "singleplayer")

local expected = {

{ name = "singleplayer", message = "Town Hall

(2,43,63)" },

{ name = "singleplayer", message = "Airport

(43,45,63)" },

assert.same(expected, chat_send_all_calls)

end)

end)

Comprobación de confirmaciones con Travis


El script de Travis del capítulo Comprobación automática de errores se
puede modificar para ejecutar también Busted.

language: generic

sudo: false

addons:

apt:

packages:
- luarocks

before_install:

- luarocks install --local luacheck && luarocks install --local

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.

Para ver un ejemplo de un mod con muchas pruebas unitarias, consulte


la elaboración de rubenwardy .

Prueba unitaria automática

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.

 Windows: siga las instrucciones de instalación en la wiki de


LuaRock .
 Debian / Ubuntu Linux: sudo apt install luarocks

A continuación, debe instalar Busted globalmente:

sudo luarocks install busted

Finalmente, verifique que esté instalado:

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

package.path = "../?.lua;" .. package.path

-- Set mymod global for API to write into

_G.mymod = {} --_

-- Run api.lua file

require("api")

-- Tests

describe("add", function()

it("adds", function()

assert.equals(2, mymod.add(1, 1))

end)

it("supports negatives", function()

assert.equals(0, mymod.add(-1, 1))

assert.equals(-2, mymod.add(-1, -1))

end)

end)
Ahora puede ejecutar las pruebas abriendo una terminal en el directorio
del mod y ejecutando busted .

Es importante que el archivo API no cree la tabla en sí, ya que los


globales en Busted funcionan de manera diferente. Cualquier variable que
sea global en Minetest es, en cambio, un archivo local en reventado. Esta
hubiera sido una mejor manera para que Minetest hiciera las cosas, pero
ahora es demasiado tarde para eso.

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.

Burlarse: usar funciones externas


Burlarse es la práctica de reemplazar funciones de las que depende la
cosa que estás probando. Esto puede tener dos propósitos; uno, es
posible que la función no esté disponible en el entorno de prueba, y dos,
es posible que desee capturar las llamadas a la función y los argumentos
pasados.

Si sigue los consejos del capítulo Arquitecturas limpias , ya tendrá un


archivo bastante limpio para probar. Sin embargo, aún tendrá que burlarse
de las cosas que no están en su área; por ejemplo, tendrá que burlarse de
la vista cuando pruebe el controlador / API. Si no siguió el consejo,
entonces las cosas son un poco más difíciles, ya que es posible que tenga
que burlarse de la API de Minetest.

-- As above, make a table

_G.minetest = {}

-- Define the mock function

local chat_send_all_calls = {}

function minetest.chat_send_all(name, message)

table.insert(chat_send_all_calls, { name = name, message =

message })

end
-- Tests

describe("list_areas", function()

it("returns a line for each area", function()

chat_send_all_calls = {} -- reset table

mymod.list_areas_to_chat("singleplayer", "singleplayer")

assert.equals(2, #chat_send_all_calls)

end)

it("sends to right player", function()

chat_send_all_calls = {} -- reset table

mymod.list_areas_to_chat("singleplayer", "singleplayer")

for _, call in pairs(chat_send_all_calls) do --_

assert.equals("singleplayer", call.name)

end

end)

-- The above two tests are actually pointless,

-- as this one tests both things

it("returns correct thing", function()

chat_send_all_calls = {} -- reset table

mymod.list_areas_to_chat("singleplayer", "singleplayer")

local expected = {

{ name = "singleplayer", message = "Town Hall

(2,43,63)" },
{ name = "singleplayer", message = "Airport

(43,45,63)" },

assert.same(expected, chat_send_all_calls)

end)

end)

Comprobación de confirmaciones con Travis


El script de Travis del capítulo Comprobación automática de errores se
puede modificar para ejecutar también Busted.

language: generic

sudo: false

addons:

apt:

packages:

- luarocks

before_install:

- luarocks install --local luacheck && luarocks install --local

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.

Para ver un ejemplo de un mod con muchas pruebas unitarias, consulte


la elaboración de rubenwardy .

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.

 Elegir una licencia


o LGPL y CC-BY-SA
o CC0
o MIT
 embalaje
o README.txt
o mod.conf / game.conf
o screenshot.png
 Subiendo
o Sistemas de control de versiones
 Lanzamiento en ContentDB
 Tema del foro

Elegir una licencia


Necesita especificar una licencia para su mod. Esto es importante porque
le dice a otras personas las formas en las que se les permite usar su
trabajo. Si su mod no tiene una licencia, las personas no sabrán si pueden
modificar, distribuir o usar su mod en un servidor público.

Su código y su arte necesitan cosas diferentes a las licencias que


utilizan. Por ejemplo, las licencias Creative Commons no deben usarse
con el código fuente, pero pueden ser opciones adecuadas para trabajos
artísticos como imágenes, texto y mallas.

Se le permite cualquier licencia; sin embargo, las modificaciones que


rechazan los derivados están prohibidas en el foro oficial de
Minetest. (Para que se permita un mod en el foro, otros desarrolladores
deben poder modificarlo y lanzar la versión modificada).

Tenga en cuenta que el dominio público no es una licencia válida ,


porque la definición varía en los diferentes países.

Es importante tener en cuenta que se desaconseja encarecidamente


WTFPL y las personas pueden optar por no usar su mod si tiene esta
licencia.

LGPL y CC-BY-SA

Esta es una combinación de licencia común en la comunidad Minetest, y


es lo que utilizan Minetest y Minetest Game.

Usted licencia su código bajo LGPL 2.1 y su arte bajo CC-BY-SA.

Esto significa que:

 Cualquiera puede modificar, redistribuir y vender versiones


modificadas o no modificadas.
 Si alguien modifica su mod, debe darle a su versión la misma
licencia.
 Debe conservar su aviso de derechos de autor.

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

El archivo README debe indicar:

 Qué hace el mod / juego, cómo usarlo.


 Qué es la licencia.
 Opcionalmente:
o dónde informar problemas u obtener ayuda.
o creditos

mod.conf / game.conf

Asegúrate de agregar una clave de descripción para explicar lo que hace


tu mod o juego. Sea conciso sin ser vago. Debe ser breve porque se
mostrará en el instalador de contenido que tiene un espacio limitado.

Buen ejemplo:

description = Adds soup, cakes, bakes and juices.

Evite esto:

description = The food mod for Minetest. (<-- BAD! It's vague)

screenshot.png

Las capturas de pantalla deben ser de 3: 2 (3 píxeles de ancho por cada 2


píxeles de alto) y tener un tamaño mínimo de 300 x 200 px.
La captura de pantalla se muestra dentro de Minetest como una miniatura
del contenido.

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:

 Estable : es poco probable que el sitio web de alojamiento se cierre


sin previo aviso.
 Enlace directo : debería poder hacer clic en un enlace y descargar
el archivo sin tener que ver otra página.
 Libre de virus : los hosts de carga fraudulentos pueden contener
anuncios inseguros.

ContentDB le permite cargar archivos zip y cumple con estos criterios.

Sistemas de control de versiones

Un sistema de control de versiones (VCS) es un software que administra


los cambios en el software, lo que a menudo facilita la distribución y la
recepción de los cambios aportados.

La mayoría de los modders de Minetest usan Git y un sitio web como


GitHub para distribuir su código.

Usar git puede ser difícil al principio. Si necesita ayuda con esto, consulte:

 Libro Pro Git : lectura gratuita en línea.


 Aplicación GitHub para Windows : use una interfaz gráfica en
Windows para cargar su código.

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.

Regístrese en ContentDB y agregue su contenido. Asegúrese de leer la


guía proporcionada en la sección de Ayuda.

Tema del foro


También puede crear un tema de foro para que los usuarios puedan
discutir su creación.

Los temas de mod deben crearse en el foro "WIP Mods" (Trabajo en


progreso) y los temas de juego en el foro "WIP Games" . Cuando ya no
considere que su mod es un trabajo en progreso, puede solicitar que se
mueva a "Lanzamientos de mod".

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.

El tema del tema debe estar en uno de estos formatos:

 [Mod] Título del mod [modname]


 [Mod] Título del mod [número de versión] [nombre del mod]

Por ejemplo:

 [Mod] Más Blox [0.1] [moreblox]

You might also like