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 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