code:
bonemeal = {}

-- Load support for intllib.
local MP = minetest.get_modpath(minetest.get_current_modname())
local S, NS = dofile(MP .. "/intllib.lua")

-- creative check
local creative_mode_cache = minetest.settings:get_bool("creative_mode")
function is_creative(name)
	return creative_mode_cache or minetest.check_player_privs(name, {creative = true})
end

-- default crops
local crops = {
	{"farming:cotton_", 8, "farming:seed_cotton"},
	{"farming:wheat_", 8, "farming:seed_wheat"},
}

-- special pine check for nearby snow
local function pine_grow(pos)

	if minetest.find_node_near(pos, 1,
		{"default:snow", "default:snowblock", "default:dirt_with_snow"}) then

		default.grow_new_snowy_pine_tree(pos)
	else
		default.grow_new_pine_tree(pos)
	end
end

-- default saplings
local saplings = {
	{"default:sapling", default.grow_new_apple_tree, "soil"},
	{"default:junglesapling", default.grow_new_jungle_tree, "soil"},
	{"default:emergent_jungle_sapling", default.grow_new_emergent_jungle_tree, "soil"},
	{"default:acacia_sapling", default.grow_new_acacia_tree, "soil"},
	{"default:aspen_sapling", default.grow_new_aspen_tree, "soil"},
	{"default:pine_sapling", pine_grow, "soil"},
	{"default:bush_sapling", default.grow_bush, "soil"},
	{"default:acacia_bush_sapling", default.grow_acacia_bush, "soil"},
	{"default:large_cactus_seedling", default.grow_large_cactus, "sand"},
	{"default:blueberry_bush_sapling", default.grow_blueberry_bush, "soil"},
	{"default:pine_bush_sapling", default.grow_pine_bush, "soil"},
}

-- helper tables ( "" denotes a blank item )
local green_grass = {
	"default:grass_2", "default:grass_3", "default:grass_4",
	"default:grass_5", "", ""
}

local dry_grass = {
	"default:dry_grass_2", "default:dry_grass_3", "default:dry_grass_4",
	"default:dry_grass_5", "", ""
}

local flowers = {
	"flowers:dandelion_white", "flowers:dandelion_yellow", "flowers:geranium",
	"flowers:rose", "flowers:tulip", "flowers:viola", ""
}

