diff --git a/.gitignore b/.gitignore index 6f36858..381f93b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -factorio-metrics-exporter_0.1.3.zip +factorio-metrics-exporter_*.zip +.vscode/ \ No newline at end of file diff --git a/control.lua b/control.lua index 67bb5a3..0cb38b1 100644 --- a/control.lua +++ b/control.lua @@ -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) diff --git a/game-stats.lua b/game-stats.lua index 0f5017e..75d78a8 100644 --- a/game-stats.lua +++ b/game-stats.lua @@ -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() diff --git a/locale/en/locale.cfg b/locale/en/locale.cfg index 42ac74a..3baafa0 100644 --- a/locale/en/locale.cfg +++ b/locale/en/locale.cfg @@ -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 \ No newline at end of file diff --git a/logistic-network-stats.lua b/logistic-network-stats.lua index 9790ba6..d5333fd 100644 --- a/logistic-network-stats.lua +++ b/logistic-network-stats.lua @@ -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 diff --git a/metrics-combinator.lua b/metrics-combinator.lua index 484594a..c7e3e87 100644 --- a/metrics-combinator.lua +++ b/metrics-combinator.lua @@ -171,5 +171,5 @@ function SendCombinatorMetrics() end end end - helpers.send_udp(udpAddress, table.concat(netParts, "\n"), serverIndex) + SendChunked(table.concat(netParts, "\n")) end diff --git a/player-statistics.lua b/player-statistics.lua new file mode 100644 index 0000000..a9e3cd3 --- /dev/null +++ b/player-statistics.lua @@ -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 \ No newline at end of file diff --git a/pollution-stats.lua b/pollution-stats.lua index e32471f..38039d5 100644 --- a/pollution-stats.lua +++ b/pollution-stats.lua @@ -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 diff --git a/power-stats.lua b/power-stats.lua index a1cb976..8b9e908 100644 --- a/power-stats.lua +++ b/power-stats.lua @@ -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 diff --git a/production-stats.lua b/production-stats.lua index 9364ec4..3a1fcc8 100644 --- a/production-stats.lua +++ b/production-stats.lua @@ -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 diff --git a/research-stats.lua b/research-stats.lua index ba0797e..e387993 100644 --- a/research-stats.lua +++ b/research-stats.lua @@ -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 \ No newline at end of file diff --git a/send-utils.lua b/send-utils.lua new file mode 100644 index 0000000..cf7902a --- /dev/null +++ b/send-utils.lua @@ -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: ------) +---@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 diff --git a/settings.lua b/settings.lua index 023533a..2930872 100644 --- a/settings.lua +++ b/settings.lua @@ -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" } }) \ No newline at end of file diff --git a/train-stats.lua b/train-stats.lua index fd086bb..038304e 100644 --- a/train-stats.lua +++ b/train-stats.lua @@ -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