From 48230eda0687552a383616c92e25c15e19e9972c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20M=C3=BCller?= <34514239+appgurueu@users.noreply.github.com> Date: Sat, 27 Aug 2022 21:21:27 +0200 Subject: [PATCH] Document MetaData & subclasses (#40) * Document MetaData & subclasses * Document `context` inv location availability * ModStorage: Add section on backends * Document client support --- doc/classes/ItemStackMetaData.adoc | 116 +++++++++++++++ doc/classes/MetaData.adoc | 227 +++++++++++++++++++++++++++++ doc/classes/ModStorage.adoc | 68 +++++++++ doc/classes/NodeMetaData.adoc | 144 ++++++++++++++++++ doc/classes/PlayerMetaData.adoc | 44 ++++++ 5 files changed, 599 insertions(+) create mode 100644 doc/classes/ItemStackMetaData.adoc create mode 100644 doc/classes/MetaData.adoc create mode 100644 doc/classes/ModStorage.adoc create mode 100644 doc/classes/NodeMetaData.adoc create mode 100644 doc/classes/PlayerMetaData.adoc diff --git a/doc/classes/ItemStackMetaData.adoc b/doc/classes/ItemStackMetaData.adoc new file mode 100644 index 0000000..6419156 --- /dev/null +++ b/doc/classes/ItemStackMetaData.adoc @@ -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. diff --git a/doc/classes/MetaData.adoc b/doc/classes/MetaData.adoc new file mode 100644 index 0000000..ac48ce8 --- /dev/null +++ b/doc/classes/MetaData.adoc @@ -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. diff --git a/doc/classes/ModStorage.adoc b/doc/classes/ModStorage.adoc new file mode 100644 index 0000000..1dc8b67 --- /dev/null +++ b/doc/classes/ModStorage.adoc @@ -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 = "", + 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 +}) +---- diff --git a/doc/classes/NodeMetaData.adoc b/doc/classes/NodeMetaData.adoc new file mode 100644 index 0000000..b420e52 --- /dev/null +++ b/doc/classes/NodeMetaData.adoc @@ -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:,,` 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. diff --git a/doc/classes/PlayerMetaData.adoc b/doc/classes/PlayerMetaData.adoc new file mode 100644 index 0000000..ca5074f --- /dev/null +++ b/doc/classes/PlayerMetaData.adoc @@ -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 `:score` +or as key-value pair with key `` 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 +|===