diff --git a/generate_textures.lua b/generate_textures.lua index 0c5953f..e201d0d 100644 --- a/generate_textures.lua +++ b/generate_textures.lua @@ -25,3 +25,25 @@ local pixels = { { K, K, K, K, K, K, _ }, } tga_encoder.image(pixels):save("textures/maps_arrow_diagonal.tga") + +local pixels = { + { _, _, K, K, K, _, _ }, + { _, K, W, W, W, K, _ }, + { K, W, W, W, W, W, K }, + { K, W, W, W, W, W, K }, + { K, W, W, W, W, W, K }, + { _, K, W, W, W, K, _ }, + { _, _, K, K, K, _, _ }, +} +tga_encoder.image(pixels):save("textures/maps_dot_large.tga") + +local pixels = { + { _, _, _, _, _, _, _ }, + { _, _, K, K, K, _, _ }, + { _, K, W, W, W, K, _ }, + { _, K, W, W, W, K, _ }, + { _, K, W, W, W, K, _ }, + { _, _, K, K, K, _, _ }, + { _, _, _, _, _, _, _ }, +} +tga_encoder.image(pixels):save("textures/maps_dot_small.tga") diff --git a/init.lua b/init.lua index bf2bd4b..62f524e 100644 --- a/init.lua +++ b/init.lua @@ -47,14 +47,17 @@ function tga_encoder.image:blit_icon(icon, pos, stop_colors) end maps = {} -maps.dots = {} -maps.maps = {} -maps.minp = {} -maps.maxp = {} -maps.work = {} -maps.sent = {} -maps.posx = {} -maps.posz = {} + +maps.dark = {} -- key: player name; value: is it dark? +maps.huds = {} -- key: player name; value: player huds +maps.maps = {} -- key: player name; value: map texture +maps.mark = {} -- key: player name; value: marker texture +maps.marx = {} -- key: player name; value: marker x offset +maps.mary = {} -- key: player name; value: marker y offset + +maps.load = {} -- maps loaded by players +maps.sent = {} -- maps sent to players +maps.work = {} -- maps being created local size = 80 @@ -62,41 +65,29 @@ local worldpath = minetest.get_worldpath() local textures_dir = worldpath .. "/maps/" minetest.mkdir(textures_dir) -maps.create_map = function(pos, player_name) +maps.get_map_filename = function(map_id) + return "maps_map_texture_" .. map_id .. ".tga" +end + +maps.create_map = function(pos) + local itemstack = ItemStack("maps:map") + local meta = itemstack:get_meta() + + local map_id = tostring(os.time() + math.random()) + meta:set_string("maps:id", map_id) + local minp = vector.multiply(vector.floor(vector.divide(pos, size)), size) + meta:set_string("maps:minp", minetest.pos_to_string(minp)) + local maxp = vector.add(minp, vector.new(size - 1, size - 1, size - 1)) + meta:set_string("maps:maxp", minetest.pos_to_string(maxp)) - local prefix, _ = minetest.pos_to_string(maxp) - prefix, _ = prefix:gsub("%(", "") - prefix, _ = prefix:gsub("%)", "") - prefix, _ = prefix:gsub(",", "_") - local filename = prefix .. ".tga" - - if maps.work[filename] then - return - end - - local player = minetest.get_player_by_name(player_name) - - if maps.sent[filename] then - maps.minp[player_name] = minp - maps.maxp[player_name] = maxp - player:hud_change( - maps.maps[player_name], - "text", - filename - ) - return - end + local xpos = vector.round(pos) + meta:set_string("maps:xpos", minetest.pos_to_string(xpos)) + local filename = maps.get_map_filename(map_id) maps.work[filename] = true - player:hud_change( - maps.maps[player_name], - "text", - "blank.png" - ) - local emerge_callback = function( blockpos, action, @@ -377,22 +368,7 @@ maps.create_map = function(pos, player_name) filepath, { colormap=colormap } ) - - minetest.dynamic_add_media( - filepath, - function() - local player =minetest.get_player_by_name(player_name) - player:hud_change( - maps.maps[player_name], - "text", - filename - ) - maps.minp[player_name] = minp - maps.maxp[player_name] = maxp - maps.sent[filename] = true - maps.work[filename] = false - end - ) + maps.work[filename] = false end minetest.emerge_area( @@ -400,6 +376,165 @@ maps.create_map = function(pos, player_name) maxp, emerge_callback ) + return itemstack +end + +maps.load_map = function(map_id) + assert( nil ~= map_id ) + if ( + "" == map_id or + maps.work[map_id] + ) then + return + end + + local filename = maps.get_map_filename(map_id) + + if not maps.sent[map_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 + minetest.dynamic_add_media( + textures_dir .. filename, + function() end + ) + maps.load[map_id] = true + else + -- minetest.dynamic_add_media() never blocks + -- in Minetest 5.5, callback runs after load + minetest.dynamic_add_media( + textures_dir .. filename, + function() + maps.load[map_id] = true + end + ) + end + maps.sent[map_id] = true + end + + if maps.load[map_id] then + return filename + end +end + +maps.encode_map_item_meta = function(input) + return minetest.encode_base64( + minetest.compress( + input, + "deflate", + 9 + ) + ) +end + +maps.decode_map_item_meta = function(input) + return minetest.decompress( + minetest.decode_base64(input), + "deflate", + 9 + ) +end + +result_original = "foo\0\01\02\x03\n\rbar" +result_roundtrip = maps.decode_map_item_meta( + maps.encode_map_item_meta(result_original) +) +assert( + result_original == result_roundtrip, + "maps: mismatch between maps.encode_map_item_meta() and maps.decode_map_item_meta()" +) + +maps.load_map_item = function(itemstack) + local meta = itemstack:get_meta() + local map_id = meta:get_string("maps:id") + + if ( + not map_id or + "" == map_id or + maps.work[map_id] + ) then + return + end + + if maps.load[map_id] then + return maps.load_map(map_id) + end + + local texture_file_name = maps.get_map_filename(map_id) + local texture_file_path = textures_dir .. 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("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 maps.decode_map_item_meta(tga_deflate_base64) ~= texture_data_from_file then + minetest.log( + "action", + "maps: update item meta from file content for map " .. map_id + ) + meta:set_string( + "maps:tga_deflate_base64", + maps.encode_map_item_meta(texture_data_from_file) + ) + end + else + -- map items without meta should not exist, so + -- we now write the file contents to item meta + minetest.log( + "action", + "maps: create item meta from file content for map " .. map_id + ) + meta:set_string( + "maps:tga_deflate_base64", + maps.encode_map_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", + "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 maps.load_map(map_id) end minetest.register_on_joinplayer( @@ -413,14 +548,171 @@ minetest.register_on_joinplayer( offset = { x = 0, y = 0 }, scale = { x = 4, y = 4 } } - local dot_def = table.copy(map_def) - maps.maps[player_name] = player:hud_add(map_def) - maps.dots[player_name] = player:hud_add(dot_def) - maps.minp[player_name] = { x=-32000, y=0, z=0 } - maps.maxp[player_name] = { x=-32000, y=0, z=0 } + local pos_def = table.copy(map_def) + maps.huds[player_name] = { + map = player:hud_add(map_def), + pos = player:hud_add(pos_def), + } end ) +maps.show_map_hud = function(player) + local wield_item = player:get_wielded_item() + local texture = maps.load_map_item(wield_item) + local player_pos = player:get_pos() + local player_name = player:get_player_name() + + if not player_pos or not texture then + if maps.maps[player_name] then + player:hud_change( + maps.huds[player_name].map, + "text", + "blank.png" + ) + player:hud_change( + maps.huds[player_name].pos, + "text", + "blank.png" + ) + maps.maps[player_name] = nil + end + return + end + + local pos = vector.round(player_pos) + local meta = wield_item:get_meta() + + local meta_minp = meta:get_string("maps:minp") + assert( "" ~= meta_minp ) + local minp = minetest.string_to_pos(meta_minp) + + local meta_maxp = meta:get_string("maps:maxp") + assert( "" ~= meta_maxp ) + local maxp = minetest.string_to_pos(meta_maxp) + + local meta_xpos = meta:get_string("maps:xpos") + if "" ~= meta_xpos then + local xpos = minetest.string_to_pos(meta_xpos) + local x_x = xpos.x - minp.x - 4 + local x_z = maxp.z - xpos.z - 4 + local x_overlay = "^[combine:" .. + size .. "x" .. size .. ":" .. + x_x .. "," .. x_z .. "=maps_x.tga" + texture = texture .. x_overlay + end + + local light_level = minetest.get_node_light(pos) or 0 + local darkness = 255 - (light_level * 17) + local light_level_overlay = "^[colorize:black:" .. darkness + if ( + texture ~= maps.maps[player_name] or + darkness ~= maps.dark[player_name] + ) then + player:hud_change( + maps.huds[player_name].map, + "text", + texture .. light_level_overlay + ) + maps.dark[player_name] = darkness + maps.maps[player_name] = texture + end + + local marker + local dot_large = "maps_dot_large.tga" .. "^[makealpha:1,1,1" + local dot_small = "maps_dot_small.tga" .. "^[makealpha:1,1,1" + + if pos.x < minp.x then + if minp.x - pos.x < size then + marker = dot_large + else + marker = dot_small + end + pos.x = minp.x + elseif pos.x > maxp.x then + if pos.x - maxp.x < size then + marker = dot_large + else + marker = dot_small + 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 ~= dot_small then + marker = dot_large + else + marker = dot_small + end + pos.z = minp.z + elseif pos.z > maxp.z then + if pos.z - maxp.z < 256 and marker ~= dot_small then + marker = dot_large + else + marker = dot_small + 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 = "maps_arrow.tga" .. + "^[makealpha:1,1,1" .. + "^[transformR" .. + yaw + elseif ( + yaw == 45 or + yaw == 135 or + yaw == 225 or + yaw == 315 + ) then + marker = "maps_arrow_diagonal.tga" .. + "^[makealpha:1,1,1" .. + "^[transformR" .. + (yaw - 45) + end + end + + if marker and marker ~= maps.mark[player_name] then + player:hud_change( + maps.huds[player_name].pos, + "text", + marker + ) + maps.mark[player_name] = marker + end + + local marker_x = (pos.x - minp.x - (size/2)) * 4 + local marker_y = (maxp.z - pos.z - size + 3) * 4 + if ( + marker_x ~= maps.marx[player_name] or + marker_y ~= maps.mary[player_name] + ) then + player:hud_change( + maps.huds[player_name].pos, + "offset", + { + x = marker_x, + y = marker_y, + } + ) + maps.marx[player_name] = marker_x + maps.mary[player_name] = marker_y + end +end + local time_elapsed = 0 minetest.register_globalstep( @@ -431,78 +723,31 @@ minetest.register_globalstep( end local players = minetest.get_connected_players() for _, player in pairs(players) do - local pos = vector.round(player:get_pos()) - local player_name = player:get_player_name() - - if ( - pos.x == maps.posx[player_name] and - pos.z == maps.posz[player_name] - ) then - -- continue - else - local minp = maps.minp[player_name] - local maxp = maps.maxp[player_name] - - if pos.x < minp.x then - maps.create_map(pos, player_name) - elseif pos.x > maxp.x then - maps.create_map(pos, player_name) - end - - if pos.z < minp.z then - maps.create_map(pos, player_name) - elseif pos.z > maxp.z then - maps.create_map(pos, player_name) - end - - local x = (pos.x - minp.x - (size/2)) * 4 - local y = (minp.z - pos.z) * 4 - 2 - player:hud_change( - maps.dots[player_name], - "offset", - { - x = x, - y = y, - } - ) - maps.posx[player_name] = pos.x - maps.posz[player_name] = pos.z - end - local marker - 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 = "maps_arrow.tga" .. - "^[makealpha:1,1,1" .. - "^[transformR" .. - yaw - elseif ( - yaw == 45 or - yaw == 135 or - yaw == 225 or - yaw == 315 - ) then - marker = "maps_arrow_diagonal.tga" .. - "^[makealpha:1,1,1" .. - "^[transformR" .. - (yaw - 45) - end - if marker then - player:hud_change( - maps.dots[player_name], - "text", - marker - ) - end + maps.show_map_hud(player) end end ) + +maps.create_map_item = function(itemstack, player, pointed_thing) + local pos = player:get_pos() + if pos then + local map = maps.create_map(pos) + return map + end +end + +minetest.override_item( + "map:mapping_kit", + { + on_place = maps.create_map_item, + on_secondary_use = maps.create_map_item, + } +) + +minetest.register_craftitem( + "maps:map", + { + description = "Map", + inventory_image = "maps_map.tga" + } +) diff --git a/mod.conf b/mod.conf index eda80f4..70136d8 100644 --- a/mod.conf +++ b/mod.conf @@ -1,3 +1,4 @@ depends = tga_encoder description = Shows maps in the player HUD name = maps +depends = map diff --git a/textures/maps_dot_large.tga b/textures/maps_dot_large.tga new file mode 100644 index 0000000..c48e7d4 Binary files /dev/null and b/textures/maps_dot_large.tga differ diff --git a/textures/maps_dot_small.tga b/textures/maps_dot_small.tga new file mode 100644 index 0000000..8c61624 Binary files /dev/null and b/textures/maps_dot_small.tga differ diff --git a/textures/maps_map.tga b/textures/maps_map.tga new file mode 100644 index 0000000..ad6338a Binary files /dev/null and b/textures/maps_map.tga differ