diff --git a/src/main/resources/data/computercraft/lua/rom/apis/rednet.lua b/src/main/resources/data/computercraft/lua/rom/apis/rednet.lua index 83f4bb6ad..3bc55a936 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/rednet.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/rednet.lua @@ -29,9 +29,9 @@ CHANNEL_REPEAT = 65533 -- greater or equal to this limit wrap around to 0. MAX_ID_CHANNELS = 65500 -local tReceivedMessages = {} -local tHostnames = {} -local nClearTimer +local received_messages = {} +local hostnames = {} +local prune_received_timer local function id_as_channel(id) return (id or os.getComputerID()) % MAX_ID_CHANNELS @@ -115,10 +115,10 @@ be @{rednet.open|opened} before sending is possible. Assuming the target was in range and also had a correctly opened modem, it may then use @{rednet.receive} to collect the message. -@tparam number nRecipient The ID of the receiving computer. +@tparam number recipient The ID of the receiving computer. @param message The message to send. This should not contain coroutines or functions, as they will be converted to @{nil}. -@tparam[opt] string sProtocol The "protocol" to send this message under. When +@tparam[opt] string protocol The "protocol" to send this message under. When using @{rednet.receive} one can filter to only receive messages sent under a particular protocol. @treturn boolean If this message was successfully sent (i.e. if rednet is @@ -131,41 +131,41 @@ actually _received_. rednet.send(2, "Hello from rednet!") ]] -function send(nRecipient, message, sProtocol) - expect(1, nRecipient, "number") - expect(3, sProtocol, "string", "nil") +function send(recipient, message, protocol) + expect(1, recipient, "number") + expect(3, protocol, "string", "nil") -- Generate a (probably) unique message ID -- We could do other things to guarantee uniqueness, but we really don't need to -- Store it to ensure we don't get our own messages back - local nMessageID = math.random(1, 2147483647) - tReceivedMessages[nMessageID] = os.clock() + 9.5 - if not nClearTimer then nClearTimer = os.startTimer(10) end + local message_id = math.random(1, 2147483647) + received_messages[message_id] = os.clock() + 9.5 + if not prune_received_timer then prune_received_timer = os.startTimer(10) end -- Create the message - local nReplyChannel = id_as_channel() - local tMessage = { - nMessageID = nMessageID, - nRecipient = nRecipient, + local reply_channel = id_as_channel() + local message_wrapper = { + nMessageID = message_id, + nRecipient = recipient, nSender = os.getComputerID(), message = message, - sProtocol = sProtocol, + sProtocol = protocol, } local sent = false - if nRecipient == os.getComputerID() then + if recipient == os.getComputerID() then -- Loopback to ourselves - os.queueEvent("rednet_message", os.getComputerID(), message, sProtocol) + os.queueEvent("rednet_message", os.getComputerID(), message_wrapper, protocol) sent = true else -- Send on all open modems, to the target and to repeaters - if nRecipient ~= CHANNEL_BROADCAST then - nRecipient = id_as_channel(nRecipient) + if recipient ~= CHANNEL_BROADCAST then + recipient = id_as_channel(recipient) end - for _, sModem in ipairs(peripheral.getNames()) do - if isOpen(sModem) then - peripheral.call(sModem, "transmit", nRecipient, nReplyChannel, tMessage) - peripheral.call(sModem, "transmit", CHANNEL_REPEAT, nReplyChannel, tMessage) + for _, modem in ipairs(peripheral.getNames()) do + if isOpen(modem) then + peripheral.call(modem, "transmit", recipient, reply_channel, message_wrapper) + peripheral.call(modem, "transmit", CHANNEL_REPEAT, reply_channel, message_wrapper) sent = true end end @@ -179,23 +179,23 @@ end -- -- @param message The message to send. This should not contain coroutines or -- functions, as they will be converted to @{nil}. --- @tparam[opt] string sProtocol The "protocol" to send this message under. When +-- @tparam[opt] string protocol The "protocol" to send this message under. When -- using @{rednet.receive} one can filter to only receive messages sent under a -- particular protocol. -- @see rednet.receive -- @changed 1.6 Added protocol parameter. -function broadcast(message, sProtocol) - expect(2, sProtocol, "string", "nil") - send(CHANNEL_BROADCAST, message, sProtocol) +function broadcast(message, protocol) + expect(2, protocol, "string", "nil") + send(CHANNEL_BROADCAST, message, protocol) end --[[- Wait for a rednet message to be received, or until `nTimeout` seconds have elapsed. -@tparam[opt] string sProtocolFilter The protocol the received message must be +@tparam[opt] string protocol_filter The protocol the received message must be sent with. If specified, any messages not sent under this protocol will be discarded. -@tparam[opt] number nTimeout The number of seconds to wait if no message is +@tparam[opt] number timeout The number of seconds to wait if no message is received. @treturn[1] number The computer which sent this message @return[1] The received message @@ -227,34 +227,34 @@ received. print(message) ]] -function receive(sProtocolFilter, nTimeout) +function receive(protocol_filter, timeout) -- The parameters used to be ( nTimeout ), detect this case for backwards compatibility - if type(sProtocolFilter) == "number" and nTimeout == nil then - sProtocolFilter, nTimeout = nil, sProtocolFilter + if type(protocol_filter) == "number" and timeout == nil then + protocol_filter, timeout = nil, protocol_filter end - expect(1, sProtocolFilter, "string", "nil") - expect(2, nTimeout, "number", "nil") + expect(1, protocol_filter, "string", "nil") + expect(2, timeout, "number", "nil") -- Start the timer local timer = nil - local sFilter = nil - if nTimeout then - timer = os.startTimer(nTimeout) - sFilter = nil + local event_filter = nil + if timeout then + timer = os.startTimer(timeout) + event_filter = nil else - sFilter = "rednet_message" + event_filter = "rednet_message" end -- Wait for events while true do - local sEvent, p1, p2, p3 = os.pullEvent(sFilter) - if sEvent == "rednet_message" then + local event, p1, p2, p3 = os.pullEvent(event_filter) + if event == "rednet_message" then -- Return the first matching rednet_message - local nSenderID, message, sProtocol = p1, p2, p3 - if sProtocolFilter == nil or sProtocol == sProtocolFilter then - return nSenderID, message, sProtocol + local sender_id, message, protocol = p1, p2, p3 + if protocol_filter == nil or protocol == protocol_filter then + return sender_id, message, protocol end - elseif sEvent == "timer" then + elseif event == "timer" then -- Return nil if we timeout if p1 == timer then return nil @@ -276,34 +276,34 @@ end -- "registering" themselves before doing so (eg while offline or part of a -- different network). -- --- @tparam string sProtocol The protocol this computer provides. --- @tparam string sHostname The name this protocol exposes for the given protocol. +-- @tparam string protocol The protocol this computer provides. +-- @tparam string hostname The name this protocol exposes for the given protocol. -- @throws If trying to register a hostname which is reserved, or currently in use. -- @see rednet.unhost -- @see rednet.lookup -- @since 1.6 -function host(sProtocol, sHostname) - expect(1, sProtocol, "string") - expect(2, sHostname, "string") - if sHostname == "localhost" then +function host(protocol, hostname) + expect(1, protocol, "string") + expect(2, hostname, "string") + if hostname == "localhost" then error("Reserved hostname", 2) end - if tHostnames[sProtocol] ~= sHostname then - if lookup(sProtocol, sHostname) ~= nil then + if hostnames[protocol] ~= hostname then + if lookup(protocol, hostname) ~= nil then error("Hostname in use", 2) end - tHostnames[sProtocol] = sHostname + hostnames[protocol] = hostname end end --- Stop @{rednet.host|hosting} a specific protocol, meaning it will no longer -- respond to @{rednet.lookup} requests. -- --- @tparam string sProtocol The protocol to unregister your self from. +-- @tparam string protocol The protocol to unregister your self from. -- @since 1.6 -function unhost(sProtocol) - expect(1, sProtocol, "string") - tHostnames[sProtocol] = nil +function unhost(protocol) + expect(1, protocol, "string") + hostnames[protocol] = nil end --- Search the local rednet network for systems @{rednet.host|hosting} the @@ -313,36 +313,36 @@ end -- If a hostname is specified, only one ID will be returned (assuming an exact -- match is found). -- --- @tparam string sProtocol The protocol to search for. --- @tparam[opt] string sHostname The hostname to search for. +-- @tparam string protocol The protocol to search for. +-- @tparam[opt] string hostname The hostname to search for. -- -- @treturn[1] { number }|nil A list of computer IDs hosting the given -- protocol, or @{nil} if none exist. -- @treturn[2] number|nil The computer ID with the provided hostname and protocol, -- or @{nil} if none exists. -- @since 1.6 -function lookup(sProtocol, sHostname) - expect(1, sProtocol, "string") - expect(2, sHostname, "string", "nil") +function lookup(protocol, hostname) + expect(1, protocol, "string") + expect(2, hostname, "string", "nil") -- Build list of host IDs - local tResults = nil - if sHostname == nil then - tResults = {} + local results = nil + if hostname == nil then + results = {} end -- Check localhost first - if tHostnames[sProtocol] then - if sHostname == nil then - table.insert(tResults, os.getComputerID()) - elseif sHostname == "localhost" or sHostname == tHostnames[sProtocol] then + if hostnames[protocol] then + if hostname == nil then + table.insert(results, os.getComputerID()) + elseif hostname == "localhost" or hostname == hostnames[protocol] then return os.getComputerID() end end if not isOpen() then - if tResults then - return table.unpack(tResults) + if results then + return table.unpack(results) end return nil end @@ -350,8 +350,8 @@ function lookup(sProtocol, sHostname) -- Broadcast a lookup packet broadcast({ sType = "lookup", - sProtocol = sProtocol, - sHostname = sHostname, + sProtocol = protocol, + sHostname = hostname, }, "dns") -- Start a timer @@ -362,30 +362,28 @@ function lookup(sProtocol, sHostname) local event, p1, p2, p3 = os.pullEvent() if event == "rednet_message" then -- Got a rednet message, check if it's the response to our request - local nSenderID, tMessage, sMessageProtocol = p1, p2, p3 - if sMessageProtocol == "dns" and type(tMessage) == "table" and tMessage.sType == "lookup response" then - if tMessage.sProtocol == sProtocol then - if sHostname == nil then - table.insert(tResults, nSenderID) - elseif tMessage.sHostname == sHostname then - return nSenderID + local sender_id, message, message_protocol = p1, p2, p3 + if message_protocol == "dns" and type(message) == "table" and message.sType == "lookup response" then + if message.sProtocol == protocol then + if hostname == nil then + table.insert(results, sender_id) + elseif message.sHostname == hostname then + return sender_id end end end - else + elseif event == "timer" and p1 == timer then -- Got a timer event, check it's the end of our timeout - if p1 == timer then - break - end + break end end - if tResults then - return table.unpack(tResults) + if results then + return table.unpack(results) end return nil end -local bRunning = false +local started = false --- Listen for modem messages and converts them into rednet messages, which may -- then be @{receive|received}. @@ -393,51 +391,51 @@ local bRunning = false -- This is automatically started in the background on computer startup, and -- should not be called manually. function run() - if bRunning then + if started then error("rednet is already running", 2) end - bRunning = true + started = true - while bRunning do - local sEvent, p1, p2, p3, p4 = os.pullEventRaw() - if sEvent == "modem_message" then + while true do + local event, p1, p2, p3, p4 = os.pullEventRaw() + if event == "modem_message" then -- Got a modem message, process it and add it to the rednet event queue - local sModem, nChannel, nReplyChannel, tMessage = p1, p2, p3, p4 - if nChannel == id_as_channel() or nChannel == CHANNEL_BROADCAST then - if type(tMessage) == "table" and type(tMessage.nMessageID) == "number" - and tMessage.nMessageID == tMessage.nMessageID and not tReceivedMessages[tMessage.nMessageID] - and ((tMessage.nRecipient and tMessage.nRecipient == os.getComputerID()) or nChannel == CHANNEL_BROADCAST) - and isOpen(sModem) + local modem, channel, reply_channel, message = p1, p2, p3, p4 + if channel == id_as_channel() or channel == CHANNEL_BROADCAST then + if type(message) == "table" and type(message.nMessageID) == "number" + and message.nMessageID == message.nMessageID and not received_messages[message.nMessageID] + and ((message.nRecipient and message.nRecipient == os.getComputerID()) or channel == CHANNEL_BROADCAST) + and isOpen(modem) then - tReceivedMessages[tMessage.nMessageID] = os.clock() + 9.5 - if not nClearTimer then nClearTimer = os.startTimer(10) end - os.queueEvent("rednet_message", tMessage.nSender or nReplyChannel, tMessage.message, tMessage.sProtocol) + received_messages[message.nMessageID] = os.clock() + 9.5 + if not prune_received_timer then prune_received_timer = os.startTimer(10) end + os.queueEvent("rednet_message", message.nSender or reply_channel, message.message, message.sProtocol) end end - elseif sEvent == "rednet_message" then + elseif event == "rednet_message" then -- Got a rednet message (queued from above), respond to dns lookup - local nSenderID, tMessage, sProtocol = p1, p2, p3 - if sProtocol == "dns" and type(tMessage) == "table" and tMessage.sType == "lookup" then - local sHostname = tHostnames[tMessage.sProtocol] - if sHostname ~= nil and (tMessage.sHostname == nil or tMessage.sHostname == sHostname) then - rednet.send(nSenderID, { + local sender, message, protocol = p1, p2, p3 + if protocol == "dns" and type(message) == "table" and message.sType == "lookup" then + local hostname = hostnames[message.sProtocol] + if hostname ~= nil and (message.sHostname == nil or message.sHostname == hostname) then + send(sender, { sType = "lookup response", - sHostname = sHostname, - sProtocol = tMessage.sProtocol, + sHostname = hostname, + sProtocol = message.sProtocol, }, "dns") end end - elseif sEvent == "timer" and p1 == nClearTimer then - -- Got a timer event, use it to clear the event queue - nClearTimer = nil - local nNow, bHasMore = os.clock(), nil - for nMessageID, nDeadline in pairs(tReceivedMessages) do - if nDeadline <= nNow then tReceivedMessages[nMessageID] = nil - else bHasMore = true end + elseif event == "timer" and p1 == prune_received_timer then + -- Got a timer event, use it to prune the set of received messages + prune_received_timer = nil + local now, has_more = os.clock(), nil + for message_id, deadline in pairs(received_messages) do + if deadline <= now then received_messages[message_id] = nil + else has_more = true end end - nClearTimer = bHasMore and os.startTimer(10) + prune_received_timer = has_more and os.startTimer(10) end end end diff --git a/src/main/resources/data/computercraft/lua/rom/apis/term.lua b/src/main/resources/data/computercraft/lua/rom/apis/term.lua index 2def0eb50..125d7c408 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/term.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/term.lua @@ -59,7 +59,8 @@ end -- @treturn Redirect The current terminal redirect -- @since 1.6 -- @usage --- Create a new @{window} which draws to the current redirect target +-- Create a new @{window} which draws to the current redirect target. +-- -- window.create(term.current(), 1, 1, 10, 10) term.current = function() return redirectTarget diff --git a/src/main/resources/data/computercraft/lua/rom/programs/rednet/repeat.lua b/src/main/resources/data/computercraft/lua/rom/programs/rednet/repeat.lua index c5ed89984..e1e0b9340 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/rednet/repeat.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/rednet/repeat.lua @@ -14,10 +14,6 @@ else print(#tModems .. " modems found.") end -local function idAsChannel(id) - return (id or os.getComputerID()) % rednet.MAX_ID_CHANNELS -end - local function open(nChannel) for n = 1, #tModems do local sModem = tModems[n] @@ -53,11 +49,16 @@ local ok, error = pcall(function() tReceivedMessages[tMessage.nMessageID] = true tReceivedMessageTimeouts[os.startTimer(30)] = tMessage.nMessageID + local recipient_channel = tMessage.nRecipient + if tMessage.nRecipient ~= rednet.CHANNEL_BROADCAST then + recipient_channel = recipient_channel % rednet.MAX_ID_CHANNELS + end + -- Send on all other open modems, to the target and to other repeaters for n = 1, #tModems do local sOtherModem = tModems[n] peripheral.call(sOtherModem, "transmit", rednet.CHANNEL_REPEAT, nReplyChannel, tMessage) - peripheral.call(sOtherModem, "transmit", idAsChannel(tMessage.nRecipient), nReplyChannel, tMessage) + peripheral.call(sOtherModem, "transmit", recipient_channel, nReplyChannel, tMessage) end -- Log the event diff --git a/src/test/resources/test-rom/spec/apis/rednet_spec.lua b/src/test/resources/test-rom/spec/apis/rednet_spec.lua index de5a85b60..35cb4c1df 100644 --- a/src/test/resources/test-rom/spec/apis/rednet_spec.lua +++ b/src/test/resources/test-rom/spec/apis/rednet_spec.lua @@ -88,6 +88,7 @@ describe("The rednet library", function() local fake_computer = require "support.fake_computer" local debugx = require "support.debug_ext" + local function dawdle() while true do coroutine.yield() end end local function computer_with_rednet(id, fn, options) local computer = fake_computer.make_computer(id, function(env) local fns = { env.rednet.run } @@ -105,6 +106,10 @@ describe("The rednet library", function() end end + if options and options.host then + env.rednet.host("some_protocol", "host_" .. id) + end + return parallel.waitForAny(table.unpack(fns)) end) local modem = fake_computer.add_modem(computer, "back") @@ -203,8 +208,8 @@ describe("The rednet library", function() env.sleep(10) -- Ensure our pending message store is empty. Bit ugly to prod internals, but there's no other way. - expect(debugx.getupvalue(rednet.run, "tReceivedMessages")):same({}) - expect(debugx.getupvalue(rednet.run, "nClearTimer")):eq(nil) + expect(debugx.getupvalue(rednet.run, "received_messages")):same({}) + expect(debugx.getupvalue(rednet.run, "prune_received_timer")):eq(nil) end, { open = true }) local computer_3, modem_3 = computer_with_rednet(3, nil, { open = true, rep = true }) @@ -222,5 +227,22 @@ describe("The rednet library", function() fake_computer.advance_all(computers, 10) fake_computer.run_all(computers, { computer_1, computer_2 }) end) + + it("handles lookups between computers with massive IDs", function() + local id_1, id_3 = 24283947, 93428798 + local computer_1, modem_1 = computer_with_rednet(id_1, function(rednet) + local ids = { rednet.lookup("some_protocol") } + expect(ids):same { id_3 } + end, { open = true }) + local computer_2, modem_2 = computer_with_rednet(2, nil, { open = true, rep = true }) + local computer_3, modem_3 = computer_with_rednet(id_3, dawdle, { open = true, host = true }) + fake_computer.add_modem_edge(modem_1, modem_2) + fake_computer.add_modem_edge(modem_2, modem_3) + + local computers = { computer_1, computer_2, computer_3 } + fake_computer.run_all(computers, false) + fake_computer.advance_all(computers, 3) + fake_computer.run_all(computers, { computer_1 }) + end) end) end)