From fde3fd4399cdc71361ebd9bf5b1e1b6700ff65e2 Mon Sep 17 00:00:00 2001 From: masterdraco Date: Mon, 28 Sep 2026 17:07:07 +0200 Subject: [PATCH] =?UTF-8?q?feat(kobling):=20Connect-mark=C3=B8rer=20?= =?UTF-8?q?=E2=80=94=20traktor=20der=20k=C3=B8rer=20AD=20'K=C3=B8r=20til'?= =?UTF-8?q?=20en=20'Connect=20'-mark=C3=B8r=20kobler=20redskabet=20b?= =?UTF-8?q?ag=20sig=20p=C3=A5=20ved=20ankomst;=20ADHitch.fetch=20til=20flo?= =?UTF-8?q?ws,=20konsol=20spuConnect=20(v1.46.0.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FS25_ADSmartPickup/adHitch.lua | 182 +++++++++++++++++++++++++++ FS25_ADSmartPickup/adSmartPickup.lua | 6 + FS25_ADSmartPickup/modDesc.xml | 3 +- tests/test_adHitch.lua | 46 +++++++ 4 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 FS25_ADSmartPickup/adHitch.lua create mode 100644 tests/test_adHitch.lua diff --git a/FS25_ADSmartPickup/adHitch.lua b/FS25_ADSmartPickup/adHitch.lua new file mode 100644 index 0000000..bd78a7c --- /dev/null +++ b/FS25_ADSmartPickup/adHitch.lua @@ -0,0 +1,182 @@ +-- AD Hitch +-- Kobler redskaber på med AutoDrive. En markør der hedder "Connect " (eller "Kobl ") ligger lige +-- under vognens trækøje, med et bakkespor ned til den. Når en traktor har kørt AD "Kør til" til sådan en markør +-- og står stille, kobles det redskab på, som spillet selv finder inden for koblingsafstand (samme kald som +-- Courseplay bruger til skærebord: updateVehiclesInAttachRange + attachImplementFromInfo). +-- ADHitch.fetch() er indgangen for flows/markarbejde: kør til markøren, kobl på, meld tilbage. + +ADHitch = {} +ADHitch.LOG_PREFIX = "ADSmartPickup: kobling: " +ADHitch.PREFIXES = {"connect", "kobl"} +ADHitch.ARRIVE_DISTANCE = 5 -- m fra markøren, AD standser traktoren tæt på waypointet +ADHitch.STILL_SPEED = 1 -- km/t +ADHitch.START_GRACE_MS = 3000 -- så længe må AD være om at komme i gang efter fetch +ADHitch.ATTACH_TIMEOUT_MS = 8000 -- så længe forsøges koblingen efter ankomst +ADHitch.TICK_MS = 250 + +-- ---- ren logik ---- + +-- "Connect Bredal" -> "Bredal"; "Connect" -> ""; andre navne -> nil +function ADHitch.connectTarget(name) + if type(name) ~= "string" then return nil end + local trimmed = name:match("^%s*(.-)%s*$") + local lower = trimmed:lower() + for _, prefix in ipairs(ADHitch.PREFIXES) do + if lower == prefix then return "" end + if lower:sub(1, #prefix + 1) == prefix .. " " then + return (trimmed:sub(#prefix + 2):match("^%s*(.-)$")) + end + end + return nil +end + +-- state = {adActive, distance, speed} +function ADHitch.hasArrived(state) + return not state.adActive and state.distance <= ADHitch.ARRIVE_DISTANCE and state.speed <= ADHitch.STILL_SPEED +end + +function ADHitch.newJob(markerId, markerName) + return {markerId = markerId, markerName = markerName, state = "driving", elapsed = 0, attachElapsed = 0, + seenActive = false} +end + +-- Et skridt i jobbet. attached = lykkedes koblingsforsøget i denne tick (kun relevant i "attaching"). +-- Tilstande: driving -> attaching -> done | failed (reason: stoppedAway / noAttachable) +function ADHitch.step(job, state, dt, attached) + if job.state == "driving" then + job.elapsed = job.elapsed + dt + if state.adActive then + job.seenActive = true + elseif ADHitch.hasArrived(state) then + job.state = "attaching" + elseif state.distance > ADHitch.ARRIVE_DISTANCE and (job.seenActive or job.elapsed >= ADHitch.START_GRACE_MS) then + job.state, job.reason = "failed", "stoppedAway" + end + elseif job.state == "attaching" then + if attached then + job.state = "done" + else + job.attachElapsed = job.attachElapsed + dt + if job.attachElapsed >= ADHitch.ATTACH_TIMEOUT_MS then job.state, job.reason = "failed", "noAttachable" end + end + end + return job +end + +-- ---- spil ---- + +local jobs = setmetatable({}, {__mode = "k"}) -- [vehicle] = job (+ onDone) +local tickTimer = 0 + +local function vehicleName(vehicle) + return vehicle.getFullName ~= nil and vehicle:getFullName() or tostring(vehicle.configFileName) +end + +local function stateModule(vehicle) + return vehicle.ad ~= nil and vehicle.ad.stateModule or nil +end + +local function vehicleState(adEnv, vehicle, markerId) + local module = stateModule(vehicle) + local wayPoint = adEnv.ADGraphManager:getWayPointById(markerId) + local distance = math.huge + if wayPoint ~= nil and vehicle.rootNode ~= nil then + local x, _, z = getWorldTranslation(vehicle.rootNode) + distance = math.sqrt((x - wayPoint.x) ^ 2 + (z - wayPoint.z) ^ 2) + end + return { + adActive = module ~= nil and module:isActive(), + distance = distance, + speed = vehicle.getLastSpeed ~= nil and vehicle:getLastSpeed() or 0, + } +end + +-- Kobler det redskab på, som spillet finder inden for koblingsafstand. -> ok, redskab eller årsag +function ADHitch.tryAttach(vehicle) + local spec = vehicle.spec_attacherJoints + if spec == nil or AttacherJoints == nil or AttacherJoints.updateVehiclesInAttachRange == nil then + return false, "noJoints" + end + AttacherJoints.updateVehiclesInAttachRange(vehicle, AttacherJoints.MAX_ATTACH_DISTANCE_SQ, + AttacherJoints.MAX_ATTACH_ANGLE, true) + local info = spec.attachableInfo + if info == nil or info.attachable == nil then return false, "noAttachable" end + local attacher = info.attacherVehicle or vehicle + local farmId = vehicle.getActiveFarm ~= nil and vehicle:getActiveFarm() or vehicle:getOwnerFarmId() + local allowed, warning = info.attachable:isAttachAllowed(farmId, attacher) + if not allowed then return false, tostring(warning or "notAllowed") end + attacher:attachImplementFromInfo(info) + return true, info.attachable +end + +-- Kør til en Connect-markør og kobl på. onDone(vehicle, ok, implementOrReason) kaldes én gang. +function ADHitch.fetch(adEnv, vehicle, markerId, onDone) + local marker = adEnv.ADGraphManager:getMapMarkerByWayPointId(markerId) + local job = ADHitch.newJob(markerId, marker ~= nil and marker.name or tostring(markerId)) + job.onDone = onDone + jobs[vehicle] = job + ADFieldWork.startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, markerId) + Logging.info(ADHitch.LOG_PREFIX .. "'%s' kører til '%s'", vehicleName(vehicle), job.markerName) + return job +end + +-- Spilleren sender selv en traktor til en Connect-markør med AD "Kør til": den kobles også på. +local function watchManualTrips(adEnv) + for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do + local module = stateModule(vehicle) + if jobs[vehicle] == nil and vehicle.spec_attacherJoints ~= nil and module ~= nil and module:isActive() + and module:getMode() == adEnv.AutoDrive.MODE_DRIVETO then + local marker = module:getFirstMarker() + if marker ~= nil and ADHitch.connectTarget(marker.name) ~= nil then + jobs[vehicle] = ADHitch.newJob(marker.id, marker.name) + end + end + end +end + +local function finish(vehicle, job, ok, detail) + jobs[vehicle] = nil + if ok then + Logging.info(ADHitch.LOG_PREFIX .. "'%s' koblede '%s' på ved '%s'", vehicleName(vehicle), vehicleName(detail), + job.markerName) + else + Logging.warning(ADHitch.LOG_PREFIX .. "'%s' kunne ikke koble på ved '%s': %s", vehicleName(vehicle), + job.markerName, tostring(detail)) + end + if job.onDone ~= nil then pcall(job.onDone, vehicle, ok, detail) end +end + +function ADHitch.tick(adEnv, dt) + if adEnv == nil or adEnv.ADGraphManager == nil then return end + tickTimer = tickTimer + dt + if tickTimer < ADHitch.TICK_MS then return end + local elapsed = tickTimer + tickTimer = 0 + watchManualTrips(adEnv) + for vehicle, job in pairs(jobs) do + local attached, detail = false, nil + if job.state == "attaching" then + attached, detail = ADHitch.tryAttach(vehicle) + if not attached then job.lastReason = detail end + end + ADHitch.step(job, vehicleState(adEnv, vehicle, job.markerId), elapsed, attached) + if job.state == "done" then + finish(vehicle, job, true, detail) + elseif job.state == "failed" then + finish(vehicle, job, false, job.lastReason or job.reason) + end + end +end + +-- Konsol: "spuConnect" kobler det nærmeste redskab på den maskine spilleren sidder i. +function ADHitch:consoleConnect() + local vehicle = g_localPlayer ~= nil and g_localPlayer.getCurrentVehicle ~= nil and g_localPlayer:getCurrentVehicle() or nil + if vehicle == nil then return "spuConnect: sæt dig i en traktor først" end + local ok, detail = ADHitch.tryAttach(vehicle) + if ok then return "spuConnect: koblede " .. vehicleName(detail) .. " på" end + return "spuConnect: intet koblet på (" .. tostring(detail) .. ")" +end + +if addConsoleCommand ~= nil then + addConsoleCommand("spuConnect", "Kobler nærmeste redskab på den maskine du sidder i", "consoleConnect", ADHitch) +end diff --git a/FS25_ADSmartPickup/adSmartPickup.lua b/FS25_ADSmartPickup/adSmartPickup.lua index dc4472b..cd109c5 100644 --- a/FS25_ADSmartPickup/adSmartPickup.lua +++ b/FS25_ADSmartPickup/adSmartPickup.lua @@ -1272,6 +1272,12 @@ function ADSmartPickup:update(dt) Logging.warning(ADSmartPickup.LOG_PREFIX .. "marksilo-flow fejlede: %s", tostring(errSilos)) end end + if ADHitch ~= nil then + local okHitch, errHitch = pcall(ADHitch.tick, getAutoDriveEnv(), dt) + if not okHitch then + Logging.warning(ADSmartPickup.LOG_PREFIX .. "kobling fejlede: %s", tostring(errHitch)) + end + end if ADStuckDiag ~= nil then pcall(ADStuckDiag.tick, dt) end if ADRunsController ~= nil and ADRunsController.adoptLoadedThrottled ~= nil then pcall(ADRunsController.adoptLoadedThrottled, getAutoDriveEnv(), dt) diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml index 60ce81e..d3c4739 100644 --- a/FS25_ADSmartPickup/modDesc.xml +++ b/FS25_ADSmartPickup/modDesc.xml @@ -1,7 +1,7 @@ masterdraco - 1.45.1.0 + 1.46.0.0 <en>AD Smart Pickup</en> @@ -46,6 +46,7 @@ + diff --git a/tests/test_adHitch.lua b/tests/test_adHitch.lua new file mode 100644 index 0000000..4a17a66 --- /dev/null +++ b/tests/test_adHitch.lua @@ -0,0 +1,46 @@ +-- Kør: luajit tests/test_adHitch.lua (fra repo-roden) +dofile("FS25_ADSmartPickup/adHitch.lua") +local H = ADHitch +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 +-- markørnavne: "Connect " (store/små bogstaver ligegyldigt), også dansk "Kobl " +check("navn: Connect Bredal", H.connectTarget("Connect Bredal"), "Bredal") +check("navn: små bogstaver", H.connectTarget("connect bredal"), "bredal") +check("navn: dansk Kobl", H.connectTarget("Kobl Vogn 2"), "Vogn 2") +check("navn: kun Connect", H.connectTarget("Connect"), "") +check("navn: Connected er ikke en koblingsmarkør", H.connectTarget("Connected silo"), nil) +check("navn: almindelig markør", H.connectTarget("Bredal"), nil) +check("navn: nil", H.connectTarget(nil), nil) +-- ankomst: AD er stoppet, traktoren står stille tæt på markøren +check("ankomst: stoppet ved markør", H.hasArrived({adActive = false, distance = 1.2, speed = 0.1}), true) +check("ankomst: AD kører stadig", H.hasArrived({adActive = true, distance = 0.5, speed = 0}), false) +check("ankomst: for langt væk", H.hasArrived({adActive = false, distance = 9, speed = 0}), false) +check("ankomst: ruller stadig", H.hasArrived({adActive = false, distance = 1, speed = 3}), false) +-- vagten: efter ankomst forsøges koblingen et stykke tid, derefter opgives den +local job = H.newJob(25, "Connect Bredal") +check("job: venter på AD", job.state, "driving") +H.step(job, {adActive = true, distance = 30, speed = 10}, 1000, false) +check("job: kører stadig", job.state, "driving") +H.step(job, {adActive = false, distance = 1, speed = 0}, 500, false) +check("job: ankommet, kobling ikke lykkedes endnu", job.state, "attaching") +H.step(job, {adActive = false, distance = 1, speed = 0}, 500, true) +check("job: koblet på", job.state, "done") +local failJob = H.newJob(25, "Connect Bredal") +H.step(failJob, {adActive = false, distance = 1, speed = 0}, 500, false) +for _ = 1, 20 do H.step(failJob, {adActive = false, distance = 1, speed = 0}, 500, false) end +check("job: opgivet efter vagt-tid", failJob.state, "failed") +check("job: årsag", failJob.reason, "noAttachable") +local lostJob = H.newJob(25, "Connect Bredal") +H.step(lostJob, {adActive = false, distance = 40, speed = 0}, 500, false) +check("job: AD ikke startet endnu (opstart-frist)", lostJob.state, "driving") +H.step(lostJob, {adActive = true, distance = 40, speed = 5}, 500, false) +H.step(lostJob, {adActive = false, distance = 40, speed = 0}, 500, false) +check("job: AD stoppede langt fra markøren", lostJob.state, "failed") +check("job: årsag stoppet undervejs", lostJob.reason, "stoppedAway") +print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) +os.exit(failures == 0 and 0 or 1)