Refactor send logic to use senderPlayerIndex and optimize content chunking
This commit is contained in:
168
send-utils.lua
168
send-utils.lua
@@ -1,138 +1,68 @@
|
||||
local function SendUDP(content)
|
||||
if content then
|
||||
helpers.send_udp(options.udpPort, content, options.senderIndex)
|
||||
helpers.send_udp(options.udpPort, content, options.senderPlayerIndex)
|
||||
else
|
||||
error("Missing udpPort or Content")
|
||||
end
|
||||
end
|
||||
|
||||
---Extracts the header from content if it exists (format: ---<headerline>---)
|
||||
---@param content string
|
||||
---@return string|nil header, number headerEndPos
|
||||
local function ExtractHeader(content)
|
||||
local headerStart, headerEnd, headerText = content:find("^%-%-%-(.-)%-%-%-\n", 1)
|
||||
if headerStart then
|
||||
return "---" .. headerText .. "---", headerEnd
|
||||
end
|
||||
return nil, 0
|
||||
end
|
||||
|
||||
---Finds all header positions in content once (format: ---<headerline>---)
|
||||
---@param content string
|
||||
---@return table headers Array of {endPos=position_after_newline, text=header_text}
|
||||
local function FindAllHeaders(content)
|
||||
local headers = {}
|
||||
local pos = 1
|
||||
while true do
|
||||
local start, endPos, headerText = content:find("%-%-%-(.-)%-%-%-\n", pos)
|
||||
if not start then break end
|
||||
table.insert(headers, {endPos = endPos + 1, text = "---" .. headerText .. "---"})
|
||||
pos = endPos + 1
|
||||
end
|
||||
return headers
|
||||
end
|
||||
|
||||
---Binary search to find nearest header before targetPos
|
||||
---@param headers table Pre-computed header positions
|
||||
---@param targetPos number Maximum position to search for
|
||||
---@return number|nil index, number|nil headerEndPos, string|nil headerText
|
||||
local function BinarySearchHeader(headers, targetPos)
|
||||
local left, right = 1, #headers
|
||||
local result = nil
|
||||
|
||||
while left <= right do
|
||||
local mid = math.floor((left + right) / 2)
|
||||
if headers[mid].endPos <= targetPos then
|
||||
result = mid
|
||||
left = mid + 1
|
||||
else
|
||||
right = mid - 1
|
||||
end
|
||||
end
|
||||
|
||||
if result then
|
||||
return result, headers[result].endPos, headers[result].text
|
||||
end
|
||||
return nil, nil, nil
|
||||
end
|
||||
|
||||
---Chunks content into appropriately sized pieces, respecting header lines
|
||||
---Chunks content greedily to minimize UDP calls
|
||||
---Fills 200KB buffer as completely as possible, including multiple headers if they fit
|
||||
---Only prepends header when chunk starts mid-section
|
||||
---@param content string
|
||||
---@return table chunks Array of {header=string|nil, content=string}---@param content string
|
||||
---@param maxSizekB number|nil Default 200KB
|
||||
---@return table chunks Array of {header=string|nil, content=string}
|
||||
local function ChunkContent(content, maxSizekB)
|
||||
local maxSize = (maxSizekB or 200) * 1024
|
||||
local contentLength = #content
|
||||
|
||||
if contentLength < maxSize then
|
||||
return {{header = nil, content = content}}
|
||||
end
|
||||
|
||||
-- Extract initial header if present
|
||||
local initialHeader, headerEndPos = ExtractHeader(content)
|
||||
local contentStart = headerEndPos > 0 and headerEndPos + 1 or 1
|
||||
local remainingContent = content:sub(contentStart)
|
||||
|
||||
-- Pre-compute all headers once for efficient lookup
|
||||
local headers = FindAllHeaders(remainingContent)
|
||||
|
||||
local len = #content
|
||||
local chunks = {}
|
||||
local start = 1
|
||||
local len = #remainingContent
|
||||
local lastStart = 0
|
||||
|
||||
-- 1. Pre-compute headers (O(N))
|
||||
local headers = {}
|
||||
local hPos = 1
|
||||
while true do
|
||||
local hStart, hEnd, hText = content:find("%%-%-%-(.-)%%-%-%-\n", hPos)
|
||||
if not hStart then break end
|
||||
table.insert(headers, {startPos = hStart, text = "---" .. hText .. "---"})
|
||||
hPos = hEnd + 1
|
||||
end
|
||||
|
||||
local start = 1
|
||||
local headerIdx = 1
|
||||
local activeHeaderText = nil
|
||||
|
||||
while start <= len do
|
||||
-- Safety check to prevent infinite loops
|
||||
if start == lastStart then
|
||||
error("Infinite loop detected in ChunkContent: cannot make progress")
|
||||
while headerIdx <= #headers and headers[headerIdx].startPos <= start do
|
||||
activeHeaderText = headers[headerIdx].text
|
||||
headerIdx = headerIdx + 1
|
||||
end
|
||||
lastStart = start
|
||||
|
||||
-- Determine the "Active Context" - which header does this position belong to?
|
||||
local _, _, activeHeaderText = BinarySearchHeader(headers, start)
|
||||
|
||||
-- Check if we're starting exactly at a header boundary (after a newline)
|
||||
local startingAtHeader = false
|
||||
for _, hdr in ipairs(headers) do
|
||||
if hdr.endPos == start then
|
||||
startingAtHeader = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- Calculate available space for content
|
||||
local needsPrepend = not startingAtHeader and activeHeaderText ~= nil
|
||||
|
||||
local isAtHeaderStart = (headerIdx > 1 and headers[headerIdx-1].startPos == start)
|
||||
local needsPrepend = (not isAtHeaderStart) and (activeHeaderText ~= nil)
|
||||
local overhead = needsPrepend and (#activeHeaderText + 1) or 0
|
||||
local availableSpace = maxSize - overhead
|
||||
local target = start + availableSpace - 1
|
||||
|
||||
-- If remaining content fits in one chunk
|
||||
if target >= len then
|
||||
local chunk = remainingContent:sub(start, len)
|
||||
table.insert(chunks, {header = needsPrepend and activeHeaderText or nil, content = chunk})
|
||||
if availableSpace <= 0 then error("Header exceeds maxSize") end
|
||||
|
||||
local target = math.min(start + availableSpace - 1, len)
|
||||
|
||||
if target == len then
|
||||
table.insert(chunks, {header = needsPrepend and activeHeaderText or nil, content = content:sub(start, len)})
|
||||
break
|
||||
end
|
||||
|
||||
-- Greedy approach: fill buffer completely
|
||||
-- Search backward from target for a newline to split cleanly
|
||||
local splitPos = target
|
||||
while splitPos > start and remainingContent:byte(splitPos) ~= 10 do
|
||||
splitPos = splitPos - 1
|
||||
|
||||
-- Optimized C-level search for newline
|
||||
local chunkView = content:sub(start, target)
|
||||
local lastNewline = chunkView:match(".*()\n")
|
||||
|
||||
if not lastNewline then
|
||||
error("Line too long at byte " .. start)
|
||||
end
|
||||
|
||||
-- Fallback if no newline found in buffer (force split at target)
|
||||
if splitPos <= start then
|
||||
splitPos = target
|
||||
end
|
||||
|
||||
local chunk = remainingContent:sub(start, splitPos)
|
||||
table.insert(chunks, {header = needsPrepend and activeHeaderText or nil, content = chunk})
|
||||
|
||||
local splitPos = start + lastNewline - 1
|
||||
table.insert(chunks, {header = needsPrepend and activeHeaderText or nil, content = content:sub(start, splitPos)})
|
||||
start = splitPos + 1
|
||||
end
|
||||
|
||||
return chunks
|
||||
end
|
||||
|
||||
@@ -144,21 +74,21 @@ function SendChunked(content, maxSizekB)
|
||||
if not content or #content == 0 then
|
||||
error("Missing or empty content")
|
||||
end
|
||||
|
||||
|
||||
local maxSize = (maxSizekB or 200) * 1024
|
||||
local initialHeader = ExtractHeader(content)
|
||||
if initialHeader and (#initialHeader + 1) > maxSize then
|
||||
error("Header itself exceeds maximum chunk size of " .. (maxSize / 1024) .. "kB")
|
||||
end
|
||||
|
||||
-- Content is smaller than maxSize, so send immediately
|
||||
if #content <= maxSize then
|
||||
SendUDP(content)
|
||||
return
|
||||
end
|
||||
|
||||
local chunks = ChunkContent(content, maxSizekB)
|
||||
|
||||
for _, chunk in ipairs(chunks) do
|
||||
local toSend = ""
|
||||
if chunk.header then
|
||||
toSend = chunk.header .. "\n"
|
||||
SendUDP(chunk.header .. "\n" .. chunk.content)
|
||||
else
|
||||
SendUDP(chunk.content)
|
||||
end
|
||||
toSend = toSend .. chunk.content
|
||||
SendUDP(toSend)
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user