WIP: Перевод на русский #1
@ -14,3 +14,5 @@ collections:
|
|||||||
output: true
|
output: true
|
||||||
it:
|
it:
|
||||||
output: true
|
output: true
|
||||||
|
ru:
|
||||||
|
output: true
|
||||||
|
@ -7,3 +7,7 @@
|
|||||||
- code: it
|
- code: it
|
||||||
name: Italiano
|
name: Italiano
|
||||||
cta: Questo libro è disponibile in italiano
|
cta: Questo libro è disponibile in italiano
|
||||||
|
|
||||||
|
- code: ru
|
||||||
|
name: Russian
|
||||||
|
cta: Эта кника доступна на русском
|
||||||
|
@ -9,6 +9,9 @@ layout: base
|
|||||||
{% if language == "_it" %}
|
{% if language == "_it" %}
|
||||||
{% assign language = "it" %}
|
{% assign language = "it" %}
|
||||||
{% assign links = site.it %}
|
{% assign links = site.it %}
|
||||||
|
{% elif language == "_ru" %}
|
||||||
|
{% assign language = "ru" %}
|
||||||
|
{% assign links = site.ru %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% assign language = "en" %}
|
{% assign language = "en" %}
|
||||||
{% assign links = site.en %}
|
{% assign links = site.en %}
|
||||||
|
243
_ru/advmap/biomesdeco.md
Normal file
243
_ru/advmap/biomesdeco.md
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
---
|
||||||
|
title: Biomes and Decorations
|
||||||
|
author: Shara
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 6.1
|
||||||
|
description: Create biomes and decorations to customise the map
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
The ability to register biomes and decorations is vital when aiming to create an
|
||||||
|
interesting and varied in-game environment. This chapter teaches you how to
|
||||||
|
register biomes, how to control biome distribution, and how to place decorations in biomes.
|
||||||
|
|
||||||
|
- [What are Biomes?](#what-are-biomes)
|
||||||
|
- [Biome Placement](#biome-placement)
|
||||||
|
- [Heat and Humidity](#heat-and-humidity)
|
||||||
|
- [Visualising Boundaries using Voronoi Diagrams](#visualising-boundaries-using-voronoi-diagrams)
|
||||||
|
- [Creating a Voronoi Diagram using Geogebra](#creating-a-voronoi-diagram-using-geogebra)
|
||||||
|
- [Registering a Biome](#registering-a-biome)
|
||||||
|
- [What are Decorations?](#what-are-decorations)
|
||||||
|
- [Registering a Simple Decoration](#registering-a-simple-decoration)
|
||||||
|
- [Registering a Schematic Decoration](#registering-a-schematic-decoration)
|
||||||
|
- [Mapgen Aliases](#mapgen-aliases)
|
||||||
|
|
||||||
|
## What are Biomes?
|
||||||
|
|
||||||
|
A Minetest biome is a specific in-game environment. When registering biomes, you
|
||||||
|
can determine the types of nodes that appear in them during map generation.
|
||||||
|
Some of the most common types of node that may vary between biomes include:
|
||||||
|
|
||||||
|
* Top node: This is the node most commonly found on the surface. A well-known
|
||||||
|
example would be "Dirt with Grass" from Minetest Game.
|
||||||
|
* Filler node: This is the layer immediately beneath the top node.
|
||||||
|
In biomes with grass, it will often be dirt.
|
||||||
|
* Stone node: This is the node you most commonly see underground.
|
||||||
|
* Water node: This is usually a liquid and will be the node that appears
|
||||||
|
where you would expect bodies of water.
|
||||||
|
|
||||||
|
Other types of node can also vary between biomes, providing an opportunity
|
||||||
|
to create vastly different environments within the same game.
|
||||||
|
|
||||||
|
## Biome Placement
|
||||||
|
|
||||||
|
### Heat and Humidity
|
||||||
|
|
||||||
|
It is not enough to simply register a biome; you must also decide where it can
|
||||||
|
occur in game. This is done by assigning a heat and a humidity value to each biome.
|
||||||
|
|
||||||
|
You should think carefully about these values; they determine which biomes can
|
||||||
|
be neighbours to each other. Poor decisions could result in what is meant to
|
||||||
|
be a hot desert sharing a border with a glacier, and other improbable
|
||||||
|
combinations which you may prefer to avoid.
|
||||||
|
|
||||||
|
In game, heat and humidity values at any point of the map will usually be between
|
||||||
|
0 and 100. The values gradually change, increasing or decreasing as you move
|
||||||
|
around the map. The biome at any given point will be determined by which of the
|
||||||
|
registered biomes has heat and humidity values closest to those at that position on the map.
|
||||||
|
|
||||||
|
Because the changes in heat and humidity are gradual, it is good practice to assign
|
||||||
|
heat and humidity values to biomes based on reasonable expectations about that
|
||||||
|
biome’s environment. For example:
|
||||||
|
|
||||||
|
* A desert might have high heat and low humidity.
|
||||||
|
* A snowy forest might have low heat and a medium humidity value.
|
||||||
|
* A swamp biome would generally have high humidity.
|
||||||
|
*
|
||||||
|
In practice, this means that, as long as you have a diverse range of biomes, you
|
||||||
|
are likely to find that the biomes which border each other form a logical progression.
|
||||||
|
|
||||||
|
### Visualising Boundaries using Voronoi Diagrams
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img src="{{ page.root }}/static/biomes_voronoi.png" alt="Vernoi">
|
||||||
|
<figcaption>
|
||||||
|
Voronoi diagram, showing the closest point.
|
||||||
|
<span class="credit">By <a href="https://en.wikipedia.org/wiki/Voronoi_diagram#/media/File:Euclidean_Voronoi_diagram.svg">Balu Ertl</a>, CC BY-SA 4.0.</span>
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
Fine-tuning heat and humidity values for biomes is
|
||||||
|
easier if you can visualise the relationship between the biomes you are using.
|
||||||
|
This is most important if you are creating a full set of your own biomes, but
|
||||||
|
can also be helpful if you are adding a biome to an existing set.
|
||||||
|
|
||||||
|
The simplest way to visualise which biomes may share borders is to create a
|
||||||
|
Voronoi diagram, which can be used to show which point on a 2-dimensional
|
||||||
|
diagram any given position is closest to.
|
||||||
|
|
||||||
|
A Voronoi diagram can reveal where biomes that should border each other do not,
|
||||||
|
and where biomes that should not border each other do. It can also give a
|
||||||
|
general insight into how common biomes will be in-game, with larger and more
|
||||||
|
central biomes being more common than smaller biomes or biomes that are located
|
||||||
|
on the outer edge of the diagram.
|
||||||
|
|
||||||
|
This is done by marking a point for each biome based on heat and humidity values,
|
||||||
|
where the x-axis is heat and the y-axis is humidity. The diagram is then
|
||||||
|
divided into areas, such that every position in a given area is closer to the
|
||||||
|
point inside that area than it is to any other point on the diagram.
|
||||||
|
|
||||||
|
Each area represents a biome. If two areas share a border, the biomes they
|
||||||
|
represent in-game can be located next to each other. The length of the border
|
||||||
|
shared between two areas, compared to the length shared with other areas, will
|
||||||
|
tell you how frequently two biomes are likely to be found next to each other.
|
||||||
|
|
||||||
|
### Creating a Voronoi Diagram using Geogebra
|
||||||
|
|
||||||
|
As well as drawing them by hand, you can also create Voronoi diagrams using
|
||||||
|
programs such as [Geogebra](https://www.geogebra.org).
|
||||||
|
|
||||||
|
1. Create points by selecting the point tool in the toolbar (icon is a point with 'A'),
|
||||||
|
and then clicking the chart. You can drag points around or explicitly set their
|
||||||
|
position in the left sidebar. You should also give each point a label, to make things clearer.
|
||||||
|
|
||||||
|
1. Next, create the voronoi by entering the following function into the
|
||||||
|
input box in the left sidebar:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
Voronoi({ A, B, C, D, E })
|
||||||
|
```
|
||||||
|
|
||||||
|
Where the each point is inside the curly brackets, separated by commas. You should now
|
||||||
|
|
||||||
|
3. Profit! You should now have a voronoi diagram with all draggable points.
|
||||||
|
|
||||||
|
|
||||||
|
## Registering a Biome
|
||||||
|
|
||||||
|
The following code registers a simple biome named grasslands biome:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.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,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
This biome has one layer of Dirt with Grass nodes on the surface, and three layers
|
||||||
|
of Dirt nodes beneath this. It does not specify a stone node, so the node defined
|
||||||
|
in the mapgen alias registration for `mapgen_stone` will be present underneath the dirt.
|
||||||
|
|
||||||
|
There are many options when registering a biome, and these are documented
|
||||||
|
in the [Minetest Lua API Reference](https://minetest.gitlab.io/minetest/definition-tables/#biome-definition),
|
||||||
|
as always.
|
||||||
|
|
||||||
|
You don’t need to define every option for every biome you create, but in some cases failure
|
||||||
|
to define either a specific option, or a suitable mapgen alias, can result in map generation errors.
|
||||||
|
|
||||||
|
## What are Decorations?
|
||||||
|
|
||||||
|
Decorations are either nodes or schematics that can be placed on the map at mapgen.
|
||||||
|
Some common examples include flowers, bushes, and trees. Other more creative uses
|
||||||
|
may include hanging icicles or stalagmites in caves, underground crystal formations,
|
||||||
|
or even the placement of small buildings.
|
||||||
|
|
||||||
|
Decorations can be restricted to specific biomes, by height, or by which nodes
|
||||||
|
they can be placed on. They are often used to develop the environment of a biome
|
||||||
|
by ensuring it has specific plants, trees or other features.
|
||||||
|
|
||||||
|
## Registering a Simple Decoration
|
||||||
|
|
||||||
|
Simple decorations are used to place single node decorations on the map during
|
||||||
|
map generation. You must specify the node that is to be placed as a decoration,
|
||||||
|
details for where it can be placed, and how frequently it occurs.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.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",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, the node named `plants:grass` will be placed in the biome named
|
||||||
|
grassy_plains on top of `base:dirt_with_grass` nodes, between the heights of `y = 1` and `y = 200`.
|
||||||
|
|
||||||
|
The fill_ratio value determines how frequently the decoration appears, with higher
|
||||||
|
values up to 1 resulting in a great number of decorations being placed. It is possible
|
||||||
|
to instead use noise parameters to determine placement.
|
||||||
|
|
||||||
|
## Registering a Schematic Decoration
|
||||||
|
|
||||||
|
Schematic decorations are very similar to simple decoration, but involve the placement
|
||||||
|
of a schematic instead of the placement of a single node. For example:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.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 = core.get_modpath("plants") .. "/schematics/cactus.mts",
|
||||||
|
flags = "place_center_x, place_center_z",
|
||||||
|
rotation = "random",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example the cactus.mts schematic is placed in desert biomes. You need to provide
|
||||||
|
a path to a schematic, which in this case is stored in a dedicated schematic directory within the mod.
|
||||||
|
|
||||||
|
This example also sets flags to center the placement of the schematic, and the rotation
|
||||||
|
is set to random. The random rotation of schematics when they are placed as decorations
|
||||||
|
helps introduce more variation when asymmetrical schematics are used.
|
||||||
|
|
||||||
|
|
||||||
|
## Mapgen Aliases
|
||||||
|
|
||||||
|
Existing games should already include suitable mapgen aliases, so you only need
|
||||||
|
to consider registering mapgen aliases of your own if you are making your own game.
|
||||||
|
|
||||||
|
Mapgen aliases provide information to the core mapgen, and can be registered in the form:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_alias("mapgen_stone", "base:smoke_stone")
|
||||||
|
```
|
||||||
|
|
||||||
|
At a minimum you should register:
|
||||||
|
|
||||||
|
* mapgen_stone
|
||||||
|
* mapgen_water_source
|
||||||
|
* mapgen_river_water_source
|
||||||
|
|
||||||
|
If you are not defining cave liquid nodes for all biomes, you should also register:
|
||||||
|
|
||||||
|
* mapgen_lava_source
|
211
_ru/advmap/lvm.md
Normal file
211
_ru/advmap/lvm.md
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
---
|
||||||
|
title: Lua Voxel Manipulators
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 6.2
|
||||||
|
description: Learn how to use LVMs to speed up map operations.
|
||||||
|
redirect_from:
|
||||||
|
- /en/chapters/lvm.html
|
||||||
|
- /en/map/lvm.html
|
||||||
|
mapgen_object:
|
||||||
|
level: warning
|
||||||
|
title: LVMs and Mapgen
|
||||||
|
message: Don't use `core.get_voxel_manip()` with mapgen, as it can cause glitches.
|
||||||
|
Use `core.get_mapgen_object("voxelmanip")` instead.
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
The functions outlined in the [Basic Map Operations](../map/environment.html) chapter
|
||||||
|
are convenient and easy to use, but for large areas they are inefficient.
|
||||||
|
Every time you call `set_node` or `get_node`, your mod needs to communicate with
|
||||||
|
the engine. This results in constant individual copying operations between the
|
||||||
|
engine and your mod, which is slow and will quickly decrease the performance of
|
||||||
|
your game. Using a Lua Voxel Manipulator (LVM) can be a better alternative.
|
||||||
|
|
||||||
|
- [Concepts](#concepts)
|
||||||
|
- [Reading into the LVM](#reading-into-the-lvm)
|
||||||
|
- [Reading Nodes](#reading-nodes)
|
||||||
|
- [Writing Nodes](#writing-nodes)
|
||||||
|
- [Example](#example)
|
||||||
|
- [Your Turn](#your-turn)
|
||||||
|
|
||||||
|
## Concepts
|
||||||
|
|
||||||
|
An LVM allows you to load large areas of the map into your mod's memory.
|
||||||
|
You can then read and write this data without further interaction with the
|
||||||
|
engine and without running any callbacks, which means that these
|
||||||
|
operations are very fast. Once done, you can then write the area back into
|
||||||
|
the engine and run any lighting calculations.
|
||||||
|
|
||||||
|
## Reading into the LVM
|
||||||
|
|
||||||
|
You can only load a cubic area into an LVM, so you need to work out the minimum
|
||||||
|
and maximum positions that you need to modify. Then you can create and read into
|
||||||
|
an LVM. For example:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local vm = core.get_voxel_manip()
|
||||||
|
local emin, emax = vm:read_from_map(pos1, pos2)
|
||||||
|
```
|
||||||
|
|
||||||
|
For performance reasons, an LVM will almost never read the exact area you tell it to.
|
||||||
|
Instead, it will likely read a larger area. The larger area is given by `emin` and `emax`,
|
||||||
|
which stand for *emerged min pos* and *emerged max pos*. An LVM will load the area
|
||||||
|
it contains for you - whether that involves loading from memory, from disk, or
|
||||||
|
calling the map generator.
|
||||||
|
|
||||||
|
{% include notice.html notice=page.mapgen_object %}
|
||||||
|
|
||||||
|
## Reading Nodes
|
||||||
|
|
||||||
|
To read the types of nodes at particular positions, you'll need to use `get_data()`.
|
||||||
|
This returns a flat array where each entry represents the type of a
|
||||||
|
particular node.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local data = vm:get_data()
|
||||||
|
```
|
||||||
|
|
||||||
|
You can get param2 and lighting data using the methods `get_light_data()` and `get_param2_data()`.
|
||||||
|
|
||||||
|
You'll need to use `emin` and `emax` to work out where a node is in the flat arrays
|
||||||
|
given by the above methods. There's a helper class called `VoxelArea` which handles
|
||||||
|
the calculation for you.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local a = VoxelArea:new{
|
||||||
|
MinEdge = emin,
|
||||||
|
MaxEdge = emax
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Get node's index
|
||||||
|
local idx = a:index(x, y, z)
|
||||||
|
|
||||||
|
-- Read node
|
||||||
|
print(data[idx])
|
||||||
|
```
|
||||||
|
|
||||||
|
When you run this, you'll notice that `data[vi]` is an integer. This is because
|
||||||
|
the engine doesn't store nodes using strings, for performance reasons.
|
||||||
|
Instead, the engine uses an integer called a content ID.
|
||||||
|
You can find out the content ID for a particular type of node with
|
||||||
|
`get_content_id()`. For example:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local c_stone = core.get_content_id("default:stone")
|
||||||
|
```
|
||||||
|
|
||||||
|
You can then check whether the node is stone:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local idx = a:index(x, y, z)
|
||||||
|
if data[idx] == c_stone then
|
||||||
|
print("is stone!")
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Content IDs of a node type may change during load time, so it is recommended that
|
||||||
|
you don't try getting them during this time.
|
||||||
|
|
||||||
|
Nodes in an LVM data array are stored in reverse co-ordinate order, so you should
|
||||||
|
always iterate in the order `z, y, x`. For example:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
The reason for this touches on the topic of computer architecture. Reading from RAM is rather
|
||||||
|
costly, so CPUs have multiple levels of caching. If the data that a process requests
|
||||||
|
is in the cache, it can very quickly retrieve it. If the data is not in the cache,
|
||||||
|
then a cache miss occurs and it will fetch the data it needs from RAM. Any data
|
||||||
|
surrounding the requested data is also fetched and then replaces the data in the cache. This is
|
||||||
|
because it's quite likely that the process will ask for data near that location again. This means
|
||||||
|
a good rule of optimisation is to iterate in a way that you read data one after
|
||||||
|
another, and avoid *cache thrashing*.
|
||||||
|
|
||||||
|
## Writing Nodes
|
||||||
|
|
||||||
|
First, you need to set the new content ID in the data array:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
When you finish setting nodes in the LVM, you then need to upload the data
|
||||||
|
array to the engine:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
vm:set_data(data)
|
||||||
|
vm:write_to_map(true)
|
||||||
|
```
|
||||||
|
|
||||||
|
For setting lighting and param2 data, use the appropriately named
|
||||||
|
`set_light_data()` and `set_param2_data()` methods.
|
||||||
|
|
||||||
|
`write_to_map()` takes a Boolean which is true if you want lighting to be
|
||||||
|
calculated. If you pass false, you need to recalculate lighting at a future
|
||||||
|
time using `core.fix_light`.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local function grass_to_dirt(pos1, pos2)
|
||||||
|
local c_dirt = core.get_content_id("default:dirt")
|
||||||
|
local c_grass = core.get_content_id("default:dirt_with_grass")
|
||||||
|
|
||||||
|
-- Read data into LVM
|
||||||
|
local vm = core.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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Your Turn
|
||||||
|
|
||||||
|
* Create `replace_in_area(from, to, pos1, pos2)`, which replaces all instances of
|
||||||
|
`from` with `to` in the area given, where `from` and `to` are node names.
|
||||||
|
* Make a function which rotates all chest nodes by 90°.
|
||||||
|
* Make a function which uses an LVM to cause mossy cobble to spread to nearby
|
||||||
|
stone and cobble nodes.
|
||||||
|
Does your implementation cause mossy cobble to spread more than a distance of one node each
|
||||||
|
time? If so, how could you stop this?
|
182
_ru/basics/getting_started.md
Normal file
182
_ru/basics/getting_started.md
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
---
|
||||||
|
title: Основы
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 1.1
|
||||||
|
description: Как создать проект мода, включая init.lua, mod.conf и прочее.
|
||||||
|
redirect_from:
|
||||||
|
- /ru/chapters/folders.html
|
||||||
|
- /ru/basics/folders.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Введение <!-- omit in toc -->
|
||||||
|
|
||||||
|
При создании модов очень важно понимать структуру проекта мода. В этой главе
|
||||||
|
вы узнаете как работает моддинг в Minetest и создадите свой первый мод.
|
||||||
|
|
||||||
|
- [Что такое Игры и что такое Моды?](#what-are-games-and-mods)
|
||||||
|
- [Где хранятся моды?](#where-are-mods-stored)
|
||||||
|
- [Создание первого мода](#creating-your-first-mod)
|
||||||
|
- [Директория](#mod-directory)
|
||||||
|
- [mod.conf](#modconf)
|
||||||
|
- [init.lua](#initlua)
|
||||||
|
- [Итого](#summary)
|
||||||
|
- [Зависимости](#dependencies)
|
||||||
|
- [Модпаки](#mod-packs)
|
||||||
|
|
||||||
|
|
||||||
|
## Что такое Игры и что такое Моды?
|
||||||
|
|
||||||
|
Сила Minetest — в возможности легко создавать игры без необходимости создания
|
||||||
|
собственной воксельной графики, воксельных алгоритмов или крутого сетевого
|
||||||
|
кода.
|
||||||
|
|
||||||
|
В Minetest Игра — это набор модулей, работающих вместе и создающих контент и
|
||||||
|
игровую механику.
|
||||||
|
Модуль (или мод) — это набор скриптов и материалов.
|
||||||
|
Конечно, можно создать игру только из одного мода, но такое встречается редко,
|
||||||
|
ведь так труднее настраивать и заменять отдельные элементы игры, не затрагивая
|
||||||
|
всё остальное.
|
||||||
|
|
||||||
|
Кроме того, вы можете распространять свой мод без привязки к конкретной игре.
|
||||||
|
В этом случае он также будет называться *модом* в традиционном смысле —
|
||||||
|
модификация. Такие моды изменяют или добавляют различные элементы игры.
|
||||||
|
|
||||||
|
Как моды в составе игры, так и просто сторонние моды используют один и тот же
|
||||||
|
API.
|
||||||
|
|
||||||
|
В этой книге описаны основы Minetest API, и она подходит как для разработчиков
|
||||||
|
игр, так и для моддеров.
|
||||||
|
|
||||||
|
|
||||||
|
## Где хранятся моды?
|
||||||
|
|
||||||
|
<a name="mod-locations"></a>
|
||||||
|
|
||||||
|
У каждого мода есть своя директория, в которой лежат Lua-код, текстуры,
|
||||||
|
модели и звуки. Minetest ищет моды в нескольких определённых папках.
|
||||||
|
В документации эти папки обозначаются как *mod load paths*.
|
||||||
|
|
||||||
|
Для каждой созданной карты или сохранённой игры Minetest ищет моды в трёх
|
||||||
|
местах в следующем порядке:
|
||||||
|
|
||||||
|
1. Моды Игры: Эти моды являются неотъемлимой частью Игры.
|
||||||
|
Папки: `minetest/games/minetest_game/mods/`, `/usr/share/minetest/games/minetest/`
|
||||||
|
2. Глобальные моды: место куда почти всегда устанавливаются моды.
|
||||||
|
Если не уверены, кладите их сюда.
|
||||||
|
Папка: `minetest/mods/`
|
||||||
|
3. Моды Мира: эти моды относятся к конкретной созданной карте.
|
||||||
|
Папка: `minetest/worlds/world/worldmods/`
|
||||||
|
|
||||||
|
`minetest` — это директория для пользовательских фалов. Чтобы её найти,
|
||||||
|
запустите Minetest и нажмите "Папка данных пользователя" на вкладке
|
||||||
|
"Подробней".
|
||||||
|
|
||||||
|
Во время загрузки модов Minetest проверяет по порядку каждое из перечисленных
|
||||||
|
расположений. Если он обнаружит несколько модов с одинаковым именем, то мод,
|
||||||
|
найденный позже, заменит собой предыдущий. Таким образом можно подменять моды
|
||||||
|
игры, помещая мод с таким же названием в папку Глобальных модов.
|
||||||
|
|
||||||
|
|
||||||
|
## Создание первого мода
|
||||||
|
|
||||||
|
### Директория
|
||||||
|
|
||||||
|
Перейдите в глобальную папку модов (Подробней > Папка данных пользователя > mods)
|
||||||
|
и создайте новую папку под названием "mymod". `mymod` — это имя мода.
|
||||||
|
|
||||||
|
У каждого мода должно быть уникальное *имя*, технический идентификатор (id),
|
||||||
|
по которому можно ссылаться на этот мод. Имя мода может состоять из букв, цифр
|
||||||
|
и подчёркиваний. Хорошее имя даёт представление о том, что делает мод.
|
||||||
|
Директория, в которой лежат компоненты мода, должна иметь такое же имя.
|
||||||
|
Моды можно искать по их имени на сайте [content.minetest.net](https://content.minetest.net).
|
||||||
|
|
||||||
|
mymod
|
||||||
|
├── textures
|
||||||
|
│ └── mymod_node.png
|
||||||
|
├── init.lua
|
||||||
|
└── mod.conf
|
||||||
|
|
||||||
|
Обязательным является только файл init.lua. Тем не менее, рекомендуется
|
||||||
|
добавлять файл mod.conf и другие компоненты в зависимости от функционала мода.
|
||||||
|
|
||||||
|
### mod.conf
|
||||||
|
|
||||||
|
Создайте файл mod.conf следующего содержания:
|
||||||
|
|
||||||
|
```
|
||||||
|
name = mymod
|
||||||
|
description = Adds foo, bar, and bo.
|
||||||
|
depends = default
|
||||||
|
```
|
||||||
|
|
||||||
|
Этот файл содержит мета-данные мода, такие как имя, описание и прочее.
|
||||||
|
|
||||||
|
### init.lua
|
||||||
|
|
||||||
|
Создайте файл init.lua следующего содержания:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
print("This file will be run at load time!")
|
||||||
|
|
||||||
|
core.register_node("mymod:node", {
|
||||||
|
description = "This is a node",
|
||||||
|
tiles = {"mymod_node.png"},
|
||||||
|
groups = {cracky = 1}
|
||||||
|
})
|
||||||
|
|
||||||
|
core.register_craft({
|
||||||
|
type = "shapeless",
|
||||||
|
output = "mymod:node 3",
|
||||||
|
recipe = { "default:dirt", "default:stone" },
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Файл init.lua запускается при загрузке мода.
|
||||||
|
|
||||||
|
|
||||||
|
### Итого
|
||||||
|
|
||||||
|
|
||||||
|
Этот мод носит имя "mymod". Он содержит два текстовых файла: init.lua
|
||||||
|
и mod.conf. Скрипт выводит сообщение, а затем регистрирует новый рецепт крафта
|
||||||
|
(об этом будет написано дальше). У этого мода указана единственная зависимость
|
||||||
|
от [мода default](https://content.minetest.net/metapackages/default/), который,
|
||||||
|
обычно, является частью Minetest Game. Также есть текстура для блока в папке
|
||||||
|
textures/.
|
||||||
|
|
||||||
|
|
||||||
|
## Зависимости
|
||||||
|
|
||||||
|
Зависимости указываются, если перед загрузкой нода необходимо загрузить другой
|
||||||
|
мод. Моду могут потребоваться код, предметы или ещё что-нибудь из другого мода.
|
||||||
|
|
||||||
|
Существует два типа зависимостей: жёсткие и необязательные.
|
||||||
|
В обоих случаях требуемый мод загружается первым. Если требуемый мод
|
||||||
|
недоступен, жёсткая зависимость не позволит загрузить мод, в то время как
|
||||||
|
при необязательной зависимости мод всего лишь потеряет часть функций.
|
||||||
|
|
||||||
|
Необязательные зависимости полезны в случаях, когда вы хотите добавить
|
||||||
|
поддержку другого мода: например, вы можете добавлять дополнительный контент,
|
||||||
|
если пользователь решит задействовать оба мода одновременно.
|
||||||
|
|
||||||
|
Несколько зависимостей перечисляются через запятую в файле mod.conf.
|
||||||
|
|
||||||
|
depends = modone, modtwo
|
||||||
|
optional_depends = modthree
|
||||||
|
|
||||||
|
## Модпаки
|
||||||
|
|
||||||
|
Несколько модов могут быть собраны в модпак, который позволяет распространять
|
||||||
|
эти моды вместе. Модпак позволяет загрузить и установить несколько модов
|
||||||
|
за раз, без необходимости скачивать их по одному.
|
||||||
|
|
||||||
|
modpack1
|
||||||
|
├── modpack.conf (обязательный) — обозначает, что это модпак
|
||||||
|
├── mod1
|
||||||
|
│ └── ... файлы мода
|
||||||
|
└── mymod (optional)
|
||||||
|
└── ... файлы мода
|
||||||
|
|
||||||
|
Не нужно путать модпак и *Игру*.
|
||||||
|
У Игры есть собственная структура, которая будет описана в главе Игры.
|
200
_ru/basics/lua.md
Normal file
200
_ru/basics/lua.md
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
---
|
||||||
|
title: Пишем на Lua
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 1.2
|
||||||
|
description: Введение в Lua
|
||||||
|
redirect_from: /ru/chapters/lua.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Введение <!-- omit in toc -->
|
||||||
|
|
||||||
|
В этой главе вы узнаете, как писать на Lua и какие для этого нужны инструменты.
|
||||||
|
Также вы познакомитесь с некоторыми полезными техниками.
|
||||||
|
|
||||||
|
- [Программирование](#programming)
|
||||||
|
- [Пишем на Lua](#coding-in-lua)
|
||||||
|
- [Редактор](#code-editors)
|
||||||
|
- [Области видимости](#local-and-global-scope)
|
||||||
|
- [Используйте локальные переменные везде](#locals-should-be-used-as-much-as-possible)
|
||||||
|
- [Подключение других скриптов](#including-other-lua-scripts)
|
||||||
|
|
||||||
|
|
||||||
|
## Программирование
|
||||||
|
|
||||||
|
Программирование — это когда вы берёте задачу (например, сортировка списка)
|
||||||
|
и превращаете её в простые шаги, который компьютер сможет выполнить.
|
||||||
|
Обучение вас программированию не является целью этой книги. Тем не менее,
|
||||||
|
эти сайты могут оказаться для вас полезными:
|
||||||
|
|
||||||
|
* [Codecademy](http://www.codecademy.com/) один из лучших ресурсов для обучения
|
||||||
|
программированию. Здесь есть интерактивные обучающие примеры.
|
||||||
|
* [Scratch](https://scratch.mit.edu) — хороший ресурс для того, чтобы начать
|
||||||
|
с самых основ и научиться технике решения задач в программировании.
|
||||||
|
Он отлично подойдёт детям и подросткам.
|
||||||
|
* [Programming with Mosh](https://www.youtube.com/user/programmingwithmosh) —
|
||||||
|
прекрасный YouTube-канал по обучению программированию.
|
||||||
|
|
||||||
|
### Пишем на Lua
|
||||||
|
|
||||||
|
Обучение вас программированию на Lua также не является целью этой книги.
|
||||||
|
Рекомендую книгу [Programming in Lua (PiL)](https://www.lua.org/pil/contents.html) —
|
||||||
|
она прекрасно подойдёт для входа в Lua.
|
||||||
|
|
||||||
|
|
||||||
|
## Редактор
|
||||||
|
|
||||||
|
Текстовый редактор с подсветкой синтаксиса очень полезен при написании на Lua.
|
||||||
|
Слова и символы выделяются разными цветами в зависимости от их назначения. Это
|
||||||
|
облегчает поиск ошибок и несоответствий.
|
||||||
|
|
||||||
|
Например:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
В этом примере подсвечены ключевые слова, такие как: `if`, `then`, `end`
|
||||||
|
и `return`. Стандартные фукнции Lua, такие как `table.insert`, также будут
|
||||||
|
подсвечены.
|
||||||
|
|
||||||
|
Широко используемые редакторы для Lua:
|
||||||
|
|
||||||
|
* [VSCode](https://code.visualstudio.com/):
|
||||||
|
открытй (как Code-OSS или VSCodium), популярный и к нему есть
|
||||||
|
[плагин для Minetest](https://marketplace.visualstudio.com/items?itemName=GreenXenith.minetest-tools).
|
||||||
|
* [Notepad++](http://notepad-plus-plus.org/): простой, только для Windows.
|
||||||
|
|
||||||
|
Есть и множество других подходящих редакторов.
|
||||||
|
|
||||||
|
|
||||||
|
## Области видимости
|
||||||
|
|
||||||
|
Переменные могут быть локальными и глобальными. К глобальным переменным можно
|
||||||
|
обращаться из любого места в скрипте и даже из другого мода.
|
||||||
|
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function one()
|
||||||
|
foo = "bar"
|
||||||
|
end
|
||||||
|
|
||||||
|
function two()
|
||||||
|
print(dump(foo)) -- Выведет сообщение: "bar"
|
||||||
|
end
|
||||||
|
|
||||||
|
one()
|
||||||
|
two()
|
||||||
|
```
|
||||||
|
|
||||||
|
Локальные же переменные доступны только там, где они объявлены.
|
||||||
|
По умолчанию, все переменные в Lua глобальные, и вам необходимо явно помечать
|
||||||
|
локальные переменные словом `local`.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Доступна в любом месте этого файла
|
||||||
|
local one = 1
|
||||||
|
|
||||||
|
function myfunc()
|
||||||
|
-- Доступна только внутри этой функции
|
||||||
|
local two = one + one
|
||||||
|
|
||||||
|
if two == one then
|
||||||
|
-- Доступна только внутри условного оператора
|
||||||
|
local three = one + two
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Используйте локальные переменные везде
|
||||||
|
|
||||||
|
Старайтесь использовать локальные переменные везде, где только можно.
|
||||||
|
Мод должен создавать только одну глобальную переменную с таким же именем, как
|
||||||
|
и у мода. Создание любых других глобальных переменных считается плохим тоном,
|
||||||
|
и Minetest будет предупреждать об этом сообщением:
|
||||||
|
|
||||||
|
Assignment to undeclared global 'foo' inside function at init.lua:2
|
||||||
|
|
||||||
|
Чтобы исправить это, используйте ключевое слово "local":
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function one()
|
||||||
|
local foo = "bar"
|
||||||
|
end
|
||||||
|
|
||||||
|
function two()
|
||||||
|
print(dump(foo)) -- Выведет сообщение: nil
|
||||||
|
end
|
||||||
|
|
||||||
|
one()
|
||||||
|
two()
|
||||||
|
```
|
||||||
|
|
||||||
|
Запомните, что "nil" означает **не инициализировано**: переменной ещё не было
|
||||||
|
присвоено значение, перменная не существует или была удалена (было присвоено
|
||||||
|
значение nil).
|
||||||
|
|
||||||
|
Функции — это особый тип переменных, но они также должны быть объявлены
|
||||||
|
локально, ведь в других модах могут оказаться свои функции с такими же именами.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local function foo(bar)
|
||||||
|
return bar * 2
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Чтобы позволить другим модам вызывать ваши функции, необходимо создать таблицу
|
||||||
|
с таким же именем, как у вашего мода, и добавить в неё нужные функции.
|
||||||
|
Такая таблица обычно называется "API мода" или namespace.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
mymod = {}
|
||||||
|
|
||||||
|
function mymod.foo(bar)
|
||||||
|
return "foo" .. bar
|
||||||
|
end
|
||||||
|
|
||||||
|
-- В другом моде или скрипте:
|
||||||
|
mymod.foo("foobar")
|
||||||
|
```
|
||||||
|
|
||||||
|
`function mymod.foo()` то же самое что и `mymod.foo = function()`, только
|
||||||
|
выглядит лучше.
|
||||||
|
|
||||||
|
## Подключение других скриптов
|
||||||
|
|
||||||
|
Лучше всего подключать другие Lua-скрипты в мод с помощью функции *dofile*.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
dofile(core.get_modpath("modname") .. "/script.lua")
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт может веруть значение, через которое предоставит доступ к локальным
|
||||||
|
переменным:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- script.lua
|
||||||
|
local module = {}
|
||||||
|
module.message = "Hello World!"
|
||||||
|
return module
|
||||||
|
|
||||||
|
-- init.lua
|
||||||
|
local ret = dofile(core.get_modpath("modname") .. "/script.lua")
|
||||||
|
print(ret.message) -- Hello world!
|
||||||
|
```
|
||||||
|
|
||||||
|
В [следующей главе](../quality/clean_arch.html) вы узнаете, как лучше всего
|
||||||
|
разбивать код мода.
|
93
_ru/games/games.md
Normal file
93
_ru/games/games.md
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
---
|
||||||
|
title: Creating Games
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 7.1
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
The power of Minetest is the ability to easily develop games without the need
|
||||||
|
to create your own voxel graphics, voxel algorithms, or fancy networking code.
|
||||||
|
|
||||||
|
- [What is a Game?](#what-is-a-game)
|
||||||
|
- [Game Directory](#game-directory)
|
||||||
|
- [Inter-game Compatibility](#inter-game-compatibility)
|
||||||
|
- [API Compatibility](#api-compatibility)
|
||||||
|
- [Groups and Aliases](#groups-and-aliases)
|
||||||
|
- [Your Turn](#your-turn)
|
||||||
|
|
||||||
|
## What is a Game?
|
||||||
|
|
||||||
|
Games are a collection of mods which work together to make a cohesive game.
|
||||||
|
A good game has a consistent underlying theme and a direction, for example,
|
||||||
|
it could be a classic crafter miner with hard survival elements, or
|
||||||
|
it could be a space simulation game with a steampunk automation aesthetic.
|
||||||
|
|
||||||
|
Game design is a complex topic and is actually a whole field of expertise.
|
||||||
|
It's beyond the scope of the book to more than briefly touch on it.
|
||||||
|
|
||||||
|
## Game Directory
|
||||||
|
|
||||||
|
The structure and location of a game will seem rather familiar after working
|
||||||
|
with mods.
|
||||||
|
Games are found in a game location, such as `minetest/games/foo_game`.
|
||||||
|
|
||||||
|
foo_game
|
||||||
|
├── game.conf
|
||||||
|
├── menu
|
||||||
|
│ ├── header.png
|
||||||
|
│ ├── background.png
|
||||||
|
│ └── icon.png
|
||||||
|
├── minetest.conf
|
||||||
|
├── mods
|
||||||
|
│ └── ... mods
|
||||||
|
├── README.txt
|
||||||
|
└── settingtypes.txt
|
||||||
|
|
||||||
|
The only thing that is required is a mods folder, but `game.conf` and `menu/icon.png`
|
||||||
|
are recommended.
|
||||||
|
|
||||||
|
## Inter-game Compatibility
|
||||||
|
|
||||||
|
### API Compatibility
|
||||||
|
|
||||||
|
It's a good idea to try to keep as much API compatibility with Minetest Game as
|
||||||
|
convenient, as it'll make porting mods to another game much easier.
|
||||||
|
|
||||||
|
The best way to keep compatibility with another game is to keep API compatibility
|
||||||
|
with any mods which have the same name.
|
||||||
|
That is, if a mod uses the same name as another mod, even if third-party,
|
||||||
|
it should have a compatible API.
|
||||||
|
For example, if a game includes a mod called `doors`, then it should have the
|
||||||
|
same API as `doors` in Minetest Game.
|
||||||
|
|
||||||
|
API compatibility for a mod is the sum of the following things:
|
||||||
|
|
||||||
|
* Lua API table - All documented/advertised functions in the global table which shares the same name.
|
||||||
|
For example, `mobs.register_mob`.
|
||||||
|
* Registered Nodes/Items - The presence of items.
|
||||||
|
|
||||||
|
Small breakages aren't that bad, such as not having a random utility
|
||||||
|
function that was only actually used internally, but bigger breakages
|
||||||
|
related to core features are very bad.
|
||||||
|
|
||||||
|
It's difficult to maintain API compatibility with a disgusting mega God-mod like
|
||||||
|
*default* in Minetest Game, in which case the game shouldn't include a mod named
|
||||||
|
default.
|
||||||
|
|
||||||
|
API compatibility also applies to other third-party mods and games,
|
||||||
|
so try to make sure that any new mods have a unique mod name.
|
||||||
|
To check whether a mod name has been taken, search for it on
|
||||||
|
[content.minetest.net](https://content.minetest.net/).
|
||||||
|
|
||||||
|
### Groups and Aliases
|
||||||
|
|
||||||
|
Groups and Aliases are both useful tools in keeping compatibility between games,
|
||||||
|
as it allows item names to be different between different games. Common nodes
|
||||||
|
like stone and wood should have groups to indicate the material. It's also a
|
||||||
|
good idea to provide aliases from default nodes to any direct replacements.
|
||||||
|
|
||||||
|
## Your Turn
|
||||||
|
|
||||||
|
* Create a simple game where the player gains points from digging special blocks.
|
35
_ru/index.md
Normal file
35
_ru/index.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
title: Обложка
|
||||||
|
layout: default
|
||||||
|
description: Простой гайд как создавать моды в Minetest
|
||||||
|
homepage: true
|
||||||
|
no_header: true
|
||||||
|
root: ..
|
||||||
|
idx: 0.1
|
||||||
|
---
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<h1>Luanti Modding Book (бывший Minetest)</h1>
|
||||||
|
|
||||||
|
<span>автор <a href="https://rubenwardy.com" rel="author">rubenwardy</a></span>
|
||||||
|
<span>редактор <a href="http://rc.minetest.tv/">Shara</a></span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
## Предисловие
|
||||||
|
|
||||||
|
Minetest поддерживает создание модов с помощью Lua-скриптов.
|
||||||
|
Цель этой книги — научить вас создавать свои собственные моды, начиная с основ.
|
||||||
|
Каждая глава охватывает определённую часть API.
|
||||||
|
|
||||||
|
Вы можете не только [читать книгу online](https://rubenwardy.com/minetest_modding_book),
|
||||||
|
но также [скачать её в формате HTML](https://gitlab.com/rubenwardy/minetest_modding_book/-/releases).
|
||||||
|
|
||||||
|
### Обратная связь
|
||||||
|
|
||||||
|
Нашли ошибку или хотите дать обратную связь? Сообщите мне об этом.
|
||||||
|
|
||||||
|
* Создайте тему в [GitLab Issue](https://gitlab.com/rubenwardy/minetest_modding_book/-/issues).
|
||||||
|
* Напишите на [форуме](https://forum.minetest.net/viewtopic.php?f=14&t=10729).
|
||||||
|
* [Свяжитесь со мной](https://rubenwardy.com/contact/).
|
||||||
|
* Желаете внести свой вклад?
|
||||||
|
[Прочтите README](https://gitlab.com/rubenwardy/minetest_modding_book/-/blob/master/README.md).
|
206
_ru/items/callbacks.md
Normal file
206
_ru/items/callbacks.md
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
---
|
||||||
|
title: Node and Item Callbacks
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 2.15
|
||||||
|
description: Learn about callbacks, actions, and events, including on_use, on_punch, on_place, on_rightclick
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
Minetest heavily uses a callback-based modding design. A callback is a function
|
||||||
|
that you give to an API and is called when an event happens. For example, you
|
||||||
|
can provide an `on_punch` function in a node definition to be called when a player
|
||||||
|
punches a node. There are also global callbacks like
|
||||||
|
`core.register_on_punchnode` to receive events for all nodes.
|
||||||
|
|
||||||
|
- [Item Callbacks](#item-callbacks)
|
||||||
|
- [on_use](#on_use)
|
||||||
|
- [on_place and on_secondary_use](#on_place-and-on_secondary_use)
|
||||||
|
- [on_drop](#on_drop)
|
||||||
|
- [after_use](#after_use)
|
||||||
|
- [item_place vs place_item](#item_place-vs-place_item)
|
||||||
|
- [Node Callbacks](#node-callbacks)
|
||||||
|
- [Right-clicking and placing a node](#right-clicking-and-placing-a-node)
|
||||||
|
- [Punching and digging](#punching-and-digging)
|
||||||
|
- [...and more!](#and-more)
|
||||||
|
|
||||||
|
|
||||||
|
## Item Callbacks
|
||||||
|
|
||||||
|
When a player has a node, craftitem, or tool in their inventory, they may trigger
|
||||||
|
certain events:
|
||||||
|
|
||||||
|
| Callback | Default binding | Default value |
|
||||||
|
|------------------|---------------------------|----------------------------------------------|
|
||||||
|
| on_use | left-click | nil |
|
||||||
|
| on_place | right-click on a node | `core.item_place` |
|
||||||
|
| on_secondary_use | right-click not on a node | `core.item_secondary_use` (does nothing) |
|
||||||
|
| on_drop | Q | `core.item_drop` |
|
||||||
|
| after_use | digging a node | nil |
|
||||||
|
|
||||||
|
|
||||||
|
### on_use
|
||||||
|
|
||||||
|
Having a use callback prevents the item from being used to dig nodes. One common
|
||||||
|
use of the use callback is for food:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_craftitem("mymod:mudpie", {
|
||||||
|
description = "Alien Mud Pie",
|
||||||
|
inventory_image = "myfood_mudpie.png",
|
||||||
|
on_use = core.item_eat(20),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The number supplied to the core.item_eat function is the number of hit
|
||||||
|
points healed when this food is consumed. Each heart icon the player has is
|
||||||
|
worth two hitpoints. A player can usually have up to 10 hearts, which is equal
|
||||||
|
to 20 hitpoints.
|
||||||
|
|
||||||
|
core.item_eat() is a function that returns a function, setting it as the
|
||||||
|
on_use callback. This means the code above is equivalent to this:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_craftitem("mymod:mudpie", {
|
||||||
|
description = "Alien Mud Pie",
|
||||||
|
inventory_image = "myfood_mudpie.png",
|
||||||
|
on_use = function(...)
|
||||||
|
return core.do_item_eat(20, nil, ...)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
By understanding how item_eat works by simply returning a function, it's
|
||||||
|
possible to modify it to do more complex behaviour like playing a custom sound.
|
||||||
|
|
||||||
|
|
||||||
|
### on_place and on_secondary_use
|
||||||
|
|
||||||
|
The difference between `on_place` and `on_secondary_use` is that `on_place` is
|
||||||
|
called when the player is pointing at a node and `on_secondary_use` when the
|
||||||
|
player isn't.
|
||||||
|
|
||||||
|
Both callbacks are called for all types of items. `on_place` defaults to the
|
||||||
|
`core.item_place` function, which handles calling the `on_rightclick`
|
||||||
|
callback of the pointed node or placing the wielded item if it is a node.
|
||||||
|
|
||||||
|
|
||||||
|
### on_drop
|
||||||
|
|
||||||
|
on_drop is called when the player requests to drop an item, for example using
|
||||||
|
the drop key (Q) or dragging it outside of the inventory. It defaults to the
|
||||||
|
`core.item_drop` function, which will handle dropping the item.
|
||||||
|
|
||||||
|
|
||||||
|
### after_use
|
||||||
|
|
||||||
|
`after_use` is called when digging a node and allows you to customise how wear
|
||||||
|
is applied to a tool. If after_use doesn't exist, then it is the same as:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
after_use = function(itemstack, user, node, digparams)
|
||||||
|
itemstack:add_wear(digparams.wear)
|
||||||
|
return itemstack
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## item_place vs place_item
|
||||||
|
|
||||||
|
Minetest's API includes many different built-in callback implementations for you
|
||||||
|
to use. These callbacks are named with the item type first, for example,
|
||||||
|
`core.item_place` and `core.node_dig`. Some callback implementations are
|
||||||
|
used directly whereas some are functions that return the callback:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_item("mymod:example", {
|
||||||
|
on_place = core.item_place,
|
||||||
|
on_use = core.item_eat(10),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Minetest's API also includes built-in functions that _do_ something. These are
|
||||||
|
often named in a confusingly similar way to built-in callback implementations
|
||||||
|
but have the verb first. Examples include `core.place_item` and
|
||||||
|
`core.dig_node` - these functions allow you to dig and place nodes with a
|
||||||
|
similar effect to players.
|
||||||
|
|
||||||
|
|
||||||
|
## Node Callbacks
|
||||||
|
|
||||||
|
When a node is in an inventory, it uses Item Callbacks, as discussed above. When
|
||||||
|
a node is placed in the world, it uses Node Callbacks. There are quite a lot of
|
||||||
|
node callbacks, too many to discuss in this book. However, quite a few of them
|
||||||
|
will be talked about later in the book.
|
||||||
|
|
||||||
|
Several of the callbacks are related to node operations such as placing and
|
||||||
|
removing from the world. It's important to note that node operation callbacks
|
||||||
|
like these aren't called from bulk changes - those that set a large number of
|
||||||
|
nodes at once - for performance reasons. Therefore, you can't rely on these
|
||||||
|
callbacks to always be called.
|
||||||
|
|
||||||
|
|
||||||
|
### Right-clicking and placing a node
|
||||||
|
|
||||||
|
When the user right-clicks with an item whilst pointing at a node, the item's
|
||||||
|
`on_place` callback is called. By default, this is set to `core.item_place`.
|
||||||
|
If the pointed node has an `on_rightclick` callback and sneak (shift) is held,
|
||||||
|
then the `on_rightclick` callback is called. Otherwise, `core.item_place`
|
||||||
|
will place the node.
|
||||||
|
|
||||||
|
Placing a node will call both `on_construct` and `after_place_node`.
|
||||||
|
`on_construct` is called by any node set event that wasn't in bulk and is just
|
||||||
|
given the node's position and value .`after_place_node` is only called by node
|
||||||
|
place, and so has more information - such as the placer and itemstack.
|
||||||
|
|
||||||
|
It's important to note that players aren't the only objects that can place
|
||||||
|
nodes; it's common for mobs and mods to place nodes. To account for this,
|
||||||
|
`placer` could be a player, entity, or nil.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("mymod:mynode", {
|
||||||
|
on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
|
||||||
|
if clicker:is_player() then
|
||||||
|
core.chat_send_player(clicker:get_player_name(), "Hello world!")
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
on_construct = function(pos, node)
|
||||||
|
local meta = core.get_meta(pos)
|
||||||
|
meta:set_string("infotext", "My node!")
|
||||||
|
end,
|
||||||
|
after_place_node = function(pos, placer, itemstack, pointed_thing)
|
||||||
|
-- Make sure to check placer
|
||||||
|
if placer and placer:is_player() then
|
||||||
|
local meta = core.get_meta(pos)
|
||||||
|
meta:set_string("owner", placer:get_player_name())
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Punching and digging
|
||||||
|
|
||||||
|
Punching is when the player left-clicks for a short period. If the wielded item
|
||||||
|
has an `on_use` callback, this will be called. Otherwise, the `on_punch`
|
||||||
|
callback on the pointed node will be called.
|
||||||
|
|
||||||
|
When the player attempts to dig a node, the `on_dig` callback on the node will be called.
|
||||||
|
This defaults to `core.node_dig`, which will check for area protection, wear
|
||||||
|
out the tool, remove the node, and run the `after_dig_node` callback.
|
||||||
|
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("mymod:mynode", {
|
||||||
|
on_punch = function(pos, node, puncher, pointed_thing)
|
||||||
|
if puncher:is_player() then
|
||||||
|
core.chat_send_player(puncher:get_player_name(), "Ow!")
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### ...and more!
|
||||||
|
|
||||||
|
Check out Minetest's Lua API reference for a list of all node callbacks, and
|
||||||
|
more information on the callbacks above.
|
98
_ru/items/creating_textures.md
Normal file
98
_ru/items/creating_textures.md
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
---
|
||||||
|
title: Creating Textures
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 2.2
|
||||||
|
description: An introduction to making textures in your editor of choice, and a guide on GIMP.
|
||||||
|
redirect_from: /en/chapters/creating_textures.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
Being able to create and optimise textures is a very useful skill when
|
||||||
|
developing for Minetest.
|
||||||
|
There are many techniques relevant to working on pixel art textures,
|
||||||
|
and understanding these techniques will greatly improve
|
||||||
|
the quality of the textures you create.
|
||||||
|
|
||||||
|
Detailed approaches to creating good pixel art are outside the scope
|
||||||
|
of this book, and instead only the most relevant basic techniques
|
||||||
|
will be covered.
|
||||||
|
There are many [good online tutorials](http://www.photonstorm.com/art/tutorials-art/16x16-pixel-art-tutorial)
|
||||||
|
available, which cover pixel art in much more detail.
|
||||||
|
|
||||||
|
- [Techniques](#techniques)
|
||||||
|
- [Using the Pencil](#using-the-pencil)
|
||||||
|
- [Tiling](#tiling)
|
||||||
|
- [Transparency](#transparency)
|
||||||
|
- [Color Palettes](#color-palettes)
|
||||||
|
- [Editors](#editors)
|
||||||
|
- [MS Paint](#ms-paint)
|
||||||
|
- [Aseprite / LibreSprite](#aseprite--libresprite)
|
||||||
|
- [GIMP](#gimp)
|
||||||
|
|
||||||
|
## Techniques
|
||||||
|
|
||||||
|
### Using the Pencil
|
||||||
|
|
||||||
|
The pencil tool is available in most editors. When set to its lowest size,
|
||||||
|
it allows you to edit one pixel at a time without changing any other parts
|
||||||
|
of the image. By manipulating the pixels one at a time, you create clear
|
||||||
|
and sharp textures without unintended blurring. It also gives you a high
|
||||||
|
level of precision and control.
|
||||||
|
|
||||||
|
### Tiling
|
||||||
|
|
||||||
|
Textures used for nodes should generally be designed to tile. This means
|
||||||
|
when you place multiple nodes with the same texture together, the edges line
|
||||||
|
up correctly.
|
||||||
|
|
||||||
|
<!-- IMAGE NEEDED - cobblestone that tiles correctly -->
|
||||||
|
|
||||||
|
If you fail to match the edges correctly, the result is far less pleasing
|
||||||
|
to look at.
|
||||||
|
|
||||||
|
<!-- IMAGE NEEDED - node that doesn't tile correctly -->
|
||||||
|
|
||||||
|
### Transparency
|
||||||
|
|
||||||
|
Transparency is important when creating textures for nearly all craftitems
|
||||||
|
and some nodes, such as glass.
|
||||||
|
Not all editors support transparency, so make sure you choose an
|
||||||
|
editor which is suitable for the textures you wish to create.
|
||||||
|
|
||||||
|
### Color Palettes
|
||||||
|
|
||||||
|
Using a consistent color palette is an easy way to make your art look a lot
|
||||||
|
better. It's a good idea to use one with a limited number of colors, perhaps 32
|
||||||
|
at most. Premade palettes can be found at
|
||||||
|
[lospec.com](https://lospec.com/palette-list).
|
||||||
|
|
||||||
|
## Editors
|
||||||
|
|
||||||
|
### MS Paint
|
||||||
|
|
||||||
|
MS Paint is a simple editor which can be useful for basic texture
|
||||||
|
design; however, it does not support transparency.
|
||||||
|
This usually won't matter when making textures for the sides of nodes,
|
||||||
|
but if you need transparency in your textures you should choose a
|
||||||
|
different editor.
|
||||||
|
|
||||||
|
### Aseprite / LibreSprite
|
||||||
|
|
||||||
|
[Aseprite](https://www.aseprite.org/) is a proprietary pixel art editor.
|
||||||
|
It contains a lot of useful features by default such as color palettes and
|
||||||
|
animation tools.
|
||||||
|
|
||||||
|
[LibreSprite](https://libresprite.github.io/) is an open-source fork of Aseprite
|
||||||
|
from before it went proprietary.
|
||||||
|
|
||||||
|
### GIMP
|
||||||
|
|
||||||
|
GIMP is commonly used in the Minetest community. It has quite a high
|
||||||
|
learning curve because many of its features are not immediately
|
||||||
|
obvious.
|
||||||
|
|
||||||
|
When using GIMP, make sure to use the Pencil tool with the Pixel brush and a
|
||||||
|
size of 1. It's also advisable to select the "Hard edge" checkbox for the Eraser
|
||||||
|
tool.
|
356
_ru/items/inventories.md
Normal file
356
_ru/items/inventories.md
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
---
|
||||||
|
title: ItemStacks and Inventories
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 2.4
|
||||||
|
description: Manipulate InvRefs and ItemStacks
|
||||||
|
redirect_from:
|
||||||
|
- /en/chapters/inventories.html
|
||||||
|
- /en/chapters/itemstacks.html
|
||||||
|
- /en/inventories/inventories.html
|
||||||
|
- /en/inventories/itemstacks.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
In this chapter, you will learn how to use and manipulate inventories, whether
|
||||||
|
that be a player inventory, a node inventory, or a detached inventory.
|
||||||
|
|
||||||
|
- [What are ItemStacks and Inventories?](#what-are-itemstacks-and-inventories)
|
||||||
|
- [ItemStacks](#itemstacks)
|
||||||
|
- [Inventory Locations](#inventory-locations)
|
||||||
|
- [Node Inventories](#node-inventories)
|
||||||
|
- [Player Inventories](#player-inventories)
|
||||||
|
- [Detached Inventories](#detached-inventories)
|
||||||
|
- [Lists](#lists)
|
||||||
|
- [Size and Width](#size-and-width)
|
||||||
|
- [Checking Contents](#checking-contents)
|
||||||
|
- [Modifying Inventories and ItemStacks](#modifying-inventories-and-itemstacks)
|
||||||
|
- [Adding to a List](#adding-to-a-list)
|
||||||
|
- [Taking Items](#taking-items)
|
||||||
|
- [Manipulating Stacks](#manipulating-stacks)
|
||||||
|
- [Wear](#wear)
|
||||||
|
- [Lua Tables](#lua-tables)
|
||||||
|
|
||||||
|
## What are ItemStacks 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 referred to as *lists* in the context of
|
||||||
|
inventories.
|
||||||
|
|
||||||
|
Players and nodes only have a single inventory; lists enable you to have
|
||||||
|
multiple grids within that inventory. By default, the player has the "main" list
|
||||||
|
for the bulk of its inventory and a few lists for the crafting system.
|
||||||
|
|
||||||
|
## ItemStacks
|
||||||
|
|
||||||
|
ItemStacks have four components to them: `name`, `count`, `wear`, and metadata.
|
||||||
|
|
||||||
|
The item name may be the item name of a registered item, 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.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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. Through normal gameplay, the count should
|
||||||
|
be no more than the maximum stack size of the item - `stack_max`. However, admin
|
||||||
|
commands and buggy mods may result in stacks exceeding the maximum size.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
print(stack:get_stack_max())
|
||||||
|
```
|
||||||
|
|
||||||
|
An ItemStack can be empty, in which case the count will be 0.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
print(stack:get_count())
|
||||||
|
stack:set_count(10)
|
||||||
|
```
|
||||||
|
|
||||||
|
ItemStacks can be constructed in multiple ways using the ItemStack function:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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 the key) to access the data (called the
|
||||||
|
value). Some keys have special meaning, such as `description` which is used to
|
||||||
|
have a per-stack item description. This will be covered in more detail in the
|
||||||
|
[Storage and Metadata](../map/storage.html) chapter.
|
||||||
|
|
||||||
|
## Inventory Locations
|
||||||
|
|
||||||
|
An Inventory Location is where and how the inventory is stored. There are three
|
||||||
|
types of inventory location: player, node, and detached. An inventory is
|
||||||
|
directly tied to one and only one location - updating the inventory will cause
|
||||||
|
it to update immediately.
|
||||||
|
|
||||||
|
### Node Inventories
|
||||||
|
|
||||||
|
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](../map/storage.html#metadata).
|
||||||
|
|
||||||
|
```lua
|
||||||
|
on_punch = function(pos, node)
|
||||||
|
local inv = core.get_inventory({ type="node", pos=pos })
|
||||||
|
-- now use the inventory
|
||||||
|
end,
|
||||||
|
```
|
||||||
|
|
||||||
|
The above obtains an *inventory reference*, commonly referred to as *InvRef*.
|
||||||
|
Inventory references are used to manipulate an inventory.
|
||||||
|
*Reference* means that the data isn't actually stored inside that object,
|
||||||
|
but the object instead directly updates the data in-place.
|
||||||
|
|
||||||
|
The location of an inventory reference can be found like so:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local location = inv:get_location()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Player Inventories
|
||||||
|
|
||||||
|
Player inventories can be obtained similarly or using a player reference.
|
||||||
|
The player must be online to access their inventory.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local inv = core.get_inventory({ type="player", name="player1" })
|
||||||
|
-- or
|
||||||
|
local inv = player:get_inventory()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Detached Inventories
|
||||||
|
|
||||||
|
A detached inventory is one that is independent of players or nodes. Detached
|
||||||
|
inventories also don't save over a restart.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local inv = core.get_inventory({
|
||||||
|
type="detached", name="inventory_name" })
|
||||||
|
```
|
||||||
|
|
||||||
|
Unlike the other types of inventory, you must first create a detached inventory
|
||||||
|
before accessing it:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.create_detached_inventory("inventory_name")
|
||||||
|
```
|
||||||
|
|
||||||
|
The `create_detached_inventory` function accepts 3 arguments, where only the
|
||||||
|
first - 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:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Input only detached inventory
|
||||||
|
core.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)
|
||||||
|
core.chat_send_all(player:get_player_name() ..
|
||||||
|
" gave " .. stack:to_string() ..
|
||||||
|
" to the donation chest from " .. core.pos_to_string(player:get_pos()))
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Permission callbacks - ie: those starting with `allow_` - return the number
|
||||||
|
of items to transfer, with 0 being used to prevent transfer completely.
|
||||||
|
|
||||||
|
On the contrary, action callbacks - starting with `on_` - don't have a return value.
|
||||||
|
|
||||||
|
## Lists
|
||||||
|
|
||||||
|
Inventory Lists are a concept used to allow multiple grids to be stored inside a
|
||||||
|
single location. This is especially useful for the player as there are several
|
||||||
|
common lists that all games have, such as the *main* inventory and *craft*
|
||||||
|
slots.
|
||||||
|
|
||||||
|
### 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 width of the list is not used when drawing the inventory in a window,
|
||||||
|
because the code behind the window determines the width to use.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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 listname or size is invalid.
|
||||||
|
For example, the new size may be too small to fit all the current items
|
||||||
|
in the inventory.
|
||||||
|
|
||||||
|
### Checking Contents
|
||||||
|
|
||||||
|
`is_empty` can be used to see if a list contains any items:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
if inv:contains_item("main", "default:stone") then
|
||||||
|
print("I've found some stone!")
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Modifying Inventories and ItemStacks
|
||||||
|
|
||||||
|
### Adding to a List
|
||||||
|
|
||||||
|
`add_item` adds items to a list (in this case `"main"`). In the example below,
|
||||||
|
the maximum stack size is also respected:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local taken = inv:remove_item("main", stack)
|
||||||
|
print("Took " .. taken:get_count())
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manipulating Stacks
|
||||||
|
|
||||||
|
You can modify individual stacks by first getting them:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local stack = inv:get_stack(listname, 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
Then modifying them by setting properties or by using the methods which
|
||||||
|
respect `stack_size`:
|
||||||
|
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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 any that could not be added.
|
||||||
|
`take_item` will take up to the number of items but may take less, and returns the stack taken.
|
||||||
|
|
||||||
|
Finally, set the item stack:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
inv:set_stack(listname, 0, stack)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wear
|
||||||
|
|
||||||
|
Tools can have wear; wear shows a progress bar and makes the tool break when completely worn.
|
||||||
|
Wear is a number out of 65535; the higher it is, the more worn the tool is.
|
||||||
|
|
||||||
|
Wear can be manipulated using `add_wear()`, `get_wear()`, and `set_wear(wear)`.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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 digging a node, the amount of wear a tool gets may depend on the node
|
||||||
|
being dug. So max_uses varies depending on what is being dug.
|
||||||
|
|
||||||
|
## Lua Tables
|
||||||
|
|
||||||
|
ItemStacks and Inventories can be converted to and from tables.
|
||||||
|
This is useful for copying and bulk operations.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- 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 table of lists returned by `get_lists()` will be in this form:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
{
|
||||||
|
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 set methods above don't change the size
|
||||||
|
of the lists.
|
||||||
|
This means that you can clear a list by setting it to an empty table and it won't
|
||||||
|
decrease in size:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
inv:set_list("main", {})
|
||||||
|
```
|
446
_ru/items/node_drawtypes.md
Normal file
446
_ru/items/node_drawtypes.md
Normal file
@ -0,0 +1,446 @@
|
|||||||
|
---
|
||||||
|
title: Node Drawtypes
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 2.3
|
||||||
|
description: Guide to all drawtypes, including node boxes/nodeboxes and mesh nodes.
|
||||||
|
redirect_from: /en/chapters/node_drawtypes.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
The method by which a node is drawn is called a *drawtype*. There are many
|
||||||
|
available drawtypes. The behaviour of a drawtype 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 items was introduced, but a
|
||||||
|
full definition of a node wasn't given. 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 function
|
||||||
|
`core.register_node` is a bit misleading in that it doesn't actually
|
||||||
|
register a node - it registers a new *type* of node.
|
||||||
|
|
||||||
|
The node params are used to control how a node is individually 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 Allfaces](#cubic-nodes-normal-and-allfaces)
|
||||||
|
- [Glasslike Nodes](#glasslike-nodes)
|
||||||
|
- [Glasslike_Framed](#glasslike_framed)
|
||||||
|
- [Airlike Nodes](#airlike-nodes)
|
||||||
|
- [Lighting and Sunlight Propagation](#lighting-and-sunlight-propagation)
|
||||||
|
- [Liquid Nodes](#liquid-nodes)
|
||||||
|
- [Node Boxes](#node-boxes)
|
||||||
|
- [Wallmounted Node Boxes](#wallmounted-node-boxes)
|
||||||
|
- [Mesh Nodes](#mesh-nodes)
|
||||||
|
- [Signlike Nodes](#signlike-nodes)
|
||||||
|
- [Plantlike Nodes](#plantlike-nodes)
|
||||||
|
- [Firelike Nodes](#firelike-nodes)
|
||||||
|
- [More Drawtypes](#more-drawtypes)
|
||||||
|
|
||||||
|
|
||||||
|
## Cubic Nodes: Normal and Allfaces
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img src="{{ page.root }}//static/drawtype_normal.png" alt="Normal Drawtype">
|
||||||
|
<figcaption>
|
||||||
|
Normal Drawtype
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
The normal drawtype is typically used to render a cubic node.
|
||||||
|
If the side of a normal node is against a solid side, then that side won't be rendered,
|
||||||
|
resulting in a large performance gain.
|
||||||
|
|
||||||
|
In contrast, the allfaces drawtype will still render the inner side when up against
|
||||||
|
a solid node. This is good for nodes with partially transparent sides, such as
|
||||||
|
leaf nodes. You can use the allfaces_optional drawtype to allow users to opt-out
|
||||||
|
of the slower drawing, in which case it'll act like a normal node.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("mymod:diamond", {
|
||||||
|
description = "Alien Diamond",
|
||||||
|
tiles = {"mymod_diamond.png"},
|
||||||
|
groups = {cracky = 3},
|
||||||
|
})
|
||||||
|
|
||||||
|
core.register_node("default:leaves", {
|
||||||
|
description = "Leaves",
|
||||||
|
drawtype = "allfaces_optional",
|
||||||
|
tiles = {"default_leaves.png"}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: the normal drawtype is the default drawtype, so you don't need to explicitly
|
||||||
|
specify it.
|
||||||
|
|
||||||
|
|
||||||
|
## Glasslike Nodes
|
||||||
|
|
||||||
|
The difference between glasslike and normal nodes is that placing a glasslike node
|
||||||
|
next to a normal node won't cause the side of the normal node to be hidden.
|
||||||
|
This is useful because glasslike nodes tend to be transparent, and so using a normal
|
||||||
|
drawtype would result in the ability to see through the world.
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img src="{{ page.root }}//static/drawtype_glasslike_edges.png" alt="Glasslike's Edges">
|
||||||
|
<figcaption>
|
||||||
|
Glasslike's Edges
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.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's edge go around the whole thing with a 3D effect, rather
|
||||||
|
than individual nodes, like the following:
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img src="{{ page.root }}//static/drawtype_glasslike_framed.png" alt="Glasslike_framed's Edges">
|
||||||
|
<figcaption>
|
||||||
|
Glasslike_Framed's Edges
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
You can use the glasslike_framed_optional drawtype to allow the user to *opt-in*
|
||||||
|
to the framed appearance.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("default:glass", {
|
||||||
|
description = "Glass",
|
||||||
|
drawtype = "glasslike_framed",
|
||||||
|
tiles = {"default_glass.png", "default_glass_detail.png"},
|
||||||
|
inventory_image = core.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()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Airlike Nodes
|
||||||
|
|
||||||
|
These nodes are not rendered and thus have no textures.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.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 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. In order to work out how to shade
|
||||||
|
a node's side, the light value of the neighbouring node is used.
|
||||||
|
Because of this, solid nodes don't have light values because they block light.
|
||||||
|
|
||||||
|
By default, a node type won't allow light to be stored in any node instances.
|
||||||
|
It's usually desirable for some nodes such as glass and air to be able to
|
||||||
|
let light through. To do this, there are two properties which need to be defined:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
paramtype = "light",
|
||||||
|
sunlight_propagates = true,
|
||||||
|
```
|
||||||
|
|
||||||
|
The first line means that param1 does, in fact, store the light level.
|
||||||
|
The second line means that sunlight should go through this node without decreasing in value.
|
||||||
|
|
||||||
|
|
||||||
|
## Liquid Nodes
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img src="{{ page.root }}//static/drawtype_liquid.png" alt="Liquid Drawtype">
|
||||||
|
<figcaption>
|
||||||
|
Liquid Drawtype
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
Each type of liquid requires two node definitions - one for the liquid source, and
|
||||||
|
another for flowing liquid.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Some properties have been removed as they are beyond
|
||||||
|
-- the scope of this chapter.
|
||||||
|
core.register_node("default:water_source", {
|
||||||
|
drawtype = "liquid",
|
||||||
|
paramtype = "light",
|
||||||
|
|
||||||
|
inventory_image = core.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},
|
||||||
|
-- ^ colour 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 full example.
|
||||||
|
|
||||||
|
|
||||||
|
## Node Boxes
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img src="{{ page.root }}//static/drawtype_nodebox.gif" alt="Nodebox drawtype">
|
||||||
|
<figcaption>
|
||||||
|
Nodebox drawtype
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
Node boxes allow you to create a node which is not cubic, but is instead made out
|
||||||
|
of as many cuboids as you like.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.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:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
{-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 which are joined to make a single node.
|
||||||
|
The first three numbers are the co-ordinates, from -0.5 to 0.5 inclusive, of
|
||||||
|
the bottom front left most corner, the last three numbers are the opposite corner.
|
||||||
|
They are in the form X, Y, Z, where Y is up.
|
||||||
|
|
||||||
|
You can use the [NodeBoxEditor](https://forum.minetest.net/viewtopic.php?f=14&t=2840) to
|
||||||
|
create node boxes by dragging the edges, it is more visual than doing it by hand.
|
||||||
|
|
||||||
|
|
||||||
|
### Wallmounted Node Boxes
|
||||||
|
|
||||||
|
Sometimes you want different nodeboxes for when it is placed on the floor, wall, or ceiling like with torches.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.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
|
||||||
|
|
||||||
|
Whilst node boxes are generally easier to make, they are limited in that
|
||||||
|
they can only consist of cuboids. Node boxes are also unoptimised;
|
||||||
|
Inner faces will still be rendered even when they're completely hidden.
|
||||||
|
|
||||||
|
A face is a flat surface on a mesh. An inner 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 so:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.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 that 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
|
||||||
|
|
||||||
|
Signlike nodes are flat nodes with can be mounted on the sides of other nodes.
|
||||||
|
|
||||||
|
Despite the name of this drawtype, signs don't actually tend to use signlike but
|
||||||
|
instead use the `nodebox` drawtype to provide a 3D effect. The `signlike` drawtype
|
||||||
|
is, however, commonly used by ladders.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("default:ladder_wood", {
|
||||||
|
drawtype = "signlike",
|
||||||
|
|
||||||
|
tiles = {"default_ladder_wood.png"},
|
||||||
|
|
||||||
|
-- Required: store the rotation in param2
|
||||||
|
paramtype2 = "wallmounted",
|
||||||
|
|
||||||
|
selection_box = {
|
||||||
|
type = "wallmounted",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Plantlike Nodes
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img src="{{ page.root }}//static/drawtype_plantlike.png" alt="Plantlike Drawtype">
|
||||||
|
<figcaption>
|
||||||
|
Plantlike Drawtype
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
Plantlike nodes draw their tiles in an X like pattern.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.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},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Firelike Nodes
|
||||||
|
|
||||||
|
Firelike is similar to plantlike, except that it is designed to "cling" to walls
|
||||||
|
and ceilings.
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img src="{{ page.root }}//static/drawtype_firelike.png" alt="Firelike nodes">
|
||||||
|
<figcaption>
|
||||||
|
Firelike nodes
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("mymod:clingere", {
|
||||||
|
drawtype = "firelike",
|
||||||
|
|
||||||
|
-- Only one texture used
|
||||||
|
tiles = { "mymod:clinger" },
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## More Drawtypes
|
||||||
|
|
||||||
|
This is not a comprehensive list, there are more types including:
|
||||||
|
|
||||||
|
* Fencelike
|
||||||
|
* Plantlike rooted - for underwater plants
|
||||||
|
* Raillike - for cart tracks
|
||||||
|
* Torchlike - for 2D wall/floor/ceiling nodes.
|
||||||
|
The 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](https://minetest.gitlab.io/minetest/nodes/#node-drawtypes)
|
||||||
|
for the complete list.
|
340
_ru/items/nodes_items_crafting.md
Normal file
340
_ru/items/nodes_items_crafting.md
Normal file
@ -0,0 +1,340 @@
|
|||||||
|
---
|
||||||
|
title: Блоки, предметы и крафт
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 2.1
|
||||||
|
description: Вы узнаете как зарегистрировать новый блок, предмет или рецепт с помощью register_node, register_item и register_craft.
|
||||||
|
redirect_from: /ru/chapters/nodes_items_crafting.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Введение <!-- omit in toc -->
|
||||||
|
|
||||||
|
Регистрация новых блоков, ингредиентов и рецептов встречается практически
|
||||||
|
во всех модах.
|
||||||
|
|
||||||
|
- [Что такое Блоки и Предметы?](#what-are-nodes-and-items)
|
||||||
|
- [Регистрация предметов](#registering-items)
|
||||||
|
- [Названия предметов](#item-names)
|
||||||
|
- [Псевдонимы](#item-aliases)
|
||||||
|
- [Текстуры](#textures)
|
||||||
|
- [Регистрация простого блока](#registering-a-basic-node)
|
||||||
|
- [Рецепты](#crafting)
|
||||||
|
- [Shaped](#shaped)
|
||||||
|
- [Shapeless](#shapeless)
|
||||||
|
- [Cooking и Fuel](#cooking-and-fuel)
|
||||||
|
- [Группы](#groups)
|
||||||
|
- [Инструменты, Возможности, and Способы ломания](#tools-capabilities-and-dig-types)
|
||||||
|
|
||||||
|
## Что такое Блоки и Предметы?
|
||||||
|
|
||||||
|
Все блоки и инструменты также являются Предметами. Предмет — это то,
|
||||||
|
что может лежать в инвентаре (даже если правила игры это запрещают).
|
||||||
|
|
||||||
|
Блок — это Предмет, который может быть поставлен или найден в мире. В каждом
|
||||||
|
вокселе мира всегда находится один и только один блок (пустое место — это,
|
||||||
|
обычно, блок воздуха).
|
||||||
|
|
||||||
|
Обычные предметы (не блоки) не могут быть поставлены в мире, а могут только
|
||||||
|
находиться в инвентаре или выпадать в мир.
|
||||||
|
|
||||||
|
Инструмент похож на обычный предмет, но его можно использовать. Когда вы
|
||||||
|
используете инструмент, шкала его прочности уменьшается до тех пор, пока он
|
||||||
|
не сломается. Инструменты не могут быть сгруппированы в стак. В будущем,
|
||||||
|
возможно, Инструменты не будут выделяться в отдельный тип, поскольку различие
|
||||||
|
между ними и обычными предметами довольно условное.
|
||||||
|
|
||||||
|
## Регистрация предметов
|
||||||
|
|
||||||
|
Объявление предмета состоит из *названия* и *аттрибутов*.
|
||||||
|
В аттрибутах записаны различные параметры, влияющие на поведение предмета.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_craftitem("modname:itemname", {
|
||||||
|
description = "My Special Item",
|
||||||
|
inventory_image = "modname_itemname.png"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Названия предметов
|
||||||
|
|
||||||
|
У каждого предмета есть название для обращения к нему, которое должно иметь
|
||||||
|
следующий формат:
|
||||||
|
|
||||||
|
modname:itemname
|
||||||
|
|
||||||
|
`modname` — это имя мода, в котором предмет зарегистрирован, а `itemname` — это
|
||||||
|
название самого предмета. Название предмета должно быть уникальным и должно
|
||||||
|
отражать его суть.
|
||||||
|
|
||||||
|
Для написания `modname` и `itemname` используются только строчные буквы
|
||||||
|
английского алфавита, цифры и подчёркивания.
|
||||||
|
|
||||||
|
### Псевдонимы
|
||||||
|
|
||||||
|
У предметов могут быть *псевдонимы*. Игровой движок работает с псевдонимами
|
||||||
|
предметов как будто это сами предметы. Псевдонимы используются в двух случаях:
|
||||||
|
|
||||||
|
* Чтобы переименовать удалённые предметы на что нибудь другое. На карте или
|
||||||
|
в инвентаре могут появится *неизвестные блоки* (unknown nodes), если
|
||||||
|
просто удалить предмет из мода без исправления кода.
|
||||||
|
* Для сокращения названий. Команда `/giveme dirt` получается короче,
|
||||||
|
чем `/giveme default:dirt`.
|
||||||
|
|
||||||
|
Зарегистрировать псевдоним очень просто. Нужно запомнить правильный порядок
|
||||||
|
аргументов `from → to`, гле *from* — это псевдоним, а *to* — это сам предмет.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_alias("dirt", "default:dirt")
|
||||||
|
```
|
||||||
|
|
||||||
|
Моды должны всегд разименовывать псевдонимы перед тем как работать с именем
|
||||||
|
предмета, потому что движок сам этого не делает. Но это довольно просто:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
itemname = core.registered_aliases[itemname] or itemname
|
||||||
|
```
|
||||||
|
|
||||||
|
### Текстуры
|
||||||
|
|
||||||
|
Файлы текстур нужно класть в папку textures/ и давать имена в следующем
|
||||||
|
формате: `modname_itemname.png`.\\
|
||||||
|
Можно использовать текстуры в формате JPEG, но они не поддерживают прозрачность
|
||||||
|
и, обычно, плохо выглядят при низком разрешении. Лучше использовать формат PNG.
|
||||||
|
|
||||||
|
Обычно, текстуры в Minetest имеют размер 16 на 16 пикселей. Вообще, разрешение
|
||||||
|
может быть любым, но рекомендуется использовать значения степени двойки,
|
||||||
|
например 16, 32, 64 или 128. В противном случае они могут вызвать проблемы
|
||||||
|
с производительностью на старых компьютерах и особенно телефонах.
|
||||||
|
|
||||||
|
## Регистрация простого блока
|
||||||
|
|
||||||
|
Блоки регистрируются так же, как и предметы, только другой функцией:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("mymod:diamond", {
|
||||||
|
description = "Alien Diamond",
|
||||||
|
tiles = {"mymod_diamond.png"},
|
||||||
|
is_ground_content = true,
|
||||||
|
groups = {cracky=3, stone=1}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
При объявлении блока в список аттрибутов добавляются дополнительные свойства,
|
||||||
|
характерные для блоков.
|
||||||
|
|
||||||
|
Свойство `tiles` — это таблица с именами текстур этого блока.
|
||||||
|
Если указана только одна текстура, она используется для всех сторон блока.
|
||||||
|
Если нужно указать разные текстуры для сторон, то они должны быть перечислены
|
||||||
|
в следующем порядке:
|
||||||
|
|
||||||
|
сверху (+Y), снизу (-Y), справа (+X), слева (-X), сзади (+Z), спереди (-Z).
|
||||||
|
(+Y, -Y, +X, -X, +Z, -Z)
|
||||||
|
|
||||||
|
Запомните, что +Y это верх в Minetest, как и в большинстве других игр.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("mymod:diamond", {
|
||||||
|
description = "Alien Diamond",
|
||||||
|
tiles = {
|
||||||
|
"mymod_diamond_up.png", -- y+
|
||||||
|
"mymod_diamond_down.png", -- y-
|
||||||
|
"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"
|
||||||
|
-- ^ Выпадает не сам алмаз, а mymod:diamond_framgents
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Свойство `is_ground_content` позволяет генерировать пещеры в каменной толще.
|
||||||
|
Это важно для всех блоков, которые должны появляться под землёй в процессе
|
||||||
|
генерации карты. Пещеры прорезаются после того, как будут сгенерированы все
|
||||||
|
остальные блоки в данной области.
|
||||||
|
|
||||||
|
## Рецепты
|
||||||
|
|
||||||
|
Существует несколько типов рецептов, которые указываются через свойство `type`.
|
||||||
|
|
||||||
|
* shaped — Ингредиенты должны быть сложены в правильном порядке.
|
||||||
|
* shapeless — Порядок ингредиентов не важен, важно лишь их наличие
|
||||||
|
и количество.
|
||||||
|
* cooking — Можно приготовить в печи.
|
||||||
|
* fuel — Может быть топливом для печи.
|
||||||
|
* tool_repair — Можно отремонтировать инструментом.
|
||||||
|
|
||||||
|
Рецепты крфта — это не предметы, и поэтому им не нужно указывать имя.
|
||||||
|
|
||||||
|
### Shaped
|
||||||
|
|
||||||
|
Для рецептов `shaped` важно расположение ингредиентов в сетке крафта.
|
||||||
|
В примере ниже фрагменты нужно расположить в форме стула, чтобы рецепт
|
||||||
|
сработал:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.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", ""}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Обратите внимание на пустую колонку в правой части.
|
||||||
|
Так обозначается, что *должна* быть пустая колонка справа, иначе рецепт
|
||||||
|
не будет работать.
|
||||||
|
Если пустая колонка не требуется, постые строки можно не указывать:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_craft({
|
||||||
|
output = "mymod:diamond_chair 99",
|
||||||
|
recipe = {
|
||||||
|
{"mymod:diamond_fragments", "" },
|
||||||
|
{"mymod:diamond_fragments", "mymod:diamond_fragments"},
|
||||||
|
{"mymod:diamond_fragments", "mymod:diamond_fragments"}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Свойство type можно не указывать, потому что все рецепты по умолчанию
|
||||||
|
считаются shaped.
|
||||||
|
|
||||||
|
### Shapeless
|
||||||
|
|
||||||
|
В рецептах `shapeless` не важно расположение ингредиентов, они просто должны
|
||||||
|
быть.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_craft({
|
||||||
|
type = "shapeless",
|
||||||
|
output = "mymod:diamond 3",
|
||||||
|
recipe = {
|
||||||
|
"mymod:diamond_fragments",
|
||||||
|
"mymod:diamond_fragments",
|
||||||
|
"mymod:diamond_fragments",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cooking и Fuel
|
||||||
|
|
||||||
|
Рецепты типа `cooking` предназначены не для сетки крафта, а для приготовления
|
||||||
|
в различных типах печей.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_craft({
|
||||||
|
type = "cooking",
|
||||||
|
output = "mymod:diamond_fragments",
|
||||||
|
recipe = "default:coalblock",
|
||||||
|
cooktime = 10,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Единственное отличие в коде такого рецепта заключается в том, что поле recipe —
|
||||||
|
это просто одно свойство, а не таблица. Кроме того, в таких рецептах можно
|
||||||
|
указывать свойство "cooktime", в котором задаётся время приготовления.
|
||||||
|
По умоланию, оно равно 3.
|
||||||
|
|
||||||
|
Указанный выше рецепт работает, когда во входной слот печи кладут блок угля,
|
||||||
|
а в слот топлива — какое-нибудь топливо. Через 10 секунд на выходе получается
|
||||||
|
кусочек алмаза.
|
||||||
|
|
||||||
|
Рецепты типа `fuel` дополняют рецепты типа `cooking`. Они указывают, что
|
||||||
|
можно использовать в качестве топлива в печах.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_craft({
|
||||||
|
type = "fuel",
|
||||||
|
recipe = "mymod:diamond",
|
||||||
|
burntime = 300,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
У таких рецептов нет свойства output, но есть свойство burntime, определяющее
|
||||||
|
время горения предмета в печи (в секнудах). В данном примере алмаз получается
|
||||||
|
хорошим топливом, потому что он горит целых 300 секунд!
|
||||||
|
|
||||||
|
## Группы
|
||||||
|
|
||||||
|
Предметы можно объединять в группы, и в группах может быть множество предметов.
|
||||||
|
Принадлежность к группе указывается с помощью аттрибута `groups` в таблице
|
||||||
|
аттрибутов.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
groups = {cracky = 3, wood = 1}
|
||||||
|
```
|
||||||
|
|
||||||
|
Есть несколько причин указывать группы.
|
||||||
|
Во первых, группы позволяют указать такие свойства, как способ ломания блока
|
||||||
|
и его горючесть.
|
||||||
|
Во вторых, в рецептах крафта в качестве ингредиентов можно указывать группы
|
||||||
|
вместо конкретных предметов.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_craft({
|
||||||
|
type = "shapeless",
|
||||||
|
output = "mymod:diamond_thing 3",
|
||||||
|
recipe = {"group:wood", "mymod:diamond"}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Инструменты, Возможности, и Способы ломания
|
||||||
|
|
||||||
|
Способы ломания — это группы, которые обозначают, насколько блок устойчив
|
||||||
|
к разным типам инструментов.
|
||||||
|
Чем большее значение задано группе, тем проще сломать блок данным видом
|
||||||
|
инструмента.
|
||||||
|
Можно комбинировать несколько способов ломания, чтобы позволить игрокам
|
||||||
|
эффективнее использовать разные виды инструментов.
|
||||||
|
Если не указать никакой из способов ломания, блок нельзя будет сломать.
|
||||||
|
|
||||||
|
|
||||||
|
| Группа | Инструмент | Описание |
|
||||||
|
|-------------------------|------------|--------------------------------------------------------------------------------------|
|
||||||
|
| crumbly | лопата | Земля, песок |
|
||||||
|
| cracky | кирка | Твердый (но хрупкий) материал. Например, камень. |
|
||||||
|
| snappy | *любой* | Можно сломать чем угодно. Например, листья, неболшие растения, проволока. |
|
||||||
|
| choppy | топор | Можно разрубить. Например, деревья, доски. |
|
||||||
|
| fleshy | меч | Живые объекты, такие как животные или игроки.<br>Могут быть эффекты крови при ударе. |
|
||||||
|
| explody | ? | Может взрываться |
|
||||||
|
| oddly_breakable_by_hand | *любой* | Факелы и подобные быстро ломаемые вещи |
|
||||||
|
|
||||||
|
|
||||||
|
У каждого инструмента есть Возможности — это список типов ломания
|
||||||
|
с соответствующими свойствами, такими как время ломания и степень износа.
|
||||||
|
Кроме того, можно указывать максимально допустимую прочность блоков, которые
|
||||||
|
можно ломать данным инструментом. Таким образом можно запретить ломать твёрдые
|
||||||
|
блоки слишком простыми инструментами. Обычно, всем инструментам указывают все
|
||||||
|
возможные способы ломания, делая неподходящие способы сильно неэффективными.
|
||||||
|
Если игрок пытается сломать блок инструментом, не поддерживающим такой тип
|
||||||
|
блоков, то будет применяться свойство рук, а не инструмента.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.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` — это список поддерживаемых Способов ломания.
|
||||||
|
`damage_groups` позволяет указать, каким образом инструмент повреждает блок,
|
||||||
|
это будет описано позже в главе "Объекты, Игроки и Сущности".
|
234
_ru/map/environment.md
Normal file
234
_ru/map/environment.md
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
---
|
||||||
|
title: Basic Map Operations
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 3.1
|
||||||
|
description: Basic operations like set_node and get_node
|
||||||
|
redirect_from: /en/chapters/environment.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
In this chapter, you will learn how to perform basic actions on the map, such as
|
||||||
|
adding, removing, and finding nodes.
|
||||||
|
|
||||||
|
- [Map Structure](#map-structure)
|
||||||
|
- [Reading](#reading)
|
||||||
|
- [Reading Nodes](#reading-nodes)
|
||||||
|
- [Finding Nodes](#finding-nodes)
|
||||||
|
- [Writing](#writing)
|
||||||
|
- [Writing Nodes](#writing-nodes)
|
||||||
|
- [Removing Nodes](#removing-nodes)
|
||||||
|
- [Loading Blocks](#loading-blocks)
|
||||||
|
- [Deleting Blocks](#deleting-blocks)
|
||||||
|
|
||||||
|
## Map Structure
|
||||||
|
|
||||||
|
The Minetest map is split into MapBlocks, each MapBlocks being a cube of
|
||||||
|
size 16. As players travel around the map, MapBlocks are created, loaded,
|
||||||
|
activated, and unloaded. Areas of the map which are not yet loaded are full of
|
||||||
|
*ignore* nodes, an impassable unselectable placeholder node. Empty space is
|
||||||
|
full of *air* nodes, an invisible node you can walk through.
|
||||||
|
|
||||||
|
An active MapBlock is one which is loaded and has updates running on it.
|
||||||
|
|
||||||
|
Loaded map blocks are often referred to as *active blocks*. Active Blocks can be
|
||||||
|
read from or written to by mods or players, and have active entities. The Engine
|
||||||
|
also performs operations on the map, such as performing liquid physics.
|
||||||
|
|
||||||
|
MapBlocks can either be loaded from the world 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. Existing MapBlocks can, however, be
|
||||||
|
loaded from the world database outside of the generation limit.
|
||||||
|
|
||||||
|
## Reading
|
||||||
|
|
||||||
|
### Reading Nodes
|
||||||
|
|
||||||
|
You can read from the map once you have a position:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local node = core.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 node name, which will be *ignore* when the area is unloaded.
|
||||||
|
* `param1` - See the node definition. This will commonly be light.
|
||||||
|
* `param2` - See the node definition.
|
||||||
|
|
||||||
|
It's worth noting that the function won't load the containing block if the block
|
||||||
|
is inactive, but will instead return a table with `name` being `ignore`.
|
||||||
|
|
||||||
|
You can use `core.get_node_or_nil` instead, which will return `nil` rather
|
||||||
|
than a table with a name of `ignore`. It still won't load the block, however.
|
||||||
|
This may 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`).
|
||||||
|
|
||||||
|
### Finding Nodes
|
||||||
|
|
||||||
|
Minetest offers a number of helper functions to speed up common map actions.
|
||||||
|
The most commonly used of these are for finding nodes.
|
||||||
|
|
||||||
|
For example, say we wanted to make a certain type of plant that grows
|
||||||
|
better near mese; you would need to search for any nearby mese nodes,
|
||||||
|
and adapt the growth rate accordingly.
|
||||||
|
|
||||||
|
`core.find_node_near` will return the first found node in a certain radius
|
||||||
|
which matches the node names or groups given. In the following example,
|
||||||
|
we look for a mese node within 5 nodes of the position:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local grow_speed = 1
|
||||||
|
local node_pos = core.find_node_near(pos, 5, { "default:mese" })
|
||||||
|
if node_pos then
|
||||||
|
core.chat_send_all("Node found at: " .. dump(node_pos))
|
||||||
|
grow_speed = 2
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's say, for example, that the growth rate increases the more mese there is
|
||||||
|
nearby. You should then use a function that can find multiple nodes in the area:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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 =
|
||||||
|
core.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
|
||||||
|
to `find_node_near`, which uses the distance to the position (ie: a *sphere*). In
|
||||||
|
order to fix this, we will need to manually check the range ourselves:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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 =
|
||||||
|
core.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
|
||||||
|
```
|
||||||
|
|
||||||
|
Now the code will correctly increase `grow_speed` based on mese nodes in range.
|
||||||
|
|
||||||
|
Note how we compared the squared distance from the position, rather than square
|
||||||
|
rooting it to obtain the actual distance. This is because computers find square
|
||||||
|
roots computationally expensive, so they should avoided 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
|
||||||
|
|
||||||
|
### Writing 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 run, which means that set_node
|
||||||
|
is fairly slow for large numbers of nodes.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.set_node({ x = 1, y = 3, z = 4 }, { name = "default:mese" })
|
||||||
|
|
||||||
|
local node = core.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 isn't desirable in all circumstances, especially if you're using multiple
|
||||||
|
node definitions to represent one conceptual node. An example of this is the
|
||||||
|
furnace node - whilst you conceptually think of it as one node, it's actually
|
||||||
|
two.
|
||||||
|
|
||||||
|
You can set a node without deleting metadata or the inventory like so:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.swap_node({ x = 1, y = 3, z = 4 }, { name = "default:mese" })
|
||||||
|
```
|
||||||
|
|
||||||
|
### Removing Nodes
|
||||||
|
|
||||||
|
A node must always be present. To remove a node, you set the position to `air`.
|
||||||
|
|
||||||
|
The following two lines will both remove a node, and are both identical:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.remove_node(pos)
|
||||||
|
core.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 `core.emerge_area` to load map blocks. Emerge area is asynchronous,
|
||||||
|
meaning the blocks won't be loaded instantly. Instead, they will be loaded
|
||||||
|
soon in the future, and the callback will be called each time.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Load a 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
|
||||||
|
core.emerge_area(pos1, pos2, emerge_callback, context)
|
||||||
|
```
|
||||||
|
|
||||||
|
Minetest will call `emerge_callback` whenever it loads a block, with some
|
||||||
|
progress information.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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
|
||||||
|
|
||||||
|
-- Increment number of blocks loaded
|
||||||
|
context.loaded_blocks = context.loaded_blocks + 1
|
||||||
|
|
||||||
|
-- Send progress message
|
||||||
|
if context.total_blocks == context.loaded_blocks then
|
||||||
|
core.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)
|
||||||
|
core.chat_send_all(msg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
This is not the only way of loading blocks; using an
|
||||||
|
[Lua Voxel Manipulator (LVM)](../advmap/lvm.html) will also cause the
|
||||||
|
encompassed blocks to be loaded synchronously.
|
||||||
|
|
||||||
|
## Deleting Blocks
|
||||||
|
|
||||||
|
You can use delete_blocks to delete a range of map blocks:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Delete a 20x20x20 area
|
||||||
|
local halfsize = { x = 10, y = 10, z = 10 }
|
||||||
|
local pos1 = vector.subtract(pos, halfsize)
|
||||||
|
local pos2 = vector.add (pos, halfsize)
|
||||||
|
|
||||||
|
core.delete_area(pos1, pos2)
|
||||||
|
```
|
||||||
|
|
||||||
|
This will delete all map blocks in that area, *inclusive*. This means that some
|
||||||
|
nodes will be deleted outside the area as they will be on a mapblock which overlaps
|
||||||
|
the area bounds.
|
361
_ru/map/objects.md
Normal file
361
_ru/map/objects.md
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
---
|
||||||
|
title: Objects, Players, and Entities
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 3.4
|
||||||
|
description: Using an ObjectRef
|
||||||
|
degrad:
|
||||||
|
level: warning
|
||||||
|
title: Degrees and Radians
|
||||||
|
message: Attachment rotation is set in degrees, whereas object rotation is in radians.
|
||||||
|
Make sure to convert to the correct angle measurement.
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
In this chapter, you will learn how to manipulate objects and how to define your
|
||||||
|
own.
|
||||||
|
|
||||||
|
- [What are Objects, Players, and Entities?](#what-are-objects-players-and-entities)
|
||||||
|
- [Position and Velocity](#position-and-velocity)
|
||||||
|
- [Object Properties](#object-properties)
|
||||||
|
- [Entities](#entities)
|
||||||
|
- [Health and Damage](#health-and-damage)
|
||||||
|
- [Health Points (HP)](#health-points-hp)
|
||||||
|
- [Punch, Damage Groups, and Armor Groups](#punch-damage-groups-and-armor-groups)
|
||||||
|
- [Example Damage Calculation](#example-damage-calculation)
|
||||||
|
- [Attachments](#attachments)
|
||||||
|
- [Your Turn](#your-turn)
|
||||||
|
|
||||||
|
## What are Objects, Players, and Entities?
|
||||||
|
|
||||||
|
Players and Entities are both types of Objects. An object is something that can move
|
||||||
|
independently of the node grid and has properties such as velocity and scale.
|
||||||
|
Objects aren't items, and they have their own separate registration system.
|
||||||
|
|
||||||
|
There are a few differences between Players and Entities.
|
||||||
|
The biggest one is that Players are player-controlled, whereas Entities are mod-controlled.
|
||||||
|
This means that the velocity of a player cannot be set by mods - players are client-side,
|
||||||
|
and entities are server-side.
|
||||||
|
Another difference is that Players will cause map blocks to be loaded, whereas Entities
|
||||||
|
will just be saved and become inactive.
|
||||||
|
|
||||||
|
This distinction is muddied by the fact that Entities are controlled using a table
|
||||||
|
which is referred to as a Lua entity, as discussed later.
|
||||||
|
|
||||||
|
## Position and Velocity
|
||||||
|
|
||||||
|
`get_pos` and `set_pos` exist to allow you to get and set an entity's position.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local object = core.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.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
object:move_to({ x = pos.x, y = pos.y + 1, z = pos.z })
|
||||||
|
```
|
||||||
|
|
||||||
|
An important thing to think about when dealing with entities is network latency.
|
||||||
|
In an ideal world, messages about entity movements would arrive immediately,
|
||||||
|
in the correct order, and with a similar interval as to how you sent them.
|
||||||
|
However, unless you're in singleplayer, this isn't an ideal world.
|
||||||
|
Messages will take a while to arrive. Position messages may arrive out of order,
|
||||||
|
resulting in some `set_pos` calls being skipped as there's no point going to
|
||||||
|
a position older than the current known position.
|
||||||
|
Moves may not be similarly spaced, which makes it difficult to use them for animation.
|
||||||
|
All this results in the client seeing different things to the server, which is something
|
||||||
|
you need to be aware of.
|
||||||
|
|
||||||
|
## Object Properties
|
||||||
|
|
||||||
|
Object properties are used to tell the client how to render and deal with an
|
||||||
|
object. It's not possible to define custom properties, because the properties are
|
||||||
|
for the engine to use, by definition.
|
||||||
|
|
||||||
|
Unlike nodes, objects have a dynamic rather than set appearance.
|
||||||
|
You can change how an object looks, among other things, at any time by updating
|
||||||
|
its properties.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
object:set_properties({
|
||||||
|
visual = "mesh",
|
||||||
|
mesh = "character.b3d",
|
||||||
|
textures = {"character_texture.png"},
|
||||||
|
visual_size = {x=1, y=1},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The updated properties will be sent to all players in range.
|
||||||
|
This is very useful to get a large amount of variety very cheaply, such as having
|
||||||
|
different skins per-player.
|
||||||
|
|
||||||
|
As shown in the next section, entities can have initial properties
|
||||||
|
provided in their definition.
|
||||||
|
The default Player properties are defined in the engine, however, so you'll
|
||||||
|
need to use `set_properties()` in `on_joinplayer` to set the properties for newly
|
||||||
|
joined players.
|
||||||
|
|
||||||
|
## Entities
|
||||||
|
|
||||||
|
An Entity has a definition table that resembles an item definition table.
|
||||||
|
This table can contain callback methods, initial object properties, and custom
|
||||||
|
members.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
Entity definitions differ in one very important way from Item definitions.
|
||||||
|
When an entity is emerged (ie: loaded or created), a new table is created for
|
||||||
|
that entity that *inherits* from the definition table.
|
||||||
|
|
||||||
|
<!--
|
||||||
|
This inheritance is done using a metatables.
|
||||||
|
Metatables are an important Lua feature that you will need to be aware of, as it
|
||||||
|
is an essential part of the Lua language. In layman's terms, a metatable allows
|
||||||
|
you to control how the table behaves when using certain Lua syntax. The most
|
||||||
|
common use of metatables is the ability to use another table as a prototype,
|
||||||
|
defaulting to the other table's properties and methods when they do not exist in
|
||||||
|
the current table.
|
||||||
|
Say you want to access `a.x`. If the table `a` has that member, then it will be
|
||||||
|
returned as normal. However, if the table doesn't have that member and the
|
||||||
|
metatable lists a table `b` as a prototype, then table `b` will be checked to
|
||||||
|
see if it has that member.
|
||||||
|
-->
|
||||||
|
|
||||||
|
Both an ObjectRef and an entity table provide ways to get the counterpart:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local entity = object:get_luaentity()
|
||||||
|
local object = entity.object
|
||||||
|
print("entity is at " .. core.pos_to_string(object:get_pos()))
|
||||||
|
```
|
||||||
|
|
||||||
|
There are a number of available callbacks for use with entities.
|
||||||
|
A complete list can be found in [lua_api.md](https://minetest.gitlab.io/minetest/minetest-namespace-reference/#registered-definition-tables).
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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 core.get_node(pos_down).name == "air" then
|
||||||
|
delta = vector.new(0, -1, 0)
|
||||||
|
elseif core.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)
|
||||||
|
core.chat_send_player(hitter:get_player_name(), self.message)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, if you were to spawn and use this entity, you'd notice that the message
|
||||||
|
would be forgotten when the entity becomes inactive then active again.
|
||||||
|
This is because the message isn't saved.
|
||||||
|
Rather than saving everything in the entity table, Minetest gives you control over
|
||||||
|
how to save things.
|
||||||
|
Staticdata is a string which contains all the custom information that
|
||||||
|
needs to stored.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function MyEntity:get_staticdata()
|
||||||
|
return core.write_json({
|
||||||
|
message = self.message,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function MyEntity:on_activate(staticdata, dtime_s)
|
||||||
|
if staticdata ~= "" and staticdata ~= nil then
|
||||||
|
local data = core.parse_json(staticdata) or {}
|
||||||
|
self:set_message(data.message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Minetest may call `get_staticdata()` as many times as it wants and at any time.
|
||||||
|
This is because Minetest doesn't wait for a MapBlock to become inactive to save
|
||||||
|
it, as this would result in data loss. MapBlocks are saved roughly every 18
|
||||||
|
seconds, so you should notice a similar interval for `get_staticdata()` being called.
|
||||||
|
|
||||||
|
`on_activate()`, on the other hand, will only be called when an entity becomes
|
||||||
|
active either from the MapBlock becoming active or from the entity spawning.
|
||||||
|
This means that staticdata could be empty.
|
||||||
|
|
||||||
|
Finally, you need to register the type table using the aptly named `register_entity`.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_entity("mymod:entity", MyEntity)
|
||||||
|
```
|
||||||
|
|
||||||
|
The entity can be spawned by a mod like so:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local pos = { x = 1, y = 2, z = 3 }
|
||||||
|
local obj = core.add_entity(pos, "mymod:entity", nil)
|
||||||
|
```
|
||||||
|
|
||||||
|
The third parameter is the initial staticdata.
|
||||||
|
To set the message, you can use the entity table method:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
obj:get_luaentity():set_message("hello!")
|
||||||
|
```
|
||||||
|
|
||||||
|
Players with the *give* [privilege](../players/privileges.html) can
|
||||||
|
use a [chat command](../players/chat.html) to spawn entities:
|
||||||
|
|
||||||
|
/spawnentity mymod:entity
|
||||||
|
|
||||||
|
|
||||||
|
## Health and Damage
|
||||||
|
|
||||||
|
### Health Points (HP)
|
||||||
|
|
||||||
|
Each object has a Health Points (HP) number, which represents the current health.
|
||||||
|
Players have a maximum hp set using the `hp_max` object property.
|
||||||
|
An object will die if its hp reaches 0.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local hp = object:get_hp()
|
||||||
|
object:set_hp(hp + 3)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Punch, Damage Groups, and Armor Groups
|
||||||
|
|
||||||
|
Damage is the reduction of an object's HP. An object can *punch* another object to
|
||||||
|
inflict damage. A punch isn't necessarily an actual punch - it can be an
|
||||||
|
explosion, a sword slash, or something else.
|
||||||
|
|
||||||
|
The total damage is calculated by multiplying the punch's damage groups with the
|
||||||
|
target's vulnerabilities. This is then limited depending on how recent the last
|
||||||
|
punch was. We will go over an example of this calculation in a bit.
|
||||||
|
|
||||||
|
Just like [node dig groups](../items/nodes_items_crafting.html#tools-capabilities-and-dig-types),
|
||||||
|
these groups can take any name and do not need to be registered. However, it's
|
||||||
|
common to use the same group names as with node digging.
|
||||||
|
|
||||||
|
How vulnerable an object is to particular types of damage depends on its
|
||||||
|
`armor_groups`. Despite its misleading name, `armor_groups` specify the
|
||||||
|
percentage damage taken from particular damage groups, not the resistance. If a
|
||||||
|
damage group is not listed in an object's armor groups, that object is
|
||||||
|
completely invulnerable to it.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
target:set_armor_groups({
|
||||||
|
fleshy = 90,
|
||||||
|
crumbly = 50,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
In the above example, the object will take 90% of `fleshy` damage and 50% of
|
||||||
|
`crumbly` damage.
|
||||||
|
|
||||||
|
When a player punches an object, the damage groups come from the item they are
|
||||||
|
currently wielding. In other cases, mods decide which damage groups are used.
|
||||||
|
|
||||||
|
### Example Damage Calculation
|
||||||
|
|
||||||
|
Let's punch the `target` object:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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)
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, let's work out what the damage will be. The punch's damage groups are
|
||||||
|
`fleshy=5` and `choppy=10`, and `target` will take 90% damage from fleshy and 0%
|
||||||
|
from choppy.
|
||||||
|
|
||||||
|
First, we multiply the damage groups by the vulnerability and sum the result.
|
||||||
|
We then multiply by a number between 0 or 1 depending on the `time_since_last_punch`.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
= (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
|
||||||
|
```
|
||||||
|
|
||||||
|
As HP is an integer, the damage is rounded to 5 points.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Attachments
|
||||||
|
|
||||||
|
Attached objects will move when the parent - the object they are attached to -
|
||||||
|
is moved. An attached object is said to be a child of the parent.
|
||||||
|
An object can have an unlimited number of children, but at most one parent.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
child:set_attach(parent, bone, position, rotation)
|
||||||
|
```
|
||||||
|
|
||||||
|
An object's `get_pos()` will always return the global position of the object, no
|
||||||
|
matter whether it is attached or not.
|
||||||
|
`set_attach` takes a relative position, but not as you'd expect.
|
||||||
|
The attachment position is relative to the parent's origin as scaled up by 10 times.
|
||||||
|
So, `0,5,0` would be half a node above the parent's origin.
|
||||||
|
|
||||||
|
{% include notice.html notice=page.degrad %}
|
||||||
|
|
||||||
|
For 3D models with animations, the bone argument is used to attach the entity
|
||||||
|
to a bone.
|
||||||
|
3D animations are based on skeletons - a network of bones in the model where
|
||||||
|
each bone can be given a position and rotation to change the model, for example,
|
||||||
|
to move the arm.
|
||||||
|
Attaching to a bone is useful if you want to make a character hold something:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Your Turn
|
||||||
|
|
||||||
|
* Make a windmill by combining nodes and an entity.
|
||||||
|
* Make a mob of your choice (using just the entity API, and without using any other mods).
|
247
_ru/map/storage.md
Normal file
247
_ru/map/storage.md
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
---
|
||||||
|
title: Storage and Metadata
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 3.3
|
||||||
|
description: Mod Storage, NodeMetaRef (get_meta).
|
||||||
|
redirect_from:
|
||||||
|
- /en/chapters/node_metadata.html
|
||||||
|
- /en/map/node_metadata.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
In this chapter, you will learn how you can store data.
|
||||||
|
|
||||||
|
- [Metadata](#metadata)
|
||||||
|
- [What is Metadata?](#what-is-metadata)
|
||||||
|
- [Obtaining a Metadata Object](#obtaining-a-metadata-object)
|
||||||
|
- [Reading and Writing](#reading-and-writing)
|
||||||
|
- [Special Keys](#special-keys)
|
||||||
|
- [Storing Tables](#storing-tables)
|
||||||
|
- [Private Metadata](#private-metadata)
|
||||||
|
- [Lua Tables](#lua-tables)
|
||||||
|
- [Mod Storage](#mod-storage)
|
||||||
|
- [Databases](#databases)
|
||||||
|
- [Deciding Which to Use](#deciding-which-to-use)
|
||||||
|
- [Your Turn](#your-turn)
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
### What is Metadata?
|
||||||
|
|
||||||
|
In Minetest, Metadata is a key-value store used to attach custom data to something.
|
||||||
|
You can use metadata to store information against a Node, Player, or ItemStack.
|
||||||
|
|
||||||
|
Each type of metadata uses the exact same API.
|
||||||
|
Metadata stores values as strings, but there are a number of methods to
|
||||||
|
convert and store other primitive types.
|
||||||
|
|
||||||
|
Some keys in metadata may have special meaning.
|
||||||
|
For example, `infotext` in node metadata is used to store the tooltip which shows
|
||||||
|
when hovering over the node using the crosshair.
|
||||||
|
To avoid conflicts with other mods, you should use the standard namespace
|
||||||
|
convention for keys: `modname:keyname`.
|
||||||
|
The exception is for conventional data such as the owner name which is stored as
|
||||||
|
`owner`.
|
||||||
|
|
||||||
|
Metadata is data about data.
|
||||||
|
The data itself, such as a node's type or an stack's count, is not metadata.
|
||||||
|
|
||||||
|
### Obtaining a Metadata Object
|
||||||
|
|
||||||
|
If you know the position of a node, you can retrieve its metadata:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local meta = core.get_meta({ x = 1, y = 2, z = 3 })
|
||||||
|
```
|
||||||
|
|
||||||
|
Player and ItemStack metadata are obtained using `get_meta()`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local pmeta = player:get_meta()
|
||||||
|
local imeta = stack:get_meta()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reading and Writing
|
||||||
|
|
||||||
|
In most cases, `get_<type>()` and `set_<type>()` methods will be used to read
|
||||||
|
and write to meta.
|
||||||
|
Metadata stores strings, so the string methods will directly set and get the value.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
print(meta:get_string("foo")) --> ""
|
||||||
|
meta:set_string("foo", "bar")
|
||||||
|
print(meta:get_string("foo")) --> "bar"
|
||||||
|
```
|
||||||
|
|
||||||
|
All of the typed getters will return a neutral default value if the key doesn't
|
||||||
|
exist, such as `""` or `0`.
|
||||||
|
You can use `get()` to return a string or nil.
|
||||||
|
|
||||||
|
As Metadata is a reference, any changes will be updated to the source automatically.
|
||||||
|
ItemStacks aren't references however, so you'll need to update the itemstack in the
|
||||||
|
inventory.
|
||||||
|
|
||||||
|
The non-typed getters and setters will convert to and from strings:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
print(meta:get_int("count")) --> 0
|
||||||
|
meta:set_int("count", 3)
|
||||||
|
print(meta:get_int("count")) --> 3
|
||||||
|
print(meta:get_string("count")) --> "3"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Special Keys
|
||||||
|
|
||||||
|
`infotext` is used in Node Metadata to show a tooltip when hovering the crosshair over a node.
|
||||||
|
This is useful when showing the ownership or status of a node.
|
||||||
|
|
||||||
|
`description` is used in ItemStack Metadata to override the description when
|
||||||
|
hovering over the stack in an inventory.
|
||||||
|
You can use colours by encoding them with `core.colorize()`.
|
||||||
|
|
||||||
|
`owner` is a common key used to store the username of the player that owns the
|
||||||
|
item or node.
|
||||||
|
|
||||||
|
### Storing Tables
|
||||||
|
|
||||||
|
Tables must be converted to strings before they can be stored.
|
||||||
|
Minetest offers two formats for doing this: Lua and JSON.
|
||||||
|
|
||||||
|
The Lua method tends to be a lot faster and matches the format Lua
|
||||||
|
uses for tables, while JSON is a more standard format, is better
|
||||||
|
structured, and is well suited for when you need to exchange information
|
||||||
|
with another program.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local data = { username = "player1", score = 1234 }
|
||||||
|
meta:set_string("foo", core.serialize(data))
|
||||||
|
|
||||||
|
data = core.deserialize(meta:get_string("foo"))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Private Metadata
|
||||||
|
|
||||||
|
By default, all node metadata is sent to the client.
|
||||||
|
You can mark keys as private to prevent this.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
meta:set_string("secret", "asd34dn")
|
||||||
|
meta:mark_as_private("secret")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lua Tables
|
||||||
|
|
||||||
|
You can convert to and from Lua tables using `to_table` and `from_table`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local tmp = meta:to_table()
|
||||||
|
tmp.foo = "bar"
|
||||||
|
meta:from_table(tmp)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mod Storage
|
||||||
|
|
||||||
|
Mod storage uses the exact same API as Metadata, although it's not technically
|
||||||
|
Metadata.
|
||||||
|
Mod storage is per-mod, and can only be obtained during load time in order to
|
||||||
|
know which mod is requesting it.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local storage = core.get_mod_storage()
|
||||||
|
```
|
||||||
|
|
||||||
|
You can now manipulate the storage just like metadata:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
storage:set_string("foo", "bar")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Databases
|
||||||
|
|
||||||
|
If the mod is likely to be used on a server and will store lots of data,
|
||||||
|
it's a good idea to offer a database storage method.
|
||||||
|
You should make this optional by separating how the data is stored and where
|
||||||
|
it is used.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local backend
|
||||||
|
if use_database then
|
||||||
|
backend =
|
||||||
|
dofile(core.get_modpath("mymod") .. "/backend_sqlite.lua")
|
||||||
|
else
|
||||||
|
backend =
|
||||||
|
dofile(core.get_modpath("mymod") .. "/backend_storage.lua")
|
||||||
|
end
|
||||||
|
|
||||||
|
backend.get_foo("a")
|
||||||
|
backend.set_foo("a", { score = 3 })
|
||||||
|
```
|
||||||
|
|
||||||
|
The backend_storage.lua file should include a mod storage implementation:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local storage = core.get_mod_storage()
|
||||||
|
local backend = {}
|
||||||
|
|
||||||
|
function backend.set_foo(key, value)
|
||||||
|
storage:set_string(key, core.serialize(value))
|
||||||
|
end
|
||||||
|
|
||||||
|
function backend.get_foo(key)
|
||||||
|
return core.deserialize(storage:get_string(key))
|
||||||
|
end
|
||||||
|
|
||||||
|
return backend
|
||||||
|
```
|
||||||
|
|
||||||
|
The backend_sqlite would do a similar thing, but use the Lua *lsqlite3* library
|
||||||
|
instead of mod storage.
|
||||||
|
|
||||||
|
Using a database such as SQLite requires using an insecure environment.
|
||||||
|
An insecure environment is a table that is only available to mods
|
||||||
|
explicitly whitelisted by the user, and it contains a less restricted
|
||||||
|
copy of the Lua API which could be abused if available to malicious mods.
|
||||||
|
Insecure environments will be covered in more detail in the
|
||||||
|
[Security](../quality/security.html) chapter.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local ie = core.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
|
||||||
|
```
|
||||||
|
|
||||||
|
Teaching about SQL or how to use the lsqlite3 library is out of scope for this book.
|
||||||
|
|
||||||
|
## Deciding Which to Use
|
||||||
|
|
||||||
|
The type of method you use depends on what the data is about,
|
||||||
|
how it is formatted, and how large it is.
|
||||||
|
As a guideline, small data is up to 10K, medium data is up to 10MB, and large
|
||||||
|
data is any size above that.
|
||||||
|
|
||||||
|
Node metadata is a good choice when you need to store node-related data.
|
||||||
|
Storing medium data is fairly efficient if you make it private.
|
||||||
|
|
||||||
|
Item metadata should not be used to store anything but small amounts of data as it is not
|
||||||
|
possible to avoid sending it to the client.
|
||||||
|
The data will also be copied every time the stack is moved, or accessed from Lua.
|
||||||
|
|
||||||
|
Mod storage is good for medium data but writing large data may be inefficient.
|
||||||
|
It's better to use a database for large data to avoid having to write all the
|
||||||
|
data out on every save.
|
||||||
|
|
||||||
|
Databases are only viable for servers due to the
|
||||||
|
need to whitelist the mod to access an insecure environment.
|
||||||
|
They're well suited for large data sets.
|
||||||
|
|
||||||
|
## Your Turn
|
||||||
|
|
||||||
|
* Make a node which disappears after it has been punched five times.
|
||||||
|
(Use `on_punch` in the node definition and `core.set_node`.)
|
110
_ru/map/timers.md
Normal file
110
_ru/map/timers.md
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
---
|
||||||
|
title: Node Timers and ABMs
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 3.2
|
||||||
|
description: Learn how to make ABMs to change blocks.
|
||||||
|
redirect_from:
|
||||||
|
- /en/chapters/abms.html
|
||||||
|
- /en/map/abms.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
Periodically running a function on certain nodes is a common task.
|
||||||
|
Minetest provides two methods of doing this: Active Block Modifiers (ABMs) and node timers.
|
||||||
|
|
||||||
|
ABMs scan all loaded MapBlocks looking for nodes that match a criteria.
|
||||||
|
They are best suited for nodes which are frequently found in the world,
|
||||||
|
such as grass.
|
||||||
|
They have a high CPU overhead, but a low memory and storage overhead.
|
||||||
|
|
||||||
|
For nodes that are uncommon or already use metadata, such as furnaces
|
||||||
|
and machines, node timers should be used instead.
|
||||||
|
Node timers work by keeping track of pending timers in each MapBlock, and then
|
||||||
|
running them when they expire.
|
||||||
|
This means that timers don't need to search all loaded nodes to find matches,
|
||||||
|
but instead require slightly more memory and storage for the tracking
|
||||||
|
of pending timers.
|
||||||
|
|
||||||
|
- [Node Timers](#node-timers)
|
||||||
|
- [Active Block Modifiers](#active-block-modifiers)
|
||||||
|
- [Your Turn](#your-turn)
|
||||||
|
|
||||||
|
## Node Timers
|
||||||
|
|
||||||
|
Node timers are directly tied to a single node.
|
||||||
|
You can manage node timers by obtaining a NodeTimerRef object.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local timer = core.get_node_timer(pos)
|
||||||
|
timer:start(10.5) -- in seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
When a node timer is up, the `on_timer` method in the node's definition table will
|
||||||
|
be called. The method only takes a single parameter, the position of the node:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_node("autodoors:door_open", {
|
||||||
|
on_timer = function(pos)
|
||||||
|
core.set_node(pos, { name = "autodoors:door" })
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Returning true in `on_timer` will cause the timer to run again for the same interval.
|
||||||
|
It's also possible to use `get_node_timer(pos)` inside of `on_timer`, just make
|
||||||
|
sure you return false to avoid conflict.
|
||||||
|
|
||||||
|
You may have noticed a limitation with timers: for optimisation reasons, it's
|
||||||
|
only possible to have one type of timer per node type, and only one timer running per node.
|
||||||
|
|
||||||
|
|
||||||
|
## Active Block Modifiers
|
||||||
|
|
||||||
|
Alien grass, for the purposes of this chapter, is a type of grass which
|
||||||
|
has a chance to appear near water.
|
||||||
|
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.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 = core.item_eat(20)
|
||||||
|
})
|
||||||
|
|
||||||
|
core.register_abm({
|
||||||
|
nodenames = {"default:dirt_with_grass"},
|
||||||
|
neighbors = {"default:water_source", "default:water_flowing"},
|
||||||
|
interval = 10.0, -- Run every 10 seconds
|
||||||
|
chance = 50, -- One node has a chance of 1 in 50 to get selected
|
||||||
|
action = function(pos, node, active_object_count,
|
||||||
|
active_object_count_wider)
|
||||||
|
local pos = {x = pos.x, y = pos.y + 1, z = pos.z}
|
||||||
|
core.set_node(pos, {name = "aliens:grass"})
|
||||||
|
end
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
This ABM runs every ten seconds, and for each matching node, there is
|
||||||
|
a 1 in 50 chance of it running.
|
||||||
|
If the ABM runs on a node, an alien grass node is placed above it.
|
||||||
|
Please be warned, this will delete any node previously located in that position.
|
||||||
|
To prevent this you should include a check using core.get_node to make sure there is space for the grass.
|
||||||
|
|
||||||
|
Specifying a neighbour is optional.
|
||||||
|
If you specify multiple neighbours, only one of them needs to be
|
||||||
|
present to meet the requirements.
|
||||||
|
|
||||||
|
Specifying chance is also optional.
|
||||||
|
If you don't specify the chance, the ABM will always run when the other conditions are met.
|
||||||
|
|
||||||
|
## Your Turn
|
||||||
|
|
||||||
|
* Midas touch: Make water turn to gold blocks with a 1 in 100 chance, every 5 seconds.
|
||||||
|
* Decay: Make wood turn into dirt when water is a neighbour.
|
||||||
|
* Burnin': Make every air node catch on fire. (Tip: "air" and "fire:basic_flame").
|
||||||
|
Warning: expect the game to crash.
|
197
_ru/players/chat.md
Normal file
197
_ru/players/chat.md
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
---
|
||||||
|
title: Chat and Commands
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 4.2
|
||||||
|
description: Registering a chatcommand and handling chat messages with register_on_chat_message
|
||||||
|
redirect_from: /en/chapters/chat.html
|
||||||
|
cmd_online:
|
||||||
|
level: warning
|
||||||
|
title: Offline players can run commands
|
||||||
|
message: |
|
||||||
|
A player name is passed instead of a player object because mods
|
||||||
|
can run commands on behalf of offline players. For example, the IRC
|
||||||
|
bridge allows players to run commands without joining the game.
|
||||||
|
|
||||||
|
So make sure that you don't assume that the player is online.
|
||||||
|
You can check by seeing if `core.get_player_by_name` returns a player.
|
||||||
|
|
||||||
|
cb_cmdsprivs:
|
||||||
|
level: warning
|
||||||
|
title: Privileges and Chat Commands
|
||||||
|
message: |
|
||||||
|
The shout privilege isn't needed for a player to trigger this callback.
|
||||||
|
This is because chat commands are implemented in Lua, and are just
|
||||||
|
chat messages that begin with a /.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
Mods can interact with player chat, including
|
||||||
|
sending messages, intercepting messages, and registering chat commands.
|
||||||
|
|
||||||
|
- [Sending Messages](#sending-messages)
|
||||||
|
- [To All Players](#to-all-players)
|
||||||
|
- [To Specific Players](#to-specific-players)
|
||||||
|
- [Chat Commands](#chat-commands)
|
||||||
|
- [Accepting Multiple Arguments](#accepting-multiple-arguments)
|
||||||
|
- [Using string.split](#using-stringsplit)
|
||||||
|
- [Using Lua patterns](#using-lua-patterns)
|
||||||
|
- [Intercepting Messages](#intercepting-messages)
|
||||||
|
|
||||||
|
## Sending Messages
|
||||||
|
|
||||||
|
### To All Players
|
||||||
|
|
||||||
|
To send a message to every player in the game, call the `chat_send_all` function.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.chat_send_all("This is a chat message to all players")
|
||||||
|
```
|
||||||
|
|
||||||
|
Here is an example of how this appears in-game:
|
||||||
|
|
||||||
|
<player1> Look at this entrance
|
||||||
|
This is a chat message to all players
|
||||||
|
<player2> What about it?
|
||||||
|
|
||||||
|
The message appears on a separate line to distinguish it from in-game player chat.
|
||||||
|
|
||||||
|
### To Specific Players
|
||||||
|
|
||||||
|
To send a message to a specific player, call the `chat_send_player` function:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.chat_send_player("player1", "This is a chat message for player1")
|
||||||
|
```
|
||||||
|
|
||||||
|
This message displays in the same manner as messages to all players, but is
|
||||||
|
only visible to the named player, in this case, player1.
|
||||||
|
|
||||||
|
## Chat Commands
|
||||||
|
|
||||||
|
To register a chat command, for example `/foo`, use `register_chatcommand`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_chatcommand("foo", {
|
||||||
|
privs = {
|
||||||
|
interact = true,
|
||||||
|
},
|
||||||
|
func = function(name, param)
|
||||||
|
return true, "You said " .. param .. "!"
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
In the above snippet, `interact` is listed as a required
|
||||||
|
[privilege](privileges.html) meaning that only players with the `interact` privilege can run the command.
|
||||||
|
|
||||||
|
`param` is a string containing everything a player writes after the chatcommand
|
||||||
|
name. For example, if a user types `/grantme one,two,three` then `param` will be
|
||||||
|
`one,two,three`.
|
||||||
|
|
||||||
|
Chat commands can return up to two values,
|
||||||
|
the first being a Boolean indicating success, and the second being a
|
||||||
|
message to send to the user.
|
||||||
|
|
||||||
|
{% include notice.html notice=page.cmd_online %}
|
||||||
|
|
||||||
|
### Accepting Multiple Arguments
|
||||||
|
|
||||||
|
<a name="complex-subcommands"></a>
|
||||||
|
|
||||||
|
`param` gives you all the arguments to a chat command in a single string. It's
|
||||||
|
common for chat commands to need to extract multiple arguments. There are two
|
||||||
|
ways of doing this, either using Minetest's string split or Lua patterns.
|
||||||
|
|
||||||
|
#### Using string.split
|
||||||
|
|
||||||
|
A string can be split up into words using `string.split(" ")`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local parts = param:split(" ")
|
||||||
|
local cmd = parts[1]
|
||||||
|
|
||||||
|
if cmd == "join" then
|
||||||
|
local team_name = parts[2]
|
||||||
|
team.join(name, team_name)
|
||||||
|
return true, "Joined team!"
|
||||||
|
elseif cmd == "max_users" then
|
||||||
|
local team_name = parts[2]
|
||||||
|
local max_users = tonumber(parts[3])
|
||||||
|
if team_name and max_users then
|
||||||
|
return true, "Set max users of team " .. team_name .. " to " .. max_users
|
||||||
|
else
|
||||||
|
return false, "Usage: /team max_users <team_name> <number>"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return false, "Command needed"
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Using Lua patterns
|
||||||
|
|
||||||
|
[Lua patterns](https://www.lua.org/pil/20.2.html) are a way of extracting stuff
|
||||||
|
from text using rules. They're best suited for when there are arguments that can
|
||||||
|
contain spaces or more control is needed on how parameters are captured.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local to, msg = param:match("^([%a%d_-]+) (.+)$")
|
||||||
|
```
|
||||||
|
|
||||||
|
The above code implements `/msg <to> <message>`. Let's go through left to right:
|
||||||
|
|
||||||
|
* `^` means match the start of the string.
|
||||||
|
* `()` is a matching group - anything that matches stuff in here will be
|
||||||
|
returned from string.match.
|
||||||
|
* `[]` means accept characters in this list.
|
||||||
|
* `%a` means accept any letter and `%d` means accept any digit.
|
||||||
|
* `[%a%d_-]` means accept any letter or digit or `_` or `-`.
|
||||||
|
* `+` means match the thing before one or more times.
|
||||||
|
* `.` means match any character in this context.
|
||||||
|
* `$` means match the end of the string.
|
||||||
|
|
||||||
|
Put simply, the pattern matches the name (a word with only letters/numbers/-/_),
|
||||||
|
then a space, then the message (one or more of any character). The name and
|
||||||
|
message are returned, because they're surrounded by parentheses.
|
||||||
|
|
||||||
|
That's how most mods implement complex chat commands. A better guide to Lua
|
||||||
|
Patterns would probably be the
|
||||||
|
[lua-users.org tutorial](http://lua-users.org/wiki/PatternsTutorial)
|
||||||
|
or the [PIL documentation](https://www.lua.org/pil/20.2.html).
|
||||||
|
|
||||||
|
## Intercepting Messages
|
||||||
|
|
||||||
|
To intercept a message, use register_on_chat_message:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_on_chat_message(function(name, message)
|
||||||
|
print(name .. " said " .. message)
|
||||||
|
return false
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
By returning false, you allow the chat message to be sent by the default
|
||||||
|
handler. You can actually remove the line `return false` and it would still
|
||||||
|
work the same, because `nil` is returned implicitly and is treated like false.
|
||||||
|
|
||||||
|
{% include notice.html notice=page.cb_cmdsprivs %}
|
||||||
|
|
||||||
|
You should make sure you take into account that it may be a chat command,
|
||||||
|
or the user may not have `shout`.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_on_chat_message(function(name, message)
|
||||||
|
if message:sub(1, 1) == "/" then
|
||||||
|
print(name .. " ran chat command")
|
||||||
|
elseif core.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)
|
||||||
|
```
|
379
_ru/players/formspecs.md
Normal file
379
_ru/players/formspecs.md
Normal file
@ -0,0 +1,379 @@
|
|||||||
|
---
|
||||||
|
title: GUIs (Formspecs)
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 4.5
|
||||||
|
description: Learn how to display GUIs using formspecs
|
||||||
|
redirect_from: /en/chapters/formspecs.html
|
||||||
|
submit_vuln:
|
||||||
|
level: warning
|
||||||
|
title: Malicious clients can submit anything at anytime
|
||||||
|
message: You should never trust a formspec submission. A malicious client
|
||||||
|
can submit anything they like at any time - even if you never showed
|
||||||
|
them the formspec. This means that you should check privileges
|
||||||
|
and make sure that they should be allowed to perform the action.
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img src="{{ page.root }}//static/formspec_example.png" alt="Furnace Inventory">
|
||||||
|
<figcaption>
|
||||||
|
Screenshot of furnace formspec, labelled.
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
In this chapter we will learn how to create a formspec and display it to the user.
|
||||||
|
A formspec is the specification code for a form.
|
||||||
|
In Minetest, forms are windows such as the player inventory and can contain a
|
||||||
|
variety of elements, such as labels, buttons and fields.
|
||||||
|
|
||||||
|
Note that if you do not need to get user input, for example when you only need
|
||||||
|
to provide information to the player, you should consider using
|
||||||
|
[Heads Up Display (HUD)](hud.html) elements instead of forms, because
|
||||||
|
unexpected windows tend to disrupt gameplay.
|
||||||
|
|
||||||
|
- [Real or Legacy Coordinates](#real-or-legacy-coordinates)
|
||||||
|
- [Anatomy of a Formspec](#anatomy-of-a-formspec)
|
||||||
|
- [Elements](#elements)
|
||||||
|
- [Header](#header)
|
||||||
|
- [Guessing Game](#guessing-game)
|
||||||
|
- [Padding and Spacing](#padding-and-spacing)
|
||||||
|
- [Receiving Formspec Submissions](#receiving-formspec-submissions)
|
||||||
|
- [Contexts](#contexts)
|
||||||
|
- [Formspec Sources](#formspec-sources)
|
||||||
|
- [Node Meta Formspecs](#node-meta-formspecs)
|
||||||
|
- [Player Inventory Formspecs](#player-inventory-formspecs)
|
||||||
|
- [Your Turn](#your-turn)
|
||||||
|
|
||||||
|
|
||||||
|
## Real or Legacy Coordinates
|
||||||
|
|
||||||
|
In older versions of Minetest, formspecs were inconsistent. The way that different
|
||||||
|
elements were positioned varied in unexpected ways; it was hard to predict the
|
||||||
|
placement of elements and align them. Minetest 5.1.0 contains a feature
|
||||||
|
called real coordinates which aims to rectify this by introducing a consistent
|
||||||
|
coordinate system. The use of real coordinates is highly recommended, and so
|
||||||
|
this chapter will use them exclusively.
|
||||||
|
|
||||||
|
Using a formspec_version of 2 or above will enable real coordinates.
|
||||||
|
|
||||||
|
## Anatomy of a Formspec
|
||||||
|
|
||||||
|
### Elements
|
||||||
|
|
||||||
|
Formspec is a domain-specific language with an unusual format.
|
||||||
|
It consists of a number of elements with the following form:
|
||||||
|
|
||||||
|
type[param1;param2]
|
||||||
|
|
||||||
|
The element type is declared and then any parameters are given
|
||||||
|
in square brackets. Multiple elements can be joined together, or placed
|
||||||
|
on multiple lines, like so:
|
||||||
|
|
||||||
|
foo[param1]bar[param1]
|
||||||
|
bo[param1]
|
||||||
|
|
||||||
|
|
||||||
|
Elements are items such as text boxes or buttons, or can be metadata such
|
||||||
|
as size or background. You should refer to
|
||||||
|
[lua_api.md](https://minetest.gitlab.io/minetest/formspec/)
|
||||||
|
for a list of all possible elements.
|
||||||
|
|
||||||
|
|
||||||
|
### Header
|
||||||
|
|
||||||
|
The header of a formspec contains information which must appear first. This
|
||||||
|
includes the size of the formspec, the position, the anchor, and whether the
|
||||||
|
game-wide theme should be applied.
|
||||||
|
|
||||||
|
The elements in the header must be defined in a specific order, otherwise you
|
||||||
|
will see an error. This order is given in the above paragraph, and, as always,
|
||||||
|
documented in the Lua API reference.
|
||||||
|
|
||||||
|
The size is in formspec slots - a unit of measurement which is roughly
|
||||||
|
around 64 pixels, but varies based on the screen density and scaling
|
||||||
|
settings of the client. Here's a formspec which is `2,2` in size:
|
||||||
|
|
||||||
|
formspec_version[4]
|
||||||
|
size[2,2]
|
||||||
|
|
||||||
|
Notice how we explicitly defined the formspec language version.
|
||||||
|
Without this, the legacy system will instead be used instead - which will
|
||||||
|
prevent the use of consistent element positioning and other new features.
|
||||||
|
|
||||||
|
The position and anchor elements are used to place the formspec on the screen.
|
||||||
|
The position sets where on the screen the formspec will be, and defaults to
|
||||||
|
the center (`0.5,0.5`). The anchor sets where on the formspec the position is,
|
||||||
|
allowing you to line the formspec up with the edge of the screen. The formspec
|
||||||
|
can be placed to the left of the screen like so:
|
||||||
|
|
||||||
|
formspec_version[4]
|
||||||
|
size[2,2]
|
||||||
|
position[0,0.5]
|
||||||
|
anchor[0,0.5]
|
||||||
|
|
||||||
|
This sets the anchor to the left middle edge of the formspec box, and then the
|
||||||
|
position of that anchor to the left of the screen.
|
||||||
|
|
||||||
|
|
||||||
|
## Guessing Game
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img src="{{ page.root }}/static/formspec_guessing.png" alt="Guessing Formspec">
|
||||||
|
<figcaption>
|
||||||
|
The guessing game formspec.
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
The best way to learn is to make something, so let's make a guessing game.
|
||||||
|
The principle is simple: the mod decides on a number, then the player makes
|
||||||
|
guesses on the number. The mod then says if the guess is higher or lower then
|
||||||
|
the actual number.
|
||||||
|
|
||||||
|
First, let's make a function to create the formspec code. It's good practice to
|
||||||
|
do this, as it makes it easier to reuse elsewhere.
|
||||||
|
|
||||||
|
<div style="clear: both;"></div>
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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;", core.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
|
||||||
|
```
|
||||||
|
|
||||||
|
In the above code, we place a field, a label, and a button. A field allows text
|
||||||
|
entry, and a button is used to submit the form. You'll notice that the elements
|
||||||
|
are positioned carefully in order to add padding and spacing, this will be explained
|
||||||
|
later.
|
||||||
|
|
||||||
|
Next, we want to allow the player to show the formspec. The main way to do this
|
||||||
|
is using `show_formspec`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function guessing.show_to(name)
|
||||||
|
core.show_formspec(name, "guessing:game", guessing.get_formspec(name))
|
||||||
|
end
|
||||||
|
|
||||||
|
core.register_chatcommand("game", {
|
||||||
|
func = function(name)
|
||||||
|
guessing.show_to(name)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The `show_formspec` function accepts a player name, the formspec name, and the
|
||||||
|
formspec itself. The formspec name should be a valid itemname, ie: in the format
|
||||||
|
`modname:itemname`.
|
||||||
|
|
||||||
|
|
||||||
|
### Padding and Spacing
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img src="{{ page.root }}/static/formspec_padding_spacing.png" alt="Padding and spacing">
|
||||||
|
<figcaption>
|
||||||
|
The guessing game formspec.
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
Padding is the gap between the edge of the formspec and its contents, or between unrelated
|
||||||
|
elements, shown in red. Spacing is the gap between related elements, shown in blue.
|
||||||
|
|
||||||
|
It is fairly standard to have a padding of `0.375` and a spacing of `0.25`.
|
||||||
|
|
||||||
|
<div style="clear: both;"></div>
|
||||||
|
|
||||||
|
|
||||||
|
### Receiving Formspec Submissions
|
||||||
|
|
||||||
|
When `show_formspec` is called, the formspec is sent to the client to be displayed.
|
||||||
|
For formspecs to be useful, information needs to be returned from the client to server.
|
||||||
|
The method for this is called formspec field submission, and for `show_formspec`, that
|
||||||
|
submission is received using a global callback:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.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()
|
||||||
|
core.chat_send_all(pname .. " guessed " .. fields.number)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
The function given in `core.register_on_player_receive_fields` is called
|
||||||
|
every time a user submits a form. Most callbacks will need to check the formname given
|
||||||
|
to the function, and exit if it is not the right form; however, some callbacks
|
||||||
|
may need to work on multiple forms, or on all forms.
|
||||||
|
|
||||||
|
The `fields` parameter to the function is a table of the values submitted by the
|
||||||
|
user, indexed by strings. Named elements will appear in the field under their own
|
||||||
|
name, depending on the event. Some elements will only be submitted if they caused
|
||||||
|
the event, such as buttons, and some elements will always appear in submissions,
|
||||||
|
such as fields.
|
||||||
|
|
||||||
|
{% include notice.html notice=page.submit_vuln %}
|
||||||
|
|
||||||
|
So, now the formspec is sent to the client and the client sends information back.
|
||||||
|
The next step is to somehow generate and remember the target value, and to update
|
||||||
|
the formspec based on guesses. The way to do this is using a concept called
|
||||||
|
"contexts".
|
||||||
|
|
||||||
|
|
||||||
|
### Contexts
|
||||||
|
|
||||||
|
In many cases you want core.show_formspec to give information
|
||||||
|
to the callback which you don't want to send to the client. This might include
|
||||||
|
what a chat command was called with, or what the dialog is about. In this case,
|
||||||
|
the target value that needs to be remembered.
|
||||||
|
|
||||||
|
A context is a per-player table to store information, and the contexts for all
|
||||||
|
online players are stored in a file-local variable:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local _contexts = {}
|
||||||
|
local function get_context(name)
|
||||||
|
local context = _contexts[name] or {}
|
||||||
|
_contexts[name] = context
|
||||||
|
return context
|
||||||
|
end
|
||||||
|
|
||||||
|
core.register_on_leaveplayer(function(player)
|
||||||
|
_contexts[player:get_player_name()] = nil
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we need to modify the show code to update the context
|
||||||
|
before showing the formspec:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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)
|
||||||
|
core.show_formspec(name, "guessing:game", fs)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
We also need to modify the formspec generation code to use the context:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that it's good practice for `get_formspec` to only read the context, and not
|
||||||
|
update it at all. This can make the function simpler, and also easier to test.
|
||||||
|
|
||||||
|
And finally, we need to update the handler to update the context with the guess:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Formspec Sources
|
||||||
|
|
||||||
|
There are three different ways that a formspec can be delivered to the client:
|
||||||
|
|
||||||
|
* [show_formspec](#guessing-game): the method used above, fields are received by `register_on_player_receive_fields`.
|
||||||
|
* [Node Meta Formspecs](#node-meta-formspecs): the node contains a formspec in its meta data, and the client
|
||||||
|
shows it *immediately* when the player rightclicks. Fields are received by a
|
||||||
|
method in the node definition called `on_receive_fields`.
|
||||||
|
* [Player Inventory Formspecs](#player-inventory-formspecs): the formspec is sent to the client at some point, and then
|
||||||
|
shown immediately when the player presses `i`. Fields are received by
|
||||||
|
`register_on_player_receive_fields`.
|
||||||
|
|
||||||
|
### Node Meta Formspecs
|
||||||
|
|
||||||
|
`core.show_formspec` is not the only way to show a formspec; you can also
|
||||||
|
add formspecs to a [node's metadata](../map/storage.html). For example,
|
||||||
|
this is used with chests to allow for faster opening times -
|
||||||
|
you don't need to wait for the server to send the player the chest formspec.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.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 = core.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
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Formspecs set this way do not trigger the same callback. In order to
|
||||||
|
receive form input for meta formspecs, you must include an
|
||||||
|
`on_receive_fields` entry when registering the node.
|
||||||
|
|
||||||
|
This style of callback triggers when you press enter
|
||||||
|
in a field, which is impossible with `core.show_formspec`;
|
||||||
|
however, this kind of form can only be shown by right-clicking on a
|
||||||
|
node. It cannot be triggered programmatically.
|
||||||
|
|
||||||
|
### Player Inventory Formspecs
|
||||||
|
|
||||||
|
The player inventory formspec is the one shown when the player presses i.
|
||||||
|
The global callback is used to receive events from this formspec, and the
|
||||||
|
formname is `""`.
|
||||||
|
|
||||||
|
There are a number of different mods which allow multiple mods to customise the
|
||||||
|
player inventory. Minetest Game uses
|
||||||
|
[SFINV](https://github.com/rubenwardy/sfinv/blob/master/Tutorial.md).
|
||||||
|
|
||||||
|
|
||||||
|
### Your Turn
|
||||||
|
|
||||||
|
* Extend the Guessing Game to keep track of each player's top score, where the
|
||||||
|
top score is how many guesses it took.
|
||||||
|
* Make a node called "Inbox" where users can open up a formspec and leave messages.
|
||||||
|
This node should store the placers' name as `owner` in the meta, and should use
|
||||||
|
`show_formspec` to show different formspecs to different players.
|
294
_ru/players/hud.md
Normal file
294
_ru/players/hud.md
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
---
|
||||||
|
title: HUD
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 4.6
|
||||||
|
description: Learn how to display HUD elements
|
||||||
|
redirect_from: /en/chapters/hud.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
Heads Up Display (HUD) elements allow you to show text, images, and other graphical elements.
|
||||||
|
|
||||||
|
The HUD doesn't accept user input; for that, you should use a [formspec](formspecs.html).
|
||||||
|
|
||||||
|
- [Positioning](#positioning)
|
||||||
|
- [Position and Offset](#position-and-offset)
|
||||||
|
- [Alignment](#alignment)
|
||||||
|
- [Scoreboard](#scoreboard)
|
||||||
|
- [Text Elements](#text-elements)
|
||||||
|
- [Parameters](#parameters)
|
||||||
|
- [Our Example](#our-example)
|
||||||
|
- [Image Elements](#image-elements)
|
||||||
|
- [Parameters](#parameters-1)
|
||||||
|
- [Scale](#scale)
|
||||||
|
- [Changing an Element](#changing-an-element)
|
||||||
|
- [Storing IDs](#storing-ids)
|
||||||
|
- [Other Elements](#other-elements)
|
||||||
|
|
||||||
|
## Positioning
|
||||||
|
|
||||||
|
### Position and Offset
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img
|
||||||
|
width="300"
|
||||||
|
src="{{ page.root }}//static/hud_diagram_center.svg"
|
||||||
|
alt="Diagram showing a centered text element">
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
Screens come in a variety of different physical sizes and resolutions, and
|
||||||
|
the HUD needs to work well on all screen types.
|
||||||
|
|
||||||
|
Minetest's solution to this is to specify the location of an element using both
|
||||||
|
a percentage position and an offset.
|
||||||
|
The percentage position is relative to the screen size, so to place an element
|
||||||
|
in the centre of the screen, you would need to provide a percentage position of half
|
||||||
|
the screen, e.g. (50%, 50%), and an offset of (0, 0).
|
||||||
|
|
||||||
|
The offset is then used to move an element relative to the percentage position.
|
||||||
|
|
||||||
|
<div style="clear:both;"></div>
|
||||||
|
|
||||||
|
### Alignment
|
||||||
|
|
||||||
|
Alignment is where the result of position and offset is on the element -
|
||||||
|
for example, `{x = -1.0, y = 0.0}` will make the result of position and offset point
|
||||||
|
to the left of the element's bounds. This is particularly useful when you want to
|
||||||
|
make a text element aligned to the left, centre, or right.
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img
|
||||||
|
width="500"
|
||||||
|
src="{{ page.root }}//static/hud_diagram_alignment.svg"
|
||||||
|
alt="Diagram showing alignment">
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
The above diagram shows 3 windows (blue), each with a single HUD element (yellow)
|
||||||
|
and a different alignment each time. The arrow is the result of the position
|
||||||
|
and offset calculation.
|
||||||
|
|
||||||
|
### Scoreboard
|
||||||
|
|
||||||
|
For the purposes of this chapter, you will learn how to position and update a
|
||||||
|
score panel like so:
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img
|
||||||
|
src="{{ page.root }}//static/hud_final.png"
|
||||||
|
alt="screenshot of the HUD we're aiming for">
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
In the above screenshot, all the elements have the same percentage position
|
||||||
|
(100%, 50%) - but different offsets. This allows the whole thing to be anchored
|
||||||
|
to the right of the window, but to resize without breaking.
|
||||||
|
|
||||||
|
## Text Elements
|
||||||
|
|
||||||
|
You can create a HUD element once you have a copy of the player object:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local player = core.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
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The `hud_add` function returns an element ID - this can be used later to modify
|
||||||
|
or remove a HUD element.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
The element's type is given using the `hud_elem_type` property in the definition
|
||||||
|
table. The meaning of other properties varies based on this type.
|
||||||
|
|
||||||
|
`scale` is the maximum bounds of text; text outside these bounds is cropped, e.g.: `{x=100, y=100}`.
|
||||||
|
|
||||||
|
`number` is the text's colour, and is in [hexadecimal form](http://www.colorpicker.com/), e.g.: `0xFF0000`.
|
||||||
|
|
||||||
|
|
||||||
|
### Our Example
|
||||||
|
|
||||||
|
Let's go ahead and place all the text in our score panel:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- 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,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
This results in the following:
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img
|
||||||
|
src="{{ page.root }}//static/hud_text.png"
|
||||||
|
alt="screenshot of the HUD we're aiming for">
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
|
||||||
|
## Image Elements
|
||||||
|
|
||||||
|
Image elements are created in a very similar way to text elements:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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 },
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
You will now have this:
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img
|
||||||
|
src="{{ page.root }}//static/hud_background_img.png"
|
||||||
|
alt="screenshot of the HUD so far">
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
The `text` field is used to provide the image name.
|
||||||
|
|
||||||
|
If a co-ordinate is positive, then it is a scale factor with 1 being the
|
||||||
|
original image size, 2 being double the size, and so on.
|
||||||
|
However, if a co-ordinate is negative, it is a percentage of the screen size.
|
||||||
|
For example, `x=-100` is 100% of the width.
|
||||||
|
|
||||||
|
### Scale
|
||||||
|
|
||||||
|
Let's make the progress bar for our score panel as an example of scale:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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 },
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
We now have a HUD that looks like the one in the first post!
|
||||||
|
There is one problem however, it won't update when the stats change.
|
||||||
|
|
||||||
|
## Changing an Element
|
||||||
|
|
||||||
|
You can use the ID returned by the `hud_add` method to update it or remove it later.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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)
|
||||||
|
```
|
||||||
|
|
||||||
|
The `hud_change` method takes the element ID, the property to change, and the new
|
||||||
|
value. The above call changes the `text` property from "Hello World" to "New text".
|
||||||
|
|
||||||
|
This means that doing the `hud_change` immediately after the `hud_add` is
|
||||||
|
functionally equivalent to the following, in a rather inefficient way:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local idx = player:hud_add({
|
||||||
|
hud_elem_type = "text",
|
||||||
|
text = "New Text",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Storing IDs
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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
|
||||||
|
|
||||||
|
core.register_on_joinplayer(score.update_hud)
|
||||||
|
|
||||||
|
core.register_on_leaveplayer(function(player)
|
||||||
|
saved_huds[player:get_player_name()] = nil
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Other Elements
|
||||||
|
|
||||||
|
Read [lua_api.md](https://minetest.gitlab.io/minetest/hud/) for a complete list of HUD elements.
|
77
_ru/players/player_physics.md
Normal file
77
_ru/players/player_physics.md
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
---
|
||||||
|
title: Player Physics
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 4.4
|
||||||
|
description: Learn how to make a player run faster, jump higher or simply float
|
||||||
|
redirect_from: /en/chapters/player_physics.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
Player physics can be modified using physics overrides.
|
||||||
|
Physics overrides can set the walking speed, jump speed,
|
||||||
|
and gravity constants.
|
||||||
|
Physics overrides are set on a player-by-player basis
|
||||||
|
and are multipliers.
|
||||||
|
For example, a value of 2 for gravity would make gravity twice as strong.
|
||||||
|
|
||||||
|
- [Basic Example](#basic-example)
|
||||||
|
- [Available Overrides](#available-overrides)
|
||||||
|
- [Old Movement Behaviour](#old-movement-behaviour)
|
||||||
|
- [Mod Incompatibility](#mod-incompatibility)
|
||||||
|
- [Your Turn](#your-turn)
|
||||||
|
|
||||||
|
## Basic Example
|
||||||
|
|
||||||
|
Here is an example of how to add an antigravity command, which
|
||||||
|
puts the caller in low G:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_chatcommand("antigravity", {
|
||||||
|
func = function(name, param)
|
||||||
|
local player = core.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,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Overrides
|
||||||
|
|
||||||
|
`player:set_physics_override()` is given a table of overrides.\\
|
||||||
|
According to [lua_api.md](https://minetest.gitlab.io/minetest/class-reference/#player-only-no-op-for-other-objects),
|
||||||
|
these can be:
|
||||||
|
|
||||||
|
* speed: multiplier to default walking speed value (default: 1)
|
||||||
|
* jump: multiplier to default jump value (default: 1)
|
||||||
|
* gravity: multiplier to default gravity value (default: 1)
|
||||||
|
* sneak: whether the player can sneak (default: true)
|
||||||
|
|
||||||
|
### Old Movement Behaviour
|
||||||
|
|
||||||
|
Player movement prior to the 0.4.16 release included the sneak glitch, which
|
||||||
|
allows various movement glitches, including the ability
|
||||||
|
to climb an 'elevator' made from a certain placement of nodes by sneaking
|
||||||
|
(pressing shift) and pressing space to ascend. Though the behaviour was
|
||||||
|
unintended, it has been preserved in overrides due to its use on many servers.
|
||||||
|
|
||||||
|
Two overrides are needed to fully restore old movement behaviour:
|
||||||
|
|
||||||
|
* new_move: whether the player uses new movement (default: true)
|
||||||
|
* sneak_glitch: whether the player can use 'sneak elevators' (default: false)
|
||||||
|
|
||||||
|
## Mod Incompatibility
|
||||||
|
|
||||||
|
Please be warned that mods which override the same physics value of a player tend
|
||||||
|
to be incompatible with each other. When setting an override, it overwrites
|
||||||
|
any overrides that have been set before. This means that if multiple overrides set a
|
||||||
|
player's speed, only the last one to run will be in effect.
|
||||||
|
|
||||||
|
## Your Turn
|
||||||
|
|
||||||
|
* **Sonic**: Set the speed multiplier to a high value (at least 6) when a player joins the game.
|
||||||
|
* **Super bounce**: Increase the jump value so that the player can jump 20 metres (1 metre is 1 node).
|
||||||
|
* **Space**: Make gravity decrease as the player gets higher.
|
138
_ru/players/privileges.md
Normal file
138
_ru/players/privileges.md
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
---
|
||||||
|
title: Privileges
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 4.1
|
||||||
|
description: Registering privs.
|
||||||
|
redirect_from: /en/chapters/privileges.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
Privileges, often called privs for short, give players the ability to perform
|
||||||
|
certain actions. Server owners can grant and revoke privileges to control
|
||||||
|
which abilities each player has.
|
||||||
|
|
||||||
|
- [When to use Privileges](#when-to-use-privileges)
|
||||||
|
- [Declaring Privileges](#declaring-privileges)
|
||||||
|
- [Checking for Privileges](#checking-for-privileges)
|
||||||
|
- [Getting and Setting Privileges](#getting-and-setting-privileges)
|
||||||
|
- [Adding Privileges to basic_privs](#adding-privileges-to-basicprivs)
|
||||||
|
|
||||||
|
## When to use Privileges
|
||||||
|
|
||||||
|
A privilege should give a player the ability to do something.
|
||||||
|
Privileges are **not** for indicating class or status.
|
||||||
|
|
||||||
|
**Good Privileges:**
|
||||||
|
|
||||||
|
* interact
|
||||||
|
* shout
|
||||||
|
* noclip
|
||||||
|
* fly
|
||||||
|
* kick
|
||||||
|
* ban
|
||||||
|
* vote
|
||||||
|
* worldedit
|
||||||
|
* area_admin - admin functions of one mod is ok
|
||||||
|
|
||||||
|
**Bad Privileges:**
|
||||||
|
|
||||||
|
* moderator
|
||||||
|
* admin
|
||||||
|
* elf
|
||||||
|
* dwarf
|
||||||
|
|
||||||
|
## Declaring Privileges
|
||||||
|
|
||||||
|
Use `register_privilege` to declare a new privilege:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_privilege("vote", {
|
||||||
|
description = "Can vote on issues",
|
||||||
|
give_to_singleplayer = true
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
`give_to_singleplayer` defaults to true when not specified, so it isn't
|
||||||
|
actually needed in the above definition.
|
||||||
|
|
||||||
|
## Checking for Privileges
|
||||||
|
|
||||||
|
To quickly check whether a player has all the required privileges:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local has, missing = core.check_player_privs(player_or_name, {
|
||||||
|
interact = true,
|
||||||
|
vote = true })
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, `has` is true if the player has all the privileges needed.
|
||||||
|
If `has` is false, then `missing` will contain a key-value table
|
||||||
|
of the missing privileges.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local has, missing = core.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
|
||||||
|
```
|
||||||
|
|
||||||
|
If you don't need to check the missing privileges, you can put
|
||||||
|
`check_player_privs` directly into the if statement.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
if not core.check_player_privs(name, { interact=true }) then
|
||||||
|
return false, "You need interact for this!"
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Getting and Setting Privileges
|
||||||
|
|
||||||
|
Player privileges can be accessed or modified regardless of the player
|
||||||
|
being online.
|
||||||
|
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local privs = core.get_player_privs(name)
|
||||||
|
print(dump(privs))
|
||||||
|
|
||||||
|
privs.vote = true
|
||||||
|
core.set_player_privs(name, privs)
|
||||||
|
```
|
||||||
|
|
||||||
|
Privileges are always specified as a key-value table with the key being
|
||||||
|
the privilege name and the value being a boolean.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
{
|
||||||
|
fly = true,
|
||||||
|
interact = true,
|
||||||
|
shout = true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding Privileges to basic_privs
|
||||||
|
|
||||||
|
Players with the `basic_privs` privilege are able to grant and revoke a limited
|
||||||
|
set of privileges. It's common to give this privilege to moderators so that
|
||||||
|
they can grant and revoke `interact` and `shout`, but can't grant themselves or other
|
||||||
|
players privileges with greater potential for abuse such as `give` and `server`.
|
||||||
|
|
||||||
|
To add a privilege to `basic_privs`, and adjust which privileges your moderators can
|
||||||
|
grant and revoke from other players, you must change the `basic_privs` setting.
|
||||||
|
|
||||||
|
By default, `basic_privs` has the following value:
|
||||||
|
|
||||||
|
basic_privs = interact, shout
|
||||||
|
|
||||||
|
To add `vote`, update this to:
|
||||||
|
|
||||||
|
basic_privs = interact, shout, vote
|
||||||
|
|
||||||
|
This will allow players with `basic_privs` to grant and revoke the `vote` privilege.
|
5
_ru/players/sfinv.md
Normal file
5
_ru/players/sfinv.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
sitemap: false
|
||||||
|
redirect_from: /en/chapters/sfinv.html
|
||||||
|
redirect_to: "https://github.com/rubenwardy/sfinv/blob/master/Tutorial.md"
|
||||||
|
---
|
253
_ru/quality/clean_arch.md
Normal file
253
_ru/quality/clean_arch.md
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
---
|
||||||
|
title: Intro to Clean Architectures
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 8.4
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
Once your mod reaches a respectable size, you'll find it harder and harder to
|
||||||
|
keep the code clean and free of bugs. This is an especially big problem when using
|
||||||
|
a dynamically typed language like Lua, given that the compiler gives you very little
|
||||||
|
compiler-time help when it comes to things like making sure that types are used correctly.
|
||||||
|
|
||||||
|
This chapter covers important concepts needed to keep your code clean,
|
||||||
|
and common design patterns to achieve that. Please note that this chapter isn't
|
||||||
|
meant to be prescriptive, but to instead give you an idea of the possibilities.
|
||||||
|
There is no one good way of designing a mod, and good mod design is very subjective.
|
||||||
|
|
||||||
|
- [Cohesion, Coupling, and Separation of Concerns](#cohesion-coupling-and-separation-of-concerns)
|
||||||
|
- [Observer](#observer)
|
||||||
|
- [Model-View-Controller](#model-view-controller)
|
||||||
|
- [API-View](#api-view)
|
||||||
|
- [Conclusion](#conclusion)
|
||||||
|
|
||||||
|
|
||||||
|
## Cohesion, Coupling, and Separation of Concerns
|
||||||
|
|
||||||
|
Without any planning, a programming project will tend to gradually descend into
|
||||||
|
spaghetti code. Spaghetti code is characterised by a lack of structure - all the
|
||||||
|
code is thrown in together with no clear boundaries. This ultimately makes a
|
||||||
|
project completely unmaintainable, ending in its abandonment.
|
||||||
|
|
||||||
|
The opposite of this is to design your project as a collection of interacting
|
||||||
|
smaller programs or areas of code. <!-- Weird wording? -->
|
||||||
|
|
||||||
|
> Inside every large program, there is a small program trying to get out.
|
||||||
|
>
|
||||||
|
> --C.A.R. Hoare
|
||||||
|
|
||||||
|
This should be done in such a way that you achieve Separation of Concerns -
|
||||||
|
each area should be distinct and address a separate need or concern.
|
||||||
|
|
||||||
|
These programs/areas should have the following two properties:
|
||||||
|
|
||||||
|
* **High Cohesion** - the area should be closely/tightly related.
|
||||||
|
* **Low Coupling** - keep dependencies between areas as low as possible, and avoid
|
||||||
|
relying on internal implementations. It's a very good idea to make sure you have
|
||||||
|
a low amount of coupling, as this means that changing the APIs of certain areas
|
||||||
|
will be more feasible.
|
||||||
|
|
||||||
|
Note that these apply both when thinking about the relationship between mods,
|
||||||
|
and the relationship between areas inside a mod.
|
||||||
|
|
||||||
|
|
||||||
|
## Observer
|
||||||
|
|
||||||
|
A simple way to separate different areas of code is to use the Observer pattern.
|
||||||
|
|
||||||
|
Let's take the example of unlocking an achievement when a player first kills a
|
||||||
|
rare animal. The naïve approach would be to have achievement code in the mob
|
||||||
|
kill function, checking the mob name and unlocking the award if it matches.
|
||||||
|
This is a bad idea, however, as it makes the mobs mod coupled to the achievements
|
||||||
|
code. If you kept on doing this - for example, adding XP to the mob death code -
|
||||||
|
you could end up with a lot of messy dependencies.
|
||||||
|
|
||||||
|
Enter the Observer pattern. Instead of the mymobs mod caring about awards,
|
||||||
|
the mymobs mod exposes a way for other areas of code to register their
|
||||||
|
interest in an event and receive data about the event.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
Then the other code registers its interest:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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)
|
||||||
|
```
|
||||||
|
|
||||||
|
You may be thinking - wait a second, this looks awfully familiar. And you're right!
|
||||||
|
The Minetest API is heavily Observer-based to stop the engine having to care about
|
||||||
|
what is listening to something.
|
||||||
|
|
||||||
|
|
||||||
|
## Model-View-Controller
|
||||||
|
|
||||||
|
In the next chapter, we will discuss how to automatically test your
|
||||||
|
code and one of the problems we will have is how to separate your logic
|
||||||
|
(calculations, what should be done) from API calls (`core.*`, other mods)
|
||||||
|
as much as possible.
|
||||||
|
|
||||||
|
One way to do this is to think about:
|
||||||
|
|
||||||
|
* What **data** you have.
|
||||||
|
* What **actions** you can take with this data.
|
||||||
|
* How **events** (ie: formspec, punches, etc) trigger these actions, and how
|
||||||
|
these actions cause things to happen in the engine.
|
||||||
|
|
||||||
|
Let's take an example of a land protection mod. The data you have is the areas
|
||||||
|
and any associated metadata. Actions you can take are `create`, `edit`, or
|
||||||
|
`delete`. The events that trigger these actions are chat commands and formspec
|
||||||
|
receive fields. These are 3 areas that can usually be separated pretty well.
|
||||||
|
|
||||||
|
In your tests, you will be able to make sure that an action when triggered does
|
||||||
|
the right thing to the data. You won't need to test that an event calls an
|
||||||
|
action (as this would require using the Minetest API, and this area of code
|
||||||
|
should be made as small as possible anyway.)
|
||||||
|
|
||||||
|
You should write your data representation using Pure Lua. "Pure" in this context
|
||||||
|
means that the functions could run outside of Minetest - none of the engine's
|
||||||
|
functions are called.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- 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
|
||||||
|
```
|
||||||
|
|
||||||
|
Your actions should also be pure, but calling other functions is more
|
||||||
|
acceptable than in the above.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- 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
|
||||||
|
```
|
||||||
|
|
||||||
|
Your event handlers will have to interact with the Minetest API. You should keep
|
||||||
|
the number of calculations to a minimum, as you won't be able to test this area
|
||||||
|
very easily.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- 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
|
||||||
|
|
||||||
|
core.register_chatcommand("/land", {
|
||||||
|
privs = { land = true },
|
||||||
|
func = function(name)
|
||||||
|
land.handle_creation_request(name)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
core.register_on_player_receive_fields(function(player,
|
||||||
|
formname, fields)
|
||||||
|
land.handle_create_submit(player:get_player_name(),
|
||||||
|
fields.area_name)
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
The above is the Model-View-Controller pattern. The model is a collection of data
|
||||||
|
with minimal functions. The view is a collection of functions which listen to
|
||||||
|
events and pass it to the controller, and also receives calls from the controller to
|
||||||
|
do something with the Minetest API. The controller is where the decisions and
|
||||||
|
most of the calculations are made.
|
||||||
|
|
||||||
|
The controller should have no knowledge about the Minetest API - notice how
|
||||||
|
there are no Minetest calls or any view functions that resemble them.
|
||||||
|
You should *NOT* have a function like `view.hud_add(player, def)`.
|
||||||
|
Instead, the view defines some actions that the controller can tell the view to do,
|
||||||
|
like `view.add_hud(info)` where info is a value or table which doesn't relate
|
||||||
|
to the Minetest API at all.
|
||||||
|
|
||||||
|
<figure class="right_image">
|
||||||
|
<img
|
||||||
|
width="100%"
|
||||||
|
src="{{ page.root }}/static/mvc_diagram.svg"
|
||||||
|
alt="Diagram showing a centered text element">
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
It is important that each area only communicates with its direct neighbours,
|
||||||
|
as shown above, in order to reduce how much you need to change if you modify
|
||||||
|
an area's internals or externals. For example, to change the formspec you
|
||||||
|
would only need to edit the view. To change the view API, you would only need to
|
||||||
|
change the view and the controller, but not the model at all.
|
||||||
|
|
||||||
|
In practice, this design is rarely used because of the increased complexity
|
||||||
|
and because it doesn't give many benefits for most types of mods. Instead,
|
||||||
|
you will commonly see a less formal and strict kind of design -
|
||||||
|
variants of the API-View.
|
||||||
|
|
||||||
|
|
||||||
|
### API-View
|
||||||
|
|
||||||
|
In an ideal world, you'd have the above 3 areas perfectly separated with all
|
||||||
|
events going into the controller before going back to the normal view. But
|
||||||
|
this isn't the real world. A good compromise is to reduce the mod into two
|
||||||
|
parts:
|
||||||
|
|
||||||
|
* **API** - This was the model and controller above. There should be no uses of
|
||||||
|
`core.` here.
|
||||||
|
* **View** - This was also the view above. It's a good idea to structure this into separate
|
||||||
|
files for each type of event.
|
||||||
|
|
||||||
|
rubenwardy's [crafting mod](https://github.com/rubenwardy/crafting) roughly
|
||||||
|
follows this design. `api.lua` is almost all pure Lua functions handling the data
|
||||||
|
storage and controller-style calculations. `gui.lua` is the view for formspecs
|
||||||
|
and formspec submission, and `async_crafter.lua` is the view and controller for
|
||||||
|
a node formspec and node timers.
|
||||||
|
|
||||||
|
Separating the mod like this means that you can very easily test the API part,
|
||||||
|
as it doesn't use any Minetest APIs - as shown in the
|
||||||
|
[next chapter](unit_testing.html) and seen in the crafting mod.
|
||||||
|
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
Good code design is subjective, and highly depends on the project you're making. As a
|
||||||
|
general rule, try to keep cohesion high and coupling low. Phrased differently,
|
||||||
|
keep related code together and unrelated code apart, and keep dependencies simple.
|
||||||
|
|
||||||
|
I highly recommend reading the [Game Programming Patterns](http://gameprogrammingpatterns.com/)
|
||||||
|
book. It's freely available to [read online](http://gameprogrammingpatterns.com/contents.html)
|
||||||
|
and goes into much more detail on common programming patterns relevant to games.
|
140
_ru/quality/common_mistakes.md
Normal file
140
_ru/quality/common_mistakes.md
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
---
|
||||||
|
title: Common Mistakes
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 8.1
|
||||||
|
redirect_from: /en/chapters/common_mistakes.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
This chapter details common mistakes, and how to avoid them.
|
||||||
|
|
||||||
|
- [Be Careful When Storing ObjectRefs (ie: players or entities)](#be-careful-when-storing-objectrefs-ie-players-or-entities)
|
||||||
|
- [Don't Trust Formspec Submissions](#dont-trust-formspec-submissions)
|
||||||
|
- [Set ItemStacks After Changing Them](#set-itemstacks-after-changing-them)
|
||||||
|
|
||||||
|
## Be Careful When Storing ObjectRefs (ie: players or entities)
|
||||||
|
|
||||||
|
An ObjectRef is invalidated when the player or entity it represents leaves
|
||||||
|
the game. This may happen when the player goes offline, or the entity is unloaded
|
||||||
|
or removed.
|
||||||
|
|
||||||
|
The methods of ObjectRefs will always return nil when invalid, since Minetest 5.2.
|
||||||
|
Any call will essentially be ignored.
|
||||||
|
|
||||||
|
You should avoid storing ObjectRefs where possible. If you do to store an
|
||||||
|
ObjectRef, you should make sure you check it before use, like so:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- This only works in Minetest 5.2+
|
||||||
|
if obj:get_pos() then
|
||||||
|
-- is valid!
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Don't Trust Formspec Submissions
|
||||||
|
|
||||||
|
Malicious clients can submit formspecs whenever they like, with
|
||||||
|
whatever content they like.
|
||||||
|
|
||||||
|
For example, the following code has a vulnerability which allows players to
|
||||||
|
give themselves moderator privileges:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local function show_formspec(name)
|
||||||
|
if not core.check_player_privs(name, { privs = true }) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
core.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
|
||||||
|
})
|
||||||
|
|
||||||
|
core.register_on_player_receive_fields(function(player,
|
||||||
|
formname, fields)
|
||||||
|
-- BAD! Missing privilege check here!
|
||||||
|
|
||||||
|
local privs = core.get_player_privs(fields.target)
|
||||||
|
privs.kick = true
|
||||||
|
privs.ban = true
|
||||||
|
core.set_player_privs(fields.target, privs)
|
||||||
|
return true
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
Add a privilege check to solve this:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_on_player_receive_fields(function(player,
|
||||||
|
formname, fields)
|
||||||
|
if not core.check_player_privs(name, { privs = true }) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- code
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Set ItemStacks After Changing Them
|
||||||
|
|
||||||
|
Have you noticed that it's simply called an `ItemStack` in the API, not an `ItemStackRef`,
|
||||||
|
similar to `InvRef`? This is because an `ItemStack` isn't a reference - it's a
|
||||||
|
copy. Stacks work on a copy of the data rather than the stack in the inventory.
|
||||||
|
This means that modifying a stack won't actually modify that stack in the inventory.
|
||||||
|
|
||||||
|
For example, don't do this:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
Do this instead:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
The behaviour of callbacks is slightly more complicated. Modifying an `ItemStack` you
|
||||||
|
are given will change it for the caller too, and any subsequent callbacks. However,
|
||||||
|
it will only be saved in the engine if the callback caller sets it.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.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)
|
||||||
|
```
|
||||||
|
|
||||||
|
If no callbacks cancel this, the stack will be set and the description will be updated,
|
||||||
|
but if a callback does cancel this, then the update may be lost.
|
||||||
|
|
||||||
|
It's better to do this instead:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.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)
|
||||||
|
```
|
||||||
|
|
||||||
|
If the callbacks cancel or the callback runner doesn't set the stack,
|
||||||
|
then the update will still be set.
|
||||||
|
If the callbacks or the callback runner set the stack, then the use of
|
||||||
|
set_stack doesn't matter.
|
107
_ru/quality/luacheck.md
Normal file
107
_ru/quality/luacheck.md
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
---
|
||||||
|
title: Automatic Error Checking
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 8.2
|
||||||
|
description: Use LuaCheck to find errors
|
||||||
|
redirect_from: /en/chapters/luacheck.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
In this chapter, you will learn how to use a tool called LuaCheck to automatically
|
||||||
|
scan your mod for any mistakes. This tool can be used in combination with your
|
||||||
|
editor to provide alerts to any mistakes.
|
||||||
|
|
||||||
|
- [Installing LuaCheck](#installing-luacheck)
|
||||||
|
- [Windows](#windows)
|
||||||
|
- [Linux](#linux)
|
||||||
|
- [Running LuaCheck](#running-luacheck)
|
||||||
|
- [Configuring LuaCheck](#configuring-luacheck)
|
||||||
|
- [Troubleshooting](#troubleshooting)
|
||||||
|
- [Using with editor](#using-with-editor)
|
||||||
|
|
||||||
|
## Installing LuaCheck
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
Simply download luacheck.exe from
|
||||||
|
[the Github Releases page](https://github.com/mpeterv/luacheck/releases).
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
|
||||||
|
First, you'll need to install LuaRocks:
|
||||||
|
|
||||||
|
sudo apt install luarocks
|
||||||
|
|
||||||
|
You can then install LuaCheck globally:
|
||||||
|
|
||||||
|
sudo luarocks install luacheck
|
||||||
|
|
||||||
|
Check that it's installed with the following command:
|
||||||
|
|
||||||
|
luacheck -v
|
||||||
|
|
||||||
|
## Running LuaCheck
|
||||||
|
|
||||||
|
The first time you run LuaCheck, it will probably pick up a lot of false
|
||||||
|
errors. This is because it still needs to be configured.
|
||||||
|
|
||||||
|
On Windows, open powershell or bash in the root folder of your project
|
||||||
|
and run `path\to\luacheck.exe .`
|
||||||
|
|
||||||
|
On Linux, run `luacheck .` whilst in the root folder of your project.
|
||||||
|
|
||||||
|
## Configuring LuaCheck
|
||||||
|
|
||||||
|
Create a file called .luacheckrc in the root of your project. This could be the
|
||||||
|
root of your game, modpack, or mod.
|
||||||
|
|
||||||
|
Put the following contents in it:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, you'll need to test that it works by running LuaCheck. You should get a lot
|
||||||
|
fewer errors this time. Starting at the first error you get, modify the code to
|
||||||
|
remove the issue, or modify the configuration if the code is correct. See the list
|
||||||
|
below.
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
* **accessing undefined variable foobar** - If `foobar` is meant to be a global,
|
||||||
|
add it to `read_globals`. Otherwise, add any missing `local`s to the mod.
|
||||||
|
* **setting non-standard global variable foobar** - If `foobar` is meant to be a global,
|
||||||
|
add it to `globals`. Remove from `read_globals` if present.
|
||||||
|
Otherwise, add any missing `local`s to the mod.
|
||||||
|
* **mutating read-only global variable 'foobar'** - Move `foobar` from `read_globals` to
|
||||||
|
`globals`, or stop writing to foobar.
|
||||||
|
|
||||||
|
## Using with editor
|
||||||
|
|
||||||
|
It is highly recommended that you find and install a plugin for your editor of choice
|
||||||
|
to show you errors without running a command. Most editors will likely have a plugin
|
||||||
|
available.
|
||||||
|
|
||||||
|
* **VSCode** - Ctrl+P, then paste: `ext install dwenegar.vscode-luacheck`
|
||||||
|
* **Sublime** - Install using package-control:
|
||||||
|
[SublimeLinter](https://github.com/SublimeLinter/SublimeLinter),
|
||||||
|
[SublimeLinter-luacheck](https://github.com/SublimeLinter/SublimeLinter-luacheck).
|
26
_ru/quality/readmore.md
Normal file
26
_ru/quality/readmore.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
title: Read More
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 8.7
|
||||||
|
redirect_from: /en/chapters/readmore.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## List of Resources
|
||||||
|
|
||||||
|
After you've read this book, take a look at the following.
|
||||||
|
|
||||||
|
### Minetest Modding
|
||||||
|
|
||||||
|
* Minetest's Lua API Reference - [multiple page version](https://minetest.gitlab.io/minetest/class-reference/#player-only-no-op-for-other-objects) |
|
||||||
|
[single page version](https://github.com/minetest/minetest/blob/master/doc/lua_api.md).
|
||||||
|
* Look at [existing mods](https://forum.minetest.net/viewforum.php?f=11).
|
||||||
|
|
||||||
|
### Lua Programming
|
||||||
|
|
||||||
|
* [Programming in Lua (PIL)](http://www.lua.org/pil/).
|
||||||
|
|
||||||
|
### 3D Modelling
|
||||||
|
|
||||||
|
* [Blender 3D: Noob to pro](https://en.wikibooks.org/wiki/Blender_3D:_Noob_to_Pro).
|
||||||
|
* [Using Blender with Minetest](http://wiki.minetest.net/Using_Blender).
|
166
_ru/quality/releasing.md
Normal file
166
_ru/quality/releasing.md
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
---
|
||||||
|
title: Releasing a Mod
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 8.6
|
||||||
|
redirect_from: /en/chapters/releasing.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
Releasing, or publishing, a mod allows other people to make use of it. Once a mod has been
|
||||||
|
released it might be used in singleplayer games or on servers, including public servers.
|
||||||
|
|
||||||
|
- [Choosing a License](#choosing-a-license)
|
||||||
|
- [LGPL and CC-BY-SA](#lgpl-and-cc-by-sa)
|
||||||
|
- [CC0](#cc0)
|
||||||
|
- [MIT](#mit)
|
||||||
|
- [Packaging](#packaging)
|
||||||
|
- [README.txt](#readmetxt)
|
||||||
|
- [mod.conf / game.conf](#modconf--gameconf)
|
||||||
|
- [screenshot.png](#screenshotpng)
|
||||||
|
- [Uploading](#uploading)
|
||||||
|
- [Version Control Systems](#version-control-systems)
|
||||||
|
- [Releasing on ContentDB](#releasing-on-contentdb)
|
||||||
|
- [Forum Topic](#forum-topic)
|
||||||
|
|
||||||
|
## Choosing a License
|
||||||
|
|
||||||
|
You need to specify a license for your mod. This is important because it tells other
|
||||||
|
people the ways in which they are allowed to use your work. If your mod doesn't have
|
||||||
|
a license, people won't know whether they are allowed to modify, distribute or use your
|
||||||
|
mod on a public server.
|
||||||
|
|
||||||
|
Your code and your art need different things from the licenses they use. For example,
|
||||||
|
Creative Commons licenses shouldn't be used with source code,
|
||||||
|
but can be suitable choices for artistic works such as images, text and meshes.
|
||||||
|
|
||||||
|
You are allowed any license; however, mods which disallow derivatives are banned from the
|
||||||
|
official Minetest forum. (For a mod to be allowed on the forum, other developers must be
|
||||||
|
able to modify it and release the modified version.)
|
||||||
|
|
||||||
|
Please note that **public domain is not a valid licence**, because the definition varies
|
||||||
|
in different countries.
|
||||||
|
|
||||||
|
It is important to note that WTFPL is
|
||||||
|
[strongly discouraged](https://content.minetest.net/help/wtfpl/) and people may
|
||||||
|
choose not to use your mod if it has this license.
|
||||||
|
|
||||||
|
### LGPL and CC-BY-SA
|
||||||
|
|
||||||
|
This is a common license combination in the Minetest community, and is what
|
||||||
|
Minetest and Minetest Game use.
|
||||||
|
|
||||||
|
You license your code under LGPL 2.1 and your art under CC-BY-SA.
|
||||||
|
|
||||||
|
This means that:
|
||||||
|
|
||||||
|
* Anyone can modify, redistribute and sell modified or unmodified versions.
|
||||||
|
* If someone modifies your mod, they must give their version the same license.
|
||||||
|
* Your copyright notice must be kept.
|
||||||
|
|
||||||
|
### CC0
|
||||||
|
|
||||||
|
This license can be used for both code and art, and allows anyone to do what
|
||||||
|
they want with your work. This means they can modify, redistribute, sell, or
|
||||||
|
leave-out attribution.
|
||||||
|
|
||||||
|
### MIT
|
||||||
|
|
||||||
|
This is a common license for code. The only restriction it places on users
|
||||||
|
of your code is that they must include the same copyright notice and license
|
||||||
|
in any copies of the code or of substantial parts of the code.
|
||||||
|
|
||||||
|
## Packaging
|
||||||
|
|
||||||
|
There are some files that are recommended to include in your mod or game
|
||||||
|
before you release it.
|
||||||
|
|
||||||
|
### README.txt
|
||||||
|
|
||||||
|
The README file should state:
|
||||||
|
|
||||||
|
* What the mod/game does, how to use it.
|
||||||
|
* What the license is.
|
||||||
|
* Optionally:
|
||||||
|
* where to report problems or get help.
|
||||||
|
* credits
|
||||||
|
|
||||||
|
### mod.conf / game.conf
|
||||||
|
|
||||||
|
Make sure you add a description key to explain what your mod or game does. Be
|
||||||
|
concise without being vague. It should be short because it will be displayed in
|
||||||
|
the content installer which has limited space.
|
||||||
|
|
||||||
|
Good example:
|
||||||
|
|
||||||
|
description = Adds soup, cakes, bakes and juices.
|
||||||
|
|
||||||
|
Avoid this:
|
||||||
|
|
||||||
|
description = The food mod for Minetest. (<-- BAD! It's vague)
|
||||||
|
|
||||||
|
### screenshot.png
|
||||||
|
|
||||||
|
Screenshots should be 3:2 (3 pixels of width for every 2 pixels of height)
|
||||||
|
and have a minimum size of 300 x 200px.
|
||||||
|
|
||||||
|
The screenshot is displayed inside of Minetest as a thumbnail for the content.
|
||||||
|
|
||||||
|
## Uploading
|
||||||
|
|
||||||
|
So that a potential user can download your mod, you need to upload it somewhere
|
||||||
|
publicly accessible. There are several ways to do this, but you should use the
|
||||||
|
approach that works best for you, as long as it meets these requirements, and any
|
||||||
|
others which may be added by forum moderators:
|
||||||
|
|
||||||
|
* **Stable** - The hosting website should be unlikely to shut down without warning.
|
||||||
|
* **Direct link** - You should be able to click a link and download the file
|
||||||
|
without having to view another page.
|
||||||
|
* **Virus Free** - Scammy upload hosts may contain insecure adverts.
|
||||||
|
|
||||||
|
ContentDB allows you to upload zip files, and meets these criteria.
|
||||||
|
|
||||||
|
### Version Control Systems
|
||||||
|
|
||||||
|
A Version Control System (VCS) is software that manages changes to software,
|
||||||
|
often making it easier to distribute and receive contributed changes.
|
||||||
|
|
||||||
|
The majority of Minetest modders use Git and a website like GitHub to distribute
|
||||||
|
their code.
|
||||||
|
|
||||||
|
Using git can be difficult at first. If you need help with this please see:
|
||||||
|
|
||||||
|
* [Pro Git book](http://git-scm.com/book/en/v1/Getting-Started) - Free to read online.
|
||||||
|
|
||||||
|
## Releasing on ContentDB
|
||||||
|
|
||||||
|
ContentDB is the official place to find and distribute content such as mods,
|
||||||
|
games, and texture packs. Users can find content using the website, or download
|
||||||
|
and install using the integration built into the Minetest main menu.
|
||||||
|
|
||||||
|
Sign up to [ContentDB](https://content.minetest.net) and add your content.
|
||||||
|
Make sure to read the guidance given in the Help section.
|
||||||
|
|
||||||
|
## Forum Topic
|
||||||
|
|
||||||
|
You can also create a forum topic to let users discuss your creation.
|
||||||
|
|
||||||
|
Mod topics should be created in ["WIP Mods"](https://forum.minetest.net/viewforum.php?f=9) (Work In Progress)
|
||||||
|
forum, and Game topics in the ["WIP Games"](https://forum.minetest.net/viewforum.php?f=50) forum.
|
||||||
|
When you no longer consider your mod a work in progress, you can
|
||||||
|
[request that it be moved](https://forum.minetest.net/viewtopic.php?f=11&t=10418)
|
||||||
|
to "Mod Releases."
|
||||||
|
|
||||||
|
The forum topic should contain similar content to the README, but should
|
||||||
|
be more promotional and also include a link to download the mod.
|
||||||
|
It's a good idea to include screenshots of your mod in action, if possible.
|
||||||
|
|
||||||
|
The subject of topic must be in one of these formats:
|
||||||
|
|
||||||
|
* [Mod] Mod Title [modname]
|
||||||
|
* [Mod] Mod Title [version number] [modname]
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
* [Mod] More Blox [0.1] [moreblox]
|
110
_ru/quality/security.md
Normal file
110
_ru/quality/security.md
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
---
|
||||||
|
title: Security
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 8.3
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
Security is very important in making sure that your mod doesn't cause the server
|
||||||
|
owner to lose data or control.
|
||||||
|
|
||||||
|
- [Core Concepts](#core-concepts)
|
||||||
|
- [Formspecs](#formspecs)
|
||||||
|
- [Never Trust Submissions](#never-trust-submissions)
|
||||||
|
- [Time of Check isn't Time of Use](#time-of-check-isnt-time-of-use)
|
||||||
|
- [(Insecure) Environments](#insecure-environments)
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
The most important concept in security is to **never trust the user**.
|
||||||
|
Anything the user submits should be treated as malicious.
|
||||||
|
This means that you should always check that the information they
|
||||||
|
enter is valid, that the user has the correct permissions,
|
||||||
|
and that they are otherwise allowed to do that action
|
||||||
|
(ie: in range or an owner).
|
||||||
|
|
||||||
|
A malicious action isn't necessarily the modification or destruction of data,
|
||||||
|
but can be accessing sensitive data, such as password hashes or
|
||||||
|
private messages.
|
||||||
|
This is especially bad if the server stores information such as emails or ages,
|
||||||
|
which some may do for verification purposes.
|
||||||
|
|
||||||
|
## Formspecs
|
||||||
|
|
||||||
|
### Never Trust Submissions
|
||||||
|
|
||||||
|
Any users can submit almost any formspec with any values at any time.
|
||||||
|
|
||||||
|
Here's some real code found in a mod:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.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
|
||||||
|
```
|
||||||
|
|
||||||
|
Can you spot the problem? A malicious user could submit a formspec containing
|
||||||
|
their own position values, allowing them to teleport to anywhere they wish to.
|
||||||
|
This could even be automated using client modifications to essentially replicate
|
||||||
|
the `/teleport` command with no need for a privilege.
|
||||||
|
|
||||||
|
The solution for this kind of issue is to use a
|
||||||
|
[Context](../players/formspecs.html#contexts), as shown previously in
|
||||||
|
the Formspecs chapter.
|
||||||
|
|
||||||
|
### Time of Check isn't Time of Use
|
||||||
|
|
||||||
|
Any users can submit any formspec with any values at any time, except where the
|
||||||
|
engine forbids it:
|
||||||
|
|
||||||
|
* A node formspec submission will be blocked if the user is too far away.
|
||||||
|
* From 5.0 onward, named formspecs will be blocked if they haven't been shown yet.
|
||||||
|
|
||||||
|
This means that you should check in the handler that the user meets the
|
||||||
|
conditions for showing the formspec in the first place, as well as any
|
||||||
|
corresponding actions.
|
||||||
|
|
||||||
|
The vulnerability caused by checking for permissions in the show formspec but not
|
||||||
|
in the handle formspec is called Time Of Check is not Time Of Use (TOCTOU).
|
||||||
|
|
||||||
|
|
||||||
|
## (Insecure) Environments
|
||||||
|
|
||||||
|
Minetest allows mods to request an unsandboxed environment, giving them access
|
||||||
|
to the full Lua API.
|
||||||
|
|
||||||
|
Can you spot the vulnerability in the following?
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local ie = core.request_insecure_environment()
|
||||||
|
ie.os.execute(("path/to/prog %d"):format(3))
|
||||||
|
```
|
||||||
|
|
||||||
|
`string.format` is a function in the global shared table `string`.
|
||||||
|
A malicious mod could override this function and pass stuff to os.execute:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
string.format = function()
|
||||||
|
return "xdg-open 'http://example.com'"
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
The mod could pass something much more malicious than opening a website, such
|
||||||
|
as giving a remote user control over the machine.
|
||||||
|
|
||||||
|
Some rules for using an insecure environment:
|
||||||
|
|
||||||
|
* Always store it in a local and never pass it into a function.
|
||||||
|
* Make sure you can trust any input given to an insecure function, to avoid the
|
||||||
|
issue above. This means avoiding globally redefinable functions.
|
198
_ru/quality/translations.md
Normal file
198
_ru/quality/translations.md
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
---
|
||||||
|
title: Translation (i18n / l10n)
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 8.05
|
||||||
|
marked_text_encoding:
|
||||||
|
level: info
|
||||||
|
title: Marked Text Encoding
|
||||||
|
message: |
|
||||||
|
You don't need to know the exact format of marked text, but it might help
|
||||||
|
you understand.
|
||||||
|
|
||||||
|
```
|
||||||
|
"\27(T@mymod)Hello everyone!\27E"
|
||||||
|
```
|
||||||
|
|
||||||
|
* `\27` is the escape character - it's used to tell Minetest to pay attention as
|
||||||
|
something special is coming up. This is used for both translations and text
|
||||||
|
colorisation.
|
||||||
|
* `(T@mymod)` says that the following text is translatable using the `mymod`
|
||||||
|
textdomain.
|
||||||
|
* `Hello everyone!` is the translatable text in English, as passed to the
|
||||||
|
translator function.
|
||||||
|
* `\27E` is the escape character again and `E`, used to signal that the end has
|
||||||
|
been reached.
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
Adding support for translation to your mods and games allows more people to
|
||||||
|
enjoy them. According to Google Play, 64% of Minetest Android users don't have
|
||||||
|
English as their primary language. Minetest doesn't track stats for user
|
||||||
|
languages across all platforms, but there's likely to be a high proportion of
|
||||||
|
non-English speaking users.
|
||||||
|
|
||||||
|
Minetest allows you to translate your mods and games into different languages by
|
||||||
|
writing your text in English, and using translation files to map into other
|
||||||
|
languages. Translation is done on each player's client, allowing each player to
|
||||||
|
see a different language.
|
||||||
|
|
||||||
|
|
||||||
|
- [How does client-side translation work?](#how-does-client-side-translation-work)
|
||||||
|
- [Marked up text](#marked-up-text)
|
||||||
|
- [Translation files](#translation-files)
|
||||||
|
- [Format strings](#format-strings)
|
||||||
|
- [Best practices and Common Falsehoods about Translation](#best-practices-and-common-falsehoods-about-translation)
|
||||||
|
- [Server-side translations](#server-side-translations)
|
||||||
|
- [Conclusion](#conclusion)
|
||||||
|
|
||||||
|
|
||||||
|
## How does client-side translation work?
|
||||||
|
|
||||||
|
### Marked up text
|
||||||
|
|
||||||
|
The server needs to tell clients how to translate text. This is done by placing
|
||||||
|
control characters in text, telling Minetest where and how to translate
|
||||||
|
text. This is referred to as marked up text, and will be discussed more later.
|
||||||
|
|
||||||
|
To mark text as translatable, use a translator function (`S()`), obtained using
|
||||||
|
`core.get_translator(textdomain)`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local S = core.get_translator("mymod")
|
||||||
|
|
||||||
|
core.register_craftitem("mymod:item", {
|
||||||
|
description = S("My Item"),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The first argument of `get_translator` is the `textdomain`, which acts as a
|
||||||
|
namespace. Rather than having all translations for a language stored in the same
|
||||||
|
file, translations are separated into textdomains, with a file per textdomain
|
||||||
|
per language. The textdomain should be the same as the mod name, as it helps
|
||||||
|
avoid mod conflicts.
|
||||||
|
|
||||||
|
Marked up text can be used in most places where human-readable text is accepted,
|
||||||
|
including formspecs, item def fields, infotext, and more. When including marked
|
||||||
|
text in formspecs, you need to escape the text using `core.formspec_escape`.
|
||||||
|
|
||||||
|
When the client encounters translatable text, such as that passed to
|
||||||
|
`description`, it looks it up in the player's language's translation file. If a
|
||||||
|
translation cannot be found, it falls back to the English translation.
|
||||||
|
|
||||||
|
Translatable marked up text contains the English source text, the textdomain,
|
||||||
|
and any additional arguments passed to `S()`. It's essentially a text encoding
|
||||||
|
of the `S` call, containing all the required information.
|
||||||
|
|
||||||
|
Another type of marked up text is that returned by `core.colorize`.
|
||||||
|
|
||||||
|
{% include notice.html notice=page.marked_text_encoding %}
|
||||||
|
|
||||||
|
|
||||||
|
### Translation files
|
||||||
|
|
||||||
|
Translation files are media files that can be found in the `locale` folder for
|
||||||
|
each mod. Currently, the only supported format is `.tr`, but support for more
|
||||||
|
common formats is likely in the future. Translation files must be named
|
||||||
|
in the following way: `[textdomain].[lang].tr`.
|
||||||
|
|
||||||
|
Files in the `.tr` start with a comment specifying the textdomain, and then
|
||||||
|
further lines mapping from the English source text to the translation.
|
||||||
|
|
||||||
|
For example, `mymod.fr.tr`:
|
||||||
|
|
||||||
|
```
|
||||||
|
# textdomain: mymod
|
||||||
|
Hello everyone!=Bonjour à tous !
|
||||||
|
I like grapefruit=J'aime le pamplemousse
|
||||||
|
```
|
||||||
|
|
||||||
|
You should create translation files based on your mod/game's source code,
|
||||||
|
using a tool like
|
||||||
|
[update_translations](https://github.com/minetest-tools/update_translations).
|
||||||
|
This tool will look for `S(` in your Lua code, and automatically create a
|
||||||
|
template that translators can use to translate into their language.
|
||||||
|
It also handles updating the translation files when your source changes.
|
||||||
|
|
||||||
|
You should always put literal text (`"`) inside S rather than using a variable,
|
||||||
|
as it helps tools find translations.
|
||||||
|
|
||||||
|
|
||||||
|
## Format strings
|
||||||
|
|
||||||
|
It's common to need to include variable information within a translation
|
||||||
|
string. It's important that text isn't just concatenated, as that prevents
|
||||||
|
translators from changing the order of variables within a sentence. Instead,
|
||||||
|
you should use the translation system's format/arguments system:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
core.register_on_joinplayer(function(player)
|
||||||
|
core.chat_send_all(S("Everyone, say hi to @1!", player:get_player_name()))
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to include a literal `@` in your translation, you'll need to escape
|
||||||
|
by writing `@@`.
|
||||||
|
|
||||||
|
You should avoid concatenation *within* a sentence, but it's recommended that
|
||||||
|
you join multiple sentences using concatenation. This helps translators by
|
||||||
|
keeping strings smaller.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
S("Hello @1!", player_name) .. " " .. S("You have @1 new messages.", #msgs)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Best practices and Common Falsehoods about Translation
|
||||||
|
|
||||||
|
* Avoid concatenating text and use format arguments instead. This gives
|
||||||
|
translators full control over changing the order of things.
|
||||||
|
* Create translation files automatically, using
|
||||||
|
[update_translations](https://github.com/minetest-tools/update_translations).
|
||||||
|
* It's common for variables to change the surrounding text, for example, with
|
||||||
|
gender and pluralisation. This is often hard to deal with, so is
|
||||||
|
frequently glossed over or worked around with gender neutral phrasings.
|
||||||
|
* Translations may be much longer or much smaller than the English text. Make
|
||||||
|
sure to leave plenty of space.
|
||||||
|
* Other languages may write numbers in a different way, for example, with commas
|
||||||
|
as decimal points. `1.000,23`, `1'000'000,32`
|
||||||
|
* Don't assume that other languages use capitalisation in the same way.
|
||||||
|
|
||||||
|
|
||||||
|
## Server-side translations
|
||||||
|
|
||||||
|
Sometimes you need to know the translation of text on the server, for example,
|
||||||
|
to sort or search text. You can use `get_player_information` to get a player's
|
||||||
|
language and `get_translated_string` to translate marked text.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local list = {
|
||||||
|
S("Hello world!"),
|
||||||
|
S("Potato")
|
||||||
|
}
|
||||||
|
|
||||||
|
core.register_chatcommand("find", {
|
||||||
|
func = function(name, param)
|
||||||
|
local info = core.get_player_information(name)
|
||||||
|
local language = info and info.language or "en"
|
||||||
|
|
||||||
|
for _, line in ipairs(list) do
|
||||||
|
local trans = core.get_translated_string(language, line)
|
||||||
|
if trans:contains(query) then
|
||||||
|
return line
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
The translation API allows making mods and games more accessible, but care is
|
||||||
|
needed in order to use it correctly.
|
||||||
|
|
||||||
|
Minetest is continuously improving, and the translation API is likely to be
|
||||||
|
extended in the future. For example, support for gettext translation files will
|
||||||
|
allow common translator tools and platforms (like weblate) to be used, and
|
||||||
|
there's likely to be support for pluralisation and gender added.
|
172
_ru/quality/unit_testing.md
Normal file
172
_ru/quality/unit_testing.md
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
---
|
||||||
|
title: Automatic Unit Testing
|
||||||
|
layout: default
|
||||||
|
root: ../..
|
||||||
|
idx: 8.5
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction <!-- omit in toc -->
|
||||||
|
|
||||||
|
Unit tests are an essential tool in proving and reassuring yourself that your code
|
||||||
|
is correct. This chapter will show you how to write tests for Minetest mods and
|
||||||
|
games using Busted. Writing unit tests for functions where you call Minetest
|
||||||
|
functions is quite difficult, but luckily [in the previous chapter](clean_arch.html),
|
||||||
|
we discussed how to structure your code avoid this.
|
||||||
|
|
||||||
|
- [Installing Busted](#installing-busted)
|
||||||
|
- [Your First Test](#your-first-test)
|
||||||
|
- [init.lua](#initlua)
|
||||||
|
- [api.lua](#apilua)
|
||||||
|
- [tests/api_spec.lua](#testsapi_speclua)
|
||||||
|
- [Mocking: Using External Functions](#mocking-using-external-functions)
|
||||||
|
- [Conclusion](#conclusion)
|
||||||
|
|
||||||
|
## Installing Busted
|
||||||
|
|
||||||
|
First, you'll need to install LuaRocks.
|
||||||
|
|
||||||
|
* Windows: Follow the [installation instructions on LuaRock's wiki](https://github.com/luarocks/luarocks/wiki/Installation-instructions-for-Windows).
|
||||||
|
* Debian/Ubuntu Linux: `sudo apt install luarocks`
|
||||||
|
|
||||||
|
Next, you should install Busted globally:
|
||||||
|
|
||||||
|
sudo luarocks install busted
|
||||||
|
|
||||||
|
Finally, check that it is installed:
|
||||||
|
|
||||||
|
busted --version
|
||||||
|
|
||||||
|
|
||||||
|
## Your First Test
|
||||||
|
|
||||||
|
Busted is Lua's leading unit test framework. Busted looks for Lua files with
|
||||||
|
names ending in `_spec`, and then executes them in a standalone Lua environment.
|
||||||
|
|
||||||
|
mymod/
|
||||||
|
├── init.lua
|
||||||
|
├── api.lua
|
||||||
|
└── tests
|
||||||
|
└── api_spec.lua
|
||||||
|
|
||||||
|
|
||||||
|
### init.lua
|
||||||
|
|
||||||
|
```lua
|
||||||
|
mymod = {}
|
||||||
|
|
||||||
|
dofile(core.get_modpath("mymod") .. "/api.lua")
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### api.lua
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function mymod.add(x, y)
|
||||||
|
return x + y
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### tests/api_spec.lua
|
||||||
|
|
||||||
|
```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)
|
||||||
|
```
|
||||||
|
|
||||||
|
You can now run the tests by opening a terminal in the mod's directory and
|
||||||
|
running `busted .`
|
||||||
|
|
||||||
|
It's important that the API file doesn't create the table itself, as globals in
|
||||||
|
Busted work differently. Any variable which would be global in Minetest is instead
|
||||||
|
a file local in busted. This would have been a better way for Minetest to do things,
|
||||||
|
but it's too late for that now.
|
||||||
|
|
||||||
|
Another thing to note is that any files you're testing should avoid calls to any
|
||||||
|
functions not inside of it. You tend to only write tests for a single file at once.
|
||||||
|
|
||||||
|
|
||||||
|
## Mocking: Using External Functions
|
||||||
|
|
||||||
|
Mocking is the practice of replacing functions that the thing you're testing depends
|
||||||
|
on. This can have two purposes; one, the function may not be available in the
|
||||||
|
test environment, and two, you may want to capture calls to the function and any
|
||||||
|
passed arguments.
|
||||||
|
|
||||||
|
If you follow the advice in the [Clean Architectures](clean_arch.html) chapter,
|
||||||
|
you'll already have a pretty clean file to test. You will still have to mock
|
||||||
|
things not in your area, however - for example, you'll have to mock the view when
|
||||||
|
testing the controller/API. If you didn't follow the advice, then things are a
|
||||||
|
little harder as you may have to mock the Minetest API.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- As above, make a table
|
||||||
|
_G.minetest = {}
|
||||||
|
|
||||||
|
-- Define the mock function
|
||||||
|
local chat_send_all_calls = {}
|
||||||
|
function core.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)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
Unit tests will greatly increase the quality and reliability of your project if used
|
||||||
|
well, but they require you to structure your code in a different way than usual.
|
||||||
|
|
||||||
|
For an example of a mod with lots of unit tests, see
|
||||||
|
[crafting by rubenwardy](https://github.com/rubenwardy/crafting).
|
Loading…
Reference in New Issue
Block a user