diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 670154e..0000000 --- a/LICENSE +++ /dev/null @@ -1,116 +0,0 @@ -CC0 1.0 Universal - -Statement of Purpose - -The laws of most jurisdictions throughout the world automatically confer -exclusive Copyright and Related Rights (defined below) upon the creator and -subsequent owner(s) (each and all, an "owner") of an original work of -authorship and/or a database (each, a "Work"). - -Certain owners wish to permanently relinquish those rights to a Work for the -purpose of contributing to a commons of creative, cultural and scientific -works ("Commons") that the public can reliably and without fear of later -claims of infringement build upon, modify, incorporate in other works, reuse -and redistribute as freely as possible in any form whatsoever and for any -purposes, including without limitation commercial purposes. These owners may -contribute to the Commons to promote the ideal of a free culture and the -further production of creative, cultural and scientific works, or to gain -reputation or greater distribution for their Work in part through the use and -efforts of others. - -For these and/or other purposes and motivations, and without any expectation -of additional consideration or compensation, the person associating CC0 with a -Work (the "Affirmer"), to the extent that he or she is an owner of Copyright -and Related Rights in the Work, voluntarily elects to apply CC0 to the Work -and publicly distribute the Work under its terms, with knowledge of his or her -Copyright and Related Rights in the Work and the meaning and intended legal -effect of CC0 on those rights. - -1. Copyright and Related Rights. A Work made available under CC0 may be -protected by copyright and related or neighboring rights ("Copyright and -Related Rights"). Copyright and Related Rights include, but are not limited -to, the following: - - i. the right to reproduce, adapt, distribute, perform, display, communicate, - and translate a Work; - - ii. moral rights retained by the original author(s) and/or performer(s); - - iii. publicity and privacy rights pertaining to a person's image or likeness - depicted in a Work; - - iv. rights protecting against unfair competition in regards to a Work, - subject to the limitations in paragraph 4(a), below; - - v. rights protecting the extraction, dissemination, use and reuse of data in - a Work; - - vi. database rights (such as those arising under Directive 96/9/EC of the - European Parliament and of the Council of 11 March 1996 on the legal - protection of databases, and under any national implementation thereof, - including any amended or successor version of such directive); and - - vii. other similar, equivalent or corresponding rights throughout the world - based on applicable law or treaty, and any national implementations thereof. - -2. Waiver. To the greatest extent permitted by, but not in contravention of, -applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and -unconditionally waives, abandons, and surrenders all of Affirmer's Copyright -and Related Rights and associated claims and causes of action, whether now -known or unknown (including existing as well as future claims and causes of -action), in the Work (i) in all territories worldwide, (ii) for the maximum -duration provided by applicable law or treaty (including future time -extensions), (iii) in any current or future medium and for any number of -copies, and (iv) for any purpose whatsoever, including without limitation -commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes -the Waiver for the benefit of each member of the public at large and to the -detriment of Affirmer's heirs and successors, fully intending that such Waiver -shall not be subject to revocation, rescission, cancellation, termination, or -any other legal or equitable action to disrupt the quiet enjoyment of the Work -by the public as contemplated by Affirmer's express Statement of Purpose. - -3. Public License Fallback. Should any part of the Waiver for any reason be -judged legally invalid or ineffective under applicable law, then the Waiver -shall be preserved to the maximum extent permitted taking into account -Affirmer's express Statement of Purpose. In addition, to the extent the Waiver -is so judged Affirmer hereby grants to each affected person a royalty-free, -non transferable, non sublicensable, non exclusive, irrevocable and -unconditional license to exercise Affirmer's Copyright and Related Rights in -the Work (i) in all territories worldwide, (ii) for the maximum duration -provided by applicable law or treaty (including future time extensions), (iii) -in any current or future medium and for any number of copies, and (iv) for any -purpose whatsoever, including without limitation commercial, advertising or -promotional purposes (the "License"). The License shall be deemed effective as -of the date CC0 was applied by Affirmer to the Work. Should any part of the -License for any reason be judged legally invalid or ineffective under -applicable law, such partial invalidity or ineffectiveness shall not -invalidate the remainder of the License, and in such case Affirmer hereby -affirms that he or she will not (i) exercise any of his or her remaining -Copyright and Related Rights in the Work or (ii) assert any associated claims -and causes of action with respect to the Work, in either case contrary to -Affirmer's express Statement of Purpose. - -4. Limitations and Disclaimers. - - a. No trademark or patent rights held by Affirmer are waived, abandoned, - surrendered, licensed or otherwise affected by this document. - - b. Affirmer offers the Work as-is and makes no representations or warranties - of any kind concerning the Work, express, implied, statutory or otherwise, - including without limitation warranties of title, merchantability, fitness - for a particular purpose, non infringement, or the absence of latent or - other defects, accuracy, or the present or absence of errors, whether or not - discoverable, all to the greatest extent permissible under applicable law. - - c. Affirmer disclaims responsibility for clearing rights of other persons - that may apply to the Work or any use thereof, including without limitation - any person's Copyright and Related Rights in the Work. Further, Affirmer - disclaims responsibility for obtaining any necessary consents, permissions - or other rights required for any use of the Work. - - d. Affirmer understands and acknowledges that Creative Commons is not a - party to this document and has no duty or obligation with respect to this - CC0 or use of the Work. - -For more information, please see - diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..f9ed8c8 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2023 MrRar +object_rotations.lua Copyright (c) 2022 Skamiz Kazzarch + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index 029123d..56d8221 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,6 @@ Edit tools and nodes can only be used by players with `edit` privilege. ## License -CC0 by MrRar check [License](LICENSE) file, this mod was started by MrRar, +MIT by MrRar check [License](LICENSE.txt) file, this mod was started by MrRar, minetest-mods community. diff --git a/copy.lua b/copy.lua new file mode 100644 index 0000000..b6361e7 --- /dev/null +++ b/copy.lua @@ -0,0 +1,84 @@ +local function copy_on_place(itemstack, player, pointed_thing) + if not edit.on_place_checks(player) then return end + if not pointed_thing.under then + pointed_thing = edit.get_pointed_thing_node(player) + end + local pos = pointed_thing.under + local d = edit.player_data[player] + + if d.copy_luaentity1 and pos then + local p1 = d.copy_luaentity1._pos + local p2 = pos + + d.copy_luaentity1.object:remove() + d.copy_luaentity1 = nil + + 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 > edit.max_operation_volume then + edit.display_size_error(player) + return + end + + d.schematic = edit.schematic_from_map(start, size) + edit.delete_paste_preview(player) + local function vector_to_string(v) return "(" .. v.x .. ", " .. v.y .. ", " .. v.z .. ")" end + minetest.chat_send_player( + player:get_player_name(), + vector_to_string(start) .. " to " .. vector_to_string(_end) .. " copied." ) + else + local obj_ref = minetest.add_entity(pos, "edit:copy") + if not obj_ref then return end + local luaentity = obj_ref:get_luaentity() + luaentity._pos = pos + luaentity._placer = player + d.copy_luaentity1 = luaentity + end +end + +minetest.register_tool("edit:copy",{ + description = "Edit Copy", + tiles = {"edit_copy.png"}, + inventory_image = "edit_copy.png", + groups = {snappy = 2, oddly_breakable_by_hand = 3}, + range = 10, + on_place = copy_on_place, + on_secondary_use = copy_on_place, +}) + +minetest.register_entity("edit:copy", { + initial_properties = { + visual = "cube", + visual_size = { x = 1.1, y = 1.1}, + physical = false, + collide_with_objects = false, + static_save = false, + use_texture_alpha = true, + glow = -1, + backface_culling = false, + hp_max = 1, + textures = { + "edit_copy.png", + "edit_copy.png", + "edit_copy.png", + "edit_copy.png", + "edit_copy.png", + "edit_copy.png", + }, + }, + on_death = function(self, killer) + if edit.player_data[self._placer] then + edit.player_data[self._placer].copy_luaentity1 = nil + end + end, +}) diff --git a/figure1.png b/figure1.png deleted file mode 100644 index 643c964..0000000 Binary files a/figure1.png and /dev/null differ diff --git a/figure2.png b/figure2.png deleted file mode 100644 index b866782..0000000 Binary files a/figure2.png and /dev/null differ diff --git a/fill.lua b/fill.lua new file mode 100644 index 0000000..dd2e6f1 --- /dev/null +++ b/fill.lua @@ -0,0 +1,182 @@ +local function fill_on_place(itemstack, player, pointed_thing) + if not edit.on_place_checks(player) then return end + + if not pointed_thing.above then + pointed_thing = edit.get_pointed_thing_node(player) + end + + local itemstack, pos = minetest.item_place_node(itemstack, player, pointed_thing) + + local player_data = edit.player_data + if player_data[player].fill1_pos and pos then + local diff = vector.subtract(player_data[player].fill1_pos, pos) + local size = vector.add(vector.apply(diff, math.abs), 1) + if size.x * size.y * size.z > edit.max_operation_volume then + edit.display_size_error(player) + minetest.remove_node(player_data[player].fill1_pos) + player_data[player].fill1_pos = nil + minetest.remove_node(pos) + return + end + + player_data[player].fill2_pos = pos + player_data[player].fill_pointed_thing = pointed_thing + + 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 .. ";]" + end + end + minetest.show_formspec(player:get_player_name(), "edit:fill", formspec) + elseif pos then + player_data[player].fill1_pos = pos + end +end + +minetest.register_node("edit:fill",{ + description = "Edit Fill", + tiles = {"edit_fill.png"}, + inventory_image = "edit_fill.png", + groups = {snappy = 2, oddly_breakable_by_hand = 3, dig_immediate = 3}, + range = 10, + on_place = fill_on_place, + on_secondary_use = fill_on_place, + on_destruct = function(pos) + for player, data in pairs(edit.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 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 + end +}) + +minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname ~= "edit:fill" then return false end + + minetest.close_formspec(player:get_player_name(), "edit:fill") + + local d = edit.player_data[player] + local p1 = d.fill1_pos + local p2 = d.fill2_pos + local pointed_thing = d.fill_pointed_thing + + if + not p1 or not p2 or + not pointed_thing or + not edit.has_privilege(player) + then return true end + + d.fill1_pos = nil + d.fill2_pos = nil + d.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 key = "air" end + + name = key + def = minetest.registered_items[name] + + if def then break end + end + + if not def then return true end + + local is_node = minetest.registered_nodes[name] + + local param2 + if def.paramtype2 == "facedir" or def.paramtype2 == "colorfacedir" then + param2 = minetest.dir_to_facedir(player:get_look_dir()) + elseif def.paramtype2 == "wallmounted" or def.paramtype2 == "colorwallmounted" then + param2 = minetest.dir_to_wallmounted(player:get_look_dir(), true) + end + + 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) + d.undo_schematic = edit.schematic_from_map(start, size) + + local volume = size.x * size.y * size.z + if is_node and volume >= edit.fast_node_fill_threshold then + local voxel_manip = VoxelManip() + local vm_start, vm_end = voxel_manip:read_from_map(start, _end) + local param2s = voxel_manip:get_param2_data() + local content_ids = voxel_manip:get_data() + local content_id = minetest.get_content_id(name) + + local ones = vector.new(1, 1, 1) + local vm_size = vector.add(vector.subtract(vm_end, vm_start), ones) + local voxel_area = VoxelArea:new({MinEdge = ones, MaxEdge = vm_size}) + local va_start = vector.add(vector.subtract(start, vm_start), ones) + local va_end = vector.subtract(vector.add(va_start, size), ones) + for i in voxel_area:iterp(va_start, va_end) do + content_ids[i] = content_id + param2s[i] = param2 + end + voxel_manip:set_data(content_ids) + voxel_manip:set_param2_data(param2s) + voxel_manip:write_to_map(true) + voxel_manip:update_liquids() + else + -- Work top to bottom so we can remove falling nodes + for x = _end.x, start.x, -1 do + for y = _end.y, start.y, -1 do + for z = _end.z, start.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.under = pos + on_place(itemstack, player, pointed_thing) + end + end + end + end + end + return true +end) \ No newline at end of file diff --git a/init.lua b/init.lua index 22bba40..1f8ddab 100644 --- a/init.lua +++ b/init.lua @@ -1,118 +1,9 @@ -------------------- --- Edit Mod v1.1 -- -------------------- +edit = {} +edit.player_data = {} -local player_data = {} - -local paste_preview_max_entities = tonumber(minetest.settings:get("edit_paste_preview_max_entities") or 2000) -local max_operation_volume = tonumber(minetest.settings:get("edit_max_operation_volume") or 20000) -local use_fast_node_fill = minetest.settings:get_bool("edit_use_fast_node_fill", false) - -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 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 +edit.paste_preview_max_entities = tonumber(minetest.settings:get("edit_paste_preview_max_entities") or 2000) +edit.max_operation_volume = tonumber(minetest.settings:get("edit_max_operation_volume") or 20000) +edit.fast_node_fill_threshold = tonumber(minetest.settings:get("edit_fast_node_fill_threshold") or 2000) minetest.register_privilege("edit", { description = "Allows usage of edit mod nodes", @@ -120,7 +11,7 @@ minetest.register_privilege("edit", { give_to_admin = true, }) -local function has_privilege(player) +function edit.has_privilege(player) local name = player:get_player_name() if minetest.check_player_privs(name, {edit = true}) then return true @@ -130,37 +21,49 @@ local function has_privilege(player) end end -local function display_size_error(player) +function edit.display_size_error(player) local msg = "Operation too large. The maximum operation volume can be changed in Minetest settings." minetest.chat_send_player(player:get_player_name(), msg) end -local function on_place_checks(player) +function edit.on_place_checks(player) return player and player:is_player() and - has_privilege(player) + edit.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 +function edit.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 + +minetest.register_on_joinplayer(function(player) + edit.player_data[player] = { + schematic_offset = vector.new(0, 0, 0) + } +end) + +minetest.register_on_leaveplayer(function(player) + edit.delete_paste_preview(player) + local d = edit.player_data[player] + if d.select_preview then + d.select_preview:remove() end - - return schematic -end + if d.place_preview then + d.place_preview:remove() + end + if d.copy_luaentity1 then + d.copy_luaentity1.object:remove() + end + edit.player_data[player] = nil +end) -local function get_pointed_thing_node(player) +function edit.get_pointed_thing_node(player) local look_dir = player:get_look_dir() local pos1 = player:get_pos() local eye_height = player:get_properties().eye_height @@ -176,97 +79,14 @@ local function get_pointed_thing_node(player) return { type = "node", under = pos, above = pos } end -local function copy_on_place(itemstack, player, pointed_thing) - if not on_place_checks(player) then return end - if not pointed_thing.under then - pointed_thing = get_pointed_thing_node(player) - end - local pos = pointed_thing.under - - if player_data[player].copy_luaentity1 and pos then - local p1 = player_data[player].copy_luaentity1._pos - local p2 = pos - - player_data[player].copy_luaentity1.object:remove() - player_data[player].copy_luaentity1 = nil - - 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 - display_size_error(player) - return - end - - player_data[player].schematic = schematic_from_map(start, size) - player_data[player].schematic._offset = vector.new(0, 0, 0) - delete_paste_preview(player) - local function vector_to_string(v) return "(" .. v.x .. ", " .. v.y .. ", " .. v.z .. ")" end - minetest.chat_send_player( - player:get_player_name(), - vector_to_string(start) .. " to " .. vector_to_string(_end) .. " copied." ) - else - local obj_ref = minetest.add_entity(pos, "edit:copy") - if not obj_ref then return end - local luaentity = obj_ref:get_luaentity() - luaentity._pos = pos - luaentity._placer = player - player_data[player].copy_luaentity1 = luaentity - end -end - -minetest.register_tool("edit:copy",{ - description = "Edit Copy", - tiles = {"edit_copy.png"}, - inventory_image = "edit_copy.png", - groups = {snappy = 2, oddly_breakable_by_hand = 3}, - range = 10, - on_place = copy_on_place, - on_secondary_use = copy_on_place, -}) - -minetest.register_entity("edit:copy", { - initial_properties = { - visual = "cube", - visual_size = { x = 1.1, y = 1.1}, - physical = false, - collide_with_objects = false, - static_save = false, - use_texture_alpha = true, - glow = -1, - backface_culling = false, - hp_max = 1, - textures = { - "edit_copy.png", - "edit_copy.png", - "edit_copy.png", - "edit_copy.png", - "edit_copy.png", - "edit_copy.png", - }, - }, - on_death = function(self, killer) - player_data[self._placer].copy_luaentity1 = nil - end, -}) - -local function pointed_thing_to_pos(pointed_thing) +function edit.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] @@ -275,620 +95,12 @@ local function pointed_thing_to_pos(pointed_thing) end end -local function paste_on_place(itemstack, player, pointed_thing) - if not on_place_checks(player) then return end - - if not pointed_thing.above then - pointed_thing = get_pointed_thing_node(player) - end - - if not player_data[player].schematic then - minetest.chat_send_player(player:get_player_name(), "Nothing to paste.") - return - 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 - -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}, - range = 10, - on_place = paste_on_place, - on_secondary_use = paste_on_place, - on_use = function(itemstack, player, pointed_thing) - local d = player_data[player] - if not d.schematic then return end - set_schematic_rotation(d.schematic, 90) - delete_paste_preview(player) - end -}) - -local 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 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 - -local function open_on_place(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:open",{ - description = "Edit Open", - inventory_image = "edit_open.png", - range = 10, - on_place = open_on_place, - on_secondary_use = open_on_place -}) - -local function undo_on_place(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 - -minetest.register_tool("edit:undo",{ - description = "Edit Undo", - inventory_image = "edit_undo.png", - range = 10, - on_place = undo_on_place, - on_secondary_use = undo_on_place -}) - -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, - on_secondary_use = function(itemstack, player, pointed_thing) - if on_place_checks(player) then show_save_dialog(player) end - end -}) - -local function fill_on_place(itemstack, player, pointed_thing) - if not on_place_checks(player) then return end - - if not pointed_thing.above then - pointed_thing = get_pointed_thing_node(player) - end - - local itemstack, pos = minetest.item_place_node(itemstack, player, pointed_thing) - - if player_data[player].fill1_pos and pos then - local diff = vector.subtract(player_data[player].fill1_pos, pos) - local size = vector.add(vector.apply(diff, math.abs), 1) - if size.x * size.y * size.z > max_operation_volume then - display_size_error(player) - minetest.remove_node(player_data[player].fill1_pos) - player_data[player].fill1_pos = nil - minetest.remove_node(pos) - return - end - - player_data[player].fill2_pos = pos - player_data[player].fill_pointed_thing = pointed_thing - - 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 .. ";]" - end - end - minetest.show_formspec(player:get_player_name(), "edit:fill", formspec) - elseif pos then - player_data[player].fill1_pos = pos - end -end - -minetest.register_node("edit:fill",{ - description = "Edit Fill", - tiles = {"edit_fill.png"}, - inventory_image = "edit_fill.png", - groups = {snappy = 2, oddly_breakable_by_hand = 3, dig_immediate = 3}, - range = 10, - on_place = fill_on_place, - on_secondary_use = fill_on_place, - on_destruct = function(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 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 - end -}) - -minetest.register_on_player_receive_fields(function(player, formname, fields) - 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 key = "air" end - - name = key - def = minetest.registered_items[name] - - if def then break end - end - - if not def then return true end - - local is_node = minetest.registered_nodes[name] - - local param2 - if def.paramtype2 == "facedir" or def.paramtype2 == "colorfacedir" then - param2 = minetest.dir_to_facedir(player:get_look_dir()) - elseif def.paramtype2 == "wallmounted" or def.paramtype2 == "colorwallmounted" then - param2 = minetest.dir_to_wallmounted(player:get_look_dir(), true) - end - - 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) - - player_data[player].undo_schematic = schematic_from_map(start, size) - - if is_node and use_fast_node_fill then - local voxel_manip = VoxelManip() - local vm_start, vm_end = voxel_manip:read_from_map(start, _end) - local param2s = voxel_manip:get_param2_data() - local content_ids = voxel_manip:get_data() - local content_id = minetest.get_content_id(name) - - local ones = vector.new(1, 1, 1) - local vm_size = vector.add(vector.subtract(vm_end, vm_start), ones) - local voxel_area = VoxelArea:new({MinEdge = ones, MaxEdge = vm_size}) - local va_start = vector.add(vector.subtract(start, vm_start), ones) - local va_end = vector.subtract(vector.add(va_start, size), ones) - for i in voxel_area:iterp(va_start, va_end) do - content_ids[i] = content_id - param2s[i] = param2 - end - voxel_manip:set_data(content_ids) - voxel_manip:set_param2_data(param2s) - voxel_manip:write_to_map(true) - voxel_manip:update_liquids() - else - 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.under = pos - on_place(itemstack, player, pointed_thing) - end - 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(0, 0, 0) - 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, - } -}) - -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 = "Punch (left click) 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 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 - -local function set_select_preview_size(preview, size) - local preview_size = vector.add(size, vector.new(0.01, 0.01, 0.01)) - - local function combine(width, height) - local tex = "" - for x = 0, math.floor(width / 8) do - for y = 0, math.floor(height / 8) do - if #tex > 0 then tex = tex .. ":" end - tex = tex .. - (x * 8 * 16) .. - "," .. (y * 8 * 16) .. - "=edit_select_preview.png" - end - end - return "[combine:" .. (width * 16) .. "x" .. (height * 16) .. ":" .. tex - end - - local x_tex = combine(size.z, size.y) - local y_tex = combine(size.x, size.z) - local z_tex = combine(size.x, size.y) - - preview:set_properties({ - visual_size = preview_size, - textures = { - y_tex, y_tex, - x_tex, x_tex, - z_tex, z_tex - } - }) -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 = pointed_thing_to_pos(get_pointed_thing_node(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 - 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 use_under = 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 - elseif item == "edit:copy" and d.copy_luaentity1 then - node1_pos = d.copy_luaentity1._pos - use_under = true - end - - if node1_pos then - if not node2_pos then - local pointed_thing = get_pointed_thing_node(player) - if use_under then - node2_pos = pointed_thing.under - else - node2_pos = pointed_thing_to_pos(pointed_thing) - end - end - - if node2_pos then - local diff = vector.subtract(node1_pos, node2_pos) - local size = vector.apply(diff, math.abs) - size = vector.add(size, vector.new(1, 1, 1)) - - 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 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) - set_select_preview_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 -end) - -minetest.register_on_joinplayer(function(player) - player_data[player] = {} -end) - -minetest.register_on_leaveplayer(function(player) - delete_paste_preview(player) - if player_data[player].select_preview then - player_data[player].select_preview:remove() - end - player_data[player] = nil -end) +edit.modpath = minetest.get_modpath("edit") +dofile(edit.modpath .. "/copy.lua") +dofile(edit.modpath .. "/fill.lua") +dofile(edit.modpath .. "/open.lua") +dofile(edit.modpath .. "/paste.lua") +dofile(edit.modpath .. "/preview.lua") +dofile(edit.modpath .. "/save.lua") +dofile(edit.modpath .. "/schematic.lua") +dofile(edit.modpath .. "/undo.lua") diff --git a/object_rotations.lua b/object_rotations.lua new file mode 100644 index 0000000..fd2b245 --- /dev/null +++ b/object_rotations.lua @@ -0,0 +1,40 @@ +-- who would have thunked, but rotation for attachd objects works differently to direct object rotation +local rotations = {} +local d = 180 +local r = d / 2 +rotations.facedir = { + [0] = vector.new(0, 0, 0), + vector.new( 0, r, 0), + vector.new( 0, d, 0), + vector.new( 0, -r, 0), + + vector.new( r, 0, 0), + vector.new( r, 0, r), + vector.new( r, 0, d), + vector.new( r, 0, -r), + + vector.new(-r, 0, 0), + vector.new(-r, 0, -r), + vector.new(-r, 0, d), + vector.new(-r, 0, r), + + vector.new( 0, 0, -r), + vector.new( 0, r, -r), + vector.new( 0, d, -r), + vector.new( 0, -r, -r), + + vector.new( 0, 0, r), + vector.new( 0, r, r), + vector.new( 0, d, r), + vector.new( 0, -r, r), + + vector.new( 0, 0, d), + vector.new( 0, r, d), + vector.new( 0, d, d), + vector.new( 0, -r, d), +} + +-- TODO: signlike nodes always display as an extrusion of texture, like in dropped item form +-- displaying them correctly will require makeing a custom display obect for them + +return rotations diff --git a/open.lua b/open.lua new file mode 100644 index 0000000..0b4c451 --- /dev/null +++ b/open.lua @@ -0,0 +1,178 @@ +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 = "formspec_version[4]size[10,10]" .. + "label[0.5,1;Delete Schematics from:\n" .. + minetest.formspec_escape(path) .. "]button_exit[8.8,0.2;1,1;quit;X]" .. + "textlist[0.5,2;9,7;schems;" .. table.concat(dir_list, ",") .. "]" + + edit.reliable_show_formspec(player, "edit:delete_schem", formspec) +end + +local function open_on_place(itemstack, player, pointed_thing) + if not edit.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 = "formspec_version[4]size[10,11]" .. + "label[0.5,1;Load a schematic from:\n" .. + minetest.formspec_escape(path) .. "]button_exit[8.8,0.2;1,1;quit;X]" .. + "textlist[0.5,2;9,7;schems;" .. table.concat(dir_list, ",") .. "]" .. + "button_exit[2,9.5;6,1;delete;Delete schematics...]" + + minetest.show_formspec(player:get_player_name(), "edit:open", formspec) +end + +minetest.register_tool("edit:open",{ + description = "Edit Open", + inventory_image = "edit_open.png", + range = 10, + on_place = open_on_place, + on_secondary_use = open_on_place +}) + +local function read_minetest_schematic(file_path) + local schematic = minetest.read_schematic(file_path, {}) + if schematic then + schematic._meta = {} + schematic._rotation = 0 + end + return schematic +end + +local function read_world_edit_schematic(file_path) + local f = io.open(file_path) + if not f then return false end + local data = f:read("*all") + f:close() + if not data then return false end + + data = data:gsub("^[^:]*:", "") + data = minetest.deserialize(data) + if not data then return false end + + -- Get the schematic size + local x_max, y_max, z_max = 0, 0, 0 + for i, node in pairs(data) do + local x, y, z = node.x, node.y, node.z + if x > x_max then x_max = x end + if y > y_max then y_max = y end + if z > z_max then z_max = z end + end + + local schem_data = {} + local meta = {} + local size = vector.new(x_max + 1, y_max + 1, z_max + 1) + + local start = vector.new(1, 1, 1) + local voxel_area = VoxelArea:new({MinEdge = start, MaxEdge = size}) + + for i, node in pairs(data) do + local x, y, z = node.x + 1, node.y + 1, node.z + 1 + local index = voxel_area:index(x, y, z) + + schem_data[index] = {} + schem_data[index].name = node.name + schem_data[index].param2 = node.param2 + if node.meta then + local key = minetest.hash_node_position(vector.new(x, y, z)) + meta[key] = node.meta + end + end + + -- Replace empty space with air nodes + for i in voxel_area:iterp(start, size) do + if not schem_data[i] then + schem_data[i] = { name = "air" } + end + end + + return { + size = size, + data = schem_data, + _meta = meta, + _rotation = 0, + } +end + +minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname == "edit:open" then + minetest.close_formspec(player:get_player_name(), "edit:open") + + if + fields.cancel + or not edit.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 + + if file_path:sub(-4, -1) == ".mts" then + schematic = read_minetest_schematic(file_path) + elseif file_path:sub(-3, -1) == ".we" then + schematic = read_world_edit_schematic(file_path) + end + + if not schematic then + minetest.chat_send_player(player:get_player_name(), + "\"" .. dir_list[index] .. "\" failed to load" ) + return true + end + edit.player_data[player].schematic = schematic + minetest.chat_send_player(player:get_player_name(), + "\"" .. dir_list[index] .. "\" loaded." ) + edit.delete_paste_preview(player) + end + return true + elseif formname == "edit:delete_schem" then + if + fields.cancel + or not edit.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 + edit.player_data[player].schem_for_delete = path .. "/" .. dir_list[index] + formspec = "formspec_version[4]size[8,3.5]label[0.6,1;Confirm delete \"" .. + dir_list[index] .. "\"]" .. + "button_exit[0.5,2;3,1;delete;Delete]" .. + "button_exit[4.5,2;3,1;quit;Cancel]" + + edit.reliable_show_formspec(player, "edit:confirm_delete_schem", formspec) + end + return true + elseif formname == "edit:confirm_delete_schem" then + if not edit.has_privilege(player) then return end + + if fields.delete then + os.remove(edit.player_data[player].schem_for_delete) + end + edit.player_data[player].schem_for_delete = nil + delete_schematics_dialog(player) + return true + end + return false +end) diff --git a/paste.lua b/paste.lua new file mode 100644 index 0000000..9e5837a --- /dev/null +++ b/paste.lua @@ -0,0 +1,40 @@ +local function paste_on_place(itemstack, player, pointed_thing) + if not edit.on_place_checks(player) then return end + + if not pointed_thing.above then + pointed_thing = edit.get_pointed_thing_node(player) + end + + if not edit.player_data[player].schematic then + minetest.chat_send_player(player:get_player_name(), "Nothing to paste.") + return + end + + local d = edit.player_data[player] + local schematic = d.schematic + local pos = edit.pointed_thing_to_pos(pointed_thing) + if not pos then return end + local pos = vector.add(pos, d.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 + edit.player_data[player].undo_schematic = edit.schematic_from_map(pos, size) + edit.schematic_to_map(pos, schematic) +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}, + range = 10, + on_place = paste_on_place, + on_secondary_use = paste_on_place, + on_use = function(itemstack, player, pointed_thing) + local d = edit.player_data[player] + if not d.schematic then return end + edit.set_schematic_rotation(d.schematic, 90) + edit.rotate_paste_preview(player) + end +}) diff --git a/preview.lua b/preview.lua new file mode 100644 index 0000000..8066dbf --- /dev/null +++ b/preview.lua @@ -0,0 +1,385 @@ +local param2_to_rotation = dofile(edit.modpath .. "/object_rotations.lua").facedir + +minetest.register_entity("edit:place_preview", { + initial_properties = { + visual = "cube", + visual_size = { x = 1.1, y = 1.1 }, + physical = false, + collide_with_objects = false, + static_save = false, + use_texture_alpha = true, + glow = -1, + backface_culling = false, + pointable = false, + } +}) + +function edit.rotate_paste_preview(player) + local d = edit.player_data[player] + local rot = d.schematic._rotation + local offset + d.paste_preview:set_yaw(-math.rad(rot)) + local size = d.schematic.size + if rot == 90 or rot == 270 then + size = vector.new(size.z, size.y, size.x) + end + if rot == 0 then + offset = vector.new(-1, -1, -1) + elseif rot == 90 then + offset = vector.new(-1, -1, size.z) + elseif rot == 180 then + offset = vector.new(size.x, -1, size.z) + elseif rot == 270 then + offset = vector.new(size.x, -1, -1) + end + d.paste_preview_offset = offset +end + +local function create_paste_preview(player) + local player_pos = player:get_pos() + local base_objref = minetest.add_entity(player_pos, "edit:paste_preview_base") + local schematic = edit.player_data[player].schematic + local vector_1 = vector.new(1, 1, 1) + local size = schematic.size + local voxel_area = VoxelArea:new({MinEdge = vector_1, MaxEdge = size}) + local schem_data = schematic.data + local count = size.x * size.y * size.z + local node_black_list = {} + + -- Remove air from the schematic preview + for i, map_node in pairs(schem_data) do + if map_node.name == "air" then + count = count - 1 + node_black_list[i] = true + end + end + + -- Hollow out sold areas in the schematic preview + local strides = { + 1, -1, + voxel_area.ystride, -voxel_area.ystride, + voxel_area.zstride, -voxel_area.zstride, + } + if math.min(size.x, size.y, size.z) > 2 then + local start = vector.new(2, 2, 2) + local _end = vector.subtract(size, 1) + for i in voxel_area:iterp(start, _end) do + if not node_black_list[i] then + local include_node = false + for _, n in pairs(strides) do + if schem_data[i + n].name == "air" then + include_node = true + break + end + end + + if not include_node then + count = count - 1 + node_black_list[i] = true + end + end + end + end + + local probability = edit.paste_preview_max_entities / count + for i in voxel_area:iterp(vector_1, size) do + local pos = voxel_area:position(i) + local name = schematic.data[i].name + if not node_black_list[i] and math.random() < probability then + local attach_pos = vector.multiply(pos, 10) + local attach_rot + local objref = minetest.add_entity(player_pos, "edit:preview_node") + objref:set_properties({wield_item = name}) + local node_def = minetest.registered_nodes[name] + if node_def and node_def.paramtype2 == "facedir" then + local param2 = schematic.data[i].param2 + attach_rot = param2_to_rotation[param2] + end + objref:set_attach(base_objref, "", attach_pos, attach_rot) + end + end + edit.player_data[player].paste_preview = base_objref + edit.player_data[player].schematic._rotation = 0 + edit.rotate_paste_preview(player) +end + +function edit.delete_paste_preview(player) + local paste_preview = edit.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 + edit.player_data[player].paste_preview:remove() + edit.player_data[player].paste_preview_visable = false + edit.player_data[player].paste_preview = nil +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, + } +}) + +minetest.register_entity("edit:paste_preview_base", { + initial_properties = { + visual = "cube", + physical = false, + pointable = false, + collide_with_objects = false, + static_save = false, + visual_size = {x = 1, y = 1}, + textures = { "blank.png", "blank.png", "blank.png", "blank.png", "blank.png", "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.68, y = 0.68 }, + glow = -1, + } +}) + +local function hide_paste_preview(player) + local d = edit.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 = edit.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 = "Punch (left click) 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 hide_select_preview(player) + local d = edit.player_data[player] + d.select_preview_shown = false + d.select_preview:set_properties({ is_visible = false }) + d.select_preview:set_attach(player) + player:hud_remove(d.select_preview_hud) + d.select_preview_hud = nil +end + +local function update_select_preview(player, pos, size) + local d = edit.player_data[player] + + if not d.select_preview or not d.select_preview:get_pos() then + d.select_preview = minetest.add_entity(player:get_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 = d.select_preview + if vector.equals(pos, preview:get_pos()) then + return + end + + preview:set_pos(pos) + local preview_size = vector.add(size, vector.new(0.01, 0.01, 0.01)) + + local function combine(width, height) + local tex = "" + for x = 0, math.floor(width / 8) do + for y = 0, math.floor(height / 8) do + if #tex > 0 then tex = tex .. ":" end + tex = tex .. + (x * 8 * 16) .. + "," .. (y * 8 * 16) .. + "=edit_select_preview.png" + end + end + return "[combine:" .. (width * 16) .. "x" .. (height * 16) .. ":" .. tex + end + + local x_tex = combine(size.z, size.y) + local y_tex = combine(size.x, size.z) + local z_tex = combine(size.x, size.y) + + preview:set_properties({ + visual_size = preview_size, + textures = { + y_tex, y_tex, + x_tex, x_tex, + z_tex, z_tex + } + }) + + if not d.select_preview_hud then + d.select_preview_hud = player:hud_add({ + hud_elem_type = "text", + position = {x = 0.5, y = 0.7}, + z_index = 100, + number = 0xffffff + }) + end + player:hud_change( + d.select_preview_hud, + "text", "X: " .. size.x .. ", Y: " .. size.y .. ", Z: " .. size.z ) +end + +local function set_schematic_offset(player) + local d = edit.player_data[player] + local yaw = player:get_look_horizontal() + local offset = vector.new(0, 0, 0) + + local rot = d.schematic._rotation + local x_max, z_max + if rot == 90 or rot == 270 then + x_max = -d.schematic.size.z + 1 + z_max = -d.schematic.size.x + 1 + else + x_max = -d.schematic.size.x + 1 + z_max = -d.schematic.size.z + 1 + end + + if yaw < math.pi then + offset.x = x_max + end + + if yaw < math.pi * 1.5 and yaw > math.pi * 0.5 then + offset.z = z_max + end + d.schematic_offset = offset +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 = edit.player_data[player] + + -- Paste preview + if item == "edit:paste" and d.schematic then + local pos = edit.pointed_thing_to_pos(edit.get_pointed_thing_node(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 = d.paste_preview:get_pos() + pos = vector.add(pos, d.paste_preview_offset) + set_schematic_offset(player) + pos = vector.add(pos, d.schematic_offset) + if not vector.equals(old_pos, pos) then + d.paste_preview:set_pos(pos) + 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 pointed_pos + local use_under = false + local fill_selected = item == "edit:fill" + local copy_selected = item == "edit:copy" + if fill_selected then + node1_pos = d.fill1_pos + node2_pos = d.fill2_pos + elseif copy_selected then + if d.copy_luaentity1 then + node1_pos = d.copy_luaentity1._pos + end + use_under = true + end + + if not node2_pos or not node1_pos then + local pointed_thing = edit.get_pointed_thing_node(player) + if use_under then + pointed_pos = pointed_thing.under + else + pointed_pos = edit.pointed_thing_to_pos(pointed_thing) + end + end + + if (fill_selected or copy_selected) and not node2_pos and pointed_pos then + if not d.place_preview or not d.place_preview:get_pos() then + d.place_preview = minetest.add_entity(player:get_pos(), "edit:place_preview") + d.place_preview_shown = true + d.place_preview_item = nil + elseif not d.place_preview_shown then + d.place_preview:set_properties({ is_visible = true }) + d.place_preview:set_detach() + d.place_preview_shown = true + end + + if not vector.equals(d.place_preview:get_pos(), pointed_pos) then + d.place_preview:set_pos(pointed_pos) + end + + if d.place_preview_item ~= item then + local tex = minetest.registered_items[item].tiles[1] .. + "^[opacity:150" + + d.place_preview_item = item + d.place_preview:set_properties({ + textures = { tex, tex, tex, tex, tex, tex } + }) + end + elseif d.place_preview_shown then + d.place_preview:set_properties({ is_visible = false }) + d.place_preview:set_attach(player) + d.place_preview_shown = false + end + + if not node2_pos then + node2_pos = pointed_pos + end + + if node1_pos and node2_pos then + local diff = vector.subtract(node1_pos, node2_pos) + local size = vector.apply(diff, math.abs) + size = vector.add(size, vector.new(1, 1, 1)) + local size_too_big = size.x * size.y * size.z > edit.max_operation_volume + if not size_too_big then + local preview_pos = vector.add(node2_pos, vector.multiply(diff, 0.5)) + update_select_preview(player, preview_pos, size) + elseif d.select_preview_shown then hide_select_preview(player) end + elseif d.select_preview_shown then hide_select_preview(player) end + end +end) diff --git a/save.lua b/save.lua new file mode 100644 index 0000000..273b7b4 --- /dev/null +++ b/save.lua @@ -0,0 +1,118 @@ +local function show_save_dialog(player, filename, save_error, file_format_index) + if not edit.player_data[player].schematic then + minetest.chat_send_player(player:get_player_name(), "Nothing to save.") + return + end + + filename = filename or "untitled" + file_format_index = file_format_index or 1 + + local path = minetest.get_worldpath() .. "/schems" + if #path > 40 then path = "..." .. path:sub(#path - 40, #path) end + + local formspec = "formspec_version[4]size[10,6]label[0.5,1;Save schematic in:\n" .. + minetest.formspec_escape(path) .. "]button_exit[8.8,0.2;1,1;cancel;X]" .. + "field[0.5,2.5;9,1;schem_filename;;" .. filename .. "]" .. + "dropdown[0.5,4;4,1;file_format;WorldEdit (.we),Minetest (.mts);" .. + file_format_index .. ";true]" .. + "button_exit[5.5,4;4,1;save;Save]" + + if save_error then + formspec = formspec .. + "label[3,5.5;" .. save_error .. "]" + end + edit.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 edit.on_place_checks(player) then show_save_dialog(player) end + end, + on_secondary_use = function(itemstack, player, pointed_thing) + if edit.on_place_checks(player) then show_save_dialog(player) end + end +}) + +local function serialize_world_edit_schematic(schematic) + local we = {} + local start = vector.new(1, 1, 1) + local voxel_area = VoxelArea:new({MinEdge = start, MaxEdge = schematic.size}) + local data = schematic.data + local meta = schematic._meta + + for i in voxel_area:iterp(start, schematic.size) do + local pos = voxel_area:position(i) + local name = data[i].name + local meta_key = minetest.hash_node_position(pos) + if name ~= "air" then + table.insert(we, { + x = pos.x - 1, + y = pos.y - 1, + z = pos.z - 1, + name = name, + param2 = data[i].param2, + meta = meta[meta_key] + }) + end + end + return "5:" .. minetest.serialize(we) +end + +minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname ~= "edit:save" then return false end + + minetest.close_formspec(player:get_player_name(), "edit:save") + + local schematic = edit.player_data[player].schematic + local schem_filename = fields.schem_filename + + if + fields.cancel or + not schem_filename or + not schematic or + not fields.file_format or + not edit.has_privilege(player) + then return end + + if not fields.key_enter and not fields.save then + show_save_dialog(player, fields.schem_filename, nil, fields.file_format) + return true + end + + local path = minetest.get_worldpath() .. "/schems" + local file_ext = fields.file_format == "1" and ".we" or ".mts" + local schem_filename = schem_filename .. file_ext + 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, + "\"" .. schem_filename .. "\" already exists.", fields.file_format ) + return true + end + end + + local data + if file_ext == ".we" then + data = serialize_world_edit_schematic(schematic) + else + data = minetest.serialize_schematic(schematic, "mts", {}) + end + + if not data 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(data) + f:close() + minetest.chat_send_player(player:get_player_name(), + "\"" .. schem_filename .. "\" saved." ) + return true +end) diff --git a/schematic.lua b/schematic.lua new file mode 100644 index 0000000..a6f1199 --- /dev/null +++ b/schematic.lua @@ -0,0 +1,121 @@ +function edit.schematic_from_map(pos, size) + local schematic = {data = {}} + schematic.size = size + schematic._pos = pos + schematic._meta = {} + + 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 + + local meta = minetest.get_meta(node_pos):to_table() + + local has_meta = false + -- Convert metadata item stacks to item strings + for name, inventory in pairs(meta.inventory) do + for i, stack in ipairs(inventory) do + has_meta = true + if stack.to_string then + inventory[i] = stack:to_string() + end + end + end + + if meta.fields and next(meta.fields) ~= nil then + has_meta = true + end + + if not has_meta then + for k in pairs(meta) do + if k ~= "inventory" and k ~= "fields" then + has_meta = true + break + end + end + end + + if has_meta then + local key = minetest.hash_node_position(offset) + schematic._meta[key] = meta + end + end + + return schematic +end + +function edit.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 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 + +function edit.schematic_to_map(pos, schematic) + minetest.place_schematic(pos, schematic, tostring(schematic._rotation), nil, true) + local size = schematic.size + for hash, metadata in pairs(schematic._meta) do + local offset = minetest.get_position_from_hash(hash) + offset = vector.subtract(offset, 1) + if schematic._rotation == 90 then + offset = vector.new(offset.z, offset.y, size.x - offset.x - 1) + elseif schematic._rotation == 180 then + offset = vector.new(size.x - offset.x - 1, offset.y, size.z - offset.z - 1) + elseif schematic._rotation == 270 then + offset = vector.new(size.z - offset.z - 1, offset.y, offset.x) + end + local node_pos = vector.add(pos, offset) + local meta = minetest.get_meta(node_pos) + meta:from_table(metadata) + end +end diff --git a/screenshot.png b/screenshot.png index fcb4cb8..49f4ac4 100644 Binary files a/screenshot.png and b/screenshot.png differ diff --git a/settingtypes.txt b/settingtypes.txt index b9f810c..4b0b89a 100644 --- a/settingtypes.txt +++ b/settingtypes.txt @@ -5,6 +5,8 @@ edit_paste_preview_max_entities (Paste preview max entities) int 2000 # The maximum volume of any edit operation. Increase to allow larger operations. edit_max_operation_volume (Max edit operation volume) int 20000 -# Fast filling of nodes. This uses VoxelManip for fast node placement. -# No node placement callbacks are called so some nodes might be broken. -edit_use_fast_node_fill (Use fast node fill) bool false +# When the fill operation has a larger volume then the specified number, fast node fill will be used. +# To disable fast node placement, set the threshold to be equil to the max operation volume. +# To disable slow node placement, set the threshold to 0. +# With fast node placement, callbacks are not called so some nodes might be broken. +edit_fast_node_fill_threshold (Fast node fill threshold volume) int 2000 diff --git a/undo.lua b/undo.lua new file mode 100644 index 0000000..acef47d --- /dev/null +++ b/undo.lua @@ -0,0 +1,19 @@ +local function undo_on_place(itemstack, player, pointed_thing) + if not edit.on_place_checks(player) then return end + + local schem = edit.player_data[player].undo_schematic + if schem then + edit.player_data[player].undo_schematic = edit.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 + +minetest.register_tool("edit:undo", { + description = "Edit Undo", + inventory_image = "edit_undo.png", + range = 10, + on_place = undo_on_place, + on_secondary_use = undo_on_place +})