diff --git a/FS25_ADSmartPickup/adFieldWork.lua b/FS25_ADSmartPickup/adFieldWork.lua index f953816..414b606 100644 --- a/FS25_ADSmartPickup/adFieldWork.lua +++ b/FS25_ADSmartPickup/adFieldWork.lua @@ -146,7 +146,8 @@ function ADFieldWork.getRigs(adEnv) local role = ADFieldFleet.roleFromSpecs(specs) local id = ADRunsController.getVehicleId(vehicle) -- traktorer i et almindeligt flow (æg, gylle, foder ...) hører til den drift og er ikke markflåde - if role ~= nil and id ~= nil and ADRunsController.getRunForVehicle(vehicle) == nil then + -- (et stoppet flows traktorer er ledige — maskinpulje/flow-vogne) + if role ~= nil and id ~= nil and (ADPool ~= nil and ADPool.isFreeFromFlows(vehicle) or ADRunsController.getRunForVehicle(vehicle) == nil) then local x, z = vehiclePosition(vehicle) local rig = { id = id, vehicle = vehicle, role = role, label = ADRuns.vehicleLabel(vehicle), diff --git a/FS25_ADSmartPickup/adFlowWagons.lua b/FS25_ADSmartPickup/adFlowWagons.lua new file mode 100644 index 0000000..0515046 --- /dev/null +++ b/FS25_ADSmartPickup/adFlowWagons.lua @@ -0,0 +1,238 @@ +-- 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 + +-- s = {hasRear, wagonId, wagonExists, hasConnect, wagonAttached, wagonBusy} -> "start" | "fetch" | "wait" +function ADFlowWagons.startAction(s) + 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 + +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 der ikke er stoppet? (så må puljen ikke låne den) +function ADFlowWagons.isHeld(runs, wagonId) + local run = ADFlowWagons.ownerOf(runs, wagonId) + return run ~= nil and not run.stopped +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}. +function ADFlowWagons.missingBuildings(run, exists) + local missing = {} + for _, key in ipairs({"sourceBuildingIds", "targetBuildingIds", "storeBuildingIds"}) do + for _, id in ipairs(run[key] or {}) do + if not exists[id] then table.insert(missing, id) end + end + end + return missing +end + +-- ---- spil ---- + +local waiting = setmetatable({}, {__mode = "k"}) -- traktor -> runId (venter på sin vogn) +local fetching = setmetatable({}, {__mode = "k"}) -- traktor -> runId (henter sin vogn) +local missingCount = {} -- runId -> tjek i træk +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. +function ADFlowWagons.isFreeTractor(vehicle) + local run = ADRunsController.getRunForVehicle(vehicle) + return run == nil or run.stopped == true +end + +function ADFlowWagons.hasWagonFor(run, tractorId) + return (run.wagons or {})[tractorId] ~= nil +end + +function ADFlowWagons.isHeldWagon(wagonId) + return ADFlowWagons.isHeld(runs(), wagonId) +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 + local wagonId = (run.wagons or {})[tractorId] + local wagon = wagonId ~= nil and findAnyVehicle(wagonId) or nil + local connectId = wagon ~= nil and connectOf(adEnv, wagon) or nil + local action = ADFlowWagons.startAction({ + 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), + }) + if action == "start" then return false end + ADRunsController.runningRuns[run.id] = true + if action == "wait" then + if waiting[vehicle] == nil then + Logging.info(ADFlowWagons.LOG_PREFIX .. "flow '%s': '%s' venter på sin vogn (i brug andetsteds)", tostring(run.name), + tostring(ADRuns.vehicleLabel(vehicle))) + end + waiting[vehicle] = run.id + return true, {vehicle = vehicle, ok = true, reason = "waitingWagon"} + end + waiting[vehicle] = nil + fetching[vehicle] = run.id + local runId = run.id + 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 + ADRunsController.startVehicle(adEnv, current, tractor) + else + 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) + 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 på plads, traktoren hjem. +function ADFlowWagons.sendHome(adEnv, run) + for _, vehicleId in ipairs(run.vehicleIds or {}) do + local vehicle = ADRunsController.findVehicleById(adEnv, vehicleId) + if vehicle ~= nil then + waiting[vehicle] = nil + if fetching[vehicle] ~= nil then + fetching[vehicle] = nil + ADHitch.cancel(vehicle) + end + local okHome, pooled = pcall(ADHitch.returnHome, adEnv, vehicle) + if not (okHome and pooled) then + local park = ADFieldWork.parkMarkerId(adEnv, vehicle) + if park ~= nil then pcall(ADFieldWork.startAd, adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end + 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 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 _, run in ipairs(runs()) do + if not run.stopped then + local missing = ADFlowWagons.missingBuildings(run, exists) + 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 + +function ADFlowWagons.tick(adEnv, dt) + if adEnv == nil or ADRunsController.store == nil then return 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 diff --git a/FS25_ADSmartPickup/adPool.lua b/FS25_ADSmartPickup/adPool.lua index 3b1c288..8c3e0f9 100644 --- a/FS25_ADSmartPickup/adPool.lua +++ b/FS25_ADSmartPickup/adPool.lua @@ -107,6 +107,7 @@ function ADPool.implementStatus(s) if s.parkName == nil then return "noPark" end if not s.hasConnect then return "noConnect", s.parkName end if s.attachedTo ~= nil then return "attached", s.attachedTo end + if s.flowName ~= nil then return "flow", s.flowName end if (s.quarantineMin or 0) > 0 then return "quarantine", s.quarantineMin end if s.reserved then return "reserved" end return "parked", s.parkName @@ -308,6 +309,12 @@ function ADPool.cargoOf(vehicle) return carries, capacity end +-- Ikke bundet af et kørende flow (traktorer i stoppede flows er ledige). +function ADPool.isFreeFromFlows(vehicle) + if ADFlowWagons ~= nil then return ADFlowWagons.isFreeTractor(vehicle) end + return ADRunsController.getRunForVehicle(vehicle) == nil +end + local function isAttached(vehicle) return vehicle.getAttacherVehicle ~= nil and vehicle:getAttacherVehicle() ~= nil end @@ -335,7 +342,8 @@ function ADPool.collect(adEnv, isBusy) local connect = park ~= nil and ADPool.connectFor(park.name, markers) or nil local specs = connect ~= nil and ADCpBridge.getSpecs(vehicle) or nil local role = specs ~= nil and ADPool.roleOf(specs) or nil - if role ~= nil then + local held = role ~= nil and ADFlowWagons ~= nil and ADFlowWagons.isHeldWagon(ADRunsController.getVehicleId(vehicle)) + if role ~= nil and not held then local x, z = positionOf(vehicle) local carries, capacity = ADPool.cargoOf(vehicle) table.insert(implements, {carries = carries, capacity = capacity, @@ -352,7 +360,7 @@ function ADPool.collect(adEnv, isBusy) local park = parkMarker(adEnv, vehicle) local controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled() if id ~= nil and park ~= nil and vehicle.spec_attacherJoints ~= nil and #ADHitch.rearImplementsOf(vehicle) == 0 - and ADRunsController.getRunForVehicle(vehicle) == nil and not controlled and not isBusy(vehicle) + and ADPool.isFreeFromFlows(vehicle) and not controlled and not isBusy(vehicle) and not ADPool.isReturning(vehicle) and ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id) and ADPool.canPull(ADPool.ownRole(vehicle)) then local x, z = positionOf(vehicle) @@ -387,6 +395,8 @@ function ADPool.overview(adEnv) attachedTo = attacher ~= nil and ADRuns.vehicleLabel(attacher) or nil, quarantineMin = id ~= nil and ADPool.quarantineMinutes(id, nowMs) or 0, reserved = id ~= nil and ADPool.isReserved(id), + flowName = id ~= nil and ADFlowWagons ~= nil and ADFlowWagons.isHeldWagon(id) + and tostring(ADFlowWagons.ownerOf(ADRunsController.store.runs, id).name) or nil, }) table.insert(implements, {id = id, label = ADRuns.vehicleLabel(vehicle), role = role, status = key, arg = arg}) end @@ -396,7 +406,7 @@ function ADPool.overview(adEnv) for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do local id = ADRunsController.getVehicleId(vehicle) -- en traktor med et redskab der giver den en markrolle, står allerede i flåden som kombination - if id ~= nil and vehicle.spec_attacherJoints ~= nil and ADRunsController.getRunForVehicle(vehicle) == nil + if id ~= nil and vehicle.spec_attacherJoints ~= nil and ADPool.isFreeFromFlows(vehicle) and ADPool.canPull(ADPool.ownRole(vehicle)) and ADFieldFleet.roleFromSpecs(ADCpBridge.getSpecs(vehicle)) == nil then local x, z = positionOf(vehicle) local distance = ADFieldWork.distanceToNetwork(adEnv, x, z) diff --git a/FS25_ADSmartPickup/adRuns.lua b/FS25_ADSmartPickup/adRuns.lua index 752ff9c..25d43a8 100644 --- a/FS25_ADSmartPickup/adRuns.lua +++ b/FS25_ADSmartPickup/adRuns.lua @@ -42,6 +42,28 @@ local function clampRange(value, default, min, max) return math.max(min, math.min(max, math.floor(number))) end +-- Flow-vogne: {[traktorId] = vognId} (maskinpulje — flowet henter sin egen vogn). +function ADRuns.copyWagons(wagons) + local copy = {} + for tractorId, wagonId in pairs(wagons or {}) do copy[tractorId] = wagonId end + return copy +end + +function ADRuns.serializeWagons(wagons) + local keys = {} + for tractorId in pairs(wagons or {}) do table.insert(keys, tractorId) end + table.sort(keys) + local parts = {} + for _, tractorId in ipairs(keys) do table.insert(parts, tractorId .. "=" .. wagons[tractorId]) end + return table.concat(parts, ";") +end + +function ADRuns.parseWagons(text) + local wagons = {} + for tractorId, wagonId in string.gmatch(tostring(text or ""), "([^=;]+)=([^=;]+)") do wagons[tractorId] = wagonId end + return wagons +end + local function copyRun(run) return { id = run.id, @@ -64,6 +86,8 @@ local function copyRun(run) loops = run.loops, returnBelowPercent = run.returnBelowPercent, vehicleIds = copyList(run.vehicleIds), + stopped = run.stopped == true, + wagons = ADRuns.copyWagons(run.wagons), } end @@ -372,6 +396,8 @@ function ADRuns.toRecords(store) storeAbove = run.storeAbovePercent, sellAbove = run.sellAbovePercent or 0, collectRound = run.collectRound and 1 or 0, + stopped = run.stopped and 1 or 0, + wagons = ADRuns.serializeWagons(run.wagons), } end return records @@ -424,6 +450,8 @@ function ADRuns.fromRecords(records) loops = clampLoops(record.loops), returnBelowPercent = clampPercent(record.returnBelow), vehicleIds = copyList(record.vehicleIds), + stopped = tonumber(record.stopped) == 1, + wagons = ADRuns.parseWagons(record.wagons), }) store.nextId = math.max(store.nextId, id + 1) end diff --git a/FS25_ADSmartPickup/adRunsController.lua b/FS25_ADSmartPickup/adRunsController.lua index d273be7..4340b6f 100644 --- a/FS25_ADSmartPickup/adRunsController.lua +++ b/FS25_ADSmartPickup/adRunsController.lua @@ -90,6 +90,7 @@ function ADRunsController.deleteRun(runId, adEnv) for _, vehicleId in ipairs(run.vehicleIds) do ADRunsController.stopIfRunning(ADRunsController.findVehicleById(adEnv, vehicleId), "run slettet") end + if ADFlowWagons ~= nil then pcall(ADFlowWagons.sendHome, adEnv, run) end end ADRunsController.store = ADRuns.deleteRun(ADRunsController.store, runId) end @@ -1074,6 +1075,12 @@ end -- og logges med årsag (samme nøgle som fanen viser). -- Start én traktor i et run: valider, anvend run'et og start AutoDrive. Returnerer resultat-posten. function ADRunsController.startVehicle(adEnv, run, vehicle) + -- solo-traktor med en husket vogn: hent den først (valideringen ville afvise en traktor uden vogn) + if ADFlowWagons ~= nil then + local okWagon, handled, wagonResult = pcall(ADFlowWagons.beforeStart, adEnv, run, vehicle) + if okWagon and handled then return wagonResult end + if not okWagon then Logging.warning(ADRunsController.LOG_PREFIX .. "flow-vogn: %s", tostring(handled)) end + end local validation = ADRunsController.validateVehicle(adEnv, run, vehicle) local result = {vehicle = vehicle, ok = validation.ok, reason = validation.reason, missingBuilding = validation.missingBuilding} if not validation.ok then @@ -1101,6 +1108,7 @@ function ADRunsController.startRun(adEnv, runId) if run == nil then return results end + run.stopped = false local vehiclesById = getFarmVehiclesById(adEnv) for _, vehicleId in ipairs(run.vehicleIds) do local vehicle = vehiclesById[vehicleId] @@ -1132,11 +1140,13 @@ function ADRunsController.queueStartAll(adEnv) if ADRunsController.getRunStatus(adEnv, run).isRunning then summary.alreadyRunning = summary.alreadyRunning + 1 else + run.stopped = false for _, vehicleId in ipairs(run.vehicleIds) do local vehicle = vehiclesById[vehicleId] if vehicle ~= nil then local validation = ADRunsController.validateVehicle(adEnv, run, vehicle) - if validation.ok then + -- solo-traktor med husket vogn: henter den ved start (valideringen ser den uden vogn) + if validation.ok or (ADFlowWagons ~= nil and ADFlowWagons.hasWagonFor(run, vehicleId)) then table.insert(queue, {runId = run.id, vehicleId = vehicleId}) else table.insert(summary.skipped, {vehicle = vehicle, ok = false, reason = validation.reason, missingBuilding = validation.missingBuilding}) @@ -1175,6 +1185,7 @@ function ADRunsController.stopAllAndPark(adEnv) end end ADRunsController.runningRuns[run.id] = nil + run.stopped = true end if ADFieldWork ~= nil and ADFieldWork.stopAllAssignments ~= nil then for _, stopped in ipairs(ADFieldWork.stopAllAssignments(adEnv) or {}) do @@ -1192,8 +1203,13 @@ function ADRunsController.stopAllAndPark(adEnv) if ADFieldSilos ~= nil and ADFieldSilos.stopAll ~= nil then pcall(ADFieldSilos.stopAll, adEnv) end if ADFieldJobs ~= nil and ADFieldJobs.setPaused ~= nil then ADFieldJobs.setPaused(true) end for _, vehicle in ipairs(toPark) do - local park = ADFieldWork ~= nil and ADFieldWork.parkMarkerId ~= nil and ADFieldWork.parkMarkerId(adEnv, vehicle) or nil - if park ~= nil then + -- flow-vogn/puljeredskab bag traktoren: sæt det af på sin plads og kør hjem + local okHome, pooled = false, false + if ADHitch ~= nil then okHome, pooled = pcall(ADHitch.returnHome, adEnv, vehicle) end + local park = not (okHome and pooled) and ADFieldWork ~= nil and ADFieldWork.parkMarkerId ~= nil and ADFieldWork.parkMarkerId(adEnv, vehicle) or nil + if okHome and pooled then + summary.parked = summary.parked + 1 + elseif park ~= nil then local ok = pcall(ADFieldWork.startAd, adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) if ok then summary.parked = summary.parked + 1 end else @@ -1246,6 +1262,12 @@ function ADRunsController.stopRun(adEnv, runId) end end ADRunsController.runningRuns[runId] = nil + -- stoppet flow: vognene på plads og traktorerne hjem (ledige i maskinpuljen, vognene er stadig flowets) + run.stopped = true + if ADFlowWagons ~= nil then + local okHome, err = pcall(ADFlowWagons.sendHome, adEnv, run) + if not okHome then Logging.warning(ADRunsController.LOG_PREFIX .. "flow-vogne hjem: %s", tostring(err)) end + end return stopped end diff --git a/FS25_ADSmartPickup/adRunsStorage.lua b/FS25_ADSmartPickup/adRunsStorage.lua index 22b7523..5357be7 100644 --- a/FS25_ADSmartPickup/adRunsStorage.lua +++ b/FS25_ADSmartPickup/adRunsStorage.lua @@ -48,6 +48,9 @@ function ADRunsStorage.writeRecords(xmlFile, records) xmlFile:setInt(key .. "#storeAbove", record.storeAbove or ADRuns.DEFAULT_STORE_ABOVE_PERCENT) xmlFile:setInt(key .. "#sellAbove", record.sellAbove or 0) xmlFile:setInt(key .. "#collectRound", record.collectRound or 0) + -- maskinpulje: flowet stoppet + husket vogn pr. traktor + xmlFile:setInt(key .. "#stopped", record.stopped or 0) + xmlFile:setString(key .. "#wagons", record.wagons or "") for vehicleIndex, vehicleId in ipairs(record.vehicleIds) do xmlFile:setString(string.format("%s.vehicle(%d)#id", key, vehicleIndex - 1), vehicleId) end @@ -77,6 +80,8 @@ function ADRunsStorage.readRecords(xmlFile) storeAbove = xmlFile:getInt(key .. "#storeAbove"), sellAbove = xmlFile:getInt(key .. "#sellAbove"), collectRound = xmlFile:getInt(key .. "#collectRound"), + stopped = xmlFile:getInt(key .. "#stopped"), + wagons = xmlFile:getString(key .. "#wagons"), vehicleIds = {}, } xmlFile:iterate(key .. ".vehicle", function(_, vehicleKey) diff --git a/FS25_ADSmartPickup/adSmartPickup.lua b/FS25_ADSmartPickup/adSmartPickup.lua index cd109c5..99c1f75 100644 --- a/FS25_ADSmartPickup/adSmartPickup.lua +++ b/FS25_ADSmartPickup/adSmartPickup.lua @@ -1272,6 +1272,10 @@ function ADSmartPickup:update(dt) Logging.warning(ADSmartPickup.LOG_PREFIX .. "marksilo-flow fejlede: %s", tostring(errSilos)) end end + if ADFlowWagons ~= nil then + local okFlow, errFlow = pcall(ADFlowWagons.tick, getAutoDriveEnv(), dt) + if not okFlow then Logging.warning(ADSmartPickup.LOG_PREFIX .. "flow-vogne fejlede: %s", tostring(errFlow)) end + end if ADHitch ~= nil then local okHitch, errHitch = pcall(ADHitch.tick, getAutoDriveEnv(), dt) if not okHitch then diff --git a/FS25_ADSmartPickup/l10n/l10n_da.xml b/FS25_ADSmartPickup/l10n/l10n_da.xml index 763069d..133e3de 100644 --- a/FS25_ADSmartPickup/l10n/l10n_da.xml +++ b/FS25_ADSmartPickup/l10n/l10n_da.xml @@ -98,6 +98,7 @@ + diff --git a/FS25_ADSmartPickup/l10n/l10n_en.xml b/FS25_ADSmartPickup/l10n/l10n_en.xml index 955ec79..82d1658 100644 --- a/FS25_ADSmartPickup/l10n/l10n_en.xml +++ b/FS25_ADSmartPickup/l10n/l10n_en.xml @@ -98,6 +98,7 @@ + diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml index a5d5e2a..902aa52 100644 --- a/FS25_ADSmartPickup/modDesc.xml +++ b/FS25_ADSmartPickup/modDesc.xml @@ -1,7 +1,7 @@ masterdraco - 1.50.5.0 + 1.51.0.0 <en>AD Smart Pickup</en> @@ -48,6 +48,7 @@ + diff --git a/docs/superpowers/specs/2026-09-28-flow-vogne-design.md b/docs/superpowers/specs/2026-09-28-flow-vogne-design.md new file mode 100644 index 0000000..2b4f02f --- /dev/null +++ b/docs/superpowers/specs/2026-09-28-flow-vogne-design.md @@ -0,0 +1,75 @@ +# Flow-vogne — design + +Dato: 2026-09-28 · Status: godkendt i chat ("1 vognen hører til flowet", "1 automatisk", "ja alle tre", "når du har +det hele klar"). Bygger på maskinpuljen (`2026-09-28-maskinpulje-design.md`). + +## Formål + +Alle påhæng har nu egen AD-parkering "P X" og en markør "Connect P X". Flows (fodring, mælk, slurry …) skal ikke +længere have traktor og vogn låst sammen: et flow henter **sin egen** vogn, når det starter, og når flowet ikke har +noget job længere, køres vognen på plads og traktoren hjem, så begge bliver ledige i puljen. + +## Brugerens valg + +| Spørgsmål | Valg | +|---|---| +| Hvem ejer en flow-vogn? | Flowet. Puljen må kun låne den, når flowet er stoppet/slettet/uden bygning | +| Hvordan kendes flowets vogn? | Automatisk: vognen der hænger på flowets traktor huskes pr. traktor | +| Hvornår "intet job"? | Alle tre: bygning væk, flow slettet, flow stoppet | +| Mejetærskere/skærebord | Uændret — skærebord er aldrig i puljen; mejetærsker henter ikke redskaber | + +## Begreber + +- **Flowets vogn:** `run.wagons = {[traktorId] = vognId}`. Opdateres automatisk: hænger der et redskab bagpå en af + flowets traktorer (mens flowet ikke er stoppet), huskes det som traktorens vogn. Gemmes i `ADSmartPickup_runs.xml` + (`wagons="traktorId=vognId;…"`). +- **Stoppet flow:** `run.stopped = true`, gemt. Sættes af Stop på flowet, "Stop alle"/"Stop alt og parkér" og + "bygning væk"; ryddes af Start / "Start alle". Ikke udledt af om AD kører (en traktor ved et Wait-punkt kan holde + stille uden at flowet er stoppet). +- **Bygning væk:** en bygning i flowets kilde-/mål-/lagerliste findes ikke længere som placeable (uniqueId) i to + tjek i træk (60 s mellem tjek). + +## Adfærd + +1. **Husk vogn** (hvert 10. s): for hvert ikke-stoppet flow og hver traktor med et bagerste redskab → `wagons[traktor] + = redskab` (ændringer gemmes med næste gem). +2. **Start** (`startVehicle`): er traktoren solo og har den en husket vogn: + - vognen frakoblet og ikke reserveret/lånt → `ADHitch.fetch` til "Connect ", derefter startes + traktoren i flowet som i dag. Fanen/loggen: "henter sin vogn". + - vognen er lånt ud / koblet på en anden → traktoren venter ("venter på sin vogn"), der prøves igen hvert 10. s. + - vognen findes ikke eller har ingen Connect-markør → startes som i dag (solo; valideringen afgør). +3. **Stop / "Stop alle" / "Stop alt og parkér"**: `stopped = true`; hver traktor stoppes og køres hjem med + `ADHitch.returnHome` (vogn af på sin parkering, traktor til sin egen). Vognen forbliver flowets. +4. **Slet flow**: som stop; derefter er vognen ikke længere flowets (flowet findes ikke). +5. **Bygning væk**: flowet stoppes som i 3, markeres i loggen og med en besked: "Flow 'X': bygning findes ikke længere + — stoppet, traktorer kørt hjem". + +## Puljen og flows + +- `ADPool.collect`: traktor i et flow er kun puljetraktor, når flowet er **stoppet**. +- Puljeredskab: en vogn, der er husket af et **ikke-stoppet** flow, er aldrig puljeredskab. +- `ADFieldWork.getRigs`: uændret for kørende flows; traktorer i stoppede flows må indgå. + +## Komponenter + +- `adFlowWagons.lua` (nyt): ren logik (serialisering af `wagons`, beslutning ved start: fetch/wait/start, "bygning + væk"-debounce, ejer-opslag) + spil-del (tick: husk vogne, ventende starter, bygningstjek; `fetchThenStart`, + `sendHome(run)`). +- `adRuns.lua`/`adRunsStorage.lua`: felterne `stopped`, `wagons` i records. +- `adRunsController.lua`: kroge i `startVehicle`, `stopRun`, `deleteRun`, `stopAllRuns`, `stopAllAndPark`, + `startRun`/`startAllRuns`/`queueStartAll` (ryd `stopped`). +- `adPool.lua`: udelukker vogne ejet af ikke-stoppede flows; traktorer i stoppede flows tæller. +- `adSmartPickup.lua`: `ADFlowWagons.tick` i update-løkken. + +## Test + +- `tests/test_adFlowWagons.lua`: serialisering frem/tilbage, startbeslutning (fetch/wait/start), debounce af + bygning væk, ejer-opslag (stoppet vs kørende). +- `tests/test_adRuns.lua`: records med `stopped` og `wagons`. +- In-game: savegame2 — stop et fodringsflow → vogne sættes på plads, traktorer hjem; start det → traktorerne henter + deres egne vogne og kører. + +## Uden for scope + +- Valg af vogn i GUI (automatisk er valgt). +- At et flow tager en anden vogn end sin egen. diff --git a/tests/test_adFlowWagons.lua b/tests/test_adFlowWagons.lua new file mode 100644 index 0000000..a9d1042 --- /dev/null +++ b/tests/test_adFlowWagons.lua @@ -0,0 +1,54 @@ +-- Kør: luajit tests/test_adFlowWagons.lua (fra repo-roden) +dofile("FS25_ADSmartPickup/adRuns.lua") +dofile("FS25_ADSmartPickup/adFlowWagons.lua") +local W = ADFlowWagons +local failures = 0 +local function check(name, actual, expected) + if actual == expected then print("OK " .. name) else + failures = failures + 1 + print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual))) + end +end +-- gem/indlæs: traktor -> vogn +local text = W.serialize({vehicleB = "wagon2", vehicleA = "wagon1"}) +check("gem: sorteret", text, "vehicleA=wagon1;vehicleB=wagon2") +local parsed = W.parse(text) +check("indlæs: A", parsed.vehicleA, "wagon1") +check("indlæs: B", parsed.vehicleB, "wagon2") +check("indlæs: tom", next(W.parse("")), nil) +check("indlæs: nil", next(W.parse(nil)), nil) +check("indlæs: skrald springes over", next(W.parse("=x;;y=")), nil) +-- husk vogn: ny tabel, gammel urørt +local before = {t1 = "w1"} +local after = W.remember(before, "t1", "w9") +check("husk: ny vogn", after.t1, "w9") +check("husk: original urørt", before.t1, "w1") +check("husk: samme vogn -> samme tabel", W.remember(after, "t1", "w9"), after) +-- start: hent / vent / start +check("start: har redskab bagpå", W.startAction({hasRear = true, wagonId = "w1", wagonExists = true, hasConnect = true}), "start") +check("start: ingen husket vogn", W.startAction({}), "start") +check("start: vognen findes ikke", W.startAction({wagonId = "w1", wagonExists = false, hasConnect = true}), "start") +check("start: vognen uden Connect", W.startAction({wagonId = "w1", wagonExists = true, hasConnect = false}), "start") +check("start: vognen på en anden", W.startAction({wagonId = "w1", wagonExists = true, hasConnect = true, wagonAttached = true}), "wait") +check("start: vognen lånt ud", W.startAction({wagonId = "w1", wagonExists = true, hasConnect = true, wagonBusy = true}), "wait") +check("start: hent", W.startAction({wagonId = "w1", wagonExists = true, hasConnect = true}), "fetch") +-- ejer: kun et ikke-stoppet flow holder vognen +local runs = {{id = 1, stopped = false, wagons = {t1 = "w1"}}, {id = 2, stopped = true, wagons = {t2 = "w2"}}} +check("ejer: kørende flow holder", W.isHeld(runs, "w1"), true) +check("ejer: stoppet flow holder ikke", W.isHeld(runs, "w2"), false) +check("ejer: ukendt vogn", W.isHeld(runs, "w3"), false) +check("ejer: flowet findes", W.ownerOf(runs, "w2").id, 2) +-- bygning væk: to tjek i træk +local count, gone = W.missingStep(0, true) +check("væk: første tjek -> ikke endnu", gone, false) +count, gone = W.missingStep(count, true) +check("væk: andet tjek -> væk", gone, true) +count, gone = W.missingStep(1, false) +check("væk: fundet igen nulstiller", count, 0) +check("væk: fundet igen -> ikke væk", gone, false) +-- manglende bygninger i et flow +local exists = {a = true, b = true} +check("mangler: alle findes", #W.missingBuildings({sourceBuildingIds = {"a"}, targetBuildingIds = {"b"}}, exists), 0) +check("mangler: én væk", W.missingBuildings({targetBuildingIds = {"a", "c"}, storeBuildingIds = {}}, exists)[1], "c") +print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) +os.exit(failures == 0 and 0 or 1) diff --git a/tests/test_adPool.lua b/tests/test_adPool.lua index e4d9aaa..ecdf97e 100644 --- a/tests/test_adPool.lua +++ b/tests/test_adPool.lua @@ -149,5 +149,8 @@ check("par-alle: antal (v2 reserveret, 2 traktorer)", #pairs3, 2) check("par-alle: v1 -> tA", pairs3[1].implement.id .. ">" .. pairs3[1].tractor.id, "v1>tA") check("par-alle: v3 -> tB (tA er brugt)", pairs3[2].implement.id .. ">" .. pairs3[2].tractor.id, "v3>tB") check("par-alle: ingen traktorer", #P.pairAll("unloader", wagons, {}, {}), 0) +-- flow-vogne: vogn der hører til et kørende flow vises sådan (og er ikke i puljen) +check("redskab: flowets vogn", ist({parkName = "P Cow Food vogn 1", hasConnect = true, flowName = "Cow Food"}), "flow|Cow Food") +check("redskab: flowets vogn koblet på", ist({parkName = "P Cow Food vogn 1", hasConnect = true, flowName = "Cow Food", attachedTo = "Cow Food 1"}), "attached|Cow Food 1") print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) os.exit(failures == 0 and 0 or 1) diff --git a/tests/test_adRuns.lua b/tests/test_adRuns.lua index c2db6a5..a08b860 100644 --- a/tests/test_adRuns.lua +++ b/tests/test_adRuns.lua @@ -322,5 +322,18 @@ do check("RS roundtrip", table.concat(ADRuns.getRun(back, run.id).storeBuildingIds, ","), "BGA500kw") end +-- flow-vogne: stoppet + husket vogn pr. traktor følger med records frem og tilbage +local wagonStore = ADRuns.newStore() +local ws, wrun = ADRuns.createRun(wagonStore, "Fodring", "supply") +wrun.stopped, wrun.wagons = true, {vehA = "wagonA"} +local wrecords = ADRuns.toRecords(ws) +check("vogne: record stoppet", wrecords[1].stopped, 1) +check("vogne: record vogne", wrecords[1].wagons, "vehA=wagonA") +local wback = ADRuns.fromRecords(wrecords).runs[1] +check("vogne: stoppet tilbage", wback.stopped, true) +check("vogne: vogn tilbage", wback.wagons.vehA, "wagonA") +local wupdated = ADRuns.getRun(ADRuns.updateRun(ws, wrun.id, {name = "Fodring 2"}), wrun.id) +check("vogne: overlever updateRun", wupdated.wagons.vehA, "wagonA") +check("vogne: stoppet overlever updateRun", wupdated.stopped, true) print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) os.exit(failures == 0 and 0 or 1)