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