This commit is contained in:
Rainer 2025-08-22 01:59:14 +02:00
commit 18e9c71cad
28 changed files with 7335 additions and 0 deletions

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 entuland
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.

89
README.md Normal file
View file

@ -0,0 +1,89 @@
# tpad
A teleporter-pads mod for Minetest
Developed and tested on Minetest 0.4.16 - try in other versions at your own risk :)
WIP mod forum discussion: https://forum.minetest.net/viewtopic.php?f=9&t=20081
**Table of Contents**
- [Recipe](#recipe)
- [Features](#features)
- [Appearance](#appearance)
- [Pad types](#pad-types)
- [Pad interaction](#pad-interaction)
- [Closest pad waypoint](#closest-pad-waypoint)
- [Pad admin](#pad-admin)
- [Screenshots](#screenshots)
## Recipe
The recipe can be customized altering the file `custom.recipes.lua`, created in the mod's folder on first run and never overwritten.
W = any wood planks
B = bronze ingot
WBW
BWB
WBW
![Crafting](/screenshots/crafting.png)
## Features
With these pads players can build their own Local Network and collaborate to build a Global Network shared among all players.
Pads are sorted by name in the lists, the Global Network list groups them by owner name first.
## Appearance
This is how a pad looks like when placed against a wall or on the floor (they can be placed under the ceiling as well):
![Pads](/screenshots/pads.png)
## Pad types
Each pad can be set as one of these three types:
- `Private` (default): only accessible to its owner or by an admin
- `Public`: accessible to anyone from any Public pad of the owner's Local Network
- `Global`: accessible to anyone from any Public pad
A pad can be edited and destroyed only by its owner or by an admin.
## Pad interaction
- place a pad down, it will be immediately active and set as "Private"
- right click a pad edit its name/type and to access the Networks
- select a pad from a list and hit "Teleport", or doubleclick on the list item
- delete any remote pad by selecting it on the Local Network list and clicking "Delete"
## Closest pad waypoint
Issue `/tpad` on the chat to get a waypoint to the closest of your pads.
Issue `/tpad off` to remove the waypoint from the HUD - it will also be removed when you teleport from any pad.
## Pad admin
A `tpad_admin` privilege is available, players with such privilege can access, alter and destroy any pad, they can set the max number of total / global pads a player can create and they can also place any amount of pads regardless of those limits.
Limits can be edited by admins directly in the admin interface (reachable from the "Global Network" dialog of any pad); limits get stored on a per-world basis in the file `/mod_storage/tpad.custom.conf`. By default a player can place up to 100 pads, and of these, only 4 can appear in the Global Network.
## Screenshots
A public pad's interface seen by a visitor:
![Local Network Visitor](/screenshots/local-network-visitor.png)
The same interface seen by its owner or by an admin (notice the highlighted "private" pad, which was hidden in the previous interface):
![Local Network](/screenshots/local-network.png)
The Global Network interface seen by a non-admin:
![Global Network](/screenshots/global-network.png)
Same as above, but the admins can see an "Admin" button:
![Global Network Admin](/screenshots/global-network-admin.png)
Admin settings interface:
![Admin Settings](/screenshots/admin-settings.png)

12
custom.recipes.lua Normal file
View file

@ -0,0 +1,12 @@
-- only alter this file if it's named "custom.recipes.lua"
-- alter the recipes as you please and delete / comment out
-- the recipes you don't want to be available in the game
-- the original versions are in "default/recipes.lua"
return {
["tpad:tpad"] = {
{'group:wood', 'default:bronze_ingot', 'group:wood'},
{'default:bronze_ingot', 'group:wood', 'default:bronze_ingot'},
{'group:wood', 'default:bronze_ingot', 'group:wood'},
},
}

2
default/README.txt Normal file
View file

@ -0,0 +1,2 @@
please do not edit any file in this folder,
corresponding custom.* files get created in the main mod's folder for you to customize

12
default/recipes.lua Normal file
View file

@ -0,0 +1,12 @@
-- only alter this file if it's named "custom.recipes.lua"
-- alter the recipes as you please and delete / comment out
-- the recipes you don't want to be available in the game
-- the original versions are in "default/recipes.lua"
return {
["tpad:tpad"] = {
{'group:wood', 'default:bronze_ingot', 'group:wood'},
{'default:bronze_ingot', 'group:wood', 'default:bronze_ingot'},
{'group:wood', 'default:bronze_ingot', 'group:wood'},
},
}

938
init.lua Normal file
View file

@ -0,0 +1,938 @@
-- ========================================================================
-- TPAD MOD v1.2 (Final, Reworked Delete Logic)
-- ========================================================================
tpad = {}
tpad.version = "1.2" -- As requested, version is not incremented
tpad.mod_name = minetest.get_current_modname()
tpad.texture = "tpad-texture.png"
tpad.mesh = "tpad-mesh.obj"
tpad.nodename = "tpad:tpad"
tpad.mod_path = minetest.get_modpath(tpad.mod_name)
tpad.sound_teleport = tpad.mod_name .. "_teleport"
tpad.particle_texture = tpad.mod_name .. "_particle.png"
-- Temporäre Variable zur sicheren Datenübergabe an den Bestätigungsdialog
tpad.pending_deletion = {}
-- ========================================================================
-- Constants
-- ========================================================================
local PRIVATE_PAD_STRING = "Privat (nur Besitzer)"
local PUBLIC_PAD_STRING = "Lokal (nur eigenes Netzwerk)"
local GLOBAL_PAD_STRING = "Global (beliebiges Netzwerk)"
local PRIVATE_PAD = 1
local PUBLIC_PAD = 2
local GLOBAL_PAD = 4
local RED_ESCAPE = minetest.get_color_escape_sequence("#FF0000")
local YELLOW_ESCAPE = minetest.get_color_escape_sequence("#FFFF00")
local CYAN_ESCAPE = minetest.get_color_escape_sequence("#00FFFF")
local WHITE_ESCAPE = minetest.get_color_escape_sequence("#FFFFFF")
local OWNER_ESCAPE_COLOR = CYAN_ESCAPE
local padtype_flag_to_string = {
[PRIVATE_PAD] = PRIVATE_PAD_STRING,
[PUBLIC_PAD] = PUBLIC_PAD_STRING,
[GLOBAL_PAD] = GLOBAL_PAD_STRING,
}
local padtype_string_to_flag = {
[PRIVATE_PAD_STRING] = PRIVATE_PAD,
[PUBLIC_PAD_STRING] = PUBLIC_PAD,
[GLOBAL_PAD_STRING] = GLOBAL_PAD,
}
local short_padtype_string = {
[PRIVATE_PAD] = "private",
[PUBLIC_PAD] = "public",
[GLOBAL_PAD] = "global",
}
-- ========================================================================
-- Dependencies and Libs
-- ========================================================================
local smartfs = dofile(tpad.mod_path .. "/lib/smartfs.lua")
local notify = dofile(tpad.mod_path .. "/notify.lua")
local waypoint_hud_ids = {}
minetest.register_privilege("tpad_admin", {
description = "Can edit and destroy any tpad",
give_to_singleplayer = true,
})
-- ========================================================================
-- Original Helper Functions
-- ========================================================================
local function copy_file(source, dest)
local src_file = io.open(source, "rb")
if not src_file then return false, "copy_file() unable to open source for reading" end
local src_data = src_file:read("*all")
src_file:close()
local dest_file = io.open(dest, "wb")
if not dest_file then return false, "copy_file() unable to open dest for writing" end
dest_file:write(src_data)
dest_file:close()
return true, "files copied successfully"
end
local function custom_or_default(modname, path, filename)
local default_filename = "default/" .. filename
local full_filename = path .. "/custom." .. filename
local full_default_filename = path .. "/" .. default_filename
local file_exists_at_path = io.open(path .. "/" .. filename, "r")
if file_exists_at_path then
file_exists_at_path:close()
os.rename(path .. "/" .. filename, full_filename)
end
local file = io.open(full_filename, "rb")
if not file then
minetest.debug("[" .. modname .. "] Copying " .. default_filename .. " to " .. filename .. " (path: " .. path .. ")")
local success, err = copy_file(full_default_filename, full_filename)
if not success then
minetest.debug("[" .. modname .. "] " .. err)
return false
end
file = io.open(full_filename, "rb")
if not file then
minetest.debug("[" .. modname .. "] Unable to load " .. filename .. " file from path " .. path)
return false
end
end
file:close()
return full_filename
end
dofile(tpad.mod_path .. "/storage.lua")
-- ========================================================================
-- Load Custom Recipe
-- ========================================================================
local recipes_filename = custom_or_default(tpad.mod_name, tpad.mod_path, "recipes.lua")
if recipes_filename then
local recipes = dofile(recipes_filename)
if type(recipes) == "table" and recipes[tpad.nodename] then
minetest.register_craft({
output = tpad.nodename,
recipe = recipes[tpad.nodename],
})
end
end
-- ========================================================================
-- Chat Command
-- ========================================================================
function tpad.command(playername, param)
tpad.hud_off(playername)
if(param == "off") then return end
local player = minetest.get_player_by_name(playername)
local pads = tpad._get_stored_pads(playername)
local shortest_distance = nil
local closest_pad = nil
local playerpos = player:getpos()
for strpos, pad in pairs(pads) do
local pos = minetest.string_to_pos(strpos)
local distance = vector.distance(pos, playerpos)
if not shortest_distance or distance < shortest_distance then
closest_pad = {
pos = pos,
name = pad.name .. " " .. strpos,
}
shortest_distance = distance
end
end
if closest_pad then
waypoint_hud_ids[playername] = player:hud_add({
hud_elem_type = "waypoint",
name = closest_pad.name,
world_pos = closest_pad.pos,
number = 0xFF0000,
})
notify(playername, "Waypoint to " .. closest_pad.name .. " displayed")
end
end
function tpad.hud_off(playername)
local player = minetest.get_player_by_name(playername)
local hud_id = waypoint_hud_ids[playername]
if hud_id then
player:hud_remove(hud_id)
end
end
-- ========================================================================
-- Teleport Logic
-- ========================================================================
function tpad.do_teleport(player, destination_pos, destination_name, form_context)
local playername = player:get_player_name()
minetest.after(0.1, function()
if not player or not player:is_player() then return end
local start_pos = player:getpos()
minetest.sound_play(tpad.sound_teleport, {pos = start_pos, max_hear_distance = 10, gain = 1.0})
minetest.add_particlespawner({
amount = 60, time = 0.5,
minpos = vector.add(start_pos, -0.5), maxpos = vector.add(start_pos, 0.5),
minvel = {x=-1, y=0, z=-1}, maxvel = {x=1, y=2, z=1},
minacc = {x=0, y=0, z=0}, maxacc = {x=0, y=0, z=0},
minexptime = 0.5, maxexptime = 2,
minsize = 1, maxsize = 3,
texture = tpad.particle_texture,
})
player:move_to(destination_pos)
minetest.sound_play(tpad.sound_teleport, {pos = destination_pos, max_hear_distance = 10, gain = 1.0})
minetest.add_particlespawner({
amount = 60, time = 0.5,
minpos = vector.add(destination_pos, {x = -0.5, y = 0, z = -0.5}),
maxpos = vector.add(destination_pos, {x = 0.5, y = 1, z = 0.5}),
minvel = {x=-1, y=1, z=-1}, maxvel = {x=1, y=2, z=1},
minacc = {x=0, y=0, z=0}, maxacc = {x=0, y=0, z=0},
minexptime = 0.5, maxexptime = 2,
minsize = 1, maxsize = 3,
texture = tpad.particle_texture,
})
tpad.hud_off(playername)
-- NEU: Nach Ankunft am Ziel die Ansicht mit neuem Kontext aktualisieren
minetest.after(0.2, function()
if not player or not player:is_player() then return end
local dest_pad_data = tpad.get_pad_data(destination_pos)
if not dest_pad_data then return end
local new_context = {
playername = playername,
clicker = player,
ownername = dest_pad_data.owner,
clicked_pos = destination_pos,
node = minetest.get_node(destination_pos),
page = 1,
-- Behalte den Netzwerk-Typ (lokal/global) bei
network_type = form_context.network_type
}
tpad.show_network_view(new_context, new_context.network_type)
end)
end)
end
-- ========================================================================
-- Node Placement and Data Helpers
-- ========================================================================
function tpad.after_place_node(pos, placer, itemstack)
local playername = placer:get_player_name()
if tpad.max_total_pads_reached(placer) then
notify.warn(playername, "Du kannst keine weiteren TPADs erstellen. Limit erreicht.")
minetest.remove_node(pos)
minetest.add_item(placer:get_pos(), itemstack:get_name())
return
end
local meta = minetest.get_meta(pos)
meta:set_string("owner", playername)
meta:set_string("infotext", "TPAD Station von " .. playername)
tpad.set_pad_data(pos, "", PRIVATE_PAD_STRING)
end
local submit = {}
function tpad.max_total_pads_reached(placer)
local placername = placer:get_player_name()
if minetest.get_player_privs(placername).tpad_admin then
return false
end
local pads = tpad._get_stored_pads(placername)
local count = 0
for _ in pairs(pads) do
count = count + 1
end
return count >= tpad.get_max_total_pads()
end
function tpad.max_global_pads_reached(playername)
if minetest.get_player_privs(playername).tpad_admin then
return false
end
local pads = tpad._get_stored_pads(playername)
local count = 0
for _, pad in pairs(pads) do
if pad.type == GLOBAL_PAD then
count = count + 1
end
end
return count >= tpad.get_max_global_pads()
end
-- ========================================================================
-- GUI DATA HELPERS
-- ========================================================================
function submit.global_helper()
local allpads = tpad._get_all_pads()
local result = {}
for ownername, pads in pairs(allpads) do
for strpos, pad in pairs(pads) do
if pad.type == GLOBAL_PAD then
-- Der 'viewername' ist hier nicht bekannt, also wird 'nil' übergeben, was die Funktion korrekt behandelt.
table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername, nil))
end
end
end
table.sort(result, function(a, b) return a.global_fullname:lower() < b.global_fullname:lower() end)
return result
end
function submit.local_helper(ownername, viewername)
local result = {}
local added_pads = {}
local owner_pads = tpad._get_stored_pads(ownername)
for strpos, pad in pairs(owner_pads) do
local is_viewer_the_owner = (ownername == viewername)
if (pad.type == PUBLIC_PAD) or (is_viewer_the_owner and pad.type == PRIVATE_PAD) then
if pad.type ~= GLOBAL_PAD then
table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername, viewername))
added_pads[strpos] = true
end
end
end
if ownername ~= viewername then
local viewer_pads = tpad._get_stored_pads(viewername)
for strpos, pad in pairs(viewer_pads) do
if (pad.type == PUBLIC_PAD or pad.type == PRIVATE_PAD) and not added_pads[strpos] then
table.insert(result, tpad.decorate_pad_data(strpos, pad, viewername, viewername))
added_pads[strpos] = true
end
end
end
table.sort(result, function(a, b) return a.local_fullname:lower() < b.local_fullname:lower() end)
return result
end
function submit.management_helper(playername)
local is_admin = minetest.get_player_privs(playername).tpad_admin
local result = {}
if is_admin then
local allpads = tpad._get_all_pads()
for ownername, pads in pairs(allpads) do
for strpos, pad in pairs(pads) do
table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername, playername))
end
end
else
local pads = tpad._get_stored_pads(playername)
for strpos, pad in pairs(pads) do
table.insert(result, tpad.decorate_pad_data(strpos, pad, playername, playername))
end
end
table.sort(result, function(a, b) return a.local_fullname:lower() < b.local_fullname:lower() end)
return result
end
-- ========================================================================
-- GUI LOGIC (REFACTORED)
-- ========================================================================
function tpad.show_network_view(form, network_type)
local is_admin = minetest.get_player_privs(form.playername).tpad_admin
local form_key = is_admin and "network_view_admin" or "network_view"
form.state = tpad.forms[form_key]:show(form.playername)
form.formname = "tpad.forms." .. form_key
local pad_list
local title_text
local function create_clean_context()
return {
playername = form.playername,
clicker = form.clicker,
ownername = form.ownername,
clicked_pos = form.clicked_pos,
node = form.node,
page = form.page or 1
}
end
local current_pad_data = tpad.get_pad_data(form.clicked_pos)
local current_pad_name = current_pad_data.name or "Unnamed"
local current_pad_type_str
if current_pad_data.type == GLOBAL_PAD then
current_pad_type_str = "GLOBALE"
else
current_pad_type_str = "LOKALE"
end
local RED_ESCAPE = minetest.get_color_escape_sequence("#FF0000") or ""
title_text = RED_ESCAPE .. current_pad_type_str .. " " .. WHITE_ESCAPE .. "TPAD-Station " .. YELLOW_ESCAPE .. current_pad_name .. WHITE_ESCAPE .. ". Wähle ein Ziel:"
if network_type == "global" then
form.network_type = "global"
pad_list = submit.global_helper()
form.state:get("toggle_network_button"):setText("Lokales Netzwerk")
form.state:get("toggle_network_button"):onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
-- KORREKTUR: Der 'ownername' wird nicht mehr überschrieben.
-- Er bleibt der des ursprünglichen Pads, was korrekt ist.
clean_form.page = 1
tpad.show_network_view(clean_form, "local")
end)
end)
else -- "local"
form.network_type = "local"
-- KORREKTUR: Übergebe den Spielernamen (string), nicht das Ergebnis eines Vergleichs (boolean).
pad_list = submit.local_helper(form.ownername, form.playername)
form.state:get("toggle_network_button"):setText("Globales Netzwerk")
form.state:get("toggle_network_button"):onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
clean_form.page = 1
tpad.show_network_view(clean_form, "global")
end)
end)
end
form.state:get("title_label"):setText(title_text)
form.state:get("management_button"):onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
clean_form.preselect_pos = clean_form.clicked_pos
submit.management(clean_form)
end)
end)
if is_admin then
form.state:get("admin_button"):onClick(function()
minetest.after(0, function() submit.admin_settings(form) end)
end)
end
local max_rows, num_columns = 10, 3
local buttons_per_page = max_rows * num_columns
local destination_pads = {}
for _, pad in ipairs(pad_list) do
if not vector.equals(pad.pos, form.clicked_pos) then
table.insert(destination_pads, pad)
end
end
local total_pages = math.ceil(#destination_pads / buttons_per_page)
if total_pages == 0 then total_pages = 1 end
local current_page = form.page or 1
if current_page > total_pages then current_page = total_pages end
if current_page < 1 then current_page = 1 end
form.page = current_page
for i = 1, buttons_per_page do
local index = ((current_page - 1) * buttons_per_page) + i
local pad_data = destination_pads[index]
local button_name = "tpad_btn_" .. i
local button = form.state:get(button_name)
if button then
if pad_data then
local display_name = (network_type == "global") and pad_data.global_fullname or pad_data.local_fullname
button:setText(display_name)
button:setVisible(true)
button:onClick(function()
tpad.do_teleport(form.clicker, pad_data.pos, display_name, form)
end)
else
button:setVisible(false)
end
end
end
if total_pages > 1 then
form.state:get("page_label"):setText("Seite " .. current_page .. " von " .. total_pages)
local prev_button, next_button = form.state:get("prev_button"), form.state:get("next_button")
prev_button:setVisible(current_page > 1)
next_button:setVisible(current_page < total_pages)
prev_button:onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
clean_form.page = current_page - 1
tpad.show_network_view(clean_form, network_type)
end)
end)
next_button:onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
clean_form.page = current_page + 1
tpad.show_network_view(clean_form, network_type)
end)
end)
else
form.state:get("page_label"):setText("")
form.state:get("prev_button"):setVisible(false)
form.state:get("next_button"):setVisible(false)
end
end
function submit.management(form)
form.formname = "tpad_management"
form.state = tpad.forms.management:show(form.playername)
local pad_list = submit.management_helper(form.playername)
local listbox = form.state:get("pads_listbox")
local is_admin = minetest.get_player_privs(form.playername).tpad_admin
local selected_pad_data = nil
listbox:clearItems()
for _, pad in ipairs(pad_list) do
local label = pad.name .. " (" .. short_padtype_string[pad.type] .. ")"
if is_admin then
label = label .. " [" .. pad.owner .. "]"
end
listbox:addItem(label)
end
local padname_field = form.state:get("padname_field")
local padtype_dropdown = form.state:get("padtype_dropdown")
local function update_fields_for_index(index)
if not index or index <= 0 then return end
selected_pad_data = pad_list[index]
if selected_pad_data then
padname_field:setText(selected_pad_data.name)
padtype_dropdown:setSelectedItem(padtype_flag_to_string[selected_pad_data.type])
end
end
if form.preselect_pos then
for i, pad in ipairs(pad_list) do
if vector.equals(pad.pos, form.preselect_pos) then
listbox:setSelected(i)
update_fields_for_index(i)
break
end
end
end
listbox:onClick(function()
local index = listbox:getSelected()
update_fields_for_index(index)
end)
form.state:get("save_button"):onClick(function()
if not selected_pad_data then
notify.warn(form.playername, "Bitte wähle zuerst ein TPAD aus der Liste aus.")
return
end
local new_name = padname_field:getText()
local new_type_str
local value_from_dropdown = padtype_dropdown:getSelectedItem()
if value_from_dropdown then
local index = tonumber(value_from_dropdown)
if index then
new_type_str = padtype_dropdown:getItem(index)
else
new_type_str = value_from_dropdown
end
end
if not new_type_str or new_type_str == "" then
notify.err(form.playername, "Konnte TPAD-Typ nicht lesen. Bitte erneut versuchen.")
return
end
if not minetest.get_player_privs(form.playername).tpad_admin then
if new_type_str == GLOBAL_PAD_STRING and tpad.max_global_pads_reached(selected_pad_data.owner) then
notify.warn(form.playername, "Der Besitzer des TPADs hat das Limit für globale TPADs erreicht.")
return
end
end
tpad.set_pad_data(selected_pad_data.pos, new_name, new_type_str)
local meta = minetest.get_meta(selected_pad_data.pos)
if new_name and new_name ~= "" then
meta:set_string("infotext", "TPAD Station " .. new_name)
else
meta:set_string("infotext", "Unbenannte TPAD Station")
end
notify(form.playername, "TPAD '" .. new_name .. "' gespeichert.")
minetest.after(0, function()
form.preselect_pos = nil
submit.management(form)
end)
end)
form.state:get("delete_button"):onClick(function()
if not selected_pad_data then
notify.warn(form.playername, "Bitte wähle zuerst ein TPAD aus der Liste aus.")
return
end
if vector.equals(selected_pad_data.pos, form.clicked_pos) then
notify.warn(form.playername, "Du kannst das TPAD, an dem du stehst, nicht über dieses Menü löschen.")
return
end
tpad.pending_deletion[form.playername] = {
pad_data = selected_pad_data,
original_form_context = form,
}
minetest.close_formspec(form.playername, form.formname)
minetest.after(0.1, function()
tpad.forms.confirm_pad_deletion:show(form.playername)
end)
end)
form.state:get("back_button"):onClick(function()
minetest.after(0, function()
local clean_form_context = {
playername = form.playername,
clicker = form.clicker,
ownername = form.ownername,
clicked_pos = form.clicked_pos,
node = form.node,
page = 1
}
tpad.show_network_view(clean_form_context, form.network_type)
end)
end)
-- NEU: Logik für den Admin-Teleport-Button
if is_admin then
local admin_teleport_button = form.state:get("admin_teleport_button")
admin_teleport_button:setVisible(true)
admin_teleport_button:onClick(function()
if not selected_pad_data then
notify.warn(form.playername, "Bitte zuerst ein TPAD aus der Liste auswählen, um dorthin zu teleportieren.")
return
end
local dest_pad = selected_pad_data
-- Erstelle den Kontext für die Ansicht, die nach dem Teleport angezeigt werden soll
local new_context_after_teleport = {
playername = form.playername,
clicker = form.clicker,
ownername = dest_pad.owner,
clicked_pos = dest_pad.pos,
node = minetest.get_node(dest_pad.pos),
page = 1,
network_type = (dest_pad.type == GLOBAL_PAD) and "global" or "local"
}
tpad.do_teleport(form.clicker, dest_pad.pos, dest_pad.name, new_context_after_teleport)
end)
end
end
function submit.admin_settings(form)
form.state = tpad.forms.admin:show(form.playername)
form.formname = "tpad_admin_settings"
local max_total_field, max_global_field = form.state:get("max_total_field"), form.state:get("max_global_field")
max_total_field:setText(tpad.get_max_total_pads())
max_global_field:setText(tpad.get_max_global_pads())
form.state:get("save_button"):onClick(function()
tpad.set_max_total_pads(tonumber(max_total_field:getText()))
tpad.set_max_global_pads(tonumber(max_global_field:getText()))
minetest.close_formspec(form.playername, form.formname)
end)
end
-- ========================================================================
-- Main Node Callbacks
-- ========================================================================
function tpad.on_rightclick(clicked_pos, node, clicker)
local playername = clicker:get_player_name()
local pad = tpad.get_pad_data(clicked_pos, playername)
if not pad or not pad.owner then
notify.err(playername, "Fehler! Fehlende oder korrupte TPAD-Daten. Bitte neu platzieren.")
return
end
-- Zugriffsschutz-Prüfung für private Pads
if pad.type == PRIVATE_PAD and pad.owner ~= playername and not minetest.get_player_privs(playername).tpad_admin then
local RED_ESCAPE = minetest.get_color_escape_sequence("#FF0000") or ""
notify.warn(playername, YELLOW_ESCAPE .. "PRIVATE" .. WHITE_ESCAPE .. " TPAD Station von " .. pad.owner .. ". " .. RED_ESCAPE .. "Zugriff verweigert.")
return
end
-- Erstelle das Kontext-Objekt für die Formulare
local form = {
playername = playername,
clicker = clicker,
ownername = pad.owner,
clicked_pos = clicked_pos,
node = node,
page = 1,
}
-- Wenn das Pad neu ist (keinen Namen hat), direkt zur Verwaltung
if pad.name == "" then
form.preselect_pos = clicked_pos
submit.management(form)
else
-- Ansonsten zeige die entsprechende Netzwerk-Ansicht
if pad.type == GLOBAL_PAD then
tpad.show_network_view(form, "global")
else
tpad.show_network_view(form, "local")
end
end
end
function tpad.can_dig(pos, player)
local meta = minetest.get_meta(pos)
local ownername = meta:get_string("owner")
local playername = player:get_player_name()
if ownername == "" or ownername == playername or minetest.get_player_privs(playername).tpad_admin then
return true
end
notify.warn(playername, "Dieses TPAD gehört dir nicht.")
return false
end
function tpad.on_destruct(pos)
local meta = minetest.get_meta(pos)
local ownername = meta:get_string("owner")
if ownername and ownername ~= "" then
tpad.del_pad(ownername, pos)
end
end
-- ========================================================================
-- FORMS (Rebuilt)
-- ========================================================================
tpad.forms = {}
local function create_network_view_form(state)
state:size(16, 11)
state:label(0.5, 0.2, "title_label", "")
local bottom_y = 10.4
state:button(0.5, bottom_y, 3.0, 0, "toggle_network_button", "")
state:button(3.6, bottom_y, 2.3, 0, "management_button", "Verwaltung")
local close_button = state:button(14.0, bottom_y, 1.5, 0, "close_button", "Schließen")
close_button:setClose(true)
state:button(6.0, bottom_y, 1.0, 0, "prev_button", "[<<]")
state:label(7.1, bottom_y, "page_label", "")
state:button(9.5, bottom_y, 1.0, 0, "next_button", "[>>]")
local max_rows, num_columns = 10, 3
local start_x, start_y = 0.5, 1.0
local button_width, button_height = 4.8, 0.8
local column_width = 5.0
for i = 1, max_rows * num_columns do
local column = math.floor((i - 1) / max_rows)
local row = (i - 1) % max_rows
local current_x = start_x + (column * column_width)
local current_y = start_y + (row * button_height)
state:button(current_x, current_y, button_width, 0, "tpad_btn_" .. i, ""):setVisible(false)
end
end
tpad.forms.teleport_success = smartfs.create("tpad.forms.teleport_success", function(state)
state:size(8, 2)
local destination_name = state.param.destination_name or "???"
state:label(0.5, 0.5, "success_label", "Teleport erfolgreich: " .. YELLOW_ESCAPE .. destination_name)
-- Dieser Button hat setClose(true), was das Fenster zuverlässig schließt.
local close_button = state:button(3, 1.2, 2, 0, "close_button", "Schließen")
close_button:setClose(true)
end)
tpad.forms.network_view = smartfs.create("tpad.forms.network_view", create_network_view_form)
tpad.forms.network_view_admin = smartfs.create("tpad.forms.network_view_admin", function(state)
create_network_view_form(state)
state:button(12.4, 10.4, 1.5, 0, "admin_button", "Admin")
end)
tpad.forms.management = smartfs.create("tpad.forms.management", function(state)
state:size(12, 9)
state:label(0.2, 0.2, "management_title", "TPAD Verwaltung")
state:listbox(0.2, 0.6, 11.6, 5, "pads_listbox", {})
state:field(0.5, 6.6, 6, 0, "padname_field", "Name", "")
local padtype_dropdown = state:dropdown(0.25, 6.7, 6.25, 0, "padtype_dropdown")
padtype_dropdown:addItem(PRIVATE_PAD_STRING)
padtype_dropdown:addItem(PUBLIC_PAD_STRING)
padtype_dropdown:addItem(GLOBAL_PAD_STRING)
state:button(7, 6.3, 2, 0, "save_button", "Speichern")
state:button(7, 7.1, 2, 0, "delete_button", "Löschen")
state:button(0.2, 8.4, 2, 0, "back_button", "Zurück")
-- NEU: Teleport-Button für Admins, standardmäßig unsichtbar
state:button(2.3, 8.4, 3, 0, "admin_teleport_button", "Teleport (Admin)"):setVisible(false)
local close_button = state:button(9.8, 8.4, 2, 0, "close_button", "Schließen")
close_button:setClose(true)
end)
-- This form now defines its OWN behavior, making it independent and robust.
tpad.forms.confirm_pad_deletion = smartfs.create("tpad.forms.confirm_pad_deletion", function(state)
state:size(8, 2.5)
state:label(0, 0, "intro_label", "Willst du das TPAD wirklich löschen?")
state:label(0, 0.5, "padname_label", "")
state:label(0, 1, "outro_label", "(es lässt sich nicht wiederherstellen)")
local confirm_button = state:button(0, 2.2, 2, 0, "confirm_button", "Ja, löschen")
local deny_button = state:button(6, 2.2, 2, 0, "deny_button", "Nein, abbrechen")
local playername = state.location.player
local pending_data = tpad.pending_deletion[playername]
if not pending_data then
state:close()
return
end
local pad_to_delete = pending_data.pad_data
local original_form_context = pending_data.original_form_context
local pad_display_name = pad_to_delete.name .. " (" .. short_padtype_string[pad_to_delete.type] .. ")"
state:get("padname_label"):setText(YELLOW_ESCAPE .. pad_display_name)
-- Diese Funktion erzwingt einen sauberen Neuaufbau der Verwaltungs-Ansicht
local function return_to_management_with_fresh_state()
tpad.pending_deletion[playername] = nil -- Temporäre Daten löschen
minetest.after(0, function()
-- Erstelle einen sauberen Kontext, anstatt den alten wiederzuverwenden
local fresh_context = {
playername = original_form_context.playername,
clicker = original_form_context.clicker,
ownername = original_form_context.ownername,
clicked_pos = original_form_context.clicked_pos,
node = minetest.get_node(original_form_context.clicked_pos), -- Node neu holen, falls sich was geändert hat
network_type = original_form_context.network_type,
page = 1
}
submit.management(fresh_context)
end)
end
confirm_button:onClick(function()
tpad.del_pad(pad_to_delete.owner, pad_to_delete.pos)
minetest.remove_node(pad_to_delete.pos)
notify(playername, "TPAD '" .. pad_to_delete.name .. "' gelöscht.")
return_to_management_with_fresh_state()
end)
deny_button:onClick(return_to_management_with_fresh_state)
end)
tpad.forms.admin = smartfs.create("tpad.forms.admin", function(state)
state:size(8, 8)
state:label(0.2, 0.2, "admin_label", "TPAD Einstellungen")
state:field(0.5, 2, 6, 0, "max_total_field", "Max. Gesamtzahl an TPADs (pro Spieler)")
state:field(0.5, 3.5, 6, 0, "max_global_field", "Max. globale TPADs (pro Spieler)")
state:button(6.5, 0.7, 1.5, 0, "save_button", "Speichern")
local close_button = state:button(6.5, 7, 1.5, 0, "close_button", "Schließen")
close_button:setClose(true)
end)
-- ========================================================================
-- Data Helper Functions
-- ========================================================================
function tpad.decorate_pad_data(pos, pad, ownername, viewername)
pad = table.copy(pad)
if type(pos) == "string" then
pad.strpos = pos
pad.pos = minetest.string_to_pos(pos)
else
pad.pos = pos
pad.strpos = minetest.pos_to_string(pos)
end
pad.owner = ownername
pad.name = pad.name or ""
pad.type = pad.type or PUBLIC_PAD
-- NEUE LOGIK: Erstellt den Anzeigenamen für das lokale Netzwerk
local is_own_pad = (viewername and ownername == viewername)
if is_own_pad then
if pad.type == PRIVATE_PAD then
pad.local_fullname = pad.name .. " (meins, privat)"
elseif pad.type == PUBLIC_PAD then
pad.local_fullname = pad.name .. " (meins)"
else
-- Fallback, falls es weitere Typen gäbe
pad.local_fullname = pad.name
end
else
-- Bisherige Logik für fremde Pads
if pad.type == PRIVATE_PAD then
pad.local_fullname = pad.name .. " (privat)"
else
pad.local_fullname = pad.name
end
end
pad.global_fullname = pad.name
return pad
end
function tpad.get_pad_data(pos)
local meta = minetest.get_meta(pos)
local ownername = meta:get_string("owner")
if not ownername or ownername == "" then return end
local pads = tpad._get_stored_pads(ownername)
local strpos = minetest.pos_to_string(pos)
local pad = pads[strpos]
if not pad then return end
return tpad.decorate_pad_data(pos, pad, ownername)
end
function tpad.set_pad_data(pos, padname, padtype_str)
local meta = minetest.get_meta(pos)
local ownername = meta:get_string("owner")
local pads = tpad._get_stored_pads(ownername)
local strpos = minetest.pos_to_string(pos)
local pad = pads[strpos] or {}
pad.name = padname
pad.type = padtype_string_to_flag[padtype_str]
pads[strpos] = pad
tpad._set_stored_pads(ownername, pads)
end
function tpad.del_pad(ownername, pos)
local pads = tpad._get_stored_pads(ownername)
pads[minetest.pos_to_string(pos)] = nil
tpad._set_stored_pads(ownername, pads)
end
-- ========================================================================
-- Register Node and Bind Callbacks
-- ========================================================================
local collision_box = {
type = "fixed",
fixed = { -0.5, -0.5, -0.5, 0.5, -0.3, 0.5 },
}
minetest.register_node(tpad.nodename, {
drawtype = "mesh",
tiles = { tpad.texture },
mesh = tpad.mesh,
paramtype = "light",
paramtype2 = "facedir",
on_place = minetest.rotate_and_place,
after_place_node = tpad.after_place_node,
collision_box = collision_box,
selection_box = collision_box,
description = "Teleporter Pad",
groups = {choppy = 2, dig_immediate = 2},
on_rightclick = tpad.on_rightclick,
can_dig = tpad.can_dig,
on_destruct = tpad.on_destruct,
})
minetest.register_chatcommand("tpad", {func = tpad.command})

