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]