Private
Public Access
1
0

2 Commits

Author SHA1 Message Date
Jan Grießhaber
0abc89d730 Refactor send logic to use senderPlayerIndex and optimize content chunking 2026-01-11 20:33:48 +01:00
Jan Grießhaber
2e3ea7dd4d Rewritten sending helper and logic 2026-01-10 18:24:52 +01:00
14 changed files with 264 additions and 141 deletions

3
.gitignore vendored
View File

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

View File

@@ -6,20 +6,26 @@ 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
isInitialized = false
sendIndex = 0
serverIndex = 0
scannedGrids = false
scannedLabs = false
scannedGenerators = false
autotrainGroupName = ""
autotrainDepotName = ""
sendIndex = 0
options = {
---@type integer
udpPort = 52555,
---@type integer
senderPlayerIndex = 0,
---@type integer
tickInterval = 300,
enableMod = false,
enablePlayers = false,
enableProduction = false,
@@ -35,12 +41,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 +60,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.senderPlayerIndex = 0
options.enableMod = settings.global["factorio-metrics-exporter-enable"].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
options.enableFluid = settings.global["factorio-metrics-exporter-export_fluid_stats"].value
@@ -84,10 +88,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.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
options.enableFluid = settings.global["factorio-metrics-exporter-export_fluid_stats"].value
@@ -103,13 +109,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 +129,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 +141,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.senderPlayerIndex = 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 +190,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 +210,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 +228,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,7 +262,7 @@ 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
@@ -311,6 +299,7 @@ function UpdateStorage(event)
end
end
function RemoveStorage(event)
if not event then return end
if event.entity.type == "lab" then
@@ -325,11 +314,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 +329,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 +345,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 +359,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 +380,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 +390,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 +403,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 +414,9 @@ function RemoveEntity(event)
end
if event.name == defines.events.on_entity_died then
end
CheckReactor(event)
end
RemoveStorage(event)
onMetricsCombinatorDied(event)
onMetricsCombinatorMined(event)
@@ -438,13 +424,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 +438,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()
@@ -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
helpers.send_udp(udpAddress,table.concat(entityParts,"\n"),serverIndex)
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
helpers.send_udp(udpAddress,table.concat(entityParts,"\n"),serverIndex)
prof.stop()
game.print(prof)
SendChunked(table.concat(resultParts, "\n"))
end
function GetMapSeed()
@@ -65,40 +77,40 @@ 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)
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()
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()

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

@@ -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
@@ -171,5 +175,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

94
send-utils.lua Normal file
View File

@@ -0,0 +1,94 @@
local function SendUDP(content)
if content then
helpers.send_udp(options.udpPort, content, options.senderPlayerIndex)
else
error("Missing udpPort or Content")
end
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
---@return table chunks Array of {header=string|nil, content=string}---@param content string
---@param maxSizekB number|nil Default 200KB
local function ChunkContent(content, maxSizekB)
local maxSize = (maxSizekB or 200) * 1024
local len = #content
local chunks = {}
-- 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
while headerIdx <= #headers and headers[headerIdx].startPos <= start do
activeHeaderText = headers[headerIdx].text
headerIdx = headerIdx + 1
end
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
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
-- 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
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
---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
-- 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
if chunk.header then
SendUDP(chunk.header .. "\n" .. chunk.content)
else
SendUDP(chunk.content)
end
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