code:
# textdomain: craftguide

Craft Guide=Guide de recettes
Crafting Guide=Guide de recettes
Crafting Guide Sign=Guide de recettes
Bookmarks=Favoris
Usage @1 of @2=Usage @1 de @2
Recipe @1 of @2=Recette @1 de @2
No recipes=Pas de recettes
No usages=Pas d'usages
Burning time: @1=Temps de combustion : @1
Cooking time: @1=Temps de cuisson : @1
Replaced by @1 on smelting=Remplacé par @1 lors de la cuisson
Replaced by @1 on burning=Remplacé par @1 lors de la combustion
Replaced by @1 on crafting=Remplacé par @1 lors de la fabrication
Repairable by step of @1=Réparable par étape de @1
Any item belonging to the group(s): @1=Tout item appartenant au(x) groupe(s) : @1
Any black dye=Quelconque colorant noir
Any black flower=Quelconque fleur noire
Any blue dye=Quelconque colorant bleu
Any blue flower=Quelconque fleur bleue
Any brown dye=Quelconque colorant marron
Any coal=Quelconque charbon
Any cyan dye=Quelconque colorant bleu ciel
Any dark green dye=Quelconque colorant vert foncé
Any dark grey dye=Quelconque colorant gris foncé
Any green dye=Quelconque colorant vert
Any green flower=Quelconque fleur verte
Any grey dye=Quelconque colorant gris
Any kind of stone block=Quelconque roche
Any magenta dye=Quelconque colorant magenta
Any orange dye=Quelconque colorant orange
Any orange flower=Quelconque fleur orange
Any pink dye=Quelconque colorant rose
Any red dye=Quelconque colorant rouge
Any red flower=Quelconque fleur rouge
Any sand=Quelconque sable
Any stick=Quelconque bâton
Any tree=Quelconque tronc d'arbre
Any vessel=Quelconque couvert
Any violet dye=Quelconque colorant violet
Any violet flower=Quelconque fleur violette
Any white dye=Quelconque colorant blanc
Any white flower=Quelconque fleur blanche
Any wood planks=Quelconques planches de bois
Any wool=Quelconque laine
Any yellow dye=Quelconque colorant jaune
Any yellow flower=Quelconque fleur jaune
Recipe's too big to be displayed (@1x@2)=La recette est trop grande pour être affichée (@1x@2)
Shapeless=Sans forme
Cooking=Cuisson
No item to show=Aucun item à afficher
Collect items to reveal more recipes=Collecte des items pour révéler plus de recettes
Show recipe(s) of the pointed node=Affiche les recettes du bloc visé
No node pointed=Aucun bloc visé
You don't know a recipe or usage for this item=Tu ne connais aucune recette pour ce bloc
No recipe or usage for this item=Aucune recette pour ce bloc
Digging=Destruction
Digging Chance=Destruction chanceuse
@1 of chance to drop=@1 de chance de tomber


code:
# textdomain: craftguide

Craft Guide=Guida di assemblaggio
Crafting Guide=Guida d'assemblaggio
Crafting Guide Sign=Cartello della guida di assemblaggio
Bookmarks=Segnalibri
Usage @1 of @2=Utilizzo @1 di @2
Recipe @1 of @2=Ricetta @1 di @2
No recipes=Nessuna ricetta
No usages=Nessun utilizzo
Burning time: @1=Tempo di combustione: @1
Cooking time: @1=Tempo di cottura: @1
Replaced by @1 on smelting=Sostituito da @1 alla fusione
Replaced by @1 on burning=Sostituito da @1 alla combustione
Replaced by @1 on crafting=Sostituito da @1 all'assemblaggio
Repairable by step of @1=Riparabile per passo di @1
Any item belonging to the group(s): @1=Qualunque oggetto appartenente al/ai gruppo/i: @1
Any black dye=Qualunque tintura nera
Any black flower=Qualunque fiore nero
Any blue dye=Qualunque tintura blu
Any blue flower=Qualunque fiore blu
Any brown dye=Qualunque tintura marrone
Any coal=Qualunque carbone
Any cyan dye=Qualunque tintura ciano
Any dark green dye=Qualunque tintura verde scura
Any dark grey dye=Qualunque tintura grigio scura
Any green dye=Qualunque tintura verde
Any green flower=Qualunque fiore verde
Any grey dye=Qualunque tintura grigia
Any kind of stone block=Qualunque tipo di blocco di pietra
Any magenta dye=Qualunque tintura magenta
Any orange dye=Qualunque tintura arancione
Any orange flower=Qualunque fiore arancione
Any pink dye=Qualunque tintura rosa
Any red dye=Qualunque tintura rossa
Any red flower=Qualunque fiore rosso
Any sand=Qualunque sabbia
Any stick=Qualunque bastone
Any tree=Qualunque albero
Any vessel=Qualunque contenitore
Any violet dye=Qualunque tintura viola
Any violet flower=Qualunque fiore viola
Any white dye=Qualunque tintura bianca
Any white flower=Qualunque fiore bianco
Any wood planks=Qualunque asse di legno
Any wool=Qualunque lana
Any yellow dye=Qualunque tintura gialla
Any yellow flower=Qualunque fiore giallo
Recipe's too big to be displayed (@1x@2)=La ricetta è troppo grande per essere mostrata (@1x@2)
Shapeless=Senza forma
Cooking=Cottura
No item to show=Nessun oggetto da mostrare
Collect items to reveal more recipes=Raccogli oggetti per svelare più ricette
Show recipe(s) of the pointed node=Mostra la/le ricetta/e del nodo puntato
No node pointed=Nessun nodo puntato
You don't know a recipe or usage for this item=Non conosci una ricetta o un utilizzo per questo oggetto
No recipe or usage for this item=Nessuna ricetta o utilizzo per questo oggetto
Digging=Scavando
Digging Chance=Probabilità di scavare
@1 of chance to drop=@1 di probabilità di rilascio

repo_name: korkut/MeseCraft

license: mit


code:
# textdomain: craftguide

Craft Guide=книга рецептов крафта
Crafting Guide=книга рецептов крафта
Crafting Guide Sign=Знак с книгой рецептов
Usage @1 of @2=использование  @1 из @2
Recipe @1 of @2=Рецепт @1 из @2
Burning time: @1=Время горения: @1
Cooking time: @1=Время преготовления: @1
Any item belonging to the group(s): @1=Любой элемент из группы: @1
Recipe's too big to be displayed (@1x@2)=Рецепт слишком большой для показа (@1x@2)
Shapeless=Бесформенный
Cooking=Приготовление
No item to show=Нет элемента для показа
Collect items to reveal more recipes=Собирайте предметы, чтобы раскрыть больше рецептов
Show recipe(s) of the pointed node=Показать рецепт(ы) выбранной ноды
No node pointed=Не указана нода
You don't know a recipe for this node=Вы не знаете рецепт для этой ноды
No recipe for this node=Нет рецептов для этой ноды


code:
# textdomain: craftguide

