commit 18e9c71cada35858afa3cc002bf2f91378088c58 Author: rainer Date: Fri Aug 22 01:59:14 2025 +0200 init diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bac5f3d --- /dev/null +++ b/LICENSE @@ -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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2574c31 --- /dev/null +++ b/README.md @@ -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) diff --git a/custom.recipes.lua b/custom.recipes.lua new file mode 100644 index 0000000..0857e5e --- /dev/null +++ b/custom.recipes.lua @@ -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'}, + }, +} diff --git a/default/README.txt b/default/README.txt new file mode 100644 index 0000000..cb168bb --- /dev/null +++ b/default/README.txt @@ -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 diff --git a/default/recipes.lua b/default/recipes.lua new file mode 100644 index 0000000..0857e5e --- /dev/null +++ b/default/recipes.lua @@ -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'}, + }, +} diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..737a289 --- /dev/null +++ b/init.lua @@ -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}) diff --git a/init.lua.bak.1 b/init.lua.bak.1 new file mode 100644 index 0000000..40222e1 --- /dev/null +++ b/init.lua.bak.1 @@ -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}) diff --git a/init.lua.bak.2 b/init.lua.bak.2 new file mode 100644 index 0000000..6130adc --- /dev/null +++ b/init.lua.bak.2 @@ -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}) diff --git a/init.lua.bak.3 b/init.lua.bak.3 new file mode 100644 index 0000000..2bdc3b7 --- /dev/null +++ b/init.lua.bak.3 @@ -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}) diff --git a/init.lua.bak.4 b/init.lua.bak.4 new file mode 100644 index 0000000..729e8e1 --- /dev/null +++ b/init.lua.bak.4 @@ -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}) diff --git a/init.lua.bak.5 b/init.lua.bak.5 new file mode 100644 index 0000000..49f33ec --- /dev/null +++ b/init.lua.bak.5 @@ -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}) diff --git a/lib/smartfs.lua b/lib/smartfs.lua new file mode 100644 index 0000000..9c502bb --- /dev/null +++ b/lib/smartfs.lua @@ -0,0 +1,1440 @@ +--------------------------- +-- SmartFS: Smart Formspecs +-- License: CC0 or WTFPL +-- by Rubenwardy +--------------------------- + +local smartfs = { + _fdef = {}, + _edef = {}, + _ldef = {}, + opened = {}, + inv = {} +} + +local function boolToStr(v) + return v and "true" or "false" +end + +-- the smartfs() function +function smartfs.__call(self, name) + return smartfs.get(name) +end + +function smartfs.get(name) + return smartfs._fdef[name] +end + +------------------------------------------------------ +-- Smartfs Interface - Creates a new form and adds elements to it by running the function. Use before Minetest loads. (like minetest.register_node) +------------------------------------------------------ +-- Register forms and elements +function smartfs.create(name, onload) + assert(not smartfs._fdef[name], + "SmartFS - (Error) Form "..name.." already exists!") + assert(not smartfs.loaded or smartfs._loaded_override, + "SmartFS - (Error) Forms should be declared while the game loads.") + + smartfs._fdef[name] = { + form_setup_callback = onload, + name = name, + show = smartfs._show_, + attach_to_node = smartfs._attach_to_node_ + } + + return smartfs._fdef[name] +end + +------------------------------------------------------ +-- Smartfs Interface - Creates a new element type +------------------------------------------------------ +function smartfs.element(name, data) + assert(not smartfs._edef[name], + "SmartFS - (Error) Element type "..name.." already exists!") + + assert(data.onCreate, "element requires onCreate method") + smartfs._edef[name] = data + return smartfs._edef[name] +end + +------------------------------------------------------ +-- Smartfs Interface - Creates a dynamic form. Returns state +------------------------------------------------------ +function smartfs.dynamic(name,player) + if not smartfs._dynamic_warned then + smartfs._dynamic_warned = true + minetest.log("warning", "SmartFS - (Warning) On the fly forms are being used. May cause bad things to happen") + end + local statelocation = smartfs._ldef.player._make_state_location_(player) + local state = smartfs._makeState_({name=name}, nil, statelocation, player) + smartfs.opened[player] = state + return state +end + +------------------------------------------------------ +-- Smartfs Interface - Returns the name of an installed and supported inventory mod that will be used above, or nil +------------------------------------------------------ +function smartfs.inventory_mod() + if minetest.global_exists("unified_inventory") then + return "unified_inventory" + elseif minetest.global_exists("inventory_plus") then + return "inventory_plus" + else + return nil + end +end + +------------------------------------------------------ +-- Smartfs Interface - Adds a form to an installed advanced inventory. Returns true on success. +------------------------------------------------------ +function smartfs.add_to_inventory(form, icon, title) + local ldef + local invmod = smartfs.inventory_mod() + if invmod then + ldef = smartfs._ldef[invmod] + else + return false + end + return ldef.add_to_inventory(form, icon, title) +end + +------------------------------------------------------ +-- Smartfs Interface - Set the form as players inventory +------------------------------------------------------ +function smartfs.set_player_inventory(form) + smartfs._ldef.inventory.set_inventory(form) +end +------------------------------------------------------ +-- Smartfs Interface - Allows you to use smartfs.create after the game loads. Not recommended! +------------------------------------------------------ +function smartfs.override_load_checks() + smartfs._loaded_override = true +end + +------------------------------------------------------ +-- Smartfs formspec locations +------------------------------------------------------ +-- Unified inventory plugin +smartfs._ldef.unified_inventory = { + add_to_inventory = function(form, icon, title) + unified_inventory.register_button(form.name, { + type = "image", + image = icon, + }) + unified_inventory.register_page(form.name, { + get_formspec = function(player, formspec) + local name = player:get_player_name() + local statelocation = smartfs._ldef.unified_inventory._make_state_location_(name) + local state = smartfs._makeState_(form, nil, statelocation, name) + if form.form_setup_callback(state) ~= false then + smartfs.inv[name] = state + return {formspec = state:_buildFormspec_(false)} + else + return nil + end + end + }) + end, + _make_state_location_ = function(player) + return { + type = "inventory", + player = player, + _show_ = function(state) + unified_inventory.set_inventory_formspec(minetest.get_player_by_name(state.location.player), state.def.name) + end + } + end +} + +-- Inventory plus plugin +smartfs._ldef.inventory_plus = { + add_to_inventory = function(form, icon, title) + minetest.register_on_joinplayer(function(player) + inventory_plus.register_button(player, form.name, title) + end) + minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname == "" and fields[form.name] then + local name = player:get_player_name() + local statelocation = smartfs._ldef.inventory_plus._make_state_location_(name) + local state = smartfs._makeState_(form, nil, statelocation, name) + if form.form_setup_callback(state) ~= false then + smartfs.inv[name] = state + state:show() + end + end + end) + end, + _make_state_location_ = function(player) + return { + type = "inventory", + player = player, + _show_ = function(state) + inventory_plus.set_inventory_formspec(minetest.get_player_by_name(state.location.player), state:_buildFormspec_(true)) + end + } + end +} + +-- Show to player +smartfs._ldef.player = { + _make_state_location_ = function(player) + return { + type = "player", + player = player, + _show_ = function(state) + if not state._show_queued then + state._show_queued = true + minetest.after(0, function(state) + if state then + state._show_queued = nil + if (not state.closed) and (not state.obsolete) then + minetest.show_formspec(state.location.player, state.def.name, state:_buildFormspec_(true)) + end + end + end, state) -- state given as reference. Maybe additional updates are done in the meantime or the form is obsolete + end + end + } + end +} + +-- Standalone inventory +smartfs._ldef.inventory = { + set_inventory = function(form) + if sfinv and sfinv.enabled then + sfinv.enabled = nil + end + minetest.register_on_joinplayer(function(player) + local name = player:get_player_name() + local statelocation = smartfs._ldef.inventory._make_state_location_(name) + local state = smartfs._makeState_(form, nil, statelocation, name) + if form.form_setup_callback(state) ~= false then + smartfs.inv[name] = state + state:show() + end + end) + minetest.register_on_leaveplayer(function(player) + local name = player:get_player_name() + smartfs.inv[name].obsolete = true + smartfs.inv[name] = nil + end) + end, + _make_state_location_ = function(name) + return { + type = "inventory", + player = name, + _show_ = function(state) + if not state._show_queued then + state._show_queued = true + minetest.after(0, function(state) + if state then + state._show_queued = nil + local player = minetest.get_player_by_name(state.location.player) + --print("smartfs formspec:", state:_buildFormspec_(true)) + player:set_inventory_formspec(state:_buildFormspec_(true)) + end + end, state) + end + end + } + end +} + +-- Node metadata +smartfs._ldef.nodemeta = { + _make_state_location_ = function(nodepos) + return { + type = "nodemeta", + pos = nodepos, + _show_ = function(state) + if not state._show_queued then + state._show_queued = true + minetest.after(0, function(state) + if state then + state._show_queued = nil + local meta = minetest.get_meta(state.location.pos) + meta:set_string("formspec", state:_buildFormspec_(true)) + meta:set_string("smartfs_name", state.def.name) + meta:mark_as_private("smartfs_name") + end + end, state) + end + end, + } + end +} + +-- Sub-container (internally used) +smartfs._ldef.container = { + _make_state_location_ = function(element) + local self = { + type = "container", + containerElement = element, + parentState = element.root + } + if self.parentState.location.type == "container" then + self.rootState = self.parentState.location.rootState + else + self.rootState = self.parentState + end + return self + end +} + +------------------------------------------------------ +-- Minetest Interface - on_receive_fields callback can be used in minetest.register_node for nodemeta forms +------------------------------------------------------ +function smartfs.nodemeta_on_receive_fields(nodepos, formname, fields, sender, params) + local meta = minetest.get_meta(nodepos) + local nodeform = meta:get_string("smartfs_name") + if not nodeform then + print("SmartFS - (Warning) smartfs.nodemeta_on_receive_fields for node without smarfs data") + return false + end + + -- get the currentsmartfs state + local opened_id = minetest.pos_to_string(nodepos) + local state + local form = smartfs.get(nodeform) + if not smartfs.opened[opened_id] or -- If opened first time + smartfs.opened[opened_id].def.name ~= nodeform or -- Or form is changed + smartfs.opened[opened_id].obsolete then + local statelocation = smartfs._ldef.nodemeta._make_state_location_(nodepos) + state = smartfs._makeState_(form, params, statelocation) + if smartfs.opened[opened_id] then + smartfs.opened[opened_id].obsolete = true + end + smartfs.opened[opened_id] = state + form.form_setup_callback(state) + else + state = smartfs.opened[opened_id] + end + + -- Set current sender check for multiple users on node + local name + if sender then + name = sender:get_player_name() + state.players:connect(name) + end + + -- take the input + state:_sfs_on_receive_fields_(name, fields) + + -- Reset form if all players disconnected + if sender and not state.players:get_first() and not state.obsolete then + local statelocation = smartfs._ldef.nodemeta._make_state_location_(nodepos) + local resetstate = smartfs._makeState_(form, params, statelocation) + if form.form_setup_callback(resetstate) ~= false then + resetstate:show() + end + smartfs.opened[opened_id] = nil + end +end + +------------------------------------------------------ +-- Minetest Interface - on_player_receive_fields callback in case of inventory or player +------------------------------------------------------ +minetest.register_on_player_receive_fields(function(player, formname, fields) + local name = player:get_player_name() + if smartfs.opened[name] and smartfs.opened[name].location.type == "player" then + if smartfs.opened[name].def.name == formname then + local state = smartfs.opened[name] + state:_sfs_on_receive_fields_(name, fields) + + -- disconnect player if form closed + if not state.players:get_first() then + smartfs.opened[name].obsolete = true + smartfs.opened[name] = nil + end + end + elseif smartfs.inv[name] and smartfs.inv[name].location.type == "inventory" then + local state = smartfs.inv[name] + state:_sfs_on_receive_fields_(name, fields) + end + return false +end) + +------------------------------------------------------ +-- Minetest Interface - Notify loading of smartfs is done +------------------------------------------------------ +minetest.after(0, function() + smartfs.loaded = true +end) + +------------------------------------------------------ +-- Form Interface [linked to form:show()] - Shows the form to a player +------------------------------------------------------ +function smartfs._show_(form, name, params) + assert(form) + assert(type(name) == "string", "smartfs: name needs to be a string") + assert(minetest.get_player_by_name(name), "player does not exist") + local statelocation = smartfs._ldef.player._make_state_location_(name) + local state = smartfs._makeState_(form, params, statelocation, name) + if form.form_setup_callback(state) ~= false then + if smartfs.opened[name] then -- set maybe previous form to obsolete + smartfs.opened[name].obsolete = true + end + smartfs.opened[name] = state + state:show() + end + return state +end + +------------------------------------------------------ +-- Form Interface [linked to form:attach_to_node()] - Attach a formspec to a node meta +------------------------------------------------------ +function smartfs._attach_to_node_(form, nodepos, params) + assert(form) + assert(nodepos and nodepos.x) + + local statelocation = smartfs._ldef.nodemeta._make_state_location_(nodepos) + local state = smartfs._makeState_(form, params, statelocation) + if form.form_setup_callback(state) ~= false then + local opened_id = minetest.pos_to_string(nodepos) + if smartfs.opened[opened_id] then -- set maybe previous form to obsolete + smartfs.opened[opened_id].obsolete = true + end + state:show() + end + return state +end + +------------------------------------------------------ +-- Smartfs Framework - create a form object (state) +------------------------------------------------------ +function smartfs._makeState_(form, params, location, newplayer) + ------------------------------------------------------ + -- State - -- Object to manage players + ------------------------------------------------------ + local function _make_players_(newplayer) + local self = { + _list = {} + } + function self.connect(self, player) + self._list[player] = true + end + function self.disconnect(self, player) + self._list[player] = nil + end + function self.get_first(self) + return next(self._list) + end + if newplayer then + self:connect(newplayer) + end + return self + end + + + ------------------------------------------------------ + -- State - create returning state object + ------------------------------------------------------ + return { + _ele = {}, + def = form, + players = _make_players_(newplayer), + location = location, + is_inv = (location.type == "inventory"), -- obsolete. Please use location.type="inventory" instead + player = newplayer, -- obsolete. Please use location.player + param = params or {}, + get = function(self,name) + return self._ele[name] + end, + close = function(self) + self.closed = true + end, + getSize = function(self) + return self._size + end, + size = function(self,w,h) + self._size = {w=w,h=h} + end, + setSize = function(self,w,h) + self._size = {w=w,h=h} + end, + getNamespace = function(self) + local ref = self + local namespace = "" + while ref.location.type == "container" do + namespace = ref.location.containerElement.name.."#"..namespace + ref = ref.location.parentState -- step near to the root + end + return namespace + end, + _buildFormspec_ = function(self,size) + local res = "" + if self._size and size then + res = "size["..self._size.w..","..self._size.h.."]" + end + for key,val in pairs(self._ele) do + if val:getVisible() then + res = res .. val:getBackgroundString() .. val:build() .. val:getTooltipString() + end + end + return res + end, + show = location._show_, + _get_element_recursive_ = function(self, field) + local topfield + for z in field:gmatch("[^#]+") do + topfield = z + break + end + local element = self._ele[topfield] + if element and field == topfield then + return element + elseif element then + if element._getSubElement_ then + local rel_field = string.sub(field, string.len(topfield)+2) + return element:_getSubElement_(rel_field) + else + return element + end + else + return nil + end + end, + -- process onInput hook for the state + _sfs_process_oninput_ = function(self, fields, player) + if self._onInput then + self:_onInput(fields, player) + end + -- recursive all onInput hooks on visible containers + for elename, eledef in pairs(self._ele) do + if eledef.getContainerState and eledef:getVisible() then + eledef:getContainerState():_sfs_process_oninput_(fields, player) + end + end + end, + -- Receive fields and actions from formspec + _sfs_on_receive_fields_ = function(self, player, fields) + + local fields_todo = {} + for field, value in pairs(fields) do + local element = self:_get_element_recursive_(field) + if element then + fields_todo[field] = { element = element, value = value } + end + end + + for field, todo in pairs(fields_todo) do + todo.element:setValue(todo.value) + end + + self:_sfs_process_oninput_(fields, player) + + for field, todo in pairs(fields_todo) do + if todo.element.submit then + todo.element:submit(todo.value, player) + end + end + -- handle key_enter + if fields.key_enter and fields.key_enter_field then + local element = self:_get_element_recursive_(fields.key_enter_field) + if element and element.submit_key_enter then + element:submit_key_enter(fields[fields.key_enter_field], player) + end + end + + if not fields.quit and not self.closed and not self.obsolete then + self:show() + else + self.players:disconnect(player) + if not fields.quit and self.closed and not self.obsolete then + --closed by application (without fields.quit). currently not supported, see: https://github.com/minetest/minetest/pull/4675 + minetest.show_formspec(player,"","size[5,1]label[0,0;Formspec closing not yet created!]") + end + end + return true + end, + onInput = function(self, func) + self._onInput = func -- (fields, player) + end, + load = function(self,file) + local file = io.open(file, "r") + if file then + local table = minetest.deserialize(file:read("*all")) + if type(table) == "table" then + if table.size then + self._size = table.size + end + for key,val in pairs(table.ele) do + self:element(val.type,val) + end + return true + end + end + return false + end, + save = function(self,file) + local res = {ele={}} + + if self._size then + res.size = self._size + end + + for key,val in pairs(self._ele) do + res.ele[key] = val.data + end + + local file = io.open(file, "w") + if file then + file:write(minetest.serialize(res)) + file:close() + return true + end + return false + end, + setparam = function(self,key,value) + if not key then return end + self.param[key] = value + return true + end, + getparam = function(self,key,default) + if not key then return end + return self.param[key] or default + end, + element = function(self,typen,data) + local type = smartfs._edef[typen] + assert(type, "Element type "..typen.." does not exist!") + assert(not self._ele[data.name], "Element "..data.name.." already exists") + + data.type = typen + local ele = { + name = data.name, + root = self, + data = data, + remove = function(self) + self.root._ele[self.name] = nil + end, + setPosition = function(self,x,y) + self.data.pos = {x=x,y=y} + end, + getPosition = function(self) + return self.data.pos + end, + setSize = function(self,w,h) + self.data.size = {w=w,h=h} + end, + getSize = function(self) + return self.data.size + end, + setVisible = function(self, visible) + if visible == nil then + self.data.visible = true + else + self.data.visible = visible + end + end, + getVisible = function(self) + return self.data.visible + end, + getAbsName = function(self) + return self.root:getNamespace()..self.name + end, + setBackground = function(self, image) + self.data.background = image + end, + getBackground = function(self) + return self.data.background + end, + getBackgroundString = function(self) + if self.data.background then + local size = self:getSize() + if size then + return "background[".. + self.data.pos.x..","..self.data.pos.y..";".. + size.w..","..size.h..";".. + self.data.background.."]" + else + return "" + end + else + return "" + end + end, + setValue = function(self, value) + self.data.value = value + end, + setTooltip = function(self,text) + self.data.tooltip = minetest.formspec_escape(text) + end, + getTooltip = function(self) + return self.data.tooltip + end, + getTooltipString = function(self) + if self.data.tooltip then + return "tooltip["..self:getAbsName()..";"..self:getTooltip().."]" + else + return "" + end + end, + } + + ele.data.visible = true --visible by default + + for key, val in pairs(type) do + ele[key] = val + end + + self._ele[data.name] = ele + + type.onCreate(ele) + + return self._ele[data.name] + end, + + ------------------------------------------------------ + -- State - Element Constructors + ------------------------------------------------------ + button = function(self, x, y, w, h, name, text, exitf) + return self:element("button", { + pos = {x=x,y=y}, + size = {w=w,h=h}, + name = name, + value = text, + closes = exitf or false + }) + end, + image_button = function(self, x, y, w, h, name, text, image, exitf) + return self:element("button", { + pos = {x=x,y=y}, + size = {w=w,h=h}, + name = name, + value = text, + image = image, + closes = exitf or false + }) + end, + item_image_button = function(self, x, y, w, h, name, text, item, exitf) + return self:element("button", { + pos = {x=x,y=y}, + size = {w=w,h=h}, + name = name, + value = text, + item = item, + closes = exitf or false + }) + end, + label = function(self, x, y, name, text) + return self:element("label", { + pos = {x=x,y=y}, + name = name, + value = text, + vertical = false + }) + end, + vertlabel = function(self, x, y, name, text) + return self:element("label", { + pos = {x=x,y=y}, + name = name, + value = text, + vertical = true + }) + end, + toggle = function(self, x, y, w, h, name, list) + return self:element("toggle", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name, + id = 1, + list = list + }) + end, + field = function(self, x, y, w, h, name, label) + return self:element("field", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name, + value = "", + label = label + }) + end, + pwdfield = function(self, x, y, w, h, name, label) + local res = self:element("field", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name, + value = "", + label = label + }) + res:isPassword(true) + return res + end, + textarea = function(self, x, y, w, h, name, label) + local res = self:element("field", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name, + value = "", + label = label + }) + res:isMultiline(true) + return res + end, + image = function(self, x, y, w, h, name, img) + return self:element("image", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name, + value = img, + imgtype = "image" + }) + end, + background = function(self, x, y, w, h, name, img) + return self:element("image", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name, + background = img, + imgtype = "background" + }) + end, + item_image = function(self, x, y, w, h, name, img) + return self:element("image", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name, + value = img, + imgtype = "item" + }) + end, + checkbox = function(self, x, y, name, label, selected) + return self:element("checkbox", { + pos = {x=x, y=y}, + name = name, + value = selected, + label = label + }) + end, + listbox = function(self, x, y, w, h, name, selected, transparent) + return self:element("list", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name, + selected = selected, + transparent = transparent + }) + end, + dropdown = function(self, x, y, w, h, name, selected) + return self:element("dropdown", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name, + selected = selected + }) + end, + inventory = function(self, x, y, w, h, name) + return self:element("inventory", { + pos = {x=x, y=y}, + size = {w=w, h=h}, + name = name + }) + end, + container = function(self, x, y, name, relative) + return self:element("container", { + pos = {x=x, y=y}, + name = name, + relative = false + }) + end, + view = function(self, x, y, name, relative) + return self:element("container", { + pos = {x=x, y=y}, + name = name, + relative = true + }) + end, + } +end + +----------------------------------------------------------------- +------------------------- ELEMENTS ---------------------------- +----------------------------------------------------------------- + +smartfs.element("button", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "button needs valid pos") + assert(self.data.size and self.data.size.w and self.data.size.h, "button needs valid size") + assert(self.name, "button needs name") + assert(self.data.value, "button needs label") + end, + build = function(self) + local specstring + if self.data.image then + if self.data.closes then + specstring = "image_button_exit[" + else + specstring = "image_button[" + end + elseif self.data.item then + if self.data.closes then + specstring = "item_image_button_exit[" + else + specstring = "item_image_button[" + end + else + if self.data.closes then + specstring = "button_exit[" + else + specstring = "button[" + end + end + + specstring = specstring .. + self.data.pos.x..","..self.data.pos.y..";".. + self.data.size.w..","..self.data.size.h..";" + if self.data.image then + specstring = specstring..self.data.image..";" + elseif self.data.item then + specstring = specstring..self.data.item..";" + end + specstring = specstring..self:getAbsName()..";".. + minetest.formspec_escape(self.data.value).."]" + return specstring + end, + submit = function(self, field, player) + if self._click then + self:_click(self.root, player) + end + end, + onClick = function(self,func) + self._click = func + end, + click = function(self,func) + self._click = func + end, + setText = function(self,text) + self:setValue(text) + end, + getText = function(self) + return self.data.value + end, + setImage = function(self,image) + self.data.image = image + self.data.item = nil + end, + getImage = function(self) + return self.data.image + end, + setItem = function(self,item) + self.data.item = item + self.data.image = nil + end, + getItem = function(self) + return self.data.item + end, + setClose = function(self,bool) + self.data.closes = bool + end, + getClose = function(self) + return self.data.closes or false + end +}) + +smartfs.element("toggle", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "toggle needs valid pos") + assert(self.data.size and self.data.size.w and self.data.size.h, "toggle needs valid size") + assert(self.name, "toggle needs name") + assert(self.data.list, "toggle needs data") + end, + build = function(self) + return "button[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self:getAbsName().. + ";".. + minetest.formspec_escape(self.data.list[self.data.id]).. + "]" + end, + submit = function(self, field, player) + self.data.id = self.data.id + 1 + if self.data.id > #self.data.list then + self.data.id = 1 + end + if self._tog then + self:_tog(self.root, player) + end + end, + onToggle = function(self,func) + self._tog = func + end, + setId = function(self,id) + self.data.id = id + end, + getId = function(self) + return self.data.id + end, + getText = function(self) + return self.data.list[self.data.id] + end +}) + +smartfs.element("label", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "label needs valid pos") + assert(self.data.value, "label needs text") + end, + build = function(self) + local specstring + if self.data.vertical then + specstring = "vertlabel[" + else + specstring = "label[" + end + return specstring.. + self.data.pos.x..","..self.data.pos.y.. + ";".. + minetest.formspec_escape(self.data.value).. + "]" + end, + setText = function(self,text) + self:setValue(text) + end, + getText = function(self) + return self.data.value + end +}) + +smartfs.element("field", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "field needs valid pos") + assert(self.data.size and self.data.size.w and self.data.size.h, "field needs valid size") + assert(self.name, "field needs name") + self.data.value = self.data.value or "" + self.data.label = self.data.label or "" + end, + build = function(self) + if self.data.ml then + return "textarea[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self:getAbsName().. + ";".. + minetest.formspec_escape(self.data.label).. + ";".. + minetest.formspec_escape(self.data.value).. + "]" + elseif self.data.pwd then + return "pwdfield[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self:getAbsName().. + ";".. + minetest.formspec_escape(self.data.label).. + "]".. + self:getCloseOnEnterString() + else + return "field[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self:getAbsName().. + ";".. + minetest.formspec_escape(self.data.label).. + ";".. + minetest.formspec_escape(self.data.value).. + "]".. + self:getCloseOnEnterString() + end + end, + setLabel = function(self,text) + self.data.label = text + end, + getLabel = function(self) + return self.data.label + end, + setText = function(self,text) + self:setValue(text) + end, + getText = function(self) + return self.data.value + end, + isPassword = function(self,bool) + self.data.pwd = bool + end, + isMultiline = function(self,bool) + self.data.ml = bool + end, + getCloseOnEnterString = function(self) + if self.close_on_enter == nil then + return "" + else + return "field_close_on_enter["..self:getAbsName()..";"..tostring(self.close_on_enter).."]" + end + end, + setCloseOnEnter = function(self, value) + self.close_on_enter = value + end, + getCloseOnEnter = function(self) + return self.close_on_enter + end, + submit_key_enter = function(self, field, player) + if self._key_enter then + self:_key_enter(self.root, player) + end + end, + onKeyEnter = function(self,func) + self._key_enter = func + end, +}) + +smartfs.element("image", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "image needs valid pos") + assert(self.data.size and self.data.size.w and self.data.size.h, "image needs valid size") + self.data.value = self.data.value or "" + end, + build = function(self) + if self.data.imgtype == "background" then + return "" -- handled in _buildFormspec_ trough getBackgroundString() + elseif self.data.imgtype == "item" then + return "item_image[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self.data.value.. + "]" + else + return "image[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self.data.value.. + "]" + end + end, + setImage = function(self,text) + if self.data.imgtype == "background" then + self.data.background = text + else + self:setValue(text) + end + end, + getImage = function(self) + if self.data.imgtype == "background" then + return self.data.background + else + return self.data.value + end + end +}) + +smartfs.element("checkbox", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "checkbox needs valid pos") + assert(self.name, "checkbox needs name") + self.data.value = minetest.is_yes(self.data.value) + self.data.label = self.data.label or "" + end, + build = function(self) + return "checkbox[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self:getAbsName().. + ";".. + minetest.formspec_escape(self.data.label).. + ";" .. boolToStr(self.data.value) .."]" + end, + submit = function(self, field, player) + -- call the toggle function if defined + if self._tog then + self:_tog(self.root, player) + end + end, + setValue = function(self, value) + self.data.value = minetest.is_yes(value) + end, + getValue = function(self) + return self.data.value + end, + onToggle = function(self,func) + self._tog = func + end, +}) + +smartfs.element("list", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "list needs valid pos") + assert(self.data.size and self.data.size.w and self.data.size.h, "list needs valid size") + assert(self.name, "list needs name") + self.data.value = minetest.is_yes(self.data.value) + self.data.items = self.data.items or {} + end, + build = function(self) + if not self.data.items then + self.data.items = {} + end + local escaped = {} + for i, v in ipairs(self.data.items) do + escaped[i] = minetest.formspec_escape(v) + end + return "textlist[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self:getAbsName().. + ";".. + table.concat(escaped, ",").. + ";".. + tostring(self.data.selected or "").. + ";".. + tostring(self.data.transparent or "false").."]" + end, + submit = function(self, field, player) + local _type = string.sub(field,1,3) + local index = tonumber(string.sub(field,5)) + self.data.selected = index + if _type == "CHG" and self._click then + self:_click(self.root, index, player) + elseif _type == "DCL" and self._doubleClick then + self:_doubleClick(self.root, index, player) + end + end, + onClick = function(self, func) + self._click = func + end, + click = function(self, func) + self._click = func + end, + onDoubleClick = function(self, func) + self._doubleClick = func + end, + doubleclick = function(self, func) + self._doubleClick = func + end, + addItem = function(self, item) + table.insert(self.data.items, item) + -- return the index of item. It is the last one + return #self.data.items + end, + removeItem = function(self,idx) + table.remove(self.data.items,idx) + end, + getItem = function(self, idx) + return self.data.items[idx] + end, + popItem = function(self) + local item = self.data.items[#self.data.items] + table.remove(self.data.items) + return item + end, + clearItems = function(self) + self.data.items = {} + end, + setSelected = function(self,idx) + self.data.selected = idx + end, + getSelected = function(self) + return self.data.selected + end, + getSelectedItem = function(self) + return self:getItem(self:getSelected()) + end, +}) + +smartfs.element("dropdown", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "dropdown needs valid pos") + assert(self.data.size and self.data.size.w and self.data.size.h, "dropdown needs valid size") + assert(self.name, "dropdown needs name") + self.data.items = self.data.items or {} + self.data.selected = self.data.selected or 1 + self.data.value = "" + end, + build = function(self) + return "dropdown[".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + self:getAbsName().. + ";".. + table.concat(self.data.items, ",").. + ";".. + tostring(self:getSelected()).. + "]" + end, + submit = function(self, field, player) + self:getSelected() + if self._select then + self:_select(self.root, field, player) + end + end, + onSelect = function(self, func) + self._select = func + end, + addItem = function(self, item) + table.insert(self.data.items, item) + if #self.data.items == self.data.selected then + self.data.value = item + end + -- return the index of item. It is the last one + return #self.data.items + end, + removeItem = function(self,idx) + table.remove(self.data.items,idx) + end, + getItem = function(self, idx) + return self.data.items[idx] + end, + popItem = function(self) + local item = self.data.items[#self.data.items] + table.remove(self.data.items) + return item + end, + clearItems = function(self) + self.data.items = {} + end, + setSelected = function(self,idx) + self.data.selected = idx + self.data.value = self:getItem(idx) or "" + end, + setSelectedItem = function(self,itm) + for idx, item in ipairs(self.data.items) do + if item == itm then + self.data.selected = idx + self.data.value = item + end + end + end, + getSelected = function(self) + self.data.selected = 1 + if #self.data.items > 1 then + for i = 1, #self.data.items do + if self.data.items[i] == self.data.value then + self.data.selected = i + end + end + end + return self.data.selected + end, + getSelectedItem = function(self) + return self.data.value + end, +}) + +smartfs.element("inventory", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "list needs valid pos") + assert(self.data.size and self.data.size.w and self.data.size.h, "list needs valid size") + assert(self.name, "list needs name") + end, + build = function(self) + return "list[".. + (self.data.invlocation or "current_player") .. + ";".. + self.name.. --no namespacing + ";".. + self.data.pos.x..","..self.data.pos.y.. + ";".. + self.data.size.w..","..self.data.size.h.. + ";".. + (self.data.index or "") .. + "]" + end, + -- available inventory locations + -- "current_player": Player to whom the menu is shown + -- "player:": Any player + -- "nodemeta:,,": Any node metadata + -- "detached:": A detached inventory + -- "context" does not apply to smartfs, since there is no node-metadata as context available + setLocation = function(self,invlocation) + self.data.invlocation = invlocation + end, + getLocation = function(self) + return self.data.invlocation or "current_player" + end, + usePosition = function(self, pos) + self.data.invlocation = string.format("nodemeta:%d,%d,%d", pos.x, pos.y, pos.z) + end, + usePlayer = function(self, name) + self.data.invlocation = "player:" .. name + end, + useDetached = function(self, name) + self.data.invlocation = "detached:" .. name + end, + setIndex = function(self,index) + self.data.index = index + end, + getIndex = function(self) + return self.data.index + end +}) + +smartfs.element("code", { + onCreate = function(self) + self.data.code = self.data.code or "" + end, + build = function(self) + if self._build then + self:_build() + end + + return self.data.code + end, + submit = function(self, field, player) + if self._sub then + self:_sub(self.root, field, player) + end + end, + onSubmit = function(self,func) + self._sub = func + end, + onBuild = function(self,func) + self._build = func + end, + setCode = function(self,code) + self.data.code = code + end, + getCode = function(self) + return self.data.code + end +}) + +smartfs.element("container", { + onCreate = function(self) + assert(self.data.pos and self.data.pos.x and self.data.pos.y, "container needs valid pos") + assert(self.name, "container needs name") + local statelocation = smartfs._ldef.container._make_state_location_(self) + self._state = smartfs._makeState_(nil, self.root.param, statelocation) + end, + + -- redefinitions. The size is not handled by data.size but by container-state:size + setSize = function(self,w,h) + self:getContainerState():setSize(w,h) + end, + getSize = function(self) + return self:getContainerState():getSize() + end, + + -- element interface methods + build = function(self) + if self.data.relative ~= true then + return "container["..self.data.pos.x..","..self.data.pos.y.."]".. + self:getContainerState():_buildFormspec_(false).. + "container_end[]" + else + return self:getContainerState():_buildFormspec_(false) + end + end, + getContainerState = function(self) + return self._state + end, + _getSubElement_ = function(self, field) + return self:getContainerState():_get_element_recursive_(field) + end, +}) + +return smartfs \ No newline at end of file diff --git a/mod.conf b/mod.conf new file mode 100644 index 0000000..81736e7 --- /dev/null +++ b/mod.conf @@ -0,0 +1,5 @@ +name = tpad +release = 28854 +author = entuland +description = A simple but powerful pad to create teleporting networks +title = Teleporter Pads diff --git a/models/tpad-mesh.obj b/models/tpad-mesh.obj new file mode 100644 index 0000000..6c958f1 --- /dev/null +++ b/models/tpad-mesh.obj @@ -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 diff --git a/notify.lua b/notify.lua new file mode 100644 index 0000000..945ab92 --- /dev/null +++ b/notify.lua @@ -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 diff --git a/screenshots/admin-settings.png b/screenshots/admin-settings.png new file mode 100644 index 0000000..8e246d0 Binary files /dev/null and b/screenshots/admin-settings.png differ diff --git a/screenshots/crafting.png b/screenshots/crafting.png new file mode 100644 index 0000000..835ff06 Binary files /dev/null and b/screenshots/crafting.png differ diff --git a/screenshots/global-network-admin.png b/screenshots/global-network-admin.png new file mode 100644 index 0000000..29d57c1 Binary files /dev/null and b/screenshots/global-network-admin.png differ diff --git a/screenshots/global-network.png b/screenshots/global-network.png new file mode 100644 index 0000000..f0e6a23 Binary files /dev/null and b/screenshots/global-network.png differ diff --git a/screenshots/local-network-visitor.png b/screenshots/local-network-visitor.png new file mode 100644 index 0000000..8466054 Binary files /dev/null and b/screenshots/local-network-visitor.png differ diff --git a/screenshots/local-network.png b/screenshots/local-network.png new file mode 100644 index 0000000..eb65f9c Binary files /dev/null and b/screenshots/local-network.png differ diff --git a/screenshots/pads.png b/screenshots/pads.png new file mode 100644 index 0000000..fcdef6c Binary files /dev/null and b/screenshots/pads.png differ diff --git a/sounds/tpad_teleport.ogg b/sounds/tpad_teleport.ogg new file mode 100644 index 0000000..b8837da Binary files /dev/null and b/sounds/tpad_teleport.ogg differ diff --git a/storage.lua b/storage.lua new file mode 100644 index 0000000..abcb412 --- /dev/null +++ b/storage.lua @@ -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() diff --git a/textures/tpad-texture.png b/textures/tpad-texture.png new file mode 100644 index 0000000..e02ea14 Binary files /dev/null and b/textures/tpad-texture.png differ diff --git a/textures/tpad_bg_transparent.png b/textures/tpad_bg_transparent.png new file mode 100644 index 0000000..f5b814a Binary files /dev/null and b/textures/tpad_bg_transparent.png differ diff --git a/textures/tpad_bg_white.png b/textures/tpad_bg_white.png new file mode 100644 index 0000000..b4bdc43 Binary files /dev/null and b/textures/tpad_bg_white.png differ diff --git a/textures/tpad_particle.png b/textures/tpad_particle.png new file mode 100644 index 0000000..6ef15cb Binary files /dev/null and b/textures/tpad_particle.png differ