--[[ Teleporter networks that allow players to choose a destination out of a list Copyright (C) 2013 Sokomine This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Version: 2.0 (with elevators!) Changelog: 26.06.13 - added inventory image for elevator (created by VanessaE) 21.06.13 - bugfix: wielding an elevator while digging a door caused the elevator_top to be placed - leftover floating elevator_top nodes can be removed by placing a new travelnet:elevator underneath them and removing that afterwards - homedecor-doors are now opened and closed correctly as well - removed nodes that are not intended for manual use from creative inventory - improved naming of station levels for the elevator 21.06.13 - elevator stations are sorted by height instead of date of creation as is the case with travelnet boxes - elevator stations are named automaticly 20.06.13 - doors can be opened and closed from inside the travelnet box/elevator - the elevator can only move vertically; the network name is defined by its x and z coordinate 13.06.13 - bugfix - elevator added (written by kpoppel) and placed into extra file - elevator doors added - groups changed to avoid accidental dig/drop on dig of node beneath - added new priv travelnet_remove for digging of boxes owned by other players - only the owner of a box or players with the travelnet_remove priv can now dig it - entering your own name as owner_name does no longer abort setup 22.03.13 - added automatic detection if yaw can be set - beam effect is disabled by default 20.03.13 - added inventory image provided by VanessaE - fixed bug that made it impossible to remove stations from the net - if the station a player beamed to no longer exists, the station will be removed automaticly - with the travelnet_attach priv, you can now attach your box to the nets of other players - in newer versions of Minetest, the players yaw is set so that he/she looks out of the receiving box - target list is now centered if there are less than 9 targets --]] local MAX_STATIONS_PER_NETWORK = 24; minetest.register_privilege("travelnet_attach", { description = "allows to attach travelnet boxes to travelnets of other players", give_to_singleplayer = false}); minetest.register_privilege("travelnet_remove", { description = "allows to dig travelnet boxes which belog to nets of other players", give_to_singleplayer = false}); travelnet = {}; travelnet.targets = {}; -- set this to true if you want a simulated beam effect travelnet_effect_enabled = false; travelnet_sound_enabled = false; -- TODO: save and restore ought to be library functions and not implemented in each individual mod! -- called whenever a station is added or removed travelnet.save_data = function() local data = minetest.serialize( travelnet.targets ); local path = minetest.get_worldpath().."/mod_travelnet.data"; local file = io.open( path, "w" ); if( file ) then file:write( data ); file:close(); else print("[Mod travelnet] Error: Savefile '"..tostring( path ).."' could not be written."); end end travelnet.restore_data = function() local path = minetest.get_worldpath().."/mod_travelnet.data"; local file = io.open( path, "r" ); if( file ) then local data = file:read("*all"); travelnet.targets = minetest.deserialize( data ); file:close(); else print("[Mod travelnet] Error: Savefile '"..tostring( path ).."' not found."); end end travelnet.update_formspec = function( pos, puncher_name ) local meta = minetest.env:get_meta(pos); local this_node = minetest.env:get_node( pos ); local is_elevator = false; if( this_node ~= nil and this_node.name == 'travelnet:elevator' ) then is_elevator = true; end if( not( meta )) then return; end local owner_name = meta:get_string( "owner" ); local station_name = meta:get_string( "station_name" ); local station_network = meta:get_string( "station_network" ); if( not( owner_name ) or not( station_name ) or station_network == '' or not( station_network )) then if( is_elevator == true ) then travelnet.add_target( nil, nil, pos, puncher_name, meta, owner_name ); return; end -- minetest.chat_send_player(puncher_name, "DEBUG DATA: owner: "..(owner_name or "?").. -- " station_name: "..(station_name or "?").. -- " station_network: "..(station_network or "?").."."); -- minetest.chat_send_player(puncher_name, "data: "..minetest.serialize( travelnet.targets )); meta:set_string("infotext", "Travelnet-box (unconfigured)"); meta:set_string("station_name", ""); meta:set_string("station_network",""); meta:set_string("owner", ""); -- request initinal data meta:set_string("formspec", "size[12,10]".. "field[0.3,7.6;9,0.9;station_name;Name of this station:;"..(station_name or "?").."]".. "field[0.3,8.6;9,0.9;station_network;Assign to Network:;"..(station_network or "?").."]".. "field[0.3,9.6;9,0.9;owner;Owned by:;"..(owner_name or "?").."]".. "button_exit[6.3,8.2;1.7,0.7;station_set;Store]" ); minetest.chat_send_player(puncher_name, "Error: Update failed! Resetting this box on the travelnet."); return; end -- if the station got lost from the network for some reason (savefile corrupted?) then add it again if( not( travelnet.targets[ owner_name ] ) or not( travelnet.targets[ owner_name ][ station_network ] ) or not( travelnet.targets[ owner_name ][ station_network ][ station_name ] )) then -- first one by this player? if( not( travelnet.targets[ owner_name ] )) then travelnet.targets[ owner_name ] = {}; end -- first station on this network? if( not( travelnet.targets[ owner_name ][ station_network ] )) then travelnet.targets[ owner_name ][ station_network ] = {}; end local zeit = meta:get_int("timestamp"); if( not( zeit) or zeit<100000 ) then zeit = os.time(); end -- add this station travelnet.targets[ owner_name ][ station_network ][ station_name ] = {pos=pos, timestamp=zeit }; minetest.chat_send_player(owner_name, "Station '"..station_name.."' has been reattached to the network '"..station_network.."'."); end -- add name of station + network + owner + update-button local formspec = "size[12,10]".. "label[3.3,0.0;Travelnet-Box:]".."label[6.3,0.0;Punch box to update target list.]".. "label[0.3,0.4;Name of this station:]".."label[6.3,0.4;"..(station_name or "?").."]".. "label[0.3,0.8;Assigned to Network:]" .."label[6.3,0.8;"..(station_network or "?").."]".. "label[0.3,1.2;Owned by:]" .."label[6.3,1.2;"..(owner_name or "?").."]".. "label[3.3,1.6;Click on target to travel there:]"; -- "button_exit[5.3,0.3;8,0.8;do_update;Punch box to update destination list. Click on target to travel there.]".. local x = 0; local y = 0; local i = 0; -- collect all station names in a table local stations = {}; for k,v in pairs( travelnet.targets[ owner_name ][ station_network ] ) do table.insert( stations, k ); end -- minetest.chat_send_player(puncher_name, "stations: "..minetest.serialize( stations )); local ground_level = 1; if( is_elevator ) then table.sort( stations, function(a,b) return travelnet.targets[ owner_name ][ station_network ][ a ].pos.y > travelnet.targets[ owner_name ][ station_network ][ b ].pos.y end); -- find ground level local vgl_timestamp = 999999999999; for index,k in ipairs( stations ) do if( travelnet.targets[ owner_name ][ station_network ][ k ].timestamp < vgl_timestamp ) then vgl_timestamp = travelnet.targets[ owner_name ][ station_network ][ k ].timestamp; ground_level = index; end end for index,k in ipairs( stations ) do if( index == ground_level ) then travelnet.targets[ owner_name ][ station_network ][ k ].nr = 'G'; else travelnet.targets[ owner_name ][ station_network ][ k ].nr = tostring( ground_level - index ); end end else -- sort the table according to the timestamp (=time the station was configured) table.sort( stations, function(a,b) return travelnet.targets[ owner_name ][ station_network ][ a ].timestamp < travelnet.targets[ owner_name ][ station_network ][ b ].timestamp end); end -- if there are only 8 stations (plus this one), center them in the formspec if( #stations < 10 ) then x = 4; end for index,k in ipairs( stations ) do -- check if there is an elevator door in front that needs to be opened local open_door_cmd = false; if( k==station_name ) then open_door_cmd = true; end if( k ~= station_name or open_door_cmd) then i = i+1; -- new column if( y==8 ) then x = x+4; y = 0; end if( open_door_cmd ) then formspec = formspec .."button_exit["..(x)..","..(y+2.5)..";1,0.5;open_door;<>]".. "label["..(x+0.9)..","..(y+2.35)..";"..tostring( k ).."]"; elseif( is_elevator ) then formspec = formspec .."button_exit["..(x)..","..(y+2.5)..";1,0.5;target;"..tostring( travelnet.targets[ owner_name ][ station_network ][ k ].nr ).."]".. "label["..(x+0.9)..","..(y+2.35)..";"..tostring( k ).."]"; else formspec = formspec .."button_exit["..(x)..","..(y+2.5)..";4,0.5;target;"..k.."]"; end -- if( is_elevator ) then -- formspec = formspec ..' ('..tostring( travelnet.targets[ owner_name ][ station_network ][ k ].pos.y )..'m)'; -- end -- formspec = formspec .. ']'; y = y+1; --x = x+4; end end meta:set_string( "formspec", formspec ); meta:set_string( "infotext", "Station '"..tostring( station_name ).."' on travelnet '"..tostring( station_network ).. "' (owned by "..tostring( owner_name )..") ready for usage. Right-click to travel, punch to update."); minetest.chat_send_player(puncher_name, "The target list of this box on the travelnet has been updated."); end -- add a new target; meta is optional travelnet.add_target = function( station_name, network_name, pos, player_name, meta, owner_name ) -- if it is an elevator, determine the network name through x and z coordinates local this_node = minetest.env:get_node( pos ); local is_elevator = false; if( this_node.name == 'travelnet:elevator' ) then -- owner_name = '*'; -- the owner name is not relevant here is_elevator = true; network_name = tostring( pos.x )..','..tostring( pos.z ); if( not( station_name ) or station_name == '' ) then station_name = 'at '..tostring( pos.y )..'m'; end end if( station_name == "" or not(station_name )) then minetest.chat_send_player(player_name, "Please provide a name for this station."); return; end if( network_name == "" or not( network_name )) then minetest.chat_send_player(player_name, "Please provide the name of the network this station ought to be connected to."); return; end if( owner_name == nil or owner_name == '' or owner_name == player_name) then owner_name = player_name; elseif( is_elevator ) then -- elevator networks owner_name = player_name; elseif( not( travelnet.targets[ owner_name ] ) or not( travelnet.targets[ owner_name ][ network_name ] )) then minetest.chat_send_player(player_name, "There is no network named "..tostring( network_name ).." owned by "..tostring( owner_name )..". Aborting."); return; elseif( not( minetest.check_player_privs(player_name, {travelnet_attach=true}))) then minetest.chat_send_player(player_name, "You do not have the travelnet_attach priv which is required to attach your box to the network of someone else. Aborting."); return; end -- first one by this player? if( not( travelnet.targets[ owner_name ] )) then travelnet.targets[ owner_name ] = {}; end -- first station on this network? if( not( travelnet.targets[ owner_name ][ network_name ] )) then travelnet.targets[ owner_name ][ network_name ] = {}; end -- lua doesn't allow efficient counting here local anz = 0; for k,v in pairs( travelnet.targets[ owner_name ][ network_name ] ) do if( k == station_name ) then minetest.chat_send_player(player_name, "Error: A station named '"..station_name.."' already exists on this network. Please choose a diffrent name!"); return; end anz = anz + 1; end -- we don't want too many stations in the same network because that would get confusing when displaying the targets if( anz+1 > MAX_STATIONS_PER_NETWORK ) then minetest.chat_send_player(player_name, "Error: Network '"..network_name.."' already contains the maximum number (=" ..(MAX_STATIONS_PER_NETWORK)..") of allowed stations per network. Please choose a diffrent/new network name."); return; end -- add this station travelnet.targets[ owner_name ][ network_name ][ station_name ] = {pos=pos, timestamp=os.time() }; -- do we have a new node to set up? (and are not just reading from a safefile?) if( meta ) then minetest.chat_send_player(player_name, "Station '"..station_name.."' has been added to the network '" ..network_name.."', which now consists of "..( anz+1 ).." station(s)."); meta:set_string( "station_name", station_name ); meta:set_string( "station_network", network_name ); meta:set_string( "owner", owner_name ); meta:set_int( "timestamp", travelnet.targets[ owner_name ][ network_name ][ station_name ].timestamp); meta:set_string("formspec", "size[12,10]".. "field[0.3,0.6;6,0.7;station_name;Station:;".. meta:get_string("station_name").."]".. "field[0.3,3.6;6,0.7;station_network;Network:;"..meta:get_string("station_network").."]" ); -- display a list of all stations that can be reached from here travelnet.update_formspec( pos, player_name ); -- save the updated network data in a savefile over server restart travelnet.save_data(); end end -- allow doors to open travelnet.open_close_door = function( pos, player, mode ) local this_node = minetest.env:get_node( pos ); local pos2 = {x=pos.x,y=pos.y,z=pos.z}; if( this_node.param2 == 0 ) then pos2 = {x=pos.x,y=pos.y,z=(pos.z-1)}; elseif( this_node.param2 == 1 ) then pos2 = {x=(pos.x-1),y=pos.y,z=pos.z}; elseif( this_node.param2 == 2 ) then pos2 = {x=pos.x,y=pos.y,z=(pos.z+1)}; elseif( this_node.param2 == 3 ) then pos2 = {x=(pos.x+1),y=pos.y,z=pos.z}; end local door_node = minetest.env:get_node( pos2 ); if( door_node ~= nil and door_node.name ~= 'ignore' and door_node.name ~= 'air' and minetest.registered_nodes[ door_node.name ].on_rightclick ~= nil) then -- at least for homedecor, same facedir would mean "door closed" -- do not close the elevator door if it is already closed if( mode==1 and ( door_node.name == 'travelnet:elevator_door_glass_closed' or door_node.name == 'travelnet:elevator_door_steel_closed' -- handle doors that change their facedir or ( door_node.param2 == this_node.param2 and door_node.name ~= 'travelnet:elevator_door_glass_open' and door_node.name ~= 'travelnet:elevator_door_steel_open'))) then return; end -- do not open the doors if they are already open (works only on elevator-doors; not on doors in general) if( mode==2 and ( door_node.name == 'travelnet:elevator_door_glass_open' or door_node.name == 'travelnet:elevator_door_steel_open' -- handle doors that change their facedir or ( door_node.param2 ~= this_node.param2 and door_node.name ~= 'travelnet:elevator_door_glass_closed' and door_node.name ~= 'travelnet:elevator_door_steel_closed'))) then return; end if( mode==2 ) then minetest.after( 1, minetest.registered_nodes[ door_node.name ].on_rightclick, pos2, door_node, player ); else minetest.registered_nodes[ door_node.name ].on_rightclick(pos2, door_node, player); end end end travelnet.on_receive_fields = function(pos, formname, fields, player) local meta = minetest.env:get_meta(pos); local name = player:get_player_name(); -- if the box has not been configured yet if( meta:get_string("station_network")=="" ) then travelnet.add_target( fields.station_name, fields.station_network, pos, name, meta, fields.owner_name ); return; end if( fields.open_door ) then travelnet.open_close_door( pos, player, 0 ); return; end if( not( fields.target )) then minetest.chat_send_player(name, "Please click on the target you want to travel to."); return; end -- if there is something wrong with the data local owner_name = meta:get_string( "owner" ); local station_name = meta:get_string( "station_name" ); local station_network = meta:get_string( "station_network" ); if( not( owner_name ) or not( station_name ) or not( station_network ) or not( travelnet.targets[ owner_name ] ) or not( travelnet.targets[ owner_name ][ station_network ] )) then minetest.chat_send_player(name, "Error: There is something wrong with the configuration of this station. ".. " DEBUG DATA: owner: "..( owner_name or "?").. " station_name: "..(station_name or "?").. " station_network: "..(station_network or "?").."."); return end local this_node = minetest.env:get_node( pos ); if( this_node ~= nil and this_node.name == 'travelnet:elevator' ) then for k,v in pairs( travelnet.targets[ owner_name ][ station_network ] ) do if( travelnet.targets[ owner_name ][ station_network ][ k ].nr --..' ('..tostring( travelnet.targets[ owner_name ][ station_network ][ k ].pos.y )..'m)' == fields.target) then fields.target = k; end end end -- if the target station is gone if( not( travelnet.targets[ owner_name ][ station_network ][ fields.target ] )) then minetest.chat_send_player(name, "Station '"..( fields.target or "?").." does not exist (anymore?) on this network."); travelnet.update_formspec( pos, name ); return; end minetest.chat_send_player(name, "Initiating transfer to station '"..( fields.target or "?").."'.'"); if( travelnet_sound_enabled ) then minetest.sound_play("128590_7037-lq.mp3", {pos = pos, gain = 1.0, max_hear_distance = 10,}) end if( travelnet_effect_enabled ) then minetest.env:add_entity( {x=pos.x,y=pos.y+0.5,z=pos.z}, "travelnet:effect"); -- it self-destructs after 20 turns end -- close the doors at the sending station travelnet.open_close_door( pos, player, 1 ); -- transport the player to the target location local target_pos = travelnet.targets[ owner_name ][ station_network ][ fields.target ].pos; player:moveto( target_pos, false); if( travelnet_sound_enabled ) then minetest.sound_play("travelnet_travel.wav", {pos = target_pos, gain = 1.0, max_hear_distance = 10,}) end if( travelnet_effect_enabled ) then minetest.env:add_entity( {x=target_pos.x,y=target_pos.y+0.5,z=target_pos.z}, "travelnet:effect"); -- it self-destructs after 20 turns end -- check if the box has at the other end has been removed. local node2 = minetest.env:get_node( target_pos ); if( node2 ~= nil and node2.name ~= 'ignore' and node2.name ~= 'travelnet:travelnet' and node2.name ~= 'travelnet:elevator') then -- provide information necessary to identify the removed box local oldmetadata = { fields = { owner = owner_name, station_name = fields.target, station_network = station_network }}; travelnet.remove_box( target_pos, nil, oldmetadata, player ); -- do this only on servers where the function exists elseif( player.set_look_yaw ) then -- rotate the player so that he/she can walk straight out of the box local yaw = 0; local param2 = node2.param2; if( param2==0 ) then yaw = 180; elseif( param2==1 ) then yaw = 90; elseif( param2==2 ) then yaw = 0; elseif( param2==3 ) then yaw = 270; end player:set_look_yaw( math.rad( yaw )); -- this is only supported in recent versions of MT player:set_look_pitch( math.rad( 0 )); -- this is only supported in recent versions of MT end travelnet.open_close_door( target_pos, player, 2 ); end travelnet.remove_box = function( pos, oldnode, oldmetadata, digger ) if( not( oldmetadata ) or oldmetadata=="nil" or not(oldmetadata.fields)) then minetest.chat_send_player( digger:get_player_name(), "Error: Could not find information about the station that is to be removed."); return; end local owner_name = oldmetadata.fields[ "owner" ]; local station_name = oldmetadata.fields[ "station_name" ]; local station_network = oldmetadata.fields[ "station_network" ]; -- station is not known? then just remove it if( not( owner_name ) or not( station_name ) or not( station_network ) or not( travelnet.targets[ owner_name ] ) or not( travelnet.targets[ owner_name ][ station_network ] )) then minetest.chat_send_player( digger:get_player_name(), "Error: Could not find the station that is to be removed."); return; end travelnet.targets[ owner_name ][ station_network ][ station_name ] = nil; -- inform the owner minetest.chat_send_player( owner_name, "Station '"..station_name.."' has been REMOVED from the network '"..station_network.."'."); if( digger ~= nil and owner_name ~= digger:get_player_name() ) then minetest.chat_send_player( digger:get_player_name(), "Station '"..station_name.."' has been REMOVED from the network '"..station_network.."'."); end -- save the updated network data in a savefile over server restart travelnet.save_data(); end travelnet.can_dig = function( pos, player, description ) if( not( player )) then return false; end local name = player:get_player_name(); -- players with that priv can dig regardless of owner if( minetest.check_player_privs(name, {travelnet_remove=true})) then return true; end local meta = minetest.env:get_meta( pos ); local owner = meta:get_string('owner'); if( not( meta ) or not( owner) or owner=='') then minetest.chat_send_player(name, "This "..description.." has not been configured yet. Please set it up first to claim it. Afterwards you can remove it because you are then the owner."); return false; elseif( owner ~= name ) then minetest.chat_send_player(name, "This "..description.." belongs to "..tostring( meta:get_string('owner'))..". You can't remove it."); return false; end return true; end minetest.register_node("travelnet:travelnet", { description = "Travelnet box", drawtype = "nodebox", sunlight_propagates = true, paramtype = 'light', paramtype2 = "facedir", selection_box = { type = "fixed", fixed = { -0.5, -0.5, -0.5, 0.5, 1.5, 0.5 } }, node_box = { type = "fixed", fixed = { { 0.45, -0.5,-0.5, 0.5, 1.45, 0.5}, {-0.5 , -0.5, 0.45, 0.45, 1.45, 0.5}, {-0.5, -0.5,-0.5 ,-0.45, 1.45, 0.5}, --groundplate to stand on { -0.5,-0.5,-0.5,0.5,-0.45, 0.5}, --roof { -0.5, 1.45,-0.5,0.5, 1.5, 0.5}, -- control panel -- { -0.2, 0.6, 0.3, 0.2, 1.1, 0.5}, }, }, tiles = { "default_clay.png", -- view from top "default_clay.png", -- view from bottom "travelnet_travelnet_side.png", -- left side "travelnet_travelnet_side.png", -- right side "travelnet_travelnet_back.png", -- front view "travelnet_travelnet_front.png", -- backward view }, inventory_image = "travelnet_inv.png", groups = {cracky=1,choppy=1,snappy=1}, light_source = 10, after_place_node = function(pos, placer, itemstack) local meta = minetest.env:get_meta(pos); meta:set_string("infotext", "Travelnet-box (unconfigured)"); meta:set_string("station_name", ""); meta:set_string("station_network",""); meta:set_string("owner", placer:get_player_name() ); -- request initinal data meta:set_string("formspec", "size[12,10]".. "field[0.3,5.6;6,0.7;station_name;Name of this station:;]".. "field[0.3,6.6;6,0.7;station_network;Assign to Network:;]".. "field[0.3,7.6;6,0.7;owner_name;(optional) owned by:;]".. "button_exit[6.3,6.2;1.7,0.7;station_set;Store]" ); end, on_receive_fields = travelnet.on_receive_fields, on_punch = function(pos, node, puncher) travelnet.update_formspec(pos, puncher:get_player_name()) end, can_dig = function( pos, player ) return travelnet.can_dig( pos, player, 'travelnet box' ) end, after_dig_node = function(pos, oldnode, oldmetadata, digger) travelnet.remove_box( pos, oldnode, oldmetadata, digger ) end, -- taken from VanessaEs homedecor fridge on_place = function(itemstack, placer, pointed_thing) local pos = pointed_thing.above; if( minetest.env:get_node({x=pos.x, y=pos.y+1, z=pos.z}).name ~= "air" ) then minetest.chat_send_player( placer:get_player_name(), 'Not enough vertical space to place the travelnet box!' ) return; end return minetest.item_place(itemstack, placer, pointed_thing); end, }) minetest.register_entity( 'travelnet:effect', { hp_max = 1, physical = false, weight = 5, collisionbox = {-0.4,-0.5,-0.4, 0.4,1.5,0.4}, visual = "upright_sprite", visual_size = {x=1, y=2}, -- mesh = "model", textures = { "travelnet_flash.png" }, -- number of required textures depends on visual -- colors = {}, -- number of required colors depends on visual spritediv = {x=1, y=1}, initial_sprite_basepos = {x=0, y=0}, is_visible = true, makes_footstep_sound = false, automatic_rotate = true, anz_rotations = 0, on_step = function( self, dtime ) -- this is supposed to be more flickering than smooth animation self.object:setyaw( self.object:getyaw()+1); self.anz_rotations = self.anz_rotations + 1; -- eventually self-destruct if( self.anz_rotations > 15 ) then self.object:remove(); end end }) minetest.register_craft({ output = "travelnet:travelnet", recipe = { {"default:glass", "default:steel_ingot", "default:glass", }, {"default:glass", "default:mese", "default:glass", }, {"default:glass", "default:steel_ingot", "default:glass", } } }) dofile(minetest.get_modpath("travelnet").."/elevator.lua"); -- upon server start, read the savefile travelnet.restore_data();