869
init.lua.bak.1 Normal file
View file

@ -0,0 +1,869 @@
-- ========================================================================
-- TPAD MOD v1.2 (Final, Reworked Delete Logic)
-- ========================================================================
tpad = {}
tpad.version = "1.2" -- As requested, version is not incremented
tpad.mod_name = minetest.get_current_modname()
tpad.texture = "tpad-texture.png"
tpad.mesh = "tpad-mesh.obj"
tpad.nodename = "tpad:tpad"
tpad.mod_path = minetest.get_modpath(tpad.mod_name)
tpad.sound_teleport = tpad.mod_name .. "_teleport"
tpad.particle_texture = tpad.mod_name .. "_particle.png"
-- Temporäre Variable zur sicheren Datenübergabe an den Bestätigungsdialog
tpad.pending_deletion = {}
-- ========================================================================
-- Constants
-- ========================================================================
local PRIVATE_PAD_STRING = "Privat (nur Besitzer)"
local PUBLIC_PAD_STRING = "Lokal (nur eigenes Netzwerk)"
local GLOBAL_PAD_STRING = "Global (beliebiges Netzwerk)"
local PRIVATE_PAD = 1
local PUBLIC_PAD = 2
local GLOBAL_PAD = 4
local YELLOW_ESCAPE = minetest.get_color_escape_sequence("#FFFF00")
local CYAN_ESCAPE = minetest.get_color_escape_sequence("#00FFFF")
local WHITE_ESCAPE = minetest.get_color_escape_sequence("#FFFFFF")
local OWNER_ESCAPE_COLOR = CYAN_ESCAPE
local padtype_flag_to_string = {
[PRIVATE_PAD] = PRIVATE_PAD_STRING,
[PUBLIC_PAD] = PUBLIC_PAD_STRING,
[GLOBAL_PAD] = GLOBAL_PAD_STRING,
}
local padtype_string_to_flag = {
[PRIVATE_PAD_STRING] = PRIVATE_PAD,
[PUBLIC_PAD_STRING] = PUBLIC_PAD,
[GLOBAL_PAD_STRING] = GLOBAL_PAD,
}
local short_padtype_string = {
[PRIVATE_PAD] = "private",
[PUBLIC_PAD] = "public",
[GLOBAL_PAD] = "global",
}
-- ========================================================================
-- Dependencies and Libs
-- ========================================================================
local smartfs = dofile(tpad.mod_path .. "/lib/smartfs.lua")
local notify = dofile(tpad.mod_path .. "/notify.lua")
local waypoint_hud_ids = {}
minetest.register_privilege("tpad_admin", {
description = "Can edit and destroy any tpad",
give_to_singleplayer = true,
})
-- ========================================================================
-- Original Helper Functions
-- ========================================================================
local function copy_file(source, dest)
local src_file = io.open(source, "rb")
if not src_file then return false, "copy_file() unable to open source for reading" end
local src_data = src_file:read("*all")
src_file:close()
local dest_file = io.open(dest, "wb")
if not dest_file then return false, "copy_file() unable to open dest for writing" end
dest_file:write(src_data)
dest_file:close()
return true, "files copied successfully"
end
local function custom_or_default(modname, path, filename)
local default_filename = "default/" .. filename
local full_filename = path .. "/custom." .. filename
local full_default_filename = path .. "/" .. default_filename
local file_exists_at_path = io.open(path .. "/" .. filename, "r")
if file_exists_at_path then
file_exists_at_path:close()
os.rename(path .. "/" .. filename, full_filename)
end
local file = io.open(full_filename, "rb")
if not file then
minetest.debug("[" .. modname .. "] Copying " .. default_filename .. " to " .. filename .. " (path: " .. path .. ")")
local success, err = copy_file(full_default_filename, full_filename)
if not success then
minetest.debug("[" .. modname .. "] " .. err)
return false
end
file = io.open(full_filename, "rb")
if not file then
minetest.debug("[" .. modname .. "] Unable to load " .. filename .. " file from path " .. path)
return false
end
end
file:close()
return full_filename
end
dofile(tpad.mod_path .. "/storage.lua")
-- ========================================================================
-- Load Custom Recipe
-- ========================================================================
local recipes_filename = custom_or_default(tpad.mod_name, tpad.mod_path, "recipes.lua")
if recipes_filename then
local recipes = dofile(recipes_filename)
if type(recipes) == "table" and recipes[tpad.nodename] then
minetest.register_craft({
output = tpad.nodename,
recipe = recipes[tpad.nodename],
})
end
end
-- ========================================================================
-- Chat Command
-- ========================================================================
function tpad.command(playername, param)
tpad.hud_off(playername)
if(param == "off") then return end
local player = minetest.get_player_by_name(playername)
local pads = tpad._get_stored_pads(playername)
local shortest_distance = nil
local closest_pad = nil
local playerpos = player:getpos()
for strpos, pad in pairs(pads) do
local pos = minetest.string_to_pos(strpos)
local distance = vector.distance(pos, playerpos)
if not shortest_distance or distance < shortest_distance then
closest_pad = {
pos = pos,
name = pad.name .. " " .. strpos,
}
shortest_distance = distance
end
end
if closest_pad then
waypoint_hud_ids[playername] = player:hud_add({
hud_elem_type = "waypoint",
name = closest_pad.name,
world_pos = closest_pad.pos,
number = 0xFF0000,
})
notify(playername, "Waypoint to " .. closest_pad.name .. " displayed")
end
end
function tpad.hud_off(playername)
local player = minetest.get_player_by_name(playername)
local hud_id = waypoint_hud_ids[playername]
if hud_id then
player:hud_remove(hud_id)
end
end
-- ========================================================================
-- Teleport Logic
-- ========================================================================
function tpad.do_teleport(player, destination_pos, destination_name, form_context)
local playername = player:get_player_name()
minetest.after(0.1, function()
if not player or not player:is_player() then return end
local start_pos = player:getpos()
minetest.sound_play(tpad.sound_teleport, {pos = start_pos, max_hear_distance = 10, gain = 1.0})
minetest.add_particlespawner({
amount = 60, time = 0.5,
minpos = vector.add(start_pos, -0.5), maxpos = vector.add(start_pos, 0.5),
minvel = {x=-1, y=0, z=-1}, maxvel = {x=1, y=2, z=1},
minacc = {x=0, y=0, z=0}, maxacc = {x=0, y=0, z=0},
minexptime = 0.5, maxexptime = 2,
minsize = 1, maxsize = 3,
texture = tpad.particle_texture,
})
player:move_to(destination_pos)
minetest.sound_play(tpad.sound_teleport, {pos = destination_pos, max_hear_distance = 10, gain = 1.0})
minetest.add_particlespawner({
amount = 60, time = 0.5,
minpos = vector.add(destination_pos, {x = -0.5, y = 0, z = -0.5}),
maxpos = vector.add(destination_pos, {x = 0.5, y = 1, z = 0.5}),
minvel = {x=-1, y=1, z=-1}, maxvel = {x=1, y=2, z=1},
minacc = {x=0, y=0, z=0}, maxacc = {x=0, y=0, z=0},
minexptime = 0.5, maxexptime = 2,
minsize = 1, maxsize = 3,
texture = tpad.particle_texture,
})
tpad.hud_off(playername)
-- NEU: Nach Ankunft am Ziel die Ansicht mit neuem Kontext aktualisieren
minetest.after(0.2, function()
if not player or not player:is_player() then return end
local dest_pad_data = tpad.get_pad_data(destination_pos)
if not dest_pad_data then return end
local new_context = {
playername = playername,
clicker = player,
ownername = dest_pad_data.owner,
clicked_pos = destination_pos,
node = minetest.get_node(destination_pos),
page = 1,
-- Behalte den Netzwerk-Typ (lokal/global) bei
network_type = form_context.network_type
}
tpad.show_network_view(new_context, new_context.network_type)
end)
end)
end
-- ========================================================================
-- Node Placement and Data Helpers
-- ========================================================================
function tpad.after_place_node(pos, placer, itemstack)
local playername = placer:get_player_name()
if tpad.max_total_pads_reached(placer) then
notify.warn(playername, "Du kannst keine weiteren TPADs erstellen. Limit erreicht.")
minetest.remove_node(pos)
minetest.add_item(placer:get_pos(), itemstack:get_name())
return
end
local meta = minetest.get_meta(pos)
meta:set_string("owner", playername)
meta:set_string("infotext", "TPAD Station von " .. playername)
tpad.set_pad_data(pos, "", PRIVATE_PAD_STRING)
end
local submit = {}
function tpad.max_total_pads_reached(placer)
local placername = placer:get_player_name()
if minetest.get_player_privs(placername).tpad_admin then
return false
end
local pads = tpad._get_stored_pads(placername)
local count = 0
for _ in pairs(pads) do
count = count + 1
end
return count >= tpad.get_max_total_pads()
end
function tpad.max_global_pads_reached(playername)
if minetest.get_player_privs(playername).tpad_admin then
return false
end
local pads = tpad._get_stored_pads(playername)
local count = 0
for _, pad in pairs(pads) do
if pad.type == GLOBAL_PAD then
count = count + 1
end
end
return count >= tpad.get_max_global_pads()
end
-- ========================================================================
-- GUI DATA HELPERS
-- ========================================================================
function submit.global_helper()
local allpads = tpad._get_all_pads()
local result = {}
for ownername, pads in pairs(allpads) do
for strpos, pad in pairs(pads) do
if pad.type == GLOBAL_PAD then
table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername))
end
end
end
table.sort(result, function(a, b) return a.global_fullname:lower() < b.global_fullname:lower() end)
return result
end
function submit.local_helper(ownername)
local pads = tpad._get_stored_pads(ownername)
local result = {}
for strpos, pad in pairs(pads) do
if pad.type ~= GLOBAL_PAD then
table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername))
end
end
table.sort(result, function(a, b) return a.local_fullname:lower() < b.local_fullname:lower() end)
return result
end
function submit.management_helper(playername)
local is_admin = minetest.get_player_privs(playername).tpad_admin
local result = {}
if is_admin then
local allpads = tpad._get_all_pads()
for ownername, pads in pairs(allpads) do
for strpos, pad in pairs(pads) do
table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername))
end
end
else
local pads = tpad._get_stored_pads(playername)
for strpos, pad in pairs(pads) do
table.insert(result, tpad.decorate_pad_data(strpos, pad, playername))
end
end
table.sort(result, function(a, b) return a.local_fullname:lower() < b.local_fullname:lower() end)
return result
end
-- ========================================================================
-- GUI LOGIC (REFACTORED)
-- ========================================================================
function tpad.show_network_view(form, network_type)
local is_admin = minetest.get_player_privs(form.playername).tpad_admin
local form_key = is_admin and "network_view_admin" or "network_view"
form.state = tpad.forms[form_key]:show(form.playername)
form.formname = "tpad.forms." .. form_key
local pad_list
local title_text
local function create_clean_context()
return {
playername = form.playername,
clicker = form.clicker,
ownername = form.ownername,
clicked_pos = form.clicked_pos,
node = form.node,
page = form.page or 1
}
end
local current_pad_data = tpad.get_pad_data(form.clicked_pos)
local current_pad_name = current_pad_data.name or "Unnamed"
if network_type == "global" then
form.network_type = "global"
pad_list = submit.global_helper()
title_text = "Du bist hier: " .. YELLOW_ESCAPE .. current_pad_name .. WHITE_ESCAPE .. ". Wähle ein globales Ziel:"
form.state:get("toggle_network_button"):setText("Lokales Netzwerk")
form.state:get("toggle_network_button"):onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
clean_form.ownername = clean_form.playername
clean_form.page = 1
tpad.show_network_view(clean_form, "local")
end)
end)
else -- "local"
form.network_type = "local"
pad_list = submit.local_helper(form.ownername)
title_text = "Du bist hier: " .. YELLOW_ESCAPE .. current_pad_name .. WHITE_ESCAPE .. ". Wähle ein lokales Ziel:"
form.state:get("toggle_network_button"):setText("Globales Netzwerk")
form.state:get("toggle_network_button"):onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
clean_form.page = 1
tpad.show_network_view(clean_form, "global")
end)
end)
end
form.state:get("title_label"):setText(title_text)
form.state:get("management_button"):onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
clean_form.preselect_pos = clean_form.clicked_pos
submit.management(clean_form)
end)
end)
if is_admin then
form.state:get("admin_button"):onClick(function()
minetest.after(0, function() submit.admin_settings(form) end)
end)
end
local max_rows, num_columns = 10, 3
local buttons_per_page = max_rows * num_columns
local destination_pads = {}
for _, pad in ipairs(pad_list) do
if not vector.equals(pad.pos, form.clicked_pos) then
table.insert(destination_pads, pad)
end
end
local total_pages = math.ceil(#destination_pads / buttons_per_page)
if total_pages == 0 then total_pages = 1 end
local current_page = form.page or 1
if current_page > total_pages then current_page = total_pages end
if current_page < 1 then current_page = 1 end
form.page = current_page
for i = 1, buttons_per_page do
local index = ((current_page - 1) * buttons_per_page) + i
local pad_data = destination_pads[index]
local button_name = "tpad_btn_" .. i
local button = form.state:get(button_name)
if button then
if pad_data then
local display_name = (network_type == "global") and pad_data.global_fullname or pad_data.local_fullname
button:setText(display_name)
button:setVisible(true)
button:onClick(function()
tpad.do_teleport(form.clicker, pad_data.pos, display_name, form)
end)
else
button:setVisible(false)
end
end
end
if total_pages > 1 then
form.state:get("page_label"):setText("Seite " .. current_page .. " von " .. total_pages)
local prev_button, next_button = form.state:get("prev_button"), form.state:get("next_button")
prev_button:setVisible(current_page > 1)
next_button:setVisible(current_page < total_pages)
-- KORREKTUR: Paginierung verwendet jetzt ebenfalls einen sauberen Kontext
prev_button:onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
clean_form.page = current_page - 1
tpad.show_network_view(clean_form, network_type)
end)
end)
next_button:onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
clean_form.page = current_page + 1
tpad.show_network_view(clean_form, network_type)
end)
end)
else
form.state:get("page_label"):setText("")
form.state:get("prev_button"):setVisible(false)
form.state:get("next_button"):setVisible(false)
end
end
function submit.management(form)
form.formname = "tpad_management"
form.state = tpad.forms.management:show(form.playername)
local pad_list = submit.management_helper(form.playername)
local listbox = form.state:get("pads_listbox")
local is_admin = minetest.get_player_privs(form.playername).tpad_admin
local selected_pad_data = nil
listbox:clearItems()
for _, pad in ipairs(pad_list) do
local label = pad.name .. " (" .. short_padtype_string[pad.type] .. ")"
if is_admin then
label = label .. " [" .. pad.owner .. "]"
end
listbox:addItem(label)
end
local padname_field = form.state:get("padname_field")
local padtype_dropdown = form.state:get("padtype_dropdown")
-- NEU: Funktion zur Aktualisierung der Felder, um Code-Dopplung zu vermeiden
local function update_fields_for_index(index)
if not index or index <= 0 then return end
selected_pad_data = pad_list[index]
if selected_pad_data then
padname_field:setText(selected_pad_data.name)
padtype_dropdown:setSelectedItem(padtype_flag_to_string[selected_pad_data.type])
end
end
-- NEU: Prüfen, ob ein Pad vorausgewählt werden soll
if form.preselect_pos then
for i, pad in ipairs(pad_list) do
if vector.equals(pad.pos, form.preselect_pos) then
listbox:setSelected(i)
update_fields_for_index(i) -- Felder für den vorausgewählten Eintrag aktualisieren
break
end
end
end
listbox:onClick(function()
local index = listbox:getSelected()
update_fields_for_index(index) -- Felder bei Klick aktualisieren
end)
form.state:get("save_button"):onClick(function()
if not selected_pad_data then
notify.warn(form.playername, "Bitte wähle zuerst ein TPAD aus der Liste aus.")
return
end
local new_name = padname_field:getText()
local new_type_str
local value_from_dropdown = padtype_dropdown:getSelectedItem()
if value_from_dropdown then
local index = tonumber(value_from_dropdown)
if index then
new_type_str = padtype_dropdown:getItem(index)
else
new_type_str = value_from_dropdown
end
end
if not new_type_str or new_type_str == "" then
notify.err(form.playername, "Konnte TPAD-Typ nicht lesen. Bitte erneut versuchen.")
return
end
if new_type_str == GLOBAL_PAD_STRING and tpad.max_global_pads_reached(selected_pad_data.owner) then
notify.warn(form.playername, "Besitzer kann keine TPADs (mehr) zum globalen Netzwerk hinzufügen.")
return
end
tpad.set_pad_data(selected_pad_data.pos, new_name, new_type_str)
notify(form.playername, "TPAD '" .. new_name .. "' gespeichert.")
minetest.after(0, function()
-- WICHTIG: preselect_pos nach dem Speichern entfernen, damit es nicht "hängen bleibt"
form.preselect_pos = nil
submit.management(form)
end)
end)
form.state:get("delete_button"):onClick(function()
if not selected_pad_data then
notify.warn(form.playername, "Bitte wähle zuerst ein TPAD aus der Liste aus.")
return
end
if vector.equals(selected_pad_data.pos, form.clicked_pos) then
notify.warn(form.playername, "Du kannst das TPAD, an dem du stehst, nicht über dieses Menü löschen.")
return
end
tpad.pending_deletion[form.playername] = {
pad_data = selected_pad_data,
original_form_context = form,
}
minetest.close_formspec(form.playername, form.formname)
minetest.after(0.1, function()
tpad.forms.confirm_pad_deletion:show(form.playername)
end)
end)
form.state:get("back_button"):onClick(function()
minetest.after(0, function()
local clean_form_context = {
playername = form.playername,
clicker = form.clicker,
ownername = form.ownername,
clicked_pos = form.clicked_pos,
node = form.node,
page = 1
}
tpad.show_network_view(clean_form_context, form.network_type)
end)
end)
end
function submit.admin_settings(form)
form.state = tpad.forms.admin:show(form.playername)
form.formname = "tpad_admin_settings"
local max_total_field, max_global_field = form.state:get("max_total_field"), form.state:get("max_global_field")
max_total_field:setText(tpad.get_max_total_pads())
max_global_field:setText(tpad.get_max_global_pads())
form.state:get("save_button"):onClick(function()
tpad.set_max_total_pads(tonumber(max_total_field:getText()))
tpad.set_max_global_pads(tonumber(max_global_field:getText()))
minetest.close_formspec(form.playername, form.formname)
end)
end
-- ========================================================================
-- Main Node Callbacks
-- ========================================================================
function tpad.on_rightclick(clicked_pos, node, clicker)
local playername = clicker:get_player_name()
local pad = tpad.get_pad_data(clicked_pos)
if not pad or not pad.owner then
notify.err(playername, "Fehler! Fehlende oder korrupte TPAD-Daten. Bitte neu platzieren.")
return
end
-- Zugriffsschutz-Prüfung zuerst
if pad.type == PRIVATE_PAD and pad.owner ~= playername and not minetest.get_player_privs(playername).tpad_admin then
notify.warn(playername, "Dieses TPAD ist privat.")
return
end
-- Erstelle das Kontext-Objekt für alle Fälle
local form = {
playername = playername,
clicker = clicker,
ownername = pad.owner,
clicked_pos = clicked_pos,
node = node,
page = 1,
}
-- NEU: Prüfe, ob das Pad neu ist (d.h. keinen Namen hat)
if pad.name == "" then
-- Setze einen Parameter zur Vorauswahl für das Verwaltungs-Menü
form.preselect_pos = clicked_pos
-- Rufe direkt die Verwaltung auf
submit.management(form)
else
-- BESTEHENDE LOGIK: Wenn das Pad bereits konfiguriert ist
if pad.type == GLOBAL_PAD then
tpad.show_network_view(form, "global")
else
tpad.show_network_view(form, "local")
end
end
end
function tpad.can_dig(pos, player)
local meta = minetest.get_meta(pos)
local ownername = meta:get_string("owner")
local playername = player:get_player_name()
if ownername == "" or ownername == playername or minetest.get_player_privs(playername).tpad_admin then
return true
end
notify.warn(playername, "Dieses TPAD gehört dir nicht.")
return false
end
function tpad.on_destruct(pos)
local meta = minetest.get_meta(pos)
local ownername = meta:get_string("owner")
if ownername and ownername ~= "" then
tpad.del_pad(ownername, pos)
end
end
-- ========================================================================
-- FORMS (Rebuilt)
-- ========================================================================
tpad.forms = {}
local function create_network_view_form(state)
state:size(16, 11)
state:label(0.5, 0.2, "title_label", "")
local bottom_y = 10.4
state:button(0.5, bottom_y, 3.0, 0, "toggle_network_button", "")
state:button(3.6, bottom_y, 2.3, 0, "management_button", "Verwaltung")
local close_button = state:button(14.0, bottom_y, 1.5, 0, "close_button", "Schließen")
close_button:setClose(true)
state:button(6.0, bottom_y, 1.0, 0, "prev_button", "[<<]")
state:label(7.1, bottom_y, "page_label", "")
state:button(9.5, bottom_y, 1.0, 0, "next_button", "[>>]")
local max_rows, num_columns = 10, 3
local start_x, start_y = 0.5, 1.0
local button_width, button_height = 4.8, 0.8
local column_width = 5.0
for i = 1, max_rows * num_columns do
local column = math.floor((i - 1) / max_rows)
local row = (i - 1) % max_rows
local current_x = start_x + (column * column_width)
local current_y = start_y + (row * button_height)
state:button(current_x, current_y, button_width, 0, "tpad_btn_" .. i, ""):setVisible(false)
end
end
tpad.forms.teleport_success = smartfs.create("tpad.forms.teleport_success", function(state)
state:size(8, 2)
local destination_name = state.param.destination_name or "???"
state:label(0.5, 0.5, "success_label", "Teleport erfolgreich: " .. YELLOW_ESCAPE .. destination_name)
-- Dieser Button hat setClose(true), was das Fenster zuverlässig schließt.
local close_button = state:button(3, 1.2, 2, 0, "close_button", "Schließen")
close_button:setClose(true)
end)
tpad.forms.network_view = smartfs.create("tpad.forms.network_view", create_network_view_form)
tpad.forms.network_view_admin = smartfs.create("tpad.forms.network_view_admin", function(state)
create_network_view_form(state)
state:button(12.4, 10.4, 1.5, 0, "admin_button", "Admin")
end)
tpad.forms.management = smartfs.create("tpad.forms.management", function(state)
state:size(12, 9)
state:label(0.2, 0.2, "management_title", "TPAD Verwaltung")
state:listbox(0.2, 0.6, 11.6, 5, "pads_listbox", {})
state:field(0.5, 6.6, 6, 0, "padname_field", "Name", "")
local padtype_dropdown = state:dropdown(0.5, 6.8, 6, 0, "padtype_dropdown")
padtype_dropdown:addItem(PRIVATE_PAD_STRING)
padtype_dropdown:addItem(PUBLIC_PAD_STRING)
padtype_dropdown:addItem(GLOBAL_PAD_STRING)
state:button(7, 6.2, 2, 0, "save_button", "Speichern")
state:button(7, 7.0, 2, 0, "delete_button", "Löschen")
state:button(0.2, 8.4, 2, 0, "back_button", "Zurück")
local close_button = state:button(9.8, 8.4, 2, 0, "close_button", "Schließen")
close_button:setClose(true)
end)
-- This form now defines its OWN behavior, making it independent and robust.
tpad.forms.confirm_pad_deletion = smartfs.create("tpad.forms.confirm_pad_deletion", function(state)
state:size(8, 2.5)
state:label(0, 0, "intro_label", "Willst du das TPAD wirklich löschen?")
state:label(0, 0.5, "padname_label", "")
state:label(0, 1, "outro_label", "(es lässt sich nicht wiederherstellen)")
local confirm_button = state:button(0, 2.2, 2, 0, "confirm_button", "Ja, löschen")
local deny_button = state:button(6, 2.2, 2, 0, "deny_button", "Nein, abbrechen")
local playername = state.location.player
local pending_data = tpad.pending_deletion[playername]
if not pending_data then
state:close()
return
end
local pad_to_delete = pending_data.pad_data
local original_form_context = pending_data.original_form_context
local pad_display_name = pad_to_delete.name .. " (" .. short_padtype_string[pad_to_delete.type] .. ")"
state:get("padname_label"):setText(YELLOW_ESCAPE .. pad_display_name)
-- Diese Funktion erzwingt einen sauberen Neuaufbau der Verwaltungs-Ansicht
local function return_to_management_with_fresh_state()
tpad.pending_deletion[playername] = nil -- Temporäre Daten löschen
minetest.after(0, function()
-- Erstelle einen sauberen Kontext, anstatt den alten wiederzuverwenden
local fresh_context = {
playername = original_form_context.playername,
clicker = original_form_context.clicker,
ownername = original_form_context.ownername,
clicked_pos = original_form_context.clicked_pos,
node = minetest.get_node(original_form_context.clicked_pos), -- Node neu holen, falls sich was geändert hat
network_type = original_form_context.network_type,
page = 1
}
submit.management(fresh_context)
end)
end
confirm_button:onClick(function()
tpad.del_pad(pad_to_delete.owner, pad_to_delete.pos)
minetest.remove_node(pad_to_delete.pos)
notify(playername, "TPAD '" .. pad_to_delete.name .. "' gelöscht.")
return_to_management_with_fresh_state()
end)
deny_button:onClick(return_to_management_with_fresh_state)
end)
tpad.forms.admin = smartfs.create("tpad.forms.admin", function(state)
state:size(8, 8)
state:label(0.2, 0.2, "admin_label", "TPAD Einstellungen")
state:field(0.5, 2, 6, 0, "max_total_field", "Max. Gesamtzahl an TPADs (pro Spieler)")
state:field(0.5, 3.5, 6, 0, "max_global_field", "Max. globale TPADs (pro Spieler)")
state:button(6.5, 0.7, 1.5, 0, "save_button", "Speichern")
local close_button = state:button(6.5, 7, 1.5, 0, "close_button", "Schließen")
close_button:setClose(true)
end)
-- ========================================================================
-- Data Helper Functions
-- ========================================================================
function tpad.decorate_pad_data(pos, pad, ownername)
pad = table.copy(pad)
if type(pos) == "string" then
pad.strpos = pos
pad.pos = minetest.string_to_pos(pos)
else
pad.pos = pos
pad.strpos = minetest.pos_to_string(pos)
end
pad.owner = ownername
pad.name = pad.name or ""
pad.type = pad.type or PUBLIC_PAD
-- NEUE LOGIK: Füge den Suffix nur bei privaten Pads hinzu.
if pad.type == PRIVATE_PAD then
pad.local_fullname = pad.name .. " (privat)"
else
-- Bei Public Pads wird kein Suffix mehr angehängt.
pad.local_fullname = pad.name
end
pad.global_fullname = pad.name
return pad
end
function tpad.get_pad_data(pos)
local meta = minetest.get_meta(pos)
local ownername = meta:get_string("owner")
if not ownername or ownername == "" then return end
local pads = tpad._get_stored_pads(ownername)
local strpos = minetest.pos_to_string(pos)
local pad = pads[strpos]
if not pad then return end
return tpad.decorate_pad_data(pos, pad, ownername)
end
function tpad.set_pad_data(pos, padname, padtype_str)
local meta = minetest.get_meta(pos)
local ownername = meta:get_string("owner")
local pads = tpad._get_stored_pads(ownername)
local strpos = minetest.pos_to_string(pos)
local pad = pads[strpos] or {}
pad.name = padname
pad.type = padtype_string_to_flag[padtype_str]
pads[strpos] = pad
tpad._set_stored_pads(ownername, pads)
end
function tpad.del_pad(ownername, pos)
local pads = tpad._get_stored_pads(ownername)
pads[minetest.pos_to_string(pos)] = nil
tpad._set_stored_pads(ownername, pads)
end
-- ========================================================================
-- Register Node and Bind Callbacks
-- ========================================================================
local collision_box = {
type = "fixed",
fixed = { -0.5, -0.5, -0.5, 0.5, -0.3, 0.5 },
}
minetest.register_node(tpad.nodename, {
drawtype = "mesh",
tiles = { tpad.texture },
mesh = tpad.mesh,
paramtype = "light",
paramtype2 = "facedir",
on_place = minetest.rotate_and_place,
after_place_node = tpad.after_place_node,
collision_box = collision_box,
selection_box = collision_box,
description = "Teleporter Pad",
groups = {choppy = 2, dig_immediate = 2},
on_rightclick = tpad.on_rightclick,
can_dig = tpad.can_dig,
on_destruct = tpad.on_destruct,
})
minetest.register_chatcommand("tpad", {func = tpad.command})

