From c0b40266c833961a0b7ce7b0f3cb16c7d0077524 Mon Sep 17 00:00:00 2001 From: Starbeamrainbowlabs Date: Thu, 14 May 2020 21:37:27 +0100 Subject: [PATCH] //replacemix: add command --- README.md | 52 ++++++++++- worldeditadditions/init.lua | 1 + worldeditadditions/lib/replacemix.lua | 56 +++++++++++ worldeditadditions/utils/strings.lua | 27 +++++- .../commands/replacemix.lua | 93 +++++++++++++++++++ worldeditadditions_commands/init.lua | 1 + 6 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 worldeditadditions/lib/replacemix.lua create mode 100644 worldeditadditions_commands/commands/replacemix.lua diff --git a/README.md b/README.md index 2b02288..b616ab0 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,9 @@ If you can dream of it, it probably belongs here! - [`//maze3d [ [ [ []]]]`](#maze3d-replace_node-seed) - [`//bonemeal [ []]`](#bonemeal-strength-chance) - [`//walls `](#walls-replace_node) + - [`//replacemix [] [] [ []] [ []] ....`] - [`//count`](#count) - - [`//multi .....`](#multi-command_a-command_b-command_c-) + - [`//multi ....`](#multi-command_a-command_b-command_c-) - [`//y`](#y) - [`//n`](#n) @@ -141,6 +142,55 @@ Creates vertical walls of `` around the inside edges of the define //walls goldblock ``` +### `//replacemix [] [] [ []] [ []] ...` +Replaces a given node with a random mix of other nodes. Functions like `//mix`. + +This command is best explained with examples: + +``` +//replacemix dirt stone +``` + +The above functions just like `//replace` - nothing special going on here. It replaces all `dirt` nodes with `stone`. + +Let's make it more interesting: + +``` +//replacemix dirt 5 stone +``` + +The above replaces 1 in every 5 `dirt` nodes with `stone`. Let's get even fancier: + +``` +//replacemix stone stone_with_diamond stone_with_gold +``` + +The above replaces `stone` nodes with a random mix of `stone_with_diamond` and `stone_with_gold` nodes. But wait - there's more! + +``` +//replacemix stone stone_with_diamond stone_with_gold 4 +``` + +The above replaces `stone` nodes with a random mix of `stone_with_diamond` and `stone_with_gold` nodes as before, but this time in the ratio 1:4 (i.e. for every `stone_with_diamond` node there will be 4 `stone_with_gold` nodes). Note that the `1` for `stone_with_diamond` is implicit there. + +If we wanted to put all of the above features together into a single command, then we might do this: + +``` +//replacemix dirt 3 sandstone 10 dry_dirt cobble 2 +``` + +The above replaces 1 in 3 `dirt` nodes with a mix of `sandstone`, `dry_dirt`, and `cobble` nodes in the ratio 10:1:2. Awesome! + +Here are all the above examples together: + +``` +//replacemix dirt stone +//replacemix dirt 5 stone +//replacemix stone stone_with_diamond stone_with_gold +//replacemix stone stone_with_diamond stone_with_gold 4 +//replacemix dirt 3 sandstone 10 dry_dirt cobble 2 +``` + ### `//count` Counts all the nodes in the defined region and returns the result along with calculated percentages (note that if the chat window used a monospace font, the returned result would be a perfect table. If someone has a ~~hack~~ solution to make the columns line up neatly, please [open an issue](https://github.com/sbrl/Minetest-WorldEditAdditions/issues/new) :D) diff --git a/worldeditadditions/init.lua b/worldeditadditions/init.lua index 5062784..5ca38c1 100644 --- a/worldeditadditions/init.lua +++ b/worldeditadditions/init.lua @@ -16,6 +16,7 @@ dofile(worldeditadditions.modpath.."/lib/overlay.lua") dofile(worldeditadditions.modpath.."/lib/ellipsoid.lua") dofile(worldeditadditions.modpath.."/lib/torus.lua") dofile(worldeditadditions.modpath.."/lib/walls.lua") +dofile(worldeditadditions.modpath.."/lib/replacemix.lua") dofile(worldeditadditions.modpath.."/lib/maze2d.lua") dofile(worldeditadditions.modpath.."/lib/maze3d.lua") diff --git a/worldeditadditions/lib/replacemix.lua b/worldeditadditions/lib/replacemix.lua new file mode 100644 index 0000000..439494d --- /dev/null +++ b/worldeditadditions/lib/replacemix.lua @@ -0,0 +1,56 @@ +--- Like //mix, but replaces a given node instead. +-- @module worldeditadditions.replacemix + +-- ██████ ███████ ██████ ██ █████ ██████ ███████ ███ ███ ██ ██ ██ +-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ████ ██ ██ ██ +-- ██████ █████ ██████ ██ ███████ ██ █████ ██ ████ ██ ██ ███ +-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +-- ██ ██ ███████ ██ ███████ ██ ██ ██████ ███████ ██ ██ ██ ██ ██ +function worldeditadditions.replacemix(pos1, pos2, target_node, target_node_chance, replacements) + pos1, pos2 = worldedit.sort_pos(pos1, pos2) + -- pos2 will always have the highest co-ordinates now + + -- Fetch the nodes in the specified area + local manip, area = worldedit.manip_helpers.init(pos1, pos2) + local data = manip:get_data() + + local node_id_target = minetest.get_content_id(target_node) + + -- Initialise statistics + local changed = 0 + local candidates = 0 + local distribution = {} + + -- Generate the list of node ids + local node_ids_replace = {} + for node_name, weight in pairs(replacements) do + local next_id = minetest.get_content_id(node_name) + for i = 1, weight do + table.insert( + node_ids_replace, + next_id + ) + end + distribution[next_id] = 0 + end + local node_ids_replace_count = #node_ids_replace + + for i in area:iterp(pos1, pos2) do + + if data[i] == node_id_target then + candidates = candidates + 1 + if math.random(target_node_chance) == 1 then + local next_id = node_ids_replace[math.random(node_ids_replace_count)] + data[i] = next_id + changed = changed + 1 + -- We initialised this above + distribution[next_id] = distribution[next_id] + 1 + end + end + end + + -- Save the modified nodes back to disk & return + worldedit.manip_helpers.finish(manip, data) + + return true, changed, candidates, distribution +end diff --git a/worldeditadditions/utils/strings.lua b/worldeditadditions/utils/strings.lua index 6d306eb..a57179c 100644 --- a/worldeditadditions/utils/strings.lua +++ b/worldeditadditions/utils/strings.lua @@ -73,7 +73,27 @@ function worldeditadditions.str_padstart(str, len, char) return string.rep(char, len - #str) .. str end -function worldeditadditions.make_ascii_table(data) +--- Turns an associative node_id → count table into a human-readable list. +-- Works well with worldeditadditions.make_ascii_table(). +function worldeditadditions.node_distribution_to_list(distribution, nodes_total) + local distribution_data = {} + for node_id, count in pairs(distribution) do + table.insert(distribution_data, { + count, + tostring(worldeditadditions.round((count / nodes_total) * 100, 2)).."%", + minetest.get_name_from_content_id(node_id) + }) + end + return distribution_data +end + +--- Makes a human-readable table of data. +-- Data should be a 2D array - i.e. a table of tables. The nested tables should +-- contain a list of items for a single row. +-- If total is specified, then a grand total is printed at the bottom - this is +-- useful when you want to print a node list - works well with +-- worldeditadditions.node_distribution_to_list(). +function worldeditadditions.make_ascii_table(data, total) local extra_padding = 2 local result = {} local max_lengths = {} @@ -94,6 +114,11 @@ function worldeditadditions.make_ascii_table(data) result[#result+1] = table.concat(row_result, "") end + if total then + result[#result+1] = string.rep("=", 6 + #tostring(total) + 6).."\n".. + "Total "..total.." nodes\n" + end + -- TODO: Add multi-column support here return table.concat(result, "\n") end diff --git a/worldeditadditions_commands/commands/replacemix.lua b/worldeditadditions_commands/commands/replacemix.lua new file mode 100644 index 0000000..60ff8bd --- /dev/null +++ b/worldeditadditions_commands/commands/replacemix.lua @@ -0,0 +1,93 @@ +local we_c = worldeditadditions_commands + +-- ██████ ███████ ██████ ██ █████ ██████ ███████ ███ ███ ██ ██ ██ +-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ████ ██ ██ ██ +-- ██████ █████ ██████ ██ ███████ ██ █████ ██ ████ ██ ██ ███ +-- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +-- ██ ██ ███████ ██ ███████ ██ ██ ██████ ███████ ██ ██ ██ ██ ██ +worldedit.register_command("replacemix", { + params = " [] [] [ []] [ []] ...", + description = "Replaces target_node with a mix of other nodes. Functions simmilarly to //mix. is optional and the chance to replace the target node at all. replace_node_a is the node to replace target_node with. If multiple nodes are specified in a space separated list, then when replacing an instance of target_node one is randomly chosen from the list. Just like with //mix, if a positive integer is present after a replace_node, that adds a weighting to that particular node making it more common.", + privs = { worldedit = true }, + require_pos = 2, + parse = function(params_text) + if not params_text or params_text == "" then + return false, "Error: No arguments specified" + end + + local parts = worldeditadditions.split(params_text, "%s+", false) + + local target_node = nil + local target_node_chance = 1 + local replace_nodes = {} + + local mode = "target_node" + local last_node_name = nil + for i, part in ipairs(parts) do + if mode == "target_node" then + target_node = worldedit.normalize_nodename(part) + if not target_node then + return false, "Error: Invalid target_node name" + end + mode = "target_chance" + elseif mode == "target_chance" and tonumber(part) then + target_node_chance = tonumber(part) + mode = "replace_node" + elseif (mode == "target_chance" and not tonumber(part)) or mode == "replace_node" then + mode = "replace_node" + if tonumber(part) then + if not last_node_name then + return false, "Error: No previous node name was found (this is a probably a bug)." + end + replace_nodes[last_node_name] = math.floor(tonumber(part)) + else + if last_node_name and not replace_nodes[last_node_name] then + replace_nodes[last_node_name] = 1 + end + + local node_name = worldedit.normalize_nodename(part) + if not node_name then + return false, "Error: Invalid replace node name '"..part.."'." + end + last_node_name = node_name + end + end + end + if not replace_nodes[last_node_name] then + replace_nodes[last_node_name] = 1 + end + + -- We unconditionally math.floor here because when we tried to test for it directly it was unreliable + return true, target_node, target_node_chance, replace_nodes + end, + nodes_needed = function(name) -- target_node, target_node_chance, replace_nodes + return worldedit.volume(worldedit.pos1[name], worldedit.pos2[name]) + end, + func = function(name, target_node, target_node_chance, replace_nodes) + local start_time = os.clock() + + local success, changed, candidates, distribution = worldeditadditions.replacemix( + worldedit.pos1[name], worldedit.pos2[name], + target_node, + target_node_chance, + replace_nodes + ) + if not success then + return success, changed + end + + local nodes_total = worldedit.volume(worldedit.pos1[name], worldedit.pos2[name]) + local percentage_replaced = worldeditadditions.round((changed / candidates)*100, 2) + local distribution_table = worldeditadditions.make_ascii_table( + worldeditadditions.node_distribution_to_list(distribution, changed), + changed + ) + + local time_taken = os.clock() - start_time + + + minetest.log("action", name .. " used //replacemix at "..worldeditadditions.vector.tostring(worldedit.pos1[name]).." - "..worldeditadditions.vector.tostring(worldedit.pos2[name])..", replacing " .. changed.." nodes (out of "..nodes_total.." nodes) in "..time_taken.."s") + + return true, distribution_table..changed.." out of "..candidates.." (~"..percentage_replaced.."%) candidates replaced in "..time_taken.."s" + end +}) diff --git a/worldeditadditions_commands/init.lua b/worldeditadditions_commands/init.lua index d98b9f0..b052f08 100644 --- a/worldeditadditions_commands/init.lua +++ b/worldeditadditions_commands/init.lua @@ -23,6 +23,7 @@ dofile(we_c.modpath.."/commands/ellipsoid.lua") dofile(we_c.modpath.."/commands/torus.lua") dofile(we_c.modpath.."/commands/walls.lua") dofile(we_c.modpath.."/commands/maze.lua") +dofile(we_c.modpath.."/commands/replacemix.lua") dofile(we_c.modpath.."/commands/count.lua")