commit 930af27e222356b982ef4156a1b3eb1b56cbe8af
Author: David G <kestral246@gmail.com>
Date:   Fri Sep 18 13:29:17 2020 -0700

    Initial commit.

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..25b60f1
--- /dev/null
+++ b/README.md
@@ -0,0 +1,62 @@
+Cave Tools [cavetools]
+======================
+Some useful tools for exploring caves.
+
+By David G (kestral246@gmail.com)
+
+![Cave Tools](screenshot.png "Cave Tools")
+
+A compilation of some of my other mods focused on making cave exploration easier.
+
+A set of flash lamps that momentarily light up the direction the player is pointed.
+
+- Flash Lamp
+- Flash Wand
+- Super Flash Lamp
+
+A set of depth finders that probe depth in the direction the player is pointed.
+
+- Flint Depth Finder
+- Diamond Depth Finder
+
+An override to the default ladders to allow them to be easily extended down by just right clicking a ladder with another ladder.
+
+- **Note:** This can create freestanding ladders. If the lack of ladder thickness bothers you, you can use Linuxdirks redef mod, which provides 3-d ladders.
+
+
+
+Dependencies
+------------
+
+- Ladder overrides only enabled if default exists.
+- Craft recipes optionally depend on default and tnt.
+- Wand also optionally depends on mana.
+
+
+Craft Recipes
+-------------
+
+![Flash Lamp](images/craft_lamp1.png "Flash Lamp (copper)")
+![Flash Lamp](images/craft_lamp2.png "Flash Lamp (tin)")
+
+![Flash Wand](images/craft_wand.png "Flash Wand")
+![Super Flash Lamp](images/craft_super.png "Super Flash Lamp")
+
+![Flint Depth Finder](images/craft_flint.png "Flint Depth Finder")
+![Diamond Depth Finder](images/craft_diamond.png "Diamond Depth Finder")
+
+
+Licenses
+--------
+
+Source code
+
+> The MIT License (MIT)
+
+Media (textures)
+
+> Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0)
+
+- Flint texture copied from default\_flint.png by Gambit.
+- Diamond texture copied from default\_diamond.png by BlockMen.
+- Stick texture copied from default\_stick.png by BlockMen.
diff --git a/depth_finder.lua b/depth_finder.lua
new file mode 100644
index 0000000..341abe4
--- /dev/null
+++ b/depth_finder.lua
@@ -0,0 +1,158 @@
+-- Depth Finder [depth_finder]
+-- by David G (kestral246@gmail.com)
+-- 2020-06-22
+
+-- Either two flints or two diamonds--bang them together and the return echo indicates depth.
+
+local scan_angle = math.pi/4  -- corresponds to 90°
+
+-- Maximum number of nodes to check
+local maxcount = 150000
+
+-- The wear and radius can now be set independently for each lamp tool.
+
+-- Set to true to print debug statistics.
+local debug = false
+
+local scanned = {}  -- Set containing scanned nodes, so they don't get scanned multiple times.
+local tocheck = {}  -- Table of nodes to check.
+local max_depth = {}  -- Deepest node scanned.
+
+minetest.register_on_joinplayer(function(player)
+	local pname = player:get_player_name()
+	scanned[pname] = {}
+	tocheck[pname] = {}
+	max_depth[pname] = 0
+end)
+
+minetest.register_on_leaveplayer(function(player)
+	local pname = player:get_player_name()
+	scanned[pname] = nil
+	tocheck[pname] = nil
+	max_depth[pname] = nil
+end)
+
+-- Determine number of elements in table, for summary output.
+local tlength = function(T)
+	local count = 0
+	for _ in pairs(T) do count = count + 1 end
+	return count
+end
+
+-- Scan neighboring nodes, flag for checking if air.
+local scan_node = function(pname, pos, origin, vdir, maxdist)
+	-- Update to search a cone pattern in direction pointed.
+	-- Need small sphere to get cone out.
+	local origin2d = {x=origin.x, y=0, z=origin.z}
+	local pos2d = {x=pos.x, y=0, z=pos.z}
+	local radius = vector.distance(origin2d, pos2d)
+	if radius <= 2 or (radius <= maxdist and vector.angle(vdir, vector.direction(origin2d, pos2d)) < scan_angle) then
+		local enc_pos = minetest.hash_node_position(pos)
+		if scanned[pname][enc_pos] ~= true then  -- hasn't been scanned
+			local name = minetest.get_node(pos).name
+			if name == "air" then  -- checkable
+				table.insert(tocheck[pname], enc_pos)  -- add to check list
+			end
+			scanned[pname][enc_pos] = true  -- don't scan again
+		end
+	end
+end
+
+-- To check, scan all neighbors and determine if this node needs to be converted to light.
+local check_node = function(pname, pos, origin, vdir, maxdist, height, depth)
+	local enc_pos = minetest.hash_node_position(pos)
+	local name = minetest.get_node(pos).name
+	scan_node(pname, vector.add(pos, {x=0,y=0,z=1}), origin, vdir, maxdist)  -- north
+	scan_node(pname, vector.add(pos, {x=1,y=0,z=0}), origin, vdir, maxdist)  -- east
+	scan_node(pname, vector.add(pos, {x=0,y=0,z=-1}), origin, vdir, maxdist)  -- south
+	scan_node(pname, vector.add(pos, {x=-1,y=0,z=0}), origin, vdir, maxdist)  -- west
+	if pos.y > origin.y - depth then
+		scan_node(pname, vector.add(pos, {x=0,y=-1,z=0}), origin, vdir, maxdist)  -- down
+	end
+	if pos.y < origin.y + height then
+		scan_node(pname, vector.add(pos, {x=0,y=1,z=0}), origin, vdir, maxdist)  -- up
+	end
+	if pos.y < max_depth[pname] then
+		max_depth[pname] = pos.y
+	end
+end
+
+local use_finder = function(player, itemstack, radius, height, depth, wear)
+	local pname = player:get_player_name()
+	local pos = vector.add(vector.round(player:get_pos()), {x=0,y=0,z=0})  -- position of wand
+	local theta = math.fmod(player:get_look_horizontal() + math.pi/2, 2*math.pi)
+	local vdir = vector.normalize({x=math.cos(theta), y=0, z=math.sin(theta)})
+	local key_stats = player:get_player_control()
+	local wear_cost = wear
+
+	-- Initialize temporary tables for safety.
+	scanned[pname] = {}
+	tocheck[pname] = {}
+	max_depth[pname] = pos.y
+	-- Search starts at finder position.
+	table.insert(tocheck[pname], minetest.hash_node_position(pos))
+	local count = 1
+	while count <= table.getn(tocheck[pname]) and count <= maxcount do
+		check_node(pname, minetest.get_position_from_hash(tocheck[pname][count]), pos, vdir, radius, height, depth)
+		count = count + 1
+	end
+	count = count - 1 
+	if debug then  -- print statistics
+		minetest.debug("depth_finder: y = "..tostring(pos.y)..", scan = "..
+			tostring(tlength(scanned[pname]))..", check = "..tostring(count)..", depth = "..
+			tostring(max_depth[pname]))
+	end
+	minetest.chat_send_player(pname, "depth = "..tostring(max_depth[pname] - pos.y))
+	-- Clear temporary tables, which could be large.
+	scanned[pname] = {}
+	tocheck[pname] = {}
+	max_depth[pname] = 0
+	-- Add wear to finder
+	itemstack:add_wear(wear_cost)
+	return itemstack
+end
+
+minetest.register_tool("cavetools:depth_finder_flint", {
+	description = "Flint Depth Finder",
+	inventory_image = "cavetools_depth_finder_flint.png",
+	stack_max = 1,
+	on_use = function(itemstack, player, pointed_thing)
+		local radius = 30
+		local height = 6
+		local depth = 40
+		local wear = math.floor(65535/25)
+		local worn_item = use_finder(player, itemstack, radius, height, depth, wear)
+		return worn_item
+	end,
+})
+
+minetest.register_tool("cavetools:depth_finder_diamond", {
+	description = "Diamond Depth Finder",
+	inventory_image = "cavetools_depth_finder_diamond.png",
+	stack_max = 1,
+	on_use = function(itemstack, player, pointed_thing)
+		local radius = 50
+		local height = 10
+		local depth = 80
+		local wear = math.floor(65535/40)
+		local worn_item = use_finder(player, itemstack, radius, height, depth, wear)
+		return worn_item
+	end,
+})
+
+-- Need default for crafting recipe.
+-- Example craft recipe.
+if minetest.get_modpath("default") ~= nil then
+	minetest.register_craft({
+		output = "cavetools:depth_finder_flint",
+		recipe = {
+			{"default:flint", "default:flint"}
+		}
+	})
+	minetest.register_craft({
+		output = "cavetools:depth_finder_diamond",
+		recipe = {
+			{"default:diamond", "default:diamond"}
+		}
+	})
+end
diff --git a/flash_lamps.lua b/flash_lamps.lua
new file mode 100644
index 0000000..6f5c738
--- /dev/null
+++ b/flash_lamps.lua
@@ -0,0 +1,274 @@
+-- Flash Lamps (formerly Wand of Illumination) [flash_lamps]
+-- by David G (kestral246@gmail.com)
+-- 2020-06-22
+
+-- Lights up what's in front, but only for a moment.
+-- Provides a flash_lamp, flash_wand, and super_flash_lamp.
+
+-- How bright and wide to make lights.
+-- For reference, the default:torch has a brightness of 12.
+local brightness_value = 11
+local light_cone = math.pi/3  -- corresponds to 120°
+
+-- Maximum number of nodes to check (use debug to determine).
+local maxcount = 100000
+
+-- The wear, mana, and radius can now be set independently for each lamp tool.
+-- For extended range, all of these values are doubled.
+
+-- Setting to allow optional use of ABM for light decay.
+-- Make sure all lights have faded before disabling this option.
+local use_abm = minetest.settings:get_bool("cavetools_use_abm", false)
+if use_abm then
+	minetest.log("warning", "Flash Lamps using ABM for light decay.")
+end
+
+-- Set to true to print debug statistics.
+local debug = false
+
+-- Check for mana mod
+local using_mana_mod = false
+if minetest.get_modpath("mana") ~= nil then
+	using_mana_mod = true
+end
+
+local scanned = {}  -- Set containing scanned nodes, so they don't get scanned multiple times.
+local tocheck = {}  -- Table of nodes to check.
+local tolight = {}  -- Table of nodes that need to be converted to fill lights.
+
+minetest.register_on_joinplayer(function(player)
+	local pname = player:get_player_name()
+	scanned[pname] = {}
+	tocheck[pname] = {}
+	tolight[pname] = {}
+end)
+
+minetest.register_on_leaveplayer(function(player)
+	local pname = player:get_player_name()
+	scanned[pname] = nil
+	tocheck[pname] = nil
+	tolight[pname] = nil
+end)
+
+local mana_check = function(player, cost)
+	local allowed
+	if cost > 0 then
+		if using_mana_mod then
+			if mana.subtract(player:get_player_name(), cost) then
+				allowed = true
+			else
+				allowed = false
+			end
+		else
+			allowed = true
+		end
+		return allowed
+	else  -- always allowed when not using mana
+		return true
+	end
+end
+
+minetest.register_node("cavetools:light", {
+	description = "Flash Lamp Light",
+	drawtype = "airlike",
+	walkable = false,
+	paramtype = "light",
+	sunlight_propagates = true,
+	light_source = brightness_value,
+	pointable = false,
+	buildable_to = true,
+	drops = "",
+	groups = {cracky = 3, oddly_breakable_by_hand = 3, fill_hidden = 1, not_in_creative_inventory = 1},
+	on_timer = function(pos)  -- use node timer
+		minetest.remove_node(pos)
+	end,
+})
+
+if use_abm then  -- optional ABM
+	minetest.register_abm({
+		nodenames = {"cavetools:light"},
+		interval = 2.0,  -- need to tune
+		chance = 10,  -- need to tune
+		action = function(pos, node, active_object_count, active_object_count_wider)
+			minetest.set_node(pos, {name = "air"})
+		end
+	})
+end
+
+-- Determine number of elements in table, for summary output.
+local tlength = function(T)
+	local count = 0
+	for _ in pairs(T) do count = count + 1 end
+	return count
+end
+
+-- Scan neighboring nodes, flag for checking if air.
+local scan_node = function(pname, pos, origin, vdir, maxdist)
+	-- Update to send out a cone of light in direction pointed.
+	-- Need small sphere to get cone of light out.
+	local radius = vector.distance(origin, pos)
+	if radius <= 2 or (radius <= maxdist and vector.angle(vdir, vector.direction(origin, pos)) < light_cone) then
+		local enc_pos = minetest.hash_node_position(pos)
+		if scanned[pname][enc_pos] ~= true then  -- hasn't been scanned
+			local name = minetest.get_node(pos).name
+			if name == "air" then  -- checkable
+				table.insert(tocheck[pname], enc_pos)  -- add to check list
+			end
+			scanned[pname][enc_pos] = true  -- don't scan again
+		end
+	end
+end
+
+-- To check, scan all neighbors and determine if this node needs to be converted to light.
+local check_node = function(pname, pos, origin, vdir, maxdist)
+	local enc_pos = minetest.hash_node_position(pos)
+	local name = minetest.get_node(pos).name
+	scan_node(pname, vector.add(pos, {x=0,y=0,z=1}), origin, vdir, maxdist)  -- north
+	scan_node(pname, vector.add(pos, {x=1,y=0,z=0}), origin, vdir, maxdist)  -- east
+	scan_node(pname, vector.add(pos, {x=0,y=0,z=-1}), origin, vdir, maxdist)  -- south
+	scan_node(pname, vector.add(pos, {x=-1,y=0,z=0}), origin, vdir, maxdist)  -- west
+	scan_node(pname, vector.add(pos, {x=0,y=-1,z=0}), origin, vdir, maxdist)  -- down
+	scan_node(pname, vector.add(pos, {x=0,y=1,z=0}), origin, vdir, maxdist)  -- up
+	if name == "air" and ((pos.x%4 == 0 and pos.y%8 == 0 and pos.z%4 == 0) or
+			(pos.x%4 == 2 and pos.y%8 == 4 and pos.z%4 == 2))
+			and minetest.get_node_light(pos) < brightness_value then
+		table.insert(tolight[pname], enc_pos)
+	end
+end
+
+local use_lamp = function(player, itemstack, radius, wear, mana)
+	local pname = player:get_player_name()
+	local pos = vector.add(vector.round(player:get_pos()), {x=0,y=1,z=0})  -- position of wand
+	local theta = math.fmod(player:get_look_horizontal() + math.pi/2, 2*math.pi)
+	local phi = player:get_look_vertical() + math.pi/2
+	local vdir = vector.normalize({x=math.sin(phi)*math.cos(theta), y=math.cos(phi), z=math.sin(phi)*math.sin(theta)})
+	-- For debug only.
+	--minetest.chat_send_player(pname, "theta = "..tostring(theta)..", phi = "..tostring(phi)..", vdir = "..tostring(minetest.serialize(vdir)))
+	local key_stats = player:get_player_control()
+	local wear_cost = wear
+	local mana_cost = mana
+	if key_stats.sneak or key_stats.aux1 then  -- extended
+		radius = 2 * radius
+		wear_cost = 2 * wear_cost
+		mana_cost = 2 * mana_cost
+	end
+	if mana_check(player, mana_cost) then
+		-- Initialize temporary tables for safety.
+		scanned[pname] = {}
+		tocheck[pname] = {}
+		tolight[pname] = {}
+		-- Search starts at wand position.
+		table.insert(tocheck[pname], minetest.hash_node_position(pos))
+		local count = 1
+		while count <= table.getn(tocheck[pname]) and count <= maxcount do
+			check_node(pname, minetest.get_position_from_hash(tocheck[pname][count]), pos, vdir, radius)
+			count = count + 1
+		end
+		count = count - 1 
+		if debug then  -- print statistics
+			minetest.debug("flash_lamps: y = "..tostring(pos.y)..", scan = "..
+				tostring(tlength(scanned[pname]))..", check = "..tostring(count)..", lights = "..
+				tostring(tlength(tolight[pname])))
+		end
+		-- Add lights to all locations flagged for lighting.
+		for _,v in ipairs(tolight[pname]) do
+			local fpos = minetest.get_position_from_hash(v)
+			minetest.set_node(fpos, {name="cavetools:light"})
+			if not use_abm then
+				local timer = minetest.get_node_timer(fpos)  -- use node timer
+				timer:set(math.random(60), 0)
+			end
+		end
+		-- Clear temporary tables, which could be large.
+		scanned[pname] = {}
+		tocheck[pname] = {}
+		tolight[pname] = {}
+		-- Add wear to wand
+		itemstack:add_wear(wear_cost)
+		return itemstack
+	end
+end
+
+minetest.register_tool("cavetools:flash_wand", {
+	description = "Flash Wand",
+	inventory_image = "cavetools_flash_wand.png",
+	stack_max = 1,
+	on_use = function(itemstack, player, pointed_thing)
+		local radius = 15  -- or 30
+		local wear = math.floor(65535/25)
+		local mana = 100
+		local worn_item = use_lamp(player, itemstack, radius, wear, mana)
+		return worn_item
+	end,
+})
+
+minetest.register_tool("cavetools:flash_lamp", {
+	description = "Flash Lamp",
+	inventory_image = "cavetools_flash_lamp.png",
+	stack_max = 1,
+	on_use = function(itemstack, player, pointed_thing)
+		local radius = 10  -- or 20
+		local wear = math.floor(65535/15)
+		local mana = 0
+		local worn_item = use_lamp(player, itemstack, radius, wear, mana)
+		return worn_item
+	end,
+})
+
+minetest.register_tool("cavetools:super_flash_lamp", {
+	description = "Super Flash Lamp",
+	inventory_image = "cavetools_super_flash_lamp.png",
+	stack_max = 1,
+	on_use = function(itemstack, player, pointed_thing)
+		local radius = 20  -- or 40
+		local wear = math.floor(65535/40)
+		local mana = 0
+		local worn_item = use_lamp(player, itemstack, radius, wear, mana)
+		return worn_item
+	end,
+})
+
+-- Need default for crafting recipe.
+-- Example craft recipe for wand.
+if minetest.get_modpath("default") ~= nil then
+	minetest.register_craft({
+		output = "cavetools:flash_wand",
+		recipe = {
+			{"default:mese_crystal"},
+			{"group:stick"}
+		}
+	})
+end
+
+-- Need default and tnt mods for crafting recipe.
+-- Add copper or tin to make harder to craft.
+if minetest.get_modpath("default") ~= nil and minetest.get_modpath("tnt") ~= nil then
+	minetest.register_craft({
+		output = "cavetools:flash_lamp",
+		recipe = {
+			{"tnt:gunpowder", "tnt:gunpowder", "tnt:gunpowder"},
+			{"", "default:tin_ingot", ""},
+			{"", "group:stick", ""}
+		}
+	})
+	minetest.register_craft({
+		output = "cavetools:flash_lamp",
+		recipe = {
+			{"tnt:gunpowder", "tnt:gunpowder", "tnt:gunpowder"},
+			{"", "default:copper_ingot", ""},
+			{"", "group:stick", ""}
+		}
+	})
+end
+
+-- Need default for crafting recipe.
+if minetest.get_modpath("default") ~= nil then
+	minetest.register_craft({
+		output = "cavetools:super_flash_lamp",
+		recipe = {
+			{"default:meselamp", "default:obsidian", "default:meselamp"},
+			{"default:obsidian", "default:meselamp", "default:obsidian"},
+			{"default:meselamp", "default:obsidian", "default:meselamp"}
+		}
+	})
+end
diff --git a/images/craft_diamond.png b/images/craft_diamond.png
new file mode 100644
index 0000000..3741f8b
Binary files /dev/null and b/images/craft_diamond.png differ
diff --git a/images/craft_flint.png b/images/craft_flint.png
new file mode 100644
index 0000000..39fa8d6
Binary files /dev/null and b/images/craft_flint.png differ
diff --git a/images/craft_lamp1.png b/images/craft_lamp1.png
new file mode 100644
index 0000000..3dc1330
Binary files /dev/null and b/images/craft_lamp1.png differ
diff --git a/images/craft_lamp2.png b/images/craft_lamp2.png
new file mode 100644
index 0000000..17223b3
Binary files /dev/null and b/images/craft_lamp2.png differ
diff --git a/images/craft_super.png b/images/craft_super.png
new file mode 100644
index 0000000..5da36c2
Binary files /dev/null and b/images/craft_super.png differ
diff --git a/images/craft_wand.png b/images/craft_wand.png
new file mode 100644
index 0000000..807f226
Binary files /dev/null and b/images/craft_wand.png differ
diff --git a/init.lua b/init.lua
new file mode 100644
index 0000000..f09bbaf
--- /dev/null
+++ b/init.lua
@@ -0,0 +1,9 @@
+-- Cave Tools [cavetools]
+
+dofile(minetest.get_modpath("cavetools").."/depth_finder.lua")
+dofile(minetest.get_modpath("cavetools").."/flash_lamps.lua")
+
+if minetest.get_modpath("default") ~= nil then
+	dofile(minetest.get_modpath("cavetools").."/ladder_overrides.lua")
+end
+
diff --git a/ladder_overrides.lua b/ladder_overrides.lua
new file mode 100644
index 0000000..211fd4f
--- /dev/null
+++ b/ladder_overrides.lua
@@ -0,0 +1,29 @@
+-- Cave Ladders [cave_ladders]
+-- by David G (kestral246@gmail.com)
+-- 2020-06-22
+
+minetest.override_item("default:ladder_wood", {
+	on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
+		if clicker:get_wielded_item():get_name() == "default:ladder_wood" then
+			local below = {x = pos.x, y = pos.y - 1, z = pos.z}
+			if minetest.get_node(below).name == "air" then
+				minetest.set_node(below, {name = "default:ladder_wood", param2 = node.param2})
+				itemstack:take_item(1)
+			end
+			return itemstack 
+		end
+	end
+})
+
+minetest.override_item("default:ladder_steel", {
+	on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
+		if clicker:get_wielded_item():get_name() == "default:ladder_steel" then
+			local below = {x = pos.x, y = pos.y - 1, z = pos.z}
+			if minetest.get_node(below).name == "air" then
+				minetest.set_node(below, {name = "default:ladder_steel", param2 = node.param2})
+				itemstack:take_item(1)
+			end
+			return itemstack 
+		end
+	end
+})
diff --git a/mod.conf b/mod.conf
new file mode 100644
index 0000000..3f57f63
--- /dev/null
+++ b/mod.conf
@@ -0,0 +1,3 @@
+name = cavetools
+description = Some useful tools for exploring caves.
+optional_depends = default, mana, tnt
diff --git a/screenshot.png b/screenshot.png
new file mode 100644
index 0000000..0edf267
Binary files /dev/null and b/screenshot.png differ
diff --git a/textures/cavetools_depth_finder_diamond.png b/textures/cavetools_depth_finder_diamond.png
new file mode 100644
index 0000000..fd6d70e
Binary files /dev/null and b/textures/cavetools_depth_finder_diamond.png differ
diff --git a/textures/cavetools_depth_finder_flint.png b/textures/cavetools_depth_finder_flint.png
new file mode 100644
index 0000000..3fd56c4
Binary files /dev/null and b/textures/cavetools_depth_finder_flint.png differ
diff --git a/textures/cavetools_flash_lamp.png b/textures/cavetools_flash_lamp.png
new file mode 100644
index 0000000..2ccf906
Binary files /dev/null and b/textures/cavetools_flash_lamp.png differ
diff --git a/textures/cavetools_flash_wand.png b/textures/cavetools_flash_wand.png
new file mode 100644
index 0000000..11948de
Binary files /dev/null and b/textures/cavetools_flash_wand.png differ
diff --git a/textures/cavetools_super_flash_lamp.png b/textures/cavetools_super_flash_lamp.png
new file mode 100644
index 0000000..51a4f50
Binary files /dev/null and b/textures/cavetools_super_flash_lamp.png differ