]> git.lizzy.rs Git - worldedit.git/blob - worldedit_commands/init.lua
Improve node name normalization again
[worldedit.git] / worldedit_commands / init.lua
1 minetest.register_privilege("worldedit", "Can use WorldEdit commands")\r
2 \r
3 worldedit.pos1 = {}\r
4 worldedit.pos2 = {}\r
5 \r
6 worldedit.set_pos = {}\r
7 worldedit.inspect = {}\r
8 worldedit.prob_pos = {}\r
9 worldedit.prob_list = {}\r
10 \r
11 \r
12 \r
13 local safe_region, reset_pending = dofile(minetest.get_modpath("worldedit_commands") .. "/safe.lua")\r
14 \r
15 function worldedit.player_notify(name, message)\r
16         minetest.chat_send_player(name, "WorldEdit -!- " .. message, false)\r
17 end\r
18 \r
19 worldedit.registered_commands = {}\r
20 \r
21 local function chatcommand_handler(cmd_name, name, param)\r
22         local def = assert(worldedit.registered_commands[cmd_name])\r
23 \r
24         if def.require_pos == 2 then\r
25                 local pos1, pos2 = worldedit.pos1[name], worldedit.pos2[name]\r
26                 if pos1 == nil or pos2 == nil then\r
27                         worldedit.player_notify(name, "no region selected")\r
28                         return\r
29                 end\r
30         elseif def.require_pos == 1 then\r
31                 local pos1 = worldedit.pos1[name]\r
32                 if pos1 == nil then\r
33                         worldedit.player_notify(name, "no position 1 selected")\r
34                         return\r
35                 end\r
36         end\r
37 \r
38         local parsed = {def.parse(param)}\r
39         local success = table.remove(parsed, 1)\r
40         if not success then\r
41                 worldedit.player_notify(name, parsed[1] or "invalid usage")\r
42                 return\r
43         end\r
44 \r
45         if def.nodes_needed then\r
46                 local count = def.nodes_needed(name, unpack(parsed))\r
47                 safe_region(name, count, function()\r
48                         local success, msg = def.func(name, unpack(parsed))\r
49                         if msg then\r
50                                 minetest.chat_send_player(name, msg)\r
51                         end\r
52                 end)\r
53         else\r
54                 -- no "safe region" check\r
55                 local success, msg = def.func(name, unpack(parsed))\r
56                 if msg then\r
57                         minetest.chat_send_player(name, msg)\r
58                 end\r
59         end\r
60 end\r
61 \r
62 -- Registers a chatcommand for WorldEdit\r
63 -- name = "about" -- Name of the chat command (without any /)\r
64 -- def = {\r
65 --     privs = {}, -- Privileges needed\r
66 --     params = "", -- Human readable parameter list (optional)\r
67 --         -- setting params = "" will automatically provide a parse() if not given \r
68 --     description = "", -- Description\r
69 --     require_pos = 0, -- Number of positions required to be set (optional)\r
70 --     parse = function(param)\r
71 --         return true, foo, bar, ...\r
72 --         -- or\r
73 --         return false\r
74 --         -- or\r
75 --         return false, "error message"\r
76 --     end,\r
77 --     nodes_needed = function(name, foo, bar, ...), -- (optional)\r
78 --         return n\r
79 --     end,\r
80 --     func = function(name, foo, bar, ...)\r
81 --         return success, "message"\r
82 --     end,\r
83 -- }\r
84 function worldedit.register_command(name, def)\r
85         local def = table.copy(def)\r
86         assert(name and #name > 0)\r
87         assert(def.privs)\r
88         def.require_pos = def.require_pos or 0\r
89         assert(def.require_pos >= 0 and def.require_pos < 3)\r
90         if def.params == "" and not def.parse then\r
91                 def.parse = function(param) return true end\r
92         else\r
93                 assert(def.parse)\r
94         end\r
95         assert(def.nodes_needed == nil or type(def.nodes_needed) == "function")\r
96         assert(def.func)\r
97 \r
98         -- for development\r
99         --[[if def.require_pos == 2 and not def.nodes_needed then\r
100                 minetest.log("warning", "//" .. name .. " might be missing nodes_needed")\r
101         end--]]\r
102 \r
103         minetest.register_chatcommand("/" .. name, {\r
104                 privs = def.privs,\r
105                 params = def.params,\r
106                 description = def.description,\r
107                 func = function(player_name, param)\r
108                         return chatcommand_handler(name, player_name, param)\r
109                 end,\r
110         })\r
111         worldedit.registered_commands[name] = def\r
112 end\r
113 \r
114 \r
115 \r
116 dofile(minetest.get_modpath("worldedit_commands") .. "/cuboid.lua")\r
117 dofile(minetest.get_modpath("worldedit_commands") .. "/mark.lua")\r
118 dofile(minetest.get_modpath("worldedit_commands") .. "/wand.lua")\r
119 \r
120 \r
121 local function check_region(name)\r
122         return worldedit.volume(worldedit.pos1[name], worldedit.pos2[name])\r
123 end\r
124 \r
125 -- Strips any kind of escape codes (translation, colors) from a string\r
126 -- https://github.com/minetest/minetest/blob/53dd7819277c53954d1298dfffa5287c306db8d0/src/util/string.cpp#L777\r
127 local function strip_escapes(input)\r
128         local s = function(idx) return input:sub(idx, idx) end\r
129         local out = ""\r
130         local i = 1\r
131         while i <= #input do\r
132                 if s(i) == "\027" then -- escape sequence\r
133                         i = i + 1\r
134                         if s(i) == "(" then -- enclosed\r
135                                 i = i + 1\r
136                                 while i <= #input and s(i) ~= ")" do\r
137                                         if s(i) == "\\" then\r
138                                                 i = i + 2\r
139                                         else\r
140                                                 i = i + 1\r
141                                         end\r
142                                 end\r
143                         end\r
144                 else\r
145                         out = out .. s(i)\r
146                 end\r
147                 i = i + 1\r
148         end\r
149         --print(("%q -> %q"):format(input, out))\r
150         return out\r
151 end\r
152 \r
153 local function string_endswith(full, part)\r
154         return full:find(part, 1, true) == #full - #part + 1\r
155 end\r
156 \r
157 local description_cache = nil\r
158 \r
159 -- normalizes node "description" `nodename`, returning a string (or nil)\r
160 worldedit.normalize_nodename = function(nodename)\r
161         nodename = nodename:gsub("^%s*(.-)%s*$", "%1") -- strip spaces\r
162         if nodename == "" then return nil end\r
163 \r
164         local fullname = ItemStack({name=nodename}):get_name() -- resolve aliases\r
165         if minetest.registered_nodes[fullname] or fullname == "air" then -- full name\r
166                 return fullname\r
167         end\r
168         nodename = nodename:lower()\r
169 \r
170         for key, _ in pairs(minetest.registered_nodes) do\r
171                 if string_endswith(key:lower(), ":" .. nodename) then -- matches name (w/o mod part)\r
172                         return key\r
173                 end\r
174         end\r
175 \r
176         if description_cache == nil then\r
177                 -- cache stripped descriptions\r
178                 description_cache = {}\r
179                 for key, value in pairs(minetest.registered_nodes) do\r
180                         local desc = strip_escapes(value.description):gsub("\n.*", "", 1):lower()\r
181                         if desc ~= "" then\r
182                                 description_cache[key] = desc\r
183                         end\r
184                 end\r
185         end\r
186 \r
187         for key, desc in pairs(description_cache) do\r
188                 if desc == nodename then -- matches description\r
189                         return key\r
190                 end\r
191         end\r
192         for key, desc in pairs(description_cache) do\r
193                 if desc == nodename .. " block" then\r
194                         -- fuzzy description match (e.g. "Steel" == "Steel Block")\r
195                         return key\r
196                 end\r
197         end\r
198 \r
199         local match = nil\r
200         for key, value in pairs(description_cache) do\r
201                 if value:find(nodename, 1, true) ~= nil then\r
202                         if match ~= nil then\r
203                                 return nil\r
204                         end\r
205                         match = key -- substring description match (only if no ambiguities)\r
206                 end\r
207         end\r
208         return match\r
209 end\r
210 \r
211 -- Determines the axis in which a player is facing, returning an axis ("x", "y", or "z") and the sign (1 or -1)\r
212 function worldedit.player_axis(name)\r
213         local dir = minetest.get_player_by_name(name):get_look_dir()\r
214         local x, y, z = math.abs(dir.x), math.abs(dir.y), math.abs(dir.z)\r
215         if x > y then\r
216                 if x > z then\r
217                         return "x", dir.x > 0 and 1 or -1\r
218                 end\r
219         elseif y > z then\r
220                 return "y", dir.y > 0 and 1 or -1\r
221         end\r
222         return "z", dir.z > 0 and 1 or -1\r
223 end\r
224 \r
225 local function check_filename(name)\r
226         return name:find("^[%w%s%^&'@{}%[%],%$=!%-#%(%)%%%.%+~_]+$") ~= nil\r
227 end\r
228 \r
229 \r
230 worldedit.register_command("about", {\r
231         privs = {},\r
232         params = "",\r
233         description = "Get information about the WorldEdit mod",\r
234         func = function(name)\r
235                 worldedit.player_notify(name, "WorldEdit " .. worldedit.version_string..\r
236                         " is available on this server. Type //help to get a list of "..\r
237                         "commands, or get more information at "..\r
238                         "https://github.com/Uberi/Minetest-WorldEdit")\r
239         end,\r
240 })\r
241 \r
242 -- mostly copied from builtin/chatcommands.lua with minor modifications\r
243 worldedit.register_command("help", {\r
244         privs = {},\r
245         params = "[all/<cmd>]",\r
246         description = "Get help for WorldEdit commands",\r
247         parse = function(param)\r
248                 return true, param\r
249         end,\r
250         func = function(name, param)\r
251                 local function format_help_line(cmd, def)\r
252                         local msg = minetest.colorize("#00ffff", "//"..cmd)\r
253                         if def.params and def.params ~= "" then\r
254                                 msg = msg .. " " .. def.params\r
255                         end\r
256                         if def.description and def.description ~= "" then\r
257                                 msg = msg .. ": " .. def.description\r
258                         end\r
259                         return msg\r
260                 end\r
261 \r
262                 if not minetest.check_player_privs(name, "worldedit") then\r
263                         return false, "You are not allowed to use any WorldEdit commands."\r
264                 end\r
265                 if param == "" then\r
266                         local msg = ""\r
267                         local cmds = {}\r
268                         for cmd, def in pairs(worldedit.registered_commands) do\r
269                                 if minetest.check_player_privs(name, def.privs) then\r
270                                         cmds[#cmds + 1] = cmd\r
271                                 end\r
272                         end\r
273                         table.sort(cmds)\r
274                         return true, "Available commands: " .. table.concat(cmds, " ") .. "\n"\r
275                                         .. "Use '//help <cmd>' to get more information,"\r
276                                         .. " or '//help all' to list everything."\r
277                 elseif param == "all" then\r
278                         local cmds = {}\r
279                         for cmd, def in pairs(worldedit.registered_commands) do\r
280                                 if minetest.check_player_privs(name, def.privs) then\r
281                                         cmds[#cmds + 1] = format_help_line(cmd, def)\r
282                                 end\r
283                         end\r
284                         table.sort(cmds)\r
285                         return true, "Available commands:\n"..table.concat(cmds, "\n")\r
286                 else\r
287                         local def = worldedit.registered_commands[param]\r
288                         if not def then\r
289                                 return false, "Command not available: " .. param\r
290                         else\r
291                                 return true, format_help_line(param, def)\r
292                         end\r
293                 end\r
294         end,\r
295 })\r
296 \r
297 worldedit.register_command("inspect", {\r
298         params = "[on/off/1/0/true/false/yes/no/enable/disable]",\r
299         description = "Enable or disable node inspection",\r
300         privs = {worldedit=true},\r
301         parse = function(param)\r
302                 if param == "on" or param == "1" or param == "true" or param == "yes" or param == "enable" or param == "" then\r
303                         return true, true\r
304                 elseif param == "off" or param == "0" or param == "false" or param == "no" or param == "disable" then\r
305                         return true, false\r
306                 end\r
307                 return false\r
308         end,\r
309         func = function(name, enable)\r
310                 if enable then\r
311                         worldedit.inspect[name] = true\r
312                         local axis, sign = worldedit.player_axis(name)\r
313                         worldedit.player_notify(name, string.format("inspector: inspection enabled for %s, currently facing the %s axis",\r
314                                 name, axis .. (sign > 0 and "+" or "-")))\r
315                 else\r
316                         worldedit.inspect[name] = nil\r
317                         worldedit.player_notify(name, "inspector: inspection disabled")\r
318                 end\r
319         end,\r
320 })\r
321 \r
322 local function get_node_rlight(pos)\r
323         local vecs = { -- neighboring nodes\r
324                 {x= 1, y= 0, z= 0},\r
325                 {x=-1, y= 0, z= 0},\r
326                 {x= 0, y= 1, z= 0},\r
327                 {x= 0, y=-1, z= 0},\r
328                 {x= 0, y= 0, z= 1},\r
329                 {x= 0, y= 0, z=-1},\r
330         }\r
331         local ret = 0\r
332         for _, v in ipairs(vecs) do\r
333                 ret = math.max(ret, minetest.get_node_light(vector.add(pos, v)))\r
334         end\r
335         return ret\r
336 end\r
337 \r
338 minetest.register_on_punchnode(function(pos, node, puncher)\r
339         local name = puncher:get_player_name()\r
340         if worldedit.inspect[name] then\r
341                 local axis, sign = worldedit.player_axis(name)\r
342                 local message = string.format("inspector: %s at %s (param1=%d, param2=%d, received light=%d) punched facing the %s axis",\r
343                         node.name, minetest.pos_to_string(pos), node.param1, node.param2, get_node_rlight(pos), axis .. (sign > 0 and "+" or "-"))\r
344                 worldedit.player_notify(name, message)\r
345         end\r
346 end)\r
347 \r
348 worldedit.register_command("reset", {\r
349         params = "",\r
350         description = "Reset the region so that it is empty",\r
351         privs = {worldedit=true},\r
352         func = function(name)\r
353                 worldedit.pos1[name] = nil\r
354                 worldedit.pos2[name] = nil\r
355                 worldedit.marker_update(name)\r
356                 worldedit.set_pos[name] = nil\r
357                 --make sure the user does not try to confirm an operation after resetting pos:\r
358                 reset_pending(name)\r
359                 worldedit.player_notify(name, "region reset")\r
360         end,\r
361 })\r
362 \r
363 worldedit.register_command("mark", {\r
364         params = "",\r
365         description = "Show markers at the region positions",\r
366         privs = {worldedit=true},\r
367         func = function(name)\r
368                 worldedit.marker_update(name)\r
369                 worldedit.player_notify(name, "region marked")\r
370         end,\r
371 })\r
372 \r
373 worldedit.register_command("unmark", {\r
374         params = "",\r
375         description = "Hide markers if currently shown",\r
376         privs = {worldedit=true},\r
377         func = function(name)\r
378                 local pos1, pos2 = worldedit.pos1[name], worldedit.pos2[name]\r
379                 worldedit.pos1[name] = nil\r
380                 worldedit.pos2[name] = nil\r
381                 worldedit.marker_update(name)\r
382                 worldedit.pos1[name] = pos1\r
383                 worldedit.pos2[name] = pos2\r
384                 worldedit.player_notify(name, "region unmarked")\r
385         end,\r
386 })\r
387 \r
388 worldedit.register_command("pos1", {\r
389         params = "",\r
390         description = "Set WorldEdit region position 1 to the player's location",\r
391         privs = {worldedit=true},\r
392         func = function(name)\r
393                 local pos = minetest.get_player_by_name(name):get_pos()\r
394                 pos.x, pos.y, pos.z = math.floor(pos.x + 0.5), math.floor(pos.y + 0.5), math.floor(pos.z + 0.5)\r
395                 worldedit.pos1[name] = pos\r
396                 worldedit.mark_pos1(name)\r
397                 worldedit.player_notify(name, "position 1 set to " .. minetest.pos_to_string(pos))\r
398         end,\r
399 })\r
400 \r
401 worldedit.register_command("pos2", {\r
402         params = "",\r
403         description = "Set WorldEdit region position 2 to the player's location",\r
404         privs = {worldedit=true},\r
405         func = function(name)\r
406                 local pos = minetest.get_player_by_name(name):get_pos()\r
407                 pos.x, pos.y, pos.z = math.floor(pos.x + 0.5), math.floor(pos.y + 0.5), math.floor(pos.z + 0.5)\r
408                 worldedit.pos2[name] = pos\r
409                 worldedit.mark_pos2(name)\r
410                 worldedit.player_notify(name, "position 2 set to " .. minetest.pos_to_string(pos))\r
411         end,\r
412 })\r
413 \r
414 worldedit.register_command("p", {\r
415         params = "set/set1/set2/get",\r
416         description = "Set WorldEdit region, WorldEdit position 1, or WorldEdit position 2 by punching nodes, or display the current WorldEdit region",\r
417         privs = {worldedit=true},\r
418         parse = function(param)\r
419                 if param == "set" or param == "set1" or param == "set2" or param == "get" then\r
420                         return true, param\r
421                 end\r
422                 return false, "unknown subcommand: " .. param\r
423         end,\r
424         func = function(name, param)\r
425                 if param == "set" then --set both WorldEdit positions\r
426                         worldedit.set_pos[name] = "pos1"\r
427                         worldedit.player_notify(name, "select positions by punching two nodes")\r
428                 elseif param == "set1" then --set WorldEdit position 1\r
429                         worldedit.set_pos[name] = "pos1only"\r
430                         worldedit.player_notify(name, "select position 1 by punching a node")\r
431                 elseif param == "set2" then --set WorldEdit position 2\r
432                         worldedit.set_pos[name] = "pos2"\r
433                         worldedit.player_notify(name, "select position 2 by punching a node")\r
434                 elseif param == "get" then --display current WorldEdit positions\r
435                         if worldedit.pos1[name] ~= nil then\r
436                                 worldedit.player_notify(name, "position 1: " .. minetest.pos_to_string(worldedit.pos1[name]))\r
437                         else\r
438                                 worldedit.player_notify(name, "position 1 not set")\r
439                         end\r
440                         if worldedit.pos2[name] ~= nil then\r
441                                 worldedit.player_notify(name, "position 2: " .. minetest.pos_to_string(worldedit.pos2[name]))\r
442                         else\r
443                                 worldedit.player_notify(name, "position 2 not set")\r
444                         end\r
445                 end\r
446         end,\r
447 })\r
448 \r
449 worldedit.register_command("fixedpos", {\r
450         params = "set1/set2 <x> <y> <z>",\r
451         description = "Set a WorldEdit region position to the position at (<x>, <y>, <z>)",\r
452         privs = {worldedit=true},\r
453         parse = function(param)\r
454                 local found, _, flag, x, y, z = param:find("^(set[12])%s+([+-]?%d+)%s+([+-]?%d+)%s+([+-]?%d+)$")\r
455                 if found == nil then\r
456                         return false\r
457                 end\r
458                 return true, flag, {x=tonumber(x), y=tonumber(y), z=tonumber(z)}\r
459         end,\r
460         func = function(name, flag, pos)\r
461                 if flag == "set1" then\r
462                         worldedit.pos1[name] = pos\r
463                         worldedit.mark_pos1(name)\r
464                         worldedit.player_notify(name, "position 1 set to " .. minetest.pos_to_string(pos))\r
465                 else --flag == "set2"\r
466                         worldedit.pos2[name] = pos\r
467                         worldedit.mark_pos2(name)\r
468                         worldedit.player_notify(name, "position 2 set to " .. minetest.pos_to_string(pos))\r
469                 end\r
470         end,\r
471 })\r
472 \r
473 minetest.register_on_punchnode(function(pos, node, puncher)\r
474         local name = puncher:get_player_name()\r
475         if name ~= "" and worldedit.set_pos[name] ~= nil then --currently setting position\r
476                 if worldedit.set_pos[name] == "pos1" then --setting position 1\r
477                         worldedit.pos1[name] = pos\r
478                         worldedit.mark_pos1(name)\r
479                         worldedit.set_pos[name] = "pos2" --set position 2 on the next invocation\r
480                         worldedit.player_notify(name, "position 1 set to " .. minetest.pos_to_string(pos))\r
481                 elseif worldedit.set_pos[name] == "pos1only" then --setting position 1 only\r
482                         worldedit.pos1[name] = pos\r
483                         worldedit.mark_pos1(name)\r
484                         worldedit.set_pos[name] = nil --finished setting positions\r
485                         worldedit.player_notify(name, "position 1 set to " .. minetest.pos_to_string(pos))\r
486                 elseif worldedit.set_pos[name] == "pos2" then --setting position 2\r
487                         worldedit.pos2[name] = pos\r
488                         worldedit.mark_pos2(name)\r
489                         worldedit.set_pos[name] = nil --finished setting positions\r
490                         worldedit.player_notify(name, "position 2 set to " .. minetest.pos_to_string(pos))\r
491                 elseif worldedit.set_pos[name] == "prob" then --setting Minetest schematic node probabilities\r
492                         worldedit.prob_pos[name] = pos\r
493                         minetest.show_formspec(puncher:get_player_name(), "prob_val_enter", "field[text;;]")\r
494                 end\r
495         end\r
496 end)\r
497 \r
498 worldedit.register_command("volume", {\r
499         params = "",\r
500         description = "Display the volume of the current WorldEdit region",\r
501         privs = {worldedit=true},\r
502         require_pos = 2,\r
503         func = function(name)\r
504                 local pos1, pos2 = worldedit.pos1[name], worldedit.pos2[name]\r
505 \r
506                 local volume = worldedit.volume(pos1, pos2)\r
507                 local abs = math.abs\r
508                 worldedit.player_notify(name, "current region has a volume of " .. volume .. " nodes ("\r
509                         .. abs(pos2.x - pos1.x) + 1 .. "*"\r
510                         .. abs(pos2.y - pos1.y) + 1 .. "*"\r
511                         .. abs(pos2.z - pos1.z) + 1 .. ")")\r
512         end,\r
513 })\r
514 \r
515 worldedit.register_command("deleteblocks", {\r
516         params = "",\r
517         description = "remove all MapBlocks (16x16x16) containing the selected area from the map",\r
518         privs = {worldedit=true},\r
519         require_pos = 2,\r
520         nodes_needed = check_region,\r
521         func = function(name)\r
522                 local pos1, pos2 = worldedit.pos1[name], worldedit.pos2[name]\r
523                 local success = minetest.delete_area(pos1, pos2)\r
524                 if success then\r
525                         worldedit.player_notify(name, "Area deleted.")\r
526                 else\r
527                         worldedit.player_notify(name, "There was an error during deletion of the area.")\r
528                 end\r
529         end,\r
530 })\r
531 \r
532 worldedit.register_command("set", {\r
533         params = "<node>",\r
534         description = "Set the current WorldEdit region to <node>",\r
535         privs = {worldedit=true},\r
536         require_pos = 2,\r
537         parse = function(param)\r
538                 local node = worldedit.normalize_nodename(param)\r
539                 if not node then\r
540                         return false, "invalid node name: " .. param\r
541                 end\r
542                 return true, node\r
543         end,\r
544         nodes_needed = check_region,\r
545         func = function(name, node)\r
546                 local count = worldedit.set(worldedit.pos1[name], worldedit.pos2[name], node)\r
547                 worldedit.player_notify(name, count .. " nodes set")\r
548         end,\r
549 })\r
550 \r
551 worldedit.register_command("param2", {\r
552         params = "<param2>",\r
553         description = "Set param2 of all nodes in the current WorldEdit region to <param2>",\r
554         privs = {worldedit=true},\r
555         require_pos = 2,\r
556         parse = function(param)\r
557                 local param2 = tonumber(param)\r
558                 if not param2 then\r
559                         return false\r
560                 elseif param2 < 0 or param2 > 255 then\r
561                         return false, "Param2 is out of range (must be between 0 and 255 inclusive!)"\r
562                 end\r
563                 return true, param2\r
564         end,\r
565         nodes_needed = check_region,\r
566         func = function(name, param2)\r
567                 local count = worldedit.set_param2(worldedit.pos1[name], worldedit.pos2[name], param2)\r
568                 worldedit.player_notify(name, count .. " nodes altered")\r
569         end,\r
570 })\r
571 \r
572 worldedit.register_command("mix", {\r
573         params = "<node1> [count1] <node2> [count2] ...",\r
574         description = "Fill the current WorldEdit region with a random mix of <node1>, ...",\r
575         privs = {worldedit=true},\r
576         require_pos = 2,\r
577         parse = function(param)\r
578                 local nodes = {}\r
579                 for nodename in param:gmatch("[^%s]+") do\r
580                         if tonumber(nodename) ~= nil and #nodes > 0 then\r
581                                 local last_node = nodes[#nodes]\r
582                                 for i = 1, tonumber(nodename) do\r
583                                         nodes[#nodes + 1] = last_node\r
584                                 end\r
585                         else\r
586                                 local node = worldedit.normalize_nodename(nodename)\r
587                                 if not node then\r
588                                         return false, "invalid node name: " .. nodename\r
589                                 end\r
590                                 nodes[#nodes + 1] = node\r
591                         end\r
592                 end\r
593                 if #nodes == 0 then\r
594                         return false\r
595                 end\r
596                 return true, nodes\r
597         end,\r
598         nodes_needed = check_region,\r
599         func = function(name, nodes)\r
600                 local pos1, pos2 = worldedit.pos1[name], worldedit.pos2[name]\r
601                 local count = worldedit.set(pos1, pos2, nodes)\r
602                 worldedit.player_notify(name, count .. " nodes set")\r
603         end,\r
604 })\r
605 \r
606 local check_replace = function(param)\r
607         local found, _, searchnode, replacenode = param:find("^([^%s]+)%s+(.+)$")\r
608         if found == nil then\r
609                 return false\r
610         end\r
611         local newsearchnode = worldedit.normalize_nodename(searchnode)\r
612         if not newsearchnode then\r
613                 return false, "invalid search node name: " .. searchnode\r
614         end\r
615         local newreplacenode = worldedit.normalize_nodename(replacenode)\r
616         if not newreplacenode then\r
617                 return false, "invalid replace node name: " .. replacenode\r
618         end\r
619         return true, newsearchnode, newreplacenode\r
620 end\r
621 \r
622 worldedit.register_command("replace", {\r
623         params = "<search node> <replace node>",\r
624         description = "Replace all instances of <search node> with <replace node> in the current WorldEdit region",\r
625         privs = {worldedit=true},\r
626         require_pos = 2,\r
627         parse = check_replace,\r
628         nodes_needed = check_region,\r
629         func = function(name, search_node, replace_node)\r
630                 local count = worldedit.replace(worldedit.pos1[name], worldedit.pos2[name],\r
631                                 search_node, replace_node)\r
632                 worldedit.player_notify(name, count .. " nodes replaced")\r
633         end,\r
634 })\r
635 \r
636 worldedit.register_command("replaceinverse", {\r
637         params = "<search node> <replace node>",\r
638         description = "Replace all nodes other than <search node> with <replace node> in the current WorldEdit region",\r
639         privs = {worldedit=true},\r
640         require_pos = 2,\r
641         parse = check_replace,\r
642         nodes_needed = check_region,\r
643         func = function(name, search_node, replace_node)\r
644                 local count = worldedit.replace(worldedit.pos1[name], worldedit.pos2[name],\r
645                                 search_node, replace_node, true)\r
646                 worldedit.player_notify(name, count .. " nodes replaced")\r
647         end,\r
648 })\r
649 \r
650 local check_cube = function(param)\r
651         local found, _, w, h, l, nodename = param:find("^(%d+)%s+(%d+)%s+(%d+)%s+(.+)$")\r
652         if found == nil then\r
653                 return false\r
654         end\r
655         local node = worldedit.normalize_nodename(nodename)\r
656         if not node then\r
657                 return false, "invalid node name: " .. nodename\r
658         end\r
659         return true, tonumber(w), tonumber(h), tonumber(l), node\r
660 end\r
661 \r
662 worldedit.register_command("hollowcube", {\r
663         params = "<width> <height> <length> <node>",\r
664         description = "Add a hollow cube with its ground level centered at WorldEdit position 1 with dimensions <width> x <height> x <length>, composed of <node>.",\r
665         privs = {worldedit=true},\r
666         require_pos = 1,\r
667         parse = check_cube,\r
668         nodes_needed = function(name, w, h, l, node)\r
669                 return w * h * l\r
670         end,\r
671         func = function(name, w, h, l, node)\r
672                 local count = worldedit.cube(worldedit.pos1[name], w, h, l, node, true)\r
673                 worldedit.player_notify(name, count .. " nodes added")\r
674         end,\r
675 })\r
676 \r
677 worldedit.register_command("cube", {\r
678         params = "<width> <height> <length> <node>",\r
679         description = "Add a cube with its ground level centered at WorldEdit position 1 with dimensions <width> x <height> x <length>, composed of <node>.",\r
680         privs = {worldedit=true},\r
681         require_pos = 1,\r
682         parse = check_cube,\r
683         nodes_needed = function(name, w, h, l, node)\r
684                 return w * h * l\r
685         end,\r
686         func = function(name, w, h, l, node)\r
687                 local count = worldedit.cube(worldedit.pos1[name], w, h, l, node)\r
688                 worldedit.player_notify(name, count .. " nodes added")\r
689         end,\r
690 })\r
691 \r
692 local check_sphere = function(param)\r
693         local found, _, radius, nodename = param:find("^(%d+)%s+(.+)$")\r
694         if found == nil then\r
695                 return false\r
696         end\r
697         local node = worldedit.normalize_nodename(nodename)\r
698         if not node then\r
699                 return false, "invalid node name: " .. nodename\r
700         end\r
701         return true, tonumber(radius), node\r
702 end\r
703 \r
704 worldedit.register_command("hollowsphere", {\r
705         params = "<radius> <node>",\r
706         description = "Add hollow sphere centered at WorldEdit position 1 with radius <radius>, composed of <node>",\r
707         privs = {worldedit=true},\r
708         require_pos = 1,\r
709         parse = check_sphere,\r
710         nodes_needed = function(name, radius, node)\r
711                 return math.ceil((4 * math.pi * (radius ^ 3)) / 3) --volume of sphere\r
712         end,\r
713         func = function(name, radius, node)\r
714                 local count = worldedit.sphere(worldedit.pos1[name], radius, node, true)\r
715                 worldedit.player_notify(name, count .. " nodes added")\r
716         end,\r
717 })\r
718 \r
719 worldedit.register_command("sphere", {\r
720         params = "<radius> <node>",\r
721         description = "Add sphere centered at WorldEdit position 1 with radius <radius>, composed of <node>",\r
722         privs = {worldedit=true},\r
723         require_pos = 1,\r
724         parse = check_sphere,\r
725         nodes_needed = function(name, radius, node)\r
726                 return math.ceil((4 * math.pi * (radius ^ 3)) / 3) --volume of sphere\r
727         end,\r
728         func = function(name, radius, node)\r
729                 local count = worldedit.sphere(worldedit.pos1[name], radius, node)\r
730                 worldedit.player_notify(name, count .. " nodes added")\r
731         end,\r
732 })\r
733 \r
734 local check_dome = function(param)\r
735         local found, _, radius, nodename = param:find("^(%d+)%s+(.+)$")\r
736         if found == nil then\r
737                 return false\r
738         end\r
739         local node = worldedit.normalize_nodename(nodename)\r
740         if not node then\r
741                 return false, "invalid node name: " .. nodename\r
742         end\r
743         return true, tonumber(radius), node\r
744 end\r
745 \r
746 worldedit.register_command("hollowdome", {\r
747         params = "<radius> <node>",\r
748         description = "Add hollow dome centered at WorldEdit position 1 with radius <radius>, composed of <node>",\r
749         privs = {worldedit=true},\r
750         require_pos = 1,\r
751         parse = check_dome,\r
752         nodes_needed = function(name, radius, node)\r
753                 return math.ceil((2 * math.pi * (radius ^ 3)) / 3) --volume of dome\r
754         end,\r
755         func = function(name, radius, node)\r
756                 local count = worldedit.dome(worldedit.pos1[name], radius, node, true)\r
757                 worldedit.player_notify(name, count .. " nodes added")\r
758         end,\r
759 })\r
760 \r
761 worldedit.register_command("dome", {\r
762         params = "<radius> <node>",\r
763         description = "Add dome centered at WorldEdit position 1 with radius <radius>, composed of <node>",\r
764         privs = {worldedit=true},\r
765         require_pos = 1,\r
766         parse = check_dome,\r
767         nodes_needed = function(name, radius, node)\r
768                 return math.ceil((2 * math.pi * (radius ^ 3)) / 3) --volume of dome\r
769         end,\r
770         func = function(name, radius, node)\r
771                 local count = worldedit.dome(worldedit.pos1[name], radius, node)\r
772                 worldedit.player_notify(name, count .. " nodes added")\r
773         end,\r
774 })\r
775 \r
776 local check_cylinder = function(param)\r
777         -- two radii\r
778         local found, _, axis, length, radius1, radius2, nodename = param:find("^([xyz%?])%s+([+-]?%d+)%s+(%d+)%s+(%d+)%s+(.+)$")\r
779         if found == nil then\r
780                 -- single radius\r
781                 found, _, axis, length, radius1, nodename = param:find("^([xyz%?])%s+([+-]?%d+)%s+(%d+)%s+(.+)$")\r
782                 radius2 = radius1\r
783         end\r
784         if found == nil then\r
785                 return false\r
786         end\r
787         local node = worldedit.normalize_nodename(nodename)\r
788         if not node then\r
789                 return false, "invalid node name: " .. nodename\r
790         end\r
791         return true, axis, tonumber(length), tonumber(radius1), tonumber(radius2), node\r
792 end\r
793 \r
794 worldedit.register_command("hollowcylinder", {\r
795         params = "x/y/z/? <length> <radius1> [radius2] <node>",\r
796         description = "Add hollow cylinder at WorldEdit position 1 along the given axis with length <length>, base radius <radius1> (and top radius [radius2]), composed of <node>",\r
797         privs = {worldedit=true},\r
798         require_pos = 1,\r
799         parse = check_cylinder,\r
800         nodes_needed = function(name, axis, length, radius1, radius2, node)\r
801                 local radius = math.max(radius1, radius2)\r
802                 return math.ceil(math.pi * (radius ^ 2) * length)\r
803         end,\r
804         func = function(name, axis, length, radius1, radius2, node)\r
805                 if axis == "?" then\r
806                         local sign\r
807                         axis, sign = worldedit.player_axis(name)\r
808                         length = length * sign\r
809                 end\r
810                 local count = worldedit.cylinder(worldedit.pos1[name], axis, length, radius1, radius2, node, true)\r
811                 worldedit.player_notify(name, count .. " nodes added")\r
812         end,\r
813 })\r
814 \r
815 worldedit.register_command("cylinder", {\r
816         params = "x/y/z/? <length> <radius1> [radius2] <node>",\r
817         description = "Add cylinder at WorldEdit position 1 along the given axis with length <length>, base radius <radius1> (and top radius [radius2]), composed of <node>",\r
818         privs = {worldedit=true},\r
819         require_pos = 1,\r
820         parse = check_cylinder,\r
821         nodes_needed = function(name, axis, length, radius1, radius2, node)\r
822                 local radius = math.max(radius1, radius2)\r
823                 return math.ceil(math.pi * (radius ^ 2) * length)\r
824         end,\r
825         func = function(name, axis, length, radius1, radius2, node)\r
826                 if axis == "?" then\r
827                         local sign\r
828                         axis, sign = worldedit.player_axis(name)\r
829                         length = length * sign\r
830                 end\r
831                 local count = worldedit.cylinder(worldedit.pos1[name], axis, length, radius1, radius2, node)\r
832                 worldedit.player_notify(name, count .. " nodes added")\r
833         end,\r
834 })\r
835 \r
836 local check_pyramid = function(param)\r
837         local found, _, axis, height, nodename = param:find("^([xyz%?])%s+([+-]?%d+)%s+(.+)$")\r
838         if found == nil then\r
839                 return false\r
840         end\r
841         local node = worldedit.normalize_nodename(nodename)\r
842         if not node then\r
843                 return false, "invalid node name: " .. nodename\r
844         end\r
845         return true, axis, tonumber(height), node\r
846 end\r
847      \r
848 worldedit.register_command("hollowpyramid", {\r
849         params = "x/y/z/? <height> <node>",\r
850         description = "Add hollow pyramid centered at WorldEdit position 1 along the given axis with height <height>, composed of <node>",\r
851         privs = {worldedit=true},\r
852         require_pos = 1,\r
853         parse = check_pyramid,\r
854         nodes_needed = function(name, axis, height, node)\r
855                 return math.ceil(((height * 2 + 1) ^ 2) * height / 3)\r
856         end,\r
857         func = function(name, axis, height, node)\r
858                 if axis == "?" then\r
859                         local sign\r
860                         axis, sign = worldedit.player_axis(name)\r
861                         height = height * sign\r
862                 end\r
863                 local count = worldedit.pyramid(worldedit.pos1[name], axis, height, node, true)\r
864                 worldedit.player_notify(name, count .. " nodes added")\r
865         end,\r
866 })\r
867 \r
868 worldedit.register_command("pyramid", {\r
869         params = "x/y/z/? <height> <node>",\r
870         description = "Add pyramid centered at WorldEdit position 1 along the given axis with height <height>, composed of <node>",\r
871         privs = {worldedit=true},\r
872         require_pos = 1,\r
873         parse = check_pyramid,\r
874         nodes_needed = function(name, axis, height, node)\r
875                 return math.ceil(((height * 2 + 1) ^ 2) * height / 3)\r
876         end,\r
877         func = function(name, axis, height, node)\r
878                 if axis == "?" then\r
879                         local sign\r
880                         axis, sign = worldedit.player_axis(name)\r
881                         height = height * sign\r
882                 end\r
883                 local count = worldedit.pyramid(worldedit.pos1[name], axis, height, node)\r
884                 worldedit.player_notify(name, count .. " nodes added")\r
885         end,\r
886 })\r
887 \r
888 worldedit.register_command("spiral", {\r
889         params = "<length> <height> <space> <node>",\r
890         description = "Add spiral centered at WorldEdit position 1 with side length <length>, height <height>, space between walls <space>, composed of <node>",\r
891         privs = {worldedit=true},\r
892         require_pos = 1,\r
893         parse = function(param)\r
894                 local found, _, length, height, space, nodename = param:find("^(%d+)%s+(%d+)%s+(%d+)%s+(.+)$")\r
895                 if found == nil then\r
896                         return false\r
897                 end\r
898                 local node = worldedit.normalize_nodename(nodename)\r
899                 if not node then\r
900                         return false, "invalid node name: " .. nodename\r
901                 end\r
902                 return true, tonumber(length), tonumber(height), tonumber(space), node\r
903         end,\r
904         nodes_needed = function(name, length, height, space, node)\r
905                 return (length + space) * height -- TODO: this is not the upper bound\r
906         end,\r
907         func = function(name, length, height, space, node)\r
908                 local count = worldedit.spiral(worldedit.pos1[name], length, height, space, node)\r
909                 worldedit.player_notify(name, count .. " nodes added")\r
910         end,\r
911 })\r
912 \r
913 worldedit.register_command("copy", {\r
914         params = "x/y/z/? <amount>",\r
915         description = "Copy the current WorldEdit region along the given axis by <amount> nodes",\r
916         privs = {worldedit=true},\r
917         require_pos = 2,\r
918         parse = function(param)\r
919                 local found, _, axis, amount = param:find("^([xyz%?])%s+([+-]?%d+)$")\r
920                 if found == nil then\r
921                         return false\r
922                 end\r
923                 return true, axis, tonumber(amount)\r
924         end,\r
925         nodes_needed = function(name, axis, amount)\r
926                 return check_region(name) * 2\r
927         end,\r
928         func = function(name, axis, amount)\r
929                 if axis == "?" then\r
930                         local sign\r
931                         axis, sign = worldedit.player_axis(name)\r
932                         amount = amount * sign\r
933                 end\r
934 \r
935                 local count = worldedit.copy(worldedit.pos1[name], worldedit.pos2[name], axis, amount)\r
936                 worldedit.player_notify(name, count .. " nodes copied")\r
937         end,\r
938 })\r
939 \r
940 worldedit.register_command("move", {\r
941         params = "x/y/z/? <amount>",\r
942         description = "Move the current WorldEdit region along the given axis by <amount> nodes",\r
943         privs = {worldedit=true},\r
944         require_pos = 2,\r
945         parse = function(param)\r
946                 local found, _, axis, amount = param:find("^([xyz%?])%s+([+-]?%d+)$")\r
947                 if found == nil then\r
948                         return false\r
949                 end\r
950                 return true, axis, tonumber(amount)\r
951         end,\r
952         nodes_needed = function(name, axis, amount)\r
953                 return check_region(name) * 2\r
954         end,\r
955         func = function(name, axis, amount)\r
956                 if axis == "?" then\r
957                         local sign\r
958                         axis, sign = worldedit.player_axis(name)\r
959                         amount = amount * sign\r
960                 end\r
961 \r
962                 local pos1, pos2 = worldedit.pos1[name], worldedit.pos2[name]\r
963                 local count = worldedit.move(pos1, pos2, axis, amount)\r
964 \r
965                 pos1[axis] = pos1[axis] + amount\r
966                 pos2[axis] = pos2[axis] + amount\r
967                 worldedit.marker_update(name)\r
968                 worldedit.player_notify(name, count .. " nodes moved")\r
969         end,\r
970 })\r
971 \r
972 worldedit.register_command("stack", {\r
973         params = "x/y/z/? <count>",\r
974         description = "Stack the current WorldEdit region along the given axis <count> times",\r
975         privs = {worldedit=true},\r
976         require_pos = 2,\r
977         parse = function(param)\r
978                 local found, _, axis, repetitions = param:find("^([xyz%?])%s+([+-]?%d+)$")\r
979                 if found == nil then\r
980                         return false\r
981                 end\r
982                 return true, axis, tonumber(repetitions)\r
983         end,\r
984         nodes_needed = function(name, axis, repetitions)\r
985                 return check_region(name) * math.abs(repetitions)\r
986         end,\r
987         func = function(name, axis, repetitions)\r
988                 if axis == "?" then\r
989                         local sign\r
990                         axis, sign = worldedit.player_axis(name)\r
991                         repetitions = repetitions * sign\r
992                 end\r
993 \r
994                 local pos1, pos2 = worldedit.pos1[name], worldedit.pos2[name]\r
995                 local count = worldedit.volume(pos1, pos2) * math.abs(repetitions)\r
996                 worldedit.stack(pos1, pos2, axis, repetitions, function()\r
997                         worldedit.player_notify(name, count .. " nodes stacked")\r
998                 end)\r
999         end,\r
1000 })\r
1001 \r
1002 worldedit.register_command("stack2", {\r
1003         params = "<count> <x> <y> <z>",\r
1004         description = "Stack the current WorldEdit region <count> times by offset <x>, <y>, <z>",\r
1005         privs = {worldedit=true},\r
1006         require_pos = 2,\r
1007         parse = function(param)\r
1008                 local repetitions, incs = param:match("(%d+)%s*(.+)")\r
1009                 if repetitions == nil then\r
1010                         return false, "invalid count: " .. param\r
1011                 end\r
1012                 local x, y, z = incs:match("([+-]?%d+) ([+-]?%d+) ([+-]?%d+)")\r
1013                 if x == nil then\r
1014                         return false, "invalid increments: " .. param\r
1015                 end\r
1016 \r
1017                 return true, tonumber(repetitions), {x=tonumber(x), y=tonumber(y), z=tonumber(z)}\r
1018         end,\r
1019         nodes_needed = function(name, repetitions, offset)\r
1020                 return check_region(name) * repetitions\r
1021         end,\r
1022         func = function(name, repetitions, offset)\r
1023                 local pos1, pos2 = worldedit.pos1[name], worldedit.pos2[name]\r
1024                 local count = worldedit.volume(pos1, pos2) * repetitions\r
1025                 worldedit.stack2(pos1, pos2, offset, repetitions, function()\r
1026                         worldedit.player_notify(name, count .. " nodes stacked")\r
1027                 end)\r
1028         end,\r
1029 })\r
1030 \r
1031 \r
1032 worldedit.register_command("stretch", {\r
1033         params = "<stretchx> <stretchy> <stretchz>",\r
1034         description = "Scale the current WorldEdit positions and region by a factor of <stretchx>, <stretchy>, <stretchz> along the X, Y, and Z axes, repectively, with position 1 as the origin",\r
1035         privs = {worldedit=true},\r
1036         require_pos = 2,\r
1037         parse = function(param)\r
1038                 local found, _, stretchx, stretchy, stretchz = param:find("^(%d+)%s+(%d+)%s+(%d+)$")\r
1039                 if found == nil then\r
1040                         return false\r
1041                 end\r
1042                 stretchx, stretchy, stretchz = tonumber(stretchx), tonumber(stretchy), tonumber(stretchz)\r
1043                 if stretchx == 0 or stretchy == 0 or stretchz == 0 then\r
1044                         return false, "invalid scaling factors: " .. param\r
1045                 end\r
1046                 return true, stretchx, stretchy, stretchz\r
1047         end,\r
1048         nodes_needed = function(name, stretchx, stretchy, stretchz)\r
1049                 return check_region(name) * stretchx * stretchy * stretchz\r
1050         end,\r
1051         func = function(name, stretchx, stretchy, stretchz)\r
1052                 local pos1, pos2 = worldedit.pos1[name], worldedit.pos2[name]\r
1053                 local count, pos1, pos2 = worldedit.stretch(pos1, pos2, stretchx, stretchy, stretchz)\r
1054 \r
1055                 --reset markers to scaled positions\r
1056                 worldedit.pos1[name] = pos1\r
1057                 worldedit.pos2[name] = pos2\r
1058                 worldedit.marker_update(name)\r
1059 \r
1060                 worldedit.player_notify(name, count .. " nodes stretched")\r
1061         end,\r
1062 })\r
1063 \r
1064 worldedit.register_command("transpose", {\r
1065         params = "x/y/z/? x/y/z/?",\r
1066         description = "Transpose the current WorldEdit region along the given axes",\r
1067         privs = {worldedit=true},\r
1068         require_pos = 2,\r
1069         parse = function(param)\r
1070                 local found, _, axis1, axis2 = param:find("^([xyz%?])%s+([xyz%?])$")\r
1071                 if found == nil then\r
1072                         return false\r
1073                 elseif axis1 == axis2 then\r
1074                         return false, "invalid usage: axes must be different"\r
1075                 end\r
1076                 return true, axis1, axis2\r
1077         end,\r
1078         nodes_needed = check_region,\r
1079         func = function(name, axis1, axis2)\r
1080                 local pos1, pos2 = worldedit.pos1[name], worldedit.pos2[name]\r
1081                 if axis1 == "?" then axis1 = worldedit.player_axis(name) end\r
1082                 if axis2 == "?" then axis2 = worldedit.player_axis(name) end\r
1083                 local count, pos1, pos2 = worldedit.transpose(pos1, pos2, axis1, axis2)\r
1084 \r
1085                 --reset markers to transposed positions\r
1086                 worldedit.pos1[name] = pos1\r
1087                 worldedit.pos2[name] = pos2\r
1088                 worldedit.marker_update(name)\r
1089 \r
1090                 worldedit.player_notify(name, count .. " nodes transposed")\r
1091         end,\r
1092 })\r
1093 \r
1094 worldedit.register_command("flip", {\r
1095         params = "x/y/z/?",\r
1096         description = "Flip the current WorldEdit region along the given axis",\r
1097         privs = {worldedit=true},\r
1098         require_pos = 2,\r
1099         parse = function(param)\r
1100                 if param ~= "x" and param ~= "y" and param ~= "z" and param ~= "?" then\r
1101                         return false\r
1102                 end\r
1103                 return true, param\r
1104         end,\r
1105         nodes_needed = check_region,\r
1106         func = function(name, param)\r
1107                 if param == "?" then param = worldedit.player_axis(name) end\r
1108                 local count = worldedit.flip(worldedit.pos1[name], worldedit.pos2[name], param)\r
1109                 worldedit.player_notify(name, count .. " nodes flipped")\r
1110         end,\r
1111 })\r
1112 \r
1113 worldedit.register_command("rotate", {\r
1114         params = "x/y/z/? <angle>",\r
1115         description = "Rotate the current WorldEdit region around the given axis by angle <angle> (90 degree increment)",\r
1116         privs = {worldedit=true},\r
1117         require_pos = 2,\r
1118         parse = function(param)\r
1119                 local found, _, axis, angle = param:find("^([xyz%?])%s+([+-]?%d+)$")\r
1120                 if found == nil then\r
1121                         return false\r
1122                 end\r
1123                 angle = tonumber(angle)\r
1124                 if angle % 90 ~= 0 or angle % 360 == 0 then\r
1125                         return false, "invalid usage: angle must be multiple of 90"\r
1126                 end\r
1127                 return true, axis, angle\r
1128         end,\r
1129         nodes_needed = check_region,\r
1130         func = function(name, axis, angle)\r
1131                 local pos1, pos2 = worldedit.pos1[name], worldedit.pos2[name]\r
1132                 if axis == "?" then axis = worldedit.player_axis(name) end\r
1133                 local count, pos1, pos2 = worldedit.rotate(pos1, pos2, axis, angle)\r
1134 \r
1135                 --reset markers to rotated positions\r
1136                 worldedit.pos1[name] = pos1\r
1137                 worldedit.pos2[name] = pos2\r
1138                 worldedit.marker_update(name)\r
1139 \r
1140                 worldedit.player_notify(name, count .. " nodes rotated")\r
1141         end,\r
1142 })\r
1143 \r
1144 worldedit.register_command("orient", {\r
1145         params = "<angle>",\r
1146         description = "Rotate oriented nodes in the current WorldEdit region around the Y axis by angle <angle> (90 degree increment)",\r
1147         privs = {worldedit=true},\r
1148         require_pos = 2,\r
1149         parse = function(param)\r
1150                 local found, _, angle = param:find("^([+-]?%d+)$")\r
1151                 if found == nil then\r
1152                         return false\r
1153                 end\r
1154                 angle = tonumber(angle)\r
1155                 if angle % 90 ~= 0 then\r
1156                         return false, "invalid usage: angle must be multiple of 90"\r
1157                 end\r
1158                 return true, angle\r
1159         end,\r
1160         nodes_needed = check_region,\r
1161         func = function(name, angle)\r
1162                 local count = worldedit.orient(worldedit.pos1[name], worldedit.pos2[name], angle)\r
1163                 worldedit.player_notify(name, count .. " nodes oriented")\r
1164         end,\r
1165 })\r
1166 \r
1167 worldedit.register_command("fixlight", {\r
1168         params = "",\r
1169         description = "Fix the lighting in the current WorldEdit region",\r
1170         privs = {worldedit=true},\r
1171         require_pos = 2,\r
1172         nodes_needed = check_region,\r
1173         func = function(name)\r
1174                 local count = worldedit.fixlight(worldedit.pos1[name], worldedit.pos2[name])\r
1175                 worldedit.player_notify(name, count .. " nodes updated")\r
1176         end,\r
1177 })\r
1178 \r
1179 worldedit.register_command("drain", {\r
1180         params = "",\r
1181         description = "Remove any fluid node within the current WorldEdit region",\r
1182         privs = {worldedit=true},\r
1183         require_pos = 2,\r
1184         nodes_needed = check_region,\r
1185         func = function(name)\r
1186                 -- TODO: make an API function for this\r
1187                 local count = 0\r
1188                 local pos1, pos2 = worldedit.sort_pos(worldedit.pos1[name], worldedit.pos2[name])\r
1189                 for x = pos1.x, pos2.x do\r
1190                 for y = pos1.y, pos2.y do\r
1191                 for z = pos1.z, pos2.z do\r
1192                         local n = minetest.get_node({x=x, y=y, z=z}).name\r
1193                         local d = minetest.registered_nodes[n]\r
1194                         if d ~= nil and (d["drawtype"] == "liquid" or d["drawtype"] == "flowingliquid") then\r
1195                                 minetest.remove_node({x=x, y=y, z=z})\r
1196                                 count = count + 1\r
1197                         end\r
1198                 end\r
1199                 end\r
1200                 end\r
1201                 worldedit.player_notify(name, count .. " nodes updated")\r
1202         end,\r
1203 })\r
1204 \r
1205 local clearcut_cache\r
1206 \r
1207 local function clearcut(pos1, pos2)\r
1208         -- decide which nodes we consider plants\r
1209         if clearcut_cache == nil then\r
1210                 clearcut_cache = {}\r
1211                 for name, def in pairs(minetest.registered_nodes) do\r
1212                         local groups = def.groups or {}\r
1213                         if (\r
1214                                 -- the groups say so\r
1215                                 groups.flower or groups.grass or groups.flora or groups.plant or\r
1216                                 groups.leaves or groups.tree or groups.leafdecay or groups.sapling or\r
1217                                 -- drawtype heuristic\r
1218                                 (def.is_ground_content and def.buildable_to and\r
1219                                         (def.sunlight_propagates or not def.walkable)\r
1220                                         and def.drawtype == "plantlike") or\r
1221                                 -- if it's flammable, it probably needs to go too\r
1222                                 (def.is_ground_content and not def.walkable and groups.flammable)\r
1223                         ) then\r
1224                                 clearcut_cache[name] = true\r
1225                         end\r
1226                 end\r
1227         end\r
1228         local plants = clearcut_cache\r
1229 \r
1230         local count = 0\r
1231         local prev, any\r
1232 \r
1233         for x = pos1.x, pos2.x do\r
1234         for z = pos1.z, pos2.z do\r
1235                 prev = false\r
1236                 any = false\r
1237                 -- first pass: remove floating nodes that would be left over\r
1238                 for y = pos1.y, pos2.y do\r
1239                         local n = minetest.get_node({x=x, y=y, z=z}).name\r
1240                         if plants[n] then\r
1241                                 prev = true\r
1242                                 any = true\r
1243                         elseif prev then\r
1244                                 local def = minetest.registered_nodes[n] or {}\r
1245                                 local groups = def.groups or {}\r
1246                                 if groups.attached_node or (def.buildable_to and groups.falling_node) then\r
1247                                         minetest.remove_node({x=x, y=y, z=z})\r
1248                                         count = count + 1\r
1249                                 else\r
1250                                         prev = false\r
1251                                 end\r
1252                         end\r
1253                 end\r
1254 \r
1255                 -- second pass: remove plants, top-to-bottom to avoid item drops\r
1256                 if any then\r
1257                         for y = pos2.y, pos1.y, -1 do\r
1258                                 local n = minetest.get_node({x=x, y=y, z=z}).name\r
1259                                 if plants[n] then\r
1260                                         minetest.remove_node({x=x, y=y, z=z})\r
1261                                         count = count + 1\r
1262                                 end\r
1263                         end\r
1264                 end\r
1265         end\r
1266         end\r
1267 \r
1268         return count\r
1269 end\r
1270 \r
1271 worldedit.register_command("clearcut", {\r
1272         params = "",\r
1273         description = "Remove any plant, tree or foilage-like nodes in the selected region",\r
1274         privs = {worldedit=true},\r
1275         require_pos = 2,\r
1276         nodes_needed = check_region,\r
1277         func = function(name)\r
1278                 local pos1, pos2 = worldedit.sort_pos(worldedit.pos1[name], worldedit.pos2[name])\r
1279                 local count = clearcut(pos1, pos2)\r
1280                 worldedit.player_notify(name, count .. " nodes removed")\r
1281         end,\r
1282 })\r
1283 \r
1284 worldedit.register_command("hide", {\r
1285         params = "",\r
1286         description = "Hide all nodes in the current WorldEdit region non-destructively",\r
1287         privs = {worldedit=true},\r
1288         require_pos = 2,\r
1289         nodes_needed = check_region,\r
1290         func = function(name)\r
1291                 local count = worldedit.hide(worldedit.pos1[name], worldedit.pos2[name])\r
1292                 worldedit.player_notify(name, count .. " nodes hidden")\r
1293         end,\r
1294 })\r
1295 \r
1296 worldedit.register_command("suppress", {\r
1297         params = "<node>",\r
1298         description = "Suppress all <node> in the current WorldEdit region non-destructively",\r
1299         privs = {worldedit=true},\r
1300         require_pos = 2,\r
1301         parse = function(param)\r
1302                 local node = worldedit.normalize_nodename(param)\r
1303                 if not node then\r
1304                         return false, "invalid node name: " .. param\r
1305                 end\r
1306                 return true, node\r
1307         end,\r
1308         nodes_needed = check_region,\r
1309         func = function(name, node)\r
1310                 local count = worldedit.suppress(worldedit.pos1[name], worldedit.pos2[name], node)\r
1311                 worldedit.player_notify(name, count .. " nodes suppressed")\r
1312         end,\r
1313 })\r
1314 \r
1315 worldedit.register_command("highlight", {\r
1316         params = "<node>",\r
1317         description = "Highlight <node> in the current WorldEdit region by hiding everything else non-destructively",\r
1318         privs = {worldedit=true},\r
1319         require_pos = 2,\r
1320         parse = function(param)\r
1321                 local node = worldedit.normalize_nodename(param)\r
1322                 if not node then\r
1323                         return false, "invalid node name: " .. param\r
1324                 end\r
1325                 return true, node\r
1326         end,\r
1327         nodes_needed = check_region,\r
1328         func = function(name, node)\r
1329                 local count = worldedit.highlight(worldedit.pos1[name], worldedit.pos2[name], node)\r
1330                 worldedit.player_notify(name, count .. " nodes highlighted")\r
1331         end,\r
1332 })\r
1333 \r
1334 worldedit.register_command("restore", {\r
1335         params = "",\r
1336         description = "Restores nodes hidden with WorldEdit in the current WorldEdit region",\r
1337         privs = {worldedit=true},\r
1338         require_pos = 2,\r
1339         nodes_needed = check_region,\r
1340         func = function(name)\r
1341                 local count = worldedit.restore(worldedit.pos1[name], worldedit.pos2[name])\r
1342                 worldedit.player_notify(name, count .. " nodes restored")\r
1343         end,\r
1344 })\r
1345 \r
1346 local function detect_misaligned_schematic(name, pos1, pos2)\r
1347         pos1, pos2 = worldedit.sort_pos(pos1, pos2)\r
1348         -- Check that allocate/save can position the schematic correctly\r
1349         -- The expected behaviour is that the (0,0,0) corner of the schematic stays\r
1350         -- sat pos1, this only works when the minimum position is actually present\r
1351         -- in the schematic.\r
1352         local node = minetest.get_node(pos1)\r
1353         local have_node_at_origin = node.name ~= "air" and node.name ~= "ignore"\r
1354         if not have_node_at_origin then\r
1355                 worldedit.player_notify(name,\r
1356                         "Warning: The schematic contains excessive free space and WILL be "..\r
1357                         "misaligned when allocated or loaded. To avoid this, shrink your "..\r
1358                         "area to cover exactly the nodes to be saved."\r
1359                 )\r
1360         end\r
1361 end\r
1362 \r
1363 worldedit.register_command("save", {\r
1364         params = "<file>",\r
1365         description = "Save the current WorldEdit region to \"(world folder)/schems/<file>.we\"",\r
1366         privs = {worldedit=true},\r
1367         require_pos = 2,\r
1368         parse = function(param)\r
1369                 if param == "" then\r
1370                         return false\r
1371                 end\r
1372                 if not check_filename(param) then\r
1373                         return false, "Disallowed file name: " .. param\r
1374                 end\r
1375                 return true, param\r
1376         end,\r
1377         nodes_needed = check_region,\r
1378         func = function(name, param)\r
1379                 local result, count = worldedit.serialize(worldedit.pos1[name],\r
1380                                 worldedit.pos2[name])\r
1381                 detect_misaligned_schematic(name, worldedit.pos1[name], worldedit.pos2[name])\r
1382 \r
1383                 local path = minetest.get_worldpath() .. "/schems"\r
1384                 -- Create directory if it does not already exist\r
1385                 minetest.mkdir(path)\r
1386 \r
1387                 local filename = path .. "/" .. param .. ".we"\r
1388                 local file, err = io.open(filename, "wb")\r
1389                 if err ~= nil then\r
1390                         worldedit.player_notify(name, "Could not save file to \"" .. filename .. "\"")\r
1391                         return\r
1392                 end\r
1393                 file:write(result)\r
1394                 file:flush()\r
1395                 file:close()\r
1396 \r
1397                 worldedit.player_notify(name, count .. " nodes saved")\r
1398         end,\r
1399 })\r
1400 \r
1401 worldedit.register_command("allocate", {\r
1402         params = "<file>",\r
1403         description = "Set the region defined by nodes from \"(world folder)/schems/<file>.we\" as the current WorldEdit region",\r
1404         privs = {worldedit=true},\r
1405         require_pos = 1,\r
1406         parse = function(param)\r
1407                 if param == "" then\r
1408                         return false\r
1409                 end\r
1410                 if not check_filename(param) then\r
1411                         return false, "Disallowed file name: " .. param\r
1412                 end\r
1413                 return true, param\r
1414         end,\r
1415         func = function(name, param)\r
1416                 local pos = worldedit.pos1[name]\r
1417 \r
1418                 local filename = minetest.get_worldpath() .. "/schems/" .. param .. ".we"\r
1419                 local file, err = io.open(filename, "rb")\r
1420                 if err ~= nil then\r
1421                         worldedit.player_notify(name, "could not open file \"" .. filename .. "\"")\r
1422                         return\r
1423                 end\r
1424                 local value = file:read("*a")\r
1425                 file:close()\r
1426 \r
1427                 local version = worldedit.read_header(value)\r
1428                 if version == nil or version == 0 then\r
1429                         worldedit.player_notify(name, "File is invalid!")\r
1430                         return\r
1431                 elseif version > worldedit.LATEST_SERIALIZATION_VERSION then\r
1432                         worldedit.player_notify(name, "File was created with newer version of WorldEdit!")\r
1433                         return\r
1434                 end\r
1435                 local nodepos1, nodepos2, count = worldedit.allocate(pos, value)\r
1436 \r
1437                 if not nodepos1 then\r
1438                         worldedit.player_notify(name, "Schematic empty, nothing allocated")\r
1439                         return\r
1440                 end\r
1441 \r
1442                 worldedit.pos1[name] = nodepos1\r
1443                 worldedit.pos2[name] = nodepos2\r
1444                 worldedit.marker_update(name)\r
1445 \r
1446                 worldedit.player_notify(name, count .. " nodes allocated")\r
1447         end,\r
1448 })\r
1449 \r
1450 worldedit.register_command("load", {\r
1451         params = "<file>",\r
1452         description = "Load nodes from \"(world folder)/schems/<file>[.we[m]]\" with position 1 of the current WorldEdit region as the origin",\r
1453         privs = {worldedit=true},\r
1454         require_pos = 1,\r
1455         parse = function(param)\r
1456                 if param == "" then\r
1457                         return false\r
1458                 end\r
1459                 if not check_filename(param) then\r
1460                         return false, "Disallowed file name: " .. param\r
1461                 end\r
1462                 return true, param\r
1463         end,\r
1464         func = function(name, param)\r
1465                 local pos = worldedit.pos1[name]\r
1466 \r
1467                 if param == "" then\r
1468                         worldedit.player_notify(name, "invalid usage: " .. param)\r
1469                         return\r
1470                 end\r
1471                 if not string.find(param, "^[%w \t.,+-_=!@#$%%^&*()%[%]{};'\"]+$") then\r
1472                         worldedit.player_notify(name, "invalid file name: " .. param)\r
1473                         return\r
1474                 end\r
1475 \r
1476                 --find the file in the world path\r
1477                 local testpaths = {\r
1478                         minetest.get_worldpath() .. "/schems/" .. param,\r
1479                         minetest.get_worldpath() .. "/schems/" .. param .. ".we",\r
1480                         minetest.get_worldpath() .. "/schems/" .. param .. ".wem",\r
1481                 }\r
1482                 local file, err\r
1483                 for index, path in ipairs(testpaths) do\r
1484                         file, err = io.open(path, "rb")\r
1485                         if not err then\r
1486                                 break\r
1487                         end\r
1488                 end\r
1489                 if err then\r
1490                         worldedit.player_notify(name, "could not open file \"" .. param .. "\"")\r
1491                         return\r
1492                 end\r
1493                 local value = file:read("*a")\r
1494                 file:close()\r
1495 \r
1496                 local version = worldedit.read_header(value)\r
1497                 if version == nil or version == 0 then\r
1498                         worldedit.player_notify(name, "File is invalid!")\r
1499                         return\r
1500                 elseif version > worldedit.LATEST_SERIALIZATION_VERSION then\r
1501                         worldedit.player_notify(name, "File was created with newer version of WorldEdit!")\r
1502                         return\r
1503                 end\r
1504 \r
1505                 local count = worldedit.deserialize(pos, value)\r
1506 \r
1507                 worldedit.player_notify(name, count .. " nodes loaded")\r
1508         end,\r
1509 })\r
1510 \r
1511 worldedit.register_command("lua", {\r
1512         params = "<code>",\r
1513         description = "Executes <code> as a Lua chunk in the global namespace",\r
1514         privs = {worldedit=true, server=true},\r
1515         parse = function(param)\r
1516                 return true, param\r
1517         end,\r
1518         func = function(name, param)\r
1519                 local err = worldedit.lua(param)\r
1520                 if err then\r
1521                         worldedit.player_notify(name, "code error: " .. err)\r
1522                         minetest.log("action", name.." tried to execute "..param)\r
1523                 else\r
1524                         worldedit.player_notify(name, "code successfully executed", false)\r
1525                         minetest.log("action", name.." executed "..param)\r
1526                 end\r
1527         end,\r
1528 })\r
1529 \r
1530 worldedit.register_command("luatransform", {\r
1531         params = "<code>",\r
1532         description = "Executes <code> as a Lua chunk in the global namespace with the variable pos available, for each node in the current WorldEdit region",\r
1533         privs = {worldedit=true, server=true},\r
1534         require_pos = 2,\r
1535         parse = function(param)\r
1536                 return true, param\r
1537         end,\r
1538         nodes_needed = check_region,\r
1539         func = function(name, param)\r
1540                 local err = worldedit.luatransform(worldedit.pos1[name], worldedit.pos2[name], param)\r
1541                 if err then\r
1542                         worldedit.player_notify(name, "code error: " .. err, false)\r
1543                         minetest.log("action", name.." tried to execute luatransform "..param)\r
1544                 else\r
1545                         worldedit.player_notify(name, "code successfully executed", false)\r
1546                         minetest.log("action", name.." executed luatransform "..param)\r
1547                 end\r
1548         end,\r
1549 })\r
1550 \r
1551 worldedit.register_command("mtschemcreate", {\r
1552         params = "<file>",\r
1553         description = "Save the current WorldEdit region using the Minetest "..\r
1554                 "Schematic format to \"(world folder)/schems/<filename>.mts\"",\r
1555         privs = {worldedit=true},\r
1556         require_pos = 2,\r
1557         parse = function(param)\r
1558                 if param == "" then\r
1559                         return false\r
1560                 end\r
1561                 if not check_filename(param) then\r
1562                         return false, "Disallowed file name: " .. param\r
1563                 end\r
1564                 return true, param\r
1565         end,\r
1566         nodes_needed = check_region,\r
1567         func = function(name, param)\r
1568                 local path = minetest.get_worldpath() .. "/schems"\r
1569                 -- Create directory if it does not already exist\r
1570                 minetest.mkdir(path)\r
1571 \r
1572                 local filename = path .. "/" .. param .. ".mts"\r
1573                 local ret = minetest.create_schematic(worldedit.pos1[name],\r
1574                                 worldedit.pos2[name], worldedit.prob_list[name],\r
1575                                 filename)\r
1576                 if ret == nil then\r
1577                         worldedit.player_notify(name, "Failed to create Minetest schematic")\r
1578                 else\r
1579                         worldedit.player_notify(name, "Saved Minetest schematic to " .. param)\r
1580                 end\r
1581                 worldedit.prob_list[name] = {}\r
1582         end,\r
1583 })\r
1584 \r
1585 worldedit.register_command("mtschemplace", {\r
1586         params = "<file>",\r
1587         description = "Load nodes from \"(world folder)/schems/<file>.mts\" with position 1 of the current WorldEdit region as the origin",\r
1588         privs = {worldedit=true},\r
1589         require_pos = 1,\r
1590         parse = function(param)\r
1591                 if param == "" then\r
1592                         return false\r
1593                 end\r
1594                 if not check_filename(param) then\r
1595                         return false, "Disallowed file name: " .. param\r
1596                 end\r
1597                 return true, param\r
1598         end,\r
1599         func = function(name, param)\r
1600                 local pos = worldedit.pos1[name]\r
1601 \r
1602                 local path = minetest.get_worldpath() .. "/schems/" .. param .. ".mts"\r
1603                 if minetest.place_schematic(pos, path) == nil then\r
1604                         worldedit.player_notify(name, "failed to place Minetest schematic")\r
1605                 else\r
1606                         worldedit.player_notify(name, "placed Minetest schematic " .. param ..\r
1607                                 " at " .. minetest.pos_to_string(pos))\r
1608                 end\r
1609         end,\r
1610 })\r
1611 \r
1612 worldedit.register_command("mtschemprob", {\r
1613         params = "start/finish/get",\r
1614         description = "Begins node probability entry for Minetest schematics, gets the nodes that have probabilities set, or ends node probability entry",\r
1615         privs = {worldedit=true},\r
1616         parse = function(param)\r
1617                 if param ~= "start" and param ~= "finish" and param ~= "get" then\r
1618                         return false, "unknown subcommand: " .. param\r
1619                 end\r
1620                 return true, param\r
1621         end,\r
1622         func = function(name, param)\r
1623                 if param == "start" then --start probability setting\r
1624                         worldedit.set_pos[name] = "prob"\r
1625                         worldedit.prob_list[name] = {}\r
1626                         worldedit.player_notify(name, "select Minetest schematic probability values by punching nodes")\r
1627                 elseif param == "finish" then --finish probability setting\r
1628                         worldedit.set_pos[name] = nil\r
1629                         worldedit.player_notify(name, "finished Minetest schematic probability selection")\r
1630                 elseif param == "get" then --get all nodes that had probabilities set on them\r
1631                         local text = ""\r
1632                         local problist = worldedit.prob_list[name]\r
1633                         if problist == nil then\r
1634                                 return\r
1635                         end\r
1636                         for k,v in pairs(problist) do\r
1637                                 local prob = math.floor(((v.prob / 256) * 100) * 100 + 0.5) / 100\r
1638                                 text = text .. minetest.pos_to_string(v.pos) .. ": " .. prob .. "% | "\r
1639                         end\r
1640                         worldedit.player_notify(name, "currently set node probabilities:")\r
1641                         worldedit.player_notify(name, text)\r
1642                 end\r
1643         end,\r
1644 })\r
1645 \r
1646 minetest.register_on_player_receive_fields(function(player, formname, fields)\r
1647         if formname == "prob_val_enter" and not (fields.text == "" or fields.text == nil) then\r
1648                 local name = player:get_player_name()\r
1649                 local prob_entry = {pos=worldedit.prob_pos[name], prob=tonumber(fields.text)}\r
1650                 local index = table.getn(worldedit.prob_list[name]) + 1\r
1651                 worldedit.prob_list[name][index] = prob_entry\r
1652         end\r
1653 end)\r
1654 \r
1655 worldedit.register_command("clearobjects", {\r
1656         params = "",\r
1657         description = "Clears all objects within the WorldEdit region",\r
1658         privs = {worldedit=true},\r
1659         require_pos = 2,\r
1660         nodes_needed = check_region,\r
1661         func = function(name)\r
1662                 local count = worldedit.clear_objects(worldedit.pos1[name], worldedit.pos2[name])\r
1663                 worldedit.player_notify(name, count .. " objects cleared")\r
1664         end,\r
1665 })\r