From 4fe4ad68d215a3a488fb388c29132045666ef2f0 Mon Sep 17 00:00:00 2001 From: Andrey Stepanov Date: Fri, 29 Nov 2024 09:55:28 +0500 Subject: [PATCH] prepare template for russian translation --- _config.yml | 2 + _data/languages.yml | 4 + _layouts/default.html | 3 + _ru/advmap/biomesdeco.md | 243 ++++++++++++++++ _ru/advmap/lvm.md | 211 ++++++++++++++ _ru/basics/getting_started.md | 187 +++++++++++++ _ru/basics/lua.md | 197 +++++++++++++ _ru/games/games.md | 93 +++++++ _ru/index.md | 36 +++ _ru/items/callbacks.md | 206 ++++++++++++++ _ru/items/creating_textures.md | 98 +++++++ _ru/items/inventories.md | 356 ++++++++++++++++++++++++ _ru/items/node_drawtypes.md | 446 ++++++++++++++++++++++++++++++ _ru/items/nodes_items_crafting.md | 345 +++++++++++++++++++++++ _ru/map/environment.md | 234 ++++++++++++++++ _ru/map/objects.md | 361 ++++++++++++++++++++++++ _ru/map/storage.md | 247 +++++++++++++++++ _ru/map/timers.md | 110 ++++++++ _ru/players/chat.md | 197 +++++++++++++ _ru/players/formspecs.md | 379 +++++++++++++++++++++++++ _ru/players/hud.md | 294 ++++++++++++++++++++ _ru/players/player_physics.md | 77 ++++++ _ru/players/privileges.md | 138 +++++++++ _ru/players/sfinv.md | 5 + _ru/quality/clean_arch.md | 253 +++++++++++++++++ _ru/quality/common_mistakes.md | 140 ++++++++++ _ru/quality/luacheck.md | 107 +++++++ _ru/quality/readmore.md | 26 ++ _ru/quality/releasing.md | 166 +++++++++++ _ru/quality/security.md | 110 ++++++++ _ru/quality/translations.md | 198 +++++++++++++ _ru/quality/unit_testing.md | 172 ++++++++++++ 32 files changed, 5641 insertions(+) create mode 100644 _ru/advmap/biomesdeco.md create mode 100644 _ru/advmap/lvm.md create mode 100644 _ru/basics/getting_started.md create mode 100644 _ru/basics/lua.md create mode 100644 _ru/games/games.md create mode 100644 _ru/index.md create mode 100644 _ru/items/callbacks.md create mode 100644 _ru/items/creating_textures.md create mode 100644 _ru/items/inventories.md create mode 100644 _ru/items/node_drawtypes.md create mode 100644 _ru/items/nodes_items_crafting.md create mode 100644 _ru/map/environment.md create mode 100644 _ru/map/objects.md create mode 100644 _ru/map/storage.md create mode 100644 _ru/map/timers.md create mode 100644 _ru/players/chat.md create mode 100644 _ru/players/formspecs.md create mode 100644 _ru/players/hud.md create mode 100644 _ru/players/player_physics.md create mode 100644 _ru/players/privileges.md create mode 100644 _ru/players/sfinv.md create mode 100644 _ru/quality/clean_arch.md create mode 100644 _ru/quality/common_mistakes.md create mode 100644 _ru/quality/luacheck.md create mode 100644 _ru/quality/readmore.md create mode 100644 _ru/quality/releasing.md create mode 100644 _ru/quality/security.md create mode 100644 _ru/quality/translations.md create mode 100644 _ru/quality/unit_testing.md diff --git a/_config.yml b/_config.yml index ccf5149..6f771a2 100644 --- a/_config.yml +++ b/_config.yml @@ -14,3 +14,5 @@ collections: output: true it: output: true + ru: + output: true diff --git a/_data/languages.yml b/_data/languages.yml index ea5b9c9..2f7d6c0 100644 --- a/_data/languages.yml +++ b/_data/languages.yml @@ -7,3 +7,7 @@ - code: it name: Italiano cta: Questo libro è disponibile in italiano + +- code: ru + name: Russian + cta: Эта кника доступна на русском diff --git a/_layouts/default.html b/_layouts/default.html index 41792a4..4d41b66 100644 --- a/_layouts/default.html +++ b/_layouts/default.html @@ -9,6 +9,9 @@ layout: base {% if language == "_it" %} {% assign language = "it" %} {% assign links = site.it %} +{% elif language == "_ru" %} + {% assign language = "ru" %} + {% assign links = site.ru %} {% else %} {% assign language = "en" %} {% assign links = site.en %} diff --git a/_ru/advmap/biomesdeco.md b/_ru/advmap/biomesdeco.md new file mode 100644 index 0000000..1c36908 --- /dev/null +++ b/_ru/advmap/biomesdeco.md @@ -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 + +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 + +
+ Vernoi +
+ Voronoi diagram, showing the closest point. + By Balu Ertl, CC BY-SA 4.0. +
+
+ +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 diff --git a/_ru/advmap/lvm.md b/_ru/advmap/lvm.md new file mode 100644 index 0000000..b259ab2 --- /dev/null +++ b/_ru/advmap/lvm.md @@ -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 + +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? diff --git a/_ru/basics/getting_started.md b/_ru/basics/getting_started.md new file mode 100644 index 0000000..622c7ff --- /dev/null +++ b/_ru/basics/getting_started.md @@ -0,0 +1,187 @@ +--- +title: Getting Started +layout: default +root: ../.. +idx: 1.1 +description: Learn how to make a mod folder, including init.lua, mod.conf and more. +redirect_from: +- /en/chapters/folders.html +- /en/basics/folders.html +--- + +## Introduction + +Understanding the basic structure of a mod's folder is an essential skill when +creating mods. In this chapter, you'll learn about how modding in Minetest works +and create your first mod. + +- [What are Games and Mods?](#what-are-games-and-mods) +- [Where are mods stored?](#where-are-mods-stored) +- [Creating your first mod](#creating-your-first-mod) + - [Mod directory](#mod-directory) + - [mod.conf](#modconf) + - [init.lua](#initlua) + - [Summary](#summary) +- [Dependencies](#dependencies) +- [Mod Packs](#mod-packs) + + +## What are Games and Mods? + +The power of Minetest is the ability to easily develop games without the need +to create your own voxel graphics, voxel algorithms, or fancy networking code. + +In Minetest, a game is a collection of modules which work together to provide the +content and behaviour of a game. +A module, commonly known as a mod, is a collection of scripts and resources. +It's possible to make a game using only one mod, but this is rarely done because it +reduces the ease by which parts of the game can be adjusted and replaced +independently of others. + +It's also possible to distribute mods outside of a game, in which case they +are also *mods* in the more traditional sense - modifications. These mods adjust +or extend the features of a game. + +Both the mods contained in a game and third-party mods use the same API. + +This book will cover the main parts of the Minetest API, +and is applicable for both game developers and modders. + + +## Where are mods stored? + + + +Each mod has its own directory where its Lua code, textures, models, and +sounds are placed. Minetest checks in several different locations for +mods. These locations are commonly called *mod load paths*. + +For a given world/save game, three mod locations are checked. +They are, in order: + +1. Game mods. These are the mods that form the game that the world is running. + Eg: `minetest/games/minetest_game/mods/`, `/usr/share/minetest/games/minetest/` +2. Global mods, the location to which mods are nearly always installed to. + If in doubt, place them here. + Eg: `minetest/mods/` +3. World mods, the location to store mods which are specific to a + particular world. + Eg: `minetest/worlds/world/worldmods/` + +`minetest` is the user-data directory. You can find the location of the +user-data directory by opening up Minetest and clicking +"Open User Data Directory" in the Credits tab. + +When loading mods, Minetest will check each of the above locations in order. +If it encounters a mod with a name the same as one found previously, the later +mod will be loaded in place of the earlier mod. This means that you can override +game mods by placing a mod with the same name in the global mod location. + + +## Creating your first mod + +### Mod directory + +Go to the global mods directory (About > Open user data directory > mods) and +create a new folder called "mymod". `mymod` is the mod name. + +Each mod should have a unique *mod name*, a technical identifier (id) used to +refer to the mod. Mod names can include letters, numbers, and underscores. A +good name should describe what the mod does, and the directory that contains +the components of a mod must have the same name as the mod name. To find out if +a mod name is available, try searching for it on +[content.minetest.net](https://content.minetest.net). + + mymod + ├── textures + │   └── mymod_node.png files + ├── init.lua + └── mod.conf + +Mods only require an init.lua file; +however, mod.conf is recommended and other components may be needed +depending on the mod's functionality. + +### mod.conf + +Create a mod.conf file with the following content: + +``` +name = mymod +description = Adds foo, bar, and bo. +depends = default +``` + +This file is used for mod metadata including the mod's name, description, and other +information. + +### init.lua + +Create an init.lua file with the following content: + +```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" }, +}) +``` + +The init.lua file is the entrypoint to a mod, and runs when the mod is loaded. + + +### Summary + + +This mod has the name "mymod". It has two text files: init.lua and mod.conf. The +script prints a message and then registers a node and a craft recipe – these +will be explained later on. There's a single dependency, the +[default mod](https://content.minetest.net/metapackages/default/), which is +usually found in Minetest Game. There is also a texture in textures/ for the +node. + + +## Dependencies + +A dependency occurs when a mod requires another mod to be loaded before itself. +One mod may require another mod's code, items, or other resources to be +available for it to use. + +There are two types of dependencies: hard and optional dependencies. +Both require the mod to be loaded first. If the mod being depended on isn't +available, a hard dependency will cause the mod to fail to load, while an optional +dependency might lead to fewer features being enabled. + +An optional dependency is useful if you want to optionally support another mod; +it can enable extra content if the user wishes to use both the mods at the same +time. + +Dependencies are specified in a comma-separated list in mod.conf. + + depends = modone, modtwo + optional_depends = modthree + +## Mod Packs + +Mods can be grouped into mod packs, which allow multiple mods to be packaged +and moved together. They are useful if you want to supply multiple mods to +a player, but don't want to make them download each one individually. + + modpack1 + ├── modpack.conf (required) - signals that this is a mod pack + ├── mod1 + │   └── ... mod files + └── mymod (optional) +    └── ... mod files + +Please note that a modpack is not a *game*. +Games have their own organisational structure which will be explained in the +Games chapter. diff --git a/_ru/basics/lua.md b/_ru/basics/lua.md new file mode 100644 index 0000000..08acca8 --- /dev/null +++ b/_ru/basics/lua.md @@ -0,0 +1,197 @@ +--- +title: Lua Scripting +layout: default +root: ../.. +idx: 1.2 +description: A basic introduction to Lua, including a guide on global/local scope. +redirect_from: /en/chapters/lua.html +--- + +## Introduction + +In this chapter, you'll learn about scripting in Lua, the tools required +to help with this, and some techniques that you may find useful. + +- [Programming](#programming) + - [Coding in Lua](#coding-in-lua) +- [Code Editors](#code-editors) +- [Local and Global Scope](#local-and-global-scope) + - [Locals should be used as much as possible](#locals-should-be-used-as-much-as-possible) +- [Including other Lua Scripts](#including-other-lua-scripts) + + +## Programming + +Programming is the action of taking a problem, such as sorting a list +of items, and turning it into steps that a computer can understand. +Teaching you the logical process of programming is beyond the scope of this book; +however, the following websites are quite useful in developing this: + +* [Codecademy](http://www.codecademy.com/) is one of the best resources for + learning to write code. It provides an interactive tutorial experience. +* [Scratch](https://scratch.mit.edu) is a good resource for starting from + absolute basics, and learning the problem-solving techniques required to program. + It's great for children and teenagers. +* [Programming with Mosh](https://www.youtube.com/user/programmingwithmosh) is + a good YouTube series to learn programming. + +### Coding in Lua + +It's also beyond the scope of this book to teach Lua coding. +The [Programming in Lua (PiL)](https://www.lua.org/pil/contents.html) book is an +excellent introduction to Lua programming. + + +## Code Editors + +A code editor with code highlighting is sufficient for writing scripts in Lua. +Code highlighting uses different colours for words and characters +depending on what they represent. This allows you to easily notice +mistakes and inconsistencies. + +For example: + +```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 +``` + +Keywords in this example are highlighted, including `if`, `then`, `end`, and `return`. +Functions which come with Lua by default, such as `table.insert`, are also highlighted. + +Commonly used editors which are well-suited for Lua include: + +* [VSCode](https://code.visualstudio.com/): + open source (as Code-OSS or VSCodium), popular, and has + [plugins for Minetest](https://marketplace.visualstudio.com/items?itemName=GreenXenith.minetest-tools). +* [Notepad++](http://notepad-plus-plus.org/): simple, Windows-only + +Other suitable editors are also available. + + +## Local and Global Scope + +Whether a variable is local or global determines where it can be written to or +read from. Global variables can be accessed from anywhere in the script file, +and from any other mod: + +```lua +function one() + foo = "bar" +end + +function two() + print(dump(foo)) -- Output: "bar" +end + +one() +two() +``` + +In constrast, a local variable is only accessible from where it is defined. +Lua defaults to variables being global, so you need to explicitly use the +`local` keyword: + +```lua +-- Accessible from within this script file +local one = 1 + +function myfunc() + -- Accessible from within this function + local two = one + one + + if two == one then + -- Accessible from within this if statement + local three = one + two + end +end +``` + + +### Locals should be used as much as possible + +Local variables should be used whenever possible. Mods should only create one +global at most, with the same name as the mod. Creating other globals is sloppy +coding, and Minetest will warn about this: + + Assignment to undeclared global 'foo' inside function at init.lua:2 + +To correct this, use "local": + +```lua +function one() + local foo = "bar" +end + +function two() + print(dump(foo)) -- Output: nil +end + +one() +two() +``` + +Remember that nil means **not initialised**. The variable hasn't been assigned a +value yet, doesn't exist, or has been uninitialised (meaning set to nil). + +Functions are variables of a special type, but should also be made local, +because other mods could have functions with the same names. + +```lua +local function foo(bar) + return bar * 2 +end +``` + +To allow mods to call your functions, you should create a table with the same +name as the mod and add your function to it. This table is often called an API +table or namespace. + +```lua +mymod = {} + +function mymod.foo(bar) + return "foo" .. bar +end + +-- In another mod, or script: +mymod.foo("foobar") +``` + +`function mymod.foo()` is equivalent to `mymod.foo = function()`, it's just a +nicer way to write it. + +## Including other Lua Scripts + +The recommended way to include other Lua scripts in a mod is to use *dofile*. + +```lua +dofile(core.get_modpath("modname") .. "/script.lua") +``` + +A script can return a value, which is useful for sharing private locals: + +```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! +``` + +[Later chapters](../quality/clean_arch.html) will discuss how best to split up +code for a mod. diff --git a/_ru/games/games.md b/_ru/games/games.md new file mode 100644 index 0000000..6d678f7 --- /dev/null +++ b/_ru/games/games.md @@ -0,0 +1,93 @@ +--- +title: Creating Games +layout: default +root: ../.. +idx: 7.1 +--- + +## Introduction + +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. diff --git a/_ru/index.md b/_ru/index.md new file mode 100644 index 0000000..7e6cf6a --- /dev/null +++ b/_ru/index.md @@ -0,0 +1,36 @@ +--- +title: Front Cover +layout: default +description: An easy guide to learn how to create mods for Minetest +homepage: true +no_header: true +root: .. +idx: 0.1 +--- + +
+

