Document MetaData & subclasses (#40)

* Document MetaData & subclasses

* Document `context` inv location availability

* ModStorage: Add section on backends

* Document client support
This commit is contained in:
Lars Müller 2022-08-27 21:21:27 +02:00 committed by GitHub
parent 6bdf60a6c4
commit 48230eda06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 599 additions and 0 deletions

@ -0,0 +1,116 @@
= ItemStackMetaData
include::../include/config.adoc[]
include::../include/types.adoc[]
:description: Per-ItemStack persistent key-value store
:keywords: meta, data, metadata, item, stack, storage, key, value, persistence
ItemStackMetaData is a subclass of MetaData obtained via `stack:get_meta()`
allowing for persistent storage of key-value pairs tied to ItemStacks.
WARNING: ItemStack metadata is serialized with ItemStacks, increasing the ItemString length.
Inventories have to store multiple ItemStrings, all of which an attacker will try to get to maximum length.
Always limit the size of your ItemStackMetaData to keep inventories sendable.
== Special Fields
=== Description
* `description`: The description to be shown when hovering over the item (see `ItemStack:get_description`).
* `short_description`: A short description of the item (see `ItemStack:get_short_description`).
=== Hardware Colorization
* `color`: ColorString to use for hardware colorization of the stack.
* `palette_index`: Palette index to use for hardware colorization of the stack (if the stack has a palette).
=== Count
Requires Minetest 5.6 clients for the count override
(older clients will simply show the item count according to the item definition);
works on all 5.x servers since it only uses ItemStackMetaData serverside.
==== `count_meta`
String to show in inventory lists instead of the item count.
==== `count_alignment`
Integer (use with `:get_int` and `:set_int`).
Alignment of the displayed item count value is encoded as `x_align + 4 * y_align`
where `x_align` and `y_align` are one of:
[%autowidth, frame=none]
|===
| Value | X-alignment (horizontally) | Y-alignment (vertically)
| `0` | Default (currently same as `3`: Right) | Default (currently same as `3`: Bottom/Down)
| `1` | Left | Top/Up
| `2` | Centered/Middle | Centered/Middle
| `3` | Right | Bottom/Down
|===
[TIP]
.Code quality
====
Magic numbers make code unreadable.
Add an explanatory comment when setting alignment:
[source,lua]
----
local meta = stack:get_meta()
meta:set_string("count_alignment", 3 + 4 * 1) -- aligned to the top-right corner
----
or use well-named (constant) local variables:
[source,lua]
----
local meta = stack:get_meta()
local align_top_right = 3 + 4 * 1
meta:set_int("count_alignment", align_top_right)
----
or perhaps wrap this all up in a useful helper:
[source,lua]
----
local vert_align = {
default = 0,
top = 1,
center = 2,
bottom = 3
}
local horz_align = {
default = 0,
left = 1,
center = 2,
right = 3
}
local function set_stack_cnt_align(stack, vertically, horizontally)
stack:get_meta():set_int("count_alignment", horz_align[horizontally] + 4 * vert_align[vertically])
end
set_stack_cnt_align(stack, "top", "right")
----
====
== Methods
=== `:set_tool_capabilities(tool_capabilities)`
Allows overriding the tool capabilities specified in the item definition.
==== Arguments
[%autowidth, frame=none]
|===
| `tool_capabilities` | `nil` or a tool capabilities table | Either:
* `nil`: Clears the tool capability override, or
* Tool capabilities table: Overrides the defined tool capabilities (see ItemDefinition for the table format)
|===
NOTE: The corresponding `:get_tool_capabilities` is not a method of ItemStackMetaData but rather of the "parent" ItemStack.

227
doc/classes/MetaData.adoc Normal file

@ -0,0 +1,227 @@
= MetaData
include::../include/config.adoc[]
include::../include/types.adoc[]
:description: Base class of various meta data classes used to persist key-value pairs
:keywords: meta, data, metadata, storage, key, value, persistence
MetaData is an interface implemented by various reference types implementing persistent
string-based key-value stores.
The methods documented below are available in all subclasses.
== Subclasses
Subclasses tie the key-value store to various objects recognized by Minetest:
* ModStorage - per mod
* NodeMetaData - per node on the map
* ItemStackMetaData - per item stack
* PlayerMetaData - per player
== Methods
=== Getters
NOTE: No type information is stored for values; values will be coerced to and from string as needed.
Mods need to know which type they expect in order to call the appropriate getters & setters.
Do not rely on coercion to work one way or another; never mix different types.
WARNING: https://github.com/minetest/minetest/issues/12577[Getters currently resolve the value `${key}` to the value associated with `key`].
[TIP]
.Storing other types
====
Due to the limitations of the provided setters & getters, you might favor using your own
(de)serialization for coercion of Lua types to strings which can be stored in the string k-v store.
* `minetest.write_json` & `minetest.parse_json` for Lua tables which are representable as JSON;
* `minetest.serialize` & `minetest.deserialize` for arbitrary Lua tables (consisting of tables & primitive types);
[source,lua]
----
local meta = ... -- some MetaData reference
local json = {key = "value", list = {1, 2, 3}}
meta:set_string("json", minetest.write_json(json))
local got_json = minetest.parse_json(meta:get_string"json")
assert(got_json.key == "value" and got_json.list[1] == 1 and got_json.list[2] == 2 and got_json.list[3] == 3)
local lua = {[42] = true, [true] = false} -- JSON only allows string keys
meta:set_string("lua", minetest.serialize(lua))
local got_lua = minetest.deserialize(meta:get_string"lua")
assert(got_lua[42] == true and got_lua[true] == false)
----
Applying serialization to numbers provides you with safe number storage;
you don't have to worry about C(++) type bounds.
====
==== Arguments
All getters take only a single argument: The key/name.
[%autowidth, frame=none]
|===
| `key` | `{type-string}` | the key/name
|===
==== `:contains(key)`
Checks for the existence of a key-value pair.
===== Returns
[%autowidth, frame=none]
|===
| `has` | `nil`, `true` or `false` |One of:
* `nil`: Invalid `self`
* `false`: No key-value pair with the given key exists.
* `true`: A key-value pair with the given key exists.
|===
==== `:get(key)`
Retrieves the value associated with a key.
===== Returns
[%autowidth, frame=none]
|===
| `value` | `nil` or `{type-string}` | Either:
* `nil` if no matching key-value pair exists, or
* `{type-string}`: The associated value
|===
==== `:get_string(key)`
Retrieves the value associated with a key & coerces to string.
===== Returns
[%autowidth, frame=none]
|===
| `value` | `{type-string}` | Either:
* `""` if no matching key-value pair exists, or
* `{type-string}`: The associated value
|===
==== `:get_int(key)`
Retrieves the value associated with a key & coerces it to an integer.
===== Returns
[%autowidth, frame=none]
|===
| `value` | `{type-number}` | Either:
* `0` if no matching key-value pair exists, or
* `{type-number}`: The associated value, coerced to an integer
|===
==== `:get_float(key)`
Retrieves the value associated with a key & coerces it to a floating-point number.
===== Returns
[%autowidth, frame=none]
|===
| `value` | `{type-number}` | Either:
* `0` if no matching key-value pair exists, or
* `{type-number}`: The associated value, coerced to a floating point number
|===
=== Setters
==== Arguments & Returns
Setters have no return values; they all take exactly two arguments: Key & value.
[%autowidth, frame=none]
|===
| `key` | `{type-string}` | the key/name
| `value` | depends on the setter | the value
|===
==== `:set_string(key, value)`
==== Arguments
[%autowidth, frame=none]
|===
| `value` | `{type-string}` | The value to associate with `key`. Either:
* `""` to remove the key-value pair, or
* any other string to update/insert a key-value pair
|===
==== `:set_int(key, value)`
==== Arguments
[%autowidth, frame=none]
|===
| `value` | `{type-number}` | The integer value to coerce to a string & associate with `key`
WARNING: Integer refers to a C(++) `int` as internally used by the implementation - usually 32 bits wide -
meaning it is unable to represent as large integer numbers as the Lua number type.
Be careful when storing integers with large absolute values; they may overflow.
Keep `value` between `-2^31` and `2^31 - 1`, both inclusive.
|===
==== `:set_float(key, value)`
==== Arguments
[%autowidth, frame=none]
|===
| `value` | `{type-number}` | The floating-point value to coerce to a string & associate with `key`
WARNING: The implementation internally uses the C(++) `float` type - usually 32 bits wide -
whereas Lua guarantees 64-bit "double-precision" floating point numbers.
This may lead to a precision loss. Large numbers in particular may be hardly representable.
|===
==== `:equals(other)`
===== Arguments
[%autowidth, frame=none]
|===
| `other` | MetaData | a MetaData object
|===
===== Returns
[%autowidth, frame=none]
|===
| `same` | `{type-bool}` | whether `self` has the same key-value pairs as `other`
|===
==== `:to_table()`
Converts the metadata to a Lua table representation.
===== Returns
[%autowidth, frame=none]
|===
| `value` | `nil` or `{type-table}` | Either:
* `nil` if the metadata is invalid (?), or
* `{type-table}`: A table representation of the metadata with the following fields:
** `fields`: Table `{[key] = value, ...}`
** Additional fields depending on the subclass
|===
TIP: Use `table = assert(meta:to_table())` to error if the operation failed.
==== `:from_table(table)`
Sets the key-value pairs to match those of a given table representation or clears the metadata.
===== Arguments
[%autowidth, frame=none]
|===
| `table` | `{type-table}` or any other type | Either:
* The table representation as produced by `:to_table()`, or
* Any non-table value: Clears the metadata
|===
===== Returns
[%autowidth, frame=none]
|===
| `value` | `{type-bool}` | whether loading the table representation succeeded
|===
TIP: Use `assert(meta:from_table(table))` to error if the operation failed.

@ -0,0 +1,68 @@
= ModStorage
include::../include/config.adoc[]
include::../include/types.adoc[]
:description: Per-mod persistent key-value store
:keywords: meta, data, metadata, mod, storage, key, value, persistence
ModStorage is a per-world, per-mod persistent string key-value store implementing all methods of MetaData.
The granularity of the persisted snapshots is determined by the `map_save_interval` setting.
== Backends
Two backends are available for ModStorage: JSON and SQLite3.
WARNING: The JSON backend is incapable of saving raw binary data due to JSON restrictions.
Even though the SQLite3 backend supports arbitrary bytestrings,
you may not rely on saving arbitrary bytestrings to work,
since you can't ensure that the SQLite3 backend is being used.
If the SQLite3 backend is used, it is usually more efficient to leverage the key-value store
than to store fully serialized data structures; fully serializing the data takes linear time
in the size of the data whereas updating the key-value store only takes linear time
in the size of the changes with the SQLite3 backend;
for the JSON backend it is irrelevant -
it has to fully serialize the data every map save interval
anyways, increasing the risk of data loss if writing the file fails
due to a hard crash (or freeze) of the Minetest server process.
== `minetest.get_mod_storage()`
Must be called at load time.
=== Returns
[%autowidth, frame=none]
|===
| `storage` | ModStorage | Private ModStorage object for the currently loading mod
|===
=== Example
A basic greeting mod with a persistent greeting might look as follows:
[source,lua]
----
local storage = minetest.get_mod_storage()
-- Send the greeting to joining players
minetest.register_on_joinplayer(function(player)
local greeting = storage:get"greeting"
if greeting then
minetest.chat_send_player(player:get_player_name(), greeting)
end
end)
-- Allow moderators to change the greeting
minetest.register_chatcommand("/set_greeting", {
params = "<greeting>",
description = "Sets the greeting",
privs = {server = true},
func = function(name, param)
param = param:trim() -- MT-provided string.trim
storage:set_string("greeting", param)
return true, param == "" and "Greeting cleared." or "Greeting set."
end
})
----

@ -0,0 +1,144 @@
= NodeMetaData
include::../include/config.adoc[]
include::../include/types.adoc[]
:description: Per-node persistent key-value store
:keywords: meta, data, metadata, node, storage, key, value, persistence
Node storage in Minetest is usually limited to
* A content ID (determining the node name),
* The `param1` unsigned byte (`0` - `255` both inclusive) which is usually reserved for lighting,
* The `param2` unsigned byte (`0` - `255` both inclusive) which can be used depending on `paramtype2`
NodeMetaData allows extending this with a per-world, per-in-world-node
persistent string key-value store implementing all methods of MetaData
allowing you to associate arbitrary data with any node.
Additionally, node metadata allows storing inventories for nodes like chests or furnaces.
== `minetest.get_meta(pos)`
Obtains a mutable NodeMetaData reference for the node at `pos`.
=== Arguments
[%autowidth, frame=none]
|===
| `pos` | Vector | Node position on the map; floats are rounded appropriately.
|===
=== Returns
[%autowidth, frame=none]
|===
| `meta` | NodeMetaData | Corresponding NodeMetaData reference
|===
== `minetest.find_nodes_with_meta(pos1, pos2)`
Searches a region for nodes with non-empty metadata.
=== Arguments
Cuboid corners are rounded properly if they are not integers.
[%autowidth, frame=none]
|===
| `pos1` | Vector | First corner of the cuboid region (inclusive)
| `pos2` | Vector | Second corner of the cuboid region (inclusive)
|===
=== Returns
[%autowidth, frame=none]
|===
| `positions` | List of Vector | List of integer positions of nodes that have non-empty meta
|===
== Special Fields
=== `formspec`
FormSpec to show when the node is interacted with using the "place/use" key (rightclick by default).
Has no effect if `on_rightclick` is defined in the node definition (see `minetest.register_node`).
The most notable advantage of this over calling
`minetest.show_formspec` in `on_rightclick` is clientside prediction:
The client can immediately show the FormSpec; the second approach requires the client to first inform
the server of the interaction, to which the server then responds with the FormSpec.
This takes one round-trip time (RTT).
The obvious disadvantage is that FormSpecs can't be dynamically generated in response to user interaction;
formspecs must be mostly static. To alleviate this, formspecs provide context-dependant placeholders
like the `context` or `current_player` inventory locations (see FormSpec).
IMPORTANT: The `context` inventory location can only be used in FormSpecs using the special `formspec` NodeMetaData field.
FormSpecs shown using `minetest.show_formspec` must use `nodemeta:<X>,<Y>,<Z>` to reference inventories instead.
Another disadvantage is that plenty of redundant metadata - often a constant FormSpec - has to be stored with every node.
This metadata also has to be sent to clients. Minetest's mapblock compression should be able to compress duplicate substrings -
FormSpecs in this case - reasonably well though.
In terms of network traffic it depends: With meta, the FormSpec is "only" sent when the mapblock gets updated;
whereas the other approach resends the FormSpec every time the user interacts with the node.
=== `infotext`
Text to show when the node is pointed at (same as the `infotext` object property).
Long texts are line-wrapped, even longer texts are truncated.
== Methods
=== `:mark_as_private(keys)`
NodeMetaData is by default fully sent to clients;
the special `formspec` and `infotext` fields triggering clientside behavior
obviously need to be sent to clients to work.
All other fields do not need to be sent to clients
unless you want to explicitly support local mapsaving.
TIP: Mark all other fields as private to reduce traffic.
If you don't want clients to be able to see private NodeMetaData fields -
usually to prevent cheating - you must mark them as private.
NOTE: The private marking is tied to a key-value pair.
If the key-value pair is deleted, the private marking is deleted as well.
If the key-value pair is recreated, the private marking must be recreated as well.
NOTE: `to_table` and `from_table` do not keep track of which fields were marked as private.
==== Arguments
[%autowidth, frame=none]
|===
| `keys` | `{type-string}` or list of `{type-string}` | Either:
* A single key to mark as private, or
* A list of keys to mark as private
|===
=== `:get_inventory()`
==== Returns
[%autowidth, frame=none]
|===
| `inv` | Inventory | The inventory associated with the node
|===
=== `:to_table()`
Extends MetaData `:to_table()` by additionally adding an `inventory` field for
a table `{[listname] = list}` where `list` is a list of ItemStrings
(`""` for empty) with the same length as the size of the inventory list.
TIP: Use `table = assert(meta:to_table())` to error if the operation failed.
==== `:from_table(table)`
Extends MetaData `:from_table(table)` to add support for the `inventory` field.

@ -0,0 +1,44 @@
= PlayerMetaData
include::../include/config.adoc[]
include::../include/types.adoc[]
:description: Per-player persistent key-value store
:keywords: meta, data, metadata, player, storage, key, value, persistence
PlayerMetaData is a per-world, per-player persistent string key-value store implementing all methods of MetaData.
The granularity of the persisted snapshots is determined by the `map_save_interval` setting.
NOTE: Since PlayerMetaData is shared across all mods,
it is considered good practice to prefix the keys set by your mod with your mod name plus a delimiter
such as `:` to avoid collisions: The `score` field of a quest mod and a mobs mod f.E. might
be called `fancy_quests:score` and `fancy_mobs:score` respectively.
== `player:get_meta()`
Used to obtain a mutable PlayerMetaData reference.
=== Arguments
[%autowidth, frame=none]
|===
| `player` | A valid Player object
|===
NOTE: Minetest requiring a valid `player` object means PlayerMetaData is only accessible
while players are online; if you need per-player storage while players are offline,
you can use ModStorage and save either a serialized table per-player or concatenate
the keys of the per-player entries with the playername using a delimiter.
Reusing the above example, `fancy_quests:score` might be stored as `<playername>:score`
or as key-value pair with key `<playername>` and value `minetest.write_json{score = ...}`
(or `minetest.serialize`) in ModStorage.
A https://github.com/minetest/minetest/issues/6193[feature request]
to make PlayerMetaData available for offline players exists;
=== Returns
[%autowidth, frame=none]
|===
| `meta` | PlayerMetaData | Public & shared PlayerMetaData object for the player
|===