mirror of
https://github.com/XeonSquared/OC-Copper.git
synced 2024-11-14 22:08:05 +11:00
174 lines
5.3 KiB
Lua
174 lines
5.3 KiB
Lua
-- I, 20kdc, release this into the public domain.
|
|
-- No warranty is provided, implied or otherwise.
|
|
|
|
-- Copper Reliability Layer
|
|
-- Notably, this should be instantiated rather than the normal Copper instance.
|
|
|
|
-- The Copper node constructor (which could be replaced with something else,
|
|
-- should you decide to run the Reliability layer over another transport)
|
|
-- is passed as function (hostname, transmit, onReceive, time),
|
|
-- where hostname is the hostname given,
|
|
-- transmit is the (target/nil (for broadcast), data) function as given,
|
|
-- onReceive is the (nfrom. nto, ndata) function
|
|
-- internal to the Reliability Layer to translate packets for onRReceive
|
|
-- and acknowledge packets that need to be acknowledged,
|
|
-- and time is the function-that-returns-current-time-in-seconds
|
|
-- function as given.
|
|
|
|
-- The resulting Copper-like node should have the following fields:
|
|
-- node.hostname: Editable hostname.
|
|
-- node.refresh(): Do various timed cleanup tasks.
|
|
-- node.input(...): relib.input is set to this for convenience.
|
|
-- Ought to be function (opaqueFrom, data).
|
|
-- node.output(nFrom, nTo, data): The normal Copper send function.
|
|
|
|
-- onRReceive is now: (from, to, port, data, unreliablePacket)
|
|
-- where to can be anything for unreliable packets,
|
|
-- but otherwise is the current hostname.
|
|
return function (hostname, transmit, onRReceive, time, culib)
|
|
-- node.hostname should be used for hostname generally.
|
|
local node
|
|
|
|
-- The maximum amount of timers (used to cap memory usage).
|
|
-- Assuming 16 bytes per key, this should be 64k.
|
|
local tuningMaxTimers = 0x400
|
|
local tuningClearAntiduplicate = 60
|
|
local tuningAttempts = 12
|
|
local tuningAttemptTime = 2.5
|
|
|
|
-- Just an array, no special index.
|
|
-- Contents : {
|
|
-- trigger function,
|
|
-- expiry time
|
|
-- }
|
|
local timers = {}
|
|
-- Indexes are globalIds, values are timers for deleting entries out of this table.
|
|
local weAcked = {}
|
|
-- Indexes are globalIds, values are { successFunc, deathTimer }
|
|
local needsAck = {}
|
|
|
|
local function addTimer(trig, expi)
|
|
if #timers < tuningMaxTimers then
|
|
local t = {trig, time() + expi}
|
|
table.insert(timers, t)
|
|
return t
|
|
end
|
|
return nil
|
|
end
|
|
local function killTimer(t)
|
|
for i = 1, #timers do
|
|
if timers[i] == t then
|
|
table.remove(timers, i)
|
|
return
|
|
end
|
|
end
|
|
end
|
|
local function gen3Random()
|
|
return string.char(math.random(256) - 1) .. string.char(math.random(256) - 1) .. string.char(math.random(256) - 1)
|
|
end
|
|
local function genGlobalId(port)
|
|
local low = math.abs(math.floor(port)) % 256
|
|
local high = math.abs(math.floor(port / 256)) % 256
|
|
local portD = string.char(high) .. string.char(low)
|
|
return portD .. gen3Random()
|
|
end
|
|
|
|
local onReceive = function (nfrom, nto, data)
|
|
if data:len() < 7 then return end
|
|
local port = data:byte(2) + (data:byte(1) * 256)
|
|
local tp = data:byte(7)
|
|
local globalId = data:sub(1, 5)
|
|
if (tp == 0x01) or (tp == 0x00) then
|
|
-- Only send one acknowledgement per packet,
|
|
-- but only receive the packet once.
|
|
-- (This is why timers are counted - to prevent the weAcked pool from getting too big.)
|
|
if not weAcked[nto .. globalId] then
|
|
onRReceive(nfrom, nto, port, data:sub(8), tp == 0x00)
|
|
else
|
|
killTimer(weAcked[nto .. globalId])
|
|
end
|
|
weAcked[nto .. globalId] = addTimer(function ()
|
|
weAcked[nto .. globalId] = nil
|
|
end, tuningClearAntiduplicate)
|
|
|
|
-- Check if this should actually be ACKed
|
|
if tp ~= 0x01 then return end
|
|
if nto ~= node.hostname then return end
|
|
node.output(nto, nfrom, data:sub(1, 6) .. "\x02")
|
|
end
|
|
-- Ignore ACKs for our packets that aren't going our way.
|
|
-- (Just a fringe case of packet ID reuse)
|
|
if nto ~= node.hostname then
|
|
return
|
|
end
|
|
if (tp == 0x02) and needsAck[nfrom .. globalId] then
|
|
needsAck[nfrom .. globalId][1](nfrom)
|
|
killTimer(needsAck[nfrom .. globalId][2])
|
|
needsAck[nfrom .. globalId] = nil
|
|
end
|
|
end
|
|
|
|
-- Instantiate the node or lookalike.
|
|
node = culib(hostname, transmit, onReceive, time)
|
|
|
|
local relib = {}
|
|
relib.setHostname = function (h)
|
|
node.hostname = h
|
|
end
|
|
relib.getHostname = function ()
|
|
return node.hostname
|
|
end
|
|
relib.refresh = function ()
|
|
node.refresh()
|
|
local i = 1
|
|
local t = time()
|
|
while i <= #timers do
|
|
if timers[i][2] <= t then
|
|
table.remove(timers, i)[1]()
|
|
else
|
|
i = i + 1
|
|
end
|
|
end
|
|
end
|
|
relib.input = node.input
|
|
-- can be reduced to output(nto, port, data) safely
|
|
relib.output = function (nto, port, data, unreliable, onSucceed, onFailure)
|
|
onSucceed = onSucceed or (function () end)
|
|
onFailure = onFailure or (function () end)
|
|
local gid = genGlobalId(port)
|
|
local tp = "\x01"
|
|
-- Unreliable packets:
|
|
-- 1. Can't be ACKed (not in the needsAck table)
|
|
-- 2. Are otherwise subject to the same rules as regular packets
|
|
if unreliable then
|
|
tp = "\x00"
|
|
end
|
|
local na = {onSucceed}
|
|
local attempt = -1
|
|
local doAttempt
|
|
doAttempt = function ()
|
|
attempt = attempt + 1
|
|
if attempt == tuningAttempts then
|
|
if not unreliable then
|
|
needsAck[nto .. gid] = nil
|
|
onFailure()
|
|
end
|
|
return
|
|
end
|
|
node.output(node.hostname, nto, gid .. string.char(attempt) .. tp .. data)
|
|
na[2] = addTimer(doAttempt, tuningAttemptTime)
|
|
if not na[2] then
|
|
needsAck[nto .. gid] = nil
|
|
if not unreliable then
|
|
onFailure()
|
|
end
|
|
end
|
|
end
|
|
if not unreliable then
|
|
needsAck[nto .. gid] = na
|
|
end
|
|
doAttempt()
|
|
end
|
|
return relib
|
|
end
|