-- 5.0 flower check
if minetest.registered_nodes["flowers:tulip_black"] then
	flowers[#flowers + 1] = "flowers:tulip_black"
	flowers[#flowers + 1] = "flowers:chrysanthemum_green"
end

-- add additional bakedclay flowers if enabled
if minetest.get_modpath("bakedclay") then
	flowers[#flowers + 1] = "bakedclay:delphinium"
	flowers[#flowers + 1] = "bakedclay:thistle"
	flowers[#flowers + 1] = "bakedclay:lazarus"
	flowers[#flowers + 1] = "bakedclay:mannagrass"
	flowers[#flowers + 1] = ""
end

-- default biomes deco
local deco = {
	{"default:dirt_with_dry_grass", dry_grass, flowers},
	{"default:sand", {}, {"default:dry_shrub", "", "", ""} },
	{"default:desert_sand", {}, {"default:dry_shrub", "", "", ""} },
	{"default:silver_sand", {}, {"default:dry_shrub", "", "", ""} },
}

----- local functions

-- particles
local function particle_effect(pos)

	minetest.add_particlespawner({
		amount = 4,
		time = 0.15,
		minpos = pos,
		maxpos = pos,
		minvel = {x = -1, y = 2, z = -1},
		maxvel = {x = 1, y = 4, z = 1},
		minacc = {x = -1, y = -1, z = -1},
		maxacc = {x = 1, y = 1, z = 1},
		minexptime = 1,
		maxexptime = 1,
		minsize = 1,
		maxsize = 3,
		texture = "bonemeal_particle.png",
	})
end

-- tree type check
local function grow_tree(pos, object)

	if type(object) == "table" and object.axiom then
		-- grow L-system tree
		minetest.remove_node(pos)
		minetest.spawn_tree(pos, object)

	elseif type(object) == "string" and minetest.registered_nodes[object] then
		-- place node
		minetest.set_node(pos, {name = object})

	elseif type(object) == "function" then
		-- function
		object(pos)
	end
end

-- sapling check
local function check_sapling(pos, nodename)

	-- what is sapling placed on?
	local under =  minetest.get_node({
		x = pos.x,
		y = pos.y - 1,
		z = pos.z
	})

	local can_grow, grow_on

	-- check list for sapling and function
	for n = 1, #saplings do

		if saplings[n][1] == nodename then

			grow_on = saplings[n][3]

			-- sapling grows on top of specific node
			if grow_on
			and grow_on ~= "soil"
			and grow_on ~= "sand"
			and grow_on == under.name then
				can_grow = true
			end

			-- sapling grows on top of soil (default)
			if can_grow == nil
			and (grow_on == nil or grow_on == "soil")
			and minetest.get_item_group(under.name, "soil") > 0 then
				can_grow = true
			end

			-- sapling grows on top of sand
			if can_grow == nil
			and grow_on == "sand"
			and minetest.get_item_group(under.name, "sand") > 0 then
				can_grow = true
			end

			-- check if we can grow sapling
			if can_grow then
				particle_effect(pos)
				grow_tree(pos, saplings[n][2])
				return
			end
		end
	end
end

-- crops check
local function check_crops(pos, nodename, strength)

	local mod, crop, stage, nod, def

	-- grow registered crops
	for n = 1, #crops do

		if string.find(nodename, crops[n][1])
		or nodename == crops[n][3] then

			-- separate mod and node name
			mod = nodename:split(":")[1] .. ":"
			crop = nodename:split(":")[2]

			-- get stage number or set to 0 for seed
			stage = tonumber( crop:split("_")[2] ) or 0
			stage = math.min(stage + strength, crops[n][2])

			-- check for place_param setting
			nod = crops[n][1] .. stage
			def = minetest.registered_nodes[nod]
			def = def and def.place_param2 or 0

			minetest.set_node(pos, {name = nod, param2 = def})

			particle_effect(pos)

			return
		end
	end
end

-- check soil for specific decoration placement
local function check_soil(pos, nodename, strength)

	-- set radius according to strength
	local side = strength - 1
	local tall = math.max(strength - 2, 0)
	local floor
	local groups = minetest.registered_items[nodename]
		and minetest.registered_items[nodename].groups or {}

	-- only place decoration on one type of surface
	if groups.soil then
		floor = {"group:soil"}
	elseif groups.sand then
		floor = {"group:sand"}
	else
		floor = {nodename}
	end

	-- get area of land with free space above
	local dirt = minetest.find_nodes_in_area_under_air(
		{x = pos.x - side, y = pos.y - tall, z = pos.z - side},
		{x = pos.x + side, y = pos.y + tall, z = pos.z + side}, floor)

	-- set default grass and decoration
	local grass = green_grass
	local decor = flowers

	-- choose grass and decoration to use on dirt patch
	for n = 1, #deco do

		-- do we have a grass match?
		if nodename == deco[n][1] then
			grass = deco[n][2] or {}
			decor = deco[n][3] or {}
		end
	end

	local pos2, nod, def

	-- loop through soil
	for _,n in pairs(dirt) do

		pos2 = n

		pos2.y = pos2.y + 1

		if math.random(1, 5) == 5 then
			-- place random decoration (rare)
			nod = decor[math.random(1, #decor)] or ""
		else
			-- place random grass (common)
			nod = #grass > 0 and grass[math.random(1, #grass)] or ""
		end

		if nod and nod ~= "" then
			def = minetest.registered_nodes[nod]
			def = def and def.place_param2 or 0
			minetest.set_node(pos2, {name = nod, param2 = def})
		end

		particle_effect(pos2)
	end
end

-- global functions

-- add to sapling list
-- {sapling node, schematic or function name, "soil"|"sand"|specific_node}
--e.g. {"default:sapling", default.grow_new_apple_tree, "soil"}

function bonemeal:add_sapling(list)

	for n = 1, #list do
		table.insert(saplings, list[n])
	end
end

-- add to crop list to force grow
-- {crop name start_, growth steps, seed node (if required)}
-- e.g. {"farming:wheat_", 8, "farming:seed_wheat"}
function bonemeal:add_crop(list)

	for n = 1, #list do
		table.insert(crops, list[n])
	end
end

-- add grass and flower/plant decoration for specific dirt types
--  {dirt_node, {grass_nodes}, {flower_nodes}
-- e.g. {"default:dirt_with_dry_grass", dry_grass, flowers}
-- if an entry already exists for a given dirt type, it will add new entries and all empty
-- entries, allowing to both add decorations and decrease their frequency.
function bonemeal:add_deco(list)

	for l = 1, #list do

		for n = 1, #deco do

			-- update existing entry
			if list[l][1] == deco[n][1] then

				-- adding grass types
				for _,extra in ipairs(list[l][2]) do

					if extra ~= "" then

						for __,entry in ipairs(deco[n][2]) do

							if extra == entry then
								extra = false
								break
							end
						end
					end

					if extra then
						table.insert(deco[n][2], extra)
					end
				end

				-- adding decoration types
				for _,extra in ipairs(list[l][3]) do

					if extra ~= "" then

						for __,entry in ipairs(deco[n][3]) do

							if extra == entry then
								extra = false
								break
							end
						end
					end

					if extra then
						table.insert(deco[n][3], extra)
					end
				end

				list[l] = false
				break
			end
		end

		if list[l] then
			table.insert(deco, list[l])
		end
	end
end

-- definitively set a decration scheme
-- this function will either add a new entry as is, or replace the existing one
function bonemeal:set_deco(list)

	for l = 1, #list do

		for n = 1, #deco do

			-- replace existing entry
			if list[l][1] == deco[n][1] then
				deco[n][2] = list[l][2]
				deco[n][3] = list[l][3]
				list[l] = false
				break
			end
		end

		if list[l] then
			table.insert(deco, list[l])
		end
	end
end

-- global on_use function for bonemeal
function bonemeal:on_use(pos, strength, node)

	-- get node pointed at
	local node = node or minetest.get_node(pos)

	-- return if nothing there
	if node.name == "ignore" then
		return
	end

	-- make sure strength is between 1 and 4
	strength = strength or 1
	strength = math.max(strength, 1)
	strength = math.min(strength, 4)

	-- papyrus and cactus
	if node.name == "default:papyrus" then

		default.grow_papyrus(pos, node)
		particle_effect(pos)
		return

	elseif node.name == "default:cactus" then

		default.grow_cactus(pos, node)
		particle_effect(pos)
		return
	end

	-- grow grass and flowers
	if minetest.get_item_group(node.name, "soil") > 0
	or minetest.get_item_group(node.name, "sand") > 0
	or minetest.get_item_group(node.name, "can_bonemeal") > 0 then
		check_soil(pos, node.name, strength)
		return
	end

	-- light check depending on strength (strength of 4 = no light needed)
	if (minetest.get_node_light(pos) or 0) < (12 - (strength * 3)) then
		return
	end

	-- check for tree growth if pointing at sapling
--	if minetest.get_item_group(node.name, "sapling") > 0
	if math.random(1, (5 - strength)) == 1 then
		check_sapling(pos, node.name)
		return
	end

	-- check for crop growth
	check_crops(pos, node.name, strength)
end

----- items

-- mulch (strength 1)
minetest.register_craftitem("bonemeal:mulch", {
	description = S("Mulch"),
	inventory_image = "bonemeal_mulch.png",

	on_use = function(itemstack, user, pointed_thing)

		-- did we point at a node?
		if pointed_thing.type ~= "node" then
			return
		end

		-- is area protected?
		if minetest.is_protected(pointed_thing.under, user:get_player_name()) then
			return
		end

		-- take item if not in creative
		if not is_creative(user:get_player_name()) then
			itemstack:take_item()
		end

		-- call global on_use function with strength of 1
		bonemeal:on_use(pointed_thing.under, 1)

		return itemstack
	end,
})

-- bonemeal (strength 2)
minetest.register_craftitem("bonemeal:bonemeal", {
	description = S("Bone Meal"),
	inventory_image = "bonemeal_item.png",

	on_use = function(itemstack, user, pointed_thing)

		-- did we point at a node?
		if pointed_thing.type ~= "node" then
			return
		end

		-- is area protected?
		if minetest.is_protected(pointed_thing.under, user:get_player_name()) then
			return
		end

		-- take item if not in creative
		if not is_creative(user:get_player_name()) then
			itemstack:take_item()
		end

		-- call global on_use function with strength of 2
		bonemeal:on_use(pointed_thing.under, 2)

		return itemstack
	end,
})

-- fertiliser (strength 3)
minetest.register_craftitem("bonemeal:fertiliser", {
	description = S("Fertiliser"),
	inventory_image = "bonemeal_fertiliser.png",

	on_use = function(itemstack, user, pointed_thing)

		-- did we point at a node?
		if pointed_thing.type ~= "node" then
			return
		end

		-- is area protected?
		if minetest.is_protected(pointed_thing.under, user:get_player_name()) then
			return
		end

		-- take item if not in creative
		if not is_creative(user:get_player_name()) then
			itemstack:take_item()
		end

		-- call global on_use function with strength of 3
		bonemeal:on_use(pointed_thing.under, 3)

		return itemstack
	end,
})

--commenting this out since we're using bones from MCMOB
-- bone
--minetest.register_craftitem("bonemeal:bone", {
--	description = S("Bone"),
--	inventory_image = "bonemeal_bone.png",
--})

-- gelatin powder
minetest.register_craftitem("bonemeal:gelatin_powder", {
	description = S("Gelatin Powder"),
	inventory_image = "bonemeal_gelatin_powder.png",
	groups = {food_gelatin = 1, flammable = 2},
})

--- crafting recipes

-- gelatin powder
minetest.register_craft({
	output = "bonemeal:gelatin_powder 2",
	recipe = {
--MCMOB BONE
		{"mobs_creatures:bone", "mobs_creatures:bone", "mobs_creatures:bone"},
		{"mesecraft_bucket:bucket_water", "mesecraft_bucket:bucket_water", "mesecraft_bucket:bucket_water"},
		{"mesecraft_bucket:bucket_water", "default:torch", "mesecraft_bucket:bucket_water"},
	},
	replacements = {
		{"mesecraft_bucket:bucket_water", "mesecraft_bucket:bucket_empty 5"},
	},
})

-- bonemeal (from creatures bone)
minetest.register_craft({
	type = "shapeless",
	output = "bonemeal:bonemeal",
	type = "shapeless",
	recipe = {"mobs_creatures:bone", "bonemeal:mulch", "farming:mortar_pestle"},
	replacements = {
		{"farming:mortar_pestle", "farming:mortar_pestle"},
	},
})

-- bonemeal (from coral skeleton)
minetest.register_craft({
	type = "shapeless",
	output = "bonemeal:bonemeal",
	recipe = {"default:coral_skeleton", "farming:mortar_pestle"},
        replacements = {
                {"farming:mortar_pestle", "farming:mortar_pestle"},
        },
})

-- mulch
minetest.register_craft({
	type = "shapeless",
	output = "bonemeal:mulch 2",
	recipe = {
		"group:tree", "group:leaves", "group:leaves",
		"group:leaves", "group:leaves", "group:leaves",
		"group:leaves", "group:leaves", "farming:mortar_pestle"
	},
        replacements = {
                {"farming:mortar_pestle", "farming:mortar_pestle"},
        },

})
-- fertiliser
minetest.register_craft({
	type = "shapeless",
	output = "bonemeal:fertiliser",
	recipe = {
		"bonemeal:bonemeal", "bonemeal:mulch", "mobs_creatures:poop_turd",
		"farming:mortar_pestle"
	},
        replacements = {
                {"farming:mortar_pestle", "farming:mortar_pestle"},
        },
})
-- add support for other mods
local path = minetest.get_modpath("bonemeal")

dofile(path .. "/mods.lua")
dofile(path .. "/lucky_block.lua")

print (S("[MOD] bonemeal loaded"))


code:
-- Fallback functions for when `intllib` is not installed.
-- Code released under Unlicense <http://unlicense.org>.

-- Get the latest version of this file at:
--   https://raw.githubusercontent.com/minetest-mods/intllib/master/lib/intllib.lua

local function format(str, ...)
	local args = { ... }
	local function repl(escape, open, num, close)
		if escape == "" then
			local replacement = tostring(args[tonumber(num)])
			if open == "" then
				replacement = replacement..close
			end
			return replacement
		else
			return "@"..open..num..close
		end
	end
	return (str:gsub("(@?)@(%(?)(%d+)(%)?)", repl))
end

local gettext, ngettext
if minetest.get_modpath("intllib") then
	if intllib.make_gettext_pair then
		-- New method using gettext.
		gettext, ngettext = intllib.make_gettext_pair()
	else
		-- Old method using text files.
		gettext = intllib.Getter()
	end
end

-- Fill in missing functions.

gettext = gettext or function(msgid, ...)
	return format(msgid, ...)
end

ngettext = ngettext or function(msgid, msgid_plural, n, ...)
	return format(n==1 and msgid or msgid_plural, ...)
end

return gettext, ngettext

repo_name: korkut/MeseCraft

license: mit


code:
# init.lua

Mulch = Paillis
Bone Meal = Poudre d'os
Fertiliser = Engrais
Bone = Os
[MOD] bonemeal loaded = [MOD] bonemeal chargé


code:
# init.lua

Mulch =
Bone Meal =
Fertiliser =
Bone =
[bonemeal] loaded =


code:
-- add lucky blocks

local function growy(pos, player)

	local dpos = minetest.find_node_near(pos, 1, "group:soil")

	if dpos then
		bonemeal:on_use(dpos, 5)
	end
end

if minetest.get_modpath("lucky_block") then

	lucky_block:add_blocks({
		{"lig"},
		{"dro", {"bonemeal:mulch"}, 10},
		{"dro", {"bonemeal:bonemeal"}, 10},
		{"dro", {"bonemeal:fertiliser"}, 10},
		{"cus", growy},
		{"nod", "default:chest", 0, {
			{name = "bonemeal:mulch", max = 20},
			{name = "bonemeal:bonemeal", max = 15},
			{name = "bonemeal:fertiliser", max = 10},
		}},
	})

end


code:
name = bonemeal
titel = Bonemeal
description = Adds bone and bonemeal giving the ability to quickly grow plants and saplings.
author = TenPlus1
depends = default, mobs_creatures
optional_depends = intllib, farming, ethereal, moretrees, technic_worldgen, lucky_block, flowers


code:
-- craft bones from animalmaterials into bonemeal
if minetest.get_modpath("animalmaterials") then

	minetest.register_craft({
		type = "shapeless",
		output = "bonemeal:bonemeal 2",
		recipe = {"animalmaterials:bone"},
	})
end

if farming and farming.mod and farming.mod == "redo" then

	bonemeal:add_crop({
		{"farming:tomato_", 8},
		{"farming:corn_", 8},
		{"farming:melon_", 8},
		{"farming:pumpkin_", 8},
		{"farming:beanpole_", 5},
		{"farming:blueberry_", 4},
		{"farming:raspberry_", 4},
		{"farming:carrot_", 8},
		{"farming:cocoa_", 4},
		{"farming:coffee_", 5},
		{"farming:cucumber_", 4},
		{"farming:potato_", 4},
		{"farming:grapes_", 8},
		{"farming:rhubarb_", 3},
		{"farming:barley_", 7},
		{"farming:hemp_", 8},
		{"farming:chili_", 8},
		{"farming:garlic_", 5},
		{"farming:onion_", 5},
		{"farming:pepper_", 5},
		{"farming:pineapple_", 8},
		{"farming:pea_", 5},
		{"farming:beetroot_", 5},
		{"farming:rye_", 8},
		{"farming:oat_", 8},
		{"farming:rice_", 8},
	})
end

if minetest.get_modpath("ethereal") then

	bonemeal:add_crop({
		{"ethereal:strawberry_", 8},
		{"ethereal:onion_", 5},
	})

	bonemeal:add_sapling({
		{"ethereal:palm_sapling", ethereal.grow_palm_tree, "soil"},
		{"ethereal:palm_sapling", ethereal.grow_palm_tree, "sand"},
		{"ethereal:yellow_tree_sapling", ethereal.grow_yellow_tree, "soil"},
		{"ethereal:big_tree_sapling", ethereal.grow_big_tree, "soil"},
		{"ethereal:banana_tree_sapling", ethereal.grow_banana_tree, "soil"},
		{"ethereal:frost_tree_sapling", ethereal.grow_frost_tree, "soil"},
		{"ethereal:mushroom_sapling", ethereal.grow_mushroom_tree, "soil"},
		{"ethereal:willow_sapling", ethereal.grow_willow_tree, "soil"},
		{"ethereal:redwood_sapling", ethereal.grow_redwood_tree, "soil"},
		{"ethereal:orange_tree_sapling", ethereal.grow_orange_tree, "soil"},
		{"ethereal:bamboo_sprout", ethereal.grow_bamboo_tree, "soil"},
		{"ethereal:birch_sapling", ethereal.grow_birch_tree, "soil"},
		{"ethereal:sakura_sapling", ethereal.grow_sakura_tree, "soil"},
	})

	local grass = {"default:grass_3", "default:grass_4", "default:grass_5", ""}

	bonemeal:add_deco({
		{"ethereal:crystal_dirt", {"ethereal:crystalgrass", "", "", "", ""}, {}},
		{"ethereal:fiery_dirt", {"ethereal:dry_shrub", "", "", "", ""}, {}},
		{"ethereal:prairie_dirt", grass, {"flowers:dandelion_white",
			"flowers:dandelion_yellow", "flowers:geranium", "flowers:rose",
			"flowers:tulip", "flowers:viola", "ethereal:strawberry_7"}},
		{"ethereal:gray_dirt", {}, {"ethereal:snowygrass", "", ""}},
		{"ethereal:cold_dirt", {}, {"ethereal:snowygrass", "", ""}},
		{"ethereal:mushroom_dirt", {}, {"flowers:mushroom_red", "flowers:mushroom_brown", "", "", ""}},
		{"ethereal:jungle_dirt", grass, {"default:junglegrass", "", "", ""}},
		{"ethereal:grove_dirt", grass, {"ethereal:fern", "", "", ""}},
		{"ethereal:bamboo_dirt", grass, {}},
	})
end

if minetest.get_modpath("moretrees") then

	-- special fir check for snow
	local function fir_grow(pos)

		if minetest.find_node_near(pos, 1,
			{"default:snow", "default:snowblock", "default:dirt_with_snow"}) then

			moretrees.grow_fir_snow(pos)
		else
			moretrees.grow_fir(pos)
		end
	end

	bonemeal:add_sapling({
		{"moretrees:beech_sapling", moretrees.spawn_beech_object, "soil"},
		{"moretrees:apple_tree_sapling", moretrees.spawn_apple_tree_object, "soil"},
		{"moretrees:oak_sapling", moretrees.spawn_oak_object, "soil"},
		{"moretrees:sequoia_sapling", moretrees.spawn_sequoia_object, "soil"},
		--{"moretrees:birch_sapling", moretrees.spawn_birch_object, "soil"},
		{"moretrees:birch_sapling", moretrees.grow_birch, "soil"},
		{"moretrees:palm_sapling", moretrees.spawn_palm_object, "soil"},
		{"moretrees:palm_sapling", moretrees.spawn_palm_object, "sand"},
		{"moretrees:date_palm_sapling", moretrees.spawn_date_palm_object, "soil"},
		{"moretrees:date_palm_sapling", moretrees.spawn_date_palm_object, "sand"},
		--{"moretrees:spruce_sapling", moretrees.spawn_spruce_object, "soil"},
		{"moretrees:spruce_sapling", moretrees.grow_spruce, "soil"},
		{"moretrees:cedar_sapling", moretrees.spawn_cedar_object, "soil"},
		{"moretrees:poplar_sapling", moretrees.spawn_poplar_object, "soil"},
		{"moretrees:willow_sapling", moretrees.spawn_willow_object, "soil"},
		{"moretrees:rubber_tree_sapling", moretrees.spawn_rubber_tree_object, "soil"},
		{"moretrees:fir_sapling", fir_grow, "soil"},
	})

elseif minetest.get_modpath("technic_worldgen") then

	bonemeal:add_sapling({
		{"moretrees:rubber_tree_sapling", technic.rubber_tree_model, "soil"},
	})
end

if minetest.get_modpath("caverealms") then

	local fil = minetest.get_modpath("caverealms") .. "/schematics/shroom.mts"
	local add_shroom = function(pos)

		minetest.swap_node(pos, {name = "air"})

		minetest.place_schematic(
			{x = pos.x - 5, y = pos.y, z = pos.z - 5}, fil, 0, nil, false)
	end

	bonemeal:add_sapling({
		{"caverealms:mushroom_sapling", add_shroom, "soil"},
	})
end


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:
#!/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+

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:
-- TODO:
-- There's potential race conditions in here if two players have the board open
-- and a culling happens or they otherwise diddle around with it. For now just
-- make sure it doesn't crash

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

local bulletin_max = 7*8

local culling_interval = 86400 -- one day in seconds
local culling_min = bulletin_max - 12 -- won't cull if there are this many or fewer bulletins

local bulletin_boards = {}
bulletin_boards.player_state = {}
bulletin_boards.board_def = {}

local path = minetest.get_worldpath() .. "/bulletin_boards.lua"
local f, e = loadfile(path);
if f then
	bulletin_boards.global_boards = f()
else
	bulletin_boards.global_boards = {}
end

local function save_boards()
	local file, e = io.open(path, "w");
	if not file then
		return error(e);
	end
	file:write(minetest.serialize(bulletin_boards.global_boards))
	file:close()
end

local max_text_size = 5000 -- half a book
local max_title_size = 60
local short_title_size = 12

-- gets the bulletins currently on a board
-- and other persisted data
local function get_board(name)
	local board = bulletin_boards.global_boards[name]
	if board then
		return board
	end
	board = {}
	board.last_culled = minetest.get_gametime()
	bulletin_boards.global_boards[name] = board
	return board
end

-- for incrementing through the bulletins on a board
local function find_next(board, start_index)
	local index = start_index + 1
	while index ~= start_index do
		if board[index] then
			return index
		end
		index = index + 1
		if index > bulletin_max then
			index = 1
		end
	end
	return index
end
local function find_prev(board, start_index)
	local index = start_index - 1
	while index ~= start_index do
		if board[index] then
			return index
		end
		index = index - 1
		if index < 1 then
			index = bulletin_max
		end
	end
	return index
end

-- Groups bulletins by count-per-player, then picks the oldest bulletin from the group with the highest count.

-- eg, if A has 1 bulletin, B has 2 bulletins, and C has 2 bulletins, then this will pick the oldest
-- bulletin from (B and C)'s bulletins. Returns index and timestamp, or nil if there's nothing.
local function find_most_cullable(board_name)
	local board = get_board(board_name)
	local player_count = {}
	local max_count = 0
	local total = 0
	for i = 1, bulletin_max do
		 local bulletin = board[i]
		 if bulletin then
			total = total + 1
			local player_name = bulletin.owner
			local count = (player_count[player_name] or 0) + 1
			max_count = math.max(count, max_count)
			player_count[player_name] = count
		 end
	end

	if total <= culling_min then
		return
	end

	local max_players = {}
	for player_name, count in pairs(player_count) do
		if count == max_count then
			max_players[player_name] = true
		end
	end

	local most_cullable_index
	local most_cullable_timestamp
	for i = 1, bulletin_max do
		local bulletin = board[i]
		if bulletin and max_players[bulletin.owner] then
			if bulletin.timestamp <= (most_cullable_timestamp or bulletin.timestamp) then
				most_cullable_timestamp = bulletin.timestamp
				most_cullable_index = i
			end
		end
	end

	return most_cullable_index, most_cullable_timestamp
end

-- safe way to get the description string of an item, in case it's not registered
local function get_item_desc(stack)
	local stack_def = stack:get_definition()
	if stack_def then
		return stack_def.description
	end
	return stack:get_name()
end

-- shows the base board to a player
local function show_board(player_name, board_name)
	local formspec = {}
	local board = get_board(board_name)
	local current_time = minetest.get_gametime()

	local intervals = (current_time - board.last_culled)/culling_interval
	local cull_count, remaining_cull_time = math.modf(intervals)
	while cull_count > 0 do
		local cull_index = find_most_cullable(board_name)
		if cull_index then
			board[cull_index] = nil
			cull_count = cull_count - 1
		else
			cull_count = 0
		end
	end
	board.last_culled = current_time - math.floor(culling_interval * remaining_cull_time)

	local def = bulletin_boards.board_def[board_name]
	local desc = minetest.formspec_escape(def.desc)
	local tip
	if def.cost then
		local stack = ItemStack(def.cost)
		tip = S("Post your bulletin here for the cost of @1 @2", stack:get_count(), get_item_desc(stack))
		desc = desc .. S(", Cost: @1 @2", stack:get_count(), get_item_desc(stack))
	else
		tip = S("Post your bulletin here")
	end

	formspec[#formspec+1] = "size[8,8.5]"
	.. "container[0,0]"
	.. "label[0.0,-0.25;"..desc.."]"
	.. "container_end[]"
	.. "container[0,0.5]"
	local i = 0
	for y = 0, 6 do
		for x = 0, 7 do
			i = i + 1
			local bulletin = board[i] or {}
			local short_title = bulletin.title or ""
			--Don't bother triming the title if the trailing dots would make it longer
			if #short_title > short_title_size + 3 then
				short_title = short_title:sub(1, short_title_size) .. "..."
			end
			local img = bulletin.icon or ""

			formspec[#formspec+1] =
				"image_button["..x..",".. y*1.2 ..";1,1;"..img..";button_"..i..";]"
				.."label["..x..","..y*1.2-0.35 ..";"..minetest.formspec_escape(short_title).."]"
			if bulletin.title and bulletin.owner and bulletin.timestamp then
				local days_ago = math.floor((current_time-bulletin.timestamp)/86400)
				formspec[#formspec+1] = "tooltip[button_"..i..";"
					..S("@1\nPosted by @2\n@3 days ago", minetest.formspec_escape(bulletin.title), bulletin.owner, days_ago).."]"
			else
				formspec[#formspec+1] = "tooltip[button_"..i..";"..tip.."]"
			end
		end
	end
	formspec[#formspec+1] = "container_end[]"

	bulletin_boards.player_state[player_name] = {board=board_name}
	minetest.show_formspec(player_name, "bulletin_boards:board", table.concat(formspec))
end

-- shows a specific bulletin on a board
local function show_bulletin(player, board_name, index)
	local board = get_board(board_name)
	local def = bulletin_boards.board_def[board_name]
	local icons = def.icons
	local bulletin = board[index] or {}
	local player_name = player:get_player_name()
	bulletin_boards.player_state[player_name] = {board=board_name, index=index}

	local tip
	local has_cost
	if def.cost then
		local stack = ItemStack(def.cost)
		local player_inventory = minetest.get_inventory({type="player", name=player_name})
		tip = S("Post bulletin with this icon at the cost of @1 @2", stack:get_count(), get_item_desc(stack))
		has_cost = player_inventory:contains_item("main", stack)
	else
		tip = S("Post bulletin with this icon")
		has_cost = true
	end

	local admin = minetest.check_player_privs(player, "server")

	local formspec = {"size[8,8]"
		.."button[0.2,0;1,1;prev;"..S("Prev").."]"
		.."button[6.65,0;1,1;next;"..S("Next").."]"}
	local esc = minetest.formspec_escape
	if ((bulletin.owner == nil or bulletin.owner == player_name) and has_cost) or admin then
		formspec[#formspec+1] =
			"field[1.5,0.75;5.5,0;title;"..S("Title:")..";"..esc(bulletin.title or "").."]"
			.."textarea[0.5,1.15;7.5,7;text;"..S("Contents:")..";"..esc(bulletin.text or "").."]"
			.."label[0.3,7;"..S("Post:").."]"
		for i, icon in ipairs(icons) do
			formspec[#formspec+1] = "image_button[".. i*0.75-0.5 ..",7.35;1,1;"..icon..";save_"..i..";]"
			.."tooltip[save_"..i..";"..tip.."]"
		end
		formspec[#formspec+1] = "image_button[".. (#icons+1)*0.75-0.25 ..",7.35;1,1;bulletin_boards_delete.png;delete;]"
			.."tooltip[delete;"..S("Delete this bulletin").."]"
			.."label["..(#icons+1)*0.75-0.25 ..",7;"..S("Delete:").."]"
	elseif bulletin.owner then
		formspec[#formspec+1] =
			"label[1.4,0.5;"..S("Posted by @1", bulletin.owner).."]"
			.."tablecolumns[color;text]"
			.."tableoptions[background=#00000000;highlight=#00000000;border=false]"
			.."table[1.35,0.25;5,0.5;title;#FFFF00,"..esc(bulletin.title or "").."]"
			.."textarea[0.5,1.5;7.5,7;;"..esc(bulletin.text or "")..";]"
			.."button[2.5,7.5;3,1;back;" .. S("Back to Board") .. "]"
		if bulletin.owner == player_name then
			formspec[#formspec+1] = "image_button[".. (#icons+1)*0.75-0.25 ..",7.35;1,1;bulletin_boards_delete.png;delete;]"
				.."tooltip[delete;"..S("Delete this bulletin").."]"
				.."label["..(#icons+1)*0.75-0.25 ..",7;"..S("Delete:").."]"
		end
	else
		return
	end

	minetest.show_formspec(player_name, "bulletin_boards:bulletin", table.concat(formspec))
end

-- interpret clicks on the base board
minetest.register_on_player_receive_fields(function(player, formname, fields)
	if formname ~= "bulletin_boards:board" then return end
	local player_name = player:get_player_name()
	for field, state in pairs(fields) do
		if field:sub(1, #"button_") == "button_" then
			local i = tonumber(field:sub(#"button_"+1))
			local state = bulletin_boards.player_state[player_name]
			if state then
				show_bulletin(player, state.board, i)
			end
			return
		end
	end
end)

-- interpret clicks on the bulletin
minetest.register_on_player_receive_fields(function(player, formname, fields)
	if formname ~= "bulletin_boards:bulletin" then return end
	local player_name = player:get_player_name()
	local state = bulletin_boards.player_state[player_name]
	if not state then return end
	local board = get_board(state.board)
	local def = bulletin_boards.board_def[state.board]
	if not board then return end

	-- no security needed on these actions
	if fields.back then
		bulletin_boards.player_state[player_name] = nil
		show_board(player_name, state.board)
	end

	if fields.prev then
		local next_index = find_prev(board, state.index)
		show_bulletin(player, state.board, next_index)
		return
	end
	if fields.next then
		local next_index = find_next(board, state.index)
		show_bulletin(player, state.board, next_index)
		return
	end

	if fields.quit then
		minetest.after(0.1, show_board, player_name, state.board)
	end

	-- check if the player's allowed to do the stuff after this
	local admin = minetest.check_player_privs(player, "server")
	local current_bulletin = board[state.index]
	if not admin and (current_bulletin and current_bulletin.owner ~= player_name) then
		-- someone's done something funny. Don't be accusatory, though - could be a race condition
		return
	end

	if fields.delete then
		board[state.index] = nil
		fields.title = ""
		save_boards()
	end

	local player_inventory = minetest.get_inventory({type="player", name=player_name})
	local has_cost = true
	if def.cost then
		has_cost = player_inventory:contains_item("main", def.cost)
	end

	if fields.text ~= "" and (has_cost or admin) then
		for field, _ in pairs(fields) do
			if field:sub(1, #"save_") == "save_" then
				local i = tonumber(field:sub(#"save_"+1))
				local bulletin = {}
				bulletin.owner = player_name
				bulletin.title = fields.title:sub(1, max_title_size)
				bulletin.text = fields.text:sub(1, max_text_size)
				bulletin.icon = def.icons[i]
				bulletin.timestamp = minetest.get_gametime()
				board[state.index] = bulletin
				if not admin and def.cost then
					player_inventory:remove_item("main", def.cost)
				end
				save_boards()
				break
			end
		end
	end

	bulletin_boards.player_state[player_name] = nil
	show_board(player_name, state.board)
end)

-- default icon set
local base_icons = {
	"bulletin_boards_document_comment_above.png",
	"bulletin_boards_document_back.png",
	"bulletin_boards_document_next.png",
	"bulletin_boards_document_image.png",
	"bulletin_boards_document_signature.png",
	"bulletin_boards_to_do_list.png",
	"bulletin_boards_documents_email.png",
	"bulletin_boards_receipt_invoice.png",
}

-- generates a random jumble of icons to superimpose on a bulletin board texture
-- rez is the "working" canvas size. 32-pixel icons get scattered on that canvas
-- before it is scaled down to 16 pixels
local function generate_random_board(rez, count, icons)
	icons = icons or base_icons
	local tex = {"([combine:"..rez.."x"..rez}
	for i = 1, count do
		tex[#tex+1] = ":"..math.random(1,rez-32)..","..math.random(1,rez-32)
			.."="..icons[math.random(1,#icons)]
	end
	tex[#tex+1] = "^[resize:16x16)"
	return table.concat(tex)
end

local function register_board(board_name, board_def)
	bulletin_boards.board_def[board_name] = board_def
	local background = board_def.background or "bulletin_boards_corkboard.png"
	local foreground = board_def.foreground or "bulletin_boards_frame.png"
	local tile = background.."^"..generate_random_board(98, 7, board_def.icons).."^"..foreground
	local bulletin_board_def = {
		description = board_def.desc,
		groups = {choppy=1},
		tiles = {tile},
		inventory_image = tile,
		paramtype = "light",
		paramtype2 = "wallmounted",
		sunlight_propagates = true,
		drawtype = "nodebox",
		node_box = {
			type = "wallmounted",
			wall_top    = {-0.5, 0.4375, -0.5, 0.5, 0.5, 0.5},
			wall_bottom = {-0.5, -0.5, -0.5, 0.5, -0.4375, 0.5},
			wall_side   = {-0.5, -0.5, -0.5, -0.4375, 0.5, 0.5},
		},

		on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
			local player_name = clicker:get_player_name()
			show_board(player_name, board_name)
		end,

		on_construct = function(pos)
			local meta = minetest.get_meta(pos)
			meta:set_string("infotext", board_def.desc or "")
		end,
	}

	minetest.register_node(board_name, bulletin_board_def)
end

if minetest.get_modpath("default") then

register_board("bulletin_boards:bulletin_board_basic", {
	desc = S("Public Bulletin Board"),
	cost = "default:paper",
	icons = base_icons,
})
minetest.register_craft({
	output = "bulletin_boards:bulletin_board_basic",
	recipe = {
		{'group:wood', 'group:wood', 'group:wood'},
		{'group:wood', 'default:paper', 'group:wood'},
		{'group:wood', 'group:wood', 'group:wood'},
	},
})

register_board("bulletin_boards:bulletin_board_copper", {
	desc = S("Copper Board"),
	cost = "default:copper_ingot",
	foreground = "bulletin_boards_frame_copper.png",
	icons = base_icons,
})
minetest.register_craft({
	output = "bulletin_boards:bulletin_board_copper",
	recipe = {
		{"default:copper_ingot", "default:copper_ingot", "default:copper_ingot"},
		{"default:copper_ingot", 'default:paper', "default:copper_ingot"},
		{"default:copper_ingot", "default:copper_ingot", "default:copper_ingot"},
	},
})
end


code:
# textdomain: bulletin_boards

### init.lua ###

#Appended to the title of a bulletin board to indicate how much it costs to post a bulletin here (@1 is a number, @2 is an item name)
, Cost: @1 @2=
#Tooltip. Title of bulletin, author of bulletin, how long ago the bulletin was posted.
@1@nPosted by @2@n@3 days ago=
#Button label
Back to Board=
#Text area label
Contents:=
#title of a bulletin board
Copper Board=
#tooltip
Delete this bulletin=
#label
Delete:=
#Button label
Next=
#tooltip
Post bulletin with this icon=
#tooltip
Post bulletin with this icon at the cost of @1 @2=
#tooltip
Post your bulletin here=
#tooltip
Post your bulletin here for the cost of @1 @2=
#label
Post:=
#player who posted this bulletin
Posted by @1=
#Button label, short for "previous"
Prev=
#title of a bulletin board
Public Bulletin Board=
#Text field label
Title:=


code:
name=bulletin_boards
description = Allows creation of global bulletin boards where players can post public messages
optional_depends = default


code:
## Bulletin boards

This mod adds global bulletin boards to Minetest. These are boards where players can post short notes for other players to see, at a nominal cost.

| ![](screenshot.png) | ![](screenshot_bulletin.png) |

Each board can hold up to 56 bulletins (in an 8 by 7 grid), with each bulletin having a title and an icon that can be set by the player posting it. Once the bulletin board nears capacity, older bulletins will start being culled from the board to make room for new bulletins. They will be culled by preference starting with the bulletins belonging to the players who have the most bulletins currently posted, followed by the oldest bulletin belonging to those players.

So for example, if Alice has 1 bulletin on the board, Bob has 2 bulletins on the board, and Collin has 2 bulletins on the board, then when it comes time to cull a bulletin the oldest one belonging to either Bob or Collin will be culled. If that happens to be Bob's, then next time it's time to cull Alice will have 1, Bob will have 1, and Collin will have 2, so the oldest of Collin's bulletins will be culled. This ordering is done to try to balance things out fairly - players that hog the board with multiple bulletins will have their bulletins culled more often, but everyone's single most recent bulletin will stick around as long as possible.

Bulletin boards can have a cost associated with posting. If the player has the cost (an item stack) in their inventory, they can post and the cost will be automatically deducted.

Boards are "global", in the sense that all instances of a given type of board will have the same content regardless of where they are in the world. This can make them useful as a means of communication over distance as well as time, serving as a sort of public post office.


code:
# Bweapons modpack
Bweapons aims at providing a full progression of ranged weapons, from primitive bows to sci-fi rifles or magic staves and spellbooks. Modpack also provides an API to easily make your own weapons.
Content packs are split into separate mods, so if you don't like something (for example you only want to leave magic), you can simply disable them.

## Requirements
**Minetest 5.0.0+**
**Minetest_game 5.0.0+**

Full list of requirements:

- default, tnt (parts of minetest_game, default)
- [technic](https://github.com/minetest-mods/technic) (optional, but strongly recommended)
- [mana](https://repo.or.cz/minetest_mana.git) (optional, but strongly recommended)
- [magic_materials](https://github.com/ClockGen/magic_materials) (dependency for bweapons_magic_pack)
- [basic_materials](https://gitlab.com/VanessaE/basic_materials) (dependency of technic, and also for bweapons_firearms_pack)

## Recommended mods
- [gadgets_modpack](https://github.com/ClockGen/gadgets_modpack) (Together with gadgets_modpack forms a single combat, status effect and magic system)
- [craftguide](https://github.com/minetest-mods/craftguide) (explore crafting recipes of the new items)

# List of mods in modpack
- ### bweapons_api

    API, implementing all of bweapons functionality and required by actual weapon packs

    **Requires** : default, [technic](https://github.com/minetest-mods/technic)  (optional), [mana](https://repo.or.cz/minetest_mana.git) (optional)

- ### bweapons_firearms_pack

    Pack of weapons, consisting of:

    - Pump-action shotgun
    - Double-barreled shotgun
    - Pistol
    - Rifle
    - Grenade launcher

    **Requires** : default, tnt, [basic_materials](https://gitlab.com/VanessaE/basic_materials), [technic](https://github.com/minetest-mods/technic)

- ### bweapons_bows_pack

    Pack of weapons, consisting of:

    - Wooden bow
    - Crossbow

    **Requires** : default

- ### bweapons_hitech_pack

    Pack of weapons, consisting of:

    - Particle Blaster
    - Laser Gun
    - Plasma Gun
    - Rail Gun
    - Rocket Launcher

    **Requires** : default, tnt, [basic_materials](https://gitlab.com/VanessaE/basic_materials), [technic](https://github.com/minetest-mods/technic)

- ### bweapons_magic_pack

    Pack of weapons, consisting of:

    - Tome Of Fireball
    - Tome Of Ice Shard
    - Tome Of Electrosphere
    - Staff Of Light
    - Staff Of Magic
    - Staff Of Void

    **requires**: default, [magic_materials](https://github.com/ClockGen/magic_materials)

- ### bweapons_utility_pack

    Pack of utilities, consisting of:

    - Torch bow

    **Requires** : default

## Tips
Magic weapons have following world interactions:

- Fireball will set some nodes on fire, if server fire is enabled
- Ice shard will freeze water and cool down lava sources into obsidian and flowing lava into stone
- Electrosphere will turn sand into glass
- Void staff will destroy a single node it hits

All interactions respect area protection

Railgun, apart from requiring ammo also needs to be charged with technic EUs.

Magic tomes use mana and don't wear down, wear bar of staves represent their "charge", and they can be recharged by combining them with februm crystals on the crafting grid.

## Settingtypes
Modpack provides some settings, accessible via "Settings->All Settings->Mods->bweapons_api
You can also put these settings directly to your minetest.conf

```
bweapons_combine_velocity = false
Combines current player velocity with projectile velocity. Disabled by default since it modifies
projectile speed and can potentionally decrease collision detection effectivenes (if it's too fast).

bweapons_damage_multiplier = 1
Global weapon damage multiplier.

bweapons_projectile_raycast_distance = 0.5
Length of a projectile raycast, performed on each server step, used for collision detection.
Bigger values means better detection, but also the projectile would stop further from the
actual target.
```

## Making your own weapons
To define weapons in your own mod you need to call `bweapons.register_weapon(def)`
where `def` is a definition table. To see a complete list of possible definition options (with comments)
refer to **[this document](bweapons_api/documentation.txt)**.

Although not required, but bweapons_api provides a second function, for
convenient ammo registration `bweapons.register_ammo(def)`. List of available definition
options can be found in the documentation mentioned above.

Also, you can see already defined weapon packs for reference, however, keep in mind, they don't contain all possible options.

## Limitations
Collision detection for projectiles is not perfect. It's much better than it was before (prior to minetest 5.0.0 it was very hacky, now it uses raycasting), however keep in mind to not make projectiles too fast, otherwise they might go through objects and nodes.
If you still want to make projectiles fast, there's a settingtypes.txt option that you can put in your minetest.conf, to increase length of the raycast ray that's used for collision detection in projectiles:
```
bweapons_projectile_raycast_distance = 0.5
```

## License
All code is GPLv3 [link to the license](https://www.gnu.org/licenses/gpl-3.0.en.html)
All resources not covered in the "credits" section are licensed under CC BY 4.0 [link to the license](https://creativecommons.org/licenses/by/4.0/legalcode)

## Credits
Sounds from following users of freesound.org were mixed, cut, edited and used to produce sounds in this modpack:

- firestorm185 - energy hum sound
- qubodup - explosion
- kastenfrosch - cannonball
- qubodup - cannon shot
- panxozerok - instant chill
- selector - rocket launch
- florianreichelt - huge explosion
- wcoltd - laser4
- adrimb96 - fairy sound
- mattiagiovanetti - laser gun shots iii
- tjcason - shortlasersound
- nettimatto - mini crossbow foley
- qubodup - launching shooting propelled grenade
- saturdaysoundguy - longbow release 1
- kinoton - whoosh 1
- drmaysta - cartoon arrow hit
- arcandio - razorback archery
- lensflare8642 - shotgun sounds
- nioczkus - 1911 reload
- lemudcrab - grenade launcher
- nioczkus - browning hi power
- nioczkus - darkscape 1911 a1
- qubodup - fire-spell
- samararaine - bonfire being lit
- midimagician - fire burning loop
- qubodup - tree burns down
- qubodup - fire magic spell sound effect
- spookymodem - magicsmite
- lamamakesmusic - step ice break 01
- steffcaffrey - chimes
- spookymodem - hitting wall
- adrimb86 - fairy sound
- terminallynerdy - timestop
- slugzilla - phoenixscreech1
- bajko - sfx thunder blast
- joelaudio - electric zap 001
- dave-welsh - thunder-clap-owb-ky-441x16
- quaker540 - supernatural-explosion
- northern87 - barbecue-fire-northern87
- cyberkineticfilms - strange-teleport-sound
- slugzilla - phoenixscreech1
- zenithinfinitivestudios - fantasy-ui-button-1
- 221beimesche - glass-shattering-and-falling
- sclolex - tunneldrone

All sounds listed above were licensed as CC0. Produced sounds are licensed under CC BY 4.0 [link to the license](https://creativecommons.org/licenses/by/4.0/legalcode)


code:
bweapons = {}
local players = {}

--Global settings
local damage_multiplier = minetest.settings:get("bweapons_damage_multiplier") or 1
local combine_velocity = minetest.settings:get("bweapons_combine_velocity") or false
local projectile_raycast_dist = minetest.settings:get("bweapons_projectile_raycast_distance") or 0.5

--Utility functions
minetest.register_on_joinplayer(function(player)
    local name = player:get_player_name()
    players[name] = {reloading=false}
    end
)

minetest.register_on_leaveplayer(function(player)
    local name = player:get_player_name()
    players[name] = nil
    end
)

local function reset_player_cooldown(user)
    if user ~= nil then
        local name = user:get_player_name()
        players[name] = {reloading=false}
    end
end

function bweapons.get_spread(spread)
    return (math.random(-32768, 32768)/65536)*spread
end

function bweapons.damage_aoe(damage, puncher, pos, radius)
    local targs = minetest.get_objects_inside_radius({x=pos.x,y=pos.y,z=pos.z}, radius)
    for _,t in pairs(targs) do
        local dist=math.sqrt(((t:get_pos().x-pos.x)^2)+((t:get_pos().y-pos.y)^2)+((t:get_pos().z-pos.z)^2))
        local DistDamage=damage/math.max(dist, 1)
        t:punch(puncher, 1.0, {
            full_punch_interval=1.0,
            damage_groups={fleshy=DistDamage},
        }, nil)
    end
end

--Utility projectile registration function
local function register_projectile(def)

    local aglow = 0

    if def.glow then
        aglow = 14
    end

    --Main projectile registration function
    minetest.register_entity(def.name, {
        physical = false,
        is_visible = true,
        static_save = false,
        visual = "sprite",
        visual_size = {x = def.visual_size, y = def.visual_size, z = def.visual_size},
        textures = {def.texture},
        glow = aglow,
        pointable = false,

        --Internal variables
        timer = 0,
        owner = "",
        node_hit = false,
        previous_pos = {},

        --Performing checks every server step
        on_step = function(self, dtime)

            self.timer = self.timer + 1
            local pos = self.object:get_pos()

            --Trail particle spawner
            if def.trail_particle then
                minetest.add_particlespawner({
                    amount = def.trail_particle_amount,
                    time = 0.05,
                    minpos = {x=pos.x-def.trail_particle_displacement, y=pos.y-def.trail_particle_displacement, z=pos.z-def.trail_particle_displacement},
                    maxpos = {x=pos.x+def.trail_particle_displacement, y=pos.y+def.trail_particle_displacement, z=pos.z+def.trail_particle_displacement},
                    minvel = {x=-def.trail_particle_velocity, y=-def.trail_particle_velocity, z=-def.trail_particle_velocity},
                    maxvel = {x=def.trail_particle_velocity, y=def.trail_particle_velocity, z=def.trail_particle_velocity},
                    minacc = {x=0, y=def.trail_particle_gravity, z=0},
                    maxacc = {x=0, y=def.trail_particle_gravity, z=0},
                    minexptime = 1,
                    maxexptime = 3,
                    minsize = def.trail_particle_min_size,
                    maxsize = def.trail_particle_size,
                    texture = def.trail_particle,
                    glow=def.trail_particle_glow,
                })
            end

            --Hit detection
            local delta = {x = pos.x-self.previous_pos.x, y = pos.y - self.previous_pos.y, z = pos.z - self.previous_pos.z}
            local ray = minetest.raycast(pos, {
                                               x = pos.x + delta.x * projectile_raycast_dist,
                                               y = pos.y + delta.y * projectile_raycast_dist,
                                               z = pos.z + delta.z * projectile_raycast_dist
                                               }, true, def.liquids_stop)
            local target = ray:next()
            if target and target.type == "node" then
                local node = minetest.get_node_or_nil(minetest.get_pointed_thing_position(target, false))
                if node and minetest.registered_nodes[node.name] and (minetest.registered_nodes[node.name].walkable or
                def.liquids_stop and minetest.registered_nodes[node.name].liquidtype ~= "none") then
                    if def.aoe then
                        bweapons.damage_aoe(def.damage, self.owner, target.intersection_point, def.aoe_radius)
                    end
                    if def.on_hit then
                        def.on_hit(self, target)
                    end
                    self.node_hit = true
                end
            elseif target and target.type == "object" then
                if def.aoe then
                    bweapons.damage_aoe(def.damage, self.owner, target.intersection_point, def.aoe_radius)
                else
                    target.ref:punch(self.owner, 1.0, {
                    full_punch_interval=1.0,
                    damage_groups={fleshy=def.damage},
                    }, nil)
                end
                if def.on_hit then
                    def.on_hit(self, target)
                end
                self.node_hit = true
            end

            --Spawn hit particles, execute on_hit if defined, drop an item if defined and remove the object
            if self.node_hit then
                --Little hit flares
                if def.hit_flare then
                    minetest.add_particle({
                        pos = self.previous_pos,
                        expirationtime = 0.2,
                        size = def.hit_flare_size,
                        collisiondetection = false,
                        vertical = false,
                        texture = def.hit_flare,
                        glow = def.hit_flare_glow,
                })
                end
                --Hit particles
                if def.hit_particle then
                    minetest.add_particlespawner({
                        amount = def.hit_particle_amount,
                        time = 0.05,
                        minpos = self.previous_pos,
                        maxpos = self.previous_pos,
                        minvel = {x=-def.hit_particle_velocity, y=-def.hit_particle_velocity, z=-def.hit_particle_velocity},
                        maxvel = {x=def.hit_particle_velocity, y=def.hit_particle_velocity+1, z=def.hit_particle_velocity},
                        minacc = {x=0, y=def.hit_particle_gravity, z=0},
                        maxacc = {x=0, y=def.hit_particle_gravity, z=0},
                        minexptime = 2,
                        maxexptime = 4,
                        minsize = def.hit_particle_min_size,
                        maxsize = def.hit_particle_size,
                        collisiondetection = false,
                        collision_removal = false,
                        object_collision = false,
                        vertical = false,
                        texture = def.hit_particle,
                        glow = def.hit_particle_glow,
                        })
                end
                --Hit sound
                if def.hit_sound then
                    minetest.sound_play(def.hit_sound, {pos=self.previous_pos, gain=def.hit_sound_gain, max_hear_distance=2*64})
                end
                --Drop a drop item, if defined
                if def.drop then
                    if math.random() < def.drop_chance then
                        minetest.add_item(self.previous_pos, def.drop)
                    end
                end
                --Remove the projectile
                self.object:remove()
            end

            --Spawn a hit flare and remove the projectile if it timeouts
            if self.timer > def.timeout then
                if def.flare then
                    minetest.add_particle({
                        pos = self.previous_pos,
                        expirationtime = 0.2,
                        size = math.floor(def.flare_size/2),
                        collisiondetection = false,
                        vertical = false,
                        texture = def.flare,
                        glow = def.flare_glow,
                    })
                end
                if def.on_timeout then def.on_timeout(self) end
                self.object:remove()
            end

            self.previous_pos = pos

        end,
    })
end

local function bweapons_repair(itemstack, player, old_craft_grid, craft_inv, name, repair_with, uses, repair_uses)
    if itemstack == nil or itemstack:get_name() ~= name then return end
    local w_count = 0
    local r_count = 0
    local modstack

    for _,v in pairs(old_craft_grid) do
        if v:get_name() == name then
            w_count = w_count + 1
            modstack = v
        elseif v:get_name() == repair_with then
            r_count = r_count + 1
        end
    end

    if w_count == 1 and r_count == 1 then
        local wear = modstack:get_wear()
        if wear > 0 then
            wear = wear - (65535/uses * repair_uses)
            if wear < 0 then wear = 0 end
            modstack:set_wear(wear)
            return modstack
        end
    end

end

function bweapons.register_weapon(def)

    --Stop registration if mod requirements are not met or not enough definition fields
    if def.has_durability and not minetest.get_modpath("technic") and def.requires_technic then
        minetest.log("error", "Technic modpack is required for technic-powered weapons!")
        return
    end

    if def.mana_per_use and not minetest.get_modpath("mana") then
        minetest.log("error", "Mana required for mana-based weapons!")
        return
    end

    if def.name == nil or def.description == nil or def.texture == nil then
        minetest.log("error", "Missing fields in weapon registration! Check api documentation")
        return
    end

    --Setting defaults if fields are not defined in definition
    local damage = def.damage or 1
    local technic_charge = def.technic_charge or 100000
    local uses = def.uses or 50
    local technic_charge_per_use = technic_charge/uses
    local hitscan = def.hitscan or false
    local spawndist = def.spawn_distance or 1
    local shot_amount = def.shot_amount or 1
    local spread = def.spread or 0
    local cooldown = def.cooldown or 0
    local liquids_stop = def.liquids_stop or false
    local distance = def.distance or 10
    local penetration = def.penetration or 1
    local aoe = def.aoe or false
    local aoe_radius = def.aoe_radius or 5
    local flare_size = def.flare_size or 10
    local flare_glow = 0
    local hit_flare_size = def.hit_flare_size or 0.5
    local hit_flare_glow = 0
    local ammo_per_shot = def.ammo_per_shot or 1
    local repair_uses = def.repair_uses or 8
    local hit_particle_velocity = def.hit_particle_velocity or 2
    local hit_particle_gravity = def.hit_particle_gravity or -10
    local hit_particle_size = def.hit_particle_size or 3
    local hit_particle_min_size = math.floor(hit_particle_size/2)
    local hit_particle_amount = def.hit_particle_amount or 32
    local hit_particle_glow = 0
    local drop_chance = def.drop_chance or 0.9
    local fire_sound_gain = def.fire_sound_gain or 1
    local hit_sound_gain = def.hit_sound_gain or 0.25
    local reload_sound_gain = def.reload_sound_gain or 0.25
    local trail_particle_distance = def.hitscan_particle_distance or 0.5
    local trail_particle_velocity = def.trail_particle_velocity or 1
    local trail_particle_gravity = def.trail_particle_gravity or 0
    local trail_particle_size = def.trail_particle_size or 1
    local trail_particle_min_size = math.floor(trail_particle_size/2)
    local trail_particle_amount = def.trail_particle_amount or 4
    local trail_particle_displacement = def.trail_particle_displacement or 0.5
    local trail_particle_glow = 0
    local tool_groups = {}

    local craft_shape = "shaped"

    if def.shapeless_recipe then
        craft_shape = "shapeless"
    end

    if def.flare_glow then
        flare_glow = 14
    end

    if def.hit_particle_glow then
        hit_particle_glow = 14
    end

    if def.hit_flare_glow then
        hit_flare_glow = 14
    end

    if def.trail_particle_glow then
        trail_particle_glow = 14
    end

    damage = damage * damage_multiplier

    local projectile_speed = def.projectile_speed or 15
    local projectile_gravity = def.projectile_gravity or 0
    local projectile_timeout = def.projectile_timeout or 25
    local projectile_texture = def.projectile_texture or "bweapons_api_missing_texture.png"
    local projectile_glow = def.projectile_glow or false
    local projectile_visual_size = def.projectile_visual_size or 1

    local on_refill = nil
    local wear_represents = "mechanical_wear"

    if def.requires_technic then
        on_refill = technic.refill_RE_charge
        wear_represents = "technic_RE_charge"
    elseif def.custom_charge then
        wear_represents = "bweapons_custom_charge"
    end

    if not def.tool_repair then
        tool_groups["disable_repair"] = 1
    end

    --Make a projectile definition and register projectile for the weapon, if hitscan=false
    if not hitscan then
        local projectiledef = {
            name=def.name.."_projectile",
            damage=damage,
            timeout=projectile_timeout,
            aoe=aoe,
            aoe_radius=aoe_radius,
            visual_size=projectile_visual_size,
            texture=projectile_texture,
            glow=projectile_glow,
            trail_particle=def.trail_particle,
            trail_particle_velocity=trail_particle_velocity,
            trail_particle_gravity=trail_particle_gravity,
            trail_particle_glow=trail_particle_glow,
            trail_particle_size=trail_particle_size,
            trail_particle_min_size=trail_particle_min_size,
            trail_particle_amount=trail_particle_amount,
            trail_particle_displacement=trail_particle_displacement,
            flare=def.flare,
            flare_size=flare_size,
            flare_glow=flare_glow,
            hit_flare=def.hit_flare,
            hit_flare_size=hit_flare_size,
            hit_flare_glow=hit_flare_glow,
            hit_particle=def.hit_particle,
            hit_particle_glow=hit_particle_glow,
            hit_particle_velocity=hit_particle_velocity,
            hit_particle_size=hit_particle_size,
            hit_particle_gravity=hit_particle_gravity,
            hit_particle_min_size=hit_particle_min_size,
            hit_particle_amount=hit_particle_amount,
            hit_sound=def.hit_sound,
            hit_sound_gain=hit_sound_gain,
            drop=def.drop,
            drop_chance=drop_chance,
            on_hit=def.on_hit,
            on_timeout=def.on_timeout,
            liquids_stop=liquids_stop,
            }
        register_projectile(projectiledef)
    end

    --Main tool registration function
    minetest.register_tool(def.name, {
        description = def.description,
        inventory_image = def.texture,
        wield_scale = 1,
        stack_max = 1,
        groups = tool_groups,
        wear_represents = wear_represents,
        on_refill = on_refill,
        on_use = function(itemstack, user, pointed_thing)

            if not user then return end

            local playername = user:get_player_name()
            local inv = user:get_inventory()

            --Return if player is reloading, not enough mana (if defined) or no ammo (if defined)
            if
            players[playername]["reloading"] or
            def.mana_per_use and mana.get(playername) < def.mana_per_use or
            def.ammo_type and not inv:contains_item("main", {name=def.ammo_type, count=ammo_per_shot})
            then
                if def.reload_sound then
                    minetest.sound_play(def.reload_sound, {object=user, gain=reload_sound_gain, max_hear_distance=2*64})
                end
                return
            end

            if def.has_durability then
                local meta = minetest.deserialize(itemstack:get_metadata())
                if
                def.requires_technic and not meta or
                def.requires_technic and meta.charge < technic_charge_per_use or
                not def.requires_technic and def.custom_charge and (65535 - itemstack:get_wear()) < (65535 / uses)
                then
                    if def.reload_sound then
                        minetest.sound_play(def.reload_sound, {object=user, gain=reload_sound_gain, max_hear_distance=2*64})
                    end
                    return
                end

                if def.requires_technic then
                    meta.charge = meta.charge - technic_charge_per_use
                    technic.set_RE_wear(itemstack, meta.charge, technic_charge)
                    itemstack:set_metadata(minetest.serialize(meta))
                else
                    local wear = itemstack:get_wear()
                    wear = wear + (65535/uses)
                    if def.custom_charge and wear > 65535 then wear = 65535 end
                    itemstack:set_wear(wear)
                end
            end

            if def.ammo_type then
                inv:remove_item("main", {name=def.ammo_type, count=ammo_per_shot})
            end

            if def.mana_per_use then
                mana.subtract(playername, def.mana_per_use)
            end

            local playerpos = user:getpos()
            local dir = user:get_look_dir()
            local vel = {x = 0, y = 0, z = 0}
            if combine_velocity then
                vel = user:get_player_velocity()
            end

            --Hitscan type, implemented in-place
            if hitscan then
                for i = 0,shot_amount-1,1 do
                    local ray = minetest.raycast({
                                                x=playerpos.x+dir.x*spawndist,
                                                y=playerpos.y+1.5+dir.y*spawndist,
                                                z=playerpos.z+dir.z*spawndist
                                                 },
                                                 {
                                                x=playerpos.x+(dir.x+bweapons.get_spread(spread))*distance,
                                                y=playerpos.y+1.5+(dir.y+bweapons.get_spread(spread))*distance,
                                                z=playerpos.z+(dir.z+bweapons.get_spread(spread))*distance
                                                 },
                                                 true, liquids_stop)
                    local j = 0
                    while j < penetration do
                        local target = ray:next()
                        local dropped = false
                        --Shoot everything except the user
                        if target and target.ref ~= user then
                            if aoe then
                                bweapons.damage_aoe(damage, user, target.intersection_point, aoe_radius)
                            else
                                if target.type == "object" then
                                    target.ref:punch(user, 1.0, {
                                    full_punch_interval=1.0,
                                    damage_groups={fleshy=damage},
                                    }, nil)
                                end
                            end
                            --Custom function called on every hit
                            if def.on_hit then
                                def.on_hit(user, target)
                            end
                            --Little hit flares
                            if def.hit_flare then
                                minetest.add_particle({
                                    pos = target.intersection_point,
                                    expirationtime = 0.2,
                                    size = hit_flare_size,
                                    collisiondetection = false,
                                    vertical = false,
                                    texture = def.hit_flare,
                                    glow = hit_flare_glow,
                            })
                            end
                            --Hit particles
                            if def.hit_particle then
                                minetest.add_particlespawner({
                                    amount = hit_particle_amount,
                                    time = 0.05,
                                    minpos = target.intersection_point,
                                    maxpos = target.intersection_point,
                                    minvel = {x=-hit_particle_velocity, y=-hit_particle_velocity, z=-hit_particle_velocity},
                                    maxvel = {x=hit_particle_velocity, y=hit_particle_velocity+1, z=hit_particle_velocity},
                                    minacc = {x=0, y=hit_particle_gravity, z=0},
                                    maxacc = {x=0, y=hit_particle_gravity, z=0},
                                    minexptime = 2,
                                    maxexptime = 4,
                                    minsize = hit_particle_min_size,
                                    maxsize = hit_particle_size,
                                    collisiondetection = false,
                                    collision_removal = false,
                                    object_collision = false,
                                    vertical = false,
                                    texture = def.hit_particle,
                                    glow = hit_particle_glow,
                            })
                            end
                            --Spawn a drop at the last stop of a hitscan if defined
                            if def.drop and j == penetration - 1 then
                                if math.random() < def.drop_chance then
                                    minetest.add_item(target.intersection_point, def.drop)
                                end
                            end
                            --Hit sound
                            if def.hit_sound then
                                minetest.sound_play(def.hit_sound, {pos=target.intersection_point, gain=hit_sound_gain, max_hear_distance=2*64})
                            end
                        --Don't shoot yourself in the legs and increase penetration count
                        elseif target and target.ref == user then
                            j = j - 1
                        end
                        --Spawn a particle trail that leads to the last surface
                        if def.trail_particle and j == penetration - 1 then
                            local trail_distance = 0
                            if target ~= nil then
                                local delta = {x = target.intersection_point.x - playerpos.x, y = target.intersection_point.y - playerpos.y, z = target.intersection_point.z - playerpos.z}
                                trail_distance = math.abs(delta.x) + math.abs(delta.y) + math.abs(delta.z)
                            else
                                trail_distance = distance
                            end
                            local trail_particles_per_node = math.floor(trail_distance/trail_particle_distance)
                            for z = 0, trail_particles_per_node - 1, 1 do
                                minetest.add_particlespawner({
                                amount = trail_particle_amount,
                                time = 0.05,
                                minpos = {
                                       x = (playerpos.x + dir.x * z * trail_particle_distance) - trail_particle_displacement,
                                       y = (playerpos.y + 1.5 + dir.y * z * trail_particle_distance) - trail_particle_displacement,
                                       z = (playerpos.z + dir.z * z * trail_particle_distance) - trail_particle_displacement
                                      },
                                maxpos = {
                                       x = (playerpos.x + dir.x * z * trail_particle_distance) + trail_particle_displacement,
                                       y = (playerpos.y + 1.5 + dir.y * z * trail_particle_distance) + trail_particle_displacement,
                                       z = (playerpos.z + dir.z * z * trail_particle_distance) + trail_particle_displacement
                                      },
                                minvel = {x=-trail_particle_velocity, y=-trail_particle_velocity, z=-trail_particle_velocity},
                                maxvel = {x=trail_particle_velocity, y=trail_particle_velocity, z=trail_particle_velocity},
                                minacc = {x=0, y=trail_particle_gravity, z=0},
                                maxacc = {x=0, y=trail_particle_gravity, z=0},
                                minexptime = 2,
                                maxexptime = 4,
                                minsize = trail_particle_min_size,
                                maxsize = trail_particle_size,
                                collisiondetection = false,
                                collision_removal = false,
                                object_collision = false,
                                vertical = false,
                                texture = def.trail_particle,
                                glow = trail_particle_glow,
                                })
                            end
                        end
                        j = j + 1
                    end
                end
            else
                --Projectile type, implemented in another function (see register_projectile())
                for i = 0,shot_amount-1,1
                do
                    local obj = minetest.add_entity({x=playerpos.x+dir.x*spawndist,
                                                     y=playerpos.y+1.5+dir.y*spawndist,
                                                     z=playerpos.z+dir.z*spawndist},
                                                    def.name.."_projectile")
                    if not obj then return end
                    --Set projectile owner
                    obj:get_luaentity().owner = user
                    --Set previous pos when just spawned
                    obj:get_luaentity().previous_pos = {x = playerpos.x, y = playerpos.y + 1, z = playerpos.z}
                    --Combined velocity with player velocity
                    obj:setvelocity({x=((dir.x+bweapons.get_spread(spread))*projectile_speed)+vel.x,
                                    y=((dir.y+bweapons.get_spread(spread))*projectile_speed)+vel.y,
                                    z=((dir.z+bweapons.get_spread(spread))*projectile_speed)+vel.z})
                    obj:setacceleration({x=0, y=projectile_gravity, z=0})
                    obj:setyaw(user:get_look_yaw()+math.pi)
                end
            end

            --Fire flash
            if def.flare then
                minetest.add_particle({
                    pos = {x=playerpos.x+dir.x*spawndist, y=playerpos.y+1.4+dir.y*spawndist, z=playerpos.z+dir.z*spawndist},
                    expirationtime = 0.1,
                    size = flare_size,
                    collisiondetection = false,
                    vertical = false,
                    texture = def.flare,
                    glow = flare_glow,
                })
            end

            --Fire sound
            if def.fire_sound then minetest.sound_play(def.fire_sound, {object=user, gain=fire_sound_gain, max_hear_distance=2*64}) end

            --Add an item to the user inventory, if defined
            if def.return_item then
                if inv:room_for_item("main", {name = def.return_item}) then
                    inv:add_item("main", {name = def.return_item})
                else
                    local obj = minetest.add_item({x = playerpos.x, y = playerpos.y + 1.5, z = playerpos.z}, def.return_item)
                    obj:setvelocity({x = dir.x * 5, y = dir.y * 5, z = dir.z * 5})
                end
            end

            --Custom function executed on fire
            if def.on_fire then
                def.on_fire(itemstack, user, pointed_thing)
            end

            --Set reloading flag and timer
            players[playername] = {reloading=true}
            minetest.after(cooldown, reset_player_cooldown, user)

            return itemstack
        end,
    })

    --Register a crafting recipe
    if def.recipe then
        for _,v in pairs(def.recipe) do
            minetest.register_craft({
            type = craft_shape,
            output = def.name,
            recipe = v,
            replacements = def.replacements,
            })
        end
    end

    --Register tool as technic_powered
    if def.requires_technic then
        technic.register_power_tool(def.name, technic_charge)
    end

        --Register a new craft to repair the tool with it, if defined
    if def.repair_with then
        minetest.register_craft({
            type = "shapeless",
            output = def.name,
            recipe = {
                def.name,
                def.repair_with,
            },
        })

        minetest.register_on_craft(
            function(itemstack, player, old_craft_grid, craft_inv)
                local stack = bweapons_repair(itemstack, player, old_craft_grid, craft_inv, def.name, def.repair_with, uses, repair_uses)
                return stack
            end)

        minetest.register_craft_predict(
            function(itemstack, player, old_craft_grid, craft_inv)
                local stack = bweapons_repair(itemstack, player, old_craft_grid, craft_inv, def.name, def.repair_with, uses, repair_uses)
                return stack
            end)

    end

end

--Main ammo registration function
function bweapons.register_ammo(def)

    local amount = def.amount or 1

    minetest.register_craftitem(def.name,{
        description = def.description,
        inventory_image = def.texture,
        wield_image = def.texture,
    })

    if def.recipe then
        for _,v in pairs(def.recipe) do
            minetest.register_craft({
            type = "shaped",
            output = def.name .. " " .. amount,
            recipe = v,
            })
        end
    end

end