Craft Guide=
Crafting Guide=
Crafting Guide Sign=
Bookmarks=
Usage @1 of @2=
Recipe @1 of @2=
No recipes=
No usages=
Burning time: @1=
Cooking time: @1=
Replaced by @1 on smelting=
Replaced by @1 on burning=
Replaced by @1 on crafting=
Repairable by step of @1=
Any item belonging to the group(s): @1=
Any black dye=
Any black flower=
Any blue dye=
Any blue flower=
Any brown dye=
Any coal=
Any cyan dye=
Any dark green dye=
Any dark grey dye=
Any green dye=
Any green flower=
Any grey dye=
Any kind of stone block=
Any magenta dye=
Any orange dye=
Any orange flower=
Any pink dye=
Any red dye=
Any red flower=
Any sand=
Any stick=
Any tree=
Any vessel=
Any violet dye=
Any violet flower=
Any white dye=
Any white flower=
Any wood planks=
Any wool=
Any yellow dye=
Any yellow flower=
Recipe's too big to be displayed (@1x@2)=
Shapeless=
Cooking=
No item to show=
Collect items to reveal more recipes=
Show recipe(s) of the pointed node=
No node pointed=
You don't know a recipe or usage for this item=
No recipe or usage for this item=
Digging=
Digging Chance=
@1 of chance to drop=

path: mods/HUD/craftguide/locale/template


code:
name = craftguide
optional_depends = sfinv, sfinv_buttons
description = The most comprehensive Crafting Guide on Minetest


code:
# The progressive mode shows recipes you can craft from items you ever had in your inventory.
craftguide_progressive_mode    (Learn crafting recipes progressively)    bool false

# Integration in the default Minetest Game inventory.
craftguide_sfinv_only    (Crafting Guide in inventory only)    bool false

# Enable pre-caching of item recipes.
# Do NOT disable the first time the mod loads.
# Disabling the auto-caching will result in faster mod loading.
# If you enable or disable mods, or edit the current mod recipes in your setup, you SHOULD re-enable this setting until the next caching at least.
# Usage at your own risk.
craftguide_autocache    (Auto-caching of recipes)    bool true


items: default:stone,              , default:stone

result: default:cobble 16


code:
--[[
death_messages - A Minetest mod which sends a chat message when a player dies.
Copyright (C) 2016  EvergreenTree

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
--]]

-----------------------------------------------------------------------------------------------
local title = "Death Messages"
local version = "1.1"
local mname = "death_messages"
-----------------------------------------------------------------------------------------------
dofile(minetest.get_modpath("death_messages").."/settings.txt")
-----------------------------------------------------------------------------------------------

death_messages = {}
death_messages.messages = {} -- Contains tables of messages for different ways to die.
death_messages.punched = {} -- (key) was last punched by (value)
death_messages.punchedWith = {} -- (key) was last punched with an item whose description is (value)
death_messages.punched_forget = {} -- Forget who punched (key) after (value) seconds
death_messages.mightBeFalling = {} -- (key) was falling fast enough to take damage (value) seconds ago.
death_messages.mob_names = {
	--["mod:entity"]="an entity"
	["mobs_monster:oerkki"]="the dreaded Oerkki",
	["mobs_monster:fireball"] = "an angry dungeon_master",
	["mobs_monster:mese_arrow"] = "an angry mese_monster",
	["zombies:1arm"] = "a hungry zombie",
	["zombies:crawler"] = "a crawling zombie",
	["zombies:normal"] = "a hungry zombie",
	["slimes:lavasmall"]="a deadly lava slime",
	["slimes:lavamedium"]="a deadly lava slime",
	["slimes:lavabig"]="a deadly lava slime",
	["slimes:greensmall"]="a deadly jungle slime",
	["slimes:greenmedium"]="a deadly jungle slime",
	["slimes:greenbig"]="a deadly jungle slime",
	["dmobs:golem"]="a wandering golem",
	["dmobs:golem_friendly"]="a golem",

}
local name
minetest.register_on_punchplayer(function(player, hitter)
	name = player:get_player_name()
	if hitter then
		if hitter:is_player() then
			if hitter:get_player_name() == name then return end -- Some mods like to have the player punch themself for some odd reason.
			death_messages.punched[name] = hitter:get_player_name()
			death_messages.punchedWith[name] = hitter:get_wielded_item():get_name()
			if death_messages.punchedWith[name] == "" or not death_messages.punchedWith[name] then
				death_messages.punchedWith[name] = "bare hands"
			end
			if minetest.registered_items[death_messages.punchedWith[name]] then
				if minetest.registered_items[death_messages.punchedWith[name]].description then
					death_messages.punchedWith[name] = minetest.registered_items[death_messages.punchedWith[name]].description
				end
			end
		elseif hitter:get_luaentity() then
			local entname = hitter:get_luaentity().name
			if type(entname) ~= "string" then return end
			if death_messages.mob_names[entname] then
				death_messages.punched[name] = death_messages.mob_names[entname]
			else
				if string.split(entname, ":")[2] then
					if hitter:get_luaentity()._cmi_is_mob then
						death_messages.punched[name] = "an angry "..(string.split(entname, ":")[2]):gsub("_", " ")
					else
						death_messages.punched[name] = string.split(entname, ":")[2]
					end
				else
					death_messages.punched[name] = entname
				end
			end
			death_messages.punchedWith[name] = ""
		else
			return
		end
	else
		death_messages.punched[name] = nil
	end
	death_messages.punched_forget[name] = 2.5
end)
-- Lava death messages
death_messages.messages.lava = {
	"(player) was melted.",
	"(player) was incinerated.",
	"(player) was sacrificed to a volcano god.",
	"(player) jumped into a volcano.",
	"(player) joined the fossil record.",
	"(player) went for a swim in lava.",
	"(player) likes to play in magma.",
	"(player) melted into a ball of fire.",
        "(player) thought that lava was cool.",
        "(player) didn't know that lava was hot.",
}

-- Drowning death messages
death_messages.messages.water = {
	"(player) forgot to breathe.",
	"(player) learned they aren't a fish.",
	"(player) didn't float.",
	"(player) ran out of air.",
	"(player) probably wasn't a witch, after all...",
	"(player) is sleeping with the fish.",
	"(player) tried to become a fish, and failed.",
	"(player) blew one too many bubbles.",
	"(player) drowned.",
}

-- Burning death messages
death_messages.messages.fire = {
	"(player) couldn't put the fire out.",
	"(player) was burnt to a crisp.",
	"(player) walked into the flames.",
	"(player) was consumed by a blazing pyre.",
	"(player) got too close to the campfire.",
	"(player) just got roasted, literally.",
	"(player) must have been a witch.",
	"(player) became ash.",
	"(player) was involuntarily cremated.",
	"Someone should have told (player) not to play with matches.",
}

-- Other death messages
death_messages.messages.other = {
	"(player) died.",
	"(player) was slain.",
	"(player) was eviscerated.",
	"(player) was destroyed.",
	"(player)'s body was mangled.",
	"(player)'s vital organs were ruptured.",
	"(player) was removed from the world.",
	"R.I.P (player).",
	"(player) expired.",
	"(player) is aliven't.",
	"(player) had a bad time.",
	"(player)'s lifetime ran out.",
	"(player) joined the black parade.",
	"(player) has gone to a better place.",
	"(player) no longer resides among the living.",
	"(player)'s soul is taking the ferry across Acheron.",
	"(player) found out whether or not there's an afterlife. They won't be telling us, though.",
}

