]> git.lizzy.rs Git - worldedit.git/blob - worldedit/manipulations.lua
Fix deserialization of schematics with node names table
[worldedit.git] / worldedit / manipulations.lua
1 --- Generic node manipulations.\r
2 -- @module worldedit.manipulations\r
3 \r
4 local mh = worldedit.manip_helpers\r
5 \r
6 \r
7 --- Sets a region to `node_names`.\r
8 -- @param pos1\r
9 -- @param pos2\r
10 -- @param node_names Node name or list of node names.\r
11 -- @return The number of nodes set.\r
12 function worldedit.set(pos1, pos2, node_names)\r
13         pos1, pos2 = worldedit.sort_pos(pos1, pos2)\r
14 \r
15         local manip, area = mh.init(pos1, pos2)\r
16         local data = mh.get_empty_data(area)\r
17 \r
18         if type(node_names) == "string" then -- Only one type of node\r
19                 local id = minetest.get_content_id(node_names)\r
20                 -- Fill area with node\r
21                 for i in area:iterp(pos1, pos2) do\r
22                         data[i] = id\r
23                 end\r
24         else -- Several types of nodes specified\r
25                 local node_ids = {}\r
26                 for i, v in ipairs(node_names) do\r
27                         node_ids[i] = minetest.get_content_id(v)\r
28                 end\r
29                 -- Fill area randomly with nodes\r
30                 local id_count, rand = #node_ids, math.random\r
31                 for i in area:iterp(pos1, pos2) do\r
32                         data[i] = node_ids[rand(id_count)]\r
33                 end\r
34         end\r
35 \r
36         mh.finish(manip, data)\r
37 \r
38         return worldedit.volume(pos1, pos2)\r
39 end\r
40 \r
41 --- Sets param2 of a region.\r
42 -- @param pos1\r
43 -- @param pos2\r
44 -- @param param2 Value of param2 to set\r
45 -- @return The number of nodes set.\r
46 function worldedit.set_param2(pos1, pos2, param2)\r
47         pos1, pos2 = worldedit.sort_pos(pos1, pos2)\r
48 \r
49         local manip, area = mh.init(pos1, pos2)\r
50         local param2_data = manip:get_param2_data()\r
51 \r
52         -- Set param2 for every node\r
53         for i in area:iterp(pos1, pos2) do\r
54                 param2_data[i] = param2\r
55         end\r
56 \r
57         -- Update map\r
58         manip:set_param2_data(param2_data)\r
59         manip:write_to_map()\r
60         manip:update_map()\r
61 \r
62         return worldedit.volume(pos1, pos2)\r
63 end\r
64 \r
65 --- Replaces all instances of `search_node` with `replace_node` in a region.\r
66 -- When `inverse` is `true`, replaces all instances that are NOT `search_node`.\r
67 -- @return The number of nodes replaced.\r
68 function worldedit.replace(pos1, pos2, search_node, replace_node, inverse)\r
69         local pos1, pos2 = worldedit.sort_pos(pos1, pos2)\r
70 \r
71         local manip, area = mh.init(pos1, pos2)\r
72         local data = manip:get_data()\r
73 \r
74         local search_id = minetest.get_content_id(search_node)\r
75         local replace_id = minetest.get_content_id(replace_node)\r
76 \r
77         local count = 0\r
78 \r
79         if not inverse then\r
80                 for i in area:iterp(pos1, pos2) do\r
81                         if data[i] == search_id then\r
82                                 data[i] = replace_id\r
83                                 count = count + 1\r
84                         end\r
85                 end\r
86         else\r
87                 for i in area:iterp(pos1, pos2) do\r
88                         if data[i] ~= search_id then\r
89                                 data[i] = replace_id\r
90                                 count = count + 1\r
91                         end\r
92                 end\r
93         end\r
94 \r
95         mh.finish(manip, data)\r
96 \r
97         return count\r
98 end\r
99 \r
100 \r
101 local function deferred_execution(next_one, finished)\r
102         -- Allocate 100% of server step for execution (might lag a little)\r
103         local allocated_usecs =\r
104                 tonumber(minetest.settings:get("dedicated_server_step")) * 1000000\r
105         local function f()\r
106                 local deadline = minetest.get_us_time() + allocated_usecs\r
107                 repeat\r
108                         local is_done = next_one()\r
109                         if is_done then\r
110                                 if finished then\r
111                                         finished()\r
112                                 end\r
113                                 return\r
114                         end\r
115                 until minetest.get_us_time() >= deadline\r
116                 minetest.after(0, f)\r
117         end\r
118         f()\r
119 end\r
120 \r
121 --- Duplicates a region `amount` times with offset vector `direction`.\r
122 -- Stacking is spread across server steps.\r
123 -- @return The number of nodes stacked.\r
124 function worldedit.stack2(pos1, pos2, direction, amount, finished)\r
125         -- Protect arguments from external changes during execution\r
126         pos1 = table.copy(pos1)\r
127         pos2 = table.copy(pos2)\r
128         direction = table.copy(direction)\r
129 \r
130         local i = 0\r
131         local translated = {x=0, y=0, z=0}\r
132         local function step()\r
133                 translated.x = translated.x + direction.x\r
134                 translated.y = translated.y + direction.y\r
135                 translated.z = translated.z + direction.z\r
136                 worldedit.copy2(pos1, pos2, translated)\r
137                 i = i + 1\r
138                 return i >= amount\r
139         end\r
140         deferred_execution(step, finished)\r
141 \r
142         return worldedit.volume(pos1, pos2) * amount\r
143 end\r
144 \r
145 \r
146 --- Copies a region along `axis` by `amount` nodes.\r
147 -- @param pos1\r
148 -- @param pos2\r
149 -- @param axis Axis ("x", "y", or "z")\r
150 -- @param amount\r
151 -- @return The number of nodes copied.\r
152 function worldedit.copy(pos1, pos2, axis, amount)\r
153         local pos1, pos2 = worldedit.sort_pos(pos1, pos2)\r
154 \r
155         -- Decide if we need to copy stuff backwards (only applies to metadata)\r
156         local backwards = amount > 0 and amount < (pos2[axis] - pos1[axis] + 1)\r
157 \r
158         local off = {x=0, y=0, z=0}\r
159         off[axis] = amount\r
160         return worldedit.copy2(pos1, pos2, off, backwards)\r
161 end\r
162 \r
163 --- Copies a region by offset vector `off`.\r
164 -- @param pos1\r
165 -- @param pos2\r
166 -- @param off\r
167 -- @param meta_backwards (not officially part of API)\r
168 -- @return The number of nodes copied.\r
169 function worldedit.copy2(pos1, pos2, off, meta_backwards)\r
170         local pos1, pos2 = worldedit.sort_pos(pos1, pos2)\r
171 \r
172         local src_manip, src_area = mh.init(pos1, pos2)\r
173         local src_stride = {x=1, y=src_area.ystride, z=src_area.zstride}\r
174         local src_offset = vector.subtract(pos1, src_area.MinEdge)\r
175 \r
176         local dpos1 = vector.add(pos1, off)\r
177         local dpos2 = vector.add(pos2, off)\r
178         local dim = vector.add(vector.subtract(pos2, pos1), 1)\r
179 \r
180         local dst_manip, dst_area = mh.init(dpos1, dpos2)\r
181         local dst_stride = {x=1, y=dst_area.ystride, z=dst_area.zstride}\r
182         local dst_offset = vector.subtract(dpos1, dst_area.MinEdge)\r
183 \r
184         local function do_copy(src_data, dst_data)\r
185                 for z = 0, dim.z-1 do\r
186                         local src_index_z = (src_offset.z + z) * src_stride.z + 1 -- +1 for 1-based indexing\r
187                         local dst_index_z = (dst_offset.z + z) * dst_stride.z + 1\r
188                         for y = 0, dim.y-1 do\r
189                                 local src_index_y = src_index_z + (src_offset.y + y) * src_stride.y\r
190                                 local dst_index_y = dst_index_z + (dst_offset.y + y) * dst_stride.y\r
191                                 -- Copy entire row at once\r
192                                 local src_index_x = src_index_y + src_offset.x\r
193                                 local dst_index_x = dst_index_y + dst_offset.x\r
194                                 for x = 0, dim.x-1 do\r
195                                         dst_data[dst_index_x + x] = src_data[src_index_x + x]\r
196                                 end\r
197                         end\r
198                 end\r
199         end\r
200 \r
201         -- Copy node data\r
202         local src_data = src_manip:get_data()\r
203         local dst_data = dst_manip:get_data()\r
204         do_copy(src_data, dst_data)\r
205         dst_manip:set_data(dst_data)\r
206 \r
207         -- Copy param1\r
208         src_manip:get_light_data(src_data)\r
209         dst_manip:get_light_data(dst_data)\r
210         do_copy(src_data, dst_data)\r
211         dst_manip:set_light_data(dst_data)\r
212 \r
213         -- Copy param2\r
214         src_manip:get_param2_data(src_data)\r
215         dst_manip:get_param2_data(dst_data)\r
216         do_copy(src_data, dst_data)\r
217         dst_manip:set_param2_data(dst_data)\r
218 \r
219         mh.finish(dst_manip)\r
220         src_data = nil\r
221         dst_data = nil\r
222 \r
223         -- Copy metadata\r
224         local get_meta = minetest.get_meta\r
225         if meta_backwards then\r
226         for z = dim.z-1, 0, -1 do\r
227                 for y = dim.y-1, 0, -1 do\r
228                         for x = dim.x-1, 0, -1 do\r
229                                 local pos = {x=pos1.x+x, y=pos1.y+y, z=pos1.z+z}\r
230                                 local meta = get_meta(pos):to_table()\r
231                                 pos = vector.add(pos, off)\r
232                                 get_meta(pos):from_table(meta)\r
233                         end\r
234                 end\r
235         end\r
236         else\r
237         for z = 0, dim.z-1 do\r
238                 for y = 0, dim.y-1 do\r
239                         for x = 0, dim.x-1 do\r
240                                 local pos = {x=pos1.x+x, y=pos1.y+y, z=pos1.z+z}\r
241                                 local meta = get_meta(pos):to_table()\r
242                                 pos = vector.add(pos, off)\r
243                                 get_meta(pos):from_table(meta)\r
244                         end\r
245                 end\r
246         end\r
247         end\r
248 \r
249         return worldedit.volume(pos1, pos2)\r
250 end\r
251 \r
252 --- Deletes all node metadata in the region\r
253 -- @param pos1\r
254 -- @param pos2\r
255 -- @return The number of nodes that had their meta deleted.\r
256 function worldedit.delete_meta(pos1, pos2)\r
257         local pos1, pos2 = worldedit.sort_pos(pos1, pos2)\r
258 \r
259         local meta_positions = minetest.find_nodes_with_meta(pos1, pos2)\r
260         local get_meta = minetest.get_meta\r
261         for _, pos in ipairs(meta_positions) do\r
262                 get_meta(pos):from_table(nil)\r
263         end\r
264 \r
265         return #meta_positions\r
266 end\r
267 \r
268 --- Moves a region along `axis` by `amount` nodes.\r
269 -- @return The number of nodes moved.\r
270 function worldedit.move(pos1, pos2, axis, amount)\r
271         local pos1, pos2 = worldedit.sort_pos(pos1, pos2)\r
272 \r
273         local dim = vector.add(vector.subtract(pos2, pos1), 1)\r
274         local overlap = math.abs(amount) < dim[axis]\r
275         -- Decide if we need to copy metadata backwards\r
276         local backwards = overlap and amount > 0\r
277 \r
278         local function nuke_area(my_off, my_dim)\r
279                 if my_dim.x == 0 or my_dim.y == 0 or my_dim.z == 0 then\r
280                         return\r
281                 end\r
282                 local my_pos1 = vector.add(pos1, my_off)\r
283                 local my_pos2 = vector.subtract(vector.add(my_pos1, my_dim), 1)\r
284                 worldedit.set(my_pos1, my_pos2, "air")\r
285                 worldedit.delete_meta(my_pos1, my_pos2)\r
286         end\r
287 \r
288         -- Copy stuff to new location\r
289         local off = {x=0, y=0, z=0}\r
290         off[axis] = amount\r
291         worldedit.copy2(pos1, pos2, off, backwards)\r
292         -- Nuke old area\r
293         if not overlap then\r
294                 nuke_area({x=0, y=0, z=0}, dim)\r
295         else\r
296                 -- Source and destination region are overlapping, which means we can't\r
297                 -- blindly delete the [pos1, pos2] area\r
298                 local leftover = vector.new(dim) -- size of the leftover slice\r
299                 leftover[axis] = math.abs(amount)\r
300                 if amount > 0 then\r
301                         nuke_area({x=0, y=0, z=0}, leftover)\r
302                 else\r
303                         local top = {x=0, y=0, z=0} -- offset of the leftover slice from pos1\r
304                         top[axis] = dim[axis] - math.abs(amount)\r
305                         nuke_area(top, leftover)\r
306                 end\r
307         end\r
308 \r
309         return worldedit.volume(pos1, pos2)\r
310 end\r
311 \r
312 --- Duplicates a region along `axis` `amount` times.\r
313 -- Stacking is spread across server steps.\r
314 -- @param pos1\r
315 -- @param pos2\r
316 -- @param axis Axis direction, "x", "y", or "z".\r
317 -- @param count\r
318 -- @return The number of nodes stacked.\r
319 function worldedit.stack(pos1, pos2, axis, count, finished)\r
320         local pos1, pos2 = worldedit.sort_pos(pos1, pos2)\r
321         local length = pos2[axis] - pos1[axis] + 1\r
322         if count < 0 then\r
323                 count = -count\r
324                 length = -length\r
325         end\r
326 \r
327         local i, distance = 0, 0\r
328         local function step()\r
329                 distance = distance + length\r
330                 worldedit.copy(pos1, pos2, axis, distance)\r
331                 i = i + 1\r
332                 return i >= count\r
333         end\r
334         deferred_execution(step, finished)\r
335 \r
336         return worldedit.volume(pos1, pos2) * count\r
337 end\r
338 \r
339 \r
340 --- Stretches a region by a factor of positive integers along the X, Y, and Z\r
341 -- axes, respectively, with `pos1` as the origin.\r
342 -- @param pos1\r
343 -- @param pos2\r
344 -- @param stretch_x Amount to stretch along X axis.\r
345 -- @param stretch_y Amount to stretch along Y axis.\r
346 -- @param stretch_z Amount to stretch along Z axis.\r
347 -- @return The number of nodes scaled.\r
348 -- @return The new scaled position 1.\r
349 -- @return The new scaled position 2.\r
350 function worldedit.stretch(pos1, pos2, stretch_x, stretch_y, stretch_z)\r
351         local pos1, pos2 = worldedit.sort_pos(pos1, pos2)\r
352 \r
353         -- Prepare schematic of large node\r
354         local get_node, get_meta, place_schematic = minetest.get_node,\r
355                         minetest.get_meta, minetest.place_schematic\r
356         local placeholder_node = {name="", param1=255, param2=0}\r
357         local nodes = {}\r
358         for i = 1, stretch_x * stretch_y * stretch_z do\r
359                 nodes[i] = placeholder_node\r
360         end\r
361         local schematic = {size={x=stretch_x, y=stretch_y, z=stretch_z}, data=nodes}\r
362 \r
363         local size_x, size_y, size_z = stretch_x - 1, stretch_y - 1, stretch_z - 1\r
364 \r
365         local new_pos2 = {\r
366                 x = pos1.x + (pos2.x - pos1.x) * stretch_x + size_x,\r
367                 y = pos1.y + (pos2.y - pos1.y) * stretch_y + size_y,\r
368                 z = pos1.z + (pos2.z - pos1.z) * stretch_z + size_z,\r
369         }\r
370         worldedit.keep_loaded(pos1, new_pos2)\r
371 \r
372         local pos = {x=pos2.x, y=0, z=0}\r
373         local big_pos = {x=0, y=0, z=0}\r
374         while pos.x >= pos1.x do\r
375                 pos.y = pos2.y\r
376                 while pos.y >= pos1.y do\r
377                         pos.z = pos2.z\r
378                         while pos.z >= pos1.z do\r
379                                 local node = get_node(pos) -- Get current node\r
380                                 local meta = get_meta(pos):to_table() -- Get meta of current node\r
381 \r
382                                 -- Calculate far corner of the big node\r
383                                 local pos_x = pos1.x + (pos.x - pos1.x) * stretch_x\r
384                                 local pos_y = pos1.y + (pos.y - pos1.y) * stretch_y\r
385                                 local pos_z = pos1.z + (pos.z - pos1.z) * stretch_z\r
386 \r
387                                 -- Create large node\r
388                                 placeholder_node.name = node.name\r
389                                 placeholder_node.param2 = node.param2\r
390                                 big_pos.x, big_pos.y, big_pos.z = pos_x, pos_y, pos_z\r
391                                 place_schematic(big_pos, schematic)\r
392 \r
393                                 -- Fill in large node meta\r
394                                 if next(meta.fields) ~= nil or next(meta.inventory) ~= nil then\r
395                                         -- Node has meta fields\r
396                                         for x = 0, size_x do\r
397                                         for y = 0, size_y do\r
398                                         for z = 0, size_z do\r
399                                                 big_pos.x = pos_x + x\r
400                                                 big_pos.y = pos_y + y\r
401                                                 big_pos.z = pos_z + z\r
402                                                 -- Set metadata of new node\r
403                                                 get_meta(big_pos):from_table(meta)\r
404                                         end\r
405                                         end\r
406                                         end\r
407                                 end\r
408                                 pos.z = pos.z - 1\r
409                         end\r
410                         pos.y = pos.y - 1\r
411                 end\r
412                 pos.x = pos.x - 1\r
413         end\r
414         return worldedit.volume(pos1, pos2) * stretch_x * stretch_y * stretch_z, pos1, new_pos2\r
415 end\r
416 \r
417 \r
418 --- Transposes a region between two axes.\r
419 -- @return The number of nodes transposed.\r
420 -- @return The new transposed position 1.\r
421 -- @return The new transposed position 2.\r
422 function worldedit.transpose(pos1, pos2, axis1, axis2)\r
423         local pos1, pos2 = worldedit.sort_pos(pos1, pos2)\r
424 \r
425         local compare\r
426         local extent1, extent2 = pos2[axis1] - pos1[axis1], pos2[axis2] - pos1[axis2]\r
427 \r
428         if extent1 > extent2 then\r
429                 compare = function(extent1, extent2)\r
430                         return extent1 > extent2\r
431                 end\r
432         else\r
433                 compare = function(extent1, extent2)\r
434                         return extent1 < extent2\r
435                 end\r
436         end\r
437 \r
438         -- Calculate the new position 2 after transposition\r
439         local new_pos2 = {x=pos2.x, y=pos2.y, z=pos2.z}\r
440         new_pos2[axis1] = pos1[axis1] + extent2\r
441         new_pos2[axis2] = pos1[axis2] + extent1\r
442 \r
443         local upper_bound = {x=pos2.x, y=pos2.y, z=pos2.z}\r
444         if upper_bound[axis1] < new_pos2[axis1] then upper_bound[axis1] = new_pos2[axis1] end\r
445         if upper_bound[axis2] < new_pos2[axis2] then upper_bound[axis2] = new_pos2[axis2] end\r
446         worldedit.keep_loaded(pos1, upper_bound)\r
447 \r
448         local pos = {x=pos1.x, y=0, z=0}\r
449         local get_node, get_meta, set_node = minetest.get_node,\r
450                         minetest.get_meta, minetest.set_node\r
451         while pos.x <= pos2.x do\r
452                 pos.y = pos1.y\r
453                 while pos.y <= pos2.y do\r
454                         pos.z = pos1.z\r
455                         while pos.z <= pos2.z do\r
456                                 local extent1, extent2 = pos[axis1] - pos1[axis1], pos[axis2] - pos1[axis2]\r
457                                 if compare(extent1, extent2) then -- Transpose only if below the diagonal\r
458                                         local node1 = get_node(pos)\r
459                                         local meta1 = get_meta(pos):to_table()\r
460                                         local value1, value2 = pos[axis1], pos[axis2] -- Save position values\r
461                                         pos[axis1], pos[axis2] = pos1[axis1] + extent2, pos1[axis2] + extent1 -- Swap axis extents\r
462                                         local node2 = get_node(pos)\r
463                                         local meta2 = get_meta(pos):to_table()\r
464                                         set_node(pos, node1)\r
465                                         get_meta(pos):from_table(meta1)\r
466                                         pos[axis1], pos[axis2] = value1, value2 -- Restore position values\r
467                                         set_node(pos, node2)\r
468                                         get_meta(pos):from_table(meta2)\r
469                                 end\r
470                                 pos.z = pos.z + 1\r
471                         end\r
472                         pos.y = pos.y + 1\r
473                 end\r
474                 pos.x = pos.x + 1\r
475         end\r
476         return worldedit.volume(pos1, pos2), pos1, new_pos2\r
477 end\r
478 \r
479 \r
480 --- Flips a region along `axis`.\r
481 -- @return The number of nodes flipped.\r
482 function worldedit.flip(pos1, pos2, axis)\r
483         local pos1, pos2 = worldedit.sort_pos(pos1, pos2)\r
484 \r
485         worldedit.keep_loaded(pos1, pos2)\r
486 \r
487         --- TODO: Flip the region slice by slice along the flip axis using schematic method.\r
488         local pos = {x=pos1.x, y=0, z=0}\r
489         local start = pos1[axis] + pos2[axis]\r
490         pos2[axis] = pos1[axis] + math.floor((pos2[axis] - pos1[axis]) / 2)\r
491         local get_node, get_meta, set_node = minetest.get_node,\r
492                         minetest.get_meta, minetest.set_node\r
493         while pos.x <= pos2.x do\r
494                 pos.y = pos1.y\r
495                 while pos.y <= pos2.y do\r
496                         pos.z = pos1.z\r
497                         while pos.z <= pos2.z do\r
498                                 local node1 = get_node(pos)\r
499                                 local meta1 = get_meta(pos):to_table()\r
500                                 local value = pos[axis] -- Save position\r
501                                 pos[axis] = start - value -- Shift position\r
502                                 local node2 = get_node(pos)\r
503                                 local meta2 = get_meta(pos):to_table()\r
504                                 set_node(pos, node1)\r
505                                 get_meta(pos):from_table(meta1)\r
506                                 pos[axis] = value -- Restore position\r
507                                 set_node(pos, node2)\r
508                                 get_meta(pos):from_table(meta2)\r
509                                 pos.z = pos.z + 1\r
510                         end\r
511                         pos.y = pos.y + 1\r
512                 end\r
513                 pos.x = pos.x + 1\r
514         end\r
515         return worldedit.volume(pos1, pos2)\r
516 end\r
517 \r
518 \r
519 --- Rotates a region clockwise around an axis.\r
520 -- @param pos1\r
521 -- @param pos2\r
522 -- @param axis Axis ("x", "y", or "z").\r
523 -- @param angle Angle in degrees (90 degree increments only).\r
524 -- @return The number of nodes rotated.\r
525 -- @return The new first position.\r
526 -- @return The new second position.\r
527 function worldedit.rotate(pos1, pos2, axis, angle)\r
528         local pos1, pos2 = worldedit.sort_pos(pos1, pos2)\r
529 \r
530         local other1, other2 = worldedit.get_axis_others(axis)\r
531         angle = angle % 360\r
532 \r
533         local count\r
534         if angle == 90 then\r
535                 worldedit.flip(pos1, pos2, other1)\r
536                 count, pos1, pos2 = worldedit.transpose(pos1, pos2, other1, other2)\r
537         elseif angle == 180 then\r
538                 worldedit.flip(pos1, pos2, other1)\r
539                 count = worldedit.flip(pos1, pos2, other2)\r
540         elseif angle == 270 then\r
541                 worldedit.flip(pos1, pos2, other2)\r
542                 count, pos1, pos2 = worldedit.transpose(pos1, pos2, other1, other2)\r
543         else\r
544                 error("Only 90 degree increments are supported!")\r
545         end\r
546         return count, pos1, pos2\r
547 end\r
548 \r
549 \r
550 --- Rotates all oriented nodes in a region clockwise around the Y axis.\r
551 -- @param pos1\r
552 -- @param pos2\r
553 -- @param angle Angle in degrees (90 degree increments only).\r
554 -- @return The number of nodes oriented.\r
555 function worldedit.orient(pos1, pos2, angle)\r
556         local pos1, pos2 = worldedit.sort_pos(pos1, pos2)\r
557         local registered_nodes = minetest.registered_nodes\r
558 \r
559         local wallmounted = {\r
560                 [90]  = {0, 1, 5, 4, 2, 3, 0, 0},\r
561                 [180] = {0, 1, 3, 2, 5, 4, 0, 0},\r
562                 [270] = {0, 1, 4, 5, 3, 2, 0, 0}\r
563         }\r
564         local facedir = {\r
565                 [90]  = { 1,  2,  3,  0, 13, 14, 15, 12, 17, 18, 19, 16,\r
566                                   9, 10, 11,  8,  5,  6,  7,  4, 23, 20, 21, 22},\r
567                 [180] = { 2,  3,  0,  1, 10, 11,  8,  9,  6,  7,  4,  5,\r
568                                  18, 19, 16, 17, 14, 15, 12, 13, 22, 23, 20, 21},\r
569                 [270] = { 3,  0,  1,  2, 19, 16, 17, 18, 15, 12, 13, 14,\r
570                                   7,  4,  5,  6, 11,  8,  9, 10, 21, 22, 23, 20}\r
571         }\r
572 \r
573         angle = angle % 360\r
574         if angle == 0 then\r
575                 return 0\r
576         end\r
577         if angle % 90 ~= 0 then\r
578                 error("Only 90 degree increments are supported!")\r
579         end\r
580         local wallmounted_substitution = wallmounted[angle]\r
581         local facedir_substitution = facedir[angle]\r
582 \r
583         worldedit.keep_loaded(pos1, pos2)\r
584 \r
585         local count = 0\r
586         local get_node, swap_node = minetest.get_node, minetest.swap_node\r
587         local pos = {x=pos1.x, y=0, z=0}\r
588         while pos.x <= pos2.x do\r
589                 pos.y = pos1.y\r
590                 while pos.y <= pos2.y do\r
591                         pos.z = pos1.z\r
592                         while pos.z <= pos2.z do\r
593                                 local node = get_node(pos)\r
594                                 local def = registered_nodes[node.name]\r
595                                 if def then\r
596                                         local paramtype2 = def.paramtype2\r
597                                         if paramtype2 == "wallmounted" or\r
598                                                         paramtype2 == "colorwallmounted" then\r
599                                                 local orient = node.param2 % 8\r
600                                                 node.param2 = node.param2 - orient +\r
601                                                                 wallmounted_substitution[orient + 1]\r
602                                                 swap_node(pos, node)\r
603                                                 count = count + 1\r
604                                         elseif paramtype2 == "facedir" or\r
605                                                         paramtype2 == "colorfacedir" then\r
606                                                 local orient = node.param2 % 32\r
607                                                 node.param2 = node.param2 - orient +\r
608                                                                 facedir_substitution[orient + 1]\r
609                                                 swap_node(pos, node)\r
610                                                 count = count + 1\r
611                                         end\r
612                                 end\r
613                                 pos.z = pos.z + 1\r
614                         end\r
615                         pos.y = pos.y + 1\r
616                 end\r
617                 pos.x = pos.x + 1\r
618         end\r
619         return count\r
620 end\r
621 \r
622 \r
623 --- Attempts to fix the lighting in a region.\r
624 -- @return The number of nodes updated.\r
625 function worldedit.fixlight(pos1, pos2)\r
626         local pos1, pos2 = worldedit.sort_pos(pos1, pos2)\r
627 \r
628         local vmanip = minetest.get_voxel_manip(pos1, pos2)\r
629         vmanip:write_to_map()\r
630         vmanip:update_map() -- this updates the lighting\r
631 \r
632         return worldedit.volume(pos1, pos2)\r
633 end\r
634 \r
635 \r
636 --- Clears all objects in a region.\r
637 -- @return The number of objects cleared.\r
638 function worldedit.clear_objects(pos1, pos2)\r
639         pos1, pos2 = worldedit.sort_pos(pos1, pos2)\r
640 \r
641         worldedit.keep_loaded(pos1, pos2)\r
642 \r
643         local function should_delete(obj)\r
644                 -- Avoid players and WorldEdit entities\r
645                 if obj:is_player() then\r
646                         return false\r
647                 end\r
648                 local entity = obj:get_luaentity()\r
649                 return not entity or not entity.name:find("^worldedit:")\r
650         end\r
651 \r
652         -- Offset positions to include full nodes (positions are in the center of nodes)\r
653         local pos1x, pos1y, pos1z = pos1.x - 0.5, pos1.y - 0.5, pos1.z - 0.5\r
654         local pos2x, pos2y, pos2z = pos2.x + 0.5, pos2.y + 0.5, pos2.z + 0.5\r
655 \r
656         local count = 0\r
657         if minetest.get_objects_in_area then\r
658                 local objects = minetest.get_objects_in_area({x=pos1x, y=pos1y, z=pos1z},\r
659                         {x=pos2x, y=pos2y, z=pos2z})\r
660 \r
661                 for _, obj in pairs(objects) do\r
662                         if should_delete(obj) then\r
663                                 obj:remove()\r
664                                 count = count + 1\r
665                         end\r
666                 end\r
667                 return count\r
668         end\r
669 \r
670         -- Fallback implementation via get_objects_inside_radius\r
671         -- Center of region\r
672         local center = {\r
673                 x = pos1x + ((pos2x - pos1x) / 2),\r
674                 y = pos1y + ((pos2y - pos1y) / 2),\r
675                 z = pos1z + ((pos2z - pos1z) / 2)\r
676         }\r
677         -- Bounding sphere radius\r
678         local radius = math.sqrt(\r
679                         (center.x - pos1x) ^ 2 +\r
680                         (center.y - pos1y) ^ 2 +\r
681                         (center.z - pos1z) ^ 2)\r
682         for _, obj in pairs(minetest.get_objects_inside_radius(center, radius)) do\r
683                 if should_delete(obj) then\r
684                         local pos = obj:get_pos()\r
685                         if pos.x >= pos1x and pos.x <= pos2x and\r
686                                         pos.y >= pos1y and pos.y <= pos2y and\r
687                                         pos.z >= pos1z and pos.z <= pos2z then\r
688                                 -- Inside region\r
689                                 obj:remove()\r
690                                 count = count + 1\r
691                         end\r
692                 end\r
693         end\r
694         return count\r
695 end\r
696 \r