code:
local S = minetest.get_translator(minetest.get_current_modname())

local use_timeout = {}

local check_timeout = function(user)
	if not user:is_player() then return false end
	local player_name = user:get_player_name()
	local curr_time = minetest.get_gametime() -- minetest.get_us_time() for microsecond precision
	local last_used = use_timeout[player_name]
	if not last_used or curr_time > last_used then
		use_timeout[player_name] = curr_time
		return true
	end
end

local drill_particles = function(pos)
	minetest.add_particlespawner({
		amount = 25,
		time = 0.5,
		minpos = pos,
		maxpos = pos,
		minvel = {x=-5, y=0, z=-5},
		maxvel = {x=5, y=10, z=5},
		minacc = {x=0, y=-10, z=0},
		maxacc = {x=0, y=-10, z=0},
		minexptime = 2.5,
		maxexptime = 7.5,
		minsize = 1,
		maxsize = 5,
		texture = "dfcaverns_glowpit_particle.png",
		collisiondetection = true,
		collision_removal = true,
		glow = 15,
	})
end

local slade_drill_def = {
	_doc_items_longdesc = df_underworld_items.doc.slade_drill_desc,
	_doc_items_usagehelp = df_underworld_items.doc.slade_drill_usage,
	light_source = minetest.LIGHT_MAX,
	description = S("Slade Drill"),
	inventory_image = "dfcaverns_slade_drill.png",
	groups = {cracky=3, stone=1, slade=1, pit_plasma_resistant=1, mese_radiation_shield=1,creative_breakable=1, material_stone=1},
	sounds = df_dependencies.sound_stone({ footstep = { name = "bedrock2_step", gain = 1 } }),
	_mcl_blast_resistance = 8,
	_mcl_hardness = 5,
	on_use = function(itemstack, user, pointed_thing)
		if pointed_thing.type == "node" then
			local pos = pointed_thing.under
			local node = minetest.get_node(pos)
			if node.name == "df_underworld_items:slade" and check_timeout(user) then
				if not minetest.is_creative_enabled(user:get_player_name()) then
					itemstack:set_wear(itemstack:get_wear()+1)
				end
				minetest.sound_play("dfcaverns_slade_drill", {pos = user:get_pos()})
				minetest.set_node(pos, {name="df_underworld_items:slade_sand"})
				drill_particles(pointed_thing.above)
				return itemstack
			end
		end
	end,
}

local tnt_boom = df_dependencies.tnt_boom
if tnt_boom then
	slade_drill_def.on_use = function(itemstack, user, pointed_thing)
		if not check_timeout(user) then return end
		local pos
		if pointed_thing.type == "node" then pos = pointed_thing.under
		elseif pointed_thing.type == "object" then pos = pointed_thing.ref:get_pos() end
		if pos then
			tnt_boom(pos, {radius=1})
			minetest.sound_play("dfcaverns_slade_drill", {pos = user:get_pos()})
			if not minetest.is_creative_enabled(user:get_player_name()) then
				itemstack:set_wear(itemstack:get_wear()+1)
			end
			drill_particles(pos)
			minetest.check_for_falling({x=pos.x, y=pos.y+1, z=pos.z})
			return itemstack
		end
	end
end

minetest.register_tool("df_underworld_items:slade_drill", slade_drill_def)


code:
The following is a comprehensive description of the various cavern layers, plants, trees, minerals, and other such novelties in the DF Caverns mod. Players may wish to avoid reading it so as to not be spoiled on some of the things they may discover in the deep places of the world.

# Common features found on many layers

## Speleothems

