feat(flows): flow-vogne — flowets vogn huskes pr. traktor, solo-traktor henter sin vogn ved start (venter hvis den er lånt ud), stop/slet/bygning væk sætter vognen på plads og kører traktoren hjem; stoppede flows er ledige i maskinpuljen (v1.51.0.0)

This commit is contained in:
2026-09-28 23:18:38 +02:00
parent 1759d62600
commit b0d22236b0
14 changed files with 464 additions and 8 deletions
+2 -1
View File
@@ -146,7 +146,8 @@ function ADFieldWork.getRigs(adEnv)
local role = ADFieldFleet.roleFromSpecs(specs) local role = ADFieldFleet.roleFromSpecs(specs)
local id = ADRunsController.getVehicleId(vehicle) local id = ADRunsController.getVehicleId(vehicle)
-- traktorer i et almindeligt flow (æg, gylle, foder ...) hører til den drift og er ikke markflåde -- 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 x, z = vehiclePosition(vehicle)
local rig = { local rig = {
id = id, vehicle = vehicle, role = role, label = ADRuns.vehicleLabel(vehicle), id = id, vehicle = vehicle, role = role, label = ADRuns.vehicleLabel(vehicle),
+238
View File
@@ -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 <vognens
-- parkering>". 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
+13 -3
View File
@@ -107,6 +107,7 @@ function ADPool.implementStatus(s)
if s.parkName == nil then return "noPark" end if s.parkName == nil then return "noPark" end
if not s.hasConnect then return "noConnect", s.parkName end if not s.hasConnect then return "noConnect", s.parkName end
if s.attachedTo ~= nil then return "attached", s.attachedTo 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.quarantineMin or 0) > 0 then return "quarantine", s.quarantineMin end
if s.reserved then return "reserved" end if s.reserved then return "reserved" end
return "parked", s.parkName return "parked", s.parkName
@@ -308,6 +309,12 @@ function ADPool.cargoOf(vehicle)
return carries, capacity return carries, capacity
end 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) local function isAttached(vehicle)
return vehicle.getAttacherVehicle ~= nil and vehicle:getAttacherVehicle() ~= nil return vehicle.getAttacherVehicle ~= nil and vehicle:getAttacherVehicle() ~= nil
end end
@@ -335,7 +342,8 @@ function ADPool.collect(adEnv, isBusy)
local connect = park ~= nil and ADPool.connectFor(park.name, markers) or nil local connect = park ~= nil and ADPool.connectFor(park.name, markers) or nil
local specs = connect ~= nil and ADCpBridge.getSpecs(vehicle) or nil local specs = connect ~= nil and ADCpBridge.getSpecs(vehicle) or nil
local role = specs ~= nil and ADPool.roleOf(specs) 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 x, z = positionOf(vehicle)
local carries, capacity = ADPool.cargoOf(vehicle) local carries, capacity = ADPool.cargoOf(vehicle)
table.insert(implements, {carries = carries, capacity = capacity, table.insert(implements, {carries = carries, capacity = capacity,
@@ -352,7 +360,7 @@ function ADPool.collect(adEnv, isBusy)
local park = parkMarker(adEnv, vehicle) local park = parkMarker(adEnv, vehicle)
local controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled() local controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled()
if id ~= nil and park ~= nil and vehicle.spec_attacherJoints ~= nil and #ADHitch.rearImplementsOf(vehicle) == 0 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 not ADPool.isReturning(vehicle) and ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id)
and ADPool.canPull(ADPool.ownRole(vehicle)) then and ADPool.canPull(ADPool.ownRole(vehicle)) then
local x, z = positionOf(vehicle) local x, z = positionOf(vehicle)
@@ -387,6 +395,8 @@ function ADPool.overview(adEnv)
attachedTo = attacher ~= nil and ADRuns.vehicleLabel(attacher) or nil, attachedTo = attacher ~= nil and ADRuns.vehicleLabel(attacher) or nil,
quarantineMin = id ~= nil and ADPool.quarantineMinutes(id, nowMs) or 0, quarantineMin = id ~= nil and ADPool.quarantineMinutes(id, nowMs) or 0,
reserved = id ~= nil and ADPool.isReserved(id), 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}) table.insert(implements, {id = id, label = ADRuns.vehicleLabel(vehicle), role = role, status = key, arg = arg})
end end
@@ -396,7 +406,7 @@ function ADPool.overview(adEnv)
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
local id = ADRunsController.getVehicleId(vehicle) local id = ADRunsController.getVehicleId(vehicle)
-- en traktor med et redskab der giver den en markrolle, står allerede i flåden som kombination -- 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 and ADPool.canPull(ADPool.ownRole(vehicle)) and ADFieldFleet.roleFromSpecs(ADCpBridge.getSpecs(vehicle)) == nil then
local x, z = positionOf(vehicle) local x, z = positionOf(vehicle)
local distance = ADFieldWork.distanceToNetwork(adEnv, x, z) local distance = ADFieldWork.distanceToNetwork(adEnv, x, z)
+28
View File
@@ -42,6 +42,28 @@ local function clampRange(value, default, min, max)
return math.max(min, math.min(max, math.floor(number))) return math.max(min, math.min(max, math.floor(number)))
end 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) local function copyRun(run)
return { return {
id = run.id, id = run.id,
@@ -64,6 +86,8 @@ local function copyRun(run)
loops = run.loops, loops = run.loops,
returnBelowPercent = run.returnBelowPercent, returnBelowPercent = run.returnBelowPercent,
vehicleIds = copyList(run.vehicleIds), vehicleIds = copyList(run.vehicleIds),
stopped = run.stopped == true,
wagons = ADRuns.copyWagons(run.wagons),
} }
end end
@@ -372,6 +396,8 @@ function ADRuns.toRecords(store)
storeAbove = run.storeAbovePercent, storeAbove = run.storeAbovePercent,
sellAbove = run.sellAbovePercent or 0, sellAbove = run.sellAbovePercent or 0,
collectRound = run.collectRound and 1 or 0, collectRound = run.collectRound and 1 or 0,
stopped = run.stopped and 1 or 0,
wagons = ADRuns.serializeWagons(run.wagons),
} }
end end
return records return records
@@ -424,6 +450,8 @@ function ADRuns.fromRecords(records)
loops = clampLoops(record.loops), loops = clampLoops(record.loops),
returnBelowPercent = clampPercent(record.returnBelow), returnBelowPercent = clampPercent(record.returnBelow),
vehicleIds = copyList(record.vehicleIds), vehicleIds = copyList(record.vehicleIds),
stopped = tonumber(record.stopped) == 1,
wagons = ADRuns.parseWagons(record.wagons),
}) })
store.nextId = math.max(store.nextId, id + 1) store.nextId = math.max(store.nextId, id + 1)
end end
+25 -3
View File
@@ -90,6 +90,7 @@ function ADRunsController.deleteRun(runId, adEnv)
for _, vehicleId in ipairs(run.vehicleIds) do for _, vehicleId in ipairs(run.vehicleIds) do
ADRunsController.stopIfRunning(ADRunsController.findVehicleById(adEnv, vehicleId), "run slettet") ADRunsController.stopIfRunning(ADRunsController.findVehicleById(adEnv, vehicleId), "run slettet")
end end
if ADFlowWagons ~= nil then pcall(ADFlowWagons.sendHome, adEnv, run) end
end end
ADRunsController.store = ADRuns.deleteRun(ADRunsController.store, runId) ADRunsController.store = ADRuns.deleteRun(ADRunsController.store, runId)
end end
@@ -1074,6 +1075,12 @@ end
-- og logges med årsag (samme nøgle som fanen viser). -- 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. -- Start én traktor i et run: valider, anvend run'et og start AutoDrive. Returnerer resultat-posten.
function ADRunsController.startVehicle(adEnv, run, vehicle) 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 validation = ADRunsController.validateVehicle(adEnv, run, vehicle)
local result = {vehicle = vehicle, ok = validation.ok, reason = validation.reason, missingBuilding = validation.missingBuilding} local result = {vehicle = vehicle, ok = validation.ok, reason = validation.reason, missingBuilding = validation.missingBuilding}
if not validation.ok then if not validation.ok then
@@ -1101,6 +1108,7 @@ function ADRunsController.startRun(adEnv, runId)
if run == nil then if run == nil then
return results return results
end end
run.stopped = false
local vehiclesById = getFarmVehiclesById(adEnv) local vehiclesById = getFarmVehiclesById(adEnv)
for _, vehicleId in ipairs(run.vehicleIds) do for _, vehicleId in ipairs(run.vehicleIds) do
local vehicle = vehiclesById[vehicleId] local vehicle = vehiclesById[vehicleId]
@@ -1132,11 +1140,13 @@ function ADRunsController.queueStartAll(adEnv)
if ADRunsController.getRunStatus(adEnv, run).isRunning then if ADRunsController.getRunStatus(adEnv, run).isRunning then
summary.alreadyRunning = summary.alreadyRunning + 1 summary.alreadyRunning = summary.alreadyRunning + 1
else else
run.stopped = false
for _, vehicleId in ipairs(run.vehicleIds) do for _, vehicleId in ipairs(run.vehicleIds) do
local vehicle = vehiclesById[vehicleId] local vehicle = vehiclesById[vehicleId]
if vehicle ~= nil then if vehicle ~= nil then
local validation = ADRunsController.validateVehicle(adEnv, run, vehicle) 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}) table.insert(queue, {runId = run.id, vehicleId = vehicleId})
else else
table.insert(summary.skipped, {vehicle = vehicle, ok = false, reason = validation.reason, missingBuilding = validation.missingBuilding}) table.insert(summary.skipped, {vehicle = vehicle, ok = false, reason = validation.reason, missingBuilding = validation.missingBuilding})
@@ -1175,6 +1185,7 @@ function ADRunsController.stopAllAndPark(adEnv)
end end
end end
ADRunsController.runningRuns[run.id] = nil ADRunsController.runningRuns[run.id] = nil
run.stopped = true
end end
if ADFieldWork ~= nil and ADFieldWork.stopAllAssignments ~= nil then if ADFieldWork ~= nil and ADFieldWork.stopAllAssignments ~= nil then
for _, stopped in ipairs(ADFieldWork.stopAllAssignments(adEnv) or {}) do 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 ADFieldSilos ~= nil and ADFieldSilos.stopAll ~= nil then pcall(ADFieldSilos.stopAll, adEnv) end
if ADFieldJobs ~= nil and ADFieldJobs.setPaused ~= nil then ADFieldJobs.setPaused(true) end if ADFieldJobs ~= nil and ADFieldJobs.setPaused ~= nil then ADFieldJobs.setPaused(true) end
for _, vehicle in ipairs(toPark) do for _, vehicle in ipairs(toPark) do
local park = ADFieldWork ~= nil and ADFieldWork.parkMarkerId ~= nil and ADFieldWork.parkMarkerId(adEnv, vehicle) or nil -- flow-vogn/puljeredskab bag traktoren: sæt det af på sin plads og kør hjem
if park ~= nil then 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) local ok = pcall(ADFieldWork.startAd, adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil)
if ok then summary.parked = summary.parked + 1 end if ok then summary.parked = summary.parked + 1 end
else else
@@ -1246,6 +1262,12 @@ function ADRunsController.stopRun(adEnv, runId)
end end
end end
ADRunsController.runningRuns[runId] = nil 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 return stopped
end end
+5
View File
@@ -48,6 +48,9 @@ function ADRunsStorage.writeRecords(xmlFile, records)
xmlFile:setInt(key .. "#storeAbove", record.storeAbove or ADRuns.DEFAULT_STORE_ABOVE_PERCENT) xmlFile:setInt(key .. "#storeAbove", record.storeAbove or ADRuns.DEFAULT_STORE_ABOVE_PERCENT)
xmlFile:setInt(key .. "#sellAbove", record.sellAbove or 0) xmlFile:setInt(key .. "#sellAbove", record.sellAbove or 0)
xmlFile:setInt(key .. "#collectRound", record.collectRound 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 for vehicleIndex, vehicleId in ipairs(record.vehicleIds) do
xmlFile:setString(string.format("%s.vehicle(%d)#id", key, vehicleIndex - 1), vehicleId) xmlFile:setString(string.format("%s.vehicle(%d)#id", key, vehicleIndex - 1), vehicleId)
end end
@@ -77,6 +80,8 @@ function ADRunsStorage.readRecords(xmlFile)
storeAbove = xmlFile:getInt(key .. "#storeAbove"), storeAbove = xmlFile:getInt(key .. "#storeAbove"),
sellAbove = xmlFile:getInt(key .. "#sellAbove"), sellAbove = xmlFile:getInt(key .. "#sellAbove"),
collectRound = xmlFile:getInt(key .. "#collectRound"), collectRound = xmlFile:getInt(key .. "#collectRound"),
stopped = xmlFile:getInt(key .. "#stopped"),
wagons = xmlFile:getString(key .. "#wagons"),
vehicleIds = {}, vehicleIds = {},
} }
xmlFile:iterate(key .. ".vehicle", function(_, vehicleKey) xmlFile:iterate(key .. ".vehicle", function(_, vehicleKey)
+4
View File
@@ -1272,6 +1272,10 @@ function ADSmartPickup:update(dt)
Logging.warning(ADSmartPickup.LOG_PREFIX .. "marksilo-flow fejlede: %s", tostring(errSilos)) Logging.warning(ADSmartPickup.LOG_PREFIX .. "marksilo-flow fejlede: %s", tostring(errSilos))
end end
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 if ADHitch ~= nil then
local okHitch, errHitch = pcall(ADHitch.tick, getAutoDriveEnv(), dt) local okHitch, errHitch = pcall(ADHitch.tick, getAutoDriveEnv(), dt)
if not okHitch then if not okHitch then
+1
View File
@@ -98,6 +98,7 @@
<text name="spu_pool_tractor_notInFleet" text="ikke i markflåden (flueben)%.0s"/> <text name="spu_pool_tractor_notInFleet" text="ikke i markflåden (flueben)%.0s"/>
<text name="spu_pool_tractor_offNetwork" text="står uden for AutoDrive-vejnettet%.0s"/> <text name="spu_pool_tractor_offNetwork" text="står uden for AutoDrive-vejnettet%.0s"/>
<text name="spu_pool_tractor_hitched" text="har et redskab på (kobl det af for at bruge den i puljen)%.0s"/> <text name="spu_pool_tractor_hitched" text="har et redskab på (kobl det af for at bruge den i puljen)%.0s"/>
<text name="spu_pool_implement_flow" text="hører til flowet '%s'"/>
<text name="spu_pool_implement_parked" text="parkeret ved %s"/> <text name="spu_pool_implement_parked" text="parkeret ved %s"/>
<text name="spu_pool_implement_noPark" text="ikke i puljen — mangler AD-parkering%.0s"/> <text name="spu_pool_implement_noPark" text="ikke i puljen — mangler AD-parkering%.0s"/>
<text name="spu_pool_implement_noConnect" text="ikke i puljen — mangler markøren &quot;Connect %s&quot;"/> <text name="spu_pool_implement_noConnect" text="ikke i puljen — mangler markøren &quot;Connect %s&quot;"/>
+1
View File
@@ -98,6 +98,7 @@
<text name="spu_pool_tractor_notInFleet" text="not in field fleet (tick)%.0s"/> <text name="spu_pool_tractor_notInFleet" text="not in field fleet (tick)%.0s"/>
<text name="spu_pool_tractor_offNetwork" text="off the AutoDrive network%.0s"/> <text name="spu_pool_tractor_offNetwork" text="off the AutoDrive network%.0s"/>
<text name="spu_pool_tractor_hitched" text="has an implement attached (detach it to use the pool)%.0s"/> <text name="spu_pool_tractor_hitched" text="has an implement attached (detach it to use the pool)%.0s"/>
<text name="spu_pool_implement_flow" text="belongs to flow '%s'"/>
<text name="spu_pool_implement_parked" text="parked at %s"/> <text name="spu_pool_implement_parked" text="parked at %s"/>
<text name="spu_pool_implement_noPark" text="not in pool — needs an AD park destination%.0s"/> <text name="spu_pool_implement_noPark" text="not in pool — needs an AD park destination%.0s"/>
<text name="spu_pool_implement_noConnect" text="not in pool — needs marker &quot;Connect %s&quot;"/> <text name="spu_pool_implement_noConnect" text="not in pool — needs marker &quot;Connect %s&quot;"/>
+2 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?> <?xml version="1.0" encoding="utf-8" standalone="no"?>
<modDesc descVersion="107"> <modDesc descVersion="107">
<author>masterdraco</author> <author>masterdraco</author>
<version>1.50.5.0</version> <version>1.51.0.0</version>
<title> <title>
<en>AD Smart Pickup</en> <en>AD Smart Pickup</en>
</title> </title>
@@ -48,6 +48,7 @@
<sourceFile filename="adCpResume.lua"/> <sourceFile filename="adCpResume.lua"/>
<sourceFile filename="adHitch.lua"/> <sourceFile filename="adHitch.lua"/>
<sourceFile filename="adPool.lua"/> <sourceFile filename="adPool.lua"/>
<sourceFile filename="adFlowWagons.lua"/>
<sourceFile filename="adFieldJobs.lua"/> <sourceFile filename="adFieldJobs.lua"/>
<sourceFile filename="adMonthView.lua"/> <sourceFile filename="adMonthView.lua"/>
<sourceFile filename="adProfiler.lua"/> <sourceFile filename="adProfiler.lua"/>
@@ -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 <vognens parkering>", 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.
+54
View File
@@ -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)
+3
View File
@@ -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: 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: 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) 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")) print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1) os.exit(failures == 0 and 0 or 1)
+13
View File
@@ -322,5 +322,18 @@ do
check("RS roundtrip", table.concat(ADRuns.getRun(back, run.id).storeBuildingIds, ","), "BGA500kw") check("RS roundtrip", table.concat(ADRuns.getRun(back, run.id).storeBuildingIds, ","), "BGA500kw")
end 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")) print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1) os.exit(failures == 0 and 0 or 1)