901
init.lua.bak.2 Normal file
View file

@ -0,0 +1,901 @@
-- ========================================================================
-- TPAD MOD v1.2 (Final, Reworked Delete Logic)
-- ========================================================================
tpad = {}
tpad.version = "1.2" -- As requested, version is not incremented
tpad.mod_name = minetest.get_current_modname()
tpad.texture = "tpad-texture.png"
tpad.mesh = "tpad-mesh.obj"
tpad.nodename = "tpad:tpad"
tpad.mod_path = minetest.get_modpath(tpad.mod_name)
tpad.sound_teleport = tpad.mod_name .. "_teleport"
tpad.particle_texture = tpad.mod_name .. "_particle.png"
-- Temporäre Variable zur sicheren Datenübergabe an den Bestätigungsdialog
tpad.pending_deletion = {}
-- ========================================================================
-- Constants
-- ========================================================================
local PRIVATE_PAD_STRING = "Privat (nur Besitzer)"
local PUBLIC_PAD_STRING = "Lokal (nur eigenes Netzwerk)"
local GLOBAL_PAD_STRING = "Global (beliebiges Netzwerk)"
local PRIVATE_PAD = 1
local PUBLIC_PAD = 2
local GLOBAL_PAD = 4
local RED_ESCAPE = minetest.get_color_escape_sequence("#FF0000")
local YELLOW_ESCAPE = minetest.get_color_escape_sequence("#FFFF00")
local CYAN_ESCAPE = minetest.get_color_escape_sequence("#00FFFF")
local WHITE_ESCAPE = minetest.get_color_escape_sequence("#FFFFFF")
local OWNER_ESCAPE_COLOR = CYAN_ESCAPE
local padtype_flag_to_string = {
[PRIVATE_PAD] = PRIVATE_PAD_STRING,
[PUBLIC_PAD] = PUBLIC_PAD_STRING,
[GLOBAL_PAD] = GLOBAL_PAD_STRING,
}
local padtype_string_to_flag = {
[PRIVATE_PAD_STRING] = PRIVATE_PAD,
[PUBLIC_PAD_STRING] = PUBLIC_PAD,
[GLOBAL_PAD_STRING] = GLOBAL_PAD,
}
local short_padtype_string = {
[PRIVATE_PAD] = "private",
[PUBLIC_PAD] = "public",
[GLOBAL_PAD] = "global",
}
-- ========================================================================
-- Dependencies and Libs
-- ========================================================================
local smartfs = dofile(tpad.mod_path .. "/lib/smartfs.lua")
local notify = dofile(tpad.mod_path .. "/notify.lua")
local waypoint_hud_ids = {}
minetest.register_privilege("tpad_admin", {
description = "Can edit and destroy any tpad",
give_to_singleplayer = true,
})
-- ========================================================================
-- Original Helper Functions
-- ========================================================================
local function copy_file(source, dest)
local src_file = io.open(source, "rb")
if not src_file then return false, "copy_file() unable to open source for reading" end
local src_data = src_file:read("*all")
src_file:close()
local dest_file = io.open(dest, "wb")
if not dest_file then return false, "copy_file() unable to open dest for writing" end
dest_file:write(src_data)
dest_file:close()
return true, "files copied successfully"
end
local function custom_or_default(modname, path, filename)
local default_filename = "default/" .. filename
local full_filename = path .. "/custom." .. filename
local full_default_filename = path .. "/" .. default_filename
local file_exists_at_path = io.open(path .. "/" .. filename, "r")
if file_exists_at_path then
file_exists_at_path:close()
os.rename(path .. "/" .. filename, full_filename)
end
local file = io.open(full_filename, "rb")
if not file then
minetest.debug("[" .. modname .. "] Copying " .. default_filename .. " to " .. filename .. " (path: " .. path .. ")")
local success, err = copy_file(full_default_filename, full_filename)
if not success then
minetest.debug("[" .. modname .. "] " .. err)
return false
end
file = io.open(full_filename, "rb")
if not file then
minetest.debug("[" .. modname .. "] Unable to load " .. filename .. " file from path " .. path)
return false
end
end
file:close()
return full_filename
end
dofile(tpad.mod_path .. "/storage.lua")
-- ========================================================================
-- Load Custom Recipe
-- ========================================================================
local recipes_filename = custom_or_default(tpad.mod_name, tpad.mod_path, "recipes.lua")
if recipes_filename then
local recipes = dofile(recipes_filename)
if type(recipes) == "table" and recipes[tpad.nodename] then
minetest.register_craft({
output = tpad.nodename,
recipe = recipes[tpad.nodename],
})
end
end
-- ========================================================================
-- Chat Command
-- ========================================================================
function tpad.command(playername, param)
tpad.hud_off(playername)
if(param == "off") then return end
local player = minetest.get_player_by_name(playername)
local pads = tpad._get_stored_pads(playername)
local shortest_distance = nil
local closest_pad = nil
local playerpos = player:getpos()
for strpos, pad in pairs(pads) do
local pos = minetest.string_to_pos(strpos)
local distance = vector.distance(pos, playerpos)
if not shortest_distance or distance < shortest_distance then
closest_pad = {
pos = pos,
name = pad.name .. " " .. strpos,
}
shortest_distance = distance
end
end
if closest_pad then
waypoint_hud_ids[playername] = player:hud_add({
hud_elem_type = "waypoint",
name = closest_pad.name,
world_pos = closest_pad.pos,
number = 0xFF0000,
})
notify(playername, "Waypoint to " .. closest_pad.name .. " displayed")
end
end
function tpad.hud_off(playername)
local player = minetest.get_player_by_name(playername)
local hud_id = waypoint_hud_ids[playername]
if hud_id then
player:hud_remove(hud_id)
end
end
-- ========================================================================
-- Teleport Logic
-- ========================================================================
function tpad.do_teleport(player, destination_pos, destination_name, form_context)
local playername = player:get_player_name()
minetest.after(0.1, function()
if not player or not player:is_player() then return end
local start_pos = player:getpos()
minetest.sound_play(tpad.sound_teleport, {pos = start_pos, max_hear_distance = 10, gain = 1.0})
minetest.add_particlespawner({
amount = 60, time = 0.5,
minpos = vector.add(start_pos, -0.5), maxpos = vector.add(start_pos, 0.5),
minvel = {x=-1, y=0, z=-1}, maxvel = {x=1, y=2, z=1},
minacc = {x=0, y=0, z=0}, maxacc = {x=0, y=0, z=0},
minexptime = 0.5, maxexptime = 2,
minsize = 1, maxsize = 3,
texture = tpad.particle_texture,
})
player:move_to(destination_pos)
minetest.sound_play(tpad.sound_teleport, {pos = destination_pos, max_hear_distance = 10, gain = 1.0})
minetest.add_particlespawner({
amount = 60, time = 0.5,
minpos = vector.add(destination_pos, {x = -0.5, y = 0, z = -0.5}),
maxpos = vector.add(destination_pos, {x = 0.5, y = 1, z = 0.5}),
minvel = {x=-1, y=1, z=-1}, maxvel = {x=1, y=2, z=1},
minacc = {x=0, y=0, z=0}, maxacc = {x=0, y=0, z=0},
minexptime = 0.5, maxexptime = 2,
minsize = 1, maxsize = 3,
texture = tpad.particle_texture,
})
tpad.hud_off(playername)
-- NEU: Nach Ankunft am Ziel die Ansicht mit neuem Kontext aktualisieren
minetest.after(0.2, function()
if not player or not player:is_player() then return end
local dest_pad_data = tpad.get_pad_data(destination_pos)
if not dest_pad_data then return end
local new_context = {
playername = playername,
clicker = player,
ownername = dest_pad_data.owner,
clicked_pos = destination_pos,
node = minetest.get_node(destination_pos),
page = 1,
-- Behalte den Netzwerk-Typ (lokal/global) bei
network_type = form_context.network_type
}
tpad.show_network_view(new_context, new_context.network_type)
end)
end)
end
-- ========================================================================
-- Node Placement and Data Helpers
-- ========================================================================
function tpad.after_place_node(pos, placer, itemstack)
local playername = placer:get_player_name()
if tpad.max_total_pads_reached(placer) then
notify.warn(playername, "Du kannst keine weiteren TPADs erstellen. Limit erreicht.")
minetest.remove_node(pos)
minetest.add_item(placer:get_pos(), itemstack:get_name())
return
end
local meta = minetest.get_meta(pos)
meta:set_string("owner", playername)
meta:set_string("infotext", "TPAD Station von " .. playername)
tpad.set_pad_data(pos, "", PRIVATE_PAD_STRING)
end
local submit = {}
function tpad.max_total_pads_reached(placer)
local placername = placer:get_player_name()
if minetest.get_player_privs(placername).tpad_admin then
return false
end
local pads = tpad._get_stored_pads(placername)
local count = 0
for _ in pairs(pads) do
count = count + 1
end
return count >= tpad.get_max_total_pads()
end
function tpad.max_global_pads_reached(playername)
if minetest.get_player_privs(playername).tpad_admin then
return false
end
local pads = tpad._get_stored_pads(playername)
local count = 0
for _, pad in pairs(pads) do
if pad.type == GLOBAL_PAD then
count = count + 1
end
end
return count >= tpad.get_max_global_pads()
end
-- ========================================================================
-- GUI DATA HELPERS
-- ========================================================================
function submit.global_helper()
local allpads = tpad._get_all_pads()
local result = {}
for ownername, pads in pairs(allpads) do
for strpos, pad in pairs(pads) do
if pad.type == GLOBAL_PAD then
table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername))
end
end
end
table.sort(result, function(a, b) return a.global_fullname:lower() < b.global_fullname:lower() end)
return result
end
function submit.local_helper(ownername, viewername)
local result = {}
local added_pads = {} -- Verhindert, dass Pads doppelt hinzugefügt werden
-- Schritt 1: Füge die öffentlichen Pads des Besitzers des angeklickten TPADs hinzu
local owner_pads = tpad._get_stored_pads(ownername)
for strpos, pad in pairs(owner_pads) do
-- Vom Besitzer werden nur die öffentlichen (public) Pads angezeigt
if pad.type == PUBLIC_PAD then
table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername))
added_pads[strpos] = true
end
end
-- Schritt 2: Füge die eigenen Pads hinzu, falls der Betrachter eine andere Person ist
if ownername ~= viewername then
local viewer_pads = tpad._get_stored_pads(viewername)
for strpos, pad in pairs(viewer_pads) do
-- Vom Betrachter werden seine privaten und öffentlichen Pads angezeigt
if (pad.type == PUBLIC_PAD or pad.type == PRIVATE_PAD) and not added_pads[strpos] then
table.insert(result, tpad.decorate_pad_data(strpos, pad, viewername))
added_pads[strpos] = true
end
end
end
table.sort(result, function(a, b) return a.local_fullname:lower() < b.local_fullname:lower() end)
return result
end
function submit.management_helper(playername)
local is_admin = minetest.get_player_privs(playername).tpad_admin
local result = {}
if is_admin then
local allpads = tpad._get_all_pads()
for ownername, pads in pairs(allpads) do
for strpos, pad in pairs(pads) do
table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername))
end
end
else
local pads = tpad._get_stored_pads(playername)
for strpos, pad in pairs(pads) do
table.insert(result, tpad.decorate_pad_data(strpos, pad, playername))
end
end
table.sort(result, function(a, b) return a.local_fullname:lower() < b.local_fullname:lower() end)
return result
end
-- ========================================================================
-- GUI LOGIC (REFACTORED)
-- ========================================================================
function tpad.show_network_view(form, network_type)
local is_admin = minetest.get_player_privs(form.playername).tpad_admin
local form_key = is_admin and "network_view_admin" or "network_view"
form.state = tpad.forms[form_key]:show(form.playername)
form.formname = "tpad.forms." .. form_key
local pad_list
local title_text
local function create_clean_context()
return {
playername = form.playername,
clicker = form.clicker,
ownername = form.ownername,
clicked_pos = form.clicked_pos,
node = form.node,
page = form.page or 1
}
end
local current_pad_data = tpad.get_pad_data(form.clicked_pos)
local current_pad_name = current_pad_data.name or "Unnamed"
if network_type == "global" then
form.network_type = "global"
pad_list = submit.global_helper()
title_text = RED_ESCAPE .. "GLOBALE " .. WHITE_ESCAPE .. "TPAD-Station " .. YELLOW_ESCAPE .. current_pad_name .. WHITE_ESCAPE .. ". Wähle ein Ziel:"
form.state:get("toggle_network_button"):setText("Lokales Netzwerk")
form.state:get("toggle_network_button"):onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
clean_form.ownername = clean_form.playername
clean_form.page = 1
tpad.show_network_view(clean_form, "local")
end)
end)
else -- "local"
form.network_type = "local"
pad_list = submit.local_helper(form.ownername, form.playername)
title_text = RED_ESCAPE .. "LOKALE " .. WHITE_ESCAPE .. "TPAD-Station " .. YELLOW_ESCAPE .. current_pad_name .. WHITE_ESCAPE .. ". Wähle ein Ziel:"
form.state:get("toggle_network_button"):setText("Globales Netzwerk")
form.state:get("toggle_network_button"):onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
clean_form.page = 1
tpad.show_network_view(clean_form, "global")
end)
end)
end
form.state:get("title_label"):setText(title_text)
form.state:get("management_button"):onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
clean_form.preselect_pos = clean_form.clicked_pos
submit.management(clean_form)
end)
end)
if is_admin then
form.state:get("admin_button"):onClick(function()
minetest.after(0, function() submit.admin_settings(form) end)
end)
end
local max_rows, num_columns = 10, 3
local buttons_per_page = max_rows * num_columns
local destination_pads = {}
for _, pad in ipairs(pad_list) do
if not vector.equals(pad.pos, form.clicked_pos) then
table.insert(destination_pads, pad)
end
end
local total_pages = math.ceil(#destination_pads / buttons_per_page)
if total_pages == 0 then total_pages = 1 end
local current_page = form.page or 1
if current_page > total_pages then current_page = total_pages end
if current_page < 1 then current_page = 1 end
form.page = current_page
for i = 1, buttons_per_page do
local index = ((current_page - 1) * buttons_per_page) + i
local pad_data = destination_pads[index]
local button_name = "tpad_btn_" .. i
local button = form.state:get(button_name)
if button then
if pad_data then
local display_name = (network_type == "global") and pad_data.global_fullname or pad_data.local_fullname
button:setText(display_name)
button:setVisible(true)
button:onClick(function()
tpad.do_teleport(form.clicker, pad_data.pos, display_name, form)
end)
else
button:setVisible(false)
end
end
end
if total_pages > 1 then
form.state:get("page_label"):setText("Seite " .. current_page .. " von " .. total_pages)
local prev_button, next_button = form.state:get("prev_button"), form.state:get("next_button")
prev_button:setVisible(current_page > 1)
next_button:setVisible(current_page < total_pages)
-- KORREKTUR: Paginierung verwendet jetzt ebenfalls einen sauberen Kontext
prev_button:onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
clean_form.page = current_page - 1
tpad.show_network_view(clean_form, network_type)
end)
end)
next_button:onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
clean_form.page = current_page + 1
tpad.show_network_view(clean_form, network_type)
end)
end)
else
form.state:get("page_label"):setText("")
form.state:get("prev_button"):setVisible(false)
form.state:get("next_button"):setVisible(false)
end
end
function submit.management(form)
form.formname = "tpad_management"
form.state = tpad.forms.management:show(form.playername)
local pad_list = submit.management_helper(form.playername)
local listbox = form.state:get("pads_listbox")
local is_admin = minetest.get_player_privs(form.playername).tpad_admin
local selected_pad_data = nil
listbox:clearItems()
for _, pad in ipairs(pad_list) do
local label = pad.name .. " (" .. short_padtype_string[pad.type] .. ")"
if is_admin then
label = label .. " [" .. pad.owner .. "]"
end
listbox:addItem(label)
end
local padname_field = form.state:get("padname_field")
local padtype_dropdown = form.state:get("padtype_dropdown")
-- NEU: Funktion zur Aktualisierung der Felder, um Code-Dopplung zu vermeiden
local function update_fields_for_index(index)
if not index or index <= 0 then return end
selected_pad_data = pad_list[index]
if selected_pad_data then
padname_field:setText(selected_pad_data.name)
padtype_dropdown:setSelectedItem(padtype_flag_to_string[selected_pad_data.type])
end
end
-- NEU: Prüfen, ob ein Pad vorausgewählt werden soll
if form.preselect_pos then
for i, pad in ipairs(pad_list) do
if vector.equals(pad.pos, form.preselect_pos) then
listbox:setSelected(i)
update_fields_for_index(i) -- Felder für den vorausgewählten Eintrag aktualisieren
break
end
end
end
listbox:onClick(function()
local index = listbox:getSelected()
update_fields_for_index(index) -- Felder bei Klick aktualisieren
end)
form.state:get("save_button"):onClick(function()
if not selected_pad_data then
notify.warn(form.playername, "Bitte wähle zuerst ein TPAD aus der Liste aus.")
return
end
local new_name = padname_field:getText()
local new_type_str
local value_from_dropdown = padtype_dropdown:getSelectedItem()
if value_from_dropdown then
local index = tonumber(value_from_dropdown)
if index then
new_type_str = padtype_dropdown:getItem(index)
else
new_type_str = value_from_dropdown
end
end
if not new_type_str or new_type_str == "" then
notify.err(form.playername, "Konnte TPAD-Typ nicht lesen. Bitte erneut versuchen.")
return
end
-- NEU: Prüfe zuerst, ob der ausführende Spieler Admin-Rechte hat.
local is_acting_admin = minetest.get_player_privs(form.playername).tpad_admin
-- Wende die Limits nur an, wenn der ausführende Spieler KEIN Admin ist.
if not is_acting_admin then
if new_type_str == GLOBAL_PAD_STRING and tpad.max_global_pads_reached(selected_pad_data.owner) then
notify.warn(form.playername, "Der Besitzer des TPADs hat das Limit für globale TPADs erreicht.")
return
end
end
tpad.set_pad_data(selected_pad_data.pos, new_name, new_type_str)
local meta = minetest.get_meta(selected_pad_data.pos)
if new_name and new_name ~= "" then
meta:set_string("infotext", "TPAD Station " .. new_name)
else
meta:set_string("infotext", "Unbenannte TPAD Station")
end
notify(form.playername, "TPAD '" .. new_name .. "' gespeichert.")
minetest.after(0, function()
form.preselect_pos = nil
submit.management(form)
end)
end)
form.state:get("delete_button"):onClick(function()
if not selected_pad_data then
notify.warn(form.playername, "Bitte wähle zuerst ein TPAD aus der Liste aus.")
return
end
if vector.equals(selected_pad_data.pos, form.clicked_pos) then
notify.warn(form.playername, "Du kannst das TPAD, an dem du stehst, nicht über dieses Menü löschen.")
return
end
tpad.pending_deletion[form.playername] = {
pad_data = selected_pad_data,
original_form_context = form,
}
minetest.close_formspec(form.playername, form.formname)
minetest.after(0.1, function()
tpad.forms.confirm_pad_deletion:show(form.playername)
end)
end)
form.state:get("back_button"):onClick(function()
minetest.after(0, function()
local clean_form_context = {
playername = form.playername,
clicker = form.clicker,
ownername = form.ownername,
clicked_pos = form.clicked_pos,
node = form.node,
page = 1
}
tpad.show_network_view(clean_form_context, form.network_type)
end)
end)
end
function submit.admin_settings(form)
form.state = tpad.forms.admin:show(form.playername)
form.formname = "tpad_admin_settings"
local max_total_field, max_global_field = form.state:get("max_total_field"), form.state:get("max_global_field")
max_total_field:setText(tpad.get_max_total_pads())
max_global_field:setText(tpad.get_max_global_pads())
form.state:get("save_button"):onClick(function()
tpad.set_max_total_pads(tonumber(max_total_field:getText()))
tpad.set_max_global_pads(tonumber(max_global_field:getText()))
minetest.close_formspec(form.playername, form.formname)
end)
end
-- ========================================================================
-- Main Node Callbacks
-- ========================================================================
function tpad.on_rightclick(clicked_pos, node, clicker)
local playername = clicker:get_player_name()
local pad = tpad.get_pad_data(clicked_pos)
if not pad or not pad.owner then
notify.err(playername, "Fehler! Fehlende oder korrupte TPAD-Daten. Bitte neu platzieren.")
return
end
-- Zugriffsschutz-Prüfung zuerst
if pad.type == PRIVATE_PAD and pad.owner ~= playername and not minetest.get_player_privs(playername).tpad_admin then
notify.warn(playername, "Dieses TPAD ist privat.")
return
end
-- Erstelle das Kontext-Objekt für alle Fälle
local form = {
playername = playername,
clicker = clicker,
ownername = pad.owner,
clicked_pos = clicked_pos,
node = node,
page = 1,
}
-- NEU: Prüfe, ob das Pad neu ist (d.h. keinen Namen hat)
if pad.name == "" then
-- Setze einen Parameter zur Vorauswahl für das Verwaltungs-Menü
form.preselect_pos = clicked_pos
-- Rufe direkt die Verwaltung auf
submit.management(form)
else
-- BESTEHENDE LOGIK: Wenn das Pad bereits konfiguriert ist
if pad.type == GLOBAL_PAD then
tpad.show_network_view(form, "global")
else
tpad.show_network_view(form, "local")
end
end
end
function tpad.can_dig(pos, player)
local meta = minetest.get_meta(pos)
local ownername = meta:get_string("owner")
local playername = player:get_player_name()
if ownername == "" or ownername == playername or minetest.get_player_privs(playername).tpad_admin then
return true
end
notify.warn(playername, "Dieses TPAD gehört dir nicht.")
return false
end
function tpad.on_destruct(pos)
local meta = minetest.get_meta(pos)
local ownername = meta:get_string("owner")
if ownername and ownername ~= "" then
tpad.del_pad(ownername, pos)
end
end
-- ========================================================================
-- FORMS (Rebuilt)
-- ========================================================================
tpad.forms = {}
local function create_network_view_form(state)
state:size(16, 11)
state:label(0.5, 0.2, "title_label", "")
local bottom_y = 10.4
state:button(0.5, bottom_y, 3.0, 0, "toggle_network_button", "")
state:button(3.6, bottom_y, 2.3, 0, "management_button", "Verwaltung")
local close_button = state:button(14.0, bottom_y, 1.5, 0, "close_button", "Schließen")
close_button:setClose(true)
state:button(6.0, bottom_y, 1.0, 0, "prev_button", "[<<]")
state:label(7.1, bottom_y, "page_label", "")
state:button(9.5, bottom_y, 1.0, 0, "next_button", "[>>]")
local max_rows, num_columns = 10, 3
local start_x, start_y = 0.5, 1.0
local button_width, button_height = 4.8, 0.8
local column_width = 5.0
for i = 1, max_rows * num_columns do
local column = math.floor((i - 1) / max_rows)
local row = (i - 1) % max_rows
local current_x = start_x + (column * column_width)
local current_y = start_y + (row * button_height)
state:button(current_x, current_y, button_width, 0, "tpad_btn_" .. i, ""):setVisible(false)
end
end
tpad.forms.teleport_success = smartfs.create("tpad.forms.teleport_success", function(state)
state:size(8, 2)
local destination_name = state.param.destination_name or "???"
state:label(0.5, 0.5, "success_label", "Teleport erfolgreich: " .. YELLOW_ESCAPE .. destination_name)
-- Dieser Button hat setClose(true), was das Fenster zuverlässig schließt.
local close_button = state:button(3, 1.2, 2, 0, "close_button", "Schließen")
close_button:setClose(true)
end)
tpad.forms.network_view = smartfs.create("tpad.forms.network_view", create_network_view_form)
tpad.forms.network_view_admin = smartfs.create("tpad.forms.network_view_admin", function(state)
create_network_view_form(state)
state:button(12.4, 10.4, 1.5, 0, "admin_button", "Admin")
end)
tpad.forms.management = smartfs.create("tpad.forms.management", function(state)
state:size(12, 9)
state:label(0.2, 0.2, "management_title", "TPAD Verwaltung")
state:listbox(0.2, 0.6, 11.6, 5, "pads_listbox", {})
state:field(0.5, 6.6, 6, 0, "padname_field", "Name", "")
local padtype_dropdown = state:dropdown(0.5, 6.8, 6, 0, "padtype_dropdown")
padtype_dropdown:addItem(PRIVATE_PAD_STRING)
padtype_dropdown:addItem(PUBLIC_PAD_STRING)
padtype_dropdown:addItem(GLOBAL_PAD_STRING)
state:button(7, 6.2, 2, 0, "save_button", "Speichern")
state:button(7, 7.0, 2, 0, "delete_button", "Löschen")
state:button(0.2, 8.4, 2, 0, "back_button", "Zurück")
local close_button = state:button(9.8, 8.4, 2, 0, "close_button", "Schließen")
close_button:setClose(true)
end)
-- This form now defines its OWN behavior, making it independent and robust.
tpad.forms.confirm_pad_deletion = smartfs.create("tpad.forms.confirm_pad_deletion", function(state)
state:size(8, 2.5)
state:label(0, 0, "intro_label", "Willst du das TPAD wirklich löschen?")
state:label(0, 0.5, "padname_label", "")
state:label(0, 1, "outro_label", "(es lässt sich nicht wiederherstellen)")
local confirm_button = state:button(0, 2.2, 2, 0, "confirm_button", "Ja, löschen")
local deny_button = state:button(6, 2.2, 2, 0, "deny_button", "Nein, abbrechen")
local playername = state.location.player
local pending_data = tpad.pending_deletion[playername]
if not pending_data then
state:close()
return
end
local pad_to_delete = pending_data.pad_data
local original_form_context = pending_data.original_form_context
local pad_display_name = pad_to_delete.name .. " (" .. short_padtype_string[pad_to_delete.type] .. ")"
state:get("padname_label"):setText(YELLOW_ESCAPE .. pad_display_name)
-- Diese Funktion erzwingt einen sauberen Neuaufbau der Verwaltungs-Ansicht
local function return_to_management_with_fresh_state()
tpad.pending_deletion[playername] = nil -- Temporäre Daten löschen
minetest.after(0, function()
-- Erstelle einen sauberen Kontext, anstatt den alten wiederzuverwenden
local fresh_context = {
playername = original_form_context.playername,
clicker = original_form_context.clicker,
ownername = original_form_context.ownername,
clicked_pos = original_form_context.clicked_pos,
node = minetest.get_node(original_form_context.clicked_pos), -- Node neu holen, falls sich was geändert hat
network_type = original_form_context.network_type,
page = 1
}
submit.management(fresh_context)
end)
end
confirm_button:onClick(function()
tpad.del_pad(pad_to_delete.owner, pad_to_delete.pos)
minetest.remove_node(pad_to_delete.pos)
notify(playername, "TPAD '" .. pad_to_delete.name .. "' gelöscht.")
return_to_management_with_fresh_state()
end)
deny_button:onClick(return_to_management_with_fresh_state)
end)
tpad.forms.admin = smartfs.create("tpad.forms.admin", function(state)
state:size(8, 8)
state:label(0.2, 0.2, "admin_label", "TPAD Einstellungen")
state:field(0.5, 2, 6, 0, "max_total_field", "Max. Gesamtzahl an TPADs (pro Spieler)")
state:field(0.5, 3.5, 6, 0, "max_global_field", "Max. globale TPADs (pro Spieler)")
state:button(6.5, 0.7, 1.5, 0, "save_button", "Speichern")
local close_button = state:button(6.5, 7, 1.5, 0, "close_button", "Schließen")
close_button:setClose(true)
end)
-- ========================================================================
-- Data Helper Functions
-- ========================================================================
function tpad.decorate_pad_data(pos, pad, ownername)
pad = table.copy(pad)
if type(pos) == "string" then
pad.strpos = pos
pad.pos = minetest.string_to_pos(pos)
else
pad.pos = pos
pad.strpos = minetest.pos_to_string(pos)
end
pad.owner = ownername
pad.name = pad.name or ""
pad.type = pad.type or PUBLIC_PAD
-- NEUE LOGIK: Füge den Suffix nur bei privaten Pads hinzu.
if pad.type == PRIVATE_PAD then
pad.local_fullname = pad.name .. " (privat)"
else
-- Bei Public Pads wird kein Suffix mehr angehängt.
pad.local_fullname = pad.name
end
pad.global_fullname = pad.name
return pad
end
function tpad.get_pad_data(pos)
local meta = minetest.get_meta(pos)
local ownername = meta:get_string("owner")
if not ownername or ownername == "" then return end
local pads = tpad._get_stored_pads(ownername)
local strpos = minetest.pos_to_string(pos)
local pad = pads[strpos]
if not pad then return end
return tpad.decorate_pad_data(pos, pad, ownername)
end
function tpad.set_pad_data(pos, padname, padtype_str)
local meta = minetest.get_meta(pos)
local ownername = meta:get_string("owner")
local pads = tpad._get_stored_pads(ownername)
local strpos = minetest.pos_to_string(pos)
local pad = pads[strpos] or {}
pad.name = padname
pad.type = padtype_string_to_flag[padtype_str]
pads[strpos] = pad
tpad._set_stored_pads(ownername, pads)
end
function tpad.del_pad(ownername, pos)
local pads = tpad._get_stored_pads(ownername)
pads[minetest.pos_to_string(pos)] = nil
tpad._set_stored_pads(ownername, pads)
end
-- ========================================================================
-- Register Node and Bind Callbacks
-- ========================================================================
local collision_box = {
type = "fixed",
fixed = { -0.5, -0.5, -0.5, 0.5, -0.3, 0.5 },
}
minetest.register_node(tpad.nodename, {
drawtype = "mesh",
tiles = { tpad.texture },
mesh = tpad.mesh,
paramtype = "light",
paramtype2 = "facedir",
on_place = minetest.rotate_and_place,
after_place_node = tpad.after_place_node,
collision_box = collision_box,
selection_box = collision_box,
description = "Teleporter Pad",
groups = {choppy = 2, dig_immediate = 2},
on_rightclick = tpad.on_rightclick,
can_dig = tpad.can_dig,
on_destruct = tpad.on_destruct,
})
minetest.register_chatcommand("tpad", {func = tpad.command})

