Refactor send logic to use senderPlayerIndex and optimize content chunking
This commit is contained in:
38
control.lua
38
control.lua
@@ -9,23 +9,21 @@ require("metrics-combinator")
|
|||||||
require("player-statistics")
|
require("player-statistics")
|
||||||
require("send-utils")
|
require("send-utils")
|
||||||
|
|
||||||
--tickInterval = tonumber(settings.global["factorio-metrics-exporter-tick-interval"].value) or 300
|
|
||||||
--options.udpPort = 52555
|
|
||||||
isInitialized = false
|
isInitialized = false
|
||||||
--options.sendIndex = 0
|
|
||||||
--serverIndex = 0
|
|
||||||
scannedGrids = false
|
scannedGrids = false
|
||||||
scannedLabs = false
|
scannedLabs = false
|
||||||
scannedGenerators = false
|
scannedGenerators = false
|
||||||
autotrainGroupName = ""
|
autotrainGroupName = ""
|
||||||
autotrainDepotName = ""
|
autotrainDepotName = ""
|
||||||
|
sendIndex = 0
|
||||||
|
|
||||||
|
|
||||||
options = {
|
options = {
|
||||||
---@type integer
|
---@type integer
|
||||||
udpPort = 52555,
|
udpPort = 52555,
|
||||||
---@type integer
|
---@type integer
|
||||||
senderIndex = 0,
|
senderPlayerIndex = 0,
|
||||||
---@type integer
|
---@type integer
|
||||||
tickInterval = 300,
|
tickInterval = 300,
|
||||||
enableMod = false,
|
enableMod = false,
|
||||||
@@ -69,10 +67,10 @@ script.on_init(function()
|
|||||||
|
|
||||||
storage.scannedGrids = false
|
storage.scannedGrids = false
|
||||||
storage.scannedLabs = false
|
storage.scannedLabs = false
|
||||||
options.senderIndex = 0
|
options.senderPlayerIndex = 0
|
||||||
|
|
||||||
options.enableMod = settings.global["factorio-metrics-exporter-enable"].value
|
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.udpPort = settings.global["factorio-metrics-exporter-udp-port"].value
|
||||||
options.enableProduction = settings.global["factorio-metrics-exporter-export_production_stats"].value
|
options.enableProduction = settings.global["factorio-metrics-exporter-export_production_stats"].value
|
||||||
options.enablePollution = settings.global["factorio-metrics-exporter-export_pollution_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)
|
log("udpAddress: " .. options.udpPort)
|
||||||
|
|
||||||
options.enableMod = settings.global["factorio-metrics-exporter-enable"].value
|
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.udpPort = settings.global["factorio-metrics-exporter-udp-port"].value
|
||||||
options.enableProduction = settings.global["factorio-metrics-exporter-export_production_stats"].value
|
options.enableProduction = settings.global["factorio-metrics-exporter-export_production_stats"].value
|
||||||
options.enablePollution = settings.global["factorio-metrics-exporter-export_pollution_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
|
end
|
||||||
|
|
||||||
if event.setting == "factorio-metrics-exporter-sending-player-index" then
|
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
|
end
|
||||||
|
|
||||||
if event.setting == "factorio-metrics-exporter-udp-port" then
|
if event.setting == "factorio-metrics-exporter-udp-port" then
|
||||||
@@ -266,23 +264,23 @@ function SendAll(event)
|
|||||||
if options.enableMod == true then
|
if options.enableMod == true then
|
||||||
local interval = math.max(1, math.floor(options.tickInterval / 10))
|
local interval = math.max(1, math.floor(options.tickInterval / 10))
|
||||||
if event.tick % interval ~= 0 then return end
|
if event.tick % interval ~= 0 then return end
|
||||||
options.senderIndex = (options.senderIndex % 10) + 1
|
sendIndex = (sendIndex % 10) + 1
|
||||||
if options.senderIndex == 1 then SendProductionStats() end
|
if sendIndex == 1 then SendProductionStats() end
|
||||||
if options.senderIndex == 2 then SendPollutionStats() end
|
if sendIndex == 2 then SendPollutionStats() end
|
||||||
if options.senderIndex == 3 then
|
if sendIndex == 3 then
|
||||||
SendKillStats()
|
SendKillStats()
|
||||||
SendPlayerEntityStats()
|
SendPlayerEntityStats()
|
||||||
end
|
end
|
||||||
if options.senderIndex == 4 then SendFluidProductionStats() end
|
if sendIndex == 4 then SendFluidProductionStats() end
|
||||||
if options.senderIndex == 5 then SendBuildStats() end
|
if sendIndex == 5 then SendBuildStats() end
|
||||||
if options.senderIndex == 6 then
|
if sendIndex == 6 then
|
||||||
SendResearchStats()
|
SendResearchStats()
|
||||||
SendCombinatorMetrics()
|
SendCombinatorMetrics()
|
||||||
end
|
end
|
||||||
if options.senderIndex == 7 then SendLogisticStats() end
|
if sendIndex == 7 then SendLogisticStats() end
|
||||||
if options.senderIndex == 8 then SendPowerStats() end
|
if sendIndex == 8 then SendPowerStats() end
|
||||||
if options.senderIndex == 9 then SendGameStats() end
|
if sendIndex == 9 then SendGameStats() end
|
||||||
if options.senderIndex == 10 then SendTrainStats() end
|
if sendIndex == 10 then SendTrainStats() end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -31,26 +31,38 @@ function GetPlayerKills()
|
|||||||
end
|
end
|
||||||
return table.concat(killParts, "\n")
|
return table.concat(killParts, "\n")
|
||||||
end
|
end
|
||||||
|
|
||||||
function SendPlayerEntityStats()
|
function SendPlayerEntityStats()
|
||||||
local entityParts = {}
|
local resultParts = {}
|
||||||
entityParts[#entityParts + 1] = "---player-build-stats---"
|
-- 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
|
for playerIndex, items in pairs(storage.constructedEntites) do
|
||||||
local playerName = game.players[playerIndex].name
|
local playerName = game.players[playerIndex].name
|
||||||
|
local prefix = playerIndex .. ":" .. playerName .. ":c:"
|
||||||
|
|
||||||
for itemName, itemCount in pairs(items) do
|
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
|
||||||
end
|
end
|
||||||
SendChunked("---player-build-stats---\n" .. table.concat(entityParts,"\n"))
|
|
||||||
entityParts = {}
|
-- Process Deconstructed
|
||||||
entityParts[#entityParts + 1] = "---player-build-stats---"
|
|
||||||
for playerIndex, items in pairs(storage.deconstructedEntities) do
|
for playerIndex, items in pairs(storage.deconstructedEntities) do
|
||||||
local playerName = game.players[playerIndex].name
|
local playerName = game.players[playerIndex].name
|
||||||
|
local prefix = playerIndex .. ":" .. playerName .. ":d:"
|
||||||
|
|
||||||
for itemName, itemCount in pairs(items) do
|
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
|
||||||
end
|
end
|
||||||
SendChunked("---player-build-stats---\n" .. table.concat(entityParts,"\n"))
|
|
||||||
|
prof.stop()
|
||||||
|
game.print(prof)
|
||||||
|
SendChunked(table.concat(resultParts, "\n"))
|
||||||
end
|
end
|
||||||
|
|
||||||
function GetMapSeed()
|
function GetMapSeed()
|
||||||
@@ -65,24 +77,24 @@ end
|
|||||||
---Takes all players that ever visited the server into account
|
---Takes all players that ever visited the server into account
|
||||||
---@return string
|
---@return string
|
||||||
function GetPlayerTime()
|
function GetPlayerTime()
|
||||||
local timeParts = {}
|
local resultParts = {}
|
||||||
timeParts[#timeParts + 1] = "---player-times---\n"
|
resultParts[#resultParts + 1] = "---player-times---\n"
|
||||||
for _, player in pairs(game.players) do
|
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
|
end
|
||||||
return table.concat(timeParts, "\n")
|
return table.concat(resultParts, "\n")
|
||||||
end
|
end
|
||||||
|
|
||||||
---comment
|
---comment
|
||||||
---@return string
|
---@return string
|
||||||
function GetPlayerDeaths()
|
function GetPlayerDeaths()
|
||||||
local deathParts = {}
|
local resultParts = {}
|
||||||
deathParts[#deathParts + 1] = "---player-deaths---\n"
|
resultParts[#resultParts + 1] = "---player-deaths---\n"
|
||||||
for _, player in pairs(game.players) do
|
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)
|
storage.playerDeathCount[player.index] or 0)
|
||||||
end
|
end
|
||||||
return table.concat(deathParts, "\n")
|
return table.concat(resultParts, "\n")
|
||||||
end
|
end
|
||||||
|
|
||||||
function onPlayerDeath(event)
|
function onPlayerDeath(event)
|
||||||
@@ -90,15 +102,15 @@ function onPlayerDeath(event)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function GetPlayerDeathCauses()
|
function GetPlayerDeathCauses()
|
||||||
local deathParts = {}
|
local resultParts = {}
|
||||||
deathParts[#deathParts + 1] = "---player-death-cause---\n"
|
resultParts[#resultParts + 1] = "---player-death-cause---\n"
|
||||||
for playerIndex, deathCauses in pairs(storage.playerDeathCause) do
|
for playerIndex, deathCauses in pairs(storage.playerDeathCause) do
|
||||||
for causeName, causeCount in pairs(deathCauses) 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)
|
causeCount)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return table.concat(deathParts, "\n")
|
return table.concat(resultParts, "\n")
|
||||||
end
|
end
|
||||||
|
|
||||||
function GetTotalPlayTime()
|
function GetTotalPlayTime()
|
||||||
|
|||||||
@@ -158,8 +158,12 @@ function SendCombinatorMetrics()
|
|||||||
local greenNet = entity.get_circuit_network(defines.wire_connector_id.circuit_green)
|
local greenNet = entity.get_circuit_network(defines.wire_connector_id.circuit_green)
|
||||||
if redNet then
|
if redNet then
|
||||||
for _, signal in pairs(redNet.signals) do
|
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,
|
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
|
||||||
end
|
end
|
||||||
if greenNet then
|
if greenNet then
|
||||||
|
|||||||
154
send-utils.lua
154
send-utils.lua
@@ -1,138 +1,68 @@
|
|||||||
local function SendUDP(content)
|
local function SendUDP(content)
|
||||||
if content then
|
if content then
|
||||||
helpers.send_udp(options.udpPort, content, options.senderIndex)
|
helpers.send_udp(options.udpPort, content, options.senderPlayerIndex)
|
||||||
else
|
else
|
||||||
error("Missing udpPort or Content")
|
error("Missing udpPort or Content")
|
||||||
end
|
end
|
||||||
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 into appropriately sized pieces, respecting header lines
|
||||||
---Chunks content greedily to minimize UDP calls
|
---Chunks content greedily to minimize UDP calls
|
||||||
---Fills 200KB buffer as completely as possible, including multiple headers if they fit
|
---Fills 200KB buffer as completely as possible, including multiple headers if they fit
|
||||||
---Only prepends header when chunk starts mid-section
|
---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
|
---@param maxSizekB number|nil Default 200KB
|
||||||
---@return table chunks Array of {header=string|nil, content=string}
|
|
||||||
local function ChunkContent(content, maxSizekB)
|
local function ChunkContent(content, maxSizekB)
|
||||||
local maxSize = (maxSizekB or 200) * 1024
|
local maxSize = (maxSizekB or 200) * 1024
|
||||||
local contentLength = #content
|
local len = #content
|
||||||
|
local chunks = {}
|
||||||
|
|
||||||
if contentLength < maxSize then
|
-- 1. Pre-compute headers (O(N))
|
||||||
return {{header = nil, content = content}}
|
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
|
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 start = 1
|
||||||
local len = #remainingContent
|
local headerIdx = 1
|
||||||
local lastStart = 0
|
local activeHeaderText = nil
|
||||||
|
|
||||||
while start <= len do
|
while start <= len do
|
||||||
-- Safety check to prevent infinite loops
|
while headerIdx <= #headers and headers[headerIdx].startPos <= start do
|
||||||
if start == lastStart then
|
activeHeaderText = headers[headerIdx].text
|
||||||
error("Infinite loop detected in ChunkContent: cannot make progress")
|
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
|
end
|
||||||
|
|
||||||
-- Calculate available space for content
|
local isAtHeaderStart = (headerIdx > 1 and headers[headerIdx-1].startPos == start)
|
||||||
local needsPrepend = not startingAtHeader and activeHeaderText ~= nil
|
local needsPrepend = (not isAtHeaderStart) and (activeHeaderText ~= nil)
|
||||||
local overhead = needsPrepend and (#activeHeaderText + 1) or 0
|
local overhead = needsPrepend and (#activeHeaderText + 1) or 0
|
||||||
local availableSpace = maxSize - overhead
|
local availableSpace = maxSize - overhead
|
||||||
local target = start + availableSpace - 1
|
|
||||||
|
|
||||||
-- If remaining content fits in one chunk
|
if availableSpace <= 0 then error("Header exceeds maxSize") end
|
||||||
if target >= len then
|
|
||||||
local chunk = remainingContent:sub(start, len)
|
local target = math.min(start + availableSpace - 1, len)
|
||||||
table.insert(chunks, {header = needsPrepend and activeHeaderText or nil, content = chunk})
|
|
||||||
|
if target == len then
|
||||||
|
table.insert(chunks, {header = needsPrepend and activeHeaderText or nil, content = content:sub(start, len)})
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Greedy approach: fill buffer completely
|
-- Optimized C-level search for newline
|
||||||
-- Search backward from target for a newline to split cleanly
|
local chunkView = content:sub(start, target)
|
||||||
local splitPos = target
|
local lastNewline = chunkView:match(".*()\n")
|
||||||
while splitPos > start and remainingContent:byte(splitPos) ~= 10 do
|
|
||||||
splitPos = splitPos - 1
|
if not lastNewline then
|
||||||
|
error("Line too long at byte " .. start)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Fallback if no newline found in buffer (force split at target)
|
local splitPos = start + lastNewline - 1
|
||||||
if splitPos <= start then
|
table.insert(chunks, {header = needsPrepend and activeHeaderText or nil, content = content:sub(start, splitPos)})
|
||||||
splitPos = target
|
|
||||||
end
|
|
||||||
|
|
||||||
local chunk = remainingContent:sub(start, splitPos)
|
|
||||||
table.insert(chunks, {header = needsPrepend and activeHeaderText or nil, content = chunk})
|
|
||||||
start = splitPos + 1
|
start = splitPos + 1
|
||||||
end
|
end
|
||||||
|
|
||||||
return chunks
|
return chunks
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -146,19 +76,19 @@ function SendChunked(content, maxSizekB)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local maxSize = (maxSizekB or 200) * 1024
|
local maxSize = (maxSizekB or 200) * 1024
|
||||||
local initialHeader = ExtractHeader(content)
|
|
||||||
if initialHeader and (#initialHeader + 1) > maxSize then
|
-- Content is smaller than maxSize, so send immediately
|
||||||
error("Header itself exceeds maximum chunk size of " .. (maxSize / 1024) .. "kB")
|
if #content <= maxSize then
|
||||||
|
SendUDP(content)
|
||||||
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local chunks = ChunkContent(content, maxSizekB)
|
local chunks = ChunkContent(content, maxSizekB)
|
||||||
|
|
||||||
for _, chunk in ipairs(chunks) do
|
for _, chunk in ipairs(chunks) do
|
||||||
local toSend = ""
|
|
||||||
if chunk.header then
|
if chunk.header then
|
||||||
toSend = chunk.header .. "\n"
|
SendUDP(chunk.header .. "\n" .. chunk.content)
|
||||||
end
|
else
|
||||||
toSend = toSend .. chunk.content
|
SendUDP(chunk.content)
|
||||||
SendUDP(toSend)
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user