868 lines
27 KiB
Lua
868 lines
27 KiB
Lua
|
|
|
|
|
|
-- TODO: test multiplayer functionality
|
|
--
|
|
-- Note: The x-coordinate is reversed in sign between minetest and minecraft,
|
|
-- and the API compensates for this.
|
|
|
|
if minetest.request_insecure_environment then
|
|
ie = minetest.request_insecure_environment()
|
|
else
|
|
ie = _G
|
|
end
|
|
|
|
local source = ie.debug.getinfo(1).source:sub(2)
|
|
-- Detect windows via backslashes in paths
|
|
local mypath = minetest.get_modpath(minetest.get_current_modname())
|
|
local is_windows = (nil ~= string.find(ie.package.path..ie.package.cpath..source..mypath, "%\\%?"))
|
|
local path_separator
|
|
if is_windows then
|
|
path_separator = "\\"
|
|
else
|
|
path_separator = "/"
|
|
end
|
|
mypath = mypath .. path_separator
|
|
|
|
local script_window_id = "minetest-rjm-python-script"
|
|
|
|
ie.package.path = ie.package.path .. ";" .. mypath .. "?.lua"
|
|
if is_windows then
|
|
ie.package.cpath = ie.package.cpath .. ";" .. mypath .. "?.dll"
|
|
else
|
|
ie.package.cpath = ie.package.cpath .. ";" .. mypath .. "?.so"
|
|
end
|
|
|
|
local block = ie.require("block")
|
|
local socket = ie.require("socket")
|
|
|
|
local block_hits = {}
|
|
local chat_record = {}
|
|
local player_table = {}
|
|
local socket_client_list = {}
|
|
|
|
script_running = false
|
|
restrict_to_sword = true
|
|
max_player_id = 0
|
|
default_player_id = -1
|
|
world_immutable = false
|
|
|
|
local settings = Settings(mypath .. "settings.conf")
|
|
local update_settings = false
|
|
python_interpreter = settings:get("python")
|
|
if python_interpreter == nil then
|
|
python_interpreter = "python"
|
|
update_settings = true
|
|
settings:set("python", python_interpreter)
|
|
end
|
|
local local_only = settings:get_bool("restrict_to_local_connections")
|
|
if local_only == nil then
|
|
local_only = false
|
|
update_settings = true
|
|
settings:set("restrict_to_local_connections", tostring(local_only))
|
|
end
|
|
local ws = settings:get_bool("support_websockets")
|
|
if ws == nil then
|
|
ws = true
|
|
update_settings = true
|
|
settings:set("support_websockets", tostring(ws))
|
|
end
|
|
if update_settings then settings:write() end
|
|
|
|
local settings = Settings(mypath .. "override.conf")
|
|
local x = settings:get("python")
|
|
if x ~= nil then
|
|
python_interpreter = x
|
|
end
|
|
x = settings:get_bool("restrict_to_local_connections")
|
|
if x ~= nil then
|
|
local_only = x
|
|
end
|
|
x = settings:get_bool("support_websockets")
|
|
if x ~= nil then
|
|
ws = x
|
|
end
|
|
|
|
local remote_address
|
|
if local_only then
|
|
remote_address = "127.0.0.1"
|
|
else
|
|
remote_address = "*"
|
|
end
|
|
|
|
local server,err = socket.bind(remote_address, 4711)
|
|
assert(server, err)
|
|
|
|
server:setoption('tcp-nodelay',true)
|
|
server:settimeout(0)
|
|
|
|
local ws_server = nil
|
|
|
|
if ws then
|
|
if not bit or not bit.bxor then bit = ie.require("slowbit32") end
|
|
tools = ie.require("tools")
|
|
base64 = ie.require("base64")
|
|
ws_server = socket.bind(remote_address, 14711)
|
|
ws_server:setoption('tcp-nodelay',true)
|
|
ws_server:settimeout(0)
|
|
end
|
|
|
|
|
|
minetest.register_globalstep(function(dtime)
|
|
local newclient,err
|
|
|
|
if server then
|
|
newclient,err = server:accept()
|
|
if not err then
|
|
newclient:settimeout(0)
|
|
table.insert(socket_client_list,
|
|
{client=newclient,handler=safe_handle_command,read_mode="*l"})
|
|
minetest.log("action", "RJM socket client connected")
|
|
end
|
|
end
|
|
if ws_server then
|
|
newclient,err = ws_server:accept()
|
|
if not err then
|
|
newclient:settimeout(0)
|
|
table.insert(socket_client_list,
|
|
{client=newclient,handler=handle_websocket_header,ws={},read_mode="*l"})
|
|
minetest.log("action", "RJM websocket client attempting handshake")
|
|
end
|
|
end
|
|
|
|
local command_count = 1000
|
|
|
|
for i = 1, #socket_client_list do
|
|
err = false
|
|
local line
|
|
local finished = false
|
|
|
|
while not err and not finished do
|
|
local source = socket_client_list[i]
|
|
line,err = source.client:receive(source.read_mode)
|
|
if err == "closed" then
|
|
table.remove(socket_client_list,i)
|
|
minetest.log("action", "RJM socket client disconnected")
|
|
finished = true
|
|
elseif not err then
|
|
err = source:handler(line)
|
|
if err then
|
|
source.client:close()
|
|
table.remove(socket_client_list,i)
|
|
if err ~= "closed" then
|
|
minetest.log("error", "Error "..err.." in command: RJM socket client disconnected")
|
|
end
|
|
finished = true
|
|
err = "handling"
|
|
else
|
|
command_count = command_count - 1
|
|
if command_count < 0 then
|
|
finished = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if finished then break end
|
|
end
|
|
|
|
flush_block_buffer()
|
|
end)
|
|
|
|
local old_is_protected = minetest.is_protected
|
|
function minetest.is_protected(pos, name)
|
|
return world_immutable or old_is_protected(pos, name)
|
|
end
|
|
|
|
minetest.register_on_shutdown(function()
|
|
if (script_running) then
|
|
minetest.log("action", "Stopping scripts")
|
|
kill(script_window_id)
|
|
script_running = false
|
|
end
|
|
minetest.log("action", "RJM socket clients disconnected")
|
|
for i = 1, #socket_client_list do
|
|
socket_client_list[i].client:close()
|
|
end
|
|
socket_client_list = {}
|
|
player_table = {}
|
|
max_player_id = 0
|
|
default_player_id = -1
|
|
end)
|
|
|
|
minetest.register_on_joinplayer(function(player)
|
|
minetest.log("action", "Hello, player "..player:get_player_name())
|
|
max_player_id = max_player_id + 1
|
|
player_table[max_player_id] = player
|
|
if default_player_id < 0 then default_player_id = max_player_id end
|
|
end)
|
|
|
|
minetest.register_on_leaveplayer(function(player)
|
|
minetest.log("action", "Goodbye, player "..player:get_player_name())
|
|
local id = get_player_id(player)
|
|
if id then player_table[id] = nil end
|
|
if id == default_player_id then
|
|
default_player_id = max_player_id
|
|
for i,p in pairs(player_table) do
|
|
if p ~= nil and i < default_player_id then default_player_id = i end
|
|
end
|
|
end
|
|
end)
|
|
|
|
minetest.register_on_punchnode(function(pos, oldnode, puncher, pointed_thing)
|
|
-- TODO: find a way to get right clicks
|
|
-- TODO: find a way to get clicked side
|
|
if (puncher:is_player()) then
|
|
local item = puncher:get_wielded_item()
|
|
if not restrict_to_sword or (item and item:get_name():find("%:sword")) then
|
|
table.insert(block_hits, pos.x..","..pos.y..","..(-pos.z)..",7,"..get_entity_id(puncher))
|
|
end
|
|
end
|
|
end)
|
|
|
|
minetest.register_chatcommand("top",
|
|
{params="" ,
|
|
description="Move player on top of everything.",
|
|
func = function(name, args)
|
|
local player = minetest.get_player_by_name(name)
|
|
local pos = player:getpos()
|
|
pos.y = get_height(pos.x, pos.z)+1.5
|
|
player:setpos(pos)
|
|
end})
|
|
|
|
if minetest.emerge_area then
|
|
minetest.register_chatcommand("emerge",
|
|
{params="[<size>]" ,
|
|
description="Emerge cubical area at and above player (default size=200).",
|
|
func = function(name, args)
|
|
local player = minetest.get_player_by_name(name)
|
|
local pos = player:getpos()
|
|
local size
|
|
if args ~= "" then
|
|
size = tonumber(args)
|
|
else
|
|
size = 200
|
|
end
|
|
local p1 = {x = math.floor(pos.x - size/2), y = pos.y, z = math.floor(pos.z - size/2) }
|
|
local p2 = {x = math.ceil(pos.x + size/2), y = pos.y + size, z = math.ceil(pos.z + size/2) }
|
|
minetest.emerge_area(p1,p2)
|
|
end})
|
|
end
|
|
|
|
minetest.register_chatcommand("py",
|
|
{params="[<script> [<args>]]" ,
|
|
description="Run python script in raspberryjammod/mcpipy directory, killing any previous script",
|
|
func = function(name, args) python(name, args, true) end })
|
|
minetest.register_chatcommand("python",
|
|
{params="[<script> [<args>]]" ,
|
|
description="Run python script in raspberryjammod/mcpipy directory, killing any previous script",
|
|
func = function(name, args) python(name, args, true) end })
|
|
minetest.register_chatcommand("apy",
|
|
{params="[<script> [<args>]]" ,
|
|
description="Run python script in raspberryjammod/mcpipy directory, without killing any previous script",
|
|
func = function(name, args) python(name, args, false) end })
|
|
minetest.register_chatcommand("addpython",
|
|
{params="[<script> [<args>]]" ,
|
|
description="Run python script in raspberryjammod/mcpipy directory, without killing any previous script",
|
|
func = function(name, args) python(name, args, false) end })
|
|
|
|
function python(name, args, kill_script)
|
|
if (kill_script and script_running) then
|
|
kill(script_window_id)
|
|
minetest.chat_send_all("Killed running scripts")
|
|
script_running = false
|
|
end
|
|
|
|
if args == "" then return end
|
|
|
|
local script, argtext = args:match("^([^ ]+) *(.*)")
|
|
if argtext:sub(#argtext) == " " then argtext = argtext:sub(1,#argtext - 1) end
|
|
|
|
if not script then return true end
|
|
|
|
if script:find("%.%.") then
|
|
minetest.chat_send_all("Sandbox violation in script name")
|
|
return true
|
|
end
|
|
|
|
script_running = true
|
|
local mcpipy_path = mypath .. path_separator .. "mcpipy" .. path_separator
|
|
background_launch(script_window_id, mcpipy_path, '"' .. python_interpreter .. '" "' .. mcpipy_path .. script .. '.py" ' .. argtext)
|
|
return true
|
|
end
|
|
|
|
local function sanitize_pipe(s)
|
|
return s:gsub("%&", "&"):gsub("%|", "|")
|
|
end
|
|
|
|
minetest.register_on_chat_message(function(name, message)
|
|
local id = get_player_id_by_name(name)
|
|
table.insert(chat_record, id .. "," .. sanitize_pipe(message))
|
|
return false
|
|
end)
|
|
|
|
function get_player_id(player)
|
|
local name = player:get_player_name()
|
|
for id,p in pairs(player_table) do
|
|
if p:get_player_name() == name then
|
|
return id
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function get_player_id_by_name(name)
|
|
for id,p in pairs(player_table) do
|
|
if p:get_player_name() == name then
|
|
return id
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function get_entity_id(entity)
|
|
if not entity:is_player() then
|
|
return 0x7FFFFFFF
|
|
else
|
|
return get_player_id(entity)
|
|
end
|
|
end
|
|
|
|
function handle_entity(cmd, id, args)
|
|
local entity
|
|
if id == nil then
|
|
entity = player_table[default_player_id]
|
|
else
|
|
entity = player_table[id]
|
|
end
|
|
if entity == nil then
|
|
return "fail"
|
|
end
|
|
if cmd == "getPos" then
|
|
local pos = entity:getpos()
|
|
return (pos.x+0.5)..","..(pos.y+0.5)..","..(0.5-pos.z)
|
|
elseif cmd == "getTile" then
|
|
local pos = entity:getpos()
|
|
return math.floor(pos.x+0.5)..","..math.floor(pos.y+0.5)..","..math.floor(0.5-pos.z)
|
|
elseif cmd == "setPos" then
|
|
entity:setpos({x=tonumber(args[1])-0.5, y=tonumber(args[2])-0.5, z=0.5-tonumber(args[3])})
|
|
elseif cmd == "setTile" then
|
|
entity:setpos({x=tonumber(args[1]), y=tonumber(args[2])-0.5, z=-tonumber(args[3])})
|
|
elseif cmd == "getPitch" then
|
|
return tonumber(entity:get_look_pitch() * -180 / math.pi)
|
|
elseif cmd == "getRotation" then
|
|
return tonumber((270 - entity:get_look_yaw() * 180 / math.pi) % 360)
|
|
elseif cmd == "getDirection" then
|
|
local dir = entity:get_look_dir()
|
|
return (dir.x)..","..(dir.y)..","..(-dir.z)
|
|
elseif cmd == "setPitch" then
|
|
-- TODO: For mysterious reasons, set_look_pitch() and get_look_pitch()
|
|
-- values are opposite sign, so we don't negate here. Ideally, the mod
|
|
-- would detect this to make sure that if it's fixed in the next version
|
|
-- this wouldn't be an issue.
|
|
entity:set_look_pitch(tonumber(args[1]) * math.pi / 180)
|
|
elseif cmd == "setRotation" then
|
|
-- TODO: For mysterious reasons, set_look_yaw() and get_look_yaw()
|
|
-- values differ by pi/2. Ideally, the mod
|
|
-- would detect this to make sure that if it's fixed in the next version
|
|
-- this wouldn't be an issue.
|
|
entity:set_look_yaw((180-tonumber(args[1])) * math.pi / 180)
|
|
elseif cmd == "setDirection" then
|
|
-- TODO: Fix set_look_yaw() and get_look_yaw() compensation.
|
|
local x = tonumber(args[1])
|
|
local y = tonumber(args[2])
|
|
local z = tonumber(args[3])
|
|
local xz = math.sqrt(x*x+z*z)
|
|
if xz >= 1e-9 then
|
|
entity:set_look_yaw(- math.atan2(-x,z))
|
|
end
|
|
if x*x + y*y + z*z >= 1e-18 then
|
|
entity:set_look_pitch(math.atan2(-y, xz))
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function parse_node(args, start)
|
|
local id, meta
|
|
if #args < start then
|
|
return {name="air"}
|
|
elseif #args == start then
|
|
return block.id_meta_to_node(tonumber(args[start]),0)
|
|
else
|
|
return block.id_meta_to_node(tonumber(args[start]),tonumber(args[start+1]))
|
|
end
|
|
return node
|
|
end
|
|
|
|
function get_height(x, z)
|
|
-- TODO: Handle larger heights than 1024
|
|
-- TODO: Wait for emergence?
|
|
for ycoord = 1024,-1024,-1 do
|
|
local node = minetest.get_node_or_nil({x=x,y=ycoord,z=z})
|
|
if node and node.name ~= "air" then
|
|
return ycoord
|
|
end
|
|
end
|
|
return -1025
|
|
end
|
|
|
|
local block_buffer = {}
|
|
local block_buffer_p1 = {}
|
|
local block_buffer_p2 = {}
|
|
|
|
function flush_block_buffer()
|
|
if #block_buffer >= 1 then
|
|
local vm = minetest.get_voxel_manip()
|
|
local emin,emax = vm:read_from_map(block_buffer_p1,block_buffer_p2)
|
|
local area = VoxelArea:new{MinEdge=emin, MaxEdge=emax}
|
|
local data = vm:get_data()
|
|
local param2 = vm:get_param2_data()
|
|
for i=1,#block_buffer do
|
|
local v = block_buffer[i]
|
|
local index = area:indexp(v.pos)
|
|
data[index] = minetest.get_content_id(v.node.name)
|
|
param2[index] = v.node.param2
|
|
end
|
|
vm:set_data(data)
|
|
vm:set_param2_data(param2)
|
|
vm:update_liquids()
|
|
vm:write_to_map()
|
|
vm:update_map()
|
|
else
|
|
for i=1,#block_buffer do
|
|
minetest.set_node(block_buffer[i].pos, block_buffer[i].node)
|
|
end
|
|
end
|
|
block_buffer = {}
|
|
block_buffer_p1 = {}
|
|
block_buffer_p2 = {}
|
|
end
|
|
|
|
local function buffered_set_node(pos, node)
|
|
-- ensure buffer cuboid is no more than about 10000 in size
|
|
|
|
local new_block_buffer_p1 = {x=block_buffer_p1.x,y=block_buffer_p1.y,z=block_buffer_p1.z}
|
|
local new_block_buffer_p2 = {x=block_buffer_p2.x,y=block_buffer_p2.y,z=block_buffer_p2.z}
|
|
if not block_buffer_p1.x or pos.x < block_buffer_p1.x then new_block_buffer_p1.x = pos.x end
|
|
if not block_buffer_p1.y or pos.y < block_buffer_p1.y then new_block_buffer_p1.y = pos.y end
|
|
if not block_buffer_p1.z or pos.z < block_buffer_p1.z then new_block_buffer_p1.z = pos.z end
|
|
if not block_buffer_p2.x or pos.x > block_buffer_p2.x then new_block_buffer_p2.x = pos.x end
|
|
if not block_buffer_p2.y or pos.y > block_buffer_p2.y then new_block_buffer_p2.y = pos.y end
|
|
if not block_buffer_p2.z or pos.z > block_buffer_p2.z then new_block_buffer_p2.z = pos.z end
|
|
|
|
if #block_buffer > 0 and (new_block_buffer_p2.x - new_block_buffer_p2.x + 1) *
|
|
(new_block_buffer_p2.y - new_block_buffer_p2.y + 1) *
|
|
(new_block_buffer_p2.z - new_block_buffer_p2.z + 1) > 10000 then
|
|
|
|
flush_block_buffer()
|
|
block_buffer_p1.x = pos.x
|
|
block_buffer_p1.y = pos.y
|
|
block_buffer_p1.z = pos.z
|
|
block_buffer_p2.x = pos.x
|
|
block_buffer_p2.y = pos.y
|
|
block_buffer_p2.z = pos.z
|
|
else
|
|
block_buffer_p1 = new_block_buffer_p1
|
|
block_buffer_p2 = new_block_buffer_p2
|
|
end
|
|
|
|
table.insert(block_buffer, {pos=pos, node=node})
|
|
end
|
|
|
|
local function set_nodes_with_voxelmanip(x1,y1,z1,x2,y2,z2,node)
|
|
local p1 = {x=x1,y=y1,z=z1}
|
|
local p2 = {x=x2,y=y2,z=z2}
|
|
local vm = minetest.get_voxel_manip()
|
|
local emin,emax = vm:read_from_map(p1,p2)
|
|
local area = VoxelArea:new{MinEdge=emin, MaxEdge=emax}
|
|
local data = vm:get_data()
|
|
local param2 = vm:get_param2_data()
|
|
local content = minetest.get_content_id(node.name)
|
|
for index in area:iterp(p1,p2) do
|
|
data[index] = content
|
|
param2[index] = node.param2
|
|
end
|
|
vm:set_data(data)
|
|
vm:set_param2_data(param2)
|
|
vm:update_liquids()
|
|
vm:write_to_map()
|
|
vm:update_map()
|
|
end
|
|
|
|
local function setNodes(args, node)
|
|
local x1 = math.min(tonumber(args[1]),tonumber(args[4]))
|
|
local x2 = math.max(tonumber(args[1]),tonumber(args[4]))
|
|
local y1 = math.min(tonumber(args[2]),tonumber(args[5]))
|
|
local y2 = math.max(tonumber(args[2]),tonumber(args[5]))
|
|
local z1 = math.min(-tonumber(args[3]),-tonumber(args[6]))
|
|
local z2 = math.max(-tonumber(args[3]),-tonumber(args[6]))
|
|
|
|
local volume = (x2+1-x1)*(y2+1-y1)*(z2+1-z1)
|
|
|
|
if 50 <= volume and volume <= 20000000 then
|
|
set_nodes_with_voxelmanip(x1,y1,z1,x2,y2,z2,node)
|
|
else
|
|
for ycoord = y1,y2 do
|
|
for xcoord = x1,x2 do
|
|
for zcoord = z1,z2 do
|
|
minetest.set_node({x=xcoord,y=ycoord,z=zcoord},node)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function handle_world(cmd, args)
|
|
if cmd == "setBlock" then
|
|
local node = parse_node(args, 4)
|
|
buffered_set_node({x=tonumber(args[1]),y=tonumber(args[2]),z=-tonumber(args[3])},node)
|
|
elseif cmd == "setNode" then
|
|
local node = {name=args[4]}
|
|
if args[5] then node.param2 = tonumber(args[5]) end
|
|
buffered_set_node({x=tonumber(args[1]),y=tonumber(args[2]),z=-tonumber(args[3])},node)
|
|
elseif cmd == "setBlocks" then
|
|
local node = parse_node(args, 7)
|
|
setNodes(args, node)
|
|
elseif cmd == "setNodes" then
|
|
local node = {name=args[7]}
|
|
if args[8] then node.param2 = tonumber(args[8]) end
|
|
setNodes(args, node)
|
|
elseif cmd == "getNode" then
|
|
local node = minetest.get_node({x=tonumber(args[1]),y=tonumber(args[2]),z=-tonumber(args[3])})
|
|
return node.name .. "," .. node.param2
|
|
elseif cmd == "getAllNodes" then
|
|
local nodes = {}
|
|
for name,_ in pairs(minetest.registered_nodes) do
|
|
table.insert(nodes,sanitize_pipe(name))
|
|
end
|
|
return table.concat(nodes,'|')
|
|
elseif cmd == "getBlockWithData" or cmd == "getBlock" then
|
|
local node = minetest.get_node({x=tonumber(args[1]),y=tonumber(args[2]),z=-tonumber(args[3])})
|
|
local id, meta = block.node_to_id_meta(node)
|
|
if cmd == "getBlock" then
|
|
return tostring(id)
|
|
else
|
|
return id..","..meta
|
|
end
|
|
elseif cmd == "getBlocksWithData" or cmd == "getBlocks" or cmd == "getNodes" then
|
|
local x1 = math.min(tonumber(args[1]),tonumber(args[4]))
|
|
local x2 = math.max(tonumber(args[1]),tonumber(args[4]))
|
|
local y1 = math.min(tonumber(args[2]),tonumber(args[5]))
|
|
local y2 = math.max(tonumber(args[2]),tonumber(args[5]))
|
|
local z1 = math.min(tonumber(args[3]),tonumber(args[6]))
|
|
local z2 = math.max(tonumber(args[3]),tonumber(args[6]))
|
|
|
|
local data = {}
|
|
|
|
if cmd == "getBlocksWithData" then
|
|
for y = y1,y2 do
|
|
for x = x1,x2 do
|
|
for z = z1,z2 do
|
|
local node = minetest.get_node({x=x,y=y,z=-z})
|
|
local id, meta = block.node_to_id_meta(node)
|
|
table.insert(data, id .. "," .. meta)
|
|
end
|
|
end
|
|
end
|
|
elseif cmd == "getNodes" then
|
|
for y = y1,y2 do
|
|
for x = x1,x2 do
|
|
for z = z1,z2 do
|
|
local node = minetest.get_node({x=x,y=y,z=-z})
|
|
table.insert(data, sanitize_pipe(node.name) .. "," .. node.param2)
|
|
end
|
|
end
|
|
end
|
|
else
|
|
for y = y1,y2 do
|
|
for x = x1,x2 do
|
|
for z = z1,z2 do
|
|
local node = minetest.get_node({x=x,y=y,z=-z})
|
|
local id, _ = block.node_to_id_meta(node)
|
|
table.insert(data, tostring(id))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if cmd == "getBlocks" then
|
|
return table.concat(data, ",")
|
|
else
|
|
return table.concat(data, "|")
|
|
end
|
|
elseif cmd == "getHeight" then
|
|
return tonumber(get_height(tonumber(args[1]),-tonumber(args[2])))
|
|
elseif cmd == "getPlayerId" then
|
|
if #args > 0 then
|
|
local id = get_player_id_by_name(args[1])
|
|
if id == nil then
|
|
return "fail"
|
|
else
|
|
return ""..id
|
|
end
|
|
else
|
|
return ""..default_player_id
|
|
end
|
|
elseif cmd == "getPlayerIds" then
|
|
local ids = {}
|
|
for id,_ in pairs(player_table) do
|
|
table.insert(ids, id)
|
|
end
|
|
table.sort(ids)
|
|
return table.concat(ids, "|")
|
|
elseif cmd == "setting" then
|
|
if args[1] == "world_immutable" then
|
|
world_immutable = (0 ~= tonumber(args[2]))
|
|
end
|
|
end
|
|
return nil
|
|
|
|
end
|
|
|
|
function handle_events(cmd, args)
|
|
if (cmd == "setting") then
|
|
if (args[1] == "restrict_to_sword") then
|
|
restrict_to_sword = (0 ~= tonumber(args[2]))
|
|
end
|
|
elseif (cmd == "block.hits") then
|
|
local h = block_hits
|
|
block_hits = {}
|
|
return table.concat(h, "|")
|
|
elseif (cmd == "chat.posts") then
|
|
local c = chat_record
|
|
chat_record = {}
|
|
return table.concat(c, "|")
|
|
elseif (cmd == "clear") then
|
|
block_hits = {}
|
|
chat_record = {}
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function background_launch(window_identifier, working_dir, cmd)
|
|
-- TODO: non-Windows
|
|
if not is_windows then return false end
|
|
local cmdline = 'start "' .. window_identifier .. '" /D "' .. working_dir .. '" /MIN ' .. cmd
|
|
minetest.log("action", "launching ["..cmdline.."]")
|
|
ie.os.execute(cmdline)
|
|
end
|
|
|
|
function kill(window_identifier)
|
|
-- TODO: non-Windows
|
|
minetest.log('taskkill /F /FI "WINDOWTITLE eq ' .. window_identifier .. '"')
|
|
ie.os.execute('taskkill /F /FI "WINDOWTITLE eq ' .. window_identifier .. '"')
|
|
end
|
|
|
|
function safe_handle_command(source,line)
|
|
local status, err = pcall(function()
|
|
local response = handle_command(line)
|
|
if response then source.client:send(response.."\n") end
|
|
end)
|
|
return err
|
|
end
|
|
|
|
function handle_command(line)
|
|
local cmd, argtext = line:match("^([^(]+)%((.*)%)")
|
|
if not cmd then return end
|
|
|
|
if #block_buffer > 0 and cmd ~= "world.setBlock" and cmd ~= "world.setNode" then
|
|
flush_block_buffer()
|
|
end
|
|
|
|
local args = {}
|
|
for argument in argtext:gmatch("([^,]+)") do
|
|
table.insert(args, argument)
|
|
end
|
|
if cmd:sub(1,6) == "world." then
|
|
return handle_world(cmd:sub(7),args)
|
|
elseif cmd:sub(1,7) == "player." then
|
|
if args[1] == "None" then
|
|
-- Compatibility with mcpi library included with RaspberryJuice --
|
|
table.remove(args,1)
|
|
end
|
|
return handle_entity(cmd:sub(8),nil,args)
|
|
elseif cmd:sub(1,7) == "entity." then
|
|
local player = tonumber(args[1])
|
|
table.remove(args,1)
|
|
return handle_entity(cmd:sub(8),player,args)
|
|
elseif cmd:sub(1,7) == "events." then
|
|
return handle_events(cmd:sub(8),args)
|
|
elseif cmd == "chat.post" then
|
|
minetest.chat_send_all(argtext)
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function complete_data(source,data)
|
|
local needed = tonumber(source.read_mode)
|
|
local have = data:len()
|
|
|
|
if source.ws.saved_data then
|
|
source.ws.saved_data = source.ws.saved_data .. data
|
|
else
|
|
source.ws.saved_data = data
|
|
end
|
|
|
|
if have >= needed then
|
|
local out = source.ws.saved_data
|
|
source.ws.saved_data = nil
|
|
return out
|
|
else
|
|
source.read_mode = ""..(needed-have)
|
|
return nil
|
|
end
|
|
end
|
|
|
|
function send_message(remote,opcode,data)
|
|
local out = string.char(0x80 + opcode)
|
|
local len = data:len()
|
|
if len > 65535 then
|
|
out = out .. string.char(127)
|
|
for i = 56,0,-8 do
|
|
out = out .. string.char(bit.band(bit.rshift(len, i),0xFF))
|
|
end
|
|
elseif len > 125 then
|
|
out = out .. string.char(126)
|
|
out = out .. string.char(bit.rshift(len, 8)) .. string.char(bit.band(len,0xFF))
|
|
else
|
|
out = out .. string.char(len)
|
|
end
|
|
out = out .. data
|
|
remote.client:send(out)
|
|
return nil
|
|
end
|
|
|
|
function handle_websocket_complete_payload(source,data)
|
|
if source.ws.opcode == 0x09 then
|
|
-- ping -> pong
|
|
send_message(source,0x0A,data)
|
|
elseif source.ws.opcode == 0x02 or source.ws.opcode == 0x01 then
|
|
local status, err = pcall(function()
|
|
local response = handle_command(data)
|
|
if response then send_message(source,0x01,response.."\n") end
|
|
end)
|
|
return err
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
function handle_websocket_payload(source,data)
|
|
data = complete_data(source,data)
|
|
if data == nil then return nil end
|
|
local mask = data:sub(1,4)
|
|
local decoded = ""
|
|
for i = 1,source.ws.payload_len do
|
|
decoded = decoded .. string.char(bit.bxor( mask:byte( ((i-1)%4) + 1 ), data:byte(4+i) ))
|
|
end
|
|
|
|
if source.ws.payload then
|
|
decoded = source.ws.payload .. decoded
|
|
end
|
|
|
|
source.read_mode = "2"
|
|
source.handler = handle_websocket_frame
|
|
|
|
if source.ws.frame_end then
|
|
source.ws.payload = nil
|
|
return handle_websocket_complete_payload(source,decoded)
|
|
else
|
|
source.ws.payload = decoded
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
function handle_websocket_payload_len(source,data)
|
|
data = complete_data(source,data)
|
|
if data == nil then return nil end
|
|
if source.read_mode == "1" then
|
|
source.ws.payload_len = data:byte(1)
|
|
elseif source.read_mode == "2" then
|
|
source.ws.payload_len = bit.lshift(data:byte(1),8)+data:byte(2)
|
|
elseif source.read_mode == "8" then
|
|
source.ws.payload_len =
|
|
bit.lshift(data:byte(1),56)+
|
|
bit.lshift(data:byte(2),48)+
|
|
bit.lshift(data:byte(3),40)+
|
|
bit.lshift(data:byte(4),32)+
|
|
bit.lshift(data:byte(5),24)+
|
|
bit.lshift(data:byte(6),16)+
|
|
bit.lshift(data:byte(7),8)+
|
|
data:byte(8)
|
|
end
|
|
source.read_mode = ""..(4+source.ws.payload_len)
|
|
source.handler = handle_websocket_payload
|
|
return nil
|
|
end
|
|
|
|
function handle_websocket_frame(source,data)
|
|
data = complete_data(source,data)
|
|
if data == nil then return nil end
|
|
local x = data:byte(1)
|
|
--print(string.format("frame %02x %02x",data:byte(1),data:byte(2)))
|
|
source.ws.frame_end = (0 ~= bit.band(0x80, x))
|
|
local opcode = bit.band(0xF, x)
|
|
if opcode == 0x08 then
|
|
--print("closing")
|
|
source.client:close()
|
|
return "closed"
|
|
end
|
|
if opcode ~= 0 then
|
|
source.ws.opcode = opcode
|
|
end
|
|
local y = data:byte(2)
|
|
source.ws.mask = (0 ~= bit.band(0x80, y))
|
|
if not source.ws.mask and opcode <= 0x02 then
|
|
return "unmasked data ("..opcode..")"
|
|
end
|
|
local payload_len = bit.band(y, 0x7F)
|
|
if payload_len == 126 then
|
|
source.handler = handle_websocket_payload_len
|
|
source.read_mode = "2"
|
|
elseif payload_len == 127 then
|
|
source.handler = handle_websocket_payload_len
|
|
source.read_mode = "8"
|
|
else
|
|
source.read_mode = "1"
|
|
return handle_websocket_payload_len(source,string.char(payload_len))
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function handle_websocket_header(source,line)
|
|
if line:find("^Upgrade%: +websocket") then
|
|
source.ws.isWebsocket = true
|
|
return nil
|
|
end
|
|
|
|
if not source.ws.key then
|
|
source.ws.key = line:match("^Sec%-WebSocket%-Key%: +([=+/0-9A-Za-z]+)")
|
|
end
|
|
|
|
if line == "" then
|
|
if source.ws.isWebsocket and source.ws.key then
|
|
local new_key = base64.encode(tools.sha1(source.ws.key .. '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'))
|
|
source.ws = {}
|
|
local response = "HTTP/1.1 101 Switching Protocols\r\n"..
|
|
"Upgrade: websocket\r\n"..
|
|
"Connection: Upgrade\r\n"..
|
|
"Sec-WebSocket-Accept: "..new_key.."\r\n\r\n";
|
|
source.client:send(response)
|
|
source.read_mode = "2"
|
|
source.handler = handle_websocket_frame
|
|
minetest.log("action","Websocket handshake")
|
|
return nil
|
|
else
|
|
return "invalid websocket request"
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
|
|
|
|
-- os.execute("python scratch_pycraft.py")
|