1 --- Generic node manipulations.
\r
2 -- @module worldedit.manipulations
\r
4 local mh = worldedit.manip_helpers
\r
7 --- Sets a region to `node_names`.
\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
15 local manip, area = mh.init(pos1, pos2)
\r
16 local data = mh.get_empty_data(area)
\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
24 else -- Several types of nodes specified
\r
26 for i, v in ipairs(node_names) do
\r
27 node_ids[i] = minetest.get_content_id(v)
\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
36 mh.finish(manip, data)
\r
38 return worldedit.volume(pos1, pos2)
\r
42 --- Replaces all instances of `search_node` with `replace_node` in a region.
\r
43 -- When `inverse` is `true`, replaces all instances that are NOT `search_node`.
\r
44 -- @return The number of nodes replaced.
\r
45 function worldedit.replace(pos1, pos2, search_node, replace_node, inverse)
\r
46 local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
\r
48 local manip, area = mh.init(pos1, pos2)
\r
49 local data = manip:get_data()
\r
51 local search_id = minetest.get_content_id(search_node)
\r
52 local replace_id = minetest.get_content_id(replace_node)
\r
56 --- TODO: This could be shortened by checking `inverse` in the loop,
\r
57 -- but that would have a speed penalty. Is the penalty big enough
\r
60 for i in area:iterp(pos1, pos2) do
\r
61 if data[i] == search_id then
\r
62 data[i] = replace_id
\r
67 for i in area:iterp(pos1, pos2) do
\r
68 if data[i] ~= search_id then
\r
69 data[i] = replace_id
\r
75 mh.finish(manip, data)
\r
81 --- Duplicates a region `amount` times with offset vector `direction`.
\r
82 -- Stacking is spread across server steps, one copy per step.
\r
83 -- @return The number of nodes stacked.
\r
84 function worldedit.stack2(pos1, pos2, direction, amount, finished)
\r
86 local translated = {x=0, y=0, z=0}
\r
87 local function next_one()
\r
90 translated.x = translated.x + direction.x
\r
91 translated.y = translated.y + direction.y
\r
92 translated.z = translated.z + direction.z
\r
93 worldedit.copy2(pos1, pos2, translated)
\r
94 minetest.after(0, next_one)
\r
102 return worldedit.volume(pos1, pos2) * amount
\r
106 --- Copies a region along `axis` by `amount` nodes.
\r
109 -- @param axis Axis ("x", "y", or "z")
\r
111 -- @return The number of nodes copied.
\r
112 function worldedit.copy(pos1, pos2, axis, amount)
\r
113 local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
\r
115 worldedit.keep_loaded(pos1, pos2)
\r
117 local get_node, get_meta, set_node = minetest.get_node,
\r
118 minetest.get_meta, minetest.set_node
\r
119 -- Copy things backwards when negative to avoid corruption.
\r
120 -- FIXME: Lots of code duplication here.
\r
124 while pos.x <= pos2.x do
\r
126 while pos.y <= pos2.y do
\r
128 while pos.z <= pos2.z do
\r
129 local node = get_node(pos) -- Obtain current node
\r
130 local meta = get_meta(pos):to_table() -- Get meta of current node
\r
131 local value = pos[axis] -- Store current position
\r
132 pos[axis] = value + amount -- Move along axis
\r
133 set_node(pos, node) -- Copy node to new position
\r
134 get_meta(pos):from_table(meta) -- Set metadata of new node
\r
135 pos[axis] = value -- Restore old position
\r
145 while pos.x >= pos1.x do
\r
147 while pos.y >= pos1.y do
\r
149 while pos.z >= pos1.z do
\r
150 local node = get_node(pos) -- Obtain current node
\r
151 local meta = get_meta(pos):to_table() -- Get meta of current node
\r
152 local value = pos[axis] -- Store current position
\r
153 pos[axis] = value + amount -- Move along axis
\r
154 set_node(pos, node) -- Copy node to new position
\r
155 get_meta(pos):from_table(meta) -- Set metadata of new node
\r
156 pos[axis] = value -- Restore old position
\r
164 return worldedit.volume(pos1, pos2)
\r
167 --- Copies a region by offset vector `off`.
\r
171 -- @return The number of nodes copied.
\r
172 function worldedit.copy2(pos1, pos2, off)
\r
173 local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
\r
175 worldedit.keep_loaded(pos1, pos2)
\r
177 local get_node, get_meta, set_node = minetest.get_node,
\r
178 minetest.get_meta, minetest.set_node
\r
181 while pos.x >= pos1.x do
\r
183 while pos.y >= pos1.y do
\r
185 while pos.z >= pos1.z do
\r
186 local node = get_node(pos) -- Obtain current node
\r
187 local meta = get_meta(pos):to_table() -- Get meta of current node
\r
188 local newpos = vector.add(pos, off) -- Calculate new position
\r
189 set_node(newpos, node) -- Copy node to new position
\r
190 get_meta(newpos):from_table(meta) -- Set metadata of new node
\r
197 return worldedit.volume(pos1, pos2)
\r
200 --- Moves a region along `axis` by `amount` nodes.
\r
201 -- @return The number of nodes moved.
\r
202 function worldedit.move(pos1, pos2, axis, amount)
\r
203 local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
\r
205 worldedit.keep_loaded(pos1, pos2)
\r
207 --- TODO: Move slice by slice using schematic method in the move axis
\r
208 -- and transfer metadata in separate loop (and if the amount is
\r
209 -- greater than the length in the axis, copy whole thing at a time and
\r
210 -- erase original after, using schematic method).
\r
211 local get_node, get_meta, set_node, remove_node = minetest.get_node,
\r
212 minetest.get_meta, minetest.set_node, minetest.remove_node
\r
213 -- Copy things backwards when negative to avoid corruption.
\r
214 --- FIXME: Lots of code duplication here.
\r
218 while pos.x <= pos2.x do
\r
220 while pos.y <= pos2.y do
\r
222 while pos.z <= pos2.z do
\r
223 local node = get_node(pos) -- Obtain current node
\r
224 local meta = get_meta(pos):to_table() -- Get metadata of current node
\r
225 remove_node(pos) -- Remove current node
\r
226 local value = pos[axis] -- Store current position
\r
227 pos[axis] = value + amount -- Move along axis
\r
228 set_node(pos, node) -- Move node to new position
\r
229 get_meta(pos):from_table(meta) -- Set metadata of new node
\r
230 pos[axis] = value -- Restore old position
\r
240 while pos.x >= pos1.x do
\r
242 while pos.y >= pos1.y do
\r
244 while pos.z >= pos1.z do
\r
245 local node = get_node(pos) -- Obtain current node
\r
246 local meta = get_meta(pos):to_table() -- Get metadata of current node
\r
247 remove_node(pos) -- Remove current node
\r
248 local value = pos[axis] -- Store current position
\r
249 pos[axis] = value + amount -- Move along axis
\r
250 set_node(pos, node) -- Move node to new position
\r
251 get_meta(pos):from_table(meta) -- Set metadata of new node
\r
252 pos[axis] = value -- Restore old position
\r
260 return worldedit.volume(pos1, pos2)
\r
264 --- Duplicates a region along `axis` `amount` times.
\r
265 -- Stacking is spread across server steps, one copy per step.
\r
268 -- @param axis Axis direction, "x", "y", or "z".
\r
270 -- @return The number of nodes stacked.
\r
271 function worldedit.stack(pos1, pos2, axis, count)
\r
272 local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
\r
273 local length = pos2[axis] - pos1[axis] + 1
\r
279 local copy = worldedit.copy
\r
281 function next_one()
\r
284 amount = amount + length
\r
285 copy(pos1, pos2, axis, amount)
\r
286 minetest.after(0, next_one)
\r
290 return worldedit.volume(pos1, pos2) * count
\r
294 --- Stretches a region by a factor of positive integers along the X, Y, and Z
\r
295 -- axes, respectively, with `pos1` as the origin.
\r
298 -- @param stretch_x Amount to stretch along X axis.
\r
299 -- @param stretch_y Amount to stretch along Y axis.
\r
300 -- @param stretch_z Amount to stretch along Z axis.
\r
301 -- @return The number of nodes scaled.
\r
302 -- @return The new scaled position 1.
\r
303 -- @return The new scaled position 2.
\r
304 function worldedit.stretch(pos1, pos2, stretch_x, stretch_y, stretch_z)
\r
305 local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
\r
307 -- Prepare schematic of large node
\r
308 local get_node, get_meta, place_schematic = minetest.get_node,
\r
309 minetest.get_meta, minetest.place_schematic
\r
310 local placeholder_node = {name="", param1=255, param2=0}
\r
312 for i = 1, stretch_x * stretch_y * stretch_z do
\r
313 nodes[i] = placeholder_node
\r
315 local schematic = {size={x=stretch_x, y=stretch_y, z=stretch_z}, data=nodes}
\r
317 local size_x, size_y, size_z = stretch_x - 1, stretch_y - 1, stretch_z - 1
\r
320 x = pos1.x + (pos2.x - pos1.x) * stretch_x + size_x,
\r
321 y = pos1.y + (pos2.y - pos1.y) * stretch_y + size_y,
\r
322 z = pos1.z + (pos2.z - pos1.z) * stretch_z + size_z,
\r
324 worldedit.keep_loaded(pos1, new_pos2)
\r
326 local pos = {x=pos2.x, y=0, z=0}
\r
327 local big_pos = {x=0, y=0, z=0}
\r
328 while pos.x >= pos1.x do
\r
330 while pos.y >= pos1.y do
\r
332 while pos.z >= pos1.z do
\r
333 local node = get_node(pos) -- Get current node
\r
334 local meta = get_meta(pos):to_table() -- Get meta of current node
\r
336 -- Calculate far corner of the big node
\r
337 local pos_x = pos1.x + (pos.x - pos1.x) * stretch_x
\r
338 local pos_y = pos1.y + (pos.y - pos1.y) * stretch_y
\r
339 local pos_z = pos1.z + (pos.z - pos1.z) * stretch_z
\r
341 -- Create large node
\r
342 placeholder_node.name = node.name
\r
343 placeholder_node.param2 = node.param2
\r
344 big_pos.x, big_pos.y, big_pos.z = pos_x, pos_y, pos_z
\r
345 place_schematic(big_pos, schematic)
\r
347 -- Fill in large node meta
\r
348 if next(meta.fields) ~= nil or next(meta.inventory) ~= nil then
\r
349 -- Node has meta fields
\r
350 for x = 0, size_x do
\r
351 for y = 0, size_y do
\r
352 for z = 0, size_z do
\r
353 big_pos.x = pos_x + x
\r
354 big_pos.y = pos_y + y
\r
355 big_pos.z = pos_z + z
\r
356 -- Set metadata of new node
\r
357 get_meta(big_pos):from_table(meta)
\r
368 return worldedit.volume(pos1, pos2) * stretch_x * stretch_y * stretch_z, pos1, new_pos2
\r
372 --- Transposes a region between two axes.
\r
373 -- @return The number of nodes transposed.
\r
374 -- @return The new transposed position 1.
\r
375 -- @return The new transposed position 2.
\r
376 function worldedit.transpose(pos1, pos2, axis1, axis2)
\r
377 local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
\r
380 local extent1, extent2 = pos2[axis1] - pos1[axis1], pos2[axis2] - pos1[axis2]
\r
382 if extent1 > extent2 then
\r
383 compare = function(extent1, extent2)
\r
384 return extent1 > extent2
\r
387 compare = function(extent1, extent2)
\r
388 return extent1 < extent2
\r
392 -- Calculate the new position 2 after transposition
\r
393 local new_pos2 = {x=pos2.x, y=pos2.y, z=pos2.z}
\r
394 new_pos2[axis1] = pos1[axis1] + extent2
\r
395 new_pos2[axis2] = pos1[axis2] + extent1
\r
397 local upper_bound = {x=pos2.x, y=pos2.y, z=pos2.z}
\r
398 if upper_bound[axis1] < new_pos2[axis1] then upper_bound[axis1] = new_pos2[axis1] end
\r
399 if upper_bound[axis2] < new_pos2[axis2] then upper_bound[axis2] = new_pos2[axis2] end
\r
400 worldedit.keep_loaded(pos1, upper_bound)
\r
402 local pos = {x=pos1.x, y=0, z=0}
\r
403 local get_node, get_meta, set_node = minetest.get_node,
\r
404 minetest.get_meta, minetest.set_node
\r
405 while pos.x <= pos2.x do
\r
407 while pos.y <= pos2.y do
\r
409 while pos.z <= pos2.z do
\r
410 local extent1, extent2 = pos[axis1] - pos1[axis1], pos[axis2] - pos1[axis2]
\r
411 if compare(extent1, extent2) then -- Transpose only if below the diagonal
\r
412 local node1 = get_node(pos)
\r
413 local meta1 = get_meta(pos):to_table()
\r
414 local value1, value2 = pos[axis1], pos[axis2] -- Save position values
\r
415 pos[axis1], pos[axis2] = pos1[axis1] + extent2, pos1[axis2] + extent1 -- Swap axis extents
\r
416 local node2 = get_node(pos)
\r
417 local meta2 = get_meta(pos):to_table()
\r
418 set_node(pos, node1)
\r
419 get_meta(pos):from_table(meta1)
\r
420 pos[axis1], pos[axis2] = value1, value2 -- Restore position values
\r
421 set_node(pos, node2)
\r
422 get_meta(pos):from_table(meta2)
\r
430 return worldedit.volume(pos1, pos2), pos1, new_pos2
\r
434 --- Flips a region along `axis`.
\r
435 -- @return The number of nodes flipped.
\r
436 function worldedit.flip(pos1, pos2, axis)
\r
437 local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
\r
439 worldedit.keep_loaded(pos1, pos2)
\r
441 --- TODO: Flip the region slice by slice along the flip axis using schematic method.
\r
442 local pos = {x=pos1.x, y=0, z=0}
\r
443 local start = pos1[axis] + pos2[axis]
\r
444 pos2[axis] = pos1[axis] + math.floor((pos2[axis] - pos1[axis]) / 2)
\r
445 local get_node, get_meta, set_node = minetest.get_node,
\r
446 minetest.get_meta, minetest.set_node
\r
447 while pos.x <= pos2.x do
\r
449 while pos.y <= pos2.y do
\r
451 while pos.z <= pos2.z do
\r
452 local node1 = get_node(pos)
\r
453 local meta1 = get_meta(pos):to_table()
\r
454 local value = pos[axis] -- Save position
\r
455 pos[axis] = start - value -- Shift position
\r
456 local node2 = get_node(pos)
\r
457 local meta2 = get_meta(pos):to_table()
\r
458 set_node(pos, node1)
\r
459 get_meta(pos):from_table(meta1)
\r
460 pos[axis] = value -- Restore position
\r
461 set_node(pos, node2)
\r
462 get_meta(pos):from_table(meta2)
\r
469 return worldedit.volume(pos1, pos2)
\r
473 --- Rotates a region clockwise around an axis.
\r
476 -- @param axis Axis ("x", "y", or "z").
\r
477 -- @param angle Angle in degrees (90 degree increments only).
\r
478 -- @return The number of nodes rotated.
\r
479 -- @return The new first position.
\r
480 -- @return The new second position.
\r
481 function worldedit.rotate(pos1, pos2, axis, angle)
\r
482 local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
\r
484 local other1, other2 = worldedit.get_axis_others(axis)
\r
485 angle = angle % 360
\r
488 if angle == 90 then
\r
489 worldedit.flip(pos1, pos2, other1)
\r
490 count, pos1, pos2 = worldedit.transpose(pos1, pos2, other1, other2)
\r
491 elseif angle == 180 then
\r
492 worldedit.flip(pos1, pos2, other1)
\r
493 count = worldedit.flip(pos1, pos2, other2)
\r
494 elseif angle == 270 then
\r
495 worldedit.flip(pos1, pos2, other2)
\r
496 count, pos1, pos2 = worldedit.transpose(pos1, pos2, other1, other2)
\r
498 error("Only 90 degree increments are supported!")
\r
500 return count, pos1, pos2
\r
504 --- Rotates all oriented nodes in a region clockwise around the Y axis.
\r
507 -- @param angle Angle in degrees (90 degree increments only).
\r
508 -- @return The number of nodes oriented.
\r
509 -- TODO: Support 6D facedir rotation along arbitrary axis.
\r
510 function worldedit.orient(pos1, pos2, angle)
\r
511 local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
\r
512 local registered_nodes = minetest.registered_nodes
\r
514 local wallmounted = {
\r
515 [90] = {[0]=0, 1, 5, 4, 2, 3},
\r
516 [180] = {[0]=0, 1, 3, 2, 5, 4},
\r
517 [270] = {[0]=0, 1, 4, 5, 3, 2}
\r
520 [90] = {[0]=1, 2, 3, 0},
\r
521 [180] = {[0]=2, 3, 0, 1},
\r
522 [270] = {[0]=3, 0, 1, 2}
\r
525 angle = angle % 360
\r
529 if angle % 90 ~= 0 then
\r
530 error("Only 90 degree increments are supported!")
\r
532 local wallmounted_substitution = wallmounted[angle]
\r
533 local facedir_substitution = facedir[angle]
\r
535 worldedit.keep_loaded(pos1, pos2)
\r
538 local set_node, get_node, get_meta, swap_node = minetest.set_node,
\r
539 minetest.get_node, minetest.get_meta, minetest.swap_node
\r
540 local pos = {x=pos1.x, y=0, z=0}
\r
541 while pos.x <= pos2.x do
\r
543 while pos.y <= pos2.y do
\r
545 while pos.z <= pos2.z do
\r
546 local node = get_node(pos)
\r
547 local def = registered_nodes[node.name]
\r
549 if def.paramtype2 == "wallmounted" then
\r
550 node.param2 = wallmounted_substitution[node.param2]
\r
551 local meta = get_meta(pos):to_table()
\r
552 set_node(pos, node)
\r
553 get_meta(pos):from_table(meta)
\r
555 elseif def.paramtype2 == "facedir" then
\r
556 node.param2 = facedir_substitution[node.param2]
\r
557 local meta = get_meta(pos):to_table()
\r
558 set_node(pos, node)
\r
559 get_meta(pos):from_table(meta)
\r
573 --- Attempts to fix the lighting in a region.
\r
574 -- @return The number of nodes updated.
\r
575 function worldedit.fixlight(pos1, pos2)
\r
576 local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
\r
578 local vmanip = minetest.get_voxel_manip(pos1, pos2)
\r
579 vmanip:write_to_map()
\r
580 vmanip:update_map() -- this updates the lighting
\r
582 return worldedit.volume(pos1, pos2)
\r
586 --- Clears all objects in a region.
\r
587 -- @return The number of objects cleared.
\r
588 function worldedit.clear_objects(pos1, pos2)
\r
589 pos1, pos2 = worldedit.sort_pos(pos1, pos2)
\r
591 worldedit.keep_loaded(pos1, pos2)
\r
593 -- Offset positions to include full nodes (positions are in the center of nodes)
\r
594 local pos1x, pos1y, pos1z = pos1.x - 0.5, pos1.y - 0.5, pos1.z - 0.5
\r
595 local pos2x, pos2y, pos2z = pos2.x + 0.5, pos2.y + 0.5, pos2.z + 0.5
\r
597 -- Center of region
\r
599 x = pos1x + ((pos2x - pos1x) / 2),
\r
600 y = pos1y + ((pos2y - pos1y) / 2),
\r
601 z = pos1z + ((pos2z - pos1z) / 2)
\r
603 -- Bounding sphere radius
\r
604 local radius = math.sqrt(
\r
605 (center.x - pos1x) ^ 2 +
\r
606 (center.y - pos1y) ^ 2 +
\r
607 (center.z - pos1z) ^ 2)
\r
609 for _, obj in pairs(minetest.get_objects_inside_radius(center, radius)) do
\r
610 local entity = obj:get_luaentity()
\r
611 -- Avoid players and WorldEdit entities
\r
612 if not obj:is_player() and (not entity or
\r
613 not entity.name:find("^worldedit:")) then
\r
614 local pos = obj:getpos()
\r
615 if pos.x >= pos1x and pos.x <= pos2x and
\r
616 pos.y >= pos1y and pos.y <= pos2y and
\r
617 pos.z >= pos1z and pos.z <= pos2z then
\r