--[[ maps – Minetest mod to render very ugly HUD maps Copyright © 2022 Nils Dagsson Moskopp (erlehmann) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Dieses Programm hat das Ziel, die Medienkompetenz der Leser zu steigern. Gelegentlich packe ich sogar einen handfesten Buffer Overflow oder eine Format String Vulnerability zwischen die anderen Codezeilen und schreibe das auch nicht dran. ]]-- -- blit an icon into the image unless its rect overlaps pixels that -- have any of the stop_colors, treating a nil pixel as transparent function tga_encoder.image:blit_icon(icon, pos, stop_colors) local x = pos.x local z = pos.z local overlap = false for i_z = 1,#icon do for i_x = 1,#icon[i_z] do local color = self.pixels[z + i_z][x + i_x][1] if stop_colors[color] then overlap = true break end end if overlap then break end end if overlap then return end for i_z = 1,#icon do for i_x = 1,#icon[i_z] do local color = icon[i_z][i_x][1] if color then self.pixels[z + i_z][x + i_x] = { color } end end end end maps = {} 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 local worldpath = minetest.get_worldpath() local textures_dir = worldpath .. "/maps/" minetest.mkdir(textures_dir) maps.get_map_filename = function(map_id) return "maps_map_texture_" .. map_id .. ".tga" end maps.create_map_item = function(pos, properties) properties = properties or {} 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)) if properties.draw_x then local xpos = vector.round(pos) meta:set_string("maps:xpos", minetest.pos_to_string(xpos)) end local filename = maps.get_map_filename(map_id) maps.work[map_id] = true local emerge_callback = function( blockpos, action, calls_remaining ) if calls_remaining > 0 then return end local pixels = {} local colormap = { { 195, 175, 140 }, -- background checkerboard light { 180, 160, 125 }, -- background checkerboard dark { 60, 35, 16 }, -- dark line { 210, 170, 130 }, -- liquid light { 135, 90, 40 }, -- liquid dark { 150, 105, 55 }, -- more liquid { 165, 120, 70 }, -- more liquid { 150, 105, 55 }, -- more liquid { 60, 35, 16 }, -- tree outline { 150, 105, 55 }, -- tree fill } for x = 1,size,1 do for z = 1,size,1 do local color = 0 + ( ( x + z ) % 2 ) pixels[z] = pixels[z] or {} pixels[z][x] = { color } end end local positions = minetest.find_nodes_in_area_under_air( minp, maxp, "group:liquid" ) for _, p in ipairs(positions) do if 14 == minetest.get_node_light(p, 0.5) then local z = p.z - minp.z + 1 local x = p.x - minp.x + 1 pixels[z][x] = { 3 } end end -- draw coastline for x = 1,size,1 do for z = 1,size,1 do if pixels[z][x][1] >= 3 then pixels[z][x] = { 3 + ( z % 2 ) } -- stripes if pixels[z][x][1] == 4 then local color = { 4 + ( ( math.floor( x / 7 ) + math.floor( 1.3 * z * z ) ) % 4 ) } pixels[z][x] = color end if z > 1 and pixels[z-1][x][1] < 3 then pixels[z-1][x] = { 2 } pixels[z][x] = { 4 } end if z < size and pixels[z+1][x][1] < 3 then pixels[z+1][x] = { 2 } pixels[z][x] = { 4 } end if x > 1 and pixels[z][x-1][1] < 3 then pixels[z][x-1] = { 2 } pixels[z][x] = { 4 } end if x < size and pixels[z][x+1][1] < 3 then pixels[z][x+1] = { 2 } pixels[z][x] = { 4 } end end end end local image = tga_encoder.image(pixels) local positions = minetest.find_nodes_in_area( minp, maxp, "group:door" ) for _, p in ipairs(positions) do local z = p.z - minp.z + 1 local x = p.x - minp.x + 1 local draw_house = ( z > 1 and z < size - 7 and x > 4 and x < size - 4 ) if draw_house then local _ = { nil } -- transparent local O = { 8 } -- outline local F = { 9 } -- filling local house = { { _, _, _, _, _, _, _ }, { _, O, O, O, O, O, _ }, { _, O, F, F, F, O, _ }, { _, O, F, F, F, O, _ }, { _, O, F, F, F, O, _ }, { _, _, O, F, O, _, _ }, { _, _, _, O, _, _, _ }, { _, _, _, _, _, _, _ }, } image:blit_icon( house, { x = x - 5, z = z - 1, }, { [4] = true, [5] = true, [6] = true, [7] = true, [8] = true, [9] = true, } ) end end local positions = minetest.find_nodes_in_area_under_air( minp, maxp, { "group:leaves", "default:snow", -- snow-covered leaves } ) for _, p in ipairs(positions) do local z = p.z - minp.z + 1 local x = p.x - minp.x + 1 local node = minetest.get_node({ x=p.x, y=p.y - 4, z=p.z, }) local draw_tree = ( minetest.get_item_group( node.name, "tree" ) > 0 ) and ( z > 1 and z < size - 7 and x > 4 and x < size - 4 ) if draw_tree then local tree = {} local _ = { nil } -- transparent local O = { 8 } -- outline local F = { 9 } -- filling if nil ~= node.name:find("pine") then tree = { { _, _, _, _, _, _, _ }, { _, _, _, O, _, _, _ }, { _, O, O, O, O, O, _ }, { _, O, F, F, F, O, _ }, { _, _, O, F, O, _, _ }, { _, _, O, F, O, _, _ }, { _, _, _, O, _, _, _ }, { _, _, _, _, _, _, _ }, } else tree = { { _, _, _, _, _, _, _ }, { _, _, _, O, _, _, _ }, { _, _, _, O, _, _, _ }, { _, _, O, O, O, _, _ }, { _, O, F, F, F, O, _ }, { _, O, F, F, F, O, _ }, { _, _, O, O, O, _, _ }, { _, _, _, _, _, _, _ }, } end image:blit_icon( tree, { x = x - 4, z = z - 1, }, { [4] = true, [5] = true, [6] = true, [7] = true, [8] = true, [9] = true, } ) end end local positions = minetest.find_nodes_in_area_under_air( minp, maxp, "group:grass" ) for _, p in ipairs(positions) do local z = p.z - minp.z + 1 local x = p.x - minp.x + 1 local draw_grass = ( z > 1 and z < size - 4 and x > 4 and x < size - 4 ) if draw_grass then local _ = { nil } -- transparent local G = { 9 } -- line local grass = { { _, _, _, _, _, _, _ }, { _, G, _, G, _, G, _ }, { _, G, _, G, _, G, _ }, { _, _, _, G, _, _, _ }, { _, _, _, _, _, _, _ }, } image:blit_icon( grass, { x = x - 5, z = z - 1, }, { [4] = true, [5] = true, [6] = true, [7] = true, [8] = true, [9] = true, } ) end end local positions = minetest.find_nodes_in_area_under_air( minp, maxp, "group:flower" ) for _, p in ipairs(positions) do local z = p.z - minp.z + 1 local x = p.x - minp.x + 1 local draw_flower = ( z > 1 and z < size - 3 and x > 2 and x < size - 2 ) if draw_flower then local _ = { nil } -- transparent local F = { 9 } -- line local flower = { { _, _, _, }, { _, F, _, }, { _, F, _, }, { _, _, _, }, } image:blit_icon( flower, { x = x - 2, z = z - 1, }, { [4] = true, [5] = true, [6] = true, [7] = true, [8] = true, [9] = true, } ) end end local filepath = textures_dir .. filename image:save( filepath, { colormap=colormap } ) maps.work[map_id] = false end minetest.emerge_area( minp, 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 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, maps.decode_map_item_meta(tga_deflate_base64) ) ) else minetest.log( "error", "no data for map " .. map_id ) return end end local texture = maps.load_map(map_id) return texture, itemstack end minetest.register_on_joinplayer( function(player) local player_name = player:get_player_name() local map_def = { hud_elem_type = "image", text = "blank.png", position = { x = 0.15, y = 0.90 }, alignment = { x = 0, y = -1 }, offset = { x = 0, y = 0 }, scale = { x = 4, y = 4 } } 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, updated_wield_item = 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 maps.mark[player_name] = nil end return end if ( texture ~= maps.maps[player_name] and updated_wield_item ) then player:set_wielded_item(updated_wield_item) 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( function(dtime) time_elapsed = time_elapsed + dtime if time_elapsed < ( 1 / 30 ) then return -- fps limiter end local players = minetest.get_connected_players() for _, player in pairs(players) do maps.show_map_hud(player) end end ) minetest.override_item( "map:mapping_kit", { on_place = function(itemstack, player, pointed_thing) local pos = pointed_thing.under if pos then local map = maps.create_map_item( pos, { draw_x = true } ) return map end end, on_secondary_use = function(itemstack, player, pointed_thing) local pos = player:get_pos() if pos then local map = maps.create_map_item(pos) return map end end, } ) minetest.register_craftitem( "maps:map", { description = "Map", inventory_image = "maps_map.tga", groups = { not_in_creative_inventory = 1 }, } )