-- Killed by a mob
death_messages.messages.killed_mob = {
	"(player) was slain by (killer).",
	"(player) was eviscerated by (killer).",
	"(player) was murdered by (killer).",
	"(player)'s face was torn off by (killer).",
	"(player) entrails were ripped out by (killer).",
	"(player) was destroyed by (killer).",
	"(player)'s skull was crushed by (killer).",
	"(player) got massacred by (killer).",
	"(player) got impaled by (killer).",
	"(player) was torn in half by (killer).",
	"(player) was decapitated.",
	"(player) let their arms get torn off by (killer).",
	"(player) watched their innards become outards by (killer).",
	"(player) was brutally dissected by (killer).",
	"(player)'s extremities were detached by (killer).",
	"(player)'s body was mangled by (killer).",
	"(player)'s vital organs were ruptured by (killer).",
	"(player) was turned into a pile of flesh by (killer).",
	"(player) was removed from the world by (killer).",
	"(player) got snapped in half by (killer).",
	"(player) was cut down the middle by (killer).",
	"(player) was chopped up by (killer).",
	"(player)'s plea for death was answered by (killer).",
	"(player)'s meat was ripped of the bone by (killer).",
	"(player)'s flailing about was finally stopped by (killer).",
	"(player) had their head removed by (killer).",
	"(player) was killed by (killer).",
	"(player) was slaughtered by (killer).",
	"(player) was unceremoniously terminated by (killer).",
	"(player) tasted the wrath of (killer).",
	"(player)'s continued existence was cancelled by (killer).",
	"(player) tried to escape from (killer).",
}
-- ...killed by a player.
death_messages.messages.killed_player = {
	"(killer) destroyed (player) with their (weapon).",
	"(killer)'s (weapon) became the bane of (player).",
	"(killer) put an end to (player)'s miserable existence.",
	"(killer) has removed (player) from the ranks of the living.",
	"(player) tasted the wrath of (killer)'s (weapon).",
	"(player) died by (killer)'s (weapon)",
}
-- Misc. node damage
death_messages.messages.node = {
	"(player) wandered into a very uncomfortable place.",
	"(player) got themselves into a sticky situation.",
	"(player) shouldn't have touched that.",
	"(player) ignored the \"DANGER! KEEP OUT!\" sign.",
}

-- Fall
death_messages.messages.fall = {
	"(player) became a pancake.",
	"(player) fell to their doom.",
	"(player) shattered into pieces.",
	"(player) can't be put back together again.",
	"(player) didn't bounce.",
	"(player) took a leap of faith, and was sorely disappointed.",
	"(player) was destroyed by gravity.",
	"(player) faceplanted the ground.",
	"(player) left a small crater.",
	"(player)'s parachute failed.",
	"(player) went \"splat\".",
	"\"Don't jump!\" they cried, but (player) jumped.",
	"It wasn't the fall that killed (player), it was the sudden stop at the end.",

}

-- Murder by fall
death_messages.messages.pushed = {
	"(killer) pushed (player) to their doom.",
	"(player) fell to their death at the hands of (killer).",
	"(player) was pushed from a great height by (killer)",
	"(player) was taught the way of Spartans by (killer)",
}

minetest.register_globalstep(function(dtime)
	for _,player in pairs(minetest.get_connected_players()) do
		name = player:get_player_name()
		if player:get_player_velocity().y < -13 then
			death_messages.mightBeFalling[name] = 2.0
		elseif death_messages.mightBeFalling[name] then
			if death_messages.mightBeFalling[name] > 0 then
				death_messages.mightBeFalling[name] = death_messages.mightBeFalling[name] - dtime
			else
				death_messages.mightBeFalling[name] = nil
			end
		end
		if death_messages.punched_forget[name] then
			if death_messages.punched_forget[name] > 0 then
				death_messages.punched_forget[name] = death_messages.punched_forget[name] - dtime
			else
				death_messages.punched[name] = nil
				death_messages.punchedWith[name] = nil
				death_messages.punched_forget[name] = nil
			end
		end
	end
end)

function death_messages.get_message(mtype)
	if RANDOM_MESSAGES then
		return death_messages.messages[mtype][math.random(1, #(death_messages.messages[mtype]))]
	else
		return death_messages.messages[1] -- 1 is the index for the non-random message
	end
end

function death_messages.reset_watchers(player)
	name = player:get_player_name()
	death_messages.punched[name] = nil
	death_messages.punched_forget[name] = nil
	death_messages.punchedWith[name] = nil
	death_messages.mightBeFalling[name] = nil
end

minetest.register_on_leaveplayer(death_messages.reset_watchers)

minetest.register_on_dieplayer(function(player)
	name = player:get_player_name()
	local node = minetest.registered_nodes[minetest.get_node(player:getpos()).name]
	local message
	--Death by entity
	if death_messages.punched[name] and death_messages.punched[name] ~= name then

		if death_messages.mightBeFalling[name] and death_messages.punched_forget[name] and (death_messages.mightBeFalling[name] > death_messages.punched_forget[name] - 0.5) then
			message = death_messages.get_message("pushed")
		elseif death_messages.punchedWith[name] and death_messages.punchedWith[name] ~= "" then
			message = death_messages.get_message("killed_player")
			message = string.gsub(message, "%(weapon%)", death_messages.punchedWith[name])
		else
			message = death_messages.get_message("killed_mob")
		end
		message = string.gsub(message, "%(player%)", name)
		message = string.gsub(message, "%(killer%)", death_messages.punched[name])
		minetest.chat_send_all(minetest.colorize("#FF0000",(message)))
	-- Death by drowning
	elseif player:get_breath() == 0 then
		message = death_messages.get_message("water")
		minetest.chat_send_all(minetest.colorize("#FF0000",(string.gsub(message, "%(player%)", name))))
	-- Death by lava
	elseif node.groups.lava then
		message = death_messages.get_message("lava")
		minetest.chat_send_all(minetest.colorize("#FF0000",(string.gsub(message, "%(player%)", name))))
	-- Death by fall
	elseif death_messages.mightBeFalling[name] then
		message = death_messages.get_message("fall")
		minetest.chat_send_all(minetest.colorize("#FF0000",(string.gsub(message, "%(player%)", name))))
	-- Death by fire
	elseif string.find(node.name, "fire") or string.find(node.name, "flame") or node.groups.igniter then
		message = death_messages.get_message("fire")
		minetest.chat_send_all(minetest.colorize("#FF0000",(string.gsub(message, "%(player%)", name))))
	-- Death by node
	elseif node.damage_per_second and node.damage_per_second > 1 then
		message = death_messages.get_message("node")
		minetest.chat_send_all(minetest.colorize("#FF0000",(string.gsub(message, "%(player%)", name))))
	--Death by GNU GNU
	else
		message = death_messages.get_message("other")
		minetest.chat_send_all(minetest.colorize("#FF0000",(string.gsub(message, "%(player%)", name))))
	end
	death_messages.reset_watchers(player)
end)

-----------------------------------------------------------------------------------------------
print("[Mod] "..title.." ["..version.."] ["..mname.."] Loaded...")
-----------------------------------------------------------------------------------------------


code:
name = death_messages
author = 4Evergreen4
description = A Minetest mod which sends a chat message when a player dies.
title = Death Messages
depends =
optional_depends =


code: RANDOM_MESSAGES = true


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


code:
# emoji

"Emoji" show emoji icon above player with command "/e"
or with text smiles in chat like 'o_O'

Available chat smiles to spawn emoji:
:) B-) :# *_* :D :* :_( >:-[ ]:-) :/ ;) :( ;P :'-D ~:[ o_O xD xP :X :P :O

