Compare commits

...

10 Commits

Author SHA1 Message Date
FaceDeer
d4cefe30ac update i18n script 2023-01-07 15:10:17 -07:00
FaceDeer
f90321ca31 update deprecated functions 2020-02-18 23:02:44 -07:00
FaceDeer
90e4d6130d update translation system 2020-02-16 22:14:30 -07:00
FaceDeer
328d8512c2 optimize pngs 2019-12-27 20:04:49 -07:00
FaceDeer
d56a93cb61 update metadata handling, add HUD waypoints 2019-12-27 20:03:17 -07:00
FaceDeer
23e3517c4a switch to markdown 2018-05-27 00:59:09 -06:00
FaceDeer
1f6c804501 switch to MIT license 2018-05-27 00:55:37 -06:00
FaceDeer
47173412e5 add loot support 2017-02-11 00:49:26 -07:00
FaceDeer
2cd11e2506 make marker signs usable as fuel 2017-02-04 00:25:50 -07:00
FaceDeer
e2ced0e40c Set this up for translation 2017-02-02 20:56:22 -07:00
12 changed files with 604 additions and 81 deletions

View File

@ -1,17 +1,21 @@
path_markers mod for Minetest
breadcrumbs mod for Minetest
Copyright (C) 2017 FaceDeer
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
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 Lesser General Public License for more details.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -2,8 +2,8 @@ This mod adds a specialized type of sign block that's intended to be used while
Path markers are initially crafted in a "blank" state. Equip the stack of markers and click on anything with them to bring up a form where you can enter a short text label for this stack of markers. This initializes the stack of markers with that label. They can then be placed on surfaces like normal signs, but the text of the sign is predefined. It will read:
<label> #1
placed by <player name>
<label> #1
placed by <player name>
Each subsequent sign placed from this stack will increment the number count, and will also add a line of text indicating how many meters away the previous sign in the stack was placed. If you right-click on the sign a stream of particles will be displayed that travel in the direction the previous sign lies in. This should allow much easier "backtracking" along the path of signs. Good practice is to choose a label that describes the starting point of your expedition, since the trail of signs you leave behind will send travelers in the direction of that origin.

View File

@ -1,2 +0,0 @@
default?
doc?

View File

@ -1 +0,0 @@
Path marker signs for use when exploring a twisty maze of passages that are all alike.

421
i18n.py Normal file
View File

