mirror of
https://github.com/Uberi/Minetest-WorldEdit.git
synced 2025-07-21 09:00:27 +02:00
Compare commits
19 Commits
1.3
...
79e5e64c44
Author | SHA1 | Date | |
---|---|---|---|
79e5e64c44 | |||
375fbf3c68 | |||
cc3aab00bc | |||
eff01bc8e7 | |||
099d5047bd | |||
7f7e928dd9 | |||
1a9f66f091 | |||
7a5d76a9bc | |||
5260f595c6 | |||
7a645eba05 | |||
9417f2bbf1 | |||
abc9efeeb8 | |||
c223ca4cec | |||
c8afa95542 | |||
670e421f57 | |||
770601dd5d | |||
2f2f5a7def | |||
7f87f1658e | |||
4378750498 |
15
.github/workflows/check.yml
vendored
Normal file
15
.github/workflows/check.yml
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
on: [push, pull_request]
|
||||
name: Check
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: apt
|
||||
run: sudo apt-get install -y luarocks
|
||||
- name: luacheck install
|
||||
run: luarocks install --local luacheck
|
||||
- name: luacheck run
|
||||
run: $HOME/.luarocks/bin/luacheck ./
|
11
.github/workflows/test.yml
vendored
Normal file
11
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
name: test
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Run tests
|
||||
run: MINETEST_VER=latest ./.util/run_tests.sh
|
8
.luacheckrc
Normal file
8
.luacheckrc
Normal file
@ -0,0 +1,8 @@
|
||||
read_globals = {"minetest", "vector", "VoxelArea", "ItemStack",
|
||||
"table",
|
||||
"unified_inventory", "sfinv", "smart_inventory", "inventory_plus"
|
||||
}
|
||||
globals = {"worldedit"}
|
||||
-- Ignore these errors until someone decides to fix them
|
||||
ignore = {"211", "212", "213", "311", "411", "412", "421", "422",
|
||||
"431", "432", "631"}
|
30
.util/run_tests.sh
Executable file
30
.util/run_tests.sh
Executable file
@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
tempdir=/tmp/mt
|
||||
confpath=$tempdir/minetest.conf
|
||||
worldpath=$tempdir/world
|
||||
|
||||
use_docker=y
|
||||
[ -x ../../bin/minetestserver ] && use_docker=
|
||||
|
||||
rm -rf $tempdir
|
||||
mkdir -p $worldpath
|
||||
# the docker image doesn't have devtest
|
||||
[ -n "$use_docker" ] || printf '%s\n' gameid=devtest >$worldpath/world.mt
|
||||
printf '%s\n' mg_name=singlenode '[end_of_params]' >$worldpath/map_meta.txt
|
||||
printf '%s\n' worldedit_run_tests=true max_forceloaded_blocks=9999 >$confpath
|
||||
|
||||
if [ -n "$use_docker" ]; then
|
||||
chmod -R 777 $tempdir
|
||||
docker run --rm -i \
|
||||
-v $confpath:/etc/minetest/minetest.conf \
|
||||
-v $tempdir:/var/lib/minetest/.minetest \
|
||||
-v "$PWD/worldedit":/var/lib/minetest/.minetest/world/worldmods/worldedit \
|
||||
registry.gitlab.com/minetest/minetest/server:${MINETEST_VER}
|
||||
else
|
||||
mkdir $worldpath/worldmods
|
||||
ln -s "$PWD/worldedit" $worldpath/worldmods/worldedit
|
||||
../../bin/minetestserver --config $confpath --world $worldpath --logfile /dev/null
|
||||
fi
|
||||
|
||||
test -f $worldpath/tests_ok || exit 1
|
||||
exit 0
|
@ -23,7 +23,7 @@ There is a nice installation guide over at the [Minetest Wiki](http://wiki.minet
|
||||
8. You should have a mod selection screen. Select the one named something like `Minetest-WorldEdit` by left clicking once and press the **Enable Modpack** button.
|
||||
9. Press the **Save** button. You can now use WorldEdit in that world. Repeat steps 7 to 9 to enable WorldEdit for other worlds too.
|
||||
|
||||
If you are having trouble, try asking for help in the [IRC channel](https://webchat.freenode.net/?channels=#minetest) (faster but may not always have helpers online)
|
||||
If you are having trouble, try asking for help in the [IRC channel](https://web.libera.chat/#minetest) (faster but may not always have helpers online)
|
||||
or ask on the [forum topic](https://forum.minetest.net/viewtopic.php?id=572) (slower but more likely to get help).
|
||||
|
||||
Usage
|
||||
|
@ -31,7 +31,7 @@ function worldedit.luatransform(pos1, pos2, code)
|
||||
|
||||
worldedit.keep_loaded(pos1, pos2)
|
||||
|
||||
local pos = {x=pos1.x, y=0, z=0}
|
||||
local pos = vector.new(pos1.x, 0, 0)
|
||||
while pos.x <= pos2.x do
|
||||
pos.y = pos1.y
|
||||
while pos.y <= pos2.y do
|
||||
|
@ -5,8 +5,8 @@
|
||||
-- `pos1` is less than or equal to the corresponding component of `pos2`.
|
||||
-- Returns the new positions.
|
||||
function worldedit.sort_pos(pos1, pos2)
|
||||
pos1 = {x=pos1.x, y=pos1.y, z=pos1.z}
|
||||
pos2 = {x=pos2.x, y=pos2.y, z=pos2.z}
|
||||
pos1 = vector.new(pos1.x, pos1.y, pos1.z)
|
||||
pos2 = vector.new(pos2.x, pos2.y, pos2.z)
|
||||
if pos1.x > pos2.x then
|
||||
pos2.x, pos1.x = pos1.x, pos2.x
|
||||
end
|
||||
|
@ -24,11 +24,14 @@ function worldedit.metasave(pos1, pos2, filename)
|
||||
return count
|
||||
end
|
||||
|
||||
function worldedit.metaload(originpos, filename)
|
||||
function worldedit.metaload(originpos, file_name)
|
||||
deprecated("load")
|
||||
filename = minetest.get_worldpath() .. "/schems/" .. file .. ".wem"
|
||||
local file, err = io.open(filename, "wb")
|
||||
if err then return 0 end
|
||||
local file_path = minetest.get_worldpath() ..
|
||||
"/schems/" .. file_name .. ".wem"
|
||||
local file, err = io.open(file_path, "wb")
|
||||
if err then
|
||||
return 0
|
||||
end
|
||||
local data = file:read("*a")
|
||||
return worldedit.deserialize(originpos, data)
|
||||
end
|
||||
|
@ -38,3 +38,7 @@ if minetest.settings:get_bool("log_mods") then
|
||||
print("[WorldEdit] Loaded!")
|
||||
end
|
||||
|
||||
if minetest.settings:get_bool("worldedit_run_tests") then
|
||||
dofile(path .. "/test.lua")
|
||||
minetest.after(0, worldedit.run_tests)
|
||||
end
|
||||
|
@ -128,7 +128,7 @@ function worldedit.stack2(pos1, pos2, direction, amount, finished)
|
||||
direction = table.copy(direction)
|
||||
|
||||
local i = 0
|
||||
local translated = {x=0, y=0, z=0}
|
||||
local translated = vector.new()
|
||||
local function step()
|
||||
translated.x = translated.x + direction.x
|
||||
translated.y = translated.y + direction.y
|
||||
@ -155,7 +155,7 @@ function worldedit.copy(pos1, pos2, axis, amount)
|
||||
-- Decide if we need to copy stuff backwards (only applies to metadata)
|
||||
local backwards = amount > 0 and amount < (pos2[axis] - pos1[axis] + 1)
|
||||
|
||||
local off = {x=0, y=0, z=0}
|
||||
local off = vector.new()
|
||||
off[axis] = amount
|
||||
return worldedit.copy2(pos1, pos2, off, backwards)
|
||||
end
|
||||
@ -170,7 +170,7 @@ function worldedit.copy2(pos1, pos2, off, meta_backwards)
|
||||
local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
|
||||
|
||||
local src_manip, src_area = mh.init(pos1, pos2)
|
||||
local src_stride = {x=1, y=src_area.ystride, z=src_area.zstride}
|
||||
local src_stride = vector.new(1, src_area.ystride, src_area.zstride)
|
||||
local src_offset = vector.subtract(pos1, src_area.MinEdge)
|
||||
|
||||
local dpos1 = vector.add(pos1, off)
|
||||
@ -178,7 +178,7 @@ function worldedit.copy2(pos1, pos2, off, meta_backwards)
|
||||
local dim = vector.add(vector.subtract(pos2, pos1), 1)
|
||||
|
||||
local dst_manip, dst_area = mh.init(dpos1, dpos2)
|
||||
local dst_stride = {x=1, y=dst_area.ystride, z=dst_area.zstride}
|
||||
local dst_stride = vector.new(1, dst_area.ystride, dst_area.zstride)
|
||||
local dst_offset = vector.subtract(dpos1, dst_area.MinEdge)
|
||||
|
||||
local function do_copy(src_data, dst_data)
|
||||
@ -226,7 +226,7 @@ function worldedit.copy2(pos1, pos2, off, meta_backwards)
|
||||
for z = dim.z-1, 0, -1 do
|
||||
for y = dim.y-1, 0, -1 do
|
||||
for x = dim.x-1, 0, -1 do
|
||||
local pos = {x=pos1.x+x, y=pos1.y+y, z=pos1.z+z}
|
||||
local pos = vector.new(pos1.x+x, pos1.y+y, pos1.z+z)
|
||||
local meta = get_meta(pos):to_table()
|
||||
pos = vector.add(pos, off)
|
||||
get_meta(pos):from_table(meta)
|
||||
@ -237,7 +237,7 @@ function worldedit.copy2(pos1, pos2, off, meta_backwards)
|
||||
for z = 0, dim.z-1 do
|
||||
for y = 0, dim.y-1 do
|
||||
for x = 0, dim.x-1 do
|
||||
local pos = {x=pos1.x+x, y=pos1.y+y, z=pos1.z+z}
|
||||
local pos = vector.new(pos1.x+x, pos1.y+y, pos1.z+z)
|
||||
local meta = get_meta(pos):to_table()
|
||||
pos = vector.add(pos, off)
|
||||
get_meta(pos):from_table(meta)
|
||||
@ -286,21 +286,21 @@ function worldedit.move(pos1, pos2, axis, amount)
|
||||
end
|
||||
|
||||
-- Copy stuff to new location
|
||||
local off = {x=0, y=0, z=0}
|
||||
local off = vector.new()
|
||||
off[axis] = amount
|
||||
worldedit.copy2(pos1, pos2, off, backwards)
|
||||
-- Nuke old area
|
||||
if not overlap then
|
||||
nuke_area({x=0, y=0, z=0}, dim)
|
||||
nuke_area(vector.new(), dim)
|
||||
else
|
||||
-- Source and destination region are overlapping, which means we can't
|
||||
-- blindly delete the [pos1, pos2] area
|
||||
local leftover = vector.new(dim) -- size of the leftover slice
|
||||
leftover[axis] = math.abs(amount)
|
||||
if amount > 0 then
|
||||
nuke_area({x=0, y=0, z=0}, leftover)
|
||||
nuke_area(vector.new(), leftover)
|
||||
else
|
||||
local top = {x=0, y=0, z=0} -- offset of the leftover slice from pos1
|
||||
local top = vector.new() -- offset of the leftover slice from pos1
|
||||
top[axis] = dim[axis] - math.abs(amount)
|
||||
nuke_area(top, leftover)
|
||||
end
|
||||
@ -358,7 +358,7 @@ function worldedit.stretch(pos1, pos2, stretch_x, stretch_y, stretch_z)
|
||||
for i = 1, stretch_x * stretch_y * stretch_z do
|
||||
nodes[i] = placeholder_node
|
||||
end
|
||||
local schematic = {size={x=stretch_x, y=stretch_y, z=stretch_z}, data=nodes}
|
||||
local schematic = {size=vector.new(stretch_x, stretch_y, stretch_z), data=nodes}
|
||||
|
||||
local size_x, size_y, size_z = stretch_x - 1, stretch_y - 1, stretch_z - 1
|
||||
|
||||
@ -369,8 +369,8 @@ function worldedit.stretch(pos1, pos2, stretch_x, stretch_y, stretch_z)
|
||||
}
|
||||
worldedit.keep_loaded(pos1, new_pos2)
|
||||
|
||||
local pos = {x=pos2.x, y=0, z=0}
|
||||
local big_pos = {x=0, y=0, z=0}
|
||||
local pos = vector.new(pos2.x, 0, 0)
|
||||
local big_pos = vector.new()
|
||||
while pos.x >= pos1.x do
|
||||
pos.y = pos2.y
|
||||
while pos.y >= pos1.y do
|
||||
@ -436,16 +436,16 @@ function worldedit.transpose(pos1, pos2, axis1, axis2)
|
||||
end
|
||||
|
||||
-- Calculate the new position 2 after transposition
|
||||
local new_pos2 = {x=pos2.x, y=pos2.y, z=pos2.z}
|
||||
local new_pos2 = vector.new(pos2)
|
||||
new_pos2[axis1] = pos1[axis1] + extent2
|
||||
new_pos2[axis2] = pos1[axis2] + extent1
|
||||
|
||||
local upper_bound = {x=pos2.x, y=pos2.y, z=pos2.z}
|
||||
local upper_bound = vector.new(pos2)
|
||||
if upper_bound[axis1] < new_pos2[axis1] then upper_bound[axis1] = new_pos2[axis1] end
|
||||
if upper_bound[axis2] < new_pos2[axis2] then upper_bound[axis2] = new_pos2[axis2] end
|
||||
worldedit.keep_loaded(pos1, upper_bound)
|
||||
|
||||
local pos = {x=pos1.x, y=0, z=0}
|
||||
local pos = vector.new(pos1.x, 0, 0)
|
||||
local get_node, get_meta, set_node = minetest.get_node,
|
||||
minetest.get_meta, minetest.set_node
|
||||
while pos.x <= pos2.x do
|
||||
@ -485,7 +485,7 @@ function worldedit.flip(pos1, pos2, axis)
|
||||
worldedit.keep_loaded(pos1, pos2)
|
||||
|
||||
--- TODO: Flip the region slice by slice along the flip axis using schematic method.
|
||||
local pos = {x=pos1.x, y=0, z=0}
|
||||
local pos = vector.new(pos1.x, 0, 0)
|
||||
local start = pos1[axis] + pos2[axis]
|
||||
pos2[axis] = pos1[axis] + math.floor((pos2[axis] - pos1[axis]) / 2)
|
||||
local get_node, get_meta, set_node = minetest.get_node,
|
||||
@ -584,7 +584,7 @@ function worldedit.orient(pos1, pos2, angle)
|
||||
|
||||
local count = 0
|
||||
local get_node, swap_node = minetest.get_node, minetest.swap_node
|
||||
local pos = {x=pos1.x, y=0, z=0}
|
||||
local pos = vector.new(pos1.x, 0, 0)
|
||||
while pos.x <= pos2.x do
|
||||
pos.y = pos1.y
|
||||
while pos.y <= pos2.y do
|
||||
@ -640,31 +640,50 @@ function worldedit.clear_objects(pos1, pos2)
|
||||
|
||||
worldedit.keep_loaded(pos1, pos2)
|
||||
|
||||
-- Offset positions to include full nodes (positions are in the center of nodes)
|
||||
local pos1x, pos1y, pos1z = pos1.x - 0.5, pos1.y - 0.5, pos1.z - 0.5
|
||||
local pos2x, pos2y, pos2z = pos2.x + 0.5, pos2.y + 0.5, pos2.z + 0.5
|
||||
local function should_delete(obj)
|
||||
-- Avoid players and WorldEdit entities
|
||||
if obj:is_player() then
|
||||
return false
|
||||
end
|
||||
local entity = obj:get_luaentity()
|
||||
return not entity or not entity.name:find("^worldedit:")
|
||||
end
|
||||
|
||||
-- Offset positions to include full nodes (positions are in the center of nodes)
|
||||
pos1 = vector.add(pos1, -0.5)
|
||||
pos2 = vector.add(pos1, 0.5)
|
||||
|
||||
local count = 0
|
||||
if minetest.get_objects_in_area then
|
||||
local objects = minetest.get_objects_in_area(pos1, pos2)
|
||||
|
||||
for _, obj in pairs(objects) do
|
||||
if should_delete(obj) then
|
||||
obj:remove()
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
-- Fallback implementation via get_objects_inside_radius
|
||||
-- Center of region
|
||||
local center = {
|
||||
x = pos1x + ((pos2x - pos1x) / 2),
|
||||
y = pos1y + ((pos2y - pos1y) / 2),
|
||||
z = pos1z + ((pos2z - pos1z) / 2)
|
||||
x = pos1.x + ((pos2.x - pos1.x) / 2),
|
||||
y = pos1.y + ((pos2.y - pos1.y) / 2),
|
||||
z = pos1.z + ((pos2.z - pos1.z) / 2)
|
||||
}
|
||||
-- Bounding sphere radius
|
||||
local radius = math.sqrt(
|
||||
(center.x - pos1x) ^ 2 +
|
||||
(center.y - pos1y) ^ 2 +
|
||||
(center.z - pos1z) ^ 2)
|
||||
local count = 0
|
||||
(center.x - pos1.x) ^ 2 +
|
||||
(center.y - pos1.y) ^ 2 +
|
||||
(center.z - pos1.z) ^ 2)
|
||||
for _, obj in pairs(minetest.get_objects_inside_radius(center, radius)) do
|
||||
local entity = obj:get_luaentity()
|
||||
-- Avoid players and WorldEdit entities
|
||||
if not obj:is_player() and (not entity or
|
||||
not entity.name:find("^worldedit:")) then
|
||||
if should_delete(obj) then
|
||||
local pos = obj:get_pos()
|
||||
if pos.x >= pos1x and pos.x <= pos2x and
|
||||
pos.y >= pos1y and pos.y <= pos2y and
|
||||
pos.z >= pos1z and pos.z <= pos2z then
|
||||
if pos.x >= pos1.x and pos.x <= pos2.x and
|
||||
pos.y >= pos1.y and pos.y <= pos2.y and
|
||||
pos.z >= pos1.z and pos.z <= pos2.z then
|
||||
-- Inside region
|
||||
obj:remove()
|
||||
count = count + 1
|
||||
|
@ -20,7 +20,7 @@ function worldedit.cube(pos, width, height, length, node_name, hollow)
|
||||
|
||||
-- Add cube
|
||||
local node_id = minetest.get_content_id(node_name)
|
||||
local stride = {x=1, y=area.ystride, z=area.zstride}
|
||||
local stride = vector.new(1, area.ystride, area.zstride)
|
||||
local offset = vector.subtract(basepos, area.MinEdge)
|
||||
local count = 0
|
||||
|
||||
@ -149,7 +149,7 @@ function worldedit.cylinder(pos, axis, length, radius1, radius2, node_name, holl
|
||||
end
|
||||
|
||||
-- Handle negative lengths
|
||||
local current_pos = {x=pos.x, y=pos.y, z=pos.z}
|
||||
local current_pos = vector.new(pos)
|
||||
if length < 0 then
|
||||
length = -length
|
||||
current_pos[axis] = current_pos[axis] - length
|
||||
@ -162,7 +162,7 @@ function worldedit.cylinder(pos, axis, length, radius1, radius2, node_name, holl
|
||||
|
||||
-- Add desired shape (anything inbetween cylinder & cone)
|
||||
local node_id = minetest.get_content_id(node_name)
|
||||
local stride = {x=1, y=area.ystride, z=area.zstride}
|
||||
local stride = vector.new(1, area.ystride, area.zstride)
|
||||
local offset = {
|
||||
x = current_pos.x - area.MinEdge.x,
|
||||
y = current_pos.y - area.MinEdge.y,
|
||||
@ -225,7 +225,7 @@ function worldedit.pyramid(pos, axis, height, node_name, hollow)
|
||||
|
||||
-- Add pyramid
|
||||
local node_id = minetest.get_content_id(node_name)
|
||||
local stride = {x=1, y=area.ystride, z=area.zstride}
|
||||
local stride = vector.new(1, area.ystride, area.zstride)
|
||||
local offset = {
|
||||
x = pos.x - area.MinEdge.x,
|
||||
y = pos.y - area.MinEdge.y,
|
||||
@ -271,7 +271,7 @@ function worldedit.spiral(pos, length, height, spacer, node_name)
|
||||
|
||||
-- Set up variables
|
||||
local node_id = minetest.get_content_id(node_name)
|
||||
local stride = {x=1, y=area.ystride, z=area.zstride}
|
||||
local stride = vector.new(1, area.ystride, area.zstride)
|
||||
local offset_x, offset_y, offset_z = pos.x - area.MinEdge.x, pos.y - area.MinEdge.y, pos.z - area.MinEdge.z
|
||||
local i = offset_z * stride.z + offset_y * stride.y + offset_x + 1
|
||||
|
||||
|
@ -66,7 +66,7 @@ function worldedit.serialize(pos1, pos2)
|
||||
has_meta[hash_node_position(meta_positions[i])] = true
|
||||
end
|
||||
|
||||
local pos = {x=pos1.x, y=0, z=0}
|
||||
local pos = vector.new(pos1.x, 0, 0)
|
||||
local count = 0
|
||||
local result = {}
|
||||
while pos.x <= pos2.x do
|
||||
@ -114,12 +114,15 @@ function worldedit.serialize(pos1, pos2)
|
||||
return LATEST_SERIALIZATION_HEADER .. result, count
|
||||
end
|
||||
|
||||
-- Contains code based on [table.save/table.load](http://lua-users.org/wiki/SaveTableToFile)
|
||||
-- by ChillCode, available under the MIT license.
|
||||
local function deserialize_workaround(content)
|
||||
local nodes
|
||||
if not jit then
|
||||
nodes = minetest.deserialize(content, true)
|
||||
local nodes, err
|
||||
if not minetest.global_exists("jit") then
|
||||
nodes, err = minetest.deserialize(content, true)
|
||||
elseif not content:match("^%s*return%s*{") then
|
||||
-- The data doesn't look like we expect it to so we can't apply the workaround.
|
||||
-- hope for the best
|
||||
minetest.log("warning", "WorldEdit: deserializing data but can't apply LuaJIT workaround")
|
||||
nodes, err = minetest.deserialize(content, true)
|
||||
else
|
||||
-- XXX: This is a filthy hack that works surprisingly well
|
||||
-- in LuaJIT, `minetest.deserialize` will fail due to the register limit
|
||||
@ -129,19 +132,28 @@ local function deserialize_workaround(content)
|
||||
local escaped = content:gsub("\\\\", "@@"):gsub("\\\"", "@@"):gsub("(\"[^\"]*\")", function(s) return string.rep("@", #s) end)
|
||||
local startpos, startpos1 = 1, 1
|
||||
local endpos
|
||||
local entry
|
||||
while true do -- go through each individual node entry (except the last)
|
||||
startpos, endpos = escaped:find("},%s*{", startpos)
|
||||
startpos, endpos = escaped:find("}%s*,%s*{", startpos)
|
||||
if not startpos then
|
||||
break
|
||||
end
|
||||
local current = content:sub(startpos1, startpos)
|
||||
local entry = minetest.deserialize("return " .. current, true)
|
||||
entry, err = minetest.deserialize("return " .. current, true)
|
||||
if not entry then
|
||||
break
|
||||
end
|
||||
table.insert(nodes, entry)
|
||||
startpos, startpos1 = endpos, endpos
|
||||
end
|
||||
local entry = minetest.deserialize("return " .. content:sub(startpos1), true) -- process the last entry
|
||||
if not err then
|
||||
entry = minetest.deserialize("return " .. content:sub(startpos1), true) -- process the last entry
|
||||
table.insert(nodes, entry)
|
||||
end
|
||||
end
|
||||
if err then
|
||||
minetest.log("warning", "WorldEdit: deserialize: " .. err)
|
||||
end
|
||||
return nodes
|
||||
end
|
||||
|
||||
@ -223,9 +235,7 @@ function worldedit.allocate_with_nodes(origin_pos, nodes)
|
||||
if y > pos2y then pos2y = y end
|
||||
if z > pos2z then pos2z = z end
|
||||
end
|
||||
local pos1 = {x=pos1x, y=pos1y, z=pos1z}
|
||||
local pos2 = {x=pos2x, y=pos2y, z=pos2z}
|
||||
return pos1, pos2, #nodes
|
||||
return vector.new(pos1x, pos1y, pos1z), vector.new(pos2x, pos2y, pos2z), #nodes
|
||||
end
|
||||
|
||||
|
||||
|
602
worldedit/test.lua
Normal file
602
worldedit/test.lua
Normal file
@ -0,0 +1,602 @@
|
||||
---------------------
|
||||
-- Helpers
|
||||
---------------------
|
||||
|
||||
local vec = vector.new
|
||||
local vecw = function(axis, n, base)
|
||||
local ret = vec(base)
|
||||
ret[axis] = n
|
||||
return ret
|
||||
end
|
||||
local pos2str = minetest.pos_to_string
|
||||
local get_node = minetest.get_node
|
||||
local set_node = minetest.set_node
|
||||
|
||||
---------------------
|
||||
-- Nodes
|
||||
---------------------
|
||||
local air = "air"
|
||||
local testnode1
|
||||
local testnode2
|
||||
local testnode3
|
||||
-- Loads nodenames to use for tests
|
||||
local function init_nodes()
|
||||
testnode1 = minetest.registered_aliases["mapgen_stone"]
|
||||
testnode2 = minetest.registered_aliases["mapgen_dirt"]
|
||||
testnode3 = minetest.registered_aliases["mapgen_cobble"] or minetest.registered_aliases["mapgen_dirt_with_grass"]
|
||||
assert(testnode1 and testnode2 and testnode3)
|
||||
end
|
||||
-- Writes repeating pattern into given area
|
||||
local function place_pattern(pos1, pos2, pattern)
|
||||
local pos = vec()
|
||||
local node = {name=""}
|
||||
local i = 1
|
||||
for z = pos1.z, pos2.z do
|
||||
pos.z = z
|
||||
for y = pos1.y, pos2.y do
|
||||
pos.y = y
|
||||
for x = pos1.x, pos2.x do
|
||||
pos.x = x
|
||||
node.name = pattern[i]
|
||||
set_node(pos, node)
|
||||
i = i % #pattern + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
---------------------
|
||||
-- Area management
|
||||
---------------------
|
||||
assert(minetest.get_mapgen_setting("mg_name") == "singlenode")
|
||||
local area = {}
|
||||
do
|
||||
local areamin, areamax
|
||||
local off
|
||||
local c_air = minetest.get_content_id(air)
|
||||
local vbuffer = {}
|
||||
-- Assign a new area for use, will emerge and then call ready()
|
||||
area.assign = function(min, max, ready)
|
||||
areamin = min
|
||||
areamax = max
|
||||
minetest.emerge_area(min, max, function(bpos, action, remaining)
|
||||
assert(action ~= minetest.EMERGE_ERRORED)
|
||||
if remaining > 0 then return end
|
||||
minetest.after(0, function()
|
||||
area.clear()
|
||||
ready()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
-- Reset area contents and state
|
||||
area.clear = function()
|
||||
local vmanip = minetest.get_voxel_manip(areamin, areamax)
|
||||
local vpos1, vpos2 = vmanip:get_emerged_area()
|
||||
local vcount = (vpos2.x - vpos1.x + 1) * (vpos2.y - vpos1.y + 1) * (vpos2.z - vpos1.z + 1)
|
||||
if #vbuffer ~= vcount then
|
||||
vbuffer = {}
|
||||
for i = 1, vcount do
|
||||
vbuffer[i] = c_air
|
||||
end
|
||||
end
|
||||
vmanip:set_data(vbuffer)
|
||||
vmanip:write_to_map()
|
||||
off = vec(0, 0, 0)
|
||||
end
|
||||
-- Returns an usable area [pos1, pos2] that does not overlap previous ones
|
||||
area.get = function(sizex, sizey, sizez)
|
||||
local size
|
||||
if sizey == nil and sizez == nil then
|
||||
size = vector.new(sizex, sizex, sizex)
|
||||
else
|
||||
size = vector.new(sizex, sizey, sizez)
|
||||
end
|
||||
local pos1 = vector.add(areamin, off)
|
||||
local pos2 = vector.subtract(vector.add(pos1, size), 1)
|
||||
if pos2.x > areamax.x or pos2.y > areamax.y or pos2.z > areamax.z then
|
||||
error("Internal failure: out of space")
|
||||
end
|
||||
off = vector.add(off, size)
|
||||
return pos1, pos2
|
||||
end
|
||||
-- Returns an axis and count (= n) relative to the last-requested area that is unoccupied
|
||||
area.dir = function(n)
|
||||
local pos1 = vector.add(areamin, off)
|
||||
if pos1.x + n <= areamax.x then
|
||||
off.x = off.x + n
|
||||
return "x", n
|
||||
elseif pos1.x + n <= areamax.y then
|
||||
off.y = off.y + n
|
||||
return "y", n
|
||||
elseif pos1.z + n <= areamax.z then
|
||||
off.z = off.z + n
|
||||
return "z", n
|
||||
end
|
||||
error("Internal failure: out of space")
|
||||
end
|
||||
-- Returns [XYZ] margin (list of pos pairs) of n around last-requested area
|
||||
-- (may actually be larger but doesn't matter)
|
||||
area.margin = function(n)
|
||||
local pos1, pos2 = area.get(n)
|
||||
return {
|
||||
{ vec(areamin.x, areamin.y, pos1.z), pos2 }, -- X/Y
|
||||
{ vec(areamin.x, pos1.y, areamin.z), pos2 }, -- X/Z
|
||||
{ vec(pos1.x, areamin.y, areamin.z), pos2 }, -- Y/Z
|
||||
}
|
||||
end
|
||||
end
|
||||
-- Split an existing area into two non-overlapping [pos1, half1], [half2, pos2] parts; returns half1, half2
|
||||
area.split = function(pos1, pos2)
|
||||
local axis
|
||||
if pos2.x - pos1.x >= 1 then
|
||||
axis = "x"
|
||||
elseif pos2.y - pos1.y >= 1 then
|
||||
axis = "y"
|
||||
elseif pos2.z - pos1.z >= 1 then
|
||||
axis = "z"
|
||||
else
|
||||
error("Internal failure: area too small to split")
|
||||
end
|
||||
local hspan = math.floor((pos2[axis] - pos1[axis] + 1) / 2)
|
||||
local half1 = vecw(axis, pos1[axis] + hspan - 1, pos2)
|
||||
local half2 = vecw(axis, pos1[axis] + hspan, pos2)
|
||||
return half1, half2
|
||||
end
|
||||
|
||||
|
||||
---------------------
|
||||
-- Checks
|
||||
---------------------
|
||||
local check = {}
|
||||
-- Check that all nodes in [pos1, pos2] are the node(s) specified
|
||||
check.filled = function(pos1, pos2, nodes)
|
||||
if type(nodes) == "string" then
|
||||
nodes = { nodes }
|
||||
end
|
||||
local _, counts = minetest.find_nodes_in_area(pos1, pos2, nodes)
|
||||
local total = worldedit.volume(pos1, pos2)
|
||||
local sum = 0
|
||||
for _, n in pairs(counts) do
|
||||
sum = sum + n
|
||||
end
|
||||
if sum ~= total then
|
||||
error((total - sum) .. " " .. table.concat(nodes, ",") .. " nodes missing in " ..
|
||||
pos2str(pos1) .. " -> " .. pos2str(pos2))
|
||||
end
|
||||
end
|
||||
-- Check that none of the nodes in [pos1, pos2] are the node(s) specified
|
||||
check.not_filled = function(pos1, pos2, nodes)
|
||||
if type(nodes) == "string" then
|
||||
nodes = { nodes }
|
||||
end
|
||||
local _, counts = minetest.find_nodes_in_area(pos1, pos2, nodes)
|
||||
for nodename, n in pairs(counts) do
|
||||
if n ~= 0 then
|
||||
error(counts[nodename] .. " " .. nodename .. " nodes found in " ..
|
||||
pos2str(pos1) .. " -> " .. pos2str(pos2))
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Check that all of the areas are only made of node(s) specified
|
||||
check.filled2 = function(list, nodes)
|
||||
for _, pos in ipairs(list) do
|
||||
check.filled(pos[1], pos[2], nodes)
|
||||
end
|
||||
end
|
||||
-- Check that none of the areas contain the node(s) specified
|
||||
check.not_filled2 = function(list, nodes)
|
||||
for _, pos in ipairs(list) do
|
||||
check.not_filled(pos[1], pos[2], nodes)
|
||||
end
|
||||
end
|
||||
-- Checks presence of a repeating pattern in [pos1, po2] (cf. place_pattern)
|
||||
check.pattern = function(pos1, pos2, pattern)
|
||||
local pos = vec()
|
||||
local i = 1
|
||||
for z = pos1.z, pos2.z do
|
||||
pos.z = z
|
||||
for y = pos1.y, pos2.y do
|
||||
pos.y = y
|
||||
for x = pos1.x, pos2.x do
|
||||
pos.x = x
|
||||
local node = get_node(pos)
|
||||
if node.name ~= pattern[i] then
|
||||
error(pattern[i] .. " not found at " .. pos2str(pos) .. " (i=" .. i .. ")")
|
||||
end
|
||||
i = i % #pattern + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
---------------------
|
||||
-- The actual tests
|
||||
---------------------
|
||||
local tests = {}
|
||||
local function register_test(name, func, opts)
|
||||
assert(type(name) == "string")
|
||||
assert(func == nil or type(func) == "function")
|
||||
if not opts then
|
||||
opts = {}
|
||||
else
|
||||
opts = table.copy(opts)
|
||||
end
|
||||
opts.name = name
|
||||
opts.func = func
|
||||
table.insert(tests, opts)
|
||||
end
|
||||
-- How this works:
|
||||
-- register_test registers a test with a name and function
|
||||
-- The function should return if the test passes or otherwise cause a Lua error
|
||||
-- The basic structure is: get areas + do operations + check results
|
||||
-- Helpers:
|
||||
-- area.get must be used to retrieve areas that can be operated on (these will be cleared before each test)
|
||||
-- check.filled / check.not_filled can be used to check the result
|
||||
-- area.margin + check.filled2 is useful to make sure nodes weren't placed too far
|
||||
-- place_pattern + check.pattern is useful to test ops that operate on existing data
|
||||
|
||||
|
||||
register_test("Internal self-test")
|
||||
register_test("is area loaded?", function()
|
||||
local pos1, _ = area.get(1)
|
||||
assert(get_node(pos1).name == "air")
|
||||
end, {dry=true})
|
||||
|
||||
register_test("area.split", function()
|
||||
for i = 2, 6 do
|
||||
local pos1, pos2 = area.get(1, 1, i)
|
||||
local half1, half2 = area.split(pos1, pos2)
|
||||
assert(pos1.x == half1.x and pos1.y == half1.y)
|
||||
assert(half1.x == half2.x and half1.y == half2.y)
|
||||
assert(half1.z + 1 == half2.z)
|
||||
if i % 2 == 0 then
|
||||
assert((half1.z - pos1.z) == (pos2.z - half2.z)) -- divided equally
|
||||
end
|
||||
end
|
||||
end, {dry=true})
|
||||
|
||||
register_test("check.filled", function()
|
||||
local pos1, pos2 = area.get(1, 2, 1)
|
||||
set_node(pos1, {name=testnode1})
|
||||
set_node(pos2, {name=testnode2})
|
||||
check.filled(pos1, pos1, testnode1)
|
||||
check.filled(pos1, pos2, {testnode1, testnode2})
|
||||
check.not_filled(pos1, pos1, air)
|
||||
check.not_filled(pos1, pos2, {air, testnode3})
|
||||
end)
|
||||
|
||||
register_test("pattern", function()
|
||||
local pos1, pos2 = area.get(3, 2, 1)
|
||||
local pattern = {testnode1, testnode3}
|
||||
place_pattern(pos1, pos2, pattern)
|
||||
assert(get_node(pos1).name == testnode1)
|
||||
check.pattern(pos1, pos2, pattern)
|
||||
end)
|
||||
|
||||
|
||||
register_test("Generic node manipulations")
|
||||
register_test("worldedit.set", function()
|
||||
local pos1, pos2 = area.get(10)
|
||||
local m = area.margin(1)
|
||||
|
||||
worldedit.set(pos1, pos2, testnode1)
|
||||
|
||||
check.filled(pos1, pos2, testnode1)
|
||||
check.filled2(m, air)
|
||||
end)
|
||||
|
||||
register_test("worldedit.set mix", function()
|
||||
local pos1, pos2 = area.get(10)
|
||||
local m = area.margin(1)
|
||||
|
||||
worldedit.set(pos1, pos2, {testnode1, testnode2})
|
||||
|
||||
check.filled(pos1, pos2, {testnode1, testnode2})
|
||||
check.filled2(m, air)
|
||||
end)
|
||||
|
||||
register_test("worldedit.replace", function()
|
||||
local pos1, pos2 = area.get(10)
|
||||
local half1, half2 = area.split(pos1, pos2)
|
||||
|
||||
worldedit.set(pos1, half1, testnode1)
|
||||
worldedit.set(half2, pos2, testnode2)
|
||||
worldedit.replace(pos1, pos2, testnode1, testnode3)
|
||||
|
||||
check.not_filled(pos1, pos2, testnode1)
|
||||
check.filled(pos1, half1, testnode3)
|
||||
check.filled(half2, pos2, testnode2)
|
||||
end)
|
||||
|
||||
register_test("worldedit.replace inverse", function()
|
||||
local pos1, pos2 = area.get(10)
|
||||
local half1, half2 = area.split(pos1, pos2)
|
||||
|
||||
worldedit.set(pos1, half1, testnode1)
|
||||
worldedit.set(half2, pos2, testnode2)
|
||||
worldedit.replace(pos1, pos2, testnode1, testnode3, true)
|
||||
|
||||
check.filled(pos1, half1, testnode1)
|
||||
check.filled(half2, pos2, testnode3)
|
||||
end)
|
||||
|
||||
-- FIXME?: this one looks overcomplicated
|
||||
register_test("worldedit.copy", function()
|
||||
local pos1, pos2 = area.get(4)
|
||||
local axis, n = area.dir(2)
|
||||
local m = area.margin(1)
|
||||
local b = pos1[axis]
|
||||
|
||||
-- create one slice with testnode1, one with testnode2
|
||||
worldedit.set(pos1, vecw(axis, b + 1, pos2), testnode1)
|
||||
worldedit.set(vecw(axis, b + 2, pos1), pos2, testnode2)
|
||||
worldedit.copy(pos1, pos2, axis, n)
|
||||
|
||||
-- should have three slices now
|
||||
check.filled(pos1, vecw(axis, b + 1, pos2), testnode1)
|
||||
check.filled(vecw(axis, b + 2, pos1), pos2, testnode1)
|
||||
check.filled(vecw(axis, b + 4, pos1), vector.add(pos2, vecw(axis, n)), testnode2)
|
||||
check.filled2(m, "air")
|
||||
end)
|
||||
|
||||
register_test("worldedit.copy2", function()
|
||||
local pos1, pos2 = area.get(6)
|
||||
local m1 = area.margin(1)
|
||||
local pos1_, pos2_ = area.get(6)
|
||||
local m2 = area.margin(1)
|
||||
|
||||
local pattern = {testnode1, testnode2, testnode3, testnode1, testnode2}
|
||||
place_pattern(pos1, pos2, pattern)
|
||||
worldedit.copy2(pos1, pos2, vector.subtract(pos1_, pos1))
|
||||
|
||||
check.pattern(pos1, pos2, pattern)
|
||||
check.pattern(pos1_, pos2_, pattern)
|
||||
check.filled2(m1, "air")
|
||||
check.filled2(m2, "air")
|
||||
end)
|
||||
|
||||
register_test("worldedit.move (overlap)", function()
|
||||
local pos1, pos2 = area.get(7)
|
||||
local axis, n = area.dir(2)
|
||||
local m = area.margin(1)
|
||||
|
||||
local pattern = {testnode2, testnode1, testnode2, testnode3, testnode3}
|
||||
place_pattern(pos1, pos2, pattern)
|
||||
worldedit.move(pos1, pos2, axis, n)
|
||||
|
||||
check.filled(pos1, vecw(axis, pos1[axis] + n - 1, pos2), "air")
|
||||
check.pattern(vecw(axis, pos1[axis] + n, pos1), vecw(axis, pos2[axis] + n, pos2), pattern)
|
||||
check.filled2(m, "air")
|
||||
end)
|
||||
|
||||
register_test("worldedit.move", function()
|
||||
local pos1, pos2 = area.get(10)
|
||||
local axis, n = area.dir(10)
|
||||
local m = area.margin(1)
|
||||
|
||||
local pattern = {testnode1, testnode3, testnode3, testnode2}
|
||||
place_pattern(pos1, pos2, pattern)
|
||||
worldedit.move(pos1, pos2, axis, n)
|
||||
|
||||
check.filled(pos1, pos2, "air")
|
||||
check.pattern(vecw(axis, pos1[axis] + n, pos1), vecw(axis, pos2[axis] + n, pos2), pattern)
|
||||
check.filled2(m, "air")
|
||||
end)
|
||||
|
||||
-- TODO: the rest (also testing param2 + metadata)
|
||||
|
||||
register_test("Schematics")
|
||||
register_test("worldedit.read_header", function()
|
||||
local value = '5,foo,BAR,-1,234:the content'
|
||||
local version, header, content = worldedit.read_header(value)
|
||||
assert(version == 5)
|
||||
assert(#header == 4)
|
||||
assert(header[1] == "foo" and header[2] == "BAR")
|
||||
assert(header[3] == "-1" and header[4] == "234")
|
||||
assert(content == "the content")
|
||||
end)
|
||||
|
||||
register_test("worldedit.allocate", function()
|
||||
local value = '3:-1 0 0 dummy 0 0\n0 0 4 dummy 0 0\n0 1 0 dummy 0 0'
|
||||
local pos1, pos2, count = worldedit.allocate(vec(1, 1, 1), value)
|
||||
assert(vector.equals(pos1, vec(0, 1, 1)))
|
||||
assert(vector.equals(pos2, vec(1, 2, 5)))
|
||||
assert(count == 3)
|
||||
end)
|
||||
|
||||
do
|
||||
local function output_weird(numbers, body)
|
||||
local s = {"return {"}
|
||||
for _, parts in ipairs(numbers) do
|
||||
s[#s+1] = "{"
|
||||
for _, n in ipairs(parts) do
|
||||
s[#s+1] = string.format(" {%d},", n)
|
||||
end
|
||||
s[#s+1] = "},"
|
||||
end
|
||||
return table.concat(s, "\n") .. table.concat(body, "\n") .. "}"
|
||||
end
|
||||
local fmt1p = '{\n ["x"]=%d,\n ["y"]=%d,\n ["z"]=%d,\n},'
|
||||
local fmt1n = '{\n ["name"]="%s",\n},'
|
||||
local fmt4 = '{ ["x"] = %d, ["y"] = %d, ["z"] = %d, ["meta"] = { ["fields"] = { }, ["inventory"] = { } }, ["param2"] = 0, ["param1"] = 0, ["name"] = "%s" }'
|
||||
local fmt5 = '{ ["x"] = %d, ["y"] = %d, ["z"] = %d, ["name"] = "%s" }'
|
||||
local fmt51 = '{[r2]=0,x=%d,y=%d,z=%d,name=r%d}'
|
||||
local fmt52 = '{x=%d,y=%d,z=%d,name=_[%d]}'
|
||||
local test_data = {
|
||||
-- used by WorldEdit 0.2 (first public release)
|
||||
{
|
||||
name = "v1", ver = 1,
|
||||
gen = function(pat)
|
||||
local numbers = {
|
||||
{2, 3, 4, 5, 6},
|
||||
{7, 8}, {9, 10}, {11, 12},
|
||||
{13, 14}, {15, 16}
|
||||
}
|
||||
return output_weird(numbers, {
|
||||
fmt1p:format(0, 0, 0),
|
||||
fmt1n:format(pat[1]),
|
||||
fmt1p:format(0, 1, 0),
|
||||
fmt1n:format(pat[3]),
|
||||
fmt1p:format(1, 1, 0),
|
||||
fmt1n:format(pat[1]),
|
||||
fmt1p:format(1, 0, 1),
|
||||
fmt1n:format(pat[3]),
|
||||
fmt1p:format(0, 1, 1),
|
||||
fmt1n:format(pat[1]),
|
||||
})
|
||||
end
|
||||
},
|
||||
|
||||
-- v2: missing because I couldn't find any code in my archives that actually wrote this format
|
||||
|
||||
{
|
||||
name = "v3", ver = 3,
|
||||
gen = function(pat)
|
||||
assert(pat[2] == "air")
|
||||
return table.concat({
|
||||
"0 0 0 " .. pat[1] .. " 0 0",
|
||||
"0 1 0 " .. pat[3] .. " 0 0",
|
||||
"1 1 0 " .. pat[1] .. " 0 0",
|
||||
"1 0 1 " .. pat[3] .. " 0 0",
|
||||
"0 1 1 " .. pat[1] .. " 0 0",
|
||||
}, "\n")
|
||||
end
|
||||
},
|
||||
|
||||
{
|
||||
name = "v4", ver = 4,
|
||||
gen = function(pat)
|
||||
return table.concat({
|
||||
"return { " .. fmt4:format(0, 0, 0, pat[1]),
|
||||
fmt4:format(0, 1, 0, pat[3]),
|
||||
fmt4:format(1, 1, 0, pat[1]),
|
||||
fmt4:format(1, 0, 1, pat[3]),
|
||||
fmt4:format(0, 1, 1, pat[1]) .. " }",
|
||||
}, ", ")
|
||||
end
|
||||
},
|
||||
|
||||
-- like v4 but no meta and param (if empty)
|
||||
{
|
||||
name = "v5 (pre-5.6)", ver = 5,
|
||||
gen = function(pat)
|
||||
return table.concat({
|
||||
"5:return { " .. fmt5:format(0, 0, 0, pat[1]),
|
||||
fmt5:format(0, 1, 0, pat[3]),
|
||||
fmt5:format(1, 1, 0, pat[1]),
|
||||
fmt5:format(1, 0, 1, pat[3]),
|
||||
fmt5:format(0, 1, 1, pat[1]) .. " }",
|
||||
}, ", ")
|
||||
end
|
||||
},
|
||||
|
||||
-- reworked engine serialization in 5.6
|
||||
{
|
||||
name = "v5 (5.6)", ver = 5,
|
||||
gen = function(pat)
|
||||
return table.concat({
|
||||
'5:r1="' .. pat[1] .. '";r2="param1";r3="' .. pat[3] .. '";return {'
|
||||
.. fmt51:format(0, 0, 0, 1),
|
||||
fmt51:format(0, 1, 0, 3),
|
||||
fmt51:format(1, 1, 0, 1),
|
||||
fmt51:format(1, 0, 1, 3),
|
||||
fmt51:format(0, 1, 1, 1) .. "}",
|
||||
}, ",")
|
||||
end
|
||||
},
|
||||
|
||||
-- small changes on engine side again
|
||||
{
|
||||
name = "v5 (post-5.7)", ver = 5,
|
||||
gen = function(pat)
|
||||
return table.concat({
|
||||
'5:local _={};_[1]="' .. pat[1] .. '";_[3]="' .. pat[3] .. '";return {'
|
||||
.. fmt52:format(0, 0, 0, 1),
|
||||
fmt52:format(0, 1, 0, 3),
|
||||
fmt52:format(1, 1, 0, 1),
|
||||
fmt52:format(1, 0, 1, 3),
|
||||
fmt52:format(0, 1, 1, 1) .. "}",
|
||||
}, ",")
|
||||
end
|
||||
},
|
||||
}
|
||||
for _, e in ipairs(test_data) do
|
||||
register_test("worldedit.deserialize " .. e.name, function()
|
||||
local pos1, pos2 = area.get(2)
|
||||
local m = area.margin(1)
|
||||
|
||||
local pat = {testnode3, "air", testnode2}
|
||||
local value = e.gen(pat)
|
||||
assert(type(value) == "string")
|
||||
|
||||
local version = worldedit.read_header(value)
|
||||
assert(version == e.ver, "version: got " .. tostring(version) .. " expected " .. e.ver)
|
||||
local count = worldedit.deserialize(pos1, value)
|
||||
assert(count ~= nil and count > 0)
|
||||
|
||||
check.pattern(pos1, pos2, pat)
|
||||
check.filled2(m, "air")
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
---------------------
|
||||
-- Main function
|
||||
---------------------
|
||||
worldedit.run_tests = function()
|
||||
do
|
||||
local v = minetest.get_version()
|
||||
print("Running " .. #tests .. " tests for WorldEdit " ..
|
||||
worldedit.version_string .. " on " .. v.project .. " " .. (v.hash or v.string))
|
||||
end
|
||||
|
||||
init_nodes()
|
||||
|
||||
-- emerge area from (0,0,0) ~ (56,56,56) and keep it loaded
|
||||
-- Note: making this area smaller speeds up tests
|
||||
local wanted = vec(56, 56, 56)
|
||||
for x = 0, math.floor(wanted.x/16) do
|
||||
for y = 0, math.floor(wanted.y/16) do
|
||||
for z = 0, math.floor(wanted.z/16) do
|
||||
assert(minetest.forceload_block(vector.new(x*16, y*16, z*16), true))
|
||||
end
|
||||
end
|
||||
end
|
||||
area.assign(vec(0, 0, 0), wanted, function()
|
||||
|
||||
local failed = 0
|
||||
for _, test in ipairs(tests) do
|
||||
if not test.func then
|
||||
local s = "---- " .. test.name .. " "
|
||||
print(s .. string.rep("-", 60 - #s))
|
||||
else
|
||||
if not test.dry then
|
||||
area.clear()
|
||||
end
|
||||
local ok, err = pcall(test.func)
|
||||
print(string.format("%-60s %s", test.name, ok and "pass" or "FAIL"))
|
||||
if not ok then
|
||||
print(" " .. err)
|
||||
failed = failed + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
print("Done, " .. failed .. " tests failed.")
|
||||
if failed == 0 then
|
||||
io.close(io.open(minetest.get_worldpath() .. "/tests_ok", "w"))
|
||||
end
|
||||
minetest.request_shutdown()
|
||||
end)
|
||||
end
|
||||
|
||||
-- for debug purposes
|
||||
minetest.register_on_joinplayer(function(player)
|
||||
minetest.set_player_privs(player:get_player_name(),
|
||||
minetest.string_to_privs("fly,fast,noclip,basic_debug,debug,interact"))
|
||||
end)
|
||||
minetest.register_on_punchnode(function(pos, node, puncher)
|
||||
minetest.chat_send_player(puncher:get_player_name(), pos2str(pos))
|
||||
end)
|
@ -19,7 +19,7 @@ function worldedit.hide(pos1, pos2)
|
||||
|
||||
worldedit.keep_loaded(pos1, pos2)
|
||||
|
||||
local pos = {x=pos1.x, y=0, z=0}
|
||||
local pos = vector.new(pos1.x, 0, 0)
|
||||
local get_node, get_meta, swap_node = minetest.get_node,
|
||||
minetest.get_meta, minetest.swap_node
|
||||
while pos.x <= pos2.x do
|
||||
@ -79,7 +79,7 @@ function worldedit.highlight(pos1, pos2, node_name)
|
||||
|
||||
worldedit.keep_loaded(pos1, pos2)
|
||||
|
||||
local pos = {x=pos1.x, y=0, z=0}
|
||||
local pos = vector.new(pos1.x, 0, 0)
|
||||
local get_node, get_meta, swap_node = minetest.get_node,
|
||||
minetest.get_meta, minetest.swap_node
|
||||
local count = 0
|
||||
|
@ -27,7 +27,7 @@ local brush_on_use = function(itemstack, placer)
|
||||
end
|
||||
|
||||
local raybegin = vector.add(placer:get_pos(),
|
||||
{x=0, y=placer:get_properties().eye_height, z=0})
|
||||
vector.new(0, placer:get_properties().eye_height, 0))
|
||||
local rayend = vector.add(raybegin, vector.multiply(placer:get_look_dir(), BRUSH_MAX_DIST))
|
||||
local ray = minetest.raycast(raybegin, rayend, false, true)
|
||||
local pointed_thing = ray:next()
|
||||
|
@ -226,6 +226,39 @@ local function check_filename(name)
|
||||
return name:find("^[%w%s%^&'@{}%[%],%$=!%-#%(%)%%%.%+~_]+$") ~= nil
|
||||
end
|
||||
|
||||
local function open_schematic(name, param)
|
||||
-- find the file in the world path
|
||||
local testpaths = {
|
||||
minetest.get_worldpath() .. "/schems/" .. param,
|
||||
minetest.get_worldpath() .. "/schems/" .. param .. ".we",
|
||||
minetest.get_worldpath() .. "/schems/" .. param .. ".wem",
|
||||
}
|
||||
local file, err
|
||||
for index, path in ipairs(testpaths) do
|
||||
file, err = io.open(path, "rb")
|
||||
if not err then
|
||||
break
|
||||
end
|
||||
end
|
||||
if err then
|
||||
worldedit.player_notify(name, "Could not open file \"" .. param .. "\"")
|
||||
return
|
||||
end
|
||||
local value = file:read("*a")
|
||||
file:close()
|
||||
|
||||
local version = worldedit.read_header(value)
|
||||
if version == nil or version == 0 then
|
||||
worldedit.player_notify(name, "File is invalid!")
|
||||
return
|
||||
elseif version > worldedit.LATEST_SERIALIZATION_VERSION then
|
||||
worldedit.player_notify(name, "Schematic was created with a newer version of WorldEdit.")
|
||||
return
|
||||
end
|
||||
|
||||
return value
|
||||
end
|
||||
|
||||
|
||||
worldedit.register_command("about", {
|
||||
privs = {},
|
||||
@ -455,7 +488,7 @@ worldedit.register_command("fixedpos", {
|
||||
if found == nil then
|
||||
return false
|
||||
end
|
||||
return true, flag, {x=tonumber(x), y=tonumber(y), z=tonumber(z)}
|
||||
return true, flag, vector.new(tonumber(x), tonumber(y), tonumber(z))
|
||||
end,
|
||||
func = function(name, flag, pos)
|
||||
if flag == "set1" then
|
||||
@ -490,7 +523,7 @@ minetest.register_on_punchnode(function(pos, node, puncher)
|
||||
worldedit.player_notify(name, "position 2 set to " .. minetest.pos_to_string(pos))
|
||||
elseif worldedit.set_pos[name] == "prob" then --setting Minetest schematic node probabilities
|
||||
worldedit.prob_pos[name] = pos
|
||||
minetest.show_formspec(puncher:get_player_name(), "prob_val_enter", "field[text;;]")
|
||||
minetest.show_formspec(name, "prob_val_enter", "field[text;;]")
|
||||
end
|
||||
end
|
||||
end)
|
||||
@ -1014,7 +1047,7 @@ worldedit.register_command("stack2", {
|
||||
return false, "invalid increments: " .. param
|
||||
end
|
||||
|
||||
return true, tonumber(repetitions), {x=tonumber(x), y=tonumber(y), z=tonumber(z)}
|
||||
return true, tonumber(repetitions), vector.new(tonumber(x), tonumber(y), tonumber(z))
|
||||
end,
|
||||
nodes_needed = function(name, repetitions, offset)
|
||||
return check_region(name) * repetitions
|
||||
@ -1186,13 +1219,16 @@ worldedit.register_command("drain", {
|
||||
-- TODO: make an API function for this
|
||||
local count = 0
|
||||
local pos1, pos2 = worldedit.sort_pos(worldedit.pos1[name], worldedit.pos2[name])
|
||||
|
||||
local get_node, remove_node = minetest.get_node, minetest.remove_node
|
||||
for x = pos1.x, pos2.x do
|
||||
for y = pos1.y, pos2.y do
|
||||
for z = pos1.z, pos2.z do
|
||||
local n = minetest.get_node({x=x, y=y, z=z}).name
|
||||
local p = vector.new(x, y, z)
|
||||
local n = get_node(p).name
|
||||
local d = minetest.registered_nodes[n]
|
||||
if d ~= nil and (d["drawtype"] == "liquid" or d["drawtype"] == "flowingliquid") then
|
||||
minetest.remove_node({x=x, y=y, z=z})
|
||||
if d ~= nil and (d.drawtype == "liquid" or d.drawtype == "flowingliquid") then
|
||||
remove_node(p)
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
@ -1230,13 +1266,15 @@ local function clearcut(pos1, pos2)
|
||||
local count = 0
|
||||
local prev, any
|
||||
|
||||
local get_node, remove_node = minetest.get_node, minetest.remove_node
|
||||
for x = pos1.x, pos2.x do
|
||||
for z = pos1.z, pos2.z do
|
||||
prev = false
|
||||
any = false
|
||||
-- first pass: remove floating nodes that would be left over
|
||||
for y = pos1.y, pos2.y do
|
||||
local n = minetest.get_node({x=x, y=y, z=z}).name
|
||||
local pos = vector.new(x, y, z)
|
||||
local n = get_node(pos).name
|
||||
if plants[n] then
|
||||
prev = true
|
||||
any = true
|
||||
@ -1244,7 +1282,7 @@ local function clearcut(pos1, pos2)
|
||||
local def = minetest.registered_nodes[n] or {}
|
||||
local groups = def.groups or {}
|
||||
if groups.attached_node or (def.buildable_to and groups.falling_node) then
|
||||
minetest.remove_node({x=x, y=y, z=z})
|
||||
remove_node(pos)
|
||||
count = count + 1
|
||||
else
|
||||
prev = false
|
||||
@ -1255,9 +1293,10 @@ local function clearcut(pos1, pos2)
|
||||
-- second pass: remove plants, top-to-bottom to avoid item drops
|
||||
if any then
|
||||
for y = pos2.y, pos1.y, -1 do
|
||||
local n = minetest.get_node({x=x, y=y, z=z}).name
|
||||
local pos = vector.new(x, y, z)
|
||||
local n = get_node(pos).name
|
||||
if plants[n] then
|
||||
minetest.remove_node({x=x, y=y, z=z})
|
||||
remove_node(pos)
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
@ -1415,28 +1454,15 @@ worldedit.register_command("allocate", {
|
||||
func = function(name, param)
|
||||
local pos = worldedit.pos1[name]
|
||||
|
||||
local filename = minetest.get_worldpath() .. "/schems/" .. param .. ".we"
|
||||
local file, err = io.open(filename, "rb")
|
||||
if err ~= nil then
|
||||
worldedit.player_notify(name, "could not open file \"" .. filename .. "\"")
|
||||
return
|
||||
local value = open_schematic(name, param)
|
||||
if not value then
|
||||
return false
|
||||
end
|
||||
local value = file:read("*a")
|
||||
file:close()
|
||||
|
||||
local version = worldedit.read_header(value)
|
||||
if version == nil or version == 0 then
|
||||
worldedit.player_notify(name, "File is invalid!")
|
||||
return
|
||||
elseif version > worldedit.LATEST_SERIALIZATION_VERSION then
|
||||
worldedit.player_notify(name, "File was created with newer version of WorldEdit!")
|
||||
return
|
||||
end
|
||||
local nodepos1, nodepos2, count = worldedit.allocate(pos, value)
|
||||
|
||||
if not nodepos1 then
|
||||
worldedit.player_notify(name, "Schematic empty, nothing allocated")
|
||||
return
|
||||
return false
|
||||
end
|
||||
|
||||
worldedit.pos1[name] = nodepos1
|
||||
@ -1464,46 +1490,16 @@ worldedit.register_command("load", {
|
||||
func = function(name, param)
|
||||
local pos = worldedit.pos1[name]
|
||||
|
||||
if param == "" then
|
||||
worldedit.player_notify(name, "invalid usage: " .. param)
|
||||
return
|
||||
end
|
||||
if not string.find(param, "^[%w \t.,+-_=!@#$%%^&*()%[%]{};'\"]+$") then
|
||||
worldedit.player_notify(name, "invalid file name: " .. param)
|
||||
return
|
||||
end
|
||||
|
||||
--find the file in the world path
|
||||
local testpaths = {
|
||||
minetest.get_worldpath() .. "/schems/" .. param,
|
||||
minetest.get_worldpath() .. "/schems/" .. param .. ".we",
|
||||
minetest.get_worldpath() .. "/schems/" .. param .. ".wem",
|
||||
}
|
||||
local file, err
|
||||
for index, path in ipairs(testpaths) do
|
||||
file, err = io.open(path, "rb")
|
||||
if not err then
|
||||
break
|
||||
end
|
||||
end
|
||||
if err then
|
||||
worldedit.player_notify(name, "could not open file \"" .. param .. "\"")
|
||||
return
|
||||
end
|
||||
local value = file:read("*a")
|
||||
file:close()
|
||||
|
||||
local version = worldedit.read_header(value)
|
||||
if version == nil or version == 0 then
|
||||
worldedit.player_notify(name, "File is invalid!")
|
||||
return
|
||||
elseif version > worldedit.LATEST_SERIALIZATION_VERSION then
|
||||
worldedit.player_notify(name, "File was created with newer version of WorldEdit!")
|
||||
return
|
||||
local value = open_schematic(name, param)
|
||||
if not value then
|
||||
return false
|
||||
end
|
||||
|
||||
local count = worldedit.deserialize(pos, value)
|
||||
|
||||
if count == nil then
|
||||
worldedit.player_notify(name, "Loading failed!")
|
||||
return false
|
||||
end
|
||||
worldedit.player_notify(name, count .. " nodes loaded")
|
||||
end,
|
||||
})
|
||||
@ -1644,11 +1640,18 @@ worldedit.register_command("mtschemprob", {
|
||||
})
|
||||
|
||||
minetest.register_on_player_receive_fields(function(player, formname, fields)
|
||||
if formname == "prob_val_enter" and not (fields.text == "" or fields.text == nil) then
|
||||
if formname == "prob_val_enter" then
|
||||
local name = player:get_player_name()
|
||||
local prob_entry = {pos=worldedit.prob_pos[name], prob=tonumber(fields.text)}
|
||||
local index = table.getn(worldedit.prob_list[name]) + 1
|
||||
worldedit.prob_list[name][index] = prob_entry
|
||||
local problist = worldedit.prob_list[name]
|
||||
if problist == nil then
|
||||
return
|
||||
end
|
||||
local e = {pos=worldedit.prob_pos[name], prob=tonumber(fields.text)}
|
||||
if e.pos == nil or e.prob == nil or e.prob < 0 or e.prob > 256 then
|
||||
worldedit.player_notify(name, "invalid node probability given, not saved")
|
||||
return
|
||||
end
|
||||
problist[#problist+1] = e
|
||||
end
|
||||
end)
|
||||
|
||||
|
@ -11,6 +11,7 @@ local gui_count2 = {} --mapping of player names to a quantity (arbitrary strings
|
||||
local gui_count3 = {} --mapping of player names to a quantity (arbitrary strings may also appear as values)
|
||||
local gui_angle = {} --mapping of player names to an angle (one of 90, 180, 270, representing the angle in degrees clockwise)
|
||||
local gui_filename = {} --mapping of player names to file names
|
||||
local gui_param2 = {} --mapping of player names to param2 values
|
||||
|
||||
--set default values
|
||||
setmetatable(gui_nodename1, {__index = function() return "Cobblestone" end})
|
||||
@ -25,6 +26,7 @@ setmetatable(gui_count2, {__index = function() return "6" end})
|
||||
setmetatable(gui_count3, {__index = function() return "4" end})
|
||||
setmetatable(gui_angle, {__index = function() return 90 end})
|
||||
setmetatable(gui_filename, {__index = function() return "building" end})
|
||||
setmetatable(gui_param2, {__index = function() return "0" end})
|
||||
|
||||
local axis_indices = {["X axis"]=1, ["Y axis"]=2, ["Z axis"]=3, ["Look direction"]=4}
|
||||
local axis_values = {"x", "y", "z", "?"}
|
||||
@ -904,3 +906,31 @@ worldedit.register_gui_function("worldedit_gui_clearobjects", {
|
||||
execute_worldedit_command("clearobjects", name, "")
|
||||
end,
|
||||
})
|
||||
|
||||
worldedit.register_gui_function("worldedit_gui_param2", {
|
||||
name = "Set Param2",
|
||||
privs = we_privs("param2"),
|
||||
get_formspec = function(name)
|
||||
local value = gui_param2[name] or "0"
|
||||
return "size[6.5,3]" .. worldedit.get_formspec_header("worldedit_gui_param2") ..
|
||||
"textarea[0.5,1;5,2;;;Some values may break the node!]"..
|
||||
string.format("field[0.5,2.5;2,0.8;worldedit_gui_param2_value;New Param2;%s]", minetest.formspec_escape(value)) ..
|
||||
"field_close_on_enter[worldedit_gui_param2_value;false]" ..
|
||||
"button_exit[3.5,2.5;3,0.8;worldedit_gui_param2_submit;Set Param2]"
|
||||
end,
|
||||
})
|
||||
|
||||
worldedit.register_gui_handler("worldedit_gui_param2", function(name, fields)
|
||||
local cg = {
|
||||
worldedit_gui_param2_value = gui_param2,
|
||||
}
|
||||
local ret = handle_changes(name, "worldedit_gui_param2", fields, cg)
|
||||
if fields.worldedit_gui_param2_submit then
|
||||
copy_changes(name, fields, cg)
|
||||
worldedit.show_page(name, "worldedit_gui_param2")
|
||||
|
||||
execute_worldedit_command("param2", name, gui_param2[name])
|
||||
return true
|
||||
end
|
||||
return ret
|
||||
end)
|
||||
|
@ -42,7 +42,7 @@ Example:
|
||||
worldedit.register_gui_handler = function(identifier, handler)
|
||||
local enabled = true
|
||||
minetest.register_on_player_receive_fields(function(player, formname, fields)
|
||||
if not enabled then return false end
|
||||
if not enabled or formname ~= "" or fields.worldedit_gui then return false end
|
||||
enabled = false
|
||||
minetest.after(0.2, function() enabled = true end)
|
||||
local name = player:get_player_name()
|
||||
@ -216,7 +216,7 @@ elseif minetest.global_exists("sfinv") then -- sfinv installed
|
||||
end
|
||||
end
|
||||
else
|
||||
error(
|
||||
return minetest.log("error",
|
||||
"worldedit_gui requires a supported gui management mod to be installed.\n"..
|
||||
"To use the it you need to either:\n"..
|
||||
"* use minetest_game or another sfinv-compatible subgame\n"..
|
||||
|
Reference in New Issue
Block a user