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 function GetTrainStates() local trainsDriving = 0 local trainsWaiting = 0 local trainsProblems = 0 local trainsManual = 0 for _, train in pairs(storage.trains) do 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 = trainsWaiting + 1 elseif train.state == defines.train_state.on_the_path or train.state == defines.train_state.arrive_signal or train.state == defines.train_state.wait_signal then trainsDriving = trainsDriving + 1 elseif train.state == defines.train_state.manual_control or train.state == defines.train_state.manual_control_stop then trainsManual = trainsManual + 1 elseif train.state == defines.train_state.no_path then trainsProblems = trainsProblems + 1 end end return ("---trains-states---\n%d:%d:%d:%d"):format(trainsDriving,trainsManual,trainsProblems,trainsWaiting) 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 } ---@param inv table[] ---@return 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 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 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} 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 function GetTrainTripStats() local tripParts = {} tripParts[#tripParts+1] = "---train-trips---\n" for trainID,stats in pairs(storage.trainStats) do for _,trip in pairs(stats.trips or {}) do tripParts[#tripParts+1] = ("%d:%s:%s:%s:%d"):format( trainID, stats.trainName, trip.startStation.backer_name, trip.endStation.backer_name, trip.timeTaken) end end 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() returnParts[#returnParts+1] = GetTrainTripStats() helpers.send_udp(udpAddress,table.concat(returnParts,"\n"),serverIndex) end end