@ -0,0 +1,421 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Script to generate the template file and update the translation files.
# Copy the script into the mod or modpack root folder and run it there.
#
# Copyright (C) 2019 Joachim Stolberg, 2020 FaceDeer, 2020 Louis Royer
# LGPLv2.1+
#
# See https://github.com/minetest-tools/update_translations for
# potential future updates to this script.
from __future__ import print_function
import os, fnmatch, re, shutil, errno
from sys import argv as _argv
# Running params
params = {"recursive": False,
"help": False,
"mods": False,
"verbose": False,
"folders": []
}
# Available CLI options
options = {"recursive": ['--recursive', '-r'],
"help": ['--help', '-h'],
"mods": ['--installed-mods'],
"verbose": ['--verbose', '-v']
}
# Strings longer than this will have extra space added between
# them in the translation files to make it easier to distinguish their
# beginnings and endings at a glance
doublespace_threshold = 60
def set_params_folders(tab: list):
'''Initialize params["folders"] from CLI arguments.'''
# Discarding argument 0 (tool name)
for param in tab[1:]:
stop_param = False
for option in options:
if param in options[option]:
stop_param = True
break
if not stop_param:
params["folders"].append(os.path.abspath(param))
def set_params(tab: list):
'''Initialize params from CLI arguments.'''
for option in options:
for option_name in options[option]:
if option_name in tab:
params[option] = True
break
def print_help(name):
'''Prints some help message.'''
print(f'''SYNOPSIS
{name} [OPTIONS] [PATHS...]
DESCRIPTION
{', '.join(options["help"])}
prints this help message
{', '.join(options["recursive"])}
run on all subfolders of paths given
{', '.join(options["mods"])}
run on locally installed modules
{', '.join(options["verbose"])}
add output information
''')
def main():
'''Main function'''
set_params(_argv)
set_params_folders(_argv)
if params["help"]:
print_help(_argv[0])
elif params["recursive"] and params["mods"]:
print("Option --installed-mods is incompatible with --recursive")
else:
# Add recursivity message
print("Running ", end='')
if params["recursive"]:
print("recursively ", end='')
# Running
if params["mods"]:
print(f"on all locally installed modules in {os.path.abspath('~/.minetest/mods/')}")
run_all_subfolders("~/.minetest/mods")
elif len(params["folders"]) >= 2:
print("on folder list:", params["folders"])
for f in params["folders"]:
if params["recursive"]:
run_all_subfolders(f)
else:
update_folder(f)
elif len(params["folders"]) == 1:
print("on folder", params["folders"][0])
if params["recursive"]:
run_all_subfolders(params["folders"][0])
else:
update_folder(params["folders"][0])
else:
print("on folder", os.path.abspath("./"))
if params["recursive"]:
run_all_subfolders(os.path.abspath("./"))
else:
update_folder(os.path.abspath("./"))
#group 2 will be the string, groups 1 and 3 will be the delimiters (" or ')
#See https://stackoverflow.com/questions/46967465/regex-match-text-in-either-single-or-double-quote
pattern_lua = re.compile(r'[\.=^\t,{\(\s]N?S\(\s*(["\'])((?:\\\1|(?:(?!\1)).)*)(\1)[\s,\)]', re.DOTALL)
pattern_lua_bracketed = re.compile(r'[\.=^\t,{\(\s]N?S\(\s*\[\[(.*?)\]\][\s,\)]', re.DOTALL)
# Handles "concatenation" .. " of strings"
pattern_concat = re.compile(r'["\'][\s]*\.\.[\s]*["\']', re.DOTALL)
pattern_tr = re.compile(r'(.+?[^@])=(.*)')
pattern_name = re.compile(r'^name[ ]*=[ ]*([^ \n]*)')
pattern_tr_filename = re.compile(r'\.tr$')
pattern_po_language_code = re.compile(r'(.*)\.po$')
#attempt to read the mod's name from the mod.conf file. Returns None on failure
def get_modname(folder):
try:
with open(os.path.join(folder, "mod.conf"), "r", encoding='utf-8') as mod_conf:
for line in mod_conf:
match = pattern_name.match(line)
if match:
return match.group(1)
except FileNotFoundError:
pass
return None
#If there are already .tr files in /locale, returns a list of their names
def get_existing_tr_files(folder):
out = []
for root, dirs, files in os.walk(os.path.join(folder, 'locale/')):
for name in files:
if pattern_tr_filename.search(name):
out.append(name)
return out
# A series of search and replaces that massage a .po file's contents into
# a .tr file's equivalent
def process_po_file(text):
# The first three items are for unused matches
text = re.sub(r'#~ msgid "', "", text)
text = re.sub(r'"\n#~ msgstr ""\n"', "=", text)
text = re.sub(r'"\n#~ msgstr "', "=", text)
# comment lines
text = re.sub(r'#.*\n', "", text)
# converting msg pairs into "=" pairs
text = re.sub(r'msgid "', "", text)
text = re.sub(r'"\nmsgstr ""\n"', "=", text)
text = re.sub(r'"\nmsgstr "', "=", text)
# various line breaks and escape codes
text = re.sub(r'"\n"', "", text)
text = re.sub(r'"\n', "\n", text)
text = re.sub(r'\\"', '"', text)
text = re.sub(r'\\n', '@n', text)
# remove header text
text = re.sub(r'=Project-Id-Version:.*\n', "", text)
# remove double-spaced lines
text = re.sub(r'\n\n', '\n', text)
return text
# Go through existing .po files and, if a .tr file for that language
# *doesn't* exist, convert it and create it.
# The .tr file that results will subsequently be reprocessed so
# any "no longer used" strings will be preserved.
# Note that "fuzzy" tags will be lost in this process.
def process_po_files(folder, modname):
for root, dirs, files in os.walk(os.path.join(folder, 'locale/')):
for name in files:
code_match = pattern_po_language_code.match(name)
if code_match == None:
continue
language_code = code_match.group(1)
tr_name = modname + "." + language_code + ".tr"
tr_file = os.path.join(root, tr_name)
if os.path.exists(tr_file):
if params["verbose"]:
print(f"{tr_name} already exists, ignoring {name}")
continue
fname = os.path.join(root, name)
with open(fname, "r", encoding='utf-8') as po_file:
if params["verbose"]:
print(f"Importing translations from {name}")
text = process_po_file(po_file.read())
with open(tr_file, "wt", encoding='utf-8') as tr_out:
tr_out.write(text)
# from https://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python/600612#600612
# Creates a directory if it doesn't exist, silently does
# nothing if it already exists
def mkdir_p(path):
try:
os.makedirs(path)
except OSError as exc: # Python >2.5
if exc.errno == errno.EEXIST and os.path.isdir(path):
pass
else: raise
# Converts the template dictionary to a text to be written as a file
# dKeyStrings is a dictionary of localized string to source file sets
# dOld is a dictionary of existing translations and comments from
# the previous version of this text
def strings_to_text(dkeyStrings, dOld, mod_name):
lOut = [f"# textdomain: {mod_name}\n"]
dGroupedBySource = {}
for key in dkeyStrings:
sourceList = list(dkeyStrings[key])
sourceList.sort()
sourceString = "\n".join(sourceList)
listForSource = dGroupedBySource.get(sourceString, [])
listForSource.append(key)
dGroupedBySource[sourceString] = listForSource
lSourceKeys = list(dGroupedBySource.keys())
lSourceKeys.sort()
for source in lSourceKeys:
localizedStrings = dGroupedBySource[source]
localizedStrings.sort()
lOut.append("")
lOut.append(source)
lOut.append("")
for localizedString in localizedStrings:
val = dOld.get(localizedString, {})
translation = val.get("translation", "")
comment = val.get("comment")
if len(localizedString) > doublespace_threshold and not lOut[-1] == "":
lOut.append("")
if comment != None:
lOut.append(comment)
lOut.append(f"{localizedString}={translation}")
if len(localizedString) > doublespace_threshold:
lOut.append("")
unusedExist = False
for key in dOld:
if key not in dkeyStrings:
val = dOld[key]
translation = val.get("translation")
comment = val.get("comment")
# only keep an unused translation if there was translated
# text or a comment associated with it
if translation != None and (translation != "" or comment):
if not unusedExist:
unusedExist = True
lOut.append("\n\n##### not used anymore #####\n")
if len(key) > doublespace_threshold and not lOut[-1] == "":
lOut.append("")
if comment != None:
lOut.append(comment)
lOut.append(f"{key}={translation}")
if len(key) > doublespace_threshold:
lOut.append("")
return "\n".join(lOut) + '\n'
# Writes a template.txt file
# dkeyStrings is the dictionary returned by generate_template
def write_template(templ_file, dkeyStrings, mod_name):
# read existing template file to preserve comments
existing_template = import_tr_file(templ_file)
text = strings_to_text(dkeyStrings, existing_template[0], mod_name)
mkdir_p(os.path.dirname(templ_file))
with open(templ_file, "wt", encoding='utf-8') as template_file:
template_file.write(text)
# Gets all translatable strings from a lua file
def read_lua_file_strings(lua_file):
lOut = []
with open(lua_file, encoding='utf-8') as text_file:
text = text_file.read()
#TODO remove comments here
text = re.sub(pattern_concat, "", text)
strings = []
for s in pattern_lua.findall(text):
strings.append(s[1])
for s in pattern_lua_bracketed.findall(text):
strings.append(s)
for s in strings:
s = re.sub(r'"\.\.\s+"', "", s)
s = re.sub("@[^@=0-9]", "@@", s)
s = s.replace('\\"', '"')
s = s.replace("\\'", "'")
s = s.replace("\n", "@n")
s = s.replace("\\n", "@n")
s = s.replace("=", "@=")
lOut.append(s)
return lOut
# Gets strings from an existing translation file
# returns both a dictionary of translations
# and the full original source text so that the new text
# can be compared to it for changes.
def import_tr_file(tr_file):
dOut = {}
text = None
if os.path.exists(tr_file):
with open(tr_file, "r", encoding='utf-8') as existing_file :
# save the full text to allow for comparison
# of the old version with the new output
text = existing_file.read()
existing_file.seek(0)
# a running record of the current comment block
# we're inside, to allow preceeding multi-line comments
# to be retained for a translation line
latest_comment_block = None
for line in existing_file.readlines():
line = line.rstrip('\n')
if line[:3] == "###":
# Reset comment block if we hit a header
latest_comment_block = None
continue
if line[:1] == "#":
# Save the comment we're inside
if not latest_comment_block:
latest_comment_block = line
else:
latest_comment_block = latest_comment_block + "\n" + line
continue
match = pattern_tr.match(line)
if match:
# this line is a translated line
outval = {}
outval["translation"] = match.group(2)
if latest_comment_block:
# if there was a comment, record that.
outval["comment"] = latest_comment_block
latest_comment_block = None
dOut[match.group(1)] = outval
return (dOut, text)
# Walks all lua files in the mod folder, collects translatable strings,
# and writes it to a template.txt file
# Returns a dictionary of localized strings to source file sets
# that can be used with the strings_to_text function.
def generate_template(folder, mod_name):
dOut = {}
for root, dirs, files in os.walk(folder):
for name in files:
if fnmatch.fnmatch(name, "*.lua"):
fname = os.path.join(root, name)
found = read_lua_file_strings(fname)
if params["verbose"]:
print(f"{fname}: {str(len(found))} translatable strings")
for s in found:
sources = dOut.get(s, set())
sources.add(f"### {os.path.basename(fname)} ###")
dOut[s] = sources
if len(dOut) == 0:
return None
templ_file = os.path.join(folder, "locale/template.txt")
write_template(templ_file, dOut, mod_name)
return dOut
# Updates an existing .tr file, copying the old one to a ".old" file
# if any changes have happened
# dNew is the data used to generate the template, it has all the
# currently-existing localized strings
def update_tr_file(dNew, mod_name, tr_file):
if params["verbose"]:
print(f"updating {tr_file}")
tr_import = import_tr_file(tr_file)
dOld = tr_import[0]
textOld = tr_import[1]
textNew = strings_to_text(dNew, dOld, mod_name)
if textOld and textOld != textNew:
print(f"{tr_file} has changed.")
shutil.copyfile(tr_file, f"{tr_file}.old")
with open(tr_file, "w", encoding='utf-8') as new_tr_file:
new_tr_file.write(textNew)
# Updates translation files for the mod in the given folder
def update_mod(folder):
modname = get_modname(folder)
if modname is not None:
process_po_files(folder, modname)
print(f"Updating translations for {modname}")
data = generate_template(folder, modname)
if data == None:
print(f"No translatable strings found in {modname}")
else:
for tr_file in get_existing_tr_files(folder):
update_tr_file(data, modname, os.path.join(folder, "locale/", tr_file))
else:
print("Unable to find modname in folder " + folder)
# Determines if the folder being pointed to is a mod or a mod pack
# and then runs update_mod accordingly
def update_folder(folder):
is_modpack = os.path.exists(os.path.join(folder, "modpack.txt")) or os.path.exists(os.path.join(folder, "modpack.conf"))
if is_modpack:
subfolders = [f.path for f in os.scandir(folder) if f.is_dir()]
for subfolder in subfolders:
update_mod(subfolder + "/")
else:
update_mod(folder)
print("Done.")
def run_all_subfolders(folder):
for modfolder in [f.path for f in os.scandir(folder) if f.is_dir()]:
update_folder(modfolder + "/")
main()