925
init.lua.bak.3 Normal file
View file

@ -0,0 +1,925 @@
-- ========================================================================
-- TPAD MOD v1.2 (Final, Reworked Delete Logic)
-- ========================================================================
tpad = {}
tpad.version = "1.2" -- As requested, version is not incremented
tpad.mod_name = minetest.get_current_modname()
tpad.texture = "tpad-texture.png"
tpad.mesh = "tpad-mesh.obj"
tpad.nodename = "tpad:tpad"
tpad.mod_path = minetest.get_modpath(tpad.mod_name)
tpad.sound_teleport = tpad.mod_name .. "_teleport"
tpad.particle_texture = tpad.mod_name .. "_particle.png"
-- Temporäre Variable zur sicheren Datenübergabe an den Bestätigungsdialog
tpad.pending_deletion = {}
-- ========================================================================
-- Constants
-- ========================================================================
local PRIVATE_PAD_STRING = "Privat (nur Besitzer)"
local PUBLIC_PAD_STRING = "Lokal (nur eigenes Netzwerk)"
local GLOBAL_PAD_STRING = "Global (beliebiges Netzwerk)"
local PRIVATE_PAD = 1
local PUBLIC_PAD = 2
local GLOBAL_PAD = 4
local RED_ESCAPE = minetest.get_color_escape_sequence("#FF0000")
local YELLOW_ESCAPE = minetest.get_color_escape_sequence("#FFFF00")
local CYAN_ESCAPE = minetest.get_color_escape_sequence("#00FFFF")
local WHITE_ESCAPE = minetest.get_color_escape_sequence("#FFFFFF")
local OWNER_ESCAPE_COLOR = CYAN_ESCAPE
local padtype_flag_to_string = {
[PRIVATE_PAD] = PRIVATE_PAD_STRING,
[PUBLIC_PAD] = PUBLIC_PAD_STRING,
[GLOBAL_PAD] = GLOBAL_PAD_STRING,
}
local padtype_string_to_flag = {
[PRIVATE_PAD_STRING] = PRIVATE_PAD,
[PUBLIC_PAD_STRING] = PUBLIC_PAD,
[GLOBAL_PAD_STRING] = GLOBAL_PAD,
}
local short_padtype_string = {
[PRIVATE_PAD] = "private",
[PUBLIC_PAD] = "public",
[GLOBAL_PAD] = "global",
}
-- ========================================================================
-- Dependencies and Libs
-- ========================================================================
local smartfs = dofile(tpad.mod_path .. "/lib/smartfs.lua")
local notify = dofile(tpad.mod_path .. "/notify.lua")
local waypoint_hud_ids = {}
minetest.register_privilege("tpad_admin", {
description = "Can edit and destroy any tpad",
give_to_singleplayer = true,
})
-- ========================================================================
-- Original Helper Functions
-- ========================================================================
local function copy_file(source, dest)
local src_file = io.open(source, "rb")
if not src_file then return false, "copy_file() unable to open source for reading" end
local src_data = src_file:read("*all")
src_file:close()
local dest_file = io.open(dest, "wb")
if not dest_file then return false, "copy_file() unable to open dest for writing" end
dest_file:write(src_data)
dest_file:close()
return true, "files copied successfully"
end
local function custom_or_default(modname, path, filename)
local default_filename = "default/" .. filename
local full_filename = path .. "/custom." .. filename
local full_default_filename = path .. "/" .. default_filename
local file_exists_at_path = io.open(path .. "/" .. filename, "r")
if file_exists_at_path then
file_exists_at_path:close()
os.rename(path .. "/" .. filename, full_filename)
end
local file = io.open(full_filename, "rb")
if not file then
minetest.debug("[" .. modname .. "] Copying " .. default_filename .. " to " .. filename .. " (path: " .. path .. ")")
local success, err = copy_file(full_default_filename, full_filename)
if not success then
minetest.debug("[" .. modname .. "] " .. err)
return false
end
file = io.open(full_filename, "rb")
if not file then
minetest.debug("[" .. modname .. "] Unable to load " .. filename .. " file from path " .. path)
return false
end
end
file:close()
return full_filename
end
dofile(tpad.mod_path .. "/storage.lua")
-- ========================================================================
-- Load Custom Recipe
-- ========================================================================
local recipes_filename = custom_or_default(tpad.mod_name, tpad.mod_path, "recipes.lua")
if recipes_filename then
local recipes = dofile(recipes_filename)
if type(recipes) == "table" and recipes[tpad.nodename] then
minetest.register_craft({
output = tpad.nodename,
recipe = recipes[tpad.nodename],
})
end
end
-- ========================================================================
-- Chat Command
-- ========================================================================
function tpad.command(playername, param)
tpad.hud_off(playername)
if(param == "off") then return end
local player = minetest.get_player_by_name(playername)
local pads = tpad._get_stored_pads(playername)
local shortest_distance = nil
local closest_pad = nil
local playerpos = player:getpos()
for strpos, pad in pairs(pads) do
local pos = minetest.string_to_pos(strpos)
local distance = vector.distance(pos, playerpos)
if not shortest_distance or distance < shortest_distance then
closest_pad = {
pos = pos,
name = pad.name .. " " .. strpos,
}
shortest_distance = distance
end
end
if closest_pad then
waypoint_hud_ids[playername] = player:hud_add({
hud_elem_type = "waypoint",
name = closest_pad.name,
world_pos = closest_pad.pos,
number = 0xFF0000,
})
notify(playername, "Waypoint to " .. closest_pad.name .. " displayed")
end
end
function tpad.hud_off(playername)
local player = minetest.get_player_by_name(playername)
local hud_id = waypoint_hud_ids[playername]
if hud_id then
player:hud_remove(hud_id)
end
end
-- ========================================================================
-- Teleport Logic
-- ========================================================================
function tpad.do_teleport(player, destination_pos, destination_name, form_context)
local playername = player:get_player_name()
minetest.after(0.1, function()
if not player or not player:is_player() then return end
local start_pos = player:getpos()
minetest.sound_play(tpad.sound_teleport, {pos = start_pos, max_hear_distance = 10, gain = 1.0})
minetest.add_particlespawner({
amount = 60, time = 0.5,
minpos = vector.add(start_pos, -0.5), maxpos = vector.add(start_pos, 0.5),
minvel = {x=-1, y=0, z=-1}, maxvel = {x=1, y=2, z=1},
minacc = {x=0, y=0, z=0}, maxacc = {x=0, y=0, z=0},
minexptime = 0.5, maxexptime = 2,
minsize = 1, maxsize = 3,
texture = tpad.particle_texture,
})
player:move_to(destination_pos)
minetest.sound_play(tpad.sound_teleport, {pos = destination_pos, max_hear_distance = 10, gain = 1.0})
minetest.add_particlespawner({
amount = 60, time = 0.5,
minpos = vector.add(destination_pos, {x = -0.5, y = 0, z = -0.5}),
maxpos = vector.add(destination_pos, {x = 0.5, y = 1, z = 0.5}),
minvel = {x=-1, y=1, z=-1}, maxvel = {x=1, y=2, z=1},
minacc = {x=0, y=0, z=0}, maxacc = {x=0, y=0, z=0},
minexptime = 0.5, maxexptime = 2,
minsize = 1, maxsize = 3,
texture = tpad.particle_texture,
})
tpad.hud_off(playername)
-- NEU: Nach Ankunft am Ziel die Ansicht mit neuem Kontext aktualisieren
minetest.after(0.2, function()
if not player or not player:is_player() then return end
local dest_pad_data = tpad.get_pad_data(destination_pos)
if not dest_pad_data then return end
local new_context = {
playername = playername,
clicker = player,
ownername = dest_pad_data.owner,
clicked_pos = destination_pos,
node = minetest.get_node(destination_pos),
page = 1,
-- Behalte den Netzwerk-Typ (lokal/global) bei
network_type = form_context.network_type
}
tpad.show_network_view(new_context, new_context.network_type)
end)
end)
end
-- ========================================================================
-- Node Placement and Data Helpers
-- ========================================================================
function tpad.after_place_node(pos, placer, itemstack)
local playername = placer:get_player_name()
if tpad.max_total_pads_reached(placer) then
notify.warn(playername, "Du kannst keine weiteren TPADs erstellen. Limit erreicht.")
minetest.remove_node(pos)
minetest.add_item(placer:get_pos(), itemstack:get_name())
return
end
local meta = minetest.get_meta(pos)
meta:set_string("owner", playername)
meta:set_string("infotext", "TPAD Station von " .. playername)
tpad.set_pad_data(pos, "", PRIVATE_PAD_STRING)
end
local submit = {}
function tpad.max_total_pads_reached(placer)
local placername = placer:get_player_name()
if minetest.get_player_privs(placername).tpad_admin then
return false
end
local pads = tpad._get_stored_pads(placername)
local count = 0
for _ in pairs(pads) do
count = count + 1
end
return count >= tpad.get_max_total_pads()
end
function tpad.max_global_pads_reached(playername)
if minetest.get_player_privs(playername).tpad_admin then
return false
end
local pads = tpad._get_stored_pads(playername)
local count = 0
for _, pad in pairs(pads) do
if pad.type == GLOBAL_PAD then
count = count + 1
end
end
return count >= tpad.get_max_global_pads()
end
-- ========================================================================
-- GUI DATA HELPERS
-- ========================================================================
function submit.global_helper()
local allpads = tpad._get_all_pads()
local result = {}
for ownername, pads in pairs(allpads) do
for strpos, pad in pairs(pads) do
if pad.type == GLOBAL_PAD then
table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername))
end
end
end
table.sort(result, function(a, b) return a.global_fullname:lower() < b.global_fullname:lower() end)
return result
end
function submit.local_helper(ownername, viewername)
local result = {}
local added_pads = {} -- Verhindert, dass Pads doppelt hinzugefügt werden
-- Schritt 1: Füge die öffentlichen Pads des Besitzers des angeklickten TPADs hinzu
local owner_pads = tpad._get_stored_pads(ownername)
for strpos, pad in pairs(owner_pads) do
-- Vom Besitzer werden nur die öffentlichen (public) Pads angezeigt
if pad.type == PUBLIC_PAD then
table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername))
added_pads[strpos] = true
end
end
-- Schritt 2: Füge die eigenen Pads hinzu, falls der Betrachter eine andere Person ist
if ownername ~= viewername then
local viewer_pads = tpad._get_stored_pads(viewername)
for strpos, pad in pairs(viewer_pads) do
-- Vom Betrachter werden seine privaten und öffentlichen Pads angezeigt
if (pad.type == PUBLIC_PAD or pad.type == PRIVATE_PAD) and not added_pads[strpos] then
table.insert(result, tpad.decorate_pad_data(strpos, pad, viewername))
added_pads[strpos] = true
end
end
end
table.sort(result, function(a, b) return a.local_fullname:lower() < b.local_fullname:lower() end)
return result
end
function submit.management_helper(playername)
local is_admin = minetest.get_player_privs(playername).tpad_admin
local result = {}
if is_admin then
local allpads = tpad._get_all_pads()
for ownername, pads in pairs(allpads) do
for strpos, pad in pairs(pads) do
table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername))
end
end
else
local pads = tpad._get_stored_pads(playername)
for strpos, pad in pairs(pads) do
table.insert(result, tpad.decorate_pad_data(strpos, pad, playername))
end
end
table.sort(result, function(a, b) return a.local_fullname:lower() < b.local_fullname:lower() end)
return result
end
-- ========================================================================
-- GUI LOGIC (REFACTORED)
-- ========================================================================
function tpad.show_network_view(form, network_type)
local is_admin = minetest.get_player_privs(form.playername).tpad_admin
local form_key = is_admin and "network_view_admin" or "network_view"
form.state = tpad.forms[form_key]:show(form.playername)
form.formname = "tpad.forms." .. form_key
local pad_list
local title_text
local function create_clean_context()
return {
playername = form.playername,
clicker = form.clicker,
ownername = form.ownername,
clicked_pos = form.clicked_pos,
node = form.node,
page = form.page or 1
}
end
local current_pad_data = tpad.get_pad_data(form.clicked_pos)
local current_pad_name = current_pad_data.name or "Unnamed"
local current_pad_type_str
if current_pad_data.type == GLOBAL_PAD then
current_pad_type_str = "GLOBALE"
else
current_pad_type_str = "LOKALE"
end
local RED_ESCAPE = minetest.get_color_escape_sequence("#FF0000") or ""
title_text = RED_ESCAPE .. current_pad_type_str .. " " .. WHITE_ESCAPE .. "TPAD-Station " .. YELLOW_ESCAPE .. current_pad_name .. WHITE_ESCAPE .. ". Wähle ein Ziel:"
if network_type == "global" then
form.network_type = "global"
pad_list = submit.global_helper()
form.state:get("toggle_network_button"):setText("Lokales Netzwerk")
form.state:get("toggle_network_button"):onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
clean_form.ownername = clean_form.playername
clean_form.page = 1
tpad.show_network_view(clean_form, "local")
end)
end)
else -- "local"
form.network_type = "local"
-- KORREKTUR: Übergebe den Spielernamen (string), nicht das Ergebnis eines Vergleichs (boolean).
pad_list = submit.local_helper(form.ownername, form.playername)
form.state:get("toggle_network_button"):setText("Globales Netzwerk")
form.state:get("toggle_network_button"):onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
clean_form.page = 1
tpad.show_network_view(clean_form, "global")
end)
end)
end
form.state:get("title_label"):setText(title_text)
form.state:get("management_button"):onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
clean_form.preselect_pos = clean_form.clicked_pos
submit.management(clean_form)
end)
end)
if is_admin then
form.state:get("admin_button"):onClick(function()
minetest.after(0, function() submit.admin_settings(form) end)
end)
end
local max_rows, num_columns = 10, 3
local buttons_per_page = max_rows * num_columns
local destination_pads = {}
for _, pad in ipairs(pad_list) do
if not vector.equals(pad.pos, form.clicked_pos) then
table.insert(destination_pads, pad)
end
end
local total_pages = math.ceil(#destination_pads / buttons_per_page)
if total_pages == 0 then total_pages = 1 end
local current_page = form.page or 1
if current_page > total_pages then current_page = total_pages end
if current_page < 1 then current_page = 1 end
form.page = current_page
for i = 1, buttons_per_page do
local index = ((current_page - 1) * buttons_per_page) + i
local pad_data = destination_pads[index]
local button_name = "tpad_btn_" .. i
local button = form.state:get(button_name)
if button then
if pad_data then
local display_name = (network_type == "global") and pad_data.global_fullname or pad_data.local_fullname
button:setText(display_name)
button:setVisible(true)
button:onClick(function()
tpad.do_teleport(form.clicker, pad_data.pos, display_name, form)
end)
else
button:setVisible(false)
end
end
end
if total_pages > 1 then
form.state:get("page_label"):setText("Seite " .. current_page .. " von " .. total_pages)
local prev_button, next_button = form.state:get("prev_button"), form.state:get("next_button")
prev_button:setVisible(current_page > 1)
next_button:setVisible(current_page < total_pages)
prev_button:onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
clean_form.page = current_page - 1
tpad.show_network_view(clean_form, network_type)
end)
end)
next_button:onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
clean_form.page = current_page + 1
tpad.show_network_view(clean_form, network_type)
end)
end)
else
form.state:get("page_label"):setText("")
form.state:get("prev_button"):setVisible(false)
form.state:get("next_button"):setVisible(false)
end
end
function submit.management(form)
form.formname = "tpad_management"
form.state = tpad.forms.management:show(form.playername)
local pad_list = submit.management_helper(form.playername)
local listbox = form.state:get("pads_listbox")
local is_admin = minetest.get_player_privs(form.playername).tpad_admin
local selected_pad_data = nil
listbox:clearItems()
for _, pad in ipairs(pad_list) do
local label = pad.name .. " (" .. short_padtype_string[pad.type] .. ")"
if is_admin then
label = label .. " [" .. pad.owner .. "]"
end
listbox:addItem(label)
end
local padname_field = form.state:get("padname_field")
local padtype_dropdown = form.state:get("padtype_dropdown")
local function update_fields_for_index(index)
if not index or index <= 0 then return end
selected_pad_data = pad_list[index]
if selected_pad_data then
padname_field:setText(selected_pad_data.name)
padtype_dropdown:setSelectedItem(padtype_flag_to_string[selected_pad_data.type])
end
end
if form.preselect_pos then
for i, pad in ipairs(pad_list) do
if vector.equals(pad.pos, form.preselect_pos) then
listbox:setSelected(i)
update_fields_for_index(i)
break
end
end
end
listbox:onClick(function()
local index = listbox:getSelected()
update_fields_for_index(index)
end)
form.state:get("save_button"):onClick(function()
if not selected_pad_data then
notify.warn(form.playername, "Bitte wähle zuerst ein TPAD aus der Liste aus.")
return
end
local new_name = padname_field:getText()
local new_type_str
local value_from_dropdown = padtype_dropdown:getSelectedItem()
if value_from_dropdown then
local index = tonumber(value_from_dropdown)
if index then
new_type_str = padtype_dropdown:getItem(index)
else
new_type_str = value_from_dropdown
end
end
if not new_type_str or new_type_str == "" then
notify.err(form.playername, "Konnte TPAD-Typ nicht lesen. Bitte erneut versuchen.")
return
end
if not minetest.get_player_privs(form.playername).tpad_admin then
if new_type_str == GLOBAL_PAD_STRING and tpad.max_global_pads_reached(selected_pad_data.owner) then
notify.warn(form.playername, "Der Besitzer des TPADs hat das Limit für globale TPADs erreicht.")
return
end
end
tpad.set_pad_data(selected_pad_data.pos, new_name, new_type_str)
local meta = minetest.get_meta(selected_pad_data.pos)
if new_name and new_name ~= "" then
meta:set_string("infotext", "TPAD Station " .. new_name)
else
meta:set_string("infotext", "Unbenannte TPAD Station")
end
notify(form.playername, "TPAD '" .. new_name .. "' gespeichert.")
minetest.after(0, function()
form.preselect_pos = nil
submit.management(form)
end)
end)
form.state:get("delete_button"):onClick(function()
if not selected_pad_data then
notify.warn(form.playername, "Bitte wähle zuerst ein TPAD aus der Liste aus.")
return
end
if vector.equals(selected_pad_data.pos, form.clicked_pos) then
notify.warn(form.playername, "Du kannst das TPAD, an dem du stehst, nicht über dieses Menü löschen.")
return
end
tpad.pending_deletion[form.playername] = {
pad_data = selected_pad_data,
original_form_context = form,
}
minetest.close_formspec(form.playername, form.formname)
minetest.after(0.1, function()
tpad.forms.confirm_pad_deletion:show(form.playername)
end)
end)
form.state:get("back_button"):onClick(function()
minetest.after(0, function()
local clean_form_context = {
playername = form.playername,
clicker = form.clicker,
ownername = form.ownername,
clicked_pos = form.clicked_pos,
node = form.node,
page = 1
}
tpad.show_network_view(clean_form_context, form.network_type)
end)
end)
-- NEU: Logik für den Admin-Teleport-Button
if is_admin then
local admin_teleport_button = form.state:get("admin_teleport_button")
admin_teleport_button:setVisible(true)
admin_teleport_button:onClick(function()
if not selected_pad_data then
notify.warn(form.playername, "Bitte zuerst ein TPAD aus der Liste auswählen, um dorthin zu teleportieren.")
return
end
local dest_pad = selected_pad_data
-- Erstelle den Kontext für die Ansicht, die nach dem Teleport angezeigt werden soll
local new_context_after_teleport = {
playername = form.playername,
clicker = form.clicker,
ownername = dest_pad.owner,
clicked_pos = dest_pad.pos,
node = minetest.get_node(dest_pad.pos),
page = 1,
network_type = (dest_pad.type == GLOBAL_PAD) and "global" or "local"
}
tpad.do_teleport(form.clicker, dest_pad.pos, dest_pad.name, new_context_after_teleport)
end)
end
end
function submit.admin_settings(form)
form.state = tpad.forms.admin:show(form.playername)
form.formname = "tpad_admin_settings"
local max_total_field, max_global_field = form.state:get("max_total_field"), form.state:get("max_global_field")
max_total_field:setText(tpad.get_max_total_pads())
max_global_field:setText(tpad.get_max_global_pads())
form.state:get("save_button"):onClick(function()
tpad.set_max_total_pads(tonumber(max_total_field:getText()))
tpad.set_max_global_pads(tonumber(max_global_field:getText()))
minetest.close_formspec(form.playername, form.formname)
end)
end
-- ========================================================================
-- Main Node Callbacks
-- ========================================================================
function tpad.on_rightclick(clicked_pos, node, clicker)
local playername = clicker:get_player_name()
local pad = tpad.get_pad_data(clicked_pos)
if not pad or not pad.owner then
notify.err(playername, "Fehler! Fehlende oder korrupte TPAD-Daten. Bitte neu platzieren.")
return
end
-- Zugriffsschutz-Prüfung zuerst
if pad.type == PRIVATE_PAD and pad.owner ~= playername and not minetest.get_player_privs(playername).tpad_admin then
notify.warn(playername, "Dieses TPAD ist privat.")
return
end
-- Erstelle das Kontext-Objekt für alle Fälle
local form = {
playername = playername,
clicker = clicker,
ownername = pad.owner,
clicked_pos = clicked_pos,
node = node,
page = 1,
}
-- NEU: Prüfe, ob das Pad neu ist (d.h. keinen Namen hat)
if pad.name == "" then
-- Setze einen Parameter zur Vorauswahl für das Verwaltungs-Menü
form.preselect_pos = clicked_pos
-- Rufe direkt die Verwaltung auf
submit.management(form)
else
-- BESTEHENDE LOGIK: Wenn das Pad bereits konfiguriert ist
if pad.type == GLOBAL_PAD then
tpad.show_network_view(form, "global")
else
tpad.show_network_view(form, "local")
end
end
end
function tpad.can_dig(pos, player)
local meta = minetest.get_meta(pos)
local ownername = meta:get_string("owner")
local playername = player:get_player_name()
if ownername == "" or ownername == playername or minetest.get_player_privs(playername).tpad_admin then
return true
end
notify.warn(playername, "Dieses TPAD gehört dir nicht.")
return false
end
function tpad.on_destruct(pos)
local meta = minetest.get_meta(pos)
local ownername = meta:get_string("owner")
if ownername and ownername ~= "" then
tpad.del_pad(ownername, pos)
end
end
-- ========================================================================
-- FORMS (Rebuilt)
-- ========================================================================
tpad.forms = {}
local function create_network_view_form(state)
state:size(16, 11)
state:label(0.5, 0.2, "title_label", "")
local bottom_y = 10.4
state:button(0.5, bottom_y, 3.0, 0, "toggle_network_button", "")
state:button(3.6, bottom_y, 2.3, 0, "management_button", "Verwaltung")
local close_button = state:button(14.0, bottom_y, 1.5, 0, "close_button", "Schließen")
close_button:setClose(true)
state:button(6.0, bottom_y, 1.0, 0, "prev_button", "[<<]")
state:label(7.1, bottom_y, "page_label", "")
state:button(9.5, bottom_y, 1.0, 0, "next_button", "[>>]")
local max_rows, num_columns = 10, 3
local start_x, start_y = 0.5, 1.0
local button_width, button_height = 4.8, 0.8
local column_width = 5.0
for i = 1, max_rows * num_columns do
local column = math.floor((i - 1) / max_rows)
local row = (i - 1) % max_rows
local current_x = start_x + (column * column_width)
local current_y = start_y + (row * button_height)
state:button(current_x, current_y, button_width, 0, "tpad_btn_" .. i, ""):setVisible(false)
end
end
tpad.forms.teleport_success = smartfs.create("tpad.forms.teleport_success", function(state)
state:size(8, 2)
local destination_name = state.param.destination_name or "???"
state:label(0.5, 0.5, "success_label", "Teleport erfolgreich: " .. YELLOW_ESCAPE .. destination_name)
-- Dieser Button hat setClose(true), was das Fenster zuverlässig schließt.
local close_button = state:button(3, 1.2, 2, 0, "close_button", "Schließen")
close_button:setClose(true)
end)
tpad.forms.network_view = smartfs.create("tpad.forms.network_view", create_network_view_form)
tpad.forms.network_view_admin = smartfs.create("tpad.forms.network_view_admin", function(state)
create_network_view_form(state)
state:button(12.4, 10.4, 1.5, 0, "admin_button", "Admin")
end)
tpad.forms.management = smartfs.create("tpad.forms.management", function(state)
state:size(12, 9)
state:label(0.2, 0.2, "management_title", "TPAD Verwaltung")
state:listbox(0.2, 0.6, 11.6, 5, "pads_listbox", {})
state:field(0.5, 6.6, 6, 0, "padname_field", "Name", "")
local padtype_dropdown = state:dropdown(0.25, 6.7, 6.25, 0, "padtype_dropdown")
padtype_dropdown:addItem(PRIVATE_PAD_STRING)
padtype_dropdown:addItem(PUBLIC_PAD_STRING)
padtype_dropdown:addItem(GLOBAL_PAD_STRING)
state:button(7, 6.3, 2, 0, "save_button", "Speichern")
state:button(7, 7.1, 2, 0, "delete_button", "Löschen")
state:button(0.2, 8.4, 2, 0, "back_button", "Zurück")
-- NEU: Teleport-Button für Admins, standardmäßig unsichtbar
state:button(2.3, 8.4, 3, 0, "admin_teleport_button", "Teleport (Admin)"):setVisible(false)
local close_button = state:button(9.8, 8.4, 2, 0, "close_button", "Schließen")
close_button:setClose(true)
end)
-- This form now defines its OWN behavior, making it independent and robust.
tpad.forms.confirm_pad_deletion = smartfs.create("tpad.forms.confirm_pad_deletion", function(state)
state:size(8, 2.5)
state:label(0, 0, "intro_label", "Willst du das TPAD wirklich löschen?")
state:label(0, 0.5, "padname_label", "")
state:label(0, 1, "outro_label", "(es lässt sich nicht wiederherstellen)")
local confirm_button = state:button(0, 2.2, 2, 0, "confirm_button", "Ja, löschen")
local deny_button = state:button(6, 2.2, 2, 0, "deny_button", "Nein, abbrechen")
local playername = state.location.player
local pending_data = tpad.pending_deletion[playername]
if not pending_data then
state:close()
return
end
local pad_to_delete = pending_data.pad_data
local original_form_context = pending_data.original_form_context
local pad_display_name = pad_to_delete.name .. " (" .. short_padtype_string[pad_to_delete.type] .. ")"
state:get("padname_label"):setText(YELLOW_ESCAPE .. pad_display_name)
-- Diese Funktion erzwingt einen sauberen Neuaufbau der Verwaltungs-Ansicht
local function return_to_management_with_fresh_state()
tpad.pending_deletion[playername] = nil -- Temporäre Daten löschen
minetest.after(0, function()
-- Erstelle einen sauberen Kontext, anstatt den alten wiederzuverwenden
local fresh_context = {
playername = original_form_context.playername,
clicker = original_form_context.clicker,
ownername = original_form_context.ownername,
clicked_pos = original_form_context.clicked_pos,
node = minetest.get_node(original_form_context.clicked_pos), -- Node neu holen, falls sich was geändert hat
network_type = original_form_context.network_type,
page = 1
}
submit.management(fresh_context)
end)
end
confirm_button:onClick(function()
tpad.del_pad(pad_to_delete.owner, pad_to_delete.pos)
minetest.remove_node(pad_to_delete.pos)
notify(playername, "TPAD '" .. pad_to_delete.name .. "' gelöscht.")
return_to_management_with_fresh_state()
end)
deny_button:onClick(return_to_management_with_fresh_state)
end)
tpad.forms.admin = smartfs.create("tpad.forms.admin", function(state)
state:size(8, 8)
state:label(0.2, 0.2, "admin_label", "TPAD Einstellungen")
state:field(0.5, 2, 6, 0, "max_total_field", "Max. Gesamtzahl an TPADs (pro Spieler)")
state:field(0.5, 3.5, 6, 0, "max_global_field", "Max. globale TPADs (pro Spieler)")
state:button(6.5, 0.7, 1.5, 0, "save_button", "Speichern")
local close_button = state:button(6.5, 7, 1.5, 0, "close_button", "Schließen")
close_button:setClose(true)
end)
-- ========================================================================
-- Data Helper Functions
-- ========================================================================
function tpad.decorate_pad_data(pos, pad, ownername)
pad = table.copy(pad)
if type(pos) == "string" then
pad.strpos = pos
pad.pos = minetest.string_to_pos(pos)
else
pad.pos = pos
pad.strpos = minetest.pos_to_string(pos)
end
pad.owner = ownername
pad.name = pad.name or ""
pad.type = pad.type or PUBLIC_PAD
-- NEUE LOGIK: Füge den Suffix nur bei privaten Pads hinzu.
if pad.type == PRIVATE_PAD then
pad.local_fullname = pad.name .. " (privat)"
else
-- Bei Public Pads wird kein Suffix mehr angehängt.
pad.local_fullname = pad.name
end
pad.global_fullname = pad.name
return pad
end
function tpad.get_pad_data(pos)
local meta = minetest.get_meta(pos)
local ownername = meta:get_string("owner")
if not ownername or ownername == "" then return end
local pads = tpad._get_stored_pads(ownername)
local strpos = minetest.pos_to_string(pos)
local pad = pads[strpos]
if not pad then return end
return tpad.decorate_pad_data(pos, pad, ownername)
end
function tpad.set_pad_data(pos, padname, padtype_str)
local meta = minetest.get_meta(pos)
local ownername = meta:get_string("owner")
local pads = tpad._get_stored_pads(ownername)
local strpos = minetest.pos_to_string(pos)
local pad = pads[strpos] or {}
pad.name = padname
pad.type = padtype_string_to_flag[padtype_str]
pads[strpos] = pad
tpad._set_stored_pads(ownername, pads)
end
function tpad.del_pad(ownername, pos)
local pads = tpad._get_stored_pads(ownername)
pads[minetest.pos_to_string(pos)] = nil
tpad._set_stored_pads(ownername, pads)
end
-- ========================================================================
-- Register Node and Bind Callbacks
-- ========================================================================
local collision_box = {
type = "fixed",
fixed = { -0.5, -0.5, -0.5, 0.5, -0.3, 0.5 },
}
minetest.register_node(tpad.nodename, {
drawtype = "mesh",
tiles = { tpad.texture },
mesh = tpad.mesh,
paramtype = "light",
paramtype2 = "facedir",
on_place = minetest.rotate_and_place,
after_place_node = tpad.after_place_node,
collision_box = collision_box,
selection_box = collision_box,
description = "Teleporter Pad",
groups = {choppy = 2, dig_immediate = 2},
on_rightclick = tpad.on_rightclick,
can_dig = tpad.can_dig,
on_destruct = tpad.on_destruct,
})
minetest.register_chatcommand("tpad", {func = tpad.command})