![Screenshot](https://i.ibb.co/SrJxw4b/Projec43t-1.gif)

---------------------------------------------------------------------

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.


code:
# emoji

"Emoji" show emoji icon above player with command "/e"
or with text smiles in chat like 'o_O'

Available chat smiles to spawn emoji:
:) B-) :# *_* :D :* :_( >:-[ ]:-) :/ ;) :( ;P :'-D ~:[ o_O xD xP :X :P :O

![Screenshot](https://i.ibb.co/SrJxw4b/Projec43t-1.gif)

---------------------------------------------------------------------
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
----------------------------------------------------------------------


code:
name = emoji
description = Adds emoji support for players.
author = bosapara
title = Emoji
depends =
optional_depends =


code:
root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

[*.{lua,luacheckrc}]
indent_style = tab
indent_size = 4


code:
std = "lua51+minetest"
unused_args = false
allow_defined_top = true
max_line_length = 90

stds.minetest = {
	read_globals = {
		"DIR_DELIM",
		"minetest",
		"core",
		"dump",
		"vector",
		"nodeupdate",
		"VoxelManip",
		"VoxelArea",
		"PseudoRandom",
		"ItemStack",
		"default",
		table = {
			fields = {
				"copy",
			},
		},
	}
}

globals = {
	"vector",
}


code:
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v2.3.0
    hooks:
      - id: end-of-file-fixer
      - id: trailing-whitespace

      - id: mixed-line-ending
        args: [--fix=lf]

  - repo: https://github.com/Calinou/pre-commit-luacheck
    rev: v1.0.0
    hooks:
      - id: luacheck


code:
dist: bionic
language: python

python:
  - 3.7.1

install:
  - sudo apt-get update -qq
  - sudo apt-get install -qqq luarocks
  - pip3 install pre-commit
  - luarocks install --local luacheck

script:
  # All linters are run with pre-commit hooks
  - export PATH="$HOME/.luarocks/bin:$PATH"
  - pre-commit run --all-files


code:
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.0.2] - 2020-01-16

### Fixed

- Fix crash when creating an entity if a player leaves the server quickly.

### Changed

- Restore Minetest 0.4.17 support. The version check works only with `minetest_game` with the `player_api` mod added in Minetest 5.0.

## [1.0.1] - 2020-01-01

### Changed

- Improve performance by localizing the `vector.distance()` function.

## 1.0.0 - 2020-01-01

- Initial versioned release.

[Unreleased]: https://github.com/minetest-mods/gauges/compare/v1.0.1...HEAD
[1.0.1]: https://github.com/minetest-mods/gauges/compare/v1.0.0...v1.0.1


code:
# Gauges

