local path = minetest.get_modpath"cave_lighting" local search_dfs = dofile(path .. "/fill_3d.lua") 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 -- 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 = { {x = -1, y = 0, z = 0}, {x = 1, y = 0, z = 0}, {x = 0, y = -1, z = 0}, {x = 0, y = 1, z = 0}, {x = 0, y = 0, z = -1}, {x = 0, y = 0, z = 1}, } local moves_near = {} for x = -1,1 do for y = -1,1 do for z = -1,1 do if x*x + y*y + z*z > 0 then moves_near[#moves_near+1] = {x = x, y = y, z = z} 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, max_positions) local visited = {} local found = {} local num_found = 0 local function on_visit(pos) local vi = minetest.hash_node_position(pos) if visited[vi] then return false end visited[vi] = true if num_found > max_positions then return false end 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 on_visit(startpos) search_dfs(on_visit, startpos, vector.add, moves_near) -- Do not return the positions if the search has found too many, to avoid -- fragmented lighting return num_found <= max_positions and num_found > 0 and found end -- Lights up a cave local function place_torches(pos, maxlight, player, name) -- Get the light_source item local inv = player:get_inventory() local wi = player:get_wield_index() local node = inv:get_stack("main", wi):get_name() local data = minetest.registered_nodes[node] if not data then -- Support the chatcommand tool node = inv:get_stack("main", wi+1):get_name() data = minetest.registered_nodes[node] if not data then return false, "You need to have a node next to or as your wielded item." end end local nodelight = data.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 = search_positions(pos, maxlight, name, 200^3) if not ps then return false, "It doesn't seem to be dark there or the cave is too big." end local sound = data.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 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 count = count+1 if sound and count < 50 then minetest.sound_play(sound.name, {pos = pos, gain = sound.gain / count}) end pt.type = "node" minetest.item_place_node(ItemStack(node), player, pt) 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--]] 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 count = count+1 if sound and count < 50 then minetest.sound_play(sound.name, {pos = pos, gain = sound.gain / count}) end pt.type = "node" minetest.item_place_node(ItemStack(node), player, pt) end end return {count, data.description or node, 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:getpos(), 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 -- Searches the position the player looked at and lights the cave local function 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 light_cave(player, tonumber(param) or 7) end }) -- Lazy torch placing local light_making_players -- 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 light_making_players = light_making_players or {} light_making_players[name] = tonumber(param) or 3 timer = -0.5 return true, "Placing lights automatically" end }) local function autoplace_step() -- Abort if noone uses it if not light_making_players then return end for name, maxlight in pairs(light_making_players) do local player = minetest.get_player_by_name(name) local pt = {type = "node"} pt.above, pt.under = get_pointed_target(player) if pt.above then local node = player:get_inventory():get_stack("main", player:get_wield_index()):get_name() local data = minetest.registered_nodes[node] local failed if not data then -- support the chatcommand tool node = player:get_inventory():get_stack("main", player:get_wield_index()+1):get_name() data = minetest.registered_nodes[node] if not data then inform(name, "You need to have a node next to or as " .. "your wielded item.") failed = true end end if data then local nodelight = data.light_source if not nodelight or nodelight < maxlight then inform(name, "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 = data.sounds if sound then sound = sound.place end if sound then minetest.sound_play(sound.name, {pos = pt.above, gain = sound.gain}) end minetest.item_place_node(ItemStack(node), player, pt) end end if failed then light_making_players[name] = nil if not next(light_making_players) then light_making_players = nil end end end end end local function autoplace() autoplace_step() minetest.after(0.1, autoplace) end autoplace()