931
init.lua.bak.4 Normal file
View file

@ -0,0 +1,931 @@
-- ========================================================================
-- TPAD MOD v1.2 (Final, Reworked Delete Logic)
-- ========================================================================
tpad = {}
tpad.version = "1.2" -- As requested, version is not incremented
tpad.mod_name = minetest.get_current_modname()
tpad.texture = "tpad-texture.png"
tpad.mesh = "tpad-mesh.obj"
tpad.nodename = "tpad:tpad"
tpad.mod_path = minetest.get_modpath(tpad.mod_name)
tpad.sound_teleport = tpad.mod_name .. "_teleport"
tpad.particle_texture = tpad.mod_name .. "_particle.png"
-- Temporäre Variable zur sicheren Datenübergabe an den Bestätigungsdialog
tpad.pending_deletion = {}
-- ========================================================================
-- Constants
-- ========================================================================
local PRIVATE_PAD_STRING = "Privat (nur Besitzer)"
local PUBLIC_PAD_STRING = "Lokal (nur eigenes Netzwerk)"
local GLOBAL_PAD_STRING = "Global (beliebiges Netzwerk)"
local PRIVATE_PAD = 1
local PUBLIC_PAD = 2
local GLOBAL_PAD = 4
local RED_ESCAPE = minetest.get_color_escape_sequence("#FF0000")
local YELLOW_ESCAPE = minetest.get_color_escape_sequence("#FFFF00")
local CYAN_ESCAPE = minetest.get_color_escape_sequence("#00FFFF")
local WHITE_ESCAPE = minetest.get_color_escape_sequence("#FFFFFF")
local OWNER_ESCAPE_COLOR = CYAN_ESCAPE
local padtype_flag_to_string = {
[PRIVATE_PAD] = PRIVATE_PAD_STRING,
[PUBLIC_PAD] = PUBLIC_PAD_STRING,
[GLOBAL_PAD] = GLOBAL_PAD_STRING,
}
local padtype_string_to_flag = {
[PRIVATE_PAD_STRING] = PRIVATE_PAD,
[PUBLIC_PAD_STRING] = PUBLIC_PAD,
[GLOBAL_PAD_STRING] = GLOBAL_PAD,
}
local short_padtype_string = {
[PRIVATE_PAD] = "private",
[PUBLIC_PAD] = "public",
[GLOBAL_PAD] = "global",
}
-- ========================================================================
-- Dependencies and Libs
-- ========================================================================
local smartfs = dofile(tpad.mod_path .. "/lib/smartfs.lua")
local notify = dofile(tpad.mod_path .. "/notify.lua")
local waypoint_hud_ids = {}
minetest.register_privilege("tpad_admin", {
description = "Can edit and destroy any tpad",
give_to_singleplayer = true,
})
-- ========================================================================
-- Original Helper Functions
-- ========================================================================
local function copy_file(source, dest)
local src_file = io.open(source, "rb")
if not src_file then return false, "copy_file() unable to open source for reading" end
local src_data = src_file:read("*all")
src_file:close()
local dest_file = io.open(dest, "wb")
if not dest_file then return false, "copy_file() unable to open dest for writing" end
dest_file:write(src_data)
dest_file:close()
return true, "files copied successfully"
end
local function custom_or_default(modname, path, filename)
local default_filename = "default/" .. filename
local full_filename = path .. "/custom." .. filename
local full_default_filename = path .. "/" .. default_filename
local file_exists_at_path = io.open(path .. "/" .. filename, "r")
if file_exists_at_path then
file_exists_at_path:close()
os.rename(path .. "/" .. filename, full_filename)
end
local file = io.open(full_filename, "rb")
if not file then
minetest.debug("[" .. modname .. "] Copying " .. default_filename .. " to " .. filename .. " (path: " .. path .. ")")
local success, err = copy_file(full_default_filename, full_filename)
if not success then
minetest.debug("[" .. modname .. "] " .. err)
return false
end
file = io.open(full_filename, "rb")
if not file then
minetest.debug("[" .. modname .. "] Unable to load " .. filename .. " file from path " .. path)
return false
end
end
file:close()
return full_filename
end
dofile(tpad.mod_path .. "/storage.lua")
-- ========================================================================
-- Load Custom Recipe
-- ========================================================================
local recipes_filename = custom_or_default(tpad.mod_name, tpad.mod_path, "recipes.lua")
if recipes_filename then
local recipes = dofile(recipes_filename)
if type(recipes) == "table" and recipes[tpad.nodename] then
minetest.register_craft({
output = tpad.nodename,
recipe = recipes[tpad.nodename],
})
end
end
-- ========================================================================
-- Chat Command
-- ========================================================================
function tpad.command(playername, param)
tpad.hud_off(playername)
if(param == "off") then return end
local player = minetest.get_player_by_name(playername)
local pads = tpad._get_stored_pads(playername)
local shortest_distance = nil
local closest_pad = nil
local playerpos = player:getpos()
for strpos, pad in pairs(pads) do
local pos = minetest.string_to_pos(strpos)
local distance = vector.distance(pos, playerpos)
if not shortest_distance or distance < shortest_distance then
closest_pad = {
pos = pos,
name = pad.name .. " " .. strpos,
}
shortest_distance = distance
end
end
if closest_pad then
waypoint_hud_ids[playername] = player:hud_add({
hud_elem_type = "waypoint",
name = closest_pad.name,
world_pos = closest_pad.pos,
number = 0xFF0000,
})
notify(playername, "Waypoint to " .. closest_pad.name .. " displayed")
end
end
function tpad.hud_off(playername)
local player = minetest.get_player_by_name(playername)
local hud_id = waypoint_hud_ids[playername]
if hud_id then
player:hud_remove(hud_id)
end
end
-- ========================================================================
-- Teleport Logic
-- ========================================================================
function tpad.do_teleport(player, destination_pos, destination_name, form_context)
local playername = player:get_player_name()
minetest.after(0.1, function()
if not player or not player:is_player() then return end
local start_pos = player:getpos()
minetest.sound_play(tpad.sound_teleport, {pos = start_pos, max_hear_distance = 10, gain = 1.0})
minetest.add_particlespawner({
amount = 60, time = 0.5,
minpos = vector.add(start_pos, -0.5), maxpos = vector.add(start_pos, 0.5),
minvel = {x=-1, y=0, z=-1}, maxvel = {x=1, y=2, z=1},
minacc = {x=0, y=0, z=0}, maxacc = {x=0, y=0, z=0},
minexptime = 0.5, maxexptime = 2,
minsize = 1, maxsize = 3,
texture = tpad.particle_texture,
})
player:move_to(destination_pos)
minetest.sound_play(tpad.sound_teleport, {pos = destination_pos, max_hear_distance = 10, gain = 1.0})
minetest.add_particlespawner({
amount = 60, time = 0.5,
minpos = vector.add(destination_pos, {x = -0.5, y = 0, z = -0.5}),
maxpos = vector.add(destination_pos, {x = 0.5, y = 1, z = 0.5}),
minvel = {x=-1, y=1, z=-1}, maxvel = {x=1, y=2, z=1},
minacc = {x=0, y=0, z=0}, maxacc = {x=0, y=0, z=0},
minexptime = 0.5, maxexptime = 2,
minsize = 1, maxsize = 3,
texture = tpad.particle_texture,
})
tpad.hud_off(playername)
-- NEU: Nach Ankunft am Ziel die Ansicht mit neuem Kontext aktualisieren
minetest.after(0.2, function()
if not player or not player:is_player() then return end
local dest_pad_data = tpad.get_pad_data(destination_pos)
if not dest_pad_data then return end
local new_context = {
playername = playername,
clicker = player,
ownername = dest_pad_data.owner,
clicked_pos = destination_pos,
node = minetest.get_node(destination_pos),
page = 1,
-- Behalte den Netzwerk-Typ (lokal/global) bei
network_type = form_context.network_type
}
tpad.show_network_view(new_context, new_context.network_type)
end)
end)
end
-- ========================================================================
-- Node Placement and Data Helpers
-- ========================================================================
function tpad.after_place_node(pos, placer, itemstack)
local playername = placer:get_player_name()
if tpad.max_total_pads_reached(placer) then
notify.warn(playername, "Du kannst keine weiteren TPADs erstellen. Limit erreicht.")
minetest.remove_node(pos)
minetest.add_item(placer:get_pos(), itemstack:get_name())
return
end
local meta = minetest.get_meta(pos)
meta:set_string("owner", playername)
meta:set_string("infotext", "TPAD Station von " .. playername)
tpad.set_pad_data(pos, "", PRIVATE_PAD_STRING)
end
local submit = {}
function tpad.max_total_pads_reached(placer)
local placername = placer:get_player_name()
if minetest.get_player_privs(placername).tpad_admin then
return false
end
local pads = tpad._get_stored_pads(placername)
local count = 0
for _ in pairs(pads) do
count = count + 1
end
return count >= tpad.get_max_total_pads()
end
function tpad.max_global_pads_reached(playername)
if minetest.get_player_privs(playername).tpad_admin then
return false
end
local pads = tpad._get_stored_pads(playername)
local count = 0
for _, pad in pairs(pads) do
if pad.type == GLOBAL_PAD then
count = count + 1
end
end
return count >= tpad.get_max_global_pads()
end
-- ========================================================================
-- GUI DATA HELPERS
-- ========================================================================
function submit.global_helper()
local allpads = tpad._get_all_pads()
local result = {}
for ownername, pads in pairs(allpads) do
for strpos, pad in pairs(pads) do
if pad.type == GLOBAL_PAD then
table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername))
end
end
end
table.sort(result, function(a, b) return a.global_fullname:lower() < b.global_fullname:lower() end)
return result
end
function submit.local_helper(ownername, viewername)
local result = {}
local added_pads = {} -- Verhindert, dass Pads doppelt hinzugefügt werden
-- Schritt 1: Hole die Pads des Besitzers, auf dessen Pad man steht.
local owner_pads = tpad._get_stored_pads(ownername)
for strpos, pad in pairs(owner_pads) do
-- KORREKTUR: Wenn der Betrachter der Besitzer ist, zeige auch seine privaten Pads an.
-- Ansonsten zeige nur die öffentlichen Pads des Besitzers.
local is_viewer_the_owner = (ownername == viewername)
if (pad.type == PUBLIC_PAD) or (is_viewer_the_owner and pad.type == PRIVATE_PAD) then
-- Füge nur Pads hinzu, die nicht global sind.
if pad.type ~= GLOBAL_PAD then
table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername))
added_pads[strpos] = true
end
end
end
-- Schritt 2: Füge die eigenen Pads des Betrachters hinzu, falls er eine andere Person ist.
if ownername ~= viewername then
local viewer_pads = tpad._get_stored_pads(viewername)
for strpos, pad in pairs(viewer_pads) do
-- Der Betrachter sieht immer seine eigenen privaten und öffentlichen Pads.
if (pad.type == PUBLIC_PAD or pad.type == PRIVATE_PAD) and not added_pads[strpos] then
table.insert(result, tpad.decorate_pad_data(strpos, pad, viewername))
added_pads[strpos] = true
end
end
end
table.sort(result, function(a, b) return a.local_fullname:lower() < b.local_fullname:lower() end)
return result
end
function submit.management_helper(playername)
local is_admin = minetest.get_player_privs(playername).tpad_admin
local result = {}
if is_admin then
local allpads = tpad._get_all_pads()
for ownername, pads in pairs(allpads) do
for strpos, pad in pairs(pads) do
table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername))
end
end
else
local pads = tpad._get_stored_pads(playername)
for strpos, pad in pairs(pads) do
table.insert(result, tpad.decorate_pad_data(strpos, pad, playername))
end
end
table.sort(result, function(a, b) return a.local_fullname:lower() < b.local_fullname:lower() end)
return result
end
-- ========================================================================
-- GUI LOGIC (REFACTORED)
-- ========================================================================
function tpad.show_network_view(form, network_type)
local is_admin = minetest.get_player_privs(form.playername).tpad_admin
local form_key = is_admin and "network_view_admin" or "network_view"
form.state = tpad.forms[form_key]:show(form.playername)
form.formname = "tpad.forms." .. form_key
local pad_list
local title_text
local function create_clean_context()
return {
playername = form.playername,
clicker = form.clicker,
ownername = form.ownername,
clicked_pos = form.clicked_pos,
node = form.node,
page = form.page or 1
}
end
local current_pad_data = tpad.get_pad_data(form.clicked_pos)
local current_pad_name = current_pad_data.name or "Unnamed"
local current_pad_type_str
if current_pad_data.type == GLOBAL_PAD then
current_pad_type_str = "GLOBALE"
else
current_pad_type_str = "LOKALE"
end
local RED_ESCAPE = minetest.get_color_escape_sequence("#FF0000") or ""
title_text = RED_ESCAPE .. current_pad_type_str .. " " .. WHITE_ESCAPE .. "TPAD-Station " .. YELLOW_ESCAPE .. current_pad_name .. WHITE_ESCAPE .. ". Wähle ein Ziel:"
if network_type == "global" then
form.network_type = "global"
pad_list = submit.global_helper()
form.state:get("toggle_network_button"):setText("Lokales Netzwerk")
form.state:get("toggle_network_button"):onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
-- KORREKTUR: Der 'ownername' wird nicht mehr überschrieben.
-- Er bleibt der des ursprünglichen Pads, was korrekt ist.
clean_form.page = 1
tpad.show_network_view(clean_form, "local")
end)
end)
else -- "local"
form.network_type = "local"
-- KORREKTUR: Übergebe den Spielernamen (string), nicht das Ergebnis eines Vergleichs (boolean).
pad_list = submit.local_helper(form.ownername, form.playername)
form.state:get("toggle_network_button"):setText("Globales Netzwerk")
form.state:get("toggle_network_button"):onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
clean_form.page = 1
tpad.show_network_view(clean_form, "global")
end)
end)
end
form.state:get("title_label"):setText(title_text)
form.state:get("management_button"):onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
clean_form.preselect_pos = clean_form.clicked_pos
submit.management(clean_form)
end)
end)
if is_admin then
form.state:get("admin_button"):onClick(function()
minetest.after(0, function() submit.admin_settings(form) end)
end)
end
local max_rows, num_columns = 10, 3
local buttons_per_page = max_rows * num_columns
local destination_pads = {}
for _, pad in ipairs(pad_list) do
if not vector.equals(pad.pos, form.clicked_pos) then
table.insert(destination_pads, pad)
end
end
local total_pages = math.ceil(#destination_pads / buttons_per_page)
if total_pages == 0 then total_pages = 1 end
local current_page = form.page or 1
if current_page > total_pages then current_page = total_pages end
if current_page < 1 then current_page = 1 end
form.page = current_page
for i = 1, buttons_per_page do
local index = ((current_page - 1) * buttons_per_page) + i
local pad_data = destination_pads[index]
local button_name = "tpad_btn_" .. i
local button = form.state:get(button_name)
if button then
if pad_data then
local display_name = (network_type == "global") and pad_data.global_fullname or pad_data.local_fullname
button:setText(display_name)
button:setVisible(true)
button:onClick(function()
tpad.do_teleport(form.clicker, pad_data.pos, display_name, form)
end)
else
button:setVisible(false)
end
end
end
if total_pages > 1 then
form.state:get("page_label"):setText("Seite " .. current_page .. " von " .. total_pages)
local prev_button, next_button = form.state:get("prev_button"), form.state:get("next_button")
prev_button:setVisible(current_page > 1)
next_button:setVisible(current_page < total_pages)
prev_button:onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
clean_form.page = current_page - 1
tpad.show_network_view(clean_form, network_type)
end)
end)
next_button:onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
clean_form.page = current_page + 1
tpad.show_network_view(clean_form, network_type)
end)
end)
else
form.state:get("page_label"):setText("")
form.state:get("prev_button"):setVisible(false)
form.state:get("next_button"):setVisible(false)
end
end
function submit.management(form)
form.formname = "tpad_management"
form.state = tpad.forms.management:show(form.playername)
local pad_list = submit.management_helper(form.playername)
local listbox = form.state:get("pads_listbox")
local is_admin = minetest.get_player_privs(form.playername).tpad_admin
local selected_pad_data = nil
listbox:clearItems()
for _, pad in ipairs(pad_list) do
local label = pad.name .. " (" .. short_padtype_string[pad.type] .. ")"
if is_admin then
label = label .. " [" .. pad.owner .. "]"
end
listbox:addItem(label)
end
local padname_field = form.state:get("padname_field")
local padtype_dropdown = form.state:get("padtype_dropdown")
local function update_fields_for_index(index)
if not index or index <= 0 then return end
selected_pad_data = pad_list[index]
if selected_pad_data then
padname_field:setText(selected_pad_data.name)
padtype_dropdown:setSelectedItem(padtype_flag_to_string[selected_pad_data.type])
end
end
if form.preselect_pos then
for i, pad in ipairs(pad_list) do
if vector.equals(pad.pos, form.preselect_pos) then
listbox:setSelected(i)
update_fields_for_index(i)
break
end
end
end
listbox:onClick(function()
local index = listbox:getSelected()
update_fields_for_index(index)
end)
form.state:get("save_button"):onClick(function()
if not selected_pad_data then
notify.warn(form.playername, "Bitte wähle zuerst ein TPAD aus der Liste aus.")
return
end
local new_name = padname_field:getText()
local new_type_str
local value_from_dropdown = padtype_dropdown:getSelectedItem()
if value_from_dropdown then
local index = tonumber(value_from_dropdown)
if index then
new_type_str = padtype_dropdown:getItem(index)
else
new_type_str = value_from_dropdown
end
end
if not new_type_str or new_type_str == "" then
notify.err(form.playername, "Konnte TPAD-Typ nicht lesen. Bitte erneut versuchen.")
return
end
if not minetest.get_player_privs(form.playername).tpad_admin then
if new_type_str == GLOBAL_PAD_STRING and tpad.max_global_pads_reached(selected_pad_data.owner) then
notify.warn(form.playername, "Der Besitzer des TPADs hat das Limit für globale TPADs erreicht.")
return
end
end
tpad.set_pad_data(selected_pad_data.pos, new_name, new_type_str)
local meta = minetest.get_meta(selected_pad_data.pos)
if new_name and new_name ~= "" then
meta:set_string("infotext", "TPAD Station " .. new_name)
else
meta:set_string("infotext", "Unbenannte TPAD Station")
end
notify(form.playername, "TPAD '" .. new_name .. "' gespeichert.")
minetest.after(0, function()
form.preselect_pos = nil
submit.management(form)
end)
end)
form.state:get("delete_button"):onClick(function()
if not selected_pad_data then
notify.warn(form.playername, "Bitte wähle zuerst ein TPAD aus der Liste aus.")
return
end
if vector.equals(selected_pad_data.pos, form.clicked_pos) then
notify.warn(form.playername, "Du kannst das TPAD, an dem du stehst, nicht über dieses Menü löschen.")
return
end
tpad.pending_deletion[form.playername] = {
pad_data = selected_pad_data,
original_form_context = form,
}
minetest.close_formspec(form.playername, form.formname)
minetest.after(0.1, function()
tpad.forms.confirm_pad_deletion:show(form.playername)
end)
end)
form.state:get("back_button"):onClick(function()
minetest.after(0, function()
local clean_form_context = {
playername = form.playername,
clicker = form.clicker,
ownername = form.ownername,
clicked_pos = form.clicked_pos,
node = form.node,
page = 1
}
tpad.show_network_view(clean_form_context, form.network_type)
end)
end)
-- NEU: Logik für den Admin-Teleport-Button
if is_admin then
local admin_teleport_button = form.state:get("admin_teleport_button")
admin_teleport_button:setVisible(true)
admin_teleport_button:onClick(function()
if not selected_pad_data then
notify.warn(form.playername, "Bitte zuerst ein TPAD aus der Liste auswählen, um dorthin zu teleportieren.")
return
end
local dest_pad = selected_pad_data
-- Erstelle den Kontext für die Ansicht, die nach dem Teleport angezeigt werden soll
local new_context_after_teleport = {
playername = form.playername,
clicker = form.clicker,
ownername = dest_pad.owner,
clicked_pos = dest_pad.pos,
node = minetest.get_node(dest_pad.pos),
page = 1,
network_type = (dest_pad.type == GLOBAL_PAD) and "global" or "local"
}
tpad.do_teleport(form.clicker, dest_pad.pos, dest_pad.name, new_context_after_teleport)
end)
end
end
function submit.admin_settings(form)
form.state = tpad.forms.admin:show(form.playername)
form.formname = "tpad_admin_settings"
local max_total_field, max_global_field = form.state:get("max_total_field"), form.state:get("max_global_field")
max_total_field:setText(tpad.get_max_total_pads())
max_global_field:setText(tpad.get_max_global_pads())
form.state:get("save_button"):onClick(function()
tpad.set_max_total_pads(tonumber(max_total_field:getText()))
tpad.set_max_global_pads(tonumber(max_global_field:getText()))
minetest.close_formspec(form.playername, form.formname)
end)
end
-- ========================================================================
-- Main Node Callbacks
-- ========================================================================
function tpad.on_rightclick(clicked_pos, node, clicker)
local playername = clicker:get_player_name()
local pad = tpad.get_pad_data(clicked_pos)
if not pad or not pad.owner then
notify.err(playername, "Fehler! Fehlende oder korrupte TPAD-Daten. Bitte neu platzieren.")
return
end
-- Zugriffsschutz-Prüfung zuerst
if pad.type == PRIVATE_PAD and pad.owner ~= playername and not minetest.get_player_privs(playername).tpad_admin then
notify.warn(playername, "Dieses TPAD ist privat.")
return
end
-- Erstelle das Kontext-Objekt für alle Fälle
local form = {
playername = playername,
clicker = clicker,
ownername = pad.owner,
clicked_pos = clicked_pos,
node = node,
page = 1,
}
-- NEU: Prüfe, ob das Pad neu ist (d.h. keinen Namen hat)
if pad.name == "" then
-- Setze einen Parameter zur Vorauswahl für das Verwaltungs-Menü
form.preselect_pos = clicked_pos
-- Rufe direkt die Verwaltung auf
submit.management(form)
else
-- BESTEHENDE LOGIK: Wenn das Pad bereits konfiguriert ist
if pad.type == GLOBAL_PAD then
tpad.show_network_view(form, "global")
else
tpad.show_network_view(form, "local")
end
end
end
function tpad.can_dig(pos, player)
local meta = minetest.get_meta(pos)
local ownername = meta:get_string("owner")
local playername = player:get_player_name()
if ownername == "" or ownername == playername or minetest.get_player_privs(playername).tpad_admin then
return true
end
notify.warn(playername, "Dieses TPAD gehört dir nicht.")
return false
end
function tpad.on_destruct(pos)
local meta = minetest.get_meta(pos)
local ownername = meta:get_string("owner")
if ownername and ownername ~= "" then
tpad.del_pad(ownername, pos)
end
end
-- ========================================================================
-- FORMS (Rebuilt)
-- ========================================================================
tpad.forms = {}
local function create_network_view_form(state)
state:size(16, 11)
state:label(0.5, 0.2, "title_label", "")
local bottom_y = 10.4
state:button(0.5, bottom_y, 3.0, 0, "toggle_network_button", "")
state:button(3.6, bottom_y, 2.3, 0, "management_button", "Verwaltung")
local close_button = state:button(14.0, bottom_y, 1.5, 0, "close_button", "Schließen")
close_button:setClose(true)
state:button(6.0, bottom_y, 1.0, 0, "prev_button", "[<<]")
state:label(7.1, bottom_y, "page_label", "")
state:button(9.5, bottom_y, 1.0, 0, "next_button", "[>>]")
local max_rows, num_columns = 10, 3
local start_x, start_y = 0.5, 1.0
local button_width, button_height = 4.8, 0.8
local column_width = 5.0
for i = 1, max_rows * num_columns do
local column = math.floor((i - 1) / max_rows)
local row = (i - 1) % max_rows
local current_x = start_x + (column * column_width)
local current_y = start_y + (row * button_height)
state:button(current_x, current_y, button_width, 0, "tpad_btn_" .. i, ""):setVisible(false)
end
end
tpad.forms.teleport_success = smartfs.create("tpad.forms.teleport_success", function(state)
state:size(8, 2)
local destination_name = state.param.destination_name or "???"
state:label(0.5, 0.5, "success_label", "Teleport erfolgreich: " .. YELLOW_ESCAPE .. destination_name)
-- Dieser Button hat setClose(true), was das Fenster zuverlässig schließt.
local close_button = state:button(3, 1.2, 2, 0, "close_button", "Schließen")
close_button:setClose(true)
end)
tpad.forms.network_view = smartfs.create("tpad.forms.network_view", create_network_view_form)
tpad.forms.network_view_admin = smartfs.create("tpad.forms.network_view_admin", function(state)
create_network_view_form(state)
state:button(12.4, 10.4, 1.5, 0, "admin_button", "Admin")
end)
tpad.forms.management = smartfs.create("tpad.forms.management", function(state)
state:size(12, 9)
state:label(0.2, 0.2, "management_title", "TPAD Verwaltung")
state:listbox(0.2, 0.6, 11.6, 5, "pads_listbox", {})
state:field(0.5, 6.6, 6, 0, "padname_field", "Name", "")
local padtype_dropdown = state:dropdown(0.25, 6.7, 6.25, 0, "padtype_dropdown")
padtype_dropdown:addItem(PRIVATE_PAD_STRING)
padtype_dropdown:addItem(PUBLIC_PAD_STRING)
padtype_dropdown:addItem(GLOBAL_PAD_STRING)
state:button(7, 6.3, 2, 0, "save_button", "Speichern")
state:button(7, 7.1, 2, 0, "delete_button", "Löschen")
state:button(0.2, 8.4, 2, 0, "back_button", "Zurück")
-- NEU: Teleport-Button für Admins, standardmäßig unsichtbar
state:button(2.3, 8.4, 3, 0, "admin_teleport_button", "Teleport (Admin)"):setVisible(false)
local close_button = state:button(9.8, 8.4, 2, 0, "close_button", "Schließen")
close_button:setClose(true)
end)
-- This form now defines its OWN behavior, making it independent and robust.
tpad.forms.confirm_pad_deletion = smartfs.create("tpad.forms.confirm_pad_deletion", function(state)
state:size(8, 2.5)
state:label(0, 0, "intro_label", "Willst du das TPAD wirklich löschen?")
state:label(0, 0.5, "padname_label", "")
state:label(0, 1, "outro_label", "(es lässt sich nicht wiederherstellen)")
local confirm_button = state:button(0, 2.2, 2, 0, "confirm_button", "Ja, löschen")
local deny_button = state:button(6, 2.2, 2, 0, "deny_button", "Nein, abbrechen")
local playername = state.location.player
local pending_data = tpad.pending_deletion[playername]
if not pending_data then
state:close()
return
end
local pad_to_delete = pending_data.pad_data
local original_form_context = pending_data.original_form_context
local pad_display_name = pad_to_delete.name .. " (" .. short_padtype_string[pad_to_delete.type] .. ")"
state:get("padname_label"):setText(YELLOW_ESCAPE .. pad_display_name)
-- Diese Funktion erzwingt einen sauberen Neuaufbau der Verwaltungs-Ansicht
local function return_to_management_with_fresh_state()
tpad.pending_deletion[playername] = nil -- Temporäre Daten löschen
minetest.after(0, function()
-- Erstelle einen sauberen Kontext, anstatt den alten wiederzuverwenden
local fresh_context = {
playername = original_form_context.playername,
clicker = original_form_context.clicker,
ownername = original_form_context.ownername,
clicked_pos = original_form_context.clicked_pos,
node = minetest.get_node(original_form_context.clicked_pos), -- Node neu holen, falls sich was geändert hat
network_type = original_form_context.network_type,
page = 1
}
submit.management(fresh_context)
end)
end
confirm_button:onClick(function()
tpad.del_pad(pad_to_delete.owner, pad_to_delete.pos)
minetest.remove_node(pad_to_delete.pos)
notify(playername, "TPAD '" .. pad_to_delete.name .. "' gelöscht.")
return_to_management_with_fresh_state()
end)
deny_button:onClick(return_to_management_with_fresh_state)
end)
tpad.forms.admin = smartfs.create("tpad.forms.admin", function(state)
state:size(8, 8)
state:label(0.2, 0.2, "admin_label", "TPAD Einstellungen")
state:field(0.5, 2, 6, 0, "max_total_field", "Max. Gesamtzahl an TPADs (pro Spieler)")
state:field(0.5, 3.5, 6, 0, "max_global_field", "Max. globale TPADs (pro Spieler)")
state:button(6.5, 0.7, 1.5, 0, "save_button", "Speichern")
local close_button = state:button(6.5, 7, 1.5, 0, "close_button", "Schließen")
close_button:setClose(true)
end)
-- ========================================================================
-- Data Helper Functions
-- ========================================================================
function tpad.decorate_pad_data(pos, pad, ownername)
pad = table.copy(pad)
if type(pos) == "string" then
pad.strpos = pos
pad.pos = minetest.string_to_pos(pos)
else
pad.pos = pos
pad.strpos = minetest.pos_to_string(pos)
end
pad.owner = ownername
pad.name = pad.name or ""
pad.type = pad.type or PUBLIC_PAD
-- NEUE LOGIK: Füge den Suffix nur bei privaten Pads hinzu.
if pad.type == PRIVATE_PAD then
pad.local_fullname = pad.name .. " (privat)"
else
-- Bei Public Pads wird kein Suffix mehr angehängt.
pad.local_fullname = pad.name
end
pad.global_fullname = pad.name
return pad
end
function tpad.get_pad_data(pos)
local meta = minetest.get_meta(pos)
local ownername = meta:get_string("owner")
if not ownername or ownername == "" then return end
local pads = tpad._get_stored_pads(ownername)
local strpos = minetest.pos_to_string(pos)
local pad = pads[strpos]
if not pad then return end
return tpad.decorate_pad_data(pos, pad, ownername)
end
function tpad.set_pad_data(pos, padname, padtype_str)
local meta = minetest.get_meta(pos)
local ownername = meta:get_string("owner")
local pads = tpad._get_stored_pads(ownername)
local strpos = minetest.pos_to_string(pos)
local pad = pads[strpos] or {}
pad.name = padname
pad.type = padtype_string_to_flag[padtype_str]
pads[strpos] = pad
tpad._set_stored_pads(ownername, pads)
end
function tpad.del_pad(ownername, pos)
local pads = tpad._get_stored_pads(ownername)
pads[minetest.pos_to_string(pos)] = nil
tpad._set_stored_pads(ownername, pads)
end
-- ========================================================================
-- Register Node and Bind Callbacks
-- ========================================================================
local collision_box = {
type = "fixed",
fixed = { -0.5, -0.5, -0.5, 0.5, -0.3, 0.5 },
}
minetest.register_node(tpad.nodename, {
drawtype = "mesh",
tiles = { tpad.texture },
mesh = tpad.mesh,
paramtype = "light",
paramtype2 = "facedir",
on_place = minetest.rotate_and_place,
after_place_node = tpad.after_place_node,
collision_box = collision_box,
selection_box = collision_box,
description = "Teleporter Pad",
groups = {choppy = 2, dig_immediate = 2},
on_rightclick = tpad.on_rightclick,
can_dig = tpad.can_dig,
on_destruct = tpad.on_destruct,
})
minetest.register_chatcommand("tpad", {func = tpad.command})

