From dfcf64c1d07f4006045af37b0b01dbfc82dbb1d1 Mon Sep 17 00:00:00 2001 From: SmallJoker <mk939@ymail.com> Date: Sat, 26 Aug 2023 10:57:05 +0200 Subject: [PATCH] Chainsaw: new setting to disable safe cutting --- technic/tools/chainsaw.lua | 540 ++++++++++++++++++++++++++++++++++------------------------- 1 files changed, 307 insertions(+), 233 deletions(-) diff --git a/technic/tools/chainsaw.lua b/technic/tools/chainsaw.lua index fd8ce2c..d2ee966 100644 --- a/technic/tools/chainsaw.lua +++ b/technic/tools/chainsaw.lua @@ -1,247 +1,304 @@ -- Configuration -local chainsaw_max_charge = 30000 -- 30000 - Maximum charge of the saw -local chainsaw_charge_per_node = 12 -- 12 - Gives 2500 nodes on a single charge (about 50 complete normal trees) -local chainsaw_leaves = true -- true - Cut down entire trees, leaves and all --- The default stuff -local timber_nodenames={["default:jungletree"] = true, - ["default:papyrus"] = true, - ["default:cactus"] = true, - ["default:tree"] = true, - ["default:apple"] = true -} +local chainsaw_max_charge = 30000 -- Maximum charge of the saw +-- Cut down tree leaves. Leaf decay may cause slowness on large trees +-- if this is disabled. +local chainsaw_leaves = true -if chainsaw_leaves == true then - timber_nodenames["default:leaves"] = true -end +local chainsaw_efficiency = 0.92 -- Drops less items --- technic_worldgen defines rubber trees if moretrees isn't installed -if minetest.get_modpath("technic_worldgen") or - minetest.get_modpath("moretrees") then - timber_nodenames["moretrees:rubber_tree_trunk_empty"] = true - timber_nodenames["moretrees:rubber_tree_trunk"] = true - if chainsaw_leaves then - timber_nodenames["moretrees:rubber_tree_leaves"] = true - end -end - --- Support moretrees if it is there -if( minetest.get_modpath("moretrees") ~= nil ) then - timber_nodenames["moretrees:apple_tree_trunk"] = true - timber_nodenames["moretrees:apple_tree_trunk_sideways"] = true - timber_nodenames["moretrees:beech_trunk"] = true - timber_nodenames["moretrees:beech_trunk_sideways"] = true - timber_nodenames["moretrees:birch_trunk"] = true - timber_nodenames["moretrees:birch_trunk_sideways"] = true - timber_nodenames["moretrees:fir_trunk"] = true - timber_nodenames["moretrees:fir_trunk_sideways"] = true - timber_nodenames["moretrees:oak_trunk"] = true - timber_nodenames["moretrees:oak_trunk_sideways"] = true - timber_nodenames["moretrees:palm_trunk"] = true - timber_nodenames["moretrees:palm_trunk_sideways"] = true - timber_nodenames["moretrees:pine_trunk"] = true - timber_nodenames["moretrees:pine_trunk_sideways"] = true - timber_nodenames["moretrees:rubber_tree_trunk_sideways"] = true - timber_nodenames["moretrees:rubber_tree_trunk_sideways_empty"] = true - timber_nodenames["moretrees:sequoia_trunk"] = true - timber_nodenames["moretrees:sequoia_trunk_sideways"] = true - timber_nodenames["moretrees:spruce_trunk"] = true - timber_nodenames["moretrees:spruce_trunk_sideways"] = true - timber_nodenames["moretrees:willow_trunk"] = true - timber_nodenames["moretrees:willow_trunk_sideways"] = true - timber_nodenames["moretrees:jungletree_trunk"] = true - timber_nodenames["moretrees:jungletree_trunk_sideways"] = true - - if chainsaw_leaves then - timber_nodenames["moretrees:apple_tree_leaves"] = true - timber_nodenames["moretrees:oak_leaves"] = true - timber_nodenames["moretrees:sequoia_leaves"] = true - timber_nodenames["moretrees:birch_leaves"] = true - timber_nodenames["moretrees:birch_leaves"] = true - timber_nodenames["moretrees:palm_leaves"] = true - timber_nodenames["moretrees:spruce_leaves"] = true - timber_nodenames["moretrees:spruce_leaves"] = true - timber_nodenames["moretrees:pine_leaves"] = true - timber_nodenames["moretrees:willow_leaves"] = true - timber_nodenames["moretrees:jungletree_leaves_green"] = true - timber_nodenames["moretrees:jungletree_leaves_yellow"] = true - timber_nodenames["moretrees:jungletree_leaves_red"] = true - end -end - --- Support growing_trees if it is there -if( minetest.get_modpath("growing_trees") ~= nil ) then - timber_nodenames["growing_trees:trunk"] = true - timber_nodenames["growing_trees:medium_trunk"] = true - timber_nodenames["growing_trees:big_trunk"] = true - timber_nodenames["growing_trees:trunk_top"] = true - timber_nodenames["growing_trees:trunk_sprout"] = true - timber_nodenames["growing_trees:branch_sprout"] = true - timber_nodenames["growing_trees:branch"] = true - timber_nodenames["growing_trees:branch_xmzm"] = true - timber_nodenames["growing_trees:branch_xpzm"] = true - timber_nodenames["growing_trees:branch_xmzp"] = true - timber_nodenames["growing_trees:branch_xpzp"] = true - timber_nodenames["growing_trees:branch_zz"] = true - timber_nodenames["growing_trees:branch_xx"] = true - - if chainsaw_leaves == true then - timber_nodenames["growing_trees:leaves"] = true - end -end - --- Support growing_cactus if it is there -if( minetest.get_modpath("growing_cactus") ~= nil ) then - timber_nodenames["growing_cactus:sprout"] = true - timber_nodenames["growing_cactus:branch_sprout_vertical"] = true - timber_nodenames["growing_cactus:branch_sprout_vertical_fixed"] = true - timber_nodenames["growing_cactus:branch_sprout_xp"] = true - timber_nodenames["growing_cactus:branch_sprout_xm"] = true - timber_nodenames["growing_cactus:branch_sprout_zp"] = true - timber_nodenames["growing_cactus:branch_sprout_zm"] = true - timber_nodenames["growing_cactus:trunk"] = true - timber_nodenames["growing_cactus:branch_trunk"] = true - timber_nodenames["growing_cactus:branch"] = true - timber_nodenames["growing_cactus:branch_xp"] = true - timber_nodenames["growing_cactus:branch_xm"] = true - timber_nodenames["growing_cactus:branch_zp"] = true - timber_nodenames["growing_cactus:branch_zm"] = true - timber_nodenames["growing_cactus:branch_zz"] = true - timber_nodenames["growing_cactus:branch_xx"] = true -end - --- Support farming_plus if it is there -if( minetest.get_modpath("farming_plus") ~= nil ) then - if chainsaw_leaves == true then - timber_nodenames["farming_plus:cocoa_leaves"] = true - end -end - +-- Maximal dimensions of the tree to cut (giant sequoia) +local tree_max_radius = 10 +local tree_max_height = 70 local S = technic.getter +--[[ +Format: [node_name] = dig_cost + +This table is filled automatically afterwards to support mods such as: + + cool_trees + ethereal + moretrees +]] +local tree_nodes = { + -- For the sake of maintenance, keep this sorted alphabetically! + ["default:acacia_bush_stem"] = -1, + ["default:bush_stem"] = -1, + ["default:pine_bush_stem"] = -1, + + ["default:cactus"] = -1, + ["default:papyrus"] = -1, + + -- dfcaves "fruits" + ["df_trees:blood_thorn_spike"] = -1, + ["df_trees:blood_thorn_spike_dead"] = -1, + ["df_trees:tunnel_tube_fruiting_body"] = -1, + + ["ethereal:bamboo"] = -1, +} + +local tree_nodes_by_cid = { + -- content ID indexed table, data populated on mod load. + -- Format: [node_name] = cost_number +} + +-- Function to decide whether or not to cut a certain node (and at which energy cost) +local function populate_costs(name, def) + repeat + if tree_nodes[name] then + break -- Manually specified node to chop + end + if (def.groups.tree or 0) > 0 then + break -- Tree node + end + if (def.groups.leaves or 0) > 0 and chainsaw_leaves then + break -- Leaves + end + if (def.groups.leafdecay_drop or 0) > 0 then + break -- Food + end + return -- Abort function: do not dig this node + + -- luacheck: push ignore 511 + until 1 + -- luacheck: pop + + -- Add the node cost to the content ID indexed table + local content_id = minetest.get_content_id(name) + + -- Make it so that the giant sequoia can be cut with a full charge + local cost = tree_nodes[name] or 0 + if def.groups.choppy then + cost = math.max(cost, def.groups.choppy * 14) -- trunks (usually 3 * 14) + end + if def.groups.snappy then + cost = math.max(cost, def.groups.snappy * 2) -- leaves + end + tree_nodes_by_cid[content_id] = math.max(4, cost) +end + +minetest.register_on_mods_loaded(function() + local ndefs = minetest.registered_nodes + -- Populate hardcoded nodes + for name in pairs(tree_nodes) do + local ndef = ndefs[name] + if ndef and ndef.groups then + populate_costs(name, ndef) + end + end + + -- Find all trees and leaves + for name, def in pairs(ndefs) do + if def.groups then + populate_costs(name, def) + end + end +end) + + technic.register_power_tool("technic:chainsaw", chainsaw_max_charge) --- Table for saving what was sawed down -local produced = nil +local pos9dir = { + { 1, 0, 0}, + {-1, 0, 0}, + { 0, 0, 1}, + { 0, 0, -1}, + { 1, 0, 1}, + {-1, 0, -1}, + { 1, 0, -1}, + {-1, 0, 1}, + { 0, 1, 0}, -- up +} --- Override the default handling routine to be able to count up the --- items sawed down so that we can drop them i an nice single stack -local function chainsaw_handle_node_drops(pos, drops, digger) - -- Add dropped items to list of collected nodes - local _, dropped_item - for _, dropped_item in ipairs(drops) do - if produced[dropped_item] == nil then - produced[dropped_item] = 1 - else - produced[dropped_item] = produced[dropped_item] + 1 - end - end +local cutter = { + -- See function cut_tree() +} + +local safe_cut = minetest.settings:get_bool("technic_safe_chainsaw") ~= false +local c_air = minetest.get_content_id("air") +local function dig_recursive(x, y, z) + local i = cutter.area:index(x, y, z) + if cutter.seen[i] then + return + end + cutter.seen[i] = 1 -- Mark as visited + + if safe_cut and cutter.param2[i] ~= 0 then + -- Do not dig manually placed nodes + -- Problem: moretrees' generated jungle trees use param2 = 2 + return + end + + local c_id = cutter.data[i] + local cost = tree_nodes_by_cid[c_id] + if not cost or cost > cutter.charge then + return -- Cannot dig this node + end + + -- Count dug nodes + cutter.drops[c_id] = (cutter.drops[c_id] or 0) + 1 + cutter.seen[i] = 2 -- Mark as dug (for callbacks) + cutter.data[i] = c_air + cutter.charge = cutter.charge - cost + + -- Expand maximal bounds for area protection check + if x < cutter.minp.x then cutter.minp.x = x end + if y < cutter.minp.y then cutter.minp.y = y end + if z < cutter.minp.z then cutter.minp.z = z end + if x > cutter.maxp.x then cutter.maxp.x = x end + if y > cutter.maxp.y then cutter.maxp.y = y end + if z > cutter.maxp.z then cutter.maxp.z = z end + + -- Traverse neighbors + local xn, yn, zn + for _, offset in ipairs(pos9dir) do + xn, yn, zn = x + offset[1], y + offset[2], z + offset[3] + if cutter.area:contains(xn, yn, zn) then + dig_recursive(xn, yn, zn) + end + end end --- This function does all the hard work. Recursively we dig the node at hand --- if it is in the table and then search the surroundings for more stuff to dig. -local function recursive_dig(pos, remaining_charge, player) - local node=minetest.env:get_node(pos) - local i=1 - -- Lookup node name in timber table: - if timber_nodenames[node.name] ~= nil then - -- Return if we are out of power - if remaining_charge < chainsaw_charge_per_node then - return 0 - end - local np - -- wood found - cut it. - minetest.env:dig_node(pos) +local handle_drops - remaining_charge=remaining_charge-chainsaw_charge_per_node - -- check surroundings and run recursively if any charge left - np={x=pos.x+1, y=pos.y, z=pos.z} - if timber_nodenames[minetest.env:get_node(np).name] ~= nil then - remaining_charge = recursive_dig(np, remaining_charge) - end - np={x=pos.x+1, y=pos.y, z=pos.z+1} - if timber_nodenames[minetest.env:get_node(np).name] ~= nil then - remaining_charge = recursive_dig(np, remaining_charge) - end - np={x=pos.x+1, y=pos.y, z=pos.z-1} - if timber_nodenames[minetest.env:get_node(np).name] ~= nil then - remaining_charge = recursive_dig(np, remaining_charge) - end +local function chainsaw_dig(player, pos, remaining_charge) + local minp = { + x = pos.x - (tree_max_radius + 1), + y = pos.y, + z = pos.z - (tree_max_radius + 1) + } + local maxp = { + x = pos.x + (tree_max_radius + 1), + y = pos.y + tree_max_height, + z = pos.z + (tree_max_radius + 1) + } - np={x=pos.x-1, y=pos.y, z=pos.z} - if timber_nodenames[minetest.env:get_node(np).name] ~= nil then - remaining_charge = recursive_dig(np, remaining_charge) - end - np={x=pos.x-1, y=pos.y, z=pos.z+1} - if timber_nodenames[minetest.env:get_node(np).name] ~= nil then - remaining_charge = recursive_dig(np, remaining_charge) - end - np={x=pos.x-1, y=pos.y, z=pos.z-1} - if timber_nodenames[minetest.env:get_node(np).name] ~= nil then - remaining_charge = recursive_dig(np, remaining_charge) - end + local vm = minetest.get_voxel_manip() + local emin, emax = vm:read_from_map(minp, maxp) - np={x=pos.x, y=pos.y+1, z=pos.z} - if timber_nodenames[minetest.env:get_node(np).name] ~= nil then - remaining_charge = recursive_dig(np, remaining_charge) - end + cutter = { + area = VoxelArea:new{MinEdge=emin, MaxEdge=emax}, + data = vm:get_data(), + param2 = vm:get_param2_data(), + seen = {}, + drops = {}, -- [content_id] = count + minp = vector.copy(pos), + maxp = vector.copy(pos), + charge = remaining_charge + } - np={x=pos.x, y=pos.y, z=pos.z+1} - if timber_nodenames[minetest.env:get_node(np).name] ~= nil then - remaining_charge = recursive_dig(np, remaining_charge) - end - np={x=pos.x, y=pos.y, z=pos.z-1} - if timber_nodenames[minetest.env:get_node(np).name] ~= nil then - remaining_charge = recursive_dig(np, remaining_charge) - end - return remaining_charge - end - -- Nothing sawed down - return remaining_charge + dig_recursive(pos.x, pos.y, pos.z) + + -- Check protection + local player_name = player:get_player_name() + if minetest.is_area_protected(cutter.minp, cutter.maxp, player_name, 6) then + minetest.chat_send_player(player_name, "The chainsaw cannot cut this tree. The cuboid " .. + minetest.pos_to_string(cutter.minp) .. ", " .. minetest.pos_to_string(cutter.maxp) .. + " contains protected nodes.") + minetest.record_protection_violation(pos, player_name) + return + end + + minetest.sound_play("chainsaw", { + pos = pos, + gain = 1.0, + max_hear_distance = 20 + }) + + handle_drops(pos) + + vm:set_data(cutter.data) + vm:write_to_map(true) + vm:update_map() + + -- Update falling nodes + for i, status in pairs(cutter.seen) do + if status == 2 then -- actually dug + minetest.check_for_falling(cutter.area:position(i)) + end + end end --- Saw down trees entry point -local function chainsaw_dig_it(pos, player,current_charge) - local remaining_charge=current_charge +-- Function to randomize positions for new node drops +local function get_drop_pos(pos) + local drop_pos = {} - -- Save the currently installed dropping mechanism so we can restore it. - local original_handle_node_drops = minetest.handle_node_drops + for i = 0, 8 do + -- Randomize position for a new drop + drop_pos.x = pos.x + math.random(-3, 3) + drop_pos.y = pos.y - 1 + drop_pos.z = pos.z + math.random(-3, 3) - -- A bit of trickery here: use a different node drop callback - -- and restore the original afterwards. - minetest.handle_node_drops = chainsaw_handle_node_drops + -- Move the randomized position upwards until + -- the node is air or unloaded. + for y = drop_pos.y, drop_pos.y + 5 do + drop_pos.y = y + local node = minetest.get_node_or_nil(drop_pos) - -- clear result and start sawing things down - produced = {} - remaining_charge = recursive_dig(pos, remaining_charge, player) - minetest.sound_play("chainsaw", {pos = pos, gain = 1.0, max_hear_distance = 10,}) + if not node then + -- If the node is not loaded yet simply drop + -- the item at the original digging position. + return pos + elseif node.name == "air" then + -- Add variation to the entity drop position, + -- but don't let drops get too close to the edge + drop_pos.x = drop_pos.x + (math.random() * 0.8) - 0.5 + drop_pos.z = drop_pos.z + (math.random() * 0.8) - 0.5 + return drop_pos + end + end + end - -- Restore the original noder drop handler - minetest.handle_node_drops = original_handle_node_drops + -- Return the original position if this takes too long + return pos +end - -- Now drop items for the player - local number, produced_item, p - for produced_item,number in pairs(produced) do - --print("ADDING ITEM: " .. produced_item .. " " .. number) - -- Drop stacks of 99 or less - p = { - x = pos.x + math.random()*4, - y = pos.y, - z = pos.z + math.random()*4 - } - while number > 99 do - minetest.env:add_item(p, produced_item .. " 99") - p = { - x = pos.x + math.random()*4, - y = pos.y, - z = pos.z + math.random()*4 - } - number = number - 99 - end - minetest.env:add_item(p, produced_item .. " " .. number) - end - return remaining_charge +local drop_inv = minetest.create_detached_inventory("technic:chainsaw_drops", {}, ":technic") +handle_drops = function(pos) + local n_slots = 100 + drop_inv:set_size("main", n_slots) + drop_inv:set_list("main", {}) + + -- Put all dropped items into the detached inventory + for c_id, count in pairs(cutter.drops) do + local name = minetest.get_name_from_content_id(c_id) + + -- Add drops in bulk -> keep some randomness + while count > 0 do + local drops = minetest.get_node_drops(name, "") + -- higher numbers are faster but return uneven sapling counts + local decrement = math.ceil(count * 0.3) + decrement = math.min(count, math.max(5, decrement)) + + for _, stack in ipairs(drops) do + stack = ItemStack(stack) + local total = math.ceil(stack:get_count() * decrement * chainsaw_efficiency) + local stack_max = stack:get_stack_max() + + -- Split into full stacks + while total > 0 do + local size = math.min(total, stack_max) + stack:set_count(size) + drop_inv:add_item("main", stack) + total = total - size + end + end + count = count - decrement + end + end + + -- Drop in random places + for i = 1, n_slots do + local stack = drop_inv:get_stack("main", i) + if stack:is_empty() then + break + end + minetest.add_item(get_drop_pos(pos), stack) + end + + drop_inv:set_size("main", 0) -- free RAM end @@ -249,33 +306,50 @@ description = S("Chainsaw"), inventory_image = "technic_chainsaw.png", stack_max = 1, + wear_represents = "technic_RE_charge", + on_refill = technic.refill_RE_charge, on_use = function(itemstack, user, pointed_thing) if pointed_thing.type ~= "node" then return itemstack end + local meta = minetest.deserialize(itemstack:get_metadata()) if not meta or not meta.charge then return end - -- Send current charge to digging function so that the chainsaw will stop after digging a number of nodes. - if meta.charge < chainsaw_charge_per_node then + + local name = user:get_player_name() + if minetest.is_protected(pointed_thing.under, name) then + minetest.record_protection_violation(pointed_thing.under, name) return end - local pos = minetest.get_pointed_thing_position(pointed_thing, above) - meta.charge = chainsaw_dig_it(pos, user, meta.charge) - technic.set_RE_wear(itemstack, meta.charge, chainsaw_max_charge) - itemstack:set_metadata(minetest.serialize(meta)) + -- Send current charge to digging function so that the + -- chainsaw will stop after digging a number of nodes + chainsaw_dig(user, pointed_thing.under, meta.charge) + meta.charge = cutter.charge + + cutter = {} -- Free RAM + + if not technic.creative_mode then + technic.set_RE_wear(itemstack, meta.charge, chainsaw_max_charge) + itemstack:set_metadata(minetest.serialize(meta)) + end return itemstack end, }) +local mesecons_button = minetest.get_modpath("mesecons_button") +local trigger = mesecons_button and "mesecons_button:button_off" or "default:mese_crystal_fragment" + minetest.register_craft({ - output = 'technic:chainsaw', - recipe = { - {'technic:stainless_steel_ingot', 'technic:stainless_steel_ingot', 'technic:battery'}, - {'technic:stainless_steel_ingot', 'technic:motor', 'technic:battery'}, - {'', '', 'default:copper_ingot'}, - } + output = "technic:chainsaw", + recipe = { + {"technic:stainless_steel_ingot", trigger, "technic:battery"}, + {"basic_materials:copper_wire", "basic_materials:motor", "technic:battery"}, + {"", "", "technic:stainless_steel_ingot"}, + }, + replacements = { {"basic_materials:copper_wire", "basic_materials:empty_spool"}, }, + }) -- Gitblit v1.8.0