xmaps/init.lua

775 lines
17 KiB
Lua
Raw Normal View History

2022-05-17 06:49:56 +02:00
--[[
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.
]]--
2022-05-19 03:19:25 +02:00
-- 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
2022-05-17 06:49:56 +02:00
maps = {}
2022-05-20 00:37:15 +02:00
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
2022-05-17 06:49:56 +02:00
2022-05-17 13:00:47 +02:00
local size = 80
2022-05-17 06:49:56 +02:00
local worldpath = minetest.get_worldpath()
local textures_dir = worldpath .. "/maps/"
minetest.mkdir(textures_dir)
2022-05-20 00:37:15 +02:00
maps.get_map_filename = function(map_id)
return "maps_map_texture_" .. map_id .. ".tga"
end
2022-05-17 06:49:56 +02:00
maps.create_map_item = function(pos, properties)
properties = properties or {}
2022-05-20 00:37:15 +02:00
local itemstack = ItemStack("maps:map")
local meta = itemstack:get_meta()
2022-05-17 06:49:56 +02:00
2022-05-20 00:37:15 +02:00
local map_id = tostring(os.time() + math.random())
meta:set_string("maps:id", map_id)
2022-05-17 06:49:56 +02:00
2022-05-20 00:37:15 +02:00
local minp = vector.multiply(vector.floor(vector.divide(pos, size)), size)
meta:set_string("maps:minp", minetest.pos_to_string(minp))
2022-05-17 06:49:56 +02:00
2022-05-20 00:37:15 +02:00
local maxp = vector.add(minp, vector.new(size - 1, size - 1, size - 1))
meta:set_string("maps:maxp", minetest.pos_to_string(maxp))
2022-05-17 06:49:56 +02:00
if properties.draw_x then
local xpos = vector.round(pos)
meta:set_string("maps:xpos", minetest.pos_to_string(xpos))
end
2022-05-17 06:49:56 +02:00
2022-05-20 00:37:15 +02:00
local filename = maps.get_map_filename(map_id)
2022-05-20 03:55:49 +02:00
maps.work[map_id] = true
2022-05-17 06:49:56 +02:00
local emerge_callback = function(
blockpos,
action,
calls_remaining
)
if calls_remaining > 0 then
return
end
local pixels = {}
local colormap = {
2022-05-19 14:20:28 +02:00
{ 195, 175, 140 }, -- background checkerboard light
{ 180, 160, 125 }, -- background checkerboard dark
2022-05-17 17:15:04 +02:00
{ 60, 35, 16 }, -- dark line
{ 210, 170, 130 }, -- liquid light
2022-05-17 20:31:14 +02:00
{ 135, 90, 40 }, -- liquid dark
{ 150, 105, 55 }, -- more liquid
{ 165, 120, 70 }, -- more liquid
{ 150, 105, 55 }, -- more liquid
2022-05-18 21:29:17 +02:00
{ 60, 35, 16 }, -- tree outline
{ 150, 105, 55 }, -- tree fill
2022-05-17 06:49:56 +02:00
}
2022-05-17 13:00:47 +02:00
for x = 1,size,1 do
for z = 1,size,1 do
2022-05-19 14:20:28 +02:00
local color = 0 + ( ( x + z ) % 2 )
2022-05-17 06:49:56 +02:00
pixels[z] = pixels[z] or {}
pixels[z][x] = { color }
2022-05-17 06:49:56 +02:00
end
end
2022-05-17 17:15:04 +02:00
local positions = minetest.find_nodes_in_area_under_air(
minp,
maxp,
"group:liquid"
)
for _, p in ipairs(positions) do
2022-05-19 06:05:33 +02:00
if 14 == minetest.get_node_light(p, 0.5) then
local z = p.z - minp.z + 1
local x = p.x - minp.x + 1
2022-05-19 14:20:28 +02:00
pixels[z][x] = { 3 }
2022-05-19 06:05:33 +02:00
end
2022-05-17 17:15:04 +02:00
end
-- draw coastline
for x = 1,size,1 do
for z = 1,size,1 do
2022-05-19 14:20:28 +02:00
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
2022-05-17 20:31:14 +02:00
end
2022-05-19 14:20:28 +02:00
if z > 1 and pixels[z-1][x][1] < 3 then
pixels[z-1][x] = { 2 }
pixels[z][x] = { 4 }
2022-05-17 17:15:04 +02:00
end
2022-05-19 14:20:28 +02:00
if z < size and pixels[z+1][x][1] < 3 then
pixels[z+1][x] = { 2 }
pixels[z][x] = { 4 }
2022-05-17 17:15:04 +02:00
end
2022-05-19 14:20:28 +02:00
if x > 1 and pixels[z][x-1][1] < 3 then
pixels[z][x-1] = { 2 }
pixels[z][x] = { 4 }
2022-05-17 17:15:04 +02:00
end
2022-05-19 14:20:28 +02:00
if x < size and pixels[z][x+1][1] < 3 then
pixels[z][x+1] = { 2 }
pixels[z][x] = { 4 }
2022-05-17 17:15:04 +02:00
end
end
2022-05-17 06:49:56 +02:00
end
end
2022-05-19 03:19:25 +02:00
local image = tga_encoder.image(pixels)
2022-05-19 15:09:08 +02:00
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
2022-05-18 21:29:17 +02:00
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 (
2022-05-19 03:46:40 +02:00
z > 1 and
z < size - 7 and
x > 4 and
x < size - 4
2022-05-18 21:29:17 +02:00
)
if draw_tree then
local tree = {}
2022-05-19 03:19:25 +02:00
local _ = { nil } -- transparent
2022-05-19 14:20:28 +02:00
local O = { 8 } -- outline
local F = { 9 } -- filling
2022-05-18 21:29:17 +02:00
if nil ~= node.name:find("pine") then
tree = {
2022-05-19 03:46:40 +02:00
{ _, _, _, _, _, _, _ },
{ _, _, _, O, _, _, _ },
{ _, O, O, O, O, O, _ },
{ _, O, F, F, F, O, _ },
{ _, _, O, F, O, _, _ },
{ _, _, O, F, O, _, _ },
{ _, _, _, O, _, _, _ },
{ _, _, _, _, _, _, _ },
2022-05-18 21:29:17 +02:00
}
else
tree = {
2022-05-19 03:46:40 +02:00
{ _, _, _, _, _, _, _ },
{ _, _, _, O, _, _, _ },
{ _, _, _, O, _, _, _ },
{ _, _, O, O, O, _, _ },
{ _, O, F, F, F, O, _ },
{ _, O, F, F, F, O, _ },
{ _, _, O, O, O, _, _ },
{ _, _, _, _, _, _, _ },
2022-05-18 21:29:17 +02:00
}
end
2022-05-19 03:19:25 +02:00
image:blit_icon(
tree,
{
2022-05-19 03:46:40 +02:00
x = x - 4,
z = z - 1,
2022-05-19 03:19:25 +02:00
},
{
[4] = true,
[5] = true,
[6] = true,
[7] = true,
2022-05-19 14:20:28 +02:00
[8] = true,
[9] = true,
2022-05-19 03:19:25 +02:00
}
)
2022-05-18 21:29:17 +02:00
end
end
2022-05-19 03:35:26 +02:00
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
2022-05-19 14:20:28 +02:00
local G = { 9 } -- line
2022-05-19 03:35:26 +02:00
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,
2022-05-19 14:20:28 +02:00
[8] = true,
[9] = true,
2022-05-19 03:35:26 +02:00
}
)
end
end
2022-05-19 19:32:47 +02:00
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
2022-05-17 06:49:56 +02:00
local filepath = textures_dir .. filename
2022-05-19 03:19:25 +02:00
image:save(
2022-05-17 06:49:56 +02:00
filepath,
{ colormap=colormap }
)
2022-05-20 15:05:27 +02:00
maps.work[map_id] = false
2022-05-17 06:49:56 +02:00
end
minetest.emerge_area(
minp,
maxp,
emerge_callback
)
2022-05-20 00:37:15 +02:00
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,
2022-05-20 15:33:40 +02:00
maps.decode_map_item_meta(tga_deflate_base64)
2022-05-20 00:37:15 +02:00
)
)
else
minetest.log(
"error",
"no data for map " .. map_id
)
return
end
end
2022-05-20 15:33:40 +02:00
local texture = maps.load_map(map_id)
return texture, itemstack
2022-05-17 06:49:56 +02:00
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 }
}
2022-05-20 00:37:15 +02:00
local pos_def = table.copy(map_def)
maps.huds[player_name] = {
map = player:hud_add(map_def),
pos = player:hud_add(pos_def),
}
2022-05-17 06:49:56 +02:00
end
)
2022-05-20 00:37:15 +02:00
maps.show_map_hud = function(player)
local wield_item = player:get_wielded_item()
2022-05-20 15:33:40 +02:00
local texture, updated_wield_item = maps.load_map_item(wield_item)
2022-05-20 00:37:15 +02:00
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
2022-05-20 00:37:15 +02:00
end
return
end
2022-05-20 15:53:21 +02:00
if (
texture ~= maps.maps[player_name] and
updated_wield_item
) then
2022-05-20 15:33:40 +02:00
player:set_wielded_item(updated_wield_item)
end
2022-05-20 00:37:15 +02:00
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.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] or
darkness ~= maps.dark[player_name]
) then
2022-05-20 00:37:15 +02:00
player:hud_change(
maps.huds[player_name].pos,
"text",
marker .. light_level_overlay
2022-05-20 00:37:15 +02:00
)
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
maps.dark[player_name] = darkness
2022-05-20 00:37:15 +02:00
end
2022-05-17 06:49:56 +02:00
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
2022-05-20 00:37:15 +02:00
maps.show_map_hud(player)
2022-05-17 06:49:56 +02:00
end
end
)
2022-05-20 00:37:15 +02:00
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,
2022-05-20 00:37:15 +02:00
}
)
minetest.register_craftitem(
"maps:map",
{
description = "Map",
2022-05-20 01:19:17 +02:00
inventory_image = "maps_map.tga",
groups = { not_in_creative_inventory = 1 },
2022-05-20 00:37:15 +02:00
}
)