938
init.lua.bak.5 Normal file
View file

@ -0,0 +1,938 @@
-- ========================================================================
-- TPAD MOD v1.2 (Final, Reworked Delete Logic)
-- ========================================================================
tpad = {}
tpad.version = "1.2" -- As requested, version is not incremented
tpad.mod_name = minetest.get_current_modname()
tpad.texture = "tpad-texture.png"
tpad.mesh = "tpad-mesh.obj"
tpad.nodename = "tpad:tpad"
tpad.mod_path = minetest.get_modpath(tpad.mod_name)
tpad.sound_teleport = tpad.mod_name .. "_teleport"
tpad.particle_texture = tpad.mod_name .. "_particle.png"
-- Temporäre Variable zur sicheren Datenübergabe an den Bestätigungsdialog
tpad.pending_deletion = {}
-- ========================================================================
-- Constants
-- ========================================================================
local PRIVATE_PAD_STRING = "Privat (nur Besitzer)"
local PUBLIC_PAD_STRING = "Lokal (nur eigenes Netzwerk)"
local GLOBAL_PAD_STRING = "Global (beliebiges Netzwerk)"
local PRIVATE_PAD = 1
local PUBLIC_PAD = 2
local GLOBAL_PAD = 4
local RED_ESCAPE = minetest.get_color_escape_sequence("#FF0000")
local YELLOW_ESCAPE = minetest.get_color_escape_sequence("#FFFF00")
local CYAN_ESCAPE = minetest.get_color_escape_sequence("#00FFFF")
local WHITE_ESCAPE = minetest.get_color_escape_sequence("#FFFFFF")
local OWNER_ESCAPE_COLOR = CYAN_ESCAPE
local padtype_flag_to_string = {
[PRIVATE_PAD] = PRIVATE_PAD_STRING,
[PUBLIC_PAD] = PUBLIC_PAD_STRING,
[GLOBAL_PAD] = GLOBAL_PAD_STRING,
}
local padtype_string_to_flag = {
[PRIVATE_PAD_STRING] = PRIVATE_PAD,
[PUBLIC_PAD_STRING] = PUBLIC_PAD,
[GLOBAL_PAD_STRING] = GLOBAL_PAD,
}
local short_padtype_string = {
[PRIVATE_PAD] = "private",
[PUBLIC_PAD] = "public",
[GLOBAL_PAD] = "global",
}
-- ========================================================================
-- Dependencies and Libs
-- ========================================================================
local smartfs = dofile(tpad.mod_path .. "/lib/smartfs.lua")
local notify = dofile(tpad.mod_path .. "/notify.lua")
local waypoint_hud_ids = {}
minetest.register_privilege("tpad_admin", {
description = "Can edit and destroy any tpad",
give_to_singleplayer = true,
})
-- ========================================================================
-- Original Helper Functions
-- ========================================================================
local function copy_file(source, dest)
local src_file = io.open(source, "rb")
if not src_file then return false, "copy_file() unable to open source for reading" end
local src_data = src_file:read("*all")
src_file:close()
local dest_file = io.open(dest, "wb")
if not dest_file then return false, "copy_file() unable to open dest for writing" end
dest_file:write(src_data)
dest_file:close()
return true, "files copied successfully"
end
local function custom_or_default(modname, path, filename)
local default_filename = "default/" .. filename
local full_filename = path .. "/custom." .. filename
local full_default_filename = path .. "/" .. default_filename
local file_exists_at_path = io.open(path .. "/" .. filename, "r")
if file_exists_at_path then
file_exists_at_path:close()
os.rename(path .. "/" .. filename, full_filename)
end
local file = io.open(full_filename, "rb")
if not file then
minetest.debug("[" .. modname .. "] Copying " .. default_filename .. " to " .. filename .. " (path: " .. path .. ")")
local success, err = copy_file(full_default_filename, full_filename)
if not success then
minetest.debug("[" .. modname .. "] " .. err)
return false
end
file = io.open(full_filename, "rb")
if not file then
minetest.debug("[" .. modname .. "] Unable to load " .. filename .. " file from path " .. path)
return false
end
end
file:close()
return full_filename
end
dofile(tpad.mod_path .. "/storage.lua")
-- ========================================================================
-- Load Custom Recipe
-- ========================================================================
local recipes_filename = custom_or_default(tpad.mod_name, tpad.mod_path, "recipes.lua")
if recipes_filename then
local recipes = dofile(recipes_filename)
if type(recipes) == "table" and recipes[tpad.nodename] then
minetest.register_craft({
output = tpad.nodename,
recipe = recipes[tpad.nodename],
})
end
end
-- ========================================================================
-- Chat Command
-- ========================================================================
function tpad.command(playername, param)
tpad.hud_off(playername)
if(param == "off") then return end
local player = minetest.get_player_by_name(playername)
local pads = tpad._get_stored_pads(playername)
local shortest_distance = nil
local closest_pad = nil
local playerpos = player:getpos()
for strpos, pad in pairs(pads) do
local pos = minetest.string_to_pos(strpos)
local distance = vector.distance(pos, playerpos)
if not shortest_distance or distance < shortest_distance then
closest_pad = {
pos = pos,
name = pad.name .. " " .. strpos,
}
shortest_distance = distance
end
end
if closest_pad then
waypoint_hud_ids[playername] = player:hud_add({
hud_elem_type = "waypoint",
name = closest_pad.name,
world_pos = closest_pad.pos,
number = 0xFF0000,
})
notify(playername, "Waypoint to " .. closest_pad.name .. " displayed")
end
end
function tpad.hud_off(playername)
local player = minetest.get_player_by_name(playername)
local hud_id = waypoint_hud_ids[playername]
if hud_id then
player:hud_remove(hud_id)
end
end
-- ========================================================================
-- Teleport Logic
-- ========================================================================
function tpad.do_teleport(player, destination_pos, destination_name, form_context)
local playername = player:get_player_name()
minetest.after(0.1, function()
if not player or not player:is_player() then return end
local start_pos = player:getpos()
minetest.sound_play(tpad.sound_teleport, {pos = start_pos, max_hear_distance = 10, gain = 1.0})
minetest.add_particlespawner({
amount = 60, time = 0.5,
minpos = vector.add(start_pos, -0.5), maxpos = vector.add(start_pos, 0.5),
minvel = {x=-1, y=0, z=-1}, maxvel = {x=1, y=2, z=1},
minacc = {x=0, y=0, z=0}, maxacc = {x=0, y=0, z=0},
minexptime = 0.5, maxexptime = 2,
minsize = 1, maxsize = 3,
texture = tpad.particle_texture,
})
player:move_to(destination_pos)
minetest.sound_play(tpad.sound_teleport, {pos = destination_pos, max_hear_distance = 10, gain = 1.0})
minetest.add_particlespawner({
amount = 60, time = 0.5,
minpos = vector.add(destination_pos, {x = -0.5, y = 0, z = -0.5}),
maxpos = vector.add(destination_pos, {x = 0.5, y = 1, z = 0.5}),
minvel = {x=-1, y=1, z=-1}, maxvel = {x=1, y=2, z=1},
minacc = {x=0, y=0, z=0}, maxacc = {x=0, y=0, z=0},
minexptime = 0.5, maxexptime = 2,
minsize = 1, maxsize = 3,
texture = tpad.particle_texture,
})
tpad.hud_off(playername)
-- NEU: Nach Ankunft am Ziel die Ansicht mit neuem Kontext aktualisieren
minetest.after(0.2, function()
if not player or not player:is_player() then return end
local dest_pad_data = tpad.get_pad_data(destination_pos)
if not dest_pad_data then return end
local new_context = {
playername = playername,
clicker = player,
ownername = dest_pad_data.owner,
clicked_pos = destination_pos,
node = minetest.get_node(destination_pos),
page = 1,
-- Behalte den Netzwerk-Typ (lokal/global) bei
network_type = form_context.network_type
}
tpad.show_network_view(new_context, new_context.network_type)
end)
end)
end
-- ========================================================================
-- Node Placement and Data Helpers
-- ========================================================================
function tpad.after_place_node(pos, placer, itemstack)
local playername = placer:get_player_name()
if tpad.max_total_pads_reached(placer) then
notify.warn(playername, "Du kannst keine weiteren TPADs erstellen. Limit erreicht.")
minetest.remove_node(pos)
minetest.add_item(placer:get_pos(), itemstack:get_name())
return
end
local meta = minetest.get_meta(pos)
meta:set_string("owner", playername)
meta:set_string("infotext", "TPAD Station von " .. playername)
tpad.set_pad_data(pos, "", PRIVATE_PAD_STRING)
end
local submit = {}
function tpad.max_total_pads_reached(placer)
local placername = placer:get_player_name()
if minetest.get_player_privs(placername).tpad_admin then
return false
end
local pads = tpad._get_stored_pads(placername)
local count = 0
for _ in pairs(pads) do
count = count + 1
end
return count >= tpad.get_max_total_pads()
end
function tpad.max_global_pads_reached(playername)
if minetest.get_player_privs(playername).tpad_admin then
return false
end
local pads = tpad._get_stored_pads(playername)
local count = 0
for _, pad in pairs(pads) do
if pad.type == GLOBAL_PAD then
count = count + 1
end
end
return count >= tpad.get_max_global_pads()
end
-- ========================================================================
-- GUI DATA HELPERS
-- ========================================================================
function submit.global_helper()
local allpads = tpad._get_all_pads()
local result = {}
for ownername, pads in pairs(allpads) do
for strpos, pad in pairs(pads) do
if pad.type == GLOBAL_PAD then
-- Der 'viewername' ist hier nicht bekannt, also wird 'nil' übergeben, was die Funktion korrekt behandelt.
table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername, nil))
end
end
end
table.sort(result, function(a, b) return a.global_fullname:lower() < b.global_fullname:lower() end)
return result
end
function submit.local_helper(ownername, viewername)
local result = {}
local added_pads = {}
local owner_pads = tpad._get_stored_pads(ownername)
for strpos, pad in pairs(owner_pads) do
local is_viewer_the_owner = (ownername == viewername)
if (pad.type == PUBLIC_PAD) or (is_viewer_the_owner and pad.type == PRIVATE_PAD) then
if pad.type ~= GLOBAL_PAD then
table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername, viewername))
added_pads[strpos] = true
end
end
end
if ownername ~= viewername then
local viewer_pads = tpad._get_stored_pads(viewername)
for strpos, pad in pairs(viewer_pads) do
if (pad.type == PUBLIC_PAD or pad.type == PRIVATE_PAD) and not added_pads[strpos] then
table.insert(result, tpad.decorate_pad_data(strpos, pad, viewername, viewername))
added_pads[strpos] = true
end
end
end
table.sort(result, function(a, b) return a.local_fullname:lower() < b.local_fullname:lower() end)
return result
end
function submit.management_helper(playername)
local is_admin = minetest.get_player_privs(playername).tpad_admin
local result = {}
if is_admin then
local allpads = tpad._get_all_pads()
for ownername, pads in pairs(allpads) do
for strpos, pad in pairs(pads) do
table.insert(result, tpad.decorate_pad_data(strpos, pad, ownername, playername))
end
end
else
local pads = tpad._get_stored_pads(playername)
for strpos, pad in pairs(pads) do
table.insert(result, tpad.decorate_pad_data(strpos, pad, playername, playername))
end
end
table.sort(result, function(a, b) return a.local_fullname:lower() < b.local_fullname:lower() end)
return result
end
-- ========================================================================
-- GUI LOGIC (REFACTORED)
-- ========================================================================
function tpad.show_network_view(form, network_type)
local is_admin = minetest.get_player_privs(form.playername).tpad_admin
local form_key = is_admin and "network_view_admin" or "network_view"
form.state = tpad.forms[form_key]:show(form.playername)
form.formname = "tpad.forms." .. form_key
local pad_list
local title_text
local function create_clean_context()
return {
playername = form.playername,
clicker = form.clicker,
ownername = form.ownername,
clicked_pos = form.clicked_pos,
node = form.node,
page = form.page or 1
}
end
local current_pad_data = tpad.get_pad_data(form.clicked_pos)
local current_pad_name = current_pad_data.name or "Unnamed"
local current_pad_type_str
if current_pad_data.type == GLOBAL_PAD then
current_pad_type_str = "GLOBALE"
else
current_pad_type_str = "LOKALE"
end
local RED_ESCAPE = minetest.get_color_escape_sequence("#FF0000") or ""
title_text = RED_ESCAPE .. current_pad_type_str .. " " .. WHITE_ESCAPE .. "TPAD-Station " .. YELLOW_ESCAPE .. current_pad_name .. WHITE_ESCAPE .. ". Wähle ein Ziel:"
if network_type == "global" then
form.network_type = "global"
pad_list = submit.global_helper()
form.state:get("toggle_network_button"):setText("Lokales Netzwerk")
form.state:get("toggle_network_button"):onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
-- KORREKTUR: Der 'ownername' wird nicht mehr überschrieben.
-- Er bleibt der des ursprünglichen Pads, was korrekt ist.
clean_form.page = 1
tpad.show_network_view(clean_form, "local")
end)
end)
else -- "local"
form.network_type = "local"
-- KORREKTUR: Übergebe den Spielernamen (string), nicht das Ergebnis eines Vergleichs (boolean).
pad_list = submit.local_helper(form.ownername, form.playername)
form.state:get("toggle_network_button"):setText("Globales Netzwerk")
form.state:get("toggle_network_button"):onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
clean_form.page = 1
tpad.show_network_view(clean_form, "global")
end)
end)
end
form.state:get("title_label"):setText(title_text)
form.state:get("management_button"):onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
clean_form.preselect_pos = clean_form.clicked_pos
submit.management(clean_form)
end)
end)
if is_admin then
form.state:get("admin_button"):onClick(function()
minetest.after(0, function() submit.admin_settings(form) end)
end)
end
local max_rows, num_columns = 10, 3
local buttons_per_page = max_rows * num_columns
local destination_pads = {}
for _, pad in ipairs(pad_list) do
if not vector.equals(pad.pos, form.clicked_pos) then
table.insert(destination_pads, pad)
end
end
local total_pages = math.ceil(#destination_pads / buttons_per_page)
if total_pages == 0 then total_pages = 1 end
local current_page = form.page or 1
if current_page > total_pages then current_page = total_pages end
if current_page < 1 then current_page = 1 end
form.page = current_page
for i = 1, buttons_per_page do
local index = ((current_page - 1) * buttons_per_page) + i
local pad_data = destination_pads[index]
local button_name = "tpad_btn_" .. i
local button = form.state:get(button_name)
if button then
if pad_data then
local display_name = (network_type == "global") and pad_data.global_fullname or pad_data.local_fullname
button:setText(display_name)
button:setVisible(true)
button:onClick(function()
tpad.do_teleport(form.clicker, pad_data.pos, display_name, form)
end)
else
button:setVisible(false)
end
end
end
if total_pages > 1 then
form.state:get("page_label"):setText("Seite " .. current_page .. " von " .. total_pages)
local prev_button, next_button = form.state:get("prev_button"), form.state:get("next_button")
prev_button:setVisible(current_page > 1)
next_button:setVisible(current_page < total_pages)
prev_button:onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
clean_form.page = current_page - 1
tpad.show_network_view(clean_form, network_type)
end)
end)
next_button:onClick(function()
minetest.after(0, function()
local clean_form = create_clean_context()
clean_form.page = current_page + 1
tpad.show_network_view(clean_form, network_type)
end)
end)
else
form.state:get("page_label"):setText("")
form.state:get("prev_button"):setVisible(false)
form.state:get("next_button"):setVisible(false)
end
end
function submit.management(form)
form.formname = "tpad_management"
form.state = tpad.forms.management:show(form.playername)
local pad_list = submit.management_helper(form.playername)
local listbox = form.state:get("pads_listbox")
local is_admin = minetest.get_player_privs(form.playername).tpad_admin
local selected_pad_data = nil
listbox:clearItems()
for _, pad in ipairs(pad_list) do
local label = pad.name .. " (" .. short_padtype_string[pad.type] .. ")"
if is_admin then
label = label .. " [" .. pad.owner .. "]"
end
listbox:addItem(label)
end
local padname_field = form.state:get("padname_field")
local padtype_dropdown = form.state:get("padtype_dropdown")
local function update_fields_for_index(index)
if not index or index <= 0 then return end
selected_pad_data = pad_list[index]
if selected_pad_data then
padname_field:setText(selected_pad_data.name)
padtype_dropdown:setSelectedItem(padtype_flag_to_string[selected_pad_data.type])
end
end
if form.preselect_pos then
for i, pad in ipairs(pad_list) do
if vector.equals(pad.pos, form.preselect_pos) then
listbox:setSelected(i)
update_fields_for_index(i)
break
end
end
end
listbox:onClick(function()
local index = listbox:getSelected()
update_fields_for_index(index)
end)
form.state:get("save_button"):onClick(function()
if not selected_pad_data then
notify.warn(form.playername, "Bitte wähle zuerst ein TPAD aus der Liste aus.")
return
end
local new_name = padname_field:getText()
local new_type_str
local value_from_dropdown = padtype_dropdown:getSelectedItem()
if value_from_dropdown then
local index = tonumber(value_from_dropdown)
if index then
new_type_str = padtype_dropdown:getItem(index)
else
new_type_str = value_from_dropdown
end
end
if not new_type_str or new_type_str == "" then
notify.err(form.playername, "Konnte TPAD-Typ nicht lesen. Bitte erneut versuchen.")
return
end
if not minetest.get_player_privs(form.playername).tpad_admin then
if new_type_str == GLOBAL_PAD_STRING and tpad.max_global_pads_reached(selected_pad_data.owner) then
notify.warn(form.playername, "Der Besitzer des TPADs hat das Limit für globale TPADs erreicht.")
return
end
end
tpad.set_pad_data(selected_pad_data.pos, new_name, new_type_str)
local meta = minetest.get_meta(selected_pad_data.pos)
if new_name and new_name ~= "" then
meta:set_string("infotext", "TPAD Station " .. new_name)
else
meta:set_string("infotext", "Unbenannte TPAD Station")
end
notify(form.playername, "TPAD '" .. new_name .. "' gespeichert.")
minetest.after(0, function()
form.preselect_pos = nil
submit.management(form)
end)
end)
form.state:get("delete_button"):onClick(function()
if not selected_pad_data then
notify.warn(form.playername, "Bitte wähle zuerst ein TPAD aus der Liste aus.")
return
end
if vector.equals(selected_pad_data.pos, form.clicked_pos) then
notify.warn(form.playername, "Du kannst das TPAD, an dem du stehst, nicht über dieses Menü löschen.")
return
end
tpad.pending_deletion[form.playername] = {
pad_data = selected_pad_data,
original_form_context = form,
}
minetest.close_formspec(form.playername, form.formname)
minetest.after(0.1, function()
tpad.forms.confirm_pad_deletion:show(form.playername)
end)
end)
form.state:get("back_button"):onClick(function()
minetest.after(0, function()
local clean_form_context = {
playername = form.playername,
clicker = form.clicker,
ownername = form.ownername,
clicked_pos = form.clicked_pos,
node = form.node,
page = 1
}
tpad.show_network_view(clean_form_context, form.network_type)
end)
end)
-- NEU: Logik für den Admin-Teleport-Button
if is_admin then
local admin_teleport_button = form.state:get("admin_teleport_button")
admin_teleport_button:setVisible(true)
admin_teleport_button:onClick(function()
if not selected_pad_data then
notify.warn(form.playername, "Bitte zuerst ein TPAD aus der Liste auswählen, um dorthin zu teleportieren.")
return
end
local dest_pad = selected_pad_data
-- Erstelle den Kontext für die Ansicht, die nach dem Teleport angezeigt werden soll
local new_context_after_teleport = {
playername = form.playername,
clicker = form.clicker,
ownername = dest_pad.owner,
clicked_pos = dest_pad.pos,
node = minetest.get_node(dest_pad.pos),
page = 1,
network_type = (dest_pad.type == GLOBAL_PAD) and "global" or "local"
}
tpad.do_teleport(form.clicker, dest_pad.pos, dest_pad.name, new_context_after_teleport)
end)
end
end
function submit.admin_settings(form)
form.state = tpad.forms.admin:show(form.playername)
form.formname = "tpad_admin_settings"
local max_total_field, max_global_field = form.state:get("max_total_field"), form.state:get("max_global_field")
max_total_field:setText(tpad.get_max_total_pads())
max_global_field:setText(tpad.get_max_global_pads())
form.state:get("save_button"):onClick(function()
tpad.set_max_total_pads(tonumber(max_total_field:getText()))
tpad.set_max_global_pads(tonumber(max_global_field:getText()))
minetest.close_formspec(form.playername, form.formname)
end)
end
-- ========================================================================
-- Main Node Callbacks
-- ========================================================================
function tpad.on_rightclick(clicked_pos, node, clicker)
local playername = clicker:get_player_name()
local pad = tpad.get_pad_data(clicked_pos)
if not pad or not pad.owner then
notify.err(playername, "Fehler! Fehlende oder korrupte TPAD-Daten. Bitte neu platzieren.")
return
end
-- Zugriffsschutz-Prüfung zuerst
if pad.type == PRIVATE_PAD and pad.owner ~= playername and not minetest.get_player_privs(playername).tpad_admin then
notify.warn(playername, "Dieses TPAD ist privat.")
return
end
-- Erstelle das Kontext-Objekt für alle Fälle
local form = {
playername = playername,
clicker = clicker,
ownername = pad.owner,
clicked_pos = clicked_pos,
node = node,
page = 1,
}
-- NEU: Prüfe, ob das Pad neu ist (d.h. keinen Namen hat)
if pad.name == "" then
-- Setze einen Parameter zur Vorauswahl für das Verwaltungs-Menü
form.preselect_pos = clicked_pos
-- Rufe direkt die Verwaltung auf
submit.management(form)
else
-- BESTEHENDE LOGIK: Wenn das Pad bereits konfiguriert ist
if pad.type == GLOBAL_PAD then
tpad.show_network_view(form, "global")
else
tpad.show_network_view(form, "local")
end
end
end
function tpad.can_dig(pos, player)
local meta = minetest.get_meta(pos)
local ownername = meta:get_string("owner")
local playername = player:get_player_name()
if ownername == "" or ownername == playername or minetest.get_player_privs(playername).tpad_admin then
return true
end
notify.warn(playername, "Dieses TPAD gehört dir nicht.")
return false
end
function tpad.on_destruct(pos)
local meta = minetest.get_meta(pos)
local ownername = meta:get_string("owner")
if ownername and ownername ~= "" then
tpad.del_pad(ownername, pos)
end
end
-- ========================================================================
-- FORMS (Rebuilt)
-- ========================================================================
tpad.forms = {}
local function create_network_view_form(state)
state:size(16, 11)
state:label(0.5, 0.2, "title_label", "")
local bottom_y = 10.4
state:button(0.5, bottom_y, 3.0, 0, "toggle_network_button", "")
state:button(3.6, bottom_y, 2.3, 0, "management_button", "Verwaltung")
local close_button = state:button(14.0, bottom_y, 1.5, 0, "close_button", "Schließen")
close_button:setClose(true)
state:button(6.0, bottom_y, 1.0, 0, "prev_button", "[<<]")
state:label(7.1, bottom_y, "page_label", "")
state:button(9.5, bottom_y, 1.0, 0, "next_button", "[>>]")
local max_rows, num_columns = 10, 3
local start_x, start_y = 0.5, 1.0
local button_width, button_height = 4.8, 0.8
local column_width = 5.0
for i = 1, max_rows * num_columns do
local column = math.floor((i - 1) / max_rows)
local row = (i - 1) % max_rows
local current_x = start_x + (column * column_width)
local current_y = start_y + (row * button_height)
state:button(current_x, current_y, button_width, 0, "tpad_btn_" .. i, ""):setVisible(false)
end
end
tpad.forms.teleport_success = smartfs.create("tpad.forms.teleport_success", function(state)
state:size(8, 2)
local destination_name = state.param.destination_name or "???"
state:label(0.5, 0.5, "success_label", "Teleport erfolgreich: " .. YELLOW_ESCAPE .. destination_name)
-- Dieser Button hat setClose(true), was das Fenster zuverlässig schließt.
local close_button = state:button(3, 1.2, 2, 0, "close_button", "Schließen")
close_button:setClose(true)
end)
tpad.forms.network_view = smartfs.create("tpad.forms.network_view", create_network_view_form)
tpad.forms.network_view_admin = smartfs.create("tpad.forms.network_view_admin", function(state)
create_network_view_form(state)
state:button(12.4, 10.4, 1.5, 0, "admin_button", "Admin")
end)
tpad.forms.management = smartfs.create("tpad.forms.management", function(state)
state:size(12, 9)
state:label(0.2, 0.2, "management_title", "TPAD Verwaltung")
state:listbox(0.2, 0.6, 11.6, 5, "pads_listbox", {})
state:field(0.5, 6.6, 6, 0, "padname_field", "Name", "")
local padtype_dropdown = state:dropdown(0.25, 6.7, 6.25, 0, "padtype_dropdown")
padtype_dropdown:addItem(PRIVATE_PAD_STRING)
padtype_dropdown:addItem(PUBLIC_PAD_STRING)
padtype_dropdown:addItem(GLOBAL_PAD_STRING)
state:button(7, 6.3, 2, 0, "save_button", "Speichern")
state:button(7, 7.1, 2, 0, "delete_button", "Löschen")
state:button(0.2, 8.4, 2, 0, "back_button", "Zurück")
-- NEU: Teleport-Button für Admins, standardmäßig unsichtbar
state:button(2.3, 8.4, 3, 0, "admin_teleport_button", "Teleport (Admin)"):setVisible(false)
local close_button = state:button(9.8, 8.4, 2, 0, "close_button", "Schließen")
close_button:setClose(true)
end)
-- This form now defines its OWN behavior, making it independent and robust.
tpad.forms.confirm_pad_deletion = smartfs.create("tpad.forms.confirm_pad_deletion", function(state)
state:size(8, 2.5)
state:label(0, 0, "intro_label", "Willst du das TPAD wirklich löschen?")
state:label(0, 0.5, "padname_label", "")
state:label(0, 1, "outro_label", "(es lässt sich nicht wiederherstellen)")
local confirm_button = state:button(0, 2.2, 2, 0, "confirm_button", "Ja, löschen")
local deny_button = state:button(6, 2.2, 2, 0, "deny_button", "Nein, abbrechen")
local playername = state.location.player
local pending_data = tpad.pending_deletion[playername]
if not pending_data then
state:close()
return
end
local pad_to_delete = pending_data.pad_data
local original_form_context = pending_data.original_form_context
local pad_display_name = pad_to_delete.name .. " (" .. short_padtype_string[pad_to_delete.type] .. ")"
state:get("padname_label"):setText(YELLOW_ESCAPE .. pad_display_name)
-- Diese Funktion erzwingt einen sauberen Neuaufbau der Verwaltungs-Ansicht
local function return_to_management_with_fresh_state()
tpad.pending_deletion[playername] = nil -- Temporäre Daten löschen
minetest.after(0, function()
-- Erstelle einen sauberen Kontext, anstatt den alten wiederzuverwenden
local fresh_context = {
playername = original_form_context.playername,
clicker = original_form_context.clicker,
ownername = original_form_context.ownername,
clicked_pos = original_form_context.clicked_pos,
node = minetest.get_node(original_form_context.clicked_pos), -- Node neu holen, falls sich was geändert hat
network_type = original_form_context.network_type,
page = 1
}
submit.management(fresh_context)
end)
end
confirm_button:onClick(function()
tpad.del_pad(pad_to_delete.owner, pad_to_delete.pos)
minetest.remove_node(pad_to_delete.pos)
notify(playername, "TPAD '" .. pad_to_delete.name .. "' gelöscht.")
return_to_management_with_fresh_state()
end)
deny_button:onClick(return_to_management_with_fresh_state)
end)
tpad.forms.admin = smartfs.create("tpad.forms.admin", function(state)
state:size(8, 8)
state:label(0.2, 0.2, "admin_label", "TPAD Einstellungen")
state:field(0.5, 2, 6, 0, "max_total_field", "Max. Gesamtzahl an TPADs (pro Spieler)")
state:field(0.5, 3.5, 6, 0, "max_global_field", "Max. globale TPADs (pro Spieler)")
state:button(6.5, 0.7, 1.5, 0, "save_button", "Speichern")
local close_button = state:button(6.5, 7, 1.5, 0, "close_button", "Schließen")
close_button:setClose(true)
end)
-- ========================================================================
-- Data Helper Functions
-- ========================================================================
function tpad.decorate_pad_data(pos, pad, ownername, viewername)
pad = table.copy(pad)
if type(pos) == "string" then
pad.strpos = pos
pad.pos = minetest.string_to_pos(pos)
else
pad.pos = pos
pad.strpos = minetest.pos_to_string(pos)
end
pad.owner = ownername
pad.name = pad.name or ""
pad.type = pad.type or PUBLIC_PAD
-- NEUE LOGIK: Erstellt den Anzeigenamen für das lokale Netzwerk
local is_own_pad = (viewername and ownername == viewername)
if is_own_pad then
if pad.type == PRIVATE_PAD then
pad.local_fullname = pad.name .. " (meins, privat)"
elseif pad.type == PUBLIC_PAD then
pad.local_fullname = pad.name .. " (meins)"
else
-- Fallback, falls es weitere Typen gäbe
pad.local_fullname = pad.name
end
else
-- Bisherige Logik für fremde Pads
if pad.type == PRIVATE_PAD then
pad.local_fullname = pad.name .. " (privat)"
else
pad.local_fullname = pad.name
end
end
pad.global_fullname = pad.name
return pad
end
function tpad.get_pad_data(pos)
local meta = minetest.get_meta(pos)
local ownername = meta:get_string("owner")
if not ownername or ownername == "" then return end
local pads = tpad._get_stored_pads(ownername)
local strpos = minetest.pos_to_string(pos)
local pad = pads[strpos]
if not pad then return end
return tpad.decorate_pad_data(pos, pad, ownername)
end
function tpad.set_pad_data(pos, padname, padtype_str)
local meta = minetest.get_meta(pos)
local ownername = meta:get_string("owner")
local pads = tpad._get_stored_pads(ownername)
local strpos = minetest.pos_to_string(pos)
local pad = pads[strpos] or {}
pad.name = padname
pad.type = padtype_string_to_flag[padtype_str]
pads[strpos] = pad
tpad._set_stored_pads(ownername, pads)
end
function tpad.del_pad(ownername, pos)
local pads = tpad._get_stored_pads(ownername)
pads[minetest.pos_to_string(pos)] = nil
tpad._set_stored_pads(ownername, pads)
end
-- ========================================================================
-- Register Node and Bind Callbacks
-- ========================================================================
local collision_box = {
type = "fixed",
fixed = { -0.5, -0.5, -0.5, 0.5, -0.3, 0.5 },
}
minetest.register_node(tpad.nodename, {
drawtype = "mesh",
tiles = { tpad.texture },
mesh = tpad.mesh,
paramtype = "light",
paramtype2 = "facedir",
on_place = minetest.rotate_and_place,
after_place_node = tpad.after_place_node,
collision_box = collision_box,
selection_box = collision_box,
description = "Teleporter Pad",
groups = {choppy = 2, dig_immediate = 2},
on_rightclick = tpad.on_rightclick,
can_dig = tpad.can_dig,
on_destruct = tpad.on_destruct,
})
minetest.register_chatcommand("tpad", {func = tpad.command})

