Files

1389 lines
53 KiB
Lua

-- Persistent Fullscreen Map Mod for Luanti
persistent_map = {}
-- Configuration
persistent_map.tile_size = 128 -- blocks per tile side
persistent_map.map_image_size = 128 -- pixels per tile image
persistent_map.scan_interval = 0.5 -- seconds between discovery checks
persistent_map.max_height = 80
persistent_map.min_height = -10
persistent_map.gui_tile_size = 5.0 -- formspec size for each tile (much larger)
persistent_map.view_radius = 6 -- tiles in each direction from center (13x13 grid, much bigger)
persistent_map.marker_scale = 7 -- Scale factor for markers (configurable)
persistent_map.player_marker_scale = 4 -- Scale factor for player marker (configurable)
persistent_map.marker_name_scale = 2.0 -- Base scale for marker name text (configurable)
persistent_map.marker_name_font_multiplier = 16 -- Font size multiplier for marker names (configurable)
persistent_map.marker_name_padding = 0.1 -- Padding between marker and name text
persistent_map.marker_name_min_zoom = 0.5 -- Minimum zoom level to show marker names
persistent_map.marker_name_zoom_multiplier = 1.5 -- Multiplier for text scale based on zoom
persistent_map.marker_name_char_width = 0.2 -- Character width estimation for text centering
persistent_map.marker_name_text_height = 0.3 -- Text height for positioning
-- Formspec UI Configuration
persistent_map.ui = {
button_size = 5, -- Size of navigation, zoom, and delete buttons
color_button_size = 5, -- Size of marker color selection buttons
side_panel_width = 15.0, -- Width of left and right control panels
padding = 1.0, -- General padding around UI elements
header_height = 1.0, -- Height of the header area
-- Bottom info section configuration
info_bottom_margin = 4, -- Distance from bottom of formspec to info section
info_line_spacing = 2.0, -- Vertical spacing between info lines
info_left_align_with_panel = true, -- Whether to align left info with left panel (true) or use padding (false)
-- Navigation panel configuration
nav_panel_offset = 2, -- Vertical offset for navigation panel from header
nav_section_spacing = 1, -- Spacing between navigation sections
nav_button_spacing = 0.2, -- Spacing between navigation buttons
nav_button_multiplier = 1.2, -- Multiplier for west button positioning
nav_center_button_offset = 1, -- Offset for center button from other nav buttons
zoom_section_offset = 2, -- Offset for zoom section from navigation buttons
zoom_button_spacing = 0.5, -- Spacing for zoom buttons
-- Marker panel configuration
marker_name_label_spacing = 0.8, -- Spacing between "Marker Name:" label and input field
marker_name_field_offset = 0.5, -- Offset for marker name input field
marker_name_field_height = 2, -- Height of marker name input field
marker_colors_offset = 3.5, -- Offset for marker color buttons from name field
colors_per_row = 2, -- Number of color buttons per row
color_button_spacing = 0.5, -- Horizontal spacing between color buttons
color_button_row_spacing = 0.3, -- Vertical spacing between color button rows
delete_buttons_offset = 1, -- Offset for delete buttons from color buttons
delete_button_spacing = 0.3, -- Spacing between delete buttons
-- Layout configuration
formspec_version = 6, -- Formspec version to use
min_formspec_height = 25, -- Minimum height for formspec
header_title_offset = 4, -- Offset for centering header title
scroll_content_center = 0.5, -- Centering factor for scroll content
field_width_offset = 0.5, -- Width offset for input fields
button_width_offset = 0.5, -- Width offset for buttons
-- Hardcoded value replacements
half_divisor = 2, -- Divisor for calculating half values (button_size / 2)
arrow_height_factor = 0.5, -- Factor for arrow height calculation in player marker
total_width_factor = 0.5, -- Factor for calculating half total width
padding_multiplier = 2, -- Multiplier for padding calculations
}
-- Player marker configuration
persistent_map.player_marker = {
arrow_size = 0.25, -- Base size of player arrow
arrow_height = 0.35, -- Height of player arrow
main_body_size = 0.2, -- Size of main body square
tip_size = 0.14, -- Size of directional tip
mid_section_size = 0.16, -- Size of mid-section indicator
wing_size = 0.1, -- Size of wing indicators
mid_section_factor = 0.5, -- Factor for mid-section positioning
wing_factor = 0.6, -- Factor for wing positioning
}
-- Map marker configuration
persistent_map.map_marker = {
scale_factor = 0.15, -- Base scale factor for markers
half_size_factor = 0.5, -- Factor for calculating half marker size
text_center_factor = 0.6, -- Factor for centering text -(pos offsets text left)
position_flip_factor = 1.0, -- Factor for flipping Z position (1.0 = flip, 0.0 = no flip)
}
-- Map generation configuration
persistent_map.generation = {
default_color_r = 100, -- Default color red component
default_color_g = 100, -- Default color green component
default_color_b = 100, -- Default color blue component
alpha_value = 255, -- Alpha value for colors
min_radius = 2, -- Minimum radius for view calculations
y_scan_step = -1, -- Step for Y scanning (negative = top to bottom)
tile_center_offset = 2, -- Offset for tile center calculation (tile_size / 2)
initial_discovery_delay = 0.1, -- Delay for initial tile discovery on player join
}
-- Zoom configuration
-- At max zoom, one tile should fill the screen (map area is ~65 units, base tile is 5, so need ~13x zoom)
persistent_map.zoom_levels = {0.5, 1.0, 2.0, 4.0, 6.0, 8.0, 10.0, 13.0, 16.0, 20.0} -- Available zoom levels
persistent_map.default_zoom_index = 2 -- Default to 1.0x zoom (index 2)
persistent_map.min_zoom_index = 1
persistent_map.max_zoom_index = 10
-- Zoom level offsets for proper player centering
-- These offsets compensate for the player marker appearing off-center at higher zoom levels
-- Values correspond to navigation button clicks: East = +X, South = -Z
persistent_map.zoom_offsets = {
[1] = {x = 0, z = 0}, -- 0.5x zoom - no offset needed
[2] = {x = 0, z = 0}, -- 1.0x zoom - no offset needed
[3] = {x = 0, z = 0}, -- 2.0x zoom - no offset needed
[4] = {x = 1, z = -1}, -- 4.0x zoom - 1 east, 1 south (1 east = +1 x, 1 south = -1 z)
[5] = {x = 2, z = -1}, -- 6.0x zoom - 1 east, 1 south
[6] = {x = 2, z = -2}, -- 8.0x zoom - 2 east, 1 south (2 east = +2 x, 1 south = -1 z)
[7] = {x = 2, z = -2}, -- 10.0x zoom - 2 east, 2 south (2 east = +2 x, 2 south = -2 z)
[8] = {x = 2, z = -2}, -- 13.0x zoom - 2 east, 2 south
[9] = {x = 2, z = -2}, -- 16.0x zoom - 2 east, 2 south
[10] = {x = 2, z = -2}, -- 20.0x zoom - 2 east, 2 south
}
-- Zoom level X offsets for marker name text positioning
-- These offsets adjust marker name text X position at different zoom levels
-- Positive values move text right, negative values move text left
persistent_map.marker_name_zoom_x_offsets = {
[1] = 0.0, -- 0.5x zoom - no offset needed
[2] = 0.0, -- 1.0x zoom - no offset needed
[3] = 0.0, -- 2.0x zoom - no offset needed
[4] = 5, -- 4.0x zoom - slight right offset
[5] = 10, -- 6.0x zoom - moderate right offset
[6] = 20, -- 8.0x zoom - more right offset
[7] = 25, -- 10.0x zoom - significant right offset
[8] = 30, -- 13.0x zoom - large right offset
[9] = 30, -- 16.0x zoom - very large right offset
[10] = 30, -- 20.0x zoom - maximum right offset
}
-- World configuration for total tile calculation
persistent_map.world_size = 64000 -- blocks per world side (64000x64000 world)
persistent_map.total_world_tiles = math.floor(persistent_map.world_size / persistent_map.tile_size) ^ 2 -- Total tiles in world
-- Marker system configuration
persistent_map.marker_colors = {
{name = "Red", color = "#FF0000"},
{name = "Blue", color = "#0000FF"},
{name = "Green", color = "#00FF00"},
{name = "Yellow", color = "#FFFF00"},
{name = "Purple", color = "#FF00FF"},
{name = "Orange", color = "#FF8000"},
{name = "White", color = "#FFFFFF"},
{name = "Pink", color = "#FF80FF"}
}
persistent_map.marker_delete_distance = 25 -- blocks within which player can delete markers
local modname = minetest.get_current_modname()
local modpath = minetest.get_modpath(modname)
local S = minetest.get_translator(modname)
-- Storage
local storage = minetest.get_mod_storage()
local worldpath = minetest.get_worldpath()
local map_path = worldpath .. "/persistent_maps/"
minetest.mkdir(map_path)
-- Make storage accessible to other modules
persistent_map.storage = storage
persistent_map.map_path = map_path
-- Player data - isolated per player
local player_data = {}
local map_view_offset = {} -- Stores view offset for each player
local map_zoom_level = {} -- Stores zoom level index for each player
local nodecolors = dofile(modpath .. "/nodecolors.lua")
-- Cache for node colors to avoid repeated lookups
local node_color_cache = {}
-- Get color for a node with caching
local function get_node_color(nodename)
local cached_color = node_color_cache[nodename]
if cached_color then
return cached_color
end
local color = nodecolors.get_color(nodename)
node_color_cache[nodename] = color
return color
end
-- Convert world position to tile coordinates with proper orientation
-- X increases East, Z increases North
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
-- Convert tile coordinates to world position (center of tile)
local function tile_coords_to_pos(tile_x, tile_z)
return vector.new(
tile_x * persistent_map.tile_size + persistent_map.tile_size / 2,
0,
tile_z * persistent_map.tile_size + persistent_map.tile_size / 2
)
end
-- Get tile ID from coordinates
local function get_tile_id(tile_x, tile_z)
return string.format("tile_%d_%d", tile_x, tile_z)
end
-- Check if tile exists on disk
local function tile_exists(tile_id)
local filename = map_path .. tile_id .. ".png"
local file = io.open(filename, "r")
if file then
file:close()
return true
end
return false
end
-- Marker system functions
local function load_player_markers(player_name)
local data = storage:get_string("markers_" .. player_name)
if data == "" then
return {}
end
local markers = minetest.deserialize(data) or {}
-- Migrate existing markers to include names if they don't have them
for marker_id, marker in pairs(markers) do
if not marker.name then
marker.name = string.format("Marker %d,%d", math.floor(marker.x), math.floor(marker.z))
end
end
return markers
end
local function save_player_markers(player_name, markers)
storage:set_string("markers_" .. player_name, minetest.serialize(markers))
end
local function add_marker(player_name, pos, color_index, name)
if not player_data[player_name] then
return false
end
local markers = player_data[player_name].markers
local marker_id = string.format("marker_%d_%d_%d", math.floor(pos.x), math.floor(pos.y), math.floor(pos.z))
-- Check if marker already exists at this position
if markers[marker_id] then
return false, "Marker already exists at this position"
end
-- Add marker with name
markers[marker_id] = {
x = pos.x,
y = pos.y,
z = pos.z,
color_index = color_index,
name = name or "Unnamed",
timestamp = os.time()
}
save_player_markers(player_name, markers)
return true, "Marker '" .. (name or "Unnamed") .. "' placed"
end
local function remove_marker_near_player(player_name, player_pos)
if not player_data[player_name] then
return false, "Player data not found"
end
local markers = player_data[player_name].markers
local removed_count = 0
-- Find markers within delete distance
for marker_id, marker in pairs(markers) do
local marker_pos = vector.new(marker.x, marker.y, marker.z)
local distance = vector.distance(player_pos, marker_pos)
if distance <= persistent_map.marker_delete_distance then
markers[marker_id] = nil
removed_count = removed_count + 1
end
end
if removed_count > 0 then
save_player_markers(player_name, markers)
return true, string.format("Removed %d marker(s)", removed_count)
else
return false, "No markers found nearby"
end
end
-- Delete all markers for a player
local function delete_all_markers(player_name)
if not player_data[player_name] then
return false, "Player data not found"
end
-- Clear markers only
player_data[player_name].markers = {}
save_player_markers(player_name, {})
return true, "All markers deleted"
end
-- Accessor functions for marker-gui.lua
function persistent_map.get_player_data(player_name)
return player_data[player_name]
end
function persistent_map.get_map_view_offset(player_name)
return map_view_offset[player_name]
end
function persistent_map.set_map_view_offset(player_name, offset)
map_view_offset[player_name] = offset
end
function persistent_map.get_map_zoom_level(player_name)
return map_zoom_level[player_name]
end
function persistent_map.set_map_zoom_level(player_name, zoom_level)
map_zoom_level[player_name] = zoom_level
end
function persistent_map.delete_all_markers_for_player(player_name)
return delete_all_markers(player_name)
end
function persistent_map.save_markers_for_player(player_name, markers)
return save_player_markers(player_name, markers)
end
function persistent_map.add_marker_for_player(player_name, pos, name, color_index)
return add_marker(player_name, pos, color_index or 1, name)
end
-- Generate a single map tile with proper coordinate mapping
local function generate_tile(tile_x, tile_z, callback)
local tile_id = get_tile_id(tile_x, tile_z)
-- Skip if already exists
if tile_exists(tile_id) then
if callback then callback(tile_id) end
return tile_id
end
-- Calculate world bounds for this tile
local minp = vector.new(
tile_x * persistent_map.tile_size,
persistent_map.min_height,
tile_z * persistent_map.tile_size
)
local maxp = vector.new(
(tile_x + 1) * persistent_map.tile_size - 1,
persistent_map.max_height,
(tile_z + 1) * persistent_map.tile_size - 1
)
-- Emerge area first
minetest.emerge_area(minp, maxp, function(blockpos, action, calls_remaining)
if calls_remaining > 0 then return end
-- Generate pixel data with proper orientation
-- Image coordinates: (0,0) at top-left, x increases right, y increases down
-- World coordinates: x increases east, z increases north
local pixels = {}
local pixel_count = persistent_map.tile_size * persistent_map.tile_size
-- Pre-allocate pixel array for better performance
for i = 1, pixel_count do
pixels[i] = ""
end
local tile_size = persistent_map.tile_size
local min_x, max_z = minp.x, maxp.z
local max_y, min_y = maxp.y, minp.y
local default_color = string.char(persistent_map.generation.default_color_r, persistent_map.generation.default_color_g, persistent_map.generation.default_color_b, persistent_map.generation.alpha_value)
-- Iterate through world coordinates properly
-- For image: top-to-bottom corresponds to north-to-south (z decreasing)
-- For image: left-to-right corresponds to west-to-east (x increasing)
local pixel_index = 1
for img_y = 0, tile_size - 1 do
local world_z = max_z - img_y -- Flip Z to match image orientation
for img_x = 0, tile_size - 1 do
local world_x = min_x + img_x
local color = default_color
-- Scan from top to bottom for surface node
for y = max_y, min_y, -1 do
local node = minetest.get_node({x=world_x, y=y, z=world_z})
if node.name ~= "air" and node.name ~= "ignore" then
local node_color = get_node_color(node.name)
color = string.char(node_color[1], node_color[2], node_color[3], persistent_map.generation.alpha_value)
break
end
end
pixels[pixel_index] = color
pixel_index = pixel_index + 1
end
end
-- Save as PNG
local filename = map_path .. tile_id .. ".png"
local png_data = minetest.encode_png(
persistent_map.tile_size,
persistent_map.tile_size,
table.concat(pixels)
)
minetest.safe_file_write(filename, png_data)
-- Load texture dynamically
minetest.dynamic_add_media(filename, function()
if callback then callback(tile_id) end
end)
end)
return tile_id
end
-- Load player discovered tiles
local function load_player_tiles(player_name)
local data = storage:get_string("player_" .. player_name)
if data == "" then
return {}
end
return minetest.deserialize(data) or {}
end
-- Save player discovered tiles
local function save_player_tiles(player_name, tiles)
storage:set_string("player_" .. player_name, minetest.serialize(tiles))
end
-- Check if a tile is discovered by a specific player
local function is_tile_discovered(player_name, tile_x, tile_z)
if not player_data[player_name] then
return false
end
local tiles = player_data[player_name].discovered_tiles
local tile_id = get_tile_id(tile_x, tile_z)
return tiles[tile_id] ~= nil
end
-- Add discovered tile for player
local function add_discovered_tile(player_name, tile_x, tile_z, callback)
if not player_data[player_name] then
return false
end
local tiles = player_data[player_name].discovered_tiles
local tile_id = get_tile_id(tile_x, tile_z)
-- Check if already discovered
if tiles[tile_id] then
return false
end
-- Add to player's discovered tiles
tiles[tile_id] = {x = tile_x, z = tile_z}
save_player_tiles(player_name, tiles)
-- Generate tile if it doesn't exist (shared resource)
generate_tile(tile_x, tile_z, function(id)
minetest.log("action", "[persistent_map] Generated tile: " .. id .. " for " .. player_name)
-- Send the tile to this specific player
local filename = map_path .. tile_id .. ".png"
minetest.dynamic_add_media({
filepath = filename,
to_player = player_name,
}, function(pname)
minetest.log("action", "[persistent_map] Sent tile " .. id .. " to " .. pname)
-- Call the callback after tile generation and sending is complete
if callback then
callback()
end
end)
end)
return true
end
-- Get player's position within their current tile (0.0 to 1.0)
local function get_position_in_tile(pos)
local tile_x, tile_z = pos_to_tile_coords(pos)
local tile_world_x = tile_x * persistent_map.tile_size
local tile_world_z = tile_z * persistent_map.tile_size
local offset_x = (pos.x - tile_world_x) / persistent_map.tile_size
local offset_z = (pos.z - tile_world_z) / persistent_map.tile_size
return offset_x, offset_z
end
-- Show map GUI with player-centered view and proper orientation
function persistent_map.show_map(player_name)
local player = minetest.get_player_by_name(player_name)
if not player then return end
-- Ensure player data exists
if not player_data[player_name] then
minetest.log("warning", "[persistent_map] Player data not found for " .. player_name)
return
end
local tiles = player_data[player_name].discovered_tiles
local markers = player_data[player_name].markers
local pos = player:get_pos()
local center_tile_x, center_tile_z = pos_to_tile_coords(pos)
-- Get current view offset (defaults to 0, 0 - centered on player)
local view_offset = map_view_offset[player_name] or {x = 0, z = 0}
-- Get current zoom level (defaults to default zoom)
local zoom_index = map_zoom_level[player_name] or persistent_map.default_zoom_index
local zoom_factor = persistent_map.zoom_levels[zoom_index]
-- Get zoom-specific offset for proper player centering
local zoom_offset = persistent_map.zoom_offsets[zoom_index] or {x = 0, z = 0}
-- Apply both manual view offset and zoom-specific centering offset
local view_center_x = center_tile_x + view_offset.x + zoom_offset.x
local view_center_z = center_tile_z + view_offset.z + zoom_offset.z
-- Calculate player's precise position within the tile (for marker placement)
local offset_x, offset_z = get_position_in_tile(pos)
-- View configuration - adjust radius based on zoom to keep reasonable map size
-- At higher zoom levels, show fewer tiles but make them much larger
local base_radius = persistent_map.view_radius
local radius = math.max(persistent_map.generation.min_radius, math.floor(base_radius / zoom_factor))
local view_size = radius * 2 + 1
local tile_display_size = persistent_map.gui_tile_size * zoom_factor
-- FIXED LAYOUT: Static formspec size regardless of zoom
-- Use base tile size for layout calculations to keep formspec static
local base_tile_size = persistent_map.gui_tile_size
local base_view_size = persistent_map.view_radius * 2 + 1
local map_width = base_view_size * base_tile_size
local map_height = base_view_size * base_tile_size
-- Side panel dimensions
local side_panel_width = persistent_map.ui.side_panel_width
local padding = persistent_map.ui.padding
local header_height = persistent_map.ui.header_height
local button_size = persistent_map.ui.button_size
local left_panel_x = padding
local map_x = left_panel_x + side_panel_width + padding
local right_panel_x = map_x + map_width + padding
-- Total formspec dimensions
local total_width = padding + side_panel_width + padding + map_width + padding + side_panel_width + padding
local total_height = math.max(map_height + header_height + padding * 2, persistent_map.ui.min_formspec_height) -- Ensure minimum height for controls
-- Pre-calculate commonly used values
local half_total_width = total_width * persistent_map.ui.total_width_factor
local header_y = padding
local map_start_x = map_x
local map_start_y = padding + header_height
-- Use table for better performance than repeated string.format calls
local formspec = {}
formspec[1] = string.format("formspec_version[%d]", persistent_map.ui.formspec_version)
formspec[2] = string.format("size[%.2f,%.2f]", total_width, total_height)
formspec[3] = "bgcolor[#000000FF;true]"
formspec[4] = string.format("label[%.2f,%.2f;Discovered Map - Close with ESC]", half_total_width - persistent_map.ui.header_title_offset, padding)
local formspec_index = 5
-- Add scrollable container for the map area
local scroll_width = map_width
local scroll_height = map_height
local actual_content_width = view_size * tile_display_size
local actual_content_height = view_size * tile_display_size
formspec[formspec_index] = string.format(
"scroll_container[%.2f,%.2f;%.2f,%.2f;map_scroll;horizontal;%.2f]",
map_x, map_start_y, scroll_width, scroll_height,
math.max(0, (actual_content_width - scroll_width) * persistent_map.ui.scroll_content_center)
)
formspec_index = formspec_index + 1
-- Count discovered tiles in view
local discovered_count = 0
local total_tiles = view_size * view_size
-- Pre-calculate values for the tile loop
local player_tiles = player_data[player_name].discovered_tiles
-- Draw tiles in a grid with proper orientation
-- GUI grid: (0,0) at top-left, x increases right, y increases down
-- World grid: x increases east, z increases north
-- Map display: North at top, East at right
for gui_row = 0, view_size - 1 do
local tile_z_base = view_center_z + radius - gui_row -- Pre-calculate Z base
local gui_y = gui_row * tile_display_size
for gui_col = 0, view_size - 1 do
-- Convert GUI grid position to world tile coordinates
local tile_x = view_center_x + gui_col - radius
local tile_z = tile_z_base
-- Calculate GUI position within scroll container (relative to container origin)
local gui_x = gui_col * tile_display_size
-- Check if THIS PLAYER has discovered this tile (optimized lookup)
local tile_id = string.format("tile_%d_%d", tile_x, tile_z)
if player_tiles[tile_id] then
discovered_count = discovered_count + 1
-- Verify tile exists before trying to display
if tile_exists(tile_id) then
formspec[formspec_index] = string.format(
"image[%.2f,%.2f;%.2f,%.2f;%s.png]",
gui_x, gui_y, tile_display_size, tile_display_size, tile_id
)
else
-- Tile should exist but doesn't - draw as fog
formspec[formspec_index] = string.format(
"box[%.2f,%.2f;%.2f,%.2f;#404040]",
gui_x, gui_y, tile_display_size, tile_display_size
)
end
else
-- Draw undiscovered tile (fog of war)
formspec[formspec_index] = string.format(
"box[%.2f,%.2f;%.2f,%.2f;#000000]",
gui_x, gui_y, tile_display_size, tile_display_size
)
end
formspec_index = formspec_index + 1
end
end
-- Draw markers on the map (optimized)
local marker_scale_factor = persistent_map.map_marker.scale_factor * persistent_map.marker_scale * zoom_factor
local half_marker_size = marker_scale_factor * persistent_map.map_marker.half_size_factor
local tile_size_f = persistent_map.tile_size
for marker_id, marker in pairs(markers) do
-- Inline tile coordinate calculation for better performance
local marker_tile_x = math.floor(marker.x / tile_size_f)
local marker_tile_z = math.floor(marker.z / tile_size_f)
-- Check if marker is within visible area
local marker_offset_x = marker_tile_x - view_center_x
local marker_offset_z = marker_tile_z - view_center_z
if math.abs(marker_offset_x) <= radius and math.abs(marker_offset_z) <= radius then
-- Convert world tile offset to GUI position
local gui_col = marker_offset_x + radius
local gui_row = radius - marker_offset_z -- Flip Z for display
local marker_tile_gui_x = gui_col * tile_display_size
local marker_tile_gui_y = gui_row * tile_display_size
-- Calculate precise position within tile (inlined for performance)
local tile_world_x = marker_tile_x * tile_size_f
local tile_world_z = marker_tile_z * tile_size_f
local marker_offset_in_tile_x = (marker.x - tile_world_x) / tile_size_f
local marker_offset_in_tile_z = (marker.z - tile_world_z) / tile_size_f
local marker_gui_x = marker_tile_gui_x + (marker_offset_in_tile_x * tile_display_size)
local marker_gui_y = marker_tile_gui_y + ((1.0 - marker_offset_in_tile_z) * tile_display_size) -- Flip Z offset
-- Draw marker dot (scaled with zoom) - stack 5 transparent squares for solid appearance
local color = persistent_map.marker_colors[marker.color_index] or persistent_map.marker_colors[1]
for stack_layer = 1, 5 do
formspec[formspec_index] = string.format(
"box[%.2f,%.2f;%.2f,%.2f;%s]",
marker_gui_x - half_marker_size, marker_gui_y - half_marker_size,
marker_scale_factor, marker_scale_factor, color.color
)
formspec_index = formspec_index + 1
end
-- Draw marker name above the marker (if it has a name and zoom is sufficient)
local marker_name = marker.name or "Unnamed"
if marker_name ~= "" and zoom_factor >= persistent_map.marker_name_min_zoom then -- Show names at lower zoom levels too
-- Scale text size with zoom factor using configurable values
local text_scale = math.max(persistent_map.marker_name_scale, zoom_factor * persistent_map.marker_name_zoom_multiplier)
local char_width = persistent_map.marker_name_char_width * text_scale -- Character width estimation scaled
local text_height = persistent_map.marker_name_text_height * text_scale -- Text height scaled
local name_y = marker_gui_y - half_marker_size - (text_height + persistent_map.marker_name_padding) -- Position above marker with configurable padding
local text_width = string.len(marker_name) * char_width
local name_x = marker_gui_x - (text_width * persistent_map.map_marker.text_center_factor) -- Center text properly
-- Apply zoom-specific X offset for marker name positioning
local marker_name_x_offset = persistent_map.marker_name_zoom_x_offsets[zoom_index] or 0.0
name_x = name_x + marker_name_x_offset
-- Add the marker name label with configurable font size
formspec[formspec_index] = string.format(
"style[marker_name_%s;font_size=+%d]",
marker_id, math.floor(text_scale * persistent_map.marker_name_font_multiplier)
)
formspec_index = formspec_index + 1
formspec[formspec_index] = string.format(
"label[%.2f,%.2f;%s]",
name_x, name_y, minetest.formspec_escape(marker_name)
)
formspec_index = formspec_index + 1
end
end
end
-- Draw player position marker with proper orientation
local player_tile_offset_x = center_tile_x - view_center_x
local player_tile_offset_z = center_tile_z - view_center_z
-- Check if player is within visible grid
if math.abs(player_tile_offset_x) <= radius and math.abs(player_tile_offset_z) <= radius then
-- Convert world tile offset to GUI position
local gui_col = player_tile_offset_x + radius
local gui_row = radius - player_tile_offset_z -- Flip Z for display
local marker_tile_x = gui_col * tile_display_size
local marker_tile_y = gui_row * tile_display_size
-- Add the offset within the current tile
-- Note: offset_z needs to be flipped for display (north = negative GUI y)
local marker_x = marker_tile_x + (offset_x * tile_display_size)
local marker_y = marker_tile_y + ((persistent_map.map_marker.position_flip_factor - offset_z) * tile_display_size) -- Flip Z offset
-- Get player's look direction (yaw is in radians)
local yaw = player:get_look_horizontal()
-- Convert yaw to compass direction (0 = north, π/2 = east, π = south, 3π/2 = west)
-- In Luanti: yaw 0 = north, π/2 = west, π = south, 3π/2 = east
-- We need to convert this to degrees and adjust for our triangle (which points north at 0°)
local angle = math.deg(-yaw) -- Convert to degrees, no additional offset needed
-- Create a triangular arrow pointing in the direction the player is facing (scaled with zoom)
local scale = persistent_map.player_marker_scale * zoom_factor
local size = persistent_map.player_marker.arrow_size * scale
local arrow_height = persistent_map.player_marker.arrow_height * scale
-- Calculate rotation matrix for the triangle
local rad = math.rad(angle)
local cos_a = math.cos(rad)
local sin_a = math.sin(rad)
-- Define triangle points (pointing up initially = north)
local x1, y1 = 0, -arrow_height
local x2, y2 = -size, arrow_height * persistent_map.ui.arrow_height_factor
local x3, y3 = size, arrow_height * persistent_map.ui.arrow_height_factor
-- Rotate points
local rx1 = x1 * cos_a - y1 * sin_a
local ry1 = x1 * sin_a + y1 * cos_a
local rx2 = x2 * cos_a - y2 * sin_a
local ry2 = x2 * sin_a + y2 * cos_a
local rx3 = x3 * cos_a - y3 * sin_a
local ry3 = x3 * sin_a + y3 * cos_a
-- Translate to marker position
local p1x, p1y = marker_x + rx1, marker_y + ry1
-- Draw filled triangle using boxes to approximate it (all scaled) - stack 5 for solid appearance
-- Main body (red square at center)
local main_size = persistent_map.player_marker.main_body_size * scale
local half_main = main_size * persistent_map.map_marker.half_size_factor
for stack_layer = 1, 5 do
formspec[formspec_index] = string.format(
"box[%.2f,%.2f;%.2f,%.2f;#FF0000]",
marker_x - half_main, marker_y - half_main, main_size, main_size
)
formspec_index = formspec_index + 1
end
-- Draw directional indicator line (pointing direction - the tip)
local tip_size = persistent_map.player_marker.tip_size * scale
local half_tip = tip_size * persistent_map.map_marker.half_size_factor
for stack_layer = 1, 5 do
formspec[formspec_index] = string.format(
"box[%.2f,%.2f;%.2f,%.2f;#FF0000]",
p1x - half_tip, p1y - half_tip, tip_size, tip_size
)
formspec_index = formspec_index + 1
end
-- Additional points to make the arrow more visible (mid-section)
local mid1x = marker_x + (rx1 * persistent_map.player_marker.mid_section_factor)
local mid1y = marker_y + (ry1 * persistent_map.player_marker.mid_section_factor)
local mid_size = persistent_map.player_marker.mid_section_size * scale
local half_mid = mid_size * persistent_map.map_marker.half_size_factor
for stack_layer = 1, 5 do
formspec[formspec_index] = string.format(
"box[%.2f,%.2f;%.2f,%.2f;#FF0000]",
mid1x - half_mid, mid1y - half_mid, mid_size, mid_size
)
formspec_index = formspec_index + 1
end
-- Wing indicators (base of arrow)
local wing1x = marker_x + (rx2 * persistent_map.player_marker.wing_factor)
local wing1y = marker_y + (ry2 * persistent_map.player_marker.wing_factor)
local wing2x = marker_x + (rx3 * persistent_map.player_marker.wing_factor)
local wing2y = marker_y + (ry3 * persistent_map.player_marker.wing_factor)
local wing_size = persistent_map.player_marker.wing_size * scale
local half_wing = wing_size * persistent_map.map_marker.half_size_factor
for stack_layer = 1, 5 do
formspec[formspec_index] = string.format(
"box[%.2f,%.2f;%.2f,%.2f;#FF0000]",
wing1x - half_wing, wing1y - half_wing, wing_size, wing_size
)
formspec_index = formspec_index + 1
end
for stack_layer = 1, 5 do
formspec[formspec_index] = string.format(
"box[%.2f,%.2f;%.2f,%.2f;#FF0000]",
wing2x - half_wing, wing2y - half_wing, wing_size, wing_size
)
formspec_index = formspec_index + 1
end
end
-- Draw party member markers (if player is in a party)
if persistent_map.get_party_member_positions then
local party_members = persistent_map.get_party_member_positions(player_name)
local party_marker_scale = persistent_map.player_marker_scale * zoom_factor * 0.8 -- Slightly smaller than main player
for member_name, member_data in pairs(party_members) do
local member_pos = member_data.pos
local member_yaw = member_data.yaw
-- Convert member position to tile coordinates
local member_tile_x, member_tile_z = pos_to_tile_coords(member_pos)
local member_tile_offset_x = member_tile_x - view_center_x
local member_tile_offset_z = member_tile_z - view_center_z
-- Check if member is within visible grid
if math.abs(member_tile_offset_x) <= radius and math.abs(member_tile_offset_z) <= radius then
-- Convert world tile offset to GUI position
local gui_col = member_tile_offset_x + radius
local gui_row = radius - member_tile_offset_z -- Flip Z for display
local member_tile_x_gui = gui_col * tile_display_size
local member_tile_y_gui = gui_row * tile_display_size
-- Calculate precise position within tile
local member_offset_x, member_offset_z = get_position_in_tile(member_pos)
local member_marker_x = member_tile_x_gui + (member_offset_x * tile_display_size)
local member_marker_y = member_tile_y_gui + ((persistent_map.map_marker.position_flip_factor - member_offset_z) * tile_display_size)
-- Draw party member marker (blue triangle)
local member_angle = math.deg(-member_yaw)
local member_scale = party_marker_scale
local member_size = persistent_map.player_marker.arrow_size * member_scale
local member_arrow_height = persistent_map.player_marker.arrow_height * member_scale
-- Calculate rotation matrix for the triangle
local member_rad = math.rad(member_angle)
local member_cos_a = math.cos(member_rad)
local member_sin_a = math.sin(member_rad)
-- Define triangle points (pointing up initially = north)
local mx1, my1 = 0, -member_arrow_height
local mx2, my2 = -member_size, member_arrow_height * persistent_map.ui.arrow_height_factor
local mx3, my3 = member_size, member_arrow_height * persistent_map.ui.arrow_height_factor
-- Rotate points
local mrx1 = mx1 * member_cos_a - my1 * member_sin_a
local mry1 = mx1 * member_sin_a + my1 * member_cos_a
local mrx2 = mx2 * member_cos_a - my2 * member_sin_a
local mry2 = mx2 * member_sin_a + my2 * member_cos_a
local mrx3 = mx3 * member_cos_a - my3 * member_sin_a
local mry3 = mx3 * member_sin_a + my3 * member_cos_a
-- Main body (blue square at center) - stack 5 for solid appearance
local member_main_size = persistent_map.player_marker.main_body_size * member_scale
local member_half_main = member_main_size * persistent_map.map_marker.half_size_factor
for stack_layer = 1, 5 do
formspec[formspec_index] = string.format(
"box[%.2f,%.2f;%.2f,%.2f;#0080FF]",
member_marker_x - member_half_main, member_marker_y - member_half_main, member_main_size, member_main_size
)
formspec_index = formspec_index + 1
end
-- Draw directional indicator (the tip)
local member_p1x, member_p1y = member_marker_x + mrx1, member_marker_y + mry1
local member_tip_size = persistent_map.player_marker.tip_size * member_scale
local member_half_tip = member_tip_size * persistent_map.map_marker.half_size_factor
for stack_layer = 1, 5 do
formspec[formspec_index] = string.format(
"box[%.2f,%.2f;%.2f,%.2f;#0080FF]",
member_p1x - member_half_tip, member_p1y - member_half_tip, member_tip_size, member_tip_size
)
formspec_index = formspec_index + 1
end
-- Mid-section indicator
local member_mid1x = member_marker_x + (mrx1 * persistent_map.player_marker.mid_section_factor)
local member_mid1y = member_marker_y + (mry1 * persistent_map.player_marker.mid_section_factor)
local member_mid_size = persistent_map.player_marker.mid_section_size * member_scale
local member_half_mid = member_mid_size * persistent_map.map_marker.half_size_factor
for stack_layer = 1, 5 do
formspec[formspec_index] = string.format(
"box[%.2f,%.2f;%.2f,%.2f;#0080FF]",
member_mid1x - member_half_mid, member_mid1y - member_half_mid, member_mid_size, member_mid_size
)
formspec_index = formspec_index + 1
end
-- Wing indicators (base of arrow)
local member_wing1x = member_marker_x + (mrx2 * persistent_map.player_marker.wing_factor)
local member_wing1y = member_marker_y + (mry2 * persistent_map.player_marker.wing_factor)
local member_wing2x = member_marker_x + (mrx3 * persistent_map.player_marker.wing_factor)
local member_wing2y = member_marker_y + (mry3 * persistent_map.player_marker.wing_factor)
local member_wing_size = persistent_map.player_marker.wing_size * member_scale
local member_half_wing = member_wing_size * persistent_map.map_marker.half_size_factor
for stack_layer = 1, 5 do
formspec[formspec_index] = string.format(
"box[%.2f,%.2f;%.2f,%.2f;#0080FF]",
member_wing1x - member_half_wing, member_wing1y - member_half_wing, member_wing_size, member_wing_size
)
formspec_index = formspec_index + 1
end
for stack_layer = 1, 5 do
formspec[formspec_index] = string.format(
"box[%.2f,%.2f;%.2f,%.2f;#0080FF]",
member_wing2x - member_half_wing, member_wing2y - member_half_wing, member_wing_size, member_wing_size
)
formspec_index = formspec_index + 1
end
-- Draw player name above the marker (if zoom is sufficient)
if zoom_factor >= persistent_map.marker_name_min_zoom then
local name_scale = math.max(persistent_map.marker_name_scale, zoom_factor * persistent_map.marker_name_zoom_multiplier)
local name_char_width = persistent_map.marker_name_char_width * name_scale
local name_text_height = persistent_map.marker_name_text_height * name_scale
local name_y = member_marker_y - member_half_main - (name_text_height + persistent_map.marker_name_padding)
local name_text_width = string.len(member_name) * name_char_width
local name_x = member_marker_x - (name_text_width * persistent_map.map_marker.text_center_factor)
-- Apply zoom-specific X offset for party member name positioning
local member_name_x_offset = persistent_map.marker_name_zoom_x_offsets[zoom_index] or 0.0
name_x = name_x + member_name_x_offset
-- Add the member name label
formspec[formspec_index] = string.format(
"style[party_member_%s;font_size=+%d]",
member_name, math.floor(name_scale * persistent_map.marker_name_font_multiplier)
)
formspec_index = formspec_index + 1
formspec[formspec_index] = string.format(
"label[%.2f,%.2f;%s]",
name_x, name_y, minetest.formspec_escape(member_name)
)
formspec_index = formspec_index + 1
end
end
end
end
-- Close scroll container
formspec[formspec_index] = "scroll_container_end[]"
formspec_index = formspec_index + 1
-- LEFT PANEL: Navigation Controls
local left_panel_y = padding + header_height + persistent_map.ui.nav_panel_offset
-- Navigation section header
formspec[formspec_index] = string.format(
"label[%.2f,%.2f;Navigation:]",
left_panel_x, left_panel_y
)
formspec_index = formspec_index + 1
local nav_start_y = left_panel_y + persistent_map.ui.nav_section_spacing
local nav_center_x = left_panel_x + side_panel_width / 2
-- North button (up on map = north = +Z)
table.insert(formspec, string.format(
"button[%f,%f;%f,%f;nav_north;N ↑]",
nav_center_x - button_size / persistent_map.ui.half_divisor, nav_start_y, button_size, button_size
))
-- West and East buttons
table.insert(formspec, string.format(
"button[%f,%f;%f,%f;nav_west;W ←]",
nav_center_x - button_size * persistent_map.ui.nav_button_multiplier, nav_start_y + button_size + persistent_map.ui.nav_button_spacing, button_size, button_size
))
table.insert(formspec, string.format(
"button[%f,%f;%f,%f;nav_east;E →]",
nav_center_x + persistent_map.ui.nav_button_spacing, nav_start_y + button_size + persistent_map.ui.nav_button_spacing, button_size, button_size
))
-- South button
table.insert(formspec, string.format(
"button[%f,%f;%f,%f;nav_south;S ↓]",
nav_center_x - button_size / persistent_map.ui.half_divisor, nav_start_y + button_size * persistent_map.ui.padding_multiplier + persistent_map.ui.nav_button_spacing * persistent_map.ui.padding_multiplier, button_size, button_size
))
-- Center on player button
table.insert(formspec, string.format(
"button[%f,%f;%f,%f;nav_center;Center on Player]",
left_panel_x, nav_start_y + button_size * 3 + persistent_map.ui.nav_center_button_offset, side_panel_width - persistent_map.ui.button_width_offset, button_size
))
-- Zoom controls
local zoom_y = nav_start_y + button_size * 4 + persistent_map.ui.zoom_section_offset
table.insert(formspec, string.format(
"label[%f,%f;Zoom: %.1fx]",
left_panel_x, zoom_y, zoom_factor
))
-- Zoom buttons side by side
local zoom_button_width = (side_panel_width - persistent_map.ui.nav_center_button_offset) / persistent_map.ui.half_divisor
table.insert(formspec, string.format(
"button[%f,%f;%f,%f;zoom_out;Zoom Out (-)]",
left_panel_x, zoom_y + persistent_map.ui.zoom_button_spacing, zoom_button_width, button_size
))
table.insert(formspec, string.format(
"button[%f,%f;%f,%f;zoom_in;Zoom In (+)]",
left_panel_x + zoom_button_width + persistent_map.ui.zoom_button_spacing, zoom_y + persistent_map.ui.zoom_button_spacing, zoom_button_width, button_size
))
-- RIGHT PANEL: Marker Controls
local right_panel_y = padding + header_height + persistent_map.ui.nav_panel_offset
-- Marker section header
table.insert(formspec, string.format(
"label[%f,%f;Place Marker:]",
right_panel_x, right_panel_y
))
-- Marker name input field
local name_input_y = right_panel_y + persistent_map.ui.nav_section_spacing
table.insert(formspec, string.format(
"label[%f,%f;Marker Name:]",
right_panel_x, name_input_y
))
table.insert(formspec, string.format(
"field[%f,%f;%f,%f;marker_name;;]",
right_panel_x, name_input_y + persistent_map.ui.marker_name_label_spacing, side_panel_width - persistent_map.ui.field_width_offset, persistent_map.ui.marker_name_field_height
))
-- Marker color buttons (2 columns in right panel)
local marker_start_y = name_input_y + persistent_map.ui.marker_colors_offset
local colors_per_row = persistent_map.ui.colors_per_row
local color_button_size = persistent_map.ui.color_button_size
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 = right_panel_x + col * (color_button_size + persistent_map.ui.color_button_spacing)
local btn_y = marker_start_y + row * (color_button_size + persistent_map.ui.color_button_row_spacing)
-- Add colored background first
table.insert(formspec, string.format(
"box[%f,%f;%f,%f;%s]",
btn_x, btn_y, color_button_size, color_button_size, color_info.color
))
table.insert(formspec, string.format(
"button[%f,%f;%f,%f;add_marker_%d;%s]",
btn_x, btn_y, color_button_size, color_button_size, i, color_info.name
))
end
-- Manage Markers button (moved up since delete buttons are removed)
local manage_y = marker_start_y + math.ceil(#persistent_map.marker_colors / colors_per_row) * (color_button_size + persistent_map.ui.color_button_row_spacing) + persistent_map.ui.delete_buttons_offset
table.insert(formspec, string.format(
"button[%f,%f;%f,%f;manage_markers;Manage Markers]",
right_panel_x, manage_y, side_panel_width - persistent_map.ui.button_width_offset, button_size
))
-- Add coordinate display with discovery stats - positioned on the right side
local offset_text = ""
if view_offset.x ~= 0 or view_offset.z ~= 0 then
offset_text = string.format(" (View offset: X%+d, Z%+d)", view_offset.x, view_offset.z)
end
-- Count total discovered tiles for this player
local total_discovered = 0
for _ in pairs(tiles) do
total_discovered = total_discovered + 1
end
-- Count markers
local marker_count = 0
for _ in pairs(markers) do
marker_count = marker_count + 1
end
-- BOTTOM INFO SECTION: Spread out information on both sides of the map
local info_y = total_height - persistent_map.ui.info_bottom_margin
local left_info_x = persistent_map.ui.info_left_align_with_panel and left_panel_x or padding
local right_info_x = right_panel_x
-- Left side information (2 pieces)
table.insert(formspec, string.format(
"label[%f,%f;Player Coords: X=%d Z=%d]",
left_info_x, info_y,
math.floor(pos.x), math.floor(pos.z)
))
table.insert(formspec, string.format(
"label[%f,%f;Tile Coords: %d, %d]%s",
left_info_x, info_y + persistent_map.ui.info_line_spacing,
center_tile_x, center_tile_z, offset_text
))
-- Right side information (2 pieces)
table.insert(formspec, string.format(
"label[%f,%f;Discovered: %d/%d total tiles]",
right_info_x, info_y,
total_discovered, persistent_map.total_world_tiles
))
table.insert(formspec, string.format(
"label[%f,%f;View: %d/%d tiles (%d markers)]",
right_info_x, info_y + persistent_map.ui.info_line_spacing,
discovered_count, total_tiles, marker_count
))
minetest.show_formspec(player_name, "persistent_map:map", table.concat(formspec))
end
-- Register chatcommand to open map
minetest.register_chatcommand("map", {
description = S("Open the persistent map"),
func = function(name)
-- Reset view offset when opening map
map_view_offset[name] = {x = 0, z = 0}
-- Initialize zoom level if not set
if not map_zoom_level[name] then
map_zoom_level[name] = persistent_map.default_zoom_index
end
persistent_map.show_map(name)
return true, S("Map opened")
end,
})
-- Handle formspec input for navigation with proper orientation and marker system
minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= "persistent_map:map" then return end
local name = player:get_player_name()
local offset = map_view_offset[name] or {x = 0, z = 0}
local updated = false
-- Navigation with proper orientation:
-- North = +Z, South = -Z, East = +X, West = -X
if fields.nav_north then
offset.z = offset.z + 1 -- Move view north (+Z)
updated = true
elseif fields.nav_south then
offset.z = offset.z - 1 -- Move view south (-Z)
updated = true
elseif fields.nav_west then
offset.x = offset.x - 1 -- Move view west (-X)
updated = true
elseif fields.nav_east then
offset.x = offset.x + 1 -- Move view east (+X)
updated = true
elseif fields.nav_center then
offset.x = 0
offset.z = 0
updated = true
elseif fields.zoom_in then
-- Zoom in (increase zoom level)
local current_zoom = map_zoom_level[name] or persistent_map.default_zoom_index
if current_zoom < persistent_map.max_zoom_index then
map_zoom_level[name] = current_zoom + 1
updated = true
end
elseif fields.zoom_out then
-- Zoom out (decrease zoom level)
local current_zoom = map_zoom_level[name] or persistent_map.default_zoom_index
if current_zoom > persistent_map.min_zoom_index then
map_zoom_level[name] = current_zoom - 1
updated = true
end
elseif fields.manage_markers then
-- Open marker management GUI
persistent_map.show_marker_gui(name)
return -- Don't update the map, just switch to marker GUI
else
-- Check for marker placement buttons
for i = 1, #persistent_map.marker_colors do
if fields["add_marker_" .. i] then
local pos = player:get_pos()
local marker_name = fields.marker_name or ""
-- If no name provided, use a default based on coordinates
if marker_name == "" then
marker_name = string.format("Marker %d,%d", math.floor(pos.x), math.floor(pos.z))
end
local success, message = add_marker(name, pos, i, marker_name)
minetest.chat_send_player(name, message)
updated = true
break
end
end
end
if updated then
map_view_offset[name] = offset
persistent_map.show_map(name)
end
end)
-- Player join - Initialize player data
minetest.register_on_joinplayer(function(player)
local name = player:get_player_name()
-- Load this player's discovered tiles
local tiles = load_player_tiles(name)
-- Load this player's markers
local markers = load_player_markers(name)
-- Initialize player data structure
player_data[name] = {
discovered_tiles = tiles,
markers = markers,
last_tile_x = nil,
last_tile_z = nil,
}
-- Count tiles and markers efficiently
local tile_count = 0
for _ in pairs(tiles) do tile_count = tile_count + 1 end
local marker_count = 0
for _ in pairs(markers) do marker_count = marker_count + 1 end
minetest.log("action", "[persistent_map] Player " .. name .. " joined with " .. tile_count .. " discovered tiles and " .. marker_count .. " markers")
-- Re-send all of THIS PLAYER's discovered tile textures to the client
for tile_id, _ in pairs(tiles) do
local filename = map_path .. tile_id .. ".png"
local file = io.open(filename, "r")
if file then
file:close()
-- Dynamically add the media for this specific player
minetest.dynamic_add_media({
filepath = filename,
to_player = name,
}, function(player_name)
minetest.log("action", "[persistent_map] Reloaded tile " .. tile_id .. " for " .. player_name)
end)
else
minetest.log("warning", "[persistent_map] Tile file not found: " .. tile_id .. " for player " .. name)
end
end
-- Trigger initial tile discovery for player's current position
minetest.after(persistent_map.generation.initial_discovery_delay, function()
local pos = player:get_pos()
if pos then
local tile_x, tile_z = pos_to_tile_coords(pos)
local data = player_data[name]
if data then
local discovered = add_discovered_tile(name, tile_x, tile_z, function()
-- Send discovery message only after tile generation is complete
minetest.chat_send_player(name, S("New area discovered!"))
end)
if discovered then
data.last_tile_x = tile_x
data.last_tile_z = tile_z
end
end
end
end)
end)
-- Player leave - Clean up player data
minetest.register_on_leaveplayer(function(player)
local name = player:get_player_name()
player_data[name] = nil
map_view_offset[name] = nil
map_zoom_level[name] = nil
minetest.log("action", "[persistent_map] Player " .. name .. " left, data cleaned up")
end)
-- Discovery scanner
local scan_timer = 0
minetest.register_globalstep(function(dtime)
scan_timer = scan_timer + dtime
if scan_timer < persistent_map.scan_interval then return end
scan_timer = 0
for _, player in ipairs(minetest.get_connected_players()) do
local name = player:get_player_name()
-- Ensure player data exists
if not player_data[name] then
minetest.log("warning", "[persistent_map] Player data missing for " .. name .. " in globalstep")
goto continue
end
local pos = player:get_pos()
local tile_x, tile_z = pos_to_tile_coords(pos)
local data = player_data[name]
-- Check if player moved to new tile
if data.last_tile_x ~= tile_x or data.last_tile_z ~= tile_z then
local discovered = add_discovered_tile(name, tile_x, tile_z, function()
-- Send discovery message only after tile generation is complete
minetest.chat_send_player(name, S("New area discovered!"))
end)
if discovered then
data.last_tile_x = tile_x
data.last_tile_z = tile_z
end
end
::continue::
end
end)
-- Register map book item
minetest.register_craftitem("discovery_maps:map_book", {
description = S("Map Book") .. "\n" .. S("Right-click to open the persistent map"),
inventory_image = "default_book.png^[colorize:#8B4513:120",
stack_max = 1,
on_use = function(itemstack, user, pointed_thing)
if user and user:is_player() then
local player_name = user:get_player_name()
-- Reset view offset when opening map
map_view_offset[player_name] = {x = 0, z = 0}
-- Initialize zoom level if not set
if not map_zoom_level[player_name] then
map_zoom_level[player_name] = persistent_map.default_zoom_index
end
persistent_map.show_map(player_name)
end
return itemstack
end,
})
-- Register craft recipe for map book
minetest.register_craft({
output = "discovery_maps:map_book",
recipe = {
{"default:paper", "default:paper", "default:paper"},
{"default:paper", "default:book", "default:paper"},
{"default:paper", "default:paper", "default:paper"}
}
})
minetest.register_craft({
output = "discovery_maps:map_book",
recipe = {
{"mcl_core:paper", "mcl_core:paper", "mcl_core:paper"},
{"mcl_core:paper", "mcl_books:writable_book", "mcl_core:paper"},
{"mcl_core:paper", "mcl_core:paper", "mcl_core:paper"}
}
})
-- Give map book to new players
minetest.register_on_newplayer(function(player)
local inv = player:get_inventory()
if inv:room_for_item("main", "discovery_maps:map_book") then
inv:add_item("main", "discovery_maps:map_book")
minetest.chat_send_player(player:get_player_name(),
S("Welcome! You've been given a Map Book. left-click it to explore the world!"))
end
end)
dofile(modpath .. "/marker-gui.lua")
dofile(modpath .. "/marker-share.lua")
dofile(modpath .. "/map-party.lua")
dofile(modpath .. "/map-party-gui.lua")
minetest.log("action", "[persistent_map] Persistent map mod loaded successfully")