Luanti Modding Book (formerly Minetest)

+ + by + with editing by Shara +
+ +## Introduction + +Minetest uses Lua scripts to provide modding support. +This book aims to teach you how to create your own mods, starting from the basics. +Each chapter focuses on a particular part of the API, and will soon get you making +your own mods. + +As well as [reading this book online](https://rubenwardy.com/minetest_modding_book), +you can also [download it in HTML form](https://gitlab.com/rubenwardy/minetest_modding_book/-/releases). + +### Feedback and Contributions + +Noticed a mistake, or want to give feedback? Make sure to tell me about it. + +* Create a [GitLab Issue](https://gitlab.com/rubenwardy/minetest_modding_book/-/issues). +* Post in the [Forum Topic](https://forum.minetest.net/viewtopic.php?f=14&t=10729). +* [Contact me](https://rubenwardy.com/contact/). +* Fancy contributing? + [Read the README](https://gitlab.com/rubenwardy/minetest_modding_book/-/blob/master/README.md). diff --git a/_ru/items/callbacks.md b/_ru/items/callbacks.md new file mode 100644 index 0000000..31a311f --- /dev/null +++ b/_ru/items/callbacks.md @@ -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 + +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. diff --git a/_ru/items/creating_textures.md b/_ru/items/creating_textures.md new file mode 100644 index 0000000..190a89b --- /dev/null +++ b/_ru/items/creating_textures.md @@ -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 + +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. + + + +If you fail to match the edges correctly, the result is far less pleasing +to look at. + + + +### 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. diff --git a/_ru/items/inventories.md b/_ru/items/inventories.md new file mode 100644 index 0000000..8e3822c --- /dev/null +++ b/_ru/items/inventories.md @@ -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 + +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", {}) +``` diff --git a/_ru/items/node_drawtypes.md b/_ru/items/node_drawtypes.md new file mode 100644 index 0000000..1da6e25 --- /dev/null +++ b/_ru/items/node_drawtypes.md @@ -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 + +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 + +
+ Normal Drawtype +
+ Normal Drawtype +
+
+ +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. + +
+ Glasslike's Edges +
+ Glasslike's Edges +
+
+ +```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: + +
+ Glasslike_framed's Edges +
+ Glasslike_Framed's Edges +
+
+ +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 + +
+ Liquid Drawtype +
+ Liquid Drawtype +
+
+ +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 + +
+ Nodebox drawtype +
+ Nodebox drawtype +
+
+ +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 + +
+ Plantlike Drawtype +
+ Plantlike Drawtype +
+
+ +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. + +
+ Firelike nodes +
+ Firelike nodes +
+
+ +```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. diff --git a/_ru/items/nodes_items_crafting.md b/_ru/items/nodes_items_crafting.md new file mode 100644 index 0000000..3702afe --- /dev/null +++ b/_ru/items/nodes_items_crafting.md @@ -0,0 +1,345 @@ +--- +title: Nodes, Items, and Crafting +layout: default +root: ../.. +idx: 2.1 +description: Learn how to register node, items, and craft recipes using register_node, register_item, and register_craft. +redirect_from: /en/chapters/nodes_items_crafting.html +--- + +## Introduction + +Registering new nodes and craftitems, and creating craft recipes, are +basic requirements for many mods. + +- [What are Nodes and Items?](#what-are-nodes-and-items) +- [Registering Items](#registering-items) + - [Item Names](#item-names) + - [Item Aliases](#item-aliases) + - [Textures](#textures) +- [Registering a basic node](#registering-a-basic-node) +- [Crafting](#crafting) + - [Shaped](#shaped) + - [Shapeless](#shapeless) + - [Cooking and Fuel](#cooking-and-fuel) +- [Groups](#groups) +- [Tools, Capabilities, and Dig Types](#tools-capabilities-and-dig-types) + +## What are Nodes and Items? + +Nodes, craftitems, and tools are all Items. An item is something that could be +found in an inventory - even if it isn't possible through normal gameplay. + +A node is an item that can be placed or be found in the world. Every position +in the world must be occupied with one and only one node - seemingly blank +positions are usually air nodes. + +A craftitem can't be placed and is only found in inventories or as a dropped item +in the world. + +A tool is like a craftitem but has the ability to wear. As you use the tool, the +wear bar goes down until the tool breaks. Tools can also never be stacked. In +the future, it's likely that craftitems and tools will merge into one type of +item, as the distinction between them is rather artificial. + +## Registering Items + +Item definitions consist of an *item name* and a *definition table*. +The definition table contains attributes that affect the behaviour of the item. + +```lua +core.register_craftitem("modname:itemname", { + description = "My Special Item", + inventory_image = "modname_itemname.png" +}) +``` + +### Item Names + +Every item has an item name used to refer to it, which should be in the +following format: + + modname:itemname + +The modname is the name of the mod in which the item is registered, and the item +name is the name of the item itself. The item name should be relevant to what +the item is and can't already be registered. + +Both `modname` and `itemname` should only contain lowercase letters, numbers, +and underscores. + +### Item Aliases + +Items can also have *aliases* pointing to their name. An *alias* is a +pseudo-item name that results in the engine treating any occurrences of the +alias as if it were the item name. There are two main common uses of this: + +* Renaming removed items to something else. + There may be unknown nodes in the world and in inventories if an item is + removed from a mod without any corrective code. +* Adding a shortcut. `/giveme dirt` is easier than `/giveme default:dirt`. + +Registering an alias is pretty simple. A good way to remember the order of the +arguments is `from → to` where *from* is the alias and *to* is the target. + +```lua +core.register_alias("dirt", "default:dirt") +``` + +Mods need to make sure to resolve aliases before dealing directly with item names, +as the engine won't do this. +This is pretty simple though: + +```lua +itemname = core.registered_aliases[itemname] or itemname +``` + +### Textures + +Textures should be placed in the textures/ folder with names in the format +`modname_itemname.png`.\\ +JPEG textures are supported, but they do not support transparency and are generally +bad quality at low resolutions. +It is often better to use the PNG format. + +Textures in Minetest are usually 16 by 16 pixels. They can be any resolution, +but it is recommended that they are in the order of 2, for example, 16, 32, 64, +or 128. This is because other resolutions may not be supported correctly on +older devices, especially phones, resulting in degraded performance. + +## Registering a basic node + +Registering nodes is similar to registering items, just with a different +function: + +```lua +core.register_node("mymod:diamond", { + description = "Alien Diamond", + tiles = {"mymod_diamond.png"}, + is_ground_content = true, + groups = {cracky=3, stone=1} +}) +``` + +Node definitions can contain any property in an item definition, and also +contain additional properties specific to nodes. + +The `tiles` property is a table of texture names the node will use. +When there is only one texture, this texture is used on every side. +To give a different texture per-side, supply the names of 6 textures in this order: + + up (+Y), down (-Y), right (+X), left (-X), back (+Z), front (-Z). + (+Y, -Y, +X, -X, +Z, -Z) + +Remember that +Y is upwards in Minetest, as is the convention with +most 3D computer games. + +```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" + -- ^ Rather than dropping diamond, drop mymod:diamond_fragments +}) +``` + +The `is_ground_content` attribute allows caves to be generated over the stone. +This is essential for any node which may be placed during map generation underground. +Caves are cut out of the world after all the other nodes in an area have generated. + +## Crafting + +There are several types of crafting recipe available, indicated by the `type` +property. + +* shaped - Ingredients must be in the correct position. +* shapeless - It doesn't matter where the ingredients are, + just that there is the right amount. +* cooking - Recipes for the furnace to use. +* fuel - Defines items which can be burned in furnaces. +* tool_repair - Defines items which can be tool repaired. + +Craft recipes are not items, so they do not use Item Names to uniquely +identify themselves. + +### Shaped + +Shaped recipes are when the ingredients need to be in the right shape or +pattern to work. In the example below, the fragments need to be in a +chair-like pattern for the craft to work. + +```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", ""} + } +}) +``` + +One thing to note is the blank column on the right-hand side. +This means that there *must* be an empty column to the right of the shape, otherwise +this won't work. +If this empty column shouldn't be required, then the empty strings can be left +out like so: + +```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"} + } +}) +``` + +The type field isn't actually needed for shaped crafts, as shaped is the +default craft type. + +### Shapeless + +Shapeless recipes are a type of recipe which is used when it doesn't matter +where the ingredients are placed, just that they're there. + +```lua +core.register_craft({ + type = "shapeless", + output = "mymod:diamond 3", + recipe = { + "mymod:diamond_fragments", + "mymod:diamond_fragments", + "mymod:diamond_fragments", + }, +}) +``` + +### Cooking and Fuel + +Recipes with the type "cooking" are not made in the crafting grid, +but are cooked in furnaces, or other cooking tools that might be found in mods. + +```lua +core.register_craft({ + type = "cooking", + output = "mymod:diamond_fragments", + recipe = "default:coalblock", + cooktime = 10, +}) +``` + +The only real difference in the code is that the recipe is just a single item, +compared to being in a table (between braces). +They also have an optional "cooktime" parameter which +defines how long the item takes to cook. +If this is not set, it defaults to 3. + +The recipe above works when the coal block is in the input slot, +with some form of fuel below it. +It creates diamond fragments after 10 seconds! + +This type is an accompaniment to the cooking type, as it defines +what can be burned in furnaces and other cooking tools from mods. + +```lua +core.register_craft({ + type = "fuel", + recipe = "mymod:diamond", + burntime = 300, +}) +``` + +They don't have an output like other recipes, but they have a burn time +which defines how long they will last as fuel in seconds. +So, the diamond is good as fuel for 300 seconds! + +## Groups + +Items can be members of many groups and groups can have many members. +Groups are defined using the `groups` property in the definition table +and have an associated value. + +```lua +groups = {cracky = 3, wood = 1} +``` + +There are several reasons you use groups. +Firstly, groups are used to describe properties such as dig types and flammability. +Secondly, groups can be used in a craft recipe instead of an item name to allow +any item in the group to be used. + +```lua +core.register_craft({ + type = "shapeless", + output = "mymod:diamond_thing 3", + recipe = {"group:wood", "mymod:diamond"} +}) +``` + + +## Tools, Capabilities, and Dig Types + +Dig types are groups which are used to define how strong a node is when dug +with different tools. +A dig type group with a higher associated value means the node is easier +and quicker to cut. +It's possible to combine multiple dig types to allow the more efficient use +of multiple types of tools. +A node with no dig types cannot be dug by any tools. + + +| Group | Best Tool | Description | +|--------|-----------|-------------| +| crumbly | spade | Dirt, sand | +| cracky | pickaxe | Tough (but brittle) stuff like stone | +| snappy | *any* | Can be cut using fine tools;
e.g. leaves, smallplants, wire, sheets of metal | +| choppy | axe | Can be cut using a sharp force; e.g. trees, wooden planks | +| fleshy | sword | Living things like animals and the player.
This could imply some blood effects when hitting. | +| explody | ? | Especially prone to explosions | +| oddly_breakable_by_hand | *any* | Torches and such - very quick to dig | + + +Every tool has a tool capability. +A capability includes a list of supported dig types, and associated properties +for each type such as dig times and the amount of wear. +Tools can also have a maximum supported hardness for each type, which makes +it possible to prevent weaker tools from digging harder nodes. +It's very common for tools to include all dig types in their capabilities, +with the less suitable ones having very inefficient properties. +If the item a player is currently wielding doesn't have an explicit tool +capability, then the capability of the current hand is used instead. + +```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 is the list of supported dig types for digging nodes. +Damage groups are for controlling how tools damage objects, which will be +discussed later in the Objects, Players, and Entities chapter. diff --git a/_ru/map/environment.md b/_ru/map/environment.md new file mode 100644 index 0000000..07e0e2c --- /dev/null +++ b/_ru/map/environment.md @@ -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 + +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. diff --git a/_ru/map/objects.md b/_ru/map/objects.md new file mode 100644 index 0000000..5c8602c --- /dev/null +++ b/_ru/map/objects.md @@ -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 + +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. + + + +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). diff --git a/_ru/map/storage.md b/_ru/map/storage.md new file mode 100644 index 0000000..7e21eab --- /dev/null +++ b/_ru/map/storage.md @@ -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 + +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_()` and `set_()` 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`.) diff --git a/_ru/map/timers.md b/_ru/map/timers.md new file mode 100644 index 0000000..9d37828 --- /dev/null +++ b/_ru/map/timers.md @@ -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 + +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. diff --git a/_ru/players/chat.md b/_ru/players/chat.md new file mode 100644 index 0000000..7315081 --- /dev/null +++ b/_ru/players/chat.md @@ -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 + +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: + + Look at this entrance + This is a chat message to all players + 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 + + + +`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 " + 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 `. 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) +``` diff --git a/_ru/players/formspecs.md b/_ru/players/formspecs.md new file mode 100644 index 0000000..ad30ac3 --- /dev/null +++ b/_ru/players/formspecs.md @@ -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 + +
+ Furnace Inventory +
+ Screenshot of furnace formspec, labelled. +
+
+ +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 + +
+ Guessing Formspec +
+ The guessing game formspec. +
+
+ +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. + +
+ +```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 + +
+ Padding and spacing +
+ The guessing game formspec. +
+
+ +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`. + +
+ + +### 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. diff --git a/_ru/players/hud.md b/_ru/players/hud.md new file mode 100644 index 0000000..e1f7fd1 --- /dev/null +++ b/_ru/players/hud.md @@ -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 + +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 + +
+ Diagram showing a centered text element +
+ +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. + +
+ +### 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. + +
+ Diagram showing alignment +
+ +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: + +
+ screenshot of the HUD we're aiming for +
+ +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: + +
+ screenshot of the HUD we're aiming for +
+ + +## 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: + +
+ screenshot of the HUD so far +
+ +### 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. diff --git a/_ru/players/player_physics.md b/_ru/players/player_physics.md new file mode 100644 index 0000000..e4d644e --- /dev/null +++ b/_ru/players/player_physics.md @@ -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 + +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. diff --git a/_ru/players/privileges.md b/_ru/players/privileges.md new file mode 100644 index 0000000..138adbb --- /dev/null +++ b/_ru/players/privileges.md @@ -0,0 +1,138 @@ +--- +title: Privileges +layout: default +root: ../.. +idx: 4.1 +description: Registering privs. +redirect_from: /en/chapters/privileges.html +--- + +## Introduction + +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. diff --git a/_ru/players/sfinv.md b/_ru/players/sfinv.md new file mode 100644 index 0000000..6671103 --- /dev/null +++ b/_ru/players/sfinv.md @@ -0,0 +1,5 @@ +--- +sitemap: false +redirect_from: /en/chapters/sfinv.html +redirect_to: "https://github.com/rubenwardy/sfinv/blob/master/Tutorial.md" +--- diff --git a/_ru/quality/clean_arch.md b/_ru/quality/clean_arch.md new file mode 100644 index 0000000..245c2d7 --- /dev/null +++ b/_ru/quality/clean_arch.md @@ -0,0 +1,253 @@ +--- +title: Intro to Clean Architectures +layout: default +root: ../.. +idx: 8.4 +--- + +## Introduction + +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. + +> 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. + +
+ Diagram showing a centered text element +
+ +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. diff --git a/_ru/quality/common_mistakes.md b/_ru/quality/common_mistakes.md new file mode 100644 index 0000000..570a6b7 --- /dev/null +++ b/_ru/quality/common_mistakes.md @@ -0,0 +1,140 @@ +--- +title: Common Mistakes +layout: default +root: ../.. +idx: 8.1 +redirect_from: /en/chapters/common_mistakes.html +--- + +## Introduction + +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. diff --git a/_ru/quality/luacheck.md b/_ru/quality/luacheck.md new file mode 100644 index 0000000..963196f --- /dev/null +++ b/_ru/quality/luacheck.md @@ -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 + +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). diff --git a/_ru/quality/readmore.md b/_ru/quality/readmore.md new file mode 100644 index 0000000..03c5f73 --- /dev/null +++ b/_ru/quality/readmore.md @@ -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). diff --git a/_ru/quality/releasing.md b/_ru/quality/releasing.md new file mode 100644 index 0000000..9036ac2 --- /dev/null +++ b/_ru/quality/releasing.md @@ -0,0 +1,166 @@ +--- +title: Releasing a Mod +layout: default +root: ../.. +idx: 8.6 +redirect_from: /en/chapters/releasing.html +--- + +## Introduction + +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] diff --git a/_ru/quality/security.md b/_ru/quality/security.md new file mode 100644 index 0000000..5a70a1d --- /dev/null +++ b/_ru/quality/security.md @@ -0,0 +1,110 @@ +--- +title: Security +layout: default +root: ../.. +idx: 8.3 +--- + +## Introduction + +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. diff --git a/_ru/quality/translations.md b/_ru/quality/translations.md new file mode 100644 index 0000000..b6a40b1 --- /dev/null +++ b/_ru/quality/translations.md @@ -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 + +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. diff --git a/_ru/quality/unit_testing.md b/_ru/quality/unit_testing.md new file mode 100644 index 0000000..3c0703d --- /dev/null +++ b/_ru/quality/unit_testing.md @@ -0,0 +1,172 @@ +--- +title: Automatic Unit Testing +layout: default +root: ../.. +idx: 8.5 +--- + +## Introduction + +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).