Private
Public Access
1
0

Rewritten sending helper and logic

This commit is contained in:
Jan Grießhaber
2026-01-10 18:24:52 +01:00
parent 8fc41c2e5a
commit 2e3ea7dd4d
14 changed files with 317 additions and 138 deletions

3
.gitignore vendored
View File

@@ -1 +1,2 @@
factorio-metrics-exporter_0.1.3.zip
factorio-metrics-exporter_*.zip
.vscode/

View File

@@ -6,12 +6,14 @@ require("power-stats")
require("logistic-network-stats")
require("train-stats")
require("metrics-combinator")
require("player-statistics")
require("send-utils")
tickInterval = tonumber(settings.global["factorio-metrics-exporter-tick-interval"].value) or 300
udpAddress = 52555
--tickInterval = tonumber(settings.global["factorio-metrics-exporter-tick-interval"].value) or 300
--options.udpPort = 52555
isInitialized = false
sendIndex = 0
serverIndex = 0
--options.sendIndex = 0
--serverIndex = 0
scannedGrids = false
scannedLabs = false
scannedGenerators = false
@@ -20,6 +22,12 @@ autotrainDepotName = ""
options = {
---@type integer
udpPort = 52555,
---@type integer
senderIndex = 0,
---@type integer
tickInterval = 300,
enableMod = false,
enablePlayers = false,
enableProduction = false,
@@ -35,12 +43,8 @@ options = {
script.on_init(function()
storage.electricGrids = {}
storage.researchedTechnologies = {}
storage.playerDeathCount = {}
storage.totalLabCount = 0
storage.totalResearchSpeed = 0
storage.totalReseachProductivity = 0
storage.labs = {}
storage.playerKillCount = {}
storage.representativePoles = {}
@@ -58,16 +62,18 @@ script.on_init(function()
storage.cargoStats = {}
storage.metrics = {}
storage.cliffsDestroyed = 0
storage.nuclearReactorDeaths = 0
storage.playersOnline = 0
storage.scannedGrids = false
storage.scannedLabs = false
sendIndex = 0
options.senderIndex = 0
options.enableMod = settings.global["factorio-metrics-exporter-enable"].value
options.senderIndex = 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
options.enableFluid = settings.global["factorio-metrics-exporter-export_fluid_stats"].value
@@ -84,10 +90,12 @@ end)
script.on_load(function()
log("factorio-metrics-exporter: on_load")
log("tickInterval: " .. tickInterval)
log("udpAddress: " .. udpAddress)
log("tickInterval: " .. options.tickInterval)
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.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
options.enableFluid = settings.global["factorio-metrics-exporter-export_fluid_stats"].value
@@ -103,13 +111,9 @@ script.on_load(function()
end)
script.on_configuration_changed(function()
storage.electricGrids = storage.electricGrids or {}
storage.labs = storage.labs or {}
storage.playerDeathCount = storage.playerDeathCount or {}
storage.researchedTechnologies = storage.researchedTechnologies or {}
storage.totalLabCount = storage.totalLabCount or 0
storage.totalReseachProductivity = storage.totalReseachProductivity or 0
storage.totalResearchSpeed = storage.totalResearchSpeed or 0
storage.playerKillCount = storage.playerKillCount or {}
storage.representativePoles = storage.representativePoles or {}
storage.scannedGrids = storage.scannedGrids or false
@@ -127,7 +131,7 @@ script.on_configuration_changed(function()
storage.metrics = storage.metrics or {}
storage.cargoStats = storage.cargoStats or {}
storage.nuclearReactorDeaths = storage.nuclearReactorDeaths or 0
storage.cliffsDestroyed = storage.cliffsDestroyed or 0
storage.playersOnline = storage.playersOnline or 0
ScanNetworks()
ScanLabs()
ScanTrains()
@@ -139,12 +143,24 @@ end
script.on_event(defines.events.on_runtime_mod_setting_changed, function(event)
log("Mod setting changed: " .. event.setting)
if event.setting == "factorio-metrics-exporter-tick-interval" then
tickInterval = settings.global["factorio-metrics-exporter-tick-interval"].value
options.tickInterval = settings.global["factorio-metrics-exporter-tick-interval"].value
end
if event.setting == "factorio-metrics-exporter-enable" then
options.enableMod = settings.global["factorio-metrics-exporter-enable"].value
end
if event.setting == "factorio-metrics-exporter-sending-player-index" then
options.senderIndex = settings.global["factorio-metrics-exporter-sending-player-index"].value
end
if event.setting == "factorio-metrics-exporter-udp-port" then
options.udpPort = settings.global["factorio-metrics-exporter-sending-player-index"].value
end
if event.setting == "factorio-metrics-exporter-export_production_stats" then
options.enableProduction = settings.global["factorio-metrics-exporter-export_production_stats"].value
end
@@ -176,15 +192,19 @@ script.on_event(defines.events.on_runtime_mod_setting_changed, function(event)
if event.setting == "factorio-metrics-exporter-export_research_stats" then
options.enableResearch = settings.global["factorio-metrics-exporter-export_research_stats"].value
end
if event.setting == "factorio-metrics-exporter-export_train_stats" then
options.enableTrains = settings.global["factorio-metrics-exporter-export_train_stats"].value
end
if event.setting == "factorio-metrics-exporter-autotrain_depot_name" then
autotrainDepotName = settings.global["factorio-metrics-exporter-autotrain_depot_name"].value
end
if event.setting == "factorio-metrics-exporter-autotrain_group_name" then
autotrainGroupName = settings.global["factorio-metrics-exporter-autotrain_group_name"].value
end
if event.setting == "factorio-metrics-exporter-export_train_trips" then
options.enableTrainTrips = settings.global["factorio-metrics-exporter-export_train_trips"].value
end
@@ -192,41 +212,11 @@ end)
script.on_event(defines.events.on_player_died, function(event)
--Log player cause by player
if event.cause and event.cause.type == "character" then
local killer = event.cause.player
if killer then
local killer_index = killer.index
local victim_index = event.player_index
local killerName = killer.name
local victimName = game.players[victim_index].name
log(("Player ID %d:%s killed player ID %d:%s"):format(killer_index, killerName, victim_index, victimName))
storage.playerKillCount[killer_index] =
storage.playerKillCount[killer_index] or {}
storage.playerKillCount[killer_index][victim_index] =
(storage.playerKillCount[killer_index][victim_index] or 0) + 1
end
end
--Log cause of player death
if event.cause and event.cause.name then
storage.playerDeathCause[event.player_index] =
storage.playerDeathCause[event.player_index] or {}
storage.playerDeathCause[event.player_index][event.cause.name] =
(storage.playerDeathCause[event.player_index][event.cause.name] or 0) + 1
log(("Player %s died from type %s"):format(game.players[event.player_index].name, event.cause.name))
end
--Log player death count
storage.playerDeathCount[event.player_index] = (storage.playerDeathCount[event.player_index] or 0) + 1
onPlayerKilledPlayer(event)
onPlayerDiedDeathCause(event)
onPlayerDeath(event)
end)
function SendGameStats()
if options.enablePlayers then
local returnParts = {}
@@ -240,7 +230,7 @@ function SendGameStats()
returnParts[#returnParts + 1] = GetPlayerDeaths()
returnParts[#returnParts + 1] = GetPlayerDeathCauses()
returnParts[#returnParts + 1] = GetPlayerKills()
helpers.send_udp(udpAddress, table.concat(returnParts, "\n"), serverIndex)
SendChunked(table.concat(returnParts, "\n"))
end
end
@@ -274,25 +264,25 @@ function SendAll(event)
end
if options.enableMod == true then
local interval = math.max(1, math.floor(tickInterval / 10))
local interval = math.max(1, math.floor(options.tickInterval / 10))
if event.tick % interval ~= 0 then return end
sendIndex = (sendIndex % 10) + 1
if sendIndex == 1 then SendProductionStats() end
if sendIndex == 2 then SendPollutionStats() end
if sendIndex == 3 then
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
SendKillStats()
SendPlayerEntityStats()
end
if sendIndex == 4 then SendFluidProductionStats() end
if sendIndex == 5 then SendBuildStats() end
if sendIndex == 6 then
if options.senderIndex == 4 then SendFluidProductionStats() end
if options.senderIndex == 5 then SendBuildStats() end
if options.senderIndex == 6 then
SendResearchStats()
SendCombinatorMetrics()
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
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
end
end
@@ -311,6 +301,7 @@ function UpdateStorage(event)
end
end
function RemoveStorage(event)
if not event then return end
if event.entity.type == "lab" then
@@ -325,11 +316,8 @@ function RemoveStorage(event)
RemoveGenerator(event)
end
--log(event.entity.name)
if event.entity.name == "crash-site-spaceship" then
--log(event.name)
if event.name == defines.events.on_player_mined_entity then
--log("in ban call")
if settings.global["factorio-metrics-exporter-enable_denkmalschutz"].value == true then
game.ban_player(event.player_index, "You violated the rules of DENKMALSCHUTZ!!!")
game.kick_player(event.player_index, "")
@@ -343,10 +331,11 @@ function RemoveStorage(event)
end
end
function CreateEntity(event)
if not event then return end
--Event is PlayerPlaced
if event.name == defines.events.on_built_entity then
if event.entity.name ~= "entity-ghost"
@@ -358,7 +347,6 @@ function CreateEntity(event)
end
end
--Event is RobotPlaced
if event.name == defines.events.on_robot_built_entity then
if event.entity.name ~= "entity-ghost"
@@ -373,7 +361,6 @@ function CreateEntity(event)
end
end
--Event is spaceplatform build
if event.name == defines.events.on_space_platform_built_entity then
if event.entity.name ~= "entity-ghost"
@@ -395,7 +382,7 @@ function RemoveEntity(event)
if event.name == defines.events.on_player_mined_entity then
if event.entity.name ~= "entity-ghost"
and event.entity.name ~= "tile-ghost"
and event.entity.name ~= "deconstructible_tile_proxy" then
and event.entity.name ~= "deconstructible-tile-proxy" then
storage.deconstructedEntities[event.player_index] = storage.deconstructedEntities[event.player_index] or {}
storage.deconstructedEntities[event.player_index][event.entity.name] = (storage.deconstructedEntities[event.player_index][event.entity.name] or 0) +
1
@@ -405,7 +392,7 @@ function RemoveEntity(event)
if event.name == defines.events.on_robot_mined_entity then
if event.entity.name ~= "entity-ghost"
and event.entity.name ~= "tile-ghost"
and event.entity.name ~= "deconstructible_tile_proxy" then
and event.entity.name ~= "deconstructible-tile-proxy" then
if event.entity.last_user then
local lastUser = event.entity.last_user.index
storage.deconstructedEntities[lastUser] = storage.deconstructedEntities[lastUser] or {}
@@ -418,7 +405,7 @@ function RemoveEntity(event)
if event.name == defines.events.on_space_platform_mined_entity then
if event.entity.name ~= "entity-ghost"
and event.entity.name ~= "tile-ghost"
and event.entity.name ~= "deconstructible_tile_proxy" then
and event.entity.name ~= "deconstructible-tile-proxy" then
if event.entity.last_user then
local lastUser = event.entity.last_user.index
storage.deconstructedEntities[lastUser] = storage.deconstructedEntities[lastUser] or {}
@@ -429,8 +416,9 @@ function RemoveEntity(event)
end
if event.name == defines.events.on_entity_died then
CheckReactor(event)
end
CheckReactor(event)
RemoveStorage(event)
onMetricsCombinatorDied(event)
onMetricsCombinatorMined(event)
@@ -438,13 +426,6 @@ end
script.on_event(defines.events.on_tick, SendAll)
--Script hooks for power and lab stats
--script.on_event(defines.events.on_built_entity,UpdateStorage,{{filter = "type", type = "electric-pole"},{filter ="type", type="lab"}})
--script.on_event(defines.events.on_player_mined_entity, RemoveStorage,{{filter = "type", type = "electric-pole"},{filter ="type", type="lab"},{filter = "type", type="container"}})
--script.on_event(defines.events.on_robot_built_entity,UpdateStorage,{{filter = "type", type = "electric-pole"},{filter ="type", type="lab"}})
--script.on_event(defines.events.on_robot_mined_entity,RemoveStorage,{{filter = "type", type = "electric-pole"},{filter ="type", type="lab"},{filter = "type", type="container"}})
--script.on_event(defines.events.on_entity_died,RemoveStorage,{{filter = "type", type = "electric-pole"},{filter ="type", type="lab"},{filter = "type", type="container"}})
script.on_event(defines.events.on_built_entity, CreateEntity)
script.on_event(defines.events.on_robot_built_entity, CreateEntity)
script.on_event(defines.events.on_space_platform_built_entity, CreateEntity)
@@ -459,6 +440,3 @@ script.on_event(defines.events.on_gui_opened, onGuiOpened)
script.on_event(defines.events.on_gui_click, onGuiClick)
script.on_event(defines.events.on_gui_closed, onClosedCombinatorGui)
script.on_event(defines.events.on_gui_text_changed, onGuiTextChanged)
script.on_event(defines.events.on_cargo_pod_delivered_cargo, onCargoDelivered)
script.on_event(defines.events.on_rocket_launched, onCargoDelivered)

View File

@@ -5,7 +5,7 @@ function GetMods()
for k, v in pairs(mods) do
modstring = modstring .. ("%s:%s\n"):format(k, v)
end
helpers.send_udp(udpAddress, modstring, serverIndex)
SendChunked(modstring)
end
function GetPlayerColors()
@@ -33,24 +33,24 @@ function GetPlayerKills()
end
function SendPlayerEntityStats()
local entityParts = {}
entityParts[#entityParts + 1] = "---player-build-stats---"
for playerIndex, items in pairs(storage.constructedEntites) do
local playerName = game.players[playerIndex].name
for itemName, itemCount in pairs(items) do
entityParts[#entityParts + 1] = ("%s:%s:constructed:%s:%s"):format(playerIndex, playerName, itemName, itemCount)
local entityParts = {}
entityParts[#entityParts + 1] = "---player-build-stats---"
for playerIndex, items in pairs(storage.constructedEntites) do
local playerName = game.players[playerIndex].name
for itemName, itemCount in pairs(items) do
entityParts[#entityParts + 1] = ("%s:%s:constructed:%s:%s"):format(playerIndex, playerName, itemName, itemCount)
end
end
end
helpers.send_udp(udpAddress,table.concat(entityParts,"\n"),serverIndex)
entityParts = {}
entityParts[#entityParts + 1] = "---player-build-stats---"
for playerIndex, items in pairs(storage.deconstructedEntities) do
local playerName = game.players[playerIndex].name
for itemName, itemCount in pairs(items) do
entityParts[#entityParts + 1] = ("%s:%s:deconstructed:%s:%s"):format(playerIndex, playerName, itemName, itemCount)
SendChunked("---player-build-stats---\n" .. table.concat(entityParts,"\n"))
entityParts = {}
entityParts[#entityParts + 1] = "---player-build-stats---"
for playerIndex, items in pairs(storage.deconstructedEntities) do
local playerName = game.players[playerIndex].name
for itemName, itemCount in pairs(items) do
entityParts[#entityParts + 1] = ("%s:%s:deconstructed:%s:%s"):format(playerIndex, playerName, itemName, itemCount)
end
end
end
helpers.send_udp(udpAddress,table.concat(entityParts,"\n"),serverIndex)
SendChunked("---player-build-stats---\n" .. table.concat(entityParts,"\n"))
end
function GetMapSeed()
@@ -86,7 +86,7 @@ function GetPlayerDeaths()
end
function onPlayerDeath(event)
helpers.send_udp(udpAddress,("---player-died---\n%s:%s:%d"):format(event.player_index,game.players[event.player_index].name,event.tick),serverIndex)
SendChunked(("---player-died---\n%s:%s:%d"):format(event.player_index,game.players[event.player_index].name,event.tick))
end
function GetPlayerDeathCauses()

View File

@@ -34,5 +34,7 @@ factorio-metrics-exporter-autotrain_depot_name=Set the name of the depot you wan
factorio-metrics-exporter-export_train_trips=Enable sending of train trup statistics.
[item-name]
metrics-combinator=Metrics combinator
[entity-name]
metrics-combinator=Metrics combinator
[item-description]
metrics-combinator=Connect this item to a circuit network, set a name and check the enable checkbox to export the values of this circuit network

View File

@@ -39,6 +39,6 @@ function SendLogisticStats()
log("Table size logistics "..table_size(returnParts))
log("Sending logistics")
--local send = GetAllLogisticGrids().."\n"..GetLogisticNetworkContents()
helpers.send_udp(udpAddress,table.concat(returnParts,"\n"),serverIndex)
SendChunked(table.concat(returnParts,"\n"))
end
end

View File

@@ -171,5 +171,5 @@ function SendCombinatorMetrics()
end
end
end
helpers.send_udp(udpAddress, table.concat(netParts, "\n"), serverIndex)
SendChunked(table.concat(netParts, "\n"))
end

47
player-statistics.lua Normal file
View File

@@ -0,0 +1,47 @@
function onPlayerJoin(event)
SendChunked(
"---player-join---\n" ..
("%d:%s"):format(event.player_index,game.players[event.player_index].name)
)
end
function onPlayerLeave(event)
SendChunked(
"---player-leave---\n" ..
("%d:%s"):format(event.player_index,game.players[event.player_index].name)
)
end
function onPlayerDiedDeathCause(event)
if event.cause and event.cause.name then
storage.playerDeathCause[event.player_index] =
storage.playerDeathCause[event.player_index] or {}
storage.playerDeathCause[event.player_index][event.cause.name] =
(storage.playerDeathCause[event.player_index][event.cause.name] or 0) + 1
log(("Player %s died from type %s"):format(game.players[event.player_index].name, event.cause.name))
end
--Log player death count
storage.playerDeathCount[event.player_index] = (storage.playerDeathCount[event.player_index] or 0) + 1
end
function onPlayerKilledPlayer(event)
--Log player cause by player
if event.cause and event.cause.type == "character" then
local killer = event.cause.player
if killer then
local killer_index = killer.index
local victim_index = event.player_index
local killerName = killer.name
local victimName = game.players[victim_index].name
log(("Player ID %d:%s killed player ID %d:%s"):format(killer_index, killerName, victim_index, victimName))
storage.playerKillCount[killer_index] =
storage.playerKillCount[killer_index] or {}
storage.playerKillCount[killer_index][victim_index] =
(storage.playerKillCount[killer_index][victim_index] or 0) + 1
end
end
end

View File

@@ -20,7 +20,7 @@ function SendPollutionStats()
pollutionParts[#pollutionParts + 1] = ("%s:out:%s:%d"):format(surface_name, name, stat)
end
end
helpers.send_udp(udpAddress, table.concat(pollutionParts, "\n"), serverIndex)
SendChunked(table.concat(pollutionParts, "\n"))
end
end
@@ -46,7 +46,7 @@ function SendKillStats()
killParts[#killParts + 1] = ("%s:out:%s:%d"):format(surface_name, name, stat)
end
end
helpers.send_udp(udpAddress, table.concat(killParts, "\n"), serverIndex)
SendChunked(table.concat(killParts, "\n"))
end
end

View File

@@ -2,7 +2,7 @@ function AddPowerPole(event)
local e = event.entity
if e then
storage.representativePoles[e.unit_number] = e
-- Update cache with new network
-- Invalidate cache
storage.networkCache = nil
end
end
@@ -187,6 +187,7 @@ function SendPowerStats()
end
end
powerPart[#powerPart + 1] = possiblePower
helpers.send_udp(udpAddress, table.concat(powerPart, "\n"), serverIndex)
--Send("---power-stats---",table.concat(powerPart,"\n"),200)
SendChunked(table.concat(powerPart, "\n"))
end
end

View File

@@ -48,7 +48,7 @@ function SendProductionStats()
productionParts[#productionParts+1] = ("%s:out:%s:%d"):format(surfaceName, itemName, itemCount)
end
end
helpers.send_udp(udpAddress, table.concat(productionParts, "\n"), serverIndex)
SendChunked(table.concat(productionParts, "\n"))
end
end
@@ -77,7 +77,7 @@ function SendFluidProductionStats()
productionParts[#productionParts+1] = ("%s:out:%s:%d"):format(surfaceName, itemName, itemCount)
end
end
helpers.send_udp(udpAddress, table.concat(productionParts, "\n"), serverIndex)
SendChunked(table.concat(productionParts, "\n"))
end
end
@@ -105,7 +105,7 @@ function SendBuildStats()
buildParts[#buildParts+1] = ("%s:out:%s:%d"):format(surfaceName, itemName, itemCount)
end
end
helpers.send_udp(udpAddress, table.concat(buildParts, "\n"), serverIndex)
SendChunked(table.concat(buildParts, "\n"))
end
end

View File

@@ -56,28 +56,6 @@ function GetEstimatedResearchTime()
return returnSpeed..returnTime..returnNameID..returnProgress..returnCost
end
function UpdateLabInfos()
local totalLabs = 0
local totalSpeed = 0
totalLabs = #storage.labs
for _, lab in pairs(storage.labs) do
if lab.valid then
if lab.status == defines.entity_status.working then
local labBase
if lab.name == "biolab" then
labBase = biolabBaseSpeed
else
labBase = labBaseSpeed
end
local labSpeed = (labBase + (labBase * game.forces["player"].laboratory_speed_modifier)) * (lab.effects.speed or 1)
totalSpeed = totalSpeed + (labSpeed* (1+(lab.effects.productivity or 0)))
end
end
end
storage.totalLabCount = totalLabs
storage.totalResearchSpeed = totalSpeed
end
function GetCurrentResearchSpeed()
local totalResearch = 0
local playerForce = game.forces["player"]
@@ -92,7 +70,7 @@ function SendResearchStats()
if options.enableResearch == true then
local researchTimeInfo = GetEstimatedResearchTime()
if researchTimeInfo then
helpers.send_udp(udpAddress, researchTimeInfo, serverIndex)
SendChunked(researchTimeInfo)
end
end
end

164
send-utils.lua Normal file
View File

@@ -0,0 +1,164 @@
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: ---<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
---@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 (---<headerline>---), 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

View File

@@ -17,7 +17,7 @@ data:extend({
{
type = "int-setting",
name = "factorio-metrics-exporter-udp-port",
setting_type = "startup",
setting_type = "runtime-global",
allow_blank = false,
default_value = 52555,
order = "c"
@@ -112,5 +112,13 @@ data:extend({
setting_type = "runtime-global",
default_value = false,
order = "zz"
},
{
type = "int-setting",
name = "factorio-metrics-exporter-sending-player-index",
setting_type = "runtime-global",
default_value = 0,
minimum_value = 0,
order = "aa"
}
})

View File

@@ -272,13 +272,13 @@ function GetTrainTripStats()
end
if #tripParts > 400 then
log("Sending at " .. tripCount .. " trips")
helpers.send_udp(udpAddress, table.concat(tripParts, "\n"), serverIndex)
SendChunked(table.concat(tripParts, "\n"))
tripParts = {}
tripParts[#tripParts + 1] = "---train-trips---\n"
end
end
--tripParts[#tripParts+1] = "--train-fin--"
helpers.send_udp(udpAddress, table.concat(tripParts, "\n"), serverIndex)
SendChunked(table.concat(tripParts, "\n"))
log("Counted " .. tripCount .. " trips")
--return table.concat(tripParts,"\n")
end
@@ -326,6 +326,6 @@ function SendTrainStats()
GetTrainTripStats()
end
log("Sending Train statistics")
helpers.send_udp(udpAddress, table.concat(returnParts, "\n"), serverIndex)
SendChunked(table.concat(returnParts, "\n"))
end
end