Translation via DeepL
parent
cf5eb03bac
commit
649f5d30cc
|
@ -0,0 +1,215 @@
|
|||
---
|
||||
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.
|
||||
|
||||
* [What are Games and Mods?](#what-are-games-and-mods)
|
||||
* [Where are mods stored?](#where-are-mods-stored)
|
||||
* [Mod Directory](#mod-directory)
|
||||
* [Dependencies](#dependencies)
|
||||
* [Mod Packs](#mod-packs)
|
||||
* [Example](#example)
|
||||
|
||||
|
||||
## 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?
|
||||
|
||||
<a name="mod-locations"></a>
|
||||
|
||||
Each mod has its own directory where its Lua code, textures, models, and
|
||||
sounds are placed. Minetest checks in a number of 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 will check the locations in the order given above. 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.
|
||||
|
||||
The actual location of each mod load path depends on what operating system you're
|
||||
using, and how you installed Minetest.
|
||||
|
||||
* **Windows:**
|
||||
* For portable builds, ie: from a .zip file, just go to the directory where
|
||||
you extracted the zip and look for the `games`, `mods`, and `worlds`
|
||||
directories.
|
||||
* For installed builds, ie: from a setup.exe,
|
||||
look in C:\\\\Minetest or C:\\\\Games\\Minetest.
|
||||
* **GNU/Linux:**
|
||||
* For system-wide installs, look in `~/.minetest`.
|
||||
Note that `~` means the user home directory, and that files and directories
|
||||
starting with a dot (`.`) are hidden.
|
||||
* For portable installs, look in the build directory.
|
||||
* For Flatpak installs, look in `~/.var/app/net.minetest.Minetest/.minetest/mods/`.
|
||||
* **MacOS**
|
||||
* Look in `~/Library/Application Support/minetest/`.
|
||||
Note that `~` means the user home, ie: `/Users/USERNAME/`.
|
||||
|
||||
## Mod Directory
|
||||
|
||||
![Find the mod's directory]({{ page.root }}/static/folder_modfolder.jpg)
|
||||
|
||||
A *mod name* is used to refer to a mod. Each mod should have a unique name.
|
||||
Mod names can include letters, numbers, and underscores. A good name should
|
||||
describe what the mod does, and the directory which 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
|
||||
├── init.lua (required) - Runs when the game loads.
|
||||
├── mod.conf (recommended) - Contains description and dependencies.
|
||||
├── textures (optional)
|
||||
│ └── ... any textures or images
|
||||
├── sounds (optional)
|
||||
│ └── ... any sounds
|
||||
└── ... any other files or directories
|
||||
|
||||
Only the init.lua file is required in a mod for it to run on game load;
|
||||
however, mod.conf is recommended and other components may be needed
|
||||
depending on the mod's functionality.
|
||||
|
||||
|
||||
## 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 should be listed in mod.conf.
|
||||
|
||||
### mod.conf
|
||||
|
||||
This file is used for mod metadata including the mod's name, description, and other
|
||||
information. For example:
|
||||
|
||||
name = mymod
|
||||
description = Adds foo, bar, and bo.
|
||||
depends = modone, modtwo
|
||||
optional_depends = modthree
|
||||
|
||||
### depends.txt
|
||||
|
||||
For compatibility with 0.4.x versions of Minetest, instead of only specifying
|
||||
dependencies in mod.conf, you need to provide a depends.txt file in which
|
||||
you list all dependencies:
|
||||
|
||||
modone
|
||||
modtwo
|
||||
modthree?
|
||||
|
||||
Each mod name is on its own line, and mod names with a question mark
|
||||
following them are optional dependencies.
|
||||
If an optional dependency is installed, it is loaded before the mod;
|
||||
however, if the dependency is not installed, the mod still loads.
|
||||
This is in contrast to normal dependencies which will cause the current
|
||||
mod not to work if the dependency is not installed.
|
||||
|
||||
## 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.lua (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.
|
||||
|
||||
## Example
|
||||
|
||||
Here is an example which puts all of this together:
|
||||
|
||||
### Mod Folder
|
||||
mymod
|
||||
├── textures
|
||||
│ └── mymod_node.png files
|
||||
├── depends.txt
|
||||
├── init.lua
|
||||
└── mod.conf
|
||||
|
||||
### depends.txt
|
||||
default
|
||||
|
||||
### init.lua
|
||||
```lua
|
||||
print("This file will be run at load time!")
|
||||
|
||||
minetest.register_node("mymod:node", {
|
||||
description = "This is a node",
|
||||
tiles = {"mymod_node.png"},
|
||||
groups = {cracky = 1}
|
||||
})
|
||||
```
|
||||
|
||||
### mod.conf
|
||||
name = mymod
|
||||
descriptions = Adds a node
|
||||
depends = default
|
||||
|
||||
This mod has the name "mymod". It has three text files: init.lua, mod.conf,
|
||||
and depends.txt.\\
|
||||
The script prints a message and then registers a node –
|
||||
which will be explained in the next chapter.\\
|
||||
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.
|
|
@ -0,0 +1,313 @@
|
|||
---
|
||||
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 we will talk about scripting in Lua, the tools required,
|
||||
and go over some techniques which you will probably find useful.
|
||||
|
||||
* [Code Editors](#code-editors)
|
||||
* [Coding in Lua](#coding-in-lua)
|
||||
* [Program Flow](#program-flow)
|
||||
* [Variable Types](#variable-types)
|
||||
* [Arithmetic Operators](#arithmetic-operators)
|
||||
* [Selection](#selection)
|
||||
* [Logical Operators](#logical-operators)
|
||||
* [Programming](#programming)
|
||||
* [Local and Global Scope](#local-and-global-scope)
|
||||
* [Including other Lua Scripts](#including-other-lua-scripts)
|
||||
|
||||
## Code Editors
|
||||
|
||||
A code editor with code highlighting is sufficient for writing scripts in Lua.
|
||||
Code highlighting gives different colours to different words and characters
|
||||
depending on what they mean. This allows you to spot mistakes.
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
For example, keywords in the above snippet are highlighted such as if, then, end, and return.
|
||||
table.insert is a function which comes with Lua by default.
|
||||
|
||||
Here is a list of common editors well suited for Lua.
|
||||
Other editors are available, of course.
|
||||
|
||||
* Windows: [Notepad++](http://notepad-plus-plus.org/), [Atom](http://atom.io/), [VS Code](https://code.visualstudio.com/)
|
||||
* Linux: Kate, Gedit, [Atom](http://atom.io/), [VS Code](https://code.visualstudio.com/)
|
||||
* OSX: [Atom](http://atom.io/), [VS Code](https://code.visualstudio.com/)
|
||||
|
||||
## Coding in Lua
|
||||
|
||||
### Program Flow
|
||||
|
||||
Programs are a series of commands that run one after another.
|
||||
We call these commands "statements."
|
||||
Program flow is how these statements are executed.
|
||||
Different types of flow allow you to skip or jump over sets of commands.
|
||||
There are three main types of flow:
|
||||
|
||||
* Sequence: Just run one statement after another, no skipping.
|
||||
* Selection: Skip over sequences depending on conditions.
|
||||
* Iteration: Repeating, looping. Keep running the same
|
||||
statements until a condition is met.
|
||||
|
||||
So, what do statements in Lua look like?
|
||||
|
||||
```lua
|
||||
local a = 2 -- Set 'a' to 2
|
||||
local b = 2 -- Set 'b' to 2
|
||||
local result = a + b -- Set 'result' to a + b, which is 4
|
||||
a = a + 10
|
||||
print("Sum is "..result)
|
||||
```
|
||||
|
||||
Whoa, what happened there?
|
||||
|
||||
a, b, and result are *variables*. Local variables are declared
|
||||
by using the local keyword, and then given an initial value.
|
||||
Local will be discussed in a bit, as it's part of a very important concept called
|
||||
*scope*.
|
||||
|
||||
The `=` means *assignment*, so `result = a + b` means set "result" to a + b.
|
||||
Variable names can be longer than one character unlike in mathematics, as seen with the "result" variable.
|
||||
It's also worth noting that Lua is *case-sensitive*; A is a different variable to a.
|
||||
|
||||
### Variable Types
|
||||
|
||||
A variable will be only one of the following types and can change type after an
|
||||
assignment.
|
||||
It's good practice to make sure a variable is only ever nil or a single non-nil type.
|
||||
|
||||
| Type | Description | Example |
|
||||
|----------|---------------------------------|----------------|
|
||||
| Nil | Not initialised. The variable is empty, it has no value | `local A`, `D = nil` |
|
||||
| Number | A whole or decimal number. | `local A = 4` |
|
||||
| String | A piece of text | `local D = "one two three"` |
|
||||
| Boolean | True or False | `local is_true = false`, `local E = (1 == 1)` |
|
||||
| Table | Lists | Explained below |
|
||||
| Function | Can run. May require inputs and may return a value | `local result = func(1, 2, 3)` |
|
||||
|
||||
### Arithmetic Operators
|
||||
|
||||
Not an exhaustive list. Doesn't contain every possible operator.
|
||||
|
||||
| Symbol | Purpose | Example |
|
||||
|--------|----------------|---------------------------|
|
||||
| A + B | Addition | 2 + 2 = 4 |
|
||||
| A - B | Subtraction | 2 - 10 = -8 |
|
||||
| A * B | Multiplication | 2 * 2 = 4 |
|
||||
| A / B | Division | 100 / 50 = 2 |
|
||||
| A ^ B | Powers | 2 ^ 2 = 2<sup>2</sup> = 4 |
|
||||
| A .. B | Join strings | "foo" .. "bar" = "foobar" |
|
||||
|
||||
### Selection
|
||||
|
||||
The most basic selection is the if statement. It looks like this:
|
||||
|
||||
```lua
|
||||
local random_number = math.random(1, 100) -- Between 1 and 100.
|
||||
if random_number > 50 then
|
||||
print("Woohoo!")
|
||||
else
|
||||
print("No!")
|
||||
end
|
||||
```
|
||||
|
||||
That example generates a random number between 1 and 100. It then prints
|
||||
"Woohoo!" if that number is bigger than 50, otherwise it prints "No!".
|
||||
What else can you get apart from '>'?
|
||||
|
||||
### Logical Operators
|
||||
|
||||
| Symbol | Purpose | Example |
|
||||
|---------|--------------------------------------|-------------------------------------------------------------|
|
||||
| A == B | Equals | 1 == 1 (true), 1 == 2 (false) |
|
||||
| A ~= B | Doesn't equal | 1 ~= 1 (false), 1 ~= 2 (true) |
|
||||
| A > B | Greater than | 5 > 2 (true), 1 > 2 (false), 1 > 1 (false) |
|
||||
| A < B | Less than | 1 < 3 (true), 3 < 1 (false), 1 < 1 (false) |
|
||||
| A >= B | Greater than or equals | 5 >= 5 (true), 5 >= 3 (true), 5 >= 6 (false) |
|
||||
| A <= B | Less than or equals | 3 <= 6 (true), 3 <= 3 (true) |
|
||||
| A and B | And (both must be correct) | (2 > 1) and (1 == 1) (true), (2 > 3) and (1 == 1) (false) |
|
||||
| A or B | either or. One or both must be true. | (2 > 1) or (1 == 2) (true), (2 > 4) or (1 == 3) (false) |
|
||||
| not A | not true | not (1 == 2) (true), not (1 == 1) (false) |
|
||||
|
||||
That doesn't contain every possible operator, and you can combine operators like this:
|
||||
|
||||
```lua
|
||||
if not A and B then
|
||||
print("Yay!")
|
||||
end
|
||||
```
|
||||
|
||||
Which prints "Yay!" if A is false and B is true.
|
||||
|
||||
Logical and arithmetic operators work exactly the same;
|
||||
they both accept inputs and return a value which can be stored.
|
||||
|
||||
```lua
|
||||
local A = 5
|
||||
local is_equal = (A == 5)
|
||||
if is_equal then
|
||||
print("Is equal!")
|
||||
end
|
||||
```
|
||||
|
||||
## Programming
|
||||
|
||||
Programming is the action of taking a problem, such as sorting a list
|
||||
of items, and then 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 'code', it provides an interactive tutorial experience.
|
||||
* [Scratch](https://scratch.mit.edu) is a good resource when starting from
|
||||
absolute basics, learning the problem solving techniques required to program.\\
|
||||
Scratch is **designed to teach children** how to program, and isn't a serious
|
||||
programming language.
|
||||
|
||||
## Local and Global Scope
|
||||
|
||||
Whether a variable is local or global determines where it can be written to or read to.
|
||||
A local variable is only accessible from where it is defined. Here are some examples:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Whereas global variables can be accessed from anywhere in the script file, and from any other mod.
|
||||
|
||||
```lua
|
||||
my_global_variable = "blah"
|
||||
|
||||
function one()
|
||||
my_global_variable = "three"
|
||||
end
|
||||
|
||||
print(my_global_variable) -- Output: "blah"
|
||||
one()
|
||||
print(my_global_variable) -- Output: "three"
|
||||
```
|
||||
|
||||
|
||||
### Locals should be used as much as possible
|
||||
|
||||
Lua is global by default (unlike most other programming languages).
|
||||
Local variables must be identified as such.
|
||||
|
||||
```lua
|
||||
function one()
|
||||
foo = "bar"
|
||||
end
|
||||
|
||||
function two()
|
||||
print(dump(foo)) -- Output: "bar"
|
||||
end
|
||||
|
||||
one()
|
||||
two()
|
||||
```
|
||||
|
||||
dump() is a function that can turn any variable into a string so the programmer can
|
||||
see what it is. The foo variable will be printed as "bar", including the quotes
|
||||
which show it is a string.
|
||||
|
||||
This is sloppy coding, and Minetest will in fact 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 (ie: set to nil).
|
||||
|
||||
The same goes for functions. Functions are variables of a special type, and
|
||||
should be made local, as other mods could have functions of the same name.
|
||||
|
||||
```lua
|
||||
local function foo(bar)
|
||||
return bar * 2
|
||||
end
|
||||
```
|
||||
|
||||
API tables should be used to allow other mods to call the functions, like so:
|
||||
|
||||
```lua
|
||||
mymod = {}
|
||||
|
||||
function mymod.foo(bar)
|
||||
return "foo" .. bar
|
||||
end
|
||||
|
||||
-- In another mod, or script:
|
||||
mymod.foo("foobar")
|
||||
```
|
||||
|
||||
## Including other Lua Scripts
|
||||
|
||||
The recommended way to include other Lua scripts in a mod is to use *dofile*.
|
||||
|
||||
```lua
|
||||
dofile(minetest.get_modpath("modname") .. "/script.lua")
|
||||
```
|
||||
|
||||
"local" variables declared outside of any functions in a script file will be local to that script.
|
||||
A script can return a value, which is useful for sharing private locals:
|
||||
|
||||
```lua
|
||||
-- script.lua
|
||||
return "Hello world!"
|
||||
|
||||
-- init.lua
|
||||
local ret = dofile(minetest.get_modpath("modname") .. "/script.lua")
|
||||
print(ret) -- Hello world!
|
||||
```
|
||||
|
||||
Later chapters will discuss how to split up the code of a mod in a lot of detail.
|
||||
However, the simplistic approach for now is to have different files for different
|
||||
types of things - nodes.lua, crafts.lua, craftitems.lua, etc.
|
|
@ -0,0 +1,93 @@
|
|||
---
|
||||
title: Creating Games
|
||||
layout: default
|
||||
root: ../..
|
||||
idx: 6.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 steam punk 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
|
||||
|
||||
* Make a game - It can be simple, if you like.
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
title: Front Cover
|
||||
layout: default
|
||||
homepage: true
|
||||
no_header: true
|
||||
root: ..
|
||||
idx: 0.1
|
||||
---
|
||||
|
||||
<div id="header">
|
||||
<h1>Minetest Modding Book</h1>
|
||||
|
||||
<span>by <a href="https://rubenwardy.com" rel="author">rubenwardy</a></span>
|
||||
<span>with editing by <a href="http://rc.minetest.tv/">Shara</a></span>
|
||||
</div>
|
||||
|
||||
## 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://github.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 [GitHub Issue](https://github.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://github.com/rubenwardy/minetest_modding_book/blob/master/README.md).
|
|
@ -0,0 +1,80 @@
|
|||
---
|
||||
title: Creating Textures
|
||||
layout: default
|
||||
root: ../..
|
||||
idx: 2.2
|
||||
description: An introduction to making textures in your editor of choice, an 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.
|
||||
|
||||
* [Learning to Draw](#learning-to-draw)
|
||||
* [Techniques](#techniques)
|
||||
* [Editors](#editors)
|
||||
|
||||
## Techniques
|
||||
|
||||
### Using the Pencil
|
||||
|
||||
The pencil tool is available in most editors. When set to its lowest size,
|
||||
it allows you to edit one pixel at a time without changing any other parts
|
||||
of the image. By manipulating the pixels one at a time, you create clear
|
||||
and sharp textures without unintended blurring. It also gives you a high
|
||||
level of precision and control.
|
||||
|
||||
### Tiling
|
||||
|
||||
Textures used for nodes should generally be designed to tile. This means
|
||||
when you place multiple nodes with the same texture together, the edges line
|
||||
up correctly.
|
||||
|
||||
<!-- IMAGE NEEDED - cobblestone that tiles correctly -->
|
||||
|
||||
If you fail to match the edges correctly, the result is far less pleasing
|
||||
to look at.
|
||||
|
||||
<!-- IMAGE NEEDED - node that doesn't tile correctly -->
|
||||
|
||||
### Transparency
|
||||
|
||||
Transparency is important when creating textures for nearly all craftitems
|
||||
and some nodes, such as glass.
|
||||
Not all editors support transparency, so make sure you choose an
|
||||
editor which is suitable for the textures you wish to create.
|
||||
|
||||
## 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.
|
||||
|
||||
### 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, the pencil tool can be selected from the Toolbox:
|
||||
|
||||
<figure>
|
||||
<img src="{{ page.root }}//static/pixel_art_gimp_pencil.png" alt="Pencil in GIMP">
|
||||
</figure>
|
||||
|
||||
It's also advisable to select the Hard edge checkbox for the eraser tool.
|
|
@ -0,0 +1,303 @@
|
|||
---
|
||||
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 Item Stacks and Inventories?](#what-are-item-stacks-and-inventories)
|
||||
* [ItemStacks](#itemstacks)
|
||||
* [Inventory Locations](#inventory-locations)
|
||||
* [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 simply called *lists* in the context
|
||||
of inventories.
|
||||
The point of an inventory is to allow multiple grids when Players
|
||||
and Nodes only have at most one inventory in them.
|
||||
|
||||
## ItemStacks
|
||||
|
||||
ItemStacks have three components to them.
|
||||
|
||||
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 Metadata and Storage 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 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](node_metadata.html).
|
||||
|
||||
```lua
|
||||
local inv = minetest.get_inventory({ type="node", pos={x=1, y=2, z=3} })
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
Player inventories can be obtained similarly or using a player reference.
|
||||
The player must be online to access their inventory.
|
||||
|
||||
```lua
|
||||
local inv = minetest.get_inventory({ type="player", name="player1" })
|
||||
-- or
|
||||
local inv = player:get_inventory()
|
||||
```
|
||||
|
||||
A detached inventory is one which is independent of players or nodes.
|
||||
Detached inventories also don't save over a restart.
|
||||
Detached inventories need to be created before they can be used -
|
||||
this will be covered later.
|
||||
|
||||
```lua
|
||||
local inv = minetest.get_inventory({
|
||||
type="detached", name="inventory_name" })
|
||||
```
|
||||
|
||||
The location of an inventory reference can be found like so:
|
||||
|
||||
```lua
|
||||
local location = inv:get_location()
|
||||
```
|
||||
|
||||
## 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 a number of common lists
|
||||
which 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 list width 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.
|
||||
|
||||
## Modifying Inventories and ItemStacks
|
||||
|
||||
### Adding to a List
|
||||
|
||||
To add items to a list named `"main"` while respecting maximum stack sizes:
|
||||
|
||||
```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 depends 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", {})
|
||||
```
|
|
@ -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
|
||||
`minetest.register_node` is a bit misleading in that it doesn't actually
|
||||
register a node - it registers a new *type* of node.
|
||||
|
||||
The node params are used to control how a node is individually rendered.
|
||||
`param1` is used to store the lighting of a node, and the meaning of
|
||||
`param2` depends on the `paramtype2` property of the node type definition.
|
||||
|
||||
* [Cubic Nodes: Normal and Allfaces](#cubic-nodes-normal-and-allfaces)
|
||||
* [Glasslike Nodes](#glasslike-nodes)
|
||||
* [Glasslike_Framed](#glasslike_framed)
|
||||
* [Airlike Nodes](#airlike-nodes)
|
||||
* [Lighting and Sunlight Propagation](#lighting-and-sunlight-propagation)
|
||||
* [Liquid Nodes](#liquid-nodes)
|
||||
* [Node Boxes](#node-boxes)
|
||||
* [Wallmounted Node Boxes](#wallmounted-node-boxes)
|
||||
* [Mesh Nodes](#mesh-nodes)
|
||||
* [Signlike Nodes](#signlike-nodes)
|
||||
* [Plantlike Nodes](#plantlike-Nodes)
|
||||
* [Firelike Nodes](#firelike-nodes)
|
||||
* [More Drawtypes](#more-drawtypes)
|
||||
|
||||
|
||||
## Cubic Nodes: Normal and Allfaces
|
||||
|
||||
<figure class="right_image">
|
||||
<img src="{{ page.root }}//static/drawtype_normal.png" alt="Normal Drawtype">
|
||||
<figcaption>
|
||||
Normal Drawtype
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
The normal drawtype is typically used to render a cubic node.
|
||||
If the side of a normal node is against a solid side, then that side won't be rendered,
|
||||
resulting in a large performance gain.
|
||||
|
||||
In contrast, the allfaces drawtype will still render the inner side when up against
|
||||
a solid node. This is good for nodes with partially transparent sides, such as
|
||||
leaf nodes. You can use the allfaces_optional drawtype to allow users to opt-out
|
||||
of the slower drawing, in which case it'll act like a normal node.
|
||||
|
||||
```lua
|
||||
minetest.register_node("mymod:diamond", {
|
||||
description = "Alien Diamond",
|
||||
tiles = {"mymod_diamond.png"},
|
||||
groups = {cracky = 3},
|
||||
})
|
||||
|
||||
minetest.register_node("default:leaves", {
|
||||
description = "Leaves",
|
||||
drawtype = "allfaces_optional",
|
||||
tiles = {"default_leaves.png"}
|
||||
})
|
||||
```
|
||||
|
||||
Note: the normal drawtype is the default drawtype, so you don't need to explicitly
|
||||
specify it.
|
||||
|
||||
|
||||
## Glasslike Nodes
|
||||
|
||||
The difference between glasslike and normal nodes is that placing a glasslike node
|
||||
next to a normal node won't cause the side of the normal node to be hidden.
|
||||
This is useful because glasslike nodes tend to be transparent, and so using a normal
|
||||
drawtype would result in the ability to see through the world.
|
||||
|
||||
<figure>
|
||||
<img src="{{ page.root }}//static/drawtype_glasslike_edges.png" alt="Glasslike's Edges">
|
||||
<figcaption>
|
||||
Glasslike's Edges
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
```lua
|
||||
minetest.register_node("default:obsidian_glass", {
|
||||
description = "Obsidian Glass",
|
||||
drawtype = "glasslike",
|
||||
tiles = {"default_obsidian_glass.png"},
|
||||
paramtype = "light",
|
||||
is_ground_content = false,
|
||||
sunlight_propagates = true,
|
||||
sounds = default.node_sound_glass_defaults(),
|
||||
groups = {cracky=3,oddly_breakable_by_hand=3},
|
||||
})
|
||||
```
|
||||
|
||||
### Glasslike_Framed
|
||||
|
||||
This makes the node's edge go around the whole thing with a 3D effect, rather
|
||||
than individual nodes, like the following:
|
||||
|
||||
<figure>
|
||||
<img src="{{ page.root }}//static/drawtype_glasslike_framed.png" alt="Glasslike_framed's Edges">
|
||||
<figcaption>
|
||||
Glasslike_Framed's Edges
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
You can use the glasslike_framed_optional drawtype to allow the user to *opt-in*
|
||||
to the framed appearance.
|
||||
|
||||
```lua
|
||||
minetest.register_node("default:glass", {
|
||||
description = "Glass",
|
||||
drawtype = "glasslike_framed",
|
||||
tiles = {"default_glass.png", "default_glass_detail.png"},
|
||||
inventory_image = minetest.inventorycube("default_glass.png"),
|
||||
paramtype = "light",
|
||||
sunlight_propagates = true, -- Sunlight can shine through block
|
||||
groups = {cracky = 3, oddly_breakable_by_hand = 3},
|
||||
sounds = default.node_sound_glass_defaults()
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
## Airlike Nodes
|
||||
|
||||
These nodes are not rendered, and thus have no textures.
|
||||
|
||||
```lua
|
||||
minetest.register_node("myair:air", {
|
||||
description = "MyAir (you hacker you!)",
|
||||
drawtype = "airlike",
|
||||
paramtype = "light",
|
||||
sunlight_propagates = true,
|
||||
|
||||
walkable = false, -- Would make the player collide with the air node
|
||||
pointable = false, -- You can't select the node
|
||||
diggable = false, -- You can't dig the node
|
||||
buildable_to = true, -- Nodes can be replace this node.
|
||||
-- (you can place a node and remove the air node
|
||||
-- that used to be there)
|
||||
|
||||
air_equivalent = true,
|
||||
drop = "",
|
||||
groups = {not_in_creative_inventory=1}
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
## Lighting and Sunlight Propagation
|
||||
|
||||
The lighting of a node is stored in param1. In order to work out how to shade
|
||||
a node's side, the light value of the neighbouring node is used.
|
||||
Because of this, solid nodes don't have light values because they block light.
|
||||
|
||||
By default, a node type won't allow light to be stored in any node instances.
|
||||
It's usually desirable for some nodes such as glass and air to be able to
|
||||
let light through. To do this, there are two properties which need to be defined:
|
||||
|
||||
```lua
|
||||
paramtype = "light",
|
||||
sunlight_propagates = true,
|
||||
```
|
||||
|
||||
The first line means that param1 does, in fact, store the light level.
|
||||
The second line means that sunlight should go through this node without decreasing in value.
|
||||
|
||||
|
||||
## Liquid Nodes
|
||||
|
||||
<figure class="right_image">
|
||||
<img src="{{ page.root }}//static/drawtype_liquid.png" alt="Liquid Drawtype">
|
||||
<figcaption>
|
||||
Liquid Drawtype
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
Each type of liquid requires two node definitions - one for the liquid source, and
|
||||
another for flowing liquid.
|
||||
|
||||
```lua
|
||||
-- Some properties have been removed as they are beyond
|
||||
-- the scope of this chapter.
|
||||
minetest.register_node("default:water_source", {
|
||||
drawtype = "liquid",
|
||||
paramtype = "light",
|
||||
|
||||
inventory_image = minetest.inventorycube("default_water.png"),
|
||||
-- ^ this is required to stop the inventory image from being animated
|
||||
|
||||
tiles = {
|
||||
{
|
||||
name = "default_water_source_animated.png",
|
||||
animation = {
|
||||
type = "vertical_frames",
|
||||
aspect_w = 16,
|
||||
aspect_h = 16,
|
||||
length = 2.0
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
special_tiles = {
|
||||
-- New-style water source material (mostly unused)
|
||||
{
|
||||
name = "default_water_source_animated.png",
|
||||
animation = {type = "vertical_frames", aspect_w = 16,
|
||||
aspect_h = 16, length = 2.0},
|
||||
backface_culling = false,
|
||||
}
|
||||
},
|
||||
|
||||
--
|
||||
-- Behavior
|
||||
--
|
||||
walkable = false, -- The player falls through
|
||||
pointable = false, -- The player can't highlight it
|
||||
diggable = false, -- The player can't dig it
|
||||
buildable_to = true, -- Nodes can be replace this node
|
||||
|
||||
alpha = 160,
|
||||
|
||||
--
|
||||
-- Liquid Properties
|
||||
--
|
||||
drowning = 1,
|
||||
liquidtype = "source",
|
||||
|
||||
liquid_alternative_flowing = "default:water_flowing",
|
||||
-- ^ when the liquid is flowing
|
||||
|
||||
liquid_alternative_source = "default:water_source",
|
||||
-- ^ when the liquid is a source
|
||||
|
||||
liquid_viscosity = WATER_VISC,
|
||||
-- ^ how fast
|
||||
|
||||
liquid_range = 8,
|
||||
-- ^ how far
|
||||
|
||||
post_effect_color = {a=64, r=100, g=100, b=200},
|
||||
-- ^ colour of screen when the player is submerged
|
||||
})
|
||||
```
|
||||
|
||||
Flowing nodes have a similar definition, but with a different name and animation.
|
||||
See default:water_flowing in the default mod in minetest_game for a full example.
|
||||
|
||||
|
||||
## Node Boxes
|
||||
|
||||
<figure class="right_image">
|
||||
<img src="{{ page.root }}//static/drawtype_nodebox.gif" alt="Nodebox drawtype">
|
||||
<figcaption>
|
||||
Nodebox drawtype
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
Node boxes allow you to create a node which is not cubic, but is instead made out
|
||||
of as many cuboids as you like.
|
||||
|
||||
```lua
|
||||
minetest.register_node("stairs:stair_stone", {
|
||||
drawtype = "nodebox",
|
||||
paramtype = "light",
|
||||
node_box = {
|
||||
type = "fixed",
|
||||
fixed = {
|
||||
{-0.5, -0.5, -0.5, 0.5, 0, 0.5},
|
||||
{-0.5, 0, 0, 0.5, 0.5, 0.5},
|
||||
},
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
The most important part is the nodebox 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
|
||||
minetest.register_node("default:sign_wall", {
|
||||
drawtype = "nodebox",
|
||||
node_box = {
|
||||
type = "wallmounted",
|
||||
|
||||
-- Ceiling
|
||||
wall_top = {
|
||||
{-0.4375, 0.4375, -0.3125, 0.4375, 0.5, 0.3125}
|
||||
},
|
||||
|
||||
-- Floor
|
||||
wall_bottom = {
|
||||
{-0.4375, -0.5, -0.3125, 0.4375, -0.4375, 0.3125}
|
||||
},
|
||||
|
||||
-- Wall
|
||||
wall_side = {
|
||||
{-0.5, -0.3125, -0.4375, -0.4375, 0.3125, 0.4375}
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Mesh Nodes
|
||||
|
||||
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
|
||||
minetest.register_node("mymod:meshy", {
|
||||
drawtype = "mesh",
|
||||
|
||||
-- Holds the texture for each "material"
|
||||
tiles = {
|
||||
"mymod_meshy.png"
|
||||
},
|
||||
|
||||
-- Path to the mesh
|
||||
mesh = "mymod_meshy.b3d",
|
||||
})
|
||||
```
|
||||
|
||||
Make sure 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
|
||||
minetest.register_node("default:ladder_wood", {
|
||||
drawtype = "signlike",
|
||||
|
||||
tiles = {"default_ladder_wood.png"},
|
||||
|
||||
-- Required: store the rotation in param2
|
||||
paramtype2 = "wallmounted",
|
||||
|
||||
selection_box = {
|
||||
type = "wallmounted",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
## Plantlike Nodes
|
||||
|
||||
<figure class="right_image">
|
||||
<img src="{{ page.root }}//static/drawtype_plantlike.png" alt="Plantlike Drawtype">
|
||||
<figcaption>
|
||||
Plantlike Drawtype
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
Plantlike nodes draw their tiles in an X like pattern.
|
||||
|
||||
```lua
|
||||
minetest.register_node("default:papyrus", {
|
||||
drawtype = "plantlike",
|
||||
|
||||
-- Only one texture used
|
||||
tiles = {"default_papyrus.png"},
|
||||
|
||||
selection_box = {
|
||||
type = "fixed",
|
||||
fixed = {-6 / 16, -0.5, -6 / 16, 6 / 16, 0.5, 6 / 16},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Firelike Nodes
|
||||
|
||||
Firelike is similar to plantlike, except that it is designed to "cling" to walls
|
||||
and ceilings.
|
||||
|
||||
<figure>
|
||||
<img src="{{ page.root }}//static/drawtype_firelike.png" alt="Firelike nodes">
|
||||
<figcaption>
|
||||
Firelike nodes
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
```lua
|
||||
minetest.register_node("mymod:clingere", {
|
||||
drawtype = "firelike",
|
||||
|
||||
-- Only one texture used
|
||||
tiles = { "mymod:clinger" },
|
||||
})
|
||||
```
|
||||
|
||||
## More Drawtypes
|
||||
|
||||
This is not a comprehensive list, there's 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](../../lua_api.html#node-drawtypes)
|
||||
for the complete list.
|
|
@ -0,0 +1,380 @@
|
|||
---
|
||||
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 and Aliases](#item-names-and-aliases)
|
||||
* [Textures](#textures)
|
||||
* [Registering a Basic Node](#registering-a-basic-node)
|
||||
* [Actions and Callbacks](#actions-and-callbacks)
|
||||
* [on_use](#on_use)
|
||||
* [Crafting](#crafting)
|
||||
* [Fuel](#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 though it may not be possible through normal game play.
|
||||
|
||||
A node is an item which 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 has the ability to wear and typically has non-default digging capabilities.
|
||||
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 which affect the behaviour of the item.
|
||||
|
||||
```lua
|
||||
minetest.register_craftitem("modname:itemname", {
|
||||
description = "My Special Item",
|
||||
inventory_image = "modname_itemname.png"
|
||||
})
|
||||
```
|
||||
|
||||
### Item Names and Aliases
|
||||
|
||||
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.
|
||||
|
||||
Items can also have *aliases* pointing to their name.
|
||||
An *alias* is a pseudo-item name which 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.
|
||||
It's important to avoid aliasing to an unobtainable node if the remove node
|
||||
could be obtained.
|
||||
* 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
|
||||
minetest.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 = minetest.registered_aliases[itemname] or itemname
|
||||
```
|
||||
|
||||
### Textures
|
||||
|
||||
Textures should be placed in the textures/ folder with names in the format
|
||||
`modname_itemname.png`.\\
|
||||
JPEG textures are supported, but they do not support transparency and are generally
|
||||
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,
|
||||
resulting in decreased performance.
|
||||
|
||||
## Registering a basic node
|
||||
|
||||
```lua
|
||||
minetest.register_node("mymod:diamond", {
|
||||
description = "Alien Diamond",
|
||||
tiles = {"mymod_diamond.png"},
|
||||
is_ground_content = true,
|
||||
groups = {cracky=3, stone=1}
|
||||
})
|
||||
```
|
||||
|
||||
The `tiles` property is a table of texture names 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
|
||||
3D computer graphics.
|
||||
|
||||
```lua
|
||||
minetest.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.
|
||||
|
||||
## Actions and Callbacks
|
||||
|
||||
Minetest heavily uses a callback-based modding design.
|
||||
Callbacks can be placed in the item definition table to allow response to various
|
||||
different user events.
|
||||
|
||||
### on_use
|
||||
|
||||
By default, the use callback is triggered when a player left-clicks with an item.
|
||||
Having a use callback prevents the item being used to dig nodes.
|
||||
One common use of the use callback is for food:
|
||||
|
||||
```lua
|
||||
minetest.register_craftitem("mymod:mudpie", {
|
||||
description = "Alien Mud Pie",
|
||||
inventory_image = "myfood_mudpie.png",
|
||||
on_use = minetest.item_eat(20),
|
||||
})
|
||||
```
|
||||
|
||||
The number supplied to the minetest.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.
|
||||
Hitpoints don't have to be integers (whole numbers); they can be decimals.
|
||||
|
||||
minetest.item_eat() is a function which returns a function, setting it
|
||||
as the on_use callback.
|
||||
This means the code above is roughly similar to this:
|
||||
|
||||
```lua
|
||||
minetest.register_craftitem("mymod:mudpie", {
|
||||
description = "Alien Mud Pie",
|
||||
inventory_image = "myfood_mudpie.png",
|
||||
on_use = function(...)
|
||||
return minetest.do_item_eat(20, nil, ...)
|
||||
end,
|
||||
})
|
||||
```
|
||||
|
||||
By understanding how item_eat works by simply returning a function, it's
|
||||
possible to modify it to do more complex behaviour such as play a custom sound.
|
||||
|
||||
## 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
|
||||
minetest.register_craft({
|
||||
type = "shaped",
|
||||
output = "mymod:diamond_chair 99",
|
||||
recipe = {
|
||||
{"mymod:diamond_fragments", "", ""},
|
||||
{"mymod:diamond_fragments", "mymod:diamond_fragments", ""},
|
||||
{"mymod:diamond_fragments", "mymod:diamond_fragments", ""}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
One thing to note is the blank column on the right-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
|
||||
minetest.register_craft({
|
||||
output = "mymod:diamond_chair 99",
|
||||
recipe = {
|
||||
{"mymod:diamond_fragments", "" },
|
||||
{"mymod:diamond_fragments", "mymod:diamond_fragments"},
|
||||
{"mymod:diamond_fragments", "mymod:diamond_fragments"}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
The type field 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
|
||||
minetest.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
|
||||
minetest.register_craft({
|
||||
type = "cooking",
|
||||
output = "mymod:diamond_fragments",
|
||||
recipe = "default:coalblock",
|
||||
cooktime = 10,
|
||||
})
|
||||
```
|
||||
|
||||
The only real difference in the code is that the recipe is just 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 a 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
|
||||
minetest.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 group to be used.
|
||||
|
||||
```lua
|
||||
minetest.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;<br>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.<br>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
|
||||
minetest.register_tool("mymod:tool", {
|
||||
description = "My Tool",
|
||||
inventory_image = "mymod_tool.png",
|
||||
tool_capabilities = {
|
||||
full_punch_interval = 1.5,
|
||||
max_drop_level = 1,
|
||||
groupcaps = {
|
||||
crumbly = {
|
||||
maxlevel = 2,
|
||||
uses = 20,
|
||||
times = { [1]=1.60, [2]=1.20, [3]=0.80 }
|
||||
},
|
||||
},
|
||||
damage_groups = {fleshy=2},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Groupcaps is the list of supported dig types for digging nodes.
|
||||
Damage groups are for controlling how tools damage objects, which will be
|
||||
discussed later in the Objects, Players, and Entities chapter.
|
|
@ -0,0 +1,225 @@
|
|||
---
|
||||
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.
|
||||
|
||||
* [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, 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.
|
||||
|
||||
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 = minetest.get_node({ x = 1, y = 3, z = 4 })
|
||||
print(dump(node)) --> { name=.., param1=.., param2=.. }
|
||||
```
|
||||
|
||||
If the position is a decimal, it will be rounded to the containing node.
|
||||
The function will always return a table containing the node information:
|
||||
|
||||
* `name` - The 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 `minetest.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.
|
||||
|
||||
```lua
|
||||
local grow_speed = 1
|
||||
local node_pos = minetest.find_node_near(pos, 5, { "default:mese" })
|
||||
if node_pos then
|
||||
minetest.chat_send_all("Node found at: " .. dump(node_pos))
|
||||
grow_speed = 2
|
||||
end
|
||||
```
|
||||
|
||||
Let's say, for example, that the growth rate increases the more mese there is
|
||||
nearby. You should then use a function which can find multiple nodes in 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 =
|
||||
minetest.find_nodes_in_area(pos1, pos2, { "default:mese" })
|
||||
local grow_speed = 1 + #pos_list
|
||||
```
|
||||
|
||||
The above code doesn't quite do what we want, as it checks based on area, whereas
|
||||
`find_node_near` checks based on range. In order to fix this we will,
|
||||
unfortunately, 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 =
|
||||
minetest.find_nodes_in_area(pos1, pos2, { "default:mese" })
|
||||
local grow_speed = 1
|
||||
for i=1, #pos_list do
|
||||
local delta = vector.subtract(pos_list[i], pos)
|
||||
if delta.x*delta.x + delta.y*delta.y <= 5*5 then
|
||||
grow_speed = grow_speed + 1
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Now your 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 you should avoid them as much as possible.
|
||||
|
||||
There are more variations of the above two functions, such as
|
||||
`find_nodes_with_meta` and `find_nodes_in_area_under_air`, which work similarly
|
||||
and are useful in other circumstances.
|
||||
|
||||
## Writing
|
||||
|
||||
### Writing Nodes
|
||||
|
||||
You can use `set_node` to write to the map. Each call to set_node will cause
|
||||
lighting to be recalculated, which means that set_node is fairly slow for large
|
||||
numbers of nodes.
|
||||
|
||||
```lua
|
||||
minetest.set_node({ x = 1, y = 3, z = 4 }, { name = "default:mese" })
|
||||
|
||||
local node = minetest.get_node({ x = 1, y = 3, z = 4 })
|
||||
print(node.name) --> default:mese
|
||||
```
|
||||
|
||||
set_node will remove any associated metadata or inventory from that position.
|
||||
This 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 think conceptually of it as one node, it's actually
|
||||
two.
|
||||
|
||||
You can set a node without deleting metadata or the inventory like so:
|
||||
|
||||
```lua
|
||||
minetest.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
|
||||
minetest.remove_node(pos)
|
||||
minetest.set_node(pos, { name = "air" })
|
||||
```
|
||||
|
||||
In fact, remove_node will call set_node with name being air.
|
||||
|
||||
## Loading Blocks
|
||||
|
||||
You can use `minetest.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
|
||||
minetest.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
|
||||
minetest.chat_send_all("Finished loading blocks!")
|
||||
end
|
||||
local perc = 100 * context.loaded_blocks / context.total_blocks
|
||||
local msg = string.format("Loading blocks %d/%d (%.2f%%)",
|
||||
context.loaded_blocks, context.total_blocks, perc)
|
||||
minetest.chat_send_all(msg)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
This is not the only way of loading blocks; using a LVM 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)
|
||||
|
||||
minetest.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.
|
|
@ -0,0 +1,204 @@
|
|||
---
|
||||
title: Lua Voxel Manipulators
|
||||
layout: default
|
||||
root: ../..
|
||||
idx: 3.5
|
||||
description: Learn how to use LVMs to speed up map operations.
|
||||
redirect_from: /en/chapters/lvm.html
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
The functions outlined in the [Basic Map Operations](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 = minetest.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.
|
||||
|
||||
## 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 their name string, as string comparison
|
||||
is slow. 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 = minetest.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
|
||||
```
|
||||
|
||||
It is recommended that you find and store the content IDs of nodes types
|
||||
at load time, because the IDs of a node type will never change. Make sure to store
|
||||
the IDs in a local variable for performance reasons.
|
||||
|
||||
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 `minetest.fix_light`.
|
||||
|
||||
## Example
|
||||
|
||||
```lua
|
||||
-- Get content IDs during load time, and store into a local
|
||||
local c_dirt = minetest.get_content_id("default:dirt")
|
||||
local c_grass = minetest.get_content_id("default:dirt_with_grass")
|
||||
|
||||
local function grass_to_dirt(pos1, pos2)
|
||||
-- Read data into LVM
|
||||
local vm = minetest.get_voxel_manip()
|
||||
local emin, emax = vm:read_from_map(pos1, pos2)
|
||||
local a = VoxelArea:new{
|
||||
MinEdge = emin,
|
||||
MaxEdge = emax
|
||||
}
|
||||
local data = vm:get_data()
|
||||
|
||||
-- Modify data
|
||||
for z = pos1.z, pos2.z do
|
||||
for y = pos1.y, pos2.y do
|
||||
for x = pos1.x, pos2.x do
|
||||
local vi = a:index(x, y, z)
|
||||
if data[vi] == c_grass then
|
||||
data[vi] = c_dirt
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Write data
|
||||
vm:set_data(data)
|
||||
vm:write_to_map(true)
|
||||
end
|
||||
```
|
||||
|
||||
## 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?
|
|
@ -0,0 +1,240 @@
|
|||
---
|
||||
title: Objects, Players, and Entities
|
||||
layout: default
|
||||
root: ../..
|
||||
idx: 3.4
|
||||
description: Using an ObjectRef
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
In this chapter, you will learn how to manipulate objects and how to define your
|
||||
own.
|
||||
|
||||
* [What are Objects, Players, and Entities?](#objects_players_and_entities)
|
||||
* [Position and Velocity](#position_and_velocity)
|
||||
* [Object Properties](#object_properties)
|
||||
* [Entities](#entities)
|
||||
* [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.
|
||||
|
||||
Entities are sometimes known as Lua Entities.
|
||||
Don't be fooled though, all entities are Lua entities.
|
||||
|
||||
## Position and Velocity
|
||||
|
||||
`get_pos` and `set_pos` exist to allow you to get and set an entity's position.
|
||||
|
||||
```lua
|
||||
local object = minetest.get_player_by_name("bob")
|
||||
local pos = object:get_pos()
|
||||
object:set_pos({ x = pos.x, y = pos.y + 1, z = pos.z })
|
||||
```
|
||||
|
||||
`set_pos` immediately sets the position, with no animation. If you'd like to
|
||||
smoothly animate an object to the new position, you should use `move_to`.
|
||||
This, unfortunately, only works for entities.
|
||||
|
||||
```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
|
||||
|
||||
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 type table much like an item does.
|
||||
This table can contain callback methods, default object properties, and custom elements.
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
When an entity has emerged, a table is created for it by copying everything from
|
||||
its type table.
|
||||
This table can be used to store variables for that particular entity.
|
||||
|
||||
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 " .. minetest.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.txt]({{ page.root }}/lua_api.html#registered-entities).
|
||||
|
||||
```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 minetest.get_node(pos_down).name == "air" then
|
||||
delta = vector.new(0, -1, 0)
|
||||
elseif minetest.get_node(pos).name == "air" then
|
||||
delta = vector.new(0, 0, 1)
|
||||
else
|
||||
delta = vector.new(0, 1, 0)
|
||||
end
|
||||
|
||||
delta = vector.multiply(delta, dtime)
|
||||
|
||||
self.object:move_to(vector.add(pos, delta))
|
||||
end
|
||||
|
||||
function MyEntity:on_punch(hitter)
|
||||
minetest.chat_send_player(hitter:get_player_name(), self.message)
|
||||
end
|
||||
```
|
||||
|
||||
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 minetest.write_json({
|
||||
message = self.message,
|
||||
})
|
||||
end
|
||||
|
||||
function MyEntity:on_activate(staticdata, dtime_s)
|
||||
if staticdata ~= "" and staticdata ~= nil then
|
||||
local data = minetest.parse_json(staticdata) or {}
|
||||
self:set_message(data.message)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Minetest 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
|
||||
minetest.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 = minetest.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
|
||||
|
||||
## 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.
|
||||
|
||||
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.
|
||||
|
||||
## Your Turn
|
||||
|
||||
* Make a windmill by combining nodes and an entity.
|
||||
* Make a mob of your choice (without using any other mods).
|
|
@ -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)
|
||||
* [Private Metadata](#private-metadata)
|
||||
* [Storing Tables](#storing-tables)
|
||||
* [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 = minetest.get_meta({ x = 1, y = 2, z = 3 })
|
||||
```
|
||||
|
||||
Player and ItemStack metadata are obtained using `get_meta()`:
|
||||
|
||||
```lua
|
||||
local pmeta = player:get_meta()
|
||||
local imeta = stack:get_meta()
|
||||
```
|
||||
|
||||
### Reading and Writing
|
||||
|
||||
In most cases, `get_<type>()` and `set_<type>()` methods will be used to read
|
||||
and write to meta.
|
||||
Metadata stores strings, so the string methods will directly set and get the value.
|
||||
|
||||
```lua
|
||||
print(meta:get_string("foo")) --> ""
|
||||
meta:set_string("foo", "bar")
|
||||
print(meta:get_string("foo")) --> "bar"
|
||||
```
|
||||
|
||||
All of the typed getters will return a neutral default value if the key doesn't
|
||||
exist, such as `""` or `0`.
|
||||
You can use `get()` to return a string or nil.
|
||||
|
||||
As Metadata is a reference, any changes will be updated to the source automatically.
|
||||
ItemStacks aren't references however, so you'll need to update the itemstack in the
|
||||
inventory.
|
||||
|
||||
The non-typed getters and setters will convert to and from strings:
|
||||
|
||||
```lua
|
||||
print(meta:get_int("count")) --> 0
|
||||
meta:set_int("count", 3)
|
||||
print(meta:get_int("count")) --> 3
|
||||
print(meta:get_string("count")) --> "3"
|
||||
```
|
||||
|
||||
### Special Keys
|
||||
|
||||
`infotext` is used in Node Metadata to show a tooltip when hovering the crosshair over a node.
|
||||
This is useful when showing the ownership or status of a node.
|
||||
|
||||
`description` is used in ItemStack Metadata to override the description when
|
||||
hovering over the stack in an inventory.
|
||||
You can use colours by encoding them with `minetest.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 when you need to exchange information
|
||||
with another program.
|
||||
|
||||
```lua
|
||||
local data = { username = "player1", score = 1234 }
|
||||
meta:set_string("foo", minetest.serialize(data))
|
||||
|
||||
data = minetest.deserialize(minetest:get_string("foo"))
|
||||
```
|
||||
|
||||
### Private Metadata
|
||||
|
||||
Entries in Node Metadata can be marked as private, and not sent to the client.
|
||||
Entries not marked as private will be sent to the client.
|
||||
|
||||
```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 = minetest.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(minetest.get_modpath("mymod") .. "/backend_sqlite.lua")
|
||||
else
|
||||
backend =
|
||||
dofile(minetest.get_modpath("mymod") .. "/backend_storage.lua")
|
||||
end
|
||||
|
||||
backend.get_foo("a")
|
||||
backend.set_foo("a", { score = 3 })
|
||||
```
|
||||
|
||||
The backend_storage.lua file should include a mod storage implementation:
|
||||
|
||||
```lua
|
||||
local storage = minetest.get_mod_storage()
|
||||
local backend = {}
|
||||
|
||||
function backend.set_foo(key, value)
|
||||
storage:set_string(key, minetest.serialize(value))
|
||||
end
|
||||
|
||||
function backend.get_foo(key, value)
|
||||
return minetest.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 = minetest.request_insecure_environment()
|
||||
assert(ie, "Please add mymod to secure.trusted_mods in the settings")
|
||||
|
||||
local _sql = ie.require("lsqlite3")
|
||||
-- Prevent other mods from using the global sqlite3 library
|
||||
if sqlite3 then
|
||||
sqlite3 = nil
|
||||
end
|
||||
```
|
||||
|
||||
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 `minetest.set_node`.)
|
|
@ -0,0 +1,120 @@
|
|||
---
|
||||
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 = minetest.get_node_timer(pos)
|
||||
timer:start(10.5) -- in seconds
|
||||
```
|
||||
|
||||
You can also check the status or stop the timer:
|
||||
|
||||
```lua
|
||||
if timer:is_started() then
|
||||
print("The timer is running, and has " .. timer:get_timeout() .. "s remaining!")
|
||||
print(timer:get_elapsed() .. "s has elapsed.")
|
||||
end
|
||||
|
||||
timer:stop()
|
||||
```
|
||||
|
||||
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
|
||||
minetest.register_node("autodoors:door_open", {
|
||||
on_timer = function(pos)
|
||||
minetest.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.
|
||||
|
||||
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
|
||||
minetest.register_node("aliens:grass", {
|
||||
description = "Alien Grass",
|
||||
light_source = 3, -- The node radiates light. Min 0, max 14
|
||||
tiles = {"aliens_grass.png"},
|
||||
groups = {choppy=1},
|
||||
on_use = minetest.item_eat(20)
|
||||
})
|
||||
|
||||
minetest.register_abm({
|
||||
nodenames = {"default:dirt_with_grass"},
|
||||
neighbors = {"default:water_source", "default:water_flowing"},
|
||||
interval = 10.0, -- Run every 10 seconds
|
||||
chance = 50, -- Select every 1 in 50 nodes
|
||||
action = function(pos, node, active_object_count,
|
||||
active_object_count_wider)
|
||||
local pos = {x = pos.x, y = pos.y + 1, z = pos.z}
|
||||
minetest.set_node(pos, {name = "aliens:grass"})
|
||||
end
|
||||
})
|
||||
```
|
||||
|
||||
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 minetest.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.
|
|
@ -0,0 +1,166 @@
|
|||
---
|
||||
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: <p>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.</p>
|
||||
|
||||
<p>So make sure that you don't assume that the player is online.
|
||||
You can check by seeing if <pre>minetest.get_player_by_name</pre> returns a player.</p>
|
||||
|
||||
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 to All Players](#sending-messages-to-all-players)
|
||||
* [Sending Messages to Specific Players](#sending-messages-to-specific-players)
|
||||
* [Chat Commands](#chat-commands)
|
||||
* [Complex Subcommands](#complex-subcommands)
|
||||
* [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
|
||||
minetest.chat_send_all("This is a chat message to all players")
|
||||
```
|
||||
|
||||
Here is an example of how this appears in-game:
|
||||
|
||||
<player1> Look at this entrance
|
||||
This is a chat message to all players
|
||||
<player2> What about it?
|
||||
|
||||
The message appears on a separate line to distinguish it from in-game player chat.
|
||||
|
||||
## Sending Messages to Specific Players
|
||||
|
||||
To send a message to a specific player, call the chat_send_player function:
|
||||
|
||||
```lua
|
||||
minetest.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
|
||||
minetest.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.
|
||||
|
||||
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 %}
|
||||
|
||||
## Complex Subcommands
|
||||
|
||||
It is often required to make complex chat commands, such as:
|
||||
|
||||
* `/msg <to> <message>`
|
||||
* `/team join <teamname>`
|
||||
* `/team leave <teamname>`
|
||||
* `/team list`
|
||||
|
||||
This is usually done using [Lua patterns](https://www.lua.org/pil/20.2.html).
|
||||
Patterns are a way of extracting stuff from text using rules.
|
||||
|
||||
```lua
|
||||
local to, msg = string.match(param, "^([%a%d_-]+) (*+)$")
|
||||
```
|
||||
|
||||
The above code implements `/msg <to> <message>`. Let's go through left to right:
|
||||
|
||||
* `^` means match the start of the string.
|
||||
* `()` is a matching group - anything that matches stuff in here will be
|
||||
returned from string.match.
|
||||
* `[]` means accept characters in this list.
|
||||
* `%a` means accept any letter and `%d` means accept any digit.
|
||||
* `[%d%a_-]` 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).
|
||||
|
||||
<p class="book_hide">
|
||||
There is also a library written by the author of this book which can be used
|
||||
to make complex chat commands without patterns called
|
||||
<a href="chat_complex.html">Chat Command Builder</a>.
|
||||
</p>
|
||||
|
||||
|
||||
## Intercepting Messages
|
||||
|
||||
To intercept a message, use register_on_chat_message:
|
||||
|
||||
```lua
|
||||
minetest.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
|
||||
minetest.register_on_chat_message(function(name, message)
|
||||
if message:sub(1, 1) == "/" then
|
||||
print(name .. " ran chat command")
|
||||
elseif minetest.check_player_privs(name, { shout = true }) then
|
||||
print(name .. " said " .. message)
|
||||
else
|
||||
print(name .. " tried to say " .. message ..
|
||||
" but doesn't have shout")
|
||||
end
|
||||
|
||||
return false
|
||||
end)
|
||||
```
|
|
@ -0,0 +1,183 @@
|
|||
---
|
||||
title: Chat Command Builder
|
||||
layout: default
|
||||
root: ../..
|
||||
idx: 4.3
|
||||
description: Use ChatCmdBuilder to make a complex chat command
|
||||
redirect_from: /en/chapters/chat_complex.html
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
This chapter will show you how to make complex chat commands with ChatCmdBuilder,
|
||||
such as `/msg <name> <message>`, `/team join <teamname>` or `/team leave <teamname>`.
|
||||
|
||||
Note that ChatCmdBuilder is a library created by the author of this book, and most
|
||||
modders tend to use the method outlined in the
|
||||
[chat commnds](chat.html#complex-subcommands) chapter.
|
||||
|
||||
* Why ChatCmdBuilder?
|
||||
* Routes.
|
||||
* Subcommand functions.
|
||||
* Installing ChatCmdBuilder.
|
||||
* Admin complex command.
|
||||
|
||||
## Why ChatCmdBuilder?
|
||||
|
||||
Traditionally mods implemented these complex commands using Lua patterns.
|
||||
|
||||
```lua
|
||||
local name = string.match(param, "^join ([%a%d_-]+)")
|
||||
```
|
||||
|
||||
I however find Lua patterns annoying to write and unreadable.
|
||||
Because of this, I created a library to do this for you.
|
||||
|
||||
```lua
|
||||
ChatCmdBuilder.new("sethp", function(cmd)
|
||||
cmd:sub(":target :hp:int", function(name, target, hp)
|
||||
local player = minetest.get_player_by_name(target)
|
||||
if player then
|
||||
player:set_hp(hp)
|
||||
return true, "Killed " .. target
|
||||
else
|
||||
return false, "Unable to find " .. target
|
||||
end
|
||||
end)
|
||||
end, {
|
||||
description = "Set hp of player",
|
||||
privs = {
|
||||
kick = true
|
||||
-- ^ probably better to register a custom priv
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
`ChatCmdBuilder.new(name, setup_func, def)` creates a new chat command called
|
||||
`name`. It then calls the function passed to it (`setup_func`), which then creates
|
||||
sub commands. Each `cmd:sub(route, func)` is a sub command.
|
||||
|
||||
A sub command is a particular response to an input param. When a player runs
|
||||
the chat command, the first sub command that matches their input will be run,
|
||||
and no others. If no subcommands match, then the user will be told of the invalid
|
||||
syntax. For example, in the above code snippet if a player
|
||||
types something of the form `/sethp username 12` then the function passed
|
||||
to cmd:sub will be called. If they type `/sethp 12 bleh`, then a wrong
|
||||
input message will appear.
|
||||
|
||||
`:name :hp:int` is a route. It describes the format of the param passed to /teleport.
|
||||
|
||||
## Routes
|
||||
|
||||
A route is made up of terminals and variables. Terminals must always be there.
|
||||
For example, `join` in `/team join :username :teamname`. The spaces also count
|
||||
as terminals.
|
||||
|
||||
Variables can change value depending on what the user types. For example, `:username`
|
||||
and `:teamname`.
|
||||
|
||||
Variables are defined as `:name:type`. The `name` is used in the help documentation.
|
||||
The `type` is used to match the input. If the type is not given, then the type is
|
||||
`word`.
|
||||
|
||||
Valid types are:
|
||||
|
||||
* `word` - default. Any string without spaces.
|
||||
* `int` - Any integer/whole number, no decimals.
|
||||
* `number` - Any number, including ints and decimals.
|
||||
* `pos` - 1,2,3 or 1.1,2,3.4567 or (1,2,3) or 1.2, 2 ,3.2
|
||||
* `text` - Any string. There can only ever be one text variable,
|
||||
no variables or terminals can come afterwards.
|
||||
|
||||
In `:name :hp:int`, there are two variables there:
|
||||
|
||||
* `name` - type of `word` as no type is specified. Accepts any string without spaces.
|
||||
* `hp` - type of `int`
|
||||
|
||||
## Subcommand functions
|
||||
|
||||
The first argument is the caller's name. The variables are then passed to the
|
||||
function in order.
|
||||
|
||||
```lua
|
||||
cmd:sub(":target :hp:int", function(name, target, hp)
|
||||
-- subcommand function
|
||||
end)
|
||||
```
|
||||
|
||||
## Installing ChatCmdBuilder
|
||||
|
||||
The source code can be found and downloaded on
|
||||
[Github](https://github.com/rubenwardy/ChatCmdBuilder/).
|
||||
|
||||
There are two ways to install:
|
||||
|
||||
1. Install ChatCmdBuilder as a mod and depend on it.
|
||||
2. Include the init.lua file in ChatCmdBuilder as chatcmdbuilder.lua in your mod,
|
||||
and dofile it.
|
||||
|
||||
## Admin complex command
|
||||
|
||||
Here is an example that creates a chat command that allows us to do this:
|
||||
|
||||
* `/admin kill <username>` - kill user
|
||||
* `/admin move <username> to <pos>` - teleport user
|
||||
* `/admin log <username>` - show report log
|
||||
* `/admin log <username> <message>` - log to report log
|
||||
|
||||
```lua
|
||||
local admin_log
|
||||
local function load()
|
||||
admin_log = {}
|
||||
end
|
||||
local function save()
|
||||
-- todo
|
||||
end
|
||||
load()
|
||||
|
||||
ChatCmdBuilder.new("admin", function(cmd)
|
||||
cmd:sub("kill :name", function(name, target)
|
||||
local player = minetest.get_player_by_name(target)
|
||||
if player then
|
||||
player:set_hp(0)
|
||||
return true, "Killed " .. target
|
||||
else
|
||||
return false, "Unable to find " .. target
|
||||
end
|
||||
end)
|
||||
|
||||
cmd:sub("move :name to :pos:pos", function(name, target, pos)
|
||||
local player = minetest.get_player_by_name(target)
|
||||
if player then
|
||||
player:setpos(pos)
|
||||
return true, "Moved " .. target .. " to " ..
|
||||
minetest.pos_to_string(pos)
|
||||
else
|
||||
return false, "Unable to find " .. target
|
||||
end
|
||||
end)
|
||||
|
||||
cmd:sub("log :username", function(name, target)
|
||||
local log = admin_log[target]
|
||||
if log then
|
||||
return true, table.concat(log, "\n")
|
||||
else
|
||||
return false, "No entries for " .. target
|
||||
end
|
||||
end)
|
||||
|
||||
cmd:sub("log :username :message", function(name, target, message)
|
||||
local log = admin_log[target] or {}
|
||||
table.insert(log, message)
|
||||
admin_log[target] = log
|
||||
save()
|
||||
return true, "Logged"
|
||||
end)
|
||||
end, {
|
||||
description = "Admin tools",
|
||||
privs = {
|
||||
kick = true,
|
||||
ban = true
|
||||
}
|
||||
})
|
||||
```
|
|
@ -0,0 +1,302 @@
|
|||
---
|
||||
title: Formspecs
|
||||
layout: default
|
||||
root: ../..
|
||||
idx: 4.5
|
||||
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
|
||||
|
||||
<figure class="right_image">
|
||||
<img src="{{ page.root }}//static/formspec_example.png" alt="Furnace Inventory">
|
||||
<figcaption>
|
||||
Screenshot of furnace formspec, labelled.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
In this chapter we will learn how to create a formspec and display it to the user.
|
||||
A formspec is the specification code for a form.
|
||||
In Minetest, forms are windows such as the player inventory, which can contain labels,
|
||||
buttons and fields to allow you to enter information.
|
||||
|
||||
* [Formspec Syntax](#formspec-syntax)
|
||||
* [Displaying Forms](#displaying-forms)
|
||||
* [Callbacks](#callbacks)
|
||||
* [Contexts](#contexts)
|
||||
* [Node Meta Formspecs](#node-meta-formspecs)
|
||||
|
||||
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) elements instead of forms, because unexpected windows tend to disrupt gameplay.
|
||||
|
||||
## Formspec Syntax
|
||||
|
||||
Formspecs have an unusual syntax.
|
||||
They consist of a series of tags which are in the following form:
|
||||
|
||||
element_type[param1;param2;...]
|
||||
|
||||
Firstly the element type is declared, and then the attributes are given
|
||||
in square brackets.
|
||||
|
||||
Elements are items such as text boxes or buttons, or can be metadata such
|
||||
as size or background.
|
||||
|
||||
Here are two elements, of types foo and bar.
|
||||
|
||||
foo[param1]bar[param1]
|
||||
|
||||
### Size[w, h]
|
||||
|
||||
Nearly all forms have a size tag. This declares the size of the form window. Note that
|
||||
**forms don't use pixels as co-ordinates; they use a grid based on inventories**.
|
||||
A size of (1, 1) means the form is big enough to host a 1x1 inventory.
|
||||
This means the size of the form is independent of screen resolution and it should work
|
||||
just as well on large screens as small screens.
|
||||
You can use decimals in sizes and co-ordinates.
|
||||
|
||||
size[5,2]
|
||||
|
||||
Co-ordinates and sizes only use one attribute.
|
||||
The x and y values are separated by a comma, as you can see above.
|
||||
|
||||
### Field[x, y; w, h; name; label; default]
|
||||
|
||||
This is a textbox element. Most other elements have a similar style of attributes.
|
||||
The name attribute is used in callbacks to get the submitted information.
|
||||
The x and y attributes determine the position of the element, and
|
||||
the w and h attributes provide the size.
|
||||
|
||||
field[1,1;3,1;firstname;Firstname;]
|
||||
|
||||
It is perfectly valid to not define an attribute.
|
||||
|
||||
### Other Elements
|
||||
|
||||
You should refer to [lua_api.txt](https://github.com/minetest/minetest/blob/master/doc/lua_api.txt#L1019)
|
||||
for a list of all possible elements. Search for "Formspec" to locate the correct part of the document.
|
||||
At the time of writing, formspec information begins on line 1765.
|
||||
|
||||
## Displaying Formspecs
|
||||
|
||||
Here is a generalised way to show a formspec:
|
||||
|
||||
minetest.show_formspec(playername, formname, formspec)
|
||||
|
||||
Formnames should be itemnames; however, this is not enforced.
|
||||
There is no need to override a formspec, because formspecs are not registered like
|
||||
nodes and items are. The formspec code is sent to the player's client for them
|
||||
to see, along with the formname.
|
||||
Formnames are used in callbacks to identify which form has been submitted,
|
||||
and to see if the callback is relevant.
|
||||
|
||||
### Example
|
||||
|
||||
This example shows a formspec to a player when they use the /formspec command.
|
||||
|
||||
<figure class="right_image">
|
||||
<img src="{{ page.root }}//static/formspec_name.png" alt="Name Formspec">
|
||||
<figcaption>
|
||||
The formspec generated by<br />
|
||||
the example's code
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
```lua
|
||||
-- Show form when the /formspec command is used.
|
||||
minetest.register_chatcommand("formspec", {
|
||||
func = function(name, param)
|
||||
minetest.show_formspec(name, "mymod:form",
|
||||
"size[4,3]" ..
|
||||
"label[0,0;Hello, " .. name .. "]" ..
|
||||
"field[1,1.5;3,1;name;Name;]" ..
|
||||
"button_exit[1,2;2,1;exit;Save]")
|
||||
end
|
||||
})
|
||||
```
|
||||
|
||||
Note: the .. is used to join two strings together. The following two lines are equivalent:
|
||||
|
||||
```lua
|
||||
"foobar"
|
||||
"foo" .. "bar"
|
||||
```
|
||||
|
||||
## Callbacks
|
||||
|
||||
It's possible to expand the previous example with a callback:
|
||||
|
||||
```lua
|
||||
-- Show form when the /formspec command is used.
|
||||
minetest.register_chatcommand("formspec", {
|
||||
func = function(name, param)
|
||||
minetest.show_formspec(name, "mymod:form",
|
||||
"size[4,3]" ..
|
||||
"label[0,0;Hello, " .. name .. "]" ..
|
||||
"field[1,1.5;3,1;name;Name;]" ..
|
||||
"button_exit[1,2;2,1;exit;Save]")
|
||||
end
|
||||
})
|
||||
|
||||
-- Register callback
|
||||
minetest.register_on_player_receive_fields(function(player,
|
||||
formname, fields)
|
||||
if formname ~= "mymod:form" then
|
||||
-- Formname is not mymod:form,
|
||||
-- exit callback.
|
||||
return false
|
||||
end
|
||||
|
||||
-- Send message to player.
|
||||
minetest.chat_send_player(player:get_player_name(),
|
||||
"You said: " .. fields.name .. "!")
|
||||
|
||||
-- Return true to stop other callbacks from
|
||||
-- receiving this submission.
|
||||
return true
|
||||
end)
|
||||
```
|
||||
|
||||
The function given in minetest.register_on_player_receive_fields is called
|
||||
every time a user submits a form. Most callbacks will 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 all forms - it depends on what you
|
||||
want to do.
|
||||
|
||||
{% include notice.html notice=page.submit_vuln %}
|
||||
|
||||
### Fields
|
||||
|
||||
The `fields` parameter to the function is a table, index by string, of the values
|
||||
submitted by the user. You can access values in the table via fields.name,
|
||||
where 'name' is the name of the element.
|
||||
|
||||
As well as retrieving the values of each element, you can also get which button
|
||||
was clicked. In this case, the button called 'exit' was clicked, so fields.exit
|
||||
will be true.
|
||||
|
||||
Some elements can submit the form without the user clicking a button,
|
||||
such as a check box. You can detect these cases by looking
|
||||
for a clicked button.
|
||||
|
||||
```lua
|
||||
-- An example of what fields could contain,
|
||||
-- using the above code
|
||||
{
|
||||
name = "Foo Bar",
|
||||
exit = true
|
||||
}
|
||||
```
|
||||
|
||||
## Contexts
|
||||
|
||||
In many cases you want minetest.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.
|
||||
|
||||
For example, you might make a form to handle land protection information:
|
||||
|
||||
```lua
|
||||
--
|
||||
-- Step 1) set context when player requests the formspec
|
||||
--
|
||||
|
||||
-- land_formspec_context[playername] gives the player's context.
|
||||
local land_formspec_context = {}
|
||||
|
||||
minetest.register_chatcommand("land", {
|
||||
func = function(name, param)
|
||||
if param == "" then
|
||||
minetest.chat_send_player(name,
|
||||
"Incorrect parameters - supply a land ID")
|
||||
return
|
||||
end
|
||||
|
||||
-- Save information
|
||||
land_formspec_context[name] = {id = param}
|
||||
|
||||
minetest.show_formspec(name, "mylandowner:edit",
|
||||
"size[4,4]" ..
|
||||
"field[1,1;3,1;plot;Plot Name;]" ..
|
||||
"field[1,2;3,1;owner;Owner;]" ..
|
||||
"button_exit[1,3;2,1;exit;Save]")
|
||||
end
|
||||
})
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Step 2) retrieve context when player submits the form
|
||||
--
|
||||
minetest.register_on_player_receive_fields(function(player,
|
||||
formname, fields)
|
||||
if formname ~= "mylandowner:edit" then
|
||||
return false
|
||||
end
|
||||
|
||||
-- Load information
|
||||
local context = land_formspec_context[player:get_player_name()]
|
||||
|
||||
if context then
|
||||
minetest.chat_send_player(player:get_player_name(), "Id " ..
|
||||
context.id .. " is now called " .. fields.plot ..
|
||||
" and owned by " .. fields.owner)
|
||||
|
||||
-- Delete context if it is no longer going to be used
|
||||
land_formspec_context[player:get_player_name()] = nil
|
||||
|
||||
return true
|
||||
else
|
||||
-- Fail gracefully if the context does not exist.
|
||||
minetest.chat_send_player(player:get_player_name(),
|
||||
"Something went wrong, try again.")
|
||||
end
|
||||
end)
|
||||
```
|
||||
|
||||
## Node Meta Formspecs
|
||||
|
||||
minetest.show_formspec is not the only way to show a formspec; you can also
|
||||
add formspecs to a [node's meta data](node_metadata.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
|
||||
minetest.register_node("mymod:rightclick", {
|
||||
description = "Rightclick me!",
|
||||
tiles = {"mymod_rightclick.png"},
|
||||
groups = {cracky = 1},
|
||||
after_place_node = function(pos, placer)
|
||||
-- This function is run when the chest node is placed.
|
||||
-- The following code sets the formspec for chest.
|
||||
-- Meta is a way of storing data onto a node.
|
||||
|
||||
local meta = minetest.get_meta(pos)
|
||||
meta:set_string("formspec",
|
||||
"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 `minetest.show_formspec`;
|
||||
however, this kind of form can only be shown by right-clicking on a
|
||||
node. It cannot be triggered programmatically.
|
|
@ -0,0 +1,293 @@
|
|||
---
|
||||
title: HUD
|
||||
layout: default
|
||||
root: ../..
|
||||
idx: 4.6
|
||||
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
|
||||
|
||||
<figure class="right_image">
|
||||
<img
|
||||
width="300"
|
||||
src="{{ page.root }}//static/hud_diagram_center.svg"
|
||||
alt="Diagram showing a centered text element">
|
||||
</figure>
|
||||
|
||||
Screens come in a variety of different physical sizes and resolutions, and
|
||||
the HUD needs to work well on all screen types.
|
||||
|
||||
Minetest's solution to this is to specify the location of an element using both
|
||||
a percentage position and an offset.
|
||||
The percentage position is relative to the screen size, so to place an element
|
||||
in the centre of the screen, you would need to provide a percentage position of half
|
||||
the screen, e.g. (50%, 50%), and an offset of (0, 0).
|
||||
|
||||
The offset is then used to move an element relative to the percentage position.
|
||||
|
||||
<div style="clear:both;"></div>
|
||||
|
||||
### Alignment
|
||||
|
||||
Alignment is where the result of position and offset is on the element -
|
||||
for example, `{x = -1.0, y = 0.0}` will make the result of position and offset point
|
||||
to the left of the element's bounds. This is particularly useful when you want to
|
||||
make a text element aligned to the left, centre, or right.
|
||||
|
||||
<figure>
|
||||
<img
|
||||
width="500"
|
||||
src="{{ page.root }}//static/hud_diagram_alignment.svg"
|
||||
alt="Diagram showing alignment">
|
||||
</figure>
|
||||
|
||||
The above diagram shows 3 windows (blue), each with a single HUD element (yellow)
|
||||
and a different alignment each time. The arrow is the result of the position
|
||||
and offset calculation.
|
||||
|
||||
### Scoreboard
|
||||
|
||||
For the purposes of this chapter, you will learn how to position and update a
|
||||
score panel like so:
|
||||
|
||||
<figure>
|
||||
<img
|
||||
src="{{ page.root }}//static/hud_final.png"
|
||||
alt="screenshot of the HUD we're aiming for">
|
||||
</figure>
|
||||
|
||||
In the above screenshot, all the elements have the same percentage position
|
||||
(100%, 50%) - but different offsets. This allows the whole thing to be anchored
|
||||
to the right of the window, but to resize without breaking.
|
||||
|
||||
## Text Elements
|
||||
|
||||
You can create a HUD element once you have a copy of the player object:
|
||||
|
||||
```lua
|
||||
local player = minetest.get_player_by_name("username")
|
||||
local idx = player:hud_add({
|
||||
hud_elem_type = "text",
|
||||
position = {x = 0.5, y = 0.5},
|
||||
offset = {x = 0, y = 0},
|
||||
text = "Hello world!",
|
||||
alignment = {x = 0, y = 0}, -- center aligned
|
||||
scale = {x = 100, y = 100}, -- covered later
|
||||
})
|
||||
```
|
||||
|
||||
The `hud_add` function returns an element ID - this can be used later to modify
|
||||
or remove a HUD element.
|
||||
|
||||
### Parameters
|
||||
|
||||
The element's type is given using the `hud_elem_type` property in the definition
|
||||
table. The meaning of other properties varies based on this type.
|
||||
|
||||
`scale` is the maximum bounds of text; text outside these bounds is cropped, e.g.: `{x=100, y=100}`.
|
||||
|
||||
`number` is the text's colour, and is in [hexadecimal form](http://www.colorpicker.com/), e.g.: `0xFF0000`.
|
||||
|
||||
|
||||
### Our Example
|
||||
|
||||
Let's go ahead and place all the text in our score panel:
|
||||
|
||||
```lua
|
||||
-- Get the dig and place count from storage, or default to 0
|
||||
local meta = player:get_meta()
|
||||
local digs_text = "Digs: " .. meta:get_int("score:digs")
|
||||
local places_text = "Places: " .. meta:get_int("score:places")
|
||||
|
||||
player:hud_add({
|
||||
hud_elem_type = "text",
|
||||
position = {x = 1, y = 0.5},
|
||||
offset = {x = -120, y = -25},
|
||||
text = "Stats",
|
||||
alignment = 0,
|
||||
scale = { x = 100, y = 30},
|
||||
number = 0xFFFFFF,
|
||||
})
|
||||
|
||||
player:hud_add({
|
||||
hud_elem_type = "text",
|
||||
position = {x = 1, y = 0.5},
|
||||
offset = {x = -180, y = 0},
|
||||
text = digs_text,
|
||||
alignment = -1,
|
||||
scale = { x = 50, y = 10},
|
||||
number = 0xFFFFFF,
|
||||
})
|
||||
|
||||
player:hud_add({
|
||||
hud_elem_type = "text",
|
||||
position = {x = 1, y = 0.5},
|
||||
offset = {x = -70, y = 0},
|
||||
text = places_text,
|
||||
alignment = -1,
|
||||
scale = { x = 50, y = 10},
|
||||
number = 0xFFFFFF,
|
||||
})
|
||||
```
|
||||
|
||||
This results in the following:
|
||||
|
||||
<figure>
|
||||
<img
|
||||
src="{{ page.root }}//static/hud_text.png"
|
||||
alt="screenshot of the HUD we're aiming for">
|
||||
</figure>
|
||||
|
||||
|
||||
## Image Elements
|
||||
|
||||
Image elements are created in a very similar way to text elements:
|
||||
|
||||
```lua
|
||||
player:hud_add({
|
||||
hud_elem_type = "image",
|
||||
position = {x = 1, y = 0.5},
|
||||
offset = {x = -220, y = 0},
|
||||
text = "score_background.png",
|
||||
scale = { x = 1, y = 1},
|
||||
alignment = { x = 1, y = 0 },
|
||||
})
|
||||
```
|
||||
|
||||
You will now have this:
|
||||
|
||||
<figure>
|
||||
<img
|
||||
src="{{ page.root }}//static/hud_background_img.png"
|
||||
alt="screenshot of the HUD so far">
|
||||
</figure>
|
||||
|
||||
### Parameters
|
||||
|
||||
The `text` field is used to provide the image name.
|
||||
|
||||
If a co-ordinate is positive, then it is a scale factor with 1 being the
|
||||
original image size, 2 being double the size, and so on.
|
||||
However, if a co-ordinate is negative, it is a percentage of the screensize.
|
||||
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 "Test".
|
||||
|
||||
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
|
||||
|
||||
minetest.register_on_joinplayer(score.update_hud)
|
||||
|
||||
minetest.register_on_leaveplayer(function(player)
|
||||
saved_huds[player:get_player_name()] = nil
|
||||
end)
|
||||
```
|
||||
|
||||
|
||||
## Other Elements
|
||||
|
||||
Read [lua_api.txt]({{ page.root }}/lua_api.html#hud-element-types) for a complete list of HUD elements.
|
|
@ -0,0 +1,75 @@
|
|||
---
|
||||
title: Player Physics
|
||||
layout: default
|
||||
root: ../..
|
||||
idx: 4.4
|
||||
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)
|
||||
* [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
|
||||
minetest.register_chatcommand("antigravity", {
|
||||
func = function(name, param)
|
||||
local player = minetest.get_player_by_name(name)
|
||||
player:set_physics_override({
|
||||
gravity = 0.1, -- set gravity to 10% of its original value
|
||||
-- (0.1 * 9.81)
|
||||
})
|
||||
end,
|
||||
})
|
||||
```
|
||||
|
||||
## Available Overrides
|
||||
|
||||
`player:set_physics_override()` is given a table of overrides.\\
|
||||
According to [lua_api.txt]({{ page.root }}/lua_api.html#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 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.
|
|
@ -0,0 +1,139 @@
|
|||
---
|
||||
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-basic-privs)
|
||||
|
||||
## 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
|
||||
minetest.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 = minetest.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 = minetest.check_player_privs(name, {
|
||||
interact = true,
|
||||
vote = true })
|
||||
|
||||
if has then
|
||||
print("Player has all privs!")
|
||||
else
|
||||
print("Player is missing privs: " .. dump(missing))
|
||||
end
|
||||
```
|
||||
|
||||
If you don't need to check the missing privileges, you can put
|
||||
`check_player_privs` directly into the if statement.
|
||||
|
||||
```lua
|
||||
if not minetest.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 = minetest.get_player_privs(name)
|
||||
print(dump(privs))
|
||||
|
||||
privs.vote = true
|
||||
minetest.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 such as `give` and `server`, which have greater potential for abuse.
|
||||
|
||||
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.
|
||||
To do this, you must edit the minetest.conf file.
|
||||
|
||||
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.
|
|
@ -0,0 +1,241 @@
|
|||
---
|
||||
title: "SFINV: Inventory Formspec"
|
||||
layout: default
|
||||
root: ../..
|
||||
idx: 4.7
|
||||
redirect_from: /en/chapters/sfinv.html
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
Simple Fast Inventory (SFINV) is a mod found in Minetest Game that is used to
|
||||
create the player's inventory [formspec](formspecs.html). SFINV comes with
|
||||
an API that allows you to add and otherwise manage the pages shown.
|
||||
|
||||
Whilst SFINV by default shows pages as tabs, pages are called pages
|
||||
because it is entirely possible that a mod or game decides to show them in
|
||||
some other format instead.
|
||||
For example, multiple pages could be shown in one formspec.
|
||||
|
||||
* [Registering a Page](#registering-a-page)
|
||||
* [Receiving events](#receiving-events)
|
||||
* [Conditionally showing to players](#conditionally-showing-to-players)
|
||||
* [on_enter and on_leave callbacks](#on_enter-and-on_leave-callbacks)
|
||||
|
||||
## Registering a Page
|
||||
|
||||
SFINV provides the aptly named `sfinv.register_page` function to create pages.
|
||||
Simply call the function with the page's name and its definition:
|
||||
|
||||
```lua
|
||||
sfinv.register_page("mymod:hello", {
|
||||
title = "Hello!",
|
||||
get = function(self, player, context)
|
||||
return sfinv.make_formspec(player, context,
|
||||
"label[0.1,0.1;Hello world!]", true)
|
||||
end
|
||||
})
|
||||
```
|
||||
|
||||
The `make_formspec` function surrounds your formspec with SFINV's formspec code.
|
||||
The fourth parameter, currently set as `true`, determines whether the
|
||||
player's inventory is shown.
|
||||
|
||||
Let's make things more exciting; here is the code for the formspec generation
|
||||
part of a player admin tab. This tab will allow admins to kick or ban players by
|
||||
selecting them in a list and clicking a button.
|
||||
|
||||
```lua
|
||||
sfinv.register_page("myadmin:myadmin", {
|
||||
title = "Tab",
|
||||
get = function(self, player, context)
|
||||
local players = {}
|
||||
context.myadmin_players = players
|
||||
|
||||
-- Using an array to build a formspec is considerably faster
|
||||
local formspec = {
|
||||
"textlist[0.1,0.1;7.8,3;playerlist;"
|
||||
}
|
||||
|
||||
-- Add all players to the text list, and to the players list
|
||||
local is_first = true
|
||||
for _ , player in pairs(minetest.get_connected_players()) do
|
||||
local player_name = player:get_player_name()
|
||||
players[#players + 1] = player_name
|
||||
if not is_first then
|
||||
formspec[#formspec + 1] = ","
|
||||
end
|
||||
formspec[#formspec + 1] =
|
||||
minetest.formspec_escape(player_name)
|
||||
is_first = false
|
||||
end
|
||||
formspec[#formspec + 1] = "]"
|
||||
|
||||
-- Add buttons
|
||||
formspec[#formspec + 1] = "button[0.1,3.3;2,1;kick;Kick]"
|
||||
formspec[#formspec + 1] = "button[2.1,3.3;2,1;ban;Kick + Ban]"
|
||||
|
||||
-- Wrap the formspec in sfinv's layout
|
||||
-- (ie: adds the tabs and background)
|
||||
return sfinv.make_formspec(player, context,
|
||||
table.concat(formspec, ""), false)
|
||||
end,
|
||||
})
|
||||
```
|
||||
|
||||
There's nothing new about the above code; all the concepts are
|
||||
covered above and in previous chapters.
|
||||
|
||||
<figure>
|
||||
<img src="{{ page.root }}//static/sfinv_admin_fs.png" alt="Player Admin Page">
|
||||
</figure>
|
||||
|
||||
## Receiving events
|
||||
|
||||
You can receive formspec events by adding a `on_player_receive_fields` function
|
||||
to a sfinv definition.
|
||||
|
||||
```lua
|
||||
on_player_receive_fields = function(self, player, context, fields)
|
||||
-- TODO: implement this
|
||||
end,
|
||||
```
|
||||
|
||||
`on_player_receive_fields` works the same as
|
||||
`minetest.register_on_player_receive_fields`, except that `context` is
|
||||
given instead of `formname`.
|
||||
Please note that SFINV will consume events relevant to itself, such as
|
||||
navigation tab events, so you won't receive them in this callback.
|
||||
|
||||
Now let's implement the `on_player_receive_fields` for our admin mod:
|
||||
|
||||
```lua
|
||||
on_player_receive_fields = function(self, player, context, fields)
|
||||
-- text list event, check event type and set index if selection changed
|
||||
if fields.playerlist then
|
||||
local event = minetest.explode_textlist_event(fields.playerlist)
|
||||
if event.type == "CHG" then
|
||||
context.myadmin_selected_idx = event.index
|
||||
end
|
||||
|
||||
-- Kick button was pressed
|
||||
elseif fields.kick then
|
||||
local player_name =
|
||||
context.myadmin_players[context.myadmin_selected_idx]
|
||||
if player_name then
|
||||
minetest.chat_send_player(player:get_player_name(),
|
||||
"Kicked " .. player_name)
|
||||
minetest.kick_player(player_name)
|
||||
end
|
||||
|
||||
-- Ban button was pressed
|
||||
elseif fields.ban then
|
||||
local player_name =
|
||||
context.myadmin_players[context.myadmin_selected_idx]
|
||||
if player_name then
|
||||
minetest.chat_send_player(player:get_player_name(),
|
||||
"Banned " .. player_name)
|
||||
minetest.ban_player(player_name)
|
||||
minetest.kick_player(player_name, "Banned")
|
||||
end
|
||||
end
|
||||
end,
|
||||
```
|
||||
|
||||
There's a rather large problem with this, however. Anyone can kick or ban players! You
|
||||
need a way to only show this to players with the kick or ban privileges.
|
||||
Luckily SFINV allows you to do this!
|
||||
|
||||
## Conditionally showing to players
|
||||
|
||||
You can add an `is_in_nav` function to your page's definition if you'd like to
|
||||
control when the page is shown:
|
||||
|
||||
```lua
|
||||
is_in_nav = function(self, player, context)
|
||||
local privs = minetest.get_player_privs(player:get_player_name())
|
||||
return privs.kick or privs.ban
|
||||
end,
|
||||
```
|
||||
|
||||
If you only need to check one priv or want to perform an 'and', you should use
|
||||
`minetest.check_player_privs()` instead of `get_player_privs`.
|
||||
|
||||
Note that the `is_in_nav` is only called when the player's inventory formspec is
|
||||
generated. This happens when a player joins the game, switches tabs, or a mod
|
||||
requests for SFINV to regenerate.
|
||||
|
||||
This means that you need to manually request that SFINV regenerates the inventory
|
||||
formspec on any events that may change `is_in_nav`'s result. In our case,
|
||||
we need to do that whenever kick or ban is granted or revoked to a player:
|
||||
|
||||
```lua
|
||||
local function on_grant_revoke(grantee, granter, priv)
|
||||
if priv ~= "kick" and priv ~= "ban" then
|
||||
return
|
||||
end
|
||||
|
||||
local player = minetest.get_player_by_name(grantee)
|
||||
if not player then
|
||||
return
|
||||
end
|
||||
|
||||
local context = sfinv.get_or_create_context(player)
|
||||
if context.page ~= "myadmin:myadmin" then
|
||||
return
|
||||
end
|
||||
|
||||
sfinv.set_player_inventory_formspec(player, context)
|
||||
end
|
||||
|
||||
minetest.register_on_priv_grant(on_grant_revoke)
|
||||
minetest.register_on_priv_revoke(on_grant_revoke)
|
||||
```
|
||||
|
||||
## on_enter and on_leave callbacks
|
||||
|
||||
A player *enters* a tab when the tab is selected, and *leaves* a
|
||||
tab when another tab is about to be selected.
|
||||
It's possible for multiple pages to be selected if a custom theme is
|
||||
used.
|
||||
|
||||
Note that these events may not be triggered by the player.
|
||||
The player may not even have the formspec open at that time.
|
||||
For example, on_enter is called for the home page when a player
|
||||
joins the game even before they open their inventory.
|
||||
|
||||
It's not possible to cancel a page change, as that would potentially
|
||||
confuse the player.
|
||||
|
||||
```lua
|
||||
on_enter = function(self, player, context)
|
||||
|
||||
end,
|
||||
|
||||
on_leave = function(self, player, context)
|
||||
|
||||
end,
|
||||
```
|
||||
|
||||
## Adding to an existing page
|
||||
|
||||
To add content to an existing page, you will need to override the page
|
||||
and modify the returned formspec.
|
||||
|
||||
```lua
|
||||
local old_func = sfinv.registered_pages["sfinv:crafting"].get
|
||||
sfinv.override_page("sfinv:crafting", {
|
||||
get = function(self, player, context, ...)
|
||||
local ret = old_func(self, player, context, ...)
|
||||
|
||||
if type(ret) == "table" then
|
||||
ret.formspec = ret.formspec .. "label[0,0;Hello]"
|
||||
else
|
||||
-- Backwards compatibility
|
||||
ret = ret .. "label[0,0;Hello]"
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
})
|
||||
```
|
|
@ -0,0 +1,257 @@
|
|||
---
|
||||
title: Intro to Clean Architectures
|
||||
layout: default
|
||||
root: ../..
|
||||
idx: 7.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)
|
||||
* [Model-View-Controller](#model-view-controller)
|
||||
* [API-View](#api-view)
|
||||
* [Observer](#observer)
|
||||
* [Conclusion](#conclusion)
|
||||
|
||||
|
||||
|
||||
## Cohesion, Coupling, and Separation of Concerns
|
||||
|
||||
Without any planning, a programming project will tend to gradually descend into
|
||||
spaghetti code. Spaghetti code is characterised by a lack of structure - all the
|
||||
code is thrown in together with no clear boundaries. This ultimately makes a
|
||||
project completely unmaintainable, ending in its abandonment.
|
||||
|
||||
The opposite of this is to design your project as a collection of interacting
|
||||
smaller programs or areas of code. <!-- Weird wording? -->
|
||||
|
||||
> Inside every large program, there is a small program trying to get out.
|
||||
>
|
||||
> --C.A.R. Hoare
|
||||
|
||||
This should be done in such a way that you achieve Separation of Concerns -
|
||||
each area should be distinct and address a separate need or concern.
|
||||
|
||||
These programs/areas should have the following two properties:
|
||||
|
||||
* **High Cohesion** - the area should be closely/tightly related.
|
||||
* **Low Coupling** - keep dependencies between areas as low as possible, and avoid
|
||||
relying on internal implementations. It's a very good idea to make sure you have
|
||||
a low amount of coupling, as this means that changing the APIs of certain areas
|
||||
will be more feasible.
|
||||
|
||||
Note that these apply both when thinking about the relationship between mods,
|
||||
and the relationship between areas inside a mod.
|
||||
|
||||
|
||||
## 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 (`minetest.*`, 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 amount 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
|
||||
|
||||
minetest.register_chatcommand("/land", {
|
||||
privs = { land = true },
|
||||
func = function(name)
|
||||
land.handle_creation_request(name)
|
||||
end,
|
||||
})
|
||||
|
||||
minetest.register_on_player_receive_fields(function(player,
|
||||
formname, fields)
|
||||
land.handle_create_submit(player:get_player_name(),
|
||||
fields.area_name)
|
||||
end)
|
||||
```
|
||||
|
||||
The above is the Model-View-Controller pattern. The model is a collection of data
|
||||
with minimal functions. The view is a collection of functions which listen to
|
||||
events and pass it to the controller, and also receives calls from the controller to
|
||||
do something with the Minetest API. The controller is where the decisions and
|
||||
most of the calculations are made.
|
||||
|
||||
The controller should have no knowledge about the Minetest API - notice how
|
||||
there are no Minetest calls or any view functions that resemble them.
|
||||
You should *NOT* have a function like `view.hud_add(player, def)`.
|
||||
Instead, the view defines some actions that the controller can tell the view to do,
|
||||
like `view.add_hud(info)` where info is a value or table which doesn't relate
|
||||
to the Minetest API at all.
|
||||
|
||||
<figure class="right_image">
|
||||
<img
|
||||
width="100%"
|
||||
src="{{ page.root }}/static/mvc_diagram.svg"
|
||||
alt="Diagram showing a centered text element">
|
||||
</figure>
|
||||
|
||||
It is important that each area only communicates with its direct neighbours,
|
||||
as shown above, in order to reduce how much you need to change if you modify
|
||||
an area's internals or externals. For example, to change the formspec you
|
||||
would only need to edit the view. To change the view API, you would only need to
|
||||
change the view and the controller, but not the model at all.
|
||||
|
||||
In practice, this design is rarely used because of the increased complexity
|
||||
and because it doesn't give many benefits for most types of mods. Instead,
|
||||
you will commonly see a less formal and strict kind of design -
|
||||
variants of the API-View.
|
||||
|
||||
|
||||
### API-View
|
||||
|
||||
In an ideal world, you'd have the above 3 areas perfectly separated with all
|
||||
events going into the controller before going back to the normal view. But
|
||||
this isn't the real world. A good compromise is to reduce the mod into two
|
||||
parts:
|
||||
|
||||
* **API** - This was the model and controller above. There should be no uses of
|
||||
`minetest.` 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.
|
||||
|
||||
|
||||
## Observer
|
||||
|
||||
Reducing coupling may seem hard to do to begin with, but you'll make a lot of
|
||||
progress by splitting your code up using a design like the one given above.
|
||||
It's not always possible to remove the need for one area to communicate with
|
||||
another, but there are ways to decouple anyway - one example being 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
|
||||
|
||||
-- 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.
|
||||
|
||||
## 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.
|
|
@ -0,0 +1,167 @@
|
|||
---
|
||||
title: Common Mistakes
|
||||
layout: default
|
||||
root: ../..
|
||||
idx: 7.1
|
||||
redirect_from: /en/chapters/common_mistakes.html
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
This chapter details common mistakes, and how to avoid them.
|
||||
|
||||
* [Never Store ObjectRefs (ie: players or entities)](#never-store-objectrefs-ie-players-or-entities)
|
||||
* [Don't Trust Formspec Submissions](#dont-trust-formspec-submissions)
|
||||
* [Set ItemStacks After Changing Them](#set-itemstacks-after-changing-them)
|
||||
|
||||
## Never Store ObjectRefs (ie: players or entities)
|
||||
|
||||
If the object an ObjectRef represents is deleted - for example, if the player goes
|
||||
offline or the entity is unloaded - then calling methods on that object
|
||||
will result in a crash.
|
||||
|
||||
For example, don't do this:
|
||||
|
||||
```lua
|
||||
minetest.register_on_joinplayer(function(player)
|
||||
local function func()
|
||||
local pos = player:get_pos() -- BAD!
|
||||
-- `player` is stored then accessed later.
|
||||
-- If the player leaves in that second, the server *will* crash.
|
||||
end
|
||||
|
||||
minetest.after(1, func)
|
||||
|
||||
foobar[player:get_player_name()] = player
|
||||
-- RISKY
|
||||
-- It's not recommended to do this.
|
||||
-- Use minetest.get_connected_players() and
|
||||
-- minetest.get_player_by_name() instead.
|
||||
end)
|
||||
```
|
||||
|
||||
Do this instead:
|
||||
|
||||
```lua
|
||||
minetest.register_on_joinplayer(function(player)
|
||||
local function func(name)
|
||||
-- Attempt to get the ref again
|
||||
local player = minetest.get_player_by_name(name)
|
||||
|
||||
-- Check that the player is still online
|
||||
if player then
|
||||
-- Yay! This is fine
|
||||
local pos = player:get_pos()
|
||||
end
|
||||
end
|
||||
|
||||
-- Pass the name into the function
|
||||
minetest.after(1, func, player:get_player_name())
|
||||
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 minetest.check_player_privs(name, { privs = true }) then
|
||||
return false
|
||||
end
|
||||
|
||||
minetest.show_formspec(name, "modman:modman", [[
|
||||
size[3,2]
|
||||
field[0,0;3,1;target;Name;]
|
||||
button_exit[0,1;3,1;sub;Promote]
|
||||
]])
|
||||
return true
|
||||
})
|
||||
|
||||
minetest.register_on_player_receive_fields(function(player,
|
||||
formname, fields)
|
||||
-- BAD! Missing privilege check here!
|
||||
|
||||
local privs = minetest.get_player_privs(fields.target)
|
||||
privs.kick = true
|
||||
privs.ban = true
|
||||
minetest.set_player_privs(fields.target, privs)
|
||||
return true
|
||||
end)
|
||||
```
|
||||
|
||||
Add a privilege check to solve this:
|
||||
|
||||
```lua
|
||||
minetest.register_on_player_receive_fields(function(player,
|
||||
formname, fields)
|
||||
if not minetest.check_player_privs(name, { privs = true }) then
|
||||
return false
|
||||
end
|
||||
|
||||
-- code
|
||||
end)
|
||||
```
|
||||
|
||||
## 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
|
||||
minetest.register_on_item_eat(function(hp_change, replace_with_item,
|
||||
itemstack, user, pointed_thing)
|
||||
itemstack:get_meta():set_string("description", "Partially eaten")
|
||||
-- Almost correct! Data will be lost if another
|
||||
-- callback cancels the behaviour
|
||||
end)
|
||||
```
|
||||
|
||||
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
|
||||
minetest.register_on_item_eat(function(hp_change, replace_with_item,
|
||||
itemstack, user, pointed_thing)
|
||||
itemstack:get_meta():set_string("description", "Partially eaten")
|
||||
user:get_inventory():set_stack("main", user:get_wield_index(),
|
||||
itemstack)
|
||||
-- Correct, description will always be set!
|
||||
end)
|
||||
```
|
||||
|
||||
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.
|
|
@ -0,0 +1,150 @@
|
|||
---
|
||||
title: Automatic Error Checking
|
||||
layout: default
|
||||
root: ../..
|
||||
idx: 7.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)
|
||||
* [Checking Commits with Travis](#checking-commits-with-travis)
|
||||
|
||||
## 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
|
||||
less errors this time. Starting at the first error you get, either modify the
|
||||
configuration to take it into account, or if there's a bug then fix it - take
|
||||
a look at 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`.
|
||||
|
||||
## 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.
|
||||
|
||||
* **Atom** - `linter-luacheck`.
|
||||
* **Sublime** - Install using package-control:
|
||||
[SublimeLinter](https://github.com/SublimeLinter/SublimeLinter),
|
||||
[SublimeLinter-luacheck](https://github.com/SublimeLinter/SublimeLinter-luacheck).
|
||||
|
||||
## Checking Commits with Travis
|
||||
|
||||
If your project is public and is on Github, you can use TravisCI - a free service
|
||||
to run jobs on commits to check them. This means that every commit you push will
|
||||
be checked against LuaCheck, and a green tick or red cross will be displayed next to them
|
||||
depending on whether LuaCheck finds any mistakes. This is especially helpful for
|
||||
when your project receives a pull request - you'll be able to see the LuaCheck output
|
||||
without downloading the code.
|
||||
|
||||
First you should visit [travis-ci.org](https://travis-ci.org/) and sign in with
|
||||
your Github account. Then find your project's repo in your Travis profile,
|
||||
and enable Travis by flipping the switch.
|
||||
|
||||
Next, create a file called .travis.yml with the following content:
|
||||
|
||||
```yml
|
||||
language: generic
|
||||
sudo: false
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- luarocks
|
||||
before_install:
|
||||
- luarocks install --local luacheck
|
||||
script:
|
||||
- $HOME/.luarocks/bin/luacheck .
|
||||
notifications:
|
||||
email: false
|
||||
```
|
||||
|
||||
If your project is a game rather than a mod or mod pack,
|
||||
change the line after `script:` to:
|
||||
|
||||
```yml
|
||||
- $HOME/.luarocks/bin/luacheck mods/
|
||||
```
|
||||
|
||||
Now commit and push to Github. Go to your project's page on Github, and click
|
||||
'commits'. You should see an orange disc next to the commit you just made.
|
||||
After a while it should change either into a green tick or a red cross depending on the
|
||||
outcome of LuaCheck. In either case, you can click the icon to see the build logs
|
||||
and the output of LuaCheck.
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
title: Read More
|
||||
layout: default
|
||||
root: ../..
|
||||
idx: 7.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 - [HTML version]({{ page.root }}/lua_api.html) |
|
||||
[Text version](https://github.com/minetest/minetest/blob/master/doc/lua_api.txt).
|
||||
* Explore the [Developer Wiki](http://dev.minetest.net/Main_Page).
|
||||
* Look at [existing mods](https://forum.minetest.net/viewforum.php?f=11).
|
||||
|
||||
### Lua Programming
|
||||
|
||||
* [Programming in Lua (PIL)](http://www.lua.org/pil/).
|
||||
* [Lua Crash Course](http://luatut.com/crash_course.html).
|
||||
|
||||
### 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).
|
|
@ -0,0 +1,205 @@
|
|||
---
|
||||
title: Releasing a Mod
|
||||
layout: default
|
||||
root: ../..
|
||||
idx: 7.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.
|
||||
|
||||
* [License Choices](#license-choices)
|
||||
* [Packaging](#packaging)
|
||||
* [Uploading](#uploading)
|
||||
* [Forum Topic](#forum-topic)
|
||||
|
||||
## License Choices
|
||||
|
||||
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.
|
||||
|
||||
### 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
|
||||
|
||||
These licenses allow anyone to do what they want with your mod,
|
||||
which means they can modify, redistribute, sell, or leave-out attribution.
|
||||
These licenses can be used for both code and art.
|
||||
|
||||
It is important to note that WTFPL is strongly discouraged and people may
|
||||
choose not to use your mod if it has this license.
|
||||
|
||||
### MIT
|
||||
|
||||
This is a common license for mod code. The only restriction it places on users
|
||||
of your mod is that they must include the same copyright notice and license
|
||||
in any copies of the mod or of substantial parts of the mod.
|
||||
|
||||
## Packaging
|
||||
|
||||
There are some files that are recommended to include in your mod
|
||||
before you release it.
|
||||
|
||||
### README.txt
|
||||
|
||||
The README file should state:
|
||||
|
||||
* What the mod does.
|
||||
* What the license is.
|
||||
* What dependencies there are.
|
||||
* How to install the mod.
|
||||
* Current version of the mod.
|
||||
* Optionally, the where to report problems or get help.
|
||||
|
||||
### description.txt
|
||||
|
||||
This should explain what your mod 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:
|
||||
|
||||
Adds soup, cakes, bakes and juices.
|
||||
|
||||
Avoid this:
|
||||
|
||||
(BAD) The food mod for Minetest.
|
||||
|
||||
### 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 in the mod store.
|
||||
|
||||
## 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 on the forum and download the file
|
||||
without having to view another page.
|
||||
* **Virus Free** - Mods with malicious content will be removed from the forum.
|
||||
|
||||
### Version Control Systems
|
||||
|
||||
It is recommended that you use a version control system which:
|
||||
|
||||
* Allows other developers to easily submit changes.
|
||||
* Allows the code to be previewed before downloading.
|
||||
* Allows users to submit bug reports.
|
||||
|
||||
The majority of Minetest modders use GitHub as a website to host their code,
|
||||
but alternatives are possible.
|
||||
|
||||
Using a GitHub can be difficult at first. If you need help with this, for
|
||||
information on using GitHub, please see:
|
||||
|
||||
* [Pro Git book](http://git-scm.com/book/en/v1/Getting-Started) - Free to read online.
|
||||
* [GitHub for Windows app](https://help.github.com/articles/getting-started-with-github-for-windows/) -
|
||||
Using a graphical interface on Windows to upload your code.
|
||||
|
||||
### Forum Attachments
|
||||
|
||||
As an alternative to using a version management system, you can use forum attachments to share
|
||||
your mods. This can be done when creating a mod's forum topic (covered below).
|
||||
|
||||
You need to zip the files for the mod into a single file. How to do this varies from
|
||||
operating system to operating system.
|
||||
This is nearly always done using the right click menu after selecting all files.
|
||||
|
||||
When making a forum topic, on the "Create a Topic" page (see below), go to the
|
||||
"Upload Attachment" tab at the bottom.
|
||||
Click "Browse" and select the zipped file. It is recommended that you
|
||||
enter the version of your mod in the comment field.
|
||||
|
||||
<figure>
|
||||
<img src="{{ page.root }}/static/releasing_attachments.png" alt="Upload Attachment">
|
||||
<figcaption>
|
||||
Upload Attachment tab.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
## Forum Topic
|
||||
|
||||
You can now create a forum topic. You should create it in
|
||||
the ["WIP Mods"](https://forum.minetest.net/viewforum.php?f=9) (Work In Progress)
|
||||
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 Minetest forum uses bbcode for formatting. Here is an example for a
|
||||
mod named superspecial:
|
||||
|
||||
|
||||
Adds magic, rainbows and other special things.
|
||||
|
||||
See download attached.
|
||||
|
||||
[b]Version:[/b] 1.1
|
||||
[b]License:[/b] LGPL 2.1 or later
|
||||
|
||||
Dependencies: default mod (found in minetest_game)
|
||||
|
||||
Report bugs or request help on the forum topic.
|
||||
|
||||
[h]Installation[/h]
|
||||
|
||||
Unzip the archive, rename the folder to superspecial and
|
||||
place it in minetest/mods/
|
||||
|
||||
( GNU/Linux: If you use a system-wide installation place
|
||||
it in ~/.minetest/mods/. )
|
||||
|
||||
( If you only want this to be used in a single world, place
|
||||
the folder in worldmods/ in your world directory. )
|
||||
|
||||
For further information or help see:
|
||||
[url]http://wiki.minetest.com/wiki/Installing_Mods[/url]
|
||||
|
||||
If you modify the above example for your mod topic, remember to
|
||||
change "superspecial" to the name of your mod.
|
||||
|
||||
### Subject
|
||||
|
||||
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]
|
|
@ -0,0 +1,110 @@
|
|||
---
|
||||
title: Security
|
||||
layout: default
|
||||
root: ../..
|
||||
idx: 7.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
|
||||
minetest.register_on_player_receive_fields(function(player,
|
||||
formname, fields)
|
||||
for key, field in pairs(fields) do
|
||||
local x,y,z = string.match(key,
|
||||
"goto_([%d-]+)_([%d-]+)_([%d-]+)")
|
||||
if x and y and z then
|
||||
player:set_pos({ x=tonumber(x), y=tonumber(y),
|
||||
z=tonumber(z) })
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
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 = minetest.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.
|
|
@ -0,0 +1,192 @@
|
|||
---
|
||||
title: Automatic Unit Testing
|
||||
layout: default
|
||||
root: ../..
|
||||
idx: 7.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 make your code avoid this.
|
||||
|
||||
* [Installing Busted](#installing-busted)
|
||||
* [Your First Test](#your-first-test)
|
||||
* [Mocking: Using External Functions](#mocking-using-external-functions)
|
||||
* [Checking Commits with Travis](#checking-commits-with-travis)
|
||||
* [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(minetest.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 minetest.chat_send_all(name, message)
|
||||
table.insert(chat_send_all_calls, { name = name, message = message })
|
||||
end
|
||||
|
||||
-- Tests
|
||||
describe("list_areas", function()
|
||||
it("returns a line for each area", function()
|
||||
chat_send_all_calls = {} -- reset table
|
||||
|
||||
mymod.list_areas_to_chat("singleplayer", "singleplayer")
|
||||
|
||||
assert.equals(2, #chat_send_all_calls)
|
||||
end)
|
||||
|
||||
it("sends to right player", function()
|
||||
chat_send_all_calls = {} -- reset table
|
||||
|
||||
mymod.list_areas_to_chat("singleplayer", "singleplayer")
|
||||
|
||||
for _, call in pairs(chat_send_all_calls) do --_
|
||||
assert.equals("singleplayer", call.name)
|
||||
end
|
||||
end)
|
||||
|
||||
-- The above two tests are actually pointless,
|
||||
-- as this one tests both things
|
||||
it("returns correct thing", function()
|
||||
chat_send_all_calls = {} -- reset table
|
||||
|
||||
mymod.list_areas_to_chat("singleplayer", "singleplayer")
|
||||
|
||||
local expected = {
|
||||
{ name = "singleplayer", message = "Town Hall (2,43,63)" },
|
||||
{ name = "singleplayer", message = "Airport (43,45,63)" },
|
||||
}
|
||||
assert.same(expected, chat_send_all_calls)
|
||||
end)
|
||||
end)
|
||||
```
|
||||
|
||||
|
||||
## Checking Commits with Travis
|
||||
|
||||
The Travis script from the [Automatic Error Checking](luacheck.html)
|
||||
chapter can be modified to also run Busted.
|
||||
|
||||
```yml
|
||||
language: generic
|
||||
sudo: false
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- luarocks
|
||||
before_install:
|
||||
- luarocks install --local luacheck && luarocks install --local busted
|
||||
script:
|
||||
- $HOME/.luarocks/bin/luacheck .
|
||||
- $HOME/.luarocks/bin/busted .
|
||||
notifications:
|
||||
email: false
|
||||
```
|
||||
|
||||
|
||||
## Conclusion
|
||||
|
||||
Unit tests will greatly increase the quality and reliability of your project if used
|
||||
well, but they require you to structure your code in a different way than usual.
|
||||
|
||||
For an example of a mod with lots of unit tests, see
|
||||
[crafting by rubenwardy](https://github.com/rubenwardy/crafting).
|
Loading…
Reference in New Issue