diff --git a/.cdb.json b/.cdb.json new file mode 100644 index 0000000..ada3089 --- /dev/null +++ b/.cdb.json @@ -0,0 +1,11 @@ +{ + "type": "MOD", + "title": "Cleaner", + "short_description": "Remove/Replace unknown entities, nodes, & items.", + "license": "MIT", + "media_license": "CC0-1.0", + "tags": ["world_tools"], + "repo": "https://codeberg.org/AntumLuanti/mod-cleaner", + "issue_tracker": "https://codeberg.org/AntumLuanti/mod-cleaner/issues", + "forums": 18381 +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..62c4b62 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +.* export-ignore +*.py export-ignore diff --git a/.github/workflows/reference.yml b/.github/workflows/reference.yml new file mode 100644 index 0000000..29dec4c --- /dev/null +++ b/.github/workflows/reference.yml @@ -0,0 +1,30 @@ + +name: Build Reference +on: + push: + tags: + - 'v[0-9]*' + workflow_dispatch: + +jobs: + build: + name: Build Reference + runs-on: ubuntu-latest + steps: + - name: Setup Lua + uses: leafo/gh-actions-lua@v8 + with: + luaVersion: 5.4 + - name: Setup Lua Rocks + uses: leafo/gh-actions-luarocks@v4 + - name: Setup dependencies + run: luarocks install --only-deps https://raw.githubusercontent.com/lunarmodules/LDoc/master/ldoc-scm-3.rockspec + - name: Setup LDoc + run: git clone --single-branch --branch=custom https://github.com/AntumDeluge/LDoc.git ldoc + - name: Checkout & Build Docs + run: git clone https://github.com/AntumMT/mod-cleaner.git cleaner && cd cleaner && chmod +x .ldoc/build_versioned_docs.sh && ./.ldoc/build_versioned_docs.sh + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: cleaner/docs/ diff --git a/.ldoc/build_versioned_docs.sh b/.ldoc/build_versioned_docs.sh new file mode 100755 index 0000000..728103f --- /dev/null +++ b/.ldoc/build_versioned_docs.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash + +# place this file in mod ".ldoc" directory + + +d_config="$(dirname $(readlink -f $0))" + +cd "${d_config}/.." + +d_root="$(pwd)" +d_export="${d_export:-${d_root}/docs/reference}" + +cmd_ldoc="${d_root}/../ldoc/ldoc.lua" +if test -f "${cmd_ldoc}"; then + if test ! -x "${cmd_ldoc}"; then + chmod +x "${cmd_ldoc}" + fi +else + cmd_ldoc="ldoc" +fi + +# clean old files +rm -rf "${d_export}" + +# store current branch +main_branch="$(git branch --show-current)" + +html_out="\n\n\n\n\n\n\n" + +cd "${d_root}" +git checkout ${main_branch} + +echo -e "${html_out}" > "${d_export}/index.html" + +echo -e "\nDone!" diff --git a/.ldoc/config.ld b/.ldoc/config.ld new file mode 100644 index 0000000..10e46db --- /dev/null +++ b/.ldoc/config.ld @@ -0,0 +1,201 @@ + +local dofile, print, error, type, table, ipairs, string, tostring +if import then + dofile = import("dofile") + print = import("print") + error = import("error") + type = import("type") + table = import("table") + ipairs = import("ipairs") + string = import("string") + tostring = import("tostring") +end + + +project = "Cleaner" +title = "Cleaner mod for Luanti" +format = "markdown" +not_luadoc=true +boilerplate = false +icon = "textures/cleaner_pencil.png" +favicon = "https://www.luanti.org/media/icon.svg" + +file = { + "settings.lua", + "api.lua", + "chat.lua", + "tools.lua", + ".ldoc/config.luadoc", +} + +new_type("chatcmd", "Chat Commands") +new_type("setting", "Settings") +new_type("tool", "Tools") +new_type("json", "JSON Configurations") + + +local function video_frame(src) + return '' +end + + +local tags +tags, custom_tags = dofile(".ldoc/tags.ld") + + +-- START: handling items to prevent re-parsing + +local registered_items = {} + +local function is_registered(item) + if not registered_items[item.type] then return false end + + for _, tbl in ipairs(registered_items[item.type]) do + if item == tbl then + return true + end + end + + return false +end + +local function register(item) + if not registered_items[item.type] then + registered_items[item.type] = {} + end + + if not is_registered(item) then + table.insert(registered_items[item.type], item) + end +end + +-- END: + + +local function format_setting_tag(desc, value) + return "\n- `" .. desc .. ":` `" .. value .. "`" +end + +local function setting_handler(item) + if not ipairs or not type then + return item + end + + local tags = { + {"settype", "type"}, + {"default"}, + {"min", "minimum value"}, + {"max", "maximum value"}, + } + + local def = { + ["settype"] = format_setting_tag("type", "string"), + } + + for _, t in ipairs(tags) do + local name = t[1] + local desc = t[2] + if not desc then desc = name end + + local value = item.tags[name] + if type(value) == "table" then + if #value > 1 then + local msg = item.file.filename .. " (line " .. item.lineno + .. "): multiple instances of tag \"" .. name .. "\" found" + if error then + error(msg) + elseif print then + print("WARNING: " .. msg) + end + end + + if value[1] then + def[name] = format_setting_tag(desc, value[1]) + end + end + end + + item.description = item.description .. "\n\n**Definition:**\n" .. def.settype + for _, t in ipairs({def.default, def.min, def.max}) do + if t then + item.description = item.description .. t + end + end + + return item +end + +local function chatcmd_handler(item) + for _, p in ipairs(item.params) do + if item.modifiers.param[p].opt then + item.name = item.name .. " [" .. p .. "]" + else + item.name = item.name .. " <" .. p .. ">" + end + end + + if #item.params > 0 then + local pstring = "### Parameters:\n" + for k, param in pairs(item.params) do + if type(k) == "number" then + local value = item.params.map[param] + + pstring = pstring .. '\n- ' + .. param .. '' + + local modifiers = item.modifiers.param[param] + if modifiers and modifiers.type then + pstring = pstring .. ' `' .. modifiers.type .. '`' + end + + if value then + pstring = pstring .. value + end + + if modifiers and modifiers.opt then + pstring = pstring .. " *(optional)*" + end + end + end + + item.description = item.description .. "\n\n" .. pstring + -- clear parameter list + item.params = {} + end + + return item +end + +function custom_display_name_handler(item, default_handler) + if not is_registered(item) then + if item.type == "setting" then + item = setting_handler(item) + elseif item.type == "chatcmd" then + item = chatcmd_handler(item) + end + + local parse_tags = {"priv", "note"} + for _, pt in ipairs(parse_tags) do + local tvalues = item.tags[pt] + if tvalues then + local tstring = "" + + local title = tags.get_title(pt) + if title then + tstring = tstring .. "\n\n### " .. title .. ":\n" + end + + for _, tv in ipairs(tvalues) do + tstring = tstring .. "\n- " .. tags.format(pt, tv) + end + + item.description = item.description .. tstring + end + end + end + + register(item) + return default_handler(item) +end diff --git a/.ldoc/config.luadoc b/.ldoc/config.luadoc new file mode 100644 index 0000000..7cb0044 --- /dev/null +++ b/.ldoc/config.luadoc @@ -0,0 +1,55 @@ + +--- World Path Configuration +-- +-- @topic config + + +--- Main configuration file. +-- +-- Registering items, entities, etc. for cleaning can be done in ***cleaner.json*** +-- in the world directory (`/cleaner.json`). If it does not exist +-- it will be created automatically when the server is started. +-- +-- It is formatted as follows: +-- +-- { +-- "entities" : +-- { +-- "remove" : [] +-- }, +-- "items" : +-- { +-- "replace" : {} +-- }, +-- "nodes" : +-- { +-- "remove" : [], +-- "replace" : {} +-- }, +-- "ores" : +-- { +-- "remove" : [] +-- } +-- } +-- +-- `remove` key works for nodes, entities, & ores. `replace` key works for +-- nodes & items. Their functions are self-explanatory. +-- +-- @json cleaner.json +-- @usage +-- Cleaning nodes example: +-- { +-- "nodes" : +-- { +-- "remove" : +-- [ +-- "old:node_1", +-- "old:node_2", +-- ], +-- "replace" : +-- { +-- "old:node_3" : "new:node_1", +-- "old:node_4" : "new:node_2", +-- }, +-- }, +-- } diff --git a/.ldoc/gendoc.sh b/.ldoc/gendoc.sh new file mode 100755 index 0000000..75ef2c3 --- /dev/null +++ b/.ldoc/gendoc.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash + +# place this file in mod ".ldoc" directory + + +d_ldoc="$(dirname $(readlink -f $0))" +f_config="${d_ldoc}/config.ld" + +cd "${d_ldoc}/.." + +d_root="$(pwd)" +d_export="${d_export:-${d_root}/docs/reference}" + +cmd_ldoc="${d_ldoc}/ldoc/ldoc.lua" +if test ! -x "${cmd_ldoc}"; then + cmd_ldoc="ldoc" +fi + +# clean old files +rm -rf "${d_export}" + +vinfo="v$(grep "^version = " "${d_root}/mod.conf" | head -1 | sed -e 's/version = //')" +d_data="${d_export}/${vinfo}/data" + +# generate new doc files +"${cmd_ldoc}" --UNSAFE_NO_SANDBOX --multimodule -c "${f_config}" -d "${d_export}/${vinfo}" "${d_root}"; retval=$? + +# check exit status +if test ${retval} -ne 0; then + echo -e "\nan error occurred (ldoc return code: ${retval})" + exit ${retval} +fi + +# show version info +for html in $(find "${d_export}/${vinfo}" -type f -name "*.html"); do + sed -i -e "s|^

[cC]leaner

$|

Cleaner (${vinfo})

|" "${html}" +done + +# copy textures to data directory +echo -e "\ncopying textures ..." +mkdir -p "${d_data}" +texture_count=0 +for png in $(find "${d_root}/textures" -maxdepth 1 -type f -name "*.png"); do + t_png="${d_data}/$(basename ${png})" + if test -f "${t_png}"; then + echo "WARNING: not overwriting existing file: ${t_png}" + else + cp "${png}" "${d_data}" + texture_count=$((texture_count + 1)) + printf "\rcopied ${texture_count} textures" + fi +done + +echo -e "\n\nDone!" diff --git a/.ldoc/tags.ld b/.ldoc/tags.ld new file mode 100644 index 0000000..91ab6dd --- /dev/null +++ b/.ldoc/tags.ld @@ -0,0 +1,81 @@ + +local tags = {} +local tag_list = {} +local custom_tags = {} + +local register_tag = function(name, tag) + local new_tag = {name, title=tag.title, hidden=tag.hidden, format=tag.format} + table.insert(custom_tags, new_tag) + tag_list[name] = {title=tag.title, format=tag.format} +end + +tags.get_title = function(tname) + local t = tag_list[tname] + if t then + return t.title + end +end + +tags.format = function(tname, value) + local t = tag_list[tname] + if t then + if type(t.format) == "function" then + value = t.format(value) + end + end + + return value +end + + +local new_tags = { + ["priv"] = { + title = "Required Privileges", + hidden = true, + }, + ["note"] = { + title = "Notes", + hidden = true, + format = function(value) + return "*" .. value .. "*" + end, + }, + ["video"] = { + title = "Video", + format = video_frame, + }, + ["youtube"] = { + title = "Video", + format = function(value) + return video_frame("https://www.youtube.com/embed/" .. value) + end, + }, + -- settings + ["settype"] = { + title = "Setting Type", + hidden = true, + }, + ["default"] = { + title = "Default Value", + hidden = true, + }, + -- craft items/tools + ["img"] = { + title = "Image", + format = function(value) + return "" + end, + }, + -- chat commands + ["chatparam"] = { + title = "Parameters", + hidden = true, + }, +} + +for k, v in pairs(new_tags) do + register_tag(k, v) +end + + +return tags, custom_tags diff --git a/LICENSE.txt b/LICENSE.txt index 9042c25..357bcd0 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,13 +1,21 @@ - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - Version 2, December 2004 - - Copyright (C) 2004 Sam Hocevar - - Everyone is permitted to copy and distribute verbatim or modified - copies of this license document, and changing it is allowed as long - as the name is changed. - - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. You just DO WHAT THE FUCK YOU WANT TO. +The MIT License (MIT) + +Copyright © 2021 Jordan Irwin (AntumDeluge) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 77b9f63..70253ef 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,86 @@ -## Clean mod for [Minetest][] +## Cleaner mod for Luanti + +### Description: + +A [Luanti (Minetest)][Luanti] mod that can be used to remove/replace unknown entities, nodes, & items. Originally forked from [PilzAdam's ***clean*** mod][f.pilzadam]. + +![screenshot](screenshot.png) + +### Licensing: + +- Code: [MIT](LICENSE.txt) +- Textures: CC0 + +### Requirements: + +- Luanti minimum version: 5.0 +- Depends: none + +### Usage: + +Registering items, entities, etc. for cleaning can be done in `cleaner.json` in the world directory. If it does not exist it will be created automatically when the server is started. + +It is formatted as follows: +```json +{ + "entities" : + { + "remove" : [] + }, + "items" : + { + "replace" : {} + }, + "nodes" : + { + "remove" : [], + "replace" : {} + }, + "ores" : + { + "remove" : [] + } +} +``` + +Cleaning nodes example: +```json +{ + "nodes" : + { + "remove" : [ + "old:node_1", + "old:node_2", + ], + "replace" : { + "old:node_3" : "new:node_1", + "old:node_4" : "new:node_2", + } + }, +} +``` + +`remove` key works for nodes, entities, & ores. `replace` key works for nodes & items. Their functions are self-explanatory. + +#### Settings: + +``` +cleaner.unsafe +- Enables unsafe methods & commands (remove_ore). +- type: bool +- default: false +``` + +### Links: + +- [![ContentDB](https://content.luanti.org/packages/AntumDeluge/cleaner/shields/title/)][ContentDB] +- [Forum](https://forum.luanti.org/viewtopic.php?t=18381) +- [Git repo](https://github.com/AntumMT/mod-cleaner) +- [Reference](https://antummt.github.io/mod-cleaner/reference/latest/) +- [Changelog](changelog.txt) +- [TODO](TODO.txt) ---- -### **Description:** - -Fork of [PilzAdam's ***clean*** mod][f.pilzadam] for Minetest. - - ---- -### **Licensing:** - -[WTFPL](LICENSE.txt) - - -[Minetest]: http://www.minetest.net/ - -[f.pilzadam]: https://forum.minetest.net/viewtopic.php?t=2777 +[Luanti]: https://luanti.org/ +[f.pilzadam]: https://forum.luanti.org/viewtopic.php?t=2777 +[ContentDB]: https://content.luanti.org/packages/AntumDeluge/cleaner/ diff --git a/TODO.txt b/TODO.txt new file mode 100644 index 0000000..b800544 --- /dev/null +++ b/TODO.txt @@ -0,0 +1,14 @@ + +TODO: +- update world file when chat commands are used +- update inventories when items are replaced: + - creative + - storage (chests, etc.) +- add LBM when removing an item if it is a node +- add "radius" option for pencil or "xlen", "ylen", & "zlen" options +- add "xrotate" & "zrorate" modes for pencil +- don't require "server" priv for "find_unknown_nodes" & "find_neaby_nodes" commands +- add chat command to find nodes with specified attributes +- may be better to update player inventories on login than add aliases for items +- use aliases for unknown nodes instead of LBM +- only use LBM when a node to replace is still registered diff --git a/api.lua b/api.lua new file mode 100644 index 0000000..a894148 --- /dev/null +++ b/api.lua @@ -0,0 +1,194 @@ + +--- Cleaner API +-- +-- @topic api + + +local replace_items = {} +local replace_nodes = {} + + +--- Retrieves list of items to be replaced. +-- +-- @treturn table Items to be replaced. +function cleaner.get_replace_items() + return replace_items +end + +--- Retrieves list of nodes to be replaced. +-- +-- @treturn table Nodes to be replaced. +function cleaner.get_replace_nodes() + return replace_nodes +end + + +--- Registers an entity to be removed. +-- +-- @tparam string src Entity technical name. +function cleaner.register_entity_removal(src) + core.register_entity(":" .. src, { + on_activate = function(self, ...) + self.object:remove() + end, + }) +end + +--- Registers a node to be removed. +-- +-- @tparam string src Node technical name. +function cleaner.register_node_removal(src) + core.register_node(":" .. src, { + groups = {to_remove=1}, + }) +end + +local function update_list(inv, listname, src, tgt) + if not inv then + cleaner.log("error", "cannot update list of unknown inventory") + return + end + + local list = inv:get_list(listname) + if not list then + cleaner.log("warning", "unknown inventory list: " .. listname) + return + end + + for idx, stack in pairs(list) do + if stack:get_name() == src then + local new_stack = ItemStack(tgt) + new_stack:set_count(stack:get_count()) + inv:set_stack(listname, idx, new_stack) + end + end +end + +--- Replaces an item with another registered item. +-- +-- @tparam string src Technical name of item to be replaced. +-- @tparam string tgt Technical name of item to be used in place. +-- @tparam[opt] bool update_players `true` updates inventory lists associated with players (default: `false`). +function cleaner.replace_item(src, tgt, update_players) + update_players = not (update_players ~= true) + + if not core.registered_items[tgt] then + return false, S('Cannot use unknown item "@1" as replacement.', tgt) + end + + if not core.registered_items[src] then + cleaner.log("info", "\"" .. src .. "\" not registered, not unregistering") + else + cleaner.log("warning", "overriding registered item \"" .. src .. "\"") + + core.unregister_item(src) + if core.registered_items[src] then + cleaner.log("error", "could not unregister \"" .. src .. "\"") + end + end + + core.register_alias(src, tgt) + if core.registered_aliases[src] == tgt then + cleaner.log("info", "registered alias \"" .. src .. "\" for \"" .. tgt .. "\"") + else + cleaner.log("error", "could not register alias \"" .. src .. "\" for \"" .. tgt .. "\"") + end + + local bags = core.get_modpath("bags") ~= nil + local armor = core.get_modpath("3d_armor") ~= nil + + -- update player inventories + if update_players then + for _, player in ipairs(core.get_connected_players()) do + local pinv = player:get_inventory() + update_list(pinv, "main", src, tgt) + + if bags then + for i = 1, 4 do + update_list(pinv, "bag" .. i .. "contents", src, tgt) + end + end + + if armor then + local armor_inv = core.get_inventory({type="detached", name=player:get_player_name() .. "_armor"}) + update_list(armor_inv, "armor", src, tgt) + end + end + end + + return true +end + +--- Registeres an item to be replaced. +-- +-- @tparam string src Technical name of item to be replaced. +-- @tparam string tgt Technical name of item to be used in place. +function cleaner.register_item_replacement(src, tgt) + replace_items[src] = tgt +end + +--- Registers a node to be replaced. +-- +-- @tparam string src Technical name of node to be replaced. +-- @tparam string tgt Technical name of node to be used in place. +function cleaner.register_node_replacement(src, tgt) + core.register_node(":" .. src, { + groups = {to_replace=1}, + }) + + replace_nodes[src] = tgt + cleaner.register_item_replacement(src, tgt) +end + + +--- Unsafe Methods. +-- +-- Enabled with [cleaner.unsafe](settings.html#cleaner.unsafe) setting. +-- +-- @section unsafe + + +if cleaner.unsafe then + local remove_ores = {} + + --- Retrieves list of ores to be removed. + -- + -- @treturn table Ores to be removed. + function cleaner.get_remove_ores() + return remove_ores + end + + --- Registers an ore to be removed after server startup. + -- + -- @tparam string src Ore technical name. + function cleaner.register_ore_removal(src) + table.insert(remove_ores, src) + end + + --- Removes an ore definition. + -- + -- @tparam string src Ore technical name. + function cleaner.remove_ore(src) + local remove_ids = {} + local total_removed = 0 + local registered = false + + for id, def in pairs(core.registered_ores) do + if def.ore == src then + table.insert(remove_ids, id) + registered = true + end + end + + for _, id in ipairs(remove_ids) do + core.registered_ores[id] = nil + if core.registered_ores[id] then + cleaner.log("error", "unable to unregister ore " .. id) + else + total_removed = total_removed + 1 + end + end + + return registered, total_removed + end +end diff --git a/changelog.txt b/changelog.txt new file mode 100644 index 0000000..0214081 --- /dev/null +++ b/changelog.txt @@ -0,0 +1,63 @@ + +2025-01-18 +---------- +- fix undeclared global +- added nil check after reading world data file + + +v1.2.1 +---- +- use sounds mod for sounds +- added nil check after reading world data file + + +v1.2 +---- +- added API +- added support for unregistering ores (unsafe) +- added setting for enabling "unsafe" methods & commands +- all types are loaded from /cleaner.json file +- added localization support +- added Spanish localization +- added pencil tool for erasing, adding, & swapping nodes +- added chat commands: + - remove_entities + - remove_nodes + - replace_items + - replace_nodes + - find_unknown_nodes + - find_nearby_nodes + - remove_ores (unsafe) + - ctool (manages wielded cleaner tool settings) + +v1.1 +---- +- uses "register_lbm" with "run_at_every_load" instead of "register_abm" to save resources + - suggested by bell07 ( https://forum.luanti.org/viewtopic.php?p=325519#p325519 ) + +v1.0 +---- +- changed license to MIT +- "clean_entities" & "clean_nodes" files now use json format +- nodes can be replaced with other nodes +- items can be replaced with other items (/clean_items.json file) + +v0.4 +---- +- changed technical name to "cleaner" +- re-added functionality to clean nodes + +v0.3 +---- +- removed functionality for cleaning anything other than entities + +v0.2 +---- +- changed license to CC0 +- added some log output +- entities to be cleaned can be configured & loaded from world directory + +v0.1 +---- +- forked from PilzAdam's "clean" mod @ forum post updated: 2013-06-08 +- replaced deprecated call to "minetest.env" diff --git a/chat.lua b/chat.lua new file mode 100644 index 0000000..3f7cf43 --- /dev/null +++ b/chat.lua @@ -0,0 +1,660 @@ + +--- Cleaner Chat Commands +-- +-- @topic commands + + +local S = core.get_translator(cleaner.modname) + + +local aux = dofile(cleaner.modpath .. "/misc_functions.lua") + +local function pos_list(ppos, radius) + local plist = {} + + for x = ppos.x - radius, ppos.x + radius, 1 do + for y = ppos.y - radius, ppos.y + radius, 1 do + for z = ppos.z - radius, ppos.z + radius, 1 do + table.insert(plist, {x=x, y=y, z=z}) + end + end + end + + return plist +end + +local param_def = { + radius = {name=S("radius"), desc=S("Search radius.")}, + entity = {name=S("entity"), desc=S("Entity technical name.")}, + node = {name=S("node"), desc=S("Node technical name.")}, + old_node = {name=S("old_node"), desc=S("Technical name of node to be replaced.")}, + new_node = {name=S("new_node"), desc=S("Technical name of node to be used in place.")}, + old_item = {name=S("old_item"), desc=S("Technical name of item to be replaced.")}, + new_item = {name=S("new_item"), desc=S("Technical name of item to be used in place.")}, + ore = {name=S("ore"), desc=S("Ore technical name.")}, + action = {name=S("action"), + desc=S('Action to execute. Can be one of "@1", "@2", or "@3".', "status", "setmode", "setnode")}, + value = {name=S("value"), desc=S('Mode or node to be set for tool (not required for "@1" action).', "status")}, +} + +local cmd_repo = { + entity = { + cmd = "remove_entities", + params = {"entity"}, + oparams = {radius=100}, + }, + rem_node = { + cmd = "remove_nodes", + params = {"node"}, + oparams = {radius=5}, + }, + rep_node = { + cmd = "replace_nodes", + params = {"old_node", "new_node"}, + oparams = {radius=5}, + }, + find_node = { + cmd = "find_unknown_nodes", + oparams = {radius=100}, + }, + near_node = { + cmd = "find_nearby_nodes", + oparams = {radius=5}, + }, + item = { + cmd = "replace_items", + params = {"old_item", "new_item"}, + }, + ore = { + cmd = "remove_ores", + params = {"ore"}, + }, + tool = { + cmd = "ctool", + params = {"action", "value"}, + }, + param = { + missing = S("Missing parameter."), + excess = S("Too many parameters."), + mal_radius = S("Radius must be a number."), + }, +} + +for k, def in pairs(cmd_repo) do + if k ~= "param" then + local cmd_help = { + param_string = "", + usage_string = "/" .. def.cmd, + } + + if def.params or def.oparams then + if def.params then + local params = {} + for _, p in ipairs(def.params) do + -- translate + table.insert(params, S(p)) + end + + cmd_help.param_string = "<" .. table.concat(params, "> <") .. ">" + end + end + + if def.oparams then + for k, v in pairs(def.oparams) do + local op = k + if type(op) == "number" then + op = v + end + + cmd_help.param_string = cmd_help.param_string .. " [" .. S(op) .. "]" + end + end + + if cmd_help.param_string ~= "" then + cmd_help.usage_string = cmd_help.usage_string .. " " .. cmd_help.param_string + end + + cmd_repo[k].help = cmd_help + end +end + +local function get_cmd_def(cmd) + for k, v in pairs(cmd_repo) do + if v.cmd == cmd then return v end + end +end + +local function format_usage(cmd) + local def = get_cmd_def(cmd) + if def then + return S("Usage:") .. "\n " .. def.help.usage_string + end +end + +local function format_params(cmd) + local def = get_cmd_def(cmd) + + local param_count + -- FIXME: unused? + local all_params = {} + if def.params then + for _, p in ipairs(def.params) do + table.insert(all_params, p) + end + end + if def.oparams then + for k, v in pairs(def.oparams) do + + end + end + + local retval = "" + local p_count = 0 + + if def.params then + for _, p in ipairs(def.params) do + if p_count == 0 then + retval = retval .. S("Params:") + end + + retval = retval .. "\n " .. S(p) .. ": " .. param_def[p].desc + + p_count = p_count + 1 + end + end + + if def.oparams then + for k, v in pairs(def.oparams) do + if p_count == 0 then + retval = retval .. S("Params:") + end + + local p = k + local dvalue = v + if type(p) == "number" then + p = v + dvalue = nil + end + + retval = retval .. "\n " .. S(p) .. ": " .. param_def[p].desc + if dvalue then + retval = retval .. " (" .. S("default: @1", dvalue) .. ")" + end + + p_count = p_count + 1 + end + end + + return retval +end + +local function format_help(cmd) + return format_usage(cmd) .. "\n\n" .. format_params(cmd) +end + + +local function check_radius(radius, pname) + local is_admin = core.check_player_privs(pname, {server=true}) + + if not is_admin and radius > 10 then + radius = 10 + return radius, S("You do not have permission to set radius that high. Reduced to @1.", radius) + end + + if radius > 100 then + radius = 100 + return radius, S("Radius is too high. Reduced to @1.", radius) + end + + return radius +end + + +--- Removes nearby entities. +-- +-- @chatcmd remove_entities +-- @param entity Entity technical name. +-- @tparam[opt] int radius Search radius (default: 100). +-- @priv server +-- @usage +-- # remove all mobs:horse entities within a radius of 10 nodes +-- /remove_entities mobs:horse 10 +core.register_chatcommand(cmd_repo.entity.cmd, { + privs = {server=true}, + description = S("Remove an entity from game.") .. "\n\n" + .. format_params(cmd_repo.entity.cmd), + params = cmd_repo.entity.help.param_string, + func = function(name, param) + local entity + local radius = cmd_repo.entity.oparams.radius + if param:find(" ") then + entity = param:split(" ") + radius = tonumber(entity[2]) + entity = entity[1] + else + entity = param + end + + local err + if not entity or entity:trim() == "" then + err = cmd_repo.param.missing + elseif not radius then + err = cmd_repo.param.mal_radius + end + + local radius, msg = check_radius(radius, name) + if msg then + core.chat_send_player(name, msg) + end + + if err then + return false, err .. "\n\n" .. format_help(cmd_repo.entity.cmd) + end + + local player = core.get_player_by_name(name) + + local total_removed = 0 + for _, object in ipairs(core.get_objects_inside_radius(player:get_pos(), radius)) do + local lent = object:get_luaentity() + + if lent then + if lent.name == entity then + object:remove() + total_removed = total_removed + 1 + end + else + if object:get_properties().infotext == entity then + object:remove() + total_removed = total_removed + 1 + end + end + end + + return true, S("Removed @1 entities.", total_removed) + end, +}) + +--- Removes nearby nodes. +-- +-- @chatcmd remove_nodes +-- @param node Node technical name. +-- @tparam[opt] int radius Search radius (default: 5). +-- @priv server +-- @usage +-- # remove all default:dirt nodes within a radius of 10 +-- /remove_nodes default:dirt 10 +core.register_chatcommand(cmd_repo.rem_node.cmd, { + privs = {server=true}, + description = S("Remove a node from game.") .. "\n\n" + .. format_params(cmd_repo.rem_node.cmd), + params = cmd_repo.rem_node.help.param_string, + func = function(name, param) + local nname + local radius = cmd_repo.rem_node.oparams.radius + if param:find(" ") then + nname = param:split(" ") + radius = tonumber(nname[2]) + nname = nname[1] + else + nname = param + end + + local err + if not nname or nname:trim() == "" then + err = cmd_repo.param.missing + elseif not radius then + err = cmd_repo.param.mal_radius + end + + local radius, msg = check_radius(radius, name) + if msg then + core.chat_send_player(name, msg) + end + + if err then + return false, err .. "\n\n" .. format_help(cmd_repo.rem_node.cmd) + end + + local ppos = core.get_player_by_name(name):get_pos() + + local total_removed = 0 + for _, npos in ipairs(pos_list(ppos, radius)) do + local node = core.get_node_or_nil(npos) + if node and node.name == nname then + core.remove_node(npos) + total_removed = total_removed + 1 + end + end + + return true, S("Removed @1 nodes.", total_removed) + end, +}) + +--- Replaces an item. +-- +-- @chatcmd replace_items +-- @param old_item Technical name of item to replace. +-- @param new_item Technical name of item to be used in place. +-- @priv server +-- @usage +-- # replace default:sword_wood with default:sword_mese +-- /replace_items default:sword_wood default:sword_mese +core.register_chatcommand(cmd_repo.item.cmd, { + privs = {server=true}, + description = S("Replace an item in game.") .. "\n\n" + .. format_params(cmd_repo.item.cmd), + params = cmd_repo.item.help.param_string, + func = function(name, param) + if not param:find(" ") then + return false, cmd_repo.param.missing .. "\n\n" .. format_help(cmd_repo.item.cmd) + end + + local src = param:split(" ") + local tgt = src[2] + src = src[1] + + local retval, msg = cleaner.replace_item(src, tgt, true) + if not retval then + return false, msg + end + + return true, S("Success!") + end, +}) + +--- Replaces nearby nodes. +-- +-- @chatcmd replace_nodes +-- @param old_node Technical name of node to replace. +-- @param new_node Technical name of node to be used in place. +-- @tparam[opt] int radius Search radius (default: 5). +-- @priv server +-- @usage +-- # replace all default:dirt nodes with default:cobble within a radius of 10 +-- /replace_nodes default:dirt default:cobble 10 +core.register_chatcommand(cmd_repo.rep_node.cmd, { + privs = {server=true}, + description = S("Replace a node in game.") .. "\n\n" + .. format_params(cmd_repo.rep_node.cmd), + params = cmd_repo.rep_node.help.param_string, + func = function(name, param) + local help = format_help(cmd_repo.rep_node.cmd) + + if not param:find(" ") then + return false, cmd_repo.param.missing .. "\n\n" .. help + end + + local radius = cmd_repo.rep_node.oparams.radius + local params = param:split(" ") + + local src = params[1] + local tgt = tostring(params[2]) + if #params > 2 then + radius = tonumber(params[3]) + end + + if not radius then + return false, cmd_repo.param.mal_radius .. "\n\n" .. help + end + + local radius, msg = check_radius(radius, name) + if msg then + core.chat_send_player(name, msg) + end + + if not core.registered_nodes[tgt] then + return false, S('Cannot use unknown node "@1" as replacement.', tgt) + end + + local total_replaced = 0 + local ppos = core.get_player_by_name(name):get_pos() + for _, npos in ipairs(pos_list(ppos, radius)) do + local node = core.get_node_or_nil(npos) + if node and node.name == src then + if core.swap_node(npos, {name=tgt}) then + total_replaced = total_replaced + 1 + else + cleaner.log("error", "could not replace node at " .. core.pos_to_string(npos, 0)) + end + end + end + + return true, S("Replaced @1 nodes.", total_replaced) + end, +}) + +--- Checks for nearby unknown nodes. +-- +-- @chatcmd find_unknown_nodes +-- @tparam[opt] int radius Search radius (default: 100). +-- @priv server +-- @usage +-- # print names of all unknown nodes within radius of 10 +-- /find_unknown_nodes 10 +core.register_chatcommand(cmd_repo.find_node.cmd, { + privs = {server=true}, + description = S("Find names of unknown nodes.") .. "\n\n" + .. format_params(cmd_repo.find_node.cmd), + params = cmd_repo.find_node.help.param_string, + func = function(name, param) + local help = format_help(cmd_repo.find_node.cmd) + + if param:find(" ") then + return false, cmd_repo.param.excess .. "\n\n" .. help + end + + local radius = cmd_repo.find_node.oparams.radius + if param and param:trim() ~= "" then + radius = tonumber(param) + end + + if not radius then + return false, cmd_repo.param.mal_radius .. "\n\n" .. help + end + + local radius, msg = check_radius(radius, name) + if msg then + core.chat_send_player(name, msg) + end + + local ppos = core.get_player_by_name(name):get_pos() + + local checked_nodes = {} + local unknown_nodes = {} + for _, npos in ipairs(pos_list(ppos, radius)) do + local node = core.get_node_or_nil(npos) + if node and not checked_nodes[node.name] then + if not core.registered_nodes[node.name] then + table.insert(unknown_nodes, node.name) + end + + checked_nodes[node.name] = true + end + end + + local node_count = #unknown_nodes + if node_count > 0 then + msg = S("Found unknown nodes: @1", node_count) .. "\n " .. table.concat(unknown_nodes, ", ") + else + msg = S("No unknown nodes found.") + end + + return true, msg + end, +}) + +--- Finds names of nearby nodes. +-- +-- @chatcmd find_nearby_nodes +-- @tparam[opt] int radius Search radius (default: 5). +-- @priv server +-- @usage +-- # print names of all node types found within radius of 10 +-- /find_nearby_nodes 10 +core.register_chatcommand(cmd_repo.near_node.cmd, { + privs = {server=true}, + description = S("Find names of nearby nodes.") .. "\n\n" + .. format_params(cmd_repo.near_node.cmd), + params = cmd_repo.near_node.help.param_string, + func = function(name, param) + local help = format_help(cmd_repo.near_node.cmd) + + if param:find(" ") then + return false, cmd_repo.param.excess .. "\n\n" .. help + end + + local radius = cmd_repo.near_node.oparams.radius + if param and param:trim() ~= "" then + radius = tonumber(param) + end + + if not radius then + return false, cmd_repo.param.mal_radius .. "\n\n" .. help + end + + local radius, msg = check_radius(radius, name) + if msg then + core.chat_send_player(name, msg) + end + + local ppos = core.get_player_by_name(name):get_pos() + + local node_names = {} + for _, npos in ipairs(pos_list(ppos, radius)) do + local node = core.get_node_or_nil(npos) + if node and not node_names[node.name] then + node_names[node.name] = true + end + end + + local found_nodes = {} + for k, _ in pairs(node_names) do + table.insert(found_nodes, k) + end + + local msg + local node_count = #found_nodes + if node_count > 0 then + msg = S("Nearby nodes: @1", node_count) .. "\n " .. table.concat(found_nodes, ", ") + else + msg = S("No nearby nodes found.") + end + + return true, msg + end, +}) + + +--- Unsafe Commands. +-- +-- Enabled with [cleaner.unsafe](settings.html#cleaner.unsafe) setting. +-- +-- @section unsafe + + +if cleaner.unsafe then + --- Registers an ore to be removed. + -- + -- @chatcmd remove_ores + -- @param ore Ore technical name. + -- @priv server + -- @note This action is reverted after server restart. To make changes permanent, + -- use the [cleaner.json](config.html#cleaner.json) config. + -- @usage + -- # remove all registered ores that add default:stone_with_iron to world + -- /remove_ores default:stone_with_iron + core.register_chatcommand(cmd_repo.ore.cmd, { + privs = {server=true}, + description = S("Remove an ore from game.") .. "\n\n" + .. format_params(cmd_repo.ore.cmd), + params = cmd_repo.ore.help.param_string, + func = function(name, param) + local err + if not param or param:trim() == "" then + err = cmd_repo.param.missing + elseif param:find(" ") then + err = cmd_repo.param.excess + end + + if err then + return false, err .. "\n\n" .. format_help(cmd_repo.ore.cmd) + end + + local success = false + local msg + local registered, total_removed = cleaner.remove_ore(param) + + if not registered then + msg = S('Ore "@1" not found, not unregistering.', param) + else + msg = S("Unregistered @1 ores (this will be undone after server restart).", total_removed) + success = true + end + + return success, msg + end + }) +end + +--- @section end + + +--- Manages settings for wielded [cleaner tool](tools.html). +-- +--

Required Privileges:

+-- +-- - server +-- +-- @chatcmd ctool +-- @param action Action to execute. Can be "status", "setmode", or "setnode". +-- @param value Mode or node to be set for tool (not required for "status" action). +-- @usage +-- # while cleaner:pencil is wielded, configure to place default:dirt node when used +-- /ctool setmode write +-- /ctool setnode default:dirt +core.register_chatcommand(cmd_repo.tool.cmd, { + privs = {server=true}, + description = S("Manage settings for wielded cleaner tool.") .. "\n\n" + .. format_params(cmd_repo.tool.cmd), + params = cmd_repo.tool.help.param_string, + func = function(name, param) + local action, value = param + local idx = param:find(" ") + if idx then + param = string.split(param, " ") + action = param[1] + value = param[2] + end + + local help = format_help(cmd_repo.tool.cmd) + + local player = core.get_player_by_name(name) + local stack = player:get_wielded_item() + local iname = aux.tool:format_name(stack) + local imeta = stack:get_meta() + + if iname ~= "cleaner:pencil" then + return false, S("Unrecognized wielded item: @1", iname) .. "\n\n" .. help + end + + if action == "status" then + core.chat_send_player(name, iname .. ": " .. S("mode") .. "=" .. imeta:get_string("mode") + .. ", " .. S("node") .. "=" .. imeta:get_string("node")) + return true + end + + if not action or not value then + return false, S("Missing parameter.") .. "\n\n" .. help + end + + if action == "setmode" then + stack = aux.tool:set_mode(stack, value, name) + elseif action == "setnode" then + stack = aux.tool:set_node(stack, value, name) + else + return false, S("Unrecognized action: @1", action) .. "\n\n" .. help + end + + return player:set_wielded_item(stack) + end, +}) diff --git a/description.txt b/description.txt deleted file mode 100644 index f04ad33..0000000 --- a/description.txt +++ /dev/null @@ -1 +0,0 @@ -A very simple mod that deletes unknown blocks and removes unknown entities. diff --git a/entities.lua b/entities.lua new file mode 100644 index 0000000..7b9da53 --- /dev/null +++ b/entities.lua @@ -0,0 +1,59 @@ + +local aux = dofile(cleaner.modpath .. "/misc_functions.lua") + +-- populate entities list from file in world path +local entities_data = aux.get_world_data().entities + + +-- START: backward compat + +local e_path = core.get_worldpath() .. "/clean_entities.json" +local e_file = io.open(e_path, "r") + +if e_file then + cleaner.log("action", "found deprecated clean_entities.json, updating") + + local data_in = core.parse_json(e_file:read("*a")) + e_file:close() + if data_in and data_in.remove then + for _, r in ipairs(data_in.remove) do + table.insert(entities_data.remove, r) + end + end + + -- don't read deprecated file again + os.rename(e_path, e_path .. ".old") +end + +local e_path_old = core.get_worldpath() .. "/clean_entities.txt" +e_file = io.open(e_path_old, "r") + +if e_file then + cleaner.log("action", "found deprecated clean_entities.txt, converting to json") + + local data_in = string.split(e_file:read("*a"), "\n") + for _, e in ipairs(data_in) do + e = e:trim() + if e ~= "" and e:sub(1, 1) ~= "#" then + table.insert(entities_data.remove, e) + end + end + + e_file:close() + os.rename(e_path_old, e_path_old .. ".bak") -- don't read deprecated file again +end + +-- END: backward compat + + +entities_data.remove = aux.clean_duplicates(entities_data.remove) + +-- update json file with any changes +aux.update_world_data("entities", entities_data) + +core.register_on_mods_loaded(function() + for _, e in ipairs(entities_data.remove) do + cleaner.log("action", "registering entity for removal: " .. e) + cleaner.register_entity_removal(e) + end +end) diff --git a/init.lua b/init.lua index 1cf3b37..71d9ef2 100644 --- a/init.lua +++ b/init.lua @@ -1,28 +1,48 @@ --- clean by PilzAdam --- LICENSE: WTFPL -local old_nodes = {"mod:a", "mod:b"} -local old_entities = {} +cleaner = {} +cleaner.modname = core.get_current_modname() +cleaner.modpath = core.get_modpath(cleaner.modname) -for _,node_name in ipairs(old_nodes) do - minetest.register_node(":"..node_name, { - groups = {old=1}, - }) +local cleaner_debug = core.settings:get_bool("enable_debug_mods", false) + +function cleaner.log(lvl, msg) + if lvl == "debug" and not cleaner_debug then return end + + if lvl and not msg then + msg = lvl + lvl = nil + end + + msg = "[" .. cleaner.modname .. "] " .. msg + if lvl == "debug" then + msg = "[DEBUG] " .. msg + lvl = nil + end + + if not lvl then + core.log(msg) + else + core.log(lvl, msg) + end end -minetest.register_abm({ - nodenames = {"group:old"}, - interval = 1, - chance = 1, - action = function(pos, node) - minetest.env:remove_node(pos) - end, -}) +local aux = dofile(cleaner.modpath .. "/misc_functions.lua") -for _,entity_name in ipairs(old_entities) do - minetest.register_entity(":"..entity_name, { - on_activate = function(self, staticdata) - self.object:remove() - end, - }) +-- initialize world file +aux.update_world_data() + + +local scripts = { + "settings", + "api", + "chat", + "tools", + "entities", + "nodes", + "items", + "ores", +} + +for _, script in ipairs(scripts) do + dofile(cleaner.modpath .. "/" .. script .. ".lua") end diff --git a/items.lua b/items.lua new file mode 100644 index 0000000..02cde65 --- /dev/null +++ b/items.lua @@ -0,0 +1,49 @@ + +local aux = dofile(cleaner.modpath .. "/misc_functions.lua") + +-- populate items list from file in world path +local items_data = aux.get_world_data().items + + +-- START: backward compat + +local i_path = core.get_worldpath() .. "/clean_items.json" +local i_file = io.open(i_path, "r") + +if i_file then + cleaner.log("action", "found deprecated clean_items.json, updating") + + local data_in = core.parse_json(i_file:read("*a")) + i_file:close() + if data_in and data_in.replace then + for k, v in pairs(data_in.replace) do + if not items_data.replace[k] then + items_data.replace[k] = v + end + end + end + + -- don't read deprecated file again + os.rename(i_path, i_path .. ".old") +end + +-- END: backward compat + + +aux.update_world_data("items", items_data) + +for i_old, i_new in pairs(items_data.replace) do + cleaner.register_item_replacement(i_old, i_new) +end + +-- register actions for after server startup +core.register_on_mods_loaded(function() + for i_old, i_new in pairs(cleaner.get_replace_items()) do + cleaner.log("action", "registering item \"" .. i_old .. "\" to be replaced with \"" .. i_new .. "\"") + + local retval, msg = cleaner.replace_item(i_old, i_new) + if not retval then + cleaner.log("warning", msg) + end + end +end) diff --git a/locale/cleaner.es.tr b/locale/cleaner.es.tr new file mode 100644 index 0000000..bbc0007 --- /dev/null +++ b/locale/cleaner.es.tr @@ -0,0 +1,66 @@ +# textdomain:cleaner + +# Translators: Jordan Irwin (AntumDeluge) + + +# chat commands +entity=entidad +mode=modo +node=nodo +radius=radio +old_item=objeto_antiguo +new_item=objeto_nuevo +old_node=nodo_antiguo +new_node=nodo_nuevo +ore=mineral +action=acción +value=valor +Usage:=Uso: +Params:=Parámetros: +default: @1=por defecto: @1 +Search radius.=Radio de búsqueda. +Entity technical name.=Nombre técnico de entidad. +Node technical name.=Nombre técnico de nodo. +Technical name of node to be replaced.=Nombre técnico del nodo reemplazado. +Technical name of node to be used in place.=Nombre técnico del nodo de reemplazo. +Technical name of item to be replaced.=Nombre técnico del objeto reemplazado. +Technical name of item to be used in place.=Nombre técnico del objeto de reemplazo. +Ore technical name.=Nombre técnico de mineral. +Action to execute. Can be one of "@1", "@2", or "@3".=La acción para ejecutar. Puede ser "@1", "@2", o "@3". +Mode or node to be set for tool (not required for "@1" action).=Modo o nodo para configurar a la herramienta (no se requiere para la acción de "@1"). +Remove an entity from game.=Eliminar una entidad del juego. +Remove a node from game.=Eliminar un nodo del juego. +Replace an item in game.=Sustituir un objecto del juego. +Replace a node in game.=Sustituir un nodo del juego. +Find names of unknown nodes.=Descubrir los nombres de nodos desconocidos. +Find names of nearby nodes.=Descubrir los nombres de nodos cercanos. +Remove an ore from game.=Eliminar un mineral del juego. +Missing parameter.=Parámetro extraviado. +Too many parameters.=Demasiados parámetros. +Radius must be a number.=El radio debe ser un número. +Cannot use unknown item "@1" as replacement.=El objeto "@1" es desonocido, no se puede utilizar como sustitución. +Cannot use unknown node "@1" as replacement.=El nodo "@1" es desonocido, no se puede utilizar como sustitución. +Replaced @1 nodes.=Nodos sustituidos: @1 +Removed @1 nodes.=Se eliminaron @1 nodos. +Removed @1 entities.=Se eliminaron @1 entidades. +Found unknown nodes: @1=Se encontraron @1 nodos desconocidos. +No unknown nodes found.=No se encontraron nodos desconocidos. +Nearby nodes: @1=Nodos cercanos: @1 +No nearby nodes found.=No se encontraron nodos cercanos. +Ore "@1" not found, not unregistering.=No se encontró el mineral "@1", se mantiene registrado. +Unregistered @1 ores (this will be undone after server restart).=Se anuló @1 minerales del registro. +Success!=¡Éxito! +Manage settings for wielded cleaner tool.=Administrar a los ajustes de la herramienta cleaner empuñada. +Unrecognized wielded item: @1=Objeto empuñado desconocido: @1 +Unrecognized action: @1=Acción desconocido: @1 +You do not have permission to set radius that high. Reduced to @1.=No tienes permiso para poner al radio tan alto. Se reduce a @1. +Radius is too high. Reduced to @1.=El radio es demasiado alto. Se reduce a @1. + +# tools: +@1: mode set to: @2=@1: modo configurado para: @2 +@1: node set to: @2=@1: nodo configurado para: @2 +Modes for tool "@1" not available.=Modos para herramienta "@1" no disponibles. +You do not have permission to use this item. Missing privs: @1=No tienes permiso para usar este objeto. Privs que faltan: @1 +Unknown mode: @1=Modo desconocido: @1 +Can't place node there.=No se puede poner nodo allí. +Cannot place unknown node: @1=No se puede poner nodo desconocido: @1 diff --git a/locale/template.txt b/locale/template.txt new file mode 100644 index 0000000..5373fb7 --- /dev/null +++ b/locale/template.txt @@ -0,0 +1,66 @@ +# textdomain:cleaner + +# Translators: + + +# chat commands +entity= +mode= +node= +radius= +old_item= +new_item= +old_node= +new_node= +ore= +action= +value= +Usage:= +Params:= +default: @1= +Search radius.= +Entity technical name.= +Node technical name.= +Technical name of node to be replaced.= +Technical name of node to be used in place.= +Technical name of item to be replaced.= +Technical name of item to be used in place.= +Ore technical name.= +Action to execute. Can be one of "@1", "@2", or "@3".= +Mode or node to be set for tool (not required for "@1" action).= +Remove an entity from game.= +Remove a node from game.= +Replace an item in game.= +Replace a node in game.= +Find names of unknown nodes.= +Find names of nearby nodes.= +Remove an ore from game.= +Missing parameter.= +Too many parameters.= +Radius must be a number.= +Cannot use unknown item "@1" as replacement.= +Cannot use unknown node "@1" as replacement.= +Replaced @1 nodes.= +Removed @1 nodes.= +Removed @1 entities.= +Found unknown nodes: @1= +No unknown nodes found.= +Nearby nodes: @1= +No nearby nodes found.= +Ore "@1" not found, not unregistering.= +Unregistered @1 ores (this will be undone after server restart).= +Success!= +Manage settings for wielded cleaner tool.= +Unrecognized wielded item: @1= +Unrecognized action: @1= +You do not have permission to set radius that high. Reduced to @1.= +Radius is too high. Reduced to @1.= + +# tools: +@1: mode set to: @2= +Modes for tool "@1" not available.= +@1: node set to: @2= +You do not have permission to use this item. Missing privs: @1= +Can't place node there.= +Unknown mode: @1= +Cannot place unknown node: @1= diff --git a/misc_functions.lua b/misc_functions.lua new file mode 100644 index 0000000..27c8654 --- /dev/null +++ b/misc_functions.lua @@ -0,0 +1,258 @@ + +local S = core.get_translator(cleaner.modname) + + +--- Cleans duplicate entries from indexed table. +-- +-- @local +-- @function clean_duplicates +-- @tparam table t +-- @treturn table +local function clean_duplicates(t) + local tmp = {} + for _, v in ipairs(t) do + tmp[v] = true + end + + t = {} + for k in pairs(tmp) do + table.insert(t, k) + end + + return t +end + +local world_file = core.get_worldpath() .. "/cleaner.json" + +local function get_world_data() + local wdata = {} + local buffer = io.open(world_file, "r") + if buffer then + local err + wdata, err = core.parse_json(buffer:read("*a"), nil, true) + buffer:close() + if wdata == nil then + cleaner.log("warning", "reading world data file failed: " .. world_file) + wdata = {} + end + end + + local rem_types = {"entities", "nodes", "ores",} + local rep_types = {"items", "nodes",} + + for _, t in ipairs(rem_types) do + wdata[t] = wdata[t] or {} + wdata[t].remove = wdata[t].remove or {} + end + + for _, t in ipairs(rep_types) do + wdata[t] = wdata[t] or {} + wdata[t].replace = wdata[t].replace or {} + end + + return wdata +end + +local function update_world_data(t, data) + local wdata = get_world_data() + if t and data then + wdata[t].remove = data.remove + wdata[t].replace = data.replace + end + + local json_string = core.write_json(wdata, true):gsub("\"remove\" : null", "\"remove\" : []") + :gsub("\"replace\" : null", "\"replace\" : {}") + + local buffer = io.open(world_file, "w") + if buffer then + buffer:write(json_string) + buffer:close() + + return true + end + + return false +end + +local tool = { + modes = { + ["cleaner:pencil"] = {"erase", "write", "swap"}, + }, + + format_name = function(self, stack) + local iname = stack:get_name() + if iname == "cleaner:pencil_1" then + iname = "cleaner:pencil" + end + + return iname + end, + + set_mode = function(self, stack, mode, pname) + local iname = self:format_name(stack) + + if not self.modes[iname] then + if pname then + core.chat_send_player(pname, iname .. ": " .. S("unknown mode: @1", mode)) + end + cleaner.log("warning", iname .. ": unknown mode: " .. mode) + return stack + end + + local imeta = stack:get_meta() + imeta:set_string("mode", mode) + + if pname then + core.chat_send_player(pname, S("@1: mode set to: @2", iname, imeta:get_string("mode"))) + end + + local new_stack + if mode == "erase" then + new_stack = ItemStack("cleaner:pencil_1") + else + new_stack = ItemStack("cleaner:pencil") + end + + local new_meta = new_stack:get_meta() + new_meta:from_table(imeta:to_table()) + + return new_stack + end, + + next_mode = function(self, stack, pname) + local iname = self:format_name(stack) + local modes = self.modes[iname] + + if not modes then + return false, stack, S('Modes for tool "@1" not available.', stack:get_name()) + end + + local imeta = stack:get_meta() + local current_mode = imeta:get_string("mode") + if not current_mode or current_mode:trim() == "" then + return true, self:set_mode(stack, modes[1], pname) + end + + local idx = 1 + for _, m in ipairs(modes) do + if current_mode == m then + break + end + idx = idx + 1 + end + + return true, self:set_mode(stack, modes[idx+1] or modes[1], pname) + end, + + set_node = function(self, stack, node, pname) + local imeta = stack:get_meta() + imeta:set_string("node", node) + + if pname then + core.chat_send_player(pname, S("@1: node set to: @2", stack:get_name(), imeta:get_string("node"))) + end + + return stack + end, +} + +local use_sounds = core.global_exists("sounds") +local sound_handle + +tool.on_use = function(stack, user, pointed_thing) + if not user:is_player() then return end + + local pname = user:get_player_name() + if not core.get_player_privs(pname).server then + core.chat_send_player(pname, S("You do not have permission to use this item. Missing privs: @1", "server")) + return stack + end + + if sound_handle then + core.sound_stop(sound_handle) + sound_handle = nil + end + + if pointed_thing.type == "node" then + local npos = core.get_pointed_thing_position(pointed_thing) + local imeta = stack:get_meta() + local mode = imeta:get_string("mode") + local new_node_name = imeta:get_string("node") + + if mode == "erase" then + core.remove_node(npos) + if use_sounds then + local sound_handle = sounds.pencil_erase({object=user}) + end + return stack + elseif core.registered_nodes[new_node_name] then + if mode == "swap" then + core.swap_node(npos, {name=new_node_name}) + if use_sounds then + local sound_handle = sounds.pencil_write({object=user}) + end + elseif mode == "write" then + local node_above = core.get_node_or_nil(pointed_thing.above) + if not node_above or node_above.name == "air" then + core.set_node(pointed_thing.above, {name=new_node_name}) + if use_sounds then + local sound_handle = sounds.pencil_write({object=user}) + end + else + core.chat_send_player(pname, S("Can't place node there.")) + end + else + core.chat_send_player(pname, S("Unknown mode: @1", mode)) + end + + return stack + end + + core.chat_send_player(pname, S("Cannot place unknown node: @1", new_node_name)) + return stack + end +end + +tool.on_secondary_use = function(stack, user, pointed_thing) + if not user:is_player() then return end + + local pname = user:get_player_name() + if not core.get_player_privs(pname).server then + core.chat_send_player(pname, S("You do not have permission to use this item. Missing privs: @1", "server")) + return stack + end + + local success, stack, msg = tool.next_mode(tool, stack, pname) + if not success then + core.chat_send_player(pname, msg) + end + + return stack +end + +tool.on_place = function(stack, placer, pointed_thing) + if not placer:is_player() then return end + + local pname = placer:get_player_name() + if not core.get_player_privs(pname).server then + core.chat_send_player(pname, S("You do not have permission to use this item. Missing privs: @1", "server")) + return stack + end + + if pointed_thing.type == "node" then + local node = core.get_node_or_nil(core.get_pointed_thing_position(pointed_thing)) + if node then + stack = tool:set_node(stack, node.name, pname) + end + end + + return stack +end + + +return { + clean_duplicates = clean_duplicates, + get_world_data = get_world_data, + update_world_data = update_world_data, + tool = tool, +} diff --git a/mod.conf b/mod.conf index 8cb9512..03b84d4 100644 --- a/mod.conf +++ b/mod.conf @@ -1,3 +1,7 @@ -name = clean -author = PilzAdam -license = WTFPL +name = cleaner +description = A mod that can be used to remove/replace unknown entities, nodes, & items. +version = 2025-01-18 +license = MIT +author = PilzAdam, Jordan Irwin (AntumDeluge) +min_minetest_version = 5.0 +optional_depends = sounds diff --git a/nodes.lua b/nodes.lua new file mode 100644 index 0000000..f3878ab --- /dev/null +++ b/nodes.lua @@ -0,0 +1,97 @@ + +local aux = dofile(cleaner.modpath .. "/misc_functions.lua") + +-- populate nodes list from file in world path +local nodes_data = aux.get_world_data().nodes + + +-- START: backward compat + +local n_path = core.get_worldpath() .. "/clean_nodes.json" +local n_file = io.open(n_path, "r") + +if n_file then + cleaner.log("action", "found deprecated clean_nodes.json, updating") + + local data_in = core.parse_json(n_file:read("*a")) + n_file:close() + if data_in then + if data_in.remove then + for _, r in ipairs(data_in.remove) do + table.insert(nodes_data.remove, r) + end + end + + if data_in.replace then + for k, v in pairs(data_in.replace) do + if not nodes_data.replace[k] then + nodes_data.replace[k] = v + end + end + end + end + + -- don't read deprecated file again + os.rename(n_path, n_path .. ".old") +end + +local n_path_old = core.get_worldpath() .. "/clean_nodes.txt" +n_file = io.open(n_path_old, "r") + +if n_file then + cleaner.log("action", "found deprecated clean_nodes.txt, converting to json") + + local data_in = string.split(n_file:read("*a"), "\n") + for _, e in ipairs(data_in) do + e = e:trim() + if e ~= "" and e:sub(1, 1) ~= "#" then + table.insert(nodes_data.remove, e) + end + end + + n_file:close() + os.rename(n_path_old, n_path_old .. ".old") -- don't read deprecated file again +end + +-- END: backward compat + + +nodes_data.remove = aux.clean_duplicates(nodes_data.remove) + +-- update json file with any changes +aux.update_world_data("nodes", nodes_data) + +core.register_lbm({ + name = "cleaner:remove_nodes", + nodenames = {"group:to_remove"}, + run_at_every_load = true, + action = function(pos, node) + core.remove_node(pos) + end, +}) + +core.register_lbm({ + name = "cleaner:replace_nodes", + nodenames = {"group:to_replace"}, + run_at_every_load = true, + action = function(pos, node) + local new_node_name = cleaner.get_replace_nodes()[node.name] + if core.registered_nodes[new_node_name] then + core.swap_node(pos, {name=new_node_name}) + else + cleaner.log("error", "cannot replace with unregistered node \"" .. tostring(new_node_name) .. "\"") + end + end, +}) + +core.register_on_mods_loaded(function() + for _, n in ipairs(nodes_data.remove) do + cleaner.log("action", "registering node for removal: " .. n) + cleaner.register_node_removal(n) + end + + for n_old, n_new in pairs(nodes_data.replace) do + cleaner.log("action", "registering node \"" .. n_old .. "\" to be replaced with \"" .. n_new .. "\"") + cleaner.register_node_replacement(n_old, n_new) + end +end) diff --git a/ores.lua b/ores.lua new file mode 100644 index 0000000..517f60a --- /dev/null +++ b/ores.lua @@ -0,0 +1,17 @@ + +if not cleaner.unsafe then return end + +local aux = dofile(cleaner.modpath .. "/misc_functions.lua") + +local ores_data = aux.get_world_data().ores + +for _, ore in ipairs(ores_data.remove) do + cleaner.register_ore_removal(ore) +end + +core.register_on_mods_loaded(function() + for _, ore in ipairs(cleaner.get_remove_ores()) do + cleaner.log("action", "unregistering ore: " .. ore) + cleaner.remove_ore(ore) + end +end) diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..639655d Binary files /dev/null and b/screenshot.png differ diff --git a/set_version.py b/set_version.py new file mode 100755 index 0000000..8c40217 --- /dev/null +++ b/set_version.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python + +import sys, os, codecs + + +f_script = os.path.realpath(__file__) +d_root = os.path.dirname(f_script) + +os.chdir(d_root) + +args = sys.argv[1:] +if len(args) < 1: + print("ERROR: must supply version as parameter") + sys.exit(1) + +new_version = args[0] + +to_update = { + "mod.conf": "version =", + "changelog.txt": "next", + os.path.normpath(".ldoc/config.ld"): "local version =", +} + +for f in to_update: + f_path = os.path.join(d_root, f) + if not os.path.isfile(f_path): + print("WARNING: {} not found, skipping ...".format(f)) + continue + + print("\nsetting version to {} in {}".format(new_version, f_path)) + + buffer = codecs.open(f_path, "r", "utf-8") + if not buffer: + print("WARNING: could not open {} for reading, skipping ...".format(f)) + continue + + read_in = buffer.read() + buffer.close() + + read_in = read_in.replace("\r\n", "\n").replace("\r", "\n") + replacement = to_update[f] + new_lines = [] + + version_set = False + for li in read_in.split("\n"): + if not version_set: + if "=" in replacement and li.startswith(replacement): + key = li.split(" = ")[0] + li = "{} = {}".format(key, new_version) + version_set = True + elif li == replacement: + li = "v{}".format(new_version) + version_set = True + + new_lines.append(li) + + write_out = "\n".join(new_lines) + if write_out == read_in: + print("no changes for {}, skipping ...".format(f)) + continue + + buffer = codecs.open(f_path, "w", "utf-8") + if not buffer: + print("WARNING: could not open {} for writing, skipping ...".format(f)) + continue + + buffer.write("\n".join(new_lines)) + buffer.close() + + print("done") diff --git a/settings.lua b/settings.lua new file mode 100644 index 0000000..16c2f42 --- /dev/null +++ b/settings.lua @@ -0,0 +1,15 @@ + +--- Cleaner Settings +-- +-- @topic settings + + +--- Enables unsafe methods & chat commands. +-- +-- - `cleaner.remove_ore` +-- - `/remove_ores` +-- +-- @setting cleaner.unsafe +-- @settype bool +-- @default false +cleaner.unsafe = core.settings:get_bool("cleaner.unsafe", false) diff --git a/settingtypes.txt b/settingtypes.txt new file mode 100644 index 0000000..b5debc7 --- /dev/null +++ b/settingtypes.txt @@ -0,0 +1,6 @@ + +# Enables unsafe methods & chat commands. +# +# - cleaner.remove_ore +# - /remove_ores +cleaner.unsafe (Enable unsafe methods) bool false diff --git a/textures/cleaner_pencil.png b/textures/cleaner_pencil.png new file mode 100644 index 0000000..3582687 Binary files /dev/null and b/textures/cleaner_pencil.png differ diff --git a/tools.lua b/tools.lua new file mode 100644 index 0000000..cc1c405 --- /dev/null +++ b/tools.lua @@ -0,0 +1,44 @@ + +--- Cleaner Tools +-- +-- @topic tools + + +local S = core.get_translator(cleaner.modname) + + +local aux = dofile(cleaner.modpath .. "/misc_functions.lua") + +--- Master Pencil +-- +-- @tool cleaner:pencil +-- @img cleaner_pencil.png +-- @priv server +-- @usage +-- place (right-click): +-- - when not pointing at a node, changes modes +-- - when pointing at a node, sets node to be used +-- +-- use (left-click): +-- - executes action for current mode: +-- - erase: erases pointed node +-- - write: adds node +-- - swap: replaces pointed node +core.register_tool(cleaner.modname .. ":pencil", { + description = S("Master Pencil"), + inventory_image = "cleaner_pencil.png", + liquids_pointable = true, + on_use = aux.tool.on_use, + on_secondary_use = aux.tool.on_secondary_use, + on_place = aux.tool.on_place, +}) + +core.register_tool(cleaner.modname .. ":pencil_1", { + description = S("Master Pencil"), + inventory_image = "cleaner_pencil.png^[transformFXFY", + liquids_pointable = true, + groups = {not_in_creative_inventory=1}, + on_use = aux.tool.on_use, + on_secondary_use = aux.tool.on_secondary_use, + on_place = aux.tool.on_place, +})