mirror of
https://github.com/HybridDog/we_undo.git
synced 2024-12-13 09:33:20 +01:00
860 lines
22 KiB
Lua
860 lines
22 KiB
Lua
|
|
|
|
----------------- Settings -----------------------------------------------------
|
|
|
|
local max_commands = 256
|
|
local min_commands = 3
|
|
local max_memory_usage = 2^25 -- 32 MiB
|
|
|
|
|
|
----------------- Journal and chatcommands -------------------------------------
|
|
|
|
local command_invoker
|
|
|
|
local function override_chatcommand(cname, func_before, func_after)
|
|
local command = minetest.registered_chatcommands[cname]
|
|
-- save the name of the player and execute func_before if present
|
|
if func_before then
|
|
local func = command.func
|
|
command.func = function(name, ...)
|
|
command_invoker = name
|
|
func_before(...)
|
|
return func(name, ...)
|
|
end
|
|
else
|
|
local func = command.func
|
|
command.func = function(name, ...)
|
|
command_invoker = name
|
|
return func(name, ...)
|
|
end
|
|
end
|
|
|
|
-- reset command_invoker and optionally execute func_after
|
|
if func_after then
|
|
local func = command.func
|
|
command.func = function(name, ...)
|
|
local rv = func(name, ...)
|
|
local custom_rv = func_after(...)
|
|
command_invoker = nil
|
|
if custom_rv ~= nil then
|
|
return custom_rv
|
|
end
|
|
return rv
|
|
end
|
|
else
|
|
local func = command.func
|
|
command.func = function(...)
|
|
local rv = func(...)
|
|
command_invoker = nil
|
|
return rv
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
local journal = {}
|
|
local function add_to_history(data, name)
|
|
name = name or command_invoker
|
|
assert(name, "Player name isn't known")
|
|
journal[name] = journal[name] or {
|
|
ring = {},
|
|
start = 0,
|
|
off_start = -1,
|
|
entry_count = 0,
|
|
mem_usage = 0,
|
|
}
|
|
local j = journal[name]
|
|
|
|
j.off_start = j.off_start+1
|
|
if j.off_start == j.entry_count then
|
|
j.entry_count = j.entry_count+1
|
|
end
|
|
if j.off_start == max_commands then
|
|
-- max_commands are stored, replace the oldest one
|
|
j.mem_usage = j.mem_usage - j.ring[j.start].mem_use
|
|
j.start = (j.start+1) % max_commands
|
|
j.off_start = j.off_start-1
|
|
j.entry_count = j.entry_count-1
|
|
assert(j.start == (j.start + j.off_start + 1) % max_commands
|
|
and j.entry_count == j.off_start+1
|
|
and j.entry_count == max_commands)
|
|
end
|
|
if j.entry_count-1 > j.off_start then
|
|
-- remove redo remnants
|
|
for i = j.off_start+1, j.entry_count-1 do
|
|
local im = (j.start + i) % max_commands
|
|
j.mem_usage = j.mem_usage - j.ring[im].mem_use
|
|
j.ring[im] = nil
|
|
end
|
|
j.entry_count = j.off_start+1
|
|
end
|
|
-- insert the new data
|
|
j.ring[(j.start + j.off_start) % max_commands] = data
|
|
j.mem_usage = j.mem_usage + data.mem_use
|
|
|
|
-- remove old data if too much memory is used
|
|
if j.mem_usage > max_memory_usage then
|
|
while j.entry_count > min_commands do
|
|
j.mem_usage = j.mem_usage - j.ring[j.start].mem_use
|
|
j.ring[j.start] = nil
|
|
j.start = (j.start+1) % max_commands
|
|
j.off_start = j.off_start-1
|
|
j.entry_count = j.entry_count-1
|
|
if j.mem_usage <= max_memory_usage then
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- remove old undo history after un- or redoing
|
|
local function trim_undo_history(j)
|
|
while j.entry_count > min_commands
|
|
and j.off_start > 0 do
|
|
j.mem_usage = j.mem_usage - j.ring[j.start].mem_use
|
|
j.ring[j.start] = nil
|
|
j.start = (j.start+1) % max_commands
|
|
j.off_start = j.off_start-1
|
|
j.entry_count = j.entry_count-1
|
|
if j.mem_usage <= max_memory_usage then
|
|
return
|
|
end
|
|
end
|
|
-- never remove redo history
|
|
end
|
|
|
|
local undo_funcs = {}
|
|
local function apply_undo(name)
|
|
local j = journal[name]
|
|
local i = (j.start + j.off_start) % max_commands
|
|
local data = j.ring[i]
|
|
local old_memuse = data.mem_use
|
|
undo_funcs[data.type](name, data)
|
|
j.mem_usage = j.mem_usage + data.mem_use - old_memuse
|
|
j.ring[i] = data
|
|
j.off_start = j.off_start-1
|
|
if j.mem_usage > max_memory_usage then
|
|
trim_undo_history(j)
|
|
end
|
|
end
|
|
|
|
local function apply_redo(name)
|
|
local j = journal[name]
|
|
j.off_start = j.off_start+1
|
|
local i = (j.start + j.off_start) % max_commands
|
|
local data = j.ring[i]
|
|
local old_memuse = data.mem_use
|
|
-- undoing an undone undo function is redoing
|
|
undo_funcs[data.type](name, data)
|
|
j.mem_usage = j.mem_usage + data.mem_use - old_memuse
|
|
j.ring[i] = data
|
|
if j.mem_usage > max_memory_usage then
|
|
trim_undo_history(j)
|
|
end
|
|
end
|
|
|
|
minetest.register_chatcommand("/undo", {
|
|
params = "",
|
|
description = "Worldedit undo",
|
|
privs = {worldedit=true},
|
|
func = function(name)
|
|
local j = journal[name]
|
|
if not j
|
|
or j.off_start < 0 then
|
|
return false, "Nothing to be undone, try //show_journal"
|
|
end
|
|
apply_undo(name)
|
|
end,
|
|
})
|
|
|
|
minetest.register_chatcommand("/redo", {
|
|
params = "",
|
|
description = "Worldedit redo",
|
|
privs = {worldedit=true},
|
|
func = function(name)
|
|
local j = journal[name]
|
|
if not j
|
|
or j.off_start == j.entry_count-1 then
|
|
return false, "Nothing to be redone, try //show_journal"
|
|
end
|
|
apply_redo(name)
|
|
end,
|
|
})
|
|
|
|
local undo_info_funcs = {}
|
|
minetest.register_chatcommand("/show_journal", {
|
|
params = "",
|
|
description = "List Worldedit undos and redos, the last one is the newest",
|
|
privs = {worldedit=true},
|
|
func = function(name)
|
|
local j = journal[name]
|
|
if not j then
|
|
return false, "Empty journal"
|
|
end
|
|
local info = j.entry_count .. " entries, " ..
|
|
j.off_start+1 .. " can be undone, " ..
|
|
j.entry_count-1 - j.off_start .. " can be redone\n"
|
|
for i = 0, j.entry_count-1 do
|
|
if i <= j.off_start then
|
|
-- undo entry
|
|
info = info ..
|
|
minetest.get_color_escape_sequence"#A47DFF" .. " "
|
|
else
|
|
-- redo entry
|
|
info = info ..
|
|
minetest.get_color_escape_sequence"#8ABDA9" .. "* "
|
|
end
|
|
local data = j.ring[(j.start + i) % max_commands]
|
|
info = info .. data.type
|
|
if undo_info_funcs[data.type] then
|
|
info = info .. ": " .. undo_info_funcs[data.type](data)
|
|
end
|
|
if i < j.entry_count-1 then
|
|
info = info .. "\n" ..
|
|
minetest.get_color_escape_sequence"#ffffff"
|
|
end
|
|
end
|
|
return true, info
|
|
end,
|
|
})
|
|
|
|
|
|
----------------- The worldedit stuff ------------------------------------------
|
|
|
|
override_chatcommand("/pos1",
|
|
function()
|
|
add_to_history{
|
|
type = "marker",
|
|
mem_use = 9 * 7,
|
|
id = 1,
|
|
pos = worldedit.pos1[command_invoker]
|
|
}
|
|
end
|
|
)
|
|
|
|
override_chatcommand("/pos2",
|
|
function()
|
|
add_to_history{
|
|
type = "marker",
|
|
mem_use = 9 * 7,
|
|
id = 2,
|
|
pos = worldedit.pos2[command_invoker]
|
|
}
|
|
end
|
|
)
|
|
|
|
-- Punch before the /p command's punch
|
|
table.insert(minetest.registered_on_punchnodes, 1, function(_,_, player)
|
|
local name = player:get_player_name()
|
|
local typ = worldedit.set_pos[name]
|
|
if typ == "pos1"
|
|
or typ == "pos1only" then
|
|
add_to_history({
|
|
type = "marker",
|
|
mem_use = 9 * 7,
|
|
id = 1,
|
|
pos = worldedit.pos1[name]
|
|
}, name)
|
|
elseif typ == "pos2" then
|
|
add_to_history({
|
|
type = "marker",
|
|
mem_use = 9 * 7,
|
|
id = 2,
|
|
pos = worldedit.pos2[name]
|
|
}, name)
|
|
end
|
|
end)
|
|
|
|
undo_funcs.marker = function(name, data)
|
|
local pos = data.pos
|
|
local i = "pos" .. data.id
|
|
local current_pos = worldedit[i][name]
|
|
worldedit[i][name] = pos
|
|
worldedit["mark_pos" .. data.id](name)
|
|
if pos then
|
|
worldedit.player_notify(name, "position " .. data.id .. " set to " ..
|
|
minetest.pos_to_string(pos))
|
|
else
|
|
worldedit.player_notify(name, "position " .. data.id .. " reset")
|
|
end
|
|
data.pos = current_pos
|
|
end
|
|
undo_info_funcs.marker = function(data)
|
|
if not data.pos then
|
|
return "Set pos" .. data.id
|
|
end
|
|
return "changed pos" .. data.id .. ", previous value: " ..
|
|
minetest.pos_to_string(data.pos)
|
|
end
|
|
|
|
|
|
-- Catch confirmation requests (/y or /n follows)
|
|
local y_pending = {}
|
|
local we_notify = worldedit.player_notify
|
|
function worldedit.player_notify(name, msg)
|
|
if msg:sub(1, 43) == "WARNING: this operation could affect up to " then
|
|
y_pending[name] = true
|
|
end
|
|
return we_notify(name, msg)
|
|
end
|
|
|
|
override_chatcommand("/n",
|
|
function()
|
|
y_pending[command_invoker] = nil
|
|
end
|
|
)
|
|
|
|
override_chatcommand("/y",
|
|
function(...)
|
|
local t = y_pending[command_invoker]
|
|
if type(t) == "table"
|
|
and t.before then
|
|
t.before(...)
|
|
end
|
|
end,
|
|
function(...)
|
|
local t = y_pending[command_invoker]
|
|
if type(t) == "table"
|
|
and t.after then
|
|
t.after(...)
|
|
end
|
|
y_pending[command_invoker] = nil
|
|
end
|
|
)
|
|
|
|
local function override_cc_with_confirm(cname, func_before, actual_func_after)
|
|
-- remember the functions for /y if needed
|
|
local function func_after(...)
|
|
if y_pending[command_invoker] then
|
|
y_pending[command_invoker] = {before = func_before,
|
|
after = func_after}
|
|
end
|
|
return actual_func_after(...)
|
|
end
|
|
return override_chatcommand(cname, func_before, func_after)
|
|
end
|
|
|
|
|
|
-- override the worldedit vmanip finish function to catch the data table
|
|
local we_data = false
|
|
local we_manip_end = worldedit.manip_helpers.finish
|
|
function worldedit.manip_helpers.finish(manip, data)
|
|
if we_data == nil then
|
|
we_data = data
|
|
end
|
|
return we_manip_end(manip, data)
|
|
end
|
|
|
|
local indic_names = {"indices_n", "indices_p1", "indices_p2", "indices_m"}
|
|
local function compress_nodedata(nodedata)
|
|
local data = {}
|
|
-- put indices first
|
|
for j = 1,#indic_names do
|
|
local indices = nodedata[indic_names[j]]
|
|
if indices then
|
|
local prev_index = 0
|
|
for i = 1,#indices do
|
|
local index = indices[i]
|
|
local off = index - prev_index -- always > 0
|
|
local v = ""
|
|
for f = nodedata.index_bytes, 0, -1 do
|
|
v = v .. string.char(math.floor(off * 2^(-8*f)) % 0x100)
|
|
end
|
|
data[#data+1] = v
|
|
prev_index = index
|
|
end
|
|
end
|
|
end
|
|
-- nodeids contain 16 bit values (see mapnode.h)
|
|
-- big endian here
|
|
if nodedata.indices_n then
|
|
for i = 1,#nodedata.nodeids do
|
|
data[#data+1] = string.char(math.floor(nodedata.nodeids[i] * 2^-8)
|
|
) .. string.char(nodedata.nodeids[i] % 0x100)
|
|
end
|
|
end
|
|
-- param1 and param2 are 8 bit values
|
|
for j = 1,2 do
|
|
if nodedata["indices_p" .. j] then
|
|
local vs = nodedata["param" .. j .. "s"]
|
|
local p = #data
|
|
for i = 1,#vs do
|
|
data[p+1] = string.char(vs[i])
|
|
end
|
|
end
|
|
end
|
|
-- meta…
|
|
if nodedata.indices_m then
|
|
data[#data+1] = minetest.serialize(nodedata.metastrings)
|
|
end
|
|
return minetest.compress(table.concat(data))
|
|
end
|
|
|
|
local cnt_names = {"nodeids_cnt", "param1s_cnt", "param2s_cnt", "metaens_cnt"}
|
|
local function decompress_nodedata(ccontent)
|
|
local result = {}
|
|
local data = minetest.decompress(ccontent.compressed_data)
|
|
local p = 1
|
|
-- get indices
|
|
for i = 1,#cnt_names do
|
|
local cnt = ccontent[cnt_names[i]]
|
|
if cnt then
|
|
local indices = {}
|
|
local prev_index = 0
|
|
for i = 1,cnt do
|
|
local v = prev_index
|
|
for f = ccontent.index_bytes, 0, -1 do
|
|
v = v + 2^(8*f) * data:byte(p)
|
|
p = p+1
|
|
end
|
|
indices[i] = v
|
|
prev_index = v
|
|
end
|
|
result[indic_names[i]] = indices
|
|
end
|
|
end
|
|
-- get nodeids
|
|
if ccontent.nodeids_cnt then
|
|
local nodeids = {}
|
|
for i = 1,ccontent.nodeids_cnt do
|
|
nodeids[i] = data:byte(p) * 0x100 + data:byte(p+1)
|
|
p = p + 2
|
|
end
|
|
result.nodeids = nodeids
|
|
end
|
|
-- get param1s and param2s
|
|
for j = 1,2 do
|
|
local cnt = ccontent["param" .. j .. "s_cnt"]
|
|
if cnt then
|
|
local vs = {}
|
|
for i = 1,cnt do
|
|
vs[i] = data:byte(p)
|
|
p = p+1
|
|
end
|
|
result["param" .. j .. "s"] = vs
|
|
end
|
|
end
|
|
-- get metaens strings
|
|
if ccontent.metaens_cnt then
|
|
result.metastrings = minetest.deserialize(data:sub(p))
|
|
end
|
|
return result
|
|
end
|
|
|
|
local we_set = worldedit.set
|
|
local function my_we_set(pos1, pos2, ...)
|
|
assert(command_invoker, "Player not known")
|
|
pos1, pos2 = worldedit.sort_pos(pos1, pos2)
|
|
-- FIXME: Protection support isn't needed
|
|
|
|
local manip = minetest.get_voxel_manip()
|
|
local e1, e2 = manip:read_from_map(pos1, pos2)
|
|
local area = VoxelArea:new{MinEdge=e1, MaxEdge=e2}
|
|
local data_before = manip:get_data()
|
|
|
|
we_data = nil
|
|
local rv = we_set(pos1, pos2, ...)
|
|
|
|
local ystride = pos2.x - pos1.x + 1
|
|
local zstride = (pos2.y - pos1.y + 1) * ystride
|
|
-- put indices separate because they don't correlate with nodeids
|
|
local indices = {}
|
|
local nodeids = {}
|
|
for z = pos1.z, pos2.z do
|
|
for y = pos1.y, pos2.y do
|
|
for x = pos1.x, pos2.x do
|
|
local i = area:index(x,y,z)
|
|
if we_data[i] ~= data_before[i] then
|
|
indices[#indices+1] =
|
|
(z - pos1.z) * zstride
|
|
+ (y - pos1.y) * ystride
|
|
+ x - pos1.x
|
|
nodeids[#nodeids+1] = data_before[i]
|
|
end
|
|
end
|
|
end
|
|
end
|
|
we_data = false
|
|
|
|
-- can be 0 if only one node is changed
|
|
local index_bytes = math.ceil(math.log(worldedit.volume(pos1, pos2)) /
|
|
math.log(8))
|
|
local compressed_data = compress_nodedata{
|
|
indices_n = indices,
|
|
nodeids = nodeids,
|
|
index_bytes = index_bytes,
|
|
}
|
|
add_to_history({
|
|
type = "nodeids",
|
|
mem_use = 9 * (2 * 7) + #compressed_data,
|
|
pos1 = pos1,
|
|
pos2 = pos2,
|
|
count = #nodeids,
|
|
index_bytes = index_bytes,
|
|
compressed_data = compressed_data
|
|
}, command_invoker)
|
|
-- Note: param1, param2 and metadata are not changed by worldedit.set
|
|
return rv
|
|
end
|
|
override_cc_with_confirm("/set",
|
|
function()
|
|
worldedit.set = my_we_set
|
|
end,
|
|
function()
|
|
worldedit.set = we_set
|
|
end
|
|
)
|
|
|
|
undo_funcs.nodeids = function(name, data)
|
|
local pos1 = data.pos1
|
|
local pos2 = data.pos2
|
|
local ylen = pos2.y - pos1.y + 1
|
|
local ystride = pos2.x - pos1.x + 1
|
|
|
|
local decompressed_data = decompress_nodedata{
|
|
compressed_data = data.compressed_data,
|
|
nodeids_cnt = data.count,
|
|
index_bytes = data.index_bytes
|
|
}
|
|
local indices = decompressed_data.indices_n
|
|
local nodeids = decompressed_data.nodeids
|
|
|
|
local manip = minetest.get_voxel_manip()
|
|
local e1, e2 = manip:read_from_map(pos1, pos2)
|
|
local area = VoxelArea:new{MinEdge=e1, MaxEdge=e2}
|
|
local mdata = manip:get_data()
|
|
|
|
-- swap the nodes in the world and history data
|
|
local new_nodeids = {}
|
|
for k = 1,#indices do
|
|
local i = indices[k]
|
|
local x = i % ystride
|
|
local y = math.floor(i / ystride) % ylen
|
|
local z = math.floor(i / (ystride * ylen))
|
|
local vi = area:index(pos1.x + x, pos1.y + y, pos1.z + z)
|
|
new_nodeids[k] = mdata[vi]
|
|
mdata[vi] = nodeids[k]
|
|
end
|
|
|
|
manip:set_data(mdata)
|
|
manip:write_to_map()
|
|
|
|
data.compressed_data = compress_nodedata{
|
|
indices_n = indices,
|
|
nodeids = new_nodeids,
|
|
index_bytes = data.index_bytes
|
|
}
|
|
data.mem_usage = #data.compressed_data
|
|
|
|
worldedit.player_notify(name, data.count .. " nodes set")
|
|
end
|
|
undo_info_funcs.nodeids = function(data)
|
|
return "pos1: " .. minetest.pos_to_string(data.pos1) .. ", pos2: " ..
|
|
minetest.pos_to_string(data.pos2) .. ", " .. data.count ..
|
|
" nodes changed"
|
|
end
|
|
|
|
|
|
-- tells if the metadata is that dummy
|
|
local function is_meta_empty(metatabl)
|
|
if metatabl.inventory
|
|
and next(metatabl.inventory) ~= nil then
|
|
return false
|
|
end
|
|
if metatabl.fields
|
|
and next(metatabl.fields) ~= nil then
|
|
return false
|
|
end
|
|
for k in pairs(metatabl) do
|
|
if k ~= "inventory"
|
|
and k ~= "fields" then
|
|
return false
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- copied from we to get the same meta format
|
|
local function make_meta_serializable(metat)
|
|
for _, inventory in pairs(metat.inventory) do
|
|
for index, stack in ipairs(inventory) do
|
|
inventory[index] = stack.to_string and stack:to_string() or stack
|
|
end
|
|
end
|
|
end
|
|
|
|
local we_deserialize = worldedit.deserialize
|
|
local function my_we_deserialize(pos, ...)
|
|
-- remember the previous nodes and meta
|
|
local nodes = {}
|
|
local metaens = {}
|
|
local add_node = minetest.add_node
|
|
local function my_add_node(entry)
|
|
local current_node = minetest.get_node(entry)
|
|
local have_changes = 3
|
|
local def_ent = minetest.registered_nodes[entry.name]
|
|
local def_cur = minetest.registered_nodes[current_node.name]
|
|
if current_node.name == entry.name then
|
|
current_node.name = nil
|
|
have_changes = 2
|
|
end
|
|
if current_node.param1 == (entry.param1 or 0)
|
|
or (def_ent and def_cur -- don't save volatile light values or param1=0
|
|
and (def_ent.paramtype == "light" or (entry.param1 or 0) == 0)
|
|
and (def_cur.paramtype == "light" or current_node.param1 == 0)
|
|
) then
|
|
current_node.param1 = nil
|
|
have_changes = have_changes-1
|
|
end
|
|
if current_node.param2 == (entry.param2 or 0) then
|
|
current_node.param2 = nil
|
|
have_changes = have_changes-1
|
|
end
|
|
local pos = {x=entry.x, y=entry.y, z=entry.z}
|
|
-- we calls add_node always before setting any meta, save it here
|
|
local metat = minetest.get_meta(pos):to_table()
|
|
if is_meta_empty(metat) then
|
|
metat = nil
|
|
else
|
|
make_meta_serializable(metat)
|
|
end
|
|
local new_metat = entry.meta
|
|
if new_metat
|
|
and is_meta_empty(new_metat) then
|
|
new_metat = nil
|
|
end
|
|
local meta_changed = (metat or new_metat)
|
|
and (not metat or not new_metat
|
|
or minetest.serialize(metat) ~= minetest.serialize(new_metat)
|
|
)
|
|
if meta_changed then
|
|
metaens[#metaens+1] = {pos, metat}
|
|
end
|
|
|
|
if have_changes > 0 then
|
|
nodes[#nodes+1] = {pos, current_node}
|
|
elseif not meta_changed then
|
|
-- neither nodes, nor meta has changed
|
|
return
|
|
end
|
|
|
|
-- set the original functions due to on_construct and on_destruct
|
|
minetest.add_node = add_node
|
|
|
|
minetest.add_node(pos, entry)
|
|
|
|
minetest.add_node = my_add_node
|
|
end
|
|
|
|
minetest.add_node = my_add_node
|
|
|
|
local count = we_deserialize(pos, ...)
|
|
|
|
minetest.add_node = add_node
|
|
|
|
if #nodes == 0
|
|
and #metaens == 0 then
|
|
-- nothing happened
|
|
return count
|
|
end
|
|
|
|
-- add nodes, param1, param2 and meta changes to history
|
|
-- get pos1 and pos2
|
|
local minp = vector.new((nodes[1] or metaens[1])[1])
|
|
local maxp = vector.new(minp)
|
|
for i = 1,#nodes do
|
|
local pos = nodes[i][1]
|
|
for c,v in pairs(pos) do
|
|
if v > maxp[c] then
|
|
maxp[c] = v
|
|
elseif v < minp[c] then
|
|
minp[c] = v
|
|
end
|
|
end
|
|
end
|
|
for i = 1,#metaens do
|
|
local pos = metaens[i][1]
|
|
for c,v in pairs(pos) do
|
|
if v > maxp[c] then
|
|
maxp[c] = v
|
|
elseif v < minp[c] then
|
|
minp[c] = v
|
|
end
|
|
end
|
|
end
|
|
|
|
-- order nodes, param1s, param2s and metaens
|
|
local ystride = maxp.x - minp.x + 1
|
|
local zstride = (maxp.y - minp.y + 1) * ystride
|
|
for i = 1,#nodes do
|
|
local rpos = vector.subtract(nodes[i][1], minp)
|
|
nodes[i][1] = rpos.z * zstride + rpos.y * ystride + rpos.x
|
|
end
|
|
table.sort(nodes, function(a, b)
|
|
return a[1] < b[1]
|
|
end)
|
|
local indices_n = {}
|
|
local indices_p1 = {}
|
|
local indices_p2 = {}
|
|
local nodeids = {}
|
|
local param1s = {}
|
|
local param2s = {}
|
|
for i = 1,#nodes do
|
|
local v = nodes[i][2]
|
|
local id = v.name and minetest.get_content_id(v.name)
|
|
if id then
|
|
indices_n[#indices_n+1] = nodes[i][1]
|
|
nodeids[#nodeids+1] = id
|
|
end
|
|
if v.param1 then
|
|
indices_p1[#indices_p1+1] = nodes[i][1]
|
|
param1s[#param1s+1] = v.param1
|
|
end
|
|
if v.param2 then
|
|
indices_p2[#indices_p2+1] = nodes[i][1]
|
|
param2s[#param2s+1] = v.param2
|
|
end
|
|
end
|
|
|
|
for i = 1,#metaens do
|
|
local rpos = vector.subtract(metaens[i][1], minp)
|
|
metaens[i][1] = rpos.z * zstride + rpos.y * ystride + rpos.x
|
|
end
|
|
table.sort(metaens, function(a, b)
|
|
return a[1] < b[1]
|
|
end)
|
|
local indices_m = {}
|
|
local metastrings = {}
|
|
for i = 1,#metaens do
|
|
indices_m[i] = metaens[i][1]
|
|
metastrings[i] = minetest.serialize(metaens[i][2])
|
|
end
|
|
|
|
-- compress the data and add it to history
|
|
local index_bytes = math.ceil(math.log(worldedit.volume(minp, maxp)) /
|
|
math.log(8))
|
|
local compressed_data = compress_nodedata{
|
|
indices_n = indices_n,
|
|
indices_p1 = indices_p1,
|
|
indices_p2 = indices_p2,
|
|
indices_m = indices_m,
|
|
nodeids = nodeids,
|
|
param1s = param1s,
|
|
param2s = param2s,
|
|
metastrings = metastrings,
|
|
index_bytes = index_bytes,
|
|
}
|
|
add_to_history({
|
|
type = "nodes",
|
|
mem_use = 9 * (2 * 7) + #compressed_data,
|
|
pos1 = minp,
|
|
pos2 = maxp,
|
|
count_n = #nodeids,
|
|
count_p1 = #param1s,
|
|
count_p2 = #param2s,
|
|
count_m = #metastrings,
|
|
index_bytes = index_bytes,
|
|
compressed_data = compressed_data
|
|
}, command_invoker)
|
|
|
|
return count
|
|
end
|
|
override_cc_with_confirm("/load",
|
|
function()
|
|
worldedit.deserialize = my_we_deserialize
|
|
end,
|
|
function()
|
|
worldedit.deserialize = we_deserialize
|
|
end
|
|
)
|
|
|
|
undo_funcs.nodes = function(name, data)
|
|
local pos1 = data.pos1
|
|
local pos2 = data.pos2
|
|
local ylen = pos2.y - pos1.y + 1
|
|
local ystride = pos2.x - pos1.x + 1
|
|
|
|
local decompressed_data = decompress_nodedata{
|
|
compressed_data = data.compressed_data,
|
|
nodeids_cnt = data.count_n,
|
|
param1s_cnt = data.count_p1,
|
|
param2s_cnt = data.count_p2,
|
|
metaens_cnt = data.count_m,
|
|
index_bytes = data.index_bytes
|
|
}
|
|
local indices_n = decompressed_data.indices_n
|
|
local indices_p1 = decompressed_data.indices_p1
|
|
local indices_p2 = decompressed_data.indices_p2
|
|
local nodeids = decompressed_data.nodeids
|
|
local param1s = decompressed_data.param1s
|
|
local param2s = decompressed_data.param2s
|
|
|
|
-- swap the nodes, param1s and param2s in the world and history data
|
|
local manip = minetest.get_voxel_manip()
|
|
local e1, e2 = manip:read_from_map(pos1, pos2)
|
|
local area = VoxelArea:new{MinEdge=e1, MaxEdge=e2}
|
|
local m_nodes = manip:get_data()
|
|
local m_param1s = manip:get_light_data()
|
|
local m_param2s = manip:get_param2_data()
|
|
|
|
local mts = {m_nodes, m_param1s, m_param2s}
|
|
local indiceses = {indices_n, indices_p1, indices_p2}
|
|
local contentses = {nodeids, param1s, param2s}
|
|
for i = 1,3 do
|
|
local mt = mts[i]
|
|
local indices = indiceses[i]
|
|
local contents = contentses[i]
|
|
for k = 1,#indices do
|
|
local i = indices[k]
|
|
local x = i % ystride
|
|
local y = math.floor(i / ystride) % ylen
|
|
local z = math.floor(i / (ystride * ylen))
|
|
local vi = area:index(pos1.x + x, pos1.y + y, pos1.z + z)
|
|
contents[k], mt[vi] = mt[vi], contents[k]
|
|
end
|
|
end
|
|
|
|
manip:set_data(m_nodes)
|
|
manip:set_light_data(m_param1s)
|
|
manip:set_param2_data(m_param2s)
|
|
manip:write_to_map()
|
|
|
|
-- swap metaens strings
|
|
local indices_m = decompressed_data.indices_m
|
|
local metastrings = decompressed_data.metastrings
|
|
for k = 1,#indices_m do
|
|
local i = indices_m[k]
|
|
local meta = minetest.get_meta(vector.add(pos1, {
|
|
x = i % ystride,
|
|
y = math.floor(i / ystride) % ylen,
|
|
z = math.floor(i / (ystride * ylen))
|
|
}))
|
|
local metat = meta:to_table()
|
|
if is_meta_empty(metat) then
|
|
metat = nil
|
|
else
|
|
make_meta_serializable(metat)
|
|
end
|
|
meta:from_table(minetest.deserialize(metastrings[k]))
|
|
metastrings[k] = minetest.serialize(metat)
|
|
end
|
|
|
|
-- update history entry
|
|
data.compressed_data = compress_nodedata{
|
|
indices_n = indices_n,
|
|
indices_p1 = indices_p1,
|
|
indices_p2 = indices_p2,
|
|
indices_m = indices_m,
|
|
nodeids = nodeids,
|
|
param1s = param1s,
|
|
param2s = param2s,
|
|
metastrings = metastrings,
|
|
index_bytes = data.index_bytes,
|
|
}
|
|
data.mem_usage = #data.compressed_data
|
|
|
|
worldedit.player_notify(name, data.count_n .. " nodes set, " ..
|
|
data.count_p1 .. " param1s set, " .. data.count_p2 ..
|
|
" param2s set and " .. #indices_m .. " metaens changed")
|
|
end
|