209
init.lua
View File

@ -9,18 +9,16 @@
-- When a blank tag stack is used to punch an in-world tag, it inherits that tag's values (continues the chain)
-- Can turn a tag stack blank again via crafting menu
local glow = minetest.setting_get("breadcrumbs_glow_in_the_dark")
local S = minetest.get_translator(minetest.get_current_modname())
local glow_level
if glow == true or glow == nil then
if minetest.settings:get_bool("breadcrumbs_glow_in_the_dark", true) then
glow_level = 4
else
glow_level = 0
end
local particles = minetest.setting_getbool("breadcrumbs_particles")
if particles == nil then
particles = true -- default true
end
local particles = minetest.settings:get_bool("breadcrumbs_particles", true)
local gui_bg, gui_bg_img, wood_sounds
if minetest.get_modpath("default") then
@ -32,20 +30,57 @@ else
gui_bg_img = ""
end
local blank_longdesc = "A blank path marker sign, ready to have a label affixed"
local blank_usagehelp = "To start marking a new path, wield a stack of blank markers. You'll be presented with a form to fill in a short text label that this path will bear, after which you can begin placing path markers as you explore. You can also use a blank marker stack on an existing path marker that's already been placed and you'll copy the marker's label and continue the path from that point when laying down new markers from your copied stack."
--Doctumentation
local blank_longdesc = S("A blank path marker sign, ready to have a label affixed")
local blank_usagehelp = S("To start marking a new path, wield a stack of blank markers. You'll be presented with a form to fill in a short text label that this path will bear, after which you can begin placing path markers as you explore. You can also use a blank marker stack on an existing path marker that's already been placed and you'll copy the marker's label and continue the path from that point when laying down new markers from your copied stack.")
local marker_longdesc = "A path marker with a label affixed"
local marker_usagehelp = "This marker has had a label assigned and is counting the markers you've been laying down."
local marker_longdesc = S("A path marker with a label affixed")
local marker_usagehelp = S("This marker has had a label assigned and is counting the markers you've been laying down.")
if particles then
marker_usagehelp = marker_usagehelp .. " Each marker knows the location of the previous marker in your path, and right-clicking on it will cause it to emit a stream of indicators that only you can see pointing the direction it lies in."
marker_usagehelp = marker_usagehelp .. " " .. S("Each marker knows the location of the previous marker in your path, and right-clicking on it will cause it to emit a stream of indicators that only you can see pointing the direction it lies in.")
end
marker_usagehelp = marker_usagehelp .. " If you place a marker incorrectly you can \"undo\" the placement by clicking on it with the stack you used to place it. Otherwise, markers can only be removed with an axe. Labeled markers can be turned back into blank markers via the crafting grid."
marker_usagehelp = marker_usagehelp .. " " .. S("If you place a marker incorrectly you can \"undo\" the placement by clicking on it with the stack you used to place it. Otherwise, markers can only be removed with an axe. Labeled markers can be turned back into blank markers via the crafting grid.")
-----------------------------------------------------------------
-- HUD markers
local MARKER_DURATION = 60
local hud_markers = {}
local add_hud_marker = function(player, pos, label)
local hud_id = player:hud_add({
hud_elem_type = "waypoint",
name = label,
text = "m",
number = 0xFFFFFF,
world_pos = pos})
table.insert(hud_markers, {player=player, hud_id=hud_id, duration=0})
end
minetest.register_globalstep(function(dtime)
for i=#hud_markers,1,-1 do
local marker = hud_markers[i]
marker.duration = marker.duration + dtime
if marker.duration > MARKER_DURATION then
marker.player:hud_remove(marker.hud_id)
table.remove(hud_markers, i)
end
end
end)
minetest.register_on_leaveplayer(function(player, timed_out)
for i=#hud_markers,1,-1 do
local marker = hud_markers[i]
if marker.player == player then
table.remove(hud_markers, i)
end
end
end)
--------------------------------------------------------------------
local label_text = S("Label:")
local save_text = S("Save")
local formspec = "size[8,2]" .. gui_bg ..
gui_bg_img ..
"field[0.5,1;7.5,0;label;Label:;]" ..
"button_exit[2.5,1.5;3,1;save;Save]"
"field[0.5,1;7.5,0;label;" .. label_text .. ";]" ..
"button_exit[2.5,1.5;3,1;save;" .. save_text .. "]"
minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= "breadcrumbs:blank" then return end
@ -53,21 +88,21 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
local stack = player:get_wielded_item()
if fields.save and fields.label ~= "" then
local data = {}
data.label = fields.label
data.number = 1
local new_stack = ItemStack({name="breadcrumbs:marker", count=stack:get_count(), wear=0, metadata=minetest.serialize(data)})
local new_stack = ItemStack({name="breadcrumbs:marker", count=stack:get_count(), wear=0})
local meta = new_stack:get_meta()
meta:set_string("label", fields.label)
meta:set_int("number", 1)
player:set_wielded_item(new_stack)
end
end)
local tag_to_itemstack = function(pos, count)
local meta = minetest.get_meta(pos)
local data = {}
data.label = meta:get_string("label")
data.number = meta:get_int("number") + 1
data.previous_pos = pos
local new_stack = ItemStack({name="breadcrumbs:marker", count=count, wear=0, metadata=minetest.serialize(data)})
local node_meta = minetest.get_meta(pos)
local new_stack = ItemStack({name="breadcrumbs:marker", count=count, wear=0})
local item_meta = new_stack:get_meta()
item_meta:set_string("label", node_meta:get_string("label"))
item_meta:set_int("number", node_meta:get_int("number") + 1)
item_meta:set_string("previous_pos", minetest.pos_to_string(pos))
return new_stack
end
@ -84,14 +119,13 @@ local read_pointed_thing_tag = function(itemstack, pointed_thing)
end
minetest.register_craftitem("breadcrumbs:blank", {
description = "Blank Marker",
description = S("Blank Marker"),
_doc_items_longdesc = blank_longdesc,
_doc_items_usagehelp = blank_usagehelp,
inventory_image = "breadcrumbs_base.png",
wield_image = "breadcrumbs_base.png",
groups = {flammable = 3},
on_place = function(itemstack, player, pointed_thing)
local itemstack, success = read_pointed_thing_tag(itemstack, pointed_thing)
if success then return itemstack end
@ -108,7 +142,7 @@ minetest.register_craftitem("breadcrumbs:blank", {
})
minetest.register_node("breadcrumbs:marker", {
description = "Marker",
description = S("Marker"),
_doc_items_longdesc = marker_longdesc,
_doc_items_usagehelp = marker_usagehelp,
drawtype = "nodebox",
@ -140,10 +174,11 @@ minetest.register_node("breadcrumbs:marker", {
local playername = placer:get_player_name()
if minetest.is_protected(pos, playername) then return itemstack end
local meta = itemstack:get_metadata()
local data = minetest.deserialize(meta)
if not data then return itemstack end
local item_meta = itemstack:get_meta()
local label = item_meta:get_string("label")
if label == "" then return itemstack end -- don't place if there's no data
local number = item_meta:get_int("number")
local previous_pos_string = item_meta:get_string("previous_pos")
local success
itemstack, success = minetest.item_place(itemstack, placer, pointed_thing)
@ -151,25 +186,23 @@ minetest.register_node("breadcrumbs:marker", {
if not success then return itemstack end
local node_meta = minetest.get_meta(pos)
node_meta:set_string("label", data.label)
node_meta:set_int("number", data.number)
node_meta:set_string("label", label)
node_meta:set_int("number", number)
if data.number > 1 and data.previous_pos then
node_meta:set_int("previous_pos_x", data.previous_pos.x)
node_meta:set_int("previous_pos_y", data.previous_pos.y)
node_meta:set_int("previous_pos_z", data.previous_pos.z)
local dist = vector.distance(pos, data.previous_pos)
node_meta:set_string("infotext",
string.format("%s #%d\nPlaced by %s\n%dm from last marker", data.label, data.number, playername, dist))
if number > 1 and previous_pos_string ~= "" then
local previous_pos = minetest.string_to_pos(previous_pos_string)
node_meta:set_string("previous_pos", previous_pos_string)
local dist = math.floor(vector.distance(pos, previous_pos))
node_meta:set_string("infotext", S("@1 #@2\nPlaced by @3", label, number, playername)
.. "\n" .. S("@1m from last marker", dist))
else
node_meta:set_string("infotext",
string.format("%s #%d\nPlaced by %s", data.label, data.number, playername))
node_meta:set_string("infotext", S("@1 #@2\nPlaced by @3", label, number, playername))
end
data.number = data.number + 1
data.previous_pos = pos
itemstack:set_metadata(minetest.serialize(data))
local item_meta = itemstack:get_meta()
item_meta:set_string("label", label)
item_meta:set_int("number", number + 1)
item_meta:set_string("previous_pos", minetest.pos_to_string(pos))
return itemstack
end,
@ -184,37 +217,41 @@ minetest.register_node("breadcrumbs:marker", {
if node.name ~= "breadcrumbs:marker" then return itemstack end
local node_meta = minetest.get_meta(pos)
local item_data = minetest.deserialize(itemstack:get_metadata())
local item_meta = itemstack:get_meta()
if node_meta:get_string("label") == item_data.label and
node_meta:get_int("number") == item_data.number - 1 then
item_data.number = item_data.number - 1
item_data.previous_pos.x = node_meta:get_int("previous_pos_x")
item_data.previous_pos.y = node_meta:get_int("previous_pos_y")
item_data.previous_pos.z = node_meta:get_int("previous_pos_z")
itemstack:set_metadata(minetest.serialize(item_data))
local item_label = item_meta:get_string("label")
local item_number = item_meta:get_int("number")
if node_meta:get_string("label") == item_label and node_meta:get_int("number") == item_number - 1 then
item_meta:set_int("number", item_number - 1)
item_meta:set_string("previous_pos", node_meta:get_string("previous_pos"))
itemstack:set_count(itemstack:get_count() + 1)
minetest.remove_node(pos)
end
return itemstack
end,
-- If the player's right-clicking with a blank sign stack, copy the sign's state onto it.
-- Show particle stream directed at last sign, provided particles are enabled for this mod
on_rightclick = function(pos, node, player, itemstack, pointed_thing)
if itemstack:get_name() == "breadcrumbs:blank" then
return tag_to_itemstack(pos, itemstack:get_count())
end
local meta = minetest.get_meta(pos)
local previous_pos = {}
previous_pos.x = meta:get_int("previous_pos_x")
previous_pos.y = meta:get_int("previous_pos_y")
previous_pos.z = meta:get_int("previous_pos_z")
local node_meta = minetest.get_meta(pos)
local previous_pos_string = node_meta:get_string("previous_pos")
if meta:get_int("number") > 1 and previous_pos.x and previous_pos.y and previous_pos.z and particles then
local dir = vector.multiply(vector.direction(pos, previous_pos), 2)
if node_meta:get_int("number") > 1 and previous_pos_string ~= "" and particles then
local previous_pos = minetest.string_to_pos(previous_pos_string)
local label = node_meta:get_string("label")
local number = node_meta:get_int("number") - 1
local distance = math.min(vector.distance(pos, previous_pos), 60) -- Particle stream extends no more than 60 meters
local dir = vector.multiply(vector.direction(pos, previous_pos), distance/10) -- divide distance by exptime
add_hud_marker(player, previous_pos, label .. " #" .. tostring(number))
minetest.add_particlespawner({
amount = 100,
time = 10,
time = MARKER_DURATION,
minpos = pos,
maxpos = pos,
minvel = dir,
@ -250,3 +287,51 @@ minetest.register_craft({
{'', 'group:stick', ''},
}
})
local wood_burn_time = minetest.get_craft_result({method="fuel", width=1, items={ItemStack("default:wood")}}).time
local stick_burn_time = minetest.get_craft_result({method="fuel", width=1, items={ItemStack("default:stick")}}).time
local marker_burn_time = math.floor((wood_burn_time * 4 + stick_burn_time) / 8)
minetest.register_craft({
type = "fuel",
recipe = "breadcrumbs:marker",
burntime = marker_burn_time,
})
minetest.register_craft({
type = "fuel",
recipe = "breadcrumbs:marker",
burntime = marker_burn_time,
})
if minetest.get_modpath("loot") then
loot.register_loot({
weights = { generic = 100 },
payload = {
stack = ItemStack("breadcrumbs:blank"),
min_size = 30,
max_size = 99,
},
})
end
minetest.register_lbm({
label = "Upgrade legacy breadcrumb previous_pos",
name = "breadcrumbs:upgrade_previous_pos",
nodenames = {"breadcrumbs:marker"},
run_at_every_load = false,
action = function(pos, node)
local node_meta = minetest.get_meta(pos)
-- The previous_pos used to be stored as a set of three integer metadatas instead of one string
local previous_pos_x = tonumber(node_meta:get_string("previous_pos_x"))
if previous_pos_x ~= nil then
local previous_pos_y = node_meta:get_int("previous_pos_y")
local previous_pos_z = node_meta:get_int("previous_pos_z")
previous_pos_string = minetest.pos_to_string({x=previous_pos_x, y=previous_pos_y, z=previous_pos_z})
node_meta:set_string("previous_pos", previous_pos_string)
node_meta:set_string("previous_pos_x", "")
node_meta:set_string("previous_pos_y", "")
node_meta:set_string("previous_pos_z", "")
end
end,
})

14
locale/template.txt Normal file
View File

@ -0,0 +1,14 @@
# ../breadcrumbs/init.lua
@1 #@2@nPlaced by @3=
@1m from last marker=
A blank path marker sign, ready to have a label affixed=
A path marker with a label affixed=
Blank Marker=
Each marker knows the location of the previous marker in your path, and right-clicking on it will cause it to emit a stream of indicators that only you can see pointing the direction it lies in.=
If you place a marker incorrectly you can "undo" the placement by clicking on it with the stack you used to place it. Otherwise, markers can only be removed with an axe. Labeled markers can be turned back into blank markers via the crafting grid.=
Label:=
Marker=
Save=
This marker has had a label assigned and is counting the markers you've been laying down.=
To start marking a new path, wield a stack of blank markers. You'll be presented with a form to fill in a short text label that this path will bear, after which you can begin placing path markers as you explore. You can also use a blank marker stack on an existing path marker that's already been placed and you'll copy the marker's label and continue the path from that point when laying down new markers from your copied stack.=

View File

@ -1 +1,3 @@
name = breadcrumbs
description = Path marker signs for use when exploring a twisty maze of passages that are all alike.
optional_depends = default, doc, loot

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 500 B

After

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 334 B

After

Width:  |  Height:  |  Size: 303 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 634 B

After

Width:  |  Height:  |  Size: 372 B