├── .github └── workflows │ ├── integration-test.yml │ └── luacheck.yml ├── .luacheckrc ├── bones.lua ├── border.lua ├── bridge ├── advtrains.lua ├── airutils_planes.lua ├── defaults.lua ├── init.lua ├── locator.lua ├── players.lua └── search.lua ├── common.lua ├── init.lua ├── integration-test.sh ├── integration_test.lua ├── label.lua ├── legacy.lua ├── mod.conf ├── poi.lua ├── privs.lua ├── readme.md ├── show_waypoint.lua ├── sounds └── whoosh.ogg ├── textures ├── mapserver_border.png ├── mapserver_gold_block.png ├── mapserver_label.png ├── mapserver_poi_blue.png ├── mapserver_poi_green.png ├── mapserver_poi_orange.png ├── mapserver_poi_purple.png ├── mapserver_poi_red.png └── mapserver_train.png └── train.lua /.github/workflows/integration-test.yml: -------------------------------------------------------------------------------- 1 | name: integration-test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | 13 | - name: integration-test 14 | run: ./integration-test.sh 15 | -------------------------------------------------------------------------------- /.github/workflows/luacheck.yml: -------------------------------------------------------------------------------- 1 | name: luacheck 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: apt 13 | run: sudo apt-get install -y luarocks 14 | - name: luacheck install 15 | run: luarocks install --local luacheck 16 | - name: luacheck run 17 | run: $HOME/.luarocks/bin/luacheck ./ 18 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | unused_args = false 2 | allow_defined_top = true 3 | 4 | globals = { 5 | "mapserver", 6 | "moditems", 7 | "airutils", 8 | "skins" 9 | } 10 | 11 | read_globals = { 12 | -- Stdlib 13 | string = {fields = {"split"}}, 14 | table = {fields = {"copy", "getn"}}, 15 | 16 | -- Minetest 17 | "minetest", 18 | "vector", "ItemStack", 19 | "dump", 20 | 21 | -- Deps 22 | "unified_inventory", "default", "advtrains", 23 | locator = { fields = { "beacons" } }, 24 | 25 | -- optional mods 26 | "xban", "monitoring", "QoS", 27 | "mcl_core", "mcl_sounds", "bones" 28 | } 29 | -------------------------------------------------------------------------------- /bones.lua: -------------------------------------------------------------------------------- 1 | -- bones owner saving workaround 2 | -- https://github.com/minetest/minetest_game/blob/master/mods/bones/init.lua#L120 3 | 4 | if bones and bones.redo then 5 | -- bones redo doesn't remove owner, so workaround is not needed 6 | return 7 | end 8 | 9 | local bones_def = minetest.registered_items["bones:bones"] 10 | if not bones_def then 11 | return 12 | end 13 | 14 | local bones_on_timer = bones_def.on_timer 15 | if not bones_on_timer or type(bones_on_timer) ~= "function" then 16 | return 17 | end 18 | 19 | minetest.override_item("bones:bones", { 20 | on_timer = function(pos, elapsed) 21 | -- save owner in separate field 22 | local meta = minetest.get_meta(pos) 23 | meta:set_string("_owner", meta:get_string("owner")) 24 | 25 | -- call original function 26 | return bones_on_timer(pos, elapsed) 27 | end 28 | }) 29 | -------------------------------------------------------------------------------- /border.lua: -------------------------------------------------------------------------------- 1 | 2 | local last_index = 0 3 | local last_name = "" 4 | 5 | local update_formspec = function(meta) 6 | local name = meta:get_string("name") 7 | local index = meta:get_string("index") 8 | local color = meta:get_string("color") or "" 9 | 10 | meta:set_string("infotext", "Border: Name=" .. name .. ", Index=" .. index) 11 | 12 | meta:set_string("formspec", "size[8,4;]" .. 13 | -- col 1 14 | "field[0,1;4,1;name;Name;" .. name .. "]" .. 15 | "button_exit[4,1;4,1;save;Save]" .. 16 | 17 | -- col 2 18 | "field[4,2.5;4,1;index;Index;" .. index .. "]" .. 19 | 20 | -- col 3 21 | "field[4,3.5;4,1;color;Color;" .. color .. "]" .. 22 | "") 23 | 24 | end 25 | 26 | 27 | minetest.register_node("mapserver:border", { 28 | description = "Mapserver Border", 29 | tiles = { 30 | "mapserver_border.png" 31 | }, 32 | groups = {cracky=3,oddly_breakable_by_hand=3,handy=1}, 33 | is_ground_content = false, 34 | sounds = moditems.sound_glass(), 35 | can_dig = mapserver.can_interact, 36 | after_place_node = mapserver.after_place_node, 37 | _mcl_blast_resistance = 1, 38 | _mcl_hardness = 0.3, 39 | 40 | on_construct = function(pos) 41 | local meta = minetest.get_meta(pos) 42 | 43 | last_index = last_index + 5 44 | 45 | meta:set_string("color", "") 46 | meta:set_string("name", last_name) 47 | meta:set_int("index", last_index) 48 | 49 | update_formspec(meta) 50 | end, 51 | 52 | on_receive_fields = function(pos, formname, fields, sender) 53 | 54 | if not mapserver.can_interact(pos, sender) then 55 | return 56 | end 57 | 58 | local meta = minetest.get_meta(pos) 59 | 60 | if fields.save then 61 | last_name = fields.name 62 | meta:set_string("name", fields.name) 63 | meta:set_string("color", fields.color) 64 | local index = tonumber(fields.index) 65 | if index ~= nil then 66 | last_index = index 67 | meta:set_int("index", index) 68 | end 69 | end 70 | 71 | update_formspec(meta) 72 | end 73 | }) 74 | 75 | if mapserver.enable_crafting then 76 | minetest.register_craft({ 77 | output = 'mapserver:border', 78 | recipe = { 79 | {"", moditems.steelblock, ""}, 80 | {moditems.paper, moditems.goldblock, moditems.paper}, 81 | {"", moditems.glass, ""} 82 | } 83 | }) 84 | end 85 | -------------------------------------------------------------------------------- /bridge/advtrains.lua: -------------------------------------------------------------------------------- 1 | 2 | mapserver.bridge.add_advtrains = function(data) 3 | -- train/wagon data 4 | data.trains = {} 5 | for _, train in pairs(advtrains.trains) do 6 | 7 | local t = { 8 | text_outside = train.text_outside, 9 | text_inside = train.text_inside, 10 | line = train.line, 11 | pos = train.last_pos, 12 | velocity = train.velocity, 13 | off_track = train.off_track, 14 | id = train.id, 15 | wagons = {} 16 | } 17 | 18 | for _, part in pairs(train.trainparts) do 19 | local wagon = advtrains.wagons[part] 20 | if wagon ~= nil then 21 | table.insert(t.wagons, { 22 | id = wagon.id, 23 | type = wagon.type, 24 | pos_in_train = wagon.pos_in_train, 25 | }) 26 | end 27 | end 28 | 29 | table.insert(data.trains, t) 30 | end 31 | 32 | -- signal data 33 | data.signals = {} 34 | local ildb = advtrains.interlocking.db.save() 35 | for _, entry in pairs(ildb.tcbs) do 36 | --print(dump(entry)) 37 | if entry[1].signal then 38 | local tcb = entry[1] 39 | local green = tcb.aspect and tcb.aspect.main ~= 0 40 | table.insert(data.signals, { 41 | pos = tcb.signal, 42 | green = green 43 | }) 44 | elseif entry[2].signal then 45 | local tcb = entry[2] 46 | local green = tcb.aspect and tcb.aspect.main ~= 0 47 | table.insert(data.signals, { 48 | pos = tcb.signal, 49 | green = green 50 | }) 51 | end 52 | end 53 | 54 | end 55 | -------------------------------------------------------------------------------- /bridge/airutils_planes.lua: -------------------------------------------------------------------------------- 1 | 2 | local planes = {} 3 | 4 | -- we monkey patch various functions within airutils to let us know when plane entities are 5 | -- added, updated, or removed from game scope 6 | -- planes deactivated by the game (no players in range, etc) will not be sent to mapserver 7 | if airutils then 8 | minetest.log("info", "[mapserver-bridge] patching airutils functions to track planes") 9 | local au_setText = airutils.setText 10 | local au_actfunc = airutils.actfunc 11 | local au_save_inv = airutils.save_inventory 12 | 13 | -- actfunc is always called when planes are activated, so it provides 14 | -- a single convenient point to start tracking them 15 | if au_actfunc and (type(au_setText) == "function") then 16 | airutils.actfunc = function(self, staticdata, dtime_s) 17 | if not self.__id then 18 | self.__id = tostring(math.random()) 19 | end 20 | planes[self.__id] = self 21 | 22 | -- call original 23 | return au_actfunc(self, staticdata, dtime_s) 24 | end 25 | end 26 | 27 | -- save_inventory is called when planes are deactivated, so it provides 28 | -- a single convenient point to untrack them 29 | if au_save_inv and (type(au_setText) == "function") then 30 | airutils.save_inventory = function(self) 31 | if planes[self.__id] then 32 | planes[self.__id] = nil 33 | end 34 | 35 | -- call original 36 | return au_save_inv(self) 37 | end 38 | end 39 | 40 | -- this is a convenience, which allows us to grab the "proper name" 41 | -- of the plane 42 | if au_setText and (type(au_setText) == "function") then 43 | airutils.setText = function(self, vehicle_name) 44 | self.__name = vehicle_name 45 | 46 | -- call original function 47 | return au_setText(self, vehicle_name) 48 | end 49 | end 50 | else 51 | minetest.log("warning", "[mapserver-bridge] no airutils!") 52 | end 53 | 54 | mapserver.bridge.add_airutils_planes = function(data) 55 | data.airutils_planes = {} 56 | for _, plane in pairs(planes) do 57 | if plane then 58 | -- do some extra work for passengers, which vary between planes 59 | local passengers 60 | if plane._passenger then 61 | passengers = plane._passenger 62 | elseif plane._passengers then 63 | for _, p in ipairs(plane._passengers) do 64 | if p then 65 | if passengers then passengers = passengers .. ", " .. p 66 | else passengers = p end 67 | end 68 | end 69 | end 70 | 71 | -- blimp uses color while others use _color 72 | local color 73 | if plane.color then color = plane.color 74 | else color = plane._color end 75 | 76 | table.insert(data.airutils_planes, { 77 | entity = plane.name, 78 | name = plane.__name, 79 | id = plane.__id, 80 | owner = plane.owner, 81 | driver = plane.driver_name, 82 | passenger = passengers, 83 | color = color, 84 | pos = plane.object:get_pos(), 85 | yaw = plane.object:get_yaw() 86 | }) 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /bridge/defaults.lua: -------------------------------------------------------------------------------- 1 | 2 | local has_get_server_max_lag 3 | 4 | if type(minetest.get_server_max_lag) == "function" then 5 | has_get_server_max_lag = true 6 | else 7 | has_get_server_max_lag = false 8 | end 9 | 10 | local function explode(sep, input) 11 | local t={} 12 | local i=0 13 | for k in string.gmatch(input,"([^"..sep.."]+)") do 14 | t[i]=k 15 | i=i+1 16 | end 17 | return t 18 | end 19 | 20 | local function get_max_lag() 21 | if has_get_server_max_lag then 22 | return minetest.get_server_max_lag() 23 | end 24 | local arrayoutput = explode(", ",minetest.get_server_status()) 25 | arrayoutput = explode("=",arrayoutput[4]) 26 | return arrayoutput[1] 27 | end 28 | 29 | mapserver.bridge.add_defaults = function(data) 30 | data.time = minetest.get_timeofday() * 24000 31 | data.uptime = minetest.get_server_uptime() 32 | data.max_lag = tonumber(get_max_lag()) 33 | 34 | end 35 | -------------------------------------------------------------------------------- /bridge/init.lua: -------------------------------------------------------------------------------- 1 | local MP = minetest.get_modpath("mapserver") 2 | dofile(MP .. "/bridge/defaults.lua") 3 | dofile(MP .. "/bridge/players.lua") 4 | dofile(MP .. "/bridge/advtrains.lua") 5 | dofile(MP .. "/bridge/locator.lua") 6 | 7 | 8 | -- mapserver http bridge 9 | local has_advtrains = minetest.get_modpath("advtrains") 10 | local has_locator = minetest.get_modpath("locator") 11 | local has_monitoring = minetest.get_modpath("monitoring") 12 | 13 | local has_airutils = minetest.get_modpath("airutils") 14 | if has_airutils then 15 | dofile(MP .. "/bridge/airutils_planes.lua") 16 | end 17 | 18 | local metric_post_size 19 | local metric_processing_post_time 20 | local metric_post_time 21 | 22 | if has_monitoring then 23 | metric_post_size = monitoring.counter( 24 | "mapserver_mod_post_size", 25 | "size in bytes of post data" 26 | ) 27 | metric_processing_post_time = monitoring.counter( 28 | "mapserver_mod_processing_post_time", 29 | "time usage in microseconds for processing post data" 30 | ) 31 | metric_post_time = monitoring.counter( 32 | "mapserver_mod_post_time", 33 | "time usage in microseconds for post data" 34 | ) 35 | end 36 | 37 | local http, url, key 38 | 39 | function send_stats() 40 | local t0 = minetest.get_us_time() 41 | 42 | -- data to send to mapserver 43 | local data = {} 44 | 45 | mapserver.bridge.add_players(data) 46 | mapserver.bridge.add_defaults(data) 47 | 48 | if has_advtrains then 49 | -- send trains if 'advtrains' mod installed 50 | mapserver.bridge.add_advtrains(data) 51 | end 52 | 53 | if has_locator then 54 | -- send locator beacons 55 | mapserver.bridge.add_locators(data) 56 | end 57 | 58 | if has_airutils then 59 | -- send airutils plane entities 60 | mapserver.bridge.add_airutils_planes(data) 61 | end 62 | 63 | 64 | local json = minetest.write_json(data) 65 | --print(json)--XXX 66 | 67 | local t1 = minetest.get_us_time() 68 | local process_time = t1 - t0 69 | if process_time > 50000 then 70 | minetest.log("warning", "[mapserver-bridge] processing took " .. process_time .. " us") 71 | end 72 | 73 | local size = string.len(json) 74 | if size > 256000 then 75 | minetest.log("warning", "[mapserver-bridge] json-size is " .. size .. " bytes") 76 | end 77 | 78 | http.fetch({ 79 | url = url .. "/api/minetest", 80 | extra_headers = { "Content-Type: application/json", "Authorization: " .. key }, 81 | timeout = 5, 82 | post_data = json 83 | }, function(res) 84 | 85 | local t2 = minetest.get_us_time() 86 | local post_time = t2 - t1 87 | if post_time > 1000000 then -- warn if over a second 88 | minetest.log("warning", "[mapserver-bridge] post took " .. post_time .. " us") 89 | end 90 | 91 | if has_monitoring then 92 | metric_post_size.inc(size) 93 | metric_processing_post_time.inc(process_time) 94 | metric_post_time.inc(post_time) 95 | end 96 | 97 | -- TODO: error-handling 98 | minetest.after(mapserver.send_interval, send_stats) 99 | end) 100 | 101 | end 102 | 103 | function mapserver.bridge_init(h, u, k) 104 | http = h 105 | url = u 106 | key = k 107 | 108 | minetest.after(mapserver.send_interval, send_stats) 109 | end 110 | -------------------------------------------------------------------------------- /bridge/locator.lua: -------------------------------------------------------------------------------- 1 | 2 | mapserver.bridge.add_locators = function(data) 3 | -- data.locators = locator.beacons 4 | end 5 | -------------------------------------------------------------------------------- /bridge/players.lua: -------------------------------------------------------------------------------- 1 | 2 | local has_skinsdb = minetest.get_modpath("skinsdb") and skins 3 | 4 | mapserver.bridge.add_players = function(data) 5 | 6 | data.players = {} 7 | 8 | for _, player in ipairs(minetest.get_connected_players()) do 9 | 10 | local is_hidden = minetest.check_player_privs(player:get_player_name(), {mapserver_hide_player = true}) 11 | local is_moderator = minetest.check_player_privs(player:get_player_name(), {ban = true}) 12 | 13 | local detail = minetest.get_player_information(player:get_player_name()) 14 | local protocol_version = -1 15 | local rtt = -1 16 | 17 | if detail then 18 | rtt = detail.avg_rtt 19 | protocol_version = detail.protocol_version 20 | end 21 | 22 | local skin 23 | if has_skinsdb then 24 | skin = skins.get_player_skin(player):get_texture() 25 | end 26 | 27 | local info = { 28 | name = player:get_player_name(), 29 | pos = player:get_pos(), 30 | hp = player:get_hp(), 31 | breath = player:get_breath(), 32 | velocity = player:get_velocity(), 33 | moderator = is_moderator, 34 | rtt = rtt, 35 | yaw = player:get_look_horizontal(), 36 | skin = skin, 37 | protocol_version = protocol_version 38 | } 39 | 40 | if not is_hidden then 41 | table.insert(data.players, info) 42 | end 43 | end 44 | 45 | end 46 | -------------------------------------------------------------------------------- /bridge/search.lua: -------------------------------------------------------------------------------- 1 | local FORMNAME = "mapserver_mod_search_results" 2 | 3 | -- playername -> {} 4 | local search_results = {} 5 | 6 | -- playername = 7 | local selected_item_data = {} 8 | 9 | minetest.register_on_leaveplayer(function(player) 10 | search_results[player:get_player_name()] = nil 11 | selected_item_data[player:get_player_name()] = nil 12 | end) 13 | 14 | minetest.register_on_player_receive_fields(function(player, formname, fields) 15 | if formname ~= FORMNAME then 16 | return 17 | end 18 | 19 | local selected_item = 0 20 | local playername = player:get_player_name() 21 | 22 | if fields.items then 23 | local parts = fields.items:split(":") 24 | if parts[1] == "CHG" then 25 | selected_item = tonumber(parts[2]) - 1 26 | end 27 | end 28 | 29 | if selected_item > 0 then 30 | local data = search_results[playername] 31 | local item = data[selected_item] 32 | 33 | selected_item_data[playername] = item 34 | end 35 | 36 | local item = selected_item_data[playername] 37 | if not item then 38 | return 39 | end 40 | 41 | if fields.teleport then 42 | -- teleport player to selected item 43 | if not minetest.check_player_privs(playername, "teleport") then 44 | minetest.chat_send_player(playername, "Missing priv: 'teleport'") 45 | return 46 | end 47 | 48 | -- flat destination coordinates per default 49 | local pos1 = vector.subtract(item.pos, {x=2, y=0, z=2}) 50 | local pos2 = vector.add(item.pos, {x=2, y=0, z=2}) 51 | 52 | if item.type == "bones" then 53 | -- search for air _above_ the bones 54 | pos1 = vector.subtract(item.pos, {x=0, y=0, z=0}) 55 | pos2 = vector.add(item.pos, {x=0, y=10, z=0}) 56 | end 57 | 58 | -- forceload target coordinates before searching for air 59 | minetest.get_voxel_manip():read_from_map(pos1, pos2) 60 | local nodes = minetest.find_nodes_in_area(pos1, pos2, "air") 61 | 62 | if #nodes > 0 then 63 | player:set_pos(nodes[1]) 64 | minetest.sound_play("whoosh", {pos = nodes[1], gain = 0.5, max_hear_distance = 10}) 65 | end 66 | elseif fields.show then 67 | mapserver.show_waypoint(playername, item.pos, item.description, 60) 68 | end 69 | 70 | end) 71 | 72 | 73 | local function show_formspec(playername, data) 74 | local list = "" 75 | local player = minetest.get_player_by_name(playername) 76 | 77 | if not player then 78 | return 79 | end 80 | 81 | local player_pos = player:get_pos() 82 | 83 | -- populate pos and distance field 84 | for i, item in ipairs(data) do 85 | item.pos = {x=item.x, y=item.y, z=item.z} 86 | item.distance = math.floor(vector.distance(item.pos, player_pos)) 87 | end 88 | 89 | -- sort by distance 90 | table.sort(data, function(a,b) 91 | return a.distance < b.distance 92 | end) 93 | 94 | -- data to store as last result 95 | local last_result_data = {} 96 | 97 | -- render list items 98 | for _, item in ipairs(data) do 99 | local owner = item.attributes.owner 100 | local distance = math.floor(item.distance) .. " m" 101 | local coords = item.pos.x .. "/" .. item.pos.y .. "/" .. item.pos.z 102 | local description = "" 103 | local color = "#FFFFFF" 104 | local add_to_list = true 105 | 106 | -- don't trust any values in attributes, they might not be present 107 | if item.type == "bones" then 108 | -- bone 109 | description = minetest.formspec_escape( 110 | (item.attributes.info or "?") .. 111 | " items: " .. (item.attributes.item_count or "?") 112 | ) 113 | 114 | elseif item.type == "shop" then 115 | -- shop 116 | 117 | if item.attributes.stock == "0" then 118 | -- don't add empty vendors to the list 119 | add_to_list = false 120 | else 121 | -- stocked shop 122 | description = minetest.formspec_escape("Shop, " .. 123 | "trading " .. (item.attributes.out_count or "?") .. 124 | "x " .. (item.attributes.out_item or "?") .. 125 | " for " .. (item.attributes.in_count or "?") .. 126 | "x " .. (item.attributes.in_item or "?") .. 127 | " Stock: " .. (item.attributes.stock or "?") 128 | ) 129 | end 130 | 131 | elseif item.type == "poi" then 132 | -- point of interest 133 | description = minetest.formspec_escape( 134 | (item.attributes.name or "?") .. 135 | " (owner: " .. (item.attributes.owner or "?") .. ")" 136 | ) 137 | end 138 | 139 | -- save description 140 | item.description = description 141 | 142 | if add_to_list then 143 | -- result data 144 | table.insert(last_result_data, item) 145 | 146 | -- formspec data 147 | list = list .. "," .. 148 | color .. "," .. 149 | distance .. "," .. 150 | (owner or "?") .. "," .. 151 | coords .. "," .. 152 | description 153 | end 154 | 155 | end 156 | 157 | -- store filtered result data 158 | search_results[playername] = last_result_data 159 | 160 | if #last_result_data == 0 then 161 | minetest.chat_send_player(playername, "Query failed, no results!") 162 | return 163 | end 164 | minetest.chat_send_player(playername, "Got " .. #last_result_data .. " results") 165 | 166 | list = list .. ";]" 167 | 168 | local teleport_button = "" 169 | 170 | -- show teleport button 171 | if minetest.check_player_privs(playername, "teleport") then 172 | teleport_button = "button_exit[4,11;4,1;teleport;Teleport]" 173 | end 174 | 175 | local formspec = [[ 176 | size[16,12;] 177 | label[0,0;Search results (]] .. #last_result_data .. [[)] 178 | button_exit[0,11;4,1;show;Show] 179 | ]] .. teleport_button .. [[ 180 | button_exit[12,11;4,1;exit;Exit] 181 | tablecolumns[color;text;text;text;text] 182 | table[0,1;15.7,10;items;#999,Distance,Owner,Coords,Description]] .. list 183 | 184 | minetest.show_formspec(playername, FORMNAME, formspec) 185 | end 186 | 187 | -- valid search types 188 | local valid_types = { 189 | shop = true, 190 | bones = true, 191 | poi = true 192 | } 193 | 194 | -- global values, passed by init 195 | local http, url 196 | 197 | -- chatcommand 198 | minetest.register_chatcommand("search", { 199 | description = "Search for shops or bones near you. Syntax: /search (bones|shop|poi) (|*)\n" 200 | .. "e.g. /search bones *", 201 | func = function(playername, param) 202 | 203 | local _, _, type, query = string.find(param, "^([^%s]+)%s+([^%s]+)%s*$") 204 | if type == nil or query == nil or not valid_types[type] then 205 | minetest.chat_send_player(playername, "syntax: /search (bones|shop|poi) (|*)") 206 | return 207 | end 208 | 209 | local json = "{" 210 | 211 | json = json .. '"pos1": {"x":-2048, "y":-2048, "z":-2048},' 212 | json = json .. '"pos2": {"x":2048, "y":2048, "z":2048},' 213 | json = json .. '"type":"' .. type .. '"' 214 | 215 | -- switch between types of queries 216 | -- search for "out_item" if it is a shop or for "owner" if bones are wanted 217 | local key_name = "unknown" 218 | if type == "poi" then 219 | key_name = "name" 220 | elseif type == "bones" then 221 | key_name = "owner" 222 | elseif type == "shop" then 223 | key_name = "out_item" 224 | end 225 | 226 | if query and query ~= "*" then 227 | json = json .. ',' 228 | json = json .. '"attributelike":{' 229 | json = json .. '"key":"' .. key_name .. '",' 230 | json = json .. '"value":"%' .. query .. '%"' 231 | json = json .. "}" 232 | end 233 | 234 | json = json .. "}" 235 | 236 | http.fetch({ 237 | url = url .. "/api/mapobjects/", 238 | timeout = 10, 239 | extra_headers = { "Content-Type: application/json" }, 240 | post_data = json 241 | }, function(res) 242 | if res.code == 200 then 243 | local data = minetest.parse_json(res.data) 244 | if data and #data > 0 then 245 | show_formspec(playername, data) 246 | else 247 | minetest.chat_send_player(playername, "Query failed, no results!") 248 | end 249 | else 250 | minetest.chat_send_player(playername, "Query failed, http-status: " .. (res.status or "")) 251 | end 252 | end) 253 | 254 | 255 | 256 | return true, "Searching for: " .. type .. " '" .. query .. "' ..." 257 | end 258 | }) 259 | 260 | function mapserver.search_init(h, u) 261 | http = h 262 | url = u 263 | end 264 | -------------------------------------------------------------------------------- /common.lua: -------------------------------------------------------------------------------- 1 | 2 | mapserver.after_place_node = function(pos, placer) 3 | local meta = minetest.get_meta(pos) 4 | meta:set_string("owner", placer:get_player_name() or "") 5 | end 6 | 7 | mapserver.can_interact = function(pos, player) 8 | if not player then 9 | return false 10 | end 11 | 12 | local meta = minetest.get_meta(pos) 13 | local owner = meta:get_string("owner") 14 | local playername = player:get_player_name() 15 | 16 | if playername == owner then 17 | return true 18 | end 19 | 20 | if minetest.check_player_privs(playername, {protection_bypass = true}) then 21 | return true 22 | end 23 | 24 | return false 25 | end 26 | -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | local default_path = minetest.get_modpath("default") and default 2 | local mineclone_path = minetest.get_modpath("mcl_core") and mcl_core 3 | 4 | moditems = {} 5 | 6 | if mineclone_path then 7 | moditems.sound_glass = mcl_sounds.node_sound_glass_defaults 8 | moditems.goldblock = "mcl_core:goldblock" 9 | moditems.steelblock = "mcl_core:ironblock" 10 | moditems.steel_ingot = "mcl_core:iron_ingot" 11 | moditems.paper = "mcl_core:paper" 12 | moditems.glass = "mcl_core:glass" 13 | moditems.dye = "mcl_dye:" 14 | elseif default_path then 15 | moditems.sound_glass = default.node_sound_glass_defaults 16 | moditems.goldblock = "default:goldblock" 17 | moditems.steelblock = "default:steelblock" 18 | moditems.steel_ingot = "default:steel_ingot" 19 | moditems.paper = "default:paper" 20 | moditems.glass = "default:glass" 21 | moditems.dye = "dye:" 22 | else 23 | moditems.sound_glass = function() end 24 | end 25 | 26 | mapserver = { 27 | enable_crafting = minetest.settings:get("mapserver.enable_crafting") == "true", 28 | send_interval = tonumber(minetest.settings:get("mapserver.send_interval")) or 2, 29 | 30 | bridge = {} 31 | } 32 | 33 | local MP = minetest.get_modpath("mapserver") 34 | dofile(MP.."/common.lua") 35 | dofile(MP.."/poi.lua") 36 | dofile(MP.."/train.lua") 37 | dofile(MP.."/label.lua") 38 | dofile(MP.."/border.lua") 39 | dofile(MP.."/legacy.lua") 40 | dofile(MP.."/privs.lua") 41 | dofile(MP.."/show_waypoint.lua") 42 | 43 | if minetest.get_modpath("bones") then 44 | dofile(MP.."/bones.lua") 45 | end 46 | 47 | local http = minetest.request_http_api and minetest.request_http_api() 48 | if minetest.get_modpath("qos") and http then 49 | -- use qos-wrapped http 50 | http = QoS(http, 2) 51 | end 52 | 53 | if http then 54 | -- check if the mapserver.json is in the world-folder 55 | local path = minetest.get_worldpath().."/mapserver.json"; 56 | local mapserver_cfg 57 | 58 | local file = io.open( path, "r" ); 59 | if file then 60 | local json = file:read("*all"); 61 | mapserver_cfg = minetest.parse_json(json); 62 | file:close(); 63 | print("[Mapserver] read settings from 'mapserver.json'") 64 | end 65 | 66 | local mapserver_url = minetest.settings:get("mapserver.url") 67 | local mapserver_key = minetest.settings:get("mapserver.key") 68 | 69 | if mapserver_cfg and mapserver_cfg.webapi then 70 | if not mapserver_key then 71 | -- apply key from json 72 | mapserver_key = mapserver_cfg.webapi.secretkey 73 | end 74 | if not mapserver_url then 75 | -- assemble url from json 76 | mapserver_url = "http://127.0.0.1:" .. mapserver_cfg.port 77 | end 78 | end 79 | 80 | if not mapserver_url then error("mapserver.url is not defined") end 81 | if not mapserver_key then error("mapserver.key is not defined") end 82 | 83 | print("[Mapserver] starting mapserver-bridge with endpoint: " .. mapserver_url) 84 | dofile(MP .. "/bridge/init.lua") 85 | 86 | -- enable ingame map-search 87 | dofile(MP.."/bridge/search.lua") 88 | mapserver.search_init(http, mapserver_url) 89 | 90 | -- initialize bridge 91 | mapserver.bridge_init(http, mapserver_url, mapserver_key) 92 | 93 | else 94 | print("[Mapserver] bridge not active, additional infos will not be visible on the map") 95 | 96 | end 97 | 98 | 99 | print("[OK] Mapserver") 100 | 101 | if minetest.settings:get_bool("enable_mapserver_integration_test") then 102 | dofile(MP.."/integration_test.lua") 103 | end 104 | -------------------------------------------------------------------------------- /integration-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # simple integration test 3 | 4 | CFG=/tmp/minetest.conf 5 | MTDIR=/tmp/mt 6 | WORLDDIR=${MTDIR}/worlds/world 7 | 8 | cat < ${CFG} 9 | enable_mapserver_integration_test = true 10 | EOF 11 | 12 | mkdir -p ${WORLDDIR} 13 | chmod 777 ${MTDIR} -R 14 | docker run --rm -i \ 15 | -v ${CFG}:/etc/minetest/minetest.conf:ro \ 16 | -v ${MTDIR}:/var/lib/minetest/.minetest \ 17 | -v $(pwd):/var/lib/minetest/.minetest/worlds/world/worldmods/mapserver \ 18 | registry.gitlab.com/minetest/minetest/server:5.2.0 19 | 20 | test -f ${WORLDDIR}/integration_test.json && exit 0 || exit 1 21 | -------------------------------------------------------------------------------- /integration_test.lua: -------------------------------------------------------------------------------- 1 | 2 | minetest.log("warning", "[TEST] integration-test enabled!") 3 | 4 | minetest.register_on_mods_loaded(function() 5 | minetest.after(1, function() 6 | 7 | local data = minetest.write_json({ success = true }, true); 8 | local file = io.open(minetest.get_worldpath().."/integration_test.json", "w" ); 9 | if file then 10 | file:write(data) 11 | file:close() 12 | end 13 | 14 | file = io.open(minetest.get_worldpath().."/registered_nodes.txt", "w" ); 15 | if file then 16 | for name in pairs(minetest.registered_nodes) do 17 | file:write(name .. '\n') 18 | end 19 | file:close() 20 | end 21 | 22 | minetest.log("warning", "[TEST] integration tests done!") 23 | minetest.request_shutdown("success") 24 | end) 25 | end) 26 | -------------------------------------------------------------------------------- /label.lua: -------------------------------------------------------------------------------- 1 | 2 | local update_formspec = function(meta) 3 | local text = meta:get_string("text") 4 | local size = meta:get_string("size") 5 | local direction = meta:get_string("direction") 6 | local color = meta:get_string("color") or "rgb(0,0,0)" 7 | 8 | meta:set_string("infotext", "Label, Text:" .. text .. ", Size:" .. size .. ", Direction:" .. direction) 9 | 10 | meta:set_string("formspec", "size[8,6;]" .. 11 | -- col 1 12 | "field[0,1;4,1;text;Text;" .. text .. "]" .. 13 | "button_exit[4,1;4,1;save;Save]" .. 14 | 15 | -- col 2 16 | "field[0,2.5;4,1;size;Size (1-200);" .. size .. "]" .. 17 | 18 | -- col 3 19 | "field[0,3.5;8,1;direction;Direction (0-360);" .. direction .. "]" .. 20 | 21 | -- col 4 22 | "field[0,4.5;8,1;color;Color;" .. color .. "]" .. 23 | 24 | "") 25 | 26 | end 27 | 28 | 29 | minetest.register_node("mapserver:label", { 30 | description = "Mapserver Label", 31 | tiles = { 32 | "mapserver_label.png" 33 | }, 34 | groups = {cracky=3,oddly_breakable_by_hand=3,handy=1}, 35 | is_ground_content = false, 36 | sounds = moditems.sound_glass(), 37 | can_dig = mapserver.can_interact, 38 | after_place_node = mapserver.after_place_node, 39 | _mcl_blast_resistance = 1, 40 | _mcl_hardness = 0.3, 41 | 42 | on_construct = function(pos) 43 | local meta = minetest.get_meta(pos) 44 | 45 | meta:set_string("text", "") 46 | meta:set_string("direction", "0") 47 | meta:set_string("size", "20") 48 | meta:set_string("color", "rgb(0,0,0)") 49 | 50 | update_formspec(meta) 51 | end, 52 | 53 | on_receive_fields = function(pos, formname, fields, sender) 54 | 55 | if not mapserver.can_interact(pos, sender) then 56 | return 57 | end 58 | 59 | local meta = minetest.get_meta(pos) 60 | 61 | if fields.save then 62 | meta:set_string("color", fields.color) 63 | meta:set_string("text", fields.text) 64 | meta:set_string("direction", fields.direction) 65 | meta:set_string("size", fields.size) 66 | end 67 | 68 | update_formspec(meta) 69 | end 70 | }) 71 | 72 | if mapserver.enable_crafting then 73 | minetest.register_craft({ 74 | output = 'mapserver:label', 75 | recipe = { 76 | {"", moditems.paper, ""}, 77 | {moditems.paper, moditems.goldblock, moditems.paper}, 78 | {"", moditems.glass, ""} 79 | } 80 | }) 81 | end 82 | -------------------------------------------------------------------------------- /legacy.lua: -------------------------------------------------------------------------------- 1 | 2 | minetest.register_lbm({ 3 | label = "Tileserver->Mapserver poi upgrade", 4 | name = "mapserver:poi", 5 | nodenames = {"tileserver:poi"}, 6 | run_at_every_load = true, 7 | action = function(pos, node) 8 | minetest.swap_node(pos, { name="mapserver:poi" }) 9 | end 10 | }) 11 | 12 | minetest.register_lbm({ 13 | label = "Tileserver->Mapserver train upgrade", 14 | name = "mapserver:train", 15 | nodenames = {"tileserver:train"}, 16 | run_at_every_load = true, 17 | action = function(pos, node) 18 | minetest.swap_node(pos, { name="mapserver:train" }) 19 | end 20 | }) 21 | -------------------------------------------------------------------------------- /mod.conf: -------------------------------------------------------------------------------- 1 | name = mapserver 2 | description = Mod for the mapserver. 3 | optional_depends = default, dye, advtrains, monitoring, bones, mcl_core, mcl_sounds, mcl_dye, qos, airutils 4 | -------------------------------------------------------------------------------- /poi.lua: -------------------------------------------------------------------------------- 1 | 2 | -- possible icons: https://fontawesome.com/icons?d=gallery&s=brands,regular,solid&m=free 3 | -- default: "home" 4 | 5 | local update_formspec = function(meta) 6 | local name = meta:get_string("name") 7 | local icon = meta:get_string("icon") or "home" 8 | local addr = meta:get_string("addr") or "" 9 | local url = meta:get_string("url") or "" 10 | local image = meta:get_string("image") or "" 11 | 12 | meta:set_string("infotext", "POI, name:" .. name .. ", icon:" .. icon) 13 | 14 | meta:set_string("formspec", "size[8,6;]" .. 15 | -- col 1 16 | "field[0.2,1;4,1;name;Name;" .. name .. "]" .. 17 | "field[4.2,1;4,1;icon;Icon;" .. icon .. "]" .. 18 | 19 | -- col 2 20 | "field[0.2,2;8,1;addr;Address;" .. addr .. "]" .. 21 | 22 | -- col 3 23 | "field[0.2,3;8,1;url;URL;" .. url .. "]" .. 24 | 25 | -- col 4 26 | "field[0.2,4;8,1;image;Image;" .. image .. "]" .. 27 | 28 | -- col 5 29 | "button_exit[0,5;8,1;save;Save]" .. 30 | "") 31 | 32 | end 33 | 34 | local on_receive_fields = function(pos, formname, fields, sender) 35 | 36 | if not mapserver.can_interact(pos, sender) then 37 | return 38 | end 39 | 40 | local meta = minetest.get_meta(pos) 41 | 42 | if fields.save then 43 | meta:set_string("name", fields.name) 44 | meta:set_string("addr", fields.addr) 45 | meta:set_string("url", fields.url) 46 | meta:set_string("image", fields.image) 47 | meta:set_string("icon", fields.icon or "home") 48 | end 49 | 50 | update_formspec(meta) 51 | end 52 | 53 | local register_poi = function(color, dye) 54 | minetest.register_node("mapserver:poi_" .. color, { 55 | description = "Mapserver POI (" .. color .. ")", 56 | tiles = { 57 | "[combine:16x16:0,0=mapserver_gold_block.png:3,2=mapserver_poi_" .. color .. ".png" 58 | }, 59 | groups = {cracky=3,oddly_breakable_by_hand=3,handy=1}, 60 | is_ground_content = false, 61 | sounds = moditems.sound_glass(), 62 | can_dig = mapserver.can_interact, 63 | after_place_node = mapserver.after_place_node, 64 | _mcl_blast_resistance = 1, 65 | _mcl_hardness = 0.3, 66 | 67 | on_construct = function(pos) 68 | local meta = minetest.get_meta(pos) 69 | 70 | meta:set_string("name", "") 71 | meta:set_string("icon", "home") 72 | meta:set_string("addr", "") 73 | meta:set_string("url", "") 74 | meta:set_string("image", "") 75 | 76 | update_formspec(meta) 77 | end, 78 | 79 | on_receive_fields = on_receive_fields 80 | }) 81 | 82 | 83 | if mapserver.enable_crafting and (minetest.get_modpath("dye") or minetest.get_modpath("mcl_core")) then 84 | minetest.register_craft({ 85 | output = 'mapserver:poi_' .. color, 86 | recipe = { 87 | {"", moditems.dye .. dye, ""}, 88 | {moditems.paper, moditems.goldblock, moditems.paper}, 89 | {"", moditems.glass, ""} 90 | } 91 | }) 92 | end 93 | end 94 | 95 | register_poi("blue", "blue") 96 | register_poi("green", "green") 97 | register_poi("orange", "orange") 98 | register_poi("red", "red") 99 | register_poi("purple", "violet") 100 | 101 | -- default poi was always blue 102 | minetest.register_alias("mapserver:poi", "mapserver:poi_blue") 103 | -------------------------------------------------------------------------------- /privs.lua: -------------------------------------------------------------------------------- 1 | 2 | minetest.register_privilege("mapserver_hide_player", { 3 | description = "Player is hidden from the map", 4 | give_to_singleplayer = false 5 | }) 6 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | # mapserver mod 3 | 4 | ![](https://github.com/minetest-mapserver/mapserver_mod/workflows/luacheck/badge.svg) 5 | ![](https://github.com/minetest-mapserver/mapserver_mod/workflows/integration-test/badge.svg) 6 | 7 | This is the complementary mod for the mapserver: https://github.com/minetest-tools/mapserver 8 | 9 | ## Documentation 10 | 11 | See: https://github.com/minetest-tools/mapserver/blob/master/doc/mod.md 12 | 13 | ## example for active mode configuration 14 | 15 | minetest.conf 16 | ``` 17 | secure.http_mods = mapserver 18 | mapserver.url = http://127.0.0.1:8080 19 | mapserver.key = myserverkey 20 | ``` 21 | 22 | # Contributors 23 | 24 | Thanks to: 25 | * @Panquesito7 (mod.conf/depends.txt cleanup) 26 | * @SwissalpS (minor corrections) 27 | * @Athemis (mineclone support) 28 | 29 | # License 30 | 31 | * Source code: MIT 32 | * Textures: CC BY-SA 3.0 (unless otherwise noted) 33 | * Sounds 34 | * whoosh.ogg: https://github.com/ChaosWormz/teleport-request 35 | -------------------------------------------------------------------------------- /show_waypoint.lua: -------------------------------------------------------------------------------- 1 | 2 | 3 | function mapserver.show_waypoint(playername, pos, name, seconds) 4 | local player = minetest.get_player_by_name(playername) 5 | if not player then 6 | return 7 | end 8 | 9 | local id = player:hud_add({ 10 | hud_elem_type = "waypoint", 11 | name = name, 12 | text = "m", 13 | number = 0xFF0000, 14 | world_pos = pos 15 | }) 16 | 17 | minetest.after(seconds, function() 18 | player = minetest.get_player_by_name(playername) 19 | if not player then 20 | return 21 | end 22 | 23 | player:hud_remove(id) 24 | end) 25 | end 26 | -------------------------------------------------------------------------------- /sounds/whoosh.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minetest-mapserver/mapserver_mod/7ee2f4f7df906bb23185eafa71d1d05d44e20ed9/sounds/whoosh.ogg -------------------------------------------------------------------------------- /textures/mapserver_border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minetest-mapserver/mapserver_mod/7ee2f4f7df906bb23185eafa71d1d05d44e20ed9/textures/mapserver_border.png -------------------------------------------------------------------------------- /textures/mapserver_gold_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minetest-mapserver/mapserver_mod/7ee2f4f7df906bb23185eafa71d1d05d44e20ed9/textures/mapserver_gold_block.png -------------------------------------------------------------------------------- /textures/mapserver_label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minetest-mapserver/mapserver_mod/7ee2f4f7df906bb23185eafa71d1d05d44e20ed9/textures/mapserver_label.png -------------------------------------------------------------------------------- /textures/mapserver_poi_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minetest-mapserver/mapserver_mod/7ee2f4f7df906bb23185eafa71d1d05d44e20ed9/textures/mapserver_poi_blue.png -------------------------------------------------------------------------------- /textures/mapserver_poi_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minetest-mapserver/mapserver_mod/7ee2f4f7df906bb23185eafa71d1d05d44e20ed9/textures/mapserver_poi_green.png -------------------------------------------------------------------------------- /textures/mapserver_poi_orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minetest-mapserver/mapserver_mod/7ee2f4f7df906bb23185eafa71d1d05d44e20ed9/textures/mapserver_poi_orange.png -------------------------------------------------------------------------------- /textures/mapserver_poi_purple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minetest-mapserver/mapserver_mod/7ee2f4f7df906bb23185eafa71d1d05d44e20ed9/textures/mapserver_poi_purple.png -------------------------------------------------------------------------------- /textures/mapserver_poi_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minetest-mapserver/mapserver_mod/7ee2f4f7df906bb23185eafa71d1d05d44e20ed9/textures/mapserver_poi_red.png -------------------------------------------------------------------------------- /textures/mapserver_train.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minetest-mapserver/mapserver_mod/7ee2f4f7df906bb23185eafa71d1d05d44e20ed9/textures/mapserver_train.png -------------------------------------------------------------------------------- /train.lua: -------------------------------------------------------------------------------- 1 | 2 | local last_set_by = {} 3 | 4 | local update_formspec = function(meta) 5 | local line = meta:get_string("line") 6 | local station = meta:get_string("station") 7 | local index = meta:get_string("index") 8 | local color = meta:get_string("color") or "" 9 | 10 | meta:set_string("infotext", "Train: Line=" .. line .. ", Station=" .. station) 11 | 12 | meta:set_string("formspec", "size[8,4;]" .. 13 | -- col 1 14 | "field[0,1;4,1;line;Line;" .. line .. "]" .. 15 | "button_exit[4,1;4,1;save;Save]" .. 16 | 17 | -- col 2 18 | "field[0,2.5;4,1;station;Station;" .. station .. "]" .. 19 | "field[4,2.5;4,1;index;Index;" .. index .. "]" .. 20 | 21 | -- col 3 22 | "field[0,3.5;4,1;color;Color;" .. color .. "]" .. 23 | "" 24 | ) 25 | 26 | end 27 | 28 | 29 | minetest.register_node("mapserver:train", { 30 | description = "Mapserver Train", 31 | tiles = { 32 | "mapserver_train.png" 33 | }, 34 | groups = {cracky=3,oddly_breakable_by_hand=3,handy=1}, 35 | is_ground_content = false, 36 | sounds = moditems.sound_glass(), 37 | can_dig = mapserver.can_interact, 38 | _mcl_blast_resistance = 1, 39 | _mcl_hardness = 0.3, 40 | 41 | after_place_node = function(pos, placer, itemstack, pointed_thing) 42 | local meta = minetest.get_meta(pos) 43 | 44 | local last_index = 0 45 | local last_line = "" 46 | local last_color = "" 47 | 48 | if minetest.is_player(placer) then 49 | local name = placer:get_player_name() 50 | if name ~= nil then 51 | name = string.lower(name) 52 | if last_set_by[name] ~= nil then 53 | last_index = last_set_by[name].index + 5 54 | last_line = last_set_by[name].line 55 | last_color = last_set_by[name].color 56 | else 57 | last_set_by[name] = {} 58 | end 59 | 60 | last_set_by[name].index = last_index 61 | last_set_by[name].line = last_line 62 | last_set_by[name].color = last_color 63 | end 64 | end 65 | 66 | meta:set_string("station", "") 67 | meta:set_string("line", last_line) 68 | meta:set_int("index", last_index) 69 | meta:set_string("color", last_color) 70 | 71 | update_formspec(meta) 72 | 73 | 74 | return mapserver.after_place_node(pos, placer, itemstack, pointed_thing) 75 | end, 76 | 77 | on_receive_fields = function(pos, formname, fields, sender) 78 | 79 | if not mapserver.can_interact(pos, sender) then 80 | return 81 | end 82 | 83 | local meta = minetest.get_meta(pos) 84 | local name = string.lower(sender:get_player_name()) 85 | 86 | if fields.save then 87 | if last_set_by[name] == nil then 88 | last_set_by[name] = {} 89 | end 90 | 91 | local index = tonumber(fields.index) or 0 92 | 93 | meta:set_string("color", fields.color) 94 | meta:set_string("line", fields.line) 95 | meta:set_string("station", fields.station) 96 | meta:set_int("index", index) 97 | 98 | last_set_by[name].color = fields.color 99 | last_set_by[name].line = fields.line 100 | last_set_by[name].station = fields.station 101 | last_set_by[name].index = index 102 | end 103 | 104 | update_formspec(meta) 105 | end 106 | }) 107 | 108 | if mapserver.enable_crafting then 109 | minetest.register_craft({ 110 | output = 'mapserver:train', 111 | recipe = { 112 | {"", moditems.steel_ingot, ""}, 113 | {moditems.paper, moditems.goldblock, moditems.paper}, 114 | {"", moditems.glass, ""} 115 | } 116 | }) 117 | end 118 | --------------------------------------------------------------------------------