----------------- Settings ----------------------------------------------------- local remember_innocuous = minetest.settings:get_bool"we_undo.remember_innocuous" ~= false local max_commands = tonumber( minetest.settings:get"we_undo.max_commands") or 256 local min_commands = tonumber(minetest.settings:get"we_undo.min_commands") or 3 local max_memory_usage = tonumber( minetest.settings:get"we_undo.max_memory_usage") or 2^25 ----------------- Journal and we_undo chatcommands ----------------------------- local command_invoker local function override_chatcommand(cname, func_before, func_after) local command = minetest.registered_chatcommands[cname] if not command then local cmds = {} for name in pairs(minetest.registered_chatcommands) do cmds[#cmds+1] = name end error("Chatcommand " .. cname .. " is not registered.\n" .. "Available commands: " .. table.concat(cmds, ", ")) end -- 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 succ, msg = func(name, ...) local new_succ, new_msg = func_after(...) command_invoker = nil -- Both have to succeed for the return value succ = succ and new_succ msg = new_msg or msg or nil return succ, msg end else local func = command.func command.func = function(...) local succ, msg = func(...) command_invoker = nil return succ, msg 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 -- make every entry supposedly have >= 16 bytes data.mem_use = (data.mem_use or 0) + 16 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 bare_apply_undo(j, 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 or 0) + 16 - old_memuse j.ring[i] = data end local function apply_undo(name) local j = journal[name] bare_apply_undo(j, name) 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 -- undoing an undone undo function is redoing bare_apply_undo(j, name) 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, }) ----------------- Harmless worldedit chatcommands ------------------------------ if remember_innocuous then -- short commands (/1 in this case) are automatically supported override_chatcommand("/pos1", function() add_to_history{ type = "marker", id = 1, pos = worldedit.pos1[command_invoker] } end ) override_chatcommand("/pos2", function() add_to_history{ type = "marker", 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", id = 1, pos = worldedit.pos1[name] }, name) elseif typ == "pos2" then add_to_history({ type = "marker", 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 end ----------------------- Functions common to other ones ------------------------- -- Remember the last command overridings for //y local y_pending = {} override_chatcommand("/n", function() y_pending[command_invoker] = nil end ) override_chatcommand("/y", function(...) local t = y_pending[command_invoker] if t and t.before then t.before(...) end end, function(...) local t = y_pending[command_invoker] if t and t.after then t.after(...) end y_pending[command_invoker] = nil end ) local function override_cc_with_confirm(cname, func_before, func_after) -- Remember the functions for //y if needed -- func_before and func_after are always executed before and after -- the command cname. -- func_before and func_after are executed a second time if the -- player then calls //y (unless the player calls //n before //y). -- Therefore these two functions should only do temporary overridings of -- relevant functions, e.g. worldedit.cube. local function func_after_wrap(...) y_pending[command_invoker] = {before = func_before, after = func_after} return func_after(...) end return override_chatcommand(cname, func_before, func_after_wrap) 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, n = {}, 0 -- 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-1, 0, -1 do v = v .. string.char(math.floor(off * 2^(-8*f)) % 0x100) end n = n+1 data[n] = 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 n = n+1 data[n] = 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"] for i = 1,#vs do n = n+1 data[n] = string.char(vs[i]) end end end -- meta… if nodedata.indices_m then n = n+1 data[n] = 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 k = 1,cnt do local v = prev_index for f = ccontent.index_bytes-1, 0, -1 do v = v + 2^(8*f) * data:byte(p) p = p+1 end indices[k] = 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 -- 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 -- Gets information about meta if it is set, otherwise returns nil -- the format of the information is the same as in WorldEdit local function get_meta_serializable(pos) if not minetest.find_nodes_with_meta(pos, pos)[1] then return end local meta = minetest.get_meta(pos) local metat = meta:to_table() if is_meta_empty(metat) then -- FIXME: is this case covered by minetest.find_nodes_with_meta? minetest.log("error", "metadata should be inexistent") return end for _, inventory in pairs(metat.inventory) do for index = 1,#inventory do local itemstack = inventory[index] if itemstack.to_string then inventory[index] = itemstack:to_string() end end end return metat, meta end -- Collects all metadata in a serialized format inside the given area -- This may be a slow function, thus should only be used when needed local function get_metadatas_in_area(pos1, pos2) local meta_ps = minetest.find_nodes_with_meta(pos1, pos2) local meta_tables_list = {} local ystride = pos2.x - pos1.x + 1 local zstride = (pos2.y - pos1.y + 1) * ystride for i = 1, #meta_ps do local pos = meta_ps[i] local meta = minetest.get_meta(pos) local metat = meta:to_table() if is_meta_empty(metat) then -- FIXME: is this case covered by minetest.find_nodes_with_meta? minetest.log("error", "metadata should be inexistent") else -- Make metat serializable for _, inventory in pairs(metat.inventory) do for index = 1,#inventory do local itemstack = inventory[index] if itemstack.to_string then inventory[index] = itemstack:to_string() end end end local rpos = vector.subtract(pos, pos1) meta_tables_list[#meta_tables_list+1] = { rpos.z * zstride + rpos.y * ystride + rpos.x, metat } end end table.sort(meta_tables_list, function(a, b) return a[1] < b[1] end) local indices_m = {} local metastrings = {} for i = 1, #meta_tables_list do indices_m[i] = meta_tables_list[i][1] metastrings[i] = minetest.serialize(meta_tables_list[i][2]) end return indices_m, metastrings end -- A generic function to collect the changed nodes and metadata -- (if collect_meta is true) between the times before and after executing func local function run_and_capture_changes(func, pos1, pos2, collect_meta) -- Get the node ids, param1s and param2s (before) local manip = minetest.get_voxel_manip() local e1, e2 = manip:read_from_map(pos1, pos2) local area = VoxelArea:new{MinEdge=e1, MaxEdge=e2} local nodeids_before = manip:get_data() local param1s_before = manip:get_light_data() local param2s_before = manip:get_param2_data() local indices_m_before, metastrings_before if collect_meta then indices_m_before, metastrings_before = get_metadatas_in_area(pos1, pos2) end -- Run the actual function func() -- Get the node ids, param1s and param2s (after) manip = minetest.get_voxel_manip() manip:read_from_map(pos1, pos2) local nodeids_after = manip:get_data() local param1s_after = manip:get_light_data() local param2s_after = manip:get_param2_data() local indices_m_after, metastrings_after if collect_meta then indices_m_after, metastrings_after = get_metadatas_in_area(pos1, pos2) end -- Collect the changed nodes local ystride = pos2.x - pos1.x + 1 local zstride = (pos2.y - pos1.y + 1) * ystride local indices_n = {} local indices_p1 = {} local indices_p2 = {} local nodeids = {} local param1s = {} local param2s = {} for z = pos1.z, pos2.z do for y = pos1.y, pos2.y do for x = pos1.x, pos2.x do local vi_vm = area:index(x,y,z) local vi_my = (z - pos1.z) * zstride + (y - pos1.y) * ystride + x - pos1.x if nodeids_after[vi_vm] ~= nodeids_before[vi_vm] then indices_n[#indices_n+1] = vi_my nodeids[#nodeids+1] = nodeids_before[vi_vm] end if param1s_after[vi_vm] ~= param1s_before[vi_vm] then indices_p1[#indices_p1+1] = vi_my param1s[#param1s+1] = param1s_before[vi_vm] end if param2s_after[vi_vm] ~= param2s_before[vi_vm] then indices_p2[#indices_p2+1] = vi_my param2s[#param2s+1] = param2s_before[vi_vm] end end end end local indices_m = {} local metastrings = {} if collect_meta then -- Collect all metadata changes local i_after = 1 for i_before = 1, #indices_m_before do local vi_before = indices_m_before[i_before] local vi_after = indices_m_after[i_after] if vi_before < vi_after then -- Metadata has been removed at vi_before indices_m[#indices_m+1] = vi_before metastrings[#metastrings+1] = metastrings_before[i_before] elseif vi_before == vi_after then -- Metadata exists before and after if metastrings_before[i_before] ~= metastrings_after[i_after] then indices_m[#indices_m+1] = vi_before metastrings[#metastrings+1] = metastrings_before[i_before] end i_after = i_after + 1 if i_after > #indices_m_after then break end else while vi_before > vi_after do -- Metadata has been added at vi_after indices_m[#indices_m+1] = vi_after metastrings[#metastrings+1] = "return nil" i_after = i_after + 1 if i_after > #indices_m_after then break end vi_after = indices_m_after[i_after] end end end for i = i_after, #indices_m_after do -- Metadata has been added at i indices_m[#indices_m+1] = indices_m_after[i] metastrings[#metastrings+1] = "return nil" end end local changes = { 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 is needed later for compression index_bytes = math.ceil(math.log(worldedit.volume(pos1, pos2)) / math.log(0x100)), } return changes 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 mtsi = 1,3 do local mt = mts[mtsi] local indices = indiceses[mtsi] local contents = contentses[mtsi] 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 pos = vector.add(pos1, { x = i % ystride, y = math.floor(i / ystride) % ylen, z = math.floor(i / (ystride * ylen)) }) local metat, meta = get_meta_serializable(pos) meta = meta or minetest.get_meta(pos) 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 ----------------------- World changing commands -------------------------------- local function we_nodeset_wrapper(pos1, pos2, func, ...) 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 = func(...) 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 local index_bytes = math.ceil(math.log(worldedit.volume(pos1, pos2)) / math.log(0x100)) local compressed_data = compress_nodedata{ indices_n = indices, nodeids = nodeids, index_bytes = index_bytes, } add_to_history({ type = "nodeids", mem_use = #compressed_data, pos1 = pos1, pos2 = pos2, count = #nodeids, index_bytes = index_bytes, compressed_data = compressed_data }, command_invoker) return rv -- Note: param1, param2 and metadata are not changed by worldedit.set and -- similar functions 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 -- A helper function to override the simple functions, e.g. cube, sphere, etc. local function override_nodeid_changer(command_names, function_name, func_get_boundaries) -- The function from worldedit which changes nodes, e.g. worldedit["cube"] local changer_original = worldedit[function_name] -- The overridden function which uses we_nodeset_wrapper to get the nodes -- before the Worldedit operation (changer_original), -- then executes the Worldedit operation, -- and after that calculates and saves the changes local function changer_overridden(...) -- func_get_boundaries depends on the command; it should determine the -- area within which node ids are changed local pos1, pos2 = func_get_boundaries(...) return we_nodeset_wrapper(pos1, pos2, changer_original, ...) end -- Do the before and after overriding for i = 1, #command_names do override_cc_with_confirm(command_names[i], function() worldedit[function_name] = changer_overridden end, function() worldedit[function_name] = changer_original end ) end end override_nodeid_changer({"/set", "/mix"}, "set", function(pos1, pos2) return pos1, pos2 end) override_nodeid_changer({"/replace", "/replaceinverse"}, "replace", function(pos1, pos2) return pos1, pos2 end) override_nodeid_changer({"/cube", "/hollowcube"}, "cube", function(pos, w, h, l) local cw, ch, cl = math.ceil(w), math.ceil(h), math.ceil(l) local ox = math.ceil((cw-1)/2) local oz = math.ceil((cl-1)/2) local pos1 = {x = pos.x - ox, y = pos.y, z = pos.z - oz} local pos2 = {x = pos.x + ox, y = pos.y + ch - 1, z = pos.z + oz} return pos1, pos2 end) override_nodeid_changer({"/sphere", "/hollowsphere"}, "sphere", function(pos, radius) local r = math.ceil(radius) local pos1 = vector.subtract(pos, r) local pos2 = vector.add(pos, r) return pos1, pos2 end) override_nodeid_changer({"/dome", "/hollowdome"}, "dome", function(pos, radius) local r = math.ceil(radius) local pos1 = vector.subtract(pos, r) local pos2 = vector.add(pos, r) -- dome with negative radius looks different, I couldn't test it because -- //dome does not accept negative radii. FIXME assert(radius >= 0) -- a dome is a semi shpere, thus it's almost the same as sphere: -- below pos.y no nodes are set. pos1.y = pos.y return pos1, pos2 end) override_nodeid_changer({"/cylinder", "/hollowcylinder"}, "cylinder", function(pos, axis, length, radius) local r = math.ceil(radius) local pos1 = vector.subtract(pos, r) local pos2 = vector.add(pos, r) assert(radius >= 0) pos1[axis] = pos[axis] pos2[axis] = pos[axis] + length - 1 if length < 0 then pos1[axis], pos2[axis] = pos2[axis], pos1[axis] -- With negative length, the cylinder is shifted one node FIXME pos1[axis] = pos1[axis]-1 pos2[axis] = pos2[axis]-1 end return pos1, pos2 end) override_nodeid_changer({"/pyramid", "/hollowpyramid"}, "pyramid", function(pos, axis, height) local h = math.ceil(math.abs(height)) local pos1 = vector.subtract(pos, h-1) local pos2 = vector.add(pos, h-1) if height > 0 then pos1[axis] = pos[axis] else pos2[axis] = pos[axis] end return pos1, pos2 end) override_nodeid_changer({"/spiral"}, "spiral", function(pos, length, height, spacer) -- FIXME adding the spacer to the extent makes it work local extent = math.ceil(length / 2) + spacer local pos1 = vector.subtract(pos, extent) local pos2 = vector.add(pos, extent) pos1.y = pos.y pos2.y = pos.y + math.ceil(height) - 1 return pos1, pos2 end) local we_deserialize = worldedit.deserialize local function my_we_deserialize(pos_base, ...) -- remember the previous nodes and meta -- Collect the changes by overriding minetest.add_node since this is -- probably faster than loading the whole area including metadata before -- and after worldedit's operation 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 = get_meta_serializable(pos) local new_metat = entry.meta if new_metat and is_meta_empty(new_metat) then -- Worldedit save files usually contain redundant metadata 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_base, ...) 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(0x100)) 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 = #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 ) -- A helper function to override a complex function, e.g. mtschemplace local function get_overridden_changer(changer_original, func_get_boundaries, collect_meta) return function(...) -- Get the boundary positions local pos1, pos2 = func_get_boundaries(...) -- Execute the actual function while capturing the changed nodes, -- param1, param2 and metas (if collect_meta is set) local params = {...} local rvs -- return values from changer_original local changes = run_and_capture_changes(function() rvs = {changer_original(unpack(params))} end, pos1, pos2, collect_meta) -- Compress the collected changes and add it to history local compressed_data = compress_nodedata(changes) add_to_history({ type = "nodes", mem_use = #compressed_data, pos1 = pos1, pos2 = pos2, count_n = #changes.nodeids, count_p1 = #changes.param1s, count_p2 = #changes.param2s, count_m = #changes.metastrings, index_bytes = changes.index_bytes, compressed_data = compressed_data }, command_invoker) -- Return the changer_original's return values return unpack(rvs) end end local original_place_schematic = minetest.place_schematic local my_place_schematic = get_overridden_changer(original_place_schematic, function(pos, schematic_path, rotation, _, _, flags) -- Get the area which is changed by the schematic if rotation then minetest.log("error", "Received a rotation from worldedit's schematic placement; " .. "not yet implemented in we_undo") end if flags then minetest.log("error", "Received flags from worldedit's schematic placement; " .. "not yet implemented in we_undo") end local schem = minetest.read_schematic(schematic_path, {}) local pos1 = pos local pos2 = vector.subtract(vector.add(pos1, schem.size), 1) return pos1, pos2 end, false) override_cc_with_confirm("/mtschemplace", function() minetest.place_schematic = my_place_schematic end, function() minetest.place_schematic = original_place_schematic end ) local we_luatransform = worldedit.luatransform local my_luatransform = get_overridden_changer(we_luatransform, function(pos1_actual, pos2_actual) local pos1_further, pos2_further = worldedit.sort_pos(pos1_actual, pos2_actual) -- For safety, add a bit extra space since players can do arbitrary -- things at arbitrary positions with luatransform pos1_further = vector.subtract(pos1_further, 5) pos2_further = vector.add(pos2_further, 5) return pos1_further, pos2_further end, true) override_cc_with_confirm("/luatransform", function() worldedit.luatransform = my_luatransform end, function() worldedit.luatransform = we_luatransform end )