1440
lib/smartfs.lua Normal file

File diff suppressed because it is too large Load diff

5
mod.conf Normal file
View file

@ -0,0 +1,5 @@
name = tpad
release = 28854
author = entuland
description = A simple but powerful pad to create teleporting networks
title = Teleporter Pads

60
models/tpad-mesh.obj Normal file
View file

@ -0,0 +1,60 @@
# Blender v2.79 (sub 0) OBJ File: 'tpad.blend'
# www.blender.org
mtllib tpad-mesh.mtl
o Cube
v -0.500000 -0.500000 -0.500000
v 0.500000 -0.500000 -0.500000
v 0.500000 -0.500000 0.500000
v -0.500000 -0.500000 0.500000
v -0.429289 -0.300000 -0.429289
v 0.429289 -0.300000 -0.429289
v 0.429289 -0.300000 0.429289
v -0.429289 -0.300000 0.429289
v -0.500000 -0.380000 -0.500000
v 0.500000 -0.380000 -0.499999
v -0.500000 -0.380000 0.500000
v 0.500000 -0.380000 0.500000
vt 0.078125 0.421875
vt 0.078125 0.000000
vt 0.484375 0.000000
vt 0.484375 0.421875
vt 0.093750 0.515625
vt 0.468750 0.515625
vt 0.468750 0.890625
vt 0.093750 0.890625
vt -0.000000 0.484375
vt 0.046875 0.484375
vt 0.046875 0.921875
vt -0.000000 0.921875
vt 0.078125 0.984375
vt 0.078125 0.937500
vt 0.484375 0.937500
vt 0.484375 0.984375
vt 0.562500 0.921875
vt 0.515625 0.921875
vt 0.515625 0.484375
vt 0.562500 0.484375
vt 0.078125 0.468750
vt 0.484375 0.468750
vn 0.0000 -1.0000 0.0000
vn 0.0000 1.0000 -0.0000
vn 0.0000 0.0000 -1.0000
vn 1.0000 -0.0000 0.0000
vn -0.0000 -0.0000 1.0000
vn -1.0000 0.0000 -0.0000
vn 0.0000 0.6623 -0.7493
vn -0.7493 0.6623 -0.0000
vn 0.7493 0.6623 0.0000
vn -0.0000 0.6623 0.7493
usemtl Material
s off
f 1/1/1 2/2/1 3/3/1 4/4/1
f 5/5/2 8/6/2 7/7/2 6/8/2
f 1/9/3 9/10/3 10/11/3 2/12/3
f 2/13/4 10/14/4 12/15/4 3/16/4
f 3/17/5 12/18/5 11/19/5 4/20/5
f 9/21/6 1/1/6 4/4/6 11/22/6
f 5/5/7 6/8/7 10/11/7 9/10/7
f 8/6/8 5/5/8 9/21/8 11/22/8
f 6/8/9 7/7/9 12/15/9 10/14/9
f 7/7/10 8/6/10 11/19/10 12/18/10

