mirror of
https://github.com/HybridDog/we_undo.git
synced 2024-12-12 17:13:16 +01:00
440 lines
11 KiB
Lua
440 lines
11 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
|
|
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
|
|
print(j.off_start, j.entry_count)
|
|
-- 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
|
|
)
|
|
|
|
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
|
|
|
|
|
|
-- 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 function compress_nodedata(nodedata)
|
|
-- nodeids contain 16 bit values (see mapnode.h)
|
|
-- big endian here
|
|
local data = {}
|
|
local prev_index = 0
|
|
for i = 1,#nodedata.indices do
|
|
local index = nodedata.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)
|
|
data[i] = v
|
|
end
|
|
prev_index = index
|
|
end
|
|
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
|
|
return minetest.compress(table.concat(data))
|
|
end
|
|
|
|
local function decompress_nodedata(ccontent)
|
|
local indices = {}
|
|
local nodeids = {}
|
|
local data = minetest.decompress(ccontent.compressed_data)
|
|
local nodeids_cnt = ccontent.nodeids_cnt
|
|
assert(#data == nodeids_cnt * (ccontent.index_bytes+1 + 2),
|
|
"invalid decompressed data")
|
|
local p = 1
|
|
local prev_index = 0
|
|
for i = 1,nodeids_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
|
|
for i = 1,nodeids_cnt do
|
|
nodeids[i] = data:byte(p) * 0x100 + data:byte(p+1)
|
|
p = p + 2
|
|
end
|
|
--~ print("compression factor: " ..
|
|
--~ (nodeids_cnt * 9) / #ccontent.compressed_data)
|
|
return {indices = indices, nodeids = nodeids}
|
|
end
|
|
|
|
local we_set = worldedit.set
|
|
local my_we_set = function(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 = 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_chatcommand("/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
|
|
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 = indices,
|
|
nodeids = new_nodeids,
|
|
index_bytes = data.index_bytes
|
|
}
|
|
|
|
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
|