There are three main varieties of [speleothem](https://en.wikipedia.org/wiki/Speleothem) that can be found in caves ranging from the surface sea level down to the Sunless Sea; stalactites, stalagmites, and large flowstone formations. Stalactites and stalagmites are small single-node-thick structures that form tapered spikes and flowstone is composed of conventional cubical blocks. All are composed of minerals deposited by flowing water. They come in "wet" and "dry" forms whose only difference is visual; wet flowstone is found in damp places where water still flows over its surface and dry flowstone is "dead." Stalactites and stalagmites mirror each other on the floor and ceiling of caves; stalagmites form from drops that fall off the tip of stalactites. When floor and ceiling are close enough together the two can eventually grow to meet, forming a column.

Inside the large caverns gigantic versions of stalactites and stalagmites can be found, producing columns that can be tens of meters in diameter.

## Cave moss

Cave moss is technically a form of mold, but fortunately a relatively benign one given its ubiquity. Its fibers form a tough but springy mat over the surface of any organic-rich soil that accumulates deep underground. It is essentially the "grass" of the deeps. Cave moss is faintly bioluminescent and so is useful for visually defining the floor spaces of giant caverns that would otherwise be a yawning pit of blackness.

Underground farming can be done directly on cave moss, but it is best if the soil is tilled with a hoe. Cave moss will slowly spread to adjacent bare soil nodes, but if it's exposed to bright light for a period of time it will die back and leave bare soil again.

## Floor fungus

Floor fungus produces a thin, slick film that spreads through the cracks of broken rock. Its ability to subsist on the tiniest traces of nutrients means it's found in relatively harsh underground environments. Floor fungus has no known uses. It can penetrate deeply into cobblestone constructions if an infestation gets hold, but it is difficult to transport and is inhibited by light so it hasn't spread beyond the deep caverns.

Underground farming can be done on floor-fungus-infested cobble, but growth is very slow. Floor fungus doesn't bioluminesce so caverns with floor fungus tend to be much darker and harder to navigate than cave moss areas.

## Glow worms

Glistening strings of silk hang from the ceilings of some of the larger and moister caverns, lit by the millions of tiny bioluminescent worms that spun them. Glow worms prey on the insects they lure and entangle with their faux starry sky - and sometimes the occasional bat or other larger flying beast.

Glow worms can be harvested and used as a source of light but they die when exposed to light significantly brighter than themselves or when immersed in water. A colony of glow worms hung in a hospitable environment will undergo a modest amount of growth, allowing it to be divided and propagated.

# Farmable organisms

![Farmable plants, except Torchspines (see Black Cap cavern)](./df_caverns/screenshots/farmable_plants.jpg)

## Plump helmet

A basic staple food of the underground realms, plump helmets are thick purplish mushrooms that grow well under almost any condition except direct exposure to light. Plump helmets can be picked and eaten unprepared, and propagate easily.

## Cave wheat

Cave wheat is literally a breed of grain-producing grass that somehow lost its ability to photosynthesize and adapted to a more fungal style of life. Like its surface cousin, cave wheat produces grain that can be ground into a form of flour. Cave wheat flour can be baked into a tough, durable form of bread, or combined with other ingredients.

## Sweet Pods

Sweet pods grow in rich soil, and once they reach maturity they draw that supply of nutrients up to concentrate it in their fruiting bodies. They turn bright red when ripe and can be processed in a variety of ways to extract the sugars they contain. When milled or dried in an oven, sweet pods produce a granular sugary substance. Crushing them in a bucket squeezes out a flavorful syrup.

## Pig tails

Pig tails are a fibrous fungal growth that's most notable for its twisting stalks. In a mature stand of pig tails the helical stalks intertwine into a dense mesh. Pig tail stalks can be processed to extract fibers useful as thread.

## Dimple cups

The distinctive midnight-blue caps of these mushrooms are inverted, exposing their gills to any breeze that might pass, and have dimpled edges that give them their name. Dimple cups can be dried, ground, and processed to extract a deep blue dye.

## Quarry bushes

A rare breed of fungus from deep underground that produces a bushy cluster of rumpled gray 'blades'. The biological function of these blades is not known, as quarry bushes reproduce via hard-shelled nodules that grow down at the blade's base. Quarry bush leaves and nodules (called 'rock nuts') can be harvested and are edible with processing. The dried blades of a quarry bush add a welcome zing to recipes containing otherwise-bland subterranean foodstuffs, but they're too spicy to be eaten on their own.

## Torchspines

Torchspines are strange organic formations that are alive only in a technical sense. They "feed" on volatile flammable vapors vented up through their structure, growing from combustion residue deposited at their tips. A torchspine alternates between active and quiescent phases and emits dim light when active. They can be harvested for torches, and their embers sprout into new torchspines when placed on flammable surfaces.

Physically, they resemble stalagmites. Unlike most underground growths they are unaffected by bright light.

# First cavern layer

By default, the first cavern layer can be found between -200 and -800 meters elevation. Biomes found on this level include Tower Cap, Fungiwood and "barren" caverns that lack significant organic growth. Some caverns have lakes of water filling their lower regions, others have little or no standing water.

## Tower Cap caverns

![Two Tower Caps](./df_caverns/screenshots/tower_cap.jpg)

Tower Caps are enormous pale mushrooms with woody stems and caps. Aside from their sheer size there's not much unusual about them; they are useful as a substitute for surface wood and can be used for most wood-requiring crafting recipes. Plump helmets also grow here.

## Fungiwood caverns

![Fungiwood, with stalagmites in foreground](./df_caverns/screenshots/fungiwood.jpg)

Fungiwoods have thinner stalks that sprout numerous thin irregular "shelves" branching out along its height. The shelves are useless except as fuel, but the main stem of a fungiwood is another wood substitute.

Plump helmets and cave wheat grow here.

## Spindlestems

![Spindlestems](./df_caverns/screenshots/spindlestems.jpg)

Spindlestem mushrooms are the weedy "saplings" of the upper fungal forests, too large to pick by hand but not exactly inspiring tree-like analogies. They are common, though, and not without their uses. Their long stems can serve as a wood substutitue in many crafts and their bioluminescent caps can produce a long-lived glowing extract that's useful as a source of light. They grow taller when crowded.

The bioluminescence of Spindlestems are actually a very interesting feature. Despite their apparent visual diversity, Spindlestems are a single species. They pick up their glow from symbiotic microorganisms that colonize their caps, and the specific breed of symbiote that thrives in any given Spindlestem depends on the minerals present in the nearby rocks. Iron results in a weak red-glowing cap, copper results in a green-tinted cap, and the combination of both can result in a brighter cyan glow. Brilliant yellow "Golden Spindlestems" have also been spotted.

For some reason, Spindlestems that grow in the presence of Tower Caps are never luminescent. Perhaps they're intimidated.

# Second cavern layer

By default, the second cavern layer can be found between -800 and -1400 meters elevation. Like the first layer, some caverns contain lakes filling their lower regions. The dry barren caverns at this level will have some occasional ruby crystal growths in them, but they will be detailed in an entry on the third cavern layer where they're much more common.

## Goblin Cap caverns

![Goblin Caps](./df_caverns/screenshots/goblin_cap.jpg)

Goblin caps are the broader, squatter cousins of the Tower Cap fungus. Like Tower Caps they have no particular special properties, but their wood comes in two different colours - the reddish orange of their caps and the cream coloured stem wood. Plump helmets and Spindlestems grow here.

For some reason, Spindlestems that grow in the presence of Goblin Caps are always red.

## Spore Tree caverns

![Spore Trees](./df_caverns/screenshots/spore_tree.jpg)

Spore trees are the smallest of the underground fungal "tree" types, with a simple stem supporting a sponge-like three-dimensional mesh of hyphae. The mesh can be easily climbed, so navigating a spore tree forest is quite easy, and when harvested can be crafted into ladders quite quickly and easily. Fruiting bodies are embedded in the mesh that produce a copious amount of spores that gently rain down around the spore tree's base.

Cave wheat and pig tails can be found growing here.

## Tunnel Tube caverns

![Tunnel Tubes](./df_caverns/screenshots/tunnel_tube.jpg)

Tunnel tubes are hollow, curved fungal growths that support a fruiting body at their tip. The trunk of a tunnel tube can be cut and processed to produce plywood-like material. The fruiting body accumulates high-energy compounds that, when ignited, produce a vigorous detonation - a unique adaptation for spreading tunnel tube spawn through the still cavern air. If the TNT mod is enabled tunnel tube fruiting bodies can be detonated like weak TNT blocks and can be harvested for gunpowder.

Sweet pods and cave wheat can be found growing in tunnel tube caverns.

## Cave pearls

![Cave Pearls](./df_caverns/screenshots/cave_pearls.jpg)

At these depths you may find a distinctive and beautiful formation encrusting the ceilings and walls of smaller tunnels and warrens; "cave pearls." These nodules are actually calcified bacterial colonies that can only form where they're left undisturbed for the long periods needed for them to grow this large.

# Third cavern layer

The third cavern layer is where things start to get really exotic. Black Caps, Nethercaps, Bloodthorn and reddish glowing crystal formations are found here. Those caverns that are partly-flooded have unusual features thanks to the strange life sharing them. By default the third cavern layer runs from -1400 to -2000 meters.

## Black Cap caverns

![Black Caps, with two Torchspines in foreground](./df_caverns/screenshots/black_cap.jpg)

Black Cap caverns are dark, sooty, carbon-rich places. The Black Caps themselves are fairly mundane as underground flora goes, giant black mushrooms whose tarry gills can be used to craft torches and whose wood burns as strongly as coal blocks.

Also found in Black Cap caverns are torchspines, stalagmite-like growths that sometimes burn volatile gasses at their tips and produce a dim, irregular light. When water lakes are found in Black Cap caverns the water has an oily sheen on the surface, collecting into thick black slicks that cling to the shores.

## Nethercap caverns

![Nethercaps, with ice sprites hovering near icicles above](./df_caverns/screenshots/nethercap.jpg)

Nethercaps break the known laws of thermodynamics by somehow "feeding" directly on heat. As a result Nethercap caverns are frigid and icy, with icicles mixed with stalactites and giant columns of frozen water. Nethercap caverns have little other life in them as a result. They are lit by the blue glow of the Nethercap's gills, as well as by the mysterious "ice sprites" that hover and sparkle near the ceilings and by the streaks of hoar moss that grow on the surfaces of ice columns.

Any liquid water that touches Nethercap wood will freeze, even if the Nethercap has been harvested and crafted into other forms. Nethercap will not burn and indeed will snuff out fires and even solidify lava it touches. If a Nethercap cavern has a lake its surface will be covered in a solid sheet of ice.

Ice sprites, mysterious glowing insect-like creatures that appear to be made partly of crystalized water, flit and flicker under the icicles hanging from the ceilings of these caverns. Softly glowing hoar moss forms in sheets on columns of ice.

## Bloodthorn caverns

![Bloodthorns](./df_caverns/screenshots/bloodthorn.jpg)

Bloodthorns are cactus-like in appearance and habitat, growing in dry sandy caverns, but they are far more agressive about seeking moisture than their surface kin. Bloodthorns have thick spines projecting randomly from their trunks that are coated in smaller barbs that greedily drain fluids from any creature that touches them. You can climb a bloodthorn quite easily, using its spines like rungs on a ladder, but it will hurt to do so. Bloodthorn caverns are dimly lit so take care not to blunder into the rows of bloodthorns that grow like living fences.

Lakes are never found in Bloodthorn caverns and the cracks in the rock that would normally seep moisture are choked with crystals of salt. Some of the salts seem to containe traces of Mese, giving them a faint unearthly glow.

Quarry bushes can be found here.

## Barren crystal caverns

![Crystals](./df_caverns/screenshots/crystals.jpg)

On the third cavern layer the dry barren caverns will sprout clusters of enormous hexagonal red crystals from their floors and ceilings, providing light. These crystals have no particular use but are rare and beautiful to look at.

# Chasms

![A chasm](./df_caverns/screenshots/chasm.jpg)

Not all vast open spaces underground are the result of aeons of erosion by water or magma. The foundations of the world shift from time to time, causing deep faults to split open in the rock. Yawning chasms can be found oriented along the north-south axis, some of them stretching for kilometers both in length and depth. They cross through multiple cavern layers and are an environment in their own right. Chasms can be a convenient way of traveling long distances and also a convenient way of falling to your death.

The great extent of chasms makes them hospitable to small flying creatures, and their narrowness makes the hospitable to creatures that feed on them - giant cave spider webs can be found strung across them here and there. A dubious salvation for anyone falling from above.

# Pit caves

![Looking up from the bottom of a pit](./df_caverns/screenshots/pit_cave.jpg)

A pit cave is a type of cave which contains one or more significant vertical shafts rather than being predominantly a conventional horizontal cave passage. Pit caves typically form in limestone as a result of long-term erosion by water. Given the vast scope of the flowstone formations found throughout the upper layers of the world's caverns, correspondingly gigantic pits can also be found. They sometimes breach the surface of the world, and at their greatest extent can have a drop over two kilometers deep. Bring plenty of rope when exploring such abysses.

# Sunless Sea

![Sunless river](./df_caverns/screenshots/sunless_river.jpg)

The Sunless Sea is the final destination of all water that flows down through the cracks in the Earth. By default the caverns of the Sunless Sea begin at -2000 and go down to -2400 meters, with the lower sea level of the world being halfway between at -2200.

Compared to the previous layer the Sunless Sea is actually a much richer and more hospitable environment, and along the shores are found a variety of fungi from higher levels. One biome has a mixture of Tower Cap and Goblin Cap, and another biome mixes Spore Tree and Fungiwood. The smaller farmable plants and fungi from the various levels are also found here, with the exception of Torchspines. One unique fungus is found here; dimple cups grow among their larger Tower Cap and Goblin Cap cousins.

The distinguishing feature of the Sunless Sea is the vast expanse of water to be found here. The caverns themselves are broader in extent than those found higher up, heavily eroded by the steady flow of water, and a network of rivers spider-web the cracks between the larger caverns and allow for an intrepid sailor to travel great distances.

![Cave Coral in the foreground, Snareweed in background](./df_caverns/screenshots/cave_coral_snareweed.jpg)

Below the surface of the water Snareweed can be found, a dangerous kelp-like growth that uses bioluminescence and reflective patches to lure cave fish close enough to snag with their barbs. Swimming through Snareweed is a painful experience. Deeper down are craggy towers of softly glowing Cave Coral.

This is the last stop for the living world. Caverns continue to stretch below but nothing grows below.

# Lakes of Oil

![Mine gas seeping into a tunnel](./df_caverns/screenshots/mine_gas.jpg)

Venturing below the Sunless Sea will require either a great deal of digging through solid rock or a lengthy SCUBA swim through flooded tunnels. Getting past the water doesn't guarantee that you'll be able to breathe, however.

Eventually, at around -2600 meters elevation, an explorer may encounter lakes of oil in lenticular caverns. Falling into a lake of oil is an unpleasant experience - one is completely blind when below the surface - but that's not the real danger. The atmosphere above the oil is filled with unbreathable gasses that are explosive when mixed with air and an ignition source (such as a carelessly placed torch). Even in the tunnels above the oil there are occasional gas seeps venting this gas from cracks and seams of coal. This gas is heavier than air and will pool in depressions, and of course it cannot be swum through so take care not to jump down into a place you can't easily climb back out of.

A few mysterious blue flames may occasionally be found flickering on the edges of oil lakes, burning without oxygen and seeming to maintain their size as they drift about through the unbreathable atmosphere. These "gas wisps" may be a strange form of plasmoidal life but they are easily snuffed and so little is known about them.

Buckets of oil serve as an excellent fuel source, powering a furnace as well as a block of coal.

# Magma Sea

![Mese crystals growing above the magma sea](./df_caverns/screenshots/magma_sea_mese.jpg)

Below the lakes of oil, around -2800 meters elevation, is the Magma Sea. A dense network of cracks filled with liquid rock, the only way through is by digging a passage of your own through whatever remains solid.

The Magma Sea is not without its treasures, however. In the most infernal regions the heat and strange vapors have resulted in the formation of Mese crystals of unparalleled purity on the ceilings hanging over the lava. These regions are rare but if you're in need of Mese then you can strike a mother lode down here.

# Underworld

![There are older and fouler things than orcs in the deep places of the world](./df_caverns/screenshots/underworld.jpg)

The foundations of the world lie at -3100 meters. The diggable rock of the world ends at a rippling layer of invulnerable Slade, a material of unparalleled density. There is a cavern layer at the seam between rock and Slade, however, as if the earth itself was reluctant to touch the strange matter below. The crevices of the ceiling have strange glowing rocks in them, producing a surprisingly bright ambiance. Don't attempt to disturb those rocks.

There is no native life in the Underworld. It appears there once _was_, however. Occasional clusters of vacant buildings can be found, impossibly crafted from bricks of Slade and empty of any furnishings. Around those clusters of vacant buildings are fields filled with sealed pits that are lined with flawless Slade blocks. It is unclear whether these pits are as vacant as the buildings, though. The seals capping them are engraved with ancient words in lost tongues, but one phrase can be translated from the oldest known languages: "This Place is Not a Place of Honor."

Just as ominously, the ancient bones of long-dead warriors are scattered amidst the buildings. They contain loot for those willing to risk disturbing them. There are no traces of who - or what - they died fighting, but their ancient sentinels still lurk nearby to guard them.

![Glowing pit](./df_caverns/screenshots/glowing_pit.jpg)

Also scattered around the Underworld are the occasional twisting shaft leading down into the Slade. The edges of these shafts are encrusted with glowing purple crystals and the bottoms of these shafts are filled with some form of shimmering liquid that destroys whatever tries to penetrate it.

There may be other ways to get through the Slade, but the ancient warriors fought hard to seal them...

![Mysterious seal](./df_caverns/screenshots/mysterious_seal.jpg)

# Primordial Deeps

What mysteries lie below? Perhaps a realm that time itself has forgotten, with life from a world that no longer exists?

And why was it locked away?

![Primordial Fungal](./df_caverns/screenshots/primordial_fungal.jpg) ![Primordial Jungle](./df_caverns/screenshots/primordial_jungle.jpg)

repo_name: korkut/MeseCraft

license: mit


code:
local S = minetest.get_translator(minetest.get_current_modname())

hunter_statue = {}

-- override this to allow achievements to be recorded without requiring a dependency
hunter_statue.player_punched = function(node_name, pos, player) end

local statue_box = {
	type = "fixed",
	fixed = {
		{-0.5, -0.5, -0.5, 0.5, 95/64, 0.5},
	},
}

local fourtyfivedegrees = math.pi/4
local sixtydegrees = math.pi/3

local default_sounds
if minetest.get_modpath("default") then
	default_sounds = default.node_sound_stone_defaults()
end

local test_array = {
	{x=0,y=0,z=0},
	{x=0,y=-1,z=0},
	{x=0,y=1,z=0},
	{x=0,y=-2,z=0},
	{x=0,y=2,z=0},
}

--statue_def = {
--	description = name of statue type
--	tiles = {}
--	drop = ""
--	sounds =
--	groups = {}
--	interval = 1
--	chance = 1
--	damage = 8
--	knockback = 16
--	tnt_vulnerable = false
--	tnt_debris =
--	hunters_allowed_here = -- function(pos)
--	other_overrides =
--}

hunter_statue.register_hunter_statue = function(node_name, statue_def)

	local def = {
		description = statue_def.description or S("Hunter Statue"),
	--		_doc_items_longdesc = long_description,
	--		_doc_items_usagehelp = usage_help,
		drawtype = "mesh",
		mesh = "hunter_statue.obj",
		tiles = statue_def.tiles or {},
		paramtype2 = "facedir",
		drop = statue_def.drop or "",
		collision_box = statue_box,
		selection_box = statue_box,
		groups = statue_def.groups or {falling_node = 1},
		sounds = statue_def.sounds or default_sounds,
	}

	if statue_def.tnt_vulnerable then
		def.on_blast = function(pos, intensity)
			if intensity >= 1.0 then
				minetest.set_node(pos, {name= statue_def.tnt_debris or "air"})
				minetest.check_for_falling(pos)
			end
		end
	end

	if statue_def.other_overrides then
		for k, v in pairs(statue_def.other_overrides) do
			def[k] = v
		end
	end

	local knockback = statue_def.knockback or 16
	local damage = statue_def.damage or 8
	local hunters_allowed_here = statue_def.hunters_allowed_here

	minetest.register_node(node_name, def)

	minetest.register_abm({
		label = node_name .. " ABM",
		nodenames = {node_name},
		interval = statue_def.interval or 1.0,
		chance = statue_def.chance or 1,
		action = function(pos, node, active_object_count, active_object_count_wider)
			local players = minetest.get_connected_players()
			local nearest_distance = 50
			local nearest_player = nil
			local nearest_pos = nil
			for _, player in pairs(players) do
				local player_pos = player:get_pos()
				local player_distance = vector.distance(pos, player_pos)
				-- ignore far-away players
				if player_distance < 50 then
					local look_dir = player:get_look_dir()
					local statue_dir = vector.direction(player_pos, pos)
					local angle = vector.angle(look_dir, statue_dir)
					if angle < sixtydegrees then
						-- raycast test?
						-- a player is looking, do nothing
						return
					elseif player_distance < nearest_distance then
						nearest_distance = player_distance
						nearest_player = player
						nearest_pos = player_pos
					end
				end
			end

			if nearest_player then
				if nearest_distance < 2 then
					local armour_multiplier = 1
					local fleshy_armour = nearest_player:get_armor_groups().fleshy
					if fleshy_armour then
						armour_multiplier = fleshy_armour/100
					end
					nearest_player:add_velocity(vector.multiply(vector.direction(pos, nearest_pos), knockback))
					nearest_player:set_hp(math.max(nearest_player:get_hp() - damage*armour_multiplier, 0))
					minetest.sound_play({name="hunter_statue_thud"}, {pos = nearest_pos})
					hunter_statue.player_punched(node_name, pos, nearest_player)
					return
				end
				local player_dir = vector.direction(pos, nearest_pos)
				local new_facedir = minetest.dir_to_facedir(player_dir)
				local new_pos = vector.round(vector.add(pos, player_dir))
				for _, add_pos in ipairs(test_array) do
					local test_base = vector.add(new_pos, add_pos)
					if hunters_allowed_here == nil or hunters_allowed_here(test_base) then
						local test_base_node = minetest.get_node(test_base)
						local test_base_node_def = minetest.registered_nodes[test_base_node.name]
						if test_base_node_def and test_base_node_def.buildable_to then
							local test_above = vector.add(test_base, {x=0, y=1, z=0})
							local test_above_node = minetest.get_node(test_above)
							local test_above_node_def = minetest.registered_nodes[test_above_node.name]
							if test_above_node_def and test_above_node_def.buildable_to then
								local test_below = vector.add(test_base, {x=0, y=-1, z=0})
								local test_below_node = minetest.get_node(test_below)
								local test_below_node_def = minetest.registered_nodes[test_below_node.name]
								if test_below_node_def and test_below_node_def.walkable then
									minetest.set_node(pos, {name="air"}) -- old location
									minetest.set_node(test_above, {name="air"}) -- some kind of filler node?
									node.param2 = new_facedir
									minetest.set_node(test_base, node)
									minetest.check_for_falling({x=pos.x, y=pos.y+1, z=pos.z})
									minetest.sound_play({name="hunter_statue_brick_step"}, {pos = pos, gain = 0.5})
									return
								end
							end
						end
					end
				end
				if node.param2 ~= new_facedir then
					node.param2 = new_facedir
					minetest.set_node(pos, node)
					minetest.sound_play({name="hunter_statue_turn_grind"}, {pos = pos, gain = 0.5})
				end
			end
		end,
	})
end


code:
# textdomain: hunter_statue

### init.lua ###

Hunter Statue=


code:
name=hunter_statue
optional_depends = default


code:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Script to generate the template file and update the translation files.
# Copy the script into the mod or modpack root folder and run it there.
#
# Copyright (C) 2019 Joachim Stolberg, 2020 FaceDeer, 2020 Louis Royer
# LGPLv2.1+
#
# See https://github.com/minetest-tools/update_translations for
# potential future updates to this script.

from __future__ import print_function
import os, fnmatch, re, shutil, errno
from sys import argv as _argv

# Running params
params = {"recursive": False,
    "help": False,
    "mods": False,
    "verbose": False,
    "folders": []
}
# Available CLI options
options = {"recursive": ['--recursive', '-r'],
    "help": ['--help', '-h'],
    "mods": ['--installed-mods'],
    "verbose": ['--verbose', '-v']
}

# Strings longer than this will have extra space added between
# them in the translation files to make it easier to distinguish their
# beginnings and endings at a glance
doublespace_threshold = 60

def set_params_folders(tab: list):
    '''Initialize params["folders"] from CLI arguments.'''
    # Discarding argument 0 (tool name)
    for param in tab[1:]:
        stop_param = False
        for option in options:
            if param in options[option]:
                stop_param = True
                break
        if not stop_param:
            params["folders"].append(os.path.abspath(param))

def set_params(tab: list):
    '''Initialize params from CLI arguments.'''
    for option in options:
        for option_name in options[option]:
            if option_name in tab:
                params[option] = True
                break

def print_help(name):
    '''Prints some help message.'''
    print(f'''SYNOPSIS
    {name} [OPTIONS] [PATHS...]
DESCRIPTION
    {', '.join(options["help"])}
        prints this help message
    {', '.join(options["recursive"])}
        run on all subfolders of paths given
    {', '.join(options["mods"])}
        run on locally installed modules
    {', '.join(options["verbose"])}
        add output information
''')

def main():
    '''Main function'''
    set_params(_argv)
    set_params_folders(_argv)
    if params["help"]:
        print_help(_argv[0])
    elif params["recursive"] and params["mods"]:
        print("Option --installed-mods is incompatible with --recursive")
    else:
        # Add recursivity message
        print("Running ", end='')
        if params["recursive"]:
            print("recursively ", end='')
        # Running
        if params["mods"]:
            print(f"on all locally installed modules in {os.path.abspath('~/.minetest/mods/')}")
            run_all_subfolders("~/.minetest/mods")
        elif len(params["folders"]) >= 2:
            print("on folder list:", params["folders"])
            for f in params["folders"]:
                if params["recursive"]:
                    run_all_subfolders(f)
                else:
                    update_folder(f)
        elif len(params["folders"]) == 1:
            print("on folder", params["folders"][0])
            if params["recursive"]:
                run_all_subfolders(params["folders"][0])
            else:
                update_folder(params["folders"][0])
        else:
            print("on folder", os.path.abspath("./"))
            if params["recursive"]:
                run_all_subfolders(os.path.abspath("./"))
            else:
                update_folder(os.path.abspath("./"))

#group 2 will be the string, groups 1 and 3 will be the delimiters (" or ')
#See https://stackoverflow.com/questions/46967465/regex-match-text-in-either-single-or-double-quote
pattern_lua = re.compile(r'[\.=^\t,{\(\s]N?S\(\s*(["\'])((?:\\\1|(?:(?!\1)).)*)(\1)[\s,\)]', re.DOTALL)
pattern_lua_bracketed = re.compile(r'[\.=^\t,{\(\s]N?S\(\s*\[\[(.*?)\]\][\s,\)]', re.DOTALL)

# Handles "concatenation" .. " of strings"
pattern_concat = re.compile(r'["\'][\s]*\.\.[\s]*["\']', re.DOTALL)

pattern_tr = re.compile(r'(.+?[^@])=(.*)')
pattern_name = re.compile(r'^name[ ]*=[ ]*([^ \n]*)')
pattern_tr_filename = re.compile(r'\.tr$')
pattern_po_language_code = re.compile(r'(.*)\.po$')

#attempt to read the mod's name from the mod.conf file. Returns None on failure
def get_modname(folder):
    try:
        with open(os.path.join(folder, "mod.conf"), "r", encoding='utf-8') as mod_conf:
            for line in mod_conf:
                match = pattern_name.match(line)
                if match:
                    return match.group(1)
    except FileNotFoundError:
        pass
    return None

#If there are already .tr files in /locale, returns a list of their names
def get_existing_tr_files(folder):
    out = []
    for root, dirs, files in os.walk(os.path.join(folder, 'locale/')):
        for name in files:
            if pattern_tr_filename.search(name):
                out.append(name)
    return out

# A series of search and replaces that massage a .po file's contents into
# a .tr file's equivalent
def process_po_file(text):
    # The first three items are for unused matches
    text = re.sub(r'#~ msgid "', "", text)
    text = re.sub(r'"\n#~ msgstr ""\n"', "=", text)
    text = re.sub(r'"\n#~ msgstr "', "=", text)
    # comment lines
    text = re.sub(r'#.*\n', "", text)
    # converting msg pairs into "=" pairs
    text = re.sub(r'msgid "', "", text)
    text = re.sub(r'"\nmsgstr ""\n"', "=", text)
    text = re.sub(r'"\nmsgstr "', "=", text)
    # various line breaks and escape codes
    text = re.sub(r'"\n"', "", text)
    text = re.sub(r'"\n', "\n", text)
    text = re.sub(r'\\"', '"', text)
    text = re.sub(r'\\n', '@n', text)
    # remove header text
    text = re.sub(r'=Project-Id-Version:.*\n', "", text)
    # remove double-spaced lines
    text = re.sub(r'\n\n', '\n', text)
    return text

# Go through existing .po files and, if a .tr file for that language
# *doesn't* exist, convert it and create it.
# The .tr file that results will subsequently be reprocessed so
# any "no longer used" strings will be preserved.
# Note that "fuzzy" tags will be lost in this process.
def process_po_files(folder, modname):
    for root, dirs, files in os.walk(os.path.join(folder, 'locale/')):
        for name in files:
            code_match = pattern_po_language_code.match(name)
            if code_match == None:
                continue
            language_code = code_match.group(1)
            tr_name = modname + "." + language_code + ".tr"
            tr_file = os.path.join(root, tr_name)
            if os.path.exists(tr_file):
                if params["verbose"]:
                    print(f"{tr_name} already exists, ignoring {name}")
                continue
            fname = os.path.join(root, name)
            with open(fname, "r", encoding='utf-8') as po_file:
                if params["verbose"]:
                    print(f"Importing translations from {name}")
                text = process_po_file(po_file.read())
                with open(tr_file, "wt", encoding='utf-8') as tr_out:
                    tr_out.write(text)

# from https://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python/600612#600612
# Creates a directory if it doesn't exist, silently does
# nothing if it already exists
def mkdir_p(path):
    try:
        os.makedirs(path)
    except OSError as exc: # Python >2.5
        if exc.errno == errno.EEXIST and os.path.isdir(path):
            pass
        else: raise

# Converts the template dictionary to a text to be written as a file
# dKeyStrings is a dictionary of localized string to source file sets
# dOld is a dictionary of existing translations and comments from
# the previous version of this text
def strings_to_text(dkeyStrings, dOld, mod_name):
    lOut = [f"# textdomain: {mod_name}\n"]

    dGroupedBySource = {}

    for key in dkeyStrings:
        sourceList = list(dkeyStrings[key])
        sourceList.sort()
        sourceString = "\n".join(sourceList)
        listForSource = dGroupedBySource.get(sourceString, [])
        listForSource.append(key)
        dGroupedBySource[sourceString] = listForSource

    lSourceKeys = list(dGroupedBySource.keys())
    lSourceKeys.sort()
    for source in lSourceKeys:
        localizedStrings = dGroupedBySource[source]
        localizedStrings.sort()
        lOut.append("")
        lOut.append(source)
        lOut.append("")
        for localizedString in localizedStrings:
            val = dOld.get(localizedString, {})
            translation = val.get("translation", "")
            comment = val.get("comment")
            if len(localizedString) > doublespace_threshold and not lOut[-1] == "":
                lOut.append("")
            if comment != None:
                lOut.append(comment)
            lOut.append(f"{localizedString}={translation}")
            if len(localizedString) > doublespace_threshold:
                lOut.append("")

    unusedExist = False
    for key in dOld:
        if key not in dkeyStrings:
            val = dOld[key]
            translation = val.get("translation")
            comment = val.get("comment")
            # only keep an unused translation if there was translated
            # text or a comment associated with it
            if translation != None and (translation != "" or comment):
                if not unusedExist:
                    unusedExist = True
                    lOut.append("\n\n##### not used anymore #####\n")
                if len(key) > doublespace_threshold and not lOut[-1] == "":
                    lOut.append("")
                if comment != None:
                    lOut.append(comment)
                lOut.append(f"{key}={translation}")
                if len(key) > doublespace_threshold:
                    lOut.append("")
    return "\n".join(lOut) + '\n'

# Writes a template.txt file
# dkeyStrings is the dictionary returned by generate_template
def write_template(templ_file, dkeyStrings, mod_name):
    # read existing template file to preserve comments
    existing_template = import_tr_file(templ_file)

    text = strings_to_text(dkeyStrings, existing_template[0], mod_name)
    mkdir_p(os.path.dirname(templ_file))
    with open(templ_file, "wt", encoding='utf-8') as template_file:
        template_file.write(text)

# Gets all translatable strings from a lua file
def read_lua_file_strings(lua_file):
    lOut = []
    with open(lua_file, encoding='utf-8') as text_file:
        text = text_file.read()
        #TODO remove comments here

        text = re.sub(pattern_concat, "", text)

        strings = []
        for s in pattern_lua.findall(text):
            strings.append(s[1])
        for s in pattern_lua_bracketed.findall(text):
            strings.append(s)

        for s in strings:
            s = re.sub(r'"\.\.\s+"', "", s)
            s = re.sub("@[^@=0-9]", "@@", s)
            s = s.replace('\\"', '"')
            s = s.replace("\\'", "'")
            s = s.replace("\n", "@n")
            s = s.replace("\\n", "@n")
            s = s.replace("=", "@=")
            lOut.append(s)
    return lOut

# Gets strings from an existing translation file
# returns both a dictionary of translations
# and the full original source text so that the new text
# can be compared to it for changes.
def import_tr_file(tr_file):
    dOut = {}
    text = None
    if os.path.exists(tr_file):
        with open(tr_file, "r", encoding='utf-8') as existing_file :
            # save the full text to allow for comparison
            # of the old version with the new output
            text = existing_file.read()
            existing_file.seek(0)
            # a running record of the current comment block
            # we're inside, to allow preceeding multi-line comments
            # to be retained for a translation line
            latest_comment_block = None
            for line in existing_file.readlines():
                line = line.rstrip('\n')
                if line[:3] == "###":
                    # Reset comment block if we hit a header
                    latest_comment_block = None
                    continue
                if line[:1] == "#":
                    # Save the comment we're inside
                    if not latest_comment_block:
                        latest_comment_block = line
                    else:
                        latest_comment_block = latest_comment_block + "\n" + line
                    continue
                match = pattern_tr.match(line)
                if match:
                    # this line is a translated line
                    outval = {}
                    outval["translation"] = match.group(2)
                    if latest_comment_block:
                        # if there was a comment, record that.
                        outval["comment"] = latest_comment_block
                    latest_comment_block = None
                    dOut[match.group(1)] = outval
    return (dOut, text)

# Walks all lua files in the mod folder, collects translatable strings,
# and writes it to a template.txt file
# Returns a dictionary of localized strings to source file sets
# that can be used with the strings_to_text function.
def generate_template(folder, mod_name):
    dOut = {}
    for root, dirs, files in os.walk(folder):
        for name in files:
            if fnmatch.fnmatch(name, "*.lua"):
                fname = os.path.join(root, name)
                found = read_lua_file_strings(fname)
                if params["verbose"]:
                    print(f"{fname}: {str(len(found))} translatable strings")

                for s in found:
                    sources = dOut.get(s, set())
                    sources.add(f"### {os.path.basename(fname)} ###")
                    dOut[s] = sources

    if len(dOut) == 0:
        return None
    templ_file = os.path.join(folder, "locale/template.txt")
    write_template(templ_file, dOut, mod_name)
    return dOut

# Updates an existing .tr file, copying the old one to a ".old" file
# if any changes have happened
# dNew is the data used to generate the template, it has all the
# currently-existing localized strings
def update_tr_file(dNew, mod_name, tr_file):
    if params["verbose"]:
        print(f"updating {tr_file}")

    tr_import = import_tr_file(tr_file)
    dOld = tr_import[0]
    textOld = tr_import[1]

    textNew = strings_to_text(dNew, dOld, mod_name)

    if textOld and textOld != textNew:
        print(f"{tr_file} has changed.")
        shutil.copyfile(tr_file, f"{tr_file}.old")

    with open(tr_file, "w", encoding='utf-8') as new_tr_file:
        new_tr_file.write(textNew)

# Updates translation files for the mod in the given folder
def update_mod(folder):
    modname = get_modname(folder)
    if modname is not None:
        process_po_files(folder, modname)
        print(f"Updating translations for {modname}")
        data = generate_template(folder, modname)
        if data == None:
            print(f"No translatable strings found in {modname}")
        else:
            for tr_file in get_existing_tr_files(folder):
                update_tr_file(data, modname, os.path.join(folder, "locale/", tr_file))
    else:
        print("Unable to find modname in folder " + folder)

# Determines if the folder being pointed to is a mod or a mod pack
# and then runs update_mod accordingly
def update_folder(folder):
    is_modpack = os.path.exists(os.path.join(folder, "modpack.txt")) or os.path.exists(os.path.join(folder, "modpack.conf"))
    if is_modpack:
        subfolders = [f.path for f in os.scandir(folder) if f.is_dir()]
        for subfolder in subfolders:
            update_mod(subfolder + "/")
    else:
        update_mod(folder)
    print("Done.")

def run_all_subfolders(folder):
    for modfolder in [f.path for f in os.scandir(folder) if f.is_dir()]:
        update_folder(modfolder + "/")

main()


code:
Minetest Game mod: ice_sprites
============================
Adds ice sprites, which can then be caught in a net (via the fireflies mod) and placed in bottles to provide light (via the vessels mod).

Authors of source code
----------------------
Shara RedCat (MIT)

Authors of media (textures)
---------------------------
Shara RedCat (CC BY-SA 3.0):
  fireflies_firefly.png
  fireflies_firefly_animated.png
  fireflies_bottle.png
  fireflies_bottle_animated.png

fireflies_bottle.png and fireflies_bottle_animated.png are
modified from a texture by Vanessa Ezekowitz (CC BY-SA 3.0)

converted to ice_sprite textures by FaceDeer, who simply changed the yellow dots to blue.


code:
ice_sprites = {}

local modname = minetest.get_current_modname()
local S = minetest.get_translator(modname)

local ice_sprite_desc
local ice_sprite_usage
local ice_sprite_bottle_desc
local ice_sprite_bottle_usage

local node_name_glass_bottle = df_dependencies.node_name_glass_bottle
local node_name_firefly = df_dependencies.node_name_fireflies

if minetest.get_modpath("doc") then
	ice_sprite_desc = S("Ice sprites are mysterious glowing insect-like creatures that appear to be made partly of crystallized water.")
	if node_name_glass_bottle then
		if node_name_firefly then
			ice_sprite_usage = S("Ice sprites can be caught with nets and placed in bottles as sources of light and freezing cold.")
		end
		ice_sprite_bottle_desc = S("A bottle containing a captured ice sprite.")
		ice_sprite_bottle_usage = S("Ice sprites radiate both light and freezing cold.")
	end
end

minetest.register_node("ice_sprites:ice_sprite", {
	description = S("Ice Sprite"),
	_doc_items_longdesc = ice_sprite_desc,
	_doc_items_usagehelp = ice_sprite_usage,
	drawtype = "plantlike",
	tiles = {{
		name = "ice_sprites_ice_sprite_animated.png",
		animation = {
			type = "vertical_frames",
			aspect_w = 16,
			aspect_h = 16,
			length = 1.5
		},
	}},
	inventory_image = "ice_sprites_ice_sprite.png",
	wield_image = "ice_sprites_ice_sprite.png",
	waving = 1,
	paramtype = "light",
	sunlight_propagates = true,
	buildable_to = true,
	is_ground_content = false,
	walkable = false,
	groups = {catchable = 1, puts_out_fire = 1, cools_lava = 1, freezes_water = 1},
	selection_box = {
		type = "fixed",
		fixed = {-0.1, -0.1, -0.1, 0.1, 0.1, 0.1},
	},
	light_source = 6,
	floodable = true,
	_mcl_blast_resistance = 0.1,
	_mcl_hardness = 0.1,
	on_place = function(itemstack, placer, pointed_thing)
		local player_name = placer:get_player_name()
		local pos = pointed_thing.above

		if not minetest.is_protected(pos, player_name) and
				not minetest.is_protected(pointed_thing.under, player_name) and
				minetest.get_node(pos).name == "air" then
			minetest.set_node(pos, {name = "ice_sprites:ice_sprite"})
			minetest.get_node_timer(pos):start(1)
			itemstack:take_item()
		end
		return itemstack
	end,
	on_timer = function(pos, elapsed)
		if minetest.get_node_light(pos) > 11 or math.random() > 0.5 then
			minetest.set_node(pos, {name = "ice_sprites:hidden_ice_sprite"})
		end
		minetest.get_node_timer(pos):start(math.random(20,40))
	end
})

minetest.register_node("ice_sprites:hidden_ice_sprite", {
	description = S("Hidden Ice Sprite"),
	drawtype = "airlike",
	inventory_image = "ice_sprites_ice_sprite.png",
	wield_image =  "ice_sprites_ice_sprite.png",
	paramtype = "light",
	sunlight_propagates = true,
	walkable = false,
	is_ground_content = false,
	pointable = false,
	diggable = false,
	buildable_to = true,
	drop = "ice_sprites:ice_sprite",
	groups = {not_in_creative_inventory = 1},
	floodable = true,
	_mcl_blast_resistance = 0.1,
	_mcl_hardness = 0.1,
	on_place = function(itemstack, placer, pointed_thing)
		local player_name = placer:get_player_name()
		local pos = pointed_thing.above

		if not minetest.is_protected(pos, player_name) and
				not minetest.is_protected(pointed_thing.under, player_name) and
				minetest.get_node(pos).name == "air" then
			minetest.set_node(pos, {name = "ice_sprites:hidden_ice_sprite"})
			minetest.get_node_timer(pos):start(1)
			itemstack:take_item()
		end
		return itemstack
	end,
	on_timer = function(pos, elapsed)
		if minetest.get_node_light(pos) <= 11 then
			minetest.set_node(pos, {name = "ice_sprites:ice_sprite"})
		end
		minetest.get_node_timer(pos):start(math.random(20,40))
	end
})

-- ice sprite in a bottle
if node_name_glass_bottle then

local glass_sounds
if minetest.get_modpath("df_dependencies") then
	glass_sounds = df_dependencies.sound_glass
end

minetest.register_node("ice_sprites:ice_sprite_bottle", {
	description = S("Ice Sprite in a Bottle"),
	_doc_items_longdesc = ice_sprite_bottle_desc,
	_doc_items_usagehelp = ice_sprite_bottle_usage,
	inventory_image = "ice_sprites_bottle.png",
	wield_image = "ice_sprites_bottle.png",
	tiles = {{
		name = "ice_sprites_bottle_animated.png",
		animation = {
			type = "vertical_frames",
			aspect_w = 16,
			aspect_h = 16,
			length = 1.5
		},
	}},
	drawtype = "plantlike",
	paramtype = "light",
	sunlight_propagates = true,
	light_source = 9,
	is_ground_content = false,
	walkable = false,
	groups = {dig_immediate = 3, attached_node = 1, puts_out_fire = 1, cools_lava = 1, freezes_water = 1, material_glass=1},
	selection_box = {
		type = "fixed",
		fixed = {-0.25, -0.5, -0.25, 0.25, 0.3, 0.25}
	},
	_mcl_blast_resistance = 0.5,
	_mcl_hardness = 0.5,
	sounds = glass_sounds(),
	on_rightclick = function(pos, node, player, itemstack, pointed_thing)
		local lower_pos = {x = pos.x, y = pos.y + 1, z = pos.z}
		if minetest.is_protected(pos, player:get_player_name()) or
				minetest.get_node(lower_pos).name ~= "air" then
			return
		end

		local upper_pos = {x = pos.x, y = pos.y + 2, z = pos.z}
		local ice_sprite_pos

		if not minetest.is_protected(upper_pos, player:get_player_name()) and
				minetest.get_node(upper_pos).name == "air" then
			ice_sprite_pos = upper_pos
		elseif not minetest.is_protected(lower_pos, player:get_player_name()) then
			ice_sprite_pos = lower_pos
		end

		if ice_sprite_pos then
			minetest.set_node(pos, {name = node_name_glass_bottle})
			minetest.set_node(ice_sprite_pos, {name = "ice_sprites:ice_sprite"})
			minetest.get_node_timer(ice_sprite_pos):start(1)
		end
	end
})

minetest.register_craft( {
	type = "shapeless",
	output = "ice_sprites:ice_sprite_bottle",
	recipe = {"ice_sprites:ice_sprite", node_name_glass_bottle}
})
end


code:
# textdomain: ice_sprites

### init.lua ###

#WARNING: AUTOTRANSLATED BY GOOGLE TRANSLATE
A bottle containing a captured ice sprite.=Eine Flasche mit einem erbeuteten Eisgeist.
#WARNING: AUTOTRANSLATED BY GOOGLE TRANSLATE
Hidden Ice Sprite=Verstecktes Eis-Sprite
Ice Sprite=
#WARNING: AUTOTRANSLATED BY GOOGLE TRANSLATE
Ice Sprite in a Bottle=Ice Sprite in einer Flasche

#WARNING: AUTOTRANSLATED BY GOOGLE TRANSLATE
Ice sprites are mysterious glowing insect-like creatures that appear to be made partly of crystallized water.=Eisgeister sind mysteriös leuchtende insektenähnliche Wesen, die teilweise aus kristallisiertem Wasser zu bestehen scheinen.

#WARNING: AUTOTRANSLATED BY GOOGLE TRANSLATE
Ice sprites can be caught with nets and placed in bottles as sources of light and freezing cold.=Eisgeister können mit Netzen aufgefangen und als Lichtquelle und eiskalte in Flaschen abgefüllt werden.

#WARNING: AUTOTRANSLATED BY GOOGLE TRANSLATE
Ice sprites radiate both light and freezing cold.=Eisgeister strahlen sowohl Licht als auch eiskalt aus.


code:
# textdomain: ice_sprites

### init.lua ###

#WARNING: AUTOTRANSLATED BY GOOGLE TRANSLATE
A bottle containing a captured ice sprite.=Una bottiglia contenente uno sprite di ghiaccio catturato.
#WARNING: AUTOTRANSLATED BY GOOGLE TRANSLATE
Hidden Ice Sprite=Sprite di ghiaccio nascosto
Ice Sprite=
#WARNING: AUTOTRANSLATED BY GOOGLE TRANSLATE
Ice Sprite in a Bottle=Ice Sprite in una bottiglia

#WARNING: AUTOTRANSLATED BY GOOGLE TRANSLATE
Ice sprites are mysterious glowing insect-like creatures that appear to be made partly of crystallized water.=Gli sprite del ghiaccio sono misteriose creature luminose simili a insetti che sembrano essere costituite in parte da acqua cristallizzata.

#WARNING: AUTOTRANSLATED BY GOOGLE TRANSLATE
Ice sprites can be caught with nets and placed in bottles as sources of light and freezing cold.=Gli sprite del ghiaccio possono essere catturati con reti e posti in bottiglie come fonti di luce e freddo gelido.

#WARNING: AUTOTRANSLATED BY GOOGLE TRANSLATE
Ice sprites radiate both light and freezing cold.=Gli sprite del ghiaccio irradiano luce e freddo gelido.


code:
# textdomain: ice_sprites

### init.lua ###

A bottle containing a captured ice sprite.=
Hidden Ice Sprite=
Ice Sprite=
Ice Sprite in a Bottle=

Ice sprites are mysterious glowing insect-like creatures that appear to be made partly of crystallized water.=

Ice sprites can be caught with nets and placed in bottles as sources of light and freezing cold.=

Ice sprites radiate both light and freezing cold.=


code:
name = ice_sprites
description = Glowing blue firefly-like nodes
depends =
optional_depends = df_dependencies, doc


code:
looped_node_sound = {}

--looped_node_sound.register({
--	node_list = {},
--	sound = <SimpleSoundSpec>,
--	radius = ,
--	cycle_time =,
--	gain_per_node =,
--	max_gain =,
--	max_hear_distance =,
--})

looped_node_sound.register = function(def)
	local handles = {}
	local timer = 0

	-- Parameters
	local radius = def.radius or 8 -- node search radius around player
	local cycle = def.cycle_time or 3 -- Cycle time for sound updates
	assert(def.node_list, "[looped_sound_node] register function called without a node_list in its definition")
	assert(def.sound, "[looped_sound_node] register function called without a sound in its definition")
	local node_list = def.node_list
	local sound = def.sound
	local gain_per_node = def.gain_per_node or 0.125
	local gain_max = def.max_gain or 1.0
	local max_hear_distance = def.max_hear_distance or 32

	-- Update sound for player
	local function update_player_sound(player)
		local player_name = player:get_player_name()
		-- Search for nodes in radius around player
		local ppos = player:get_pos()
		local areamin = vector.subtract(ppos, radius)
		local areamax = vector.add(ppos, radius)
		local fpos, num = minetest.find_nodes_in_area(
			areamin,
			areamax,
			node_list
		)
		-- Total number of nodes in radius
		local total = 0
		for _, count in pairs(num) do
			total = total + count
		end
		-- Stop previous sound
		if handles[player_name] then
			minetest.sound_stop(handles[player_name])
		end
		-- If nodes
		if total > 0 then
			-- Find centre of node positions
			local fposmid = fpos[1]
			-- If more than 1 node
			if #fpos > 1 then
				local fposmin = areamax
				local fposmax = areamin
				for i = 1, #fpos do
					local fposi = fpos[i]
					if fposi.x > fposmax.x then
						fposmax.x = fposi.x
					end
					if fposi.y > fposmax.y then
						fposmax.y = fposi.y
					end
					if fposi.z > fposmax.z then
						fposmax.z = fposi.z
					end
					if fposi.x < fposmin.x then
						fposmin.x = fposi.x
					end
					if fposi.y < fposmin.y then
						fposmin.y = fposi.y
					end
					if fposi.z < fposmin.z then
						fposmin.z = fposi.z
					end
				end
				fposmid = vector.divide(vector.add(fposmin, fposmax), 2)
			end
			-- Play sound
			local handle = minetest.sound_play(sound, {
				pos = fposmid,
				to_player = player_name,
				gain = math.min(total * gain_per_node, gain_max),
				max_hear_distance = max_hear_distance,
				loop = true -- In case of lag
			})
			-- Store sound handle for this player
			if handle then
				handles[player_name] = handle
			else
				handles[player_name] = nil
			end
		else
			handles[player_name] = nil
		end
	end

	-- Cycle for updating players sounds
	minetest.register_globalstep(function(dtime)
		timer = timer + dtime
		if timer < cycle then
			return
		end

		timer = 0
		local players = minetest.get_connected_players()
		for n = 1, #players do
			update_player_sound(players[n])
		end
	end)

	-- Stop sound and clear handle on player leave
	minetest.register_on_leaveplayer(function(player)
		local player_name = player:get_player_name()
		if handles[player_name] then
			minetest.sound_stop(handles[player_name])
			handles[player_name] = nil
		end
	end)
end


code:
name = looped_node_sound
description = A simple API for causing nodes to appear to be emitting a looped sound
depends =
optional_depends =


code:
# Auto detect text files and perform LF normalization
* text=auto


code:
# Compiled Lua sources
luac.out

# luarocks build files
*.src.rock
*.zip
*.tar.gz

# Object files
*.o
*.os
*.ko
*.obj
*.elf

# Precompiled Headers
*.gch
*.pch

# Libraries
*.lib
*.a
*.la
*.lo
*.def
*.exp

# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib

# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex


code:
Mapgen Helper
=============

This mod provides a library of methods intended for use with other Minetest mapgen mods, automating routine tasks and giving tools to more easily perform mapgen operations. It's a bit of a random grab-bag, I put methods in here when I find that I use them frequently in other mods or when I just did a bunch of work solving a specific problem that could potentially be reused in future mods.

Part of the purpose of this mod is to make sure these methods are as efficient as possible, allowing them to be used in mapgens or ABMs or other such contexts where speed is of the essence or where the code will be executed many millions of times. Several of these methods may seem trivial to accomplish already by using a combination of existing methods (for example `VoxelArea:get_y(vi)` can already be accomplished with `VoxelArea:position(i).y`) but in this mod they've been written to eliminate unnecessary intermediate steps and unused calculations.

## Voxel Manipulator methods

*  `mapgen_helper.mapgen_vm_data()`
	* returns vm, data, area
	* Almost every mapgen mod does this operation first. This method retrieves the Voxel Manipulator for the generated map block, the voxel data array (automatically using a buffer internal to mapgen_helper to store it), and a VoxelArea for indexing the data array.
	* Note that you still need to write the data array to the voxel manipulator at the end of map generation.

* `mapgen_helper.mapgen_vm_data_param2()`
	* returns vm, data, param2_data, area
	* Almost identical to the previous method, this one also returns the param2 data array.

# VoxelArea methods

These methods are added to the existing VoxelArea LUA class.

* `VoxelArea:iterp_xyz(minp, maxp)`
	* Returns an iterator that gives not just the voxel index (vi), but also the x, y and z coordinates it represents. This lets you perform actions in your mapgen loop based on x, y and z coordinates without having to look them up from the voxel index. For example:
		`for vi, x, y, z in area:iterp_xyz(minp, maxp) do`

* `VoxelArea:iter_xyz(minx, miny, minz, maxx, maxy, maxz)`
	* The same as above, but takes individual coordinate arguments instead of points.

* `VoxelArea:iterp_yxz(minp, maxp)`
	* Similar to the above, but the order of iteration is changed. Using this iterator, indices are visited in the +y direction along "columns". This is handy for mapgens that want to build stuff based on the nodes that are "below" the current node, for example if you're scanning through the map block looking for a walkable surface to place something on.

* `VoxelArea:iter_yxz(minx, miny, minz, maxx, maxy, maxz)`
	* Same as above, but takes individual coordinate arguments instead of points.

* `VoxelArea:get_y(vi)`
	* Returns the absolute y coordinate of the index provided. Useful for mapgens that want to do something at a specific elevation.

* `VoxelArea:transform(area, vi)`
	* Takes a VoxelArea and an index into that VoxelArea as parameters returns an index into its own VoxelArea, or nil if it's not in the voxelarea. This is useful when you've got two 3d data arrays that cover different volumes and would like to "map" values from one into the other. For example, if you've generated a 3d noise array using the mapgen's minp and maxp and want to map it onto the voxelmanipulator's emin and emax data array.

## Biomes

* `mapgen_helper.get_biome_def(biome_id)`
	* Given an id from the biome map, returns a biome definition. Normally translating a biome_id into a biome definition requires several steps and lookups, this method caches the intermediate results internal to mapgen_helper to speed up the operation.

* `mapgen_helper.get_biome_def_i(biomemap, minp, maxp, area, vi)`
	* When iterating through a voxelmanip's data array it is common to want to check what biome a given index belongs to. This method automates that lookup. Give it the biomemap that was provided by get_mapgen_object("biomemap"), the minp and maxp parameters from register_on_generated, the VoxelArea representing the generated map chunk, and the voxel index, and this method returns the biome_def that that voxel index lies inside.

## Node definitions

* `mapgen_helper.buildable_to(c_node)`
	*Given a node id, returns whether that node's definition is "buildable_to"

* `mapgen_helper.is_ground_content(c_node)`
	* Given a node id, returns whether that node's definition is "ground_content"

## Noise manager

* `mapgen_helper.perlin3d(name, minp, maxp, perlin_params)`
	* Returns nvals_perlin, area
	* Efficient generation and use of 3d perlin noise requires a bunch of steps that this method automates. It uses the "name" parameter to allocate buffers and generator objects in a table stored internally by mapgen_helper, if you're using multiple different noise fields in the same scope make sure to give them different names.
	* The perlin_params argument is actually optional if you've already made a call to this method (or used the 'mapgen_helper.register_perlin' method below), the perlin parameters are stored when provided and can be looked back up again by mapgen_helper.
	* The second return value is a VoxelArea for minp, maxp.

* `mapgen_helper.perlin2d(name, minp, maxp, perlin_params)`
	* Similar to the above method but for generating 2d noise. This method only returns the data array, it does not return a VoxelArea.

* `mapgen_helper.register_perlin(name, perlin_params)`
	* Pre-registering a perlin_params table is optional, any call to one of the above perlin methods with perlin_params provided will override the parameters registered here.

## 2D array indexing

* `mapgen_helper.index2d(minp, maxp, x, z)`
	* Returns an index for getting values out of 2d arrays (such as heightmap, biomemap, heatmap, arrays generated by perlin2d, etc) given x and z coordinates.
	* minp and maxp must be provided, these are the extents that were used when generating the 2d array.

* `mapgen_helper.index2dp(minp, maxp, pos)`
	* Version of the above that takes a pos instead of separate coordinates. pos.y is ignored.

* `mapgen_helper.index2di(minp, maxp, area, vi)`
	* Version of the above that indexes a 2d array based on the index in a 3d array.

## Schematics

*NOTE:* These methods operate on the LUA table format of schematic. To convert a .mts file into its lua representation, use `minetest.deserialize(minetest.serialize_schematic(schematic, "lua"))` to convert an .mts into a lua table.

* `mapgen_helper.get_schematic_bounding_box(pos, schematic, rotation, flags)`
	* Returns the minpos and maxpos of the bounding box that this schematic will be placed in given the rotation and flag parameters. Useful for testing whether a schematic will fit in a place before actually writing it to the data.

* `mapgen_helper.place_schematic_on_data(data, data_param2, area, pos, schematic, rotation, replacements, force_placement, flags)`
	* Takes a lua-format schematic and applies it to the data and param2_data arrays produced by vmanip instead of being applied to the vmanip directly. Useful in a mapgen loop that's doing other things with the data before and after applying schematics. A VoxelArea for the data also needs to be provided.
	* *NOTE:* As of the writing of this documentation, this method is still somewhat of a work-in-progress. It only supports the table version of the `flags` parameter (eg, `{["place_center_x"] = true}`), and doesn't support rotating nodes of paramtype2 `wallmounted`, `colorfacedir`, or `colorwallmounted`. Support is planned in the near future.

	* This method has some additional features that the existing minetest schematic-placement API doesn't support:
		* node defs can have a "`place_on_condition(c_node)`" property defined. This is a function that is given the node content ID of the node the schematic is about to replace  and returns true to indicate the schematic should replace it or false to indicate it should not. Useful, for example, in a schematic that should replace water but not earth (placing decorations on the bottom of the ocean), or a schematic that replaces all buildable_to nodes (to prevent tufts of grass from knocking holes in foundations).
		* The schematic can have a "`center_pos`" position defined relative to the schematic's minpos that will be treated as the rotation and placement origin, unless overridden by the flags parameter.
	* Returns true if the schematic was entirely contained within the given voxelarea, false otherwise.

## Bounding boxes

* `mapgen_helper.is_within_distance_box(pos1, pos2, distance)`
	* A very cheap nearness test using Manhattan distance, useful as a fast first-pass check to see whether a more math-heavy test is needed.

* `mapgen_helper.intersect(minpos1, maxpos1, minpos2, maxpos2)`
	* Given two bounding boxes, returns a pair of points representing the minpos and maxpos of the intersection between them. If no intersection exists it returns nil.

* `mapgen_helper.intersect_exists(minpos1, maxpos1, minpos2, maxpos2)`
	* A cheaper version of the above method that only returns a true/false indication that an intersection exists.

* `mapgen_helper.intersect_exists_xz(minpos1, maxpos1, minpos2, maxpos2)`
	* An _even cheaper_ version of the above method that returns true/false based on whether the two bounding boxes overlap on the x-z plane.

## Lines

* `mapgen_helper.distance_to_segment(pos, start, end)`
	* Returns the minimum distance from the point "pos" to any point along the line segment defined by the points "start" and "end"

* `mapgen_helper.distance_to_path(pos, path)`
	* Similar to the above, but "path" is a list of points forming a continuous series of line segments. Distance returned is the shortest distance to any segment in the list.

## Random values

* `mapgen_helper.get_random_points(minp, maxp, min_output_size, max_output_size)`
	* Returns a consistent list of random points within a volume. Each call to this method will give the same set of points if the same parameters are provided, making this useful for placing things that extend outside of the currently-generated map chunk - for example, minp and maxp could be kilometers apart and the points designate the locations where mountains are to be placed.

* `mapgen_helper.xz_consistent_randomp(pos)'
	* Returns a random value based on the x and z coordinates of pos. The random value is always the same for the same x and z.

* `mapgen_helper.xz_consistent_randomi(area, vi)`
	* Same as above, but takes a VoxelArea and a voxel index as parameters.

## Region tiling

These methods are useful with things like `mapgen_helper.get_random_points`, allowing you to seamlessly place items that cross mapchunk boundaries by generating sets of random points for all adjacent regions.

* `mapgen_helper.get_region_minp_xz(pos_xz, mapblocks)`
	* Returns a consistent large region that doesn't cross mapgen block boundaries. Regions are `mapblocks * chunksize` nodes in width and depth. Ignores the y coordinate.

* `mapgen_helper.get_nearest_regions(pos_xz, gridscale)`

	* Given a position and a region scale, returns the minp corners of the four regions that the player is closest to.
	* For example, if we have a region scale of 6 and the parameter position is at "*", the four points marked "R" would be returned:

			|-----|-----|-----|      |-----|-----|-----|      |-----|-----|-----|
			|     |     |     |      |     |     |     |      |     |     |     |
			|     |     |     |      |     |     |     |      |     |     |     |
			|     |     |     |      |     |     |     |      |     |     |     |
			|     |     |     |      |     |     |     |      |     |     |     |
			|     |     |     |      |     |     |     |      |     |     |     |
			|-----|-----|-----|      |-----|-----|-----|      |-----R-----R-----|
			|     |     |     |      |     |     |     |      |     |     |     |
			|     |     |     |      |     |     |     |      |     |    *|     |
			|     |     |     |      |     |     |     |      |     |     |     |
			|     | *   |     |      |     |    *|     |      |     |     |     |
			|     |     |     |      |     |     |     |      |     |     |     |
			R-----R-----|-----|      |-----R-----R-----|      |-----R-----R-----|
			|     |     |     |      |     |     |     |      |     |     |     |
			|     |     |     |      |     |     |     |      |     |     |     |
			|     |     |     |      |     |     |     |      |     |     |     |
			|     |     |     |      |     |     |     |      |     |     |     |
			|     |     |     |      |     |     |     |      |     |     |     |
			R-----R-----|-----|      |-----R-----R-----|      |-----|-----|-----|

* `mapgen_helper.get_adjacent_regions(pos_xz, gridscale)`
	* Similar to the above, but this method returns all adjacent regions and not just the nearest ones:

			|---|---|---|
			|   |   |   |
			|   |   |   |
			R---R---R---|
			|   |   |   |
			|   |  *|   |
			R---R---R---|
			|   |   |   |
			|   |   |   |
			R---R---R---|


code:
-- A cheap nearness test, using Manhattan distance.
mapgen_helper.is_within_distance_box = function(pos1, pos2, distance)
	return math.abs(pos1.x-pos2.x) <= distance and
		math.abs(pos1.y-pos2.y) <= distance and
		math.abs(pos1.z-pos2.z) <= distance
end

-- Finds an intersection between two axis-aligned bounding boxes (AABB)s, or nil if there's no overlap
mapgen_helper.intersect = function(minpos1, maxpos1, minpos2, maxpos2)
	--checking x and z first since they're more likely to fail to overlap
	if minpos1.x <= maxpos2.x and maxpos1.x >= minpos2.x and
		minpos1.z <= maxpos2.z and maxpos1.z >= minpos2.z and
		minpos1.y <= maxpos2.y and maxpos1.y >= minpos2.y then

		return {
				x = math.max(minpos1.x, minpos2.x),
				y = math.max(minpos1.y, minpos2.y),
				z = math.max(minpos1.z, minpos2.z)
			},
			{
				x = math.min(maxpos1.x, maxpos2.x),
				y = math.min(maxpos1.y, maxpos2.y),
				z = math.min(maxpos1.z, maxpos2.z)
			}
	end
	return nil, nil
end

-- a simpler version of the above if all you care about is whether an intersection exists
mapgen_helper.intersect_exists = function(minpos1, maxpos1, minpos2, maxpos2)
	--checking x and z first since they're more likely to fail to overlap
	return (minpos1.x <= maxpos2.x and maxpos1.x >= minpos2.x and
		minpos1.z <= maxpos2.z and maxpos1.z >= minpos2.z and
		minpos1.y <= maxpos2.y and maxpos1.y >= minpos2.y)
end

-- A version of the above that only cares about the x and z parameters, useful for example to lay out non-overlapping trees or buildings
mapgen_helper.intersect_exists_xz = function(minpos1, maxpos1, minpos2, maxpos2)
	return (minpos1.x <= maxpos2.x and maxpos1.x >= minpos2.x and
		minpos1.z <= maxpos2.z and maxpos1.z >= minpos2.z)
end

-- Simply tests whether pos is within the bounding box defined by minpos and maxpos
local intersect_exists = mapgen_helper.intersect_exists
mapgen_helper.is_pos_within_box = function(pos, minpos, maxpos)
	return intersect_exists(pos, pos, minpos, maxpos)
end

-- Tests whether box 1 is entirely contained within box 2
mapgen_helper.is_box_within_box = function(minpos1, maxpos1, minpos2, maxpos2)
	return (minpos1.x >= minpos2.x and maxpos1.x <= maxpos2.x and
		minpos1.z >= minpos2.z and maxpos1.z <= maxpos2.z and
		minpos1.y >= minpos2.y and maxpos1.y <= maxpos2.y)
end


code:
mapgen_helper = {}

mapgen_helper.mapgen_seed = tonumber(minetest.get_mapgen_setting("seed"))

-- The "sidelen" used in almost every mapgen
mapgen_helper.block_size = tonumber(minetest.get_mapgen_setting("chunksize")) * 16

local MP = minetest.get_modpath(minetest.get_current_modname())
dofile(MP.."/bounding_boxes.lua")
dofile(MP.."/random.lua")
dofile(MP.."/voxelarea_iterator.lua")
dofile(MP.."/voxeldata.lua")
dofile(MP.."/region_functions.lua")
dofile(MP.."/lines.lua")
dofile(MP.."/place_schematic.lua")
dofile(MP.."/noise_manager.lua")
dofile(MP.."/metrics.lua")
dofile(MP.."/log_location.lua")

local map_data = {}
local map_param2_data = {}

mapgen_helper.mapgen_vm_data = function()
	local vm, emin, emax = minetest.get_mapgen_object("voxelmanip")
	vm:get_data(map_data)
	return vm, map_data, VoxelArea:new{MinEdge=emin, MaxEdge=emax}
end

mapgen_helper.mapgen_vm_data_param2 = function()
	local vm, emin, emax = minetest.get_mapgen_object("voxelmanip")
	vm:get_data(map_data)
	vm:get_param2_data(map_param2_data)
	return vm, map_data, map_param2_data, VoxelArea:new{MinEdge=emin, MaxEdge=emax}
end

mapgen_helper.biome_defs = nil
mapgen_helper.get_biome_def = function(biome_id) -- given an id from the biome map, returns a biome definition.
	if mapgen_helper.biome_defs == nil then
		-- First time this was asked for, populate the table.
		-- Biome registration is only done at load time so we don't have to worry about new biomes invalidating this table
		mapgen_helper.biome_defs = {}
		for name, desc in pairs(minetest.registered_biomes) do
			local i = minetest.get_biome_id(desc.name)
			mapgen_helper.biome_defs[i] = desc
		end
	end
	return mapgen_helper.biome_defs[biome_id]
end

mapgen_helper.get_biome_def_i = function(biomemap, minp, maxp, area, vi)
	if biomemap == nil then
		return nil
	end
	local index2d = mapgen_helper.index2di(minp, maxp, area, vi)
	return mapgen_helper.get_biome_def(biomemap[index2d])
end

-- returns whether a content ID is "buildable to"
local buildable_to
mapgen_helper.buildable_to = function(c_node)
	if buildable_to then return buildable_to[c_node] end
	buildable_to = {}
	for k, v in pairs(minetest.registered_nodes) do
		if v.buildable_to then
			buildable_to[minetest.get_content_id(k)] = true
		end
	end
	return buildable_to[c_node]
end

local is_ground_content = nil
mapgen_helper.is_ground_content = function(c_node) -- If false, the cave generator will not carve through this node
	if is_ground_content ~= nil then
		return is_ground_content[c_node]
	end
	is_ground_content = {}
	for k, v in pairs(minetest.registered_nodes) do
		if v.is_ground_content == true then
			is_ground_content[minetest.get_content_id(k)] = true
		end
	end
	return is_ground_content[c_node]
end


code:
-- algorithms taken from https://gist.github.com/reciprocum/4e3599a9563ec83ba2a63f5a6cdd39eb

local dot = function(a, b)
	return a.x * b.x + a.y * b.y + a.z * b.z
end

local modulus = function(v)
	return math.sqrt(dot(v,v))
end

local cross = function(a, b)
	return {x= a.y * b.z - a.z * b.y,
		y= a.z * b.x - a.x * b.z,
		z= a.x * b.y - a.y * b.x}
end

-- TODO: add bounding box tests to early-out on distant segments

-- Calculates the distance from a point to a line segment.
-- v - the point
-- a - start of line segment
-- b - end of line segment

mapgen_helper.distance_to_segment = function(v, a, b)
	local ab = vector.subtract(b, a)
    local av = vector.subtract(v, a)

	if dot(av, ab) <= 0.0 then -- Point is lagging behind start of the segment, so perpendicular distance is not viable.
		return modulus(av) -- Use distance to start of segment instead.
	end

    local bv = vector.subtract(v, b)

    if dot(bv, ab) >= 0.0 then -- Point is advanced past the end of the segment, so perpendicular distance is not viable.
        return modulus(bv) -- Use distance to end of the segment instead.
	end

	return modulus( cross(ab, av )) / modulus(ab) -- Perpendicular distance of point to segment.
end

-- Calculates the euclidean distance from a point to a line segmented path.
-- v - the point from with the distance is measured
-- path - the array of points which, when sequentialy joined by line segments, form a path
-- returns the distance from v to the closest of the path forming line segments

mapgen_helper.distance_to_path = function(v, path)
	local minimum_distance = 2^32 -- Should be big enough for any practical minetest needs
	for i = 2, table.getn(path) do
		local distance = mapgen_helper.distance_to_segment(path[i-1], path[i])
        if (distance < minimum_distance) then
			minimum_distance = distance
		end
	end
	return minimum_distance
end

-- For digging out straight lines

local dist_sq = function(x1, y1, z1, x2, y2, z2)
	return (x2-x1)^2 + (y2-y1)^2 + (z2-z1)^2
end
local constrain = function(n, min_n, max_n)
	if n < min_n then return min_n
	elseif n > max_n then return max_n
	else return n end
end
local dist_to_segment_squared = function(px, py, pz, lx1, ly1, lz1, lx2, ly2, lz2)
	local line_dist = dist_sq(lx1, ly1, lz1, lx2, ly2, lz2)
	if (line_dist == 0) then
		return dist_sq(px, py, pz, lx1, ly1, lz1)
	end
	local t = ((px - lx1) * (lx2 - lx1) + (py - ly1) * (ly2 - ly1) + (pz - lz1) * (lz2 - lz1)) / line_dist
	t = constrain(t, 0, 1)
	return dist_sq(px, py, pz, lx1 + t * (lx2 - lx1), ly1 + t * (ly2 - ly1), lz1 + t * (lz2 - lz1))
end


code:
if not minetest.settings:get_bool("mapgen_helper_log_locations", false) then
	mapgen_helper.log_first_location = function()
		return
	end
	return
end

mapgen_helper.log_location_enabled = true

local worldpath = minetest.get_worldpath()
local locations = {}

local save_data = function()
	local file = io.open(worldpath.."/mapgen_helper_test_locations.json", "w")
	if file then
		local data = {}
		data.locations = locations
		file:write(minetest.serialize(data))
		file:close()
	end
end

local read_data = function()
	local file = io.open(worldpath.."/mapgen_helper_test_locations.json", "r")
	if file then
		local data = minetest.deserialize(file:read("*all"))
		locations = data.locations or {}
		-- to upgrade legacy data that didn't have counts recorded
		for _, location in pairs(locations) do
			location.count = location.count or 1
		end
		file:close()
	else
		locations = {}
	end
end
read_data()

-- logs only the first location of a particular type, ignores any subsequent ones
mapgen_helper.log_first_location = function(name, pos, desc, notes)
	if not locations[name] then
		locations[name] = {pos=pos, desc=desc, notes=notes, count = 1}
		minetest.log("info", "[mapgen_helper] recorded location " .. name .. " at " .. minetest.pos_to_string(pos) .. " with desc '" .. (desc or "") .. "'")
		save_data()
	end
end

mapgen_helper.log_location = function(name, pos, desc, notes)
	local location = locations[name] or {pos=pos, desc=desc, notes=notes, count = 0}
	locations[name] = location
	location.pos = pos
	location.desc = desc
	location.notes = notes
	location.count = location.count + 1
end

minetest.register_chatcommand("mapgen_helper_loc", {
	params = "[location name]",
	description = "sends the player to the named test location, or lists recorded locations if no parameter is given",
	privs = {server=true},
	func = function(name, param)
		if not param or param == "" then
			minetest.chat_send_player(name,"test locations available:")
			local names = {}
			for loc_name, loc in pairs(locations) do
				table.insert(names, loc_name)
			end
			table.sort(names)
			for number, loc_name in ipairs(names) do
				minetest.chat_send_player(name, "\t"..loc_name .. " - " .. (locations[loc_name].desc or "") .. " - " .. tostring(locations[loc_name].count))
			end
			return
		end
		local loc = locations[param]
		if not loc then
			minetest.chat_send_all("test location " .. param .. " not found")
			return
		end
		local player = minetest.get_player_by_name(name)
		player:set_pos(loc.pos)
		if loc.notes then
			minetest.chat_send_player(name, loc.notes)
		end
	end,
})

local tour_index = 0
minetest.register_chatcommand("mapgen_helper_tour", {
	params = "<next|prev>",
	description = "cycles through the various recorded mapgen test locations, forward or backward",
	privs = {server=true},
	func = function(name, param)
		local names = {}
		for name, loc in pairs(locations) do
			table.insert(names, name)
		end
		table.sort(names)
		if param == "prev" then
			tour_index = tour_index - 1
			if tour_index < 1 then
				tour_index = #names
			end
		else
			tour_index = tour_index + 1
			if tour_index > #names then
				tour_index = 1
			end
		end

		local loc = locations[names[tour_index]]
		local player = minetest.get_player_by_name(name)
		player:set_pos(loc.pos)
		minetest.chat_send_player(name, "Teleport to test location " .. names[tour_index])
		if loc.notes then
			minetest.chat_send_player(name, loc.notes)
		end
	end
})

minetest.register_on_shutdown(function()
	save_data()
end)


code:
if not minetest.settings:get_bool("mapgen_helper_record_time", false) then
	mapgen_helper.record_time = function()
		return
	end
	return
end

local worldpath = minetest.get_worldpath()

local persist = minetest.settings:get_bool("mapgen_helper_persist_recorded_time", false)
local filename = "mapgen_helper_metrics.lua"
local filename_old = "mapgen_helper_metrics_old.lua"

local resolution = minetest.settings:get("mapgen_helper_record_time_resolution") or 0.1

local data = {}
data.times = {}

if persist then
	local read_data = loadfile(worldpath.."/"..filename)
	if read_data then
		data = read_data()
	end
end

if data.resolution ~= nil and data.resolution ~= resolution then
	minetest.log("error", "[mapgen_helper] resolution of recorded metrics doesn't match saved metrics. Moving the old saved metrics to " .. filename_old)
	local file, e = io.open(worldpath.."/"..filename_old, "w");
	if not file then
		return error(e);
	end
	file:write(minetest.serialize(data))
	file:close()
	data = {}
	data.times = {}
end

data.resolution = resolution
local times = data.times

mapgen_helper.record_time = function(label, time)
	local time_slot = math.ceil(time/resolution)
	local label_data = times[label] or {}
	local counts = label_data.counts or {}
	local existing_count = counts[time_slot] or 0
	local total_time = label_data.total_time or 0

	counts[time_slot] = existing_count + 1
	total_time = total_time + time
	label_data.total_time = total_time
	label_data.counts = counts
	times[label] = label_data
end

minetest.register_on_shutdown(function()
	if next(times) == nil then
		return
	end
	minetest.log("warning", "[mapgen_helper] Average chunk generation times:")
	for name, label_data in pairs(times) do
		minetest.log("warning", string.rep("-", 40))

		local min_slot
		local max_slot
		local highest_slot_count = 0
		local total_count = 0
		local max_digits

		for slot, count in pairs(label_data.counts) do
			if (not min_slot) or (slot < min_slot) then
				min_slot = slot
			end
			if (not max_slot) or (slot > max_slot) then
				max_slot = slot
			end
			if (not max_digits) or (#tostring(slot*resolution) > max_digits) then
				max_digits = #tostring(slot*resolution)
			end
			total_count = total_count + count
			highest_slot_count = math.max(highest_slot_count, count)
		end

		local average = label_data.total_time / total_count

		minetest.log("warning", "For " .. name .. ":")
		if min_slot == max_slot then
			minetest.log("warning", "All chunks took between " .. (min_slot-1)*resolution .. " and " .. min_slot*resolution .. " seconds.")
		else
			for i = min_slot, max_slot do
				local count = label_data.counts[i] or 0
				local spacing = max_digits - #tostring(i*resolution)
				local count_string = ""
				if count > 0 then
					count_string = " " .. count
				end
				minetest.log("warning", i*resolution .. "s" .. string.rep(" ", spacing) .. ": " ..
					string.rep("=", math.ceil((count / highest_slot_count) * 40)) .. count_string)
			end
		end

		minetest.log("warning", "Total number of chunks: " .. total_count .. ". Average time per chunk: " .. average .. " seconds.")
	end
	minetest.log("warning", string.rep("-", 40))

	if persist then
		local file, e = io.open(worldpath.."/"..filename, "w");
		if not file then
			return error(e);
		end
		file:write(minetest.serialize(data))
		file:close()
	end
end)


code:
name = mapgen_helper
description = A library of mapgen-related functions and tools.


code:
local perlin_buffers = {}

mapgen_helper.register_perlin = function(name, perlin_params)
	perlin_buffers[name] = perlin_buffers[name] or {}
	if perlin_buffers[name].perlin_params then
		minetest.log("error", "mapgen_helper.register_perlin3d called for " .. name .. " when perlin parameters were already registered.")
		return
	end
	perlin_buffers[name].perlin_params = perlin_params
end

local get_buffer = function(name, sidelen, perlin_params)
	perlin_buffers[name] = perlin_buffers[name] or {}
	local buffer = perlin_buffers[name]

	if buffer.sidelen ~= nil and buffer.sidelen ~= sidelen then
		buffer.nobj_perlin = nil -- parameter changed, force regenerate of object
	end
	buffer.sidelen = sidelen

	if perlin_params then
		if buffer.perlin_params then
			for k, v in pairs(buffer.perlin_params) do
				if perlin_params[k] ~= v then
					buffer.nobj_perlin = nil -- parameter changed, force regenerate of object
				end
			end
		end
		buffer.perlin_params = perlin_params -- record provided parameters
	elseif buffer.perlin_params == nil then
		minetest.log("error", "mapgen_helper.register_perlin3d called for " .. name .. " with no registered or provided perlin parameters.")
		return
	else
		perlin_params = buffer.perlin_params -- retrieve recorded parameters
	end

	buffer.last_used = minetest.get_gametime()
	return buffer, perlin_params
end

mapgen_helper.perlin3d = function(name, minp, maxp, perlin_params)
	local minx = minp.x
	local minz = minp.z
	local sidelen = maxp.x - minp.x + 1 --length of a mapblock
	local chunk_lengths = {x = sidelen, y = sidelen, z = sidelen} --table of chunk edges

	local buffer, perlin_params = get_buffer(name, sidelen, perlin_params)

	buffer.nvals_perlin_buffer = buffer.nvals_perlin_buffer or {}
	buffer.nobj_perlin = buffer.nobj_perlin or minetest.get_perlin_map(perlin_params, chunk_lengths)
	if buffer.nobj_perlin.get_3d_map_flat then
		return buffer.nobj_perlin:get_3d_map_flat(minp, buffer.nvals_perlin_buffer), VoxelArea:new{MinEdge=minp, MaxEdge=maxp}
	else
		return buffer.nobj_perlin:get3dMap_flat(minp, buffer.nvals_perlin_buffer), VoxelArea:new{MinEdge=minp, MaxEdge=maxp}
	end
end

mapgen_helper.perlin2d = function(name, minp, maxp, perlin_params)
	local minx = minp.x
	local minz = minp.z
	local sidelen = maxp.x - minp.x + 1 --length of a mapblock
	local chunk_lengths = {x = sidelen, y = sidelen, z = sidelen} --table of chunk edges

	local buffer, perlin_params = get_buffer(name, sidelen, perlin_params)

	buffer.nvals_perlin_buffer = perlin_buffers[name].nvals_perlin_buffer or {}
	buffer.nobj_perlin = perlin_buffers[name].nobj_perlin or minetest.get_perlin_map(perlin_params, chunk_lengths)
	if buffer.nobj_perlin.get_2d_map_flat then
		return buffer.nobj_perlin:get_2d_map_flat({x=minp.x, y=minp.z}, perlin_buffers[name].nvals_perlin_buffer)
	else
		return buffer.nobj_perlin:get2dMap_flat({x=minp.x, y=minp.z}, perlin_buffers[name].nvals_perlin_buffer)
	end
end

mapgen_helper.index2d = function(minp, maxp, x, z)
	return x - minp.x +
		(maxp.x - minp.x + 1) -- sidelen
		*(z - minp.z)
		+ 1
end

mapgen_helper.index2dp = function(minp, maxp, pos)
	return mapgen_helper.index2d(minp, maxp, pos.x, pos.z)
end

-- Takes an index into a 3D area and returns the corresponding 2D index
-- assumes equal edge lengths
mapgen_helper.index2di = function(minp, maxp, area, vi)
	local MinEdge = area.MinEdge
	local zstride = area.zstride
	local minpx = minp.x
	local i = vi - 1
	local z = math.floor(i / zstride) + MinEdge.z
	local x = math.floor((i % zstride) % area.ystride) + MinEdge.x
	return x - minpx +
		(maxp.x - minpx + 1) -- sidelen
		*(z - minp.z)
		+ 1
end

-- If a noise buffer hasn't been used in 60 seconds, let garbage collection take it.
local time_elapsed = 0
minetest.register_globalstep(function(dtime)
	time_elapsed = time_elapsed + dtime
	if time_elapsed > 60 then
		local current_time = minetest.get_gametime()
		time_elapsed = 0
		for _, buffer in pairs(perlin_buffers) do
			if current_time - buffer.last_used > 60 then
				buffer.nvals_perlin_buffer = nil
			end
		end
	end
end)