612 lines
18 KiB
Lua
612 lines
18 KiB
Lua
mcl_maps = {
|
||
map_update_fps = tonumber(minetest.settings:get("mcl_maps_map_update_fps")) or 15
|
||
}
|
||
|
||
local modname = minetest.get_current_modname()
|
||
local modpath = minetest.get_modpath(modname)
|
||
local S = minetest.get_translator(modname)
|
||
|
||
local math = math
|
||
local vector = vector
|
||
local table = table
|
||
local pairs = pairs
|
||
|
||
local pos_to_string = minetest.pos_to_string
|
||
local string_to_pos = minetest.string_to_pos
|
||
local get_item_group = minetest.get_item_group
|
||
local dynamic_add_media = minetest.dynamic_add_media
|
||
local get_connected_players = minetest.get_connected_players
|
||
|
||
local worldpath = minetest.get_worldpath()
|
||
local map_textures_path = worldpath .. "/mcl_maps/"
|
||
|
||
minetest.mkdir(map_textures_path)
|
||
|
||
local function load_json_file(name)
|
||
local file = assert(io.open(modpath .. "/" .. name .. ".json", "r"))
|
||
local data = minetest.parse_json(file:read())
|
||
file:close()
|
||
return data
|
||
end
|
||
|
||
local texture_colors = load_json_file("colors")
|
||
local palettes = load_json_file("palettes")
|
||
|
||
local color_cache = {}
|
||
|
||
local creating_maps = {}
|
||
local loaded_maps = {}
|
||
|
||
local c_air = minetest.get_content_id("air")
|
||
|
||
function mcl_maps.create_map(pos)
|
||
local minp = vector.multiply(vector.floor(vector.divide(pos, 128)), 128)
|
||
local maxp = vector.add(minp, vector.new(127, 127, 127))
|
||
|
||
local itemstack = ItemStack("mcl_maps:filled_map")
|
||
local meta = itemstack:get_meta()
|
||
local id = tostring(os.time() + math.random())
|
||
meta:set_string("mcl_maps:id", id)
|
||
meta:set_string("mcl_maps:minp", pos_to_string(minp))
|
||
meta:set_string("mcl_maps:maxp", pos_to_string(maxp))
|
||
tt.reload_itemstack_description(itemstack)
|
||
|
||
creating_maps[id] = true
|
||
minetest.emerge_area(minp, maxp, function(blockpos, action, calls_remaining)
|
||
if calls_remaining > 0 then
|
||
return
|
||
end
|
||
local vm = minetest.get_voxel_manip()
|
||
local emin, emax = vm:read_from_map(minp, maxp)
|
||
local data = vm:get_data()
|
||
local param2data = vm:get_param2_data()
|
||
local area = VoxelArea:new({MinEdge = emin, MaxEdge = emax})
|
||
local pixels = {}
|
||
local last_heightmap
|
||
for x = 1, 128 do
|
||
local map_x = minp.x - 1 + x
|
||
local heightmap = {}
|
||
for z = 1, 128 do
|
||
local map_z = minp.z - 1 + z
|
||
local color, height
|
||
for map_y = maxp.y, minp.y, -1 do
|
||
local index = area:index(map_x, map_y, map_z)
|
||
local c_id = data[index]
|
||
if c_id ~= c_air then
|
||
color = color_cache[c_id]
|
||
if color == nil then
|
||
local nodename = minetest.get_name_from_content_id(c_id)
|
||
local def = minetest.registered_nodes[nodename]
|
||
if def then
|
||
local texture
|
||
if def.palette then
|
||
texture = def.palette
|
||
elseif def.tiles then
|
||
texture = def.tiles[1]
|
||
if type(texture) == "table" then
|
||
texture = texture.name
|
||
end
|
||
end
|
||
if texture then
|
||
texture = texture:match("([^=^%^]-([^.]+))$"):split("^")[1]
|
||
end
|
||
if def.palette then
|
||
local palette = palettes[texture]
|
||
color = palette and {palette = palette}
|
||
else
|
||
color = texture_colors[texture]
|
||
end
|
||
end
|
||
end
|
||
|
||
if color and color.palette then
|
||
color = color.palette[param2data[index] + 1]
|
||
else
|
||
color_cache[c_id] = color or false
|
||
end
|
||
|
||
if color and last_heightmap then
|
||
local last_height = last_heightmap[z]
|
||
if last_height < map_y then
|
||
color = {
|
||
math.min(255, color[1] + 16),
|
||
math.min(255, color[2] + 16),
|
||
math.min(255, color[3] + 16),
|
||
}
|
||
elseif last_height > map_y then
|
||
color = {
|
||
math.max(0, color[1] - 16),
|
||
math.max(0, color[2] - 16),
|
||
math.max(0, color[3] - 16),
|
||
}
|
||
end
|
||
end
|
||
height = map_y
|
||
break
|
||
end
|
||
end
|
||
heightmap[z] = height or minp.y
|
||
pixels[z] = pixels[z] or {}
|
||
pixels[z][x] = color or {0, 0, 0}
|
||
end
|
||
last_heightmap = heightmap
|
||
end
|
||
tga_encoder.image(pixels):save(map_textures_path .. "mcl_maps_map_texture_" .. id .. ".tga")
|
||
creating_maps[id] = nil
|
||
end)
|
||
return itemstack
|
||
end
|
||
|
||
function mcl_maps.load_map(id)
|
||
if id == "" or creating_maps[id] then
|
||
return
|
||
end
|
||
|
||
local texture = "mcl_maps_map_texture_" .. id .. ".tga"
|
||
|
||
if not loaded_maps[id] then
|
||
if not minetest.features.dynamic_add_media_table then
|
||
-- minetest.dynamic_add_media() blocks in
|
||
-- Minetest 5.3 and 5.4 until media loads
|
||
loaded_maps[id] = true
|
||
dynamic_add_media(map_textures_path .. texture, function() end)
|
||
else
|
||
-- minetest.dynamic_add_media() never blocks
|
||
-- in Minetest 5.5, callback runs after load
|
||
dynamic_add_media(map_textures_path .. texture, function()
|
||
loaded_maps[id] = true
|
||
end)
|
||
end
|
||
end
|
||
|
||
if loaded_maps[id] then
|
||
return texture
|
||
end
|
||
end
|
||
|
||
local function encode_item_meta(input)
|
||
return minetest.encode_base64(
|
||
minetest.compress(
|
||
input,
|
||
"deflate",
|
||
9
|
||
)
|
||
)
|
||
end
|
||
|
||
local function decode_item_meta(input)
|
||
return minetest.decompress(
|
||
minetest.decode_base64(input),
|
||
"deflate",
|
||
9
|
||
)
|
||
end
|
||
|
||
result_original = "foo\0\01\02\x03\n\rbar"
|
||
result_roundtrip = decode_item_meta(
|
||
encode_item_meta(result_original)
|
||
)
|
||
assert(
|
||
result_original == result_roundtrip,
|
||
"mcl_maps: mismatch between encode_item_meta() and decode_item_meta()"
|
||
)
|
||
|
||
function mcl_maps.load_map_item(itemstack)
|
||
local meta = itemstack:get_meta()
|
||
local map_id = meta:get_string("mcl_maps:id")
|
||
|
||
if "" == map_id or creating_maps[map_id] then
|
||
-- wait for map content to be created
|
||
return
|
||
end
|
||
|
||
if loaded_maps[map_id] then
|
||
-- texture has already been sent
|
||
return mcl_maps.load_map(map_id)
|
||
end
|
||
|
||
local texture_file_name = "mcl_maps_map_texture_" .. map_id .. ".tga"
|
||
local texture_file_path = map_textures_path .. texture_file_name
|
||
|
||
-- does the texture file exist?
|
||
local texture_file_handle_read = io.open(
|
||
texture_file_path,
|
||
"rb"
|
||
)
|
||
local texture_file_exists = true
|
||
local texture_data_from_file
|
||
if nil == texture_file_handle_read then
|
||
texture_file_exists = false
|
||
else
|
||
texture_data_from_file = texture_file_handle_read:read("*a")
|
||
texture_file_handle_read:close()
|
||
end
|
||
|
||
-- does the texture item meta exist?
|
||
local tga_deflate_base64 = meta:get_string("mcl_maps:tga_deflate_base64")
|
||
local texture_item_meta_exists = true
|
||
if "" == tga_deflate_base64 then
|
||
texture_item_meta_exists = false
|
||
end
|
||
|
||
if texture_file_exists and nil ~= texture_data_from_file then
|
||
if texture_item_meta_exists then
|
||
-- sanity check: do we have the same textures?
|
||
-- if server-side texture has changed, take it
|
||
if decode_item_meta(tga_deflate_base64) ~= texture_data_from_file then
|
||
minetest.log(
|
||
"action",
|
||
"mcl_maps: update item meta from file content for map " .. map_id
|
||
)
|
||
meta:set_string(
|
||
"mcl_maps:tga_deflate_base64",
|
||
encode_item_meta(texture_data_from_file)
|
||
)
|
||
end
|
||
else
|
||
-- probably an older map from mcl2 or mcl5, so
|
||
-- we now write the file contents to item meta
|
||
minetest.log(
|
||
"action",
|
||
"mcl_maps: create item meta from file content for map " .. map_id
|
||
)
|
||
meta:set_string(
|
||
"mcl_maps:tga_deflate_base64",
|
||
encode_item_meta(texture_data_from_file)
|
||
)
|
||
end
|
||
else
|
||
-- no texture file → could be a world download
|
||
-- so we look for missing texture in item meta
|
||
-- and write that to the map texture file here
|
||
if texture_item_meta_exists then
|
||
minetest.log(
|
||
"action",
|
||
"mcl_maps: create file content from item meta for map " .. map_id
|
||
)
|
||
assert(
|
||
minetest.safe_file_write(
|
||
texture_file_path,
|
||
decode_item_meta(tga_deflate_base64)
|
||
)
|
||
)
|
||
else
|
||
minetest.log(
|
||
"error",
|
||
"no data for map " .. map_id
|
||
)
|
||
return
|
||
end
|
||
end
|
||
|
||
return mcl_maps.load_map(map_id)
|
||
end
|
||
|
||
local function fill_map(itemstack, placer, pointed_thing)
|
||
local new_stack = mcl_util.call_on_rightclick(itemstack, placer, pointed_thing)
|
||
if new_stack then
|
||
return new_stack
|
||
end
|
||
|
||
if minetest.settings:get_bool("enable_real_maps", true) then
|
||
local new_map = mcl_maps.create_map(placer:get_pos())
|
||
itemstack:take_item()
|
||
if itemstack:is_empty() then
|
||
return new_map
|
||
else
|
||
local inv = placer:get_inventory()
|
||
if inv:room_for_item("main", new_map) then
|
||
inv:add_item("main", new_map)
|
||
else
|
||
minetest.add_item(placer:get_pos(), new_map)
|
||
end
|
||
return itemstack
|
||
end
|
||
end
|
||
end
|
||
|
||
minetest.register_craftitem("mcl_maps:empty_map", {
|
||
description = S("Empty Map"),
|
||
_doc_items_longdesc = S("Empty maps are not useful as maps, but they can be stacked and turned to maps which can be used."),
|
||
_doc_items_usagehelp = S("Rightclick to create a filled map (which can't be stacked anymore)."),
|
||
inventory_image = "mcl_maps_map_empty.png",
|
||
on_place = fill_map,
|
||
on_secondary_use = fill_map,
|
||
stack_max = 64,
|
||
})
|
||
|
||
local filled_def = {
|
||
description = S("Map"),
|
||
_tt_help = S("Shows a map image."),
|
||
_doc_items_longdesc = S("When created, the map saves the nearby area as an image that can be viewed any time by holding the map."),
|
||
_doc_items_usagehelp = S("Hold the map in your hand. This will display a map on your screen."),
|
||
inventory_image = "mcl_maps_map_filled.png^(mcl_maps_map_filled_markings.png^[colorize:#000000)",
|
||
stack_max = 64,
|
||
groups = { filled_map = 1, not_in_creative_inventory = 1, tool = 1 },
|
||
}
|
||
|
||
minetest.register_craftitem("mcl_maps:filled_map", filled_def)
|
||
|
||
local function add_marker(itemstack, placer, pointed_thing)
|
||
local new_stack = mcl_util.call_on_rightclick(itemstack, placer, pointed_thing)
|
||
if new_stack then
|
||
return new_stack
|
||
end
|
||
|
||
if "node" == pointed_thing.type then
|
||
local item_meta = itemstack:get_meta()
|
||
local node_pos = pointed_thing.under
|
||
local node = minetest.get_node(node_pos)
|
||
local name = node.name
|
||
if name == "mcl_banners:hanging_banner" or name == "mcl_banners:standing_banner" then
|
||
local banners_string = item_meta:get_string("mcl_maps:banners")
|
||
local banners_table
|
||
if "" == banners_string then
|
||
banners_table = {}
|
||
else
|
||
banners_table = minetest.deserialize(banners_string)
|
||
end
|
||
local colorid = mcl_banners.get_banner_colorid(node_pos)
|
||
local banner = {
|
||
node_pos,
|
||
colorid
|
||
}
|
||
table.insert(
|
||
banners_table,
|
||
banner
|
||
)
|
||
item_meta:set_string(
|
||
"mcl_maps:banners",
|
||
minetest.serialize(banners_table)
|
||
)
|
||
end
|
||
end
|
||
|
||
return itemstack
|
||
end
|
||
|
||
local filled_wield_def = table.copy(filled_def)
|
||
filled_wield_def.use_texture_alpha = minetest.features.use_texture_alpha_string_modes and "opaque" or false
|
||
filled_wield_def.visual_scale = 1
|
||
filled_wield_def.wield_scale = {x = 1, y = 1, z = 1}
|
||
filled_wield_def.paramtype = "light"
|
||
filled_wield_def.drawtype = "mesh"
|
||
filled_wield_def.node_placement_prediction = ""
|
||
filled_wield_def.range = minetest.registered_items[""].range
|
||
filled_wield_def.on_place = add_marker
|
||
|
||
for _, texture in pairs(mcl_skins.list) do
|
||
local def = table.copy(filled_wield_def)
|
||
def.tiles = {texture .. ".png"}
|
||
def.mesh = "mcl_meshhand.b3d"
|
||
def._mcl_hand_id = texture
|
||
minetest.register_node("mcl_maps:filled_map_" .. texture, def)
|
||
|
||
local female_def = table.copy(def)
|
||
female_def.mesh = "mcl_meshhand_female.b3d"
|
||
female_def._mcl_hand_id = texture .. "_female"
|
||
minetest.register_node("mcl_maps:filled_map_" .. texture .. "_female", female_def)
|
||
end
|
||
|
||
local old_add_item = minetest.add_item
|
||
function minetest.add_item(pos, stack)
|
||
stack = ItemStack(stack)
|
||
if get_item_group(stack:get_name(), "filled_map") > 0 then
|
||
stack:set_name("mcl_maps:filled_map")
|
||
end
|
||
return old_add_item(pos, stack)
|
||
end
|
||
|
||
tt.register_priority_snippet(function(itemstring, _, itemstack)
|
||
if itemstack and get_item_group(itemstring, "filled_map") > 0 then
|
||
local id = itemstack:get_meta():get_string("mcl_maps:id")
|
||
if id ~= "" then
|
||
return "#" .. id, mcl_colors.GRAY
|
||
end
|
||
end
|
||
end)
|
||
|
||
minetest.register_craft({
|
||
output = "mcl_maps:empty_map",
|
||
recipe = {
|
||
{ "mcl_core:paper", "mcl_core:paper", "mcl_core:paper" },
|
||
{ "mcl_core:paper", "group:compass", "mcl_core:paper" },
|
||
{ "mcl_core:paper", "mcl_core:paper", "mcl_core:paper" },
|
||
}
|
||
})
|
||
|
||
minetest.register_craft({
|
||
type = "shapeless",
|
||
output = "mcl_maps:filled_map 2",
|
||
recipe = {"group:filled_map", "mcl_maps:empty_map"},
|
||
})
|
||
|
||
local function on_craft(itemstack, player, old_craft_grid, craft_inv)
|
||
if itemstack:get_name() == "mcl_maps:filled_map" then
|
||
for _, stack in pairs(old_craft_grid) do
|
||
if get_item_group(stack:get_name(), "filled_map") > 0 then
|
||
itemstack:get_meta():from_table(stack:get_meta():to_table())
|
||
return itemstack
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
minetest.register_on_craft(on_craft)
|
||
minetest.register_craft_predict(on_craft)
|
||
|
||
local maps = {}
|
||
local huds = {}
|
||
local light = {}
|
||
|
||
minetest.register_on_joinplayer(function(player)
|
||
local map_def = {
|
||
hud_elem_type = "image",
|
||
text = "blank.png",
|
||
position = {x = 0.75, y = 0.8},
|
||
alignment = {x = 0, y = -1},
|
||
offset = {x = 0, y = 0},
|
||
scale = {x = 2, y = 2},
|
||
}
|
||
local marker_def = table.copy(map_def)
|
||
marker_def.alignment = {x = 0, y = 0}
|
||
huds[player] = {
|
||
map = player:hud_add(map_def),
|
||
marker = player:hud_add(marker_def),
|
||
}
|
||
end)
|
||
|
||
minetest.register_on_leaveplayer(function(player)
|
||
maps[player] = nil
|
||
light[player] = nil
|
||
huds[player] = nil
|
||
end)
|
||
|
||
function mcl_maps.make_banner_overlay(banners, minp, maxp, border)
|
||
local texture = ""
|
||
for i=1, #banners do
|
||
local banner_pos = banners[i][1]
|
||
local banner_colorid = banners[i][2]
|
||
local banner_hexcolor = mcl_banners.colorid_to_hexcolor(banner_colorid)
|
||
if (
|
||
banner_pos.x - 3 > minp.x and
|
||
banner_pos.x - 3 < maxp.x and
|
||
banner_pos.z - 7 > minp.z and
|
||
banner_pos.z - 7 < maxp.z
|
||
) then
|
||
local height = math.abs(maxp.x - minp.x) + border.top + border.bottom
|
||
local width = math.abs(maxp.z - minp.z) + border.left + border.right
|
||
-- marker is 8×8 & points down, map is 140×140
|
||
-- offsets have been experimentally determined
|
||
local banner_x = banner_pos.x - minp.x - 3 + border.left
|
||
local banner_z = maxp.z - banner_pos.z - 7 + border.top
|
||
texture = texture .. "^([combine:" .. width .. "x" .. height .. ":" .. banner_x .. "," .. banner_z .. "=mcl_maps_banner_location_marker.png^[colorize:" .. banner_hexcolor .. ":127)"
|
||
end
|
||
end
|
||
return texture
|
||
end
|
||
|
||
local time_since_last_frame = 0
|
||
|
||
minetest.register_globalstep(function(dtime)
|
||
time_since_last_frame = time_since_last_frame + dtime
|
||
if time_since_last_frame < ( 1 / mcl_maps.map_update_fps ) then
|
||
return
|
||
end
|
||
time_since_last_frame = 0
|
||
for _, player in pairs(get_connected_players()) do
|
||
local wield = player:get_wielded_item()
|
||
local texture = mcl_maps.load_map_item(wield)
|
||
local hud = huds[player]
|
||
if texture then
|
||
local wield_def = wield:get_definition()
|
||
local hand_def = player:get_inventory():get_stack("hand", 1):get_definition()
|
||
|
||
if hand_def and wield_def and hand_def._mcl_hand_id ~= wield_def._mcl_hand_id then
|
||
wield:set_name("mcl_maps:filled_map_" .. hand_def._mcl_hand_id)
|
||
player:set_wielded_item(wield)
|
||
end
|
||
|
||
local pos = vector.round(player:get_pos())
|
||
local meta = wield:get_meta()
|
||
local minp = string_to_pos(meta:get_string("mcl_maps:minp"))
|
||
local maxp = string_to_pos(meta:get_string("mcl_maps:maxp"))
|
||
|
||
local banners_string = meta:get_string("mcl_maps:banners")
|
||
if "" ~= banners_string then
|
||
local banners = minetest.deserialize(banners_string)
|
||
if #banners > 0 then
|
||
local border = { top = 6, right = 6, bottom = 6, left = 6 }
|
||
local banner_overlay = mcl_maps.make_banner_overlay(
|
||
banners,
|
||
minp,
|
||
maxp,
|
||
border
|
||
)
|
||
texture = texture .. banner_overlay
|
||
end
|
||
end
|
||
|
||
local light_level
|
||
local light_level_overlay = ""
|
||
if nil ~= pos then -- do not crash while joining game
|
||
light_level = minetest.get_node_light(pos) or 0 -- can be nil somehow
|
||
light_level_overlay = "^[colorize:black:" .. 255 - (light_level * 17)
|
||
end
|
||
|
||
if texture ~= maps[player] or light_level ~= light[player] then
|
||
player:hud_change(hud.map, "text", "[combine:140x140:0,0=mcl_maps_map_background.png:6,6=" .. texture .. light_level_overlay)
|
||
if nil == maps[player] then
|
||
-- show tmp message if player
|
||
-- did not wield a map before
|
||
mcl_tmp_message.message(
|
||
player,
|
||
S("Right click on a banner to add a colored marker.")
|
||
)
|
||
end
|
||
light[player] = light_level
|
||
maps[player] = texture
|
||
end
|
||
|
||
local marker
|
||
|
||
if pos.x < minp.x then
|
||
if minp.x - pos.x < 256 then
|
||
marker = "mcl_maps_player_dot_large.png"
|
||
else
|
||
marker = "mcl_maps_player_dot.png"
|
||
end
|
||
pos.x = minp.x
|
||
elseif pos.x > maxp.x then
|
||
if pos.x - maxp.x < 256 then
|
||
marker = "mcl_maps_player_dot_large.png"
|
||
else
|
||
marker = "mcl_maps_player_dot.png"
|
||
end
|
||
pos.x = maxp.x
|
||
end
|
||
|
||
-- we never override the small marker
|
||
-- yes, this is a literal corner case
|
||
if pos.z < minp.z then
|
||
if minp.z - pos.z < 256 and marker ~= "mcl_maps_player_dot.png" then
|
||
marker = "mcl_maps_player_dot_large.png"
|
||
else
|
||
marker = "mcl_maps_player_dot.png"
|
||
end
|
||
pos.z = minp.z
|
||
elseif pos.z > maxp.z then
|
||
if pos.z - maxp.z < 256 and marker ~= "mcl_maps_player_dot.png" then
|
||
marker = "mcl_maps_player_dot_large.png"
|
||
else
|
||
marker = "mcl_maps_player_dot.png"
|
||
end
|
||
pos.z = maxp.z
|
||
end
|
||
|
||
if nil == marker then
|
||
local yaw = (math.floor(player:get_look_horizontal() * 180 / math.pi / 45 + 0.5) % 8) * 45
|
||
if yaw == 0 or
|
||
yaw == 90 or
|
||
yaw == 180 or
|
||
yaw == 270 then
|
||
marker = "mcl_maps_player_arrow.png" .. "^[transformR" .. yaw
|
||
end
|
||
if yaw == 45 or
|
||
yaw == 135 or
|
||
yaw == 225 or
|
||
yaw == 315 then
|
||
marker = "mcl_maps_player_arrow_diagonal.png" .. "^[transformR" .. (yaw - 45)
|
||
end
|
||
end
|
||
|
||
player:hud_change(hud.marker, "text", marker .. light_level_overlay)
|
||
player:hud_change(hud.marker, "offset", {x = (6 - 140 / 2 + pos.x - minp.x) * 2, y = (6 - 140 + maxp.z - pos.z) * 2})
|
||
elseif maps[player] then
|
||
player:hud_change(hud.map, "text", "blank.png")
|
||
player:hud_change(hud.marker, "text", "blank.png")
|
||
maps[player] = nil
|
||
end
|
||
end
|
||
end)
|