diff --git a/control.lua b/control.lua index 0cb38b1..58906e0 100644 --- a/control.lua +++ b/control.lua @@ -9,23 +9,21 @@ require("metrics-combinator") require("player-statistics") require("send-utils") ---tickInterval = tonumber(settings.global["factorio-metrics-exporter-tick-interval"].value) or 300 ---options.udpPort = 52555 + isInitialized = false ---options.sendIndex = 0 ---serverIndex = 0 scannedGrids = false scannedLabs = false scannedGenerators = false autotrainGroupName = "" autotrainDepotName = "" +sendIndex = 0 options = { ---@type integer udpPort = 52555, ---@type integer - senderIndex = 0, + senderPlayerIndex = 0, ---@type integer tickInterval = 300, enableMod = false, @@ -69,10 +67,10 @@ script.on_init(function() storage.scannedGrids = false storage.scannedLabs = false - options.senderIndex = 0 + options.senderPlayerIndex = 0 options.enableMod = settings.global["factorio-metrics-exporter-enable"].value - options.senderIndex = settings.global["factorio-metrics-exporter-sending-player-index"].value + options.senderPlayerIndex = settings.global["factorio-metrics-exporter-sending-player-index"].value options.udpPort = settings.global["factorio-metrics-exporter-udp-port"].value options.enableProduction = settings.global["factorio-metrics-exporter-export_production_stats"].value options.enablePollution = settings.global["factorio-metrics-exporter-export_pollution_stats"].value @@ -94,7 +92,7 @@ script.on_load(function() log("udpAddress: " .. options.udpPort) options.enableMod = settings.global["factorio-metrics-exporter-enable"].value - options.senderIndex = settings.global["factorio-metrics-exporter-sending-player-index"].value + options.senderPlayerIndex = settings.global["factorio-metrics-exporter-sending-player-index"].value options.udpPort = settings.global["factorio-metrics-exporter-udp-port"].value options.enableProduction = settings.global["factorio-metrics-exporter-export_production_stats"].value options.enablePollution = settings.global["factorio-metrics-exporter-export_pollution_stats"].value @@ -154,7 +152,7 @@ script.on_event(defines.events.on_runtime_mod_setting_changed, function(event) end if event.setting == "factorio-metrics-exporter-sending-player-index" then - options.senderIndex = settings.global["factorio-metrics-exporter-sending-player-index"].value + options.senderPlayerIndex = settings.global["factorio-metrics-exporter-sending-player-index"].value end if event.setting == "factorio-metrics-exporter-udp-port" then @@ -266,23 +264,23 @@ function SendAll(event) if options.enableMod == true then local interval = math.max(1, math.floor(options.tickInterval / 10)) if event.tick % interval ~= 0 then return end - options.senderIndex = (options.senderIndex % 10) + 1 - if options.senderIndex == 1 then SendProductionStats() end - if options.senderIndex == 2 then SendPollutionStats() end - if options.senderIndex == 3 then + sendIndex = (sendIndex % 10) + 1 + if sendIndex == 1 then SendProductionStats() end + if sendIndex == 2 then SendPollutionStats() end + if sendIndex == 3 then SendKillStats() SendPlayerEntityStats() end - if options.senderIndex == 4 then SendFluidProductionStats() end - if options.senderIndex == 5 then SendBuildStats() end - if options.senderIndex == 6 then + if sendIndex == 4 then SendFluidProductionStats() end + if sendIndex == 5 then SendBuildStats() end + if sendIndex == 6 then SendResearchStats() SendCombinatorMetrics() end - if options.senderIndex == 7 then SendLogisticStats() end - if options.senderIndex == 8 then SendPowerStats() end - if options.senderIndex == 9 then SendGameStats() end - if options.senderIndex == 10 then SendTrainStats() end + if sendIndex == 7 then SendLogisticStats() end + if sendIndex == 8 then SendPowerStats() end + if sendIndex == 9 then SendGameStats() end + if sendIndex == 10 then SendTrainStats() end end end diff --git a/game-stats.lua b/game-stats.lua index 75d78a8..eb73bc0 100644 --- a/game-stats.lua +++ b/game-stats.lua @@ -31,26 +31,38 @@ function GetPlayerKills() end return table.concat(killParts, "\n") end - function SendPlayerEntityStats() - local entityParts = {} - entityParts[#entityParts + 1] = "---player-build-stats---" + local resultParts = {} + -- Localize for speed + local insert = table.insert + + local prof = game.create_profiler() + + insert(resultParts, "---player-build-stats---") + + -- Process Constructed for playerIndex, items in pairs(storage.constructedEntites) do local playerName = game.players[playerIndex].name + local prefix = playerIndex .. ":" .. playerName .. ":c:" + for itemName, itemCount in pairs(items) do - entityParts[#entityParts + 1] = ("%s:%s:constructed:%s:%s"):format(playerIndex, playerName, itemName, itemCount) + insert(resultParts, prefix .. itemName .. ":" .. itemCount) end end - SendChunked("---player-build-stats---\n" .. table.concat(entityParts,"\n")) - entityParts = {} - entityParts[#entityParts + 1] = "---player-build-stats---" + + -- Process Deconstructed for playerIndex, items in pairs(storage.deconstructedEntities) do local playerName = game.players[playerIndex].name + local prefix = playerIndex .. ":" .. playerName .. ":d:" + for itemName, itemCount in pairs(items) do - entityParts[#entityParts + 1] = ("%s:%s:deconstructed:%s:%s"):format(playerIndex, playerName, itemName, itemCount) + insert(resultParts, prefix .. itemName .. ":" .. itemCount) end end - SendChunked("---player-build-stats---\n" .. table.concat(entityParts,"\n")) + + prof.stop() + game.print(prof) + SendChunked(table.concat(resultParts, "\n")) end function GetMapSeed() @@ -65,24 +77,24 @@ end ---Takes all players that ever visited the server into account ---@return string function GetPlayerTime() - local timeParts = {} - timeParts[#timeParts + 1] = "---player-times---\n" + local resultParts = {} + resultParts[#resultParts + 1] = "---player-times---\n" for _, player in pairs(game.players) do - timeParts[#timeParts + 1] = ("%s:%d:%d"):format(player.name, player.index, player.online_time) + resultParts[#resultParts + 1] = ("%s:%d:%d"):format(player.name, player.index, player.online_time) end - return table.concat(timeParts, "\n") + return table.concat(resultParts, "\n") end ---comment ---@return string function GetPlayerDeaths() - local deathParts = {} - deathParts[#deathParts + 1] = "---player-deaths---\n" + local resultParts = {} + resultParts[#resultParts + 1] = "---player-deaths---\n" for _, player in pairs(game.players) do - deathParts[#deathParts + 1] = ("%s:%d:%d"):format(player.name, player.index, + resultParts[#resultParts + 1] = ("%s:%d:%d"):format(player.name, player.index, storage.playerDeathCount[player.index] or 0) end - return table.concat(deathParts, "\n") + return table.concat(resultParts, "\n") end function onPlayerDeath(event) @@ -90,15 +102,15 @@ function onPlayerDeath(event) end function GetPlayerDeathCauses() - local deathParts = {} - deathParts[#deathParts + 1] = "---player-death-cause---\n" + local resultParts = {} + resultParts[#resultParts + 1] = "---player-death-cause---\n" for playerIndex, deathCauses in pairs(storage.playerDeathCause) do for causeName, causeCount in pairs(deathCauses) do - deathParts[#deathParts + 1] = ("%s:%d:%s:%d"):format(game.players[playerIndex].name, playerIndex, causeName, + resultParts[#resultParts + 1] = ("%s:%d:%s:%d"):format(game.players[playerIndex].name, playerIndex, causeName, causeCount) end end - return table.concat(deathParts, "\n") + return table.concat(resultParts, "\n") end function GetTotalPlayTime() diff --git a/metrics-combinator.lua b/metrics-combinator.lua index c7e3e87..3139270 100644 --- a/metrics-combinator.lua +++ b/metrics-combinator.lua @@ -158,8 +158,12 @@ function SendCombinatorMetrics() local greenNet = entity.get_circuit_network(defines.wire_connector_id.circuit_green) if redNet then for _, signal in pairs(redNet.signals) do + local quality = "-normal" + if signal.signal.quality then + quality = signal.signal.quality.name + end netParts[#netParts + 1] = ("%s:red:%s:%d"):format(combinatorFlags.name, - signal.signal.name .. signal.signal.quality.name, signal.count) + signal.signal.name.."-"..quality, signal.count) end end if greenNet then diff --git a/send-utils.lua b/send-utils.lua index cf7902a..8a45129 100644 --- a/send-utils.lua +++ b/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: ------) ----@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 +---@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