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. +![replacer_template](https://user-images.githubusercontent.com/3192173/74016149-36b36200-4992-11ea-86d1-2d3b64035557.png) + + + +### Single Mode + +Left click: +![replacer_single_leftclick](https://user-images.githubusercontent.com/3192173/74015937-e1775080-4991-11ea-912b-4f4e75c53918.png) + +Right click: +![replacer_single_rightclick](https://user-images.githubusercontent.com/3192173/74015939-e20fe700-4991-11ea-9e4d-8f8c8900024d.png) + +### Field Mode + +The replacer changes nodes in a 2D slice (it is 1D in these illustrations). +Left click: +![replacer_field_leftclick](https://user-images.githubusercontent.com/3192173/74015955-e63c0480-4991-11ea-95b9-4b312bc62ed1.png) +Right click: +![replacer_field_rightclick](https://user-images.githubusercontent.com/3192173/74015933-e0deba00-4991-11ea-8321-de9c0499dcf3.png) + +### Crust Mode + +Left click: the replacer changes visually adjacent nodes (of the same type) on a surface +![replacer_crust_leftclick](https://user-images.githubusercontent.com/3192173/74015951-e5a36e00-4991-11ea-8cdf-f8b1c8897da9.png) + +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 +![replacer_crust_rightclick](https://user-images.githubusercontent.com/3192173/74015954-e63c0480-4991-11ea-956c-ee6848c182be.png) + 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 +![crust_left_click_0_015353](https://user-images.githubusercontent.com/161979/96197649-02496a00-0f53-11eb-9538-d809689d51fb.png) +Before is depicted above and after is below. +![crust_left_click_1_015402](https://user-images.githubusercontent.com/161979/96197651-02e20080-0f53-11eb-9e72-f1ccecceb09c.png) + + +### 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. + +![crust_right_click_0_143612](https://user-images.githubusercontent.com/161979/96177419-28a8de80-0f2e-11eb-8c28-15caa3ea9335.png) +Before is depicted above and after is below. +![crust_right_click_1_143622](https://user-images.githubusercontent.com/161979/96177422-29417500-0f2e-11eb-955c-fcc3213733fd.png) + 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. + +![field_left_click_2_135844](https://user-images.githubusercontent.com/161979/96177701-8b01df00-0f2e-11eb-92e6-9dab76616fe7.png) +Before is depicted above and after is below. +![field_left_click_3_135856](https://user-images.githubusercontent.com/161979/96177702-8b9a7580-0f2e-11eb-9006-c055198092b9.png) + +### Right click: +The replacer places nodes in a 2D slice onto given surface following contour. + +![field_right_click_0_003725](https://user-images.githubusercontent.com/161979/96196639-2ce5f380-0f50-11eb-8115-4c986cce313f.png) +Before is depicted above and after is below. +![field_right_click_1_003729](https://user-images.githubusercontent.com/161979/96196647-31121100-0f50-11eb-94cc-5e05e44772d9.png) + 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. + +![single_left_click_0_025357](https://user-images.githubusercontent.com/161979/96200805-227d2700-0f5b-11eb-9115-0b4a31cb89cc.png) +Before is depicted above and after is below. +![single_left_click_1_025400](https://user-images.githubusercontent.com/161979/96200806-2315bd80-0f5b-11eb-8d8f-32fa401cde79.png) + +### Right click: +The replacer places a node onto the surface. + +![single_right_click_0_023544](https://user-images.githubusercontent.com/161979/96200006-27d97200-0f59-11eb-9928-58491380fe15.png) +Before is depicted above and after is below. +![single_right_click_1_023551](https://user-images.githubusercontent.com/161979/96200007-28720880-0f59-11eb-8606-def11db1a184.png) 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 +