diff --git a/README.md b/README.md index 75ac5b2..4dfa43b 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,84 @@ -Minetest Mod Edit -================== +# Minetest Edit Mod -This mod provides a block way to cut, paste, fill or delete regions [![ContentDB](https://content.minetest.net/packages/Mr.%20Rar/edit/shields/downloads/)](https://content.minetest.net/packages/Mr.%20Rar/edit/) -Information ------------ +## Overview -This mod is named `edit`, permits to a player, manage regions by the usage -of blocks to mark ones. It provides functionalities of `cut`, `copy`, `paste` and `delete`, -but using always special blocks to mark the dessired area. + +This mod named `edit` allows copying, pasting, filling, deleting, opening and saving 3D areas. +Schematics are loaded and saved from .mts files located in the world subfolder `schems`. This mod was inspired by the Fill Start and Fill End blocks in Manic Digger. ![screenshot](screenshot.png) -Technical information ---------------------- +## Items -This source provides mod named `edit`, a block way to cut, paste, fill -or delete regions by the usage of special blocks. - -| Block | internal name | item | -| ----------- | ---------- | ------------------- | -| Copy block | edit:copy | ![](textures/edit_copy.png) | -| Paste block | edit:paste | ![](textures/edit_paste.png) | -| Fill block | edit:fill | ![](textures/edit_fill.png) | -| Delete block | edit:delete | ![](textures/edit_delete.png) | - -##### dependences - -none - -Usage ------- - -The usage is by the mark of areas beetween two blocks, player must have the special blocks, -must mark an area using two paired blocks of the same type, later can operate that area: +| Name | Node ID | Image | +| ------ | ----------- | ----------------------------- | +| Copy | edit:copy | ![](textures/edit_copy.png) | +| Paste | edit:paste | ![](textures/edit_paste.png) | +| Fill | edit:fill | ![](textures/edit_fill.png) | +| Delete | edit:delete | ![](textures/edit_delete.png) | +| Open | edit:open | ![](textures/edit_open.png) | +| Save | edit:save | ![](textures/edit_save.png) | +| Undo | edit:undo | ![](textures/edit_undo.png) | -#### Copy Block and Paste Block +## Dependences -![editblocks1copy.png](editblocks1copy.png) - -When two Copy Blocks are placed they select an area between them as show -in figure 1. When an area is surrounded it is copied and the two Copy -Blocks are deleted. - -The Paste Block is used for pasting a 3D area copied by the Copy Block. -When a Paste Block is placed the copied area is pasted relative to the -position of the first placed Copy Block when it was copied. - -###### Delete Block - -Delete Blocks are used to delete a 3D area. When two Delete Blocks are -placed they select an area between them as show in figure 1. When an area -is surrounded it is deleted and the two Delete Blocks are deleted as well. - -#### Fill Block - -![](editblocks2delete.png) +None -Fill Blocks are used to fill a 3D area with a certain block. You can do so -by first selecting an area by surrounding it with two fill blocks. Fill -Blocks work differently when surrounding an area in that the area selected -includes the position of the Fill Blocks themselves as show in figure 2. +## Usage -Once an area is surrounded a dialog pops up and shows you all the blocks -in your inventory. If you press one, the selected area will be filled with -the block pressed. If you press a blank slot the selected area will be -filled with air. To cancel, press the "X". +### Copy Node -LICENCE -------- +![figure1.png](figure1.png) -CC0 by MrRar check [LICENSE](LICENSE) file, this mod was started by MrRar, -minetest-mods community later absorved. +When two copy nodes are placed at opposite corners of an area, they select the area as show in figure 1. When an area is selected, it is copied and the two copy nodes are deleted. + + +### Paste Node + +The paste node is used for pasting the area copied by the copy node or a schematic loaded with the open node. When a paste node is placed, the copied area or schematic is placed at the corner of the paste node. In the case of a copied area, area is pasted relative to the position of the first placed copy node when it was copied. The copied area can be rotated when the paste preview is visable by pressing the sneak key and an arrow key. + + +### Delete Node + +Delete nodes are used to delete an area. When two delete nodes are placed at opposite corners of an area, they select the area as show in figure 1. When an area is selected, it is deleted and the two delete nodes are removed as well. + + +### Fill Node + +![figure1.png](figure2.png) + +Fill nodes are used to fill a 3D area with a certain item. Start by placing two fill nodes at opposite corners of the desired area. The selected area includes the positions of the fill nodes themselves as shown in figure 2. + +Once a second fill node is placed, a dialog appears listing all items in the players inventory. Clicking an item will cause it to be used used for filling the selected area. Clicking on a blank slot will cause the selected area to be filled with air. To cancel the fill, press the "X". + + +### Open Tool + +Right click with this tool to load a .mts schematic for pasting from the the world subfolder `schems`. + + +### Save Tool + +Right click with this tool to save copied area as a .mts schematic in the the world subfolder `schems`. + + +### Undo Tool + +Right click with this tool to undo a world modification like filling or pasting. +Use a second time to redo the undo. +Only the most resent world modification can be undone. + + +### License + +CC0 by MrRar check [License](LICENSE) file, this mod was started by MrRar, +minetest-mods community. diff --git a/description.txt b/description.txt deleted file mode 100644 index f1c4a8d..0000000 --- a/description.txt +++ /dev/null @@ -1 +0,0 @@ -This mod allows you to copy, paste, fill and delete 3D areas using blocks pairs. diff --git a/editblocks1copy.png b/editblocks1copy.png deleted file mode 100644 index b00a433..0000000 Binary files a/editblocks1copy.png and /dev/null differ diff --git a/editblocks2delete.png b/editblocks2delete.png deleted file mode 100644 index dce20d3..0000000 Binary files a/editblocks2delete.png and /dev/null differ diff --git a/figure1.png b/figure1.png new file mode 100644 index 0000000..643c964 Binary files /dev/null and b/figure1.png differ diff --git a/figure2.png b/figure2.png new file mode 100644 index 0000000..b866782 Binary files /dev/null and b/figure2.png differ diff --git a/init.lua b/init.lua index 12ecfac..96d10d4 100644 --- a/init.lua +++ b/init.lua @@ -1,236 +1,488 @@ ---[[ - Edit Mod v0.3 -]] +------------------- +-- Edit Mod v1.0 -- +------------------- + +local player_data = {} + +local paste_preview_max_entities = tonumber(minetest.settings:get("paste_preview_max_entities") or 2000) +local max_operation_volume = tonumber(minetest.settings:get("max_operation_volume") or 20000) + +local function create_paste_preview(player) + local player_pos = player:get_pos() + local base_objref = minetest.add_entity(player_pos, "edit:preview_base") + local schematic = player_data[player].schematic + + local count = 0 + for i, map_node in pairs(schematic.data) do + if map_node.name ~= "air" then count = count + 1 end + end + local probability = paste_preview_max_entities / count + + local start = vector.new(1, 1, 1) + local voxel_area = VoxelArea:new({MinEdge = start, MaxEdge = schematic.size}) + local size = schematic.size + for i in voxel_area:iterp(start, size) do + local pos = voxel_area:position(i) + + if schematic._rotation == 90 then + pos = vector.new(pos.z, pos.y, size.x - pos.x + 1) + elseif schematic._rotation == 180 then + pos = vector.new(size.x - pos.x + 1, pos.y, size.z - pos.z + 1) + elseif schematic._rotation == 270 then + pos = vector.new(size.z - pos.z + 1, pos.y, pos.x) + end + + local name = schematic.data[i].name + if name ~= "air" and math.random() < probability then + local attach_pos = vector.multiply(vector.subtract(vector.add(pos, schematic._offset), 1), 10) + local objref = minetest.add_entity(player_pos, "edit:preview_node") + objref:set_properties({wield_item = name}) + objref:set_attach(base_objref, "", attach_pos) + end + end + player_data[player].paste_preview = base_objref + player_data[player].paste_preview_yaw = 0 +end + +local function delete_paste_preview(player) + local paste_preview = player_data[player].paste_preview + if not paste_preview or not paste_preview:get_pos() then return end + + local objrefs = paste_preview:get_children() + for i, objref in pairs(objrefs) do + objref:remove() + end + player_data[player].paste_preview:remove() + player_data[player].paste_preview_visable = false + player_data[player].paste_preview = nil +end + +local function set_schematic_rotation(schematic, angle) + if not schematic._rotation then schematic._rotation = 0 end + schematic._rotation = schematic._rotation + angle + if schematic._rotation < 0 then + schematic._rotation = schematic._rotation + 360 + elseif schematic._rotation > 270 then + schematic._rotation = schematic._rotation - 360 + end + + local size = schematic.size + if schematic._rotation == 90 or schematic._rotation == 270 then + size = vector.new(size.z, size.y, size.x) + end + + local sign = vector.apply(schematic._offset, math.sign) + schematic._offset = vector.apply( + vector.multiply(size, sign), + function(n) return n < 0 and n or 1 end + ) + --[[local old_schematic = player_data[player].schematic + local new_schematic = {data = {}} + player_data[player].schematic = new_schematic + + local old_size = old_schematic.size + local new_size + if direction == "L" or direction == "R" then + new_size = vector.new(old_size.z, old_size.y, old_size.x) + elseif direction == "U" or direction == "D" then + new_size = vector.new(old_size.y, old_size.x, old_size.z) + end + new_schematic.size = new_size + + local sign = vector.apply(old_schematic._offset, math.sign) + new_schematic._offset = vector.apply( + vector.multiply(new_size, sign), + function(n) return n < 0 and n or 1 end + ) + + local start = vector.new(1, 1, 1) + local old_voxel_area = VoxelArea:new({MinEdge = start, MaxEdge = old_size}) + local new_voxel_area = VoxelArea:new({MinEdge = start, MaxEdge = new_size}) + for old_index in old_voxel_area:iterp(start, old_schematic.size) do + local old_pos = old_voxel_area:position(old_index) + local new_pos + local node = old_schematic.data[old_index] + + if direction == "L" then + new_pos = vector.new(old_pos.z, old_pos.y, old_size.x - old_pos.x + 1) + elseif direction == "R" then + new_pos = vector.new(old_size.z - old_pos.z + 1, old_pos.y, old_pos.x) + elseif direction == "U" then + new_pos = vector.new(old_pos.y, old_size.x - old_pos.x + 1, old_pos.z) + elseif direction == "D" then + new_pos = vector.new(old_size.y - old_pos.y + 1, old_pos.x, old_pos.z) + end + + local new_index = new_voxel_area:indexp(new_pos) + new_schematic.data[new_index] = node + end + delete_paste_preview(player)]] +end ---Add priv minetest.register_privilege("edit", { - description = "Lets you use edit, copy, paste, delete blocks", - give_to_singleplayer= true, + description = "Allows usage of edit mod nodes", + give_to_singleplayer = true, give_to_admin = true, }) ---end add priv - --- add in priv check -local function check_privilege(player) +local function has_privilege(player) local name = player:get_player_name() if minetest.check_player_privs(name, {edit = true}) then return true else - minetest.chat_send_player(name, "You can't use an edit block without the edit privilege.") + minetest.chat_send_player(name, "Using edit nodes requires the edit privilege.") return false end end ---end priv check -local function sign(x) -- different from math.sign never returns 0. - if x > 0 then - return 1 - elseif x < -0 then - return -1 - end - return 1 +local function on_place_checks(player) + return player and + player:is_player() and + has_privilege(player) +end + + +local function schematic_from_map(pos, size) + local schematic = {data = {}} + schematic.size = size + schematic._pos = pos + + local start = vector.new(1, 1, 1) + local voxel_area = VoxelArea:new({MinEdge = start, MaxEdge = size}) + + for i in voxel_area:iterp(start, size) do + local offset = voxel_area:position(i) + local node_pos = vector.subtract(vector.add(pos, offset), start) + local node = minetest.get_node(node_pos) + node.param1 = nil + schematic.data[i] = node + end + + return schematic end --- Delete Block minetest.register_node("edit:delete", { - description = "Delete", + description = "Edit Delete", inventory_image = "edit_delete.png", groups = {snappy = 2, oddly_breakable_by_hand = 3}, tiles = {"edit_delete.png"}, - on_place = function(itemstack, placer, pointed_thing) - if not placer then return itemstack end + range = 10, + on_place = function(itemstack, player, pointed_thing) + if not on_place_checks(player) then return end - if not check_privilege(placer) then return itemstack end + local itemstack, pos = minetest.item_place_node(itemstack, player, pointed_thing) - if clipboard[placer:get_player_name()].delete_block1_pos then - local p1 = clipboard[placer:get_player_name()].delete_block1_pos - local p2 = pointed_thing.above + if player_data[player].delete_node1_pos and pos then + local p1 = player_data[player].delete_node1_pos + player_data[player].delete_node1_pos = nil + local p2 = pos minetest.remove_node(p1) + minetest.remove_node(p2) - p1.x = p1.x + math.sign(p2.x - p1.x) - p1.y = p1.y + math.sign(p2.y - p1.y) - p1.z = p1.z + math.sign(p2.z - p1.z) - p2.x = p2.x + math.sign(p1.x - p2.x) - p2.y = p2.y + math.sign(p1.y - p2.y) - p2.z = p2.z + math.sign(p1.z - p2.z) + -- Is the volume of the selected area 0? + local test = vector.apply(vector.subtract(p2, p1), math.abs) + if test.x <= 1 or test.y <= 1 or test.z <= 1 then return end - for x = p1.x, p2.x, sign(p2.x - p1.x) do - for y = p1.y, p2.y, sign(p2.y - p1.y) do - for z = p1.z, p2.z, sign(p2.z - p1.z) do - minetest.remove_node(vector.new(x, y, z)); + local sign = vector.new(vector.apply(vector.subtract(p2, p1), math.sign)) + p1 = vector.add(p1, sign) + p2 = vector.add(p2, vector.multiply(sign, -1)) + + local start = vector.new( + math.min(p1.x, p2.x), + math.min(p1.y, p2.y), + math.min(p1.z, p2.z) + ) + local _end = vector.new( + math.max(p1.x, p2.x), + math.max(p1.y, p2.y), + math.max(p1.z, p2.z) + ) + local size = vector.add(vector.subtract(_end, start), 1) + if size.x * size.y * size.z > max_operation_volume then + minetest.chat_send_player(player:get_player_name(), "Delete operation too big.") + return + end + player_data[player].undo_schematic = schematic_from_map(start, size) + + for x = start.x, _end.x, 1 do + for y = start.y, _end.y, 1 do + for z = start.z, _end.z, 1 do + minetest.remove_node(vector.new(x, y, z)) end end end - clipboard[placer:get_player_name()].delete_block1_pos = nil - else - minetest.set_node(pointed_thing.above, {name = "edit:delete"}) - clipboard[placer:get_player_name()].delete_block1_pos = pointed_thing.above + elseif pos then + player_data[player].delete_node1_pos = pos end end, on_dig = function(pos, node, digger) - for name, value in pairs(clipboard) do - minetest.remove_node(pos); + minetest.remove_node(pos) + for player, data in pairs(player_data) do if - clipboard[name].delete_block1_pos - and clipboard[name].delete_block1_pos.x == pos.x - and clipboard[name].delete_block1_pos.y == pos.y - and clipboard[name].delete_block1_pos.z == pos.z + data.delete_node1_pos and + vector.equals(data.delete_node1_pos, pos) then - clipboard[name].delete_block1_pos = nil + data.delete_node1_pos = nil break end end end }) --- Copy Block minetest.register_node("edit:copy",{ - description = "Copy", + description = "Edit Copy", tiles = {"edit_copy.png"}, inventory_image = "edit_copy.png", groups = {snappy = 2, oddly_breakable_by_hand = 3}, - on_place = function(itemstack, placer, pointed_thing) - if not placer then return itemstack end + range = 10, + on_place = function(itemstack, player, pointed_thing) + if not on_place_checks(player) then return end - if not check_privilege(placer) then return itemstack end + local itemstack, pos = minetest.item_place_node(itemstack, player, pointed_thing) - local name = placer:get_player_name() - if clipboard[name].copy_block1_pos then - clipboard[name].copy_data = {} -- clear out old copy data - local copy_data = clipboard[name].copy_data - local p1 = clipboard[name].copy_block1_pos - local p2 = pointed_thing.above + if player_data[player].copy_node1_pos and pos then + local p1 = player_data[player].copy_node1_pos + local p2 = pos - minetest.remove_node(p1); -- remove copy block 1. - -- We don't have to remove copy block 2 because we never placed! + player_data[player].copy_node1_pos = nil + minetest.remove_node(p1) + minetest.remove_node(p2) - local offsetX = math.sign(p2.x - p1.x) - local offsetY = math.sign(p2.y - p1.y) - local offsetZ = math.sign(p2.z - p1.z) + local diff = vector.subtract(p2, p1) - p1.x = p1.x + math.sign(p2.x - p1.x) - p1.y = p1.y + math.sign(p2.y - p1.y) - p1.z = p1.z + math.sign(p2.z - p1.z) - - p2.x = p2.x + math.sign(p1.x - p2.x) - p2.y = p2.y + math.sign(p1.y - p2.y) - p2.z = p2.z + math.sign(p1.z - p2.z) + -- Is the volume of the selected area 0? + local test = vector.apply(diff, math.abs) + if test.x <= 1 or test.y <= 1 or test.z <= 1 then return itemstack end - for x = p1.x, p2.x, sign(p2.x - p1.x) do - local xDif = offsetX + x - p1.x - copy_data[xDif] = {} - for y = p1.y, p2.y, sign(p2.y - p1.y) do - local yDif = offsetY + y - p1.y - copy_data[xDif][yDif] = {} - for z = p1.z, p2.z, sign(p2.z - p1.z) do - local zDif = offsetZ + z - p1.z - copy_data[xDif][yDif][zDif] = minetest.get_node( - vector.new(x, y, z) - ) - end - end + local sign = vector.apply(vector.subtract(p2, p1), math.sign) + p1 = vector.add(p1, sign) + p2 = vector.add(p2, vector.multiply(sign, -1)) + + local start = vector.new( + math.min(p1.x, p2.x), + math.min(p1.y, p2.y), + math.min(p1.z, p2.z) + ) + local _end = vector.new( + math.max(p1.x, p2.x), + math.max(p1.y, p2.y), + math.max(p1.z, p2.z) + ) + + local size = vector.add(vector.subtract(_end, start), vector.new(1, 1, 1)) + if size.x * size.y * size.z > max_operation_volume then + minetest.chat_send_player(player:get_player_name(), "Copy operation too big.") + return end - clipboard[placer:get_player_name()].copy_block1_pos = nil - else - minetest.set_node(pointed_thing.above, {name = "edit:copy"}) - clipboard[placer:get_player_name()].copy_block1_pos = pointed_thing.above + + player_data[player].schematic = schematic_from_map(start, size) + player_data[player].schematic._offset = vector.apply( + diff, + function(n) return n < 0 and n + 1 or 1 end + ) + delete_paste_preview(player) + elseif pos then + player_data[player].copy_node1_pos = pos end end, on_dig = function(pos, node, digger) - for name, value in pairs(clipboard) do - minetest.remove_node(pos); + minetest.remove_node(pos) + for player, data in pairs(player_data) do if - clipboard[name].copy_block1_pos - and clipboard[name].copy_block1_pos.x == pos.x - and clipboard[name].copy_block1_pos.y == pos.y - and clipboard[name].copy_block1_pos.z == pos.z + data.copy_node1_pos and + vector.equals(data.copy_node1_pos, pos) then - clipboard[name].copy_block1_pos = nil + data.copy_node1_pos = nil break end end end }) --- Paste Block -minetest.register_node("edit:paste", { - description = "Paste", +local function pointed_thing_to_pos(pointed_thing) + local pos = pointed_thing.under + local node = minetest.get_node_or_nil(pos) + local def = node and minetest.registered_nodes[node.name] + if def and def.buildable_to then + return pos + end + + pos = pointed_thing.above + node = minetest.get_node_or_nil(pos) + def = node and minetest.registered_nodes[node.name] + if def and def.buildable_to then + return pos + end +end + +minetest.register_tool("edit:paste", { + description = "Edit Paste", tiles = {"edit_paste.png"}, inventory_image = "edit_paste.png", groups = {snappy = 2, oddly_breakable_by_hand = 3}, - on_place = function(itemstack, placer, pointed_thing) - if not placer then return itemstack end + range = 10, + on_place = function(itemstack, player, pointed_thing) + if not on_place_checks(player) then return end - if not check_privilege(placer) then return itemstack end + if not player_data[player].schematic then + minetest.chat_send_player(player:get_player_name(), "Nothing to paste.") + return + end - local copy_data = clipboard[placer:get_player_name()].copy_data - local pos = pointed_thing.above - for x, yTable in pairs(copy_data) do - for y, zTable in pairs(yTable) do - for z, node in pairs(zTable) do - minetest.set_node(vector.new( - pos.x + x, - pos.y + y, - pos.z + z - ), node) - end - end + local schematic = player_data[player].schematic + local pos = pointed_thing_to_pos(pointed_thing) + if not pos then return end + local pos = vector.add(pos, schematic._offset) + local size = schematic.size + if schematic._rotation == 90 or schematic._rotation == 270 then + size = vector.new(size.z, size.y, size.x) + end + player_data[player].undo_schematic = schematic_from_map(pos, size) + minetest.place_schematic(pos, schematic, tostring(schematic._rotation or 0), nil, true) + end +}) + +local function delete_schematics_dialog(player) + local path = minetest.get_worldpath() .. "/schems" + local dir_list = minetest.get_dir_list(path) + if #path > 40 then path = "..." .. path:sub(#path - 40, #path) end + local formspec = "size[10,10]label[0.5,0.5;Delete Schematics from:\n" .. + minetest.formspec_escape(path) .. "]button_exit[9,0;1,1;quit;X]" .. + "textlist[0.5,2;9,7;schems;" .. table.concat(dir_list, ",") .. "]" + + reliable_show_formspec(player, "edit:delete_schem", formspec) +end + +minetest.register_tool("edit:open",{ + description = "Edit Open", + inventory_image = "edit_open.png", + range = 10, + on_place = function(itemstack, player, pointed_thing) + if not on_place_checks(player) then return end + + local path = minetest.get_worldpath() .. "/schems" + local dir_list = minetest.get_dir_list(path) + if #path > 40 then path = "..." .. path:sub(#path - 40, #path) end + local formspec = "size[10,10]label[0.5,0.5;Load a schematic into copy buffer from:\n" .. + minetest.formspec_escape(path) .. "]button_exit[9,0;1,1;quit;X]" .. + "textlist[0.5,2;9,7;schems;" .. table.concat(dir_list, ",") .. "]" .. + "button_exit[3,9.25;4,1;delete;Delete schematics...]" + + minetest.show_formspec(player:get_player_name(), "edit:open", formspec) + end +}) + +minetest.register_tool("edit:undo",{ + description = "Edit Undo", + inventory_image = "edit_undo.png", + range = 10, + on_place = function(itemstack, player, pointed_thing) + if not on_place_checks(player) then return end + + local schem = player_data[player].undo_schematic + if schem then + player_data[player].undo_schematic = schematic_from_map(schem._pos, schem.size) + minetest.place_schematic(schem._pos, schem, nil, nil, true) + else + minetest.chat_send_player(player:get_player_name(), "Nothing to undo.") end end }) --- Fill Block +function reliable_show_formspec(player, name, formspec) + -- We need to do this nonsense because there is bug in Minetest + -- Sometimes no formspec is shown if you call minetest.show_formspec + -- from minetest.register_on_player_receive_fields + minetest.after(0.1, function() + if not player or not player:is_player() then return end + minetest.show_formspec(player:get_player_name(), name, formspec) + end) +end + +local function show_save_dialog(player, filename, save_error) + if not player_data[player].schematic then + minetest.chat_send_player(player:get_player_name(), "Nothing to save.") + return + end + + filename = filename or "untitled" + + local path = minetest.get_worldpath() .. "/schems" + if #path > 40 then path = "..." .. path:sub(#path - 40, #path) end + + local formspec = "size[8,3]label[0.5,0.1;Save schematic in:\n" .. + minetest.formspec_escape(path) .. "]button_exit[7,0;1,1;cancel;X]" .. + "field[0.5,1.5;5.5,1;schem_filename;;" .. filename .. "]" .. + "button_exit[5.7,1.2;2,1;save;Save]" + + if save_error then + formspec = formspec .. + "label[0.5,2.5;" .. save_error .. "]" + end + reliable_show_formspec(player, "edit:save", formspec) +end + +minetest.register_tool("edit:save",{ + description = "Edit Save", + inventory_image = "edit_save.png", + range = 10, + on_place = function(itemstack, player, pointed_thing) + if on_place_checks(player) then show_save_dialog(player) end + end +}) + minetest.register_node("edit:fill",{ - description = "Fill", + description = "Edit Fill", tiles = {"edit_fill.png"}, inventory_image = "edit_fill.png", groups = {snappy = 2, oddly_breakable_by_hand = 3}, - on_place = function(itemstack, placer, pointed_thing) - if not placer then return itemstack end + range = 10, + on_place = function(itemstack, player, pointed_thing) + if not on_place_checks(player) then return end - if not check_privilege(placer) then return itemstack end + local itemstack, pos = minetest.item_place_node(itemstack, player, pointed_thing) - if clipboard[placer:get_player_name()].fill_block1 then - minetest.set_node(pointed_thing.above, {name = "edit:fill"}) - clipboard[placer:get_player_name()].fill_block2 = pointed_thing + if player_data[player].fill1_pos and pos then + player_data[player].fill2_pos = pos + player_data[player].fill_pointed_thing = pointed_thing - local inv = minetest.get_inventory({type = "player", name = placer:get_player_name()}) - local formSpec = "size[8,6]label[0.5,0.5;Select the material you would like to use]button_exit[7,0;1,1;quit;X]" + local inv = minetest.get_inventory({type = "player", name = player:get_player_name()}) + local formspec = "size[8,6]label[2,0.5;Select item for filling]button_exit[7,0;1,1;quit;X]" for y = 1, 4 do for x = 1, 8 do local name = inv:get_stack("main", ((y - 1) * 8) + x):get_name() - formSpec = - formSpec - .. "item_image_button[" - .. (x - 1) .. "," - .. (y + 1) .. ";1,1;" - .. name .. ";" - .. name .. ";]" + formspec = + formspec .. + "item_image_button[" .. + (x - 1) .. "," .. + (y + 1) .. ";1,1;" .. + name .. ";" .. + name .. ";]" end end - minetest.show_formspec(placer:get_player_name(), "edit:select_fill_type", formSpec) - else - minetest.set_node(pointed_thing.above, {name = "edit:fill"}) - clipboard[placer:get_player_name()].fill_block1 = pointed_thing + minetest.show_formspec(player:get_player_name(), "edit:fill", formspec) + elseif pos then + player_data[player].fill1_pos = pos end end, on_dig = function(pos, node, digger) - minetest.remove_node(pos); - for name, fills in pairs(clipboard) do - local block1 = fills.fill_block1 - local block2 = fills.fill_block2 - if block1 and vector.equals(block1.above, pos) then - fills.fill_block1 = nil - fills.fill_block2 = nil - minetest.remove_node(block1.above) + minetest.remove_node(pos) + for player, data in pairs(player_data) do + local p1 = data.fill1_pos + local p2 = data.fill2_pos + if p1 and vector.equals(p1, pos) then + data.fill1_pos = nil + data.fill2_pos = nil + data.fill_pointed_thing = nil + minetest.remove_node(p1) return end - if block2 and vector.equals(block2.above, pos) then - fills.fill_block1 = nil - fills.fill_block2 = nil - minetest.remove_node(block2.above) + if p2 and vector.equals(p2, pos) then + data.fill1_pos = nil + data.fill2_pos = nil + data.fill_pointed_thing = nil + minetest.remove_node(p2) return end end @@ -238,79 +490,392 @@ minetest.register_node("edit:fill",{ }) minetest.register_on_player_receive_fields(function(player, formname, fields) - if not player then return false end - if formname ~= "edit:select_fill_type" then return false end - - minetest.close_formspec(player:get_player_name(), "edit:select_fill_type") - - if - not clipboard[player:get_player_name()].fill_block1 - or not clipboard[player:get_player_name()].fill_block2 - then return true end - - local p1 = clipboard[player:get_player_name()].fill_block1.above - local p2 = clipboard[player:get_player_name()].fill_block2.above - local pointed_thing = clipboard[player:get_player_name()].fill_block1 - clipboard[player:get_player_name()].fill_block1 = nil - clipboard[player:get_player_name()].fill_block2 = nil - minetest.remove_node(p1) - minetest.remove_node(p2) - - local item = next(fields) - if item == "quit" then return true end - if item == "" then item = "air" end + if formname == "edit:fill" then + minetest.close_formspec(player:get_player_name(), "edit:fill") + + local p1 = player_data[player].fill1_pos + local p2 = player_data[player].fill2_pos + local pointed_thing = player_data[player].fill_pointed_thing + + if + not p1 or not p2 or + not pointed_thing or + not has_privilege(player) + then return true end + + player_data[player].fill1_pos = nil + player_data[player].fill2_pos = nil + player_data[player].fill_pointed_thing = nil + minetest.remove_node(p1) + minetest.remove_node(p2) + + local name + local def + for key, val in pairs(fields) do + if key == "quit" then return true end + if key == "" then item = "air" end - local def = minetest.registered_nodes[item] - or minetest.registered_craftitems[item] - or minetest.registered_tools[item] - or minetest.registered_items[item] - - if not def then return true end + name = key + def = minetest.registered_nodes[name] or + minetest.registered_craftitems[name] or + minetest.registered_tools[name] or + minetest.registered_items[name] + + if def then break end + end - local is_node = minetest.registered_nodes[item] + if not def then return true end + + local is_node = minetest.registered_nodes[name] + + local param2 + if def.paramtype2 == "facedir" then + param2 = minetest.dir_to_facedir(player:get_look_dir()) + elseif def.paramtype2 == "wallmounted" then + param2 = minetest.dir_to_wallmounted(player:get_look_dir(), true) + end - local param2 - if def.paramtype2 == "facedir" then - param2 = minetest.dir_to_facedir(player:get_look_dir()) - elseif def.paramtype2 == "wallmounted" then - param2 = minetest.dir_to_wallmounted(player:get_look_dir(), true) - end - - for x = p1.x, p2.x, sign(p2.x - p1.x) do - for y = p1.y, p2.y, sign(p2.y - p1.y) do - for z = p1.z, p2.z, sign(p2.z - p1.z) do - if is_node then - minetest.set_node(vector.new(x, y, z), {name = item, param2 = param2}) - else - minetest.remove_node(vector.new(x, y, z)) - end - if def.on_place then - local itemstack = ItemStack(item) - pointed_thing.intersection_point = vector.new(x + 0.5, y, z + 0.5) - pointed_thing.above = vector.new(x, y, z) - pointed_thing.below = vector.new(x, y - 1, z) - def.on_place(itemstack, player, pointed_thing) + local on_place = def.on_place + + local start = vector.new( + math.min(p1.x, p2.x), + math.min(p1.y, p2.y), + math.min(p1.z, p2.z) + ) + local _end = vector.new( + math.max(p1.x, p2.x), + math.max(p1.y, p2.y), + math.max(p1.z, p2.z) + ) + + local size = vector.add(vector.subtract(_end, start), 1) + if size.x * size.y * size.z > max_operation_volume then + minetest.chat_send_player(player:get_player_name(), "Fill operation too large.") + return true + end + player_data[player].undo_schematic = schematic_from_map(start, size) + + for x = start.x, _end.x, 1 do + for y = start.y, _end.y, 1 do + for z = start.z, _end.z, 1 do + local pos = vector.new(x, y, z) + if is_node then + minetest.set_node(pos, {name = name, param2 = param2}) + else + minetest.remove_node(pos) + end + if on_place then + local itemstack = ItemStack(name) + pointed_thing.intersection_point = vector.new(x + 0.5, y, z + 0.5) + pointed_thing.above = pos + pointed_thing.below = vector.new(x, y - 1, z) + on_place(itemstack, player, pointed_thing) + end end end end + return true + elseif formname == "edit:open" then + minetest.close_formspec(player:get_player_name(), "edit:open") + + if + fields.cancel + or not has_privilege(player) + then return true end + + if fields.delete then + delete_schematics_dialog(player) + return true + end + + if not fields.schems then return end + + local index = tonumber(fields.schems:sub(5, #(fields.schems))) + if not index then return true end + index = math.floor(index) + + local path = minetest.get_worldpath() .. "/schems" + local dir_list = minetest.get_dir_list(path) + if index > 0 and index <= #dir_list then + local file_path = path .. "/" .. dir_list[index] + local schematic = minetest.read_schematic(file_path, {}) + if not schematic then return true end + player_data[player].schematic = schematic + player_data[player].schematic._offset = vector.new(1, 1, 1) + minetest.chat_send_player(player:get_player_name(), dir_list[index] .. " loaded.") + delete_paste_preview(player) + end + return true + elseif formname == "edit:save" then + minetest.close_formspec(player:get_player_name(), "edit:save") + + local schematic = player_data[player].schematic + local schem_filename = fields.schem_filename + + if + fields.cancel or + not schem_filename or + not schematic or + not has_privilege(player) + then return end + + local path = minetest.get_worldpath() .. "/schems" + local schem_filename = schem_filename .. ".mts" + local dir_list = minetest.get_dir_list(path) + for _, filename in pairs(dir_list) do + if filename == schem_filename then + show_save_dialog(player, fields.schem_filename, fields.schem_filename .. " already exists.") + return true + end + end + + local mts = minetest.serialize_schematic(schematic, "mts", {}) + if not mts then return true end + + minetest.mkdir(path) + local schem_path = path .. "/" .. schem_filename + local f = io.open(schem_path, "wb"); + if not f then + minetest.chat_send_player(player:get_player_name(), "IO error saving schematic.") + return true + end + f:write(mts); + f:close() + minetest.chat_send_player(player:get_player_name(), schem_filename .. " saved.") + return true + elseif formname == "edit:delete_schem" then + if + fields.cancel + or not has_privilege(player) + then return true end + + if not fields.schems then return end + + local index = tonumber(fields.schems:sub(5, #(fields.schems))) + if not index then return true end + index = math.floor(index) + + local path = minetest.get_worldpath() .. "/schems" + local dir_list = minetest.get_dir_list(path) + if index > 0 and index <= #dir_list then + player_data[player].schem_for_delete = path .. "/" .. dir_list[index] + formspec = "size[8,3]label[0.5,0.5;Confirm delete \"" .. + dir_list[index] .. "\"]" .. + "button_exit[1,2;2,1;delete;Delete]" .. + "button_exit[5,2;2,1;quit;Cancel]" + + reliable_show_formspec(player, "edit:confirm_delete_schem", formspec) + end + return true + elseif formname == "edit:confirm_delete_schem" then + if not has_privilege(player) then return end + + if fields.delete then + os.remove(player_data[player].schem_for_delete) + end + player_data[player].schem_for_delete = nil + delete_schematics_dialog(player) + end + return false +end) + +minetest.register_entity("edit:select_preview", { + initial_properties = { + visual = "cube", + physical = false, + pointable = false, + collide_with_objects = false, + static_save = false, + use_texture_alpha = true, + glow = -1, + backface_culling = false, + textures = { + "edit_select_preview.png", + "edit_select_preview.png", + "edit_select_preview.png", + "edit_select_preview.png", + "edit_select_preview.png", + "edit_select_preview.png", + }, + } +}) + +minetest.register_entity("edit:preview_base", { + initial_properties = { + visual = "sprite", + physical = false, + pointable = false, + collide_with_objects = false, + static_save = false, + visual_size = {x = 1, y = 1}, + textures = {"blank.png"}, + } +}) + +minetest.register_entity("edit:preview_node", { + initial_properties = { + visual = "item", + physical = false, + pointable = false, + collide_with_objects = false, + static_save = false, + visual_size = {x = 0.69, y = 0.69}, + glow = -1, + } +}) + +local function hide_paste_preview(player) + local d = player_data[player] + --d.paste_preview:set_properties({is_visible = false}) + -- This does not work right. + -- Some child entities do not become visable when you set is_visable back to true + + for _, objref in pairs(d.paste_preview:get_children()) do + objref:set_properties({is_visible = false}) + end + d.paste_preview:set_attach(player) + player:hud_remove(d.paste_preview_hud) + d.paste_preview_hud = nil +end + +local function show_paste_preview(player) + local d = player_data[player] + for _, objref in pairs(d.paste_preview:get_children()) do + objref:set_properties({is_visible = true}) + end + d.paste_preview:set_detach() + d.paste_preview_hud = player:hud_add({ + hud_elem_type = "text", + text = "Press sneak + right or left to rotate.", + position = {x = 0.5, y = 0.8}, + z_index = 100, + number = 0xffffff + }) + + -- Minetset bug: set_pos does not get to the client + -- sometimes after showing a ton of children + minetest.after(0.3, + function(objref) + local pos = objref:get_pos() + if pos then objref:set_pos(pos) end + end, + d.paste_preview + ) +end + +local function get_player_pointed_thing_pos(player) + local look_dir = player:get_look_dir() + local pos1 = player:get_pos() + local eye_height = player:get_properties().eye_height + pos1.y = pos1.y + eye_height + local pos2 = vector.add(pos1, vector.multiply(look_dir, 10)) + local pointed_thing = minetest.raycast(pos1, pos2, false, false):next() + if pointed_thing then + return pointed_thing_to_pos(pointed_thing) + end +end + +local function hide_select_preview(player) + local d = player_data[player] + d.select_preview_shown = false + d.select_preview:set_properties({is_visible = false}) + d.select_preview:set_attach(player) +end + +minetest.register_globalstep(function(dtime) + for _, player in pairs(minetest.get_connected_players()) do + local item = player:get_wielded_item():get_name() + local d = player_data[player] + + -- Paste preview + if item == "edit:paste" and d.schematic then + local pos = get_player_pointed_thing_pos(player) + if pos then + if not d.paste_preview or not d.paste_preview:get_pos() then + create_paste_preview(player) + end + + if not d.paste_preview_hud then show_paste_preview(player) end + + local old_pos = player_data[player].paste_preview:get_pos() + if not vector.equals(old_pos, pos) then + player_data[player].paste_preview:set_pos(pos) + end + local control = player:get_player_control() + if control.sneak and control.left then + if d.paste_preview_can_rotate then + set_schematic_rotation(d.schematic, -90) + delete_paste_preview(player) + d.paste_preview_can_rotate = false + end + elseif control.sneak and control.right then + if d.paste_preview_can_rotate then + set_schematic_rotation(d.schematic, 90) + delete_paste_preview(player) + d.paste_preview_can_rotate = false + end + else d.paste_preview_can_rotate = true end + elseif d.paste_preview_hud then hide_paste_preview(player) end + elseif d.paste_preview_hud then hide_paste_preview(player) end + + -- Select preview + local node1_pos + local node2_pos + local include_node_pos = false + if item == "edit:fill" and d.fill1_pos then + node1_pos = d.fill1_pos + if d.fill2_pos then node2_pos = d.fill2_pos end + include_node_pos = true + elseif item == "edit:copy" and d.copy_node1_pos then + node1_pos = d.copy_node1_pos + elseif item == "edit:delete" and d.delete_node1_pos then + node1_pos = d.delete_node1_pos + end + + if node1_pos then + if not node2_pos then + node2_pos = get_player_pointed_thing_pos(player) + end + + if node2_pos then + local diff = vector.subtract(node1_pos, node2_pos) + local size = vector.apply(diff, math.abs) + if include_node_pos then size = vector.add(size, vector.new(1, 1, 1)) + else size = vector.add(size, vector.new(-1, -1, -1)) end + + local test = vector.apply(diff, math.abs) + local has_volume = test.x > 1 and test.y > 1 and test.z > 1 + local size_too_big = size.x * size.y * size.z > max_operation_volume + if (include_node_pos or has_volume) and not size_too_big then + if not d.select_preview or not d.select_preview:get_pos() then + d.select_preview = minetest.add_entity(node2_pos, "edit:select_preview") + d.select_preview_shown = true + elseif not d.select_preview_shown then + d.select_preview:set_detach() + d.select_preview:set_properties({is_visible = true}) + d.select_preview_shown = true + end + local preview_pos = vector.add(node2_pos, vector.multiply(diff, 0.5)) + local preview = d.select_preview + if not vector.equals(preview_pos, preview:get_pos()) then + preview:set_pos(preview_pos) + local preview_size = vector.add(size, vector.new(0.01, 0.01, 0.01)) + preview:set_properties({visual_size = preview_size}) + end + elseif d.select_preview_shown then hide_select_preview(player) end + elseif d.select_preview_shown then hide_select_preview(player) end + elseif d.select_preview_shown then hide_select_preview(player) end end - return true end) -clipboard = {}; minetest.register_on_joinplayer(function(player) - if player then - clipboard[player:get_player_name()] = { - fill_block1 = nil, - fill_block2 = nil, - copy_block1_pos = nil, - delete_block1_pos = nil, - copy_data = {}, - }; - end -end); + player_data[player] = {} +end) + minetest.register_on_leaveplayer(function(player) - if player then - clipboard[player:get_player_name()] = nil + delete_paste_preview(player) + if player_data[player].select_preview then + player_data[player].select_preview:remove() end -end); + player_data[player] = nil +end) diff --git a/mod.conf b/mod.conf index f0d5f6a..a37e746 100644 --- a/mod.conf +++ b/mod.conf @@ -1,2 +1,3 @@ name = edit -description = This mod allows you to copy, paste, fill and delete 3D areas using blocks pairs +description = Allows copying, pasting, filling, deleting, opening and saving 3D areas. +min_minetest_version = 5.4 diff --git a/screenshot.png b/screenshot.png index 7a146b6..300fc3b 100644 Binary files a/screenshot.png and b/screenshot.png differ diff --git a/settingtypes.txt b/settingtypes.txt new file mode 100644 index 0000000..7434cda --- /dev/null +++ b/settingtypes.txt @@ -0,0 +1,4 @@ + +paste_preview_max_entities (Paste preview max entities) int 2000 + +max_operation_volume (Max edit operation volume) int 20000 diff --git a/textures/edit_fill.png b/textures/edit_fill.png index d5ce84f..191ba11 100644 Binary files a/textures/edit_fill.png and b/textures/edit_fill.png differ diff --git a/textures/edit_open.png b/textures/edit_open.png new file mode 100644 index 0000000..4ed90bb Binary files /dev/null and b/textures/edit_open.png differ diff --git a/textures/edit_save.png b/textures/edit_save.png new file mode 100644 index 0000000..c3910be Binary files /dev/null and b/textures/edit_save.png differ diff --git a/textures/edit_select_preview.png b/textures/edit_select_preview.png new file mode 100644 index 0000000..680dcfd Binary files /dev/null and b/textures/edit_select_preview.png differ diff --git a/textures/edit_undo.png b/textures/edit_undo.png new file mode 100644 index 0000000..59f8368 Binary files /dev/null and b/textures/edit_undo.png differ