--[[ xmaps – Minetest mod that adds map items that show terrain in HUD 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. ]]-- local protector = minetest.get_modpath ("protector") -- Detect stopping colors. function tga_encoder.image:detect_stop_colors(icon, pos, stop_colors) local x = pos.x local z = pos.z for i_z = 1,#icon do for i_x = 1,#icon[i_z] do if stop_colors[self.pixels[z + i_z][x + i_x][1]] then return true end end end end -- 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 -- Just as the comment above says, exit IMMEDIATELY if there -- are stop_colors present in the image. if self:detect_stop_colors(icon, pos, stop_colors) 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 xmaps = {} xmaps.dark = {} -- key: player name; value: is it dark? xmaps.huds = {} -- key: player name; value: player huds xmaps.maps = {} -- key: player name; value: map texture xmaps.mark = {} -- key: player name; value: marker texture xmaps.marx = {} -- key: player name; value: marker x offset xmaps.mary = {} -- key: player name; value: marker y offset xmaps.load = {} -- maps loaded by players xmaps.sent = {} -- maps sent to players xmaps.work = {} -- maps being created local size = 80 local worldpath = minetest.get_worldpath() local textures_dir = worldpath .. "/xmaps/" minetest.mkdir(textures_dir) function xmaps.get_map_filename(map_id) return "xmaps_map_texture_" .. map_id .. ".tga" end function xmaps.create_map_item(pos, properties) properties = properties or {} local itemstack = ItemStack("xmaps:map") local meta = itemstack:get_meta() local map_id = tostring(os.time() + math.random()) meta:set_string("xmaps:id", map_id) -- Size is defined above -- I love this, it's so clever local minp = vector.multiply(vector.floor(vector.divide(pos, size)), size) meta:set_string("xmaps:minp", minetest.pos_to_string(minp)) local maxp = vector.add(minp, vector.new(size - 1, size - 1, size - 1)) meta:set_string("xmaps:maxp", minetest.pos_to_string(maxp)) if properties.draw_x then local xpos = vector.round(pos) meta:set_string("xmaps:xpos", minetest.pos_to_string(xpos)) end local filename = xmaps.get_map_filename(map_id) xmaps.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 } ) xmaps.work[map_id] = false end minetest.emerge_area( minp, maxp, emerge_callback ) return itemstack end xmaps.load_map = function(map_id) assert( nil ~= map_id ) if ( "" == map_id or xmaps.work[map_id] ) then return end local filename = xmaps.get_map_filename(map_id) if not xmaps.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 ) xmaps.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() xmaps.load[map_id] = true end ) end xmaps.sent[map_id] = true end if xmaps.load[map_id] then return filename end end xmaps.encode_map_item_meta = function(input) return minetest.encode_base64( minetest.compress( input, "deflate", 9 ) ) end xmaps.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 = xmaps.decode_map_item_meta( xmaps.encode_map_item_meta(result_original) ) assert( result_original == result_roundtrip, "xmaps: mismatch between xmaps.encode_map_item_meta() and xmaps.decode_map_item_meta()" ) xmaps.load_map_item = function(itemstack) local meta = itemstack:get_meta() local map_id = meta:get_string("xmaps:id") if ( not map_id or "" == map_id or xmaps.work[map_id] ) then return end local texture_file_name = xmaps.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("xmaps: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 xmaps.decode_map_item_meta(tga_deflate_base64) ~= texture_data_from_file then minetest.log( "action", "xmaps: update item meta from file content for map " .. map_id ) meta:set_string( "xmaps:tga_deflate_base64", xmaps.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", "xmaps: create item meta from file content for map " .. map_id ) meta:set_string( "xmaps:tga_deflate_base64", xmaps.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", "xmaps: create file content from item meta for map " .. map_id ) assert( minetest.safe_file_write( texture_file_path, xmaps.decode_map_item_meta(tga_deflate_base64) ) ) else minetest.log( "error", "no data for map " .. map_id ) return end end local texture = xmaps.load_map(map_id) local meta_xpos = meta:get_string("xmaps:xpos") if texture and "" ~= meta_xpos then local meta_minp = meta:get_string("xmaps:minp") assert( "" ~= meta_minp ) local minp = minetest.string_to_pos(meta_minp) local meta_maxp = meta:get_string("xmaps:maxp") assert( "" ~= meta_maxp ) local maxp = minetest.string_to_pos(meta_maxp) 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 .. "=xmaps_x.tga" texture = texture .. x_overlay end 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) xmaps.huds[player_name] = { map = player:hud_add(map_def), pos = player:hud_add(pos_def), } end ) xmaps.show_map_hud = function(player) local wield_item = player:get_wielded_item() local texture, updated_wield_item = xmaps.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 xmaps.maps[player_name] then player:hud_change( xmaps.huds[player_name].map, "text", "blank.png" ) player:hud_change( xmaps.huds[player_name].pos, "text", "blank.png" ) xmaps.maps[player_name] = nil xmaps.mark[player_name] = nil end return end if ( texture ~= xmaps.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("xmaps:minp") assert( "" ~= meta_minp ) local minp = minetest.string_to_pos(meta_minp) local meta_maxp = meta:get_string("xmaps:maxp") assert( "" ~= meta_maxp ) local maxp = minetest.string_to_pos(meta_maxp) 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 ~= xmaps.maps[player_name] or darkness ~= xmaps.dark[player_name] ) then player:hud_change( xmaps.huds[player_name].map, "text", texture .. light_level_overlay ) xmaps.maps[player_name] = texture end local marker local dot_large = "xmaps_dot_large.tga" .. "^[makealpha:1,1,1" local dot_small = "xmaps_dot_small.tga" .. "^[makealpha:1,1,1" local dot_tiny = "xmaps_dot_tiny.tga" .. "^[makealpha:1,1,1" if pos.x < minp.x then if minp.x - pos.x < size * 2 then marker = dot_large elseif minp.x - pos.x < size * 4 then marker = dot_small else marker = dot_tiny end pos.x = minp.x elseif pos.x > maxp.x then if pos.x - maxp.x < size * 2 then marker = dot_large elseif pos.x - maxp.x < size * 4 then marker = dot_small else marker = dot_tiny end pos.x = maxp.x end -- we never override a smaller marker -- yes, this is a literal corner case if pos.z < minp.z then if ( minp.z - pos.z < size * 2 and marker ~= dot_small and marker ~= dot_tiny ) then marker = dot_large elseif ( minp.z - pos.z < size * 4 and marker ~= dot_tiny ) then marker = dot_small else marker = dot_tiny end pos.z = minp.z elseif pos.z > maxp.z then if ( pos.z - maxp.z < size * 2 and marker ~= dot_small and marker ~= dot_tiny ) then marker = dot_large elseif ( pos.z - maxp.z < size * 4 and marker ~= dot_tiny ) then marker = dot_small else marker = dot_tiny 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 = "xmaps_arrow.tga" .. "^[makealpha:1,1,1" .. "^[transformR" .. yaw elseif ( yaw == 45 or yaw == 135 or yaw == 225 or yaw == 315 ) then marker = "xmaps_arrow_diagonal.tga" .. "^[makealpha:1,1,1" .. "^[transformR" .. (yaw - 45) end end if marker and ( marker ~= xmaps.mark[player_name] or darkness ~= xmaps.dark[player_name] ) then player:hud_change( xmaps.huds[player_name].pos, "text", marker .. light_level_overlay ) xmaps.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 ~= xmaps.marx[player_name] or marker_y ~= xmaps.mary[player_name] ) then player:hud_change( xmaps.huds[player_name].pos, "offset", { x = marker_x, y = marker_y, } ) xmaps.marx[player_name] = marker_x xmaps.mary[player_name] = marker_y end xmaps.dark[player_name] = darkness 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 xmaps.show_map_hud(player) end end ) minetest.register_entity( "xmaps:map", { visual = "upright_sprite", visual_size = { x = 1, y = 1 }, physical = false, collide_with_objects = false, textures = { "xmaps_map.tga" }, on_activate = function(self, staticdata) if ( staticdata and "" ~= staticdata ) then local data = minetest.deserialize(staticdata) if not data then return end self._wallmounted = data._wallmounted assert( self._wallmounted ) self._itemstring = data._itemstring assert( self._itemstring ) local min, max = -8/16, 8/16 local len = 1/64 local sbox if 2 == self._wallmounted then sbox = { -len, min, min, len, max, max } elseif 3 == self._wallmounted then sbox = { -len, min, min, len, max, max } elseif 4 == self._wallmounted then sbox = { min, min, -len, max, max, len } elseif 5 == self._wallmounted then sbox = { min, min, -len, max, max, len } end assert( sbox ) self.object:set_properties({ selectionbox = sbox, textures = { "blank.png" }, }) local yaw = minetest.dir_to_yaw( minetest.wallmounted_to_dir( self._wallmounted ) ) self.object:set_yaw(yaw) end end, on_step = function(self) if self._texture then return end local itemstack = ItemStack(self._itemstring) self._texture, itemstack = xmaps.load_map_item(itemstack) self._itemstring = itemstack:to_string() self.object:set_properties({ textures = { self._texture } }) end, get_staticdata = function(self) return minetest.serialize( { _wallmounted = self._wallmounted, _itemstring = self._itemstring, } ) end, on_punch = function(self, puncher) -- Protection implemented. local pos = self.object:get_pos() if protector then if minetest.is_protected (pos, puncher) then return end end local itemstring = self._itemstring if pos and itemstring then minetest.add_item( pos, itemstring ) self.object:remove() end end } ) minetest.register_craftitem( "xmaps:map", { description = "Map", inventory_image = "xmaps_map.tga", groups = { not_in_creative_inventory = 1 }, on_place = function(itemstack, player, pointed_thing) if pointed_thing.type ~= "node" then return end local player_pos = player:get_pos() if not player_pos then return end local node_pos = pointed_thing.under if protector and minetest.is_protected (pointed_thing.above, player) then return end local direction = vector.normalize( vector.subtract( node_pos, player_pos ) ) local wallmounted = minetest.dir_to_wallmounted( direction ) -- TODO: implement maps on floor or ceiling if wallmounted < 2 then return end direction = minetest.wallmounted_to_dir( wallmounted ) local pos = vector.subtract( node_pos, vector.multiply( direction, 1/2 + 1/256 -- avoid z-fighting ) ) local itemstring = itemstack:to_string() if pos and "" ~= itemstring then local staticdata = { _wallmounted = wallmounted, _itemstring = itemstring, } local obj = minetest.add_entity( pos, "xmaps:map", minetest.serialize(staticdata) ) if obj then -- TODO: creative mode itemstack:take_item() end end return itemstack end } ) if minetest.registered_items["map:mapping_kit"] then minetest.override_item( "map:mapping_kit", { on_place = function(itemstack, player, pointed_thing) local pos = pointed_thing.under if protector and minetest.is_protected (pointed_thing.above, player) then return end if pos then local map = xmaps.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 = xmaps.create_map_item(pos) return map end end, } ) end