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