minetest_modding_book/_en/players/formspecs.md

380 lines
13 KiB
Markdown
Raw Permalink Normal View History

2014-12-14 22:15:36 +03:00
---
2020-05-05 00:10:23 +03:00
title: GUIs (Formspecs)
2014-12-14 22:15:36 +03:00
layout: default
2018-07-15 21:36:35 +03:00
root: ../..
2018-07-15 17:28:10 +03:00
idx: 4.5
description: Learn how to display GUIs using formspecs
2018-07-15 21:13:16 +03:00
redirect_from: /en/chapters/formspecs.html
2018-07-20 22:51:31 +03:00
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.
2014-12-14 22:15:36 +03:00
---
## Introduction <!-- omit in toc -->
2014-12-14 22:15:36 +03:00
<figure class="right_image">
2018-07-15 21:36:35 +03:00
<img src="{{ page.root }}//static/formspec_example.png" alt="Furnace Inventory">
<figcaption>
Screenshot of furnace formspec, labelled.
</figcaption>
</figure>
2014-12-14 22:15:36 +03:00
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.
2019-08-14 02:50:30 +03:00
In Minetest, forms are windows such as the player inventory and can contain a
variety of elements, such as labels, buttons and fields.
2017-10-26 01:28:23 +03:00
Note that if you do not need to get user input, for example when you only need
2019-08-14 02:50:30 +03:00
to provide information to the player, you should consider using
[Heads Up Display (HUD)](hud.html) elements instead of forms, because
unexpected windows tend to disrupt gameplay.
2014-12-14 22:15:36 +03:00
2019-08-14 02:50:30 +03:00
- [Real or Legacy Coordinates](#real-or-legacy-coordinates)
- [Anatomy of a Formspec](#anatomy-of-a-formspec)
2022-07-31 22:33:16 +03:00
- [Elements](#elements)
- [Header](#header)
2019-08-14 02:50:30 +03:00
- [Guessing Game](#guessing-game)
2022-07-31 22:33:16 +03:00
- [Padding and Spacing](#padding-and-spacing)
- [Receiving Formspec Submissions](#receiving-formspec-submissions)
- [Contexts](#contexts)
2019-08-14 02:50:30 +03:00
- [Formspec Sources](#formspec-sources)
2022-07-31 22:33:16 +03:00
- [Node Meta Formspecs](#node-meta-formspecs)
- [Player Inventory Formspecs](#player-inventory-formspecs)
- [Your Turn](#your-turn)
2014-12-14 22:15:36 +03:00
2019-08-14 02:50:30 +03:00
## Real or Legacy Coordinates
2014-12-14 22:15:36 +03:00
2019-08-14 02:50:30 +03:00
In older versions of Minetest, formspecs were inconsistent. The way that different
elements were positioned varied in unexpected ways; it was hard to predict the
placement of elements and align them. Minetest 5.1.0 contains a feature
called real coordinates which aims to rectify this by introducing a consistent
coordinate system. The use of real coordinates is highly recommended, and so
this chapter will use them exclusively.
2015-09-25 02:23:03 +03:00
2020-12-24 16:25:31 +03:00
Using a formspec_version of 2 or above will enable real coordinates.
2019-08-14 02:50:30 +03:00
## Anatomy of a Formspec
### Elements
2014-12-14 22:15:36 +03:00
2019-08-14 02:50:30 +03:00
Formspec is a domain-specific language with an unusual format.
It consists of a number of elements with the following form:
2014-12-14 22:15:36 +03:00
2019-08-14 02:50:30 +03:00
type[param1;param2]
2014-12-14 22:15:36 +03:00
2019-08-14 02:50:30 +03:00
The element type is declared and then any parameters are given
in square brackets. Multiple elements can be joined together, or placed
on multiple lines, like so:
2014-12-14 22:15:36 +03:00
2019-08-14 02:50:30 +03:00
foo[param1]bar[param1]
bo[param1]
Elements are items such as text boxes or buttons, or can be metadata such
as size or background. You should refer to
2023-05-01 14:22:02 +03:00
[lua_api.md](https://minetest.gitlab.io/minetest/formspec/)
for a list of all possible elements.
2014-12-14 22:15:36 +03:00
2019-08-14 02:50:30 +03:00
### Header
2014-12-14 22:15:36 +03:00
2019-08-14 02:50:30 +03:00
The header of a formspec contains information which must appear first. This
includes the size of the formspec, the position, the anchor, and whether the
game-wide theme should be applied.
2014-12-14 22:15:36 +03:00
2019-08-14 02:50:30 +03:00
The elements in the header must be defined in a specific order, otherwise you
will see an error. This order is given in the above paragraph, and, as always,
2020-12-24 16:38:26 +03:00
documented in the Lua API reference.
2014-12-14 22:15:36 +03:00
2019-08-14 02:50:30 +03:00
The size is in formspec slots - a unit of measurement which is roughly
around 64 pixels, but varies based on the screen density and scaling
settings of the client. Here's a formspec which is `2,2` in size:
2014-12-14 22:15:36 +03:00
2020-12-24 16:25:31 +03:00
formspec_version[4]
2019-08-14 02:50:30 +03:00
size[2,2]
2014-12-14 22:15:36 +03:00
2020-05-05 00:10:23 +03:00
Notice how we explicitly defined the formspec language version.
Without this, the legacy system will instead be used instead - which will
2023-01-27 16:54:49 +03:00
prevent the use of consistent element positioning and other new features.
2014-12-14 22:15:36 +03:00
2019-08-14 02:50:30 +03:00
The position and anchor elements are used to place the formspec on the screen.
The position sets where on the screen the formspec will be, and defaults to
the center (`0.5,0.5`). The anchor sets where on the formspec the position is,
allowing you to line the formspec up with the edge of the screen. The formspec
can be placed to the left of the screen like so:
2014-12-14 22:15:36 +03:00
2020-12-24 16:25:31 +03:00
formspec_version[4]
2019-08-14 02:50:30 +03:00
size[2,2]
position[0,0.5]
anchor[0,0.5]
2014-12-14 22:15:36 +03:00
2019-08-14 02:50:30 +03:00
This sets the anchor to the left middle edge of the formspec box, and then the
position of that anchor to the left of the screen.
2014-12-14 22:15:36 +03:00
2019-08-14 02:50:30 +03:00
## Guessing Game
2017-10-26 01:28:23 +03:00
<figure class="right_image">
2019-08-14 02:50:30 +03:00
<img src="{{ page.root }}/static/formspec_guessing.png" alt="Guessing Formspec">
<figcaption>
2019-08-14 02:50:30 +03:00
The guessing game formspec.
</figcaption>
</figure>
2019-08-14 02:50:30 +03:00
The best way to learn is to make something, so let's make a guessing game.
The principle is simple: the mod decides on a number, then the player makes
guesses on the number. The mod then says if the guess is higher or lower then
the actual number.
First, let's make a function to create the formspec code. It's good practice to
do this, as it makes it easier to reuse elsewhere.
<div style="clear: both;"></div>
```lua
2019-08-14 02:50:30 +03:00
guessing = {}
function guessing.get_formspec(name)
-- TODO: display whether the last guess was higher or lower
local text = "I'm thinking of a number... Make a guess!"
local formspec = {
2020-12-24 16:25:31 +03:00
"formspec_version[4]",
2019-08-14 02:50:30 +03:00
"size[6,3.476]",
2024-10-23 02:39:22 +03:00
"label[0.375,0.5;", core.formspec_escape(text), "]",
2019-08-14 02:50:30 +03:00
"field[0.375,1.25;5.25,0.8;number;Number;]",
"button[1.5,2.3;3,0.8;guess;Guess]"
}
-- table.concat is faster than string concatenation - `..`
return table.concat(formspec, "")
end
```
2019-08-14 02:50:30 +03:00
In the above code, we place a field, a label, and a button. A field allows text
entry, and a button is used to submit the form. You'll notice that the elements
are positioned carefully in order to add padding and spacing, this will be explained
later.
Next, we want to allow the player to show the formspec. The main way to do this
is using `show_formspec`:
```lua
2019-08-14 02:50:30 +03:00
function guessing.show_to(name)
2024-10-23 02:39:22 +03:00
core.show_formspec(name, "guessing:game", guessing.get_formspec(name))
2019-08-14 02:50:30 +03:00
end
2024-10-23 02:39:22 +03:00
core.register_chatcommand("game", {
2019-08-14 02:50:30 +03:00
func = function(name)
guessing.show_to(name)
end,
})
```
The `show_formspec` function accepts a player name, the formspec name, and the
2019-08-14 02:50:30 +03:00
formspec itself. The formspec name should be a valid itemname, ie: in the format
`modname:itemname`.
### Padding and Spacing
<figure class="right_image">
<img src="{{ page.root }}/static/formspec_padding_spacing.png" alt="Padding and spacing">
<figcaption>
The guessing game formspec.
</figcaption>
</figure>
Padding is the gap between the edge of the formspec and its contents, or between unrelated
elements, shown in red. Spacing is the gap between related elements, shown in blue.
It is fairly standard to have a padding of `0.375` and a spacing of `0.25`.
<div style="clear: both;"></div>
### Receiving Formspec Submissions
2014-12-14 22:15:36 +03:00
2019-08-14 02:50:30 +03:00
When `show_formspec` is called, the formspec is sent to the client to be displayed.
For formspecs to be useful, information needs to be returned from the client to server.
The method for this is called formspec field submission, and for `show_formspec`, that
submission is received using a global callback:
```lua
2024-10-23 02:39:22 +03:00
core.register_on_player_receive_fields(function(player, formname, fields)
2019-08-14 02:50:30 +03:00
if formname ~= "guessing:game" then
return
end
2014-12-14 22:15:36 +03:00
2019-08-14 02:50:30 +03:00
if fields.guess then
local pname = player:get_player_name()
2024-10-23 02:39:22 +03:00
core.chat_send_all(pname .. " guessed " .. fields.number)
end
2014-12-14 22:15:36 +03:00
end)
```
2014-12-14 22:15:36 +03:00
2024-10-23 02:39:22 +03:00
The function given in `core.register_on_player_receive_fields` is called
2019-08-14 02:50:30 +03:00
every time a user submits a form. Most callbacks will need to check the formname given
2017-10-26 01:28:23 +03:00
to the function, and exit if it is not the right form; however, some callbacks
2019-08-14 02:50:30 +03:00
may need to work on multiple forms, or on all forms.
The `fields` parameter to the function is a table of the values submitted by the
user, indexed by strings. Named elements will appear in the field under their own
2023-01-27 16:54:49 +03:00
name, depending on the event. Some elements will only be submitted if they caused
the event, such as buttons, and some elements will always appear in submissions,
such as fields.
2014-12-14 22:15:36 +03:00
2018-07-20 22:51:31 +03:00
{% include notice.html notice=page.submit_vuln %}
2019-08-14 02:50:30 +03:00
So, now the formspec is sent to the client and the client sends information back.
The next step is to somehow generate and remember the target value, and to update
the formspec based on guesses. The way to do this is using a concept called
"contexts".
2014-12-14 22:15:36 +03:00
2019-08-14 02:50:30 +03:00
### Contexts
2014-12-14 22:15:36 +03:00
2024-10-23 02:39:22 +03:00
In many cases you want core.show_formspec to give information
2019-08-14 02:50:30 +03:00
to the callback which you don't want to send to the client. This might include
what a chat command was called with, or what the dialog is about. In this case,
the target value that needs to be remembered.
A context is a per-player table to store information, and the contexts for all
online players are stored in a file-local variable:
2014-12-14 22:15:36 +03:00
```lua
2019-08-14 02:50:30 +03:00
local _contexts = {}
local function get_context(name)
local context = _contexts[name] or {}
_contexts[name] = context
return context
end
2024-10-23 02:39:22 +03:00
core.register_on_leaveplayer(function(player)
2019-08-14 02:50:30 +03:00
_contexts[player:get_player_name()] = nil
end)
```
2019-08-14 02:50:30 +03:00
Next, we need to modify the show code to update the context
before showing the formspec:
2014-12-14 22:15:36 +03:00
2019-08-14 02:50:30 +03:00
```lua
function guessing.show_to(name)
local context = get_context(name)
context.target = context.target or math.random(1, 10)
2014-12-14 22:15:36 +03:00
2019-08-14 02:50:30 +03:00
local fs = guessing.get_formspec(name, context)
2024-10-23 02:39:22 +03:00
core.show_formspec(name, "guessing:game", fs)
2019-08-14 02:50:30 +03:00
end
```
We also need to modify the formspec generation code to use the context:
2014-12-14 22:15:36 +03:00
```lua
2019-08-14 02:50:30 +03:00
function guessing.get_formspec(name, context)
local text
if not context.guess then
text = "I'm thinking of a number... Make a guess!"
elseif context.guess == context.target then
text = "Hurray, you got it!"
elseif context.guess > context.target then
text = "Too high!"
2019-08-14 02:50:30 +03:00
else
text = "Too low!"
end
2019-08-14 02:50:30 +03:00
```
2014-12-14 22:15:36 +03:00
Note that it's good practice for `get_formspec` to only read the context, and not
2019-08-14 02:50:30 +03:00
update it at all. This can make the function simpler, and also easier to test.
2019-08-14 02:50:30 +03:00
And finally, we need to update the handler to update the context with the guess:
2019-08-14 02:50:30 +03:00
```lua
if fields.guess then
local name = player:get_player_name()
local context = get_context(name)
context.guess = tonumber(fields.number)
guessing.show_to(name)
end
```
2014-12-14 22:15:36 +03:00
2019-08-14 02:50:30 +03:00
## Formspec Sources
2014-12-14 22:15:36 +03:00
2019-08-14 02:50:30 +03:00
There are three different ways that a formspec can be delivered to the client:
2014-12-14 22:15:36 +03:00
* [show_formspec](#guessing-game): the method used above, fields are received by `register_on_player_receive_fields`.
2019-08-14 02:50:30 +03:00
* [Node Meta Formspecs](#node-meta-formspecs): the node contains a formspec in its meta data, and the client
shows it *immediately* when the player rightclicks. Fields are received by a
method in the node definition called `on_receive_fields`.
* [Player Inventory Formspecs](#player-inventory-formspecs): the formspec is sent to the client at some point, and then
shown immediately when the player presses `i`. Fields are received by
`register_on_player_receive_fields`.
2014-12-14 23:04:14 +03:00
2019-08-14 02:50:30 +03:00
### Node Meta Formspecs
2014-12-14 23:04:14 +03:00
2024-10-23 02:39:22 +03:00
`core.show_formspec` is not the only way to show a formspec; you can also
2023-04-30 03:06:04 +03:00
add formspecs to a [node's metadata](../map/storage.html). For example,
2017-10-26 01:28:23 +03:00
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.
2014-12-14 23:04:14 +03:00
```lua
2024-10-23 02:39:22 +03:00
core.register_node("mymod:rightclick", {
description = "Rightclick me!",
tiles = {"mymod_rightclick.png"},
groups = {cracky = 1},
after_place_node = function(pos, placer)
2020-05-05 00:10:23 +03:00
-- 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.
2024-10-23 02:39:22 +03:00
local meta = core.get_meta(pos)
meta:set_string("formspec",
2020-12-24 16:25:31 +03:00
"formspec_version[4]" ..
2020-05-05 00:10:23 +03:00
"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)
2020-05-05 00:10:23 +03:00
if fields.quit then
return
end
print(fields.x)
end
2014-12-14 23:04:14 +03:00
})
```
2014-12-14 23:04:14 +03:00
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.
2017-10-26 01:28:23 +03:00
This style of callback triggers when you press enter
2024-10-23 02:39:22 +03:00
in a field, which is impossible with `core.show_formspec`;
however, this kind of form can only be shown by right-clicking on a
node. It cannot be triggered programmatically.
2019-08-14 02:50:30 +03:00
### Player Inventory Formspecs
The player inventory formspec is the one shown when the player presses i.
The global callback is used to receive events from this formspec, and the
formname is `""`.
2022-07-31 22:33:16 +03:00
There are a number of different mods which allow multiple mods to customise the
player inventory. Minetest Game uses
[SFINV](https://github.com/rubenwardy/sfinv/blob/master/Tutorial.md).
2019-08-14 02:50:30 +03:00
### Your Turn
* Extend the Guessing Game to keep track of each player's top score, where the
top score is how many guesses it took.
* Make a node called "Inbox" where users can open up a formspec and leave messages.
This node should store the placers' name as `owner` in the meta, and should use
`show_formspec` to show different formspecs to different players.