From 2be856f3d132312fac9ec868bf0c080405376b5c Mon Sep 17 00:00:00 2001 From: Wuzzy Date: Sat, 18 Mar 2017 01:00:20 +0100 Subject: [PATCH] Import Help 1.1.0: doc, doc_items, doc_identifier --- mods/HELP/doc/README.md | 18 + mods/HELP/doc/description.txt | 1 + mods/HELP/doc/doc/API.md | 543 +++++++ mods/HELP/doc/doc/README.md | 52 + mods/HELP/doc/doc/depends.txt | 5 + mods/HELP/doc/doc/description.txt | 1 + mods/HELP/doc/doc/init.lua | 1270 +++++++++++++++ mods/HELP/doc/doc/locale/de.txt | 42 + mods/HELP/doc/doc/locale/template.txt | 42 + mods/HELP/doc/doc/mod.conf | 1 + mods/HELP/doc/doc/screenshot.png | Bin 0 -> 12315 bytes mods/HELP/doc/doc/sounds/doc_reveal.ogg | Bin 0 -> 8873 bytes .../doc/textures/doc_awards_icon_generic.png | Bin 0 -> 639 bytes .../doc/textures/doc_button_icon_hires.png | Bin 0 -> 2336 bytes .../doc/textures/doc_button_icon_lores.png | Bin 0 -> 722 bytes .../inventory_plus_doc_inventory_plus.png | Bin 0 -> 722 bytes mods/HELP/doc/doc_identifier/API.md | 40 + mods/HELP/doc/doc_identifier/README.md | 29 + mods/HELP/doc/doc_identifier/depends.txt | 5 + mods/HELP/doc/doc_identifier/description.txt | 1 + mods/HELP/doc/doc_identifier/init.lua | 190 +++ mods/HELP/doc/doc_identifier/locale/de.txt | 13 + .../doc/doc_identifier/locale/template.txt | 13 + mods/HELP/doc/doc_identifier/mod.conf | 1 + mods/HELP/doc/doc_identifier/screenshot.png | Bin 0 -> 28682 bytes .../textures/doc_identifier_identifier.png | Bin 0 -> 429 bytes .../doc_identifier_identifier_liquid.png | Bin 0 -> 414 bytes mods/HELP/doc/doc_items/API.md | 357 +++++ mods/HELP/doc/doc_items/README.md | 68 + mods/HELP/doc/doc_items/depends.txt | 2 + mods/HELP/doc/doc_items/description.txt | 1 + mods/HELP/doc/doc_items/init.lua | 1406 +++++++++++++++++ mods/HELP/doc/doc_items/locale/de.txt | 140 ++ mods/HELP/doc/doc_items/locale/template.txt | 140 ++ mods/HELP/doc/doc_items/mod.conf | 1 + mods/HELP/doc/doc_items/screenshot.png | Bin 0 -> 15507 bytes mods/HELP/doc/doc_items/settingtypes.txt | 16 + mods/HELP/doc/modpack.txt | 0 38 files changed, 4398 insertions(+) create mode 100644 mods/HELP/doc/README.md create mode 100644 mods/HELP/doc/description.txt create mode 100644 mods/HELP/doc/doc/API.md create mode 100644 mods/HELP/doc/doc/README.md create mode 100644 mods/HELP/doc/doc/depends.txt create mode 100644 mods/HELP/doc/doc/description.txt create mode 100644 mods/HELP/doc/doc/init.lua create mode 100644 mods/HELP/doc/doc/locale/de.txt create mode 100644 mods/HELP/doc/doc/locale/template.txt create mode 100644 mods/HELP/doc/doc/mod.conf create mode 100644 mods/HELP/doc/doc/screenshot.png create mode 100644 mods/HELP/doc/doc/sounds/doc_reveal.ogg create mode 100644 mods/HELP/doc/doc/textures/doc_awards_icon_generic.png create mode 100644 mods/HELP/doc/doc/textures/doc_button_icon_hires.png create mode 100644 mods/HELP/doc/doc/textures/doc_button_icon_lores.png create mode 100644 mods/HELP/doc/doc/textures/inventory_plus_doc_inventory_plus.png create mode 100644 mods/HELP/doc/doc_identifier/API.md create mode 100644 mods/HELP/doc/doc_identifier/README.md create mode 100644 mods/HELP/doc/doc_identifier/depends.txt create mode 100644 mods/HELP/doc/doc_identifier/description.txt create mode 100644 mods/HELP/doc/doc_identifier/init.lua create mode 100644 mods/HELP/doc/doc_identifier/locale/de.txt create mode 100644 mods/HELP/doc/doc_identifier/locale/template.txt create mode 100644 mods/HELP/doc/doc_identifier/mod.conf create mode 100644 mods/HELP/doc/doc_identifier/screenshot.png create mode 100644 mods/HELP/doc/doc_identifier/textures/doc_identifier_identifier.png create mode 100644 mods/HELP/doc/doc_identifier/textures/doc_identifier_identifier_liquid.png create mode 100644 mods/HELP/doc/doc_items/API.md create mode 100644 mods/HELP/doc/doc_items/README.md create mode 100644 mods/HELP/doc/doc_items/depends.txt create mode 100644 mods/HELP/doc/doc_items/description.txt create mode 100644 mods/HELP/doc/doc_items/init.lua create mode 100644 mods/HELP/doc/doc_items/locale/de.txt create mode 100644 mods/HELP/doc/doc_items/locale/template.txt create mode 100644 mods/HELP/doc/doc_items/mod.conf create mode 100644 mods/HELP/doc/doc_items/screenshot.png create mode 100644 mods/HELP/doc/doc_items/settingtypes.txt create mode 100644 mods/HELP/doc/modpack.txt diff --git a/mods/HELP/doc/README.md b/mods/HELP/doc/README.md new file mode 100644 index 0000000000..c8fd1407d4 --- /dev/null +++ b/mods/HELP/doc/README.md @@ -0,0 +1,18 @@ +# Help +MineClone 2 uses some of the mods found in the Help modpack by Wuzzy. + +The goal of this modpack is to make using Minetest and mods easier for both +newcomers and advanced users. +It makes it easier for newcomers by making help more accessible. +It makes life easier for advanced user by making it more convenient to use, by +centralizing the help where you most need it: Inside the game. This modpack +will also make the life of modder easier by allowing them to add help texts +directly into mods (via `doc_items`). + +More information is given in the respective mods. + +Overview of the mods used in MineClone 2: + +* `doc`: Documentation System. Core API and user interface. Mods can add arbitrary categories and entries +* `doc_items`: Item Help. Adds automatically generated help texts for items and an API +* `doc_identifier`: Lookup Tool. A tool to identify and show help texts for pointed things diff --git a/mods/HELP/doc/description.txt b/mods/HELP/doc/description.txt new file mode 100644 index 0000000000..59a7a4c216 --- /dev/null +++ b/mods/HELP/doc/description.txt @@ -0,0 +1 @@ +Provides an extensible in-game help with texts about gameplay basics (such a crafting), items and advanced usage. diff --git a/mods/HELP/doc/doc/API.md b/mods/HELP/doc/doc/API.md new file mode 100644 index 0000000000..59b88250b9 --- /dev/null +++ b/mods/HELP/doc/doc/API.md @@ -0,0 +1,543 @@ +# API documentation for the Documentation System +## Core concepts +As a modder, you are free to write basically about everything and are also +relatively free in the presentation of information. There are no +restrictions on content whatsoever. + +### Categories and entries +In the Documentation System, everything is built on categories and entries. +An entry is a single piece of documentation and is the basis of all actual +documentation. Categories group multiple entries of the same topic together. + +Categories also define a template function which is used to determine how the +final result in the tab “Entry list” looks like. Entries themselves have +a data field attached to them, this is a table containing arbitrary metadata +which is used to construct the final formspec in the Entry tab. It may also +be used for sorting entries in the entry list. + +## Advanced concepts +### Viewed and hidden entries +The mod keeps track of which entries have been viewed on a per-player basis. +Any entry which has been accessed by a player is immediately marked as +“viewed”. + +Entries can also be hidden. Hidden entries are not visible or otherwise +accessible to players until they become revealed by function calls. + +Marking an entry as viewed or revealed is not reversible with this API. +The viewed and hidden states are stored in the file `doc.mt` inside the +world directory. You can safely delete this file if you want to reset +the player states. + +### Entry aliases +Entry aliases are alternative identifiers for entry identifiers. With the +exception of the alias functions themselves, for functions demanding an +`entry_id` you can either supply the original `entry_id` or any alias of the +`entry_id`. + +## Possible use cases +This section shows some possible use cases to give you a rough idea what +this mod is capable of and how these use cases could be implemented. + +### Simple use case: Minetest basics +Let's say you want to write in free form short help texts about the basic +concepts of Minetest or your subgame. First you could define a category +called “Basics”, the data for each of its entry is just a free form text. +The template function simply creates a formspec where this free form +text is displayed. + +This is one of the most simple use cases and the mod `doc_basics` does +exactly that. + +### Complex use case: Blocks +You could create a category called “Blocks”, and this category is supposed to +contain entries for every single block (i.e. node) in the game. For this use +case, a free form approach would be very inefficient and error-prone, as a +lot of data can be reused. + +Here the template function comes in handy: The internal entry data +contain a lot of different things about a block, like block name, identifier, +custom description and most importantly, the definition table of the block. + +Finally, the template function takes all that data and turns it into +sentences which are just concatenated, telling as many useful facts about +this block as possible. + +## Functions +This is a list of all publicly available functions. + +### Overview +The most important functions are `doc.add_category` and `doc.ad_entry`. All other functions +are mostly used for utility and examination purposes. + +If not mentioned otherwise, the return value of all functions is `nil`. + +These functions are available: + +#### Core +* `doc.add_category`: Adds a new category +* `doc.add_entry`: Adds a new entry + +#### Display +* `doc.show_entry`: Shows a particular entry to a player +* `doc.show_category`: Shows the entry list of a category to a player +* `doc.show_doc`: Opens the main help form for a player + +#### Query +* `doc.get_category_definition`: Returns the definition table of a category +* `doc.get_entry_definition`: Returns the definition table of an entry +* `doc.entry_exists`: Checks whether an entry exists +* `doc.entry_viewed`: Checks whether an entry has been viewed/read by a player +* `doc.entry_revealed`: Checks whether an entry is visible and normally accessible to a player +* `doc.get_category_count`: Returns the total number of categories +* `doc.get_entry_count`: Returns the total number of entries in a category +* `doc.get_viewed_count`: Returns the number of entries a player has viewed in a category +* `doc.get_revealed_count`: Returns the number of entries a player has access to in a category +* `doc.get_hidden_count`: Returns the number of entries which are hidden from a player in a category +* `doc.get_selection`: Returns the currently viewed entry/category of a player + +#### Modify +* `doc.set_category_order`: Sets the order of categories in the category list +* `doc.mark_entry_as_viewed`: Manually marks an entry as viewed/read by a player +* `doc.mark_entry_as_revealed`: Make a hidden entry visible and accessible to a player +* `doc.mark_all_entries_as_revealed`: Make all hidden entries visible and accessible to a player + +#### Aliases +* `doc.add_entry_alias`: Add an alternative name which can be used to access an entry + +#### Special widgets +This API provides functions to add unique “widgets” for functionality +you may find useful when creating entry templates. You find these +functions in `doc.widgets`. +Currently there is a widget for scrollable multi-line text and a +widget providing an image gallery. + + + +### `doc.add_category(id, def)` +Adds a new category. You have to define an unique identifier, a name +and a template function to build the entry formspec from the entry +data. + +**Important**: You must call this function *before* any player joins. + +#### Parameters +* `id`: Unique category identifier as a string +* `def`: Definition table with the following fields: + * `name`: Category name to be shown in the interface + * `description`: (optional) Short description of the category, + will be shown as tooltip. Recommended style (in English): + First letter capitalized, no punctuation at the end, + max. 100 characters + * `build_formspec`: The template function (see below). Takes entry data + as its first parameter (has the data type of the entry data) and the + name of the player who views the entry as its second parameter. It must + return a formspec which is inserted in the Entry tab. + * `sorting`: (optional) Sorting algorithm for display order of entries + * `"abc"`: Alphabetical (default) + * `"nosort"`: Entries appear in no particular order + * `"custom"`: Manually define the order of entries in `sorting_data` + * `"function"`: Sort by function defined in `sorting_data` + * `sorting_data`: (optional) Additional data for special sorting methods. + * If `sorting=="custom"`, this field must contain a table (list form) in which + the entry IDs are specified in the order they are supposed to appear in the + entry list. All entries which are missing in this table will appear in no + particular order below the final specified one. + * If `sorting=="function"`, this field is a compare function to be used as + the `comp` parameter of `table.sort`. The parameters given are two entries. + * This field is not required if `sorting` has any other value + * `hide_entries_by_default` (optional): If `true`, all entries + added to this category will start as hidden, unless explicitly specified otherwise + (default: `false`) + +Note: For function-based sorting, the entries provided to the compare function +will have the following format: + + { + eid = e, -- unique entry identifier + name = n, -- entry name + data = d, -- arbitrary entry data + } + +#### Using `build_formspec` +For `build_formspec` you can either define your own function which +procedurally generates the entry formspec or you use one of the +following predefined convenience functions: + +* `doc.entry_builders.text`: Expects entry data to be a string. + It will be inserted directly into the entry. Useful for entries with + a free form text. +* `doc.entry_builders.text_and_gallery`: For entries with text and + an optional standard gallery (3 rows, 3:2 aspect ratio). Expects + entry data to be a table with these fields: + * `text`: The entry text + * `images`: The images of the gallery, the format is the same as the + `imagedata` parameter of `doc.widgets.gallery`. Can be `nil`, in + which case no gallery is shown for the entry +* `doc.entry_builders.formspec`: Entry data is expected to contain the + complete entry formspec as a string. Useful if your entries. Useful + if you expect your entries to differ wildly in layouts. + +##### Formspec restrictions +When building your formspec, you have to respect the size limitations. +The help form currently uses a size of 15×10.5 and you must make sure +all entry widgets are inside a boundary box. The remaining space is +reserved for widgets of the help form and should not be used to avoid +overlapping. +Read from the following variables to calculate the final formspec coordinates: + +* `doc.FORMSPEC.WIDTH`: Width of help formspec +* `doc.FORMSPEC.HEIGHT`: Height of help formspec +* `doc.FORMSPEC.ENTRY_START_X`: Leftmost X point of bounding box +* `doc.FORMSPEC.ENTRY_START_Y`: Topmost Y point of bounding box +* `doc.FORMSPEC.ENTRY_END_X`: Rightmost X point of bounding box +* `doc.FORMSPEC.ENTRY_END_Y`: Bottom Y point of bounding box +* `doc.FORMSPEC.ENTRY_WIDTH`: Width of the entry widgets bounding box +* `doc.FORMSPEC.ENTRY_HEIGHT`: Height of the entry widgets bounding box + +Finally, to avoid naming collisions, you must make sure that all identifiers +of your own formspec elements do *not* begin with “`doc_`”. + +##### Receiving formspec events +You can even use the formspec elements you have added with `build_formspec` to +receive formspec events, just like with any other formspec. For receiving, use +the standard function `minetest.register_on_player_receive_fields` to register +your event handling. The `formname` parameter will be `doc:entry`. Use +`doc.get_selection` to get the category ID and entry ID of the entry in question. + +### `doc.add_entry(category_id, entry_id, def)` +Adds a new entry into an existing category. You have to define the category +to which to insert the entry, the entry's identifier, a name and some +data which defines the entry. Note you do not directly define here how the +end result of an entry looks like, this is done by `build_formspec` from +the category definition. + +**Important**: You must call this function *before* any player joins. + +#### Parameters +* `category_id`: Identifier of the category to add the entry into +* `entry_id`: Unique identifier of the new entry, as a string +* `def`: Definition table, it has the following fields: + * `name`: Entry name to be shown in the interface + * `hidden`: (optional) If `true`, entry will not be displayed in entry list + initially (default: `false`); it can be revealed later + * `data`: Arbitrary data attached to the entry. Any data type is allowed; + The data in this field will be used to create the actual formspec + with `build_formspec` from the category definition + +### `doc.set_category_order(category_list)` +Sets the order of categories in the category list. +The help starts with this default order: + + {"basics", "nodes", "tools", "craftitems", "advanced"} + +This function can be called at any time, but it recommended to only call +this function once for the entire server session and to only call it +from subgame mods, to avoid contradictions. If this function is called a +second time by any mod, a warning is written into the log. + +#### Parameters +* `category_list`: List of category IDs in the order they should appear + in the category list. All unspecified categories will be appended to + the end + + +### `doc.show_doc(playername)` +Opens the main help formspec for the player (“Category list” tab). + +#### Parameters +* `playername`: Name of the player to show the formspec to + +### `doc.show_category(playername, category_id)` +Opens the help formspec for the player at the specified category +(“Entry list” tab). + +#### Parameters +* `playername`: Name of the player to show the formspec to +* `category_id`: Category identifier of the selected category + +### `doc.show_entry(playername, category_id, entry_id, ignore_hidden)` +Opens the help formspec for the player showing the specified entry +of a category (“Entry” tab). If the entry is hidden, an error message +is displayed unless `ignore_hidden==true`. + +#### Parameters +* `playername`: Name of the player to show the formspec to +* `category_id`: Category identifier of the selected category +* `entry_id`: Entry identifier of the entry to show +* `ignore_hidden`: (optional) If `true`, shows entry even if it is still hidden + to the player; this will automatically reveal the entry to this player for the + rest of the game + +### `doc.get_category_definition(category_id)` +Returns the definition of the specified category. + +#### Parameters +* `category_id`: Category identifier of the category to the the definition + for + +#### Return value +The category's definition table as specified in the `def` argument of +`doc.add_category`. The table fields are the same. + +### `doc.get_entry_definition(category_id, entry_id)` +Returns the definition of the specified entry. + +#### Parameters +* `category_id`: Category identifier of entry's category +* `entry_id`: Entry identifier of the entry to get the definition for + +#### Return value +The entry's definition table as specified in the `def` argument of +`doc.add_entry`. The table fields are the same. + +### `doc.entry_exists(category_id, entry_id)` +Checks whether the specified entry exists and returns `true` or `false`. +Entry aliases are taken into account. + +#### Parameters +* `category_id`: Category identifier of the category to check +* `entry_id`: Entry identifier of the entry to check for its existence + +#### Return value +Returns `true` if and only if: + +* The specified category exists +* It contains the specified entry + +Otherwise, returns `false`. + +### `doc.entry_viewed(playername, category_id, entry_id)` +Tells whether the specified entry is marked as “viewed” (or read) by +the player. + +#### Parameters +* `playername`: Name of the player to check +* `category_id`: Category identifier of the category to check +* `entry_id`: Entry identifier of the entry to check + +#### Return value +`true`, if entry is viewed, `false` otherwise. + +### `doc.entry_revealed(playername, category_id, entry_id)` +Tells whether the specified entry is marked as “revealed” to the player +and thus visible and accessible to the player. + +#### Parameters +* `playername`: Name of the player to check +* `category_id`: Category identifier of the category to check +* `entry_id`: Entry identifier of the entry to check + +#### Return value +`true`, if entry is revealed, `false` otherwise. + +### `doc.mark_entry_as_viewed(playername, category_id, entry_id)` +Marks a particular entry as “viewed” (or read) by a player. This will +also automatically reveal the entry to the player for the rest of +the game. + +#### Parameters +* `playername`: Name of the player for whom to mark an entry as “viewed” +* `category_id`: Category identifier of the category of the entry to mark +* `entry_id`: Entry identifier of the entry to mark + +### `doc.mark_entry_as_revealed(playername, category_id, entry_id)` +Marks a particular entry as “revealed” to a player. If the entry is +declared as hidden, it will become visible in the list of entries for +this player and will always be accessible with `doc.show_entry`. This +change remains for the rest of the game. + +For entries which are not normally hidden, this function has no direct +effect. + +#### Parameters +* `playername`: Name of the player for whom to reveal the entry +* `category_id`: Category identifier of the category of the entry to reveal +* `entry_id`: Entry identifier of the entry to reveal + +### `doc.mark_all_entries_as_revealed(playername)` +Marks all entries as “revealed” to a player. This change remains for the +rest of the game. + +#### Parameters +* `playername`: Name of the player for whom to reveal the entries + +### `doc.add_entry_alias(category_id_orig, entry_id_orig, category_id_alias, entry_id_orig)` +Adds a single alias for an entry. If an entry has an alias, supplying the +alias to a function which demand `category_id` and `entry_id` will work as expected. +When using this function, you must make sure the category already exists. + +This function could be useful for legacy support after changing an entry ID or +moving an entry to a different category. + +#### Parameters +* `category_id_orig`: Category identifier of the category of the entry in question +* `entry_id_orig`: The original (!) entry identifier of the entry to create an alias + for +* `category_id_alias`: The category ID of the alias +* `entry_id_alias`: The entry ID of the alias + +#### Example + + doc.add_entry_alias("nodes", "test", "craftitems", "test2") + +When calling a function with category ID “craftitems” and entry ID “test2”, it will +act as if you supplied “nodes” as category ID and “test” as entry ID. + +### `doc.get_category_count()` +Returns the number of registered categories. + +#### Return value +Number of registered categories. + +### `doc.get_entry_count(category_id)` +Returns the number of entries in a category. + +#### Parameters +* `category_id`: Category identifier of the category in which to count entries + +#### Return value +Number of entries in the specified category. + +### `doc.get_viewed_count(playername, category_id)` +Returns how many entries have been viewed by a player. + +#### Parameters +* `playername`: Name of the player to count the viewed entries for +* `category_id`: Category identifier of the category in which to count the + viewed entries + +#### Return value +Amount of entries the player has viewed in the specified category. If the +player does not exist, this function returns `nil`. + +### `doc.get_revealed_count(playername, category_id)` +Returns how many entries the player has access to (non-hidden entries) +in this category. + +#### Parameters +* `playername`: Name of the player to count the revealed entries for +* `category_id`: Category identifier of the category in which to count the + revealed entries + +#### Return value +Amount of entries the player has access to in the specified category. If the +player does not exist, this function returns `nil`. + +### `doc.get_hidden_count(playername, category_id)` +Returns how many entries are hidden from the player in this category. + +#### Parameters +* `playername`: Name of the player to count the hidden entries for +* `category_id`: Category identifier of the category in which to count the + hidden entries + +#### Return value +Amount of entries hidden from the player. If the player does not exist, +this function returns `nil`. + +### `doc.get_selection(playername)` +Returns the currently or last viewed entry and/or category of a player. + +#### Parameter +* `playername`: Name of the player to query + +#### Return value +It returns up to 2 values. The first one is the category ID, the second one +is the entry ID of the entry/category which the player is currently viewing +or is the last entry the player viewed in this session. If the player only +viewed a category so far, the second value is `nil`. If the player has not +viewed a category as well, both returned values are `nil`. + + +### `doc.widgets.text(data, x, y, width, height)` +This is a convenience function for creating a special formspec widget. It creates +a widget in which you can insert scrollable multi-line text. + +As of Minetest 0.4.14, this function is only provided because Minetest lacks +native support for such a widget. When Minetest supports such a widget natively, +this function may become just a simple wrapper. + +#### Parameters +* `data`: Text to be written inside the widget +* `x`: Formspec X coordinate (optional) +* `y`: Formspec Y coordinate (optional) +* `width`: Width of the widget in formspec units (optional) +* `height`: Height of the widget in formspec units (optional) + +The default values for the optional parameters result in a widget which fills +nearly the entire entry page. + +#### Return value +Two values are returned, in this order: + +* string: Contains a complete formspec definition building the widget +* string: Formspec element ID of the created widget + +#### Note +If you use this function to build a formspec string, do not use identifiers +beginning with `doc_widget_text` to avoid naming collisions, as this function +makes use of such identifiers internally. + + +### `doc.widgets.gallery(imagedata, playername, x, y, aspect_ratio, width, rows, align_left, align_top)` +This function creates an image gallery which allows you to display an +arbitrary amount of images aligned horizontally. It is possible to add more +images than the space of an entry would normally held, this is done by adding +“scroll” buttons to the left and right which allows the user to see more images +of the gallery. + +This function is useful for adding multiple illustration to your entry without +worrying about space too much. Adding illustrations can help you to create +entry templates which aren't just lengthy walls of text. ;-) + +You can define the position, image aspect ratio, total gallery width and the +number of images displayed at once. You can *not* directly define the image +size, nor the resulting height of the overall gallery, those values will +be derived from the parameters. + +You can only really use this function efficiently inside a *custom* +`build_formspec` function definition. This is because you need to pass a +`playername`. You can currently also only add up to one gallery per entry; +adding more galleries is not supported and will lead to bugs. + +### Parameters +* `imagedata`: List of images to be displayed in the specified order. All images must + have the same aspect ratio. It's a table of tables with this format: + * `imagetype`: Type of image to be used (optional): + * `"image"`: Texture file (default) + * `"item"`: Item image, specified as itemstring + * `image`: What to display. Depending on `imagetype`, a texture file or itemstring +* `playername`: Name of the player who is viewing the entry in question +* `x`: Formspec X coordinate of the top left corner (optional) +* `y`: Formspec Y coordinate of the top left corner (optional) +* `aspect_ratio`: Aspect ratio of all the images (width/height) +* `width`: Total gallery width in formspec units (optional) +* `rows`: Number of images which can be seen at once (optional) +* `align_left`: If `false`, gallery is aligned to the left instead of the right (optional) +* `align_right`: If `false`, gallery is aligned to the bottom instead of the top (optional) + +The default values for the optional parameters result in a gallery with +3 rows which is placed at the top left corner and spans the width of the +entry and assumes an aspect ratio of two thirds. + +If the number of images is greater than `rows`, “scroll” buttons will appear +at the left and right side of the images. + +#### Return values +Two values are returned, in this order: + +* string: Contains a complete formspec definition building the gallery +* number: The height the gallery occupies in the formspec + +## Extending this mod (naming conventions) +If you want to extend this mod with your own functionality, it is recommended +that you put all API functions into `doc.sub.`. +As a naming convention, if you mod *primarily* depends on `doc`, it is recommended +to use a short mod name which starts with “`doc_`”, like `doc_items`, +`doc_minetest_game`, or `doc_identifier`. + +One mod which uses this convention is `doc_items` which uses the `doc.sub.items` +table. + + diff --git a/mods/HELP/doc/doc/README.md b/mods/HELP/doc/doc/README.md new file mode 100644 index 0000000000..bbed7292d2 --- /dev/null +++ b/mods/HELP/doc/doc/README.md @@ -0,0 +1,52 @@ +# Documentation System [`doc`] +This mod provides a simple and highly extensible form in which the user +can access help pages about various things and the modder can add those pages. +The mod itself does not provide any help texts, just the framework. +It is the heart of the Help modpack, on which the other Help mods depend. + +Current version: 1.0.1 + +## For players +### Accessing the help +To open the help, there are multiple ways: + +- Use the `/helpform` chat command. This works always. +- If you use one of these mods, there's a help button in the inventory menu: + - Unified Inventory [`unified_inventory`] + - Simple Fast Inventory Buttons [`sfinv_buttons`] + - Inventory++ [`inventory_plus`] + +The help itself should be more or less self-explanatory. + +This mod is useless on its own, you will only need this mod as a dependency +for mods which actually add some help entries. + +### Hidden entries +Some entries are initially hidden from you. You can't see them until you +unlocked them. Mods can decide for themselves how particular entries are +revealed. Normally you just have to proceed in the game to unlock more +entries. Hidden entries exist to avoid spoilers and give players a small +sense of progress. + +Players with the `help_reveal` privilege can use the `/help_reveal` chat +command to reveal all hidden entries instantly. + +### Maintenance +The information of which player has viewed and revealed which entries is +stored in the world directory in the file `doc.mt`. You can safely reset +the viewed/revealed state of all players by deleting this file. Players +then need to start over revealing all entries. + +## For modders and subgame authors +This mod helps you in creating extensive and flexible help entries for your +mods or subgame. You can write about basically anything in the presentation +you prefer. + +To get started, read `API.md` in the directory of this mod. + +Note: If you want to add help texts for items and nodes, refer to the API +documentation of `doc_items`, instead of manually adding entries. +For custom entities, you may also want to add support for `doc_identifier`. + +## License of everything +MIT License diff --git a/mods/HELP/doc/doc/depends.txt b/mods/HELP/doc/doc/depends.txt new file mode 100644 index 0000000000..4907c641d4 --- /dev/null +++ b/mods/HELP/doc/doc/depends.txt @@ -0,0 +1,5 @@ +intllib? +unified_inventory? +sfinv_buttons? +central_message? +inventory_plus? diff --git a/mods/HELP/doc/doc/description.txt b/mods/HELP/doc/doc/description.txt new file mode 100644 index 0000000000..808a218b11 --- /dev/null +++ b/mods/HELP/doc/doc/description.txt @@ -0,0 +1 @@ +A simple in-game documentation system which enables mods to add help entries based on templates. diff --git a/mods/HELP/doc/doc/init.lua b/mods/HELP/doc/doc/init.lua new file mode 100644 index 0000000000..64e9a504ea --- /dev/null +++ b/mods/HELP/doc/doc/init.lua @@ -0,0 +1,1270 @@ +-- Boilerplate to support localized strings if intllib mod is installed. +local S, F +if minetest.get_modpath("intllib") then + S = intllib.Getter() +else + S = function(s,a,...)a={a,...}return s:gsub("@(%d+)",function(n)return a[tonumber(n)]end)end +end +F = function(f) return minetest.formspec_escape(S(f)) end + +-- Compability for 0.4.14 or earlier +local colorize +if core.colorize then + colorize = core.colorize +else + colorize = function(color, text) return text end +end + +doc = {} + +-- Some informational variables +-- DO NOT CHANGE THEM AFTERWARDS AT RUNTIME! + +-- Version number (follows the SemVer specification 2.0.0) +doc.VERSION = {} +doc.VERSION.MAJOR = 1 +doc.VERSION.MINOR = 0 +doc.VERSION.PATCH = 1 +doc.VERSION.STRING = doc.VERSION.MAJOR.."."..doc.VERSION.MINOR.."."..doc.VERSION.PATCH + +-- Formspec information +doc.FORMSPEC = {} +-- Width of formspec +doc.FORMSPEC.WIDTH = 15 +doc.FORMSPEC.HEIGHT = 10.5 + +--[[ Recommended bounding box coordinates for widgets to be placed in entry pages. Make sure +all entry widgets are completely inside these coordinates to avoid overlapping. ]] +doc.FORMSPEC.ENTRY_START_X = 0 +doc.FORMSPEC.ENTRY_START_Y = 0.5 +doc.FORMSPEC.ENTRY_END_X = doc.FORMSPEC.WIDTH +doc.FORMSPEC.ENTRY_END_Y = doc.FORMSPEC.HEIGHT - 0.5 +doc.FORMSPEC.ENTRY_WIDTH = doc.FORMSPEC.ENTRY_END_X - doc.FORMSPEC.ENTRY_START_X +doc.FORMSPEC.ENTRY_HEIGHT = doc.FORMSPEC.ENTRY_END_Y - doc.FORMSPEC.ENTRY_START_Y + +--TODO: Use container formspec element later + +-- Internal helper variables +local DOC_INTRO = S("This is the help.") + +local COLOR_NOT_VIEWED = "#00FFFF" -- cyan +local COLOR_VIEWED = "#FFFFFF" -- white +local COLOR_HIDDEN = "#999999" -- gray +local COLOR_ERROR = "#FF0000" -- red + +local CATEGORYFIELDSIZE = { + WIDTH = math.ceil(doc.FORMSPEC.WIDTH / 4), + HEIGHT = math.floor(doc.FORMSPEC.HEIGHT-1), +} + +-- Maximum characters per line in the text widget +local TEXT_LINELENGTH = 80 + +doc.data = {} +doc.data.categories = {} +doc.data.aliases = {} +-- Default order (includes categories of other mods from the Docuentation System modpack) +doc.data.category_order = {"basics", "nodes", "tools", "craftitems", "advanced"} +doc.data.category_count = 0 +doc.data.players = {} + +-- Space for additional APIs +doc.sub = {} + +-- Status variables +local set_category_order_was_called = false + +-- Returns the entry definition and true entry ID of an entry, taking aliases into account +local function get_entry(category_id, entry_id) + local category = doc.data.categories[category_id] + local entry + if category ~= nil then + entry = category.entries[entry_id] + end + if category == nil or entry == nil then + local c_alias = doc.data.aliases[category_id] + if c_alias then + local alias = c_alias[entry_id] + if alias then + category_id = alias.category_id + entry_id = alias.entry_id + category = doc.data.categories[category_id] + if category then + entry = category.entries[entry_id] + else + return nil + end + else + return nil + end + else + return nil + end + end + return entry, category_id, entry_id +end + +--[[ Core API functions ]] + +-- Add a new category +function doc.add_category(id, def) + if doc.data.categories[id] == nil and id ~= nil then + doc.data.categories[id] = {} + doc.data.categories[id].entries = {} + doc.data.categories[id].entry_count = 0 + doc.data.categories[id].hidden_count = 0 + doc.data.categories[id].def = def + -- Determine order position + local order_id = nil + for i=1,#doc.data.category_order do + if doc.data.category_order[i] == id then + order_id = i + break + end + end + if order_id == nil then + table.insert(doc.data.category_order, id) + doc.data.categories[id].order_position = #doc.data.category_order + else + doc.data.categories[id].order_position = order_id + end + doc.data.category_count = doc.data.category_count + 1 + return true + else + return false + end +end + +-- Add a new entry +function doc.add_entry(category_id, entry_id, def) + local cat = doc.data.categories[category_id] + if cat ~= nil then + local hidden = def.hidden or (def.hidden == nil and cat.def.hide_entries_by_default) + if hidden then + cat.hidden_count = cat.hidden_count + 1 + def.hidden = hidden + end + cat.entry_count = doc.data.categories[category_id].entry_count + 1 + if def.name == nil or def.name == "" then + minetest.log("warning", "[doc] Nameless entry added. Entry ID: "..entry_id) + end + cat.entries[entry_id] = def + return true + else + return false + end +end + +-- Marks a particular entry as viewed by a certain player, which also +-- automatically reveals it +function doc.mark_entry_as_viewed(playername, category_id, entry_id) + local entry, category_id, entry_id = get_entry(category_id, entry_id) + if not entry then + return + end + if doc.data.players[playername].stored_data.viewed[category_id] == nil then + doc.data.players[playername].stored_data.viewed[category_id] = {} + doc.data.players[playername].stored_data.viewed_count[category_id] = 0 + end + if doc.entry_exists(category_id, entry_id) and doc.data.players[playername].stored_data.viewed[category_id][entry_id] ~= true then + doc.data.players[playername].stored_data.viewed[category_id][entry_id] = true + doc.data.players[playername].stored_data.viewed_count[category_id] = doc.data.players[playername].stored_data.viewed_count[category_id] + 1 + -- Needed because viewed entries get a different color + doc.data.players[playername].entry_textlist_needs_updating = true + end + doc.mark_entry_as_revealed(playername, category_id, entry_id) +end + +-- Marks a particular entry as revealed/unhidden by a certain player +function doc.mark_entry_as_revealed(playername, category_id, entry_id) + local entry, category_id, entry_id = get_entry(category_id, entry_id) + if not entry then + return + end + if doc.data.players[playername].stored_data.revealed[category_id] == nil then + doc.data.players[playername].stored_data.revealed[category_id] = {} + doc.data.players[playername].stored_data.revealed_count[category_id] = doc.get_entry_count(category_id) - doc.data.categories[category_id].hidden_count + end + if doc.entry_exists(category_id, entry_id) and entry.hidden and doc.data.players[playername].stored_data.revealed[category_id][entry_id] ~= true then + doc.data.players[playername].stored_data.revealed[category_id][entry_id] = true + doc.data.players[playername].stored_data.revealed_count[category_id] = doc.data.players[playername].stored_data.revealed_count[category_id] + 1 + -- Needed because a new entry is added to the list of visible entries + doc.data.players[playername].entry_textlist_needs_updating = true + if minetest.get_modpath("central_message") ~= nil then + local cat = doc.data.categories[category_id] + cmsg.push_message_player(minetest.get_player_by_name(playername), S("New help entry unlocked: @1 > @2", cat.def.name, entry.name)) + end + -- To avoid sound spamming, don't play sound more than once per second + local last_sound = doc.data.players[playername].last_reveal_sound + if last_sound == nil or os.difftime(os.time(), last_sound) >= 1 then + -- Play notification sound + minetest.sound_play({ name = "doc_reveal", gain = 0.2 }, { to_player = playername }) + doc.data.players[playername].last_reveal_sound = os.time() + end + end +end + +-- Reveal +function doc.mark_all_entries_as_revealed(playername) + -- Has at least 1 new entry been revealed? + local reveal1 = false + for category_id, category in pairs(doc.data.categories) do + if doc.data.players[playername].stored_data.revealed[category_id] == nil then + doc.data.players[playername].stored_data.revealed[category_id] = {} + doc.data.players[playername].stored_data.revealed_count[category_id] = doc.get_entry_count(category_id) - doc.data.categories[category_id].hidden_count + end + for entry_id, _ in pairs(category.entries) do + if doc.data.players[playername].stored_data.revealed[category_id][entry_id] ~= true then + doc.data.players[playername].stored_data.revealed[category_id][entry_id] = true + doc.data.players[playername].stored_data.revealed_count[category_id] = doc.data.players[playername].stored_data.revealed_count[category_id] + 1 + reveal1 = true + end + end + end + + local msg + if reveal1 then + -- Needed because new entries are added to player's view on entry list + doc.data.players[playername].entry_textlist_needs_updating = true + + msg = S("All help entries revealed!") + + -- Play notification sound (ignore sound limit intentionally) + minetest.sound_play({ name = "doc_reveal", gain = 0.2 }, { to_player = playername }) + doc.data.players[playername].last_reveal_sound = os.time() + else + msg = S("All help entries are already revealed.") + end + -- Notify + if minetest.get_modpath("central_message") ~= nil then + cmsg.push_message_player(minetest.get_player_by_name(playername), msg) + else + minetest.chat_send_player(playername, msg) + end +end + +-- Returns true if the specified entry has been viewed by the player +function doc.entry_viewed(playername, category_id, entry_id) + local entry, category_id, entry_id = get_entry(category_id, entry_id) + if doc.data.players[playername].stored_data.viewed[category_id] == nil then + return false + else + return doc.data.players[playername].stored_data.viewed[category_id][entry_id] == true + end +end + +-- Returns true if the specified entry is hidden from the player +function doc.entry_revealed(playername, category_id, entry_id) + local entry, category_id, entry_id = get_entry(category_id, entry_id) + local hidden = doc.data.categories[category_id].entries[entry_id].hidden + if doc.data.players[playername].stored_data.revealed[category_id] == nil then + return not hidden + else + if hidden then + return doc.data.players[playername].stored_data.revealed[category_id][entry_id] == true + else + return true + end + end +end + +-- Returns category definition +function doc.get_category_definition(category_id) + if doc.data.categories[category_id] == nil then + return nil + end + return doc.data.categories[category_id].def +end + +-- Returns entry definition +function doc.get_entry_definition(category_id, entry_id) + if not doc.entry_exists(category_id, entry_id) then + return nil + end + local entry, _, _ = get_entry(category_id, entry_id) + return entry +end + +-- Opens the main documentation formspec for the player +function doc.show_doc(playername) + if doc.get_category_count() <= 0 then + minetest.show_formspec(playername, "doc:error_no_categories", doc.formspec_error_no_categories()) + return + end + local formspec = doc.formspec_core()..doc.formspec_main(playername) + minetest.show_formspec(playername, "doc:main", formspec) +end + +-- Opens the documentation formspec for the player at the specified category +function doc.show_category(playername, category_id) + if doc.get_category_count() <= 0 then + minetest.show_formspec(playername, "doc:error_no_categories", doc.formspec_error_no_categories()) + return + end + doc.data.players[playername].catsel = nil + doc.data.players[playername].category = category_id + doc.data.players[playername].entry = nil + local formspec = doc.formspec_core(2)..doc.formspec_category(category_id, playername) + minetest.show_formspec(playername, "doc:category", formspec) +end + +-- Opens the documentation formspec for the player showing the specified entry in a category +function doc.show_entry(playername, category_id, entry_id, ignore_hidden) + if doc.get_category_count() <= 0 then + minetest.show_formspec(playername, "doc:error_no_categories", doc.formspec_error_no_categories()) + return + end + local entry, category_id, entry_id = get_entry(category_id, entry_id) + if ignore_hidden or doc.entry_revealed(playername, category_id, entry_id) then + local playerdata = doc.data.players[playername] + playerdata.category = category_id + playerdata.entry = entry_id + + doc.mark_entry_as_viewed(playername, category_id, entry_id) + playerdata.entry_textlist_needs_updating = true + doc.generate_entry_list(category_id, playername) + + playerdata.catsel = playerdata.catsel_list[entry_id] + playerdata.galidx = 1 + + local formspec = doc.formspec_core(3)..doc.formspec_entry(category_id, entry_id, playername) + minetest.show_formspec(playername, "doc:entry", formspec) + else + minetest.show_formspec(playername, "doc:error_hidden", doc.formspec_error_hidden(category_id, entry_id)) + end +end + +-- Returns true if and only if: +-- * The specified category exists +-- * This category contains the specified entry +-- Aliases are taken into account +function doc.entry_exists(category_id, entry_id) + return get_entry(category_id, entry_id) ~= nil +end + +-- Sets the order of categories in the category list +function doc.set_category_order(categories) + local reverse_categories = {} + for cid=1,#categories do + reverse_categories[categories[cid]] = cid + end + doc.data.category_order = categories + for cid, cat in pairs(doc.data.categories) do + if reverse_categories[cid] == nil then + table.insert(doc.data.category_order, cid) + end + end + reverse_categories = {} + for cid=1, #doc.data.category_order do + reverse_categories[categories[cid]] = cid + end + + for cid, cat in pairs(doc.data.categories) do + cat.order_position = reverse_categories[cid] + end + if set_category_order_was_called then + minetest.log("warning", "[doc] doc.set_category_order was called again!") + end + set_category_order_was_called = true +end + +-- Adds an alias for an entry. Attempting to open an entry by an alias name +-- results in opening the entry of the original name. +function doc.add_entry_alias(category_id_orig, entry_id_orig, category_id_alias, entry_id_alias) + if not doc.data.aliases[category_id_alias] then + doc.data.aliases[category_id_alias] = {} + end + doc.data.aliases[category_id_alias][entry_id_alias] = { category_id = category_id_orig, entry_id = entry_id_orig } +end + +-- Returns number of categories +function doc.get_category_count() + return doc.data.category_count +end + +-- Returns number of entries in category +function doc.get_entry_count(category_id) + return doc.data.categories[category_id].entry_count +end + +-- Returns how many entries have been viewed by the player +function doc.get_viewed_count(playername, category_id) + local playerdata = doc.data.players[playername] + if playerdata == nil then + return nil + end + local count = playerdata.stored_data.viewed_count[category_id] + if count == nil then + playerdata.stored_data.viewed[category_id] = {} + count = 0 + playerdata.stored_data.viewed_count[category_id] = count + return count + else + return count + end +end + +-- Returns how many entries have been revealed by the player +function doc.get_revealed_count(playername, category_id) + local playerdata = doc.data.players[playername] + if playerdata == nil then + return nil + end + local count = playerdata.stored_data.revealed_count[category_id] + if count == nil then + playerdata.stored_data.revealed[category_id] = {} + count = doc.get_entry_count(category_id) - doc.data.categories[category_id].hidden_count + playerdata.stored_data.revealed_count[category_id] = count + return count + else + return count + end +end + +-- Returns how many entries are hidden from the player +function doc.get_hidden_count(playername, category_id) + local playerdata = doc.data.players[playername] + if playerdata == nil then + return nil + end + local total = doc.get_entry_count(category_id) + local rcount = playerdata.stored_data.revealed_count[category_id] + if rcount == nil then + return total + else + return total - rcount + end +end + +-- Returns the currently viewed entry and/or category of the player +function doc.get_selection(playername) + local playerdata = doc.data.players[playername] + if playerdata ~= nil then + local cat = playerdata.category + if cat then + local entry = playerdata.entry + if entry then + return cat, entry + else + return cat + end + else + return nil + end + else + return nil + end +end + +-- Template function templates, to be used for build_formspec in doc.add_category +doc.entry_builders = {} + +-- Inserts line breaks into a single paragraph and collapses all whitespace (including newlines) +-- into spaces +local linebreaker_single = function(text, linelength) + if linelength == nil then + linelength = TEXT_LINELENGTH + end + local remain = linelength + local res = {} + local line = {} + local split = function(s) + local res = {} + for w in string.gmatch(s, "%S+") do + res[#res+1] = w + end + return res + end + + for _, word in ipairs(split(text)) do + if string.len(word) + 1 > remain then + table.insert(res, table.concat(line, " ")) + line = { word } + remain = linelength - string.len(word) + else + table.insert(line, word) + remain = remain - (string.len(word) + 1) + end + end + + table.insert(res, table.concat(line, " ")) + return table.concat(res, "\n") +end + +-- Inserts automatic line breaks into an entire text and preserves existing newlines +local linebreaker = function(text, linelength) + local out = "" + for s in string.gmatch(text, "([^\n]*)") do + local l = linebreaker_single(s, linelength) + out = out .. l + if(string.len(l) == 0) then + out = out .. "\n" + end + end + -- Remove last newline + if string.len(out) >= 1 then + out = string.sub(out, 1, string.len(out) - 1) + end + return out +end + +-- Inserts text suitable for a textlist (including automatic word-wrap) +local text_for_textlist = function(text, linelength) + text = linebreaker(text, linelength) + text = minetest.formspec_escape(text) + text = string.gsub(text, "\n", ",") + return text +end + +-- Scrollable freeform text +doc.entry_builders.text = function(data) + local formstring = doc.widgets.text(data, doc.FORMSPEC.ENTRY_START_X, doc.FORMSPEC.ENTRY_START_Y, doc.FORMSPEC.ENTRY_WIDTH - 0.2, doc.FORMSPEC.ENTRY_HEIGHT) + return formstring +end + +-- Scrollable freeform text with an optional standard gallery (3 rows, 3:2 aspect ratio) +doc.entry_builders.text_and_gallery = function(data, playername) + -- How much height the image gallery “steals” from the text widget + local stolen_height = 0 + local formstring = "" + -- Only add the gallery if images are in the data, otherwise, the text widget gets all of the space + if data.images ~= nil then + local gallery + gallery, stolen_height = doc.widgets.gallery(data.images, playername, nil, doc.FORMSPEC.ENTRY_END_Y + 0.2, nil, nil, nil, nil, false) + formstring = formstring .. gallery + end + formstring = formstring .. doc.widgets.text(data.text, + doc.FORMSPEC.ENTRY_START_X, + doc.FORMSPEC.ENTRY_START_Y, + doc.FORMSPEC.ENTRY_WIDTH - 0.2, + doc.FORMSPEC.ENTRY_HEIGHT - stolen_height) + + return formstring +end + +doc.widgets = {} + +local text_id = 1 +-- Scrollable freeform text +doc.widgets.text = function(data, x, y, width, height) + if x == nil then + x = doc.FORMSPEC.ENTRY_START_X + end + if y == nil then + y = doc.FORMSPEC.ENTRY_START_Y + end + if width == nil then + width = doc.FORMSPEC.ENTRY_WIDTH + end + if height == nil then + height = doc.FORMSPEC.ENTRY_HEIGHT + end + local baselength = TEXT_LINELENGTH + local widget_basewidth = doc.FORMSPEC.WIDTH + local linelength = math.max(20, math.floor(baselength * (width / widget_basewidth))) + + local widget_id = "doc_widget_text"..text_id + text_id = text_id + 1 + -- TODO: Wait for Minetest to provide a native widget for scrollable read-only text with automatic line breaks. + -- Currently, all of this had to be hacked into this script manually by using/abusing the table widget + local formstring = "tablecolumns[text]".. + "tableoptions[background=#000000FF;highlight=#000000FF;border=false]".. + "table["..tostring(x)..","..tostring(y)..";"..tostring(width)..","..tostring(height)..";"..widget_id..";"..text_for_textlist(data, linelength).."]" + return formstring, widget_id +end + +-- Image gallery +-- Currently, only one gallery per entry is supported. TODO: Add support for multiple galleries in an entry (low priority) +doc.widgets.gallery = function(imagedata, playername, x, y, aspect_ratio, width, rows, align_left, align_top) + if playername == nil then return nil end -- emergency exit + + local formstring = "" + + -- Defaults + if x == nil then + if align_left == false then + x = doc.FORMSPEC.ENTRY_END_X + else + x = doc.FORMSPEC.ENTRY_START_X + end + end + if y == nil then + if align_top == false then + y = doc.FORMSPEC.ENTRY_END_Y + else + y = doc.FORMSPEC.ENTRY_START_Y + end + end + if width == nil then width = doc.FORMSPEC.ENTRY_WIDTH end + if rows == nil then rows = 3 end + + if align_left == false then + x = x - width + end + + local imageindex = doc.data.players[playername].galidx + doc.data.players[playername].maxgalidx = #imagedata + doc.data.players[playername].galrows = rows + + if aspect_ratio == nil then aspect_ratio = (2/3) end + local pos = 0 + local totalimagewidth, iw, ih + local bw = 0.5 + local buttonoffset = 0 + if #imagedata > rows then + totalimagewidth = width - bw*2 + iw = totalimagewidth / rows + ih = iw * aspect_ratio + if align_top == false then + y = y - ih + end + + local tt + if imageindex > 1 then + formstring = formstring .. "button["..x..","..y..";"..bw..","..ih..";doc_button_gallery_prev;"..F("<").."]" + if rows == 1 then + tt = F("Show previous image") + else + tt = F("Show previous gallery page") + end + formstring = formstring .. "tooltip[doc_button_gallery_prev;"..tt.."]" + end + if (imageindex + rows) <= #imagedata then + local rightx = buttonoffset + (x + rows * iw) + formstring = formstring .. "button["..rightx..","..y..";"..bw..","..ih..";doc_button_gallery_next;"..F(">").."]" + if rows == 1 then + tt = F("Show next image") + else + tt = F("Show next gallery page") + end + formstring = formstring .. "tooltip[doc_button_gallery_next;"..tt.."]" + end + buttonoffset = bw + else + totalimagewidth = width + iw = totalimagewidth / rows + ih = iw * aspect_ratio + if align_top == false then + y = y - ih + end + end + for i=imageindex, math.min(#imagedata, (imageindex-1)+rows) do + local xoffset = buttonoffset + (x + pos * iw) + local nx = xoffset - 0.2 + local ny = y - 0.05 + if imagedata[i].imagetype == "item" then + formstring = formstring .. "item_image["..xoffset..","..y..";"..iw..","..ih..";"..imagedata[i].image.."]" + else + formstring = formstring .. "image["..xoffset..","..y..";"..iw..","..ih..";"..imagedata[i].image.."]" + end + formstring = formstring .. "label["..nx..","..ny..";"..i.."]" + pos = pos + 1 + end + local bw, bh + + return formstring, ih +end + +-- Direct formspec +doc.entry_builders.formspec = function(data) + return data +end + +--[[ Internal stuff ]] + +-- Loading and saving player data +do + local filepath = minetest.get_worldpath().."/doc.mt" + local file = io.open(filepath, "r") + if file then + minetest.log("action", "[doc] doc.mt opened.") + local string = file:read() + io.close(file) + if(string ~= nil) then + local savetable = minetest.deserialize(string) + for name, players_stored_data in pairs(savetable.players_stored_data) do + doc.data.players[name] = {} + doc.data.players[name].stored_data = players_stored_data + end + minetest.debug("[doc] doc.mt successfully read.") + end + end +end + +function doc.save_to_file() + local savetable = {} + savetable.players_stored_data = {} + for name, playerdata in pairs(doc.data.players) do + savetable.players_stored_data[name] = playerdata.stored_data + end + + local savestring = minetest.serialize(savetable) + + local filepath = minetest.get_worldpath().."/doc.mt" + local file = io.open(filepath, "w") + if file then + file:write(savestring) + io.close(file) + minetest.log("action", "[doc] Wrote player data into "..filepath..".") + else + minetest.log("error", "[doc] Failed to write player data into "..filepath..".") + end +end + +minetest.register_on_leaveplayer(function(player) + doc.save_to_file() +end) + +minetest.register_on_shutdown(function() + minetest.log("action", "[doc] Server shuts down. Player data is about to be saved.") + doc.save_to_file() +end) + +--[[ Functions for internal use ]] + +function doc.formspec_core(tab) + if tab == nil then tab = 1 else tab = tostring(tab) end + return "size["..doc.FORMSPEC.WIDTH..","..doc.FORMSPEC.HEIGHT.."]tabheader[0,0;doc_header;".. + minetest.formspec_escape(S("Category list")) .. "," .. + minetest.formspec_escape(S("Entry list")) .. "," .. + minetest.formspec_escape(S("Entry")) .. ";" + ..tab..";true;true]" .. + "bgcolor[#343434FF]" +end + +function doc.formspec_main(playername) + local formstring = "label[0,0;"..minetest.formspec_escape(DOC_INTRO) .. "\n" + if doc.get_category_count() >= 1 then + formstring = formstring .. F("Please select a category you wish to learn more about:").."]" + if doc.get_category_count() <= (CATEGORYFIELDSIZE.WIDTH * CATEGORYFIELDSIZE.HEIGHT) then + local y = 1 + local x = 1 + -- Show all categories in order + for c=1,#doc.data.category_order do + local id = doc.data.category_order[c] + local data = doc.data.categories[id] + local bw = doc.FORMSPEC.WIDTH / math.floor(((doc.data.category_count-1) / CATEGORYFIELDSIZE.HEIGHT)+1) + -- Skip categories which do not exist + if data ~= nil then + -- Category buton + local button = "button["..((x-1)*bw)..","..y..";"..bw..",1;doc_button_category_"..id..";"..minetest.formspec_escape(data.def.name).."]" + local tooltip = "" + -- Optional description + if data.def.description ~= nil then + tooltip = "tooltip[doc_button_category_"..id..";"..minetest.formspec_escape(data.def.description).."]" + end + formstring = formstring .. button .. tooltip + y = y + 1 + if y > CATEGORYFIELDSIZE.HEIGHT then + x = x + 1 + y = 1 + end + end + end + else + formstring = formstring .. "textlist[0,1;"..(doc.FORMSPEC.WIDTH-0.2)..","..(doc.FORMSPEC.HEIGHT-2)..";doc_mainlist;" + for c=1,#doc.data.category_order do + local id = doc.data.category_order[c] + local data = doc.data.categories[id] + formstring = formstring .. minetest.formspec_escape(data.def.name) + if c < #doc.data.category_order then + formstring = formstring .. "," + end + end + local sel = doc.data.categories[doc.data.players[playername].category] + if sel ~= nil then + formstring = formstring .. ";" + formstring = formstring .. doc.data.categories[doc.data.players[playername].category].order_position + end + formstring = formstring .. "]" + formstring = formstring .. "button[0,"..(doc.FORMSPEC.HEIGHT-1)..";3,1;doc_button_goto_category;"..F("Show category").."]" + end + else + formstring = formstring .. "]" + end + return formstring +end + +function doc.formspec_error_no_categories() + local formstring = "size[8,6]textarea[0.25,0;8,6;;" + formstring = formstring .. + minetest.formspec_escape( + colorize(COLOR_ERROR, S("Error: No help available.")) .. "\n\n" .. +S("No categories have been registered, but they are required to provide help.\nThe Documentation System [doc] does not come with help contents on its own, it needs additional mods to add help content. Please make sure such mods are enabled on for this world, and try again.")) .. "\n\n" .. +S("Recommended mods: doc_basics, doc_items, doc_identifier, doc_encyclopedia.") + formstring = formstring .. ";]button_exit[3,5;2,1;okay;"..F("OK").."]" + return formstring +end + +function doc.formspec_error_hidden(category_id, entry_id) + local formstring = "size[8,6]textarea[0.25,0;8,6;;" + formstring = formstring .. minetest.formspec_escape( + colorize(COLOR_ERROR, S("Error: Access denied.")) .. "\n\n" .. + S("Access to the requested entry has been denied; this entry is secret. You may unlock access by progressing in the game. Figure out on your own how to unlock this entry.")) + formstring = formstring .. ";]button_exit[3,5;2,1;okay;"..F("OK").."]" + return formstring +end + +function doc.generate_entry_list(cid, playername) + local formstring + if doc.data.players[playername].entry_textlist == nil + or doc.data.players[playername].catsel_list == nil + or doc.data.players[playername].category ~= cid + or doc.data.players[playername].entry_textlist_needs_updating == true then + local entry_textlist = "textlist[0,1;"..(doc.FORMSPEC.WIDTH-0.2)..","..(doc.FORMSPEC.HEIGHT-2)..";doc_catlist;" + local counter = 0 + doc.data.players[playername].entry_ids = {} + local entries = doc.get_sorted_entry_names(cid) + doc.data.players[playername].catsel_list = {} + for i=1, #entries do + local eid = entries[i] + local edata = doc.data.categories[cid].entries[eid] + if doc.entry_revealed(playername, cid, eid) then + table.insert(doc.data.players[playername].entry_ids, eid) + doc.data.players[playername].catsel_list[eid] = counter + 1 + -- Colorize entries based on viewed status + local viewedprefix = COLOR_NOT_VIEWED + local name = edata.name + if name == nil or name == "" then + name = S("Nameless entry (@1)", eid) + if doc.entry_viewed(playername, cid, eid) then + viewedprefix = "#FF4444" + else + viewedprefix = COLOR_ERROR + end + elseif doc.entry_viewed(playername, cid, eid) then + viewedprefix = COLOR_VIEWED + end + entry_textlist = entry_textlist .. viewedprefix .. minetest.formspec_escape(name) .. "," + counter = counter + 1 + end + end + if counter >= 1 then + entry_textlist = string.sub(entry_textlist, 1, #entry_textlist-1) + end + local catsel = doc.data.players[playername].catsel + if catsel then + entry_textlist = entry_textlist .. ";"..catsel + end + entry_textlist = entry_textlist .. "]" + doc.data.players[playername].entry_textlist = entry_textlist + formstring = entry_textlist + doc.data.players[playername].entry_textlist_needs_updating = false + else + formstring = doc.data.players[playername].entry_textlist + end + return formstring +end + +function doc.get_sorted_entry_names(cid) + local sort_table = {} + local entry_table = {} + local cat = doc.data.categories[cid] + local used_eids = {} + -- Helper function to extract the entry ID out of the output table + local extract = function(entry_table) + local eids = {} + for k,v in pairs(entry_table) do + local eid = v.eid + table.insert(eids, eid) + end + return eids + end + -- Predefined sorting + if cat.def.sorting == "custom" then + for i=1,#cat.def.sorting_data do + local new_entry = table.copy(cat.entries[cat.def.sorting_data[i]]) + new_entry.eid = cat.def.sorting_data[i] + table.insert(entry_table, new_entry) + used_eids[cat.def.sorting_data[i]] = true + end + end + for eid,entry in pairs(cat.entries) do + local new_entry = table.copy(entry) + new_entry.eid = eid + if not used_eids[eid] then + table.insert(entry_table, new_entry) + end + table.insert(sort_table, entry.name) + end + if cat.def.sorting == "custom" then + return extract(entry_table) + else + table.sort(sort_table) + end + local reverse_sort_table = table.copy(sort_table) + for i=1, #sort_table do + reverse_sort_table[sort_table[i]] = i + end + local comp + if cat.def.sorting ~= "nosort" then + -- Sorting by user function + if cat.def.sorting == "function" then + comp = cat.def.sorting_data + -- Alphabetic sorting + elseif cat.def.sorting == "abc" or cat.def.sorting == nil then + comp = function(e1, e2) + if reverse_sort_table[e1.name] < reverse_sort_table[e2.name] then return true else return false end + end + end + table.sort(entry_table, comp) + end + + return extract(entry_table) +end + +function doc.formspec_category(id, playername) + local formstring + if id == nil then + formstring = "label[0,0;"..F("Help > (No Category)") .. "]" + formstring = formstring .. "label[0,0.5;"..F("You haven't chosen a category yet. Please choose one in the category list first.").."]" + formstring = formstring .. "button[0,1;3,1;doc_button_goto_main;"..F("Go to category list").."]" + else + formstring = "label[0,0;"..minetest.formspec_escape(S("Help > @1", doc.data.categories[id].def.name)).."]" + local total = doc.get_entry_count(id) + if total >= 1 then + local revealed = doc.get_revealed_count(playername, id) + if revealed == 0 then + formstring = formstring .. "label[0,0.5;"..F("Currently all entries in this category are hidden from you.\nUnlock new entries by progressing in the game.").."]" + formstring = formstring .. "button[0,1.5;3,1;doc_button_goto_main;"..F("Go to category list").."]" + else + formstring = formstring .. "label[0,0.5;"..F("This category has the following entries:").."]" + formstring = formstring .. doc.generate_entry_list(id, playername) + formstring = formstring .. "button[0,"..(doc.FORMSPEC.HEIGHT-1)..";3,1;doc_button_goto_entry;"..F("Show entry").."]" + formstring = formstring .. "label["..(doc.FORMSPEC.WIDTH-4)..","..(doc.FORMSPEC.HEIGHT-1)..";"..minetest.formspec_escape(S("Number of entries: @1", total)).."\n" + local viewed = doc.get_viewed_count(playername, id) + local hidden = total - revealed + local new = total - viewed - hidden + -- TODO/FIXME: Check if number of hidden/viewed entries is always correct + if viewed < total then + formstring = formstring .. colorize(COLOR_NOT_VIEWED, minetest.formspec_escape(S("New entries: @1", new))) + if hidden > 0 then + formstring = formstring .. "\n" + formstring = formstring .. colorize(COLOR_HIDDEN, minetest.formspec_escape(S("Hidden entries: @1", hidden))).."]" + else + formstring = formstring .. "]" + end + else + formstring = formstring .. F("All entries read.").."]" + end + end + else + formstring = formstring .. "label[0,0.5;"..F("This category is empty.").."]" + formstring = formstring .. "button[0,1.5;3,1;doc_button_goto_main;"..F("Go to category list").."]" + end + end + return formstring +end + +function doc.formspec_entry_navigation(category_id, entry_id) + if doc.get_entry_count(category_id) < 1 then + return "" + end + local formstring = "" + formstring = formstring .. "button["..(doc.FORMSPEC.WIDTH-2)..","..(doc.FORMSPEC.HEIGHT-0.5)..";1,1;doc_button_goto_prev;"..F("<").."]" + formstring = formstring .. "button["..(doc.FORMSPEC.WIDTH-1)..","..(doc.FORMSPEC.HEIGHT-0.5)..";1,1;doc_button_goto_next;"..F(">").."]" + formstring = formstring .. "tooltip[doc_button_goto_prev;"..F("Show previous entry").."]" + formstring = formstring .. "tooltip[doc_button_goto_next;"..F("Show next entry").."]" + return formstring +end + +function doc.formspec_entry(category_id, entry_id, playername) + local formstring + if category_id == nil then + formstring = "label[0,0;"..F("Help > (No Category)") .. "]" + formstring = formstring .. "label[0,0.5;"..F("You haven't chosen a category yet. Please choose one in the category list first.").."]" + formstring = formstring .. "button[0,1;3,1;doc_button_goto_main;"..F("Go to category list").."]" + elseif entry_id == nil then + formstring = "label[0,0;"..minetest.formspec_escape(S("Help > @1 > (No Entry)", doc.data.categories[category_id].def.name)) .. "]" + if doc.get_entry_count(category_id) >= 1 then + formstring = formstring .. "label[0,0.5;"..F("You haven't chosen an entry yet. Please choose one in the entry list first.").."]" + formstring = formstring .. "button[0,1.5;3,1;doc_button_goto_category;"..F("Go to entry list").."]" + else + formstring = formstring .. "label[0,0.5;"..F("This category does not have any entries.").."]" + formstring = formstring .. "button[0,1.5;3,1;doc_button_goto_main;"..F("Go to category list").."]" + end + else + + local category = doc.data.categories[category_id] + local entry = get_entry(category_id, entry_id) + local ename = entry.name + if ename == nil or ename == "" then + ename = S("Nameless entry (@1)", entry_id) + end + formstring = "label[0,0;"..minetest.formspec_escape(S("Help > @1 > @2", category.def.name, ename)).."]" + formstring = formstring .. category.def.build_formspec(entry.data, playername) + formstring = formstring .. doc.formspec_entry_navigation(category_id, entry_id) + end + return formstring +end + +function doc.process_form(player,formname,fields) + local playername = player:get_player_name() + --[[ process clicks on the tab header ]] + if(formname == "doc:main" or formname == "doc:category" or formname == "doc:entry") then + if fields.doc_header ~= nil then + local tab = tonumber(fields.doc_header) + local formspec, subformname, contents + local cid, eid + cid = doc.data.players[playername].category + eid = doc.data.players[playername].entry + if(tab==1) then + contents = doc.formspec_main(playername) + subformname = "main" + elseif(tab==2) then + contents = doc.formspec_category(cid, playername) + subformname = "category" + elseif(tab==3) then + doc.data.players[playername].galidx = 1 + contents = doc.formspec_entry(cid, eid, playername) + if cid ~= nil and eid ~= nil then + doc.mark_entry_as_viewed(playername, cid, eid) + end + subformname = "entry" + end + formspec = doc.formspec_core(tab)..contents + minetest.show_formspec(playername, "doc:" .. subformname, formspec) + return + end + end + if(formname == "doc:main") then + for cid,_ in pairs(doc.data.categories) do + if fields["doc_button_category_"..cid] then + doc.data.players[playername].catsel = nil + doc.data.players[playername].category = cid + doc.data.players[playername].entry = nil + doc.data.players[playername].entry_textlist_needs_updating = true + local formspec = doc.formspec_core(2)..doc.formspec_category(cid, playername) + minetest.show_formspec(playername, "doc:category", formspec) + break + end + end + if fields["doc_mainlist"] then + local event = minetest.explode_textlist_event(fields["doc_mainlist"]) + local cid = doc.data.category_order[event.index] + if cid ~= nil then + if event.type == "CHG" then + doc.data.players[playername].catsel = nil + doc.data.players[playername].category = cid + doc.data.players[playername].entry = nil + doc.data.players[playername].entry_textlist_needs_updating = true + elseif event.type == "DCL" then + doc.data.players[playername].catsel = nil + doc.data.players[playername].category = cid + doc.data.players[playername].entry = nil + doc.data.players[playername].entry_textlist_needs_updating = true + local formspec = doc.formspec_core(2)..doc.formspec_category(cid, playername) + minetest.show_formspec(playername, "doc:category", formspec) + end + end + end + if fields["doc_button_goto_category"] then + local cid = doc.data.players[playername].category + doc.data.players[playername].catsel = nil + doc.data.players[playername].entry = nil + doc.data.players[playername].entry_textlist_needs_updating = true + local formspec = doc.formspec_core(2)..doc.formspec_category(cid, playername) + minetest.show_formspec(playername, "doc:category", formspec) + end + elseif(formname == "doc:category") then + if fields["doc_button_goto_entry"] then + local cid = doc.data.players[playername].category + if cid ~= nil then + local eid = nil + local eids, catsel = doc.data.players[playername].entry_ids, doc.data.players[playername].catsel + if eids ~= nil and catsel ~= nil then + eid = eids[catsel] + end + doc.data.players[playername].galidx = 1 + local formspec = doc.formspec_core(3)..doc.formspec_entry(cid, eid, playername) + minetest.show_formspec(playername, "doc:entry", formspec) + doc.mark_entry_as_viewed(playername, cid, eid) + end + end + if fields["doc_button_goto_main"] then + local formspec = doc.formspec_core(1)..doc.formspec_main(playername) + minetest.show_formspec(playername, "doc:main", formspec) + end + if fields["doc_catlist"] then + local event = minetest.explode_textlist_event(fields["doc_catlist"]) + if event.type == "CHG" then + doc.data.players[playername].catsel = event.index + doc.data.players[playername].entry = doc.data.players[playername].entry_ids[event.index] + doc.data.players[playername].entry_textlist_needs_updating = true + elseif event.type == "DCL" then + local cid = doc.data.players[playername].category + local eid = nil + local eids, catsel = doc.data.players[playername].entry_ids, event.index + if eids ~= nil and catsel ~= nil then + eid = eids[catsel] + end + doc.mark_entry_as_viewed(playername, cid, eid) + doc.data.players[playername].entry_textlist_needs_updating = true + doc.data.players[playername].galidx = 1 + local formspec = doc.formspec_core(3)..doc.formspec_entry(cid, eid, playername) + minetest.show_formspec(playername, "doc:entry", formspec) + end + end + elseif(formname == "doc:entry") then + if fields["doc_button_goto_main"] then + local formspec = doc.formspec_core(1)..doc.formspec_main(playername) + minetest.show_formspec(playername, "doc:main", formspec) + elseif fields["doc_button_goto_category"] then + local formspec = doc.formspec_core(2)..doc.formspec_category(doc.data.players[playername].category, playername) + minetest.show_formspec(playername, "doc:category", formspec) + elseif fields["doc_button_goto_next"] then + if doc.data.players[playername].catsel == nil then return end -- emergency exit + local eids = doc.data.players[playername].entry_ids + local cid = doc.data.players[playername].category + local new_catsel= doc.data.players[playername].catsel + 1 + local new_eid = eids[new_catsel] + if #eids > 1 and new_catsel <= #eids then + doc.mark_entry_as_viewed(playername, cid, new_eid) + doc.data.players[playername].catsel = new_catsel + doc.data.players[playername].entry = new_eid + doc.data.players[playername].galidx = 1 + local formspec = doc.formspec_core(3)..doc.formspec_entry(cid, new_eid, playername) + minetest.show_formspec(playername, "doc:entry", formspec) + end + elseif fields["doc_button_goto_prev"] then + if doc.data.players[playername].catsel == nil then return end -- emergency exit + local eids = doc.data.players[playername].entry_ids + local cid = doc.data.players[playername].category + local new_catsel= doc.data.players[playername].catsel - 1 + local new_eid = eids[new_catsel] + if #eids > 1 and new_catsel >= 1 then + doc.mark_entry_as_viewed(playername, cid, new_eid) + doc.data.players[playername].catsel = new_catsel + doc.data.players[playername].entry = new_eid + doc.data.players[playername].galidx = 1 + local formspec = doc.formspec_core(3)..doc.formspec_entry(cid, new_eid, playername) + minetest.show_formspec(playername, "doc:entry", formspec) + end + elseif fields["doc_button_gallery_prev"] then + local cid, eid = doc.get_selection(playername) + if doc.data.players[playername].galidx - doc.data.players[playername].galrows > 0 then + doc.data.players[playername].galidx = doc.data.players[playername].galidx - doc.data.players[playername].galrows + end + local formspec = doc.formspec_core(3)..doc.formspec_entry(cid, eid, playername) + minetest.show_formspec(playername, "doc:entry", formspec) + elseif fields["doc_button_gallery_next"] then + local cid, eid = doc.get_selection(playername) + if doc.data.players[playername].galidx + doc.data.players[playername].galrows <= doc.data.players[playername].maxgalidx then + doc.data.players[playername].galidx = doc.data.players[playername].galidx + doc.data.players[playername].galrows + end + local formspec = doc.formspec_core(3)..doc.formspec_entry(cid, eid, playername) + minetest.show_formspec(playername, "doc:entry", formspec) + end + else + if fields["doc_inventory_plus"] and minetest.get_modpath("inventory_plus") then + doc.show_doc(playername) + return + end + end +end + +minetest.register_on_player_receive_fields(doc.process_form) + +minetest.register_chatcommand("helpform", { + params = "", + description = S("Open a window providing help entries about Minetest and more"), + privs = {}, + func = function(playername, param) + doc.show_doc(playername) + end, + } +) + +minetest.register_on_joinplayer(function(player) + local playername = player:get_player_name() + local playerdata = doc.data.players[playername] + if playerdata == nil then + -- Initialize player data + doc.data.players[playername] = {} + playerdata = doc.data.players[playername] + -- Gallery index, stores current index of first displayed image in a gallery + playerdata.galidx = 1 + -- Maximum gallery index (index of last image in gallery) + playerdata.maxgalidx = 1 + -- Number of rows in an gallery of the current entry + playerdata.galrows = 1 + -- Table for persistant data + playerdata.stored_data = {} + -- Contains viewed entries + playerdata.stored_data.viewed = {} + -- Count viewed entries + playerdata.stored_data.viewed_count = {} + -- Contains revealed/unhidden entries + playerdata.stored_data.revealed = {} + -- Count revealed entries + playerdata.stored_data.revealed_count = {} + else + -- Completely rebuild viewed and revealed counts from scratch + for cid, cat in pairs(doc.data.categories) do + if playerdata.stored_data.viewed[cid] == nil then + playerdata.stored_data.viewed[cid] = {} + end + if playerdata.stored_data.revealed[cid] == nil then + playerdata.stored_data.revealed[cid] = {} + end + local vc = 0 + local rc = doc.get_entry_count(cid) - doc.data.categories[cid].hidden_count + for eid, entry in pairs(cat.entries) do + if playerdata.stored_data.viewed[cid][eid] then + vc = vc + 1 + playerdata.stored_data.revealed[cid][eid] = true + end + if playerdata.stored_data.revealed[cid][eid] and entry.hidden then + rc = rc + 1 + end + end + playerdata.stored_data.viewed_count[cid] = vc + playerdata.stored_data.revealed_count[cid] = rc + end + end + + -- Add button for Inventory++ + if minetest.get_modpath("inventory_plus") ~= nil then + inventory_plus.register_button(player, "doc_inventory_plus", S("Help")) + end +end) + +---[[ Add buttons for inventory mods ]] +local button_action = function(player) + doc.show_doc(player:get_player_name()) +end + +-- Unified Inventory +if minetest.get_modpath("unified_inventory") ~= nil then + unified_inventory.register_button("doc", { + type = "image", + image = "doc_button_icon_hires.png", + tooltip = S("Help"), + action = button_action, + }) +end + +-- sfinv_buttons +if minetest.get_modpath("sfinv_buttons") ~= nil then + sfinv_buttons.register_button("doc", { + image = "doc_button_icon_lores.png", + tooltip = S("Collection of help texts"), + title = S("Help"), + action = button_action, + }) +end + + +minetest.register_privilege("help_reveal", { + description = S("Allows you to reveal all hidden help entries with /help_reveal"), + give_to_singleplayer = false +}) + +minetest.register_chatcommand("help_reveal", { + params = "", + description = S("Reveal all hidden help entries to you"), + privs = { help_reveal = true }, + func = function(name, param) + doc.mark_all_entries_as_revealed(name) + end, +}) diff --git a/mods/HELP/doc/doc/locale/de.txt b/mods/HELP/doc/doc/locale/de.txt new file mode 100644 index 0000000000..113307ed52 --- /dev/null +++ b/mods/HELP/doc/doc/locale/de.txt @@ -0,0 +1,42 @@ +< = < +> = > +Access to the requested entry has been denied; this entry is secret. You may unlock access by progressing in the game. Figure out on your own how to unlock this entry. = Der Zugriff auf den angeforderten Eintrag wurde verweigert; dieser Eintrag ist geheim. Sie können durch weiteren Spielfortschritt den Zugriff freischalten. Finden Sie selbst heraus, wie Sie diesen Eintrag freischalten können. +All entries read. = Alle Einträge gelesen. +All help entries revealed! = Alle Hilfseinträge aufgedeckt! +All help entries are already revealed. = Alle Hilfseinträge sind schon aufgedeckt. +Allows you to reveal all hidden help entries with /help_reveal = Ermöglicht es Ihnen, alle verborgenen Hilfseinträge mit /help_reveal freizuschalten +Category list = Kategorienliste +Currently all entries in this category are hidden from you.\nUnlock new entries by progressing in the game. = Momentan sind alle Einträge in dieser Kategorie vor Ihnen verborgen.\nSchalten Sie neue Einträge frei, indem Sie im Spiel fortschreiten. +Help = Hilfe +Entry = Eintrag +Entry list = Eintragsliste +Error: Access denied. = Fehler: Zugriff verweigert. +Error: No help available. = Fehler: Keine Hilfe verfügbar. +Go to category list = Zur Kategorienliste +Go to entry list = Zur Eintragsliste +Help > (No Category) = Hilfe > (Keine Kategorie) +Help > @1 = Hilfe > @1 +Help > @1 > @2 = Hilfe > @1 > @2 +Help > @1 > (No Entry) = Hilfe > @1 > (Kein Eintrag) +Hidden entries: @1 = Verborgene Einträge: @1 +New entries: @1 = Neue Einträge: @1 +New help entry unlocked: @1 > @2 = Neuen Hilfseintrag freigeschaltet: @1 > @2 +No categories have been registered, but they are required to provide help.\nThe Documentation System [doc] does not come with help contents on its own, it needs additional mods to add help content. Please make sure such mods are enabled on for this world, and try again. = Es wurden keine Kategorien registriert, aber sie werden benötigt, um die Hilfe anbieten zu können.\nDas Dokumentationssystem [doc] bringt von sich aus keine eigenen Hilfsinhalte mit, es benötigt zusätzliche Mods, um sie hinzuzufügen. Bitte stellen Sie sicher, dass solche Mods für diese Welt aktiviert sind und versuchen Sie es erneut. +Number of entries: @1 = Anzahl der Einträge: @1 +OK = OK +Open a window providing help entries about Minetest and more = Ein Fenster mit Hilfseinträgen über Minetest und mehr öffnen +Please select a category you wish to learn more about: = Bitte wählen Sie eine Kategorie, über die Sie mehr erfahren möchten, aus: +Recommended mods: doc_basics, doc_items, doc_identifier, doc_encyclopedia. = Empfohlene Mods: doc_basics, doc_items, doc_identifier, doc_encyclopedia. +Reveal all hidden help entries to you = Alle für Sie verborgenen Hilfseinträge freischalten +Show entry = Eintrag zeigen +Show category = Kategorie zeigen +Show next entry = Nächsten Eintrag zeigen +Show previous entry = Vorherigen Eintrag zeigen +This category does not have any entries. = Diese Kategorie hat keine Einträge. +This category has the following entries: = Diese Kategorie hat die folgenden Einträge: +This category is empty. = Diese Kategorie ist leer. +This is the help. = Dies ist die Hilfe. +You haven't chosen a category yet. Please choose one in the category list first. = Sie haben noch keine Kategorie gewählt. Bitte wählen Sie zuerst eine Kategorie in der Kategorienliste aus. +You haven't chosen an entry yet. Please choose one in the entry list first. = Sie haben noch keinen Eintrag gewählt. Bitte wählen Sie zuerst einen Eintrag in der Eintragsliste aus. +Nameless entry (@1) = Namenloser Eintrag (@1) +Collection of help texts = Sammlung von Hilfstexten diff --git a/mods/HELP/doc/doc/locale/template.txt b/mods/HELP/doc/doc/locale/template.txt new file mode 100644 index 0000000000..7d852e9a4c --- /dev/null +++ b/mods/HELP/doc/doc/locale/template.txt @@ -0,0 +1,42 @@ +< = +> = +Access to the requested entry has been denied; this entry is secret. You may unlock access by progressing in the game. Figure out on your own how to unlock this entry. = +All entries read. = +All help entries revealed! = +All help entries are already revealed. = +Allows you to reveal all hidden help entries with /help_reveal = +Category list = +Currently all entries in this category are hidden from you.\\nUnlock new entries by progressing in the game. = +Help = +Entry = +Entry list = +Error: Access denied. = +Error: No help available. = +Go to category list = +Go to entry list = +Help > @1 = +Help > @1 > @2 = +Help > @1 > (No Entry) = +Help > (No Category) = +Hidden entries: @1 = +Nameless entry (@1) = +New entries: @1 = +New help entry unlocked: @1 > @2 = +No categories have been registered, but they are required to provide help.\nThe Documentation System [doc] does not come with help contents on its own, it needs additional mods to add help content. Please make sure such mods are enabled on for this world, and try again. = +Number of entries: @1 = +OK = +Open a window providing help entries about Minetest and more = +Please select a category you wish to learn more about: = +Recommended mods: doc_basics, doc_items, doc_identifier, doc_encyclopedia. = +Reveal all hidden help entries to you = +Show entry = +Show category = +Show next entry = +Show previous entry = +This category does not have any entries. = +This category has the following entries: = +This category is empty. = +This is the help. = +You haven't chosen a category yet. Please choose one in the category list first. = +You haven't chosen an entry yet. Please choose one in the entry list first. = +Collection of help texts = diff --git a/mods/HELP/doc/doc/mod.conf b/mods/HELP/doc/doc/mod.conf new file mode 100644 index 0000000000..5e2f430808 --- /dev/null +++ b/mods/HELP/doc/doc/mod.conf @@ -0,0 +1 @@ +name = doc diff --git a/mods/HELP/doc/doc/screenshot.png b/mods/HELP/doc/doc/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..90946a9999d0558787fb1a7d3d6fdbfa56a92fb5 GIT binary patch literal 12315 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 literal 0 HcmV?d00001 diff --git a/mods/HELP/doc/doc/sounds/doc_reveal.ogg b/mods/HELP/doc/doc/sounds/doc_reveal.ogg new file mode 100644 index 0000000000000000000000000000000000000000..3fbe176b6330bfa46b09bedc24d71b37e537e796 GIT binary patch literal 8873 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 literal 0 HcmV?d00001 diff --git a/mods/HELP/doc/doc/textures/doc_button_icon_hires.png b/mods/HELP/doc/doc/textures/doc_button_icon_hires.png new file mode 100644 index 0000000000000000000000000000000000000000..25c9fe4e95cefa9dda2f5acc74b48dcf1ce4c014 GIT binary patch literal 2336 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 Ef@YaF-v9sr literal 0 HcmV?d00001 diff --git a/mods/HELP/doc/doc/textures/inventory_plus_doc_inventory_plus.png b/mods/HELP/doc/doc/textures/inventory_plus_doc_inventory_plus.png new file mode 100644 index 0000000000000000000000000000000000000000..3df6195c7f6e0f317daf6962008c56f782904512 GIT binary patch literal 722 zcmV;@0xkWCP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2jB}5 z5jY04NYAbS00L4;L_t(I%dL~qOH%<9#(#IaU2U6b<|gV~qf%4~Ey)%PjZrWNNstId zVUPX~(o?-jU_JO2>|r%XdfLlgs+UDpv_^``nyH&5ZDQNJyVE&$d$?)NpqDz2=lj0H z;XB_EI9hIx3Frq#0Ozl62=D;`f?oMBT{t2hLJ*!}Yl994G`DK~L=@&{AifByI!w^( z-YgV^DMjBG*U5-<$O13dnbH+Imo%L2Rfi1TN_4G6=s8k@+}TPqw5wG zHK(YotHU<_O;uoP1Lzll5jq@Lj1mA=_ia#1RM}e(>`~Se0O&*!0EXH z)L!T*!;-mz7A)|IK!ErnIeCkeLBed%%vAS^H1ypBptO@mUugg!r4-1?TUsh1pd;v& zQPp9lCt1mElTx%z^2x6(`KMU&Pf^IG55z0kZJ3_ikF=~MF&3G_+9DDS_onTgWQqc6 zb`$y28vu@V^^({XN&QHJe*)6U`vkp@{>w87|N7290hmSd@&JoXkpKVy07*qoM6N<$ Ef@YaF-v9sr literal 0 HcmV?d00001 diff --git a/mods/HELP/doc/doc_identifier/API.md b/mods/HELP/doc/doc_identifier/API.md new file mode 100644 index 0000000000..fb7f88c3d7 --- /dev/null +++ b/mods/HELP/doc/doc_identifier/API.md @@ -0,0 +1,40 @@ +# Minimal API for `doc_identifier` +## Introduction +The tool can identify blocks and players natively, and also handles falling +nodes (`__builtin:falling_node`) and dropped items (`__builtin:item`) on its +own. + +However, the identifier can't “identify” (=open the appropriate entry) custom +objects because the mod doesn't know which help entry to open (if there is +any). One example would be the boat object (`boats:boat`) from the boats mod +in Minetest Game. +If the player tries to use the tool on an unknown object, an error message is +shown. + +Because of this, this mod provides a minimal API for mods to assign a help +entry to an object type: `doc_identifier.register_object`. + +## `doc.sub.identifier.register_object(object_name, category_id, entry_id)` +Registers the object/entity with the internal name `object_name` to the +entry `entry_id` in the category `category_id`. +It is in the modder's responsibility to make sure that both the category and +entry already exist (use `doc.entry_exists` or depend (optionally or not) on +the respective mods) at the time of the function call, otherwise, stability can +not be guaranteed. + +Returns `nil`. + +### Example +From `doc_minetest_game`: + + if minetest.get_modpath("doc_identifier") ~= nil then + doc.sub.identifier.register_object("boats:boat", "craftitems", "boats:boat") + end + +This enables the tool to be used on the boat object itself. The conditional is +an idiom to check for the existence of this mod. + +## Note on dependencies +If you just need `doc.sub.identifier.register_object` using only an **optional** +dependency for your mod is probably enough. + diff --git a/mods/HELP/doc/doc_identifier/README.md b/mods/HELP/doc/doc_identifier/README.md new file mode 100644 index 0000000000..7620be7c53 --- /dev/null +++ b/mods/HELP/doc/doc_identifier/README.md @@ -0,0 +1,29 @@ +# Lookup Tool [`doc_identifier`] +Version: 1.2.1 + +## Description +The lookup tool is an useful little helper which can be used to quickly learn +more about about one's closer environment. It identifies blocks, dropped items +and other objects and it shows extensive information about the item on which it +is used, provided documentation is available. + +## How to use the lookup tool +Punch any block or item about you wish to learn more about. This will open up +the help entry of this particular item. +The tool comes in two modes which are changed by a right-click. In liquid mode +(blue) this tool points to liquids as well while in solid mode (red) this is not +the case. Liquid mode is required if you want to identify a liquid. + +## For modders +If you want the tool to identify nodes and (dropped) items, you probably don't +have to do anything, it is probably already supported. The only thing you have +to make sure is that all pointable blocks and items have a help entry, which +is already the case for most items, but you may want to do some testing on +“tricky” items. Consult the documentation of Documentation System [`doc`] +and Item Help [`doc_items`] for getting the item documentation right. + +For the lookup tool to be able to work on custom objects/entities, you have to +use the tiny API of this mod, see `API.md`. + +## License +Everything in this mod is licensed under the MIT License. diff --git a/mods/HELP/doc/doc_identifier/depends.txt b/mods/HELP/doc/doc_identifier/depends.txt new file mode 100644 index 0000000000..b5ad06b062 --- /dev/null +++ b/mods/HELP/doc/doc_identifier/depends.txt @@ -0,0 +1,5 @@ +doc +doc_items +doc_basics? +default? +intllib? diff --git a/mods/HELP/doc/doc_identifier/description.txt b/mods/HELP/doc/doc_identifier/description.txt new file mode 100644 index 0000000000..8294c740fd --- /dev/null +++ b/mods/HELP/doc/doc_identifier/description.txt @@ -0,0 +1 @@ +Adds a tool which shows help entries about almost anything which it punches. diff --git a/mods/HELP/doc/doc_identifier/init.lua b/mods/HELP/doc/doc_identifier/init.lua new file mode 100644 index 0000000000..5824d47a20 --- /dev/null +++ b/mods/HELP/doc/doc_identifier/init.lua @@ -0,0 +1,190 @@ +-- Boilerplate to support localized strings if intllib mod is installed. +local S +if minetest.get_modpath("intllib") then + S = intllib.Getter() +else + S = function(s) return s end +end + +local doc_identifier = {} + +doc_identifier.registered_objects = {} + +-- API +doc.sub.identifier = {} +doc.sub.identifier.register_object = function(object_name, category_id, entry_id) + doc_identifier.registered_objects[object_name] = { category = category_id, entry = entry_id } +end + +-- END OF API + +doc_identifier.identify = function(itemstack, user, pointed_thing) + local username = user:get_player_name() + local show_message = function(username, itype, param) + local vsize = 2 + local message + if itype == "error_item" then + message = S("No help entry for this item could be found.") + elseif itype == "error_node" then + message = S("No help entry for this block could be found.") + elseif itype == "error_unknown" then + vsize = vsize + 3 + local mod + if param ~= nil then + local colon = string.find(param, ":") + if colon ~= nil and colon > 1 then + mod = string.sub(param,1,colon-1) + end + end + message = S("Error: This node, item or object is undefined. This is always an error.\nThis can happen for the following reasons:\n• The mod which is required for it is not enabled\n• The author of the subgame or a mod has made a mistake") + message = message .. "\n\n" + + if mod ~= nil then + if minetest.get_modpath(mod) ~= nil then + message = message .. string.format(S("It appears to originate from the mod “%s”, which is enabled."), mod) + message = message .. "\n" + else + message = message .. string.format(S("It appears to originate from the mod “%s”, which is not enabled!"), mod) + message = message .. "\n" + end + end + if param ~= nil then + message = message .. string.format(S("Its identifier is “%s”."), param) + end + elseif itype == "error_ignore" then + message = S("This block cannot be identified because the world has not materialized at this point yet. Try again in a few seconds.") + elseif itype == "error_object" or itype == "error_unknown_thing" then + message = S("No help entry for this object could be found.") + elseif itype == "player" then + message = S("This is a player.") + end + minetest.show_formspec( + username, + "doc_identifier:error_missing_item_info", + "size[12,"..vsize..";]" .. + "label[0,0.2;"..minetest.formspec_escape(message).."]" .. + "button_exit[4.5,"..(-0.5+vsize)..";3,1;okay;"..minetest.formspec_escape(S("OK")).."]" + ) + end + if pointed_thing.type == "node" then + local pos = pointed_thing.under + local node = minetest.get_node(pos) + if minetest.registered_nodes[node.name] ~= nil then + local nodedef = minetest.registered_nodes[node.name] + if(node.name == "ignore") then + show_message(username, "error_ignore") + elseif doc.entry_exists("nodes", node.name) then + doc.show_entry(username, "nodes", node.name, true) + else + show_message(username, "error_node") + end + else + show_message(username, "error_unknown", node.name) + end + elseif pointed_thing.type == "object" then + local object = pointed_thing.ref + local le = object:get_luaentity() + if object:is_player() then + if minetest.get_modpath("doc_basics") ~= nil and doc.entry_exists("basics", "players") then + doc.show_entry(username, "basics", "players", true) + else + -- Fallback message + show_message(username, "player") + end + -- luaentity exists + elseif le ~= nil then + local ro = doc_identifier.registered_objects[le.name] + -- Dropped items + if le.name == "__builtin:item" then + local itemstring = ItemStack(minetest.deserialize(le:get_staticdata()).itemstring):get_name() + if doc.entry_exists("nodes", itemstring) then + doc.show_entry(username, "nodes", itemstring, true) + elseif doc.entry_exists("tools", itemstring) then + doc.show_entry(username, "tools", itemstring, true) + elseif doc.entry_exists("craftitems", itemstring) then + doc.show_entry(username, "craftitems", itemstring, true) + elseif minetest.registered_items[itemstring] == nil or itemstring == "unknown" then + show_message(username, "error_unknown", itemstring) + else + show_message(username, "error_item") + end + -- Falling nodes + elseif le.name == "__builtin:falling_node" then + local itemstring = minetest.deserialize(le:get_staticdata()).name + if doc.entry_exists("nodes", itemstring) then + doc.show_entry(username, "nodes", itemstring, true) + end + -- A known registered object + elseif ro ~= nil then + doc.show_entry(username, ro.category, ro.entry, true) + -- Undefined object (error) + elseif minetest.registered_entities[le.name] == nil then + show_message(username, "error_unknown", le.name) + -- Other object (undocumented) + else + show_message(username, "error_object") + end + else + --show_message(username, "error_object") + show_message(username, "error_unknown") + end + elseif pointed_thing.type ~= "nothing" then + show_message(username, "error_unknown_thing") + end + return itemstack +end + +function doc_identifier.solid_mode(itemstack, user, pointed_thing) + return ItemStack("doc_identifier:identifier_solid") +end + +function doc_identifier.liquid_mode(itemstack, user, pointed_thing) + return ItemStack("doc_identifier:identifier_liquid") +end + +minetest.register_tool("doc_identifier:identifier_solid", { + description = S("Lookup tool"), + _doc_items_longdesc = S("This useful little helper can be used to quickly learn more about about one's closer environment. It identifies and analyzes blocks, items and other things and it shows extensive information about the thing on which it is used."), + _doc_items_usagehelp = S("Punch any block, item or other thing about you wish to learn more about. This will open up the appropriate help entry. The tool comes in two modes which are changed by a rightclick. In liquid mode (blue) this tool points to liquids as well while in solid mode (red) this is not the case. Liquid mode is required if you want to identify a liquid."), + _doc_items_hidden = false, + tool_capabilities = {}, + range = 10, + wield_image = "doc_identifier_identifier.png", + inventory_image = "doc_identifier_identifier.png", + liquids_pointable = false, + on_use = doc_identifier.identify, + on_place = doc_identifier.liquid_mode, + on_secondary_use = doc_identifier.liquid_mode, +}) +minetest.register_tool("doc_identifier:identifier_liquid", { + description = S("Lookup tool"), + _doc_items_create_entry = false, + tool_capabilities = {}, + range = 10, + groups = { not_in_creative_inventory = 1, not_in_craft_guide = 1 }, + wield_image = "doc_identifier_identifier_liquid.png", + inventory_image = "doc_identifier_identifier_liquid.png", + liquids_pointable = true, + on_use = doc_identifier.identify, + on_place = doc_identifier.solid_mode, + on_secondary_use = doc_identifier.solid_mode, +}) + +minetest.register_craft({ + output = "doc_identifier:identifier_solid", + recipe = { {"group:stick", "group:stick" }, + {"", "group:stick"}, + {"group:stick", ""} } +}) + +if minetest.get_modpath("default") ~= nil then + minetest.register_craft({ + output = "doc_identifier:identifier_solid", + recipe = { { "default:glass" }, + { "group:stick" } } + }) +end + +minetest.register_alias("doc_identifier:identifier", "doc_identifier:identifier_solid") + +doc.add_entry_alias("tools", "doc_identifier:identifier_solid", "tools", "doc_identifier:identifier_liquid") diff --git a/mods/HELP/doc/doc_identifier/locale/de.txt b/mods/HELP/doc/doc_identifier/locale/de.txt new file mode 100644 index 0000000000..996c369f3c --- /dev/null +++ b/mods/HELP/doc/doc_identifier/locale/de.txt @@ -0,0 +1,13 @@ +Error: This node, item or object is undefined. This is always an error.\nThis can happen for the following reasons:\n• The mod which is required for it is not enabled\n• The author of the subgame or a mod has made a mistake = Fehler: Dieser Node, Gegenstand oder dieses Objekt ist nicht definiert.\nDas ist immer ein Fehler.\nDies kann aus folgenden Gründen passieren:\n• Die Mod, die dafür benötigt wird, ist nicht aktiv\n• Der Subgame-Autor oder ein Mod-Autor machte einen Fehler +It appears to originate from the mod “%s”, which is enabled. = Es scheint von der Mod »%s« zu stammen. Sie ist aktiv. +It appears to originate from the mod “%s”, which is not enabled! = Es scheint von der Mod »%s« zu stammen. Sie ist nicht aktiv! +Its identifier is “%s”. = Der Identifkator ist »%s«. +Lookup tool = Nachschlagewerkzeug +No help entry for this block could be found. = Für diesen Block konnte kein Hilfseintrag gefunden werden. +No help entry for this item could be found. = Für diesen Gegenstand konnte kein Hilfseintrag gefunden werden. +No help entry for this object could be found. = Für dieses Objekt konnte kein Hilfseintrag gefunden werden. +OK = OK +Punch any block, item or other thing about you wish to learn more about. This will open up the appropriate help entry. The tool comes in two modes which are changed by a rightclick. In liquid mode (blue) this tool points to liquids as well while in solid mode (red) this is not the case. Liquid mode is required if you want to identify a liquid. = Schlagen Sie einen beliebigen Block, Gegenstand oder irgendwas, worüber Sie mehr erfahren wollen. Das wird den passenden Hilfseintrag öffnen. Das Werkzeug hat zwei Modi, welcher mit einem Rechtsklick gewechselt werden kann. Im Flüssigmodus (blau) zeigt das Werkzeug auch auf Flüssigkeiten. Im Festmodus (rot) ist das nicht der Fall. Der Flüssigmodis ist notwendig, wenn Sie eine Flüssigkeit identifizieren wollen. +This block cannot be identified because the world has not materialized at this point yet. Try again in a few seconds. = Dieser Block kann nicht identifiziert werden, weil sich die Welt an dieser Stelle noch nicht materialisiert hat. Versuch es in ein paar Sekunden erneut. +This is a player. = Dies ist ein Spieler. +This useful little helper can be used to quickly learn more about about one's closer environment. It identifies and analyzes blocks, items and other things and it shows extensive information about the thing on which it is used. = Dieser nützliche kleine Helfer kann benutzt werden, um schnell etwas über die nähere Umgebung zu erfahren. Er identifiziert und analysiert Blöcke, Gegenstände und andere Dinge und zeigt ausführliche Informationen über all das, worauf man ihn anwendet. diff --git a/mods/HELP/doc/doc_identifier/locale/template.txt b/mods/HELP/doc/doc_identifier/locale/template.txt new file mode 100644 index 0000000000..b0ede61627 --- /dev/null +++ b/mods/HELP/doc/doc_identifier/locale/template.txt @@ -0,0 +1,13 @@ +Error: This node, item or object is undefined. This is always an error.\\nThis can happen for the following reasons:\\n• The mod which is required for it is not enabled\\n• The author of the subgame or a mod has made a mistake = +It appears to originate from the mod “%s”, which is enabled. = +It appears to originate from the mod “%s”, which is not enabled! = +Its identifier is “%s”. = +Lookup tool = +No help entry for this block could be found. = +No help entry for this item could be found. = +No help entry for this object could be found. = +OK = +Punch any block, item or other thing about you wish to learn more about. This will open up the appropriate help entry. The tool comes in two modes which are changed by a rightclick. In liquid mode (blue) this tool points to liquids as well while in solid mode (red) this is not the case. Liquid mode is required if you want to identify a liquid. = +This block cannot be identified because the world has not materialized at this point yet. Try again in a few seconds. = +This is a player. = +This useful little helper can be used to quickly learn more about about one's closer environment. It identifies and analyzes blocks, items and other things and it shows extensive information about the thing on which it is used. = diff --git a/mods/HELP/doc/doc_identifier/mod.conf b/mods/HELP/doc/doc_identifier/mod.conf new file mode 100644 index 0000000000..df963a0cf5 --- /dev/null +++ b/mods/HELP/doc/doc_identifier/mod.conf @@ -0,0 +1 @@ +name = doc_identifier diff --git a/mods/HELP/doc/doc_identifier/screenshot.png b/mods/HELP/doc/doc_identifier/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..13b2540df519067df7d1b4fa7a090f4909a34f90 GIT binary patch literal 28682 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 literal 0 HcmV?d00001 diff --git a/mods/HELP/doc/doc_identifier/textures/doc_identifier_identifier.png b/mods/HELP/doc/doc_identifier/textures/doc_identifier_identifier.png new file mode 100644 index 0000000000000000000000000000000000000000..00b3147a0befecc4dc438364a0b78bb743495fa2 GIT binary patch literal 429 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|I14-?iy0XB zj({-ZRBb+KpdfpRr>`sf0}d7e5mxntw{k!sp3IPl66gHf+|;}hAeVt*&$>-Bfo7?D zx;TbdoK8-V$T;BPad2_puKzqhP`|$}^~cXAip|IVm}*qb;K+=7I`7Pf7ia6+w(qZy z$h+p3zGI@qr}-MoZC|bUEx-Hkq5ofRrv6{;fB)zGf@c^1GrOAp7r$5YP5!2USWTNu z_5aBacV&w(|6i@oqxf3PQ0DaMQUFS+FC773S%9TM|Wo*HZX`FHlfxA*EF151<*pVWVx=KO#9 z{o3#Ui*jH8pSgYhe|LVDH5V-``+n|^{AS2jTwLr{!u#T42jjA1#{d8CQvdk>v8CF7 zPruO1LJ|M!?@s>n|6`8wB~yR@l%M}g^I!dUH~#v6^3-Mf-+BN3pV9WNzD<#x;n0p| Uxob}@h=GF6)78&qol`;+06>Gnp#T5? literal 0 HcmV?d00001 diff --git a/mods/HELP/doc/doc_identifier/textures/doc_identifier_identifier_liquid.png b/mods/HELP/doc/doc_identifier/textures/doc_identifier_identifier_liquid.png new file mode 100644 index 0000000000000000000000000000000000000000..9f3aef969a008d9c6c4c9dc3f204465b7c4b018d GIT binary patch literal 414 zcmV;P0b%}$P)Px#32;bRa{vGi!~g&e!~vBn4jTXf00(qQO+^Rb2m}xmCWQx_YybcOFiAu~R5;7k zlD$g;aU8}!*WJlszEGAL(GV>nL=a6)O|=_NL1s})Lw`YQ4NW1vVT)7K%`Mg#HMNCM z1bTy1l1Ti-i=tp%wU=?kQhcd(AI0HT}@%2|~ju+3HO#{dM zi8(W-L{7(0o-9TUo$Hez(O#9tuG7-Qw>4%KRT-TtlUxWZ?O$)N@r(9SsucOOw2r4_ z*Q?rJ(xk|*NGm(pJ$#VXq&QoZrK1V?t4)jZHCZ|wm;5`u19iS~k;Sb(w*UYD07*qo IM6N<$g5c+>dH?_b literal 0 HcmV?d00001 diff --git a/mods/HELP/doc/doc_items/API.md b/mods/HELP/doc/doc_items/API.md new file mode 100644 index 0000000000..a683771c7a --- /dev/null +++ b/mods/HELP/doc/doc_items/API.md @@ -0,0 +1,357 @@ +# API documentation for `doc_items` +## Introduction +This document explains the API of `doc_items`. It contains a reference of +all functions. + +## Quick start +The most common use case for using this API requires only to set some +hand-written help texts for your items. + +The preferred way is to add the following optional fields to the +item definition when using `minetest.register_node`, etc.: + +* `_doc_items_longdesc`: Long description of this item. + Describe here what this item is, what it is for, its purpose, etc. +* `_doc_items_usagehelp`: Description of *how* this item can be used. + Only set this if needed, e.g. standard mining tools don't need this. + +Example: + + minetest.register_node("example:dice", { + description = "Dice", + _doc_items_longdesc = "A decorative dice which shows the numbers 1-6 on its sides.", + _doc_items_usagehelp = "Right-click the dice to roll it.", + tiles = { "example_dice.png" }, + is_ground_content = false, + --[[ and so on … ]] + }) + +When using this method, your mod does not need additional dependencies. + +See below for some recommendations on writing good help texts. + +If you need more customization, read ahead. ;-) + +## New item fields +This mod adds support for new fields of the item definition. They allow for +easy and quick manipulation of the item help entries. All fields are optional. + +* `_doc_items_longdesc`: Long description +* `_doc_items_usagehelp`: Usage help +* `_doc_items_image`: Entry image (default: inventory image) +* `_doc_items_hidden`: Whether entry is hidden (default: `false` for air and hand, `true` for everything else) +* `_doc_items_create_entry`: Whether to create an entry for this item (default: `true`) +* `_doc_items_entry_name`: The title of the entry. By default, this is the same as the `description` field + of the item. This field is required if the `description` is empty +* `_doc_items_durability`: This field is for describing how long a tool can be used before it breaks. Choose one data type: + * It it is a `number`: Fixed number of uses before it breaks + * If it is a `string`: Free-form text which explains how the durability works. Try to keep it short and only use it if the other types won't work + +A full explanation of these fields is provided below. + +## Concepts +This section explains the core concepts of an item help entry in-depth. + +### Factoids +Basically, a factoid is usually a single sentence telling the player a specific +fact about the item. The task of each factoid is to basically convert parts +of the item definition to useful, readable, understandable text. + +Example: It's a fact that `default:sand` has the group `falling_node=1`. +A factoid for this is basically just a simple conditional which puts the +the sentence “This block is affected to gravity and can fall.” into the +text if the node is member of said group. + +Factoids can be more complex than that. The factoid for node drops needs to +account for different drop types and probabilities, etc. + +`doc_items` has many predefined factoids already. This includes all “special” +groups (like `falling_node`), drops, mining capabilities, punch interval, +and much more. + +Custom factoids can be added with `doc.sub.items.register_factoid`. + +The idea behind factoids is to generate as much information as possible +automatically to reduce redundancy, inconsistencies and the workload of hand- +written descriptions. + +### Long description and usage help +Factoids are not always sufficient to describe an item. This is the case +for facts where the item definition can not be used to automatically +generate texts. Examples: Custom formspecs, ABMs, special tool action +on right-click. + +That's where the long description and usage help comes into play. +Those are two texts which are written manually for a specific item. + +Roughly, the long description is for describing **what** the item is, how it +acts, what it is for. The usage help is for explaining **how** the +item can be used. It is less important for standard mining tools and weapons. + +There is no hard length limit for the long description and the usage help. + +#### Recommendations for long description +The long description should roughly contain the following info: + +* What the item does +* What it is good for +* How it may be generated in the world +* Maybe some background info if you're in a funny mood +* Notable information which doesn't fit elsewhere + +The description should normally **not** contain: + +* Information which is already covered by factoids, like digging groups, + damage, group memberships, etc. +* How the item can be used +* Direct information about other items +* Any other redundant information +* Crafting recipes + +One exception from the rule may be for highlighting the most important +purpose of a simple item, like that coal lumps are primarily used as fuel. + +Sometimes, a long description is not necessary because the item is already +exhaustively explained by factoids. + +For very simple items, consider using one of the template texts (see below). + +Minimal style guide: Use complete sentences. + +#### Recommendations for usage help +The usage help should only be set for items which are in some way special +in their usage. Standard tools and weapons should never have an usage help. + +The rule of thumb is this: If a new player who already knows the Minetest +basics, but not this item, will not directly know how to use this item, +then the usage help should be added. If basic Minetest knowledge or +existing factoids are completely sufficient, usage help should not be added. + +The recommendations for what not to put into the usage help is the same +as for long descriptions. + +#### Template texts +For your convenience, a few template texts are provided for common texts +to avoid redundancy and to increase consistency for simple things. Read +`init.lua` to see the actual texts. + +##### Long description +* `doc.sub.items.temp.build`: For building blocks like the brick block in Minetest Game +* `doc.sub.items.temp.deco`: For other decorational blocks. +* `doc.sub.items.temp.craftitem`: For items solely or almost solely used for crafting + +##### Usage help +* `doc.sub.items.temp.eat`: For eatable items using the `on_use=minetest.item_eat(1)` idiom +* `doc.sub.items.temp.eat_bad`: Same as above, but eating them is considered a bad idea +* `doc.sub.items.temp.rotate_node`: For nodes with `on_place=minetest.rotate_node`, + explains placement and rotation + +### Entry creation +By default, an entry for each item is added automatically, except for items +without a description (`description == nil`). This behaviour can be changed +on a per-item basis. + +By setting the item definition's field `_doc_items_create_entry` to `true` +or `false`you can explicitly define whether this item should get its own +entry. + +Suppressing an entry is useful for items which aren't supposed to be directly +seen or obtained by the player or if they are only used for technical +and/or internal purposes. Another possible reason to suppress an entry is +to scrub the entry list of lots of very similar related items where the +difference is too small to justify two separate entries (e.g. +burning furnace vs inactive furnace, because the gameplay mechanics are +identical for both). + +### Hidden entries +Hidden entries are entries which are not visible in the list of entries. This +concept directly comes from the Documentation System. The entry will still be +created, it is just not selectable by normal means. Players might be able to +“unlock” an entry later. Refer to the API documentation of the Documentation +System to learn more. + +By default, all entries are hidden except air and the hand. + +To mark an entry as hidden, add the field `_doc_items_hidden=true` to its +item definition. To make sure an entry is never hidden, add +`_doc_items_hidden=false` instead (this rarely needs to be specified +explicitly). + +### Hand and air +The mod adds some default help texts for the hand and the air which are +written in a way that they probably are true for most subgames out of the +box, but especially the hand help text is kept intentionally vague. +If you want to change these help texts or the entry names or other +attributes, just add `_doc_items_*` fields to the item definition, either +by re-defining or overwriting these items (e.g. with +`minetest.override_item`). + +In the mod `doc_minetest_game`, the default hand help text is overwritten +to explain the hand in more detail, especially the hand behaviour in +Creative Mode. + +## Functions +This is the reference of all available functions in this API. + +### `doc.sub.items.register_factoid(category_id, factoid_type, factoid_generator)` +Add a custom factoid (see above) for the specified category. + +* `category_id`: The help category for which the factoid applies: + * `"nodes"`: Blocks + * `"tools"`: Tools and weapons + * `"craftitems"`: Misc. items + * `nil`: All of the above +* `factoid_type`: Rough categorization of the factoid's content, used to + optimize the final text display. This currently determines where in the + entry text the factoid appears. Possible values: + * For all items: + * `"use"`: It's about using the item in some way (written right after the fixed usage help) + * `"groups"`: Group-related factoid (very vague) + * `"misc"`: Factoid doesn't fit anywhere else, is shown near the end + * For nodes only: + * `damage`: Related to player/mob damage or health + * `movement`: Related to player movement on, in or at node + * `sound`: Related to node sounds + * `gravity`: Related to gravity (e.g. falling node) + * `drop_destroy`: Related to node being destroyed or node dropping as an item + * `light`: Related to node light (luminance) + * `mining`: Related to mining + * `drops`: Related to node drops +* `factoid_generator`: A function which turns item definition into a string + (see blow) + +#### `factoid_generator(itemstring, def)` +`itemstring` is the itemstring of the item to be documented, and `def` is the +complete item definition table (from Minetest). + +This function must return a helpful string which turns a part of the item's +definition into an useful sentence or text. The text can contain newlines, +but it must not end with a newline. + +This function must **always** return a string. If you don't want to add any text, +return the empty string. + +Style guide: Try to use complete sentences and avoid too many newlines. + +#### Example +This factoid will add the sentence “This block will extinguish nearby fire.” +to all blocks which are member of the group `puts_out_fire`. + + doc.sub.items.register_factoid("nodes", "groups", function(itemstring, def) + if def.groups.puts_out_fire ~= nil then + return "This block will extinguish nearby fire." + else + return "" + end + end) + + +### `doc.sub.items.disable_core_factoid(factoid_name)` +This function removes a core (built-in) factoid entirely, its text will never be displayed in any +entry then. + +#### Parameter +`factoid_name` chooses the factoid you want to disable. The following core factoids can be disabled: + +* `"node_mining"`: Mining properties of nodes +* `"tool_capabilities"`: Tool capabilities such as digging times +* `"groups"`: Group memberships +* `"fuel"`: How long the item burns as a fuel and if there's a leftover +* `"itemstring"`: The itemstring +* `"drops"`: Node drops +* `"connects_to"`: Tells to which nodes the node connects to +* `"light"`: Light and transparency information for nodes +* `"drop_destroy"`: Information about when the node causes to create its “drop” and if it gets destroyed by flooding +* `"gravity"`: Factoid for `falling_node` group +* `"sounds"`: Infos about sound effects related to the item +* `"node_damage"`: Direct damage and drowning damage caused by nodes +* `"node_movement"`: Nodes affecting player movement +* `"liquid"`: Liquid-related infos of a node +* `"basics"`: Collection of many basic factoids: The custom help texts, pointability, collidability, range, stack size, `liquids_pointable`, “punches don't work as usual”. Be careful with this one! + +#### Background +Normally, the core factoids are written in a very general-purpose style, so this function might +not be needed at all. But it might be useful for subgames and mods which radically break with +some of the underlying core assumptions in Minetest. For example, if your mod completely changes +the digging system, the help texts provided by `doc_items` are probably incorrect, so you can +disable `node_mining` and register a custom factoid as a replacement. + +Please do not use this function lightly because it touches the very core of `doc_items`. Try to +understand a core factoid before you consider to disable it. If you think a core factoid is just +broken or buggy in general, please file a bug instead. + + +### `doc.sub.items.add_friendly_group_names(groupnames)` +Use this function so set some more readable group names to show them +in the formspec, since the internal group names are somewhat cryptic +to players. + +`groupnames` is a table where the keys are the “internal” group names and +the values are the group names which will be actually shown in the +Documentation System. + +***Note***: This function is mostly there to work around a problem in +Minetest as it does not support “friendly” group names, which means exposing +groups to an interface is not pretty. Therefore, this function may be +deprecated when Minetest supports such a thing. + +### `doc.sub.items.get_group_name(internal_group_name)` +Returns the group name of the specified group as displayed by this mod. +If the setting `doc_items_friendly_group_names` is `true`, this might +return a “friendly” group name (see above). If no friendly group name +exists, `internal_group_name` is returned. +If `doc_items_friendly_group_names` is `false`, the argument is always +returned. + +### `doc.sub.items.add_notable_groups(groupnames)` +Add a list of groups you think are notable enough to be mentioned in the +“This item belongs to the following groups: (…)” factoid. This factoid +is intended to give a quick rundown of misc. groups which don't fit +to other factoids, yet they are still somewhat relevant to gameplay. + +`groupnames` is a table of group names you wish to add. + +#### What groups should be added +What is “notable” is subjective, but there are some guidelines: + +Do add a group if: + +* It is used in an ABM +* It is used for a custom interaction with another item +* It is simple enough for the player to know an item is member of this group +* You want to refer to this group in help texts +* The “don'ts” below don't apply + +Do not add a group if: + +* It is *only* used for crafting, `connects_to`, mining times or damage groups +* A factoid covering this group already exists +* The group membership itself requires an explanation (consider writing a factoid instead) +* The group has no gameplay relevance +* Group rating is important to gameplay (consider writing a factoid instead) + +Groups which are used for crafting or in the `connects_to` field of item +definitions are already automatically added to this factoid. + +##### Examples for good additions + +* A group where its members can be placed in bookshelves. + so this group meets the “custom interaction” criterion +* `water` in Minetest Game: Used for water nodes in the obsidian ABM +* `sand` in Minetest Game: Used for the cactus growth ABM, but also crafting. + Since it is not *only* used for crafting, it is OK to be added + +##### Examples for bad additions + +* `stick` in Minetest Game: This group appears in many crafting recipes and + has no other use. It is already added automatically +* A group in which members turn into obsidian when they touch water (ABM): + This group is not trivial and should be introduced in a factoid instead +* `cracky` in Min +* `dig_immediate`: This group is already covered by the default factoids of this + mod + +## Dependencies +If you only add the custom fields to your items, you do *not* need to depend +on this mod. If you use anything else from this mod (e.g. a function), you +probably *do* need to depend (optionally or mandatorily) on this mod. diff --git a/mods/HELP/doc/doc_items/README.md b/mods/HELP/doc/doc_items/README.md new file mode 100644 index 0000000000..d18a62ea94 --- /dev/null +++ b/mods/HELP/doc/doc_items/README.md @@ -0,0 +1,68 @@ +# Item Help [`doc_items`] (Version 1.1.0) +## Description +Automatically generated help texts of blocks, tools, weapons, crafting +items and other items. + +The goal is to tell the player as much about basically almost all items as +possible, making it very convenient to look up simple things. + +The ultimate goal of this mod is that eventually all relevant items have +a complete in-game documentation so no item leaves you confused. + +This mod is useful to learn the hard facts about practically all items, like +how much damage weapon XYZ deals or whether you can dig that block. +This mod does *not* give you long explanations about how to use certain +nontrivial things, like the furnace from Minetest Game. This info might be +provided by other mods and insert it into the help. + +This mod provides 3 new help categories (using the +Documentation System [`doc`]): + +* Blocks (e.g. dirt, stone, wooden stair) +* Tools and weapons (e.g. wooden pickaxe, steel sword, screwdriver) +* Misc. items (e.g. dye, stick, flour) + +Entries are automatically added. The information in the entries is +mostly automatically generated. It contains information about a wide range +of topics: + +* Blocks + * Physics + * Digging properties + * Drops (including probabilities) + * Liquid information + * Pointability + * Luminance + * Much more +* Tools and weapons + * Mining capabilities + * Damage + * Durability + * More +* All items + * Range + * Stack size + * Group memberships + * Other information added by mods + +This mod also allows for mods to adding custom written description +and usage help texts in free-form and even custom automatically generated texts +for mod-specific information like flammability in Minetest Game. This is +one of the core features of this mod; the mod relies on other mods to +provide their custom help texts for a complete help. + +If you find a particular item which is lacking an explanation on usage, +request the mod author to add `doc_items` support. + +## API +This mod has an API so that modders can add their own custom help texts, +custom factoids (single pieces of information extracted from the +item definition) and more. + +For example, if your mods have some complex items which need +explanation, this mod can help you in adding help texts for them. + +Read `API.md` to learn more. + +## License +Everything in this mod is licensed under the MIT License. diff --git a/mods/HELP/doc/doc_items/depends.txt b/mods/HELP/doc/doc_items/depends.txt new file mode 100644 index 0000000000..31aa390349 --- /dev/null +++ b/mods/HELP/doc/doc_items/depends.txt @@ -0,0 +1,2 @@ +doc +intllib? diff --git a/mods/HELP/doc/doc_items/description.txt b/mods/HELP/doc/doc_items/description.txt new file mode 100644 index 0000000000..7291077e6d --- /dev/null +++ b/mods/HELP/doc/doc_items/description.txt @@ -0,0 +1 @@ +Adds automatically generated help texts for items. diff --git a/mods/HELP/doc/doc_items/init.lua b/mods/HELP/doc/doc_items/init.lua new file mode 100644 index 0000000000..1776fdf9fc --- /dev/null +++ b/mods/HELP/doc/doc_items/init.lua @@ -0,0 +1,1406 @@ +-- Boilerplate to support localized strings if intllib mod is installed. +local S +if minetest.get_modpath("intllib") then + S = intllib.Getter() +else + S = function(s,a,...)a={a,...}return s:gsub("@(%d+)",function(n)return a[tonumber(n)]end)end +end + +doc.sub.items = {} + +-- Template texts +doc.sub.items.temp = {} +doc.sub.items.temp.deco = S("This is a decorational block.") +doc.sub.items.temp.build = S("This block is a building block for creating various buildings.") +doc.sub.items.temp.craftitem = S("This item is primarily used for crafting other items.") + +doc.sub.items.temp.eat = S("Hold it in your hand, then leftclick to eat it.") +doc.sub.items.temp.eat_bad = S("Hold it in your hand, then leftclick to eat it. But why would you want to do this?") +doc.sub.items.temp.rotate_node = S("This block's rotation is affected by the way you place it: Place it on the floor or ceiling for a vertical orientation; place it at the side for a horizontal orientation. Sneaking while placing it leads to a perpendicular orientation instead.") + +doc.sub.items.settings = {} +doc.sub.items.settings.friendly_group_names = false +local setting = minetest.setting_getbool("doc_items_friendly_group_names") +if setting ~= nil then + doc.sub.items.settings.friendly_group_names = setting +end +doc.sub.items.settings.itemstring = false +setting = minetest.setting_getbool("doc_items_show_itemstrings") +if setting ~= nil then + doc.sub.items.settings.itemstring = setting +end + +-- Local stuff +local groupdefs = {} +local mininggroups = {} +local miscgroups = {} +local item_name_overrides = { + [""] = S("Hand"), + ["air"] = S("Air") +} +local suppressed = { + ["ignore"] = true, +} + +-- This table contains which of the builtin factoids must NOT be displayed because +-- they have been disabled by a mod +local forbidden_core_factoids = {} + +-- Helper functions +local yesno = function(bool) + if bool==true then return S("Yes") + elseif bool==false then return S("No") + else return "N/A" end +end + +local groups_to_string = function(grouptable, filter) + local gstring = "" + local groups_count = 0 + for id, value in pairs(grouptable) do + if (filter == nil or filter[id] == true) then + -- Readable group name + if groups_count > 0 then + -- List seperator + gstring = gstring .. S(", ") + end + if groupdefs[id] ~= nil and doc.sub.items.settings.friendly_group_names == true then + gstring = gstring .. groupdefs[id] + else + gstring = gstring .. id + end + groups_count = groups_count + 1 + end + end + if groups_count == 0 then + return nil, 0 + else + return gstring, groups_count + end +end + +-- Replaces all newlines with spaces +local scrub_newlines = function(text) + local new, x = string.gsub(text, "\n", " ") + return new +end + +--[[ Append a newline to text, unless it already ends with a newline. ]] +local newline = function(text) + if string.sub(text, #text, #text) == "\n" or text == "" then + return text + else + return text .. "\n" + end +end + +--[[ Make sure the text ends with two newlines by appending any missing newlines at the end, if neccessary. ]] +local newline2 = function(text) + if string.sub(text, #text-1, #text) == "\n\n" or text == "" then + return text + elseif string.sub(text, #text, #text) == "\n" then + return text .. "\n" + else + return text .. "\n\n" + end +end + + +-- Extract suitable item description for formspec +local description_for_formspec = function(itemstring) + if minetest.registered_items[itemstring] == nil then + -- Huh? The item doesn't exist for some reason. Better give a dummy string + minetest.log("warning", "[doc] Unknown item detected: "..tostring(itemstring)) + return S("Unknown item (@1)", tostring(itemstring)) + end + local description = minetest.registered_items[itemstring].description + if description == nil or description == "" then + return minetest.formspec_escape(itemstring) + else + return minetest.formspec_escape(scrub_newlines(description)) + end +end + +local get_entry_name = function(itemstring) + local def = minetest.registered_items[itemstring] + if def._doc_items_entry_name ~= nil then + return def._doc_items_entry_name + elseif item_name_overrides[itemstring] ~= nil then + return item_name_overrides[itemstring] + else + return def.description + end +end + +doc.sub.items.get_group_name = function(groupname) + if groupdefs[groupname] ~= nil and doc.sub.items.settings.friendly_group_names == true then + return groupdefs[groupname] + else + return groupname + end +end + +local burntime_to_text = function(burntime) + if burntime == nil then + return S("unknown") + elseif burntime == 1 then + return S("1 second") + else + return S("@1 seconds", burntime) + end +end + +--[[ Convert tool capabilities to readable text. Extracted information: +* Mining capabilities +* Durability (when mining +* Full punch interval +* Damage groups +]] +local factoid_toolcaps = function(tool_capabilities, check_uses) + if forbidden_core_factoids.tool_capabilities then + return "" + end + + local formstring = "" + if check_uses == nil then check_uses = false end + if tool_capabilities ~= nil and tool_capabilities ~= {} then + local groupcaps = tool_capabilities.groupcaps + if groupcaps ~= nil then + local miningcapstr = "" + local miningtimesstr = "" + local miningusesstr = "" + local caplines = 0 + local timelines = 0 + local useslines = 0 + for k,v in pairs(groupcaps) do + -- Mining capabilities + local minrating, maxrating + if v.times then + for rating, time in pairs(v.times) do + if minrating == nil then minrating = rating else + if minrating > rating then minrating = rating end + end + if maxrating == nil then maxrating = rating else + if maxrating < rating then maxrating = rating end + end + end + else + minrating = 1 + maxrating = 1 + end + local maxlevel = v.maxlevel + if not maxlevel then + -- Default from tool.h + maxlevel = 1 + end + miningcapstr = miningcapstr .. S("• @1: @2", doc.sub.items.get_group_name(k), maxlevel) + miningcapstr = miningcapstr .. "\n" + caplines = caplines + 1 + + for rating=3, 1, -1 do + if v.times ~= nil and v.times[rating] ~= nil then + local maxtime = v.times[rating] + local mintime + local mintimestr, maxtimestr + local maxlevel_calc = maxlevel + if maxlevel_calc < 1 then + maxlevel_calc = 1 + end + mintime = maxtime / maxlevel_calc + mintimestr = string.format("%.1f", mintime) + maxtimestr = string.format("%.1f", maxtime) + if mintimestr ~= maxtimestr then + miningtimesstr = miningtimesstr .. + S("• @1, rating @2: @3 s - @4 s", + doc.sub.items.get_group_name(k), rating, + mintimestr, maxtimestr) + else + miningtimesstr = miningtimesstr .. + S("• @1, rating @2: @3 s", + doc.sub.items.get_group_name(k), rating, + mintimestr) + end + miningtimesstr = miningtimesstr.. "\n" + timelines = timelines + 1 + end + end + + -- Number of mining uses + local base_uses = v.uses + if not base_uses then + -- Default from tool.h + base_uses = 20 + end + if check_uses and base_uses > 0 then + for level=0, maxlevel do + local real_uses = base_uses * math.pow(3, maxlevel - level) + if real_uses < 65535 then + miningusesstr = miningusesstr .. S("• @1, level @2: @3 uses", doc.sub.items.get_group_name(k), level, real_uses) + else + miningusesstr = miningusesstr .. S("• @1, level @2: Unlimited", doc.sub.items.get_group_name(k), level) + end + miningusesstr = miningusesstr .. "\n" + useslines = useslines + 1 + end + end + end + if caplines > 0 then + formstring = formstring .. S("This tool is capable of mining.") .. "\n" + formstring = formstring .. S("Maximum toughness levels:") .. "\n" + formstring = formstring .. miningcapstr + formstring = newline(formstring) + end + if timelines > 0 then + formstring = formstring .. S("Mining times:") .. "\n" + formstring = formstring .. miningtimesstr + end + if useslines > 0 then + formstring = formstring .. S("Mining durability:") .. "\n" + formstring = formstring .. miningusesstr + end + if caplines > 0 or useslines > 0 or timelines > 0 then + formstring = newline2(formstring) + end + end + + -- Weapon data + local damage_groups = tool_capabilities.damage_groups + if damage_groups ~= nil then + formstring = formstring .. S("This is a melee weapon which deals damage by punching.") .. "\n" + -- Damage groups + formstring = formstring .. S("Maximum damage per hit:") .. "\n" + for k,v in pairs(damage_groups) do + formstring = formstring .. S("• @1: @2 HP", doc.sub.items.get_group_name(k), v) + formstring = formstring .. "\n" + end + + -- Full punch interval + local punch = 1.0 + if tool_capabilities.full_punch_interval ~= nil then + punch = tool_capabilities.full_punch_interval + end + formstring = formstring .. S("Full punch interval: @1 s", string.format("%.1f", punch)) + formstring = formstring .. "\n" + end + + end + return formstring +end + +--[[ Factoid for the mining times properties of a node. Extracted infos: +- dig_immediate group +- Digging times/groups +- level group +]] +local factoid_mining_node = function(data) + if forbidden_core_factoids.node_mining then + return "" + end + + local datastring = "" + if data.def.pointable ~= false and (data.def.liquid_type == "none" or data.def.liquid_type == nil) then + -- Check if there are no mining groups at all + local nogroups = true + for groupname,_ in pairs(mininggroups) do + if data.def.groups[groupname] ~= nil or groupname == "dig_immediate" then + nogroups = false + break + end + end + -- dig_immediate + if data.def.drop ~= "" then + if data.def.groups.dig_immediate == 2 then + datastring = datastring .. S("This block can be mined by any mining tool in half a second.").."\n" + elseif data.def.groups.dig_immediate == 3 then + datastring = datastring .. S("This block can be mined by any mining tool immediately.").."\n" + -- Note: “unbreakable” is an unofficial group for undiggable blocks + elseif data.def.diggable == false or nogroups or data.def.groups.immortal == 1 or data.def.groups.unbreakable == 1 then + datastring = datastring .. S("This block can not be mined by ordinary mining tools.").."\n" + end + else + if data.def.groups.dig_immediate == 2 then + datastring = datastring .. S("This block can be destroyed by any mining tool in half a second.").."\n" + elseif data.def.groups.dig_immediate == 3 then + datastring = datastring .. S("This block can be destroyed by any mining tool immediately.").."\n" + elseif data.def.diggable == false or nogroups or data.def.groups.immortal == 1 or data.def.groups.unbreakable == 1 then + datastring = datastring .. S("This block can not be destroyed by ordinary mining tools.").."\n" + end + end + -- Expose “ordinary” mining groups (crumbly, cracky, etc.) and level group + -- Skip this for immediate digging to avoid redundancy + if data.def.groups.dig_immediate ~= 3 then + local mstring = S("This block can be mined by mining tools which match any of the following mining ratings and its toughness level.").."\n" + mstring = mstring .. S("Mining ratings:").."\n" + local minegroupcount = 0 + for group,_ in pairs(mininggroups) do + local rating = data.def.groups[group] + if rating ~= nil then + mstring = mstring .. S("• @1: @2", doc.sub.items.get_group_name(group), rating).."\n" + minegroupcount = minegroupcount + 1 + end + end + local level = data.def.groups.level + if not level then + level = 0 + end + mstring = mstring .. S("Toughness level: @1", level).."\n" + + if minegroupcount > 0 then + datastring = datastring .. mstring + end + end + end + return datastring +end + +-- Pointing range of itmes +local range_factoid = function(itemstring, def) + local handrange = minetest.registered_items[""].range + local itemrange = def.range + if itemstring == "" then + if handrange ~= nil then + return S("Range: @1", itemrange) + else + return S("Range: 4") + end + else + if handrange == nil then handrange = 4 end + if itemrange ~= nil then + return S("Range: @1", itemrange) + else + return S("Range: @1 (@2)", get_entry_name(""), handrange) + end + end +end + +-- Smelting fuel factoid +local factoid_fuel = function(itemstring, ctype) + if forbidden_core_factoids.fuel then + return "" + end + + local formstring = "" + local result, decremented = minetest.get_craft_result({method = "fuel", items = {itemstring}}) + if result ~= nil and result.time > 0 then + local base + local burntext = burntime_to_text(result.time) + if ctype == "tools" then + base = S("This tool can serve as a smelting fuel with a burning time of @1.", burntext) + elseif ctype == "nodes" then + base = S("This block can serve as a smelting fuel with a burning time of @1.", burntext) + else + base = S("This item can serve as a smelting fuel with a burning time of @1.", burntext) + end + formstring = formstring .. base + local replaced = decremented.items[1]:get_name() + if not decremented.items[1]:is_empty() and replaced ~= itemstring then + formstring = formstring .. S(" Using it as fuel turns it into: @1.", description_for_formspec(replaced)) + end + formstring = newline(formstring) + end + return formstring +end + +-- Shows the itemstring of an item +local factoid_itemstring = function(itemstring, playername) + if forbidden_core_factoids.itemstring then + return "" + end + + local privs = minetest.get_player_privs(playername) + if doc.sub.items.settings.itemstring or (privs.give or privs.debug) then + return S("Itemstring: \"@1\"", itemstring) + else + return "" + end +end + +local entry_image = function(data) + local formstring = "" + -- No image for air + if data.itemstring ~= "air" then + -- Hand + if data.itemstring == "" then + formstring = formstring .. "image["..(doc.FORMSPEC.ENTRY_END_X-1)..","..doc.FORMSPEC.ENTRY_START_Y..";1,1;".. + minetest.registered_items[""].wield_image.."]" + -- Other items + elseif data.image ~= nil then + formstring = formstring .. "image["..(doc.FORMSPEC.ENTRY_END_X-1)..","..doc.FORMSPEC.ENTRY_START_Y..";1,1;"..data.image.."]" + else + formstring = formstring .. "item_image["..(doc.FORMSPEC.ENTRY_END_X-1)..","..doc.FORMSPEC.ENTRY_START_Y..";1,1;"..data.itemstring.."]" + end + end + return formstring +end + +-- Stuff for factoids +local factoid_generators = {} +factoid_generators.nodes = {} +factoid_generators.tools = {} +factoid_generators.craftitems = {} + +--[[ Returns a list of all registered factoids for the specified category and type +* category_id: Identifier of the Documentation System category in which the factoid appears +* factoid_type: If set, oly returns factoid with a matching factoid_type. + If nil, all factoids for this category will be generated +* data: Entry data to parse ]] +local factoid_custom = function(category_id, factoid_type, data) + local ftable = factoid_generators[category_id] + local datastring = "" + -- Custom factoids are inserted here + for i=1,#ftable do + if factoid_type == nil or ftable[i].ftype == factoid_type then + datastring = datastring .. ftable[i].fgen(data.itemstring, data.def) + if datastring ~= "" then + datastring = newline(datastring) + end + end + end + return datastring +end + +-- Shows core information shared by all items, to be inserted at the top +local factoids_header = function(data, ctype) + local datastring = "" + if not forbidden_core_factoids.basics then + + local longdesc = data.longdesc + local usagehelp = data.usagehelp + if longdesc ~= nil then + datastring = datastring .. S("Description: @1", longdesc) + datastring = newline2(datastring) + end + if usagehelp ~= nil then + datastring = datastring .. S("Usage help: @1", usagehelp) + datastring = newline2(datastring) + end + datastring = datastring .. factoid_custom(ctype, "use", data) + datastring = newline2(datastring) + + if data.itemstring ~= "" then + datastring = datastring .. S("Maximum stack size: @1", data.def.stack_max) + datastring = newline(datastring) + end + datastring = datastring .. range_factoid(data.itemstring, data.def) + + datastring = newline2(datastring) + + if data.def.liquids_pointable == true then + if ctype == "nodes" then + datastring = datastring .. S("This block points to liquids.").."\n" + elseif ctype == "tools" then + datastring = datastring .. S("This tool points to liquids.").."\n" + elseif ctype == "craftitems" then + datastring = datastring .. S("This item points to liquids.").."\n" + end + end + if data.def.on_use ~= nil then + if ctype == "nodes" then + datastring = datastring .. S("Punches with this block don't work as usual; melee combat and mining are either not possible or work differently.").."\n" + elseif ctype == "tools" then + datastring = datastring .. S("Punches with this tool don't work as usual; melee combat and mining are either not possible or work differently.").."\n" + elseif ctype == "craftitems" then + datastring = datastring .. S("Punches with this item don't work as usual; melee combat and mining are either not possible or work differently.").."\n" + end + end + + end + + datastring = newline(datastring) + + -- Show tool capability stuff, including durability if not overwritten by custom field + local check_uses = false + if ctype == "tools" then + check_uses = data.def._doc_items_durability == nil + end + datastring = datastring .. factoid_toolcaps(data.def.tool_capabilities, check_uses) + datastring = newline2(datastring) + + return datastring +end + +-- Shows less important information shared by all items, to be inserted at the bottom +local factoids_footer = function(data, playername, ctype) + local datastring = "" + datastring = datastring .. factoid_custom(ctype, "groups", data) + datastring = newline2(datastring) + + -- Show other “exposable” groups + if not forbidden_core_factoids.groups then + local gstring, gcount = groups_to_string(data.def.groups, miscgroups) + if gstring ~= nil then + if gcount == 1 then + if ctype == "nodes" then + datastring = datastring .. S("This block belongs to the @1 group.", gstring) .. "\n" + elseif ctype == "tools" then + datastring = datastring .. S("This tool belongs to the @1 group.", gstring) .. "\n" + elseif ctype == "craftitems" then + datastring = datastring .. S("This item belongs to the @1 group.", gstring) .. "\n" + end + else + if ctype == "nodes" then + datastring = datastring .. S("This block belongs to these groups: @1.", gstring) .. "\n" + elseif ctype == "tools" then + datastring = datastring .. S("This tool belongs to these groups: @1.", gstring) .. "\n" + elseif ctype == "craftitems" then + datastring = datastring .. S("This item belongs to these groups: @1.", gstring) .. "\n" + end + end + end + end + datastring = newline2(datastring) + + -- Show fuel recipe + datastring = datastring .. factoid_fuel(data.itemstring, ctype) + datastring = newline2(datastring) + + -- Other custom factoids + datastring = datastring .. factoid_custom(ctype, "misc", data) + datastring = newline2(datastring) + + -- Itemstring + datastring = datastring .. factoid_itemstring(data.itemstring, playername) + + return datastring +end + +function doc.sub.items.register_factoid(category_id, factoid_type, factoid_generator) + local ftable = { fgen = factoid_generator, ftype = factoid_type } + if category_id == "nodes" or category_id == "tools" or category_id == "craftitems" then + table.insert(factoid_generators[category_id], ftable) + return true + elseif category_id == nil then + table.insert(factoid_generators.nodes, ftable) + table.insert(factoid_generators.tools, ftable) + table.insert(factoid_generators.craftitems, ftable) + return false + end +end + +function doc.sub.items.disable_core_factoid(factoid_name) + forbidden_core_factoids[factoid_name] = true +end + +doc.add_category("nodes", { + hide_entries_by_default = true, + name = S("Blocks"), + description = S("Item reference of blocks and other things which are capable of occupying space"), + build_formspec = function(data, playername) + if data then + local formstring = "" + local datastring = "" + + formstring = entry_image(data) + datastring = factoids_header(data, "nodes") + + if not forbidden_core_factoids.basics then + datastring = datastring .. S("Collidable: @1", yesno(data.def.walkable)) .. "\n" + local liquid + if data.def.liquidtype ~= "none" then liquid = true else liquid = false end + if data.def.pointable == true then + datastring = datastring .. S("Pointable: Yes") .. "\n" + elseif liquid then + datastring = datastring .. S("Pointable: Only by special items") .. "\n" + else + datastring = datastring .. S("Pointable: No") .. "\n" + end + end + datastring = newline2(datastring) + if not forbidden_core_factoids.liquid and liquid then + datastring = newline(datastring, false) + datastring = datastring .. S("This block is a liquid with these properties:") .. "\n" + local range, renew, viscos + if data.def.liquid_range then range = data.def.liquid_range else range = 8 end + if data.def.liquid_renewable ~= nil then renew = data.def.liquid_renewable else renew = true end + if data.def.liquid_viscosity then viscos = data.def.liquid_viscosity else viscos = 0 end + if renew then + datastring = datastring .. S("• Renewable") .. "\n" + else + datastring = datastring .. S("• Not renewable") .. "\n" + end + if range == 0 then + datastring = datastring .. S("• No flowing") .. "\n" + else + datastring = datastring .. S("• Flowing range: @1", range) .. "\n" + end + datastring = datastring .. S("• Viscosity: @1", viscos) .. "\n" + end + datastring = newline2(datastring) + + -- Global factoids + --- Direct interaction with the player + ---- Damage (very important) + if not forbidden_core_factoids.node_damage then + if data.def.damage_per_second ~= nil and data.def.damage_per_second > 1 then + datastring = datastring .. S("This block causes a damage of @1 hit points per second.", data.def.damage_per_second) .. "\n" + elseif data.def.damage_per_second == 1 then + datastring = datastring .. S("This block causes a damage of @1 hit point per second.", data.def.damage_per_second) .. "\n" + end + if data.def.drowning then + if data.def.drowning > 1 then + datastring = datastring .. S("This block decreases your breath and causes a drowning damage of @1 hit points every 2 seconds.", data.def.drowning) .. "\n" + elseif data.def.drowning == 1 then + datastring = datastring .. S("This block decreases your breath and causes a drowning damage of @1 hit point every 2 seconds.", data.def.drowning) .. "\n" + end + end + local fdap = data.def.groups.fall_damage_add_percent + if fdap ~= nil then + if fdap > 0 then + datastring = datastring .. S("The fall damage on this block is increased by @1%.", fdap) .. "\n" + elseif fdap <= -100 then + datastring = datastring .. S("This block negates all fall damage.") .. "\n" + else + datastring = datastring .. S("The fall damage on this block is reduced by @1%.", math.abs(fdap)) .. "\n" + end + end + end + datastring = datastring .. factoid_custom("nodes", "damage", data) + datastring = newline2(datastring) + + ---- Movement + if forbidden_core_factoids.node_movement then + if data.def.groups.disable_jump == 1 then + datastring = datastring .. S("You can not jump while standing on this block.").."\n" + end + if data.def.climbable == true then + datastring = datastring .. S("This block can be climbed.").."\n" + end + local bouncy = data.def.groups.bouncy + if bouncy ~= nil then + datastring = datastring .. S("This block will make you bounce off with an elasticity of @1%.", bouncy).."\n" + end + datastring = datastring .. factoid_custom("nodes", "movement", data) + datastring = newline2(datastring) + end + + ---- Sounds + if not forbidden_core_factoids.sounds then + local function is_silent(def, soundtype) + return type(def.sounds) ~= "table" or def.sounds[soundtype] == nil or def.sounds[soundtype] == "" or (type(data.def.sounds[soundtype]) == "table" and (data.def.sounds[soundtype].name == nil or data.def.sounds[soundtype].name == "")) + end + local silentstep, silentdig, silentplace = false, false, false + if data.def.walkable and is_silent(data.def, "footstep") then + silentstep = true + end + if data.def.diggable and is_silent(data.def, "dig") and is_silent(data.def, "dug") then + silentdig = true + end + if is_silent(data.def, "place") and is_silent(data.def, "place_failed") and data.itemstring ~= "air" then + silentplace = true + end + if silentstep and silentdig and silentplace then + datastring = datastring .. S("This block is completely silent when walked on, mined or built.").."\n" + elseif silentdig and silentplace then + datastring = datastring .. S("This block is completely silent when mined or built.").."\n" + else + if silentstep then + datastring = datastring .. S("Walking on this block is completely silent.").."\n" + end + if silentdig then + datastring = datastring .. S("Mining this block is completely silent.").."\n" + end + if silentplace then + datastring = datastring .. S("Building this block is completely silent.").."\n" + end + end + end + datastring = datastring .. factoid_custom("nodes", "sound", data) + datastring = newline2(datastring) + + -- Block activity + --- Gravity + if not forbidden_core_factoids.gravity then + if data.def.groups.falling_node == 1 then + datastring = datastring .. S("This block is affected by gravity and can fall.").."\n" + end + end + datastring = datastring .. factoid_custom("nodes", "gravity", data) + datastring = newline2(datastring) + + --- Dropping and destruction + if not forbidden_core_factoids.drop_destroy then + if data.def.buildable_to == true then + datastring = datastring .. S("Building another block at this block will place it inside and replace it.").."\n" + if data.def.walkable then + datastring = datastring .. S("Falling blocks can go through this block; they destroy it when doing so.").."\n" + end + end + if data.def.walkable == false then + if data.def.buildable_to == false and data.def.drop ~= "" then + datastring = datastring .. S("This block will drop as an item when a falling block ends up inside it.").."\n" + else + datastring = datastring .. S("This block is destroyed when a falling block ends up inside it.").."\n" + end + end + if data.def.groups.attached_node == 1 then + if data.def.paramtype2 == "wallmounted" then + datastring = datastring .. S("This block will drop as an item when it is not attached to a surrounding block.").."\n" + else + datastring = datastring .. S("This block will drop as an item when no collidable block is below it.").."\n" + end + end + if data.def.floodable == true then + datastring = datastring .. S("Liquids can flow into this block and destroy it.").."\n" + end + end + datastring = datastring .. factoid_custom("nodes", "drop_destroy", data) + datastring = newline2(datastring) + + -- Block appearance + --- Light + if not forbidden_core_factoids.light and data.def.light_source then + if data.def.light_source > 3 then + datastring = datastring .. S("This block is a light source with a light level of @1.", data.def.light_source).."\n" + elseif data.def.light_source > 0 then + datastring = datastring .. S("This block glows faintly with a light level of @1.", data.def.light_source).."\n" + end + if data.def.paramtype == "light" and data.def.sunlight_propagates then + datastring = datastring .. S("This block allows light to propagate with a small loss of brightness, and sunlight can even go through losslessly.").."\n" + elseif data.def.paramtype == "light" then + datastring = datastring .. S("This block allows light to propagate with a small loss of brightness.").."\n" + elseif data.def.sunlight_propagates then + datastring = datastring .. S("This block allows sunlight to propagate without loss in brightness.").."\n" + end + end + datastring = datastring .. factoid_custom("nodes", "light", data) + datastring = newline2(datastring) + + --- List nodes/groups to which this node connects to + if not forbidden_core_factoids.connects_to and data.def.connects_to ~= nil then + local nodes = {} + local groups = {} + for c=1,#data.def.connects_to do + local itemstring = data.def.connects_to[c] + if string.sub(itemstring,1,6) == "group:" then + groups[string.sub(itemstring,7,#itemstring)] = 1 + else + table.insert(nodes, itemstring) + end + end + + local nstring = "" + for n=1,#nodes do + local name + if item_name_overrides[nodes[n]] ~= nil then + name = item_name_overrides[nodes[n]] + else + name = description_for_formspec(nodes[n]) + end + if n > 1 then + nstring = nstring .. S(", ") + end + if name ~= nil then + nstring = nstring .. name + else + nstring = nstring .. S("Unknown Node") + end + end + if #nodes == 1 then + datastring = datastring .. S("This block connects to this block: @1.", nstring) .. "\n" + elseif #nodes > 1 then + datastring = datastring .. S("This block connects to these blocks: @1.", nstring) .. "\n" + end + + local gstring, gcount = groups_to_string(groups) + if gcount == 1 then + datastring = datastring .. S("This block connects to blocks of the @1 group.", gstring) .. "\n" + elseif gcount > 1 then + datastring = datastring .. S("This block connects to blocks of the following groups: @1.", gstring) .. "\n" + end + end + + datastring = newline2(datastring) + + -- Mining groups + datastring = datastring .. factoid_custom("nodes", "mining", data) + + datastring = newline(datastring) + + datastring = datastring .. factoid_mining_node(data) + datastring = newline2(datastring) + + -- Non-default drops + if not forbidden_core_factoids.drops and data.def.drop ~= nil and data.def.drop ~= data.itemstring and data.itemstring ~= "air" then + -- TODO: Calculate drop probabilities of max > 1 like for max == 1 + local get_desc = function(stack) + return description_for_formspec(stack:get_name()) + end + if data.def.drop == "" then + datastring = datastring .. S("This block won't drop anything when mined.").."\n" + elseif type(data.def.drop) == "string" then + local dropstack = ItemStack(data.def.drop) + if dropstack:get_name() ~= data.itemstring and dropstack:get_name() ~= 1 then + local desc = get_desc(dropstack) + local count = dropstack:get_count() + if count > 1 then + datastring = datastring .. S("This block will drop the following when mined: @1×@2.", count, desc).."\n" + else + datastring = datastring .. S("This block will drop the following when mined: @1.", desc).."\n" + end + end + elseif type(data.def.drop) == "table" and data.def.drop.items ~= nil then + local max = data.def.drop.max_items + local dropstring = "" + local dropstring_base = "" + if max == nil then + dropstring_base = S("This block will drop the following items when mined: %s.") + elseif max == 1 then + if #data.def.drop.items == 1 then + dropstring_base = S("This block will drop the following when mined: %s.") + else + dropstring_base = S("This block will randomly drop one of the following when mined: %s.") + end + else + dropstring_base = S("This block will randomly drop up to %d drops of the following possible drops when mined: %s.") + end + -- Save calculated probabilities into a table for later output + local probtables = {} + local probtable + local rarity_history = {} + for i=1,#data.def.drop.items do + local local_rarity = data.def.drop.items[i].rarity + local chance = 1 + local rarity = 1 + if local_rarity == nil then + local_rarity = 1 + end + if max == 1 then + -- Chained probability + table.insert(rarity_history, local_rarity) + chance = 1 + for r=1, #rarity_history do + local chance_factor + if r > 1 and rarity_history[r-1] == 1 then + chance = 0 + break + end + if r == #rarity_history then + chance_factor = 1/rarity_history[r] + else + chance_factor = (rarity_history[r]-1)/rarity_history[r] + end + chance = chance * chance_factor + end + if chance > 0 then + rarity = 1/chance + end + else + rarity = local_rarity + chance = 1/rarity + end + -- Exclude impossible drops + if chance > 0 then + probtable = {} + probtable.items = {} + for j=1,#data.def.drop.items[i].items do + local dropstack = ItemStack(data.def.drop.items[i].items[j]) + local itemstring = dropstack:get_name() + local desc = get_desc(dropstack) + local count = dropstack:get_count() + if not(itemstring == nil or itemstring == "" or count == 0) then + if probtable.items[itemstring] == nil then + probtable.items[itemstring] = {desc = desc, count = count} + else + probtable.items[itemstring].count = probtable.items[itemstring].count + count + end + end + end + probtable.rarity = rarity + if #data.def.drop.items[i].items > 0 then + table.insert(probtables, probtable) + end + end + end + -- Do some cleanup of the probability table + if max == 1 or max == nil then + -- Sort by rarity + local comp = function(p1, p2) + return p1.rarity < p2.rarity + end + table.sort(probtables, comp) + end + -- Output probability table + local pcount = 0 + for i=1, #probtables do + if pcount > 0 then + -- List seperator + dropstring = dropstring .. S(", ") + end + local probtable = probtables[i] + local icount = 0 + local dropstring_this = "" + for _, itemtable in pairs(probtable.items) do + if icount > 0 then + -- Final list seperator + dropstring_this = dropstring_this .. S(" and ") + end + local desc = S(itemtable.desc) + local count = itemtable.count + if count ~= 1 then + desc = S("@1×@2", count, desc) + end + dropstring_this = dropstring_this .. desc + icount = icount + 1 + end + + local rarity = probtable.rarity + local raritystring = "" + -- No percentage if there's only one possible guaranteed drop + if not(rarity == 1 and #data.def.drop.items == 1) then + local chance = (1/rarity)*100 + if rarity > 200 then -- <0.5% + -- For very low percentages + dropstring_this = S("@1 (<0.5%)", dropstring_this) + else + -- Add circa indicator for percentages with decimal point + local fchance = string.format("%.0f", chance) + if math.fmod(chance, 1) > 0 then + dropstring_this = S("@1 (ca. @2%)", dropstring_this, fchance) + else + dropstring_this = S("@1 (@2%)", dropstring_this, fchance) + end + end + end + dropstring = dropstring .. dropstring_this + pcount = pcount + 1 + end + if max ~= nil and max > 1 then + datastring = datastring .. string.format(dropstring_base, max, dropstring) + else + datastring = datastring .. string.format(dropstring_base, dropstring) + end + datastring = newline(datastring) + end + end + datastring = datastring .. factoid_custom("nodes", "drops", data) + datastring = newline2(datastring) + + datastring = datastring .. factoids_footer(data, playername, "nodes") + + formstring = formstring .. doc.widgets.text(datastring, nil, nil, doc.FORMSPEC.ENTRY_WIDTH - 1.2) + + return formstring + else + return "label[0,1;NO DATA AVALIABLE!]" + end + end +}) + +doc.add_category("tools", { + hide_entries_by_default = true, + name = S("Tools and weapons"), + description = S("Item reference of all wieldable tools and weapons"), + sorting = "function", + -- Roughly sort tools based on their capabilities. Tools which dig the same stuff end up in the same group + sorting_data = function(entry1, entry2) + local entries = { entry1, entry2 } + -- Hand beats all + if entries[1].eid == "" then return true end + if entries[2].eid == "" then return false end + + local comp = {} + for e=1, 2 do + comp[e] = {} + end + -- No tool capabilities: Instant loser + if entries[1].data.def.tool_capabilities == nil and entries[2].data.def.tool_capabilities ~= nil then return false end + if entries[2].data.def.tool_capabilities == nil and entries[1].data.def.tool_capabilities ~= nil then return true end + -- No tool capabilities for both: Compare by uses + if entries[1].data.def.tool_capabilities == nil and entries[2].data.def.tool_capabilities == nil then + for e=1, 2 do + if type(entries[e].data.def._doc_items_durability) == "number" then + comp[e].uses = entries[e].data.def._doc_items_durability + else + comp[e].uses = 0 + end + end + return comp[1].uses > comp[2].uses + end + for e=1, 2 do + comp[e].gc = entries[e].data.def.tool_capabilities.groupcaps + end + -- No group capabilities = instant loser + if comp[1].gc == nil then return false end + if comp[2].gc == nil then return true end + for e=1, 2 do + local groups = {} + local gc = comp[e].gc + local group = nil + local mintime = nil + local groupcount = 0 + local realuses = nil + for k,v in pairs(gc) do + local maxlevel = v.maxlevel + if maxlevel == nil then + -- Default from tool.h + maxlevel = 1 + end + if groupcount == 0 then + group = k + local uses = v.uses + if v.uses == nil then + -- Default from tool.h + uses = 20 + end + realuses = uses * math.pow(3, maxlevel) + end + if v.times and #v.times > 1 then + for rating, time in pairs(v.times) do + local realtime = time / maxlevel + if mintime == nil or realtime < mintime then + mintime = realtime + end + end + else + mintime = 0 + end + if groups[k] ~= true then + groupcount = groupcount + 1 + groups[k] = true + end + end + comp[e].count = groupcount + comp[e].group = group + comp[e].mintime = mintime + if realuses ~= nil then + comp[e].uses = realuses + elseif type(entries[e].data.def._doc_items_durability) == "number" then + comp[e].uses = entries[e].data.def._doc_items_durability + else + comp[e].uses = 0 + end + end + + -- We want to sort out digging tools with multiple capabilities + if comp[1].count > 1 and comp[1].count > comp[2].count then + return false + elseif comp[1].group == comp[2].group then + -- Tiebreaker 1: Minimum digging time + if comp[1].mintime == comp[2].mintime then + -- Tiebreaker 2: Use count + return comp[1].uses > comp[2].uses + else + return comp[1].mintime < comp[2].mintime + end + -- Final tiebreaker: Sort by group name + else + if comp[1].group and comp[2].group then + return comp[1].group < comp[2].group + else + return false + end + end + end, + build_formspec = function(data, playername) + if data then + local formstring = "" + local datastring = "" + + formstring = entry_image(data) + datastring = factoids_header(data, "tools") + + -- Overwritten durability info + if type(data.def._doc_items_durability) == "number" then + -- Fixed number of uses + datastring = datastring .. S("Durability: @1 uses", data.def._doc_items_durability) + datastring = newline2(datastring) + elseif type(data.def._doc_items_durability) == "string" then + -- Manually described durability + datastring = datastring .. S("Durability: @1", data.def._doc_items_durability) + datastring = newline2(datastring) + end + + datastring = datastring .. factoids_footer(data, playername, "tools") + + formstring = formstring .. doc.widgets.text(datastring, nil, nil, doc.FORMSPEC.ENTRY_WIDTH - 1.2) + + return formstring + else + return "label[0,1;NO DATA AVALIABLE!]" + end + end +}) + +doc.add_category("craftitems", { + hide_entries_by_default = true, + name = S("Miscellaneous items"), + description = S("Item reference of items which are neither blocks, tools or weapons (esp. crafting items)"), + build_formspec = function(data, playername) + if data then + local formstring = "" + local datastring = "" + + formstring = entry_image(data) + datastring = factoids_header(data, "craftitems") + datastring = datastring .. factoids_footer(data, playername, "craftitems") + + formstring = formstring .. doc.widgets.text(datastring, nil, nil, doc.FORMSPEC.ENTRY_WIDTH - 1.2) + + return formstring + else + return "label[0,1;NO DATA AVALIABLE!]" + end + end +}) + +-- Register group definition stuff +-- More (user-)friendly group names to replace the rather technical names +-- for better understanding +function doc.sub.items.add_friendly_group_names(groupnames) + for internal, real in pairs(groupnames) do + groupdefs[internal] = real + end +end + +-- Adds groups to be displayed in the generic “misc.” groups +-- factoid. Those groups should be neither be used as mining +-- groups nor as damage groups and should be relevant to the +-- player in someway. +function doc.sub.items.add_notable_groups(groupnames) + for g=1,#groupnames do + miscgroups[groupnames[g]] = true + end +end + +-- Collect information about all items +local function gather_descs() + -- Internal help texts for default items + local help = { + longdesc = {}, + usagehelp = {}, + } + + -- 1st pass: Gather groups of interest + for id, def in pairs(minetest.registered_items) do + -- Gather all groups used for mining + if def.tool_capabilities ~= nil then + local groupcaps = def.tool_capabilities.groupcaps + if groupcaps ~= nil then + for k,v in pairs(groupcaps) do + if mininggroups[k] ~= true then + mininggroups[k] = true + end + end + end + end + + -- ... and gather all groups which appear in crafting recipes + local crafts = minetest.get_all_craft_recipes(id) + if crafts ~= nil then + for c=1,#crafts do + for k,v in pairs(crafts[c].items) do + if string.sub(v,1,6) == "group:" then + local groupstring = string.sub(v,7,-1) + local groups = string.split(groupstring, ",") + for g=1, #groups do + miscgroups[groups[g]] = true + end + end + end + end + end + + -- ... and gather all groups used in connects_to + if def.connects_to ~= nil then + for c=1, #def.connects_to do + if string.sub(def.connects_to[c],1,6) == "group:" then + local group = string.sub(def.connects_to[c],7,-1) + miscgroups[group] = true + end + end + end + end + + -- 2nd pass: Add entries + + -- Set default air text + -- Custom longdesc and usagehelp may be set by mods through the add_helptexts function + if minetest.registered_items["air"]._doc_items_longdesc then + help.longdesc["air"] = minetest.registered_items["air"]._doc.items_longdesc + else + help.longdesc["air"] = S("A transparent block, basically empty space. It is usually left behind after digging something.") + end + if minetest.registered_items["ignore"]._doc_items_create_entry ~= nil then + suppressed["ignore"] = minetest.registered_items["ignore"]._doc_items_create_entry == true + end + + -- Add entry for the default tool (“hand”) + -- Custom longdesc and usagehelp may be set by mods through the add_helptexts function + local handdef = minetest.registered_items[""] + if handdef._doc_items_create_entry ~= false then + if handdef._doc_items_longdesc then + help.longdesc[""] = handdef._doc_items_longdesc + else + -- Default text + help.longdesc[""] = S("Whenever you are not wielding any item, you use the hand which acts as a tool with its own capabilities. When you are wielding an item which is not a mining tool or a weapon it will behave as if it would be the hand.") + end + if handdef._doc_items_entry_name then + item_name_overrides[""] = handdef._doc_items_entry_name + end + doc.add_entry("tools", "", { + name = item_name_overrides[""], + hidden = handdef._doc_items_hidden == true, + data = { + longdesc = help.longdesc[""], + usagehelp = help.usagehelp[""], + itemstring = "", + def = handdef, + } + }) + end + + local add_entries = function(deftable, category_id) + for id, def in pairs(deftable) do + local name, ld, uh, im + local forced = false + if def._doc_items_create_entry == true and def ~= nil then forced = true end + name = get_entry_name(id) + if not (((def.description == nil or def.description == "") and def._doc_items_entry_name == nil) or (def._doc_items_create_entry == false) or (suppressed[id] == true)) or forced then + if def._doc_items_longdesc then + ld = def._doc_items_longdesc + end + if help.longdesc[id] ~= nil then + ld = help.longdesc[id] + end + if def._doc_items_usagehelp then + uh = def._doc_items_usagehelp + end + if help.usagehelp[id] ~= nil then + uh = help.usagehelp[id] + end + if def._doc_items_image then + im = def._doc_items_image + end + local hidden + if id == "air" or id == "" then hidden = false end + if type(def._doc_items_hidden) == "boolean" then + hidden = def._doc_items_hidden + end + local custom_image + name = scrub_newlines(name) + local infotable = { + name = name, + hidden = hidden, + data = { + longdesc = ld, + usagehelp = uh, + image = im, + itemstring = id, + def = def, + } + } + doc.add_entry(category_id, id, infotable) + end + end + end + + -- Add node entries + add_entries(minetest.registered_nodes, "nodes") + + -- Add tool entries + add_entries(minetest.registered_tools, "tools") + + -- Add craftitem entries + add_entries(minetest.registered_craftitems, "craftitems") +end + +--[[ Reveal items as the player progresses through the game. +Items are revealed by: +* Digging, punching or placing node, +* Crafting +* Having item in inventory (not instantly revealed) ]] + +local function reveal_item(playername, itemstring) + local category_id + if itemstring == nil or itemstring == "" or playername == nil or playername == "" then + return false + end + if minetest.registered_nodes[itemstring] ~= nil then + category_id = "nodes" + elseif minetest.registered_tools[itemstring] ~= nil then + category_id = "tools" + elseif minetest.registered_craftitems[itemstring] ~= nil then + category_id = "craftitems" + elseif minetest.registered_items[itemstring] ~= nil then + category_id = "craftitems" + else + return false + end + doc.mark_entry_as_revealed(playername, category_id, itemstring) + return true +end + +local function reveal_items_in_inventory(player) + local inv = player:get_inventory() + local list = inv:get_list("main") + for l=1, #list do + reveal_item(player:get_player_name(), list[l]:get_name()) + end +end + +minetest.register_on_dignode(function(pos, oldnode, digger) + if digger == nil then return end + local playername = digger:get_player_name() + if playername ~= nil and playername ~= "" and oldnode ~= nil then + reveal_item(playername, oldnode.name) + reveal_items_in_inventory(digger) + end +end) + +minetest.register_on_punchnode(function(pos, node, puncher, pointed_thing) + if puncher == nil then return end + local playername = puncher:get_player_name() + if playername ~= nil and playername ~= "" and node ~= nil then + reveal_item(playername, node.name) + end +end) + +minetest.register_on_placenode(function(pos, newnode, placer, oldnode, itemstack, pointed_thing) + if placer == nil then return end + local playername = placer:get_player_name() + if playername ~= nil and playername ~= "" and itemstack ~= nil and not itemstack:is_empty() then + reveal_item(playername, itemstack:get_name()) + end +end) + +minetest.register_on_craft(function(itemstack, player, old_craft_grid, craft_inv) + if player == nil then return end + local playername = player:get_player_name() + if playername ~= nil and playername ~= "" and itemstack ~= nil and not itemstack:is_empty() then + reveal_item(playername, itemstack:get_name()) + end +end) + +minetest.register_on_item_eat(function(hp_change, replace_with_item, itemstack, user, pointed_thing) + if user == nil then return end + local playername = user:get_player_name() + if playername ~= nil and playername ~= "" and itemstack ~= nil and not itemstack:is_empty() then + reveal_item(playername, itemstack:get_name()) + if replace_with_item ~= nil then + reveal_item(playername, replace_with_item) + end + end +end) + +minetest.register_on_joinplayer(function(player) + reveal_items_in_inventory(player) +end) + +--[[ Periodically check all items in player inventory and reveal them all. +TODO: Check whether there's a serious performance impact on servers with many players. +TODO: If possible, try to replace this functionality by updating the revealed items as + soon the player obtained a new item (probably needs new Minetest callbacks). ]] +local checktime = 8 +local timer = 0 +minetest.register_globalstep(function(dtime) + timer = timer + dtime + if timer > checktime then + local players = minetest.get_connected_players() + for p=1, #players do + reveal_items_in_inventory(players[p]) + end + + timer = math.fmod(timer, checktime) + end +end) + +minetest.after(0, gather_descs) diff --git a/mods/HELP/doc/doc_items/locale/de.txt b/mods/HELP/doc/doc_items/locale/de.txt new file mode 100644 index 0000000000..8bc67ad76d --- /dev/null +++ b/mods/HELP/doc/doc_items/locale/de.txt @@ -0,0 +1,140 @@ +\sUsing it as fuel turns it into: @1. = \sWird dieser Gegenstand als Brennstoff verwendet, verwandelt er sich zu: @1. +@1 seconds = @1 Sekunden +# Item count times item name +@1×@2 = @1×@2 +# Itemname (25%) +@1 (@2%) = @1 (@2%) +# Itemname (<0.5%) +@1 (<0.5%) = @1 (<0,5%) +# Itemname (ca. 25%) +@1 (ca. @2%) = @1 (ca. @2%) +# List separator (e.g. “one, two, three”) +,\s = ,\s +# Final list separator (e.g. “One, two and three”) +\sand\s = \sund\s +1 second = 1 Sekunde +A transparent block, basically empty space. It is usually left behind after digging something. = Ein transparenter Block, praktisch leerer Raum. Er wird üblicherweise hinterlassen, nachdem man etwas ausgegraben hat. +Air = Luft +Blocks = Blöcke +Building another block at this block will place it inside and replace it. = Wird ein anderer Block an diesem Block gebaut, wird dieser andere Block seine Stelle einnehmen. +Building this block is completely silent. = Das Bauen dieses Blocks ist völlig lautlos. +Collidable: @1 = Kollidiert: @1 +Description: @1 = Beschreibung: @1 +Falling blocks can go through this block; they destroy it when doing so. = Fallende Blöcke können diesen Block durchdringen; sie zerstören ihn dabei. +Full punch interval: @1 s = Schlagintervall: @1 s +Hand = Hand +Hold it in your hand, then leftclick to eat it. = Halten Sie es in Ihrer Hand, dann klicken Sie mit der linken Maustaste, um es zu essen. +Hold it in your hand, then leftclick to eat it. But why would you want to do this? = Halten Sie es in Ihrer Hand, dann klicken Sie mit der linken Maustaste, um es zu essen. Aber warum sollten Sie das tun wollen? +Item reference of all wieldable tools and weapons = Gegenstandsreferenz aller tragbaren Werkzeugen und Waffen +Item reference of blocks and other things which are capable of occupying space = Gegenstandsreferenz aller Blöcke und anderen Dingen, die Raum belegen +Item reference of items which are neither blocks, tools or weapons (esp. crafting items) = Gegenstandsreferenz aller Gegenstände, welche weder Blöcke, Werkzeuge oder Waffen sind (insb. Fertigungsgegenstände) +Liquids can flow into this block and destroy it. = Flüssigkeiten können in diesen Block hereinfließen und ihn zerstören. +Maximum stack size: @1 = Maximale Stapelgröße: @1 +Mining level: @1 = Grabestufe: @1 +Mining ratings: = Grabewertungen: +• @1, rating @2: @3 s - @4 s = • @1, Wertung @2: @3 s - @4 s +• @1, rating @2: @3 s = • @1, Wertung @2: @3 s +Mining times: = Grabezeiten: +Mining this block is completely silent. = Das Abbauen dieses Blocks ist völlig lautlos. +Miscellaneous items = Sonstige Gegenstände +No = Nein +Pointable: No = Zeigbar: Nein +Pointable: Only by special items = Zeigbar: Nur von besonderen Gegenständen +Pointable: Yes = Zeigbar: Ja +Punches with this block don't work as usual; melee combat and mining are either not possible or work differently. = Schläge mit diesem Block funktionieren nicht auf die übliche Weise; Nahkampf und Graben sind damit entweder nicht möglich oder funktionieren auf andere Weise. +Punches with this item don't work as usual; melee combat and mining are either not possible or work differently. = Schläge mit diesem Gegenstand funktionieren nicht auf die übliche Weise; Nahkampf und Graben sind damit entweder nicht möglich oder funktionieren auf andere Weise. +Punches with this tool don't work as usual; melee combat and mining are either not possible or work differently. = Schläge mit diesem Werkzeug funktionieren nicht auf die übliche Weise; Nahkampf und Graben sind damit entweder nicht möglich oder funktionieren auf andere Weise. +Range: @1 = Reichweite: @1 +# Range: () +Range: @1 (@2) = Reichweite: @1 (@2) +Range: 4 = Reichweite: 4 +# Rating used for digging times +Rating @1 = Wertung @1 +Rating @1-@2 = Wertung @1-@2 +The fall damage on this block is increased by @1%. = Der Fallschaden auf diesem Block ist um @1% erhöht. +The fall damage on this block is reduced by @1%. = Der Fallschaden auf diesem Block ist um @1% reduziert. +This block allows light to propagate with a small loss of brightness, and sunlight can even go through losslessly. = Dieser Block ist lichtdurchlässig mit einen geringfügigen Helligkeitsverlust; Sonnenlicht passiert jedoch ohne Verlust. +This block allows light to propagate with a small loss of brightness. = Dieser Block ist lichtdurchlässig mit einen geringfügigen Helligkeitsverlust. +This block allows sunlight to propagate without loss in brightness. = Dieser Block ist vollkommen durchlässig für Sonnenlicht. +This block belongs to the @1 group. = Dieser Block gehört zur Gruppe »@1«. +This block belongs to these groups: @1. = Dieser Block gehört zu den folgenden Gruppen: @1. +This block can be climbed. = Dieser Block kann beklettert werden. +This block can be destroyed by any mining tool immediately. = Dieser Block kann von einem beliebigen Grabewerkzeug sofort zerstört werden. +This block can be destroyed by any mining tool in half a second. = Dieser Block kann von einem beliebigen Grabewerkzeug in einer halben Sekunde zerstört werden. +This block can be mined by any mining tool immediately. = Dieser Block kann von einem beliebigen Grabewerkzeug sofort abgebaut werden. +This block can be mined by any mining tool in half a second. = Dieser Block kann von einem beliebigen Grabewerkzeug in einer halben Sekunde abgebaut werden. +This block can be mined by mining tools which match any of the following mining ratings and its toughness level. = Dieser Block kann von Grabewerkzeugen abgebaut werden, falls sie auf eine der folgenden Grabewertungen sowie seinem Härtegrad passen. +This block can not be destroyed by ordinary mining tools. = Dieser Block kann nicht von Grabewerkzeugen zerstört werden. +This block can not be mined by ordinary mining tools. = Dieser Block kann nicht von gewöhnlichen Grabewerkzeugen abgebaut werden. +This block can serve as a smelting fuel with a burning time of @1. = Dieser Block kann als Brennstoff mit einer Brenndauer von @1 dienen. +This block causes a damage of @1 hit point per second. = Dieser Block richtet einen Schaden von @1 Trefferpunkt pro Sekunde an. +This block causes a damage of @1 hit points per second. = Dieser Block richtet einen Schaden von @1 Trefferpunkten pro Sekunde an. +This block connects to blocks of the @1 group. = Dieser Block verbindet sich mit Blöcken der Gruppe »@1«. +This block connects to blocks of the following groups: @1. = Dieser Block verbindet sich mit Blöcken der folgenden Gruppen: @1. +This block connects to these blocks: @1. = Dieser Block verbindet sich mit den folgenden Blöcken: @1. +This block connects to this block: @1. = Dieser Block verbindet sich mit diesem Block: @1. +This block decreases your breath and causes a drowning damage of @1 hit point every 2 seconds. = Dieser Block reduziert Ihren Atem und verursacht beim Ertrinken einen Schaden von @1 Trefferpunkt alle 2 Sekunden. +This block decreases your breath and causes a drowning damage of @1 hit points every 2 seconds. = Dieser Block reduziert Ihren Atem und verursacht beim Ertrinken einen Schaden von @1 Trefferpunkten alle 2 Sekunden. +This block glows faintly. It is barely noticable. = Dieser Block leuchtet schwach. Es ist kaum merklich. +This block is a light source with a light level of @1. = Dieser Block ist eine Lichtquelle mit einer Helligkeitsstufe von @1. +This block glows faintly with a light level of @1. = Dieser Block leuchtet schwach mit einer Helligkeitsstufe von @1. +This block is a building block for creating various buildings. = Dieser Block ist für den Bau diverser Gebäude vorgesehen. +This block is a liquid with these properties: = Dieser Block ist eine Flüssigkeit mit folgenden Eigenschaften: +This block is affected by gravity and can fall. = Dieser Block wird von der Schwerkraft beeinflusst und kann fallen. +This block is completely silent when mined or built. = Dieser Block kann vollkommen lautlos gebaut oder abgebaut werden. +This block is completely silent when walked on, mined or built. = Es ist vollkommen lautlos, wenn man auf diesen Block geht, ihn baut oder abbaut. +This block is destroyed when a falling block ends up inside it. = Dieser Block wird zerstört, wenn ein fallender Block in ihm landet. +This block negates all fall damage. = Auf diesem Block gibt es keinen Fallschaden. +This block points to liquids. = Mit diesem Block zeigt man auf Flüssigkeiten. +This block will drop as an item when a falling block ends up inside it. = Dieser Block wird sich als Gegenstand abwerfen, wenn ein fallender Block in ihn landet. +This block will drop as an item when it is not attached to a surrounding block. = Dieser Block wird sich als Gegenstand abwerfen, wenn er nicht an einen benachbarten Block befestigt ist. +This block will drop as an item when no collidable block is below it. = Dieser Block wird sich als Gegenstand abwerfen, wenn kein kollidierender Block unter ihn liegt. +This block will drop the following items when mined: %s. = Dieser Block wird nach dem Abbauen die folgenden Gegenstände abwerfen: %s. +This block will drop the following when mined: @1×@2. = Dieser Block wird nach dem Abbauen folgendes abwerfen: @1×@2. +This block will drop the following when mined: @1. = Dieser Block wird nach dem Abbauen folgendes abwerfen: @1. +This block will drop the following when mined: %s. = Dieser Block wird nach dem Abbauen folgendes abwerfen: %s. +This block will make you bounce off with an elasticity of @1%. = Dieser Block wird Sie mit einer Elastizität von @1% abprallen lassen. +This block will randomly drop one of the following when mined: %s. = Dieser Block wird nach dem Abbauen zufällig eines von den folgenden Dingen abwerfen: %s. +This block will randomly drop up to %d drops of the following possible drops when mined: %s. = Dieser Block nach dem Abbauen wird zufällig bis zu %d Abwürfe von den folgenden möglichen Abwürfen abwerfen: %s. +This block won't drop anything when mined. = Dieser Block wird nach dem Abbauen nichts abwerfen. +This is a decorational block. = Dieser Block dient zur Dekoration. +This is a melee weapon which deals damage by punching. = Dies ist eine Nahkampfwaffe, welche Schaden durch Schläge verursacht. +Maximum damage per hit: = Maximaler Schaden pro Treffer: +This item belongs to the @1 group. = Dieser Gegenstand gehört zur Gruppe »@1«. +This item belongs to these groups: @1. = Dieser Gegenstand gehört zu den folgenden Gruppen: @1. +This item can serve as a smelting fuel with a burning time of @1. = Dieser Gegenstand kann als Brennstoff mit einer Brenndauer von @1 dienen. +This item is primarily used for crafting other items. = Dieser Gegenstand wird primär für die Fertigung von anderen Gegenständen benutzt. +This item points to liquids. = Mit diesem Gegenstand zeigt man auf Flüssigkeiten. +This tool belongs to the @1 group. = Dieses Werkzeug gehört zur Gruppe »@1«. +This tool belongs to these groups: @1. = Dieses Werkzeug gehört zu den folgenden Gruppen: @1. +This tool can serve as a smelting fuel with a burning time of @1. = Dieses Werkzeug kann als Brennstoff mit einer Brenndauer von @1 dienen. +This tool is capable of mining. = Dies ist ein Grabewerkzeug. +Maximum toughness levels: = Maximale Härtegrade: +This tool points to liquids. = Mit diesem Werkzeug zeigt man auf Flüssigkeiten. +Tools and weapons = Werkzeuge und Waffen +Unknown Node = Unbekannter Node +Usage help: @1 = Benutzung: @1 +Walking on this block is completely silent. = Auf diesem Block sind Schritte lautlos. +Whenever you are not wielding any item, you use the hand which acts as a tool with its own capabilities. When you are wielding an item which is not a mining tool or a weapon it will behave as if it would be the hand. = Wenn Sie keinen Gegenstand halten, benutzen Sie die Hand, welches als ein Werkzeug mit seinen eigenen Fägihkeiten dient. Wenn Sie einen Gegenstand halten, der kein Grabewerkzeug oder eine Waffe ist, wird er sich verhalten als wäre er die Hand. +Yes = Ja +You can not jump while standing on this block. = Man kann von diesem Block nicht abspringen. +any level = beliebige Stufe +level 0 = Stufe 0 +level 0-@1 = Stufen 0-@1 +unknown = unbekannt +Unknown item (@1) = Unbekannter Gegenstand (@1) +• @1: @2 = • @1: @2 +• @1: @2 HP = • @1: @2 TP +• @1: @2, @3 = • @1: @2, @3 +• Flowing range: @1 = • Fließweite: @1 +• No flowing = • Kein Fließen +• Not renewable = • Nicht erneuerbar +• Renewable = • Erneuerbar +• Viscosity: @1 = • Zähflüssigkeit: @1 +Itemstring: "@1" = Itemstring: »@1« +Durability: @1 uses = Haltbarkeit: @1 Benutzungen +Durability: @1 = Haltbarkeit: @1 +Mining durability: = Grabehaltbarkeit: +• @1, level @2: @3 uses = • @1, Stufe @2: @3 Benutzungen +• @1, level @2: Unlimited = • @1, Stufe @2: Unbegrenzt +This block's rotation is affected by the way you place it: Place it on the floor or ceiling for a vertical orientation; place it at the side for a horizontal orientation. Sneaking while placing it leads to a perpendicular orientation instead. = Die Rotation dieses Blocks hängt davon ab, wie sie ihn platzieren: Platzieren Sie ihn auf den Boden oder an die Decke, um ihn vertikal aufzustellen; platzieren Sie in an der Seite für eine horizontale Ausrichtung. Wenn Sie während des Bauens schleichen, wird der Block stattdessen senkrecht zur üblichen Ausrichtung rotiert. diff --git a/mods/HELP/doc/doc_items/locale/template.txt b/mods/HELP/doc/doc_items/locale/template.txt new file mode 100644 index 0000000000..5f71c75a60 --- /dev/null +++ b/mods/HELP/doc/doc_items/locale/template.txt @@ -0,0 +1,140 @@ +\sUsing it as fuel turns it into: @1. = +@1 seconds = +# Item count times item name +%@1×@2 = +# Itemname (25%) +@1 (@2%) = +# Itemname (<0.5%) +@1 (<0.5%) = +# Itemname (ca. 25%) +@1 (ca. @2%) = +# List separator (e.g. “one, two, three”) +,\s = +# Final list separator (e.g. “One, two and three”) +\sand\s = +1 second = +A transparent block, basically empty space. It is usually left behind after digging something. = +Air = +Blocks = +Building another block at this block will place it inside and replace it. = +Building this block is completely silent. = +Collidable: @1 = +Description: @1 = +Falling blocks can go through this block; they destroy it when doing so. = +Full punch interval: @1 s = +Hand = +Hold it in your hand, then leftclick to eat it. = +Hold it in your hand, then leftclick to eat it. But why would you want to do this? = +Item reference of all wieldable tools and weapons = +Item reference of blocks and other things which are capable of occupying space = +Item reference of items which are neither blocks, tools or weapons (esp. crafting items) = +Liquids can flow into this block and destroy it. = +Maximum stack size: @1 = +Mining level: @1 = +Mining ratings: = +• @1, rating @2: @3 s - @4 s = +• @1, rating @2: @3 s = +Mining times: = +Mining this block is completely silent. = +Miscellaneous items = +No = +Pointable: No = +Pointable: Only by special items = +Pointable: Yes = +Punches with this block don't work as usual; melee combat and mining are either not possible or work differently. = +Punches with this item don't work as usual; melee combat and mining are either not possible or work differently. = +Punches with this tool don't work as usual; melee combat and mining are either not possible or work differently. = +Range: @1 = +# Range: () +Range: @1 (@2) = +Range: 4 = +# Rating used for digging times +Rating @1 = +# @1 is minimal rating, @2 is maximum rating +Rating @1-@2 = +The fall damage on this block is increased by @1%. = +The fall damage on this block is reduced by @1%. = +This block allows light to propagate with a small loss of brightness, and sunlight can even go through losslessly. = +This block allows light to propagate with a small loss of brightness. = +This block allows sunlight to propagate without loss in brightness. = +This block belongs to the @1 group. = +This block belongs to these groups: @1. = +This block can be climbed. = +This block can be destroyed by any mining tool immediately. = +This block can be destroyed by any mining tool in half a second. = +This block can be mined by any mining tool immediately. = +This block can be mined by any mining tool in half a second. = +This block can be mined by mining tools which match any of the following mining ratings and its toughness level. = +This block can not be destroyed by ordinary mining tools. = +This block can not be mined by ordinary mining tools. = +This block can serve as a smelting fuel with a burning time of @1. = +This block causes a damage of @1 hit point per second. = +This block causes a damage of @1 hit points per second. = +This block connects to blocks of the @1 group. = +This block connects to blocks of the following groups: @1. = +This block connects to these blocks: @1. = +This block connects to this block: @1. = +This block decreases your breath and causes a drowning damage of @1 hit point every 2 seconds. = +This block decreases your breath and causes a drowning damage of @1 hit points every 2 seconds. = +This block is a light source with a light level of @1. = +This block glows faintly with a light level of @1. = +This block is a building block for creating various buildings. = +This block is a liquid with these properties: = +This block is affected by gravity and can fall. = +This block is completely silent when mined or built. = +This block is completely silent when walked on, mined or built. = +This block is destroyed when a falling block ends up inside it. = +This block negates all fall damage. = +This block points to liquids. = +This block will drop as an item when a falling block ends up inside it. = +This block will drop as an item when it is not attached to a surrounding block. = +This block will drop as an item when no collidable block is below it. = +This block will drop the following items when mined: %s. = +This block will drop the following when mined: @1×@2. = +This block will drop the following when mined: @1. = +This block will drop the following when mined: %s. = +This block will make you bounce off with an elasticity of @1%. = +This block will randomly drop one of the following when mined: %s. = +This block will randomly drop up to %d drops of the following possible drops when mined: %s. = +This block won't drop anything when mined. = +This is a decorational block. = +This is a melee weapon which deals damage by punching. = +Maximum damage per hit: = +This item belongs to the @1 group. = +This item belongs to these groups: @1. = +This item can serve as a smelting fuel with a burning time of @1. = +This item is primarily used for crafting other items. = +This item points to liquids. = +This tool belongs to the @1 group. = +This tool belongs to these groups: @1. = +This tool can serve as a smelting fuel with a burning time of @1. = +This tool is capable of mining. = +Maximum toughness levels: = +This tool points to liquids. = +Tools and weapons = +Unknown Node = +Usage help: @1 = +Walking on this block is completely silent. = +Whenever you are not wielding any item, you use the hand which acts as a tool with its own capabilities. When you are wielding an item which is not a mining tool or a weapon it will behave as if it would be the hand. = +Yes = +You can not jump while standing on this block. = +any level = +level 0 = +level 0-@1 = +unknown = +Unknown item (@1) = +• @1: @2 = +• @1: @2 HP = +• @1: @2, @3 = +• Flowing range: @1 = +• No flowing = +• Not renewable = +• Renewable = +• Viscosity: @1 = +Itemstring: "@1" = +Durability: @1 uses = +Durability: @1 = +Mining durability: = +• @1, level @2: @3 uses = +• @1, level @2: Unlimited = +This block's rotation is affected by the way you place it: Place it on the floor or ceiling for a vertical orientation; place it at the side for a horizontal orientation. Sneaking while placing it leads to a perpendicular orientation instead. = diff --git a/mods/HELP/doc/doc_items/mod.conf b/mods/HELP/doc/doc_items/mod.conf new file mode 100644 index 0000000000..74fa80dfc8 --- /dev/null +++ b/mods/HELP/doc/doc_items/mod.conf @@ -0,0 +1 @@ +name = doc_items diff --git a/mods/HELP/doc/doc_items/screenshot.png b/mods/HELP/doc/doc_items/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..8e7f5656ca39c3724a9ed0129f88af609e418fc7 GIT binary patch literal 15507 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 WMSq??Meywb;ZKr^5|v^`f&T;HjM^~( literal 0 HcmV?d00001 diff --git a/mods/HELP/doc/doc_items/settingtypes.txt b/mods/HELP/doc/doc_items/settingtypes.txt new file mode 100644 index 0000000000..8b9d635e84 --- /dev/null +++ b/mods/HELP/doc/doc_items/settingtypes.txt @@ -0,0 +1,16 @@ +#This feature is experimental! +#If enabled, the mod will show alternative group names which are a bit +#more readable than the internally used (but canonical) group names. For +#example, the group “wood” may be rendered as “Wood”, “leaves” as +#“Leaves and Needles”, “oddly_breakable_by_hand” as “Hand-breakable”, +#and so on. Note that these alternative names are only used for better +#understanding, they are not official. +#This feature might be removed in later versions if it becomes obsolete. +doc_items_friendly_group_names (Show “friendly” group names) bool false + +#If enabled, the mod will show the itemstring of the entry for each item to +#all players. If disabled, the itemstring will only be shown to players +#with the “give” or “debug” privilege. +#The itemstring is useful to power users and programmers and +#is used e.g. for the /give and /giveme commands. +doc_items_show_itemstrings (Show itemstrings) bool false diff --git a/mods/HELP/doc/modpack.txt b/mods/HELP/doc/modpack.txt new file mode 100644 index 0000000000..e69de29bb2