Mineclonia/mods/ITEMS/mcl_maps/init.lua

612 lines
18 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)