local function SendUDP(content) if content then helpers.send_udp(options.udpPort, content, options.senderIndex) else error("Missing udpPort or Content") end end ---Extracts the header from content if it exists (format: ------) ---@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: ------) ---@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 ---@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 chunks = {} local start = 1 local len = #remainingContent local lastStart = 0 while start <= len do -- Safety check to prevent infinite loops if start == lastStart then error("Infinite loop detected in ChunkContent: cannot make progress") 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 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}) 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 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}) start = splitPos + 1 end return chunks end ---Sends content in chunks with optional header ---If content starts with header (------), it's automatically repeated in each chunk ---@param content string ---@param maxSizekB number|nil Default 200KB 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 local chunks = ChunkContent(content, maxSizekB) for _, chunk in ipairs(chunks) do local toSend = "" if chunk.header then toSend = chunk.header .. "\n" end toSend = toSend .. chunk.content SendUDP(toSend) end end