79
notify.lua Normal file
View file

@ -0,0 +1,79 @@
local mod_name = minetest.get_current_modname()
local huds = {}
local hud_timeout_seconds = 3
-- defaults
local position = { x = 0.1, y = 0.9}
local alignment = { x = 1, y = -1}
local normal_color = 0xFFFFFF
local warning_color = 0xFFFF00
local error_color = 0xDD0000
local direction = 0
local notify = {}
notify.__index = notify
setmetatable(notify, notify)
local function hud_remove(player, playername)
local hud = huds[playername]
if not hud then return end
if os.time() < hud_timeout_seconds + hud.time then
return
end
player:hud_remove(hud.id)
huds[playername] = nil
end
local function hud_create(player, message, params)
local playername = player:get_player_name()
local def = type(params) == "table" and params or {}
def.position = def.position or position
def.alignment = def.alignment or alignment
def.number = def.number or def.color or normal_color
def.color = nil
def.position = def.position or position
def.direction = def.direction or direction
def.text = message or def.text
def.hud_elem_type = def.hud_elem_type or "text"
def.name = mod_name .. "_feedback"
local id = player:hud_add(def)
huds[playername] = {
id = id,
time = os.time(),
}
end
notify.warn = function(player, message)
notify(player, message, {color = warning_color })
end
notify.warning = notify.warn
notify.err = function(player, message)
notify(player, message, {color = error_color })
end
notify.error = notify.err
notify.__call = function(self, player, message, params)
local playername
if type(player) == "string" then
playername = player
player = minetest.get_player_by_name(playername)
elseif player and player.get_player_name then
playername = player:get_player_name()
else
return
end
message = "[" .. mod_name .. "] " .. message
local hud = huds[playername]
if hud then
player:hud_remove(hud.id)
end
hud_create(player, message, params)
minetest.after(hud_timeout_seconds, function()
hud_remove(player, playername)
end)
end
return notify

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

BIN
screenshots/crafting.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

BIN
screenshots/pads.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

BIN
sounds/tpad_teleport.ogg Normal file

Binary file not shown.

113
storage.lua Normal file
View file

@ -0,0 +1,113 @@
local storage = minetest.get_mod_storage()
function tpad._get_all_pads()
local storage_table = storage:to_table()
local allpads = {}
for key, value in pairs(storage_table.fields) do
local parts = key:split(":")
if parts[1] == "pads" then
local pads = minetest.deserialize(value)
if type(pads) == "table" then
allpads[parts[2]] = pads
end
end
end
return allpads
end
function tpad._get_stored_pads(ownername)
local serial_pads = storage:get_string("pads:" .. ownername)
if serial_pads == nil or serial_pads == "" then return {} end
return minetest.deserialize(serial_pads)
end
function tpad._set_stored_pads(ownername, pads)
if ownername == nil or ownername == "" then
return
end
storage:set_string("pads:" .. ownername, minetest.serialize(pads))
end
function tpad.set_max_total_pads(max)
if not max then max = 0 end
storage:set_string("max_total_pads_per_player", max)
end
function tpad.get_max_total_pads()
local max = tonumber(storage:get_string("max_total_pads_per_player"))
if not max then
tpad.set_max_total_pads(100)
return 100
end
return max
end
function tpad.set_max_global_pads(max)
if not max then max = 0 end
storage:set_string("max_global_pads_per_player", max)
end
function tpad.get_max_global_pads()
local max = tonumber(storage:get_string("max_global_pads_per_player"))
if not max then
tpad.set_max_global_pads(4)
return 4
end
return max
end
local function _convert_legacy_settings()
local legacy_settings_file = minetest.get_worldpath() .. "/mod_storage/" .. tpad.mod_name .. ".custom.conf"
local file = io.open(legacy_settings_file, "r")
if file then
file:close()
local settings = Settings(legacy_settings_file)
local max_global = tonumber(settings:get("max_global_pads_per_player"))
if max_global then
tpad.set_max_global_pads(max_global)
end
local max_total = tonumber(settings:get("max_total_pads_per_player"))
if max_total then
tpad.set_max_total_pads(max_total)
end
os.remove(legacy_settings_file)
end
end
_convert_legacy_settings()
tpad.get_max_total_pads()
tpad.get_max_global_pads()
local function _convert_storage_1_1()
local storage_table = storage:to_table()
for field, value in pairs(storage_table.fields) do
local parts = field:split(":")
if parts[1] == "pads" then
local pads = minetest.deserialize(value)
for key, name in pairs(pads) do
pads[key] = { name = name }
end
storage_table.fields[field] = minetest.serialize(pads)
end
end
storage:from_table(storage_table)
end
local function _storage_version_check()
local storage_version = storage:get_string("_version")
local storage_path = minetest.get_worldpath() .. "/mod_storage/"
if storage_version == "1.1" then
local file = io.open(storage_path .. tpad.mod_name, "r")
if file then
file:close()
tpad._copy_file(storage_path .. tpad.mod_name, storage_path .. tpad.mod_name .. ".1.1.backup")
end
_convert_storage_1_1()
elseif storage_version ~= "" and storage_version ~= tpad.version then
error("Mod storage version not supported, aborting to prevent data corruption")
end
storage:set_string("_version", tpad.version)
end
_storage_version_check()

BIN
textures/tpad-texture.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 B

BIN
textures/tpad_bg_white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

BIN
textures/tpad_particle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 B