From 2537583096f99e286751fcbb59ef04e856d3da61 Mon Sep 17 00:00:00 2001 From: HybridDog Date: Tue, 28 Jul 2020 20:28:44 +0200 Subject: [PATCH] Many changes Change the search algorithm Change default values and messages Use minetest.after instead of minetest.globalstep --- LICENSE.txt | 3 +- README.md | 10 +- fill_3d.lua | 53 ++++++++++ init.lua | 279 ++++++++++++++++++++++++++++------------------------ 4 files changed, 212 insertions(+), 133 deletions(-) create mode 100644 fill_3d.lua diff --git a/LICENSE.txt b/LICENSE.txt index a22a2da..97603aa 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1 +1,2 @@ -MIT +fill_3d.lua: LGPL 2.1; copied and modified from Minetest builtin +init.lua: MIT diff --git a/README.md b/README.md index a17b23f..ee44be8 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ [Mod] cave lighting chatcommand [cave_lighting] -This mod adds the chatcommand light_cave to automatically light up a cave with e.g. torches. +This mod adds the chatcommand light_cave to automatically light up a cave with e.g. torches. maxlight is the maximum light a lighting node is placed (to be able to see light strength at specific position, try my superpick mod) -**License:** see [LICENSE.txt](https://raw.githubusercontent.com/HybridDog/cave_lighting/master/LICENSE.txt) -**Download:** [zip](https://github.com/HybridDog/cave_lighting/archive/master.zip), [tar.gz](https://github.com/HybridDog/cave_lighting/archive/master.tar.gz) +**License:** see [LICENSE.txt](https://raw.githubusercontent.com/HybridDog/cave_lighting/master/LICENSE.txt) +**Download:** [zip](https://github.com/HybridDog/cave_lighting/archive/master.zip), [tar.gz](https://github.com/HybridDog/cave_lighting/archive/master.tar.gz) ![I'm a screenshot!](https://forum.minetest.net/download/file.php?mode=view&id=2922&sid=3e0175b669497750711e30b69568e805) @@ -13,5 +13,5 @@ If you got ideas or found bugs, please tell them to me. [How to install a mod?](http://wiki.minetest.net/Installing_Mods) -TODO: -— streamline position selecting +TODO: +* Take inventory items diff --git a/fill_3d.lua b/fill_3d.lua new file mode 100644 index 0000000..eaa7142 --- /dev/null +++ b/fill_3d.lua @@ -0,0 +1,53 @@ +-- Algorithm created by sofar and changed by others: +-- https://github.com/minetest/minetest/commit/d7908ee49480caaab63d05c8a53d93103579d7a9 + +local function search(go, p, apply_move, moves) + local num_moves = #moves + + -- We make a stack, and manually maintain size for performance. + -- Stored in the stack, we will maintain tables with pos, and + -- last neighbor visited. This way, when we get back to each + -- node, we know which directions we have already walked, and + -- which direction is the next to walk. + local s = {} + local n = 0 + -- The neighbor order we will visit from our table. + local v = 1 + + while true do + -- Push current pos onto the stack. + n = n + 1 + s[n] = {p = p, v = v} + -- Select next node from neighbor list. + p = apply_move(p, moves[v]) + -- Now we check out the node. If it is in need of an update, + -- it will let us know in the return value (true = updated). + if not go(p) then + -- If we don't need to "recurse" (walk) to it then pop + -- our previous pos off the stack and continue from there, + -- with the v value we were at when we last were at that + -- node + repeat + local pop = s[n] + p = pop.p + v = pop.v + s[n] = nil + n = n - 1 + -- If there's nothing left on the stack, and no + -- more sides to walk to, we're done and can exit + if n == 0 and v == num_moves then + return + end + until v < num_moves + -- The next round walk the next neighbor in list. + v = v + 1 + else + -- If we did need to walk the neighbor, then + -- start walking it from the walk order start (1), + -- and not the order we just pushed up the stack. + v = 1 + end + end +end + +return search diff --git a/init.lua b/init.lua index c96c17d..82fec93 100644 --- a/init.lua +++ b/init.lua @@ -1,88 +1,101 @@ + +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 --- tests if theres a node an e.g. torch is allowed to be placed on +-- 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 = minetest.get_node(pos).name + local undernode = get_node_loaded(pos).name if undernode == "air" then return false end local data = minetest.registered_nodes[undernode] - if not data then - return false - end - if data.drawtype == "normal" - or not data.drawtype then + 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 --- tests if it's a possible place for a light node +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 minetest.get_node(pos).name ~= "air" + or get_node_loaded(pos).name ~= "air" or minetest.is_protected(pos, name) then - return false + return end - for i = -1,1,2 do - for _,p2 in pairs{ - {x=pos.x+i, y=pos.y, z=pos.z}, - {x=pos.x, y=pos.y+i, z=pos.z}, - {x=pos.x, y=pos.y, z=pos.z+i}, - } do - if pos_placeable(p2) then - return p2 - end + for k = 1, 6 do + local p2 = vector.add(pos, moves_touch[k]) + if pos_placeable(p2) then + return p2 end end - return false end --- finds out possible places for a light node in a cave -local function get_ps(pos, maxlight, name, max) - local tab = {} - local num = 1 - - local todo = {pos} - local nt = 1 - - local tab_avoid = {} - while nt ~= 0 do - local p = todo[nt] - todo[nt] = nil - nt = nt-1 - - for i = -1,1 do - for j = -1,1 do - for k = -1,1 do - local p2 = {x=p.x+i, y=p.y+j, z=p.z+k} - local vi = minetest.hash_node_position(p2) - if not tab_avoid[vi] then - local atpos = pos_allowed(p2, maxlight, name) - if atpos then - tab[num] = {above=p2, under=atpos} - num = num+1 - - nt = nt+1 - todo[nt] = p2 - - tab_avoid[vi] = true - if max - and num > max then - return false - end - end - 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 - return tab + 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 @@ -97,22 +110,19 @@ local function place_torches(pos, maxlight, player, name) node = inv:get_stack("main", wi+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.") - return + 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 - inform(name, "You need a node emitting light (enough light).") - return + return false, "You need a node emitting light (enough light)." end -- Get possible positions - local ps = get_ps(pos, maxlight, name, 200^3) + local ps = search_positions(pos, maxlight, name, 200^3) if not ps then - inform(name, "It doesn't seem to be dark there or the cave is too big.") - return + return false, "It doesn't seem to be dark there or the cave is too big." end local sound = data.sounds if sound then @@ -121,37 +131,45 @@ local function place_torches(pos, maxlight, player, name) local count = 0 -- [[ -- should search for optimal places for torches - local l1 = math.max(2*maxlight-nodelight+1, 1) - local found = true - while found do - found = false - for n,pt in pairs(ps) do - 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) - found = true - ps[n] = nil - elseif light > maxlight then - ps[n] = nil + -- 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 _,pt in pairs(ps) do + 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}) + minetest.sound_play(sound.name, {pos = pos, + gain = sound.gain / count}) end pt.type = "node" minetest.item_place_node(ItemStack(node), player, pt) @@ -181,9 +199,9 @@ local function get_pointed_target(player) return end local def_under = minetest.registered_nodes[ - minetest.get_node(pointed.under).name] + get_node_loaded(pointed.under).name] local def_above = minetest.registered_nodes[ - minetest.get_node(pointed.above).name] + 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 @@ -193,45 +211,48 @@ local function get_pointed_target(player) return pointed.above, pointed.under end --- searches the position the player looked at and lights the cave -local function light_cave(player, name, maxlight) - local pos = get_pointed_target(player, name) +-- 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(name, "Lighting a cave…") - local t = place_torches(pos, maxlight, player, name) - if t then - if t[1] == 0 then - return false, "No nodes placed." - end - return true, t[1].." "..t[2].."s placed. (maxlight="..maxlight..", used_light="..t[3]..")" + 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 --- the chatcommand +-- Chatcommand to light a full cave minetest.register_chatcommand("light_cave",{ description = "light a cave", - params = "[maxlight]", + 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, name, tonumber(param) or 7) + return light_cave(player, tonumber(param) or 7) end }) --- lazy torch placing -local light_making_players, timer +-- Lazy torch placing --- the chatcommand +local light_making_players + +-- Chatcommand to automatically light the way while playing minetest.register_chatcommand("auto_light_placing",{ description = "automatically places lights", - params = "[maxlight]", + params = "[maxlight=3]", privs = {give=true, interact=true}, func = function(name, param) local player = minetest.get_player_by_name(name) @@ -239,39 +260,35 @@ minetest.register_chatcommand("auto_light_placing",{ return false, "Player not found" end light_making_players = light_making_players or {} - light_making_players[name] = tonumber(param) or 7 + light_making_players[name] = tonumber(param) or 3 timer = -0.5 + return true, "Placing lights automatically" end }) -minetest.register_globalstep(function(dtime) - -- abort if noone uses it +local function autoplace_step() + -- Abort if noone uses it if not light_making_players then return end - -- abort that it doesn't shoot too often (change it if your pc runs faster) - timer = timer+dtime - if timer < 0.1 then - return - end - timer = 0 - - for name,maxlight in pairs(light_making_players) do + 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, name) - if pt.above - and pos_placeable(pt.under) then - local node = player:get_inventory():get_stack("main", player:get_wield_index()):get_name() + 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() + 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.") + inform(name, "You need to have a node next to or as " .. + "your wielded item.") failed = true end end @@ -279,18 +296,20 @@ minetest.register_globalstep(function(dtime) local nodelight = data.light_source if not nodelight or nodelight < maxlight then - inform(name, "You need a node emitting light (enough light).") + 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 minetest.get_node(pt.above).name == "air" then + 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}) + minetest.sound_play(sound.name, + {pos = pt.above, gain = sound.gain}) end minetest.item_place_node(ItemStack(node), player, pt) end @@ -303,4 +322,10 @@ minetest.register_globalstep(function(dtime) end end end -end) +end + +local function autoplace() + autoplace_step() + minetest.after(0.1, autoplace) +end +autoplace()