![Preview](https://content.minetest.net/uploads/GBoKauWSou.png)

Gauges for [Minetest](https://www.minetest.net/), a free and open source infinite
world block sandbox game.

[**Forum topic**](https://forum.minetest.net/viewtopic.php?f=11&t=10250)

## Installation

### Download the mod

To install Gauges, clone this Git repository into your Minetest's `mods/`
directory:

```bash
git clone https://github.com/minetest-mods/gauges.git
```

You can also
[download a ZIP archive](https://github.com/minetest-mods/gauges/archive/master.zip)
of Gauges.

### Enable the mod

Once you have installed Gauges, you need to enable it in Minetest.
The procedure is as follows:

#### Using the client's main menu

This is the easiest way to enable Gauges when playing in singleplayer
(or on a server hosted from a client).

1. Start Minetest and switch to the **Local Game** tab.
2. Select the world you want to enable Gauges in.
3. Click **Configure**, then enable `gauges` by double-clicking it
   (or ticking the **Enabled** checkbox).
4. Save the changes, then start a game on the world you enabled Gauges on.
5. Gauges should now be running on your world.

#### Using a text editor

This is the recommended way to enable the mod on a server without using a GUI.

1. Make sure Minetest is not currently running (otherwise, it will overwrite
   the changes when exiting).
2. Open the world's `world.mt` file using a text editor.
3. Add the following line at the end of the file:

```text
load_mod_gauges = true
```

If the line is already present in the file, then replace `false` with `true`
on that line.

4. Save the file, then start a game on the world you enabled Gauges on.
5. Gauges should now be running on your world.

## Version compatibility

Gauges is currently primarily tested with Minetest 5.1.0 and 0.4.17. It may or may not work
with newer or older versions. Issues arising in versions older than 5.0.0
will generally not be fixed.

## License

Copyright © 2014-2020 4aiman, Hugo Locurcio and contributors

- Code is licensed under the MIT license, see
  [`LICENSE.md`](LICENSE.md) for details.
- Textures are licensed under CC0 1.0 Universal, see
  [`LICENSE.assets.md`](LICENSE.assets.md) for details.


code:
-- gauges: Adds health/breath bars above players
--
-- Copyright © 2014-2020 4aiman, Hugo Locurcio and contributors - MIT License
-- See `LICENSE.md` included in the source distribution for details.

if minetest.settings:get_bool("health_bars") == false or
		not minetest.settings:get_bool("enable_damage")
then return end

-- Localize the vector distance function for better performance, as it's called
-- on every step
local vector_distance = vector.distance

minetest.register_entity("gauges:hp_bar", {
	visual = "sprite",
	visual_size = {x=1, y=1/16, z=1},
	-- The texture is changed later in the code
	textures = {"blank.png"},
	collisionbox = {0},
	physical = false,

	on_step = function(self)
		local player = self.wielder
		local gauge = self.object

		if not player or
				not minetest.is_player(player) or
				vector_distance(player:get_pos(), gauge:get_pos()) > 3
		then
			gauge:remove()
			return
		end

		local hp = player:get_hp() <= 20 and player:get_hp() or 20
		local breath = player:get_breath() <= 10 and player:get_breath() or 11

		if self.hp ~= hp or self.breath ~= breath then
			gauge:set_properties({
				textures = {
					"health_"..tostring(hp)..".png^"..
					"breath_"..tostring(breath)..".png"
				}
			})
			self.hp = hp
			self.breath = breath
		end
	end
})

local function add_gauge(player)
	if player and minetest.is_player(player) then
		local entity = minetest.add_entity(player:get_pos(), "gauges:hp_bar")
		-- check for minetest_game 0.4.*
		local height = minetest.get_modpath("player_api") and 19 or 9

		entity:set_attach(player, "", {x=0, y=height, z=0}, {x=0, y=0, z=0})
		entity:get_luaentity().wielder = player
	end
end

minetest.register_on_joinplayer(function(player)
	minetest.after(1, add_gauge, player)
end)


code:
name = gauges
description = Adds health and breath gauges above players.


code:
name = HUD
description = Meta-modpack containing HUD-related mods for MeseCraft.


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:
local worldpath = minetest.get_worldpath()

local S = minetest.get_translator("named_waypoints")

named_waypoints = {}

local test_interval = 5

local player_huds = {} -- Each player will have a table of [position_hash] = hud_id pairs in here
local waypoint_defs = {} -- the registered definition tables
local waypoint_areastores = {} -- areastores containing waypoint data

local inventory_string = "inventory"
local hotbar_string = "hotbar"
local wielded_string = "wielded"

--waypoint_def = {
--	default_name = , -- a string that's used if a waypoint's data doesn't have a "name" property
--	default_color = , -- if not defined, defaults to 0xFFFFFFFF
--	visibility_requires_item = , -- item, if not defined then nothing is required
--	visibility_item_location = , -- "inventory", "hotbar", "wielded" (defaults to inventory if not provided)
--	visibility_volume_radius = , -- If not defined, HUD waypoints will not be shown.
--	visibility_volume_height = , -- if defined, then visibility check is done in a cylindrical volume rather than a sphere
--	discovery_requires_item = ,-- item, if not defined then nothing is required
--	discovery_item_location = ,-- -- "inventory", "hotbar", "wielded" (defaults to inventory if not provided)
--	discovery_volume_radius = , -- radius within which a waypoint can be auto-discovered by a player. "discovered_by" property is used in waypoint_data to store discovery info
--	discovery_volume_height = , -- if defined, then discovery check is done in a cylindrical volume rather than a sphere
--	on_discovery = function(player, pos, waypoint_data, waypoint_def) -- use "named_waypoints.default_discovery_popup" for a generic discovery notification
--}

named_waypoints.register_named_waypoints = function(waypoints_type, waypoints_def)
	waypoint_defs[waypoints_type] = waypoints_def
	player_huds[waypoints_type] = {}

	local areastore_filename = worldpath.."/named_waypoints_".. waypoints_type ..".txt"
	local area_file = io.open(areastore_filename, "r")
	local areastore = AreaStore()
	if area_file then
		area_file:close()
		areastore:from_file(areastore_filename)
	end
	waypoint_areastores[waypoints_type] = areastore
end

local function save(waypoints_type)
	local areastore_filename = worldpath.."/named_waypoints_".. waypoints_type ..".txt"
	local areastore = waypoint_areastores[waypoints_type]
	if areastore then
		areastore:to_file(areastore_filename)
	else
		minetest.log("error", "[named_waypoints] attempted to save areastore for unregistered type " .. waypoints_type)
	end
end

-- invalidates a hud marker at a specific location
local function remove_hud_marker(waypoints_type, pos)
	local waypoint_def = waypoint_defs[waypoints_type]
	if not waypoint_def.visibility_volume_radius then
		-- if there's no visibility, there won't be any hud markers to remove
		return
	end
	local target_hash = minetest.hash_node_position(pos)
	local waypoints_for_this_type = player_huds[waypoints_type]
	for player_name, waypoints in pairs(waypoints_for_this_type) do
		local player = minetest.get_player_by_name(player_name)
		if player then
			for pos_hash, hud_id in pairs(waypoints) do
				if pos_hash == target_hash then
					player:hud_remove(hud_id)
					waypoints[pos_hash] = nil
					break
				end
			end
		end
	end
end

local function add_waypoint(waypoints_type, pos, waypoint_data, update_existing)
	assert(type(waypoint_data) == "table")
	local areastore = waypoint_areastores[waypoints_type]
	if not areastore then
		minetest.log("error", "[named_waypoints] attempted to add waypoint for unregistered type " .. waypoints_type)
		return false
	end
	local existing_area = areastore:get_areas_for_pos(pos, false, true)
	local id = next(existing_area)
	if id and not update_existing then
		return false -- already exists
	end

	local data
	if id then
		data = minetest.deserialize(existing_area[id].data)
		for k,v in pairs(waypoint_data) do
			data[k] = v
		end
		areastore:remove_area(id)
		remove_hud_marker(waypoints_type, pos)
	else
		data = waypoint_data
	end

	local waypoint_def = waypoint_defs[waypoints_type]
	if not (data.name or waypoint_def.default_name) then
		minetest.log("error", "[named_waypoints] Waypoint of type " .. waypoints_type .. " at "
			.. minetest.pos_to_string(pos) .. " was missing a name field in its data " .. dump(data)
			.. " and its type definition has no default to fall back on.")
		return false
	end
	areastore:insert_area(pos, pos, minetest.serialize(data), id)
	save(waypoints_type)
	return true
end

named_waypoints.add_waypoint = function(waypoints_type, pos, waypoint_data)
	if not waypoint_data then
		waypoint_data = {}
	end
	return add_waypoint(waypoints_type, pos, waypoint_data, false)
end

named_waypoints.update_waypoint = function(waypoints_type, pos, waypoint_data)
	return add_waypoint(waypoints_type, pos, waypoint_data, true)
end

named_waypoints.get_waypoint = function(waypoints_type, pos)
	local areastore = waypoint_areastores[waypoints_type]
	local existing_area = areastore:get_areas_for_pos(pos, false, true)
	local id = next(existing_area)
	if not id then
		return nil -- nothing here
	end
	return minetest.deserialize(existing_area[id].data)
end

-- returns a list of tables with the values {pos=, data=}
named_waypoints.get_waypoints_in_area = function(waypoints_type, minp, maxp)
	local areastore = waypoint_areastores[waypoints_type]
	local areas = areastore:get_areas_in_area(minp, maxp, true, true, true)
	local returnval = {}
	for id, data in pairs(areas) do
		table.insert(returnval, {pos=data.min, data=minetest.deserialize(data.data)})
	end
	return returnval
end

named_waypoints.remove_waypoint = function(waypoints_type, pos)
	local areastore = waypoint_areastores[waypoints_type]
	local existing_area = areastore:get_areas_for_pos(pos, false, true)
	local id = next(existing_area)
	if not id then
		return false -- nothing here
	end
	areastore:remove_area(id)
	remove_hud_marker(waypoints_type, pos)
	save(waypoints_type)
	return true
end

local function add_hud_marker(waypoints_type, player, player_name, pos, label, color)
	local waypoints_for_this_type = player_huds[waypoints_type]
	local waypoints = waypoints_for_this_type[player_name] or {}
	local pos_hash = minetest.hash_node_position(pos)
	if waypoints[pos_hash] then
		-- already exists
		return
	end
	waypoints_for_this_type[player_name] = waypoints
	color = color or 0xFFFFFF
	local hud_id = player:hud_add({
		hud_elem_type = "waypoint",
		name = label,
		text = "m",
		number = color,
		world_pos = pos})
	waypoints[pos_hash] = hud_id
end

local grouplen = #"group:"
local function test_items(player, item, location)
	if not item then
		return true
	end

	location = location or inventory_string
	local group
	if item:sub(1,grouplen) == "group:" then
		group = item:sub(grouplen+1)
	end

	if location == inventory_string then
		local player_inv = player:get_inventory()
		if group then
			for _, itemstack in pairs(player_inv:get_list("main")) do
				if minetest.get_item_group(itemstack:get_name(), group) > 0 then
					return true
				end
			end
		elseif player_inv:contains_item("main", ItemStack(item)) then
			return true
		end

	elseif location == hotbar_string then
		local player_inv = player:get_inventory()
		if group then
			for i = 1,8 do
				local hot_item = player_inv:get_Stack("main", i)
				if minetest.get_item_group(hot_item:get_name(), group) > 0 then
					return true
				end
			end
		else
			local hot_required = ItemStack(item)
			for i = 1, 8 do
				local hot_item = player_inv:get_stack("main", i)
				if hot_item:get_name() == hot_required:get_name() and hot_item:get_count() >= hot_required:get_count() then
					return true
				end
			end
		end

	elseif location == wielded_string then
		local wielded_item = player:get_wielded_item()
		if group then
			return minetest.get_item_group(wielded_item:get_name(), group) > 0
		else
			local wielded_required = ItemStack(item)
			if wielded_item:get_name() == wielded_required:get_name() and wielded_item:get_count() >= wielded_required:get_count() then
				return true
			end
		end
	else
		minetest.log("error", "[named_waypoints] Illegal inventory location " .. location .. " to test for an item.")
	end
	return false
end

local function test_range(player_pos, waypoint_pos, volume_radius, volume_height)
	if not volume_radius then
		return false
	end
	if volume_height then
		if math.abs(player_pos.y - waypoint_pos.y) > volume_height then
			return false
		end
		return math.sqrt(
			((player_pos.x - waypoint_pos.x)*(player_pos.x - waypoint_pos.x))+
			((player_pos.z - waypoint_pos.z)*(player_pos.z - waypoint_pos.z))) <= volume_radius
	else
		return vector.distance(player_pos, waypoint_pos) <= volume_radius
	end
end

-- doesn't test for discovery status being lost, it is assumed that waypoints are
-- rarely ever un-discovered once discovered.
local function remove_distant_hud_markers(waypoint_type)
	local waypoint_def = waypoint_defs[waypoint_type]
	local vis_radius = waypoint_def.visibility_volume_radius
	if not vis_radius then
		-- if there's no visibility, there won't be any hud markers to remove
		return
	end
	local waypoints_for_this_type = player_huds[waypoint_type]
	local players_to_remove = {}
	local vis_inv = waypoint_def.visibility_requires_item
	local vis_loc = waypoint_def.visibility_item_location
	local vis_height = waypoint_def.visibility_volume_height

	for player_name, waypoints in pairs(waypoints_for_this_type) do
		local player = minetest.get_player_by_name(player_name)
		if player then
			local waypoints_to_remove = {}
			local player_pos = player:get_pos()
			for pos_hash, hud_id in pairs(waypoints) do
				local pos = minetest.get_position_from_hash(pos_hash)
				if not (test_items(player, vis_inv, vis_loc)
					and test_range(player_pos, pos, vis_radius, vis_height)) then
					table.insert(waypoints_to_remove, pos_hash)
					player:hud_remove(hud_id)
				end
			end
			for _, pos_hash in ipairs(waypoints_to_remove) do
				waypoints[pos_hash] = nil
			end
			if not next(waypoints) then -- player's waypoint list is empty, remove it
				table.insert(players_to_remove, player_name)
			end
		else
			table.insert(players_to_remove, player_name)
		end
	end
	for _, player_name in ipairs(players_to_remove) do
		player_huds[player_name] = nil
	end
end

local function get_range_box(pos, volume_radius, volume_height)
	if volume_height then
		return {x = pos.x - volume_radius, y = pos.y - volume_height, z = pos.z - volume_radius},
			{x = pos.x + volume_radius, y = pos.y + volume_height, z = pos.z + volume_radius}
	else
		return vector.subtract(pos, volume_radius), vector.add(pos, volume_radius)
	end
end

local elapsed = 0
minetest.register_globalstep(function(dtime)
	elapsed = elapsed + dtime
	if elapsed < test_interval then
		return
	end
	elapsed = 0

	local connected_players = minetest.get_connected_players()
	for waypoint_type, waypoint_def in pairs(waypoint_defs) do
		local vis_radius = waypoint_def.visibility_volume_radius
		local disc_radius = waypoint_def.discovery_volume_radius
		if vis_radius or disc_radius then

			local areastore = waypoint_areastores[waypoint_type]
			local dirty_areastore = false

			local vis_height = waypoint_def.visibility_volume_height
			local vis_inv = waypoint_def.visibility_requires_item
			local vis_loc = waypoint_def.visibility_item_location

			local disc_height = waypoint_def.discovery_volume_height
			local disc_inv = waypoint_def.discovery_requires_item
			local disc_loc = waypoint_def.discovery_item_location

			local on_discovery = waypoint_def.on_discovery
			local default_color = waypoint_def.default_color
			local default_name = waypoint_def.default_name

			for _, player in ipairs(connected_players) do
				local player_pos = player:get_pos()
				local player_name = player:get_player_name()

				if disc_radius then
					local min_discovery_edge, max_discovery_edge = get_range_box(player_pos, disc_radius, disc_height)
					local potentially_discoverable = areastore:get_areas_in_area(min_discovery_edge, max_discovery_edge, true, true, true)
					for id, area_data in pairs(potentially_discoverable) do
						local pos = area_data.min
						local data = minetest.deserialize(area_data.data)
						local discovered_by = data.discovered_by or {}

						if not discovered_by[player_name] and
							test_items(player, disc_inv, disc_loc)
							and test_range(player_pos, pos, disc_radius, disc_height) then

							discovered_by[player_name] = true
							data.discovered_by = discovered_by
							areastore:remove_area(id)
							areastore:insert_area(pos, pos, minetest.serialize(data), id)

							if on_discovery then
								on_discovery(player, pos, data, waypoint_def)
							end

							dirty_areastore = true
						end
					end
				end

				if vis_radius then
					local min_visual_edge, max_visual_edge = get_range_box(player_pos, vis_radius, vis_height)
					local potentially_visible = areastore:get_areas_in_area(min_visual_edge, max_visual_edge, true, true, true)
					for id, area_data in pairs(potentially_visible) do
						local pos = area_data.min
						local data = minetest.deserialize(area_data.data)
						local discovered_by = data.discovered_by

						if (not disc_radius or (discovered_by and discovered_by[player_name])) and
							test_items(player, vis_inv, vis_loc)
							and test_range(player_pos, pos, vis_radius, vis_height) then
							add_hud_marker(waypoint_type, player, player_name, pos,
								data.name or default_name, data.color or default_color)
						end
					end
				end
			end
			if dirty_areastore then
				save(waypoint_type)
			end
			remove_distant_hud_markers(waypoint_type)
		end
	end
end)

-- Use this as a definition's on_discovery for a generic popup and sound alert
named_waypoints.default_discovery_popup = function(player, pos, data, waypoint_def)
	local player_name = player:get_player_name()
	local discovery_name = data.name or waypoint_def.default_name
	local discovery_note = S("You've discovered @1", discovery_name)
	local formspec = "formspec_version[2]" ..
		"size[5,2]" ..
		"label[1.25,0.75;" .. minetest.formspec_escape(discovery_note) ..
		"]button_exit[1.0,1.25;3,0.5;btn_ok;".. S("OK") .."]"
	minetest.show_formspec(player_name, "named_waypoints:discovery_popup", formspec)
	minetest.chat_send_player(player_name, discovery_note)
	minetest.log("action", "[named_waypoints] " .. player_name .. " discovered " .. discovery_name)
	minetest.sound_play({name = "named_waypoints_chime01", gain = 0.25}, {to_player=player_name})
end

------------------------------------------------------------------------------------------------------------------
--- Admin commands

local formspec_state = {}
local function get_formspec(player_name)
	local player = minetest.get_player_by_name(player_name)
	local player_pos = player:get_pos()
	local state = formspec_state[player_name] or {}
	formspec_state[player_name] = state
	state.row_index = state.row_index or 1

	local formspec = {
		"formspec_version[2]"
		.."size[8,9]"
		.."button_exit[7.0,0.25;0.5,0.5;close;X]"
		.."label[0.5,0.6;Type:]dropdown[1.25,0.5;2,0.25;type_select;"
	}

	local types = {}
	local i = 0
	local dropdown_selected_index
	for waypoint_type, def in pairs(waypoint_defs) do
		i = i + 1
		if not state.selected_type then
			state.selected_type = waypoint_type
		end
		if state.selected_type == waypoint_type then
			dropdown_selected_index = i
		end
		table.insert(types, waypoint_type)
	end
	local selected_def = waypoint_defs[state.selected_type]
	formspec[#formspec+1] = table.concat(types, ",") .. ";"..dropdown_selected_index.."]"

	formspec[#formspec+1] = "tablecolumns[text;text;text]table[0.5,1.0;7,4;waypoint_table;"
	local areastore = waypoint_areastores[state.selected_type]
	if not areastore then
		return ""
	end
	local areas_by_id = areastore:get_areas_in_area({x=-32000, y=-32000, z=-32000}, {x=32000, y=32000, z=32000}, true, true, true)
	local areas = {}
	for id, area in pairs(areas_by_id) do
		area.id = id
		table.insert(areas, area)
	end

	table.sort(areas, function(area1, area2)
		local dist1 = vector.distance(area1.min, player_pos)
		local dist2 = vector.distance(area2.min, player_pos)
		return dist1 < dist2
	end)

	local selected_area = areas[state.row_index]
	if not selected_area then
		state.row_index = 1
	end

	local selected_name = ""
	local selected_data_string = ""

	state.selected_id = nil
	state.selected_pos = nil

	for i, area in ipairs(areas) do
		if i == state.row_index then
			state.selected_id = area.id
			state.selected_pos = area.min
			selected_area = area
			selected_data_string = selected_area.data
			local selected_data = minetest.deserialize(selected_data_string)
			selected_name = minetest.formspec_escape(selected_data.name or selected_def.default_name or "unnamed")
		end
		local pos = area.min
		local data_string = area.data
		local data = minetest.deserialize(data_string)
		formspec[#formspec+1] = minetest.formspec_escape(data.name or selected_def.default_name or "unnamed")
			..","..minetest.formspec_escape(minetest.pos_to_string(pos))
			..",".. minetest.formspec_escape(data_string)
		formspec[#formspec+1] = ","
	end
	formspec[#formspec] = ";"..state.row_index.."]" -- don't use +1, this overwrites the last ","

	state.selected_pos = state.selected_pos or {x=0,y=0,z=0}

	formspec[#formspec+1] = "container[0.5,5.25]"
		.."label[0,0.15;X]field[0.25,0;1,0.25;pos_x;;"..state.selected_pos.x.."]"
		.."label[1.5,0.15;Y]field[1.75,0;1,0.25;pos_y;;"..state.selected_pos.y.."]"
		.."label[3.0,0.15;Z]field[3.25,0;1,0.25;pos_z;;"..state.selected_pos.z.."]"
		.."container_end[]"

	formspec[#formspec+1] = "textarea[0.5,5.75;7,2.25;waypoint_data;;".. minetest.formspec_escape(selected_data_string) .."]"

	formspec[#formspec+1] = "container[0.5,8.25]"
		.."button[0,0;1,0.5;teleport;"..S("Teleport").."]button[1,0;1,0.5;save;"..S("Save").."]"
		.."button[2,0;1,0.5;rename;"..S("Rename").."]field[3,0;2,0.5;waypoint_name;;" .. selected_name .."]"
		.."button[5,0;1,0.5;create;"..S("New").."]button[6,0;1,0.5;delete;"..S("Delete").."]"
		.."container_end[]"

	return table.concat(formspec)
end

minetest.register_chatcommand("named_waypoints", {
    description = S("Open server controls for named_waypoints"),
    func = function(name, param)
		if not minetest.check_player_privs(name, {server = true}) then
			minetest.chat_send_player(name, S("This command is for server admins only."))
			return
		end
		minetest.show_formspec(name, "named_waypoints:server_controls", get_formspec(name))
	end,
})

minetest.register_on_player_receive_fields(function(player, formname, fields)
	if formname ~= "named_waypoints:server_controls" then
		return
	end

	if fields.close then
		return
	end

	local player_name = player:get_player_name()
	if not minetest.check_player_privs(player_name, {server = true}) then
		minetest.chat_send_player(player_name, S("This command is for server admins only."))
		return
	end

	local refresh = false
	local state = formspec_state[player_name]
	if fields.type_select then
		formspec_state[player_name].selected_type = fields.type_select
		refresh = true
	end

	if fields.waypoint_table then
		local table_event = minetest.explode_table_event(fields.waypoint_table)
		if table_event.type == "CHG" then
			formspec_state[player_name].row_index = table_event.row
			refresh = true
		end
	end

	if fields.save then
		local deserialized = minetest.deserialize(fields.waypoint_data)
		local pos_x = tonumber(fields.pos_x)
		local pos_y = tonumber(fields.pos_y)
		local pos_z = tonumber(fields.pos_z)
		if deserialized and pos_x and pos_y and pos_z and state.selected_id then
			local areastore = waypoint_areastores[state.selected_type]
			local pos = vector.floor({x=pos_x, y=pos_y, z=pos_z})
			areastore:remove_area(state.selected_id)
			areastore:insert_area(pos, pos,
				fields.waypoint_data, state.selected_id)

			save(state.selected_type)
			remove_hud_marker(state.selected_type, state.selected_pos)
			minetest.chat_send_player(player_name, S("Waypoint updated."))
		else
			minetest.chat_send_player(player_name, S("Invalid syntax."))
		end
		refresh = true
	end

	if fields.delete then
		local areastore = waypoint_areastores[state.selected_type]
		areastore:remove_area(state.selected_id)
		save(state.selected_type)
		remove_hud_marker(state.selected_type, state.selected_pos)
		refresh = true
	end

	if fields.create then
		local pos = player:get_pos()
		local areastore = waypoint_areastores[state.selected_type]
		local existing_area = areastore:get_areas_for_pos(pos, false, true)
		local id = next(existing_area)
		if id then
			minetest.chat_send_player(player_name, S("There's already a waypoint there."))
			return
		end
		areastore:insert_area(pos, pos, minetest.serialize({}))
		save(state.selected_type)
		refresh = true
	end

	if fields.rename then
		local areastore = waypoint_areastores[state.selected_type]
		local area = areastore:get_area(state.selected_id, true, true)
		local data = minetest.deserialize(area.data)
		data.name = fields.waypoint_name
		areastore:remove_area(state.selected_id)
		areastore:insert_area(area.min, area.min, minetest.serialize(data), state.selected_id)
		save(state.selected_type)
		remove_hud_marker(state.selected_type, state.selected_pos)
		minetest.chat_send_player(player_name, S("Waypoint updated."))
	end

	if fields.teleport then
		player:set_pos(state.selected_pos)
	end

	if refresh then
		minetest.show_formspec(player_name, "named_waypoints:server_controls", get_formspec(player_name))
	end
end)

local function set_all_discovered(player_name, waypoint_type, state)
	local waypoint_list = named_waypoints.get_waypoints_in_area(waypoint_type,
		{x=-32000, y=-32000, z=-32000}, {x=32000, y=32000, z=32000})
	for id, waypoint in pairs(waypoint_list) do
		waypoint.data.discovered_by = waypoint.data.discovered_by or {}
		waypoint.data.discovered_by[player_name] = state
		named_waypoints.update_waypoint(waypoint_type, waypoint.pos, waypoint.data)
	end
end

minetest.register_chatcommand("named_waypoints_discover_all", {
	description = S("Set all waypoints of a type as discovered by you"),
	params = S("waypoint type"),
	privs = {["server"]=true},
	func = function(name, param)
		if param == "" or waypoint_defs[param] == nil then
			minetest.chat_send_player(name, S("Please provide a valid waypoint type as a parameter"))
			return
		end
		set_all_discovered(name, param, true)
	end,
})

minetest.register_chatcommand("named_waypoints_undiscover_all", {
	description = S("Set all waypoints of a type as not discovered by you"),
	params = S("waypoint type"),
	privs = {["server"]=true},
	func = function(name, param)
		if param == "" or waypoint_defs[param] == nil then
			minetest.chat_send_player(name, S("Please provide a valid waypoint type as a parameter"))
			return
		end
		set_all_discovered(name, param, nil)
	end,
})


code:
# textdomain: named_waypoints

### init.lua ###

#button label
Delete=
#warning that incorrect data was entered for a waypoint in the UI
Invalid syntax.=
#button label
New=
#button label
OK=
#chat command help text
Open server controls for named_waypoints=
#chat command error message
Please provide a valid waypoint type as a parameter=
#button label
Rename=
#button label
Save=
#chat command help text
Set all waypoints of a type as discovered by you=
#chat command help text
Set all waypoints of a type as not discovered by you=
#button label
Teleport=
#error message for when trying to create a waypoint where one already exists
There's already a waypoint there.=
#chat command error message
This command is for server admins only.=
#player chat
Waypoint updated.=
#text of the default popup shown when a player discovers a waypoint
You've discovered @1=
#chat command parameter help
waypoint type=


code:
name = named_waypoints
description = A library mod for managing waypoints shown in player HUDs that can be discovered by exploration.


code:
## API

This mod is intended as a library for other mods to make use of. As such it has no effect on a game when installed on its own. It provides a set of API functions and an administrative interface.

### named_waypoints.register_named_waypoints(waypoints_type, waypoints_def)

A waypoint type needs to be registered before the other methods in this mod can be used. ``waypoints_type`` is a unique string that will be used to reference this type of waypoint, and waypoints_def is a table with the following possible properties (all of which are optional):

	waypoints_def = {
		default_name = , -- a string that's used as the waypoint's label if a waypoint's data doesn't have a "name" property
		default_color = , -- label text color if waypoint_data doesn't have a "color" property.
					-- If not defined defaults to 0xFFFFFFFF (opaque white)

		visibility_volume_radius = , -- The radius within which a player will see the waypoint
					-- marked on their HUD. If not defined then HUD markers will not be shown.
		visibility_volume_height = , -- if defined, visibility check is done in a cylindrical
					-- volume rather than a sphere. Height extends both upward and downward from player position.

		visibility_requires_item = , -- item string or "group:groupname", the player needs to have an item
					-- that matches this in their inventory to see waypoints on their HUD.
					-- If not defined then there's no requirement to see waypoints on the HUD.
		visibility_item_location = , -- "inventory", "hotbar", or "wielded" are valid options here(defaults to inventory if not defined)

		discovery_volume_radius = , -- radius within which a waypoint can be auto-discovered by a player.
					-- "discovered_by" property is used in waypoint_data to store discovery info. If this
					-- is not defined then discovery is not required - waypoints will always be visible
					-- dwhen within visible range.
		discovery_volume_height = , -- if defined, then discovery check is done in a cylindrical volume rather than a sphere.

		discovery_requires_item = ,-- item string or "group:groupname", an item matching this is needed
					-- in player inventory for a waypoint within range to be marked as "discovered" for that player
		discovery_item_location = ,-- "inventory", "hotbar", "wielded" (defaults to inventory if not defined)

		on_discovery = function(player, pos, waypoint_data, waypoints_def) -- If defined, this method
					-- will be called when a player first discovers a particular waypoint. Set it to
					-- "named_waypoints.default_discovery_popup" for a generic discovery notification
					-- popup and audible chime.
	}

### named_waypoints.add_waypoint(waypoints_type, pos, waypoint_data)

Adds a new waypoint at pos. Fails if there's already a waypoint of that type at that location. Returns true on success, false on failure.

waypoint_data is a freeform table you can put whatever you want into (though it will be stored as a serialized string so don't get fancy if you can avoid it). There are three properties on waypoint_data with special meaning:

	name = a string that will be used as the label for this waypoint
		-- if not defined, named_waypoints will fall back to the "default_name" property of the waypoint definition

	color = a hex integer that defines the colour of this waypoint
		-- if not defined, named_waypoints will fall back to the "default_color" property of the waypoint definition
		-- which itself falls back to 0xFFFFFFFF - opaque white

	discovered_by = a set containing the names of players that have discovered this waypoint
		-- provided discovery_volume_radius was defined for this waypoint type

### named_waypoints.update_waypoint(waypoints_type, pos, waypoint_data)

The same as add_waypoint, but if there's already a waypoint at pos then the values of any fields in waypoint_data will replace the corresponding fields in the existing waypoint.

### named_waypoints.get_waypoint(waypoints_type, pos)

Returns the waypoint_data of the waypoint at pos, or nil if there isn't one at that location.

### named_waypoints.get_waypoints_in_area(waypoints_type, minp, maxp)

Returns a table with values of ``{pos = pos, data = waypoint_data}`` for all waypoints in the region specified.

### named_waypoints.remove_waypoint(waypoints_type, pos)

Deletes the waypoint at pos, if there is one. Returns true on success and false on failure.

## Server admin interface

The "``/named_waypoints``" chat command brings up an interface for use by server admins to view and edit waypoints:

![](screenshot.png)

Select a waypoint type from the dropdown and all waypoints of that type will be listed in the table, sorted by increasing distance from your current location. Select a waypoint from the table and its location and serialized data will be shown below. Clicking the "save" button will save any changes you've made to the waypoint's position or serialized data (provided the serialized data has valid syntax). There's also a "name" field for more convenient renaming than editing the serialized data manually. You can also teleport to the selected waypoint or delete it.

The "New" button creates a new waypoint at your current location with empty waypoint data.

Caution: This interface lets you access the guts of the data stored for each waypoint, only make edits if you know what you're doing. You can disrupt other mods that may be using waypoint data to store information. In the pictured example the "settlement_type" property is specific to the mod that is using named_waypoints, so changing or removing this value will cause undefined behaviour.

### Other commands

"``/named_waypoints_discover_all <waypoints_type>``" and "``/named_waypoints_undiscover_all <waypoints_type>``" will set all existing waypoints of the provided type to be either discovered or not discovered by you. It can be useful for a server administrator to "know all and see all" as it were. Note that visibility item and range limitations still apply.

## License

This mod is released under the MIT license.


code:
*.sublime-workspace
*.sublime-project
*.log
git-commit.bat