diff --git a/.github/workflows/luacheck.yml b/.github/workflows/luacheck.yml
new file mode 100644
index 0000000..6d20257
--- /dev/null
+++ b/.github/workflows/luacheck.yml
@@ -0,0 +1,14 @@
+name: luacheck
+on: [push, pull_request]
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@master
+ - name: apt
+ run: sudo apt-get install -y luarocks
+ - name: luacheck install
+ run: luarocks install --local luacheck
+ - name: luacheck run
+ run: $HOME/.luarocks/bin/luacheck ./
+
diff --git a/.luacheckrc b/.luacheckrc
new file mode 100644
index 0000000..c7f3553
--- /dev/null
+++ b/.luacheckrc
@@ -0,0 +1,30 @@
+
+globals = {
+ "replacer",
+ minetest = { fields = { "translate", "get_translator" } },
+}
+
+read_globals = {
+ -- Stdlib
+ string = { fields = { "split", "match", "find", "lower" } },
+ table = { fields = { "copy", "getn", "insert", "shuffle", "sort" } },
+
+ -- Minetest
+ "minetest",
+ "vector", "ItemStack",
+ "dump", "VoxelArea",
+
+ -- deps
+ "circular_saw",
+ "colormachine",
+ "creative",
+ "default",
+ "dye",
+ "flowers",
+ "moreblocks",
+ "stairsplus",
+ "technic",
+ "unified_inventory",
+ "unifieddyes",
+}
+
diff --git a/CHANGELOG b/CHANGELOG
new file mode 100644
index 0000000..3e6e85c
--- /dev/null
+++ b/CHANGELOG
@@ -0,0 +1,115 @@
+20240225 * SwissalpS patched a crash situation with multinode-nodes. Thanks frogTheSecond and Huhhila for detecting and reporting.
+20221107 * fluxionary fixed typo unkown -> unknown
+20220830 * SwissalpS fixed de translation typo. Thanks Niklp09 for reporting.
+20220626 * SwissalpS fixed case where other drops were prefered from actual node that user tried to set to.
+20220624 * SX added technic.plus compat fix
+20220215 * SwissalpS added compat for itemholder and powered_stand when override is used
+ to make them work nicely:
+ https://github.com/pandorabox-io/pandorabox_custom/blob/master/scifi_override.lua
+20220214 * SwissalpS added compat for itemframe, pedestal, itemholder and powered_stand
+ (itemframes and scifi_nodes) showing info on what they are holding.
+20220210 * SwissalpS added some more info to inspection tool when inspecting players.
+20220205 * SwissalpS tweaked formspec of replacer to adjust when minor modes are disabled.
+ * Enabled changing modes with place without pointing at a node.
+ * Added more info on mobs.
+20220204 * SwissalpS added tooltip to protection info as that often clipped.
+ * fixed the alias resolving to not change node_name.
+ * fixed group:food_pumpkin.
+ * dropped datastructure usage for final sorting as it left weird patterns
+ when time-outs were reached. Instead using clasic table.sort with vector.distance
+ comparisons. Also using faster repeat ... until loop.
+ * In field and crust mode only count nodes that were actually changed.
+20220203 * SwissalpS fixed some issues: forced aliases, items that have no drops defined
+ Added compat for buckets, canisters and beertap/mugs
+ Fixed formspec for MT 5.5.0 (that now actually uses padding)
+ Added server setting to disable minor modes.
+ Updated documentation and locales.
+20220127 * SwissalpS cleaned up var names in inspect.lua
+ * Added .luacheckrc and print_dump()
+ * Bumped version to 4.0
+20220126 * SwissalpS improved some compat items and redid translations.
+ * Documentation updated.
+ * Added LICENSE file.
+20220125 * SwissalpS improved sytem for showing dynamicly added crafting methods
+ which allowed to improve the output of technic and saw methods.
+ * Better drop detection method.
+ * Added sound playback for setting replacer errors, mainly.
+ * Change /replacer_mute to /replacer and changed the arguments
+ so sound can be muted separately.
+ * Added tooltips so users have more chance of reading long lines. Adding
+ textarea to make itemstring selectable failed and is post-poned to when
+ new version of formspec is being used for inspection tool as well.
+ * Added experimental hold zoom+click with inspection tool to open unified_inventory.
+20220124 * SwissalpS added wrap around for recipes - yes, long due.
+ * Bugfix, now replacer actually checks groups.
+ * Compat for shears (wool & vines) also fermenting/pickling, advtrains and sci-fi plastic
+ * ehlphabet and letters compat.
+20220123 * SwissalpS added dev-mode and /place_all chat command.
+ * Technic crafting methods for inspection tool, including cnc nodes.
+ * planetoidgen:airlight is treated as air.
+ * New way to determine if replacer can be set to node
+ implementing callbacks better. Such as group denial.
+ * A lot of compatibility added to many mods.
+20220120 * SwissalpS made folder structure to create easier overview.
+ * Improved circular saw item detection for inspect and replacer.
+ * Added beacon beam and base support (no longer needed to be in inv when setting).
+ * Inspection tool now inspects light correctly and respects right-click.
+ * Added locale for inspection tool.
+20220119 * SwissalpS added first draft of de,es,fi,fr,it,pt,ru locales.
+20220118 * SwissalpS cleaned up more code, giving more discriptive variable names and
+ cleaning out ugly modes table that had both number and string indexes
+ * Don't allow replacer to be set to deny_list nodes
+ * History works for users with priv. Various settings added to
+ fine-tune how it behaves.
+ * Implemented minor-modes with more colours ;)
+ * Added non-formspec way to cycle minor modes: Special+Sneak+right-click
+ * Especially in functions with tight loops, local references to global functions was added.
+20220117 * SwissalpS changed mode storage in tool meta to major.minor format
+ * Added version 4 formspec that enables changing minor mode
+ and has prepared history selector.
+ * Moved changelog and cleaned it up adding some dates
+ * Moved formspec code to separate file.
+ * Started implementing translations and history.
+20220115 * SwissalpS refactored constraints and renamed blacklist to deny_list
+20220114 * SwissalpS added support for cable plates and similar nodes
+20220113 * SwissalpS worked in HybridDog's nicer pattern algorithm, modifying a little.
+ Also cleaned up some code and give-priv does not grant modes anymore,
+ creative still does.
+20220112 * SwissalpS improved field mode: when replacing also check for same param2
+ improved crust mode: when placing also allow vacuum instead of only air
+20211202 * SwissalpS added /replacer_mute command
+20210930 * SwissalpS merged patch provided by S-S-X to prevent a rare but possible crash with
+ Unknown Items in hotbar
+ * Also cleaned up tool change messages to blabla.lua
+20201016 * HybridDog provided first documentation and SwissalpS added some more explaining modes.
+20201015 * SwissalpS cleaned up inspector code and made inspector better readable on smaller screens
+20200322 * HybridDog removed fourth mode and improved node search
+ * SwissalpS added backward compatibility for non technic servers, restored
+ creative/give behaviour and fixed the 'too many nodes detected' issue
+ * S-S-X and some players from pandorabox.io requested and inspired ideas to
+ implement which SwissalpS tried to satisfy.
+20200131 * SwissalpS added method to change mode via formspec
+20200109 * BuckarooBanzay added server-setting max_nodes, moved crafts and replacer to
+ separate files, added .luacheckrc and cleaned up inspection tool, fixing
+ some issues on the way and updated readme to look nice
+20191217 * OgelGames fixed digging to be simulated properly
+20191212 * coil0 made modes available as technic tool and added limits
+ * SwissalpS merged Sokomine's and HybridDog's versions
+ * HybridDog added modes for creative mode
+20190628 * coil0 fixed issue by using buildable_to
+20171209 * Got rid of outdated minetest.env
+ * Fixed error in protection function.
+ * Fixed minor bugs.
+ * Added blacklist
+20141002 * Some more improvements for inspect-tool. Added craft-guide.
+20141001 * Added inspect-tool.
+20130112 * If digging the node was unsuccessful, then the replacement will now fail
+ (instead of destroying the old node with its metadata; i.e. chests with content)
+20131120 * if the server version is new enough, minetest.is_protected is used
+ in order to check if the replacement is allowed
+20130424 * param1 and param2 are now stored
+ * hold sneak + right click to store new pattern
+ * right click: place one of the itmes
+ * receipe changed
+ * inventory image added
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..5357f69
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,166 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+ This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+ 0. Additional Definitions.
+
+ As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+ "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+ An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+ A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+ The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+ The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+ 1. Exception to Section 3 of the GNU GPL.
+
+ You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+ 2. Conveying Modified Versions.
+
+ If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+ a) under this License, provided that you make a good faith effort to
+ ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+
+ b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+ 3. Object Code Incorporating Material from Library Header Files.
+
+ The object code form of an Application may incorporate material from
+a header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+ a) Give prominent notice with each copy of the object code that the
+ Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the object code with a copy of the GNU GPL and this license
+ document.
+
+ 4. Combined Works.
+
+ You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+ a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
+ document.
+
+ c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+
+ d) Do one of the following:
+
+ 0) Convey the Minimal Corresponding Source under the terms of this
+ License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+
+ 1) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (a) uses at run time
+ a copy of the Library already present on the user's computer
+ system, and (b) will operate properly with a modified version
+ of the Library that is interface-compatible with the Linked
+ Version.
+
+ e) Provide Installation Information, but only if you would otherwise
+ be required to provide such information under section 6 of the
+ GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the
+ Application with a modified version of the Linked Version. (If
+ you use option 4d0, the Installation Information must accompany
+ the Minimal Corresponding Source and Corresponding Application
+ Code. If you use option 4d1, you must provide the Installation
+ Information in the manner specified by section 6 of the GNU GPL
+ for conveying Corresponding Source.)
+
+ 5. Combined Libraries.
+
+ You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+ a) Accompany the combined library with a copy of the same work based
+ on the Library, uncombined with any other library facilities,
+ conveyed under the terms of this License.
+
+ b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+ 6. Revised Versions of the GNU Lesser General Public License.
+
+ The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+ If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
+
diff --git a/README.md b/README.md
index 0a9269d..cdb7aab 100644
--- a/README.md
+++ b/README.md
@@ -1,32 +1,121 @@
Replacement tool for creative building (Mod for Minetest)
+=========================================================
-This tool is helpful for creative purposes (i.e. build a wall and "paint" windows into it).
+This tool is helpful for creative purposes (e.g. build a wall and "paint" windows into it).
It replaces nodes with a previously selected other type of node (i.e. places said windows
into a brick wall).
-**Crafting:**
- chest - -
- - stick -
- - - chest
-Or just use `/giveme replacer:replacer`
-
-**Usage:** Right-click on a node of that type you want to replace other nodes with.
- Left-click (normal usage) on any nodes you want to replace with the type you previously right-clicked on.
- SHIFT-Right-click in order to store a new pattern.
+# Crafting
+
+Availability of recipes can be configured with server settings.
+Basic replacer:
+```
+ | chest | | gold ingot |
+ | | mese fragment | |
+ | steel ingot | | chest |
+```
+Or `/giveme replacer:replacer`
+
+Technic replacer as upgrade to basic tool:
+```
+ | replacer:replacer | green energy crystal | |
+ | | | |
+ | | | |
+```
+Or `/giveme replacer:replacer_technic`
+
+Technic replacer directly crafted:
+```
+ | chest | green energy crystal | gold ingot |
+ | | mese fragment | |
+ | steel ingot | | chest |
+```
+Or `/giveme replacer:replacer_technic`
+
+# Usage
+
+Sneak-right-click on a node of which type you want to replace other nodes with.
+ Left-click (normal usage) on any nodes you want to replace with that type.
+ Right-click to place a node of that type onto clicked node.
When in creative mode, the node will just be replaced. Your inventory will not be changed.
-When *not* in creative mode, digging will be simulated and you will get what was there. In return, the replacement node
-will be taken from your inventory.
-
-
-The second tool included in this mod is the inspector.
-
-Crafting: torch
- stick
-
-Just wield it and click on any node or entity you want to know more about. A limited craft-guide is included.
-
+When *not* in creative mode, digging will be simulated and you will get what was there.
+In return, the replacement node will be taken from your inventory.
+
+If technic mod is installed, modes are available and use depletes charge.
+This is true for users without "give" privs and also on servers not running in creative mode.
+
+# Modes (Major)
+
+Special+Place or Special+Use anywhere to change the mode.
+The first informs you in chat about the mode change while the second opens a formspec.
+Single-mode does not need any charge. The other modes do.
+For a description of the modes with pictures, refer to:
+* [Single Mode (doc/usageSingle.md)](doc/usageSingle.md)
+* [Field Mode (doc/usageField.md)](doc/usageField.md)
+* [Crust Mode (doc/usageCrust.md)](doc/usageCrust.md)
+
+# Modes (Minor)
+
+Using the formspec or Sneak+Special+Place to change the minor mode.
+* Both (standard)
+* Node: Replaces the node using rotation of dug node.
+* Rotation: Basically a screwdriver with set rotation. Doesn't dig the clicked node, only applies the stored rotation.
+In Field and Crust Modes this will apply to multiple nodes according to respective search pattern.
+When Place-button is pressed, Rotation Mode does not make much sense as mostly air is being rotated. This can lead to confusion for beginners.
+
+# Chat Commands
+
+* /replacer (audio|chat) (1|0) toggles chat messages for advanced users. Also allows muting sounds. This command also accepts variants of "on"/"off" words in several languages.
+* /place_all [dry-run][ move_player][ no_support_node][ [] ... [ ]]
+ This is only available to players with **priv** priv and only in development mode. [Read the comments in (test.lua)](test.lua)
+
+# Privelages
+
+**creative** priv allows setting to more node-types and **give** priv allows to set to any. They both
+unlock modes even for basic replacer and allow using without charge constrain.
+They both also allow user to use unlimited amounts of items without checking inventory.
+The configurable priv allows using history.
+
+# Inspection tool
+
+The third tool included in this mod is the inspection tool.
+
+Crafting:
+```
+ | torch | | |
+ | stick | | |
+ | | | |
+```
+Punch (dig/use on) any node or entity you want to know more about.
+Right-click (place) on any node to get information of the adjacent node. The node that would be placed if you were weilding something placeable.
+This is useful to inspect a node that is otherwise unclickable. E.g. airlight or activated stealthnodes.
+Apart from node name and description, light value and a limited craft-guide is included.
+Compatibility to many mods with special use cases have been added. Read [Customization (doc/customization.md)]((doc/customization.md)) for more information.
+
+# Settings
+
+* **replacer.max_nodes** max allowed nodes to replace per action (default: 3168)
+* **replacer.hide_recipe_basic** hide the basic recipe (default: 0)
+These two require technic to be installed, if not, the recipes are hidden.
+* **replacer.hide_recipe_technic_upgrade** hide the upgrade recipe (default: 0)
+* **replacer.hide_recipe_technic_direct** hide the direct technic recipe (default: 1)
+* **replacer.history_priv** priv needed for using history (default: creative)
+[All settings with medium length description in (settingtypes.txt)](settingtypes.txt)
+Lengthy information about settings and customization can be found in the [Customization Guide (doc/customization.md)](doc/customization.md)
+
+# Contributors
+
+* Sokomine
+* coil0
+* HybridDog
+* SwissalpS
+* OgelGames
+* BuckarooBanzay
+* S-S-X
+
+# License
Copyright (C) 2013,2014,2015 Sokomine
diff --git a/TODO b/TODO
new file mode 100644
index 0000000..252817a
--- /dev/null
+++ b/TODO
@@ -0,0 +1,95 @@
+-- inspection tool: add button to open unified_inventory's formspec
+ this may no longer be as important and isn't that practical as there are
+ now so many buttons. It would be hard to keep clear which item will be shown.
+ SwissalpS: by using player:set_inventory_formspec() unified_inventory
+ binds itself to the 'i'-key
+ https://github.com/minetest-mods/unified_inventory/blob/d6688872c84417d2f61d6f5e607aea39d78920aa/internal.lua#L302-L306
+ I think you could open it by sending the formspec from
+ unified_inventory.get_formspec(player, page)
+ maybe we can use this on group buttons
+
+-- inspection tool:
+ stop passing context in hidden fields, use runtime cache to keep
+ context -> enables to keep all recipes in cycle and possibly reduce look-ups.
+ crafting info on craftable entities like bikes, trains
+ figure out how to make the itemstring selectable for copy -> do that with new formspec version
+ add mixing method
+ add info for plants telling when they are ripe -> allowing user to click on the ripe
+ stage and see possible seed drop -> showing info about planting.
+ don't forget wine:blue_agave when doing that
+
+-- known incompat replacer ------------------------
+-- doors in general (low priority)
+ (lowest priority)
+-- elphabet: add human readable letter to shortstring mainly for history
+-- letters: similar to elphabet
+
+------ replacer fixes --------
+
+try to add a formspec for player to be able to choose from multiple valid drops
+ when node does not drop itself.
+
+-- can't be set --> no recipe
+homedecor:glass_table_large_square
+homedecor:dvd_cd_cabinet
+homedecor:chains
+homedecor:wood_table_large_square
+scifi_nodes:junk
+scifi_nodes:engine
+scifi_nodes:doomengine
+scifi_nodes:crate
+scifi_nodes:capsule2
+scifi_nodes:capsule3
+scifi_nodes:builder
+
+-- can be set but can't be placed -- need alias for inspect tool too
+-- these seem to work now, maybe placing them with place_all isn't good simulation?
+homedecor:desk_lamp_7 -> 14
+homedecor:ceiling_lantern_6 -> 14
+homedecor:ceiling_lamp_8|on -> 14
+homedecor:glowlight_half_8|on -> 14
+homedecor:glowlight_quarter_0|on -> 14
+homedecor:glowlight_small_cube_1|on -> 14
+homedecor:ground_lantern_0-13|on homedecor:ground_lantern_14
+homedecor:hanging_lantern_10|on -> 14
+homedecor:lattice_lantern_large_1|on -> 14
+homedecor:lattice_lantern_small_1
+homedecor:plasma_ball_off -> homedecor:plasma_ball_on -- meh
+homedecor:plasma_lamp_8|on -> 14
+homedecor:standing_lamp_8
+homedecor:table_lamp_12
+
+------- need 'cable plate override' ---------
+homedecor:speaker_open -> homedecor:speaker (cable plate overrides) -- not fixing as user can have both in inventory
+
+---------- known incompat inspect -----------
+
+---- missing icons ----
+cottages:wool;cottages:wool (used by homedecor:curtain_open) -- meh, there is working recipe
+(homedecor:stained_glass) -- cottages:glass_pane_side
+
+-- fix in scifi_nodes
+flowers:dandelion --> dandelion_white and dandelion_yellow would exist (*scifi_nodes:plant10 also 9)
+
+-- fix in homedecor_kitchen
+homedecor:kitchen_cabinet --> homedecor:kitchen_cabinet_colorable *
+
+-- fix these in bridger mod
+bridger:corrugated_steelgreen --> bridger:corrugated_steel_green *
+bridger:corrugated_steelwhite
+bridger:corrugated_steelred
+bridger:corrugated_steelsteel
+bridger:corrugated_steelyellow
+
+-- christmas presents say they don't drop anything
+
+-- will not implement --
+ add helper so when setting replacer to an itemframe that contains something,
+ set to the contents --> but which param values? This isn't a good idea.
+ add biofuel hints also general 'fuel' craft types -> if biofuel doesn't expose a list
+ this is also doomed (as I don't want to maintain lists)
+ add digging method for ores --> maybe not. Doing this dynamically seems overkill to
+ check every registered item to see if it drops the requested item
+ add info for seeds and saplings about planting -> this too seems like a task that will
+ include mainting a list of all plant types of any mod that adds them. Not my intention.
+
diff --git a/blabla.lua b/blabla.lua
new file mode 100644
index 0000000..f9a8c5c
--- /dev/null
+++ b/blabla.lua
@@ -0,0 +1,129 @@
+if not minetest.translate then
+ function minetest.translate(_, str, ...)
+ local arg = { n = select('#', ...), ... }
+ return str:gsub('@(.)', function(matched)
+ local c = string.byte(matched)
+ if string.byte('1') <= c and c <= string.byte('9') then
+ return arg[c - string.byte('0')]
+ else
+ return matched
+ end
+ end)
+ end
+
+ function minetest.get_translator(textdomain)
+ return function(str, ...) return minetest.translate(textdomain or '', str, ...) end
+ end
+end -- backward compatibility
+replacer.S = minetest.get_translator('replacer')
+local S = replacer.S
+
+replacer.blabla = {}
+replacer.blabla.inspect = {}
+local rb = replacer.blabla
+local rbi = replacer.blabla.inspect
+
+rb.log_messages = '[replacer] %s: %s'
+rb.choose_history = S('History')
+rb.choose_mode = S('Choose mode')
+rb.mode_minor1 = S('Both')
+rb.mode_minor2 = S('Node')
+rb.mode_minor3 = S('Rotation')
+rb.mode_minor1_info = S('Replace node and apply orientation.')
+rb.mode_minor2_info = S('Replace node without changing orientation.')
+rb.mode_minor3_info = S('Apply orientation without changing node type.')
+rb.mode_single = S('Single')
+rb.mode_field = S('Field')
+rb.mode_crust = S('Crust')
+rb.mode_single_tooltip = S('Replace single node.')
+rb.mode_field_tooltip = S('Left click: Replace field of nodes of a kind where a '
+ .. 'translucent node is in front of it.@nRight click: Replace field of air '
+ .. 'where no translucent node is behind the air.')
+rb.mode_crust_tooltip = S('Left click: Replace nodes which touch another one of '
+ .. 'its kind and a translucent node, e.g. air.@nRight click: Replace air nodes '
+ .. 'which touch the crust.')
+rb.wait_for_load = S('Target node not yet loaded. Please wait a moment for the '
+ .. 'server to catch up.')
+rb.nothing_to_replace = S('Nothing to replace.')
+rb.need_more_charge = S('Not enough charge to use this mode.')
+rb.too_many_nodes_detected = S('Aborted, too many nodes detected.')
+rb.none_selected = S('Error: No node selected.')
+rb.description_basic = S('Node replacement tool')
+rb.description_technic = S('Node replacement tool (technic)')
+rb.log_limit_override = '[replacer] Setting already set node-limit for "%s" was %d.'
+rb.log_limit_insert = '[replacer] Setting node-limit for "%s" to %d.'
+rb.log_deny_list_insert = '[replacer] Added "%s" to deny list.'
+rb.timed_out = S('Time-limit reached.')
+rb.tool_short_description = '(%s %s%s) %s'
+rb.tool_long_description = '%s\n%s\n%s'
+rb.ccm_params = '(chat|audio) (0|1)'
+rb.ccm_description = S('Toggles verbosity.\nchat: When on, '
+ .. 'messages are posted to chat.\naudio: When off, replacer is silent.')
+rb.ccm_player_not_found = 'Player not found'
+rb.ccm_player_meta_error = 'Player meta not existant'
+rb.log_reg_exception_override = '[replacer] register_exception: '
+ .. 'exception for "%s" already exists.'
+rb.log_reg_exception = '[replacer] registered exception for "%s" to "%s"'
+rb.log_reg_exception_callback = '[replacer] registered after on_place callback for "%s"'
+rb.log_reg_alias_override = '[replacer] register_non_creative_alias: '
+ .. ' alias for "%s" already exists.'
+rb.log_reg_alias = '[replacer] registered alias for "%s" to "%s"'
+rb.log_reg_set_callback_fail = '[replacer] register_set_enabler called without passing function.'
+rb.formspec_error = '[replacer] formspec error, user "%s" attempting to change history. Fields: %s'
+rb.formspec_hacker = '[replacer] formspec forge? By user "%s" Fields: %s'
+rb.minor_modes_disabled = S('Minor modes are disabled on this server.')
+rb.no_pos = S('')
+rb.days = S('days')
+
+----------------- replacer:inspect -----------------
+rbi.description = S('Inspection Tool\nUse to inspect target node or entity.\n'
+ .. 'Place to inspect the adjacent node.')
+rbi.broken_object = S('This is a broken object. We have no further information about it. It is located')
+rbi.owned_protected_locked = S('owned, protected and locked')
+rbi.owned_protected = S('owned and protected')
+rbi.owned_locked = S('owned and locked')
+rbi.this_is_object = S('This is an object')
+rbi.is_protected = S('WARNING: You can\'t dig this node. It is protected.')
+rbi.you_can_dig = S('INFO: You can dig this node, others can\'t.')
+rbi.no_description = S('~ no description provided ~')
+rbi.no_node_description = S('~ no node description provided ~')
+rbi.no_item_description = S('~ no item description provided ~')
+rbi.name = S('Name:')
+rbi.exit = S('Exit')
+rbi.this_is = S('This is:')
+rbi.prev = S('previous recipe')
+rbi.next = S('next recipe')
+rbi.no_recipes = S('No recipes.')
+rbi.drops_on_dig = S('Drops on dig:')
+rbi.nothing = S('nothing.')
+rbi.may_drop_on_dig = S('May drop on dig:')
+rbi.can_be_fuel = S('This can be used as a fuel.')
+rbi.unknown_recipe = S('Error: Unknown recipe.')
+rbi.log_reg_craft_method_wrong_arguments = '[replacer] register_craft_method invalid arguments given.'
+rbi.log_reg_craft_method_overriding_method = '[replacer] register_craft_method overriding existing method '
+rbi.log_reg_craft_method_added = '[replacer] register_craft_method method added: %s %s'
+rbi.scoop = S('scoop up')
+rbi.pour = S('pour out')
+rbi.filling = S('filling')
+rbi.mobs_disclaimer = S('(Functions may exist that change attributes and conditions of mobs)')
+rbi.mobs_of_type = S('Is of type')
+rbi.mobs_loyal = S('Is loyal to owner.')
+rbi.mobs_attacks = S('Likes to attack:')
+rbi.mobs_follows = S('Follows players holding:')
+rbi.mobs_drops = S('May drop:')
+rbi.mobs_shoots = S('Can shoot misiles.')
+rbi.mobs_breed = S('Can breed.')
+rbi.mobs_spawns_on = S('Spawns on:')
+rbi.mobs_spawns_neighbours = S('with neighours:')
+rbi.player_placed = S('Placed:')
+rbi.player_digs = S('Digs:')
+rbi.player_inflicted = S('Inflicted:')
+rbi.player_punches = S('Punched:')
+rbi.player_xp = S('XP:')
+rbi.player_deaths = S('Deaths:')
+rbi.player_duration = S('Played:')
+rbi.player_has_active_mission = S('Is currently on a mission.')
+rbi.player_no_common_channels = S("You don't have any common channels.")
+rbi.player_common_channels = S('You are both on these channels:')
+rbi.player_is_wearing = S('Is wearing:')
+
diff --git a/chat_commands.lua b/chat_commands.lua
new file mode 100644
index 0000000..8460a5c
--- /dev/null
+++ b/chat_commands.lua
@@ -0,0 +1,57 @@
+
+local rb = replacer.blabla
+
+-- let's hope there isn't a yes that means no in another language :/
+-- TODO: better option would be to simply toggle (see postool)
+local lOn = { '1', 'on', 'yes', 'an', 'ja', 'si', 'sí', 'да', 'oui', 'joo', 'juu', 'kyllä', 'sim', 'em' }
+local lOff = { '0', 'off', 'no', 'aus', 'nein', 'non', 'нет', 'ei', 'fora', 'não', 'desligado' }
+local tOn, tOff = {}, {}
+for _, s in ipairs(lOn) do tOn[s] = true end
+for _, s in ipairs(lOff) do tOff[s] = true end
+
+replacer.chatcommand_mute = {
+
+ params = rb.ccm_params,--(chat|audio) (0|1)
+ description = rb.ccm_description,
+ func = function(name, param)
+ local player = minetest.get_player_by_name(name)
+ if not player then -- TODO: seems unlikely to happen
+ return false, rb.ccm_player_not_found
+ end
+ local meta = player:get_meta()
+ if not meta then -- TODO: seems unlikely to happen
+ return false, rb.ccm_player_meta_error
+ end
+
+ local lower = string.lower(param)
+ local parts = lower:split(' ')
+ local usage = rb.ccm_params .. '\n'
+ .. rb.ccm_description
+ if 2 > #parts then return false, usage end
+
+ local command, value, key = parts[1], parts[2]
+ if 'chat' == command then
+ key = 'replacer_mute'
+ elseif 'audio' == command then
+ key = 'replacer_muteS'
+ elseif 'version' == command then
+ return true, tostring(replacer.version)
+ else
+ return false, usage
+ end
+
+ if tOff[value] then
+ value = 1
+ elseif tOn[value] then
+ value = 0
+ else
+ return false, usage
+ end
+
+ meta:set_int(key, value)
+ return true, ''
+ end
+}
+
+minetest.register_chatcommand('replacer', replacer.chatcommand_mute)
+
diff --git a/check_owner.lua b/check_owner.lua
deleted file mode 100644
index 6a63f69..0000000
--- a/check_owner.lua
+++ /dev/null
@@ -1,44 +0,0 @@
--- taken from Vanessa Ezekowitz' homedecor mod
--- see http://forum.minetest.net/viewtopic.php?pid=26061 or https://github.com/VanessaE/homedecor for details!
-function replacer_homedecor_node_is_owned(pos, placer)
-
- if( not( placer ) or not(pos )) then
- return true;
- end
- local pname = placer:get_player_name();
- if (type( minetest.is_protected ) == "function") then
- local res = minetest.is_protected( pos, pname );
- if( res ) then
- minetest.chat_send_player( pname, "Cannot replace node. It is protected." );
- end
- return res;
- end
-
- local ownername = false
- if type(IsPlayerNodeOwner) == "function" then -- node_ownership mod
- if HasOwner(pos, placer) then -- returns true if the node is owned
- if not IsPlayerNodeOwner(pos, pname) then
- if type(getLastOwner) == "function" then -- ...is an old version
- ownername = getLastOwner(pos)
- elseif type(GetNodeOwnerName) == "function" then -- ...is a recent version
- ownername = GetNodeOwnerName(pos)
- else
- ownername = "someone"
- end
- end
- end
-
- elseif type(isprotect)=="function" then -- glomie's protection mod
- if not isprotect(5, pos, placer) then
- ownername = "someone"
- end
- end
-
- if ownername ~= false then
- minetest.chat_send_player( pname, "Sorry, "..ownername.." owns that spot." )
- return true
- else
- return false
- end
-end
-
diff --git a/compat/advtrains.lua b/compat/advtrains.lua
new file mode 100644
index 0000000..7598918
--- /dev/null
+++ b/compat/advtrains.lua
@@ -0,0 +1,24 @@
+if not minetest.get_modpath('advtrains') then return end
+
+local function add_advtrains_aliases()
+ local core_get_node_drops = minetest.get_node_drops
+ local reg = replacer.register_non_creative_alias
+ -- these cause crashes
+ local deny_list = {
+ ['advtrains_interlocking:dtrack_npr_st'] = true,
+ ['advtrains_line_automation:dtrack_stop_st'] = true,
+ }
+ local drops, drop_name
+ for name, _ in pairs(minetest.registered_nodes) do
+ if not deny_list[name] and name:find('^advtrains') then
+ drops = core_get_node_drops(name)
+ drop_name = drops and drops[1] or ''
+ if drop_name ~= name then
+ reg(name, drop_name)
+ end
+ end
+ end
+end -- add_advtrains_aliases
+
+minetest.after(.2, add_advtrains_aliases)
+
diff --git a/compat/bakedclay.lua b/compat/bakedclay.lua
new file mode 100644
index 0000000..4d9fadd
--- /dev/null
+++ b/compat/bakedclay.lua
@@ -0,0 +1,12 @@
+if not replacer.has_bakedclay then return end
+
+-- add bakedclay items for inspection tool
+replacer.group_placeholder['group:bakedclay'] = 'bakedclay:natural'
+-- unfortunately bakedclay does not expose anything, so we have to manually
+-- maintain the list
+local rgp = replacer.group_placeholder
+rgp['group:flower,color_cyan'] = 'bakedclay:delphinium'
+rgp['group:flower,color_pink'] = 'bakedclay:lazarus'
+rgp['group:flower,color_dark_green'] = 'bakedclay:mannagrass'
+rgp['group:flower,color_magenta'] = 'bakedclay:thistle'
+
diff --git a/compat/beacon.lua b/compat/beacon.lua
new file mode 100644
index 0000000..1172a12
--- /dev/null
+++ b/compat/beacon.lua
@@ -0,0 +1,16 @@
+if not minetest.get_modpath('beacon') then return end
+
+local function is_beacon_beam_or_base(node_name)
+ if 'string' ~= type(node_name) then return nil end
+ if node_name:match('^beacon:(.*)beam$') then return true end
+ if node_name:match('^beacon:(.*)base$') then return true end
+ return false
+end -- is_beacon_beam_or_base
+
+replacer.register_set_enabler(function(node)
+ return node and node.name and is_beacon_beam_or_base(node.name)
+end)
+
+-- for inspection tool
+replacer.group_placeholder['group:beacon'] = 'beacon:white'
+
diff --git a/compat/biofuel.lua b/compat/biofuel.lua
new file mode 100644
index 0000000..a6704a9
--- /dev/null
+++ b/compat/biofuel.lua
@@ -0,0 +1,4 @@
+if not minetest.get_modpath('biofuel') then return end
+
+replacer.register_non_creative_alias('biofuel:refinery_active', 'biofuel:refinery')
+
diff --git a/compat/bridger.lua b/compat/bridger.lua
new file mode 100644
index 0000000..4a70c8e
--- /dev/null
+++ b/compat/bridger.lua
@@ -0,0 +1,9 @@
+if not minetest.get_modpath('bridger') then return end
+
+local rir = replacer.image_replacements
+rir['bridger:corrugated_steelgreen'] = 'bridger:corrugated_steel_green'
+rir['bridger:corrugated_steelwhite'] = 'bridger:corrugated_steel_white'
+rir['bridger:corrugated_steelred'] = 'bridger:corrugated_steel_red'
+rir['bridger:corrugated_steelsteel'] = 'bridger:corrugated_steel_steel'
+rir['bridger:corrugated_steelyellow'] = 'bridger:corrugated_steel_yellow'
+
diff --git a/compat/bucket.lua b/compat/bucket.lua
new file mode 100644
index 0000000..5b15af1
--- /dev/null
+++ b/compat/bucket.lua
@@ -0,0 +1,47 @@
+if not minetest.get_modpath('bucket') then return end
+
+local rbi = replacer.blabla.inspect
+
+local pours = {
+ ['default:water_source'] = 'bucket:bucket_water',
+ ['default:river_water_source'] = 'bucket:bucket_river_water',
+ ['default:lava_source'] = 'bucket:bucket_lava',
+ ['technic:corium_source'] = 'technic:bucket_corium',
+}
+local scoops = {}
+for k, v in pairs(pours) do scoops[v] = k end
+
+local function add_recipe(item_name, _, recipes)
+ local item, method, empty_bucket
+ if scoops[item_name] then
+ item = scoops[item_name]
+ method = rbi.scoop
+ elseif pours[item_name] then
+ item = pours[item_name]
+ method = rbi.pour
+ empty_bucket = true
+ else
+ return
+ end
+
+ recipes[#recipes + 1] = {
+ method = method,
+ type = 'bucket:bucket',
+ items = { item },
+ output = item_name,
+ empty_bucket = empty_bucket,
+ }
+end -- add_recipe
+
+local function add_formspec(recipe)
+ if not recipe.empty_bucket then
+ return ''
+ end
+
+ return 'item_image_button[5,3;1.0,1.0;'
+ .. replacer.image_button_link('bucket:bucket_empty') .. ']'
+end -- add_formspec
+
+replacer.register_craft_method(
+ 'bucket:bucket', 'bucket:bucket_empty', add_recipe, add_formspec)
+
diff --git a/compat/canned_food.lua b/compat/canned_food.lua
new file mode 100644
index 0000000..3ea19ec
--- /dev/null
+++ b/compat/canned_food.lua
@@ -0,0 +1,31 @@
+if not minetest.get_modpath('canned_food') then return end
+
+local S = replacer.S
+
+-- for inspection tool
+local function add_recipe(item_name, _, recipes)
+ local base_name = item_name:match('^(canned_food:.+)_plus$')
+ if not base_name then return end
+
+ recipes[#recipes + 1] = {
+ method = S('fermenting/pickling'),
+ type = 'canned_food',
+ items = { base_name },
+ output = item_name,
+ }
+end -- add_recipe
+
+--luacheck: no unused args
+local function add_formspec(recipe)
+ return 'label[0.5,3.5;' .. S('Store near group:wood, light < 12.') .. ']'
+end
+
+replacer.register_craft_method('canned_food', 'default:wood', add_recipe, add_formspec)
+
+
+-- for replacer
+replacer.register_set_enabler(function(node)
+ return node and 'string' == type(node.name)
+ and node.name:find('^(canned_food:.+)_plus$') and true or false
+end)
+
diff --git a/compat/colormachine.lua b/compat/colormachine.lua
new file mode 100644
index 0000000..9827851
--- /dev/null
+++ b/compat/colormachine.lua
@@ -0,0 +1,21 @@
+if not replacer.has_colormachine_mod then return end
+
+local function add_colormachine_recipe(node_name, _, recipes)
+
+ local res = colormachine.get_node_name_painted(node_name, '')
+ if not res or not res.possible or 1 > #res.possible then
+ return
+ end
+
+ -- paintable node found
+ recipes[#recipes + 1] = {
+ method = 'colormachine',
+ type = 'colormachine',
+ items = { res.possible[1] },
+ output = node_name
+ }
+end -- add_colormachine_recipe
+
+replacer.register_craft_method(
+ 'colormachine', 'colormachine:colormachine', add_colormachine_recipe)
+
diff --git a/compat/default.lua b/compat/default.lua
new file mode 100644
index 0000000..70cffcc
--- /dev/null
+++ b/compat/default.lua
@@ -0,0 +1,61 @@
+-- replacer has default mod as hard dependancy, so no checking
+-- helpers for inspection tool
+-- some common groups
+replacer.group_placeholder['group:water_bucket'] = 'bucket:bucket_river_water'
+replacer.group_placeholder['group:coal'] = 'default:coal_lump'
+replacer.group_placeholder['group:fence'] = 'default:fence_wood'
+replacer.group_placeholder['group:leaves'] = 'default:leaves'
+replacer.group_placeholder['group:marble']= 'technic:marble' -- building_blocks:Marble
+replacer.group_placeholder['group:sand'] = 'default:sand'
+replacer.group_placeholder['group:sapling']= 'default:sapling'
+replacer.group_placeholder['group:stick'] = 'default:stick'
+-- 'default:stone' point people to the cheaper cobble
+replacer.group_placeholder['group:stone'] = 'default:cobble'
+replacer.group_placeholder['group:tar_block'] = 'building_blocks:Tar'
+replacer.group_placeholder['group:tree'] = 'default:tree'
+replacer.group_placeholder['group:vessel'] = 'fireflies:firefly_bottle'
+replacer.group_placeholder['group:wood'] = 'default:wood'
+replacer.group_placeholder['group:wood_slab'] = 'stairs:slab_wood'
+replacer.group_placeholder['group:wool'] = 'wool:white'
+-- pickaxes
+local picks = { 'diamond', 'bronze', 'mese', 'steel', 'stone', 'wood' }
+table.shuffle(picks)
+replacer.group_placeholder['group:pickaxe'] = 'default:pick_' .. picks[1]
+
+-- add default game dyes
+for _, color in pairs(dye.dyes) do
+ replacer.group_placeholder['group:dye,color_' .. color[1] ] = 'dye:' .. color[1]
+end
+
+-- add default game flowers
+local name, groups
+for _, flower in pairs(flowers.datas) do
+ name = flower[1]
+ groups = flower[4]
+ for k, _ in pairs(groups) do
+ if 1 == k:find('color_') then
+ replacer.group_placeholder['group:flower,' .. k] = 'flowers:' .. name
+ end
+ end
+end
+
+-- handle the standard dye color groups
+if replacer.has_basic_dyes then
+ for _, color in ipairs(dye.basecolors) do
+ local def = minetest.registered_items['dye:' .. color]
+ if def and def.groups then
+ for k, _ in pairs(def.groups) do
+ if 'dye' ~= k then
+ replacer.group_placeholder['group:dye,' .. k] = 'dye:' .. color
+ end
+ end
+ replacer.group_placeholder['group:flower,color_' .. color] = 'dye:' .. color
+ end
+ end
+end
+
+-- fix dandelion
+replacer.image_replacements['flowers:dandelion'] = 'flowers:dandelion_white'
+-- fix pink
+replacer.group_placeholder['group:dye,unicolor_light_red'] = 'dye:pink'
+
diff --git a/compat/digtron.lua b/compat/digtron.lua
new file mode 100644
index 0000000..e5d38e0
--- /dev/null
+++ b/compat/digtron.lua
@@ -0,0 +1,7 @@
+if not minetest.get_modpath('digtron') then return end
+
+-- prevent accidental replacement of digtron crates
+-- also placing isn't a good idea either
+replacer.deny_list['digtron:loaded_crate'] = true
+replacer.deny_list['digtron:loaded_locked_crate'] = true
+
diff --git a/compat/drawers.lua b/compat/drawers.lua
new file mode 100644
index 0000000..a9a5529
--- /dev/null
+++ b/compat/drawers.lua
@@ -0,0 +1,5 @@
+if not minetest.get_modpath('drawers') then return end
+
+local rgp = replacer.group_placeholder
+rgp['group:drawer'] = 'drawers:wood2'
+
diff --git a/compat/ehlphabet.lua b/compat/ehlphabet.lua
new file mode 100644
index 0000000..8250ac5
--- /dev/null
+++ b/compat/ehlphabet.lua
@@ -0,0 +1,39 @@
+if not minetest.get_modpath('ehlphabet') then return end
+
+-- for inspection tool
+local S = replacer.S
+replacer.group_placeholder['group:ehlphabet_block'] = 'ehlphabet:208164'
+
+local exceptions = { '231140', '229140', '228184',
+ '230157', '229141', '232165', '231171' }
+local skip = {}
+for _, n in ipairs(exceptions) do skip[n] = true end
+
+local function ehlphabet_number_sticker(item_name)
+ if not item_name or 'string' ~= type(item_name) then return end
+
+ return item_name:match('^ehlphabet:([0-9]+)'),
+ item_name:find('_sticker$') and true
+end
+
+local function add_recipe(item_name, _, recipes)
+ local number, sticker = ehlphabet_number_sticker(item_name)
+ if not number or skip[number] then return end
+
+ local input = sticker and 'default:paper' or 'ehlphabet:block'
+ recipes[#recipes + 1] = {
+ method = S('printing'),
+ type = 'ehlphabet',
+ items = { input },
+ output = item_name,
+ }
+end -- add_recipe
+
+replacer.register_craft_method('ehlphabet', 'ehlphabet:machine', add_recipe)
+
+
+-- for replacer
+replacer.register_set_enabler(function(node)
+ return node and node.name and ehlphabet_number_sticker(node.name)
+end)
+
diff --git a/compat/farming.lua b/compat/farming.lua
new file mode 100644
index 0000000..8354f05
--- /dev/null
+++ b/compat/farming.lua
@@ -0,0 +1,18 @@
+if not minetest.get_modpath('farming') then return end
+
+local rgp = replacer.group_placeholder
+rgp['group:food_cheese'] = 'farming:cheese_vegan'
+rgp['group:food_coffee'] = 'farming:coffee_beans'
+rgp['group:food_corn'] = 'farming:corn'
+rgp['group:food_juicer'] = 'farming:juicer'
+rgp['group:food_meat'] = 'farming:tofu_cooked'
+rgp['group:food_peppercorn'] = 'farming:peppercorn'
+rgp['group:food_pot'] = 'farming:pot'
+rgp['group:food_potato'] = 'farming:potato'
+rgp['group:food_pumpkin'] = 'farming:pumpkin_8'
+rgp['group:food_salt'] = 'farming:salt'
+rgp['group:food_saucepan'] = 'farming:saucepan'
+rgp['group:food_soy'] = 'farming:soy_beans'
+rgp['group:food_sunflower_seeds'] = 'farming:seed_sunflower'
+rgp['group:food_vanilla'] = 'farming:vanilla'
+
diff --git a/compat/home_workshop_misc.lua b/compat/home_workshop_misc.lua
new file mode 100644
index 0000000..cebdbf5
--- /dev/null
+++ b/compat/home_workshop_misc.lua
@@ -0,0 +1,22 @@
+if not minetest.get_modpath('home_workshop_misc') then return end
+
+-- for replacer
+local mug = 'home_workshop_misc:beer_mug'
+replacer.register_exception(mug, mug)
+
+-- for inspection tool
+
+local function add_recipe(item_name, _, recipes)
+ if 'home_workshop_misc:beer_mug' ~= item_name then return end
+
+ recipes[#recipes + 1] = {
+ method = replacer.blabla.inspect.filling,
+ type = 'home_workshop_misc:beer_tap',
+ items = { 'vessels:drinking_glass' },
+ output = item_name,
+ }
+end -- add_recipe
+
+replacer.register_craft_method(
+ 'home_workshop_misc:beer_tap', 'home_workshop_misc:beer_tap', add_recipe)
+
diff --git a/compat/homedecor.lua b/compat/homedecor.lua
new file mode 100644
index 0000000..5306578
--- /dev/null
+++ b/compat/homedecor.lua
@@ -0,0 +1,5 @@
+if not minetest.get_modpath('homedecor_kitchen') then return end
+
+local rir = replacer.image_replacements
+rir['homedecor:kitchen_cabinet'] = 'homedecor:kitchen_cabinet_colorable'
+
diff --git a/compat/itemframes.lua b/compat/itemframes.lua
new file mode 100644
index 0000000..c897ef5
--- /dev/null
+++ b/compat/itemframes.lua
@@ -0,0 +1,44 @@
+if not minetest.get_modpath('itemframes') then return end
+
+-- for inspection tool --
+local S = replacer.S
+
+local function add_recipe_itemframe(item_name, context, recipes)
+ if 'itemframes:frame' ~= item_name then return end
+
+ if not (context and context.pos) then return end
+
+ local held_name = minetest.get_meta(context.pos):get_string('item')
+ if '' == held_name then return end
+
+ recipes[#recipes + 1] = {
+ method = S('holding'),
+ type = 'itemframes:frame',
+ items = { held_name },
+ output = nil,
+ }
+end -- add_recipe_itemframe
+
+replacer.register_craft_method(
+ 'itemframes:frame', 'itemframes:frame', add_recipe_itemframe)
+
+
+local function add_recipe_pedestal(item_name, context, recipes)
+ if 'itemframes:pedestal' ~= item_name then return end
+
+ if not (context and context.pos) then return end
+
+ local held_name = minetest.get_meta(context.pos):get_string('item')
+ if '' == held_name then return end
+
+ recipes[#recipes + 1] = {
+ method = S('holding'),
+ type = 'itemframes:pedestal',
+ items = { held_name },
+ output = nil,
+ }
+end -- add_recipe_pedestal
+
+replacer.register_craft_method(
+ 'itemframes:pedestal', 'itemframes:pedestal', add_recipe_pedestal)
+
diff --git a/compat/jumping.lua b/compat/jumping.lua
new file mode 100644
index 0000000..9ace6d9
--- /dev/null
+++ b/compat/jumping.lua
@@ -0,0 +1,8 @@
+if not minetest.get_modpath('jumping') then return end
+
+local sBaseName = 'jumping:trampoline'
+local sDropName = sBaseName .. '1'
+for i = 2, 6 do
+ replacer.register_exception(sBaseName .. tostring(i), sDropName)
+end
+
diff --git a/compat/letters.lua b/compat/letters.lua
new file mode 100644
index 0000000..16ac770
--- /dev/null
+++ b/compat/letters.lua
@@ -0,0 +1,45 @@
+if not minetest.get_modpath('letters') then return end
+
+-- for inspection tool
+local S = replacer.S
+local function add_recipe_u(item_name, _, recipes)
+ if not item_name or 'string' ~= type(item_name) then return end
+
+ local input, letter = item_name:match('^(.+)_letter_(.)u$')
+ if not input then return end
+
+ recipes[#recipes + 1] = {
+ method = S('cutting'),
+ type = 'letters:upper',
+ items = { input },
+ output = item_name,
+ letter = letter
+ }
+end -- add_recipe_u
+
+replacer.register_craft_method('letters:upper', 'letters:letter_cutter_upper', add_recipe_u)
+
+
+local function add_recipe_l(item_name, _, recipes)
+ if not item_name or 'string' ~= type(item_name) then return end
+
+ local input, letter = item_name:match('^(.+)_letter_(.)l$')
+ if not input then return end
+
+ recipes[#recipes + 1] = {
+ method = S('cutting'),
+ type = 'letters:lower',
+ items = { input },
+ output = item_name,
+ letter = letter
+ }
+end -- add_recipe_l
+
+replacer.register_craft_method('letters:lower', 'letters:letter_cutter_lower', add_recipe_l)
+
+
+-- for replacer
+replacer.register_set_enabler(function(node)
+ return node and node.name and node.name:find('^.+_letter_(.)[lu]$')
+end)
+
diff --git a/compat/mesecons.lua b/compat/mesecons.lua
new file mode 100644
index 0000000..2b2bac2
--- /dev/null
+++ b/compat/mesecons.lua
@@ -0,0 +1,4 @@
+if not minetest.get_modpath('mesecons') then return end
+
+replacer.group_placeholder['group:mesecon_conductor_craftable'] = 'mesecons:wire_00000000_off'
+
diff --git a/compat/mobs.lua b/compat/mobs.lua
new file mode 100644
index 0000000..3943182
--- /dev/null
+++ b/compat/mobs.lua
@@ -0,0 +1,37 @@
+if not minetest.get_modpath('mobs') then return end
+
+local rgp = replacer.group_placeholder
+if not rgp['group:food_cheese'] then rgp['group:food_cheese'] = 'mobs:cheese' end
+if not rgp['group:food_meat'] then rgp['group:food_meat'] = 'mobs:meat' end
+
+
+if not minetest.get_modpath('mobs_animal') then return end
+if not minetest.get_modpath('mobs_animal') then return end
+
+local S = replacer.S
+
+local colours = { 'black', 'blue', 'brown', 'cyan', 'dark_green', 'dark_grey', 'green',
+ 'grey', 'magenta', 'orange', 'pink', 'red', 'violet', 'white', 'yellow' }
+local map = {}
+for _, colour in ipairs(colours) do
+ map['wool:' .. colour] = 'mobs_animal:sheep_' .. colour
+end
+local function add_recipe(item_name, _, recipes)
+ local output = map[item_name]
+ if not output then return end
+
+ recipes[#recipes + 1] = {
+ method = S('shearing'),
+ type = 'sheep:cut',
+ items = { output }, -- tecnically input, but hey
+ output = item_name,
+ }
+end -- add_recipe
+
+--luacheck: no unused args
+local function add_formspec(recipe)
+ return 'label[0.5,3.5;' .. S('Cut with shears.') .. ']'
+end
+
+replacer.register_craft_method('sheep:cut', 'mobs:shears', add_recipe, add_formspec)
+
diff --git a/compat/moreblocks.lua b/compat/moreblocks.lua
new file mode 100644
index 0000000..94328f8
--- /dev/null
+++ b/compat/moreblocks.lua
@@ -0,0 +1,90 @@
+local r = replacer
+if not r.has_circular_saw then return end
+-- ?? TODO do we need to also check for stairsplus and add it to optional_depends ??
+
+local core_registered_nodes = minetest.registered_nodes
+local shapes_list_sorted = nil
+local confirmed_saw_items = {}
+
+local function is_saw_output(node_name)
+ if not node_name or 'moreblocks:circular_saw' == node_name then
+ return nil
+ end
+
+ -- if we already confirmed this item, let's take the shortcut
+ if confirmed_saw_items[node_name] then return confirmed_saw_items[node_name] end
+
+ -- first time this function is called
+ -- make a copy and sort so longest postfixes are used first
+ if nil == shapes_list_sorted then
+ shapes_list_sorted = table.copy(stairsplus.shapes_list)
+ table.sort(shapes_list_sorted, function(a, b) return #a[2] > #b[2] end)
+ end
+ -- now iterate looking for match
+ local mod_name, material, found
+ for _, t in ipairs(shapes_list_sorted) do
+ mod_name, material = string.match(node_name,
+ '^([^:]+):' .. t[1] .. '(.*)' .. t[2] .. '$')
+ if mod_name and material then
+ -- double check
+ if circular_saw.known_nodes[mod_name .. ':' .. material] then
+ break
+ elseif circular_saw.known_nodes['default:' .. material] then
+ -- many are from default
+ mod_name = 'default'
+ break
+ else
+ -- need to try the long way
+ found = false
+ for itemstring, t2 in pairs(circular_saw.known_nodes) do
+ if t2[1] == mod_name and t2[2] == material then
+ mod_name = itemstring:match('^([^:]+)') or ''
+ found = true
+ break
+ end
+ end
+ if found then break end
+ -- make sure we don't accidently have a false-positive
+ mod_name = nil
+ end -- double check
+ end -- match found
+ end -- loop shapes_list_sorted
+ if not (mod_name and material) then
+ return nil
+ end
+
+ local basic_node_name = mod_name .. ':' .. material
+ -- final check
+ if not core_registered_nodes[basic_node_name] then
+ return nil
+ end
+
+ -- cache to speed up next call
+ confirmed_saw_items[node_name] = basic_node_name
+ return basic_node_name
+end -- is_saw_output
+
+local S = replacer.S
+local function add_circular_saw_recipe(node_name, _, recipes)
+ local basic_node_name = is_saw_output(node_name)
+ if not basic_node_name then return end
+
+ -- node found that fits into the saw
+ recipes[#recipes + 1] = {
+ method = S('sawing'),
+ type = 'saw',
+ items = { basic_node_name },
+ output = node_name
+ }
+end -- add_circular_saw_recipe
+
+
+-- for replacer
+r.register_set_enabler(function(node)
+ return node and is_saw_output(node.name)
+end)
+
+
+-- for inspection tool
+r.register_craft_method('saw', 'moreblocks:circular_saw', add_circular_saw_recipe)
+
diff --git a/compat/protectors.lua b/compat/protectors.lua
new file mode 100644
index 0000000..084cff4
--- /dev/null
+++ b/compat/protectors.lua
@@ -0,0 +1,7 @@
+-- prevent accidental replacement of your protectors
+replacer.deny_list['priv_protector:protector'] = true
+replacer.deny_list['protector:protect'] = true
+replacer.deny_list['protector:protect2'] = true
+replacer.deny_list['xp_redo:protector'] = true
+replacer.deny_list['xp_redo:xpgate'] = true
+
diff --git a/compat/realTest.lua b/compat/realTest.lua
new file mode 100644
index 0000000..efc46c0
--- /dev/null
+++ b/compat/realTest.lua
@@ -0,0 +1,18 @@
+-- overrides for replacer:inspect
+-- support for RealTest
+if minetest.get_modpath('trees')
+ and minetest.get_modpath('core')
+ and minetest.get_modpath('instruments')
+ and minetest.get_modpath('anvil')
+ and minetest.get_modpath('scribing_table')
+then
+ replacer.image_replacements['group:planks'] = 'trees:pine_planks'
+ replacer.image_replacements['group:plank'] = 'trees:pine_plank'
+ replacer.image_replacements['group:wood'] = 'trees:pine_planks'
+ replacer.image_replacements['group:tree'] = 'trees:pine_log'
+ replacer.image_replacements['group:sapling'] = 'trees:pine_sapling'
+ replacer.image_replacements['group:leaves'] = 'trees:pine_leaves'
+ replacer.image_replacements['default:furnace'] = 'oven:oven'
+ replacer.image_replacements['default:furnace_active'] = 'oven:oven_active'
+end
+
diff --git a/compat/ropes.lua b/compat/ropes.lua
new file mode 100644
index 0000000..fa1c4ad
--- /dev/null
+++ b/compat/ropes.lua
@@ -0,0 +1,9 @@
+if not minetest.get_modpath('ropes') then return end
+
+replacer.register_non_creative_alias('ropes:ropeladder', 'ropes:ropeladder_top')
+replacer.register_non_creative_alias('ropes:ropeladder_bottom', 'ropes:ropeladder_top')
+-- for these there are multiple targets. For now, alias to cheapest one
+replacer.register_non_creative_alias('ropes:rope', 'ropes:wood1rope_block')
+-- for these there are multiple targets. For now, alias to longest one
+replacer.register_non_creative_alias('ropes:rope_bottom', 'ropes:steel9rope_block')
+
diff --git a/compat/scifi_nodes.lua b/compat/scifi_nodes.lua
new file mode 100644
index 0000000..b408600
--- /dev/null
+++ b/compat/scifi_nodes.lua
@@ -0,0 +1,101 @@
+if not minetest.get_modpath('scifi_nodes') then return end
+
+local S = replacer.S
+
+-- for replacer --
+
+replacer.register_exception('scifi_nodes:laptop_open', 'scifi_nodes:laptop_closed')
+
+-- for inspection tool --
+
+-- These are not 100% accurate as other items could be dropped on the holders.
+-- We can't be more accurate as long as scifi_nodes does't store the item's name.
+-- Like https://github.com/pandorabox-io/pandorabox_custom/blob/master/scifi_override.lua
+-- does. When this override isn't installed, up to 9 objects are returned.
+
+local function add_recipe_itemholder(item_name, context, recipes)
+ if 'scifi_nodes:itemholder' ~= item_name then return end
+
+ if not (context and context.pos) then return end
+
+ local held_name = minetest.get_meta(context.pos):get_string('item')
+ local items
+ if '' ~= held_name then
+ items = { held_name }
+ else
+ -- servers without override need to search for dropped items.
+ items = {}
+ local luaentity
+ local objects = minetest.get_objects_inside_radius(context.pos, .5)
+ if (not objects) or (0 == #objects) then return end
+
+ for _, obj in ipairs(objects) do
+ if obj and obj.get_luaentity then
+ luaentity = obj:get_luaentity()
+ if luaentity and luaentity.itemstring
+ and ('' ~= luaentity.itemstring)
+ and minetest.registered_items[ItemStack(luaentity.itemstring):get_name()]
+ then
+ table.insert(items, luaentity.itemstring)
+ end
+ end
+ if 9 == #items then break end
+ end
+ if 0 == #items then return end
+ end
+
+ recipes[#recipes + 1] = {
+ method = S('holding'),
+ type = 'scifi_nodes:itemholder',
+ items = items,
+ output = nil,
+ }
+end -- add_recipe_itemholder
+
+replacer.register_craft_method(
+ 'scifi_nodes:itemholder', 'scifi_nodes:itemholder', add_recipe_itemholder)
+
+
+local function add_recipe_powered_stand(item_name, context, recipes)
+ if 'scifi_nodes:powered_stand' ~= item_name then return end
+
+ if not (context and context.pos) then return end
+
+ local held_name = minetest.get_meta(context.pos):get_string('item')
+ local items
+ if '' ~= held_name then
+ items = { held_name }
+ else
+ -- servers without override need to search for dropped items.
+ items = {}
+ local luaentity
+ local objects = minetest.get_objects_inside_radius(
+ vector.add(context.pos, vector.new(0, 1, 0)), .5)
+ if (not objects) or (0 == #objects) then return end
+
+ for _, obj in ipairs(objects) do
+ if obj and obj.get_luaentity then
+ luaentity = obj:get_luaentity()
+ if luaentity and luaentity.itemstring
+ and ('' ~= luaentity.itemstring)
+ and minetest.registered_items[ItemStack(luaentity.itemstring):get_name()]
+ then
+ table.insert(items, luaentity.itemstring)
+ end
+ end
+ if 9 == #items then break end
+ end
+ if 0 == #items then return end
+ end
+
+ recipes[#recipes + 1] = {
+ method = S('holding'),
+ type = 'scifi_nodes:powered_stand',
+ items = items,
+ output = nil,
+ }
+end -- add_recipe_powered_stand
+
+replacer.register_craft_method(
+ 'scifi_nodes:powered_stand', 'scifi_nodes:powered_stand', add_recipe_powered_stand)
+
diff --git a/compat/technic.lua b/compat/technic.lua
new file mode 100644
index 0000000..c5bb9d2
--- /dev/null
+++ b/compat/technic.lua
@@ -0,0 +1,331 @@
+-- skip if technic isn't loaded at all
+if not replacer.has_technic_mod then return end
+
+local rbi = replacer.blabla.inspect
+
+-- adds exceptions for technic cable plates
+local lTiers = { 'lv', 'mv', 'hv' }
+local lPlates = { '_digi_cable_plate_', '_cable_plate_' }
+local sDropName, sBaseName
+for _, sTier in ipairs(lTiers) do
+ for _, sPlate in ipairs(lPlates) do
+ sBaseName = 'technic:' .. sTier .. sPlate
+ sDropName = sBaseName .. '1'
+ for i = 2, 6 do
+ replacer.register_exception(sBaseName .. tostring(i), sDropName)
+ end
+ end
+end
+
+-- can be frozen, so let it be placed
+replacer.register_exception('default:dirt_with_snow', 'default:dirt_with_snow')
+
+-- allow cnc output nodes
+replacer.register_set_enabler(function(node)
+ return node and node.name and node.name:find('_technic_cnc_') and true or false
+end)
+
+------------------------------------------
+----------- for inspection tool -----------
+------------------------------------------
+-- have not tested 'other' technic mod, so just support technic.plus
+--if not technic.plus then return end
+
+--[[
+--"cooking" is probably not needed as that is a standard method
+]]
+local S = replacer.S
+
+local function add_recipe_alloy(item_name, _, recipes)
+--pd(technic.recipes['alloy'])
+ for _, def in pairs(technic.recipes['alloy']['recipes']) do
+ if def.output and 'string' == type(def.output)
+ and def.output:find('^' .. item_name .. ' ?[0-9]*$')
+ and def.input and 'table' == type(def.input)
+ then
+ local l = {}
+ for k, v in pairs(def.input) do
+ table.insert(l, k .. ' ' .. tostring(v))
+ end
+ local i1, i2 = l[1], l[2]
+ recipes[#recipes + 1] = {
+ method = S('alloying'),
+ type = 'technic:alloy',
+ items = { i1, i2 },
+ output = def.output
+ }
+ end
+ end
+end -- add_recipe_alloy
+
+replacer.register_craft_method(
+ 'technic:alloy', 'technic:coal_alloy_furnace', add_recipe_alloy)
+
+
+local function add_recipe_cnc(item_name, _, recipes)
+ local base_name, program = item_name:match('^(.*)_technic_cnc_(.*)$')
+ if not base_name then return end
+
+ recipes[#recipes + 1] = {
+ method = S('CNC machining'),
+ type = 'technic:cnc',
+ items = { base_name },
+ output = item_name,
+ program = program --:gsub('_', ' ')
+ }
+end -- add_recipe_cnc
+
+local function add_formspec_cnc(recipe)
+ if not recipe.program then return '' end
+ return 'label[3,1;' .. recipe.program .. ']'
+end
+
+replacer.register_craft_method('technic:cnc', 'technic:cnc', add_recipe_cnc, add_formspec_cnc)
+
+
+local function add_recipe_compress(item_name, _, recipes)
+--pd(technic.recipes['compressing'])
+ for input_name, def in pairs(technic.recipes['compressing']['recipes']) do
+ if def.output and def.output == item_name then
+ recipes[#recipes + 1] = {
+ method = S('compressing'),
+ type = 'technic:compress',
+ items = { input_name .. ' ' .. tostring(def.input[input_name]) },
+ output = item_name
+ }
+ end
+ end
+end -- add_recipe_compress
+
+replacer.register_craft_method('technic:compress', 'technic:lv_compressor', add_recipe_compress)
+
+
+local function add_recipe_extract(item_name, _, recipes)
+--pd(technic.recipes['extracting'])
+ for input_name, def in pairs(technic.recipes['extracting']['recipes']) do
+ if def.output and 'string' == type(def.output)
+ and def.output:find('^' .. item_name .. ' ?[0-9]*$')
+ then
+ recipes[#recipes + 1] = {
+ method = S('extracting'),
+ type = 'technic:extract',
+ items = { input_name .. ' ' .. tostring(def.input[input_name]) },
+ output = def.output
+ }
+ end
+ end
+end -- add_recipe_extract
+
+replacer.register_craft_method('technic:extract', 'technic:lv_extractor', add_recipe_extract)
+
+
+local function add_recipe_freeze(item_name, _, recipes)
+--pd(technic.recipes['freezing'])
+ local def_out_type, outputs, main_output
+ for input_name, def in pairs(technic.recipes['freezing']['recipes']) do
+ def_out_type = type(def.output)
+ if 'string' == def_out_type
+ and def.output:find('^' .. item_name .. ' ?[0-9]*$')
+ then
+ recipes[#recipes + 1] = {
+ method = S('freezing'),
+ type = 'technic:freeze',
+ items = { input_name },
+ output = def.output,
+ }
+
+ elseif 'table' == def_out_type then
+ outputs, main_output = {}, nil
+ for _, output_string in ipairs(def.output) do
+ if output_string:find('^' .. item_name .. ' ?[0-9]*$') then
+ main_output = output_string
+ else
+ table.insert(outputs, output_string)
+ end
+ end
+ if main_output then
+ recipes[#recipes + 1] = {
+ method = 'freezing',
+ type = 'technic:freeze',
+ items = { input_name .. ' '.. tostring(def.input[input_name]) },
+ output = main_output,
+ output_other = outputs,
+ }
+ end
+ end
+ end
+end -- add_recipe_freeze
+
+local function add_formspec_freeze(recipe)
+ if not recipe.output_other or 0 == #recipe.output_other then
+ return ''
+ end
+
+ local out = 'item_image_button[5,3;1.0,1.0;'
+ .. replacer.image_button_link(recipe.output_other[1]) .. ']'
+ if recipe.output_other[2] then
+ out = out .. 'item_image_button[5,1;1.0,1.0;'
+ .. replacer.image_button_link(recipe.output_other[2]) .. ']'
+ end
+ return out
+end -- add_formspec_freeze
+
+replacer.register_craft_method(
+ 'technic:freeze', 'technic:mv_freezer', add_recipe_freeze, add_formspec_freeze)
+
+
+local function add_recipe_grind(item_name, _, recipes)
+--pd(technic.recipes['grinding'])
+ local inputs, input_type
+ for _, def in pairs(technic.recipes['grinding']['recipes']) do
+ if 'string' == type(def.output)
+ and def.output:find('^' .. item_name .. ' ?[0-9]*$')
+ then
+ input_type = type(def.input)
+ if 'string' == input_type then
+ inputs = { def.input }
+ elseif 'table' == input_type then
+ inputs = {}
+ -- there is only one, but that's how lua works so we need to loop
+ for k, v in pairs(def.input) do
+ table.insert(inputs, k .. ' ' .. tostring(v))
+ end
+ else
+ inputs = {}
+ end
+ recipes[#recipes + 1] = {
+ method = S('grinding'),
+ type = 'technic:grind',
+ items = inputs,
+ output = def.output
+ }
+ end
+ end
+end -- add_recipe_grind
+
+replacer.register_craft_method('technic:grind', 'technic:lv_grinder', add_recipe_grind)
+
+
+local function add_recipe_separate(item_name, _, recipes)
+--pd(technic.recipes['separating'])
+ local outputs, main_output, inputs
+ for _, def in pairs(technic.recipes['separating']['recipes']) do
+ if def.output and 'table' == type(def.output)
+ and def.input and 'table' == type(def.input)
+ then
+ outputs, main_output = {}, nil
+ for _, output_string in ipairs(def.output) do
+ if output_string:find('^' .. item_name .. ' ?[0-9]*$') then
+ main_output = output_string
+ else
+ table.insert(outputs, output_string)
+ end
+ end
+ if main_output then
+ inputs = {}
+ -- there is only one, but that's how lua works so we need to loop
+ for k, v in pairs(def.input) do
+ table.insert(inputs, k .. ' ' .. tostring(v))
+ end
+ recipes[#recipes + 1] = {
+ method = S('separating'),
+ type = 'technic:separate',
+ items = inputs,
+ output = main_output,
+ output_other = outputs,
+ }
+ end -- if found
+ end -- if output is table
+ end -- loop
+end -- add_recipe_separate
+
+local function add_formspec_separate(recipe)
+ if not recipe.output_other or 0 == #recipe.output_other then
+ return ''
+ end
+
+ local out = 'item_image_button[5,3;1.0,1.0;'
+ .. replacer.image_button_link(recipe.output_other[1]) .. ']'
+ if recipe.output_other[2] then
+ out = out .. 'item_image_button[5,1;1.0,1.0;'
+ .. replacer.image_button_link(recipe.output_other[2]) .. ']'
+ end
+ return out
+end -- add_formspec_separate
+
+replacer.register_craft_method(
+ 'technic:separate', 'technic:mv_centrifuge',
+ add_recipe_separate, add_formspec_separate)
+
+
+local function add_recipe_can_lava(item_name, _, recipes)
+ local item, method
+ if 'technic:lava_can' == item_name then
+ item = 'default:lava_source'
+ method = rbi.scoop
+ elseif 'default:lava_source' == item_name then
+ item = 'technic:lava_can'
+ method = rbi.pour
+ else
+ return
+ end
+
+ recipes[#recipes + 1] = {
+ method = method,
+ type = 'technic:lava_can',
+ items = { item },
+ output = item_name,
+ }
+end -- add_recipe_can_lava
+
+replacer.register_craft_method(
+ 'technic:lava_can', 'technic:lava_can', add_recipe_can_lava)
+
+
+local function add_recipe_can_river(item_name, _, recipes)
+ local item, method
+ if 'technic:river_water_can' == item_name then
+ item = 'default:river_water_source'
+ method = rbi.scoop
+ elseif 'default:river_water_source' == item_name then
+ item = 'technic:river_water_can'
+ method = rbi.pour
+ else
+ return
+ end
+
+ recipes[#recipes + 1] = {
+ method = method,
+ type = 'technic:river_water_can',
+ items = { item },
+ output = item_name,
+ }
+end -- add_recipe_can_river
+
+replacer.register_craft_method(
+ 'technic:river_water_can', 'technic:river_water_can', add_recipe_can_river)
+
+
+local function add_recipe_can_water(item_name, _, recipes)
+ local item, method
+ if 'technic:water_can' == item_name then
+ item = 'default:water_source'
+ method = rbi.scoop
+ elseif 'default:water_source' == item_name then
+ item = 'technic:water_can'
+ method = rbi.pour
+ else
+ return
+ end
+
+ recipes[#recipes + 1] = {
+ method = method,
+ type = 'technic:water_can',
+ items = { item },
+ output = item_name,
+ }
+end -- add_recipe_can_water
+
+replacer.register_craft_method(
+ 'technic:water_can', 'technic:water_can', add_recipe_can_water)
+
diff --git a/compat/telemosaic.lua b/compat/telemosaic.lua
new file mode 100644
index 0000000..c153fa6
--- /dev/null
+++ b/compat/telemosaic.lua
@@ -0,0 +1,7 @@
+if not minetest.get_modpath('telemosaic') then return end
+
+local rgp = replacer.group_placeholder
+rgp['group:telemosaic_extender_one'] = 'telemosaic:extender_one'
+rgp['group:telemosaic_extender_two'] = 'telemosaic:extender_two'
+rgp['group:telemosaic_extender_three'] = 'telemosaic:extender_three'
+
diff --git a/compat/tnt.lua b/compat/tnt.lua
new file mode 100644
index 0000000..d5aa66f
--- /dev/null
+++ b/compat/tnt.lua
@@ -0,0 +1,7 @@
+-- playing with tnt and creative building are usually contradictory
+-- (except when doing large-scale landscaping in singleplayer)
+replacer.deny_list['tnt:boom'] = true
+replacer.deny_list['tnt:gunpowder'] = true
+replacer.deny_list['tnt:gunpowder_burning'] = true
+replacer.deny_list['tnt:tnt'] = true
+
diff --git a/compat/travelnet.lua b/compat/travelnet.lua
new file mode 100644
index 0000000..1cb3e68
--- /dev/null
+++ b/compat/travelnet.lua
@@ -0,0 +1,5 @@
+if not minetest.get_modpath('telemosaic') then return end
+
+local rgp = replacer.group_placeholder
+rgp['group:travelnet'] = 'travelnet:travelnet'
+
diff --git a/compat/unifieddyes.lua b/compat/unifieddyes.lua
new file mode 100644
index 0000000..748e55b
--- /dev/null
+++ b/compat/unifieddyes.lua
@@ -0,0 +1,83 @@
+replacer.unifieddyes = {}
+local ud = replacer.unifieddyes
+
+if not replacer.has_unifieddyes_mod then
+ -- replacer uses this
+ function ud.colour_name() return '' end
+ return
+end
+
+local make_readable_color = unifieddyes.make_readable_color
+local colour_to_name = unifieddyes.color_to_name
+
+-- for inspection tool formspec
+local S = replacer.S
+local function add_recipe(node_name, context, recipes)
+ if not (context and context.param2) then return end
+
+ local param2 = context.param2
+ local node_def = minetest.registered_items[node_name]
+ if ud.is_airbrushed(node_def) then
+ -- find the correct recipe and append it to bottom of list
+ local first
+ local needle = 'u0002' .. tostring(param2)
+ for _, t in ipairs(recipes) do
+ first = t.output:find(needle)
+ if nil ~= first then
+ t.method = S('painting')
+ t.type = 'unifieddyes:airbrush'
+ recipes[#recipes + 1] = t
+ return
+ end
+ end
+ end
+end -- add_recipe
+
+replacer.register_craft_method('unifieddyes:airbrush', 'unifieddyes:airbrush', add_recipe)
+
+
+function replacer.unifieddyes.colour_name(param2, node_def)
+ param2 = tonumber(param2)
+ if param2 and ud.is_airbrushed(node_def) then
+ return make_readable_color(
+ colour_to_name(param2, node_def))
+ else
+ return ''
+ end
+end -- colour_name
+
+
+function replacer.unifieddyes.dye_name(param2, node_def)
+ param2 = tonumber(param2)
+ if param2 and ud.is_airbrushed(node_def) then
+ return 'dye:' .. colour_to_name(param2, node_def)
+ else
+ return ''
+ end
+end -- dye_name
+
+
+function replacer.unifieddyes.is_airbrush_compatible(node_def)
+ return node_def and node_def.palette
+ and node_def.groups and node_def.groups.ud_param2_colorable
+ and 0 < node_def.groups.ud_param2_colorable
+end -- is_airbrush_compatible
+
+
+function replacer.unifieddyes.is_airbrushed(node_def)
+ if not ud.is_airbrush_compatible(node_def) then
+ return false
+ end
+ if nil ~= node_def.name:find('_tinted$') then
+ return true
+ end
+ return not node_def.airbrush_replacement_node
+end -- is_airbrushed
+
+
+-- mostly for scifi_nodes plastic.
+replacer.register_set_enabler(function(node)
+ return node and node.name
+ and ud.is_airbrush_compatible(minetest.registered_nodes[node.name])
+end)
+
diff --git a/compat/vines.lua b/compat/vines.lua
new file mode 100644
index 0000000..ed39ce9
--- /dev/null
+++ b/compat/vines.lua
@@ -0,0 +1,45 @@
+if not minetest.get_modpath('vines') then return end
+
+local S = replacer.S
+
+-- for replacer, so player can set to any part of vine and then replacer
+-- uses the '_end' part to place
+local cutables = {}
+local vines = { 'jungle', 'root', 'side', 'vine', 'willow' }
+local name_base, name_end, name_middle
+for _, name in ipairs(vines) do
+ name_base = 'vines:' .. name
+ name_end = name_base .. '_end'
+ name_middle = name_base .. '_middle'
+ cutables[name_end] = name_end
+ cutables[name_middle] = name_end
+ replacer.register_exception(name_end, name_end)
+ replacer.register_non_creative_alias(name_middle, name_end)
+end
+replacer.register_non_creative_alias('vines:rope', 'vines:rope_block')
+replacer.register_non_creative_alias('vines:rope_end', 'vines:rope_block')
+
+
+-- for inspection tool
+replacer.group_placeholder['group:vines'] = 'vines:vines'
+
+
+local function add_recipe(item_name, _, recipes)
+ local output = cutables[item_name]
+ if not output then return end
+
+ recipes[#recipes + 1] = {
+ method = 'cutting',
+ type = 'vines:cut',
+ items = { output }, -- not 'correct' but should do the job
+ output = item_name,
+ }
+end -- add_recipe
+
+--luacheck: no unused args
+local function add_formspec(recipe)
+ return 'label[0.5,3.5;' .. S('Cut with shears.') .. ']'
+end
+
+replacer.register_craft_method('vines:cut', 'vines:shears', add_recipe, add_formspec)
+
diff --git a/compat/wine.lua b/compat/wine.lua
new file mode 100644
index 0000000..46307d0
--- /dev/null
+++ b/compat/wine.lua
@@ -0,0 +1,45 @@
+if not minetest.get_modpath('wine') then return end
+
+local S = replacer.S
+
+-- for inspection tool
+-- order preserved from wines mod
+local in_out = {}
+in_out['wine:glass_wine'] = 'farming:grapes'
+in_out['wine:glass_beer'] = 'farming:barley'
+in_out['wine:glass_mead'] = 'mobs:honey'
+in_out['wine:glass_mead'] = 'xdecor:honey'
+in_out['wine:glass_cider'] = 'default:apple'
+in_out['wine:glass_rum'] = 'default:papyrus'
+in_out['wine:glass_tequila'] = 'wine:blue_agave'
+in_out['wine:glass_wheat_beer'] = 'farming:wheat'
+in_out['wine:glass_sake'] = 'farming:rice'
+in_out['wine:glass_bourbon'] = 'farming:corn'
+in_out['wine:glass_vodka'] = 'farming:baked_potato'
+in_out['wine:glass_coffee_liquor'] = 'farming:coffee_beans'
+in_out['wine:glass_champagne'] = 'wine:glass_champagne_raw'
+
+local function add_recipe(item_name, _, recipes)
+ if 'string' ~= type(item_name)
+ or not item_name:find('^wine:glass_') then return end
+
+ -- this one is an exception
+ if 'wine:glass_champagne_raw' == item_name then return end
+
+ -- allow new items to show up using air as icon
+ local input = in_out[item_name] or 'air'
+ recipes[#recipes + 1] = {
+ method = S('fermenting'),
+ type = 'wine:ferment',
+ items = { input },
+ output = item_name,
+ }
+end -- add_recipe
+
+--luacheck: no unused args
+local function add_formspec(recipe)
+ return 'label[0.5,3.5;' .. S('Ferment in barrel.') .. ']'
+end
+
+replacer.register_craft_method('wine:ferment', 'wine:wine_barrel', add_recipe, add_formspec)
+
diff --git a/crafts.lua b/crafts.lua
new file mode 100644
index 0000000..2e3dc26
--- /dev/null
+++ b/crafts.lua
@@ -0,0 +1,46 @@
+
+if not replacer.hide_recipe_basic then
+ minetest.register_craft({
+ output = replacer.tool_name_basic,
+ recipe = {
+ { 'default:chest', '', 'default:gold_ingot' },
+ { '', 'default:mese_crystal_fragment', '' },
+ { 'default:steel_ingot', '', 'default:chest' },
+ }
+ })
+end
+
+
+-- only if technic mod is installed
+if replacer.has_technic_mod then
+ if not replacer.hide_recipe_technic_upgrade then
+ minetest.register_craft({
+ output = replacer.tool_name_technic,
+ recipe = {
+ { replacer.tool_name_basic, 'technic:green_energy_crystal', '' },
+ { '', '', '' },
+ { '', '', '' },
+ }
+ })
+ end
+ if not replacer.hide_recipe_technic_direct then
+ -- direct upgrade craft
+ minetest.register_craft({
+ output = replacer.tool_name_technic,
+ recipe = {
+ { 'default:chest', 'technic:green_energy_crystal', 'default:gold_ingot' },
+ { '', 'default:mese_crystal_fragment', '' },
+ { 'default:steel_ingot', '', 'default:chest' },
+ }
+ })
+ end
+end
+
+
+minetest.register_craft({
+ output = 'replacer:inspect',
+ recipe = {
+ { 'default:torch' },
+ { 'default:stick' },
+ }
+})
diff --git a/depends.txt b/depends.txt
deleted file mode 100644
index afef51e..0000000
--- a/depends.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-default?
-dye?
-technic
diff --git a/doc/customization.md b/doc/customization.md
new file mode 100644
index 0000000..3245f76
--- /dev/null
+++ b/doc/customization.md
@@ -0,0 +1,173 @@
+Customization documentation for replacer minetest mod
+=====================================================
+
+- [Settings](#settings)
+- [API Commands](#api-commands)
+- [Inspection Tool](#inspection-tool)
+
+## Settings
+
+_default values in parentheses ()_
+All the setting names correspond to equivalents in Lua namespace. Allowing them to be
+changed "on the fly". The recipe related ones won't have any effect though.
+For history related settings, users will have to relog or have their history priv
+revoked and granted again for the values to take effect correctly.
+
+### replacer.max_nodes (3168)
+Replace / place up to this many nodes when using modes other than single.
+Depending on server hardware and amount of users, this value needs adapting.
+On singleplayer you can mostly use a higher value.
+
+### replacer.max_time (1.0)
+Some nodes take a long time to be placed. This value limits the time in seconds
+in which the nodes are placed. This prevents more lag on an already lagging server
+with a high replacer.max_nodes setting.
+This time does not include the time used to search for nodes, only the time used
+to replace them is measured and limited by this value.
+
+### replacer.radius_factor (0.4)
+Radius limit factor when more possible positions are found than either max_nodes or charge
+allow. Positions are traversed again and only those within radius * this factor are
+passed back for replacement. Generates nice circles in field mode.
+Radius == floor(max_positions ^ radius_factor + .5) where max_positions is a min(max())
+of available charge and max_nodes. Small changes to this value can have big effects.
+Set radius_factor to 0 or less for behaviour prior to version 3.3
+
+### replacer.disable_minor_modes (bool)
+If you don't want to use the minor modes at all, set to true. These are the modes where
+only node or rotation is applied.
+
+### replacer.history_priv (creative)
+You can make history available to users with this priv. By default it is set to **creative**
+as survival users can make several replacers. You can make this an acheivment for busy players
+to work towards, or set to **interact** to allow any player to use history of previously
+used node settings.
+
+### replacer.history_disable_persistancy (false)
+When set, does not save history over sessions. Reason might be old MT version.
+Currently history is stored in player's meta on logoff and at intervals.
+
+### replacer.history_save_interval (7)
+How frequently, in minutes, history is saved to player-meta.
+Only users with the priv are affected.
+
+### replacer.history_include_mode (false)
+When set, changes the replacer's major and minor modes when picking an item from history.
+The modes are stored either way.
+
+### replacer.history_max (7)
+Limits history length. Duplicates are removed so there isn't much need for long histories.
+
+### replacer.hide_recipe_basic (false)
+You may choose to hide basic recipe but then make sure to enable the technic direct one
+or add your own registration. Reason might be that you want another recipe and don't
+want to use an override.
+
+### replacer.hide_recipe_technic_upgrade (false)
+Hides the upgrade recipe.
+Only available if technic is installed.
+
+### replacer.hide_recipe_technic_direct (true)
+Hides the direct recipe of technic replacer that does not require a basic replacer as
+ingredient.
+Only available if technic is installed.
+
+### replacer.dev_mode (false)
+Enable developer mode which gives users with **priv** priv to run **/place_all** chat command.
+This is not recommended on live servers as some nodes your mods provide may crash the server
+when placed this way. [Read the comments in (test.lua)](test.lua)
+
+## API commands
+
+### Deny Groups
+You can add groups that you don't want your users to be able to use replacer with.
+For example by default items from **group:seed** are forbidden.
+```lua
+replacer.deny_groups['seed'] = true
+```
+
+### Deny Nodes
+A selection of nodes are added by default, such as tnt:* and protectors.
+You may want to deny the replacement **and** placement of certain nodes.
+```lua
+replacer.deny_list['tnt:boom'] = true
+```
+
+### Limit Node Count
+This setting will be clamped to **replacer.max_nodes** if it exceeds it.
+If you pass 0, the node will be added to **deny_list**. Negative numbers are ignored.
+```lua
+replacer.register_limit('beacon:red', 5)
+```
+Above snippet limits technic replacer to only place maximum 5 red beacon boxes per usage.
+
+### Max Technic Replacer Charge
+Bellow example reduces the amount of charge a technic replacer can carry.
+```lua
+replacer.max_charge = 10000
+```
+
+### Charge per Node
+Bellow example increases the amount of charge a technic replacer uses to place/replace a node.
+```lua
+replacer.charge_per_node = 30
+```
+
+### Replace Intervention
+You can override ```replacer.permit_replace(pos, old_node_def, new_node_def, player_ref, player_name, player_inv, creative_or_give)```
+function to implement server specific rules about where, when, who may place/replace what.
+E.g. check if player has sufficient funds or privs to be using replacer in a certain region.
+[Read more about this function in (replacer/constrain.lua)](replacer/constrain.lua)
+
+### Enable Special Nodes
+Register exceptions that don't rotate/colour using param1 and param2 by calling
+```lua
+replacer.register_exception(node_name, drop_name, callback)
+```
+* **node_name** is the name of the node user clicks on.
+* **drop_name** is the name of the item to be taken from inventory.
+* **callback** is an optional function that is called after **drop_name** has been placed.
+This function can apply other changes or build structures around the placed node. Your
+imagination is the limit. (Well computational resources too.)
+The callback signature is: ```f(pos, old_node_def, new_node_def, player_ref)```
+[More details in (replacer/enable.lua)](replacer/enable.lua)
+
+### Aliases
+For players without **give** or **creative** priv, you can add aliases.
+```lua
+replacer.register_non_creative_alias('vines:jungle_middle', 'vines:jungle_end')
+```
+This allows users to click on "vines:jungle_middle" but set the replacer to "vines:jungle_end".
+Many examples of these can be found in the "compat" directory.
+
+### Enable Set Callbacks
+Some nodes don't show up in crafting guide and the above methods don't suffice.
+To still enable these, you can register a callback function which is called
+after several pre-checks have passed. The first callback to respond with something
+other than **false** or **nil** allows the node to be used to set the replacer to.
+The callback signature is ```f(node, player_ref, pointed_thing)```
+```lua
+replacer.register_set_enabler(callback)
+```
+[More details in (replacer/enable.lua)](replacer/enable.lua)
+
+## Inspection Tool
+
+### adding craft methods
+Some mods provide precesses that go beyond simple crafting, mixing or cooking. To provide
+better support for those there is:
+```lua
+replacer.register_craft_method(uid, machine_itemstring, func_inspect, func_formspec)
+```
+* **uid** is a unique identifier for this method/mod. A good format is "mod_name:method_name".
+* **machine_itemstring** is the node name that provides the service. It is used to lookup
+the image displayed and the recipe of how to make the machine.
+* **func_inspect** is a function that is called by the inspection tool when gathering
+information for an item. It's signature is ```f(node_name, param2, recipes)``` and it
+can manipulate **recipes** table adding more recipes.
+* **func_formspec** is an optional function that is called when displaying the craft info.
+The signature is ```f(recipe)``` where **recipe** is the recipe table **func_inspect** added.
+It returns a formspec string to be added to the main formspec.
+[It is defined in (inspect.lua)](inspect.lua)
+[Best examples of usage in (compat/technic.lua)](compat/technic.lua)
+
diff --git a/doc/usage.md b/doc/usage.md
new file mode 100644
index 0000000..8c14999
--- /dev/null
+++ b/doc/usage.md
@@ -0,0 +1,40 @@
+
+## Replacer Modes
+
+Situation: the player points at a node and wants to use the replacer.
+* Bright blue: the ray of sight and pointed thing above and under
+* Different nodes:
+ * White: air
+ * Dark grey: a solid node which should be replaced
+ * Brown: another solid node adjacent to the grey one
+ * Yellow green: a translucent node, such as leaves or glass
+ * Red (in later pictures): the node which the replacer (re)places
+* The black thing: it should depict an eye for the camera position.
+
+
+
+
+### Single Mode
+
+Left click:
+
+
+Right click:
+
+
+### Field Mode
+
+The replacer changes nodes in a 2D slice (it is 1D in these illustrations).
+Left click:
+
+Right click:
+
+
+### Crust Mode
+
+Left click: the replacer changes visually adjacent nodes (of the same type) on a surface
+
+
+Right click: the replacer places nodes onto the surface so that the surface below it is hidden; the added crust can be bounded by other solid nodes but not translucent nodes
+
+
diff --git a/doc/usageCrust.md b/doc/usageCrust.md
new file mode 100644
index 0000000..8717be5
--- /dev/null
+++ b/doc/usageCrust.md
@@ -0,0 +1,16 @@
+## Crust Mode
+
+### Left click:
+The replacer changes visually adjacent nodes (of the same type) on a surface
+
+Before is depicted above and after is below.
+
+
+
+### Right click:
+The replacer places nodes onto the surface so that the surface below it is hidden; the added crust can be bounded by other solid nodes but not translucent nodes.
+
+
+Before is depicted above and after is below.
+
+
diff --git a/doc/usageField.md b/doc/usageField.md
new file mode 100644
index 0000000..b308266
--- /dev/null
+++ b/doc/usageField.md
@@ -0,0 +1,17 @@
+## Field Mode
+
+
+### Left click:
+The replacer changes nodes in a 2D slice.
+
+
+Before is depicted above and after is below.
+
+
+### Right click:
+The replacer places nodes in a 2D slice onto given surface following contour.
+
+
+Before is depicted above and after is below.
+
+
diff --git a/doc/usageSingle.md b/doc/usageSingle.md
new file mode 100644
index 0000000..b6f837a
--- /dev/null
+++ b/doc/usageSingle.md
@@ -0,0 +1,15 @@
+## Single Mode
+
+### Left click:
+The replacer changes the clicked node.
+
+
+Before is depicted above and after is below.
+
+
+### Right click:
+The replacer places a node onto the surface.
+
+
+Before is depicted above and after is below.
+
diff --git a/i18n.py b/i18n.py
new file mode 100755
index 0000000..67305b0
--- /dev/null
+++ b/i18n.py
@@ -0,0 +1,477 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Script to generate the template file and update the translation files.
+# Copy the script into the mod or modpack root folder and run it there.
+#
+# Copyright (C) 2019 Joachim Stolberg, 2020 FaceDeer, 2020 Louis Royer
+# LGPLv2.1+
+#
+# See https://github.com/minetest-tools/update_translations for
+# potential future updates to this script.
+
+from __future__ import print_function
+import os, fnmatch, re, shutil, errno
+from sys import argv as _argv
+from sys import stderr as _stderr
+
+# Running params
+params = {"recursive": False,
+ "help": False,
+ "mods": False,
+ "verbose": False,
+ "folders": [],
+ "no-old-file": True,
+ "break-long-lines": True,
+ "sort": False,
+ "print-source": False,
+ "truncate-unused": False,
+}
+# Available CLI options
+options = {"recursive": ['--recursive', '-r'],
+ "help": ['--help', '-h'],
+ "mods": ['--installed-mods', '-m'],
+ "verbose": ['--verbose', '-v'],
+ "no-old-file": ['--no-old-file', '-O'],
+ "break-long-lines": ['--break-long-lines', '-b'],
+ "sort": ['--sort', '-s'],
+ "print-source": ['--print-source', '-p'],
+ "truncate-unused": ['--truncate-unused', '-t'],
+}
+
+# Strings longer than this will have extra space added between
+# them in the translation files to make it easier to distinguish their
+# beginnings and endings at a glance
+doublespace_threshold = 80
+
+def set_params_folders(tab: list):
+ '''Initialize params["folders"] from CLI arguments.'''
+ # Discarding argument 0 (tool name)
+ for param in tab[1:]:
+ stop_param = False
+ for option in options:
+ if param in options[option]:
+ stop_param = True
+ break
+ if not stop_param:
+ params["folders"].append(os.path.abspath(param))
+
+def set_params(tab: list):
+ '''Initialize params from CLI arguments.'''
+ for option in options:
+ for option_name in options[option]:
+ if option_name in tab:
+ params[option] = True
+ break
+
+def print_help(name):
+ '''Prints some help message.'''
+ print(f'''SYNOPSIS
+ {name} [OPTIONS] [PATHS...]
+DESCRIPTION
+ {', '.join(options["help"])}
+ prints this help message
+ {', '.join(options["recursive"])}
+ run on all subfolders of paths given
+ {', '.join(options["mods"])}
+ run on locally installed modules
+ {', '.join(options["no-old-file"])}
+ do not create *.old files
+ {', '.join(options["sort"])}
+ sort output strings alphabetically
+ {', '.join(options["break-long-lines"])}
+ add extra line breaks before and after long strings
+ {', '.join(options["print-source"])}
+ add comments denoting the source file
+ {', '.join(options["verbose"])}
+ add output information
+ {', '.join(options["truncate-unused"])}
+ delete unused strings from files
+''')
+
+
+def main():
+ '''Main function'''
+ set_params(_argv)
+ set_params_folders(_argv)
+ if params["help"]:
+ print_help(_argv[0])
+ elif params["recursive"] and params["mods"]:
+ print("Option --installed-mods is incompatible with --recursive")
+ else:
+ # Add recursivity message
+ print("Running ", end='')
+ if params["recursive"]:
+ print("recursively ", end='')
+ # Running
+ if params["mods"]:
+ print(f"on all locally installed modules in {os.path.expanduser('~/.minetest/mods/')}")
+ run_all_subfolders(os.path.expanduser("~/.minetest/mods"))
+ elif len(params["folders"]) >= 2:
+ print("on folder list:", params["folders"])
+ for f in params["folders"]:
+ if params["recursive"]:
+ run_all_subfolders(f)
+ else:
+ update_folder(f)
+ elif len(params["folders"]) == 1:
+ print("on folder", params["folders"][0])
+ if params["recursive"]:
+ run_all_subfolders(params["folders"][0])
+ else:
+ update_folder(params["folders"][0])
+ else:
+ print("on folder", os.path.abspath("./"))
+ if params["recursive"]:
+ run_all_subfolders(os.path.abspath("./"))
+ else:
+ update_folder(os.path.abspath("./"))
+
+#group 2 will be the string, groups 1 and 3 will be the delimiters (" or ')
+#See https://stackoverflow.com/questions/46967465/regex-match-text-in-either-single-or-double-quote
+pattern_lua_s = re.compile(r'[\.=^\t,{\(\s]N?S\(\s*(["\'])((?:\\\1|(?:(?!\1)).)*)(\1)[\s,\)]', re.DOTALL)
+pattern_lua_fs = re.compile(r'[\.=^\t,{\(\s]N?FS\(\s*(["\'])((?:\\\1|(?:(?!\1)).)*)(\1)[\s,\)]', re.DOTALL)
+pattern_lua_bracketed_s = re.compile(r'[\.=^\t,{\(\s]N?S\(\s*\[\[(.*?)\]\][\s,\)]', re.DOTALL)
+pattern_lua_bracketed_fs = re.compile(r'[\.=^\t,{\(\s]N?FS\(\s*\[\[(.*?)\]\][\s,\)]', re.DOTALL)
+
+# Handles "concatenation" .. " of strings"
+pattern_concat = re.compile(r'["\'][\s]*\.\.[\s]*["\']', re.DOTALL)
+
+pattern_tr = re.compile(r'(.*?[^@])=(.*)')
+pattern_name = re.compile(r'^name[ ]*=[ ]*([^ \n]*)')
+pattern_tr_filename = re.compile(r'\.tr$')
+pattern_po_language_code = re.compile(r'(.*)\.po$')
+
+#attempt to read the mod's name from the mod.conf file or folder name. Returns None on failure
+def get_modname(folder):
+ try:
+ with open(os.path.join(folder, "mod.conf"), "r", encoding='utf-8') as mod_conf:
+ for line in mod_conf:
+ match = pattern_name.match(line)
+ if match:
+ return match.group(1)
+ except FileNotFoundError:
+ if not os.path.isfile(os.path.join(folder, "modpack.txt")):
+ folder_name = os.path.basename(folder)
+ # Special case when run in Minetest's builtin directory
+ if folder_name == "builtin":
+ return "__builtin"
+ else:
+ return folder_name
+ else:
+ return None
+ return None
+
+#If there are already .tr files in /locale, returns a list of their names
+def get_existing_tr_files(folder):
+ out = []
+ for root, dirs, files in os.walk(os.path.join(folder, 'locale/')):
+ for name in files:
+ if pattern_tr_filename.search(name):
+ out.append(name)
+ return out
+
+# A series of search and replaces that massage a .po file's contents into
+# a .tr file's equivalent
+def process_po_file(text):
+ # The first three items are for unused matches
+ text = re.sub(r'#~ msgid "', "", text)
+ text = re.sub(r'"\n#~ msgstr ""\n"', "=", text)
+ text = re.sub(r'"\n#~ msgstr "', "=", text)
+ # comment lines
+ text = re.sub(r'#.*\n', "", text)
+ # converting msg pairs into "=" pairs
+ text = re.sub(r'msgid "', "", text)
+ text = re.sub(r'"\nmsgstr ""\n"', "=", text)
+ text = re.sub(r'"\nmsgstr "', "=", text)
+ # various line breaks and escape codes
+ text = re.sub(r'"\n"', "", text)
+ text = re.sub(r'"\n', "\n", text)
+ text = re.sub(r'\\"', '"', text)
+ text = re.sub(r'\\n', '@n', text)
+ # remove header text
+ text = re.sub(r'=Project-Id-Version:.*\n', "", text)
+ # remove double-spaced lines
+ text = re.sub(r'\n\n', '\n', text)
+ return text
+
+# Go through existing .po files and, if a .tr file for that language
+# *doesn't* exist, convert it and create it.
+# The .tr file that results will subsequently be reprocessed so
+# any "no longer used" strings will be preserved.
+# Note that "fuzzy" tags will be lost in this process.
+def process_po_files(folder, modname):
+ for root, dirs, files in os.walk(os.path.join(folder, 'locale/')):
+ for name in files:
+ code_match = pattern_po_language_code.match(name)
+ if code_match == None:
+ continue
+ language_code = code_match.group(1)
+ tr_name = f'{modname}.{language_code}.tr'
+ tr_file = os.path.join(root, tr_name)
+ if os.path.exists(tr_file):
+ if params["verbose"]:
+ print(f"{tr_name} already exists, ignoring {name}")
+ continue
+ fname = os.path.join(root, name)
+ with open(fname, "r", encoding='utf-8') as po_file:
+ if params["verbose"]:
+ print(f"Importing translations from {name}")
+ text = process_po_file(po_file.read())
+ with open(tr_file, "wt", encoding='utf-8') as tr_out:
+ tr_out.write(text)
+
+# from https://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python/600612#600612
+# Creates a directory if it doesn't exist, silently does
+# nothing if it already exists
+def mkdir_p(path):
+ try:
+ os.makedirs(path)
+ except OSError as exc: # Python >2.5
+ if exc.errno == errno.EEXIST and os.path.isdir(path):
+ pass
+ else: raise
+
+# Converts the template dictionary to a text to be written as a file
+# dKeyStrings is a dictionary of localized string to source file sets
+# dOld is a dictionary of existing translations and comments from
+# the previous version of this text
+def strings_to_text(dkeyStrings, dOld, mod_name, header_comments):
+ lOut = [f"# textdomain: {mod_name}"]
+ if header_comments is not None:
+ lOut.append(header_comments)
+
+ dGroupedBySource = {}
+
+ for key in dkeyStrings:
+ sourceList = list(dkeyStrings[key])
+ if params["sort"]:
+ sourceList.sort()
+ sourceString = "\n".join(sourceList)
+ listForSource = dGroupedBySource.get(sourceString, [])
+ listForSource.append(key)
+ dGroupedBySource[sourceString] = listForSource
+
+ lSourceKeys = list(dGroupedBySource.keys())
+ lSourceKeys.sort()
+ for source in lSourceKeys:
+ localizedStrings = dGroupedBySource[source]
+ if params["sort"]:
+ localizedStrings.sort()
+ if params["print-source"]:
+ if lOut[-1] != "":
+ lOut.append("")
+ lOut.append(source)
+ for localizedString in localizedStrings:
+ val = dOld.get(localizedString, {})
+ translation = val.get("translation", "")
+ comment = val.get("comment")
+ if params["break-long-lines"] and len(localizedString) > doublespace_threshold and not lOut[-1] == "":
+ lOut.append("")
+ if comment != None and comment != "" and not comment.startswith("# textdomain:"):
+ lOut.append(comment)
+ lOut.append(f"{localizedString}={translation}")
+ if params["break-long-lines"] and len(localizedString) > doublespace_threshold:
+ lOut.append("")
+
+
+ unusedExist = False
+ if not params["truncate-unused"]:
+ for key in dOld:
+ if key not in dkeyStrings:
+ val = dOld[key]
+ translation = val.get("translation")
+ comment = val.get("comment")
+ # only keep an unused translation if there was translated
+ # text or a comment associated with it
+ if translation != None and (translation != "" or comment):
+ if not unusedExist:
+ unusedExist = True
+ lOut.append("\n\n##### not used anymore #####\n")
+ if params["break-long-lines"] and len(key) > doublespace_threshold and not lOut[-1] == "":
+ lOut.append("")
+ if comment != None:
+ lOut.append(comment)
+ lOut.append(f"{key}={translation}")
+ if params["break-long-lines"] and len(key) > doublespace_threshold:
+ lOut.append("")
+ return "\n".join(lOut) + '\n'
+
+# Writes a template.txt file
+# dkeyStrings is the dictionary returned by generate_template
+def write_template(templ_file, dkeyStrings, mod_name):
+ # read existing template file to preserve comments
+ existing_template = import_tr_file(templ_file)
+
+ text = strings_to_text(dkeyStrings, existing_template[0], mod_name, existing_template[2])
+ mkdir_p(os.path.dirname(templ_file))
+ with open(templ_file, "wt", encoding='utf-8') as template_file:
+ template_file.write(text)
+
+
+# Gets all translatable strings from a lua file
+def read_lua_file_strings(lua_file):
+ lOut = []
+ with open(lua_file, encoding='utf-8') as text_file:
+ text = text_file.read()
+ #TODO remove comments here
+
+ text = re.sub(pattern_concat, "", text)
+
+ strings = []
+ for s in pattern_lua_s.findall(text):
+ strings.append(s[1])
+ for s in pattern_lua_bracketed_s.findall(text):
+ strings.append(s)
+ for s in pattern_lua_fs.findall(text):
+ strings.append(s[1])
+ for s in pattern_lua_bracketed_fs.findall(text):
+ strings.append(s)
+
+ for s in strings:
+ s = re.sub(r'"\.\.\s+"', "", s)
+ s = re.sub("@[^@=0-9]", "@@", s)
+ s = s.replace('\\"', '"')
+ s = s.replace("\\'", "'")
+ s = s.replace("\n", "@n")
+ s = s.replace("\\n", "@n")
+ s = s.replace("=", "@=")
+ lOut.append(s)
+ return lOut
+
+# Gets strings from an existing translation file
+# returns both a dictionary of translations
+# and the full original source text so that the new text
+# can be compared to it for changes.
+# Returns also header comments in the third return value.
+def import_tr_file(tr_file):
+ dOut = {}
+ text = None
+ header_comment = None
+ if os.path.exists(tr_file):
+ with open(tr_file, "r", encoding='utf-8') as existing_file :
+ # save the full text to allow for comparison
+ # of the old version with the new output
+ text = existing_file.read()
+ existing_file.seek(0)
+ # a running record of the current comment block
+ # we're inside, to allow preceeding multi-line comments
+ # to be retained for a translation line
+ latest_comment_block = None
+ for line in existing_file.readlines():
+ line = line.rstrip('\n')
+ if line.startswith("###"):
+ if header_comment is None and not latest_comment_block is None:
+ # Save header comments
+ header_comment = latest_comment_block
+ # Strip textdomain line
+ tmp_h_c = ""
+ for l in header_comment.split('\n'):
+ if not l.startswith("# textdomain:"):
+ tmp_h_c += l + '\n'
+ header_comment = tmp_h_c
+
+ # Reset comment block if we hit a header
+ latest_comment_block = None
+ continue
+ elif line.startswith("#"):
+ # Save the comment we're inside
+ if not latest_comment_block:
+ latest_comment_block = line
+ else:
+ latest_comment_block = latest_comment_block + "\n" + line
+ continue
+ match = pattern_tr.match(line)
+ if match:
+ # this line is a translated line
+ outval = {}
+ outval["translation"] = match.group(2)
+ if latest_comment_block:
+ # if there was a comment, record that.
+ outval["comment"] = latest_comment_block
+ latest_comment_block = None
+ dOut[match.group(1)] = outval
+ return (dOut, text, header_comment)
+
+# Walks all lua files in the mod folder, collects translatable strings,
+# and writes it to a template.txt file
+# Returns a dictionary of localized strings to source file sets
+# that can be used with the strings_to_text function.
+def generate_template(folder, mod_name):
+ dOut = {}
+ for root, dirs, files in os.walk(folder):
+ for name in files:
+ if fnmatch.fnmatch(name, "*.lua"):
+ fname = os.path.join(root, name)
+ found = read_lua_file_strings(fname)
+ if params["verbose"]:
+ print(f"{fname}: {str(len(found))} translatable strings")
+
+ for s in found:
+ sources = dOut.get(s, set())
+ sources.add(f"### {os.path.basename(fname)} ###")
+ dOut[s] = sources
+
+ if len(dOut) == 0:
+ return None
+ templ_file = os.path.join(folder, "locale/template.txt")
+ write_template(templ_file, dOut, mod_name)
+ return dOut
+
+# Updates an existing .tr file, copying the old one to a ".old" file
+# if any changes have happened
+# dNew is the data used to generate the template, it has all the
+# currently-existing localized strings
+def update_tr_file(dNew, mod_name, tr_file):
+ if params["verbose"]:
+ print(f"updating {tr_file}")
+
+ tr_import = import_tr_file(tr_file)
+ dOld = tr_import[0]
+ textOld = tr_import[1]
+
+ textNew = strings_to_text(dNew, dOld, mod_name, tr_import[2])
+
+ if textOld and textOld != textNew:
+ print(f"{tr_file} has changed.")
+ if not params["no-old-file"]:
+ shutil.copyfile(tr_file, f"{tr_file}.old")
+
+ with open(tr_file, "w", encoding='utf-8') as new_tr_file:
+ new_tr_file.write(textNew)
+
+# Updates translation files for the mod in the given folder
+def update_mod(folder):
+ modname = get_modname(folder)
+ if modname is not None:
+ process_po_files(folder, modname)
+ print(f"Updating translations for {modname}")
+ data = generate_template(folder, modname)
+ if data == None:
+ print(f"No translatable strings found in {modname}")
+ else:
+ for tr_file in get_existing_tr_files(folder):
+ update_tr_file(data, modname, os.path.join(folder, "locale/", tr_file))
+ else:
+ print(f"\033[31mUnable to find modname in folder {folder}.\033[0m", file=_stderr)
+ exit(1)
+
+# Determines if the folder being pointed to is a mod or a mod pack
+# and then runs update_mod accordingly
+def update_folder(folder):
+ is_modpack = os.path.exists(os.path.join(folder, "modpack.txt")) or os.path.exists(os.path.join(folder, "modpack.conf"))
+ if is_modpack:
+ subfolders = [f.path for f in os.scandir(folder) if f.is_dir() and not f.name.startswith('.')]
+ for subfolder in subfolders:
+ update_mod(subfolder)
+ else:
+ update_mod(folder)
+ print("Done.")
+
+def run_all_subfolders(folder):
+ for modfolder in [f.path for f in os.scandir(folder) if f.is_dir() and not f.name.startswith('.')]:
+ update_folder(modfolder)
+
+
+main()
+
diff --git a/init.lua b/init.lua
index edf9e24..a317a32 100644
--- a/init.lua
+++ b/init.lua
@@ -1,655 +1,82 @@
--[[
- Replacement tool for creative building (Mod for MineTest)
- Copyright (C) 2013 Sokomine
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
+ Replacement tool for creative building (Mod for MineTest)
+ Copyright (C) 2013 Sokomine
+ Copyright (C) 2019 coil0
+ Copyright (C) 2019 HybridDog
+ Copyright (C) 2019-2022 SwissalpS
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
--]]
--- Version 3.0
-
--- Changelog:
--- 09.12.2017 * Got rid of outdated minetest.env
--- * Fixed error in protection function.
--- * Fixed minor bugs.
--- * Added blacklist
--- 02.10.2014 * Some more improvements for inspect-tool. Added craft-guide.
--- 01.10.2014 * Added inspect-tool.
--- 12.01.2013 * If digging the node was unsuccessful, then the replacement will now fail
--- (instead of destroying the old node with its metadata; i.e. chests with content)
--- 20.11.2013 * if the server version is new enough, minetest.is_protected is used
--- in order to check if the replacement is allowed
--- 24.04.2013 * param1 and param2 are now stored
--- * hold sneak + right click to store new pattern
--- * right click: place one of the itmes
--- * receipe changed
--- * inventory image added
-
-local path = minetest.get_modpath("replacer")
+-- Version 4.91 (20220830)
--- adds a function to check ownership of a node; taken from VanessaEs homedecor mod
-dofile(path.."/check_owner.lua")
+-- Changelog: see CHANGELOG file
replacer = {}
-
-replacer.blacklist = {};
-
--- playing with tnt and creative building are usually contradictory
--- (except when doing large-scale landscaping in singleplayer)
-replacer.blacklist[ "tnt:boom"] = true;
-replacer.blacklist[ "tnt:gunpowder"] = true;
-replacer.blacklist[ "tnt:gunpowder_burning"] = true;
-replacer.blacklist[ "tnt:tnt"] = true;
-
--- prevent accidental replacement of your protector
-replacer.blacklist[ "protector:protect"] = true;
-replacer.blacklist[ "protector:protect2"] = true;
-
-replacer.max_charge = 30000
-replacer.charge_per_node = 10
-
+replacer.version = 20240225
+
+replacer.has_bakedclay = minetest.get_modpath('bakedclay')
+replacer.has_basic_dyes = minetest.get_modpath('dye')
+ and minetest.global_exists('dye')
+ and dye.basecolors
+replacer.has_circular_saw = minetest.get_modpath('moreblocks')
+ and minetest.global_exists('moreblocks')
+ and minetest.global_exists('circular_saw')
+ and circular_saw.names
+replacer.has_colormachine_mod = minetest.get_modpath('colormachine')
+ and minetest.global_exists('colormachine')
+replacer.has_technic_mod = minetest.get_modpath('technic')
+ and minetest.global_exists('technic')
+replacer.has_unifieddyes_mod = minetest.get_modpath('unifieddyes')
+ and minetest.global_exists('unifieddyes')
+replacer.has_unified_inventory_mod = minetest.get_modpath('unified_inventory')
+ and true or false
+
+-- image mapping tables for replacer:inspect
+replacer.group_placeholder = {}
+replacer.image_replacements = {}
+
+local path = minetest.get_modpath('replacer') .. '/'
+-- for developers
+dofile(path .. 'test.lua')
+-- strings for translation (i+r)
+dofile(path .. 'blabla.lua')
+-- utilities (i+r)
+dofile(path .. 'utils.lua')
+-- more settings and functions
+dofile(path .. 'replacer/constrain.lua')
+-- register set enable functions
+dofile(path .. 'replacer/enable.lua')
-- adds a tool for inspecting nodes and entities
-dofile(path.."/inspect.lua")
-
-local function inform(name, msg)
- minetest.chat_send_player(name, msg)
- minetest.log("info", "[replacer] "..name..": "..msg)
-end
-
-local mode_infos = {
- single = "Replace single node.",
- field = "Left click: Replace field of nodes of a kind where a translucent node is in front of it. Right click: Replace field of air where no translucent node is behind the air.",
- crust = "Left click: Replace nodes which touch another one of its kind and a translucent node, e.g. air. Right click: Replace air nodes which touch the crust",
- chunkborder = "TODO",
-}
-local mode_colours = {
- single = "#ffffff",
- field = "#54FFAC",
- crust = "#9F6200",
- chunkborder = "#FF5457",
-}
-local modes = {"single", "field", "crust", "chunkborder"}
-for n = 1,#modes do
- modes[modes[n]] = n
-end
-
-local function get_data(stack)
- local daten = stack:get_meta():get_string"replacer":split" " or {}
- return {
- name = daten[1] or "default:dirt",
- param1 = tonumber(daten[2]) or 0,
- param2 = tonumber(daten[3]) or 0
- },
- modes[daten[4]] and daten[4] or modes[1]
-end
-
-local function set_data(stack, node, mode)
- mode = mode or modes[1]
- local metadata = (node.name or "default:dirt") .. " "
- .. (node.param1 or 0) .. " "
- .. (node.param2 or 0) .." "
- .. mode
- local meta = stack:get_meta()
- meta:set_string("replacer", metadata)
- meta:set_string("color", mode_colours[mode])
- return metadata
-end
-
-technic.register_power_tool("replacer:replacer", replacer.max_charge)
-
-minetest.register_tool("replacer:replacer", {
- description = "Node replacement tool",
- inventory_image = "replacer_replacer.png",
- stack_max = 1, -- it has to store information - thus only one can be stacked
- wear_represents = "technic_RE_charge",
- on_refill = technic.refill_RE_charge,
- liquids_pointable = true, -- it is ok to painit in/with water
- --node_placement_prediction = nil,
- metadata = "default:dirt", -- default replacement: common dirt
-
- on_place = function(itemstack, placer, pt)
- if not placer
- or not pt then
- return
- end
-
- local keys = placer:get_player_control()
- local name = placer:get_player_name()
- local creative_enabled = creative.is_enabled_for(name)
- local has_give = minetest.check_player_privs(name, "give")
-
- if keys.aux1 then
- -- Change Mode when holding the fast key
- local node, mode = get_data(itemstack)
- mode = modes[modes[mode]%#modes+1]
- set_data(itemstack, node, mode)
- inform(name, "Mode changed to: "..mode..": "..mode_infos[mode])
- return itemstack
- end
-
- -- If not holding shift, place node(s)
- if not keys.sneak then
- return replacer.replace(itemstack, placer, pt, true)
- end
-
- -- Select new node
- if pt.type ~= "node" then
- inform(name, "Error: No node selected.")
- return
- end
-
- local node, mode = get_data(itemstack)
- node = minetest.get_node_or_nil(pt.under) or node
-
- local inv = placer:get_inventory()
- if not (creative_enabled and has_give)
- and not inv:contains_item("main", node.name) then
- if creative_enabled then
- if minetest.get_item_group(node.name,
- "not_in_creative_inventory") > 0 then
- -- search for a drop available in creative inventory
- local found_item = false
- local drops = minetest.get_node_drops(node.name)
- for i = 1,#drops do
- local name = drops[i]
- if minetest.registered_nodes[name]
- and minetest.get_item_group(name,
- "not_in_creative_inventory") == 0 then
- node.name = name
- found_item = true
- break
- end
- end
- if not found_item then
- inform(name, "Node not in creative invenotry: \"" ..
- node.name .. "\".")
- return
- end
- end
- else
- local found_item = false
- -- search for a drop that the player has if possible
- local drops = minetest.get_node_drops(node.name)
- for i = 1,#drops do
- local name = drops[i]
- if minetest.registered_nodes[name]
- and inv:contains_item("main", name) then
- node.name = name
- found_item = true
- break
- end
- end
- if not found_item then
- -- search for a drop available in creative inventory
- -- that first configuring the replacer,
- -- then digging the nodes works
- for i = 1,#drops do
- local name = drops[i]
- if minetest.registered_nodes[name]
- and minetest.get_item_group(name,
- "not_in_creative_inventory") == 0 then
- node.name = name
- found_item = true
- break
- end
- end
- end
- if not found_item
- and not has_give then
- inform(name, "Item not in your inventory: '" .. node.name ..
- "'.")
- return
- end
- end
- end
-
- local metadata = set_data(itemstack, node, mode)
-
- inform(name, "Node replacement tool set to: '" .. metadata .. "'.")
-
- return itemstack --data changed
- end,
-
--- on_drop = func(itemstack, dropper, pos),
-
- on_use = function(...)
- -- Replace nodes
- return replacer.replace(...)
- end,
-})
-
-local poshash = minetest.hash_node_position
-
--- cache results of minetest.get_node
-local known_nodes = {}
-local function get_node(pos)
- local i = poshash(pos)
- local node = known_nodes[i]
- if node then
- return node
- end
- node = minetest.get_node(pos)
- known_nodes[i] = node
- return node
-end
-
--- tests if there's a node at pos which should be replaced
-local function replaceable(pos, name, pname)
- return get_node(pos).name == name
- and not minetest.is_protected(pos, pname)
-end
-
-local trans_nodes = {}
-local function node_translucent(name)
- if trans_nodes[name] ~= nil then
- return trans_nodes[name]
- end
- local data = minetest.registered_nodes[name]
- if data
- and (not data.drawtype or data.drawtype == "normal") then
- trans_nodes[name] = false
- return false
- end
- trans_nodes[name] = true
- return true
-end
-
-local function field_position(pos, data)
- return replaceable(pos, data.name, data.pname)
- and node_translucent(
- get_node(vector.add(data.above, pos)).name) ~= data.right_clicked
-end
-
-local offsets_touch = {
- {x=-1, y=0, z=0},
- {x=1, y=0, z=0},
- {x=0, y=-1, z=0},
- {x=0, y=1, z=0},
- {x=0, y=0, z=-1},
- {x=0, y=0, z=1},
-}
-
--- 3x3x3 hollow cube
-local offsets_hollowcube = {}
-for x = -1,1 do
- for y = -1,1 do
- for z = -1,1 do
- local p = {x=x, y=y, z=z}
- if x ~= 0
- or y ~= 0
- or z ~= 0 then
- offsets_hollowcube[#offsets_hollowcube+1] = p
- end
- end
- end
-end
-
--- To get the crust, first nodes near it need to be collected
-local function crust_above_position(pos, data)
- -- test if the node at pos is a translucent node and not part of the crust
- local nd = get_node(pos).name
- if nd == data.name
- or not node_translucent(nd) then
- return false
- end
- -- test if a node of the crust is near pos
- for i = 1,26 do
- local p2 = offsets_hollowcube[i]
- if replaceable(vector.add(pos, p2), data.name, data.pname) then
- return true
- end
- end
- return false
-end
-
--- used to get nodes the crust belongs to
-local function crust_under_position(pos, data)
- if not replaceable(pos, data.name, data.pname) then
- return false
- end
- for i = 1,26 do
- local p2 = offsets_hollowcube[i]
- if data.aboves[poshash(vector.add(pos, p2))] then
- return true
- end
- end
- return false
-end
-
--- extract the crust from the nodes the crust belongs to
-local function reduce_crust_ps(data)
- local newps = {}
- local n = 0
- for i = 1,data.num do
- local p = data.ps[i]
- for i = 1,6 do
- local p2 = offsets_touch[i]
- if data.aboves[poshash(vector.add(p, p2))] then
- n = n+1
- newps[n] = p
- break
- end
- end
- end
- data.ps = newps
- data.num = n
-end
-
--- gets the air nodes touching the crust
-local function reduce_crust_above_ps(data)
- local newps = {}
- local n = 0
- for i = 1,data.num do
- local p = data.ps[i]
- if replaceable(p, "air", data.pname) then
- for i = 1,6 do
- local p2 = offsets_touch[i]
- if replaceable(vector.add(p, p2), data.name, data.pname) then
- n = n+1
- newps[n] = p
- break
- end
- end
- end
- end
- data.ps = newps
- data.num = n
-end
-
-local function mantle_position(pos, data)
- if not replaceable(pos, data.name, data.pname) then
- return false
- end
- for i = 1,6 do
- if get_node(vector.add(pos, offsets_touch[i])).name ~= data.name then
- return true
- end
- end
- return false
-end
-
--- finds out positions using depth first search
-local function get_ps(pos, fdata, adps, max)
- adps = adps or offsets_touch
-
- local tab = {}
- local num = 0
-
- local todo = {pos}
- local ti = 1
-
- local tab_avoid = {}
-
- while ti ~= 0 do
- local p = todo[ti]
- --~ todo[ti] = nil
- ti = ti-1
-
- for _,p2 in pairs(adps) do
- p2 = vector.add(p, p2)
- local i = poshash(p2)
- if not tab_avoid[i]
- and fdata.func(p2, fdata) then
-
- num = num+1
- tab[num] = p2
-
- ti = ti+1
- todo[ti] = p2
-
- tab_avoid[i] = true
-
- if max
- and num >= max then
- return false
- end
- end
- end
- end
- return tab, num, tab_avoid
-end
-
--- replaces one node with another one and returns if it was successful
-local function replace_single_node(pos, node, nnd, player, name, inv, creative)
- if minetest.is_protected(pos, name) then
- return false, "Protected at "..minetest.pos_to_string(pos)
- end
-
- if replacer.blacklist[node.name] then
- return false, "Replacing blocks of the type '" ..
- node.name ..
- "' is not allowed on this server. Replacement failed."
- end
-
- -- do not replace if there is nothing to be done
- if node.name == nnd.name then
- -- only the orientation was changed
- if node.param1 ~= nnd.param1
- or node.param2 ~= nnd.param2 then
- minetest.swap_node(pos, nnd)
- end
- return true
- end
-
- -- does the player carry at least one of the desired nodes with him?
- if not creative
- and not inv:contains_item("main", nnd.name) then
- return false, "You have no further '"..(nnd.name or "?")..
- "'. Replacement failed."
- end
-
- local ndef = minetest.registered_nodes[node.name]
- if not ndef then
- return false, "Unknown node: "..node.name
- end
- local new_ndef = minetest.registered_nodes[nnd.name]
- if not new_ndef then
- return false, "Unknown node should be placed: "..nnd.name
- end
-
- -- dig the current node if needed
- if not ndef.buildable_to then
- -- give the player the item by simulating digging if possible
- ndef.on_dig(pos, node, player)
- -- test if digging worked
- local dug_node = minetest.get_node_or_nil(pos)
- if not dug_node
- or not minetest.registered_nodes[dug_node.name].buildable_to then
- return false, "Couldn't dig '".. node.name .."' properly."
- end
- end
-
- -- place the node similar to how a player does it
- -- (other than the pointed_thing)
- local newitem, succ = new_ndef.on_place(ItemStack(nnd.name), player,
- {type = "node", under = vector.new(pos), above = vector.new(pos)})
- if succ == false then
- return false, "Couldn't place '" .. nnd.name .. "'."
- end
-
- -- update inventory in survival mode
- if not creative then
- -- consume the item
- inv:remove_item("main", nnd.name.." 1")
- -- if placing the node didn't result in empty stack…
- if newitem:to_string() ~= "" then
- inv:add_item("main", newitem)
- end
- end
-
- -- test whether the placed node differs from the supposed node
- local placed_node = minetest.get_node(pos)
- if placed_node.name ~= nnd.name then
- -- Sometimes placing doesn't put the node but does something different
- -- e.g. when placing snow on snow with the snow mod
- return true
- end
-
- -- fix orientation if needed
- if placed_node.param1 ~= nnd.param1
- or placed_node.param2 ~= nnd.param2 then
- minetest.swap_node(pos, nnd)
- end
-
- return true
-end
-
--- the function which happens when the replacer is used
-function replacer.replace(itemstack, user, pt, right_clicked)
- if not user
- or not pt then
- return
- end
-
- local name = user:get_player_name()
- local creative_enabled = creative.is_enabled_for(name)
-
- if pt.type ~= "node" then
- inform(name, "Error: " .. pt.type .. " is not a node.")
- return
- end
-
- local pos = minetest.get_pointed_thing_position(pt, right_clicked)
- local node_toreplace = minetest.get_node_or_nil(pos)
-
- if not node_toreplace then
- inform(name, "Target node not yet loaded. Please wait a " ..
- "moment for the server to catch up.")
- return
- end
-
- local nnd, mode = get_data(itemstack)
- if node_toreplace.name == nnd.name
- and node_toreplace.param1 == nnd.param1
- and node_toreplace.param2 == nnd.param2 then
- inform(name, "Nothing to replace.")
- return
- end
-
- if replacer.blacklist[nnd.name] then
- minetest.chat_send_player(name, "Placing blocks of the type '" ..
- nnd.name ..
- "' with the replacer is not allowed on this server. " ..
- "Replacement failed.")
- return
- end
-
- if mode == "single" then
- local succ,err = replace_single_node(pos, node_toreplace, nnd, user,
- name, user:get_inventory(), creative_enabled)
-
- if not succ then
- inform(name, err)
- end
- return
- end
-
- local ps,num
- if mode == "field" then
- -- get connected positions for plane field replacing
- local pdif = vector.subtract(pt.above, pt.under)
- local adps,n = {},1
- for _,i in pairs{"x", "y", "z"} do
- if pdif[i] == 0 then
- for a = -1,1,2 do
- local p = {x=0, y=0, z=0}
- p[i] = a
- adps[n] = p
- n = n+1
- end
- end
- end
- if right_clicked then
- pdif = vector.multiply(pdif, -1)
- end
- right_clicked = right_clicked and true or false
- ps,num = get_ps(pos, {func=field_position, name=node_toreplace.name,
- pname=name, above=pdif, right_clicked=right_clicked}, adps, 8799)
- elseif mode == "crust" then
- local nodename_clicked = get_node(pt.under).name
- local aps,n,aboves = get_ps(pt.above, {func=crust_above_position,
- name=nodename_clicked, pname=name}, nil, 8799)
- if aps then
- if right_clicked then
- local data = {ps=aps, num=n, name=nodename_clicked, pname=name}
- reduce_crust_above_ps(data)
- ps,num = data.ps, data.num
- else
- ps,num = get_ps(pt.under, {func=crust_under_position,
- name=node_toreplace.name, pname=name, aboves=aboves},
- offsets_hollowcube, 8799)
- if ps then
- local data = {aboves=aboves, ps=ps, num=num}
- reduce_crust_ps(data)
- ps,num = data.ps, data.num
- end
- end
- end
- elseif mode == "chunkborder" then
- ps,num = get_ps(pos, {func=mantle_position, name=node_toreplace.name,
- pname=name}, nil, 8799)
- end
-
- -- reset known nodes table
- known_nodes = {}
-
- if not ps then
- inform(name, "Aborted, too many nodes detected.")
- return
- end
-
- local charge_needed = replacer.charge_per_node * num
- local meta = minetest.deserialize(itemstack:get_metadata())
- if not meta or not meta.charge
- or meta.charge < charge_needed then
- inform(name, "Need " .. charge_needed .. " charge to replace " .. num .. " nodes.")
- return
- end
-
- -- set nodes
- local inv = user:get_inventory()
- for i = 1,num do
- local pos = ps[i]
- local succ,err = replace_single_node(pos, minetest.get_node(pos), nnd,
- user, name, inv, creative_enabled)
- if not succ then
- inform(name, err)
- if not technic.creative_mode then
- meta.charge = meta.charge - replacer.charge_per_node * i
- technic.set_RE_wear(itemstack, meta.charge, replacer.max_charge)
- itemstack:set_metadata(minetest.serialize(meta))
- return itemstack
- end
- return
- end
- end
-
- if not technic.creative_mode then
- meta.charge = meta.charge - replacer.charge_per_node * num
- technic.set_RE_wear(itemstack, meta.charge, replacer.max_charge)
- itemstack:set_metadata(minetest.serialize(meta))
- return itemstack
- end
- inform(name, num.." nodes replaced.")
-end
-
-
-minetest.register_craft({
- output = "replacer:replacer",
- recipe = {
- {'default:chest', '', ''},
- {'', 'technic:green_energy_crystal', ''},
- {'', '', 'default:chest'},
- }
-})
+dofile(path .. 'inspect.lua')
+
+-- loop through compat dir
+local path_compat = path .. 'compat/'
+for _, file in ipairs(minetest.get_dir_list(path_compat, false)) do
+ if file:find('^[^._].+[.]lua$') then
+ dofile(path_compat .. file)
+ end
+end
+
+replacer.datastructures = dofile(path .. 'replacer/datastructures.lua')
+dofile(path .. 'replacer/formspecs.lua')
+dofile(path .. 'replacer/history.lua')
+dofile(path .. 'replacer/patterns.lua')
+dofile(path .. 'replacer/replacer.lua')
+dofile(path .. 'crafts.lua')
+dofile(path .. 'chat_commands.lua')
+--------------------------------------------------------------------------------
+--------------------------------------------------------------------------------
+print('[replacer] loaded')
diff --git a/inspect.lua b/inspect.lua
index d0f0123..d7db9d3 100644
--- a/inspect.lua
+++ b/inspect.lua
@@ -1,400 +1,765 @@
+-- a crafting guide wanabe. Better than nothing for servers without
+-- unified_inventory installed.
+-- most useful feature is probably light measuring.
+-- when using (lc), info about the node that was punched is presented
+-- when placing (rc), info about the adjacent node that was clicked is
+-- presented. Mostly air.
+
+local r = replacer
+local rb = replacer.blabla
+local rbi = replacer.blabla.inspect
+local ui = r.has_unified_inventory_mod and unified_inventory or false
+local nice_pos_string = replacer.nice_pos_string
+local S = replacer.S
+local floor = math.floor
+local max, min = math.max, math.min
+local concat = table.concat
+local insert = table.insert
+local chat = minetest.chat_send_player
+local mfe = minetest.formspec_escape
+local core_log = minetest.log
+local deserialize = minetest.deserialize
+local parse_json = minetest.parse_json
+local get_node_or_nil = minetest.get_node_or_nil
+local get_node_light = minetest.get_node_light
+local get_pointed_thing_position = minetest.get_pointed_thing_position
+local get_all_craft_recipes = minetest.get_all_craft_recipes
+local is_protected = minetest.is_protected
+local show_formspec = minetest.show_formspec
+local registered_abms = minetest.registered_abms
+local registered_lbms = minetest.registered_lbms
+local registered_aliases = minetest.registered_aliases
+local registered_tools = minetest.registered_tools
+local registered_nodes = minetest.registered_nodes
+local registered_items = minetest.registered_items
+local registered_entities = minetest.registered_entities
+local registered_craftitems = minetest.registered_craftitems
+-- luacheck: push ignore unused pd
+local pd = r.print_dump
+-- luacheck: pop
+-- use r.register_craft_method() to populate
+replacer.recipe_adders = {}
+
+-- uid: unique identifier e.g. 'saw', 'compress', 'freeze'
+-- machine_itemstring: the itemstring that has registered icon texture,
+-- generally the machine used.
+-- func_inspect: function that manipulates recipes list with signature:
+-- f(node_name, param2, recipes)
+-- returns nothing, just adds recipes to recipes list
+-- func_formspec: optional function that returns extra formspec elements
+-- f(recipe)
+-- where recipe is the recipe table func_inspect added.
+-- returns a string, even if empty.
+function replacer.register_craft_method(uid, machine_itemstring, func_inspect,
+ func_formspec)
+ if (('string' ~= type(uid)) or ('' == uid))
+ or ('string' ~= type(machine_itemstring))
+ or ('function' ~= type(func_inspect))
+ then
+ core_log('warning', rbi.log_reg_craft_method_wrong_arguments)
+ return
+ end
-replacer.image_replacements = {}
-
--- support for RealTest
-if (minetest.get_modpath("trees")
- and minetest.get_modpath("core")
- and minetest.get_modpath("instruments")
- and minetest.get_modpath("anvil")
- and minetest.get_modpath("scribing_table")) then
-
- replacer.image_replacements["group:planks"] = "trees:pine_planks"
- replacer.image_replacements["group:plank"] = "trees:pine_plank"
- replacer.image_replacements["group:wood"] = "trees:pine_planks"
- replacer.image_replacements["group:tree"] = "trees:pine_log"
- replacer.image_replacements["group:sapling"] = "trees:pine_sapling"
- replacer.image_replacements["group:leaves"] = "trees:pine_leaves"
- replacer.image_replacements["default:furnace"] = "oven:oven"
- replacer.image_replacements["default:furnace_active"] = "oven:oven_active"
-end
+ if r.recipe_adders[uid] then
+ core_log('warning', rbi.log_reg_craft_method_overriding_method .. uid)
+ end
-minetest.register_tool("replacer:inspect",
-{
- description = "Node inspection tool",
- groups = {},
- inventory_image = "replacer_inspect.png",
- wield_image = "",
- wield_scale = {x=1,y=1,z=1},
+ r.recipe_adders[uid] = {
+ machine = machine_itemstring,
+ add_recipe = func_inspect,
+ formspec = ('function' == type(func_formspec) and func_formspec) or nil
+ }
+ core_log('info', rbi.log_reg_craft_method_added:format(uid, machine_itemstring))
+end -- register_craft_method
+
+
+minetest.register_tool('replacer:inspect', {
+ description = rbi.description,
+ groups = {},
+ inventory_image = 'replacer_inspect.png',
+ wield_image = '',
+ wield_scale = { x = 1, y = 1, z = 1 },
liquids_pointable = true, -- it is ok to request information about liquids
- on_use = function(itemstack, user, pointed_thing)
- return replacer.inspect(itemstack, user, pointed_thing, nil, true) --false)
- end,
+ on_use = function(itemstack, player, pointed_thing)
+ return r.inspect(itemstack, player, pointed_thing)
+ end,
- on_place = function(itemstack, placer, pointed_thing)
- return replacer.inspect(itemstack, placer, pointed_thing, nil, true)
- end,
+ on_place = function(itemstack, player, pointed_thing)
+ return r.inspect(itemstack, player, pointed_thing, true)
+ end,
})
-replacer.inspect = function(itemstack, user, pointed_thing, mode, show_receipe)
-
- if (user == nil or pointed_thing == nil) then
+function replacer.inspect(_, player, pointed_thing, right_clicked)
+ if nil == player or nil == pointed_thing then
return nil
end
- local name = user:get_player_name()
- local keys = user:get_player_control()
- if (keys["sneak"]) then
- show_receipe = true
- end
-
- if (pointed_thing.type == 'object') then
- local text = 'This is '
- local ref = pointed_thing.ref
- if (not(ref)) then
- text = text..'a broken object. We have no further information about it. It is located'
- elseif (ref:is_player()) then
- text = text..'your fellow player \"'..tostring(ref:get_player_name())..'\"'
- else
- local luaob = ref:get_luaentity()
- if( luaob and luaob.get_staticdata) then
- text = text..'entity \"'..tostring( luaob.name )..'\"'
- local sdata = luaob:get_staticdata()
- if (0 < #sdata) then
- sdata = minetest.deserialize(sdata) or {}
- if (sdata.itemstring) then
- text = text..' ['..tostring(sdata.itemstring)..']'
- if (show_receipe) then
- -- the fields part is used here to provide additional information about the entity
- replacer.inspect_show_crafting(name, sdata.itemstring, { pos=pos, luaob=luaob})
- end
- end
- if (sdata.age) then
- text = text..', dropped '..tostring(math.floor(sdata.age/60))..' minutes ago'
- end
- end
- else
- text = text..'object \"'..tostring(ref:get_entity_name())..'\"'
- end
- end
- text = text..' at '..minetest.pos_to_string(ref:getpos())
- minetest.chat_send_player(name, text)
+ local player_name = player:get_player_name()
+ if 'object' == pointed_thing.type then
+ chat(player_name, r.inspect_entity(pointed_thing.ref, player))
return nil
- elseif (pointed_thing.type ~= 'node') then
- minetest.chat_send_player(name, 'Sorry. This is an unkown something of type \"'..tostring(pointed_thing.type)..'\". No information available.')
+ elseif 'node' ~= pointed_thing.type then
+ chat(player_name, S('Sorry, this is an unknown something of type "@1". '
+ .. 'No information available.', pointed_thing.type))
return nil
end
-
- local pos = minetest.get_pointed_thing_position(pointed_thing, mode)
- local node = minetest.get_node_or_nil(pos)
-
- if (node == nil) then
- minetest.chat_send_player(name, "Error: Target node not yet loaded. Please wait a moment for the server to catch up.")
+
+ local pos = get_pointed_thing_position(pointed_thing, right_clicked)
+ local node = get_node_or_nil(pos)
+ if not node then
+ chat(player_name, rb.wait_for_load)
return nil
end
- local text = ' ['..tostring(node.name)..'] with param2='..tostring(node.param2)..' at '..minetest.pos_to_string(pos)..'.'
- if (not(minetest.registered_nodes[node.name])) then
- text = 'This node is an UNKOWN block'..text
- else
- text = 'This is a \"'..tostring(minetest.registered_nodes[node.name].description or ' - no description provided -')..'\" block'..text
- end
- local protected_info = ""
- if (minetest.is_protected( pos, name)) then
- protected_info = 'WARNING: You can\'t dig this node. It is protected.'
- elseif (minetest.is_protected(pos, '_THIS_NAME_DOES_NOT_EXIST_')) then
- protected_info = 'INFO: You can dig this node, but others can\'t.'
- end
- text = text..' '..protected_info
--- no longer spam the chat; the craft guide is more informative
--- minetest.chat_send_player(name, text)
-
- if (show_receipe) then
- -- get light of the node at the current time
- local light = minetest.get_node_light(pos, nil)
- if (light==0) then
- light = minetest.get_node_light({x=pos.x,y=pos.y+1,z=pos.z})
+ -- EXPERIMENTAL: attempt to open unified_inventory's crafting guide
+ if ui then
+ local keys = player:get_player_control()
+ -- while testing let's use zoom until we either drop the idea
+ -- or get it to work
+ if keys.zoom then --aux1 then ---and keys.sneak then
+ ui.current_item[player_name] = node.name
+ ui.current_craft_direction[player_name] = 'recipe'-- keys.x and 'usage' or 'recipe'
+ ui.current_searchbox[player_name] = node.name
+ ui.apply_filter(player, node.name, 'recipe')--'usage' --nochange')
+ show_formspec(player_name, '', ui.get_formspec(player, 'craftguide'))
+ return
end
- -- the fields part is used here to provide additional information about the node
- replacer.inspect_show_crafting(name, node.name, {pos=pos, param2=node.param2, light=light, protected_info=protected_info})
end
- return nil; -- no item shall be removed from inventory
+
+--pd(node, registered_nodes[node.name].mod_origin)
+ local protected_info = ''
+ if is_protected(pos, player_name) then
+ protected_info = rbi.is_protected
+ elseif is_protected(pos, '_THIS_NAME_DOES_NOT_EXIST_') then
+ protected_info = rbi.you_can_dig
+ end
+
+ -- get light of the node at the current time
+ local light = get_node_light(pos, nil)
+ -- the fields part is used here to provide additional
+ -- information about the node
+ r.inspect_show_crafting(player_name, node.name, {
+ pos = pos,
+ param2 = node.param2,
+ light = light,
+ protected_info = protected_info
+ })
+
+ return nil -- no item shall be removed from inventory
+end -- replacer.inspect
+
+
+-- bug work around to prevent using inspection tool as a weapon
+local function is_endangered(luaob)
+ if not luaob._cmi_is_mob then return false end
+ if not registered_entities[luaob.name] then return false end
+
+ return '' == luaob.owner
+end -- is_endangered
+
+
+-- helper for inspect_entity()/inspect_mob()
+local function is_registered(item_name_or_group)
+ if 'string' ~= type(item_name_or_group) then return false end
+ if item_name_or_group:find('^:?group:') then return true end
+ return registered_items[item_name_or_group] and true or false
end
--- some common groups
-replacer.group_placeholder = {}
-replacer.group_placeholder['group:wood'] = 'default:wood'
-replacer.group_placeholder['group:tree'] = 'default:tree'
-replacer.group_placeholder['group:sapling']= 'default:sapling'
-replacer.group_placeholder['group:stick'] = 'default:stick'
-replacer.group_placeholder['group:stone'] = 'default:cobble' -- 'default:stone' point people to the cheaper cobble
-replacer.group_placeholder['group:sand'] = 'default:sand'
-replacer.group_placeholder['group:leaves'] = 'default:leaves'
-replacer.group_placeholder['group:wood_slab'] = 'stairs:slab_wood'
-replacer.group_placeholder['group:wool'] = 'wool:white'
-
-
--- handle the standard dye color groups
-if (minetest.get_modpath("dye") and dye and dye.basecolors) then
- for i,color in ipairs(dye.basecolors) do
- local def = minetest.registered_items["dye:"..color]
- if (def and def.groups) then
- for k,v in pairs(def.groups) do
- if (k ~= 'dye') then
- replacer.group_placeholder['group:dye,'..k] = 'dye:'..color
+function replacer.inspect_mob(luaob)
+
+ local index, list
+ local entity_def = registered_entities[luaob.name]
+ local text = '\n'
+ if 'string' == type(entity_def.type) then
+ text = text .. rbi.mobs_of_type .. ' "' .. entity_def.type .. '". '
+ end
+ if entity_def.owner_loyal then
+ text = text .. rbi.mobs_loyal .. ' '
+ end
+--[[ these are too inacurate.
+ if entity_def.attack_players then
+ text = text .. 'Can attack players. '
+ end
+ if entity_def.attack_animals then
+ text = text .. 'Can attack animals. '
+ end
+ if entity_def.attack_monsters then
+ text = text .. 'Can attack monsters. '
+ end
+ if entity_def.attack_npcs then
+ text = text .. 'Can attack NPCs. '
+ end
+--]]
+ if 'table' == type(entity_def.specific_attack) then
+ list, index = {}, #entity_def.specific_attack
+ if 0 < index then repeat
+ if is_registered(entity_def.specific_attack[index]) then
+ list[#list + 1] = entity_def.specific_attack[index]
+ end
+ index = index - 1
+ until 0 == index
+ if 0 < #list then
+ text = text .. rbi.mobs_attacks .. ' ' .. concat(list, ', ') .. '\n'
+ end end
+ end
+ if 'table' == type(entity_def.follow) then
+ list, index = {}, #entity_def.follow
+ if 0 < index then repeat
+ if is_registered(entity_def.follow[index]) then
+ list[#list + 1] = entity_def.follow[index]
+ end
+ index = index - 1
+ until 0 >= index
+ if 0 < #list then
+ text = text .. rbi.mobs_follows .. ' ' .. concat(list, ', ') .. '\n'
+ end end
+ end
+ if 'table' == type(entity_def.drops) then
+ list, index = {}, #entity_def.drops
+ if 0 < index then repeat
+ if 'table' == type(entity_def.drops[index])
+ and is_registered(entity_def.drops[index].name)
+ then
+ list[#list + 1] = entity_def.drops[index].name
+ end
+ index = index - 1
+ until 0 >= index
+ if 0 < #list then
+ text = text .. rbi.mobs_drops .. ' ' .. concat(list, ', ') .. '\n'
+ end end
+ end
+ if 'number' == type(entity_def.damage) and 0 < entity_def.damage then
+ text = text .. S('Can deal @1 damage.', entity_def.damage) .. ' '
+ end
+ if 'number' == type(entity_def.armor) and 0 < entity_def.armor then
+ text = text .. S('Has @1 armour.', entity_def.armor) .. ' '
+ end
+ if entity_def.arrow then
+ text = text .. rbi.mobs_shoots .. ' '
+ end
+ -- some mobs could still be breedable without these two fields
+ if 'function' == type(entity_def.on_breed) or entity_def.child_texture then
+ text = text .. rbi.mobs_breed .. ' '
+ end
+--[[ some of these might be of interest too
+reach
+immune_to
+light_damage
+light_damage_min
+light_damage_max
+water_damage
+lava_damage
+fire_damage
+air_damage
+suffocation
+stay_near
+hp_min
+hp_max
+jump
+jump_height
+fear_height
+fly
+fly_in
+floats
+glow
+passive
+attack_type
+docile_by_day
+group_attack
+group_helper
+runaway
+runaway_from
+view_range
+--]]
+ -- investigate spawning conditions (nodes and neighours)
+ local found, entry, j
+ local search_string = luaob.name .. ' spawning'
+ index = #registered_abms
+ if 0 < index then repeat
+ entry = registered_abms[index]
+ if entry.label and search_string == entry.label then
+ found = true
+ list, j = {}, #entry.nodenames
+ if 0 < j then repeat
+ if is_registered(entry.nodenames[j]) then
+ list[#list + 1] = entry.nodenames[j]
+ end
+ j = j - 1
+ until 0 == j
+ if 0 < #list then
+ text = text .. '\n' .. rbi.mobs_spawns_on .. ' ' .. concat(list, ', ')
+ end end
+ list, j = {}, #entry.neighbors
+ if 0 < j then repeat
+ if is_registered(entry.neighbors[j]) then
+ list[#list + 1] = entry.neighbors[j]
end
+ j = j - 1
+ until 0 == j
+ if 0 < #list then
+ text = text .. ' ' .. rbi.mobs_spawns_neighbours .. ' '
+ .. concat(list, ', ')
+ end end
+ end
+ index = index - 1
+ until (0 == index) or found end
+ if not found then
+ search_string = luaob.name .. '_spawning'
+ index = #registered_lbms
+ if 0 < index then repeat
+ entry = registered_lbms[index]
+ if entry.name and search_string == entry.name then
+ found = true
+ list, j = {}, #entry.nodenames
+ if 0 < j then repeat
+ if is_registered(entry.nodenames[j]) then
+ list[#list + 1] = entry.nodenames[j]
+ end
+ j = j - 1
+ until 0 == j
+ if 0 < #list then
+ text = text .. '\n' .. rbi.mobs_spawns_on .. ' ' .. concat(list, ', ')
+ end end
+ end
+ index = index - 1
+ until (0 == index) or found end
+ end
+ return text
+end -- inspect_mob
+
+
+function replacer.inspect_player(object_ref, player)
+ local lines = { S('This is your fellow player "@1"', object_ref:get_player_name()) }
+ local meta = object_ref:get_meta()
+ local xp_hud_on = 'off' ~= meta:get_string('hud_state')
+ local placed = xp_hud_on and meta:get_int('placed_nodes')
+ local digs = xp_hud_on and meta:get_int('digged_nodes')
+ local punches = xp_hud_on and meta:get_int('punch_count')
+ local inflicted = xp_hud_on and meta:get_int('inflicted_damage')
+ local xp = xp_hud_on and meta:get_int('xp')
+ local play_seconds = meta:get_int('played_time')
+ local deaths = meta:get_int('died')
+ -- TODO: not accurate if either player has never joined any channel, then #main is not yet in list
+ local channels = nil --parse_json(meta:get_string('beerchat:channels'))
+ local has_active_mission = deserialize(meta:get_string('currentmission')) and true or false
+ local wearing = deserialize(meta:get_string('3d_armor_inventory'))
+ -- other possible interesting points:
+ -- ["stamina:poisoned"] = "no",
+ -- ["stamina:exhaustion"] = "0"
+
+ -- short_data_points
+ local shorts = {}
+ if placed and 0 < placed then
+ insert(shorts, rbi.player_placed .. ' ' .. r.nice_number(placed))
+ end
+ if digs and 0 < digs then
+ insert(shorts, rbi.player_digs .. ' ' .. r.nice_number(digs))
+ end
+ if punches and 0 < punches then
+ insert(shorts, rbi.player_punches .. ' ' .. r.nice_number(punches))
+ end
+ if inflicted and 0 < inflicted then
+ insert(shorts, rbi.player_inflicted .. ' ' .. r.nice_number(inflicted))
+ end
+ if xp and 0 < xp then
+ insert(shorts, rbi.player_xp .. ' ' .. r.nice_number(xp))
+ end
+ if 0 < deaths then
+ insert(shorts, rbi.player_deaths .. ' ' .. r.nice_number(deaths))
+ end
+ if 0 < #shorts then
+ insert(lines, concat(shorts, '\t'))
+ end
+ if 0 < play_seconds then
+ insert(lines, rbi.player_duration .. ' ' .. r.nice_duration(play_seconds))
+ end
+ if has_active_mission then
+ insert(lines, rbi.player_has_active_mission)
+ end
+ if channels then
+ local common = r.common_list_items(channels,
+ parse_json(player:get_meta():get_string('beerchat:channels')))
+ if 0 == #common then
+ insert(lines, rbi.player_no_common_channels)
+ else
+ insert(lines, rbi.player_common_channels .. ' ' .. concat(common, ', '))
+ end
+ end
+ if wearing and 0 < #wearing then
+ local index, parts = #wearing, {}
+ repeat
+ if '' ~= wearing[index] then
+ insert(parts, wearing[index])
end
- replacer.group_placeholder['group:flower,color_'..color] = 'dye:'..color
+ index = index - 1
+ until 0 == index
+ if 0 < #parts then
+ insert(lines, rbi.player_is_wearing .. ' ' .. concat(parts, ', '))
end
end
-end
-replacer.image_button_link = function(stack_string)
- local group = ''
- if (replacer.image_replacements[stack_string]) then
- stack_string = replacer.image_replacements[stack_string]
+ return concat(lines, '\n')
+end -- inspect_player
+
+
+function replacer.inspect_entity(object_ref, player)
+ if not object_ref then return rbi.broken_object end
+
+ local pos_string = S('at @1', nice_pos_string(object_ref:getpos()))
+ if object_ref:is_player() then
+ return r.inspect_player(object_ref, player) --.. ' ' .. pos_string
end
- if (replacer.group_placeholder[stack_string]) then
- stack_string = replacer.group_placeholder[stack_string]
- group = 'G'
- end
--- TODO: show information about other groups not handled above
- local stack = ItemStack(stack_string)
- local new_node_name = stack_string
- if (stack and stack:get_name()) then
- new_node_name = stack:get_name()
+
+ local luaob = object_ref:get_luaentity()
+ if not luaob then return rbi.this_is_object .. ' ' .. pos_string end
+
+ if (not luaob.get_staticdata) and (not registered_entities[luaob.name]) then
+ return S('This is an object "@1"', luaob.name) .. ' ' .. pos_string
end
- return tostring(stack_string)..';'..tostring(new_node_name)..';'..group
-end
-replacer.add_circular_saw_receipe = function(node_name, receipes)
- if (not(node_name) or not(minetest.get_modpath("moreblocks")) or not(circular_saw) or not(circular_saw.names) or (node_name=='moreblocks:circular_saw')) then
- return
+ local text = (luaob._cmi_is_mob and rbi.mobs_disclaimer .. '\n' or '')
+ .. S('This is an entity "@1"', luaob.name)
+ if luaob.get_staticdata and not is_endangered(luaob) then
+ text = text .. r.inspect_staticdata(luaob:get_staticdata())
end
- local help = node_name:split(':')
- if (not(help) or #help ~= 2 or help[1]=='stairs') then
- return
+ if not registered_entities[luaob.name] then return text end
+
+ if luaob._cmi_is_mob then return text .. r.inspect_mob(luaob) end
+
+ return text
+end -- inspect_entity
+
+
+function replacer.inspect_staticdata(staticdata)
+ if (not staticdata) or (0 == #staticdata) then return '' end
+
+ local text = ''
+ local sdata = deserialize(staticdata) or {}
+ if sdata.itemstring then
+ text = text .. ' [' .. sdata.itemstring .. ']'
end
- help2 = help[2]:split('_')
- if (not(help2) or #help2 < 2 or (help2[1]~='micro' and help2[1]~='panel' and help2[1]~='stair' and help2[1]~='slab')) then
- return
+ if sdata.age then
+ text = text .. S(', dropped @1 minutes ago',
+ tostring(floor((sdata.age / 60) + .5)))
+ end
+ if sdata.owner then
+ if true == sdata.protected then
+ if true == sdata.locked then
+ text = text .. ' ' .. rbi.owned_protected_locked
+ else
+ text = text .. ' ' .. rbi.owned_protected
+ end
+ else
+ if true == sdata.locked then
+ text = text .. ' ' .. rbi.owned_locked
+ end
+ end
+ text = text .. ' ' .. S('by "@1"', sdata.owner)
+ end
+ if 'table' == type(sdata.follow) and 0 < #sdata.follow then
+ text = text .. ' is tamable'
+ end
+ if 'string' == type(sdata.order) then
+ text = text .. ' ' .. S('with order to @1', sdata.order)
+ end
+ if 'table' == type(sdata.inv) then
+ local item_count = 0
+ local type_count = 0
+ for _, v in pairs(sdata.inv) do
+ type_count = type_count + 1
+ item_count = item_count + v
+ end
+ if 0 < type_count then
+ text = text .. '\n'
+ if 1 < type_count then
+ text = text .. S('Has @1 different types of items,',
+ tostring(type_count)) .. ' '
+ end
+ text = text .. S('total of @1 items in inventory.',
+ tostring(item_count))
+ end
end
--- for i,v in ipairs(circular_saw.names) do
--- modname..":"..v[1].."_"..material..v[2]
+ return text
+end -- inspect_staticdata
--- TODO: write better and more correct method of getting the names of the materials
--- TODO: make sure only nodes produced by the saw are listed here
-help[1]='default'
- local basic_node_name = help[1]..':'..help2[2]
- -- node found that fits into the saw
- receipes[#receipes+1] = { method = 'saw', type = 'saw', items = { basic_node_name}, output = node_name}
- return receipes
-end
-replacer.add_colormachine_receipe = function(node_name, receipes)
- if (not(minetest.get_modpath("colormachine")) or not(colormachine)) then
- return
+function replacer.image_button_link(stack_string)
+ local group = ''
+ if r.image_replacements[stack_string] then
+ stack_string = r.image_replacements[stack_string]
end
- local res = colormachine.get_node_name_painted(node_name, "")
-
- if (not(res) or not(res.possible) or #res.possible < 1) then
- return
+ if r.group_placeholder[stack_string] then
+ stack_string = r.group_placeholder[stack_string]
+ group = 'G'
end
- -- paintable node found
- receipes[#receipes+1] = { method = 'colormachine', type = 'colormachine', items = { res.possible[1]}, output = node_name}
- return receipes
-end
+-- TODO: show information about other groups not handled above
+ local stack = ItemStack(stack_string)
+ local new_node_name = stack:get_name()
+--pd(stack_string .. ';' .. new_node_name .. ';' .. group)
+ return stack_string .. ';' .. new_node_name .. ';' .. group
+end -- image_button_link
-replacer.inspect_show_crafting = function(name, node_name, fields)
- if (not(name)) then
+function replacer.inspect_show_crafting(player_name, node_name, fields)
+ if not player_name then
return
end
- local receipe_nr = 1
- if (not(node_name)) then
+ local recipe_nr = 1
+ if not node_name then
node_name = fields.node_name
- receipe_nr = tonumber(fields.receipe_nr)
+ recipe_nr = tonumber(fields.recipe_nr)
end
-- turn it into an item stack so that we can handle dropped stacks etc
local stack = ItemStack(node_name)
node_name = stack:get_name()
- -- the player may ask for receipes of indigrents to the current receipe
- if (fields) then
- for k,v in pairs(fields) do
- if (v and v=="" and (minetest.registered_items[k]
- or minetest.registered_nodes[k]
- or minetest.registered_craftitems[k]
- or minetest.registered_tools[k])) then
+ -- the player may ask for recipes of indigrents to the current recipe
+ if fields then
+ for k, v in pairs(fields) do
+ if v and '' == v
+ and registered_items[k]
+ or registered_nodes[k]
+ or registered_craftitems[k]
+ or registered_tools[k]
+ then
node_name = k
- receipe_nr = 1
+ recipe_nr = 1
+ end
+ end
+ end
+
+ -- fetch recipes from core
+ local recipes = get_all_craft_recipes(node_name) or {}
+ if 0 == #recipes then
+ -- some items have aliases that are set with force, and thus
+ -- don't show up in core.get_all_craft_recipes()
+ -- e.g. https://github.com/mt-mods/basic_materials/blob/d9e06980d33ec02c2321269f47ab9ec32b36551f/aliases.lua#L32
+ -- https://github.com/mt-mods/basic_materials/blob/d9e06980d33ec02c2321269f47ab9ec32b36551f/crafts.lua#L256
+ -- we try to reverse lookup here
+ for k, v in pairs(registered_aliases) do
+ if v == node_name then
+ recipes = get_all_craft_recipes(k)
+ if recipes then break end
end
end
+ recipes = recipes or {}
end
+--pd(recipes)
+ -- TODO: filter out invalid recipes with no items
+ -- such as "group:flower,color_dark_grey"
- local res = minetest.get_all_craft_recipes(node_name)
- if (not(res)) then
- res = {}
+ -- add special recipes for nodes created by machines
+ for _, adder in pairs(r.recipe_adders) do
+ adder.add_recipe(node_name, fields, recipes)
end
- -- add special receipes for nodes created by machines
- replacer.add_circular_saw_receipe(node_name, res)
- replacer.add_colormachine_receipe(node_name, res)
- -- offer all alternate creafting receipes thrugh prev/next buttons
- if ( fields and fields.prev_receipe and receipe_nr > 1) then
- receipe_nr = receipe_nr - 1
- elseif (fields and fields.next_receipe and receipe_nr < #res) then
- receipe_nr = receipe_nr + 1
+ -- offer all alternate crafting recipes through prev/next buttons
+ if fields and fields.prev_recipe then
+ recipe_nr = recipe_nr - 1
+ elseif fields and fields.next_recipe then
+ recipe_nr = recipe_nr + 1
+ end
+ -- wrap around
+ if #recipes < recipe_nr then
+ recipe_nr = 1
+ elseif 1 > recipe_nr then
+ recipe_nr = #recipes
end
- local desc = nil
- if ( minetest.registered_nodes[node_name]) then
- if ( minetest.registered_nodes[node_name].description
- and minetest.registered_nodes[node_name].description~= "") then
- desc = "\""..minetest.registered_nodes[node_name].description.."\" block"
- elseif (minetest.registered_nodes[node_name].name) then
- desc = "\""..minetest.registered_nodes[node_name].name.."\" block"
+ -- fetch description
+ -- when clicking unknown nodes
+ local description = ' ' .. rbi.no_description .. ' '
+ if registered_nodes[node_name] then
+ if registered_nodes[node_name].description
+ and '' ~= registered_nodes[node_name].description
+ then
+ description = registered_nodes[node_name].description
+ elseif registered_nodes[node_name].name then
+ description = registered_nodes[node_name].name
else
- desc = " - no description provided - block"
+ description = ' ' .. rbi.no_node_description .. ' '
end
- elseif (minetest.registered_items[node_name]) then
- if ( minetest.registered_items[node_name].description
- and minetest.registered_items[node_name].description~= "") then
- desc = "\""..minetest.registered_items[node_name].description.."\" item"
- elseif (minetest.registered_items[node_name].name) then
- desc = "\""..minetest.registered_items[node_name].name.."\" item"
+ elseif registered_items[node_name] then
+ if registered_items[node_name].description
+ and '' ~= registered_items[node_name].description
+ then
+ description = registered_items[node_name].description
+ elseif registered_items[node_name].name then
+ description = registered_items[node_name].name
else
- desc = " - no description provided - item"
+ description = ' ' .. rbi.no_item_description .. ' '
end
end
- if (not(desc) or desc=="") then
- desc = ' - no description provided - '
- end
-
- local formspec = "size[6,6]"..
- "label[0,5.5;This is a "..minetest.formspec_escape(desc)..".]"..
- "button_exit[5.0,4.3;1,0.5;quit;Exit]"..
- "label[0,0;Name:]"..
- "field[20,20;0.1,0.1;node_name;node_name;"..node_name.."]".. -- invisible field for passing on information
- "field[21,21;0.1,0.1;receipe_nr;receipe_nr;"..tostring(receipe_nr).."]".. -- another invisible field
- "label[1,0;"..tostring(node_name).."]"..
- "item_image_button[5,2;1.0,1.0;"..tostring(node_name)..";normal;]"
-
- -- provide additional information regarding the node in particular that has been inspected
- if (fields.pos) then
- formspec = formspec.."label[0.0,0.3;Located at "..
- minetest.formspec_escape(minetest.pos_to_string(fields.pos))
- if (fields.param2) then
- formspec = formspec.." with param2="..tostring(fields.param2)
- end
- if (fields.light) then
- formspec = formspec.." and receiving "..tostring(fields.light).." light"
- end
- formspec = formspec..".]"
+
+ -- base info
+ local formspec = 'size[6,6]'
+ -- label on top
+ --.. 'textarea[-9,-18,6,1;;' .. mfe(rbi.name) .. ' ' .. node_name .. ';]'
+ .. 'label[0,0;' .. mfe(rbi.name) .. ' ' .. node_name .. ']'
+ .. 'tooltip[-1,-1;7,2;' .. mfe(rbi.name) .. ' ' .. node_name .. ']'
+ .. 'button_exit[5.0,4.3;1,0.5;quit;X]'
+ .. 'tooltip[quit;'.. mfe(rbi.exit) .. ']'
+
+ -- prev. and next buttons
+ if 1 < #recipes then
+ formspec = formspec
+ .. 'button[4.1,5;1,0.75;prev_recipe;<-]'
+ .. 'tooltip[prev_recipe;'.. mfe(rbi.prev) .. ']'
+ .. 'button[5.0,5;1,0.75;next_recipe;->]'
+ .. 'tooltip[next_recipe;'.. mfe(rbi.next) .. ']'
+ end
+
+ formspec = formspec
+ -- description at bottom
+ .. 'label[0,5.7;' .. mfe(rbi.this_is) .. ' ' .. mfe(description) .. ']'
+ .. 'tooltip[-1,5.7;7,2;' .. mfe(rbi.this_is) .. ' ' .. mfe(description) .. ']'
+ -- invisible field for passing on information
+ .. 'field[20,20;0.1,0.1;node_name;node_name;' .. node_name .. ']'
+ -- another invisible field
+ .. 'field[21,21;0.1,0.1;recipe_nr;recipe_nr;' .. tostring(recipe_nr) .. ']'
+
+ -- location and param2
+ formspec = formspec .. 'label[0.0,0.3;'
+ if fields.pos then
+ formspec = formspec .. mfe(S('Located at @1', nice_pos_string(fields.pos)))
+ end
+ if fields.param2 then
+ formspec = formspec .. ' '
+ .. mfe(S('with param2 of @1', tostring(fields.param2)))
+ end
+
+ -- light
+ formspec = formspec .. ']'
+ if fields.light then
+ formspec = formspec .. 'label[0.0,0.6;'
+ .. mfe(S('and receiving @1 light', tostring(fields.light))) .. ']'
end
-- show information about protection
- if (fields.protected_info and fields.protected_info ~= "") then
- formspec = formspec.."label[0.0,4.5;"..minetest.formspec_escape(fields.protected_info).."]"
- end
-
- if (not(res) or receipe_nr > #res or receipe_nr < 1) then
- receipe_nr = 1
- end
- if (res and receipe_nr > 1) then
- formspec = formspec.."button[3.8,5;1,0.5;prev_receipe;prev]"
- end
- if (res and receipe_nr < #res) then
- formspec = formspec.."button[5.0,5.0;1,0.5;next_receipe;next]"
- end
- if (not(res) or #res<1) then
- formspec = formspec..'label[3,1;No receipes.]'
- if (minetest.registered_nodes[node_name]
- and minetest.registered_nodes[node_name].drop) then
- local drop = minetest.registered_nodes[node_name].drop
- if (drop) then
- if ( type(drop)=='string' and drop ~= node_name) then
- formspec = formspec.."label[2,1.6;Drops on dig:]"..
- "item_image_button[2,2;1.0,1.0;"..replacer.image_button_link(drop).."]"
- elseif (type(drop)=='table' and drop.items) then
- local droplist = {}
- for _,drops in ipairs(drop.items) do
- for _,item in ipairs(drops.items) do
- -- avoid duplicates; but include the item itshelf
- droplist[item] = 1
- end
- end
- local i = 1
- formspec = formspec.."label[2,1.6;May drop on dig:]"
- for k,v in pairs(droplist) do
- formspec = formspec..
- "item_image_button["..(((i-1)%3)+1)..","..math.floor(((i-1)/3)+2)..";1.0,1.0;"..replacer.image_button_link(k).."]"
- i = i+1
- end
- end
- end
+ if fields.protected_info and '' ~= fields.protected_info then
+ formspec = formspec .. 'label[0.0,4.7;'
+ .. mfe(fields.protected_info) .. ']'
+ .. 'tooltip[-1,4.7;5,1;' .. mfe(fields.protected_info) .. ']'
+ end
+
+ -- if no recipes, collect drops else show current recipe
+ if 1 > #recipes then
+ formspec = formspec .. 'label[3,1;' .. mfe(rbi.no_recipes) .. ']'
+ -- always returns a table
+ local drops = r.possible_node_drops(node_name)
+ formspec = formspec .. 'label[0,1.5;'
+ if 0 == #drops then
+ formspec = formspec .. mfe(rbi.drops_on_dig) .. ' ' .. mfe(rbi.nothing) .. ']'
+ elseif 1 == #drops then
+ formspec = formspec .. mfe(rbi.drops_on_dig) .. ']'
+ else
+ formspec = formspec .. mfe(rbi.may_drop_on_dig) .. ']'
end
+ for i, drop_name in ipairs(drops) do
+ formspec = formspec .. 'item_image_button['
+ .. (((i - 1) % 3) + 1) .. ','
+ .. tostring(floor(((i - 1) / 3) + 2))
+ .. ';1.0,1.0;' .. r.image_button_link(drop_name) .. ']'
+ end
+ -- output item on the right
+ formspec = formspec
+ .. 'item_image_button[5,2;1.0,1.0;' .. node_name .. ';normal;]'
+
else
- formspec = formspec.."label[1,5;Alternate "..tostring(receipe_nr).."/"..tostring(#res).."]"
- -- reverse order; default receipes (and thus the most intresting ones) are usually the oldest
- local receipe = res[#res+1-receipe_nr]
- if (receipe.type=='normal' and receipe.items) then
- local width = receipe.width
- if (not(width) or width==0) then
+ if 1 < #recipes then
+ formspec = formspec .. 'label[1,5;'
+ .. mfe(S('Alternate @1/@2', tostring(recipe_nr), tostring(#recipes))) .. ']'
+ end
+ -- reverse order; default recipes (and thus the most intresting ones)
+ -- are usually the oldest
+ local recipe = recipes[#recipes + 1 - recipe_nr]
+ if 'normal' == recipe.type and recipe.items then
+ local width = recipe.width
+ if not width or 0 == width then
width = 3
end
- for i=1,9 do
- if (receipe.items[i]) then
- formspec = formspec.."item_image_button["..(((i-1)%width)+1)..','..(math.floor((i-1)/width)+1)..";1.0,1.0;"..
- replacer.image_button_link(receipe.items[i]).."]"
+ for i = 1, 9 do
+ if recipe.items[i] then
+ formspec = formspec .. 'item_image_button['
+ .. (((i - 1) % width) + 1) .. ','
+ .. tostring(floor((i - 1) / width) + 1)
+ .. ';1.0,1.0;'
+ .. r.image_button_link(recipe.items[i]) .. ']'
end
end
- elseif (receipe.type=='cooking' and receipe.items and #receipe.items==1
- and receipe.output=="") then
- formspec = formspec.."item_image_button[1,1;3.4,3.4;"..replacer.image_button_link('default:furnace_active').."]".. --default_furnace_front.png]"..
- "item_image_button[2.9,2.7;1.0,1.0;"..replacer.image_button_link(receipe.items[1]).."]"..
- "label[1.0,0;"..tostring(receipe.items[1]).."]"..
- "label[0,0.5;This can be used as a fuel.]"
- elseif (receipe.type=='cooking' and receipe.items and #receipe.items==1) then
- formspec = formspec.."item_image_button[1,1;3.4,3.4;"..replacer.image_button_link('default:furnace').."]".. --default_furnace_front.png]"..
- "item_image_button[2.9,2.7;1.0,1.0;"..replacer.image_button_link(receipe.items[1]).."]"
- elseif (receipe.type=='colormachine' and receipe.items and #receipe.items==1) then
- formspec = formspec.."item_image_button[1,1;3.4,3.4;"..replacer.image_button_link('colormachine:colormachine').."]".. --colormachine_front.png]"..
- "item_image_button[2,2;1.0,1.0;"..replacer.image_button_link(receipe.items[1]).."]"
- elseif (receipe.type=='saw' and receipe.items and #receipe.items==1) then
- --formspec = formspec.."item_image[1,1;3.4,3.4;moreblocks:circular_saw]"..
- formspec = formspec.."item_image_button[1,1;3.4,3.4;"..replacer.image_button_link('moreblocks:circular_saw').."]"..
- "item_image_button[2,0.6;1.0,1.0;"..replacer.image_button_link(receipe.items[1]).."]"
+ elseif ('cooking' == recipe.type or 'fuel' == recipe.type)
+ and recipe.items
+ and 1 == #recipe.items
+ and '' == recipe.output
+ then
+ formspec = formspec .. 'item_image_button[1,1;3.4,3.4;'
+ .. r.image_button_link('default:furnace_active') .. ']'
+ .. 'item_image_button[2.9,2.7;1.0,1.0;'
+ .. r.image_button_link(recipe.items[1]) .. ']'
+ .. 'label[1.0,0;' .. tostring(recipe.items[1]) .. ']'
+ .. 'label[0,0.5;' .. mfe(rbi.can_be_fuel) .. ']'
+ elseif 'cooking' == recipe.type
+ and recipe.items
+ and 1 == #recipe.items
+ then
+ formspec = formspec .. 'item_image_button[1,1;3.4,3.4;'
+ .. r.image_button_link('default:furnace') .. ']'
+ .. 'item_image_button[2.2,2.2;1.0,1.0;'
+ .. r.image_button_link(recipe.items[1]) .. ']'
+ elseif recipe.items
+ and 0 < #recipe.items
+ and r.recipe_adders[recipe.type]
+ then
+ local handler = r.recipe_adders[recipe.type]
+ formspec = formspec .. 'item_image_button[1,1;3.4,3.4;'
+ .. r.image_button_link(handler.machine) .. ']'
+ .. 'label[0.1,4.3;' .. mfe(recipe.method) .. ']'
+ local width = recipe.width or #recipe.items
+ width = max(1, min(3, width))
+ local offsets = { 2.2, 1.7, 1.2 }
+ local offset = offsets[width]
+ for i = 1, 9 do
+ if not recipe.items[i] then break end
+ formspec = formspec .. 'item_image_button['
+ .. (((i - 1) % width) + offset) .. ','
+ .. tostring(floor((i - 1) / width) + offset)
+ .. ';1.0,1.0;'
+ .. r.image_button_link(recipe.items[i]) .. ']'
+ end
+ formspec = formspec
+ .. (handler.formspec and handler.formspec(recipe) or '')
else
- formspec = formspec..'label[3,1;Error: Unkown receipe.]'
+--pd('unhandled recipe encountered', recipe)
+--r.play_sound(player_name, true)
+ formspec = formspec .. 'label[3,1;' .. mfe(rbi.unknown_recipe) .. ']'
end
- -- show how many of the items the receipe will yield
- local outstack = ItemStack(receipe.output)
- if (outstack and outstack:get_count() and outstack:get_count()>1) then
- formspec = formspec..'label[5.5,2.5;'..tostring(outstack:get_count())..']'
+ -- output item on the right
+ if recipe.output then
+ formspec = formspec
+ .. 'item_image_button[5,2;1.0,1.0;' .. recipe.output .. ';normal;]'
end
end
- minetest.show_formspec(name, "replacer:crafting", formspec)
-end
+ show_formspec(player_name, 'replacer:crafting', formspec)
+end -- inspect_show_crafting
+
-- translate general formspec calls back to specific calls
-replacer.form_input_handler = function(player, formname, fields)
- if (formname and formname == "replacer:crafting" and player and not(fields.quit)) then
- replacer.inspect_show_crafting(player:get_player_name(), nil, fields)
+function replacer.form_input_handler(player, formname, fields)
+ if formname and 'replacer:crafting' == formname
+ and player and not fields.quit
+ then
+ -- too bad keys are all false :/ could have implemented easy
+ -- switch to unified_inventory formspec
+ --local keys = player:get_player_control()
+ r.inspect_show_crafting(player:get_player_name(), nil, fields)
return
- end
+ end
end
--- establish a callback so that input from the player-specific formspec gets handled
+-- establish a callback so that input from the player-specific
+-- formspec gets handled
minetest.register_on_player_receive_fields(replacer.form_input_handler)
-
-minetest.register_craft({
- output = 'replacer:inspect',
- recipe = {
- { 'default:torch'},
- { 'default:stick'},
- }
-})
diff --git a/locale/replacer.de.tr b/locale/replacer.de.tr
new file mode 100644
index 0000000..8a188ac
--- /dev/null
+++ b/locale/replacer.de.tr
@@ -0,0 +1,128 @@
+# textdomain: replacer
+History=Verlauf
+Choose mode=Wähle Modus
+Both=Beide
+Node=Node
+Rotation=Drehung
+Replace node and apply orientation.=Ersetze Node und Ausrichtung.
+Replace node without changing orientation.=Ersetze Node ohne zu drehen.
+Apply orientation without changing node type.=Nur Ausrichtung anwenden ohne Nodetyp zu ändern.
+Single=Einzel
+Field=Feld
+Crust=Kruste
+Replace single node.=Ersetze einzelnen Node
+
+Left click: Replace field of nodes of a kind where a translucent node is in front of it.@@Right click: Replace field of air where no translucent node is behind the air.=Links Klick: Ersetzt ein Feld gleichartiger Node welche durchsichtige Nachbarnode haben.@@Rechts Klick: Ersetzt die Luft vor dem Feld undurchsichtiger Node.
+
+Left click: Replace nodes which touch another one of its kind and a translucent node, e.g. air.@@Right click: Replace air nodes which touch the crust.=Links Klick: Ersetzt benachbarte, gleichartige Node welche gleichzeitig durchsichtige (nicht feste) Node berühren.@@Rechts Klick: Ersetzt Luft an der Kruste.
+
+Target node not yet loaded. Please wait a moment for the server to catch up.=Zielnode is noch nicht geladen. Bitte warte einen Moment damit der Server aufholen kann.
+Nothing to replace.=Nichts zu ersetzen.
+Not enough charge to use this mode.=Nicht genug Ladung, um diesen Modus zu verwenden.
+Aborted, too many nodes detected.=Abgebrochen, zu viele Knoten gefunden.
+Error: No node selected.=Fehler: Kein Knoten ausgwählt.
+Node replacement tool=Ersetzer
+Node replacement tool (technic)=Ersetzer (Technik)
+Time-limit reached.=Zeitbegrenzung erreicht.
+
+Toggles verbosity.@nchat: When on, messages are posted to chat.@naudio: When off, replacer is silent.=Schaltet die Ausführlichkeit um.@nchat: Wenn eingeschaltet, werden Nachrichten im Chat ausgegeben.@naudio: Wenn ausgeschaltet, ist der Ersetzer stumm.
+
+Minor modes are disabled on this server.=Nebenmodi sind auf diesem Server deaktiviert.
+=
+days=Tage
+
+Inspection Tool@nUse to inspect target node or entity.@nPlace to inspect the adjacent node.=Inspektionswerkzeug@nSchlagen zum Inspizieren der Zielnode oder der Entität.@nPlatzieren zum Inspizieren der angrenzenden Node.
+
+This is a broken object. We have no further information about it. It is located=Dies ist ein kaputtes Objekt. Wir haben keine weiteren Informationen darüber. Es befindet sich
+owned, protected and locked=hat einen Besitzer, ist geschützt und ist gesperrt
+owned and protected=hat einen Besitzer und ist geschützt
+owned and locked=hat einen Besitzer und ist gesperrt
+This is an object=Dies ist ein Objekt
+WARNING: You can't dig this node. It is protected.=WARNUNG: Du kannst diesen Node nicht graben. Es ist geschützt.
+INFO: You can dig this node, others can't.=INFO: Du kannst diese Node graben, andere nicht.
+~ no description provided ~=~ keine Beschreibung vorhanden ~
+~ no node description provided ~=~ keine Nodebeschreibung vorhanden ~
+~ no item description provided ~=~ keine Artikelbeschreibung vorhanden ~
+Name:=Name:
+Exit=Schliessen
+This is:=Dies ist:
+previous recipe=vorheriges Rezept
+next recipe=nächstes Rezept
+No recipes.=Keine Rezepte.
+Drops on dig:=Lässt fallen beim Graben:
+nothing.=nichts.
+May drop on dig:=Kann beim Graben fallen lassen:
+This can be used as a fuel.=Dies kann als Brennstoff verwendet werden.
+Error: Unknown recipe.=Fehler: Unbekanntes Rezept.
+scoop up=schöpfen
+pour out=ausgiessen
+filling=auffüllen
+(Functions may exist that change attributes and conditions of mobs)=(Möglicherweise gibt es Funktionen, die Attribute und Zustände von Mobs ändern)
+Is of type=Ist vom Typ
+Is loyal to owner.=Ist dem Besitzer treu.
+Likes to attack:=Greift gerne an:
+Follows players holding:=Folgt Spielern mit:
+May drop:=Kann fallen lassen:
+Can shoot misiles.=Kann schiessen.
+Can breed.=Kann gezüchtet werden.
+Spawns on:=Erscheinen auf:
+with neighours:=mit Nachbarn:
+Placed:=Platziert:
+Digs:=Ausgrabungen:
+Inflicted:=Verltezte:
+Punched:=Geschlagen:
+XP:=XP:
+Deaths:=Todesfälle:
+Played:=Gespielt:
+Is currently on a mission.=Befindet sich derzeit auf einer Mission.
+You don't have any common channels.=Ihr habt keine gemeinsamen Kanäle.
+You are both on these channels:=Ihr seid beide auf diesen Kanälen:
+Is wearing:=Trägt:
+fermenting/pickling=Gären
+Store near group:wood, light < 12.=In der Nähe der Holzgruppe@n(group:wood) lagern, Licht < 12.
+Replacing nodes of type "@1" is not allowed on this server. Replacement failed.=Node des Typs „@1“ dürfen auf diesem Server nicht ersetzt werden. Versuch fehlgeschlagen.
+Protected at @1=Geschützt bei @1
+printing=Drucken
+Sorry, this is an unknown something of type "@1". No information available.=Tut mir leid, das ist etwas unbekanntes vom Typ „@1“. Keine Informationen verfügbar.
+Can deal @1 damage.=Kann @1 Schaden verursachen.
+Has @1 armour.=Hat @1 Rüstung.
+This is your fellow player "@1"=Dies ist dein Mitspieler „@1“
+at @1=bei @1
+This is an object "@1"=Dies ist ein „@1“ Objekt
+This is an entity "@1"=Das ist eine Entität „@1“
+, dropped @1 minutes ago=, vor @1 Minuten gefallen
+by "@1"=von „@1“
+with order to @1=mit Befehl zu „@1“
+Has @1 different types of items,=Hat @1 verschiedene Arten von Gegenständen,
+total of @1 items in inventory.=insgesamt @1 Artikel im Inventar.
+Located at @1=Befindet sich bei @1
+with param2 of @1=mit param2 von @1
+and receiving @1 light=und empfängt @1 licht
+Alternate @1/@2=Alternative @1/@2
+holding=Halten
+cutting=Schneiden
+shearing=Scheren
+Cut with shears.=Mit Schere schneiden.
+sawing=Sägen
+You have no further "@1". Replacement failed.=Du hast keine weitere „@1“. Ersetzung fehlgeschlagen.
+Unknown node: "@1"=Unbekannter Node: „@1“
+Unknown node to place: "@1"=Unbekant zu setzender Node: „@1“
+Could not dig "@1" properly.=Kann „@1“ nicht richtig graben.
+Could not place "@1".=Konnte „@1“ nicht platzieren.
+Error: "@1" is not a node.=Fehler: „@1“ ist kein Node.
+@1 nodes replaced.=@1 Node ersetzt.
+Mode changed to @1: @2=Modus gewechselt auf @1: @2
+Placing nodes of type "@1" is not allowed on this server.=Das Platzieren von Node vom Typ "@1" ist auf diesem Server nicht erlaubt.
+Failed to set replacer to "@1". If there was one in your inventory, then maybe.=Ersetzer konnte nicht auf "@1" gesetzt werden. Wenn es einen in deinem Inventar gäbe, dann vielleicht.
+Node replacement tool set to:@n@1.=Ersetzer konfiguriert auf:@n@1.
+alloying=Legieren
+CNC machining=CNC-Bearbeitung
+compressing=Komprimieren
+extracting=Extrahieren
+freezing=Einfrieren
+grinding=Mahlen
+separating=Trennen
+painting=Malen
+fermenting=Fermentieren
+Ferment in barrel.=Gärung im Fass.
+
diff --git a/locale/replacer.es.tr b/locale/replacer.es.tr
new file mode 100644
index 0000000..df40df9
--- /dev/null
+++ b/locale/replacer.es.tr
@@ -0,0 +1,127 @@
+# textdomain: replacer
+History=Historia
+Choose mode=Elegir modo
+Both=Ambos
+Node=Nodo
+Rotation=Rotación
+Replace node and apply orientation.=Reemplazar nodo y aplicar orientación.
+Replace node without changing orientation.=Reemplazar nodo sin cambiar la orientación.
+Apply orientation without changing node type.=Aplicar orientación sin cambiar el tipo de nodo.
+Single=Único
+Field=Campo
+Crust=Corteza
+Replace single node.=Reemplazar nodo único.
+
+Left click: Replace field of nodes of a kind where a translucent node is in front of it.@@Right click: Replace field of air where no translucent node is behind the air.=Clic izquierdo: Reemplazar campo de nodos de un tipo donde hay un nodo translúcido frente a él.@@ Clic derecho: Reemplazar campo de aire donde no hay un nodo translúcido detrás del aire.
+
+Left click: Replace nodes which touch another one of its kind and a translucent node, e.g. air.@@Right click: Replace air nodes which touch the crust.=Clic izquierdo: reemplaza los nodos que tocan otro de su tipo y un nodo translúcido, p. aire.@@Clic derecho: Reemplazar los nodos de aire que tocan la corteza.
+
+Target node not yet loaded. Please wait a moment for the server to catch up.=Nodo de destino aún no cargado. Espere un momento a que el servidor se ponga al día.
+Nothing to replace.=Nada para reemplazar.
+Not enough charge to use this mode.=No hay suficiente carga para usar este modo.
+Aborted, too many nodes detected.=Anulado, demasiados nodos detectados.
+Error: No node selected.=Error: No se seleccionó ningún nodo.
+Node replacement tool=Intercambiador
+Node replacement tool (technic)=Intercambiador (técnica)
+Time-limit reached.=Límite de tiempo alcanzado.
+
+Toggles verbosity.@nchat: When on, messages are posted to chat.@naudio: When off, replacer is silent.=Alterna la verbosidad. @nchat: cuando está activado, los mensajes se publican en el chat. @naudio: cuando está desactivado, el intercambiador está en silencio.
+
+Minor modes are disabled on this server.=Los modos menores están deshabilitados en este servidor.
+=
+days=días
+
+Inspection Tool@nUse to inspect target node or entity.@nPlace to inspect the adjacent node.=Herramienta de inspección@nUsar para inspeccionar el nodo o la entidad de destino.@nColocar para inspeccionar el nodo adyacente.
+
+This is a broken object. We have no further information about it. It is located=Este es un objeto roto. No tenemos más información al respecto. se encuentra
+owned, protected and locked=propietario, protegido y cerrado
+owned and protected=propietario y protegido
+owned and locked=propietario y bloqueado
+This is an object=Este es un objeto
+WARNING: You can't dig this node. It is protected.=ADVERTENCIA: No puedes excavar este nodo. Está protegido.
+INFO: You can dig this node, others can't.=INFO: Puedes excavar este nodo, pero otros no.
+~ no description provided ~=~ no se proporciona descripción ~
+~ no node description provided ~=~ no se proporcionó una descripción del nodo ~
+~ no item description provided ~=~ no se proporciona descripción del artículo ~
+Name:=Nombre:
+Exit=Salir
+This is:=Esto es:
+previous recipe=receta anterior
+next recipe=siguiente receta
+No recipes.=Sin recetas.
+Drops on dig:=Cae en excavación:
+nothing.=nada.
+May drop on dig:=Puede caer en excavación:
+This can be used as a fuel.=Esto se puede usar como combustible.
+Error: Unknown recipe.=Error: Receta desconocida.
+scoop up=recoger
+pour out=derramar
+filling=llena
+(Functions may exist that change attributes and conditions of mobs)=(Pueden existir funciones que cambien los atributos y condiciones de los mobs)
+Is of type=es de tipo
+Is loyal to owner.=Es leal al dueño.
+Likes to attack:=Le gusta atacar:
+Follows players holding:=Sigue a los jugadores que tienen:
+May drop:=Puede caer:
+Can shoot misiles.=Puede disparar misiles.
+Can breed.=Puede reproducirse.
+Spawns on:=Aparece en:
+with neighours:=con vecinos:
+Placed:=Colocados:
+Digs:=Excavaciones:
+Inflicted:=Infligido:
+Punched:=Golpes:
+XP:=EXP:
+Deaths:=Fallecidos:
+Played:=Jugó:
+Is currently on a mission.=Está actualmente en una misión.
+You don't have any common channels.=No tienes ningún canal común.
+You are both on these channels:=Ambos están en estos canales:
+Is wearing:=Está vistiendo:
+fermenting/pickling=fermentación/decapado
+Store near group:wood, light < 12.=Almacenar cerca del grupo de madera@n(group:wood), luz < 12.
+Replacing nodes of type "@1" is not allowed on this server. Replacement failed.=No se permite reemplazar nodos de tipo "@1" en este servidor. Reemplazo fallido.
+Protected at @1=Protegido en @1
+printing=impresión
+Sorry, this is an unknown something of type "@1". No information available.=Lo sentimos, esto es algo desconocido de tipo "@1". No hay información disponible.
+Can deal @1 damage.=Puede causar daño @1.
+Has @1 armour.=Tiene armadura @1.
+This is your fellow player "@1"=Este es tu compañero de juego "@1"
+at @1=en @1
+This is an object "@1"=Este es un objeto "@1"
+This is an entity "@1"=Esta es una entidad "@1"
+, dropped @1 minutes ago=, caído hace @1 minutos
+by "@1"=por "@1"
+with order to @1=con pedido a "@1"
+Has @1 different types of items,=Tiene @1 tipos diferentes de artículos,
+total of @1 items in inventory.=total de @1 artículos en inventario.
+Located at @1=Ubicado en @1
+with param2 of @1=con param2 de @1
+and receiving @1 light=y recibiendo @1 luz
+Alternate @1/@2=Alternativo @1/@2
+holding=sosteniendo
+cutting=corte
+shearing=cizallamiento
+Cut with shears.=Cortar con tijeras.
+sawing=aserradura
+You have no further "@1". Replacement failed.=No tienes más "@1". Reemplazo fallido.
+Unknown node: "@1"=Nodo desconocido: "@1"
+Unknown node to place: "@1"=Nodo desconocido a colocar: "@1"
+Could not dig "@1" properly.=No se pudo excavar "@1" correctamente.
+Could not place "@1".=No se pudo colocar "@1".
+Error: "@1" is not a node.=Error: "@1" no es un nodo.
+@1 nodes replaced.=@1 nodos reemplazados.
+Mode changed to @1: @2=Modo cambiado a @1: @2
+Placing nodes of type "@1" is not allowed on this server.=No se permite colocar nodos de tipo "@1" en este servidor.
+Failed to set replacer to "@1". If there was one in your inventory, then maybe.=No se pudo establecer el intercambiador en "@1". Si habías uno en tu inventario, entonces tal vez.
+Node replacement tool set to:@n@1.=Intercambiador establecida en:@n@1.
+alloying=aleación
+CNC machining=Mecanizado CNC
+compressing=comprimiendo
+extracting=extrayendo
+freezing=congelación
+grinding=molienda
+separating=separando
+painting=pintaando
+fermenting=fermentando
+Ferment in barrel.=Fermentación en barrica.
diff --git a/locale/replacer.fi.tr b/locale/replacer.fi.tr
new file mode 100644
index 0000000..a91c3e1
--- /dev/null
+++ b/locale/replacer.fi.tr
@@ -0,0 +1,127 @@
+# textdomain: replacer
+History=Historia
+Choose mode=Valitse tila
+Both=Molemmat
+Node=Solmu
+Rotation=Kierto
+Replace node and apply orientation.=Korvaa solmu ja käytä suuntaa.
+Replace node without changing orientation.=Vaihda solmu suuntaa muuttamatta.
+Apply orientation without changing node type.=Käytä suuntaa muuttamatta solmun tyyppiä.
+Single=Single
+Field=Kenttä
+Crust=Kuori
+Replace single node.=Korvaa yksi solmu.
+
+Left click: Replace field of nodes of a kind where a translucent node is in front of it.@@Right click: Replace field of air where no translucent node is behind the air.=Vasen napsautus: Korvaa sellaisten solmujen kenttä, joissa läpikuultava solmu on sen edessä.@@Oikea napsautus: Korvaa ilmakenttä, jossa ilman takana ei ole läpikuultavaa solmua.
+
+Left click: Replace nodes which touch another one of its kind and a translucent node, e.g. air.@@Right click: Replace air nodes which touch the crust.=Vasen napsautus: Vaihda solmut, jotka koskettavat toista laatuaan ja läpikuultavaa solmua, esim. air.@@Kakkosnapsautus: Vaihda kuorta koskettavat ilmasolmut.
+
+Target node not yet loaded. Please wait a moment for the server to catch up.=Kohdesolmua ei ole vielä ladattu. Odota hetki, että palvelin ottaa kiinni.
+Nothing to replace.=Ei mitään korvattavaa.
+Not enough charge to use this mode.=Lataus ei riitä tämän tilan käyttämiseen.
+Aborted, too many nodes detected.=Keskeytetty, liian monta solmua havaittu.
+Error: No node selected.=Virhe: solmua ei ole valittu.
+Node replacement tool=Solmun vaihtotyökalu
+Node replacement tool (technic)=Solmun vaihtotyökalu (tekninen)
+Time-limit reached.=Aikaraja saavutettu.
+
+Toggles verbosity.@nchat: When on, messages are posted to chat.@naudio: When off, replacer is silent.=Vaihtaa verbosity.@nchat: Kun päällä, viestit lähetetään chat.@naudio: Kun pois päältä, korvaaja on äänetön.
+
+Minor modes are disabled on this server.=Pienet tilat on poistettu käytöstä tällä palvelimella.
+=
+days=päivää
+
+Inspection Tool@nUse to inspect target node or entity.@nPlace to inspect the adjacent node.=Tarkastustyökalu@nTarkista kohdesolmu tai entiteetti.@nTarkista viereinen solmu.
+
+This is a broken object. We have no further information about it. It is located=Tämä on rikkinäinen esine. Meillä ei ole asiasta enempää tietoa. Se sijaitsee
+owned, protected and locked=omistettu, suojattu ja lukittu
+owned and protected=omistettu ja suojattu
+owned and locked=omistettu ja lukittu
+This is an object=Tämä on objekti
+WARNING: You can't dig this node. It is protected.=VAROITUS: Et voi kaivaa tätä solmua. Se on suojattu.
+INFO: You can dig this node, others can't.=INFO: Voit kaivaa tämän solmun, mutta muut eivät.
+~ no description provided ~=~ ei kuvausta ~
+~ no node description provided ~=~ solmun kuvausta ei ole annettu ~
+~ no item description provided ~=~ ei tuotekuvausta ~
+Name:=Nimi:
+Exit=Poistu
+This is:=Tämä on:
+previous recipe=edellinen resepti
+next recipe=seuraava resepti
+No recipes.=Ei reseptejä.
+Drops on dig:=Pudotukset kaivamaan:
+nothing.=ei mitään.
+May drop on dig:=Saattaa pudota kaivamaan:
+This can be used as a fuel.=Tätä voidaan käyttää polttoaineena.
+Error: Unknown recipe.=Virhe: Tuntematon resepti.
+scoop up=kauhoa
+pour out=kaataa
+filling=täyttää
+(Functions may exist that change attributes and conditions of mobs)=(Voi olla toimintoja, jotka muuttavat väkijoukon ominaisuuksia ja ehtoja)
+Is of type=On tyyppiä
+Is loyal to owner.=On lojaali omistajalleen.
+Likes to attack:=Tykkää hyökätä:
+Follows players holding:=Seuraa pelaajia, joilla on:
+May drop:=Saattaa pudota:
+Can shoot misiles.=Osaa ampua ohjuksia.
+Can breed.=Voi lisääntyä.
+Spawns on:=Syntyy:
+with neighours:=naapureiden kanssa:
+Placed:=Sijoitettu:
+Digs:=Kaivaukset:
+Inflicted:=Aiheutettu:
+Punched:=Lävistetty:
+XP:=XP:
+Deaths:=Kuolemat:
+Played:=Pelasi:
+Is currently on a mission.=On tällä hetkellä tehtävässä.
+You don't have any common channels.=Sinulla ei ole yhteisiä kanavia.
+You are both on these channels:=Olette molemmat näillä kanavilla:
+Is wearing:=On yllään:
+fermenting/pickling=käyminen/peittaus
+Store near group:wood, light < 12.=Varasto lähellä group:wood,@nkevyt < 12.
+Replacing nodes of type "@1" is not allowed on this server. Replacement failed.=Tyypin "@1" solmujen korvaaminen ei ole sallittua tässä palvelimessa. Vaihto epäonnistui.
+Protected at @1=Suojattu @1
+printing=painatus
+Sorry, this is an unknown something of type "@1". No information available.=Anteeksi, tämä on tuntematon asia, jonka tyyppi on "@1". Tietoja ei ole saatavilla.
+Can deal @1 damage.=Voi tehdä @1 vahinkoa.
+Has @1 armour.=Siinä on @1-panssari.
+This is your fellow player "@1"=Tämä on pelikaverisi "@1"
+at @1=osoitteessa @1
+This is an object "@1"=Tämä on objekti "@1"
+This is an entity "@1"=Tämä on entiteetti "@1"
+, dropped @1 minutes ago=, pudonnut @1 minuuttia sitten
+by "@1"=kirjoittaja "@1"
+with order to @1=tilauksella "@1"
+Has @1 different types of items,=Sisältää @1 erityyppistä tuotetta,
+total of @1 items in inventory.=yhteensä @1 tuotetta varastossa.
+Located at @1=Sijaitsee @1
+with param2 of @1=param2 @1
+and receiving @1 light=ja vastaanottaa @1 valoa
+Alternate @1/@2=Vaihtoehto @1/@2
+holding=pitää
+cutting=leikkaus
+shearing=leikkaus
+Cut with shears.=Leikkaa saksilla.
+sawing=sahaus
+You have no further "@1". Replacement failed.=Sinulla ei ole enempää "@1". Vaihto epäonnistui.
+Unknown node: "@1"=Tuntematon solmu: "@1"
+Unknown node to place: "@1"=Tuntematon sijoitettava solmu: "@1"
+Could not dig "@1" properly.=Ei voitu kaivaa "@1" oikein.
+Could not place "@1".=Ei voitu sijoittaa "@1".
+Error: "@1" is not a node.=Virhe: "@1" ei ole solmu.
+@1 nodes replaced.=@1 solmua vaihdettu.
+Mode changed to @1: @2=Tila muutettu tilaksi @1: @2
+Placing nodes of type "@1" is not allowed on this server.=Tyypin "@1" solmujen sijoittaminen ei ole sallittua tälle palvelimelle.
+Failed to set replacer to "@1". If there was one in your inventory, then maybe.=Korvaajan asettaminen arvoon "@1" epäonnistui. Jos sellainen oli varastossasi, niin ehkä.
+Node replacement tool set to:@n@1.=Solmun korvaustyökalu asetettu arvoon:@n@1.
+alloying=seostus
+CNC machining=CNC-työstö
+compressing=pakkaaminen
+extracting=purkaminen
+freezing=jäätyminen
+grinding=hionta
+separating=erottava
+painting=maalaus
+fermenting=käyminen
+Ferment in barrel.=Fermentoida tynnyrissä.
diff --git a/locale/replacer.fr.tr b/locale/replacer.fr.tr
new file mode 100644
index 0000000..ed8189b
--- /dev/null
+++ b/locale/replacer.fr.tr
@@ -0,0 +1,127 @@
+# textdomain: replacer
+History=Historique
+Choose mode=Choisi le mode
+Both=Les deux
+Node=Nœud
+Rotation=Rotation
+Replace node and apply orientation.=Remplacer le nœud et appliquer l'orientation.
+Replace node without changing orientation.=Remplacer le nœud sans changer l'orientation.
+Apply orientation without changing node type.=Appliquer l'orientation sans changer le type de nœud.
+Single=Unique
+Field=Champ
+Crust=Croûte
+Replace single node.=Remplacer un nœud unique.
+
+Left click: Replace field of nodes of a kind where a translucent node is in front of it.@@Right click: Replace field of air where no translucent node is behind the air.=Clic gauche : Remplacer le champ de nœuds d'un type où un nœud translucide est devant.@@Clic droit : Remplacer le champ d'air où aucun nœud translucide n'est derrière l'air.
+
+Left click: Replace nodes which touch another one of its kind and a translucent node, e.g. air.@@Right click: Replace air nodes which touch the crust.=Clic gauche : Remplacer les nœuds qui touchent un autre du même genre et un nœud translucide, par ex. air.@@Clic droit : Remplacer les nœuds d'air qui touchent la croûte.
+
+Target node not yet loaded. Please wait a moment for the server to catch up.=Le nœud cible n'est pas encore chargé. Patienter quelques instants, que le serveur rattrape.
+Nothing to replace.=Rien à remplacer.
+Not enough charge to use this mode.=Pas assez de charge pour utiliser ce mode.
+Aborted, too many nodes detected.=Abandonné, trop de nœuds détectés.
+Error: No node selected.=Erreur : Aucun nœud sélectionné.
+Node replacement tool=Remplaçant
+Node replacement tool (technic)=Remplaçant (technique)
+Time-limit reached.=Délai atteint.
+
+Toggles verbosity.@nchat: When on, messages are posted to chat.@naudio: When off, replacer is silent.=Active/désactive la verbosité.@nchat : lorsque cette option est activée, les messages sont publiés sur le chat.@naudio : lorsqu'elle est désactivée, le remplaçant est silencieux.
+
+Minor modes are disabled on this server.=Les modes mineurs sont désactivés sur ce serveur.
+=
+days=jours
+
+Inspection Tool@nUse to inspect target node or entity.@nPlace to inspect the adjacent node.=Outil d'inspection@nUtiliser pour inspecter le nœud ou l'entité cible.@nPlacer pour inspecter le nœud adjacent.
+
+This is a broken object. We have no further information about it. It is located=Ceci est un objet cassé. Nous n'avons pas d'autres informations à ce sujet. Il est situé
+owned, protected and locked=possédé, protégé et verrouillé
+owned and protected=possédé et protégé
+owned and locked=possédé et verrouillé
+This is an object=Ceci est un objet
+WARNING: You can't dig this node. It is protected.=AVERTISSEMENT : Tu ne peux pas creuser ce nœud. Il est protégé.
+INFO: You can dig this node, others can't.=INFO : Tu peux creuser ce nœud, mais les autres ne le peuvent pas.
+~ no description provided ~=~ aucune description fournie ~
+~ no node description provided ~=~ aucune description de nœud fournie ~
+~ no item description provided ~=~ aucune description d'article fournie ~
+Name:=Nom :
+Exit=Sortir
+This is:=C'est :
+previous recipe=recette précédente
+next recipe=recette suivante
+No recipes.=Aucune recette.
+Drops on dig:=Gouttes sur creuser :
+nothing.=rien.
+May drop on dig:=Peut tomber en creusant :
+This can be used as a fuel.=Cela peut être utilisé comme carburant.
+Error: Unknown recipe.=Erreur : recette inconnue.
+scoop up=ramasser
+pour out=déverser
+filling=remplir
+(Functions may exist that change attributes and conditions of mobs)=(Des fonctions peuvent exister qui modifient les attributs et les conditions des monstres)
+Is of type=Est de type
+Is loyal to owner.=Est fidèle au propriétaire.
+Likes to attack:=Aime attaquer :
+Follows players holding:=Suit les joueurs détenant :
+May drop:=Peut baisser :
+Can shoot misiles.=Peut tirer des missiles.
+Can breed.=Peut se reproduire.
+Spawns on:=Apparaît sur :
+with neighours:=avec des voisins :
+Placed:=Mises en place :
+Digs:=Creusers :
+Inflicted:=Infligé :
+Punched:=Frappes :
+XP:=XP :
+Deaths:=Décès :
+Played:=Joué :
+Is currently on a mission.=Est actuellement en mission.
+You don't have any common channels.=Vous n'avez aucun canal commun.
+You are both on these channels:=Vous êtes tous les deux sur ces canaux :
+Is wearing:=Porte :
+fermenting/pickling=fermentation/marinage
+Store near group:wood, light < 12.=Entreposer près du groupe @ngroup:wood, lumière < 12.
+Replacing nodes of type "@1" is not allowed on this server. Replacement failed.=Le remplacement de nœuds de type "@1" n'est pas autorisé sur ce serveur. Échec du remplacement.
+Protected at @1=Protégé à @1
+printing=impression
+Sorry, this is an unknown something of type "@1". No information available.=Désolé, c'est quelque chose d'inconnu de type "@1". Aucune information disponible.
+Can deal @1 damage.=Peut infliger @1 dégâts.
+Has @1 armour.=A @1 armure.
+This is your fellow player "@1"=C'est votre coéquipier "@1"
+at @1=à @1
+This is an object "@1"=Ceci est un objet "@1"
+This is an entity "@1"=Ceci est une entité "@1"
+, dropped @1 minutes ago=, déposé il y a @1 minutes
+by "@1"=par "@1"
+with order to @1=avec l'ordre de "@1"
+Has @1 different types of items,=A @1 différents types d'éléments,
+total of @1 items in inventory.=total de @1 articles dans l'inventaire.
+Located at @1=Situé à @1
+with param2 of @1=avec param2 de @1
+and receiving @1 light=et recevant @1 lumière
+Alternate @1/@2=Alternance @1/@2
+holding=Tient
+cutting=Coupe
+shearing=Tonte
+Cut with shears.=Couper avec des cisailles.
+sawing=Sciage
+You have no further "@1". Replacement failed.=Tu n'as plus de "@1". Échec du remplacement.
+Unknown node: "@1"=Nœud inconnu : "@1"
+Unknown node to place: "@1"=Nœud inconnu à placer : "@1"
+Could not dig "@1" properly.=Impossible de creuser "@1" correctement.
+Could not place "@1".=Impossible de placer "@1".
+Error: "@1" is not a node.=Erreur : "@1" n'est pas un nœud.
+@1 nodes replaced.=@1 nœuds remplacés.
+Mode changed to @1: @2=Mode changé en @1 : @2
+Placing nodes of type "@1" is not allowed on this server.=Le placement de nœuds de type "@1" n'est pas autorisé sur ce serveur.
+Failed to set replacer to "@1". If there was one in your inventory, then maybe.=Impossible de définir le remplaçant sur "@1". S'il y en avait un dans votre inventaire, alors peut-être.
+Node replacement tool set to:@n@1.=Remplaçant défini sur :@n@1.
+alloying=alliage
+CNC machining=Usinage CNC
+compressing=compression
+extracting=extraire
+freezing=gelé
+grinding=affûtage
+separating=séparer
+painting=peindre
+fermenting=fermentation
+Ferment in barrel.=Fermentation en barrique.
diff --git a/locale/replacer.it.tr b/locale/replacer.it.tr
new file mode 100644
index 0000000..f7083dd
--- /dev/null
+++ b/locale/replacer.it.tr
@@ -0,0 +1,127 @@
+# textdomain: replacer
+History=Storia
+Choose mode=Scegli modalità
+Both=Entrambi
+Node=Nodo
+Rotation=Rotazione
+Replace node and apply orientation.=Sostituisci il nodo e applica l'orientamento.
+Replace node without changing orientation.=Sostituisci il nodo senza modificare l'orientamento.
+Apply orientation without changing node type.=Applicare l'orientamento senza modificare il tipo di nodo.
+Single=Singolo
+Field=Campo
+Crust=Crosta
+Replace single node.=Sostituisci singolo nodo.
+
+Left click: Replace field of nodes of a kind where a translucent node is in front of it.@@Right click: Replace field of air where no translucent node is behind the air.=Clic sinistro: Sostituisci il campo di nodi di un tipo in cui un nodo traslucido è davanti ad esso.@@Clic destro: Sostituisci il campo d'aria dove nessun nodo traslucido è dietro l'aria.
+
+Left click: Replace nodes which touch another one of its kind and a translucent node, e.g. air.@@Right click: Replace air nodes which touch the crust.=Clic sinistro: Sostituisci i nodi che toccano un altro del suo genere e un nodo traslucido, ad es. air.@@Clic destro: Sostituisci i nodi d'aria che toccano la crosta.
+
+Target node not yet loaded. Please wait a moment for the server to catch up.=Nodo di destinazione non ancora caricato. Si prega di attendere un momento affinché il server raggiunga.
+Nothing to replace.=Niente da sostituire.
+Not enough charge to use this mode.=Carica insufficiente per utilizzare questa modalità.
+Aborted, too many nodes detected.=Interrotto, troppi nodi rilevati.
+Error: No node selected.=Errore: nessun nodo selezionato.
+Node replacement tool=Strumento di sostituzione del nodo
+Node replacement tool (technic)=Strumento di sostituzione del nodo (tecnico)
+Time-limit reached.=Tempo limite raggiunto.
+
+Toggles verbosity.@nchat: When on, messages are posted to chat.@naudio: When off, replacer is silent.=Attiva/disattiva verbosità.@nchat: quando è attivo, i messaggi vengono inviati alla chat.@naudio: quando è disattivato, il sostituto è silenzioso.
+
+Minor modes are disabled on this server.=Le modalità minori sono disabilitate su questo server.
+=
+days=giorni
+
+Inspection Tool@nUse to inspect target node or entity.@nPlace to inspect the adjacent node.=Strumento di ispezione@nUtilizzare per ispezionare il nodo o l'entità di destinazione.@nPosizionare per ispezionare il nodo adiacente.
+
+This is a broken object. We have no further information about it. It is located=Questo è un oggetto rotto. Non abbiamo ulteriori informazioni a riguardo. Si trova
+owned, protected and locked=posseduto, protetto e bloccato
+owned and protected=posseduto e protetto
+owned and locked=posseduto e bloccato
+This is an object=Questo è un oggetto
+WARNING: You can't dig this node. It is protected.=ATTENZIONE: non puoi scavare questo nodo. È protetto.
+INFO: You can dig this node, others can't.=INFO: puoi scavare questo nodo, ma altri no.
+~ no description provided ~=~ nessuna descrizione fornita ~
+~ no node description provided ~=~ nessuna descrizione del nodo fornita ~
+~ no item description provided ~=~ nessuna descrizione dell'oggetto fornita ~
+Name:=Nome:
+Exit=Uscita
+This is:=Questo è:
+previous recipe=ricetta precedente
+next recipe=prossima ricetta
+No recipes.=Nessuna ricetta.
+Drops on dig:=Gocce allo scavo:
+nothing.=niente.
+May drop on dig:=Può cadere durante lo scavo:
+This can be used as a fuel.=Questo può essere usato come carburante.
+Error: Unknown recipe.=Errore: ricetta sconosciuta.
+scoop up=raccogliere
+pour out=versare
+filling=riempire
+(Functions may exist that change attributes and conditions of mobs)=(Potrebbero esistere funzioni che modificano gli attributi e le condizioni dei mob)
+Is of type=È di tipo
+Is loyal to owner.=È fedele al proprietario.
+Likes to attack:=Ama attaccare:
+Follows players holding:=Segue i giocatori che tengono:
+May drop:=Può cadere:
+Can shoot misiles.=Può sparare missili.
+Can breed.=Può riprodursi.
+Spawns on:=Genera su:
+with neighours:=con i vicini:
+Placed:=Posizionato:
+Digs:=Scavi:
+Inflicted:=Inflitto:
+Punched:=Punzonato:
+XP:=XP:
+Deaths:=Deceduti:
+Played:=Giocato:
+Is currently on a mission.=Attualmente è in missione.
+You don't have any common channels.=Non hai canali comuni.
+You are both on these channels:=Siete entrambi su questi canali:
+Is wearing:=Indossa:
+fermenting/pickling=fermentazione/decapaggio
+Store near group:wood, light < 12.=Negozio vicino al gruppo group:wood,@nluce < 12.
+Replacing nodes of type "@1" is not allowed on this server. Replacement failed.=La sostituzione dei nodi di tipo "@1" non è consentita su questo server. Sostituzione non riuscita.
+Protected at @1=Protetto a @1
+printing=stampa
+Sorry, this is an unknown something of type "@1". No information available.=Siamo spiacenti, questo è un qualcosa sconosciuto di tipo "@1". Nessuna informazione disponibile.
+Can deal @1 damage.=Può infliggere @1 danno.
+Has @1 armour.=Ha @1 armatura.
+This is your fellow player "@1"=Questo è il tuo compagno di gioco "@1"
+at @1=alle @1
+This is an object "@1"=Questo è un oggetto "@1"
+This is an entity "@1"=Questa è un'entità "@1"
+, dropped @1 minutes ago=, caduto @1 minuti fa
+by "@1"=per "@1"
+with order to @1=con ordine a "@1"
+Has @1 different types of items,=Ha @1 diversi tipi di elementi,
+total of @1 items in inventory.=totale di @1 articoli nell'inventario.
+Located at @1=Situato a @1
+with param2 of @1=con param2 di @1
+and receiving @1 light=e ricevendo @1 luce
+Alternate @1/@2=Alternativo @1/@2
+holding=tenendo
+cutting=taglio
+shearing=tosatura
+Cut with shears.=Tagliare con le forbici.
+sawing=segare
+You have no further "@1". Replacement failed.=Non hai più "@1". Sostituzione non riuscita.
+Unknown node: "@1"=Nodo sconosciuto: "@1"
+Unknown node to place: "@1"=Nodo sconosciuto da posizionare: "@1"
+Could not dig "@1" properly.=Impossibile scavare correttamente "@1".
+Could not place "@1".=Impossibile posizionare "@1".
+Error: "@1" is not a node.=Errore: "@1" non è un nodo.
+@1 nodes replaced.=@1 nodi sostituiti.
+Mode changed to @1: @2=Modalità modificata in @1: @2
+Placing nodes of type "@1" is not allowed on this server.=Il posizionamento di nodi di tipo "@1" non è consentito su questo server.
+Failed to set replacer to "@1". If there was one in your inventory, then maybe.=Impossibile impostare il sostituto su "@1". Se ce n'era uno nel tuo inventario, allora forse.
+Node replacement tool set to:@n@1.=Strumento di sostituzione del nodo impostato su:@n@1.
+alloying=legare
+CNC machining=Lavorazione CNC
+compressing=compressione
+extracting=estraendo
+freezing=congelamento
+grinding=macinazione
+separating=separare
+painting=dipingere
+fermenting=fermentazione
+Ferment in barrel.=Fermentazione in botte.
diff --git a/locale/replacer.pt.tr b/locale/replacer.pt.tr
new file mode 100644
index 0000000..d628650
--- /dev/null
+++ b/locale/replacer.pt.tr
@@ -0,0 +1,127 @@
+# textdomain: replacer
+History=Histórico
+Choose mode=Escolha o modo
+Both=Ambos
+Node=Nó
+Rotation=Rotação
+Replace node and apply orientation.=Substitua o nó e aplique a orientação.
+Replace node without changing orientation.=Substitua o nó sem alterar a orientação.
+Apply orientation without changing node type.=Aplique a orientação sem alterar o tipo de nó.
+Single=Único
+Field=Campo
+Crust=Crosta
+Replace single node.=Substitua um único nó.
+
+Left click: Replace field of nodes of a kind where a translucent node is in front of it.@@Right click: Replace field of air where no translucent node is behind the air.=Clique com o botão esquerdo: Substitui o campo de nós de um tipo onde um nó translúcido está na frente dele.@@Clique com o botão direito: Substitui o campo de ar onde nenhum nó translúcido está atrás do ar.
+
+Left click: Replace nodes which touch another one of its kind and a translucent node, e.g. air.@@Right click: Replace air nodes which touch the crust.=Clique com o botão esquerdo: Substitui os nós que tocam em outro de seu tipo e um nó translúcido, por exemplo air.@@Clique com o botão direito: Substitua os nós de ar que tocam a crosta.
+
+Target node not yet loaded. Please wait a moment for the server to catch up.=O nó de destino ainda não foi carregado. Aguarde um momento para o servidor recuperar o atraso.
+Nothing to replace.=Nada para substituir.
+Not enough charge to use this mode.=Carga insuficiente para usar este modo.
+Aborted, too many nodes detected.=Abortado, muitos nós detectados.
+Error: No node selected.=Erro: Nenhum nó selecionado.
+Node replacement tool=Ferramenta de substituição de nós
+Node replacement tool (technic)=Ferramenta de substituição de nós (técnica)
+Time-limit reached.=Limite de tempo atingido.
+
+Toggles verbosity.@nchat: When on, messages are posted to chat.@naudio: When off, replacer is silent.=Alterna a verbosidade.@nchat: Quando ativado, as mensagens são postadas no chat.@naudio: Quando desativado, o substituto é silencioso.
+
+Minor modes are disabled on this server.=Os modos secundários estão desabilitados neste servidor.
+=
+days=dias
+
+Inspection Tool@nUse to inspect target node or entity.@nPlace to inspect the adjacent node.=Ferramenta de Inspeção@nUse para inspecionar o nó ou entidade de destino.@nPlace para inspecionar o nó adjacente.
+
+This is a broken object. We have no further information about it. It is located=Este é um objeto quebrado. Não temos mais informações a respeito. está localizado
+owned, protected and locked=possuído, protegido e bloqueado
+owned and protected=possuído e protegido
+owned and locked=possuído e bloqueado
+This is an object=Este é um objeto
+WARNING: You can't dig this node. It is protected.=AVISO: você não pode cavar este nó. Está protegido.
+INFO: You can dig this node, others can't.=INFO: Você pode cavar este nó, mas outros não podem.
+~ no description provided ~=~ nenhuma descrição fornecida ~
+~ no node description provided ~=~ nenhuma descrição do nó fornecida ~
+~ no item description provided ~=~ nenhuma descrição do item fornecida ~
+Name:=Nome:
+Exit=Saída
+This is:=Isto é:
+previous recipe=receita anterior
+next recipe=próxima receita
+No recipes.=Sem receitas.
+Drops on dig:=Gotas na escavação:
+nothing.=nada.
+May drop on dig:=Pode cair na escavação:
+This can be used as a fuel.=Isso pode ser usado como combustível.
+Error: Unknown recipe.=Erro: receita desconhecida.
+scoop up=escavar
+pour out=derramar
+filling=encher
+(Functions may exist that change attributes and conditions of mobs)=(Podem existir funções que alteram atributos e condições dos mobs)
+Is of type=É do tipo
+Is loyal to owner.=É leal ao dono.
+Likes to attack:=Gosta de atacar:
+Follows players holding:=Segue jogadores segurando:
+May drop:=Pode cair:
+Can shoot misiles.=Pode disparar mísseis.
+Can breed.=Pode procriar.
+Spawns on:=Gera em:
+with neighours:=com vizinhos:
+Placed:=Colocada:
+Digs:=Escavações:
+Inflicted:=Infligido:
+Punched:=Golpes:
+XP:=EXP:
+Deaths:=Mortes:
+Played:=Jogado:
+Is currently on a mission.=Atualmente está em uma missão.
+You don't have any common channels.=Você não tem canais comuns.
+You are both on these channels:=Vocês dois estão nestes canais:
+Is wearing:=Está vestindo:
+fermenting/pickling=fermentação/decapagem
+Store near group:wood, light < 12.=Armazenar perto do grupo group:wood,@nluz < 12.
+Replacing nodes of type "@1" is not allowed on this server. Replacement failed.=A substituição de nós do tipo "@1" não é permitida neste servidor. Falha na substituição.
+Protected at @1=Protegido em @1
+printing=impressão
+Sorry, this is an unknown something of type "@1". No information available.=Desculpe, isso é algo desconhecido do tipo "@1". Nenhuma informação disponível.
+Can deal @1 damage.=Pode causar @1 de dano.
+Has @1 armour.=Tem armadura @1.
+This is your fellow player "@1"=Este é seu colega jogador "@1"
+at @1=em @1
+This is an object "@1"=Este é um objeto "@1"
+This is an entity "@1"=Esta é uma entidade "@1"
+, dropped @1 minutes ago=, caiu @1 minutos atrás
+by "@1"=por "@1"
+with order to @1=com ordem para "@1"
+Has @1 different types of items,=Tem @1 tipos diferentes de itens,
+total of @1 items in inventory.=total de @1 itens no inventário.
+Located at @1=Localizado em @1
+with param2 of @1=com param2 de @1
+and receiving @1 light=e recebendo @1 luz
+Alternate @1/@2=Alternativo @1/@2
+holding=segurando
+cutting=corte
+shearing=cisalhamento
+Cut with shears.=Corte com tesoura.
+sawing=serrar
+You have no further "@1". Replacement failed.=Você não tem mais "@1". Falha na substituição.
+Unknown node: "@1"=Nó desconhecido: "@1"
+Unknown node to place: "@1"=Nó desconhecido para colocar: "@1"
+Could not dig "@1" properly.=Não foi possível cavar "@1" corretamente.
+Could not place "@1".=Não foi possível colocar "@1".
+Error: "@1" is not a node.=Erro: "@1" não é um nó.
+@1 nodes replaced.=@1 nós substituídos.
+Mode changed to @1: @2=Modo alterado para @1: @2
+Placing nodes of type "@1" is not allowed on this server.=A colocação de nós do tipo "@1" não é permitida neste servidor.
+Failed to set replacer to "@1". If there was one in your inventory, then maybe.=Falha ao definir o substituto para "@1". Se houvesse um em seu inventário, então talvez.
+Node replacement tool set to:@n@1.=Ferramenta de substituição de nó definida como:@n@1.
+alloying=liga
+CNC machining=Usinagem CNC
+compressing=comprimir
+extracting=extrair
+freezing=congelando
+grinding=esmerilhamento
+separating=separando
+painting=pintar
+fermenting=fermentando
+Ferment in barrel.=Fermentar em barril.
diff --git a/locale/replacer.ru.tr b/locale/replacer.ru.tr
new file mode 100644
index 0000000..ca897b7
--- /dev/null
+++ b/locale/replacer.ru.tr
@@ -0,0 +1,127 @@
+# textdomain: replacer
+History=История
+Choose mode=Выберите режим
+Both=Оба
+Node=Узел
+Rotation=Вращение
+Replace node and apply orientation.=Замените узел и примените ориентацию.
+Replace node without changing orientation.=Заменить узел без изменения ориентации.
+Apply orientation without changing node type.=Применить ориентацию без изменения типа узла.
+Single=Один
+Field=Поле
+Crust=Корка
+Replace single node.=Заменить один узел.
+
+Left click: Replace field of nodes of a kind where a translucent node is in front of it.@@Right click: Replace field of air where no translucent node is behind the air.=Щелчок левой кнопкой мыши: заменить поле узлов такого типа, перед которым находится полупрозрачный узел. Щелчок правой кнопкой мыши: заменить поле воздуха там, где за воздухом не находится полупрозрачный узел.
+
+Left click: Replace nodes which touch another one of its kind and a translucent node, e.g. air.@@Right click: Replace air nodes which touch the crust.=Щелкните левой кнопкой мыши: замените узлы, которые соприкасаются с другим в своем роде и полупрозрачным узлом, например. воздух.@@Щелкните правой кнопкой мыши: замените воздушные узлы, которые касаются корки.
+
+Target node not yet loaded. Please wait a moment for the server to catch up.=Целевой узел еще не загружен. Пожалуйста, подождите, пока сервер наверстает упущенное.
+Nothing to replace.=Нечем заменить.
+Not enough charge to use this mode.=Недостаточно заряда для использования этого режима.
+Aborted, too many nodes detected.=Прервано, обнаружено слишком много узлов.
+Error: No node selected.=Ошибка: узел не выбран.
+Node replacement tool=Инструмент замены узлов
+Node replacement tool (technic)=Инструмент замены узла (техника)
+Time-limit reached.=Достигнут лимит времени.
+
+Toggles verbosity.@nchat: When on, messages are posted to chat.@naudio: When off, replacer is silent.=Включает многословие. @nchat: когда включено, сообщения отправляются в чат. @naudio: когда выключено, заменитель молчит.
+
+Minor modes are disabled on this server.=Второстепенные режимы отключены на этом сервере.
+=<нет информации о местоположении>
+days=дни
+
+Inspection Tool@nUse to inspect target node or entity.@nPlace to inspect the adjacent node.=Inspection Tool@nИспользуйте для проверки целевого узла или объекта. @nPlace для проверки соседнего узла.
+
+This is a broken object. We have no further information about it. It is located=Это сломанный объект. У нас нет никакой дополнительной информации об этом. Он расположен.
+owned, protected and locked=принадлежит, защищено и заблокировано
+owned and protected=в собственности и под защитой
+owned and locked=принадлежит и заблокирован
+This is an object=Это объект
+WARNING: You can't dig this node. It is protected.=ВНИМАНИЕ: Вы не можете копать этот узел. Он защищен.
+INFO: You can dig this node, others can't.=ИНФОРМАЦИЯ: Вы можете копать этот узел, а другие нет.
+~ no description provided ~=~ описание не предоставлено ~
+~ no node description provided ~=~ описание узла не предоставлено ~
+~ no item description provided ~=~ описание предмета не предоставлено ~
+Name:=Имя:
+Exit=Выход
+This is:=Это:
+previous recipe=предыдущий рецепт
+next recipe=следующий рецепт
+No recipes.=Нет рецептов.
+Drops on dig:=Выпадает при раскопках:
+nothing.=ничего.
+May drop on dig:=Может выпасть при раскопках:
+This can be used as a fuel.=Это можно использовать в качестве топлива.
+Error: Unknown recipe.=Ошибка: Неизвестный рецепт.
+scoop up=зачерпнуть
+pour out=вылить
+filling=заполнить
+(Functions may exist that change attributes and conditions of mobs)=(Могут существовать функции, изменяющие атрибуты и состояния мобов)
+Is of type=Типа
+Is loyal to owner.=Верен хозяину.
+Likes to attack:=Любит атаковать:
+Follows players holding:=Следует за игроками, держащими:
+May drop:=Может выпасть:
+Can shoot misiles.=Может стрелять ракетами.
+Can breed.=Может размножаться.
+Spawns on:=Появляется на:
+with neighours:=с соседями:
+Placed:=Размещено:
+Digs:=Раскопки:
+Inflicted:=Нанесено:
+Punched:=Перфорировано:
+XP:=Опыт:
+Deaths:=Летальные исходы:
+Played:=Играл:
+Is currently on a mission.=В настоящее время находится в командировке.
+You don't have any common channels.=У вас нет общих каналов.
+You are both on these channels:=Вы оба на этих каналах:
+Is wearing:=Носит:
+fermenting/pickling=ферментация/маринование
+Store near group:wood, light < 12.=Хранить рядом с группой дерево@n(group:wood), свет < 12.
+Replacing nodes of type "@1" is not allowed on this server. Replacement failed.=Замена узлов типа "@1" на этом сервере запрещена. Замена не удалась.
+Protected at @1=Защищено в @1
+printing=печать
+Sorry, this is an unknown something of type "@1". No information available.=Извините, это неизвестное что-то типа "@1". Информация отсутствует.
+Can deal @1 damage.=Может нанести @1 урона.
+Has @1 armour.=Имеет @1 броню.
+This is your fellow player "@1"=Это ваш товарищ по игре "@1"
+at @1=в @1
+This is an object "@1"=Это объект "@1"
+This is an entity "@1"=Это сущность "@1"
+, dropped @1 minutes ago=, выпало @1 минут назад
+by "@1"=по "@1"
+with order to @1=с заказом на "@1"
+Has @1 different types of items,=Имеет @1 различных типов предметов,
+total of @1 items in inventory.=всего @1 предметов в инвентаре.
+Located at @1=Находится по адресу @1
+with param2 of @1=с параметром2 из @1
+and receiving @1 light=и получаю @1 свет
+Alternate @1/@2=Альтернатива @1/@2
+holding=держит
+cutting=резка
+shearing=стрижка
+Cut with shears.=Вырезать ножницами.
+sawing=пиление
+You have no further "@1". Replacement failed.=У вас больше нет "@1". Замена не удалась.
+Unknown node: "@1"=Неизвестный узел: "@1"
+Unknown node to place: "@1"=Неизвестный узел для размещения: "@1"
+Could not dig "@1" properly.=Не удалось правильно раскопать "@1".
+Could not place "@1".=Не удалось разместить "@1".
+Error: "@1" is not a node.=Ошибка: "@1" не является узлом.
+@1 nodes replaced.=Заменено узлов: @1.
+Mode changed to @1: @2=Режим изменен на @1: @2
+Placing nodes of type "@1" is not allowed on this server.=Размещение узлов типа "@1" на этом сервере запрещено.
+Failed to set replacer to "@1". If there was one in your inventory, then maybe.=Не удалось установить заменитель на "@1". Если бы он был в вашем инвентаре, то, возможно.
+Node replacement tool set to:@n@1.=Инструмент замены узлов установлен на:@n@1.
+alloying=легирование
+CNC machining=ЧПУ обработка
+compressing=сжатие
+extracting=извлечение
+freezing=замораживание
+grinding=шлифовка
+separating=разделяющий
+painting=рисования
+fermenting=брожение
+Ferment in barrel.=Ферментация в бочке.
diff --git a/locale/template.txt b/locale/template.txt
new file mode 100644
index 0000000..3f48f4d
--- /dev/null
+++ b/locale/template.txt
@@ -0,0 +1,127 @@
+# textdomain: replacer
+History=
+Choose mode=
+Both=
+Node=
+Rotation=
+Replace node and apply orientation.=
+Replace node without changing orientation.=
+Apply orientation without changing node type.=
+Single=
+Field=
+Crust=
+Replace single node.=
+
+Left click: Replace field of nodes of a kind where a translucent node is in front of it.@@Right click: Replace field of air where no translucent node is behind the air.=
+
+Left click: Replace nodes which touch another one of its kind and a translucent node, e.g. air.@@Right click: Replace air nodes which touch the crust.=
+
+Target node not yet loaded. Please wait a moment for the server to catch up.=
+Nothing to replace.=
+Not enough charge to use this mode.=
+Aborted, too many nodes detected.=
+Error: No node selected.=
+Node replacement tool=
+Node replacement tool (technic)=
+Time-limit reached.=
+
+Toggles verbosity.@nchat: When on, messages are posted to chat.@naudio: When off, replacer is silent.=
+
+Minor modes are disabled on this server.=
+=
+days=
+
+Inspection Tool@nUse to inspect target node or entity.@nPlace to inspect the adjacent node.=
+
+This is a broken object. We have no further information about it. It is located=
+owned, protected and locked=
+owned and protected=
+owned and locked=
+This is an object=
+WARNING: You can't dig this node. It is protected.=
+INFO: You can dig this node, others can't.=
+~ no description provided ~=
+~ no node description provided ~=
+~ no item description provided ~=
+Name:=
+Exit=
+This is:=
+previous recipe=
+next recipe=
+No recipes.=
+Drops on dig:=
+nothing.=
+May drop on dig:=
+This can be used as a fuel.=
+Error: Unknown recipe.=
+scoop up=
+pour out=
+filling=
+(Functions may exist that change attributes and conditions of mobs)=
+Is of type=
+Is loyal to owner.=
+Likes to attack:=
+Follows players holding:=
+May drop:=
+Can shoot misiles.=
+Can breed.=
+Spawns on:=
+with neighours:=
+Placed:=
+Digs:=
+Inflicted:=
+Punched:=
+XP:=
+Deaths:=
+Played:=
+Is currently on a mission.=
+You don't have any common channels.=
+You are both on these channels:=
+Is wearing:=
+fermenting/pickling=
+Store near group:wood, light < 12.=
+Replacing nodes of type "@1" is not allowed on this server. Replacement failed.=
+Protected at @1=
+printing=
+Sorry, this is an unknown something of type "@1". No information available.=
+Can deal @1 damage.=
+Has @1 armour.=
+This is your fellow player "@1"=
+at @1=
+This is an object "@1"=
+This is an entity "@1"=
+, dropped @1 minutes ago=
+by "@1"=
+with order to @1=
+Has @1 different types of items,=
+total of @1 items in inventory.=
+Located at @1=
+with param2 of @1=
+and receiving @1 light=
+Alternate @1/@2=
+holding=
+cutting=
+shearing=
+Cut with shears.=
+sawing=
+You have no further "@1". Replacement failed.=
+Unknown node: "@1"=
+Unknown node to place: "@1"=
+Could not dig "@1" properly.=
+Could not place "@1".=
+Error: "@1" is not a node.=
+@1 nodes replaced.=
+Mode changed to @1: @2=
+Placing nodes of type "@1" is not allowed on this server.=
+Failed to set replacer to "@1". If there was one in your inventory, then maybe.=
+Node replacement tool set to:@n@1.=
+alloying=
+CNC machining=
+compressing=
+extracting=
+freezing=
+grinding=
+separating=
+painting=
+fermenting=
+Ferment in barrel.=
diff --git a/mod.conf b/mod.conf
new file mode 100644
index 0000000..353583d
--- /dev/null
+++ b/mod.conf
@@ -0,0 +1,5 @@
+name = replacer
+description = Replacement tool for creative building and tool to inspect nodes.
+depends = default
+optional_depends = colormachine, dye, moreblocks, technic, unifieddyes
+
diff --git a/replacer/constrain.lua b/replacer/constrain.lua
new file mode 100644
index 0000000..fc35d2b
--- /dev/null
+++ b/replacer/constrain.lua
@@ -0,0 +1,105 @@
+local r = replacer
+local rb = replacer.blabla
+local S = replacer.S
+local is_protected = minetest.is_protected
+local pos_to_string = replacer.nice_pos_string
+
+-- limit by node, use replacer.register_limit(sName, iMax)
+replacer.limit_list = {}
+
+-- don't allow items of these groups for setting replacer to
+replacer.deny_groups = {}
+replacer.deny_groups['seed'] = true
+
+-- don't allow these at all, neither for placing nor replacing
+-- example: r.deny_list['tnt:boom'] = true
+replacer.deny_list = {}
+
+-- charge limits
+replacer.max_charge = 30000
+replacer.charge_per_node = 15
+-- node count limit
+replacer.max_nodes = tonumber(minetest.settings:get('replacer.max_nodes') or 3168)
+-- Time limit when placing the nodes, in seconds (not including search time)
+replacer.max_time = tonumber(minetest.settings:get('replacer.max_time') or 1.0)
+-- Radius limit factor when more possible positions are found than either max_nodes or charge
+-- Set to 0 or less for behaviour of before version 3.3
+-- [see replacer_patterns.lua>replacer.patterns.search_positions()]
+replacer.radius_factor = tonumber(minetest.settings:get('replacer.radius_factor') or 0.4)
+
+-- disable minor modes on server
+replacer.disable_minor_modes =
+ minetest.settings:get_bool('replacer.disable_minor_modes') or false
+
+-- priv to allow using history
+replacer.history_priv = minetest.settings:get('replacer.history_priv') or 'creative'
+-- disable saving history over sessions/reboots. IOW: don't use player meta e.g. if using old MT
+replacer.history_disable_persistancy =
+ minetest.settings:get_bool('replacer.history_disable_persistancy') or false
+-- ignored when persistancy is disabled. Interval in minutes to
+replacer.history_save_interval =
+ tonumber(minetest.settings:get('replacer.history_save_interval') or 7)
+-- include mode when changing from history
+replacer.history_include_mode =
+ minetest.settings:get_bool('replacer.history_include_mode') or false
+-- amount of items in history
+replacer.history_max = tonumber(minetest.settings:get('replacer.history_max') or 7)
+
+-- select which recipes to hide (not all combinations make sense)
+replacer.hide_recipe_basic =
+ minetest.settings:get_bool('replacer.hide_recipe_basic') or false
+replacer.hide_recipe_technic_upgrade =
+ minetest.settings:get_bool('replacer.hide_recipe_technic_upgrade') or false
+replacer.hide_recipe_technic_direct =
+ minetest.settings:get_bool('replacer.hide_recipe_technic_direct')
+if nil == replacer.hide_recipe_technic_direct then
+ replacer.hide_recipe_technic_direct = true
+end
+
+-- function that other mods, especially custom server mods,
+-- can override. e.g. restrict usage of replacer in certain
+-- areas, privs, throttling etc.
+-- This is called before replacing the node/air and expects
+-- a boolean return and in the case of fail, an optional message
+-- that will be sent to player
+--luacheck: no unused args
+function replacer.permit_replace(pos, old_node_def, new_node_def,
+ player_ref, player_name, player_inv, creative_or_give)
+
+ if r.deny_list[old_node_def.name] then
+ return false, S('Replacing nodes of type "@1" is not allowed '
+ .. 'on this server. Replacement failed.', old_node_def.name)
+ end
+
+ if is_protected(pos, player_name) then
+ return false, S('Protected at @1', pos_to_string(pos))
+ end
+
+ return true
+end -- permit_replace
+
+
+local function is_positive_int(value)
+ return (type(value) == 'number') and (math.floor(value) == value) and (0 <= value)
+end
+function replacer.register_limit(node_name, node_max)
+ -- ignore nil, negative numbers and non-integers
+ if not is_positive_int(node_max) then
+ return
+ end
+
+ -- add to deny_list if limit is zero
+ if 0 == node_max then
+ r.deny_list[node_name] = true
+ minetest.log('info', rb.log_deny_list_insert:format(node_name))
+ return
+ end
+
+ -- log info if already limited
+ if nil ~= r.limit_list[node_name] then
+ minetest.log('info', rb.log_limit_override:format(node_name, r.limit_list[node_name]))
+ end
+ r.limit_list[node_name] = node_max
+ minetest.log('info', rb.log_limit_insert:format(node_name, node_max))
+end -- register_limit
+
diff --git a/replacer/datastructures.lua b/replacer/datastructures.lua
new file mode 100644
index 0000000..2d2d69c
--- /dev/null
+++ b/replacer/datastructures.lua
@@ -0,0 +1,344 @@
+local funcs = {}
+local floor = math.floor
+local log = math.log
+local table_concat = table.concat
+
+local stack_mt
+stack_mt = {
+ __index = {
+ push = function(self, v)
+ self.n = self.n + 1
+ self[self.n] = v
+ end,
+ pop = function(self)
+ local v = self[self.n]
+ self[self.n] = nil
+ self.n = self.n - 1
+ return v
+ end,
+ top = function(self)
+ return self[self.n]
+ end,
+ get = function(self, i)
+ if 0 >= i then
+ return self[self.n + i]
+ end
+ return self[i]
+ end,
+ is_empty = function(self)
+ return 0 == self.n
+ end,
+ size = function(self)
+ return self.n
+ end,
+ clone = function(self, copy_element)
+ local stack, n
+ if copy_element then
+ stack = { n = self.n, true }
+ for i = 1, self.n do
+ stack[i] = copy_element(self[i])
+ end
+ else
+ stack, n = self:to_table()
+ stack.n = n
+ end
+ setmetatable(stack, stack_mt)
+ return stack
+ end,
+ to_table = function(self)
+ local t = {}
+ for i = 1, self.n do
+ t[i] = self[i]
+ end
+ return t, self.n
+ end,
+ to_string = function(self, value_tostring)
+ if 0 == self.n then
+ return 'empty stack'
+ end
+ value_tostring = value_tostring or tostring
+ local t = {}
+ for i = 1, self.n do
+ t[i] = value_tostring(self[i])
+ end
+ return self.n .. ' elements; bottom to top: '
+ .. table_concat(t, ', ')
+ end,
+ }
+}
+
+function funcs.create_stack(data)
+ local stack
+ if 'table' == type(data)
+ and data.input then
+ stack = data.input
+ stack.n = data.n or #data.input
+ else
+ -- setting the first element to true makes it ~10 times faster with
+ -- luajit when the stack always contains less or equal to one element
+ stack = { n = 0, true }
+ end
+ setmetatable(stack, stack_mt)
+ return stack
+end
+
+
+local fifo_mt
+fifo_mt = {
+ __index = {
+ add = function(self, v)
+ local n = self.n_in + 1
+ self.n_in = n
+ self.sink[n] = v
+ end,
+ take = function(self)
+ local p = self.p_out
+ if p <= self.n_out then
+ local v = self.source[p]
+ self.source[p] = nil
+ self.p_out = p + 1
+ return v
+ end
+ -- source is empty, swap it with sink
+ self.source, self.sink = self.sink, self.source
+ self.n_out = self.n_in
+ self.n_in = 0
+ local v = self.source[1]
+ self.source[1] = nil
+ self.p_out = 2
+ return v
+ end,
+ peek = function(self)
+ local p = self.p_out
+ if p <= self.n_out then
+ return self.source[p]
+ end
+ -- source is empty
+ return self.sink[1]
+ end,
+ is_empty = function(self)
+ return 0 == self.n_in and self.p_out == self.n_out + 1
+ end,
+ size = function(self)
+ return self.n_in + self.n_out - self.p_out + 1
+ end,
+ clone = function(self, copy_element)
+ local source, n = self:to_table()
+ if copy_element then
+ for i = 1, n do
+ source[i] = copy_element(source[i])
+ end
+ end
+ local fifo = { n_in = 0, n_out = n, p_out = 1,
+ sink = { true }, source = source }
+ setmetatable(fifo, fifo_mt)
+ return fifo
+ end,
+ to_table = function(self)
+ local t = {}
+ local k = 1
+ for i = self.p_out, self.n_out do
+ t[k] = self.source[i]
+ k = k + 1
+ end
+ for i = 1, self.n_in do
+ t[k] = self.sink[i]
+ k = k + 1
+ end
+ return t, k - 1
+ end,
+ to_string = function(self, value_tostring)
+ local size = self:size()
+ if 0 == size then
+ return 'empty fifo'
+ end
+ value_tostring = value_tostring or tostring
+ local t = self:to_table()
+ for i = 1, #t do
+ t[i] = value_tostring(t[i])
+ end
+ return size .. ' elements; oldest to newest: '
+ .. table_concat(t, ', ')
+ end,
+ }
+}
+
+function funcs.create_queue(data)
+ local fifo
+ if 'table' == type(data)
+ and data.input then
+ fifo = { n_in = 0, n_out = data.n or #data.input, p_out = 1,
+ sink = { true }, source = data.input }
+ else
+ fifo = { n_in = 0, n_out = 0, p_out = 1, sink = { true }, source = { true } }
+ end
+ setmetatable(fifo, fifo_mt)
+ return fifo
+end
+
+
+local function sift_up(binary_heap, i)
+ local p = floor(i * .5)
+ while p > 0
+ and binary_heap.compare(binary_heap[i], binary_heap[p])
+ do
+ -- new data has higher priority than its parent
+ binary_heap[i], binary_heap[p] = binary_heap[p], binary_heap[i]
+ i = p
+ p = floor(p * .5)
+ end
+end
+
+local function sift_down(binary_heap, i)
+ local n = binary_heap.n
+ while true do
+ local l = i + i
+ local r = l + 1
+ if l > n then
+ break
+ end
+ if r > n then
+ if binary_heap.compare(binary_heap[l], binary_heap[i]) then
+ binary_heap[i], binary_heap[l] = binary_heap[l], binary_heap[i]
+ end
+ break
+ end
+ local preferred_child =
+ binary_heap.compare(binary_heap[l], binary_heap[r]) and l or r
+ if not binary_heap.compare(
+ binary_heap[preferred_child], binary_heap[i])
+ then
+ break
+ end
+ binary_heap[i], binary_heap[preferred_child] =
+ binary_heap[preferred_child], binary_heap[i]
+ i = preferred_child
+ end
+end
+
+local function build(binary_heap)
+ for i = floor(binary_heap.n * .5), 1, -1 do
+ sift_down(binary_heap, i)
+ end
+end
+
+local binary_heap_mt
+binary_heap_mt = {
+ __index = {
+ peek = function(self)
+ return self[1]
+ end,
+ add = function(self, v)
+ local i = self.n + 1
+ self.n = i
+ self[i] = v
+ sift_up(self, i)
+ end,
+ take = function(self)
+ local v = self[1]
+ self[1] = self[self.n]
+ self[self.n] = nil
+ self.n = self.n - 1
+ sift_down(self, 1)
+ return v
+ end,
+ find = function(self, cond)
+ for i = 1, self.n do
+ if cond(self[i]) then
+ return i
+ end
+ end
+ end,
+ change_element = function(self, v, i)
+ i = i or 1
+ local priority_lower = self.compare(self[i], v)
+ self[i] = v
+ if priority_lower then
+ sift_down(self, i)
+ elseif 1 < i then
+ sift_up(self, i)
+ end
+ end,
+ merge = function(self, other)
+ local n = self.n
+ for i = 1, other.n do
+ self[n + i] = other[i]
+ end
+ self.n = n + other.n
+ build(self)
+ end,
+ is_empty = function(self)
+ return 0 == self.n
+ end,
+ size = function(self)
+ return self.n
+ end,
+ clone = function(self, copy_element)
+ local binary_heap, n = self:to_table()
+ if copy_element then
+ for i = 1, n do
+ binary_heap[i] = copy_element(binary_heap[i])
+ end
+ end
+ binary_heap.n = n
+ binary_heap.compare = self.compare
+ setmetatable(binary_heap, binary_heap_mt)
+ return binary_heap
+ end,
+ to_table = function(self)
+ local t = {}
+ for i = 1, self.n do
+ t[i] = self[i]
+ end
+ return t, self.n
+ end,
+ sort = function(self)
+ for i = self.n, 1, -1 do
+ self[i], self[1] = self[1], self[i]
+ self.n = self.n - 1
+ sift_down(self, 1)
+ end
+ setmetatable(self, nil)
+ self.compare = nil
+ end,
+ to_string = function(self, value_tostring)
+ if 0 == self.n then
+ return 'empty binary heap'
+ end
+ value_tostring = value_tostring or tostring
+ local t = {}
+ for i = 1, self.n do
+ local sep = ''
+ if 1 < i then
+ sep = (0 == (log(i) / log(2)) % 1) and '; ' or ', '
+ end
+ t[i] = sep .. value_tostring(self[i])
+ end
+ return self.n .. ' elements: ' .. table_concat(t, '0')
+ end,
+ }
+}
+
+function funcs.create_binary_heap(data)
+ local compare = data
+ if 'table' == type(data) then
+ if data.input then
+ -- make data.elements a binary heap
+ local binary_heap = data.input
+ binary_heap.n = data.n or #binary_heap
+ binary_heap.compare = data.compare
+ setmetatable(binary_heap, binary_heap_mt)
+ if not data.input_sorted then
+ build(binary_heap)
+ end
+ return
+ end
+ compare = data.compare
+ end
+ local binary_heap = { compare = compare, n = 0, true }
+ setmetatable(binary_heap, binary_heap_mt)
+ return binary_heap
+end
+
+return funcs
+
diff --git a/replacer/enable.lua b/replacer/enable.lua
new file mode 100644
index 0000000..ef69bcd
--- /dev/null
+++ b/replacer/enable.lua
@@ -0,0 +1,84 @@
+local r = replacer
+local rb = replacer.blabla
+
+-- see replacer.register_exception()
+replacer.exception_map = {}
+replacer.exception_callbacks = {}
+
+-- see replacer.register_set_enabler()
+replacer.enable_set_callbacks = {}
+
+-- see replacer.register_non_creative_alias()
+replacer.alias_map = {}
+
+-- some nodes don't rotate using param2. They can be added
+-- using replacer.register_exception(node_name, inv_node_name[, callback_function])
+-- where: node_name is the itemstring of node when placed in world
+-- inv_node_name the itemstring of item in inventory to consume
+-- callback_function is optional and will be called after node is placed.
+-- It must return true on success and false, error_message on fail.
+-- In order to register only a callback, pass two identical itemstrings.
+-- Generally the callback is not needed as on_place() is called on the placed node
+-- callback signature is: (pos, old_node_def, new_node_def, player_ref)
+-- Examples:
+-- 1) Technic cable plate 'technic:lv_cable_plate_4' needs to consume 'technic:lv_cable_plate_1'
+-- r.register_exception('technic:lv_cable_plate_4', 'technic:lv_cable_plate_1')
+-- 2) Cobwebs don't drop cobwebs, to enable setting replacer to them without having any in
+-- user's inventory, register like so:
+-- r.register_exception('mobs:cobweb', 'mobs:cobweb')
+function replacer.register_exception(node_name, drop_name, callback)
+ if r.exception_map[node_name] then
+ minetest.log('warning', rb.log_reg_exception_override:format(node_name))
+ end
+ r.exception_map[node_name] = drop_name
+ minetest.log('info', rb.log_reg_exception:format(node_name, drop_name))
+
+ if 'function' ~= type(callback) then return end
+
+ r.exception_callbacks[node_name] = callback
+ minetest.log('info', rb.log_reg_exception_callback:format(node_name))
+end -- register_exception
+
+
+-- sometimes you want a reverse exception, for that you use:
+-- replacer.register_non_creative_alias(name_sibling, name_placed)
+-- Example vines have middle and end parts. To enable setting replacer on middle part
+-- to then place an end part in world (only when player does not have creative priv)
+-- register like so:
+-- replacer.register_non_creative_alias('vines:jungle_middle', 'vines:jungle_end')
+function replacer.register_non_creative_alias(name_sibling, name_placed)
+ if r.alias_map[name_sibling] then
+ minetest.log('warning', rb.log_reg_alias_override:format(name_sibling))
+ end
+ r.alias_map[name_sibling] = name_placed
+ minetest.log('info', rb.log_reg_alias:format(name_sibling, name_placed))
+end -- register_non_creative_alias
+
+
+-- The callback will be called when setting replacer and *none* of these
+-- criteria are matched:
+-- - user has give priv
+-- - user has the item in inventory
+-- - user does not have creative priv and item is in alias map
+-- see register_non_creative_alias()
+-- - item is in exceptions, see register_exception()
+-- - the item can be obtained by crafting, cooking etc.
+-- The first callback to respond with something other than false or nil
+-- allows setting the replacer to node. Such a callback may also
+-- modify node-table's name, param1 and param2 fields.
+-- If you want all your users to be able to set replacer to any node,
+-- looking @Sokomine ;), then in your custom server mod register:
+-- replacer.register_set_enabler(replacer.yes)
+-- although, at that point you might as well just give users creative.
+-- the callback signature is f(node, player, pointed_thing)
+function replacer.register_set_enabler(callback)
+ if 'function' ~= type(callback) then
+ minetest.log('error', rb.log_reg_set_callback_fail)
+ return
+ end
+
+ table.insert(r.enable_set_callbacks, callback)
+end -- register_set_enabler
+
+function replacer.yes() return true end
+
diff --git a/replacer/formspecs.lua b/replacer/formspecs.lua
new file mode 100644
index 0000000..9a83b09
--- /dev/null
+++ b/replacer/formspecs.lua
@@ -0,0 +1,193 @@
+-- https://github.com/minetest/minetest_docs/blob/413b559f1a49e59c2649eea2835fc7620d407dca/doc/formspecs.adoc#versions
+-- https://rubenwardy.com/minetest_modding_book/en/players/formspecs.html
+
+local r = replacer
+local rb = replacer.blabla
+local log = minetest.log
+local get_player_information = minetest.get_player_information
+local mfe = minetest.formspec_escape
+local check_player_privs = minetest.check_player_privs
+local show_formspec = minetest.show_formspec
+
+replacer.form_name_modes = 'replacer_replacer_mode_change'
+
+function replacer.get_form_modes_4(player, mode)
+ local major, minor = mode.major, mode.minor
+ local name = player:get_player_name()
+ local has_history_priv = check_player_privs(name, r.history_priv)
+ local form_dimensions = r.disable_minor_modes and '5.375,3.5' or '5.375,4.25'
+ local button_height = '1'
+ local button_single_dimensions = '.33,.77;2,'
+ local button_field_dimensions = '2.9,.77;2,'
+ local button_crust_dimensions = '1.44,2.1;2,'
+ local minor_dimensions = '.38,3.42;4.5,.55'
+ local history_label_position, history_dropdown_position
+ if has_history_priv then
+ button_single_dimensions = '0.33,0.77;2.3,'
+ button_field_dimensions = '3.04,0.77;2.3,'
+ button_crust_dimensions = '5.75,0.77;2.3,'
+ if r.disable_minor_modes then
+ form_dimensions = '8.375,3.39'
+ history_label_position = '0.33,2.22;'
+ history_dropdown_position = '0.38,2.55'
+ else
+ form_dimensions = '8.375,4.39'
+ button_single_dimensions = '0.33,0.77;2.3,'
+ button_field_dimensions = '3.04,0.77;2.3,'
+ button_crust_dimensions = '5.75,0.77;2.3,'
+ minor_dimensions = '1.88,2.12;4.5,0.60'
+ history_label_position = '0.33,3.22;'
+ history_dropdown_position = '0.38,3.55'
+ end
+ end
+ local tmp_name = '_'
+ local formspec = 'formspec_version[4]'
+ .. 'size[' .. form_dimensions .. ']'
+ .. 'label[0.33,0.44;' .. mfe(rb.choose_mode)
+ .. ']button_exit[' .. button_single_dimensions .. button_height .. ';'
+ if 1 == major then
+ formspec = formspec .. '_;< ' .. mfe(rb.mode_single) .. ' >'
+ else
+ tmp_name = 'mode1'
+ formspec = formspec .. 'mode1;' .. mfe(rb.mode_single)
+ end
+ formspec = formspec .. ']tooltip[' .. tmp_name .. ';'
+ .. mfe(rb.mode_single_tooltip) .. ']button_exit['
+ .. button_field_dimensions .. button_height .. ';'
+ if 2 == major then
+ tmp_name = '_'
+ formspec = formspec .. '_;< ' .. mfe(rb.mode_field) .. ' >'
+ else
+ tmp_name = 'mode2'
+ formspec = formspec .. 'mode2;' .. mfe(rb.mode_field)
+ end
+ formspec = formspec .. ']tooltip[' .. tmp_name .. ';'
+ .. mfe(rb.mode_field_tooltip) .. ']button_exit['
+ .. button_crust_dimensions .. button_height .. ';'
+ if 3 == major then
+ tmp_name = '_'
+ formspec = formspec .. '_;< ' .. mfe(rb.mode_crust) .. ' >'
+ else
+ tmp_name = 'mode3'
+ formspec = formspec .. 'mode3;' .. mfe(rb.mode_crust)
+ end
+ formspec = formspec .. ']tooltip[' .. tmp_name .. ';'
+ .. mfe(rb.mode_crust_tooltip) .. ']'
+ if not r.disable_minor_modes then
+ formspec = formspec .. 'dropdown[' .. minor_dimensions .. ';minor;'
+ .. mfe(rb.mode_minor1) .. ',' .. mfe(rb.mode_minor2) .. ',' .. mfe(rb.mode_minor3)
+ .. ';' .. tostring(minor) .. ';true]tooltip[' .. minor_dimensions .. ';'
+ .. mfe(rb.mode_minor1 .. ': ' .. rb.mode_minor1_info .. '\n'
+ .. rb.mode_minor2 .. ': ' .. rb.mode_minor2_info .. '\n'
+ .. rb.mode_minor3 .. ': ' .. rb.mode_minor3_info) .. ']'
+ end
+
+ if not has_history_priv then return formspec end
+
+ formspec = formspec .. 'label[' .. history_label_position .. mfe(rb.choose_history)
+ .. ']dropdown[' .. history_dropdown_position .. ';7.5,0.6;history;'
+ local db = r.history.get_player_table(player)
+ for _, data in ipairs(db) do
+ if r.history_include_mode then
+ formspec = formspec .. data.mode.major .. '.' .. data.mode.minor .. ' '
+ end
+ formspec = formspec .. mfe(data.human_string) .. ','
+ end
+ formspec = formspec .. '~~~~~~~~~~~~~~;1;true]'
+ return formspec
+end -- get_form_modes_4
+
+
+function replacer.get_form_modes_default(mode)
+ local major = mode.major
+ local tmp_name = '_'
+ local formspec = 'size[3.9,2]'
+ .. 'label[0,0;' .. mfe(rb.choose_mode)
+ .. ']button_exit[0.0,0.6;2,0.5;'
+ if 1 == major then
+ formspec = formspec .. '_;< ' .. mfe(rb.mode_single) .. ' >'
+ else
+ tmp_name = 'mode1'
+ formspec = formspec .. 'mode1;' .. mfe(rb.mode_single)
+ end
+ formspec = formspec .. ']tooltip[' .. tmp_name .. ';'
+ .. mfe(rb.mode_single_tooltip) .. ']button_exit[1.9,0.6;2,0.5;'
+ if 2 == major then
+ formspec = formspec .. '_;< ' .. mfe(rb.mode_field) .. ' >'
+ else
+ tmp_name = 'mode2'
+ formspec = formspec .. 'mode2;' .. mfe(rb.mode_field)
+ end
+ formspec = formspec .. ']tooltip[' .. tmp_name .. ';'
+ .. mfe(rb.mode_field_tooltip) .. ']button_exit[0.0,1.4;2,0.5;'
+ if 3 == major then
+ formspec = formspec .. '_;< ' .. mfe(rb.mode_crust) .. ' >'
+ else
+ tmp_name = 'mode3'
+ formspec = formspec .. 'mode3;' .. mfe(rb.mode_crust)
+ end
+ formspec = formspec .. ']tooltip[' .. tmp_name .. ';'
+ .. mfe(rb.mode_crust_tooltip) .. ']'
+ return formspec
+end -- get_form_modes_default
+
+function replacer.on_player_receive_fields(player, form_name, fields)
+ -- no need to process if it's not expected formspec that triggered call
+ if form_name ~= r.form_name_modes then return end
+ -- collect some information
+ local name = player:get_player_name()
+ local wielded = player:get_wielded_item()
+ local node, mode = r.get_data(wielded)
+ -- user clicked on currently active mode
+ if fields._ then return end
+ if fields.mode1 or fields.mode2 or fields.mode3 then
+ -- user clicked on one of the major modes
+ mode.major = (fields.mode1 and 1) or (fields.mode2 and 2) or (fields.mode3 and 3)
+ elseif fields.minor then
+ -- clamp to { 1, 2, 3 }
+ mode.minor = math.min(3, math.max(1, tonumber(fields.minor) or 1))
+ if r.disable_minor_modes then mode.minor = 1 end
+ elseif fields.history then
+ -- ignore if user doesn't have privs
+ if not check_player_privs(name, r.history_priv) then
+ log('info', rb.formspec_error:format(name, dump(fields)))
+ return
+ end
+ local entry = r.history.get_by_index(player, tonumber(fields.history) or 1)
+ if not entry then return end
+
+ node = entry.node
+ mode = r.history_include_mode and entry.mode or nil
+ elseif fields.quit then
+ -- user closed formspec with escape or other clicking manouver
+ return
+ else
+ -- some hacked client forging formspec?
+ log('info', rb.formspec_hacker:format(name, dump(fields)))
+ return
+ end
+
+ -- set metadata and itemstring
+ r.set_data(wielded, node, mode)
+ -- update wielded item
+ player:set_wielded_item(wielded)
+end -- on_player_receive_fields
+-- listen to submitted fields
+minetest.register_on_player_receive_fields(r.on_player_receive_fields)
+
+function replacer.show_mode_formspec(player, mode)
+ if not player then return end
+
+ local name = player:get_player_name()
+ local version = get_player_information(name).formspec_version
+ local formspec
+ if 4 > version then
+ formspec = r.get_form_modes_default(mode)
+ else
+ -- version 4 allows us to use proper dropdowns and other gimmics
+ formspec = r.get_form_modes_4(player, mode)
+ end
+ -- show the formspec to player
+ show_formspec(name, r.form_name_modes, formspec)
+end -- show_mode_formspec
+
diff --git a/replacer/history.lua b/replacer/history.lua
new file mode 100644
index 0000000..bd9320b
--- /dev/null
+++ b/replacer/history.lua
@@ -0,0 +1,128 @@
+replacer.history = {}
+replacer.history.db = {}
+replacer.history.dirty = {}
+local r = replacer
+local rb = replacer.blabla
+local rud_colour_name = replacer.unifieddyes.colour_name
+
+function replacer.history.add_item(player, mode, node, short_description)
+ local name = player:get_player_name()
+ local db_old = r.history.db[name]
+ if not db_old then return end
+
+ local i = 1
+ local db = { { node = node, mode = mode, human_string = short_description } }
+ for _, entry in ipairs(db_old) do
+ if entry.human_string ~= short_description then
+ i = i + 1
+ db[i] = entry
+ if i == r.history_max then break end
+ end
+ end
+ r.history.db[name] = db
+ r.history.dirty[name] = true
+end -- add_item
+
+function r.history.auto_save()
+ minetest.after(r.history_save_interval, r.history.auto_save)
+ for _, player in ipairs(minetest.get_connected_players()) do
+ if r.history.dirty[player:get_player_name()] then
+ r.history.save(player)
+ end
+ end
+end -- auto_save
+
+function replacer.history.dealloc_player(player)
+ r.history.save(player)
+ r.history.db[player:get_player_name()] = nil
+end -- dealloc_player
+
+function replacer.history.get_by_index(player, index)
+ return r.history.get_player_table(player)[index]
+end -- get_by_index
+
+function replacer.history.get_player_table(player)
+ return r.history.db[player:get_player_name()] or {}
+end -- get_player_table
+
+function replacer.history.init_player(player)
+ local name = player:get_player_name()
+ if not minetest.check_player_privs(name, r.history_priv) then return end
+
+ local db_strings =
+ player:get_meta():get_string('replacer_his'):split('||', false, r.history_max) or {}
+ local db = {}
+ local data, entry, mode, mode_raw, colour_name, node_def
+ for i, entry_raw in ipairs(db_strings) do
+ data = entry_raw:split(' ', false, 4)
+ mode_raw = data[4] or '1.1'
+ mode = mode_raw:split('.', false, 2)
+ entry = {
+ node = {
+ name = data[1] or r.tool_default_node,
+ param1 = tonumber(data[2]) or 0,
+ param2 = tonumber(data[3]) or 0,
+ },
+ mode = {
+ major = tonumber(mode[1]) or 1,
+ minor = tonumber(mode[2]) or 1,
+ },
+ }
+ if r.disable_minor_modes then entry.mode.minor = 1 end
+ node_def = minetest.registered_items[entry.node.name]
+ colour_name = rud_colour_name(entry.node.param2, node_def)
+ if 0 < #colour_name then
+ colour_name = ' ' .. colour_name
+ end
+ entry.human_string = tostring(entry.mode.major) .. '.' .. tostring(entry.mode.minor)
+ .. ' ' .. rb.tool_short_description:format(entry.node.param1, entry.node.param2,
+ colour_name, entry.node.name)
+ db[i] = entry
+ end
+ r.history.db[name] = db
+end -- init_player
+
+function replacer.history.on_priv_grant(name, granter, priv)
+ -- skip duplicate calls
+ if granter then return end
+ if priv ~= r.history_priv then return end
+ r.history.init_player(minetest.get_player_by_name(name))
+end -- on_priv_grant
+
+function replacer.history.on_priv_revoke(name, revoker, priv)
+ -- skip duplicate calls
+ if revoker then return end
+ if priv ~= r.history_priv then return end
+ r.history.dealloc_player(minetest.get_player_by_name(name))
+end -- on_priv_revoke
+
+function replacer.history.save(player)
+ local name = player:get_player_name()
+ if r.history_disable_persistancy then
+ r.history.db[name] = nil
+ r.history.dirty[name] = nil
+ return
+ end
+
+ local db = r.history.db[name]
+ if not db then return end
+
+ local t = {}
+ for i, entry in ipairs(db) do
+ if i > r.history_max then break end
+ t[i] = table.concat({ entry.node.name, entry.node.param1, entry.node.param2,
+ table.concat({ entry.mode.major, entry.mode.minor }, '.') }, ' ')
+ end
+ player:get_meta():set_string('replacer_his', table.concat(t, '||'))
+ r.history.dirty[name] = nil
+end -- save
+
+minetest.register_on_joinplayer(r.history.init_player)
+minetest.register_on_leaveplayer(r.history.dealloc_player)
+minetest.register_on_priv_grant(r.history.on_priv_grant)
+minetest.register_on_priv_revoke(r.history.on_priv_revoke)
+if not r.history_disable_persistancy then
+ r.history_save_interval = 60 * r.history_save_interval
+ minetest.after(r.history_save_interval, r.history.auto_save)
+end -- if persistancy is enabled
+
diff --git a/replacer/patterns.lua b/replacer/patterns.lua
new file mode 100644
index 0000000..d60d82c
--- /dev/null
+++ b/replacer/patterns.lua
@@ -0,0 +1,282 @@
+replacer.patterns = {}
+local r = replacer
+local rp = replacer.patterns
+local floor = math.floor
+local is_protected = minetest.is_protected
+local poshash = minetest.hash_node_position
+local core_registered_nodes = minetest.registered_nodes
+local vector_add = vector.add
+local vector_distance = vector.distance
+
+-- cache results of minetest.get_node
+replacer.patterns.known_nodes = {}
+function replacer.patterns.get_node(pos)
+ local i = poshash(pos)
+ local node = rp.known_nodes[i]
+ if nil ~= node then
+ return node
+ end
+ node = minetest.get_node(pos)
+ rp.known_nodes[i] = node
+ return node
+end -- get_node
+
+-- The cache is only valid as long as no node is changed in the world.
+function replacer.patterns.reset_nodes_cache()
+ rp.known_nodes = {}
+end
+
+-- tests if there's a node at pos which should be replaced
+function replacer.patterns.replaceable(pos, node_name, player_name, param2)
+ local node = rp.get_node(pos)
+ if nil == param2 then
+ -- crust mode ignores param2
+ if 'air' ~= node_name then
+ return (node.name == node_name) and (not is_protected(pos, player_name))
+ end
+ -- right clicking in crust mode checks for air, but vacuum should work too
+ -- TODO: add mechanism to register allowed types
+ -- some servers might have other nodes like mars-lights that should be allowed too
+ -- maybe dummy lights, on the other hand, it might be useful to be able to stop replacer with dummy lights
+ return (('air' == node.name) or ('vacuum:vacuum' == node.name)
+ or 'planetoidgen:airlight' == node.name)
+ and (not is_protected(pos, player_name))
+ end
+ -- in field mode we also check that param2 is the same as the
+ -- initial node that was clicked on
+ return (node.name == node_name) and (node.param2 == param2)
+ and (not is_protected(pos, player_name))
+end -- replaceable
+
+replacer.patterns.translucent_nodes = {}
+function replacer.patterns.node_translucent(name)
+ local is_translucent = rp.translucent_nodes[name]
+ if nil ~= is_translucent then
+ return is_translucent
+ end
+ local def = core_registered_nodes[name]
+ if def and ((not def.drawtype) or ('normal' == def.drawtype)) then
+ rp.translucent_nodes[name] = false
+ return false
+ end
+ rp.translucent_nodes[name] = true
+ return true
+end -- node_translucent
+
+function replacer.patterns.field_position(pos, data)
+ return rp.replaceable(pos, data.name, data.pname, data.param2)
+ and rp.node_translucent(
+ rp.get_node(vector_add(data.above, pos)).name) ~= data.right_clicked
+end -- field_position
+
+replacer.patterns.offsets_touch = {
+ { x =-1, y = 0, z = 0 },
+ { x = 1, y = 0, z = 0 },
+ { x = 0, y =-1, z = 0 },
+ { x = 0, y = 1, z = 0 },
+ { x = 0, y = 0, z =-1 },
+ { x = 0, y = 0, z = 1 },
+}
+
+-- 3x3x3 hollow cube
+replacer.patterns.offsets_hollowcube = {}
+do
+local p
+for x = -1, 1 do
+ for y = -1, 1 do
+ for z = -1, 1 do
+ if (0 ~= x) or (0 ~= y) or (0 ~= z) then
+ p = { x = x, y = y, z = z }
+ rp.offsets_hollowcube[#rp.offsets_hollowcube + 1] = p
+ end
+ end
+ end
+end
+end
+
+-- To get the crust, first nodes near it need to be collected
+function replacer.patterns.crust_above_position(pos, data)
+ -- test if the node at pos is a translucent node and not part of the crust
+ local node_name = rp.get_node(pos).name
+ if (node_name == data.name) or (not rp.node_translucent(node_name)) then
+ return false
+ end
+ -- test if a node of the crust is near pos
+ local p2
+ for i = 1, 26 do
+ p2 = rp.offsets_hollowcube[i]
+ if rp.replaceable(vector_add(pos, p2), data.name, data.pname) then
+ return true
+ end
+ end
+ return false
+end -- crust_above_position
+
+-- used to get nodes the crust belongs to
+function replacer.patterns.crust_under_position(pos, data)
+ if not rp.replaceable(pos, data.name, data.pname) then
+ return false
+ end
+ local p2
+ for i = 1, 26 do
+ p2 = rp.offsets_hollowcube[i]
+ if data.aboves[poshash(vector_add(pos, p2))] then
+ return true
+ end
+ end
+ return false
+end -- crust_under_position
+
+-- extract the crust from the nodes the crust belongs to
+function replacer.patterns.reduce_crust_ps(data)
+ local newps = {}
+ local n = 0
+ local p, p2
+ for i = 1, data.num do
+ p = data.ps[i]
+ for i2 = 1, 6 do
+ p2 = rp.offsets_touch[i2]
+ if data.aboves[poshash(vector_add(p, p2))] then
+ n = n + 1
+ newps[n] = p
+ break
+ end
+ end
+ end
+ data.ps = newps
+ data.num = n
+end -- reduce_crust_ps
+
+-- gets the air nodes touching the crust
+function replacer.patterns.reduce_crust_above_ps(data)
+ local newps = {}
+ local n = 0
+ local p, p2
+ for i = 1, data.num do
+ p = data.ps[i]
+ if rp.replaceable(p, 'air', data.pname) then
+ for i2 = 1, 6 do
+ p2 = rp.offsets_touch[i2]
+ if rp.replaceable(vector_add(p, p2), data.name, data.pname) then
+ n = n + 1
+ newps[n] = p
+ break
+ end
+ end
+ end
+ end
+ data.ps = newps
+ data.num = n
+end -- reduce_crust_above_ps
+
+
+-- Algorithm created by sofar and changed by others:
+-- https://github.com/minetest/minetest/commit/d7908ee49480caaab63d05c8a53d93103579d7a9
+
+local function search_dfs(go, p, apply_move, moves)
+ local num_moves = #moves
+
+ -- Uncomment if the starting position should be walked even if its
+ -- neighbours cannot be walked
+ --~ go(p)
+
+ -- The stack contains the path to the current position;
+ -- an element of it contains a position and direction (index to moves)
+ local s = r.datastructures.create_stack()
+ -- The neighbor order we will visit from our table.
+ local v = 1
+
+ while true do
+ -- Push current state onto the stack.
+ s:push({ p = p, v = v })
+ -- Go to the next position.
+ p = apply_move(p, moves[v])
+ -- Now we check out the node. If it is in need of an update,
+ -- it will let us know in the return value (true = updated).
+ local can_go, abort = go(p)
+ if not can_go then
+ if abort then
+ return
+ end
+ -- If we don't need to "recurse" (walk) to it then pop
+ -- our previous pos off the stack and continue from there,
+ -- with the v value we were at when we last were at that
+ -- node
+ repeat
+ local pop = s:pop()
+ p = pop.p
+ v = pop.v
+ -- If there's nothing left on the stack, and no
+ -- more sides to walk to, we're done and can exit
+ if s:is_empty() and v == num_moves then
+ return
+ end
+ until v < num_moves
+ -- The next round walk the next neighbor in list.
+ v = v + 1
+ else
+ -- If we did need to walk the neighbor/current position, then
+ -- start walking from here from the walk order start (1),
+ -- and not the order we just pushed up the stack.
+ v = 1
+ end
+ end
+end -- search_dfs
+
+
+function replacer.patterns.search_positions(params)
+ local moves = params.moves
+ local max_positions = params.max_positions
+ local fdata = params.fdata
+ local startpos = params.startpos
+ -- visiteds has only positions where fdata.func evaluated to true
+ local visiteds = {}
+ local founds = {}
+ local n_founds = 0
+ local function go(p)
+ local vi = poshash(p)
+ if visiteds[vi] or not fdata.func(p, fdata) then
+ return false
+ end
+ n_founds = n_founds + 1
+ founds[n_founds] = p
+ visiteds[vi] = true
+ if n_founds >= max_positions then
+ -- Abort, too many positions
+ return false, true
+ end
+ return true
+ end
+ search_dfs(go, startpos, vector_add, moves)
+ if n_founds < max_positions or 0 >= r.radius_factor then
+ return founds, n_founds, visiteds
+ end
+
+ -- Too many positions were found, so search again but only within
+ -- a limited sphere around startpos
+ local rr = floor(max_positions ^ r.radius_factor + .5)
+ local visiteds_old = visiteds
+ visiteds = {}
+ founds = {}
+ n_founds = 0
+ local function go2(p)
+ local vi = poshash(p)
+ if visiteds[vi] then
+ return false
+ end
+ if vector_distance(p, startpos) > rr then
+ -- Outside of the sphere
+ return false
+ end
+ if not visiteds_old[vi] and not fdata.func(p, fdata) then
+ return false
+ end
+ n_founds = n_founds + 1
+ founds[n_founds] = p
+ visiteds[vi] = true
+ return true
+ end
+ search_dfs(go2, startpos, vector_add, moves)
+ return founds, n_founds, visiteds
+end -- search_positions
+
diff --git a/replacer/replacer.lua b/replacer/replacer.lua
new file mode 100644
index 0000000..983534a
--- /dev/null
+++ b/replacer/replacer.lua
@@ -0,0 +1,744 @@
+replacer.tool_name_basic = 'replacer:replacer'
+replacer.tool_name_technic = 'replacer:replacer_technic'
+replacer.tool_default_node = 'default:dirt'
+
+-- pulling to local scope especially those used in loops
+local r = replacer
+local rb = replacer.blabla
+local rp = replacer.patterns
+local rud = replacer.unifieddyes
+local S = replacer.S
+local max_time_us = 1000000 * r.max_time
+-- math
+local max, min, floor = math.max, math.min, math.floor
+local core_check_player_privs = minetest.check_player_privs
+local core_get_node = minetest.get_node
+local core_get_node_or_nil = minetest.get_node_or_nil
+local core_get_item_group = minetest.get_item_group
+local core_registered_items = minetest.registered_items
+local core_registered_nodes = minetest.registered_nodes
+local core_swap_node = minetest.swap_node
+local deserialize = minetest.deserialize
+local get_craft_recipe = minetest.get_craft_recipe
+local has_creative = creative.is_enabled_for
+local serialize = minetest.serialize
+local us_time = minetest.get_us_time
+-- vector
+local vector_distance = vector.distance
+local vector_multiply = vector.multiply
+local vector_new = vector.new
+local vector_subtract = vector.subtract
+
+replacer.mode_major_names = { rb.mode_single, rb.mode_field, rb.mode_crust }
+replacer.mode_major_infos = {
+ rb.mode_single_tooltip,
+ rb.mode_field_tooltip:gsub('\n', ' '),
+ rb.mode_crust_tooltip:gsub('\n', ' ')
+}
+replacer.mode_minor_names = { rb.mode_minor1, rb.mode_minor2, rb.mode_minor3 }
+replacer.mode_minor_infos = {
+ rb.mode_minor1_info, rb.mode_minor2_info, rb.mode_minor3_info
+}
+
+replacer.mode_colours = {
+ { '#ffffff', '#cccccc', '#999999' },
+ { '#38fb9a', '#21cc79', '#10bb68' },
+ { '#f4b755', '#d29533', '#9F6200' }
+}
+
+
+function replacer.get_data(stack)
+ local meta = stack:get_meta()
+ local data = meta:get_string('replacer'):split(' ') or {}
+ local node = {
+ name = data[1] or r.tool_default_node,
+ param1 = tonumber(data[2]) or 0,
+ param2 = tonumber(data[3]) or 0
+ }
+ local mode, mode_bare = {}, meta:get_string('mode'):split('.') or {}
+ mode.major = tonumber(mode_bare[1] or 1) or 1
+ mode.minor = tonumber(mode_bare[2] or 1)
+ if r.disable_minor_modes then mode.minor = 1 end
+ return node, mode
+end -- get_data
+
+
+function replacer.set_data(stack, node, mode)
+ node = 'table' == type(node) and node or {}
+ -- allow passing nil mode -> when ignoring mode in history
+ if 'table' ~= type(mode) then
+ local _
+ _, mode = r.get_data(stack)
+ end
+ if r.disable_minor_modes then mode.minor = 1 end
+ local tool_itemstring = stack:get_name()
+ local tool_def = core_registered_items[tool_itemstring]
+ -- some accidents or deliberate actions can be harmful
+ -- if user has an unknown item. So we check here to
+ -- prevent possible server crash
+ if (not tool_itemstring) or (not tool_def) then
+ local t = {
+ 'Blessed', 'Somewhat known', 'Unknown if known',
+ 'Pwned', 'Hued', 'Strange', 'Found'
+ }
+ return t[os.date('*t').wday] .. ' Item'
+ end
+ local param1 = tostring(node.param1 or 0)
+ local param2 = tostring(node.param2 or 0)
+ local node_name = node.name or r.tool_default_node
+ local data = node_name .. ' ' .. param1 .. ' ' .. param2
+ local meta = stack:get_meta()
+ meta:set_string('mode', mode.major .. '.' .. mode.minor)
+ meta:set_string('replacer', data)
+ meta:set_string('color', r.mode_colours[mode.major][mode.minor])
+ local node_def = core_registered_items[node_name]
+ local node_description = node_name
+ if node_def and node_def.description then
+ node_description = node_def.description
+ end
+ local colour_name = rud.colour_name(param2, node_def)
+ if 0 < #colour_name then
+ colour_name = ' ' .. colour_name
+ end
+ local tool_name = tool_def.description
+ local short_description = rb.tool_short_description:format(
+ param1, param2, colour_name, node_name)
+ local description = rb.tool_long_description:format(
+ tool_name, short_description, node_description) -- r.titleCase(colour_name))
+
+ meta:set_string('description', description)
+ return short_description
+end -- set_data
+
+
+if r.has_technic_mod then
+ if technic.plus then
+ replacer.get_charge = technic.get_RE_charge
+ replacer.set_charge = technic.set_RE_charge
+ else
+ -- technic still stores data serialized, so this is the nearest we get to current standard
+ function replacer.get_charge(itemstack)
+ local meta = deserialize(itemstack:get_meta():get_string(''))
+ if (not meta) or (not meta.charge) then
+ return 0
+ end
+ return meta.charge
+ end
+
+ function replacer.set_charge(itemstack, charge, maximum)
+ technic.set_RE_wear(itemstack, charge, maximum)
+ local meta = itemstack:get_meta()
+ local data = deserialize(meta:get_string(''))
+ if (not data) or (not data.charge) then
+ data = { charge = 0 }
+ end
+ data.charge = charge
+ meta:set_string('', serialize(data))
+ end
+ end
+
+ function replacer.discharge(itemstack, charge, num_nodes, has_creative_or_give)
+ if (not technic.creative_mode) and (not has_creative_or_give) then
+ charge = charge - r.charge_per_node * num_nodes
+ r.set_charge(itemstack, charge, r.max_charge)
+ return itemstack
+ end
+ end
+else
+ function replacer.discharge() end
+ function replacer.get_charge() return r.max_charge end
+end
+
+
+-- replaces one node with another one and returns if it was successful
+function replacer.replace_single_node(pos, node_old, node_new, player,
+ name, inv, creative)
+ local succ, error = r.permit_replace(pos, node_old, node_new, player,
+ name, inv, creative)
+ if not succ then
+ return false, error
+ end
+
+ -- do not replace if there is nothing to be done
+ if node_old.name == node_new.name then
+ -- only the orientation was changed
+ if (node_old.param1 ~= node_new.param1)
+ or (node_old.param2 ~= node_new.param2)
+ then
+ core_swap_node(pos, node_new)
+ end
+ return true
+ end
+
+ -- map exception
+ local inv_name = r.exception_map[node_new.name] or node_new.name
+ -- does the player carry at least one of the desired nodes with him?
+ if (not creative) and (not inv:contains_item('main', inv_name)) then
+ return false, S('You have no further "@1". Replacement failed.',
+ node_new.name or '?')
+ end
+
+ local node_old_def = core_registered_nodes[node_old.name]
+ if not node_old_def then
+ return false, S('Unknown node: "@1"', node_old.name)
+ end
+ local node_new_def = core_registered_nodes[node_new.name]
+ if not node_new_def then
+ return false, S('Unknown node to place: "@1"', node_new.name)
+ end
+
+ -- dig the current node if needed
+ if not node_old_def.buildable_to then
+ -- give the player the item by simulating digging if possible
+ node_old_def.on_dig(pos, node_old, player)
+ -- test if digging worked
+ local dug_node = core_get_node_or_nil(pos)
+ if (not dug_node) or
+ (not core_registered_nodes[dug_node.name].buildable_to) then
+ return false, S('Could not dig "@1" properly.', node_old.name)
+ end
+ end
+
+ -- place the node similar to how a player does it
+ -- (other than the pointed_thing)
+ local new_item
+ new_item, succ = node_new_def.on_place(ItemStack(node_new.name), player,
+ { type = 'node', under = vector_new(pos), above = vector_new(pos) })
+ -- replacing with trellis set, succ is returned but new_item is nil
+ -- possible that other nodes react the same way.
+ -- this allows users to dig nodes, I don't see reason to stop that
+ -- as long as no crash occurs - SwissalpS
+ if (false == succ) or (nil == new_item) then
+ return false, S('Could not place "@1".', node_new.name)
+ end
+
+ -- update inventory in survival mode
+ if not creative then
+ -- consume the item
+ inv:remove_item('main', inv_name .. ' 1')
+ -- if placing the node didn't result in empty stack…
+ if '' ~= new_item:to_string() then
+ inv:add_item('main', new_item)
+ end
+ end
+
+ -- test whether the placed node differs from the supposed node
+ local placed_node = core_get_node(pos)
+ if placed_node.name ~= node_new.name then
+ -- Sometimes placing doesn't put the node but does something different
+ -- e.g. when placing snow on snow with the snow mod
+ return true
+ end
+
+ -- fix orientation if needed
+ if placed_node.param1 ~= node_new.param1 or
+ placed_node.param2 ~= node_new.param2 then
+ core_swap_node(pos, node_new)
+ end
+
+ if 'function' == type(r.exception_callbacks[node_new.name]) then
+ succ, error = r.exception_callbacks[node_new.name](
+ pos, node_old, node_new, player)
+ if (not succ) and error and ('' ~= error) then
+ r.inform(name, rb.callback_error:format(error))
+ end
+ end
+
+ return true
+end -- replace_single_node
+
+
+-- the function which happens when the replacer is used
+-- also called by on_place if sneak isn't pressed
+function replacer.on_use(itemstack, player, pt, right_clicked)
+ if (not player) or (not pt) then
+ return
+ end
+
+ local succ, error
+ local keys = player:get_player_control()
+ local name = player:get_player_name()
+ local creative_enabled = has_creative(name)
+ local has_give = core_check_player_privs(name, 'give')
+ local has_creative_or_give = creative_enabled or has_give
+ local is_technic = itemstack:get_name() == r.tool_name_technic
+ local modes_are_available = is_technic or creative_enabled
+
+ -- is special-key held? (aka fast-key)
+ if keys.aux1 then
+ if not modes_are_available then return itemstack end
+ -- fetch current mode
+ local _, mode = r.get_data(itemstack)
+ -- Show formspec to choose mode
+ r.show_mode_formspec(player, mode)
+ -- return unchanged tool
+ return itemstack
+ end
+
+ if 'node' ~= pt.type then
+ r.play_sound(name, true)
+ r.inform(name, S('Error: "@1" is not a node.', pt.type))
+ return
+ end
+
+ local pos = minetest.get_pointed_thing_position(pt, right_clicked)
+ local node_old = core_get_node_or_nil(pos)
+
+ if not node_old then
+ r.play_sound(name, true)
+ r.inform(name, rb.wait_for_load)
+ return
+ end
+
+ local node_new, mode = r.get_data(itemstack)
+ if not modes_are_available then
+ mode = { major = 1, minor = 1 }
+ end
+ -- utility function to adjust new node to mode.minor
+ -- returns true if adjustments make them equal
+ local function adjust_new_to_minor()
+ -- minor mode overrides to node_new
+ if 2 == mode.minor then
+ -- node only
+ node_new.param1 = node_old.param1
+ node_new.param2 = node_old.param2
+ elseif 3 == mode.minor then
+ -- rotation only
+ node_new.name = node_old.name
+ end
+ -- can we skip right away?
+ if (node_old.name == node_new.name)
+ and (node_old.param1 == node_new.param1)
+ and (node_old.param2 == node_new.param2)
+ then
+ return true
+ end
+ end -- adjust_new_to_minor
+ if adjust_new_to_minor() then
+ r.inform(name, rb.nothing_to_replace)
+ return
+ end
+
+ local inv = player:get_inventory()
+ if 1 == mode.major then
+ -- single
+ succ, error = r.replace_single_node(pos, node_old, node_new,
+ player, name, inv, has_creative_or_give)
+ if not succ then
+ r.inform(name, error)
+ end
+ return
+ end
+
+ -- figure out how many nodes we can modify before we reach
+ -- either the count or charge limit
+ local max_nodes = r.limit_list[node_new.name] or r.max_nodes
+ local charge = r.get_charge(itemstack)
+ if not has_creative_or_give then
+ if charge < r.charge_per_node then
+ r.play_sound(name, true)
+ r.inform(name, rb.need_more_charge)
+ --return
+ end
+
+ -- clamp so it works as single mode even without charge
+ local max_charge_to_use = min(charge, r.max_charge)
+ max_nodes = floor(max_charge_to_use / r.charge_per_node)
+ max_nodes = max(1, min(max_nodes, r.max_nodes))
+ end
+
+ local found_positions, found_count
+ if 2 == mode.major then
+ -- field
+ -- Get four walk directions which are orthogonal to the field
+ local normal = vector_subtract(pt.above, pt.under)
+ local dirs, n = {}, 1
+ local p
+ for coord in pairs(normal) do
+ if 0 == normal[coord] then
+ for a = -1, 1, 2 do
+ p = { x = 0, y = 0, z = 0 }
+ p[coord] = a
+ dirs[n] = p
+ n = n + 1
+ end
+ end
+ end
+ -- with multinode-nodes it is possible to click the
+ -- node in a way that none of the coordinates of
+ -- ``normal`` is 0, leading to empty ``dirs`` and crash
+ -- when passing nil to vector functions.
+ -- Player can click on another part of the node to
+ -- have success.
+ if 0 == #dirs then
+ r.play_sound(name, true)
+ r.inform(name, rb.no_pos)
+ return
+ end
+
+ -- The normal is used as offset to test if the searched position
+ -- is next to the field; the offset goes in the other direction when
+ -- a right click happens
+ if right_clicked then
+ normal = vector_multiply(normal, -1)
+ end
+ -- Search along the plane next to the field
+ right_clicked = (right_clicked and true) or false
+ found_positions, found_count = rp.search_positions({
+ startpos = pos,
+ fdata = {
+ func = rp.field_position,
+ name = node_old.name,
+ param2 = node_old.param2,
+ pname = name,
+ above = normal,
+ right_clicked = right_clicked
+ },
+ moves = dirs,
+ max_positions = max_nodes,
+ })
+ elseif 3 == mode.major then
+ -- crust
+ -- Search positions of air (or similar) nodes next to the crust
+ local nodename_clicked = rp.get_node(pt.under).name
+ local unders, under_count, aboves = rp.search_positions({
+ startpos = pt.above,
+ fdata = {
+ func = rp.crust_above_position,
+ name = nodename_clicked,
+ pname = name
+ },
+ moves = rp.offsets_touch,
+ max_positions = max_nodes,
+ })
+ local data
+ if right_clicked then
+ -- Remove positions which are not directly touching the crust
+ data = {
+ ps = unders,
+ num = under_count,
+ name = nodename_clicked,
+ pname = name
+ }
+ rp.reduce_crust_above_ps(data)
+ found_positions, found_count = data.ps, data.num
+ else
+ -- Search crust positions which are next to the previously found
+ -- air (or similar) node positions
+ found_positions, found_count = rp.search_positions({
+ startpos = pt.under,
+ fdata = {
+ func = rp.crust_under_position,
+ name = node_old.name,
+ pname = name,
+ aboves = aboves
+ },
+ moves = rp.offsets_hollowcube,
+ max_positions = max_nodes
+ })
+ -- Keep only positions which are directly touching those previously
+ -- found positions
+ data = { aboves = aboves, ps = found_positions, num = found_count }
+ rp.reduce_crust_ps(data)
+ found_positions, found_count = data.ps, data.num
+ end
+ end
+
+ rp.reset_nodes_cache()
+
+ -- at least do the one that was clicked on
+ if 0 == found_count then
+ succ, error = r.replace_single_node(pos, node_old, node_new, player,
+ name, inv, has_creative_or_give)
+ if not succ then
+ r.inform(name, error)
+ end
+ return
+ end
+
+ local charge_needed = r.charge_per_node * found_count
+ local possible_count = found_count
+ if not has_creative_or_give then
+ if charge < charge_needed then
+ possible_count = floor(charge / r.charge_per_node)
+ end
+ end
+
+ -- sort by distance, nearest last as we work backwards
+ table.sort(found_positions, function(pos1, pos2)
+ return vector_distance(pos1, pos) > vector_distance(pos2, pos)
+ end)
+
+ -- set nodes
+ local pos3
+ local actual_node_count = 0
+ local us_time_limit = us_time() + max_time_us
+ local index = found_count
+ repeat -- use fast repeat loop
+ pos3 = found_positions[index]
+ node_old = core_get_node(pos3)
+ -- only change nodes that need changing
+ if not adjust_new_to_minor() then
+ succ, error = r.replace_single_node(pos3, node_old, node_new,
+ player, name, inv, has_creative_or_give)
+ if not succ then
+ r.inform(name, error)
+ break
+ end
+ actual_node_count = actual_node_count + 1
+ if actual_node_count > max_nodes then
+ -- This can happen if too many nodes were detected and the nodes
+ -- limit has been set to a small value
+ r.inform(name, rb.too_many_nodes_detected)
+ break
+ end
+ end -- if can't skip
+ -- time-out check
+ if us_time() > us_time_limit then
+ r.inform(name, rb.timed_out)
+ break
+ end
+ index = index - 1
+ until (0 == index) or (actual_node_count == possible_count) -- loop found nodes
+
+ r.discharge(itemstack, charge, actual_node_count, has_creative_or_give)
+ if has_creative_or_give then
+ r.inform(name, S('@1 nodes replaced.', actual_node_count))
+ end
+ return itemstack
+end -- on_use
+
+
+-- right-click with tool -> place set node
+-- special+right-click -> cycle major mode (if tool/privs permit)
+-- special+sneak+right-click -> cycle minor mode (if tool/privs permit)
+-- sneak+right-click -> set node
+function replacer.on_place(itemstack, player, pt)
+ if (not player) or (not pt) then
+ return
+ end
+
+ local keys = player:get_player_control()
+ local name = player:get_player_name()
+ local creative_enabled = has_creative(name)
+ local has_give = core_check_player_privs(name, 'give')
+ --local has_creative_or_give = creative_enabled or has_give
+ local is_technic = itemstack:get_name() == r.tool_name_technic
+ local modes_are_available = is_technic or creative_enabled
+ local _, node, mode
+
+ -- is special-key held? (aka fast-key) -> change mode
+ if keys.aux1 then
+ -- don't want anybody to think that special+rc = place
+ if not modes_are_available then return end
+ -- fetch current mode
+ node, mode = r.get_data(itemstack)
+ if keys.sneak then
+ if r.disable_minor_modes then
+ r.play_sound(name, true)
+ r.inform(name, rb.minor_modes_disabled)
+ return itemstack
+ end
+
+ -- increment and roll-over minor mode
+ mode.minor = mode.minor % 3 + 1
+ -- spam chat
+ r.inform(name, S('Mode changed to @1: @2',
+ r.mode_minor_names[mode.minor], r.mode_minor_infos[mode.minor]))
+ else
+ -- increment and roll-over major mode
+ mode.major = mode.major % 3 + 1
+ -- spam chat
+ r.inform(name, S('Mode changed to @1: @2',
+ r.mode_major_names[mode.major], r.mode_major_infos[mode.major]))
+ end
+ -- update tool
+ r.set_data(itemstack, node, mode)
+ -- return changed tool
+ return itemstack
+ end -- change mode
+
+ -- If not holding sneak key, place node(s)
+ if not keys.sneak then
+ return r.on_use(itemstack, player, pt, true)
+ end
+
+ -- Select new node
+ if 'node' ~= pt.type then
+ r.play_sound(name, true)
+ r.inform(name, rb.none_selected)
+ return
+ end
+
+ node = core_get_node_or_nil(pt.under)
+ if not node then
+ r.play_sound(name, true)
+ return
+ end
+
+ -- don't allow setting replacer to denied nodes
+ -- helper function for valid()
+ local function denied_group(item_name)
+ for group, val in pairs(r.deny_groups) do
+ if val and 0 < core_get_item_group(item_name, group) then
+ return true
+ end
+ end
+ return false
+ end
+ if r.deny_list[node.name] or denied_group(node.name) then
+ r.play_sound(name, true)
+ r.inform(name, S('Placing nodes of type "@1" is not '
+ .. 'allowed on this server.', node.name))
+ return
+ end
+
+ _, mode = r.get_data(itemstack)
+ if not modes_are_available then
+ mode = { major = 1, minor = 1 }
+ end
+ local inv = player:get_inventory()
+ -- helper functions for simpler mechanics
+ local function can_be_crafted(itemstring)
+ local input = get_craft_recipe(itemstring)
+ return input and input.items and true or false
+ end
+ local function valid()
+ -- user with give can get and place anything available
+ if has_give then return true end
+
+ -- if user has it in inventory it must be ok
+ if inv:contains_item('main', node.name) then return true end
+
+ -- if there is an alias, apply and allow it
+ if (not creative_enabled) and r.alias_map[node.name] then
+ node.name = r.alias_map[node.name] return true
+ end
+
+ -- it's one of those nodes that consume an item with different name
+ -- and/or have an after_on_place callback registered
+ if r.exception_map[node.name] then return true end
+
+ -- if it can be crafted, it's ok to use (includes cooking etc.)
+ if can_be_crafted(node.name) then return true end
+
+ -- give callbacks a chance to allow it
+ -- they can also manipulate the passed node-table
+ for _, f in ipairs(r.enable_set_callbacks) do
+ -- first callback to return something other than nil or false
+ -- permits to setting the replacer to node
+ if f(node, player, pt) then return true end
+ end
+
+ -- now we are scraping the bottom of the barrel
+ -- let's check if digging this would drop something use-able
+ local drops = r.possible_node_drops(node.name, true)
+ local drop_name
+ local valid_drops = {}
+ -- if 0 == core_get_item_group(node.name, 'not_in_creative_inventory') then
+ for i = 1, #drops do
+ drop_name = drops[i]
+ if core_registered_nodes[drop_name] -- it's a node not an item-drop
+ and (not denied_group(drop_name))
+ and (inv:contains_item('main', drop_name)
+ or (0 == core_get_item_group(drop_name,
+ 'not_in_creative_inventory')))
+ then
+ -- it drops itself, so let's shortcut and set to it
+ if drop_name == node.name then
+ return true
+ end
+ -- otherwise let's add to valid options so user can choose (once we add that feature)
+ table.insert(valid_drops, drop_name)
+ -- example dirt_with_rainforest_litter can not be
+ -- crafted on all servers but drops dirt, so
+ -- replacer would be set to dirt
+ --node.name = drop_name
+ --return true
+ end
+ end -- loop drops
+ -- end -- node is in creative inventory
+
+ -- TODO: show formspec for user to choose, if there are multiple options
+ if 0 < #valid_drops then
+ -- for now we just take the first option
+ node.name = valid_drops[1]
+ return true
+ end
+
+ if not creative_enabled then return false end
+
+ -- creative users have access to more items
+ -- but it must be a dig-able node
+ for i = 1, #drops do
+ drop_name = drops[i]
+ if core_registered_nodes[drop_name] -- it's a node not an item-drop
+ and (not denied_group(drop_name))
+ and (0 == core_get_item_group(drop_name,
+ 'not_in_creative_inventory'))
+ then
+ node.name = drop_name
+ return true
+ end
+ end -- loop drops
+
+ -- well, better luck next time
+ return false
+ end -- valid
+ if not valid() then
+ r.play_sound(name, true)
+ r.inform(name, S('Failed to set replacer to "@1". '
+ .. 'If there was one in your inventory, then maybe.', node.name))
+ return
+ end
+
+ -- set the params to tool
+ local short_description = r.set_data(itemstack, node, mode)
+ -- add to history
+ r.history.add_item(player, mode, node, short_description)
+ -- inform player about successful setting
+ r.play_sound(name)
+ r.inform(name, S('Node replacement tool set to:\n@1.', short_description))
+
+ return itemstack --data changed
+end -- on_place
+
+
+function replacer.tool_def_basic()
+ return {
+ description = rb.description_basic,
+ inventory_image = 'replacer_replacer.png',
+ stack_max = 1, -- it has to store information - thus only one can be stacked
+ liquids_pointable = true, -- it is ok to painit in/with water
+ --node_placement_prediction = nil,
+ -- place node(s)
+ on_place = r.on_place,
+ on_secondary_use = r.on_place,
+ -- Replace node(s)
+ on_use = r.on_use
+ }
+end
+
+
+minetest.register_tool(r.tool_name_basic, r.tool_def_basic())
+
+if r.has_technic_mod then
+ function replacer.tool_def_technic()
+ local def = r.tool_def_basic()
+ def.description = rb.description_technic
+ if technic.plus then
+ def.technic_max_charge = r.max_charge
+ else
+ def.wear_represents = 'technic_RE_charge'
+ def.on_refill = technic.refill_RE_charge
+ end
+ return def
+ end
+ if technic.plus then
+ technic.register_power_tool(r.tool_name_technic, r.tool_def_technic())
+ else
+ technic.register_power_tool(r.tool_name_technic, r.max_charge)
+ minetest.register_tool(r.tool_name_technic, r.tool_def_technic())
+ end
+end
+
diff --git a/settingtypes.txt b/settingtypes.txt
new file mode 100644
index 0000000..a8cc543
--- /dev/null
+++ b/settingtypes.txt
@@ -0,0 +1,34 @@
+# Replace / place up to this many nodes when using modes other than single.
+# Depending on server hardware and amount of users, this value needs adapting.
+# On singleplayer you can mostly use a higher value.
+# The default is 3168
+replacer.max_nodes (Maximum nodes to replace with one click) int 3168
+# Some nodes take a long time to be placed. This value limits the time
+# in seconds in which the nodes are placed.
+replacer.max_time (Time limit when putting nodes) float 1.0
+# Radius limit factor when more possible positions are found than either max_nodes or charge
+# Set to 0 or less for behaviour of before version 3.3
+replacer.radius_factor (A factor to adjust size limit) float 0.4
+# If you don't want to use the minor modes at all, set to true
+replacer.disable_minor_modes (Disable using minor modes) bool false
+# You can make history available to users with this priv. By default it is set to creative
+# as non-creative users can make several replacers.
+replacer.history_priv (Priv needed to allow using history) string creative
+# When set, does not save history over sessions. Reason might be old MT version.
+replacer.history_disable_persistancy (Disable saving history) bool false
+# How frequently history is saved to player-meta. Only users with the priv are affected.
+replacer.history_save_interval (Interval in minutes at which history is saved) int 7
+# When set, changes the replacer's major and minor modes when picking an item from history.
+# The modes are stored either way.
+replacer.history_include_mode (Should picking from history also set mode) bool false
+# Limit history length. Duplicates are removed so there isn't much need for long histories.
+replacer.history_max (Maximum amount of history items) int 7 2 55555
+# You may choose to hide basic recipe but then make sure to enable the technic direct one
+replacer.hide_recipe_basic (Hide basic recipe) bool false
+# Hide the upgrade recipe. Only available if technic is installed.
+replacer.hide_recipe_technic_upgrade (Hide upgrade recipe) bool false
+# Hide the direct upgrade recipe. Only available if technic is installed.
+replacer.hide_recipe_technic_direct (Hide direct recipe) bool true
+# Enable developer mode
+replacer.dev_mode (Enable developer mode) bool false
+
diff --git a/test.lua b/test.lua
new file mode 100644
index 0000000..93497c4
--- /dev/null
+++ b/test.lua
@@ -0,0 +1,264 @@
+-- enable developer mode
+replacer.dev_mode =
+ minetest.settings:get_bool('replacer.dev_mode') or false
+if not replacer.dev_mode then return end
+
+replacer.test = {}
+local r = replacer
+local rt = replacer.test
+local pd = r.print_dump
+rt.spacing = 2
+rt.player = nil
+rt.facing = vector.new(0, 0, 0)
+rt.active = false
+rt.no_support = false
+rt.move_player = false
+rt.nodes_per_step = 444
+rt.air_node = { name = 'air' }
+rt.seconds_between_steps = 1.1
+rt.support_node = { name = 'default:cobble' }
+-- skip these patterns that return a match with string:find(pattern)
+rt.skip = {
+ -- these can be counter-productive and not replacer nodes
+ '^air$', '^ignore$', 'corium', '^tnt:',
+ '^technic:hv_nuclear_reactor_core_active$',
+ '^default:lava_source$', '^default:lava_flowing$',
+ '^default:.*water_source$', '^default:.*water_flowing$',
+ --'^default:large_cactus_seedling$', -- depends on support_node
+ '^digistuff:heatsink_onic$', -- depends on support_node
+ --'^farming:seed_', -- depends on support_node
+ -- these are removed right away
+ '^illumination:light_', '^morelights_extras:stairlight$',
+ '^throwing:arrow', '^vacuum:vacuum$',
+ -- sun matter is harmless, but not needed as not pointable
+ '^planetoidgen:sun$',
+ -- not pointable (can right-click with inspection tool to see them)
+ '^digistuff:piston_pusher$', '^doors:hidden$', '^elevator:placeholder$',
+ '^fancy_vend:display_node$',
+ -- these cause crashes
+ '^advtrains_interlocking:dtrack_npr_st',
+ '^advtrains_line_automation:dtrack_stop_st',
+ '^basic_signs:sign_',
+ '^default:sign_'
+}
+
+function replacer.test.inform(message)
+ if not rt.player or not rt.player.get_player_name then return end
+ r.inform(rt.player:get_player_name(), message)
+end -- inform
+local rti = rt.inform
+
+
+-- This function is quite robust but it still can happen that game crashes.
+-- It has worked best if area was already generated and loaded at least once.
+-- Don't be too hasty to add nodes to deny-patterns.
+-- It probably helps to turn off mesecons_debug and metrics in general for this
+-- kind of excercise.
+-- It's also advisable to have damage turned off or to wear a hazmat suit when
+-- technic is involved and move_player flag is set.
+function replacer.test.chatcommand_place_all(player_name, param)
+ if rt.active then
+ return false, 'There is an active task in progress, try again later'
+ end
+ param = param or ''
+ local dry_run
+ local params = param:split(' ')
+ local patterns = {}
+ rt.no_support = false
+ rt.move_player = false
+ for _, param2 in ipairs(params) do
+ if 'dry-run' == param2 then
+ dry_run = true
+ elseif 'move_player' == param2 then
+ rt.move_player = true
+ elseif 'no_support_node' == param2 then
+ rt.no_support = true
+ else
+ table.insert(patterns, param2)
+ end
+ end
+ if 0 == #patterns then table.insert(patterns, '.*') end
+ rt.player = minetest.get_player_by_name(player_name)
+ rt.pos = rt.player:get_pos()--vector.add(player:get_pos(), vector.new(1, 0, 1))--
+ rt.selected = {}
+ rt.count = 0
+ local function has_match(name, patterns_to_check)
+ for _, pattern in ipairs(patterns_to_check) do
+ if name:find(pattern) then return true end
+ end
+ return false
+ end -- has_match
+ for name, _ in pairs(minetest.registered_nodes) do
+ if not has_match(name, rt.skip)
+ and has_match(name, patterns)
+ then
+ table.insert(rt.selected, name)
+ rt.count = rt.count + 1
+ end
+ end
+ if 0 == rt.count then
+ return true, 'Nothing to do.'
+ end
+ table.sort(rt.selected)
+ rt.side, rt.x, rt.z = math.floor((rt.count ^ .5) + .5), 0, 0
+ local full_side = rt.spacing * (rt.side + 1)
+ local pos2 = vector.add(rt.pos, vector.new(full_side, 0, full_side))
+ if dry_run then
+ return true, 'Required space: ' .. r.nice_pos_string(rt.pos)
+ .. ' to ' .. r.nice_pos_string(pos2)
+ end
+
+ minetest.emerge_area(rt.pos, vector.add(pos2, vector.new(0, -1, 0)))
+ rt.i = 1
+ rt.active = true
+ rt.succ_count = 0
+ minetest.after(.1, rt.step)
+ return true, 'Started process'
+end -- chatcommand_place_all
+
+
+function replacer.test.step()
+ -- player may have logged off already
+ if not rt.active then return end
+
+ local new_item, succ, pos_, pos__, node, name
+ local function move_player()
+ if not rt.move_player then return end
+
+ rt.player:set_pos(vector.add(pos_, vector.new(-.25, 0, -1)))
+ --rt.player:set_rotation(rt.facing)
+ rt.player:set_look_horizontal(math.rad(0))
+ rt.player:set_look_vertical(math.rad(45))
+ end -- move_player
+
+ for _ = 1, rt.nodes_per_step do
+ name = rt.selected[rt.i]
+ node = minetest.registered_nodes[name]
+ pos_ = vector.add(rt.pos, vector.new(rt.x, 0, rt.z))
+ pos__ = vector.add(pos_, vector.new(0, -1, 0))
+ -- ensure area is generated and loaded
+ if rt.check_mapgen(pos_) then
+ rti('waiting for mapgen')
+ minetest.after(5, rt.step)
+ return
+ end
+
+ if minetest.find_node_near(pos_, 1, 'ignore', true) then
+ rti('emerging area')
+ move_player()
+ minetest.emerge_area(pos_, pos__)
+ minetest.after(2, rt.step)
+ return
+ end
+
+ minetest.set_node(pos_, rt.air_node)
+ if not rt.no_support then
+ minetest.set_node(pos__, rt.support_node)
+ end
+ move_player()
+ print(r.nice_pos_string(pos_) .. ' ' .. name)
+ new_item, succ = node.on_place(ItemStack(node.name), rt.player, {
+ type = 'node',
+ under = vector.new(pos_),
+ above = vector.add(pos_, vector.new(0, 1, 0))
+ })
+ if (false == succ) or (nil == new_item) then
+ pd('Could not place ' .. node.name .. ' at ' .. r.nice_pos_string(pos_))
+ else
+ rt.succ_count = rt.succ_count + 1
+ end
+ rt.x = rt.x + rt.spacing if rt.spacing * rt.side < rt.x then
+ rt.x = 0
+ rt.z = rt.z + rt.spacing
+ end
+ rt.i = rt.i + 1
+ if rt.count < rt.i then break end
+ end
+ -- keep player alive
+ --rt.player:set_hp(55555, { type = 'set_hp', from = 'mod' })
+ minetest.do_item_eat(55555, 'farming:bread 99', ItemStack('farming:bread 99'),
+ rt.player, { type = 'nothing' })
+ if rt.count <= rt.i then
+ rti(tostring(rt.succ_count) .. ' of ' .. tostring(rt.count)
+ .. ' nodes placed successfuly')
+ rt.active = false
+ return
+ end
+
+ rti('Step ' .. tostring(rt.i) .. ' of ' .. tostring(rt.count) .. ' done')
+
+ minetest.after(rt.seconds_between_steps, rt.step)
+end -- step
+
+
+function replacer.test.dealloc_player(player)
+ if not rt.player or not rt.player.get_player_name then return end
+ if rt.player:get_player_name() ~= player:get_player_name() then return end
+ rt.active = false
+ rt.player = nil
+end -- dealloc_player
+
+
+minetest.register_on_leaveplayer(rt.dealloc_player)
+minetest.register_chatcommand('place_all', {
+ params = '[dry-run][ move_player][ no_support_node][ [] ... [ ] ]',
+ description = 'Places one of all registered nodes on a grid in +x,+z plane starting '
+ .. 'at player position. You can use dry-run option to detect how much space you will need. '
+ .. 'Pass patterns to only place nodes whose name matches. e.g. "^beacon:" "_tinted$" '
+ .. '-> only place nodes beginning with "beacon:" i.e. beacon-mod, and all nodes ending '
+ .. 'with "_tinted" i.e. paintbrush nodes. To exclude patterns, edit test.lua and add to '
+ .. 'rt.skip table.',
+ func = rt.chatcommand_place_all,
+ privs = { privs = true }
+})
+
+
+-- from jumpdrive code, mapgen tracking
+local events = {} -- list of { minp, maxp, time }
+
+-- update last mapgen event time
+--luacheck: no unused args
+minetest.register_on_generated(function(minp, maxp, seed)
+ table.insert(events, {
+ minp = minp,
+ maxp = maxp,
+ time = minetest.get_us_time()
+ })
+end)
+
+
+-- true = mapgen recently active in that area
+function replacer.test.check_mapgen(pos)
+ for _, event in ipairs(events) do
+ if 200 > vector.distance(pos, event.minp) then
+ return true
+ end
+ end
+
+ return false
+end -- check_mapgen
+
+
+-- cleanup
+local timer = 0
+minetest.register_globalstep(function(dtime)
+ timer = timer + dtime
+ if 5 > timer then return end
+
+ timer = 0
+ local time = minetest.get_us_time()
+ local delay_seconds = 10
+
+ local copied_events = events
+ events = {}
+
+ local count = 0
+ for _, event in ipairs(copied_events) do
+ if event.time > (time - (delay_seconds * 1000000)) then
+ -- still recent
+ table.insert(events, event)
+ count = count + 1
+ end
+ end
+end)
+
diff --git a/utils.lua b/utils.lua
new file mode 100644
index 0000000..7dbde33
--- /dev/null
+++ b/utils.lua
@@ -0,0 +1,179 @@
+local r = replacer
+local rb = replacer.blabla
+local chat_send_player = minetest.chat_send_player
+local get_player_by_name = minetest.get_player_by_name
+local get_node_drops = minetest.get_node_drops
+local core_log = minetest.log
+local floor = math.floor
+local absolute = math.abs
+local concat = table.concat
+local insert = table.insert
+local gmatch = string.gmatch
+local registered_nodes = minetest.registered_nodes
+local pos_to_string = minetest.pos_to_string
+local sound_play = minetest.sound_play
+local sound_fail = 'default_break_glass'
+local sound_success = 'default_item_smoke'
+local sound_gain = 0.5
+if r.has_technic_mod then
+ sound_fail = 'technic_prospector_miss'
+ --sound_success = 'technic_prospector_hit'
+ sound_gain = 0.1
+end
+
+
+function replacer.common_list_items(list1, list2)
+ if 'table' ~= type(list1) or 'table' ~= type(list2) then return {} end
+ if 0 == #list1 or 0 == #list2 then return {} end
+
+ local common, index1, total2, index2 = {}, #list1, #list2
+ repeat
+ index2 = total2
+ repeat
+ if list1[index1] == list2[index2] then
+ insert(common, list2[index2])
+ break
+ end
+ index2 = index2 - 1
+ until 0 == index2
+ index1 = index1 - 1
+ until 0 == index1
+
+ return common
+end -- common_list_items
+
+
+function replacer.inform(name, message)
+ if (not message) or ('' == message) then return end
+
+ core_log('info', rb.log_messages:format(name, message))
+ local player = get_player_by_name(name)
+ if not player then return end
+
+ local meta = player:get_meta() if not meta then return end
+
+ if 0 < meta:get_int('replacer_mute') then return end
+
+ chat_send_player(name, message)
+end -- inform
+
+
+function replacer.nice_duration(seconds)
+ if 'number' ~= type(seconds) then return '' end
+
+ seconds = absolute(seconds)
+ local days = floor(seconds / 86400)
+ seconds = seconds % 86400
+ local text = (0 == days and '') or (tostring(days) .. ' ' .. rb.days .. ' ')
+ return text .. os.date('! %H:%M:%S', seconds)
+end -- nice_duration
+
+
+function replacer.nice_number(number, seperator)
+ if 'number' ~= type(number) then return '' end
+
+ local sign = 0 > number and '-' or ''
+ -- TODO: use default depending on locale, won't work as not all 'de' use same
+ -- and not all 'en' use same, hindi has it's own format: 12'34'567
+ seperator = seperator or "'"
+ local reversed = tostring(absolute(number)):reverse()
+ local list = {}
+ for s in gmatch(reversed, '...') do insert(list, s) end
+ local rest = #reversed % 3
+ if 0 ~= rest then insert(list, reversed:sub(-rest, -1)) end
+ return sign .. concat(list, seperator):reverse()
+end -- nice_number
+
+
+function replacer.nice_pos_string(pos)
+ if 'table' ~= type(pos) then return rb.no_pos end
+ if not (pos.x and pos.y and pos.z) then return rb.no_pos end
+
+ pos = { x = floor(pos.x + .5), y = floor(pos.y + .5), z = floor(pos.z + .5) }
+ return pos_to_string(pos)
+end -- nice_pos_string
+
+
+function replacer.play_sound(player_name, fail)
+ local player = get_player_by_name(player_name)
+ if not player then return end
+
+ local meta = player:get_meta() if not meta then return end
+
+ if 0 < meta:get_int('replacer_muteS') then return end
+
+ sound_play(fail and sound_fail or sound_success, {
+ to_player = player_name,
+ max_hear_distance = 2,
+ gain = sound_gain }, true)
+end -- play_sound
+
+
+function replacer.possible_node_drops(node_name, return_names_only)
+ if not registered_nodes[node_name] then return {} end
+
+ local droplist = {}
+ local drop = registered_nodes[node_name].drop or ''
+ if 'string' == type(drop) then
+ if '' == drop then
+ -- this returns value with randomness applied :/
+ drop = get_node_drops(node_name)
+ if 0 == #drop then return {} end
+
+ if not return_names_only then return drop end
+
+ for _, item in ipairs(drop) do
+ insert(droplist, item:match('^([^ ]+)'))
+ end
+ return droplist
+ end
+
+ if not return_names_only then return { drop } end
+
+ return { drop:match('^([^ ]+)') }
+ end -- if string
+
+ if 'table' ~= type(drop) or not drop.items then return {} end
+
+ local checks = {}
+ for _, drops in ipairs(drop.items) do
+ for _, item in ipairs(drops.items) do
+ -- avoid duplicates; but include the item itself
+ -- these are itemstrings so same item can appear multiple times with
+ -- different amounts and/or rarity
+ if return_names_only then
+ item = item:match('^([^ ]+)')
+ end
+ if not checks[item] then
+ checks[item] = 1
+ insert(droplist, item)
+ end
+ end
+ end
+ return droplist
+end -- possible_node_drops
+
+
+function replacer.print_dump(...)
+ if not r.dev_mode then return end
+
+ for _, m in ipairs({ ... }) do
+ print(dump(m))
+ end
+end -- print_dump
+
+
+-- from: http://lua-users.org/wiki/StringRecipes
+function replacer.titleCase(str)
+ local function titleCaseHelper(first, rest)
+ return first:upper() .. rest:lower()
+ end
+ -- Add extra characters to the pattern if you need to. _ and ' are
+ -- found in the middle of identifiers and English words.
+ -- We must also put %w_' into [%w_'] to make it handle normal stuff
+ -- and extra stuff the same.
+ -- This also turns hex numbers into, eg. 0Xa7d4
+ str = str:gsub("(%a)([%w_']*)", titleCaseHelper)
+ return str
+end -- titleCase
+