From 5a2701fc62c1315a1bd4b566bffcd3c3c60fc209 Mon Sep 17 00:00:00 2001 From: masterdraco Date: Mon, 28 Sep 2026 19:02:28 +0200 Subject: [PATCH] =?UTF-8?q?fix(pulje):=20review=20v1.50=20=E2=80=94=20egne?= =?UTF-8?q?=20t=C3=B8mmevogne=20virkede=20ikke=20under=20h=C3=B8st=20(glob?= =?UTF-8?q?al=20'implement'),=20hentning=20kan=20afbrydes=20(h=C3=B8st=20s?= =?UTF-8?q?lut/Stop/spiller),=20hjemtur=20ved=20t=C3=B8mmevogns-fejl,=20ma?= =?UTF-8?q?rksilo-l=C3=A5n=20kan=20ikke=20h=C3=A6nge=20i=20'henter'=20(tid?= =?UTF-8?q?sgr=C3=A6nse,=20spiller,=20fejl=20i=20start),=20ingen=20ekstra?= =?UTF-8?q?=20parkeringstur=20med=20puljevogn,=20Stop=20holder=20en=20bar?= =?UTF-8?q?=20mark;=20regressionstest=20for=20fejlagtige=20globale=20(v1.5?= =?UTF-8?q?0.1.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FS25_ADSmartPickup/adFieldFleet.lua | 6 ++++- FS25_ADSmartPickup/adFieldModel.lua | 5 ++++ FS25_ADSmartPickup/adFieldSilos.lua | 38 ++++++++++++++++++++++++----- FS25_ADSmartPickup/adFieldWork.lua | 27 +++++++++++++++++--- FS25_ADSmartPickup/adHitch.lua | 10 ++++++++ FS25_ADSmartPickup/adPool.lua | 15 ++++++++++-- FS25_ADSmartPickup/modDesc.xml | 2 +- tests/test_adFieldFleet.lua | 1 + tests/test_adFieldModel.lua | 6 +++++ tests/test_adHitch.lua | 9 +++++++ tests/test_noStrayGlobals.lua | 18 ++++++++++++++ 11 files changed, 123 insertions(+), 14 deletions(-) create mode 100644 tests/test_noStrayGlobals.lua diff --git a/FS25_ADSmartPickup/adFieldFleet.lua b/FS25_ADSmartPickup/adFieldFleet.lua index b477791..7c54ce2 100644 --- a/FS25_ADSmartPickup/adFieldFleet.lua +++ b/FS25_ADSmartPickup/adFieldFleet.lua @@ -40,7 +40,11 @@ end ADFieldFleet.EXTRA_ROLES = {plow = {plow = true, cultivator = true}} function ADFieldFleet.rolesFor(role) - return ADFieldFleet.EXTRA_ROLES[role] + local roles = ADFieldFleet.EXTRA_ROLES[role] + if roles == nil then return nil end + local copy = {} + for key, value in pairs(roles) do copy[key] = value end + return copy end -- Kandidater med rollen, sorteret nærmest først, delt i brugbare og afviste (med første grund). diff --git a/FS25_ADSmartPickup/adFieldModel.lua b/FS25_ADSmartPickup/adFieldModel.lua index b89116e..03a334c 100644 --- a/FS25_ADSmartPickup/adFieldModel.lua +++ b/FS25_ADSmartPickup/adFieldModel.lua @@ -159,6 +159,7 @@ end function ADFieldModel.setTarget(field, fruitName) local copy = copyField(field) copy.plan.targetFruit = fruitName or "" + copy.held = nil copy.plan.mode = copy.plan.targetFruit ~= "" and "auto" or "manual" if not ADFieldModel.isWorking(field) and field.stage == "done" then copy.stage = "idle" @@ -172,6 +173,7 @@ end function ADFieldModel.shouldStartBare(field, kind) if field.plan.targetFruit == nil or field.plan.targetFruit == "" then return false end -- kun fra idle: efter egen såning står marken "done" (nysået kan læses som bar) — ingen dobbelt-såning + if field.held and not field.startRequested then return false end return kind == "bare" and field.stage == "idle" end @@ -277,6 +279,7 @@ end -- currentFruit (valgfri): afgrøden på marken nu; med mål-afgrøde udfyldes planen ud fra den. function ADFieldModel.start(field, kind, needs, currentFruit) local copy = copyField(field) + copy.held = nil copy.plan = ADFieldModel.applyTarget(copy.plan, kind, currentFruit) local stages = ADFieldModel.workStages(copy.plan, kind, needs) copy.stage = stages[1] or "done" @@ -320,6 +323,8 @@ function ADFieldModel.stop(field) end copy.startRequested = false copy.reason = nil + -- stoppet af spilleren: en bar mark med mål-afgrøde starter ikke selv igen (Start eller nyt mål slipper) + copy.held = true return copy end diff --git a/FS25_ADSmartPickup/adFieldSilos.lua b/FS25_ADSmartPickup/adFieldSilos.lua index 24d13a0..9a0a70f 100644 --- a/FS25_ADSmartPickup/adFieldSilos.lua +++ b/FS25_ADSmartPickup/adFieldSilos.lua @@ -17,6 +17,7 @@ ADFieldSilos.CARGO_MIN_LITERS = 100 -- rest under dette tæller ikke som last ADFieldSilos.FARM_RADIUS = 100 -- Siloer højst så langt fra en nabo hører til samme klynge (Farma-rækker: 15 m mellem siloer, 18 m mellem rækker) ADFieldSilos.CLUSTER_LINK = 60 +ADFieldSilos.FETCH_TIMEOUT_MS = 300000 -- så længe må en traktor være om at hente en puljevogn ADFieldSilos.MOVING_SPEED = 0.00028 -- m/ms, samme grænse som AutoDrive ADFieldSilos.FILE_NAME = "ADSmartPickup_fieldsilos.xml" ADFieldSilos.ROOT = "ADSmartPickupFieldSilos" @@ -340,7 +341,7 @@ local function fillTypeName(fillType) return desc ~= nil and (desc.title or desc.name) or tostring(fillType) end -local function dispatchOne(adEnv, order, vehicle, silosById, storesById) +local function dispatchOne(adEnv, order, vehicle, silosById, storesById, isPool) local module = vehicle.ad.stateModule module:setMode(adEnv.AutoDrive.MODE_PICKUPANDDELIVER) module:setFirstMarkerByWayPointId(order.siloMarkerId) @@ -349,7 +350,9 @@ local function dispatchOne(adEnv, order, vehicle, silosById, storesById) module:setFillType(order.fillType) module.loopCounter = 1 if module.setLoopsDone ~= nil then module:setLoopsDone(0) end - ADFieldWork.enableParkAtJobFinished(vehicle) + -- puljevogn: ingen AD-parkering — ved turens slut sætter returnHome vognen af og kører traktoren hjem + -- (med AD-parkering kørte den først hjem med vognen og så tilbage efter den) + if not isPool then ADFieldWork.enableParkAtJobFinished(vehicle) end -- rotation fra: ellers kan modens egne pickup-hooks sende vognen til en anden silo i mappen if ADRunsController ~= nil and ADRunsController.setSetting ~= nil then ADRunsController.setSetting(adEnv, vehicle, "rotateTargets", adEnv.AutoDrive.RT_NONE) @@ -371,7 +374,6 @@ local function dispatchPool(adEnv, order, pair, silosById, storesById) local owner = {siloId = order.siloId, storeId = order.storeId, siloName = silo ~= nil and silo.name or order.siloId, fillType = order.fillType, liters = order.liters, startedMs = nowMs, lastProgressMs = nowMs, lastFill = 0, fetching = true} loans[tractor] = owner - ADPool.reserve(owner, {pair.implement.id, pair.tractor.id}) ADHitch.fetch(adEnv, tractor, pair.implement.connectId, function(_, ok, detail) ADPool.release(owner) if loans[tractor] ~= owner then return end -- lånet blev afbrudt imens (Stop alt / flow slået fra) @@ -385,8 +387,14 @@ local function dispatchPool(adEnv, order, pair, silosById, storesById) if park ~= nil then ADFieldWork.startAd(adEnv, tractor, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end return end - dispatchOne(adEnv, order, tractor, silosById, storesById) + local okDispatch, err = pcall(dispatchOne, adEnv, order, tractor, silosById, storesById, true) + if not okDispatch then + Logging.warning(ADFieldSilos.LOG_PREFIX .. "'%s' kunne ikke starte turen med puljevognen: %s", tostring(ADRuns.vehicleLabel(tractor)), tostring(err)) + ADFieldSilos.releaseVehicle(adEnv, tractor, "failed") + end end) + -- reservationen først når hentningen er sat i gang (fejler fetch, er der intet at frigive) + ADPool.reserve(owner, {pair.implement.id, pair.tractor.id}) Logging.info(ADFieldSilos.LOG_PREFIX .. "'%s' henter puljevognen '%s' til %d l %s i %s", tostring(ADRuns.vehicleLabel(tractor)), tostring(pair.implement.name), math.floor(order.liters), fillTypeName(order.fillType), tostring(silo ~= nil and silo.name or order.siloId)) end @@ -395,6 +403,11 @@ local function release(adEnv, vehicle, reason) local loan = loans[vehicle] loans[vehicle] = nil if loan == nil then return end + if loan.fetching then + -- henter stadig puljevognen: afbryd koblingen og frigiv reservationen + if ADHitch ~= nil then ADHitch.cancel(vehicle) end + if ADPool ~= nil then ADPool.release(loan) end + end if reason == "stuck" or reason == "failed" then local k = ADFieldSiloPlanner.key(loan.siloId, loan.fillType) paused[k] = nowMs + ADFieldSilos.PAUSE_MS @@ -416,6 +429,10 @@ local function release(adEnv, vehicle, reason) end end +function ADFieldSilos.releaseVehicle(adEnv, vehicle, reason) + release(adEnv, vehicle, reason) +end + function ADFieldSilos.stopAll(adEnv) local list = {} for vehicle in pairs(loans) do table.insert(list, vehicle) end @@ -431,8 +448,17 @@ local function checkLoans(adEnv) else local ok, err = pcall(function() local loan = loans[vehicle] - -- henter puljevognen: AD holder stille under koblingen — ikke "færdig" - if loan.fetching then return end + -- henter puljevognen: AD holder stille under koblingen — ikke "færdig"; men spilleren kan overtage, + -- og en hentning der ikke bliver færdig (trafik, rute), opgives + if loan.fetching then + local controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled() or false + if controlled then + release(adEnv, vehicle, "player") + elseif nowMs - (loan.startedMs or nowMs) > ADFieldSilos.FETCH_TIMEOUT_MS then + release(adEnv, vehicle, "stuck") + end + return + end local stateModule = vehicle.ad ~= nil and vehicle.ad.stateModule or nil local fill = fillLevel(adEnv, vehicle) local vehicleState = { diff --git a/FS25_ADSmartPickup/adFieldWork.lua b/FS25_ADSmartPickup/adFieldWork.lua index 1a934f2..3e08820 100644 --- a/FS25_ADSmartPickup/adFieldWork.lua +++ b/FS25_ADSmartPickup/adFieldWork.lua @@ -287,6 +287,10 @@ end function ADFieldWork.release(assignment, phase) restoreToolOffset(assignment) if assignment.handle ~= nil then assignment.handle.cancelled = true end + -- henter stadig puljeredskabet: afbryd koblingen, så det ikke kobles på uden en opgave + if assignment.phase == "fetching" and assignment.hitch ~= nil and not assignment.hitch.done and ADHitch ~= nil then + ADHitch.cancel(assignment.vehicle) + end assignment.phase = phase or "released" if ADPool ~= nil and assignment.pool ~= nil then ADPool.release(assignment) end ADFieldJobs.state.assignments[assignment.vehicleId] = nil @@ -447,8 +451,6 @@ function ADFieldWork.dispatch(adEnv, field, info, decision) local offNetwork = {} local accept = function(candidate) if roleAccept ~= nil and not roleAccept(candidate) then return false end - -- tømmevogn: skal kunne bære høstens vare - if role == "unloader" and not ADPool.canCarry(implement, info.harvestFillType) then return false end -- manuel transport: spilleren kører den — afstand til AutoDrive-vejnettet er ligegyldig if role == "harvester" and candidate.manual then return true end local d = candidate.x ~= nil and ADFieldWork.distanceToNetwork(adEnv, candidate.x, candidate.z) or nil @@ -697,6 +699,7 @@ local function beginUnloader(adEnv, field, info, assignment) local store, why = ADFieldWork.chooseStore(adEnv, field, info, vehicle, fillType) if store == nil then ADFieldWork.release(assignment) + ADHitch.returnHome(adEnv, vehicle) fail(field.id, "intet lager til høsten: " .. tostring(why)) return end @@ -716,11 +719,13 @@ local function beginUnloader(adEnv, field, info, assignment) log(field.id, "aflæsningsvogn '%s' følger høsteren -> %s", ADRuns.vehicleLabel(vehicle), tostring(store.name)) else ADFieldWork.release(assignment) + ADHitch.returnHome(adEnv, vehicle) fail(field.id, "aflæsningsvogn: " .. tostring(reason)) end end) if handle == nil then ADFieldWork.release(assignment) + ADHitch.returnHome(adEnv, vehicle) fail(field.id, "aflæsningsvogn: " .. tostring(err)) return end @@ -1284,7 +1289,16 @@ end -- Høsten er færdig: aflæsningsvognene stoppes; har de rest, kører den til lager, ellers parkering. function ADFieldWork.windDownHelpers(adEnv, fieldId, stage) for _, assignment in ipairs(ADFieldJobs.assignmentsFor(fieldId)) do - if assignment.helper and assignment.stage == stage then + if assignment.helper and assignment.stage == stage and assignment.phase == "fetching" then + -- høsten sluttede mens puljevognen blev hentet: afbryd, vognen tilbage hvis den nåede at komme på + local vehicle = assignment.vehicle + ADFieldWork.release(assignment) + if not ADHitch.returnHome(adEnv, vehicle) then + local park = parkMarkerId(adEnv, vehicle) + if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end + end + log(fieldId, "tømmevogn '%s' afbrudt under hentning — høsten er færdig", ADRuns.vehicleLabel(vehicle)) + elseif assignment.helper and assignment.stage == stage then if assignment.handle ~= nil then assignment.handle.cancelled = true end ADCpBridge.stop(assignment.vehicle) assignment.phase = "working" @@ -1315,7 +1329,12 @@ function ADFieldWork.stopAll(adEnv, fieldId) for _, assignment in ipairs(ADFieldJobs.assignmentsFor(fieldId)) do pcall(stopAd, assignment.vehicle) ADCpBridge.stop(assignment.vehicle) + local wasFetching = assignment.phase == "fetching" ADFieldWork.release(assignment) - pcall(ADHitch.returnHome, adEnv, assignment.vehicle) + local okHome, pooled = pcall(ADHitch.returnHome, adEnv, assignment.vehicle) + if wasFetching and not (okHome and pooled) then + local park = parkMarkerId(adEnv, assignment.vehicle) + if park ~= nil then pcall(startAd, adEnv, assignment.vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end + end end end diff --git a/FS25_ADSmartPickup/adHitch.lua b/FS25_ADSmartPickup/adHitch.lua index 77fa89b..92ba065 100644 --- a/FS25_ADSmartPickup/adHitch.lua +++ b/FS25_ADSmartPickup/adHitch.lua @@ -253,6 +253,16 @@ local function start(adEnv, vehicle, markerId, action, onDone) return job end +-- Afbryd et hente-/koble-job (Stop alt, høsten slut, spilleren overtager): intet kobles på bagefter, og +-- onDone kaldes ikke. -> true hvis der var et job. +function ADHitch.cancel(vehicle) + local had = jobs[vehicle] ~= nil + jobs[vehicle] = nil + if ADPool ~= nil and ADPool.setReturning ~= nil then ADPool.setReturning(vehicle, false) end + if had and vehicle.stopAutoDrive ~= nil then pcall(vehicle.stopAutoDrive, vehicle) end + return had +end + -- Kør til en Connect-markør og kobl på. onDone(vehicle, ok, redskab eller årsag) kaldes én gang. function ADHitch.fetch(adEnv, vehicle, markerId, onDone) return start(adEnv, vehicle, markerId, "attach", onDone) diff --git a/FS25_ADSmartPickup/adPool.lua b/FS25_ADSmartPickup/adPool.lua index 7d8f306..3b1c288 100644 --- a/FS25_ADSmartPickup/adPool.lua +++ b/FS25_ADSmartPickup/adPool.lua @@ -277,8 +277,18 @@ function ADPool.readNeededHp(vehicle) return item.specs ~= nil and ADPool.numberOf(item.specs.neededPower) or nil end --- Traktorens effekt i hk: motorens topeffekt (kW) omregnet; ellers butiksdata. +local tractorHpCache = setmetatable({}, {__mode = "k"}) -- vehicle -> hk eller false + +-- Traktorens effekt i hk: motorens topeffekt (kW) omregnet; ellers butiksdata. Gemmes pr. traktor. local function tractorHpOf(vehicle) + local cached = tractorHpCache[vehicle] + if cached ~= nil then return cached or nil end + local value = ADPool.readTractorHp(vehicle) + tractorHpCache[vehicle] = value or false + return value +end + +function ADPool.readTractorHp(vehicle) local motor = vehicle.spec_motorized ~= nil and vehicle.spec_motorized.motor or nil local kw = motor ~= nil and motor.peakMotorPower or nil if type(kw) == "number" and kw > 0 then return kw * 1.35962 end @@ -385,8 +395,9 @@ function ADPool.overview(adEnv) local state = ADFieldJobs.state 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 - and ADPool.canPull(ADPool.ownRole(vehicle)) then + 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) local assignment = state.assignments[id] diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml index 1c398e3..237a24e 100644 --- a/FS25_ADSmartPickup/modDesc.xml +++ b/FS25_ADSmartPickup/modDesc.xml @@ -1,7 +1,7 @@ masterdraco - 1.50.0.0 + 1.50.1.0 <en>AD Smart Pickup</en> diff --git a/tests/test_adFieldFleet.lua b/tests/test_adFieldFleet.lua index 5792e63..4e1846d 100644 --- a/tests/test_adFieldFleet.lua +++ b/tests/test_adFieldFleet.lua @@ -140,5 +140,6 @@ check("FLP plov kan pløje", F.rolesFor("plow").plow, true) check("FLP kultivator kan ikke pløje", F.rolesFor("cultivator"), nil) local plough = {id = "p", role = "plow", roles = F.rolesFor("plow"), enabled = true, x = 0, z = 0, width = 3} check("FLP plov vælges til kultivering", (F.pick({plough}, "cultivator", 0, 0, nil)).id, "p") +check("FLP rolesFor giver en kopi", F.rolesFor("plow") ~= F.rolesFor("plow"), true) print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) os.exit(failures == 0 and 0 or 1) diff --git a/tests/test_adFieldModel.lua b/tests/test_adFieldModel.lua index 1a27acf..b0a5e9e 100644 --- a/tests/test_adFieldModel.lua +++ b/tests/test_adFieldModel.lua @@ -310,5 +310,11 @@ check("M24 klar = ikke passiv", M.isPassive(M.with(calm, {stage = "ready"})), fa check("M24 i gang = ikke passiv", M.isPassive(started), false) check("M24 Start nu = ikke passiv", M.isPassive(M.with(calm, {startRequested = true})), false) +-- Stop holder en bar mark (ellers starter klargøringen straks forfra); Start eller ny mål-afgrøde slipper +local stoppedBare = M.stop(M.with(target, {stage = "cultivating"})) +check("M21 stoppet bar mark starter ikke selv", M.shouldStartBare(stoppedBare, "bare"), false) +check("M21 Start slipper", M.shouldStartBare(M.with(stoppedBare, {startRequested = true}), "bare"), true) +check("M21 ny mål-afgrøde slipper", M.shouldStartBare(M.setTarget(stoppedBare, "BARLEY"), "bare"), true) +check("M21 start nulstiller hold (næste sæson starter selv)", M.start(stoppedBare, "bare", {}, "WHEAT").held, nil) print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) os.exit(failures == 0 and 0 or 1) diff --git a/tests/test_adHitch.lua b/tests/test_adHitch.lua index eb0afe6..e489da2 100644 --- a/tests/test_adHitch.lua +++ b/tests/test_adHitch.lua @@ -89,5 +89,14 @@ check("hjem: to trin", #home, 2); check("hjem: redskab først", home[1], 4); che check("hjem: uden redskab kun traktor", #H.homeRoute({}, 2), 1) check("hjem: samme parkering to gange tæller én", #H.homeRoute({4, 4}, 2), 2) check("hjem: uden traktorparkering", #H.homeRoute({4}, nil), 1) +-- afbrydelse: et hente-job kan annulleres (Stop alt, høst slut, spilleren overtager) +ADFieldWork = ADFieldWork or {startAd = function() end} +Logging = Logging or {info = function() end, warning = function() end} +local stubEnv = {ADGraphManager = {getMapMarkerByWayPointId = function() return {name = "Connect X"} end}, AutoDrive = {MODE_DRIVETO = 1}} +local tractorStub = {getFullName = function() return "T" end, stopAutoDrive = function(v) v.stopped = true end} +H.fetch(stubEnv, tractorStub, 9, function() end) +check("annullér: job fandtes", H.cancel(tractorStub), true) +check("annullér: AD stoppet", tractorStub.stopped, true) +check("annullér: intet job tilbage", H.cancel(tractorStub), false) print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) os.exit(failures == 0 and 0 or 1) diff --git a/tests/test_noStrayGlobals.lua b/tests/test_noStrayGlobals.lua new file mode 100644 index 0000000..d47903f --- /dev/null +++ b/tests/test_noStrayGlobals.lua @@ -0,0 +1,18 @@ +-- Kør: luajit tests/test_noStrayGlobals.lua (fra repo-roden) +-- Fanger lokale navne der ved en fejl bruges som globale (fx 'implement' uden for sin closure — v1.49 gjorde +-- egne tømmevogne ubrugelige under høst). Læser luajit-bytecode: GGET/GSET af typiske lokale navne. +local SUSPECT = {implement = true, candidate = true, vehicle = true, rig = true, pair = true, assignment = true, + tractor = true, loan = true, field = true, info = true, marker = true, job = true, owner = true, order = true} +local failures = 0 +local files = io.popen("ls FS25_ADSmartPickup/*.lua FS25_ADSmartPickup/gui/*.lua"):read("*a") +for file in files:gmatch("[^\n]+") do + local listing = io.popen("luajit -bl " .. file .. " 2>&1"):read("*a") + for op, name in listing:gmatch("(G[GS]ET)%s+%d+%s+%d+%s+; \"([%w_]+)\"") do + if SUSPECT[name] then + failures = failures + 1 + print(string.format("FAIL %s: %s af global '%s'", file, op, name)) + end + end +end +print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) +os.exit(failures == 0 and 0 or 1)