Private
Public Access
1
0
Files
lua-prometheus-exporter/train-stats.lua
2026-01-10 18:24:52 +01:00

332 lines
11 KiB
Lua

function ScanTrains()
storage.trains = {}
for _, train in pairs(game.train_manager.get_trains({})) do
storage.trains[train.id] = train
end
end
---comment Get the first locomotive in the front, if non, in the back, if none empty string
---@param train LuaTrain
function GetTrainName(train)
if train.locomotives.front_movers[1] then
return train.locomotives.front_movers[1].backer_name or ""
elseif train.locomotives.back_movers[1] then
return train.locomotives.back_movers[1].backer_name or ""
end
return ""
end
function GetTrainsInDepot()
local trainsInDepot = 0
for trainsID, train in pairs(storage.trains) do
if train.state == defines.train_state.wait_station
and train.station and train.station.backer_name == autotrainDepotName then
trainsInDepot = trainsInDepot + 1
end
end
local trainsInGroup = #game.train_manager.get_trains({ group = autotrainGroupName })
return ("---autotrain-stats---\n%d:%d"):format(trainsInDepot, trainsInGroup)
end
function GetTrainPlayerKills()
local trainKills = {}
trainKills[#trainKills + 1] = "---train-player-kills---"
---@type LuaTrain
for _, train in pairs(storage.trains) do
for killedPlayerID, killedPlayerCount in pairs(train.killed_players) do
trainKills[#trainKills + 1] = ("%s:%s:%s:%s:%d"):format(train.id, GetTrainName(train), killedPlayerID,
game.players[killedPlayerID].name, killedPlayerCount)
end
end
return table.concat(trainKills, "\n")
end
function GetTrainTotalKills()
local trainKills = {}
trainKills[#trainKills + 1] = "---train-total-kills---"
---@type LuaTrain
for _, train in pairs(storage.trains) do
trainKills[#trainKills + 1] = ("%s:%s:%d"):format(train.id, GetTrainName(train), train.kill_count)
end
return table.concat(trainKills, "\n")
end
--TODO: Seperate by surface
function GetTrainStates()
local trainsWaiting = {}
local trainsDriving = {}
local trainsManual = {}
local trainsProblems = {}
for _, train in pairs(storage.trains) do
local surfaceName
if train.locomotives then
if train.locomotives.front_movers[1] then
surfaceName = train.locomotives.front_movers[1].surface.name
end
elseif train.locomotives.back_movers[1] then
surfaceName = train.locomotives.back_movers[1].surface.name
end
if surfaceName then
if train.state == defines.train_state.wait_station
or train.state == defines.train_state.destination_full
or train.state == defines.train_state.no_schedule
then
trainsWaiting[surfaceName] = (trainsWaiting[surfaceName] or 0) + 1
elseif train.state == defines.train_state.on_the_path
or train.state == defines.train_state.arrive_signal
or train.state == defines.train_state.arrive_station
or train.state == defines.train_state.wait_signal
then
trainsDriving[surfaceName] = (trainsDriving[surfaceName] or 0) + 1
elseif train.state == defines.train_state.manual_control
or train.state == defines.train_state.manual_control_stop
then
trainsManual[surfaceName] = (trainsManual[surfaceName] or 0) + 1
elseif train.state == defines.train_state.no_path
then
trainsProblems[surfaceName] = (trainsProblems[surfaceName] or 0) + 1
end
end
end
local stateParts = {}
stateParts[#stateParts + 1] = "---trains-states---\n"
for _, surface in pairs(game.surfaces) do
local surfaceName = surface.name
stateParts[#stateParts + 1] = ("%s:%d:%d:%d:%d"):format(
surfaceName,
trainsDriving[surfaceName] or 0,
trainsManual[surfaceName] or 0,
trainsProblems[surfaceName] or 0,
trainsWaiting[surfaceName] or 0)
end
return table.concat(stateParts, "\n")
end
---@class trainStat
trainStat = {
trainID = 0,
trainName = "",
lastInventory = {},
currentInventory = {},
lastState = 0,
lastStationUnitNumber = 0,
currentStationUnitNumber = 0,
totalCargoCount = 0,
totalCargo = {},
lastArrivalTime = 0,
currentArrivalTime = 0,
---@type trip
trips = {}
}
---@class trip
trip = {
startStation = {},
endStation = {},
timeTaken = 0,
tick = 0
}
---@param inv table[]
---@return table<string, table>
local function toLookup(inv)
local t = {}
for _, item in ipairs(inv) do
local key = item.name .. ":" .. (item.quality or 0) -- eindeutiger Key
t[key] = item.count
end
return t
end
---@param oldInv table[]
---@param newInv table[]
---@return table[]
local function inventoryDiff(oldInv, newInv)
local oldLookup = toLookup(oldInv)
local newLookup = toLookup(newInv)
local diff = {}
-- Items, die neu hinzugekommen oder verändert wurden
for key, newCount in pairs(newLookup) do
local oldCount = oldLookup[key] or 0
if newCount ~= oldCount then
local name, quality = key:match("([^:]+):([^:]+)")
table.insert(diff, {
name = name,
quality = tonumber(quality),
oldCount = oldCount,
newCount = newCount,
delta = newCount - oldCount
})
end
end
-- Items, die komplett entfernt wurden
for key, oldCount in pairs(oldLookup) do
if newLookup[key] == nil then
local name, quality = key:match("([^:]+):([^:]+)")
table.insert(diff, {
name = name,
quality = tonumber(quality),
oldCount = oldCount,
newCount = 0,
delta = -oldCount
})
end
end
return diff
end
function onTrainStateChange(event)
-- Train arrived at station, so we store current data
---@type LuaTrain
local train = event.train
local trainID = train.id
---@type trainStat
local stat = storage.trainStats[trainID] or {}
stat.trips = stat.trips or {}
stat.trainID = trainID
stat.trainName = GetTrainName(train)
if event.train.state == defines.train_state.wait_station then
if not train.station then return end
if train.station.unit_number == stat.lastStationUnitNumber then return end
stat.lastStationUnitNumber = stat.currentStationUnitNumber
stat.lastInventory = stat.currentInventory
stat.lastArrivalTime = stat.currentArrivalTime
stat.currentStationUnitNumber = train.station.unit_number
stat.currentInventory = train.get_contents()
stat.currentArrivalTime = game.tick
if stat.lastStationUnitNumber
and stat.currentStationUnitNumber
and (stat.lastStationUnitNumber ~= stat.currentStationUnitNumber) then
local tripIdentifier = tostring(stat.lastStationUnitNumber) .. tostring(stat.currentStationUnitNumber)
stat.trips[tripIdentifier] = {
startStation = game.get_entity_by_unit_number(stat.lastStationUnitNumber),
endStation = game.get_entity_by_unit_number(stat.currentStationUnitNumber),
timeTaken = stat.currentArrivalTime - stat.lastArrivalTime,
time = game.tick
}
end
if stat.currentInventory and stat.lastInventory then
--Get Total Cargo
for key, value in pairs(inventoryDiff(stat.lastInventory, stat.currentInventory)) do
stat.totalCargoCount = (stat.totalCargoCount or 0) + math.abs(value.delta)
end
end
storage.trainStats[trainID] = stat
end
--log("inEvent")
end
--Checks if trip is still valid by checking of st
function isTripValid(trip, tripID, trainID)
if trip.startStation == nil
or trip.endStation == nil
or trip.startStation.valid == false
or trip.endStation.valid == false then
--One station is nil so we delete this trip
log("Deleting trip" .. tripID)
storage.trainStats[trainID].trips[tripID] = nil
return false
end
--Stations are valid, so we true
return true
end
function SortTrips()
for trainID, stat in pairs(storage.trainStats) do
table.sort(stat.trips, function(a, b)
return a.tick > b.tick
end)
end
end
function GetTrainTripStats()
--SortTrips()
local tripParts = {}
local tripCount = 0
tripParts[#tripParts + 1] = "---train-trips---\n"
for trainID, stats in pairs(storage.trainStats) do
for tripIndex, trip in pairs(stats.trips or {}) do
if isTripValid(trip, tripIndex, trainID) then
tripParts[#tripParts + 1] =
("%d:%s:%s:%s:%s:%d"):format(
trainID,
stats.trainName,
trip.startStation.surface.name,
trip.startStation.backer_name,
trip.endStation.backer_name,
trip.timeTaken)
tripCount = tripCount + 1
end
--To clean up, we delete this trip now
stats.trips[tripIndex] = nil
end
if #tripParts > 400 then
log("Sending at " .. tripCount .. " trips")
SendChunked(table.concat(tripParts, "\n"))
tripParts = {}
tripParts[#tripParts + 1] = "---train-trips---\n"
end
end
--tripParts[#tripParts+1] = "--train-fin--"
SendChunked(table.concat(tripParts, "\n"))
log("Counted " .. tripCount .. " trips")
--return table.concat(tripParts,"\n")
end
function GetTrainStatistics()
local trainParts = {}
trainParts[#trainParts + 1] = "---train-total-statistics---\n"
for trainID, stat in pairs(storage.trainStats) do
trainParts[#trainParts + 1] = ("%d:%s:%d"):format(trainID, stat.trainName, stat.totalCargoCount or 0)
end
return table.concat(trainParts, "\n")
end
---Purges stats for trains that no longer exist
---@param trainID integer|nil If provided, purges only that train. If nil, purges all dead trains.
function PurgeDeadTrainStats(trainID)
if trainID then
-- Purge a specific train if it doesn't exist
if not storage.trains[trainID] then
storage.trainStats[trainID] = nil
return true
end
else
-- Purge all trains not in storage.trains
for statTrainID in pairs(storage.trainStats) do
if not storage.trains[statTrainID] then
storage.trainStats[statTrainID] = nil
end
end
end
return false
end
function SendTrainStats()
if options.enableTrains then
ScanTrains()
PurgeDeadTrainStats()
local returnParts = {}
returnParts[#returnParts + 1] = GetTrainPlayerKills()
returnParts[#returnParts + 1] = GetTrainTotalKills()
returnParts[#returnParts + 1] = GetTrainStates()
returnParts[#returnParts + 1] = GetTrainStatistics()
returnParts[#returnParts + 1] = GetTrainsInDepot()
if options.enableTrainTrips then
GetTrainTripStats()
end
log("Sending Train statistics")
SendChunked(table.concat(returnParts, "\n"))
end
end