-- AD Flow Wagons -- Flows ejer deres vogne, men traktor og vogn er ikke låst sammen: den vogn der hænger på en flow-traktor huskes -- (run.wagons = {[traktorId] = vognId}). Starter flowet med en solo-traktor, henter den sin vogn ved "Connect ". Stoppes/slettes flowet, eller er dets bygning væk, sættes vognen på plads og traktoren kører hjem — -- så er begge ledige i maskinpuljen (en stoppet flows vogn må lånes; en kørende flows aldrig). ADFlowWagons = {} ADFlowWagons.LOG_PREFIX = "ADSmartPickup: flow-vogne: " ADFlowWagons.REMEMBER_MS = 10000 -- husk vogne / prøv ventende starter igen ADFlowWagons.BUILDING_CHECK_MS = 60000 ADFlowWagons.MISSING_CHECKS = 2 -- så mange tjek i træk før en bygning regnes for væk -- ---- ren logik ---- ADFlowWagons.serialize = function(wagons) return ADRuns.serializeWagons(wagons) end ADFlowWagons.parse = function(text) return ADRuns.parseWagons(text) end -- Husk traktorens vogn: ny tabel ved ændring, samme tabel når intet ændres. function ADFlowWagons.remember(wagons, tractorId, wagonId) if (wagons or {})[tractorId] == wagonId then return wagons end local copy = ADRuns.copyWagons(wagons) copy[tractorId] = wagonId return copy end -- Må redskabet bag traktoren huskes som flowets vogn? Kun noget der kan rumme last — ikke plov/kultivator (ingen -- plads) og ikke såmaskine (frøtank). s = {capacity, isSowingMachine}. in-game 30/9: 'Sell Slurry 2' huskede ploven. function ADFlowWagons.isRememberableWagon(s) return (s.capacity or 0) > 0 and not s.isSowingMachine end -- s = {hasRear, wagonId, wagonExists, hasConnect, wagonAttached, wagonBusy} -> "start" | "fetch" | "wait" function ADFlowWagons.startAction(s) -- lånt ud af puljen (markarbejde, marksilo, på hjemtur): vent til den er fri if s.tractorBusy then return "wait" end if s.hasRear or s.wagonId == nil or not s.wagonExists or not s.hasConnect then return "start" end if s.wagonAttached or s.wagonBusy then return "wait" end return "fetch" end -- Hent vogn fejlede: ét nyt forsøg når AD standsede undervejs (forbigående, fx spærret af en anden traktor); -- ikke ved kobling der fejler ved markøren (den skal rettes). ADFlowWagons.FETCH_RETRIES = 1 function ADFlowWagons.retryFetch(reason, retries) return tostring(reason):find("^stoppedAway") ~= nil and (retries or 0) < ADFlowWagons.FETCH_RETRIES end -- s = {flowRunning, ownWagon, adActive} -> start traktoren efter spillerens egen påkobling? function ADFlowWagons.startAfterManualAttach(s) return s.flowRunning == true and s.ownWagon == true and not s.adActive end -- Husk vognen bag traktoren ved hjemtur? Kun når et KØRENDE flow stoppes (reason "stop") — ved oprydning efter -- indlæsning ("afterLoad") kan traktoren have et puljeredskab på fra et lån (in-game 29/9). function ADFlowWagons.shouldRememberOnHome(reason) return reason == "stop" end -- Påkobling uden ejer (hentet før genindlæsning, markopgaven tabt): s = {owned, idleMs} -> "home"|"wait"|"none" ADFlowWagons.ORPHAN_MS = 120000 -- s.atOwnPark: traktoren holder på sin egen parkering = parkeret sæt, ikke forældreløs (vinmarks-REX, 29/9) ADFlowWagons.OWN_PARK_M = 30 function ADFlowWagons.orphanAction(s) if s.owned or s.atOwnPark then return "none" end return (s.idleMs or 0) >= ADFlowWagons.ORPHAN_MS and "home" or "wait" end -- Hvorfor "wait" (til loggen). s som startAction. function ADFlowWagons.waitReason(s) if s.tractorBusy then return "tractorBusy" end if s.wagonAttached then return "wagonAttached" end return "wagonReserved" end -- Ingen husket vogn: gæt ud fra parkeringsnavnet — en ledig vogn hvis parkering indeholder flowets navn, helst med -- traktorens nummer til sidst ("Cow Food 3" -> "P Cow Food vogn 3"). candidates = {{id, parkName}}, taken = {[id]=true} function ADFlowWagons.guessWagon(runName, tractorNumber, candidates, taken) local wanted = tostring(runName or ""):lower() if wanted == "" then return nil end local matching = {} for _, candidate in ipairs(candidates or {}) do local park = tostring(candidate.parkName or ""):lower() if not (taken or {})[candidate.id] and park:find(wanted, 1, true) ~= nil then table.insert(matching, candidate) end end table.sort(matching, function(a, b) return tostring(a.parkName) < tostring(b.parkName) end) for _, candidate in ipairs(matching) do if tractorNumber ~= nil and tostring(candidate.parkName):match("%s(%d+)$") == tostring(tractorNumber) then return candidate.id end end return matching[1] ~= nil and matching[1].id or nil end -- Ledige vogne til flow-guiden, bedste først: parkering med flowets navn, så "Disponibel", så resten (alfabetisk). -- candidates = {{id, label}} -> ny sorteret liste. function ADFlowWagons.rankWagons(runName, candidates) local wanted = tostring(runName or ""):lower() local function rank(wagon) local label = tostring(wagon.label or ""):lower() if wanted ~= "" and label:find(wanted, 1, true) ~= nil then return 1 end if label:find("disponibel", 1, true) ~= nil then return 2 end return 3 end local ranked = {} for _, wagon in ipairs(candidates or {}) do table.insert(ranked, wagon) end table.sort(ranked, function(a, b) local ra, rb = rank(a), rank(b) if ra ~= rb then return ra < rb end return tostring(a.label) < tostring(b.label) end) return ranked end function ADFlowWagons.ownerOf(runs, wagonId) for _, run in ipairs(runs or {}) do for _, id in pairs(run.wagons or {}) do if id == wagonId then return run end end end return nil end -- Holdes vognen af et flow? Flowets vogn er flowets, også når flowet er stoppet — puljen, markarbejde og -- marksilo må ikke låne den (in-game 29/9: marksilo lånte Cow Food-traktoren med vognen på efter stop). function ADFlowWagons.isHeld(runs, wagonId) return ADFlowWagons.ownerOf(runs, wagonId) ~= nil end -- Et tjek af om bygningen mangler: -> ny tæller, væk? function ADFlowWagons.missingStep(count, missingNow) if not missingNow then return 0, false end local next = (count or 0) + 1 return next, next >= ADFlowWagons.MISSING_CHECKS end -- Flowets bygnings-id'er der ikke findes i exists = {[id] = true}. Kun bygninger der er set i denne session -- (seen) tæller: et forældet id i et gammelt flow må ikke stoppe et flow der virker. function ADFlowWagons.missingBuildings(run, exists, seen) local missing = {} for _, key in ipairs({"sourceBuildingIds", "targetBuildingIds", "storeBuildingIds"}) do for _, id in ipairs(run[key] or {}) do if not exists[id] and (seen or {})[id] then table.insert(missing, id) end end end return missing end -- Hvor læsset afleveres ved stop: AD's 2. markør kun hvis den er et af flowets mål — den kan være forældet -- (gammel AD-tilstand) eller et Wait-punkt, Wait-logikken har skiftet til. Ellers flowets første mål; nil = silo. function ADFlowWagons.emptyTarget(secondMarkerId, runTargetIds) local targets = runTargetIds or {} for _, id in ipairs(targets) do if id == secondMarkerId then return id end end return targets[1] end -- Tøm vognen før den parkeres (flowet stoppet): flowets mål -> silo/egen fabrik -> hjem. -- stage: "start" | "target" | "dump"; s = {cargo, hasTarget, adActive, elapsedMs} -- -> "home" | "target" | "dump" | "wait" | "giveUp" ADFlowWagons.EMPTY_MIN_LITERS = 100 ADFlowWagons.EMPTY_TIMEOUT_MS = 600000 ADFlowWagons.EMPTY_STALL_MS = 45000 -- ved målet uden at læsset falder så længe = målet er fuldt ADFlowWagons.EMPTY_NEAR_M = 40 -- m fra aflæsningsmarkøren tæller som "ved målet" ADFlowWagons.MAX_DUMP_ATTEMPTS = 3 -- siloer der prøves før vognen parkeres med last -- s = {nearTarget, sinceChangeMs} function ADFlowWagons.isStalled(s) return s.nearTarget == true and (s.sinceChangeMs or 0) >= ADFlowWagons.EMPTY_STALL_MS end function ADFlowWagons.emptyStep(stage, s) local cargo = s.cargo or 0 if stage == "start" then if cargo < ADFlowWagons.EMPTY_MIN_LITERS then return "home" end return s.hasTarget and "target" or "dump" end if (s.elapsedMs or 0) >= ADFlowWagons.EMPTY_TIMEOUT_MS then return "giveUp" end if s.stalled and cargo >= ADFlowWagons.EMPTY_MIN_LITERS then if stage == "target" then return "dump" end -- silo der ikke kunne tage imod: prøv den næste (op til MAX_DUMP_ATTEMPTS) return (s.dumpAttempts or 0) < ADFlowWagons.MAX_DUMP_ATTEMPTS and "dump" or "giveUp" end if s.adActive then return "wait" end if cargo < ADFlowWagons.EMPTY_MIN_LITERS then return "home" end if stage == "target" then return "dump" end return "giveUp" end -- ---- spil ---- local waiting = setmetatable({}, {__mode = "k"}) -- traktor -> runId (venter på sin vogn) local fetching = setmetatable({}, {__mode = "k"}) -- traktor -> runId (henter sin vogn) local emptying = setmetatable({}, {__mode = "k"}) -- traktor -> {stage, since, fillType, runName} local homing = setmetatable({}, {__mode = "k"}) -- traktor -> true (flowets hjemtur: vogn af, traktor hjem) local fetchRetries = setmetatable({}, {__mode = "k"}) -- traktor -> antal nye hente-forsøg local lastWaitText = setmetatable({}, {__mode = "k"}) -- traktor -> sidst loggede venteårsag (kun ændringer logges) local orphans = setmetatable({}, {__mode = "k"}) -- traktor -> ms siden påkobling uden ejer local missingCount = {} -- runId -> tjek i træk local seenBuildings = {} -- bygnings-id'er set i denne session local rememberTimer, buildingTimer = 0, 0 local function runs() return ADRunsController.store ~= nil and ADRunsController.store.runs or {} end local function findAnyVehicle(id) for _, vehicle in pairs(g_currentMission.vehicleSystem.vehicles) do if ADRunsController.getVehicleId(vehicle) == id then return vehicle end end return nil end local function connectOf(adEnv, wagon) local index = wagon.advd ~= nil and wagon.advd.parkDestination or nil local park = type(index) == "number" and index >= 1 and adEnv.ADGraphManager:getMapMarkerById(index) or nil if park == nil then return nil end local marker = ADPool.connectFor(park.name, adEnv.ADGraphManager:getMapMarkers()) return marker ~= nil and marker.id or nil end -- Traktorer i stoppede flows er ledige i puljen; traktorer i kørende flows aldrig. -- s = {hasRun, stopped, carriesFlowWagon, homeTrip} -> må puljen/markarbejde/marksilo låne traktoren? function ADFlowWagons.tractorFree(s) if not s.hasRun then return true end return s.stopped == true and not s.carriesFlowWagon and not s.homeTrip end function ADFlowWagons.isFreeTractor(vehicle) local run = ADRunsController.getRunForVehicle(vehicle) local carries = false if run ~= nil then for _, implement in ipairs(ADHitch.rearImplementsOf(vehicle)) do local wagonId = ADRunsController.getVehicleId(implement.object) if wagonId ~= nil and ADFlowWagons.ownerOf(runs(), wagonId) ~= nil then carries = true end end end return ADFlowWagons.tractorFree({hasRun = run ~= nil, stopped = run ~= nil and run.stopped == true, carriesFlowWagon = carries, homeTrip = emptying[vehicle] ~= nil or homing[vehicle] ~= nil}) end -- Holder flowet noget: en traktor henter/venter på sin vogn eller har en vogn bagpå (til Stop-knappen). function ADFlowWagons.hasActivity(run, vehiclesById) for _, vehicleId in ipairs(run.vehicleIds or {}) do local vehicle = vehiclesById ~= nil and vehiclesById[vehicleId] or nil -- vogn bagpå tæller kun i et flow der er startet: et flow der aldrig kom i gang (fx 'ingen vogn', og spilleren -- har selv koblet på) skal vise Start, ikke Stop (in-game 29/9: Milk Store) local holdsWagon = vehicle ~= nil and ADRunsController.runningRuns[run.id] == true and #ADHitch.rearImplementsOf(vehicle) > 0 if vehicle ~= nil and (waiting[vehicle] ~= nil or fetching[vehicle] ~= nil or holdsWagon) then return true end end return false end function ADFlowWagons.hasWagonFor(run, tractorId) return (run.wagons or {})[tractorId] ~= nil end function ADFlowWagons.isHeldWagon(wagonId) return ADFlowWagons.isHeld(runs(), wagonId) end -- Frakoblede vogne med parkering, som intet kørende flow holder, og som flowet ikke allerede har givet en traktor. local function guessFor(adEnv, run, vehicle, tractorId) local taken = {} for _, id in pairs(run.wagons or {}) do taken[id] = true end local candidates = {} local farmId = g_currentMission:getFarmId() for _, any in pairs(g_currentMission.vehicleSystem.vehicles) do local index = any.advd ~= nil and any.advd.parkDestination or nil local attached = any.getAttacherVehicle ~= nil and any:getAttacherVehicle() ~= nil if any.spec_motorized == nil and any.spec_attachable ~= nil and not attached and type(index) == "number" and index >= 1 and any.getOwnerFarmId ~= nil and any:getOwnerFarmId() == farmId then local id = ADRunsController.getVehicleId(any) local park = adEnv.ADGraphManager:getMapMarkerById(index) if id ~= nil and park ~= nil and not ADFlowWagons.isHeld(runs(), id) then table.insert(candidates, {id = id, parkName = park.name}) end end end local number = nil for index, id in ipairs(run.vehicleIds or {}) do if id == tractorId then number = index end end local label = tostring(ADRuns.vehicleLabel(vehicle)) number = tonumber(label:match("%s(%d+)$")) or number local wagonId = ADFlowWagons.guessWagon(run.name, number, candidates, taken) if wagonId ~= nil then Logging.info(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' får vognen ved parkeringen der matcher flowets navn", tostring(run.name), label) end return wagonId end -- Flowet startes igen mens traktoren tømmer/kører hjem efter et stop: afbryd, så den kører videre med sin vogn. -- Puljens egen hjemtur (fra markarbejde) røres ikke — der sidder et andet redskab bagpå. -- s = {emptying, flowHoming, poolReturning} -> true = afbryd function ADFlowWagons.abortHomeTripOnStart(s) return s.emptying == true or s.flowHoming == true end -- Hvilken af isPoolBusy's grunde (til loggen). function ADFlowWagons.busyWhy(vehicle, tractorId) if ADFieldJobs ~= nil and ADFieldJobs.state.assignments[tractorId] ~= nil then return "markopgave" end if ADFieldSilos ~= nil and ADFieldSilos.isLoaned(vehicle) then return "lånt af marksilo" end if ADPool ~= nil and ADPool.isReturning(vehicle) then return "sætter redskab af" end if ADCpBridge ~= nil and ADCpBridge.isWorking(vehicle) then return "Courseplay kører" end if emptying[vehicle] ~= nil then return "tømmer vognen" end return "?" end -- Lånt ud af puljen: markarbejde, marksilo-lån, på hjemtur eller Courseplay kører den. function ADFlowWagons.isPoolBusy(vehicle, tractorId) return (ADFieldJobs ~= nil and ADFieldJobs.state.assignments[tractorId] ~= nil) or (ADFieldSilos ~= nil and ADFieldSilos.isLoaned(vehicle)) or (ADPool ~= nil and ADPool.isReturning(vehicle)) or (ADCpBridge ~= nil and ADCpBridge.isWorking(vehicle)) or (ADFlowWagons.isEmptying ~= nil and ADFlowWagons.isEmptying(vehicle)) end -- Før en flow-traktor startes: hent sin vogn / vent på den. -> håndteret, resultat (som startVehicle) function ADFlowWagons.beforeStart(adEnv, run, vehicle) local tractorId = ADRunsController.getVehicleId(vehicle) if fetching[vehicle] ~= nil then return true, {vehicle = vehicle, ok = true, reason = "fetchingWagon"} end if ADFlowWagons.abortHomeTripOnStart({emptying = emptying[vehicle] ~= nil, flowHoming = homing[vehicle] ~= nil}) then Logging.info(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' startet igen — tømning/hjemtur afbrudt, kører videre med vognen", tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle))) emptying[vehicle] = nil if homing[vehicle] ~= nil then homing[vehicle] = nil ADHitch.cancel(vehicle) end end local wagonId = (run.wagons or {})[tractorId] if wagonId == nil and #ADHitch.rearImplementsOf(vehicle) == 0 then wagonId = guessFor(adEnv, run, vehicle, tractorId) if wagonId ~= nil then run.wagons = ADFlowWagons.remember(run.wagons, tractorId, wagonId) end end local wagon = wagonId ~= nil and findAnyVehicle(wagonId) or nil local connectId = wagon ~= nil and connectOf(adEnv, wagon) or nil local startState = { tractorBusy = ADFlowWagons.isPoolBusy(vehicle, tractorId), hasRear = #ADHitch.rearImplementsOf(vehicle) > 0, wagonId = wagonId, wagonExists = wagon ~= nil, hasConnect = connectId ~= nil, wagonAttached = wagon ~= nil and wagon.getAttacherVehicle ~= nil and wagon:getAttacherVehicle() ~= nil, wagonBusy = wagonId ~= nil and ADPool.isReserved(wagonId), } local action = ADFlowWagons.startAction(startState) if action == "start" then lastWaitText[vehicle] = nil return false end ADRunsController.runningRuns[run.id] = true if action == "wait" then local holder = wagon ~= nil and wagon.getAttacherVehicle ~= nil and wagon:getAttacherVehicle() or nil local waitText = ADFlowWagons.waitReason(startState) .. ": " .. (startState.tractorBusy and ADFlowWagons.busyWhy(vehicle, tractorId) or (holder ~= nil and ("sidder på " .. tostring(ADRuns.vehicleLabel(holder)))) or "reserveret af puljen") if lastWaitText[vehicle] ~= waitText then lastWaitText[vehicle] = waitText Logging.info(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' venter på sin vogn (%s)", tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle)), waitText) end waiting[vehicle] = run.id return true, {vehicle = vehicle, ok = true, reason = "waitingWagon"} end waiting[vehicle] = nil lastWaitText[vehicle] = nil fetching[vehicle] = run.id local runId = run.id local okFetch, errFetch = pcall(ADHitch.fetch, adEnv, vehicle, connectId, function(tractor, ok, detail) fetching[tractor] = nil local current = ADRuns.getRun(ADRunsController.store, runId) if current == nil or current.stopped then ADHitch.returnHome(adEnv, tractor) return end if ok then fetchRetries[tractor] = nil local result = ADRunsController.startVehicle(adEnv, current, tractor) if result ~= nil and not result.ok then -- flowet kan ikke startes (markør, bygning …): ikke efterladt ved Connect med vognen Logging.warning(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' kunne ikke starte (%s) — vognen sættes tilbage", tostring(current.name), tostring(ADRuns.vehicleLabel(tractor)), tostring(result.reason)) ADHitch.returnHome(adEnv, tractor) end elseif ADFlowWagons.retryFetch(detail, fetchRetries[tractor]) then fetchRetries[tractor] = (fetchRetries[tractor] or 0) + 1 waiting[tractor] = runId -- retryWaiting starter den igen om lidt Logging.warning(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' kunne ikke hente sin vogn (%s) — prøver igen", tostring(current.name), tostring(ADRuns.vehicleLabel(tractor)), tostring(detail)) else fetchRetries[tractor] = nil Logging.warning(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' kunne ikke hente sin vogn (%s)", tostring(current.name), tostring(ADRuns.vehicleLabel(tractor)), tostring(detail)) end end) if not okFetch then fetching[vehicle] = nil return true, {vehicle = vehicle, ok = false, reason = "error"} end Logging.info(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' henter sin vogn", tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle))) return true, {vehicle = vehicle, ok = true, reason = "fetchingWagon"} end -- Stop/slet/bygning væk: vognen tømmes (flowets mål, ellers silo/egen fabrik), sættes på plads, traktoren hjem. local nowMs = 0 local function cargoOf(adEnv, vehicle) local level, fillType = 0, nil local ok, units = pcall(adEnv.AutoDrive.getAllDischargeableUnits, vehicle) for _, unit in ipairs(ok and units or {}) do local object, index = unit.object, unit.fillUnitIndex if object ~= nil and object.getFillUnitFillLevel ~= nil then local unitLevel = object:getFillUnitFillLevel(index) or 0 if unitLevel > 0 then level = level + unitLevel fillType = fillType or object:getFillUnitFillType(index) end end end return level, fillType end local function goHome(adEnv, vehicle) emptying[vehicle] = nil homing[vehicle] = true local okHome, pooled = pcall(ADHitch.returnHome, adEnv, vehicle, function(tractor) homing[tractor] = nil end) if not (okHome and pooled) then homing[vehicle] = nil local park = ADFieldWork.parkMarkerId(adEnv, vehicle) if park ~= nil then pcall(ADFieldWork.startAd, adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end end end local function fillName(fillType) local desc = fillType ~= nil and g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeByIndex(fillType) or nil return desc ~= nil and (desc.title or desc.name) or tostring(fillType) end local function startDump(adEnv, vehicle, state) local cargo, fillType = cargoOf(adEnv, vehicle) local isReachable = function(markerId) return ADUnloadWait == nil or ADUnloadWait.hasRouteTo == nil or ADUnloadWait.hasRouteTo(adEnv, vehicle, markerId) end -- marksiloer er ikke tømmesteder (de er til markens høst); siloer der allerede fejlede springes over local okSet, fieldSilos = pcall(function() return ADFieldSilos ~= nil and ADFieldSilos.fieldSiloPlaceableSet(adEnv) or nil end) state.triedMarkers = state.triedMarkers or {} local ok, marker, why = pcall(ADLoadSwap.findDumpMarker, adEnv, vehicle, fillType, cargo, nil, isReachable, okSet and fieldSilos or nil, state.triedMarkers, true) if not ok or marker == nil then Logging.warning(ADFlowWagons.LOG_PREFIX .. "'%s' har stadig %d l %s — intet sted at aflæsse (%s); parkeres med last", tostring(ADRuns.vehicleLabel(vehicle)), math.floor(cargo), fillName(fillType), tostring(ok and why or marker)) goHome(adEnv, vehicle) return end state.stage, state.since = "dump", nowMs state.markerId, state.markerName, state.lastCargo = marker.id, marker.name, nil state.dumpAttempts = (state.dumpAttempts or 0) + 1 state.triedMarkers[marker.id] = true ADFieldWork.startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DELIVERTO, marker.id, nil, fillType) Logging.info(ADFlowWagons.LOG_PREFIX .. "'%s' aflæsser resten (%d l %s) ved %s før parkering", tostring(ADRuns.vehicleLabel(vehicle)), math.floor(cargo), fillName(fillType), tostring(marker.name)) end -- Husk vognen bag traktoren som flowets (også når flowet lige er stoppet — rememberAll ser kun kørende flows). -- in-game 29/9: Milk Store's tanke blev sat af ved Stop alt uden at være husket -> 'noTrailer' ved næste start. local function isCargoWagon(object) local _, capacity = ADPool.cargoOf(object) return ADFlowWagons.isRememberableWagon({capacity = capacity, isSowingMachine = object.spec_sowingMachine ~= nil}) end local function rememberWagonOf(vehicle) local run = ADRunsController.getRunForVehicle(vehicle) local rear = run ~= nil and ADHitch.rearImplementsOf(vehicle) or {} local tractorId = ADRunsController.getVehicleId(vehicle) local wagonId = rear[1] ~= nil and isCargoWagon(rear[1].object) and ADRunsController.getVehicleId(rear[1].object) or nil if wagonId == nil or tractorId == nil then return end local updated = ADFlowWagons.remember(run.wagons, tractorId, wagonId) if updated ~= run.wagons then run.wagons = updated Logging.info(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' har vognen '%s'", tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle)), tostring(ADRuns.vehicleLabel(rear[1].object))) end end -- Én traktor hjem (fra et stoppet/slettet flow): tøm først, så parkering. -- reason: "stop" (et kørende flow stoppes: vognen huskes) | "afterLoad" (oprydning: intet huskes) function ADFlowWagons.homeVehicle(adEnv, vehicle, runName, reason) waiting[vehicle] = nil if ADFlowWagons.shouldRememberOnHome(reason or "stop") then rememberWagonOf(vehicle) -- vognen sættes af nu: flowet skal kunne finde den igen ved næste start end ADHitch.stow(vehicle, "flowet stoppet") if fetching[vehicle] ~= nil then fetching[vehicle] = nil ADHitch.cancel(vehicle) end local module = vehicle.ad ~= nil and vehicle.ad.stateModule or nil local second = module ~= nil and module.getSecondMarker ~= nil and module:getSecondMarker() or nil local run = ADRunsController.getRunForVehicle(vehicle) local targetId = ADFlowWagons.emptyTarget(second ~= nil and second.id or nil, run ~= nil and run.targetWayPointIds or nil) local target = targetId ~= nil and adEnv.ADGraphManager:getMapMarkerByWayPointId(targetId) or nil local cargo, fillType = cargoOf(adEnv, vehicle) local action = ADFlowWagons.emptyStep("start", {cargo = cargo, hasTarget = target ~= nil}) if action == "home" then return goHome(adEnv, vehicle) end local state = {stage = action, since = nowMs, runName = runName, markerId = target ~= nil and target.id or nil, markerName = target ~= nil and target.name or nil} emptying[vehicle] = state if action == "target" then ADFieldWork.startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DELIVERTO, target.id, nil, fillType) Logging.info(ADFlowWagons.LOG_PREFIX .. "'%s' afleverer sit læs (%d l %s) ved %s før parkering", tostring(ADRuns.vehicleLabel(vehicle)), math.floor(cargo), fillName(fillType), tostring(target.name)) else startDump(adEnv, vehicle, state) end end -- ADHitch: spilleren har selv kørt traktoren til Connect og koblet på. function ADFlowWagons.onManualAttach(adEnv, vehicle, wagon) local run = ADRunsController.getRunForVehicle(vehicle) orphans[vehicle] = 0 -- ryddes af tickOrphans når nogen ejer opgaven (flow, markopgave, AD, spilleren) if run == nil then return end local tractorId = ADRunsController.getVehicleId(vehicle) local wagonId = wagon ~= nil and ADRunsController.getVehicleId(wagon) or nil local remembered = (run.wagons or {})[tractorId] local module = vehicle.ad ~= nil and vehicle.ad.stateModule or nil -- ikke stoppet af spilleren = gælder også et flow hvis Start blev afvist ('ingen vogn') — det er derfor han henter vognen if not ADFlowWagons.startAfterManualAttach({flowRunning = not run.stopped, ownWagon = wagonId ~= nil and (remembered == nil or remembered == wagonId), adActive = module ~= nil and module:isActive()}) then return end fetchRetries[vehicle], waiting[vehicle], orphans[vehicle] = nil, nil, nil Logging.info(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' har selv hentet sin vogn — startes", tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle))) ADRunsController.startVehicle(adEnv, run, vehicle) end -- Traktorer der har koblet et redskab på uden ejer: efter ORPHAN_MS sættes redskabet tilbage og traktoren kører hjem. local function tickOrphans(adEnv, elapsed) for vehicle, idle in pairs(orphans) do local id = ADRunsController.getVehicleId(vehicle) local run = ADRunsController.getRunForVehicle(vehicle) local module = vehicle.ad ~= nil and vehicle.ad.stateModule or nil local owned = (module ~= nil and module:isActive()) or (vehicle.getIsControlled ~= nil and vehicle:getIsControlled()) or (run ~= nil and not run.stopped) or (ADFieldJobs ~= nil and id ~= nil and ADFieldJobs.state.assignments[id] ~= nil) or (ADCpBridge ~= nil and ADCpBridge.isWorking(vehicle)) or #ADHitch.rearImplementsOf(vehicle) == 0 local atOwnPark = false local parkId = ADFieldWork ~= nil and ADFieldWork.parkMarkerId ~= nil and ADFieldWork.parkMarkerId(adEnv, vehicle) or nil local parkWp = parkId ~= nil and adEnv.ADGraphManager:getWayPointById(parkId) or nil if parkWp ~= nil and vehicle.rootNode ~= nil then local x, _, z = getWorldTranslation(vehicle.rootNode) atOwnPark = (x - parkWp.x) ^ 2 + (z - parkWp.z) ^ 2 <= ADFlowWagons.OWN_PARK_M ^ 2 end local action = ADFlowWagons.orphanAction({owned = owned, atOwnPark = atOwnPark, idleMs = idle + elapsed}) if action == "none" then orphans[vehicle] = nil elseif action == "home" then orphans[vehicle] = nil Logging.info(ADFlowWagons.LOG_PREFIX .. "'%s' har et redskab på uden opgave (hentet før genindlæsning?) — sættes tilbage og kører hjem", tostring(ADRuns.vehicleLabel(vehicle))) goHome(adEnv, vehicle) else orphans[vehicle] = idle + elapsed end end end function ADFlowWagons.isEmptying(vehicle) return emptying[vehicle] ~= nil end function ADFlowWagons.sendHome(adEnv, run) for _, vehicleId in ipairs(run.vehicleIds or {}) do local vehicle = ADRunsController.findVehicleById(adEnv, vehicleId) if vehicle ~= nil then local ok, err = pcall(ADFlowWagons.homeVehicle, adEnv, vehicle, run.name) if not ok then Logging.warning(ADFlowWagons.LOG_PREFIX .. "hjem fejlede: %s", tostring(err)) goHome(adEnv, vehicle) end end end end -- Står traktoren ved tømningens markør, uden at læsset er faldet i EMPTY_STALL_MS? local function stalledAt(adEnv, vehicle, state, cargo) if state.lastCargo == nil or cargo < state.lastCargo - 1 then state.lastCargo, state.changedAt = cargo, nowMs end local wayPoint = state.markerId ~= nil and adEnv.ADGraphManager:getWayPointById(state.markerId) or nil local near = false if wayPoint ~= nil and vehicle.rootNode ~= nil then local x, _, z = getWorldTranslation(vehicle.rootNode) near = math.sqrt((x - wayPoint.x) ^ 2 + (z - wayPoint.z) ^ 2) <= ADFlowWagons.EMPTY_NEAR_M end return ADFlowWagons.isStalled({nearTarget = near, sinceChangeMs = nowMs - (state.changedAt or nowMs)}) end local function tickEmptying(adEnv) local list = {} for vehicle, state in pairs(emptying) do table.insert(list, {vehicle = vehicle, state = state}) end for _, item in ipairs(list) do local vehicle, state = item.vehicle, item.state local module = vehicle.ad ~= nil and vehicle.ad.stateModule or nil local cargo, fillType = cargoOf(adEnv, vehicle) local controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled() if controlled then emptying[vehicle] = nil -- spilleren har overtaget else local stalled = stalledAt(adEnv, vehicle, state, cargo) local action = ADFlowWagons.emptyStep(state.stage, {cargo = cargo, adActive = module ~= nil and module:isActive(), elapsedMs = nowMs - state.since, stalled = stalled, dumpAttempts = state.dumpAttempts}) if stalled and action ~= "wait" then Logging.info(ADFlowWagons.LOG_PREFIX .. "'%s' kan ikke aflæsse ved %s (fuldt?) — videre", tostring(ADRuns.vehicleLabel(vehicle)), tostring(state.markerName)) end if action == "home" then goHome(adEnv, vehicle) elseif action == "dump" then startDump(adEnv, vehicle, state) elseif action == "giveUp" then Logging.warning(ADFlowWagons.LOG_PREFIX .. "'%s' kunne ikke tømme vognen (%d l %s tilbage) — parkeres med last", tostring(ADRuns.vehicleLabel(vehicle)), math.floor(cargo), fillName(fillType)) goHome(adEnv, vehicle) end end end end local function rememberAll(adEnv) for _, run in ipairs(runs()) do if not run.stopped then for _, vehicleId in ipairs(run.vehicleIds or {}) do local vehicle = ADRunsController.findVehicleById(adEnv, vehicleId) local rear = vehicle ~= nil and ADHitch.rearImplementsOf(vehicle) or {} local wagonId = rear[1] ~= nil and isCargoWagon(rear[1].object) and ADRunsController.getVehicleId(rear[1].object) or nil if wagonId ~= nil then local updated = ADFlowWagons.remember(run.wagons, vehicleId, wagonId) if updated ~= run.wagons then run.wagons = updated Logging.info(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' har vognen '%s'", tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle)), tostring(ADRuns.vehicleLabel(rear[1].object))) end end end end end end local function retryWaiting(adEnv) local list = {} for vehicle, runId in pairs(waiting) do table.insert(list, {vehicle = vehicle, runId = runId}) end for _, item in ipairs(list) do waiting[item.vehicle] = nil local run = ADRuns.getRun(ADRunsController.store, item.runId) if run ~= nil and not run.stopped then pcall(ADRunsController.startVehicle, adEnv, run, item.vehicle) end end end local function checkBuildings(adEnv) local exists = {} local placeables = g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {} for _, placeable in pairs(placeables) do local id = ADBuildings.getId(placeable) if id ~= nil then exists[id] = true end end if next(exists) == nil then return end -- listen er ikke klar: sig intet for id in pairs(exists) do seenBuildings[id] = true end for _, run in ipairs(runs()) do if not run.stopped then local missing = ADFlowWagons.missingBuildings(run, exists, seenBuildings) local count, gone = ADFlowWagons.missingStep(missingCount[run.id], #missing > 0) missingCount[run.id] = count if gone then missingCount[run.id] = 0 Logging.warning(ADFlowWagons.LOG_PREFIX .. "flow '%s': bygning findes ikke længere (%s) — stoppet, traktorer kørt hjem", tostring(run.name), table.concat(missing, ", ")) ADRunsController.stopRun(adEnv, run.id) if ADFieldJobs ~= nil and ADFieldJobs.notify ~= nil then pcall(ADFieldJobs.notify, string.format("Flow '%s': bygning findes ikke længere — stoppet, traktorer kørt hjem", tostring(run.name)), true) end end end end end -- Én gang efter indlæsning: et stoppet flows traktor der holder stille med sin vogn (gemt midt i hjemturen — AD -- kørte den færdig, men afkoblingen er glemt) køres hjem. Ikke løbende: spilleren kan selv have koblet på. ADFlowWagons.AFTER_LOAD_MS = 120000 local sinceLoad, cleanedUp = 0, false -- Efter indlæsning: traktorer uden for flows med et puljeredskab på (hentet før gemning, opgaven tabt) følges som -- forældreløse — får de ingen opgave inden ORPHAN_MS, sættes redskabet tilbage (in-game 29/9: 3 læssevogne). local function adoptOrphansAfterLoad(adEnv) for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do local run = ADRunsController.getRunForVehicle(vehicle) if (run == nil or run.stopped) and vehicle.spec_attacherJoints ~= nil and #ADHitch.poolParkIds(adEnv, vehicle) > 0 and not ADFlowWagons.isEmptying(vehicle) and not homing[vehicle] then orphans[vehicle] = orphans[vehicle] or 0 end end end local function cleanupAfterLoad(adEnv) for _, run in ipairs(runs()) do if run.stopped then for _, vehicleId in ipairs(run.vehicleIds or {}) do local vehicle = ADRunsController.findVehicleById(adEnv, vehicleId) local module = vehicle ~= nil and vehicle.ad ~= nil and vehicle.ad.stateModule or nil local controlled = vehicle ~= nil and vehicle.getIsControlled ~= nil and vehicle:getIsControlled() if module ~= nil and not module:isActive() and not controlled and #ADHitch.poolParkIds(adEnv, vehicle) > 0 and not ADFlowWagons.isPoolBusy(vehicle, vehicleId) then Logging.info(ADFlowWagons.LOG_PREFIX .. "flow '%s' er stoppet: '%s' tømmer og sætter sin vogn på plads", tostring(run.name), tostring(ADRuns.vehicleLabel(vehicle))) pcall(ADFlowWagons.homeVehicle, adEnv, vehicle, run.name, "afterLoad") end end end end end local emptyTimer = 0 function ADFlowWagons.tick(adEnv, dt) if adEnv == nil or ADRunsController.store == nil then return end if ADHitch ~= nil and ADHitch.onManualAttach == nil then ADHitch.onManualAttach = ADFlowWagons.onManualAttach end nowMs = nowMs + dt emptyTimer = emptyTimer + dt if emptyTimer >= 2000 then emptyTimer = 0 local okOrphan, errOrphan = pcall(tickOrphans, adEnv, 2000) if not okOrphan then Logging.warning(ADFlowWagons.LOG_PREFIX .. "redskaber uden opgave: %s", tostring(errOrphan)) end local okEmpty, errEmpty = pcall(tickEmptying, adEnv) if not okEmpty then Logging.warning(ADFlowWagons.LOG_PREFIX .. "tømning: %s", tostring(errEmpty)) end end if not cleanedUp then sinceLoad = sinceLoad + dt if sinceLoad >= ADFlowWagons.AFTER_LOAD_MS then cleanedUp = true local ok, err = pcall(cleanupAfterLoad, adEnv) if not ok then Logging.warning(ADFlowWagons.LOG_PREFIX .. "oprydning efter indlæsning: %s", tostring(err)) end local okAdopt, errAdopt = pcall(adoptOrphansAfterLoad, adEnv) if not okAdopt then Logging.warning(ADFlowWagons.LOG_PREFIX .. "redskaber uden opgave efter indlæsning: %s", tostring(errAdopt)) end local okPrune, errPrune = pcall(ADRunsController.pruneMissingVehicles, adEnv) if not okPrune then Logging.warning(ADFlowWagons.LOG_PREFIX .. "oprydning af solgte traktorer: %s", tostring(errPrune)) end end end rememberTimer = rememberTimer + dt buildingTimer = buildingTimer + dt if rememberTimer >= ADFlowWagons.REMEMBER_MS then rememberTimer = 0 rememberAll(adEnv) retryWaiting(adEnv) end if buildingTimer >= ADFlowWagons.BUILDING_CHECK_MS then buildingTimer = 0 checkBuildings(adEnv) end end