mods that hopefully work for a TechAge-based pack

This commit is contained in:
2026-09-09 06:27:51 -06:00
parent 4e6b08d194
commit c27db2a537
6234 changed files with 375578 additions and 0 deletions
+9
View File
@@ -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.
+15
View File
@@ -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!
![Screenshot showing fullscreen map zoomed in](images/README/fullmap1.png "title")
![Screenshot showing file directory structure](images/README/fullmap2.png "title")
Visible markers with easy teleport (if you have permissions!)
![Screenshot showing file directory structure](images/README/markergui1.png "title")
Discover the world with your friends!
![Screenshot showing file directory structure](images/README/mapparty1.png "title")
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
+582
View File
@@ -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")
+527
View File
@@ -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")
+794
View File
@@ -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")
+404
View File
@@ -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")
+5
View File
@@ -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
+338
View File
@@ -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