mods that hopefully work for a TechAge-based pack
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 TomCon
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,15 @@
|
||||
# discovery_maps
|
||||
|
||||
adds fullscreen mapping, markers, and multiplayer team map sharing system to luanti
|
||||
|
||||
Features:
|
||||
Chart your world your way with a fully featured fullscreen map!
|
||||

|
||||
|
||||

|
||||
|
||||
Visible markers with easy teleport (if you have permissions!)
|
||||

|
||||
Discover the world with your friends!
|
||||
|
||||

|
||||
Binary file not shown.
|
After Width: | Height: | Size: 450 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 425 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 615 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 521 KiB |
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,582 @@
|
||||
-- Map Party GUI System for Persistent Map Mod
|
||||
-- Provides a graphical interface for managing map parties
|
||||
-- Includes proper permission controls for admins and party leaders
|
||||
|
||||
local S = minetest.get_translator(minetest.get_current_modname())
|
||||
|
||||
-- GUI state storage for each player
|
||||
local gui_state = {}
|
||||
|
||||
-- Helper function to check if player is server admin
|
||||
local function is_server_admin(player_name)
|
||||
return minetest.check_player_privs(player_name, {server = true}) or
|
||||
minetest.check_player_privs(player_name, {ban = true}) or
|
||||
minetest.check_player_privs(player_name, {kick = true})
|
||||
end
|
||||
|
||||
-- Helper function to get party data from map-party.lua
|
||||
local function get_map_parties()
|
||||
local storage = persistent_map.storage
|
||||
if not storage then
|
||||
return {}
|
||||
end
|
||||
|
||||
local data = storage:get_string("map_parties")
|
||||
if data ~= "" then
|
||||
return minetest.deserialize(data) or {}
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
-- Helper function to get player's current party
|
||||
local function get_player_party(player_name)
|
||||
local parties = get_map_parties()
|
||||
for party_name, party_data in pairs(parties) do
|
||||
for _, member in ipairs(party_data.members) do
|
||||
if member == player_name then
|
||||
return party_name, party_data
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
-- Helper function to check if player is party owner
|
||||
local function is_party_owner(player_name, party_name)
|
||||
local parties = get_map_parties()
|
||||
local party = parties[party_name]
|
||||
return party and party.owner == player_name
|
||||
end
|
||||
|
||||
-- Helper function to save map parties
|
||||
local function save_map_parties(parties)
|
||||
local storage = persistent_map.storage
|
||||
if storage then
|
||||
storage:set_string("map_parties", minetest.serialize(parties))
|
||||
end
|
||||
end
|
||||
|
||||
-- Main party management GUI
|
||||
function persistent_map.show_party_gui(player_name)
|
||||
local player = minetest.get_player_by_name(player_name)
|
||||
if not player then return end
|
||||
|
||||
local is_admin = is_server_admin(player_name)
|
||||
local current_party_name, current_party_data = get_player_party(player_name)
|
||||
local is_owner = current_party_name and is_party_owner(player_name, current_party_name)
|
||||
local parties = get_map_parties()
|
||||
|
||||
-- Initialize GUI state if not exists
|
||||
if not gui_state[player_name] then
|
||||
gui_state[player_name] = {
|
||||
view_mode = "main", -- main, create, manage, list
|
||||
selected_party = nil,
|
||||
scroll_pos = 0
|
||||
}
|
||||
end
|
||||
|
||||
local state = gui_state[player_name]
|
||||
local formspec = {}
|
||||
|
||||
-- Base formspec setup
|
||||
formspec[1] = "formspec_version[6]"
|
||||
formspec[2] = "size[16,12]"
|
||||
formspec[3] = "bgcolor[#00000080;true]"
|
||||
|
||||
local formspec_index = 4
|
||||
|
||||
if state.view_mode == "main" then
|
||||
-- Main menu view
|
||||
formspec[formspec_index] = "label[8,0.5;Map Party Management]"
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
-- Current party status
|
||||
if current_party_name then
|
||||
formspec[formspec_index] = string.format("label[1,2;Current Party: %s]", current_party_name)
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
formspec[formspec_index] = string.format("label[1,2.8;Members (%d): %s]",
|
||||
#current_party_data.members, table.concat(current_party_data.members, ", "))
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
formspec[formspec_index] = string.format("label[1,3.6;Owner: %s]", current_party_data.owner)
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
if is_owner then
|
||||
formspec[formspec_index] = "label[1,4.4;You are the party owner]"
|
||||
formspec_index = formspec_index + 1
|
||||
end
|
||||
else
|
||||
formspec[formspec_index] = "label[1,2;You are not in any party]"
|
||||
formspec_index = formspec_index + 1
|
||||
end
|
||||
|
||||
-- Action buttons
|
||||
local button_y = 6
|
||||
|
||||
-- Create party button (always available)
|
||||
if not current_party_name then
|
||||
formspec[formspec_index] = string.format("button[1,%f;4,1;create_party;Create New Party]", button_y)
|
||||
formspec_index = formspec_index + 1
|
||||
end
|
||||
|
||||
-- Manage current party button (owner only)
|
||||
if current_party_name and is_owner then
|
||||
formspec[formspec_index] = string.format("button[6,%f;4,1;manage_party;Manage Party]", button_y)
|
||||
formspec_index = formspec_index + 1
|
||||
end
|
||||
|
||||
-- Leave party button (if in a party)
|
||||
if current_party_name then
|
||||
formspec[formspec_index] = string.format("button[11,%f;4,1;leave_party;Leave Party]", button_y)
|
||||
formspec_index = formspec_index + 1
|
||||
end
|
||||
|
||||
-- List all parties button
|
||||
formspec[formspec_index] = string.format("button[1,%f;4,1;list_parties;List All Parties]", button_y + 1.5)
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
-- Admin controls (admin only)
|
||||
if is_admin then
|
||||
formspec[formspec_index] = "label[1,9;Admin Controls:]"
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
formspec[formspec_index] = "button[1,9.8;4,1;admin_manage;Manage All Parties]"
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
formspec[formspec_index] = "button[6,9.8;4,1;admin_delete;Delete Any Party]"
|
||||
formspec_index = formspec_index + 1
|
||||
end
|
||||
|
||||
elseif state.view_mode == "create" then
|
||||
-- Create party view
|
||||
formspec[formspec_index] = "label[8,0.5;Create New Party]"
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
formspec[formspec_index] = "label[1,2;Party Name:]"
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
formspec[formspec_index] = "field[1,2.8;8,1;party_name;;]"
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
formspec[formspec_index] = "label[1,4;Initial Members (optional, one per line):]"
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
formspec[formspec_index] = "textarea[1,4.8;8,4;initial_members;;]"
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
formspec[formspec_index] = "button[1,9.5;3,1;create_confirm;Create Party]"
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
formspec[formspec_index] = "button[5,9.5;3,1;back_main;Back]"
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
elseif state.view_mode == "manage" then
|
||||
-- Manage party view
|
||||
formspec[formspec_index] = string.format("label[8,0.5;Manage Party: %s]", current_party_name or "Unknown")
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
if current_party_data then
|
||||
-- Current members list
|
||||
formspec[formspec_index] = "label[1,2;Current Members:]"
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
local member_list = {}
|
||||
for i, member in ipairs(current_party_data.members) do
|
||||
local status = ""
|
||||
if member == current_party_data.owner then
|
||||
status = " (Owner)"
|
||||
end
|
||||
if minetest.get_player_by_name(member) then
|
||||
status = status .. " [Online]"
|
||||
else
|
||||
status = status .. " [Offline]"
|
||||
end
|
||||
table.insert(member_list, member .. status)
|
||||
end
|
||||
|
||||
formspec[formspec_index] = string.format("textlist[1,2.8;7,4;member_list;%s;1;false]",
|
||||
table.concat(member_list, ","))
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
-- Add member section
|
||||
formspec[formspec_index] = "label[10,2;Add Member:]"
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
formspec[formspec_index] = "field[10,2.8;5,1;add_member_name;;]"
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
formspec[formspec_index] = "button[10,3.8;3,1;add_member;Add Member]"
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
-- Remove member section (only show if there are members to remove)
|
||||
if #current_party_data.members > 1 then
|
||||
formspec[formspec_index] = "label[10,5;Remove Selected Member:]"
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
formspec[formspec_index] = "button[10,5.8;3,1;remove_member;Remove Member]"
|
||||
formspec_index = formspec_index + 1
|
||||
end
|
||||
|
||||
-- Delete party button (owner only)
|
||||
if is_owner then
|
||||
formspec[formspec_index] = "button[10,7.5;3,1;delete_party;Delete Party]"
|
||||
formspec_index = formspec_index + 1
|
||||
end
|
||||
end
|
||||
|
||||
formspec[formspec_index] = "button[1,10.5;3,1;back_main;Back to Main]"
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
elseif state.view_mode == "list" then
|
||||
-- List all parties view
|
||||
formspec[formspec_index] = "label[8,0.5;All Map Parties]"
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
if next(parties) == nil then
|
||||
formspec[formspec_index] = "label[1,3;No parties exist]"
|
||||
formspec_index = formspec_index + 1
|
||||
else
|
||||
local party_list = {}
|
||||
for party_name, party_data in pairs(parties) do
|
||||
local online_count = 0
|
||||
for _, member in ipairs(party_data.members) do
|
||||
if minetest.get_player_by_name(member) then
|
||||
online_count = online_count + 1
|
||||
end
|
||||
end
|
||||
|
||||
local party_info = string.format("%s | Owner: %s | Members: %d (%d online)",
|
||||
party_name, party_data.owner, #party_data.members, online_count)
|
||||
table.insert(party_list, party_info)
|
||||
end
|
||||
|
||||
formspec[formspec_index] = string.format("textlist[1,2;13,7;party_list;%s;1;false]",
|
||||
table.concat(party_list, ","))
|
||||
formspec_index = formspec_index + 1
|
||||
end
|
||||
|
||||
formspec[formspec_index] = "button[1,10.5;3,1;back_main;Back to Main]"
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
elseif state.view_mode == "admin" then
|
||||
-- Admin management view
|
||||
formspec[formspec_index] = "label[8,0.5;Admin Party Management]"
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
if next(parties) == nil then
|
||||
formspec[formspec_index] = "label[1,3;No parties exist]"
|
||||
formspec_index = formspec_index + 1
|
||||
else
|
||||
local party_list = {}
|
||||
for party_name, party_data in pairs(parties) do
|
||||
local party_info = string.format("%s | Owner: %s | Members: %d | Created: %s",
|
||||
party_name, party_data.owner, #party_data.members,
|
||||
os.date("%Y-%m-%d", party_data.created or 0))
|
||||
table.insert(party_list, party_info)
|
||||
end
|
||||
|
||||
formspec[formspec_index] = string.format("textlist[1,2;13,6;admin_party_list;%s;1;false]",
|
||||
table.concat(party_list, ","))
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
formspec[formspec_index] = "button[1,9;3,1;admin_delete_selected;Delete Selected]"
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
formspec[formspec_index] = "button[5,9;3,1;admin_view_details;View Details]"
|
||||
formspec_index = formspec_index + 1
|
||||
end
|
||||
|
||||
formspec[formspec_index] = "button[1,10.5;3,1;back_main;Back to Main]"
|
||||
formspec_index = formspec_index + 1
|
||||
end
|
||||
|
||||
-- Close button (always present)
|
||||
formspec[formspec_index] = "button[13,10.5;2,1;close;Close]"
|
||||
|
||||
minetest.show_formspec(player_name, "persistent_map:party_gui", table.concat(formspec))
|
||||
end
|
||||
|
||||
-- Handle formspec input
|
||||
minetest.register_on_player_receive_fields(function(player, formname, fields)
|
||||
if formname ~= "persistent_map:party_gui" then return end
|
||||
|
||||
local player_name = player:get_player_name()
|
||||
local is_admin = is_server_admin(player_name)
|
||||
local current_party_name, current_party_data = get_player_party(player_name)
|
||||
local is_owner = current_party_name and is_party_owner(player_name, current_party_name)
|
||||
|
||||
if not gui_state[player_name] then
|
||||
gui_state[player_name] = {view_mode = "main", selected_party = nil, scroll_pos = 0}
|
||||
end
|
||||
|
||||
local state = gui_state[player_name]
|
||||
|
||||
-- Handle navigation
|
||||
if fields.close or fields.quit then
|
||||
gui_state[player_name] = nil
|
||||
return
|
||||
elseif fields.back_main then
|
||||
state.view_mode = "main"
|
||||
persistent_map.show_party_gui(player_name)
|
||||
return
|
||||
elseif fields.create_party then
|
||||
if current_party_name then
|
||||
minetest.chat_send_player(player_name, S("You are already in a party. Leave it first."))
|
||||
return
|
||||
end
|
||||
state.view_mode = "create"
|
||||
persistent_map.show_party_gui(player_name)
|
||||
return
|
||||
elseif fields.manage_party then
|
||||
if not is_owner then
|
||||
minetest.chat_send_player(player_name, S("Only party owners can manage parties."))
|
||||
return
|
||||
end
|
||||
state.view_mode = "manage"
|
||||
persistent_map.show_party_gui(player_name)
|
||||
return
|
||||
elseif fields.list_parties then
|
||||
state.view_mode = "list"
|
||||
persistent_map.show_party_gui(player_name)
|
||||
return
|
||||
elseif fields.admin_manage then
|
||||
if not is_admin then
|
||||
minetest.chat_send_player(player_name, S("Admin privileges required."))
|
||||
return
|
||||
end
|
||||
state.view_mode = "admin"
|
||||
persistent_map.show_party_gui(player_name)
|
||||
return
|
||||
end
|
||||
|
||||
-- Handle actions based on current view
|
||||
if state.view_mode == "create" then
|
||||
if fields.create_confirm then
|
||||
local party_name = fields.party_name or ""
|
||||
party_name = party_name:trim()
|
||||
|
||||
if party_name == "" then
|
||||
minetest.chat_send_player(player_name, S("Party name cannot be empty."))
|
||||
return
|
||||
end
|
||||
|
||||
-- Check if party already exists
|
||||
local parties = get_map_parties()
|
||||
if parties[party_name] then
|
||||
minetest.chat_send_player(player_name, S("Party '@1' already exists.", party_name))
|
||||
return
|
||||
end
|
||||
|
||||
-- Create the party using the chat command system
|
||||
local initial_members = fields.initial_members or ""
|
||||
local cmd_params = "create " .. party_name
|
||||
|
||||
if initial_members ~= "" then
|
||||
local members = initial_members:split("\n")
|
||||
for _, member in ipairs(members) do
|
||||
member = member:trim()
|
||||
if member ~= "" and member ~= player_name then
|
||||
cmd_params = cmd_params .. " " .. member
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Execute the mapparty command
|
||||
local success, message = minetest.registered_chatcommands["mapparty"].func(player_name, cmd_params)
|
||||
minetest.chat_send_player(player_name, message)
|
||||
|
||||
if success then
|
||||
state.view_mode = "main"
|
||||
persistent_map.show_party_gui(player_name)
|
||||
end
|
||||
end
|
||||
|
||||
elseif state.view_mode == "manage" then
|
||||
if fields.add_member then
|
||||
local member_name = fields.add_member_name or ""
|
||||
member_name = member_name:trim()
|
||||
|
||||
if member_name == "" then
|
||||
minetest.chat_send_player(player_name, S("Member name cannot be empty."))
|
||||
return
|
||||
end
|
||||
|
||||
if not is_owner then
|
||||
minetest.chat_send_player(player_name, S("Only party owners can add members."))
|
||||
return
|
||||
end
|
||||
|
||||
-- Use the mapparty command to add member
|
||||
local success, message = minetest.registered_chatcommands["mapparty"].func(
|
||||
player_name, "add " .. current_party_name .. " " .. member_name)
|
||||
minetest.chat_send_player(player_name, message)
|
||||
|
||||
if success then
|
||||
persistent_map.show_party_gui(player_name)
|
||||
end
|
||||
|
||||
elseif fields.remove_member then
|
||||
if not is_owner then
|
||||
minetest.chat_send_player(player_name, S("Only party owners can remove members."))
|
||||
return
|
||||
end
|
||||
|
||||
local selected_idx = tonumber(fields.member_list) or 0
|
||||
if selected_idx > 0 and current_party_data and selected_idx <= #current_party_data.members then
|
||||
local member_to_remove = current_party_data.members[selected_idx]
|
||||
|
||||
if member_to_remove == player_name then
|
||||
minetest.chat_send_player(player_name, S("Use 'Leave Party' to remove yourself."))
|
||||
return
|
||||
end
|
||||
|
||||
-- Use the mapparty command to remove member
|
||||
local success, message = minetest.registered_chatcommands["mapparty"].func(
|
||||
player_name, "remove " .. current_party_name .. " " .. member_to_remove)
|
||||
minetest.chat_send_player(player_name, message)
|
||||
|
||||
if success then
|
||||
persistent_map.show_party_gui(player_name)
|
||||
end
|
||||
else
|
||||
minetest.chat_send_player(player_name, S("Please select a member to remove."))
|
||||
end
|
||||
|
||||
elseif fields.delete_party then
|
||||
if not is_owner then
|
||||
minetest.chat_send_player(player_name, S("Only party owners can delete parties."))
|
||||
return
|
||||
end
|
||||
|
||||
-- Use the mapparty command to delete party
|
||||
local success, message = minetest.registered_chatcommands["mapparty"].func(
|
||||
player_name, "delete " .. current_party_name)
|
||||
minetest.chat_send_player(player_name, message)
|
||||
|
||||
if success then
|
||||
state.view_mode = "main"
|
||||
persistent_map.show_party_gui(player_name)
|
||||
end
|
||||
end
|
||||
|
||||
elseif state.view_mode == "admin" then
|
||||
if fields.admin_delete_selected then
|
||||
if not is_admin then
|
||||
minetest.chat_send_player(player_name, S("Admin privileges required."))
|
||||
return
|
||||
end
|
||||
|
||||
local selected_idx = tonumber(fields.admin_party_list) or 0
|
||||
local parties = get_map_parties()
|
||||
local party_names = {}
|
||||
for party_name, _ in pairs(parties) do
|
||||
table.insert(party_names, party_name)
|
||||
end
|
||||
table.sort(party_names)
|
||||
|
||||
if selected_idx > 0 and selected_idx <= #party_names then
|
||||
local party_to_delete = party_names[selected_idx]
|
||||
local party_data = parties[party_to_delete]
|
||||
|
||||
-- Notify all members
|
||||
for _, member_name in ipairs(party_data.members) do
|
||||
if minetest.get_player_by_name(member_name) then
|
||||
minetest.chat_send_player(member_name,
|
||||
S("Map party '@1' has been deleted by admin @2", party_to_delete, player_name))
|
||||
end
|
||||
end
|
||||
|
||||
-- Delete the party
|
||||
parties[party_to_delete] = nil
|
||||
save_map_parties(parties)
|
||||
|
||||
minetest.chat_send_player(player_name, S("Party '@1' deleted successfully.", party_to_delete))
|
||||
persistent_map.show_party_gui(player_name)
|
||||
else
|
||||
minetest.chat_send_player(player_name, S("Please select a party to delete."))
|
||||
end
|
||||
|
||||
elseif fields.admin_view_details then
|
||||
local selected_idx = tonumber(fields.admin_party_list) or 0
|
||||
local parties = get_map_parties()
|
||||
local party_names = {}
|
||||
for party_name, _ in pairs(parties) do
|
||||
table.insert(party_names, party_name)
|
||||
end
|
||||
table.sort(party_names)
|
||||
|
||||
if selected_idx > 0 and selected_idx <= #party_names then
|
||||
local party_name = party_names[selected_idx]
|
||||
local success, message = minetest.registered_chatcommands["mapparty"].func(
|
||||
player_name, "info " .. party_name)
|
||||
minetest.chat_send_player(player_name, message)
|
||||
else
|
||||
minetest.chat_send_player(player_name, S("Please select a party to view details."))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Handle main view actions
|
||||
if state.view_mode == "main" then
|
||||
if fields.leave_party then
|
||||
if not current_party_name then
|
||||
minetest.chat_send_player(player_name, S("You are not in any party."))
|
||||
return
|
||||
end
|
||||
|
||||
-- Use the mapparty command to leave party
|
||||
local success, message = minetest.registered_chatcommands["mapparty"].func(player_name, "leave")
|
||||
minetest.chat_send_player(player_name, message)
|
||||
|
||||
if success then
|
||||
persistent_map.show_party_gui(player_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Clean up GUI state when players leave
|
||||
minetest.register_on_leaveplayer(function(player)
|
||||
local player_name = player:get_player_name()
|
||||
gui_state[player_name] = nil
|
||||
end)
|
||||
|
||||
-- Add button to marker GUI to access party management
|
||||
local original_show_marker_gui = persistent_map.show_marker_gui
|
||||
if original_show_marker_gui then
|
||||
function persistent_map.show_marker_gui(player_name)
|
||||
-- Call original function first
|
||||
original_show_marker_gui(player_name)
|
||||
|
||||
-- We could modify the marker GUI to include a party management button
|
||||
-- but for now, we'll keep them separate
|
||||
end
|
||||
end
|
||||
|
||||
-- Chat command to open party GUI
|
||||
minetest.register_chatcommand("partygui", {
|
||||
description = S("Open the map party management GUI"),
|
||||
func = function(name)
|
||||
persistent_map.show_party_gui(name)
|
||||
return true, S("Party management GUI opened")
|
||||
end,
|
||||
})
|
||||
|
||||
-- Add party management button to the main map GUI
|
||||
local original_show_map = persistent_map.show_map
|
||||
if original_show_map then
|
||||
-- We'll hook into the formspec handler instead to add the button
|
||||
local original_receive_fields = minetest.registered_on_player_receive_fields
|
||||
|
||||
-- Register additional handler for map formspec to add party button
|
||||
minetest.register_on_player_receive_fields(function(player, formname, fields)
|
||||
if formname == "persistent_map:map" and fields.party_gui then
|
||||
local player_name = player:get_player_name()
|
||||
persistent_map.show_party_gui(player_name)
|
||||
return true
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
minetest.log("action", "[persistent_map] Map party GUI system loaded successfully")
|
||||
@@ -0,0 +1,527 @@
|
||||
-- Map Party System for Persistent Map Mod
|
||||
-- Allows multiple players to create map parties and automatically share discovered tiles
|
||||
-- All players in a party collectively discover and update each other's tiles
|
||||
|
||||
local S = minetest.get_translator(minetest.get_current_modname())
|
||||
|
||||
-- Map party storage
|
||||
local map_parties = {}
|
||||
local player_parties = {} -- Maps player_name -> party_name
|
||||
|
||||
-- Load map parties from storage
|
||||
local function load_map_parties()
|
||||
local storage = persistent_map.storage
|
||||
if not storage then
|
||||
return
|
||||
end
|
||||
|
||||
local data = storage:get_string("map_parties")
|
||||
if data ~= "" then
|
||||
map_parties = minetest.deserialize(data) or {}
|
||||
end
|
||||
|
||||
-- Rebuild player_parties lookup table
|
||||
player_parties = {}
|
||||
for party_name, party_data in pairs(map_parties) do
|
||||
for _, player_name in ipairs(party_data.members) do
|
||||
player_parties[player_name] = party_name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Save map parties to storage
|
||||
local function save_map_parties()
|
||||
local storage = persistent_map.storage
|
||||
if not storage then
|
||||
return false
|
||||
end
|
||||
|
||||
storage:set_string("map_parties", minetest.serialize(map_parties))
|
||||
return true
|
||||
end
|
||||
|
||||
-- Check if a party exists
|
||||
local function party_exists(party_name)
|
||||
return map_parties[party_name] ~= nil
|
||||
end
|
||||
|
||||
-- Check if a player is in a party
|
||||
local function get_player_party(player_name)
|
||||
return player_parties[player_name]
|
||||
end
|
||||
|
||||
-- Check if a player is the owner of a party
|
||||
local function is_party_owner(player_name, party_name)
|
||||
local party = map_parties[party_name]
|
||||
return party and party.owner == player_name
|
||||
end
|
||||
|
||||
-- Add a player to a party
|
||||
local function add_player_to_party(party_name, player_name)
|
||||
local party = map_parties[party_name]
|
||||
if not party then
|
||||
return false, "Party does not exist"
|
||||
end
|
||||
|
||||
-- Check if player is already in this party
|
||||
for _, member in ipairs(party.members) do
|
||||
if member == player_name then
|
||||
return false, "Player is already in this party"
|
||||
end
|
||||
end
|
||||
|
||||
-- Check if player is in another party
|
||||
if player_parties[player_name] then
|
||||
return false, "Player is already in party '" .. player_parties[player_name] .. "'"
|
||||
end
|
||||
|
||||
-- Add player to party
|
||||
table.insert(party.members, player_name)
|
||||
player_parties[player_name] = party_name
|
||||
|
||||
save_map_parties()
|
||||
return true, "Player added to party successfully"
|
||||
end
|
||||
|
||||
-- Remove a player from a party
|
||||
local function remove_player_from_party(party_name, player_name)
|
||||
local party = map_parties[party_name]
|
||||
if not party then
|
||||
return false, "Party does not exist"
|
||||
end
|
||||
|
||||
-- Find and remove player from members list
|
||||
for i, member in ipairs(party.members) do
|
||||
if member == player_name then
|
||||
table.remove(party.members, i)
|
||||
player_parties[player_name] = nil
|
||||
|
||||
-- If this was the owner and there are other members, transfer ownership
|
||||
if party.owner == player_name and #party.members > 0 then
|
||||
party.owner = party.members[1]
|
||||
end
|
||||
|
||||
-- If no members left, delete the party
|
||||
if #party.members == 0 then
|
||||
map_parties[party_name] = nil
|
||||
end
|
||||
|
||||
save_map_parties()
|
||||
return true, "Player removed from party successfully"
|
||||
end
|
||||
end
|
||||
|
||||
return false, "Player is not in this party"
|
||||
end
|
||||
|
||||
-- Check if tile exists on disk
|
||||
local function tile_exists(tile_id)
|
||||
local filename = persistent_map.map_path .. tile_id .. ".png"
|
||||
local file = io.open(filename, "r")
|
||||
if file then
|
||||
file:close()
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Share a tile with all party members (only after tile is generated)
|
||||
local function share_tile_with_party(sender_name, tile_x, tile_z, callback)
|
||||
local party_name = get_player_party(sender_name)
|
||||
if not party_name then
|
||||
return false, "You are not in a party"
|
||||
end
|
||||
|
||||
local party = map_parties[party_name]
|
||||
if not party then
|
||||
return false, "Party data not found"
|
||||
end
|
||||
|
||||
-- Check if sender has discovered this tile
|
||||
local sender_data = persistent_map.get_player_data(sender_name)
|
||||
if not sender_data then
|
||||
return false, "Sender data not found"
|
||||
end
|
||||
|
||||
local tile_id = string.format("tile_%d_%d", tile_x, tile_z)
|
||||
if not sender_data.discovered_tiles[tile_id] then
|
||||
return false, "Tile not discovered by sender"
|
||||
end
|
||||
|
||||
-- Wait for tile to be generated before sharing
|
||||
local function attempt_share()
|
||||
if not tile_exists(tile_id) then
|
||||
-- Tile doesn't exist yet, wait a bit longer
|
||||
minetest.after(0.5, attempt_share)
|
||||
return
|
||||
end
|
||||
|
||||
local storage = persistent_map.storage
|
||||
if not storage then
|
||||
if callback then callback(false, "Storage not initialized") end
|
||||
return
|
||||
end
|
||||
|
||||
local shared_count = 0
|
||||
local map_path = persistent_map.map_path
|
||||
|
||||
-- Share with all party members except the sender
|
||||
for _, member_name in ipairs(party.members) do
|
||||
if member_name ~= sender_name then
|
||||
-- Load member's data (works for offline players too)
|
||||
local member_data = storage:get_string("player_" .. member_name)
|
||||
local member_tiles = {}
|
||||
|
||||
if member_data ~= "" then
|
||||
member_tiles = minetest.deserialize(member_data) or {}
|
||||
end
|
||||
|
||||
-- Check if member already has this tile
|
||||
if not member_tiles[tile_id] then
|
||||
-- Add tile to member's discovered tiles
|
||||
member_tiles[tile_id] = {x = tile_x, z = tile_z}
|
||||
storage:set_string("player_" .. member_name, minetest.serialize(member_tiles))
|
||||
shared_count = shared_count + 1
|
||||
|
||||
-- If member is online, update their runtime data and send the tile
|
||||
local member_player = minetest.get_player_by_name(member_name)
|
||||
if member_player then
|
||||
local member_player_data = persistent_map.get_player_data(member_name)
|
||||
if member_player_data then
|
||||
member_player_data.discovered_tiles[tile_id] = {x = tile_x, z = tile_z}
|
||||
end
|
||||
|
||||
-- Send the tile texture to the member with a small delay
|
||||
minetest.after(0.1, function()
|
||||
local filename = map_path .. tile_id .. ".png"
|
||||
minetest.dynamic_add_media({
|
||||
filepath = filename,
|
||||
to_player = member_name,
|
||||
}, function(player_name)
|
||||
minetest.chat_send_player(member_name,
|
||||
S("Party member @1 discovered a new area at (@2, @3)!", sender_name, tile_x, tile_z))
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if callback then callback(true, shared_count) end
|
||||
end
|
||||
|
||||
-- Start the sharing process with initial delay
|
||||
minetest.after(0.2, attempt_share)
|
||||
return true, 0 -- Return immediately, actual sharing happens asynchronously
|
||||
end
|
||||
|
||||
-- Hook into the tile discovery system to automatically share with party members
|
||||
local original_add_discovered_tile = nil
|
||||
|
||||
-- Override the tile discovery function to add party sharing
|
||||
local function hook_tile_discovery()
|
||||
-- We need to hook into the globalstep function that calls add_discovered_tile
|
||||
-- Since we can't directly modify the original function, we'll use a different approach
|
||||
-- by monitoring player positions and sharing tiles when they're discovered
|
||||
end
|
||||
|
||||
-- Get tile coordinates from player position
|
||||
local function pos_to_tile_coords(pos)
|
||||
return math.floor(pos.x / persistent_map.tile_size),
|
||||
math.floor(pos.z / persistent_map.tile_size)
|
||||
end
|
||||
|
||||
-- Get positions of all online party members (excluding the requesting player)
|
||||
function persistent_map.get_party_member_positions(player_name)
|
||||
local party_name = get_player_party(player_name)
|
||||
if not party_name then
|
||||
return {}
|
||||
end
|
||||
|
||||
local party = map_parties[party_name]
|
||||
if not party then
|
||||
return {}
|
||||
end
|
||||
|
||||
local member_positions = {}
|
||||
for _, member_name in ipairs(party.members) do
|
||||
if member_name ~= player_name then
|
||||
local member_player = minetest.get_player_by_name(member_name)
|
||||
if member_player then
|
||||
local pos = member_player:get_pos()
|
||||
local yaw = member_player:get_look_horizontal()
|
||||
member_positions[member_name] = {
|
||||
pos = pos,
|
||||
yaw = yaw,
|
||||
name = member_name
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return member_positions
|
||||
end
|
||||
|
||||
-- Monitor player movement and auto-share newly discovered tiles
|
||||
local party_scan_timer = 0
|
||||
minetest.register_globalstep(function(dtime)
|
||||
party_scan_timer = party_scan_timer + dtime
|
||||
if party_scan_timer < persistent_map.scan_interval then return end
|
||||
party_scan_timer = 0
|
||||
|
||||
for _, player in ipairs(minetest.get_connected_players()) do
|
||||
local name = player:get_player_name()
|
||||
local party_name = get_player_party(name)
|
||||
|
||||
-- Only process players who are in parties
|
||||
if party_name then
|
||||
local player_data = persistent_map.get_player_data(name)
|
||||
if player_data then
|
||||
local pos = player:get_pos()
|
||||
local tile_x, tile_z = pos_to_tile_coords(pos)
|
||||
local tile_id = string.format("tile_%d_%d", tile_x, tile_z)
|
||||
|
||||
-- Check if this is a newly discovered tile
|
||||
if player_data.discovered_tiles[tile_id] and
|
||||
(not player_data.last_shared_tile or
|
||||
player_data.last_shared_tile ~= tile_id) then
|
||||
|
||||
-- Share this tile with party members (asynchronously)
|
||||
share_tile_with_party(name, tile_x, tile_z, function(success, shared_count)
|
||||
if success and shared_count > 0 then
|
||||
player_data.last_shared_tile = tile_id
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Chat command to create a new map party
|
||||
minetest.register_chatcommand("mapparty", {
|
||||
params = "create <party_name> [player1] [player2] [player3] ... | add <party_name> <player> | remove <party_name> <player> | delete <party_name> | list | info [party_name] | leave",
|
||||
description = S("Manage map parties for automatic tile sharing"),
|
||||
func = function(name, param)
|
||||
local parts = param:split(" ")
|
||||
if #parts == 0 or parts[1] == "" then
|
||||
return false, S("Usage: /mapparty <create|add|remove|delete|list|info|leave> [arguments]")
|
||||
end
|
||||
|
||||
local action = parts[1]:lower()
|
||||
|
||||
if action == "create" then
|
||||
if #parts < 2 then
|
||||
return false, S("Usage: /mapparty create <party_name> [player1] [player2] ...")
|
||||
end
|
||||
|
||||
local party_name = parts[2]
|
||||
|
||||
-- Check if party already exists
|
||||
if party_exists(party_name) then
|
||||
return false, S("Party '@1' already exists", party_name)
|
||||
end
|
||||
|
||||
-- Check if player is already in a party
|
||||
if get_player_party(name) then
|
||||
return false, S("You are already in party '@1'. Leave it first.", get_player_party(name))
|
||||
end
|
||||
|
||||
-- Create new party
|
||||
map_parties[party_name] = {
|
||||
owner = name,
|
||||
members = {name},
|
||||
created = os.time()
|
||||
}
|
||||
player_parties[name] = party_name
|
||||
|
||||
-- Add additional players if specified
|
||||
local added_players = {}
|
||||
for i = 3, #parts do
|
||||
local player_name = parts[i]
|
||||
if minetest.player_exists(player_name) then
|
||||
if not get_player_party(player_name) then
|
||||
table.insert(map_parties[party_name].members, player_name)
|
||||
player_parties[player_name] = party_name
|
||||
table.insert(added_players, player_name)
|
||||
|
||||
-- Notify the added player if they're online
|
||||
if minetest.get_player_by_name(player_name) then
|
||||
minetest.chat_send_player(player_name,
|
||||
S("You have been added to map party '@1' by @2", party_name, name))
|
||||
end
|
||||
else
|
||||
minetest.chat_send_player(name,
|
||||
S("Player @1 is already in another party", player_name))
|
||||
end
|
||||
else
|
||||
minetest.chat_send_player(name,
|
||||
S("Player @1 does not exist", player_name))
|
||||
end
|
||||
end
|
||||
|
||||
save_map_parties()
|
||||
|
||||
local message = S("Map party '@1' created successfully", party_name)
|
||||
if #added_players > 0 then
|
||||
message = message .. S(" with members: @1", table.concat(added_players, ", "))
|
||||
end
|
||||
return true, message
|
||||
|
||||
elseif action == "add" then
|
||||
if #parts ~= 3 then
|
||||
return false, S("Usage: /mapparty add <party_name> <player>")
|
||||
end
|
||||
|
||||
local party_name = parts[2]
|
||||
local player_name = parts[3]
|
||||
|
||||
-- Check if party exists
|
||||
if not party_exists(party_name) then
|
||||
return false, S("Party '@1' does not exist", party_name)
|
||||
end
|
||||
|
||||
-- Check if caller is the party owner
|
||||
if not is_party_owner(name, party_name) then
|
||||
return false, S("Only the party owner can add players")
|
||||
end
|
||||
|
||||
-- Check if target player exists
|
||||
if not minetest.player_exists(player_name) then
|
||||
return false, S("Player '@1' does not exist", player_name)
|
||||
end
|
||||
|
||||
local success, message = add_player_to_party(party_name, player_name)
|
||||
if success then
|
||||
-- Notify the added player if they're online
|
||||
if minetest.get_player_by_name(player_name) then
|
||||
minetest.chat_send_player(player_name,
|
||||
S("You have been added to map party '@1' by @2", party_name, name))
|
||||
end
|
||||
end
|
||||
return success, message
|
||||
|
||||
elseif action == "remove" then
|
||||
if #parts ~= 3 then
|
||||
return false, S("Usage: /mapparty remove <party_name> <player>")
|
||||
end
|
||||
|
||||
local party_name = parts[2]
|
||||
local player_name = parts[3]
|
||||
|
||||
-- Check if party exists
|
||||
if not party_exists(party_name) then
|
||||
return false, S("Party '@1' does not exist", party_name)
|
||||
end
|
||||
|
||||
-- Check if caller is the party owner or removing themselves
|
||||
if not is_party_owner(name, party_name) and name ~= player_name then
|
||||
return false, S("Only the party owner can remove other players")
|
||||
end
|
||||
|
||||
local success, message = remove_player_from_party(party_name, player_name)
|
||||
if success then
|
||||
-- Notify the removed player if they're online
|
||||
if minetest.get_player_by_name(player_name) and player_name ~= name then
|
||||
minetest.chat_send_player(player_name,
|
||||
S("You have been removed from map party '@1'", party_name))
|
||||
end
|
||||
end
|
||||
return success, message
|
||||
|
||||
elseif action == "delete" then
|
||||
if #parts ~= 2 then
|
||||
return false, S("Usage: /mapparty delete <party_name>")
|
||||
end
|
||||
|
||||
local party_name = parts[2]
|
||||
|
||||
-- Check if party exists
|
||||
if not party_exists(party_name) then
|
||||
return false, S("Party '@1' does not exist", party_name)
|
||||
end
|
||||
|
||||
-- Check if caller is the party owner
|
||||
if not is_party_owner(name, party_name) then
|
||||
return false, S("Only the party owner can delete the party")
|
||||
end
|
||||
|
||||
local party = map_parties[party_name]
|
||||
|
||||
-- Notify all members
|
||||
for _, member_name in ipairs(party.members) do
|
||||
player_parties[member_name] = nil
|
||||
if minetest.get_player_by_name(member_name) and member_name ~= name then
|
||||
minetest.chat_send_player(member_name,
|
||||
S("Map party '@1' has been deleted by @2", party_name, name))
|
||||
end
|
||||
end
|
||||
|
||||
-- Delete the party
|
||||
map_parties[party_name] = nil
|
||||
save_map_parties()
|
||||
|
||||
return true, S("Map party '@1' deleted successfully", party_name)
|
||||
|
||||
elseif action == "leave" then
|
||||
local party_name = get_player_party(name)
|
||||
if not party_name then
|
||||
return false, S("You are not in any party")
|
||||
end
|
||||
|
||||
local success, message = remove_player_from_party(party_name, name)
|
||||
return success, message
|
||||
|
||||
elseif action == "list" then
|
||||
if next(map_parties) == nil then
|
||||
return true, S("No map parties exist")
|
||||
end
|
||||
|
||||
local party_list = {}
|
||||
for party_name, party_data in pairs(map_parties) do
|
||||
table.insert(party_list, string.format("%s (%d members, owner: %s)",
|
||||
party_name, #party_data.members, party_data.owner))
|
||||
end
|
||||
|
||||
return true, S("Map parties:\n@1", table.concat(party_list, "\n"))
|
||||
|
||||
elseif action == "info" then
|
||||
local party_name = parts[2] or get_player_party(name)
|
||||
|
||||
if not party_name then
|
||||
return false, S("Usage: /mapparty info <party_name> or join a party first")
|
||||
end
|
||||
|
||||
if not party_exists(party_name) then
|
||||
return false, S("Party '@1' does not exist", party_name)
|
||||
end
|
||||
|
||||
local party = map_parties[party_name]
|
||||
local info = S("Party '@1':\n", party_name) ..
|
||||
S("Owner: @1\n", party.owner) ..
|
||||
S("Members (@1): @2\n", #party.members, table.concat(party.members, ", ")) ..
|
||||
S("Created: @1", os.date("%Y-%m-%d %H:%M:%S", party.created))
|
||||
|
||||
return true, info
|
||||
|
||||
else
|
||||
return false, S("Unknown action '@1'. Use create, add, remove, delete, list, info, or leave", action)
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
-- Initialize the system when the mod loads
|
||||
minetest.register_on_mods_loaded(function()
|
||||
load_map_parties()
|
||||
minetest.log("action", "[persistent_map] Map party system loaded with " ..
|
||||
table.maxn(map_parties) .. " parties")
|
||||
end)
|
||||
|
||||
-- Clean up when players leave
|
||||
minetest.register_on_leaveplayer(function(player)
|
||||
local name = player:get_player_name()
|
||||
-- Note: We don't automatically remove players from parties when they leave
|
||||
-- This allows offline players to remain in parties and receive shared tiles
|
||||
end)
|
||||
|
||||
minetest.log("action", "[persistent_map] Map party system loaded successfully")
|
||||
@@ -0,0 +1,794 @@
|
||||
-- Marker GUI for Persistent Map Mod
|
||||
-- Provides a list interface for managing markers with delete and teleport functionality
|
||||
|
||||
local modname = minetest.get_current_modname()
|
||||
local S = minetest.get_translator(modname)
|
||||
|
||||
-- Store active waypoint HUDs for players
|
||||
local active_waypoints = {}
|
||||
|
||||
-- GUI configuration
|
||||
local marker_gui = {
|
||||
formspec_version = 6,
|
||||
window_width = 21,
|
||||
window_height = 12,
|
||||
padding = 0.5,
|
||||
button_height = 0.8,
|
||||
button_width = 3,
|
||||
list_item_height = 1.2,
|
||||
scroll_height = 8,
|
||||
header_height = 1.5,
|
||||
footer_height = 2,
|
||||
}
|
||||
|
||||
-- Get marker list for a player
|
||||
local function get_player_markers_list(player_name)
|
||||
local player_data_entry = persistent_map.get_player_data(player_name)
|
||||
if not player_data_entry then
|
||||
return {}
|
||||
end
|
||||
|
||||
local markers = player_data_entry.markers
|
||||
local marker_list = {}
|
||||
|
||||
for marker_id, marker in pairs(markers) do
|
||||
table.insert(marker_list, {
|
||||
id = marker_id,
|
||||
name = marker.name or "Unnamed",
|
||||
x = marker.x,
|
||||
y = marker.y,
|
||||
z = marker.z,
|
||||
color_index = marker.color_index,
|
||||
timestamp = marker.timestamp
|
||||
})
|
||||
end
|
||||
|
||||
-- Sort by timestamp (newest first)
|
||||
table.sort(marker_list, function(a, b)
|
||||
return (a.timestamp or 0) > (b.timestamp or 0)
|
||||
end)
|
||||
|
||||
return marker_list
|
||||
end
|
||||
|
||||
-- Add waypoint HUD for a marker
|
||||
local function addWaypointHud(player, marker)
|
||||
local player_name = player:get_player_name()
|
||||
local wayName = marker.name or "Unnamed"
|
||||
local wayPos = vector.new(marker.x, marker.y, marker.z)
|
||||
|
||||
-- Remove existing waypoint if any
|
||||
if active_waypoints[player_name] then
|
||||
player:hud_remove(active_waypoints[player_name].hudId)
|
||||
active_waypoints[player_name] = nil
|
||||
end
|
||||
|
||||
-- Add new waypoint HUD
|
||||
local hudId = player:hud_add({
|
||||
hud_elem_type = "waypoint",
|
||||
name = wayName,
|
||||
text = "m",
|
||||
number = 0xFFFFFF,
|
||||
world_pos = wayPos,
|
||||
})
|
||||
|
||||
-- Store waypoint info
|
||||
active_waypoints[player_name] = {
|
||||
hudId = hudId,
|
||||
marker_id = marker.id,
|
||||
name = wayName
|
||||
}
|
||||
|
||||
return hudId
|
||||
end
|
||||
|
||||
-- Remove waypoint HUD for a player
|
||||
local function removeWaypointHud(player)
|
||||
local player_name = player:get_player_name()
|
||||
if active_waypoints[player_name] then
|
||||
player:hud_remove(active_waypoints[player_name].hudId)
|
||||
active_waypoints[player_name] = nil
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Check if player has an active waypoint for a specific marker
|
||||
local function hasWaypointForMarker(player_name, marker_id)
|
||||
return active_waypoints[player_name] and active_waypoints[player_name].marker_id == marker_id
|
||||
end
|
||||
|
||||
-- Check if player has teleport permission
|
||||
local function can_teleport(player_name)
|
||||
local player = minetest.get_player_by_name(player_name)
|
||||
if not player then return false end
|
||||
|
||||
-- Check for teleport privilege or if player is admin
|
||||
return minetest.check_player_privs(player_name, {teleport = true}) or
|
||||
minetest.check_player_privs(player_name, {server = true})
|
||||
end
|
||||
|
||||
-- Show marker management GUI
|
||||
function persistent_map.show_marker_gui(player_name)
|
||||
local player = minetest.get_player_by_name(player_name)
|
||||
if not player then return end
|
||||
|
||||
local markers = get_player_markers_list(player_name)
|
||||
local can_tp = can_teleport(player_name)
|
||||
|
||||
-- Calculate content height based on number of markers
|
||||
local content_height = math.max(marker_gui.scroll_height, #markers * marker_gui.list_item_height)
|
||||
|
||||
local formspec = {}
|
||||
formspec[1] = string.format("formspec_version[%d]", marker_gui.formspec_version)
|
||||
formspec[2] = string.format("size[%.2f,%.2f]", marker_gui.window_width, marker_gui.window_height)
|
||||
formspec[3] = "bgcolor[#00000080;true]"
|
||||
|
||||
-- Header
|
||||
local header_y = marker_gui.padding
|
||||
formspec[4] = string.format("label[%.2f,%.2f;Marker Management]",
|
||||
marker_gui.window_width / 2 - 3, header_y)
|
||||
formspec[5] = string.format("label[%.2f,%.2f;Total Markers: %d]",
|
||||
marker_gui.padding, header_y + 0.5, #markers)
|
||||
|
||||
-- Scrollable container for marker list
|
||||
local scroll_y = header_y + marker_gui.header_height
|
||||
formspec[6] = string.format(
|
||||
"scroll_container[%.2f,%.2f;%.2f,%.2f;marker_list;vertical;0.1]",
|
||||
marker_gui.padding, scroll_y,
|
||||
marker_gui.window_width - marker_gui.padding * 2,
|
||||
marker_gui.scroll_height
|
||||
)
|
||||
|
||||
local formspec_index = 7
|
||||
|
||||
-- List markers
|
||||
if #markers == 0 then
|
||||
formspec[formspec_index] = string.format(
|
||||
"label[%.2f,%.2f;No markers found. Create markers using the map interface (/map)]",
|
||||
1, 1
|
||||
)
|
||||
formspec_index = formspec_index + 1
|
||||
else
|
||||
for i, marker in ipairs(markers) do
|
||||
local y_pos = (i - 1) * marker_gui.list_item_height
|
||||
local color = persistent_map.marker_colors[marker.color_index] or persistent_map.marker_colors[1]
|
||||
|
||||
-- Marker color indicator
|
||||
formspec[formspec_index] = string.format(
|
||||
"box[%.2f,%.2f;%.2f,%.2f;%s]",
|
||||
0.2, y_pos + 0.1, 0.6, 0.6, color.color
|
||||
)
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
-- Marker information
|
||||
local coords_text = string.format("(%d, %d, %d)",
|
||||
math.floor(marker.x), math.floor(marker.y), math.floor(marker.z))
|
||||
local distance = ""
|
||||
|
||||
-- Calculate distance from player
|
||||
local player_pos = player:get_pos()
|
||||
if player_pos then
|
||||
local marker_pos = vector.new(marker.x, marker.y, marker.z)
|
||||
local dist = vector.distance(player_pos, marker_pos)
|
||||
distance = string.format(" - %.1fm away", dist)
|
||||
end
|
||||
|
||||
formspec[formspec_index] = string.format(
|
||||
"label[%.2f,%.2f;%s]",
|
||||
1, y_pos + 0.1, minetest.formspec_escape(marker.name)
|
||||
)
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
formspec[formspec_index] = string.format(
|
||||
"label[%.2f,%.2f;%s%s]",
|
||||
1, y_pos + 0.4, coords_text, distance
|
||||
)
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
-- Action buttons
|
||||
local has_waypoint = hasWaypointForMarker(player_name, marker.id)
|
||||
local button_count = can_tp and 5 or 4 -- 5 buttons if teleport available, 4 otherwise
|
||||
local buttons_start_x = marker_gui.window_width - marker_gui.padding * 2 - marker_gui.button_width * button_count - 0.8
|
||||
|
||||
-- Edit Name button
|
||||
formspec[formspec_index] = string.format(
|
||||
"button[%.2f,%.2f;%.2f,%.2f;edit_name_%s;Edit Name]",
|
||||
buttons_start_x,
|
||||
y_pos + 0.1, marker_gui.button_width, marker_gui.button_height,
|
||||
marker.id
|
||||
)
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
-- Edit Color button
|
||||
formspec[formspec_index] = string.format(
|
||||
"button[%.2f,%.2f;%.2f,%.2f;edit_color_%s;Edit Color]",
|
||||
buttons_start_x + marker_gui.button_width + 0.1,
|
||||
y_pos + 0.1, marker_gui.button_width, marker_gui.button_height,
|
||||
marker.id
|
||||
)
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
-- Waypoint button
|
||||
local waypoint_button_text = has_waypoint and "Remove Waypoint" or "Show Waypoint"
|
||||
local waypoint_action = has_waypoint and "remove_waypoint_" or "show_waypoint_"
|
||||
formspec[formspec_index] = string.format(
|
||||
"button[%.2f,%.2f;%.2f,%.2f;%s%s;%s]",
|
||||
buttons_start_x + marker_gui.button_width * 2 + 0.2,
|
||||
y_pos + 0.1, marker_gui.button_width, marker_gui.button_height,
|
||||
waypoint_action, marker.id, waypoint_button_text
|
||||
)
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
-- Delete button
|
||||
formspec[formspec_index] = string.format(
|
||||
"button[%.2f,%.2f;%.2f,%.2f;delete_%s;Delete]",
|
||||
buttons_start_x + marker_gui.button_width * 3 + 0.3,
|
||||
y_pos + 0.1, marker_gui.button_width, marker_gui.button_height,
|
||||
marker.id
|
||||
)
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
-- Teleport button (only if player has permission)
|
||||
if can_tp then
|
||||
formspec[formspec_index] = string.format(
|
||||
"button[%.2f,%.2f;%.2f,%.2f;teleport_%s;Teleport]",
|
||||
buttons_start_x + marker_gui.button_width * 4 + 0.4,
|
||||
y_pos + 0.1, marker_gui.button_width, marker_gui.button_height,
|
||||
marker.id
|
||||
)
|
||||
formspec_index = formspec_index + 1
|
||||
end
|
||||
|
||||
-- Separator line
|
||||
if i < #markers then
|
||||
formspec[formspec_index] = string.format(
|
||||
"box[%.2f,%.2f;%.2f,%.2f;#404040]",
|
||||
0, y_pos + marker_gui.list_item_height - 0.1,
|
||||
marker_gui.window_width - marker_gui.padding * 2, 0.05
|
||||
)
|
||||
formspec_index = formspec_index + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Close scroll container
|
||||
formspec[formspec_index] = "scroll_container_end[]"
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
-- Add marker section
|
||||
local add_marker_y = marker_gui.window_height - marker_gui.footer_height - 2.5
|
||||
|
||||
-- Add marker label
|
||||
formspec[formspec_index] = string.format(
|
||||
"label[%.2f,%.2f;Add Marker by Coordinates:]",
|
||||
marker_gui.padding, add_marker_y
|
||||
)
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
-- Coordinate input fields
|
||||
local field_width = 2.5
|
||||
local field_spacing = 0.2
|
||||
local fields_start_x = marker_gui.padding
|
||||
|
||||
-- X coordinate field
|
||||
formspec[formspec_index] = string.format(
|
||||
"field[%.2f,%.2f;%.2f,%.2f;coord_x;;]",
|
||||
fields_start_x, add_marker_y + 0.4, field_width, 0.8
|
||||
)
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
formspec[formspec_index] = string.format(
|
||||
"label[%.2f,%.2f;X:]",
|
||||
fields_start_x, add_marker_y + 0.2
|
||||
)
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
-- Y coordinate field
|
||||
local y_field_x = fields_start_x + field_width + field_spacing
|
||||
formspec[formspec_index] = string.format(
|
||||
"field[%.2f,%.2f;%.2f,%.2f;coord_y;;]",
|
||||
y_field_x, add_marker_y + 0.4, field_width, 0.8
|
||||
)
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
formspec[formspec_index] = string.format(
|
||||
"label[%.2f,%.2f;Y:]",
|
||||
y_field_x, add_marker_y + 0.2
|
||||
)
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
-- Z coordinate field
|
||||
local z_field_x = y_field_x + field_width + field_spacing
|
||||
formspec[formspec_index] = string.format(
|
||||
"field[%.2f,%.2f;%.2f,%.2f;coord_z;;]",
|
||||
z_field_x, add_marker_y + 0.4, field_width, 0.8
|
||||
)
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
formspec[formspec_index] = string.format(
|
||||
"label[%.2f,%.2f;Z:]",
|
||||
z_field_x, add_marker_y + 0.2
|
||||
)
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
-- Marker name field
|
||||
local name_field_x = z_field_x + field_width + field_spacing
|
||||
formspec[formspec_index] = string.format(
|
||||
"field[%.2f,%.2f;%.2f,%.2f;marker_name;;]",
|
||||
name_field_x, add_marker_y + 0.4, field_width, 0.8
|
||||
)
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
formspec[formspec_index] = string.format(
|
||||
"label[%.2f,%.2f;Name:]",
|
||||
name_field_x, add_marker_y + 0.2
|
||||
)
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
-- Add marker button
|
||||
local add_button_x = name_field_x + field_width + field_spacing
|
||||
formspec[formspec_index] = string.format(
|
||||
"button[%.2f,%.2f;%.2f,%.2f;add_marker;Add Marker]",
|
||||
add_button_x, add_marker_y + 0.4, marker_gui.button_width, marker_gui.button_height
|
||||
)
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
-- Footer buttons
|
||||
local footer_y = marker_gui.window_height - marker_gui.footer_height
|
||||
|
||||
-- Close button
|
||||
formspec[formspec_index] = string.format(
|
||||
"button[%.2f,%.2f;%.2f,%.2f;close;Close]",
|
||||
marker_gui.padding, footer_y,
|
||||
marker_gui.button_width, marker_gui.button_height
|
||||
)
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
-- Open map button
|
||||
formspec[formspec_index] = string.format(
|
||||
"button[%.2f,%.2f;%.2f,%.2f;open_map;Open Map]",
|
||||
marker_gui.padding + marker_gui.button_width + 0.2, footer_y,
|
||||
marker_gui.button_width, marker_gui.button_height
|
||||
)
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
-- Delete all button
|
||||
if #markers > 0 then
|
||||
formspec[formspec_index] = string.format(
|
||||
"button[%.2f,%.2f;%.2f,%.2f;delete_all_confirm;Delete All]",
|
||||
marker_gui.window_width - marker_gui.padding - marker_gui.button_width, footer_y,
|
||||
marker_gui.button_width, marker_gui.button_height
|
||||
)
|
||||
formspec_index = formspec_index + 1
|
||||
end
|
||||
|
||||
-- Permission info
|
||||
if not can_tp then
|
||||
formspec[formspec_index] = string.format(
|
||||
"label[%.2f,%.2f;Note: You need 'teleport' privilege to use teleport buttons]",
|
||||
marker_gui.padding, footer_y + marker_gui.button_height + 0.2
|
||||
)
|
||||
formspec_index = formspec_index + 1
|
||||
end
|
||||
|
||||
minetest.show_formspec(player_name, "persistent_map:marker_gui", table.concat(formspec))
|
||||
end
|
||||
|
||||
-- Show color selection dialog for a marker
|
||||
local function show_color_selection_dialog(player_name, marker_id)
|
||||
local player = minetest.get_player_by_name(player_name)
|
||||
if not player then return end
|
||||
|
||||
local player_data_entry = persistent_map.get_player_data(player_name)
|
||||
if not player_data_entry or not player_data_entry.markers[marker_id] then
|
||||
minetest.chat_send_player(player_name, "Error: Marker not found")
|
||||
return
|
||||
end
|
||||
|
||||
local marker = player_data_entry.markers[marker_id]
|
||||
local current_color_index = marker.color_index or 1
|
||||
|
||||
-- Create color selection formspec
|
||||
local formspec = {}
|
||||
formspec[1] = string.format("formspec_version[%d]", marker_gui.formspec_version)
|
||||
formspec[2] = "size[8,6]"
|
||||
formspec[3] = "bgcolor[#00000080;true]"
|
||||
|
||||
-- Header
|
||||
formspec[4] = string.format("label[4,0.5;Select Color for '%s']",
|
||||
minetest.formspec_escape(marker.name or "Unnamed"))
|
||||
|
||||
-- Color buttons in a 4x2 grid
|
||||
local colors_per_row = 4
|
||||
local button_size = 1.5
|
||||
local button_spacing = 0.2
|
||||
local start_x = 1
|
||||
local start_y = 1.5
|
||||
|
||||
local formspec_index = 5
|
||||
|
||||
for i, color_info in ipairs(persistent_map.marker_colors) do
|
||||
local row = math.floor((i - 1) / colors_per_row)
|
||||
local col = (i - 1) % colors_per_row
|
||||
local btn_x = start_x + col * (button_size + button_spacing)
|
||||
local btn_y = start_y + row * (button_size + button_spacing)
|
||||
|
||||
-- Add colored background
|
||||
formspec[formspec_index] = string.format(
|
||||
"box[%.2f,%.2f;%.2f,%.2f;%s]",
|
||||
btn_x, btn_y, button_size, button_size, color_info.color
|
||||
)
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
-- Add selection indicator for current color
|
||||
if i == current_color_index then
|
||||
formspec[formspec_index] = string.format(
|
||||
"box[%.2f,%.2f;%.2f,%.2f;#FFFFFF40]",
|
||||
btn_x - 0.1, btn_y - 0.1, button_size + 0.2, button_size + 0.2
|
||||
)
|
||||
formspec_index = formspec_index + 1
|
||||
end
|
||||
|
||||
-- Color selection button
|
||||
formspec[formspec_index] = string.format(
|
||||
"button[%.2f,%.2f;%.2f,%.2f;select_color_%d_%s;%s]",
|
||||
btn_x, btn_y, button_size, button_size, i, marker_id, color_info.name
|
||||
)
|
||||
formspec_index = formspec_index + 1
|
||||
end
|
||||
|
||||
-- Cancel button
|
||||
formspec[formspec_index] = string.format(
|
||||
"button[2,4.5;2,0.8;cancel_color_edit;Cancel]"
|
||||
)
|
||||
formspec_index = formspec_index + 1
|
||||
|
||||
minetest.show_formspec(player_name, "persistent_map:color_selection", table.concat(formspec))
|
||||
end
|
||||
|
||||
-- Show name editing dialog for a marker
|
||||
local function show_name_edit_dialog(player_name, marker_id)
|
||||
local player = minetest.get_player_by_name(player_name)
|
||||
if not player then return end
|
||||
|
||||
local player_data_entry = persistent_map.get_player_data(player_name)
|
||||
if not player_data_entry or not player_data_entry.markers[marker_id] then
|
||||
minetest.chat_send_player(player_name, "Error: Marker not found")
|
||||
return
|
||||
end
|
||||
|
||||
local marker = player_data_entry.markers[marker_id]
|
||||
local current_name = marker.name or "Unnamed"
|
||||
|
||||
-- Create name editing formspec
|
||||
local formspec = {}
|
||||
formspec[1] = string.format("formspec_version[%d]", marker_gui.formspec_version)
|
||||
formspec[2] = "size[8,4]"
|
||||
formspec[3] = "bgcolor[#00000080;true]"
|
||||
|
||||
-- Header
|
||||
formspec[4] = string.format("label[4,0.5;Edit Marker Name]")
|
||||
|
||||
-- Current name label
|
||||
formspec[5] = string.format("label[1,1.2;Current name: %s]",
|
||||
minetest.formspec_escape(current_name))
|
||||
|
||||
-- Name input field
|
||||
formspec[6] = string.format("field[1,1.8;6,0.8;new_marker_name;New name:;%s]",
|
||||
minetest.formspec_escape(current_name))
|
||||
|
||||
-- Store marker ID for the handler
|
||||
formspec[7] = string.format("field[0,0;0,0;marker_id_hidden;;%s]", marker_id)
|
||||
|
||||
-- Buttons
|
||||
formspec[8] = "button[1,2.8;2,0.8;save_name;Save]"
|
||||
formspec[9] = "button[4,2.8;2,0.8;cancel_name_edit;Cancel]"
|
||||
|
||||
minetest.show_formspec(player_name, "persistent_map:name_edit", table.concat(formspec))
|
||||
end
|
||||
|
||||
-- Handle marker GUI input
|
||||
minetest.register_on_player_receive_fields(function(player, formname, fields)
|
||||
if formname ~= "persistent_map:marker_gui" then return end
|
||||
|
||||
local player_name = player:get_player_name()
|
||||
|
||||
-- Close button
|
||||
if fields.close or fields.quit then
|
||||
return
|
||||
end
|
||||
|
||||
-- Open map button
|
||||
if fields.open_map then
|
||||
-- Reset view offset when opening map
|
||||
persistent_map.set_map_view_offset(player_name, {x = 0, z = 0})
|
||||
-- Initialize zoom level if not set
|
||||
if not persistent_map.get_map_zoom_level(player_name) then
|
||||
persistent_map.set_map_zoom_level(player_name, persistent_map.default_zoom_index)
|
||||
end
|
||||
persistent_map.show_map(player_name)
|
||||
return
|
||||
end
|
||||
|
||||
-- Delete all confirmation
|
||||
if fields.delete_all_confirm then
|
||||
local success, message = persistent_map.delete_all_markers_for_player(player_name)
|
||||
minetest.chat_send_player(player_name, message)
|
||||
if success then
|
||||
persistent_map.show_marker_gui(player_name) -- Refresh the GUI
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- Add marker by coordinates
|
||||
if fields.add_marker then
|
||||
local x_str = fields.coord_x or ""
|
||||
local y_str = fields.coord_y or ""
|
||||
local z_str = fields.coord_z or ""
|
||||
local name_str = fields.marker_name or ""
|
||||
|
||||
-- Validate coordinates
|
||||
local x = tonumber(x_str)
|
||||
local y = tonumber(y_str)
|
||||
local z = tonumber(z_str)
|
||||
|
||||
if not x or not y or not z then
|
||||
minetest.chat_send_player(player_name, "Error: Please enter valid numeric coordinates for X, Y, and Z")
|
||||
return
|
||||
end
|
||||
|
||||
-- Use default name if none provided
|
||||
if name_str == "" then
|
||||
name_str = string.format("Marker at %d,%d,%d", math.floor(x), math.floor(y), math.floor(z))
|
||||
end
|
||||
|
||||
-- Create the marker
|
||||
local marker_pos = {x = x, y = y, z = z}
|
||||
local success, message = persistent_map.add_marker_for_player(player_name, marker_pos, name_str)
|
||||
|
||||
minetest.chat_send_player(player_name, message)
|
||||
|
||||
if success then
|
||||
-- Refresh the GUI to show the new marker
|
||||
persistent_map.show_marker_gui(player_name)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- Check for individual marker actions
|
||||
for field_name, _ in pairs(fields) do
|
||||
-- Delete marker
|
||||
local delete_match = field_name:match("^delete_(.+)$")
|
||||
if delete_match then
|
||||
local marker_id = delete_match
|
||||
local player_data_entry = persistent_map.get_player_data(player_name)
|
||||
if player_data_entry and player_data_entry.markers[marker_id] then
|
||||
local marker = player_data_entry.markers[marker_id]
|
||||
player_data_entry.markers[marker_id] = nil
|
||||
persistent_map.save_markers_for_player(player_name, player_data_entry.markers)
|
||||
minetest.chat_send_player(player_name,
|
||||
string.format("Deleted marker '%s'", marker.name or "Unnamed"))
|
||||
persistent_map.show_marker_gui(player_name) -- Refresh the GUI
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- Teleport to marker
|
||||
local teleport_match = field_name:match("^teleport_(.+)$")
|
||||
if teleport_match then
|
||||
local marker_id = teleport_match
|
||||
|
||||
-- Check permission
|
||||
if not can_teleport(player_name) then
|
||||
minetest.chat_send_player(player_name,
|
||||
"You don't have permission to teleport. Need 'teleport' privilege.")
|
||||
return
|
||||
end
|
||||
|
||||
local player_data_entry = persistent_map.get_player_data(player_name)
|
||||
if player_data_entry and player_data_entry.markers[marker_id] then
|
||||
local marker = player_data_entry.markers[marker_id]
|
||||
local pos = vector.new(marker.x, marker.y + 1, marker.z) -- +1 to avoid spawning in ground
|
||||
|
||||
-- Check if position is safe (not in solid blocks)
|
||||
local node_at = minetest.get_node(pos)
|
||||
local node_above = minetest.get_node(vector.add(pos, {x=0, y=1, z=0}))
|
||||
|
||||
-- If position is not safe, try to find a safe spot nearby
|
||||
if node_at.name ~= "air" or node_above.name ~= "air" then
|
||||
-- Try to find air space above the marker
|
||||
for y_offset = 2, 10 do
|
||||
local test_pos = vector.new(marker.x, marker.y + y_offset, marker.z)
|
||||
local test_node = minetest.get_node(test_pos)
|
||||
local test_above = minetest.get_node(vector.add(test_pos, {x=0, y=1, z=0}))
|
||||
|
||||
if test_node.name == "air" and test_above.name == "air" then
|
||||
pos = test_pos
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
player:set_pos(pos)
|
||||
minetest.chat_send_player(player_name,
|
||||
string.format("Teleported to marker '%s' at %d, %d, %d",
|
||||
marker.name or "Unnamed",
|
||||
math.floor(marker.x), math.floor(marker.y), math.floor(marker.z)))
|
||||
|
||||
-- Close the GUI after teleporting
|
||||
minetest.close_formspec(player_name, "persistent_map:marker_gui")
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- Edit color for marker
|
||||
local edit_color_match = field_name:match("^edit_color_(.+)$")
|
||||
if edit_color_match then
|
||||
local marker_id = edit_color_match
|
||||
show_color_selection_dialog(player_name, marker_id)
|
||||
return
|
||||
end
|
||||
|
||||
-- Edit name for marker
|
||||
local edit_name_match = field_name:match("^edit_name_(.+)$")
|
||||
if edit_name_match then
|
||||
local marker_id = edit_name_match
|
||||
show_name_edit_dialog(player_name, marker_id)
|
||||
return
|
||||
end
|
||||
|
||||
-- Show waypoint for marker
|
||||
local show_waypoint_match = field_name:match("^show_waypoint_(.+)$")
|
||||
if show_waypoint_match then
|
||||
local marker_id = show_waypoint_match
|
||||
local player_data_entry = persistent_map.get_player_data(player_name)
|
||||
if player_data_entry and player_data_entry.markers[marker_id] then
|
||||
local marker = player_data_entry.markers[marker_id]
|
||||
marker.id = marker_id -- Ensure marker has ID for waypoint tracking
|
||||
addWaypointHud(player, marker)
|
||||
minetest.chat_send_player(player_name,
|
||||
string.format("Waypoint set for marker '%s'", marker.name or "Unnamed"))
|
||||
persistent_map.show_marker_gui(player_name) -- Refresh the GUI
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- Remove waypoint for marker
|
||||
local remove_waypoint_match = field_name:match("^remove_waypoint_(.+)$")
|
||||
if remove_waypoint_match then
|
||||
local marker_id = remove_waypoint_match
|
||||
if removeWaypointHud(player) then
|
||||
minetest.chat_send_player(player_name, "Waypoint removed")
|
||||
else
|
||||
minetest.chat_send_player(player_name, "No active waypoint found")
|
||||
end
|
||||
persistent_map.show_marker_gui(player_name) -- Refresh the GUI
|
||||
return
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Handle name editing dialog input
|
||||
minetest.register_on_player_receive_fields(function(player, formname, fields)
|
||||
if formname ~= "persistent_map:name_edit" then return end
|
||||
|
||||
local player_name = player:get_player_name()
|
||||
|
||||
-- Cancel button
|
||||
if fields.cancel_name_edit or fields.quit then
|
||||
persistent_map.show_marker_gui(player_name) -- Return to marker GUI
|
||||
return
|
||||
end
|
||||
|
||||
-- Save button
|
||||
if fields.save_name then
|
||||
local new_name = fields.new_marker_name or ""
|
||||
local marker_id = fields.marker_id_hidden or ""
|
||||
|
||||
-- Validate input
|
||||
if marker_id == "" then
|
||||
minetest.chat_send_player(player_name, "Error: Invalid marker ID")
|
||||
persistent_map.show_marker_gui(player_name)
|
||||
return
|
||||
end
|
||||
|
||||
-- Trim whitespace and validate name
|
||||
new_name = string.gsub(new_name, "^%s*(.-)%s*$", "%1")
|
||||
if new_name == "" then
|
||||
minetest.chat_send_player(player_name, "Error: Marker name cannot be empty")
|
||||
return
|
||||
end
|
||||
|
||||
-- Update marker name
|
||||
local player_data_entry = persistent_map.get_player_data(player_name)
|
||||
if player_data_entry and player_data_entry.markers[marker_id] then
|
||||
local marker = player_data_entry.markers[marker_id]
|
||||
local old_name = marker.name or "Unnamed"
|
||||
|
||||
marker.name = new_name
|
||||
persistent_map.save_markers_for_player(player_name, player_data_entry.markers)
|
||||
|
||||
minetest.chat_send_player(player_name,
|
||||
string.format("Renamed marker from '%s' to '%s'", old_name, new_name))
|
||||
|
||||
-- Return to marker GUI
|
||||
persistent_map.show_marker_gui(player_name)
|
||||
else
|
||||
minetest.chat_send_player(player_name, "Error: Marker not found")
|
||||
persistent_map.show_marker_gui(player_name)
|
||||
end
|
||||
return
|
||||
end
|
||||
end)
|
||||
|
||||
-- Handle color selection dialog input
|
||||
minetest.register_on_player_receive_fields(function(player, formname, fields)
|
||||
if formname ~= "persistent_map:color_selection" then return end
|
||||
|
||||
local player_name = player:get_player_name()
|
||||
|
||||
-- Cancel button
|
||||
if fields.cancel_color_edit or fields.quit then
|
||||
persistent_map.show_marker_gui(player_name) -- Return to marker GUI
|
||||
return
|
||||
end
|
||||
|
||||
-- Check for color selection
|
||||
for field_name, _ in pairs(fields) do
|
||||
local color_match = field_name:match("^select_color_(%d+)_(.+)$")
|
||||
if color_match then
|
||||
local color_index = tonumber(color_match)
|
||||
local marker_id = field_name:match("^select_color_%d+_(.+)$")
|
||||
|
||||
if color_index and marker_id then
|
||||
local player_data_entry = persistent_map.get_player_data(player_name)
|
||||
if player_data_entry and player_data_entry.markers[marker_id] then
|
||||
local marker = player_data_entry.markers[marker_id]
|
||||
local old_color = persistent_map.marker_colors[marker.color_index] or persistent_map.marker_colors[1]
|
||||
local new_color = persistent_map.marker_colors[color_index] or persistent_map.marker_colors[1]
|
||||
|
||||
-- Update marker color
|
||||
marker.color_index = color_index
|
||||
persistent_map.save_markers_for_player(player_name, player_data_entry.markers)
|
||||
|
||||
minetest.chat_send_player(player_name,
|
||||
string.format("Changed color of marker '%s' from %s to %s",
|
||||
marker.name or "Unnamed", old_color.name, new_color.name))
|
||||
|
||||
-- Return to marker GUI
|
||||
persistent_map.show_marker_gui(player_name)
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Register chat command to open marker GUI
|
||||
minetest.register_chatcommand("markers", {
|
||||
description = S("Open the marker management interface"),
|
||||
func = function(name)
|
||||
persistent_map.show_marker_gui(name)
|
||||
return true, S("Marker GUI opened")
|
||||
end,
|
||||
})
|
||||
|
||||
-- Also register as /marker for convenience
|
||||
minetest.register_chatcommand("marker", {
|
||||
description = S("Open the marker management interface"),
|
||||
func = function(name)
|
||||
persistent_map.show_marker_gui(name)
|
||||
return true, S("Marker GUI opened")
|
||||
end,
|
||||
})
|
||||
|
||||
-- Clean up waypoints when player leaves
|
||||
minetest.register_on_leaveplayer(function(player)
|
||||
local player_name = player:get_player_name()
|
||||
if active_waypoints[player_name] then
|
||||
active_waypoints[player_name] = nil
|
||||
end
|
||||
end)
|
||||
|
||||
minetest.log("action", "[persistent_map] Marker GUI loaded successfully")
|
||||
@@ -0,0 +1,404 @@
|
||||
-- Marker Share System for Persistent Map Mod
|
||||
-- Allows players to share discovered tiles with each other via chat commands
|
||||
|
||||
local S = minetest.get_translator(minetest.get_current_modname())
|
||||
|
||||
-- Share a discovered tile with another player
|
||||
local function share_tile_with_player(sender_name, receiver_name, tile_x, tile_z)
|
||||
-- Check if sender exists and is online
|
||||
local sender = minetest.get_player_by_name(sender_name)
|
||||
if not sender then
|
||||
return false, "Sender not found"
|
||||
end
|
||||
|
||||
-- Check if receiver exists (can be offline)
|
||||
local receiver_exists = minetest.player_exists(receiver_name)
|
||||
if not receiver_exists then
|
||||
return false, "Player '" .. receiver_name .. "' does not exist"
|
||||
end
|
||||
|
||||
-- Check if sender has discovered this tile
|
||||
if not persistent_map.get_player_data(sender_name) then
|
||||
return false, "Sender data not found"
|
||||
end
|
||||
|
||||
local sender_tiles = persistent_map.get_player_data(sender_name).discovered_tiles
|
||||
local tile_id = string.format("tile_%d_%d", tile_x, tile_z)
|
||||
|
||||
if not sender_tiles[tile_id] then
|
||||
return false, "You haven't discovered tile at coordinates " .. tile_x .. ", " .. tile_z
|
||||
end
|
||||
|
||||
-- Load receiver's data (works for offline players too)
|
||||
local storage = persistent_map.storage
|
||||
if not storage then
|
||||
return false, "Storage not initialized"
|
||||
end
|
||||
local receiver_data = storage:get_string("player_" .. receiver_name)
|
||||
local receiver_tiles = {}
|
||||
|
||||
if receiver_data ~= "" then
|
||||
receiver_tiles = minetest.deserialize(receiver_data) or {}
|
||||
end
|
||||
|
||||
-- Check if receiver already has this tile
|
||||
if receiver_tiles[tile_id] then
|
||||
return false, "Player '" .. receiver_name .. "' already has this tile"
|
||||
end
|
||||
|
||||
-- Add tile to receiver's discovered tiles
|
||||
receiver_tiles[tile_id] = {x = tile_x, z = tile_z}
|
||||
storage:set_string("player_" .. receiver_name, minetest.serialize(receiver_tiles))
|
||||
|
||||
-- If receiver is online, update their runtime data and send the tile
|
||||
local receiver = minetest.get_player_by_name(receiver_name)
|
||||
if receiver then
|
||||
local receiver_player_data = persistent_map.get_player_data(receiver_name)
|
||||
if receiver_player_data then
|
||||
receiver_player_data.discovered_tiles[tile_id] = {x = tile_x, z = tile_z}
|
||||
end
|
||||
|
||||
-- Send the tile texture to the receiver
|
||||
local map_path = persistent_map.map_path
|
||||
local filename = map_path .. tile_id .. ".png"
|
||||
|
||||
minetest.dynamic_add_media({
|
||||
filepath = filename,
|
||||
to_player = receiver_name,
|
||||
}, function(player_name)
|
||||
minetest.chat_send_player(receiver_name,
|
||||
S("@1 shared a map tile with you at coordinates @2, @3!", sender_name, tile_x, tile_z))
|
||||
end)
|
||||
end
|
||||
|
||||
return true, "Tile shared successfully with " .. receiver_name
|
||||
end
|
||||
|
||||
-- Get tile coordinates from player position
|
||||
local function pos_to_tile_coords(pos)
|
||||
return math.floor(pos.x / persistent_map.tile_size),
|
||||
math.floor(pos.z / persistent_map.tile_size)
|
||||
end
|
||||
|
||||
-- Register chat command to share current tile
|
||||
minetest.register_chatcommand("share_tile", {
|
||||
params = "<player_name>",
|
||||
description = S("Share your current discovered tile with another player"),
|
||||
func = function(name, param)
|
||||
if param == "" then
|
||||
return false, S("Usage: /share_tile <player_name>")
|
||||
end
|
||||
|
||||
local player = minetest.get_player_by_name(name)
|
||||
if not player then
|
||||
return false, S("Player not found")
|
||||
end
|
||||
|
||||
local pos = player:get_pos()
|
||||
local tile_x, tile_z = pos_to_tile_coords(pos)
|
||||
|
||||
local success, message = share_tile_with_player(name, param, tile_x, tile_z)
|
||||
return success, message
|
||||
end,
|
||||
})
|
||||
|
||||
-- Register chat command to share specific tile coordinates
|
||||
minetest.register_chatcommand("share_tile_coords", {
|
||||
params = "<player_name> <tile_x> <tile_z>",
|
||||
description = S("Share a specific discovered tile with another player by coordinates"),
|
||||
func = function(name, param)
|
||||
local parts = param:split(" ")
|
||||
if #parts ~= 3 then
|
||||
return false, S("Usage: /share_tile_coords <player_name> <tile_x> <tile_z>")
|
||||
end
|
||||
|
||||
local receiver_name = parts[1]
|
||||
local tile_x = tonumber(parts[2])
|
||||
local tile_z = tonumber(parts[3])
|
||||
|
||||
if not tile_x or not tile_z then
|
||||
return false, S("Invalid coordinates. Use numbers for tile_x and tile_z")
|
||||
end
|
||||
|
||||
local success, message = share_tile_with_player(name, receiver_name, tile_x, tile_z)
|
||||
return success, message
|
||||
end,
|
||||
})
|
||||
|
||||
-- Register chat command to list discovered tiles
|
||||
minetest.register_chatcommand("list_tiles", {
|
||||
params = "[player_name]",
|
||||
description = S("List discovered tiles (your own or another player's if you have permission)"),
|
||||
privs = {server = true}, -- Only server admins can view other players' tiles
|
||||
func = function(name, param)
|
||||
local target_name = param ~= "" and param or name
|
||||
|
||||
-- Non-admins can only view their own tiles
|
||||
if target_name ~= name and not minetest.check_player_privs(name, {server = true}) then
|
||||
return false, S("You can only view your own tiles")
|
||||
end
|
||||
|
||||
local storage = persistent_map.storage
|
||||
if not storage then
|
||||
return false, "Storage not initialized"
|
||||
end
|
||||
local player_data = storage:get_string("player_" .. target_name)
|
||||
|
||||
if player_data == "" then
|
||||
return false, S("No tile data found for player @1", target_name)
|
||||
end
|
||||
|
||||
local tiles = minetest.deserialize(player_data) or {}
|
||||
local tile_list = {}
|
||||
|
||||
for tile_id, tile_data in pairs(tiles) do
|
||||
table.insert(tile_list, string.format("(%d, %d)", tile_data.x, tile_data.z))
|
||||
end
|
||||
|
||||
if #tile_list == 0 then
|
||||
return true, S("Player @1 has no discovered tiles", target_name)
|
||||
end
|
||||
|
||||
table.sort(tile_list)
|
||||
return true, S("Player @1 has @2 discovered tiles: @3",
|
||||
target_name, #tile_list, table.concat(tile_list, ", "))
|
||||
end,
|
||||
})
|
||||
|
||||
-- Register chat command to share multiple tiles in an area
|
||||
minetest.register_chatcommand("share_area", {
|
||||
params = "<player_name> <radius>",
|
||||
description = S("Share all discovered tiles in a radius around your current position"),
|
||||
func = function(name, param)
|
||||
local parts = param:split(" ")
|
||||
if #parts ~= 2 then
|
||||
return false, S("Usage: /share_area <player_name> <radius>")
|
||||
end
|
||||
|
||||
local receiver_name = parts[1]
|
||||
local radius = tonumber(parts[2])
|
||||
|
||||
if not radius or radius < 1 or radius > 10 then
|
||||
return false, S("Radius must be a number between 1 and 10")
|
||||
end
|
||||
|
||||
local player = minetest.get_player_by_name(name)
|
||||
if not player then
|
||||
return false, S("Player not found")
|
||||
end
|
||||
|
||||
local pos = player:get_pos()
|
||||
local center_tile_x, center_tile_z = pos_to_tile_coords(pos)
|
||||
|
||||
local shared_count = 0
|
||||
local failed_count = 0
|
||||
|
||||
-- Share tiles in a square area around the player
|
||||
for dx = -radius, radius do
|
||||
for dz = -radius, radius do
|
||||
local tile_x = center_tile_x + dx
|
||||
local tile_z = center_tile_z + dz
|
||||
|
||||
local success, message = share_tile_with_player(name, receiver_name, tile_x, tile_z)
|
||||
if success then
|
||||
shared_count = shared_count + 1
|
||||
else
|
||||
failed_count = failed_count + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return true, S("Shared @1 tiles with @2 (@3 failed)",
|
||||
shared_count, receiver_name, failed_count)
|
||||
end,
|
||||
})
|
||||
|
||||
-- Register chat command to share ALL discovered tiles with another player
|
||||
minetest.register_chatcommand("sharemap", {
|
||||
params = "<player_name>",
|
||||
description = S("Share ALL your discovered tiles with another player"),
|
||||
func = function(name, param)
|
||||
if param == "" then
|
||||
return false, S("Usage: /share_all_tiles <player_name>")
|
||||
end
|
||||
|
||||
local receiver_name = param
|
||||
|
||||
-- Check if receiver exists (can be offline)
|
||||
local receiver_exists = minetest.player_exists(receiver_name)
|
||||
if not receiver_exists then
|
||||
return false, "Player '" .. receiver_name .. "' does not exist"
|
||||
end
|
||||
|
||||
-- Get sender's discovered tiles
|
||||
local sender_data = persistent_map.get_player_data(name)
|
||||
if not sender_data then
|
||||
return false, "Your player data not found"
|
||||
end
|
||||
|
||||
local sender_tiles = sender_data.discovered_tiles
|
||||
local tile_count = 0
|
||||
for _ in pairs(sender_tiles) do
|
||||
tile_count = tile_count + 1
|
||||
end
|
||||
|
||||
if tile_count == 0 then
|
||||
return false, "You have no discovered tiles to share"
|
||||
end
|
||||
|
||||
-- Confirm with sender before sharing (large operation)
|
||||
minetest.chat_send_player(name, S("You are about to share @1 tiles with @2. This may take a moment...", tile_count, receiver_name))
|
||||
|
||||
-- Load receiver's data (works for offline players too)
|
||||
local storage = persistent_map.storage
|
||||
if not storage then
|
||||
return false, "Storage not initialized"
|
||||
end
|
||||
local receiver_data = storage:get_string("player_" .. receiver_name)
|
||||
local receiver_tiles = {}
|
||||
|
||||
if receiver_data ~= "" then
|
||||
receiver_tiles = minetest.deserialize(receiver_data) or {}
|
||||
end
|
||||
|
||||
local shared_count = 0
|
||||
local already_had_count = 0
|
||||
|
||||
-- Share all tiles
|
||||
for tile_id, tile_data in pairs(sender_tiles) do
|
||||
if not receiver_tiles[tile_id] then
|
||||
-- Add tile to receiver's discovered tiles
|
||||
receiver_tiles[tile_id] = {x = tile_data.x, z = tile_data.z}
|
||||
shared_count = shared_count + 1
|
||||
else
|
||||
already_had_count = already_had_count + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- Save receiver's updated tiles
|
||||
if shared_count > 0 then
|
||||
storage:set_string("player_" .. receiver_name, minetest.serialize(receiver_tiles))
|
||||
end
|
||||
|
||||
-- If receiver is online, update their runtime data and send the tiles
|
||||
local receiver = minetest.get_player_by_name(receiver_name)
|
||||
if receiver and shared_count > 0 then
|
||||
local receiver_player_data = persistent_map.get_player_data(receiver_name)
|
||||
if receiver_player_data then
|
||||
-- Update runtime data
|
||||
for tile_id, tile_data in pairs(sender_tiles) do
|
||||
if not receiver_player_data.discovered_tiles[tile_id] then
|
||||
receiver_player_data.discovered_tiles[tile_id] = {x = tile_data.x, z = tile_data.z}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Send all new tile textures to the receiver
|
||||
local map_path = persistent_map.map_path
|
||||
local tiles_to_send = {}
|
||||
|
||||
for tile_id, tile_data in pairs(sender_tiles) do
|
||||
if receiver_tiles[tile_id] and not (already_had_count > 0 and receiver_tiles[tile_id] == tile_data) then
|
||||
local filename = map_path .. tile_id .. ".png"
|
||||
table.insert(tiles_to_send, {
|
||||
filepath = filename,
|
||||
to_player = receiver_name,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- Send tiles in batches to avoid overwhelming the system
|
||||
local function send_tile_batch(batch_start)
|
||||
local batch_size = 10 -- Send 10 tiles at a time
|
||||
local batch_end = math.min(batch_start + batch_size - 1, #tiles_to_send)
|
||||
|
||||
if batch_start > #tiles_to_send then
|
||||
-- All tiles sent, notify receiver
|
||||
minetest.chat_send_player(receiver_name,
|
||||
S("@1 shared @2 map tiles with you! (@3 were already discovered)",
|
||||
name, shared_count, already_had_count))
|
||||
return
|
||||
end
|
||||
|
||||
for i = batch_start, batch_end do
|
||||
minetest.dynamic_add_media(tiles_to_send[i], function(player_name)
|
||||
-- Continue with next batch when this batch is complete
|
||||
if i == batch_end then
|
||||
minetest.after(0.1, function()
|
||||
send_tile_batch(batch_end + 1)
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
if #tiles_to_send > 0 then
|
||||
send_tile_batch(1)
|
||||
else
|
||||
minetest.chat_send_player(receiver_name,
|
||||
S("@1 shared @2 map tiles with you! (@3 were already discovered)",
|
||||
name, shared_count, already_had_count))
|
||||
end
|
||||
end
|
||||
|
||||
local result_message = S("Shared @1 tiles with @2", shared_count, receiver_name)
|
||||
if already_had_count > 0 then
|
||||
result_message = result_message .. S(" (@1 were already discovered)", already_had_count)
|
||||
end
|
||||
|
||||
return true, result_message
|
||||
end,
|
||||
})
|
||||
|
||||
-- Register chat command to accept/reject tile shares (for future expansion)
|
||||
minetest.register_chatcommand("tile_info", {
|
||||
params = "[tile_x] [tile_z]",
|
||||
description = S("Get information about a tile (current tile if no coordinates given)"),
|
||||
func = function(name, param)
|
||||
local player = minetest.get_player_by_name(name)
|
||||
if not player then
|
||||
return false, S("Player not found")
|
||||
end
|
||||
|
||||
local tile_x, tile_z
|
||||
|
||||
if param == "" then
|
||||
-- Use current position
|
||||
local pos = player:get_pos()
|
||||
tile_x, tile_z = pos_to_tile_coords(pos)
|
||||
else
|
||||
local parts = param:split(" ")
|
||||
if #parts ~= 2 then
|
||||
return false, S("Usage: /tile_info [tile_x] [tile_z]")
|
||||
end
|
||||
|
||||
tile_x = tonumber(parts[1])
|
||||
tile_z = tonumber(parts[2])
|
||||
|
||||
if not tile_x or not tile_z then
|
||||
return false, S("Invalid coordinates")
|
||||
end
|
||||
end
|
||||
|
||||
-- Check if player has discovered this tile
|
||||
local player_data = persistent_map.get_player_data(name)
|
||||
if not player_data then
|
||||
return false, S("Player data not found")
|
||||
end
|
||||
|
||||
local tile_id = string.format("tile_%d_%d", tile_x, tile_z)
|
||||
local discovered = player_data.discovered_tiles[tile_id] ~= nil
|
||||
|
||||
-- Convert tile coordinates to world coordinates (center of tile)
|
||||
local world_x = tile_x * persistent_map.tile_size + persistent_map.tile_size / 2
|
||||
local world_z = tile_z * persistent_map.tile_size + persistent_map.tile_size / 2
|
||||
|
||||
local info = S("Tile (@1, @2):\n", tile_x, tile_z) ..
|
||||
S("World center: (@1, @2)\n", math.floor(world_x), math.floor(world_z)) ..
|
||||
S("Discovered: @1", discovered and S("Yes") or S("No"))
|
||||
|
||||
return true, info
|
||||
end,
|
||||
})
|
||||
|
||||
minetest.log("action", "[persistent_map] Marker share system loaded successfully")
|
||||
@@ -0,0 +1,5 @@
|
||||
name = discovery_maps
|
||||
title = Discovery Maps
|
||||
description = Adds a fullscreen mapping GUI system with markers/waypoints
|
||||
supported_games = *, minetest_game, nodecore, mineclonia, mineclone2, voxelibre
|
||||
author = TomCon
|
||||
@@ -0,0 +1,338 @@
|
||||
-- Node Color Mapping System for Luanti
|
||||
-- Dynamically generates colors based on node properties, keywords, and groups
|
||||
|
||||
local nodecolors = {}
|
||||
|
||||
-- ============================================================================
|
||||
-- COLOR PALETTE - 16 base colors for all terrain types
|
||||
-- ============================================================================
|
||||
local PALETTE = {
|
||||
GRASS_GREEN = {95, 159, 53},
|
||||
DIRT_BROWN = {134, 96, 67},
|
||||
STONE_GRAY = {128, 128, 128},
|
||||
SAND_TAN = {194, 178, 128},
|
||||
DESERT_SAND = {210, 180, 140},
|
||||
WATER_BLUE = {38, 92, 255},
|
||||
SNOW_WHITE = {240, 240, 240},
|
||||
ICE_BLUE = {175, 210, 255},
|
||||
TREE_BROWN = {101, 84, 50},
|
||||
LEAVES_GREEN = {60, 100, 30},
|
||||
LAVA_ORANGE = {255, 100, 0},
|
||||
COAL_BLACK = {64, 64, 64},
|
||||
IRON_GRAY = {140, 120, 110},
|
||||
GOLD_YELLOW = {220, 180, 50},
|
||||
GRAVEL_GRAY = {136, 126, 126},
|
||||
DEFAULT_GRAY = {100, 100, 100}
|
||||
}
|
||||
|
||||
-- ============================================================================
|
||||
-- MANUAL OVERRIDES - Highest priority, exact node name matches
|
||||
-- ============================================================================
|
||||
local manual_overrides = {
|
||||
-- Special cases that don't fit auto-detection well
|
||||
["default:apple"] = PALETTE.GOLD_YELLOW,
|
||||
["mcl_core:obsidian"] = {20, 10, 30},
|
||||
["mcl_core:bedrock"] = {40, 40, 40},
|
||||
["mcl_nether:netherrack"] = {100, 50, 50},
|
||||
["mcl_nether:soul_sand"] = {80, 70, 60},
|
||||
["mcl_end:end_stone"] = {220, 220, 180},
|
||||
["nc_fire:ash"] = {80, 80, 80},
|
||||
["nc_concrete:concrete"] = {160, 160, 160},
|
||||
}
|
||||
|
||||
-- ============================================================================
|
||||
-- KEYWORD DETECTION - Pattern matching in node names
|
||||
-- ============================================================================
|
||||
local keyword_colors = {
|
||||
-- Grass and vegetation
|
||||
{pattern = "grass", color = PALETTE.GRASS_GREEN},
|
||||
{pattern = "turf", color = PALETTE.GRASS_GREEN},
|
||||
{pattern = "tallgrass", color = PALETTE.GRASS_GREEN},
|
||||
{pattern = "sedge", color = PALETTE.GRASS_GREEN},
|
||||
-- Dirt and soil
|
||||
{pattern = "dirt", color = PALETTE.DIRT_BROWN},
|
||||
{pattern = "soil", color = PALETTE.DIRT_BROWN},
|
||||
{pattern = "podzol", color = {90, 70, 50}},
|
||||
{pattern = "coarse_dirt", color = {120, 85, 60}},
|
||||
{pattern = "mycelium", color = {110, 90, 110}},
|
||||
{pattern = "clay", color = {160, 160, 180}},
|
||||
|
||||
-- Stone types
|
||||
{pattern = "stone", color = PALETTE.STONE_GRAY},
|
||||
{pattern = "cobble", color = PALETTE.DEFAULT_GRAY},
|
||||
{pattern = "mossycobble", color = {80, 120, 80}},
|
||||
{pattern = "granite", color = {150, 100, 80}},
|
||||
{pattern = "andesite", color = {130, 130, 130}},
|
||||
{pattern = "diorite", color = {180, 180, 180}},
|
||||
{pattern = "annealed", color = {150, 150, 150}},
|
||||
{pattern = "pumice", color = {200, 190, 180}},
|
||||
{pattern = "lode", color = {110, 110, 110}},
|
||||
|
||||
-- Sand and desert
|
||||
{pattern = "sand", color = PALETTE.SAND_TAN},
|
||||
{pattern = "redsand", color = PALETTE.DESERT_SAND},
|
||||
{pattern = "desert", color = PALETTE.DESERT_SAND},
|
||||
|
||||
-- Water
|
||||
{pattern = "water", color = PALETTE.WATER_BLUE},
|
||||
{pattern = "river_water", color = {20, 100, 200}},
|
||||
|
||||
-- Snow and ice
|
||||
{pattern = "snow", color = PALETTE.SNOW_WHITE},
|
||||
{pattern = "ice", color = PALETTE.ICE_BLUE},
|
||||
{pattern = "packed_ice", color = {160, 200, 255}},
|
||||
|
||||
-- Trees and wood
|
||||
{pattern = "tree", color = PALETTE.TREE_BROWN},
|
||||
{pattern = "trunk", color = PALETTE.TREE_BROWN},
|
||||
{pattern = "bark", color = {90, 70, 45}},
|
||||
{pattern = "root", color = {80, 60, 40}},
|
||||
{pattern = "log", color = PALETTE.TREE_BROWN},
|
||||
{pattern = "wood", color = PALETTE.TREE_BROWN},
|
||||
|
||||
-- Specific tree types
|
||||
{pattern = "pine", color = {85, 70, 45}},
|
||||
{pattern = "spruce", color = {85, 70, 45}},
|
||||
{pattern = "jungle", color = {120, 90, 50}},
|
||||
{pattern = "birch", color = {220, 220, 180}},
|
||||
{pattern = "acacia", color = {160, 100, 60}},
|
||||
{pattern = "dark", color = {60, 40, 20}},
|
||||
|
||||
-- Leaves
|
||||
{pattern = "leaves", color = PALETTE.LEAVES_GREEN},
|
||||
{pattern = "needles", color = {45, 80, 25}},
|
||||
{pattern = "jungleleaves", color = {50, 90, 30}},
|
||||
{pattern = "birchleaves", color = {80, 120, 50}},
|
||||
{pattern = "acacialeaves", color = {90, 110, 40}},
|
||||
{pattern = "darkleaves", color = {40, 70, 20}},
|
||||
|
||||
-- Ores
|
||||
{pattern = "coal", color = PALETTE.COAL_BLACK},
|
||||
{pattern = "iron", color = PALETTE.IRON_GRAY},
|
||||
{pattern = "copper", color = {180, 120, 80}},
|
||||
{pattern = "gold", color = PALETTE.GOLD_YELLOW},
|
||||
{pattern = "diamond", color = {100, 200, 255}},
|
||||
{pattern = "emerald", color = {50, 200, 100}},
|
||||
{pattern = "lapis", color = {50, 100, 200}},
|
||||
{pattern = "redstone", color = {200, 50, 50}},
|
||||
|
||||
-- Lava and gravel
|
||||
{pattern = "lava", color = PALETTE.LAVA_ORANGE},
|
||||
{pattern = "gravel", color = PALETTE.GRAVEL_GRAY},
|
||||
|
||||
-- Flora
|
||||
{pattern = "flower", color = {200, 100, 150}},
|
||||
{pattern = "junglegrass", color = {40, 80, 20}},
|
||||
|
||||
-- Farmland
|
||||
{pattern = "farmland", color = {120, 80, 50}},
|
||||
{pattern = "grass_path", color = {140, 120, 80}},
|
||||
}
|
||||
|
||||
-- ============================================================================
|
||||
-- GROUP-BASED DETECTION - Fallback based on node groups
|
||||
-- ============================================================================
|
||||
local group_colors = {
|
||||
soil = PALETTE.DIRT_BROWN,
|
||||
sand = PALETTE.SAND_TAN,
|
||||
stone = PALETTE.STONE_GRAY,
|
||||
tree = PALETTE.TREE_BROWN,
|
||||
leaves = PALETTE.LEAVES_GREEN,
|
||||
water = PALETTE.WATER_BLUE,
|
||||
lava = PALETTE.LAVA_ORANGE,
|
||||
ice = PALETTE.ICE_BLUE,
|
||||
snow = PALETTE.SNOW_WHITE,
|
||||
}
|
||||
|
||||
-- ============================================================================
|
||||
-- COLOR CACHE - Store computed colors to avoid recalculation
|
||||
-- ============================================================================
|
||||
local color_cache = {}
|
||||
|
||||
-- ============================================================================
|
||||
-- HELPER FUNCTIONS
|
||||
-- ============================================================================
|
||||
|
||||
-- Check if a string contains a pattern (case-insensitive)
|
||||
local function contains_pattern(str, pattern)
|
||||
return str:lower():find(pattern:lower(), 1, true) ~= nil
|
||||
end
|
||||
|
||||
-- Detect color based on keywords in node name
|
||||
local function detect_color_from_keywords(nodename)
|
||||
for _, entry in ipairs(keyword_colors) do
|
||||
if contains_pattern(nodename, entry.pattern) then
|
||||
return entry.color
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Detect color based on node groups
|
||||
local function detect_color_from_groups(nodename)
|
||||
local node_def = minetest.registered_nodes[nodename]
|
||||
if not node_def or not node_def.groups then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Check each group that has a color mapping
|
||||
for group, color in pairs(group_colors) do
|
||||
if node_def.groups[group] and node_def.groups[group] > 0 then
|
||||
return color
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- PUBLIC API
|
||||
-- ============================================================================
|
||||
|
||||
-- Get color for a node (main function)
|
||||
function nodecolors.get_color(nodename)
|
||||
-- Return from cache if available
|
||||
if color_cache[nodename] then
|
||||
return color_cache[nodename]
|
||||
end
|
||||
|
||||
local color = nil
|
||||
|
||||
-- Priority 1: Manual overrides
|
||||
if manual_overrides[nodename] then
|
||||
color = manual_overrides[nodename]
|
||||
end
|
||||
|
||||
-- Priority 2: Keyword detection
|
||||
if not color then
|
||||
color = detect_color_from_keywords(nodename)
|
||||
end
|
||||
|
||||
-- Priority 3: Group-based detection
|
||||
if not color then
|
||||
color = detect_color_from_groups(nodename)
|
||||
end
|
||||
|
||||
-- Priority 4: Default color
|
||||
if not color then
|
||||
color = PALETTE.DEFAULT_GRAY
|
||||
end
|
||||
|
||||
-- Cache the result
|
||||
color_cache[nodename] = color
|
||||
|
||||
return color
|
||||
end
|
||||
|
||||
-- Set a manual override color for a specific node
|
||||
function nodecolors.set_color(nodename, color)
|
||||
manual_overrides[nodename] = color
|
||||
color_cache[nodename] = color
|
||||
end
|
||||
|
||||
-- Check if a node has a defined color (not default)
|
||||
function nodecolors.has_color(nodename)
|
||||
local color = nodecolors.get_color(nodename)
|
||||
return color ~= PALETTE.DEFAULT_GRAY
|
||||
end
|
||||
|
||||
-- Add a new keyword pattern and color
|
||||
function nodecolors.add_keyword(pattern, color)
|
||||
table.insert(keyword_colors, {pattern = pattern, color = color})
|
||||
end
|
||||
|
||||
-- Add a new group color mapping
|
||||
function nodecolors.add_group_color(group, color)
|
||||
group_colors[group] = color
|
||||
end
|
||||
|
||||
-- Get the color palette (for external customization)
|
||||
function nodecolors.get_palette()
|
||||
return PALETTE
|
||||
end
|
||||
|
||||
-- Clear the color cache (useful after bulk changes)
|
||||
function nodecolors.clear_cache()
|
||||
color_cache = {}
|
||||
end
|
||||
|
||||
-- Get statistics about color detection
|
||||
function nodecolors.get_stats()
|
||||
local stats = {
|
||||
manual_overrides = 0,
|
||||
keyword_patterns = 0,
|
||||
group_mappings = 0,
|
||||
cached_colors = 0
|
||||
}
|
||||
|
||||
for _ in pairs(manual_overrides) do
|
||||
stats.manual_overrides = stats.manual_overrides + 1
|
||||
end
|
||||
|
||||
stats.keyword_patterns = #keyword_colors
|
||||
|
||||
for _ in pairs(group_colors) do
|
||||
stats.group_mappings = stats.group_mappings + 1
|
||||
end
|
||||
|
||||
for _ in pairs(color_cache) do
|
||||
stats.cached_colors = stats.cached_colors + 1
|
||||
end
|
||||
|
||||
return stats
|
||||
end
|
||||
|
||||
-- Debug function: show how a color was determined
|
||||
function nodecolors.debug_color(nodename)
|
||||
local info = {
|
||||
nodename = nodename,
|
||||
color = nil,
|
||||
method = "none",
|
||||
details = ""
|
||||
}
|
||||
|
||||
-- Check manual override
|
||||
if manual_overrides[nodename] then
|
||||
info.color = manual_overrides[nodename]
|
||||
info.method = "manual_override"
|
||||
return info
|
||||
end
|
||||
|
||||
-- Check keywords
|
||||
local keyword_color = detect_color_from_keywords(nodename)
|
||||
if keyword_color then
|
||||
info.color = keyword_color
|
||||
info.method = "keyword"
|
||||
for _, entry in ipairs(keyword_colors) do
|
||||
if contains_pattern(nodename, entry.pattern) then
|
||||
info.details = "matched pattern: " .. entry.pattern
|
||||
break
|
||||
end
|
||||
end
|
||||
return info
|
||||
end
|
||||
|
||||
-- Check groups
|
||||
local group_color = detect_color_from_groups(nodename)
|
||||
if group_color then
|
||||
info.color = group_color
|
||||
info.method = "group"
|
||||
local node_def = minetest.registered_nodes[nodename]
|
||||
if node_def and node_def.groups then
|
||||
for group, color in pairs(group_colors) do
|
||||
if node_def.groups[group] and node_def.groups[group] > 0 then
|
||||
info.details = "matched group: " .. group
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
return info
|
||||
end
|
||||
|
||||
-- Default
|
||||
info.color = PALETTE.DEFAULT_GRAY
|
||||
info.method = "default"
|
||||
return info
|
||||
end
|
||||
|
||||
return nodecolors
|
||||
Reference in New Issue
Block a user