local path = minetest.get_modpath"cave_lighting" local search_dfs = dofile(path .. "/fill_3d.lua") cave_lighting = {} local function inform(name, msg) minetest.chat_send_player(name, msg) minetest.log("info", "[cave_lighting] "..name..": "..msg) end -- Like minetest.get_node but also works in unloaded areas local function get_node_loaded(pos) local node = minetest.get_node_or_nil(pos) if node then return node end minetest.load_area(pos) return minetest.get_node(pos) end -- Places a node with the same effects than the player's placing local function place_node(def, wi, stack, player, pt, inv) local leftover = def.on_place(stack, player, pt) inv:set_stack("main", wi, leftover) return leftover end -- Tests if theres a node an e.g. torch is allowed to be placed on local function pos_placeable(pos) local undernode = get_node_loaded(pos).name if undernode == "air" then return false end local data = minetest.registered_nodes[undernode] if data and (data.drawtype == "normal" or not data.drawtype) and data.pointable and not data.buildable_to then return true end return false end local moves_touch = { vector.new(-1, 0, 0), vector.new(1, 0, 0), vector.new(0, -1, 0), vector.new(0, 1, 0), vector.new(0, 0, -1), vector.new(0, 0, 1), } local moves_vi_near = {} for z = -1,1 do for y = -1,1 do for x = -1,1 do if x*x + y*y + z*z > 0 then moves_vi_near[#moves_vi_near+1] = z * 0x100000000 + y * 0x10000 + x end end end end -- Tests if it's a possible place for a light node local function pos_allowed(pos, maxlight, name) local light = minetest.get_node_light(pos, 0.5) if not light or light > maxlight or get_node_loaded(pos).name ~= "air" or minetest.is_protected(pos, name) then return end for k = 1, 6 do local p2 = vector.add(pos, moves_touch[k]) if pos_placeable(p2) then return p2 end end end -- Finds out possible places for a light node in a cave with an efficient -- variant of Depth First Search local function search_positions(startpos, maxlight, pname) local max_positions = tonumber(minetest.settings:get( "cave_lighting.maximum_search_positions")) or 8000000 local visited = {} local found = {} local num_found = 0 local function on_visit(vi) if visited[vi] then return false end visited[vi] = true if num_found > max_positions then return false end local pos = minetest.get_position_from_hash(vi) local under_pos = pos_allowed(pos, maxlight, pname) if under_pos then num_found = num_found+1 found[num_found] = {under = under_pos, above = pos} return true end return false end local vi_start = minetest.hash_node_position(startpos) on_visit(vi_start) search_dfs(on_visit, vi_start, function(vi, vi_off) return vi + vi_off end, moves_vi_near) -- Do not return the positions if the search has found too many, to avoid -- fragmented lighting return found, num_found <= max_positions end -- Lights up a cave local function place_torches(startpos, maxlight, player) -- Get the light_source item local inv = player:get_inventory() local wi = player:get_wield_index() local stack = inv:get_stack("main", wi) local node_name = stack:get_name() local def = minetest.registered_nodes[node_name] if not def then -- Support the chatcommand tool wi = wi+1 stack = inv:get_stack("main", wi) node_name = stack:get_name() def = minetest.registered_nodes[node_name] if not def then return false, "You need to have a node next to or as your wielded item." end end local nodelight = def.light_source if not nodelight or nodelight < maxlight then return false, "You need a node emitting light (enough light)." end -- Get possible positions local ps, found_all = search_positions(startpos, maxlight, player:get_player_name()) if #ps == 0 then return false, "Found no potential placement positions." end if not found_all then local allow_incomplete_lighting = minetest.settings:get_bool( "cave_lighting.allow_incomplete_lighting", false) if not allow_incomplete_lighting then return false, "The cave is too big." end inform(player:get_player_name(), "The cave is too big, so it is lit only partially.") end inform(player:get_player_name(), ('Collected %d potential placement positions. Placing "%s"s at some ' .. "of them..."):format(#ps, def.description)) local sound = def.sounds if sound then sound = sound.place end local count = 0 -- [[ -- should search for optimal places for torches -- The light depends on the manhattan distance to the light source. -- If for example maxlight=4 and nodelight=7 and a light stripe is -- (2,3,4,5,6,7), then I assume it is advisable to first put light nodes -- where the light is 3: (6,7,6,5,6,7) local l1 = math.max(maxlight - (nodelight - maxlight) + 2, 0) local n = #ps local found = true while found do found = false for k = n, 1, -1 do local pt = ps[k] local pos = pt.above local light = minetest.get_node_light(pos, 0.5) or 0 if light == l1 then pt.type = "node" stack = place_node(def, wi, stack, player, pt, inv) if stack:get_name() ~= node_name then return false, "No remaining light nodes" end count = count+1 if sound and count < 50 then minetest.sound_play(sound.name, {pos = pos, gain = sound.gain / count}) end found = true ps[k] = ps[n] ps[n] = nil n = n-1 elseif light > maxlight then ps[k] = ps[n] ps[n] = nil n = n-1 end end end--]] for k = 1, n do local pt = ps[k] local pos = pt.above local light = minetest.get_node_light(pos, 0.5) or 0 if light <= maxlight then pt.type = "node" stack = place_node(def, wi, stack, player, pt, inv) if stack:get_name() ~= node_name then return false, "No remaining light nodes" end count = count+1 if sound and count < 50 then minetest.sound_play(sound.name, {pos = pos, gain = sound.gain / count}) end end end return {count, def.description or stack:get_name(), nodelight} end -- Returns the camera position of the player; it does not include -- the client-side offset, e.g. bobbing (see view_bobbing_amount) local function get_eye_pos(player) local pos = vector.add(player:get_pos(), player:get_eye_offset()) pos.y = pos.y + player:get_properties().eye_height return pos end -- Returns pointed thing above and under positions for the player's view -- and tests if a node can be placed there local function get_pointed_target(player) -- Search a target node where the player would be able to place a torch local pos1 = get_eye_pos(player) local dir = player:get_look_dir() local pos2 = vector.add(pos1, vector.multiply(dir, 20)) local pointed = minetest.raycast(pos1, pos2, false, true)() if not pointed then return end local def_under = minetest.registered_nodes[ get_node_loaded(pointed.under).name] local def_above = minetest.registered_nodes[ get_node_loaded(pointed.above).name] if not def_under or not def_above or not def_above.buildable_to or def_under.buildable_to then -- Cannot place a node here return end return pointed.above, pointed.under end function cave_lighting.light_cave(player, maxlight) local pos = get_pointed_target(player) if not pos then return false, "No valid position for a torch placement found" end inform(player:get_player_name(), "Lighting a cave…") local t, errormsg = place_torches(pos, maxlight, player) if not t then return false, errormsg end if t[1] == 0 then return false, "No nodes placed." end return true, ("%d \"%s\"s placed. (maxlight: %d, placed nodes' light: %d)" ):format(t[1], t[2], maxlight, t[3]) end -- Chatcommand to light a full cave minetest.register_chatcommand("light_cave",{ description = "light a cave", params = "[maxlight=7]", privs = {give=true, interact=true}, func = function(name, param) local player = minetest.get_player_by_name(name) if not player then return false, "Player not found" end return cave_lighting.light_cave(player, tonumber(param) or 7) end }) -- Lazy torch placing local light_making_players function cave_lighting.enable_auto_placing(pname, maxlight) light_making_players = light_making_players or {} light_making_players[pname] = maxlight end function cave_lighting.disable_auto_placing(pname) light_making_players[pname] = nil if not next(light_making_players) then light_making_players = nil end end -- Chatcommand to automatically light the way while playing minetest.register_chatcommand("auto_light_placing",{ description = "automatically places lights", params = "[maxlight=3]", privs = {give=true, interact=true}, func = function(name, param) local player = minetest.get_player_by_name(name) if not player then return false, "Player not found" end cave_lighting.enable_auto_placing(name, tonumber(param) or 3) return true, "Placing lights automatically" end }) local function autoplace_step() -- Abort if noone uses it if not light_making_players then return end for pname, maxlight in pairs(light_making_players) do local player = minetest.get_player_by_name(pname) local pt = {type = "node"} pt.above, pt.under = get_pointed_target(player) if pt.above then local wi = player:get_wield_index() local inv = player:get_inventory() local stack = inv:get_stack("main", wi) local def = minetest.registered_nodes[stack:get_name()] local failed if not def then -- support the chatcommand tool wi = wi + 1 stack = inv:get_stack("main", wi) def = minetest.registered_nodes[stack:get_name()] if not def then inform(pname, "You need to have a node next to or as " .. "your wielded item.") failed = true end end if def then local nodelight = def.light_source if not nodelight or nodelight < maxlight then inform(pname, "You need a node emitting light (enough light).") failed = true end local light = minetest.get_node_light(pt.above, 0.5) or 0 if light <= maxlight and get_node_loaded(pt.above).name == "air" then local sound = def.sounds if sound then sound = sound.place end if sound then minetest.sound_play(sound.name, {pos = pt.above, gain = sound.gain}) end place_node(def, wi, stack, player, pt, inv) end end if failed then cave_lighting.disable_auto_placing(pname) end end end end local function autoplace() autoplace_step() minetest.after(0.1, autoplace) end autoplace()