From be043ac2204d977e6b9fbeb399463a515c8e463f Mon Sep 17 00:00:00 2001 From: masterdraco Date: Mon, 28 Sep 2026 17:14:20 +0200 Subject: [PATCH] =?UTF-8?q?feat(kobling):=20Afkobl-mark=C3=B8rer=20kobler?= =?UTF-8?q?=20redskaber=20bag=20traktoren=20af=20(frontv=C3=A6gt=20bliver)?= =?UTF-8?q?;=20'hent=20vogn=20og=20k=C3=B8r=20til'=20som=20ADHitch.trip=20?= =?UTF-8?q?+=20konsol=20spuTrip/spuDetach=20(v1.47.0.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FS25_ADSmartPickup/adHitch.lua | 216 ++++++++++++++++++++++++++------- FS25_ADSmartPickup/modDesc.xml | 2 +- tests/test_adHitch.lua | 46 +++++-- 3 files changed, 210 insertions(+), 54 deletions(-) diff --git a/FS25_ADSmartPickup/adHitch.lua b/FS25_ADSmartPickup/adHitch.lua index bd78a7c..571fdc5 100644 --- a/FS25_ADSmartPickup/adHitch.lua +++ b/FS25_ADSmartPickup/adHitch.lua @@ -1,63 +1,105 @@ -- 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. +-- Kobler redskaber på og af med AutoDrive. +-- "Connect " (eller "Kobl "): markøren ligger lige under vognens trækøje med et bakkespor ned til +-- den. Når en traktor har kørt AD "Kør til" dertil 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). +-- "Afkobl " (eller "Kobl af", "Detach", "Disconnect"): ved ankomst kobles redskaberne BAG traktoren af; +-- frontvægt og frontlæsser bliver siddende. +-- ADHitch.fetch()/ADHitch.drop() er indgangen for flows/markarbejde: kør til markøren, kobl på/af, meld tilbage. ADHitch = {} ADHitch.LOG_PREFIX = "ADSmartPickup: kobling: " -ADHitch.PREFIXES = {"connect", "kobl"} +-- afkobling tjekkes først, så "Kobl af X" ikke læses som "Kobl " +ADHitch.MARKER_PREFIXES = { + {prefix = "afkobl", action = "detach"}, + {prefix = "kobl af", action = "detach"}, + {prefix = "detach", action = "detach"}, + {prefix = "disconnect", action = "detach"}, + {prefix = "connect", action = "attach"}, + {prefix = "kobl", action = "attach"}, +} +ADHitch.TIMEOUT_REASON = {attach = "noAttachable", detach = "nothingToDetach"} 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.START_GRACE_MS = 3000 -- så længe må AD være om at komme i gang efter fetch/drop +ADHitch.ACT_TIMEOUT_MS = 8000 -- så længe forsøges kobling/afkobling efter ankomst ADHitch.TICK_MS = 250 -- ---- ren logik ---- --- "Connect Bredal" -> "Bredal"; "Connect" -> ""; andre navne -> nil -function ADHitch.connectTarget(name) +-- "Connect Bredal" -> "attach", "Bredal"; "Afkobl" -> "detach", ""; andre navne -> nil +function ADHitch.parseMarker(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 + for _, entry in ipairs(ADHitch.MARKER_PREFIXES) do + local prefix = entry.prefix + if lower == prefix then return entry.action, "" end if lower:sub(1, #prefix + 1) == prefix .. " " then - return (trimmed:sub(#prefix + 2):match("^%s*(.-)$")) + return entry.action, (trimmed:sub(#prefix + 2):match("^%s*(.-)$")) end end return nil end +-- implements = {{z = position i traktorens rum, ...}} -> kun dem bag traktoren +function ADHitch.rearImplements(implements) + local rear = {} + for _, implement in ipairs(implements or {}) do + if implement.z ~= nil and implement.z < 0 then table.insert(rear, implement) end + end + return rear +end + +-- konsol-argumenter "Connect Bredal > Start" -> "Connect Bredal", "Start" (mål nil uden ">") +function ADHitch.parseTrip(...) + local text = table.concat({...}, " ") + local first, second = text:match("^%s*(.-)%s*>%s*(.-)%s*$") + if first == nil then return (text:match("^%s*(.-)%s*$")), nil end + return first, second +end + +-- markers = {{name, id}} -> markøren med navnet (store/små bogstaver ligegyldigt) +function ADHitch.findMarker(markers, name) + if type(name) ~= "string" then return nil end + local wanted = name:lower() + for _, marker in pairs(markers or {}) do + if type(marker.name) == "string" and marker.name:lower() == wanted then return marker 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} +function ADHitch.newJob(markerId, markerName, action) + return {markerId = markerId, markerName = markerName, action = action, state = "driving", elapsed = 0, + actElapsed = 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) +-- Et skridt i jobbet. succeeded = lykkedes kobling/afkobling i denne tick (kun relevant i "acting"). +-- Tilstande: driving -> acting -> done | failed (reason: stoppedAway / noAttachable / nothingToDetach) +function ADHitch.step(job, state, dt, succeeded) 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" + job.state = "acting" 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 + elseif job.state == "acting" then + if succeeded 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 + job.actElapsed = job.actElapsed + dt + if job.actElapsed >= ADHitch.ACT_TIMEOUT_MS then + job.state, job.reason = "failed", ADHitch.TIMEOUT_REASON[job.action] + end end end return job @@ -69,6 +111,7 @@ local jobs = setmetatable({}, {__mode = "k"}) -- [vehicle] = job (+ onDone) local tickTimer = 0 local function vehicleName(vehicle) + if type(vehicle) ~= "table" then return tostring(vehicle) end return vehicle.getFullName ~= nil and vehicle:getFullName() or tostring(vehicle.configFileName) end @@ -109,10 +152,40 @@ function ADHitch.tryAttach(vehicle) 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) +-- Redskaber koblet direkte på traktoren, med deres placering langs traktoren (z > 0 foran, z < 0 bagved). +local function attachedImplements(vehicle) + local list = {} + if vehicle.getAttachedImplements == nil then return list end + for _, implement in ipairs(vehicle:getAttachedImplements()) do + local jointDesc = vehicle.spec_attacherJoints.attacherJoints[implement.jointDescIndex] + local node = jointDesc ~= nil and (jointDesc.jointTransform or jointDesc.rootNode) or nil + if implement.object ~= nil and node ~= nil then + local _, _, z = localToLocal(node, vehicle.rootNode, 0, 0, 0) + table.insert(list, {object = implement.object, z = z, name = vehicleName(implement.object)}) + end + end + return list +end + +-- Kobler redskaberne bag traktoren af. -> ok, navne eller årsag +function ADHitch.tryDetach(vehicle) + local rear = ADHitch.rearImplements(attachedImplements(vehicle)) + if #rear == 0 then return false, "nothingToDetach" end + local names = {} + for _, implement in ipairs(rear) do + local object = implement.object + local allowed, warning = true, nil + if object.isDetachAllowed ~= nil then allowed, warning = object:isDetachAllowed() end + if not allowed then return false, tostring(warning or "detachNotAllowed") end + vehicle:detachImplementByObject(object) + table.insert(names, implement.name) + end + return true, table.concat(names, ", ") +end + +local function start(adEnv, vehicle, markerId, action, onDone) local marker = adEnv.ADGraphManager:getMapMarkerByWayPointId(markerId) - local job = ADHitch.newJob(markerId, marker ~= nil and marker.name or tostring(markerId)) + local job = ADHitch.newJob(markerId, marker ~= nil and marker.name or tostring(markerId), action) job.onDone = onDone jobs[vehicle] = job ADFieldWork.startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, markerId) @@ -120,32 +193,61 @@ function ADHitch.fetch(adEnv, vehicle, markerId, onDone) return job end --- Spilleren sender selv en traktor til en Connect-markør med AD "Kør til": den kobles også på. +-- 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) +end + +-- Kør til en Afkobl-markør og kobl af. onDone(vehicle, ok, navne eller årsag) kaldes én gang. +function ADHitch.drop(adEnv, vehicle, markerId, onDone) + return start(adEnv, vehicle, markerId, "detach", onDone) +end + +-- "Hent vogn og kør til": kør til Connect-markøren, kobl på, kør så med AD til målet. +function ADHitch.trip(adEnv, vehicle, connectId, destinationId, onDone) + return ADHitch.fetch(adEnv, vehicle, connectId, function(tractor, ok, detail) + if not ok then + if onDone ~= nil then onDone(tractor, false, detail) end + return + end + local destination = adEnv.ADGraphManager:getMapMarkerByWayPointId(destinationId) + ADFieldWork.startAd(adEnv, tractor, adEnv.AutoDrive.MODE_DRIVETO, destinationId) + Logging.info(ADHitch.LOG_PREFIX .. "'%s' kører med '%s' til '%s'", vehicleName(tractor), vehicleName(detail), + destination ~= nil and destination.name or tostring(destinationId)) + if onDone ~= nil then onDone(tractor, true, detail) end + end) +end + +-- Spilleren sender selv en traktor til en Connect-/Afkobl-markør med AD "Kør til": den kobles også. 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 + local action = marker ~= nil and ADHitch.parseMarker(marker.name) or nil + if action ~= nil then jobs[vehicle] = ADHitch.newJob(marker.id, marker.name, action) end end end end +local DONE_TEXT = {attach = "koblede '%s' på", detach = "koblede '%s' af"} +local FAIL_TEXT = {attach = "kunne ikke koble på", detach = "kunne ikke koble af"} + 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) + Logging.info(ADHitch.LOG_PREFIX .. "'%s' " .. DONE_TEXT[job.action] .. " ved '%s'", vehicleName(vehicle), + vehicleName(detail), job.markerName) else - Logging.warning(ADHitch.LOG_PREFIX .. "'%s' kunne ikke koble på ved '%s': %s", vehicleName(vehicle), + Logging.warning(ADHitch.LOG_PREFIX .. "'%s' %s ved '%s': %s", vehicleName(vehicle), FAIL_TEXT[job.action], job.markerName, tostring(detail)) end if job.onDone ~= nil then pcall(job.onDone, vehicle, ok, detail) end end +local ACTIONS = {attach = ADHitch.tryAttach, detach = ADHitch.tryDetach} + function ADHitch.tick(adEnv, dt) if adEnv == nil or adEnv.ADGraphManager == nil then return end tickTimer = tickTimer + dt @@ -154,12 +256,12 @@ function ADHitch.tick(adEnv, dt) 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 + local succeeded, detail = false, nil + if job.state == "acting" then + succeeded, detail = ACTIONS[job.action](vehicle) + if not succeeded then job.lastReason = detail end end - ADHitch.step(job, vehicleState(adEnv, vehicle, job.markerId), elapsed, attached) + ADHitch.step(job, vehicleState(adEnv, vehicle, job.markerId), elapsed, succeeded) if job.state == "done" then finish(vehicle, job, true, detail) elseif job.state == "failed" then @@ -168,15 +270,47 @@ function ADHitch.tick(adEnv, dt) end end --- Konsol: "spuConnect" kobler det nærmeste redskab på den maskine spilleren sidder i. +-- Konsol: "spuConnect" / "spuDetach" på den maskine spilleren sidder i. +local function currentVehicle() + return g_localPlayer ~= nil and g_localPlayer.getCurrentVehicle ~= nil and g_localPlayer:getCurrentVehicle() or nil +end + function ADHitch:consoleConnect() - local vehicle = g_localPlayer ~= nil and g_localPlayer.getCurrentVehicle ~= nil and g_localPlayer:getCurrentVehicle() or nil + local vehicle = currentVehicle() 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 +function ADHitch:consoleDetach() + local vehicle = currentVehicle() + if vehicle == nil then return "spuDetach: sæt dig i en traktor først" end + local ok, detail = ADHitch.tryDetach(vehicle) + if ok then return "spuDetach: koblede " .. tostring(detail) .. " af" end + return "spuDetach: intet koblet af (" .. tostring(detail) .. ")" +end + +-- "spuTrip Connect Bredal > Start": maskinen spilleren sidder i henter vognen og kører til målet (spilleren +-- kan blive siddende; AD kører også med spilleren i førerhuset). +function ADHitch:consoleTrip(...) + local vehicle = currentVehicle() + if vehicle == nil then return "spuTrip: sæt dig i en traktor først" end + local adEnv = ADFieldSilos ~= nil and ADFieldSilos.adEnv or nil + if adEnv == nil then return "spuTrip: AutoDrive er ikke klar endnu" end + local connectName, destinationName = ADHitch.parseTrip(...) + local markers = adEnv.ADGraphManager:getMapMarkers() + local connect, destination = ADHitch.findMarker(markers, connectName), ADHitch.findMarker(markers, destinationName) + if connect == nil or ADHitch.parseMarker(connect.name) ~= "attach" then + return "spuTrip: ukendt Connect-markør '" .. tostring(connectName) .. "'" + end + if destination == nil then return "spuTrip: ukendt mål '" .. tostring(destinationName) .. "' (brug: spuTrip Connect Bredal > Start)" end + ADHitch.trip(adEnv, vehicle, connect.id, destination.id) + return "spuTrip: " .. vehicleName(vehicle) .. " henter ved '" .. connect.name .. "' og kører til '" .. destination.name .. "'" +end + if addConsoleCommand ~= nil then addConsoleCommand("spuConnect", "Kobler nærmeste redskab på den maskine du sidder i", "consoleConnect", ADHitch) + addConsoleCommand("spuTrip", "Hent vogn og kør: spuTrip Connect Bredal > Start", "consoleTrip", ADHitch) + addConsoleCommand("spuDetach", "Kobler redskaberne bag den maskine du sidder i af", "consoleDetach", ADHitch) end diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml index d3c4739..77df79f 100644 --- a/FS25_ADSmartPickup/modDesc.xml +++ b/FS25_ADSmartPickup/modDesc.xml @@ -1,7 +1,7 @@ masterdraco - 1.46.0.0 + 1.47.0.0 <en>AD Smart Pickup</en> diff --git a/tests/test_adHitch.lua b/tests/test_adHitch.lua index 4a17a66..8e15f51 100644 --- a/tests/test_adHitch.lua +++ b/tests/test_adHitch.lua @@ -8,39 +8,61 @@ local function check(name, actual, expected) 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) +-- markørnavne -> handling + navn (store/små bogstaver ligegyldigt) +local function parsed(name) local a, n = H.parseMarker(name) return tostring(a) .. "|" .. tostring(n) end +check("navn: Connect Bredal", parsed("Connect Bredal"), "attach|Bredal") +check("navn: små bogstaver", parsed("connect bredal"), "attach|bredal") +check("navn: dansk Kobl", parsed("Kobl Vogn 2"), "attach|Vogn 2") +check("navn: kun Connect", parsed("Connect"), "attach|") +check("navn: Connected er ikke en koblingsmarkør", parsed("Connected silo"), "nil|nil") +check("navn: almindelig markør", parsed("Bredal"), "nil|nil") +check("navn: nil", parsed(nil), "nil|nil") +check("navn: Afkobl Bredal", parsed("Afkobl Bredal"), "detach|Bredal") +check("navn: Kobl af er afkobling, ikke kobling", parsed("Kobl af Bredal"), "detach|Bredal") +check("navn: Detach", parsed("detach"), "detach|") +check("navn: Disconnect", parsed("Disconnect Vogn"), "detach|Vogn") +-- bagerste redskaber: kun led bag traktoren (z < 0 i traktorens rum), ikke frontvægt/frontlæsser +local rear = H.rearImplements({{name = "vægt", z = 3.1}, {name = "Bredal", z = -2.4}, {name = "læsser", z = 1.5}, {name = "plov", z = -1}}) +check("bag: kun bagerste", #rear, 2) +check("bag: første", rear[1].name, "Bredal") -- 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") +local job = H.newJob(25, "Connect Bredal", "attach") check("job: venter på AD", job.state, "driving") +check("job: handling", job.action, "attach") 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") +check("job: ankommet, kobling ikke lykkedes endnu", job.state, "acting") 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") +local failJob = H.newJob(25, "Connect Bredal", "attach") 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") +local dropJob = H.newJob(9, "Afkobl Bredal", "detach") +H.step(dropJob, {adActive = false, distance = 1, speed = 0}, 500, false) +for _ = 1, 20 do H.step(dropJob, {adActive = false, distance = 1, speed = 0}, 500, false) end +check("job: afkobling opgivet", dropJob.reason, "nothingToDetach") +local lostJob = H.newJob(25, "Connect Bredal", "attach") 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") +-- tur: "Connect Bredal > Start" -> to markørnavne +local a, b = H.parseTrip("Connect", "Bredal", ">", "Start") +check("tur: hent", a, "Connect Bredal") +check("tur: mål", b, "Start") +check("tur: uden mål", select(2, H.parseTrip("Connect", "Bredal")), nil) +local markers = {{name = "Start", id = 1}, {name = "Connect Bredal", id = 9}} +check("markør: navn uden hensyn til store/små", H.findMarker(markers, "connect bredal").id, 9) +check("markør: ukendt", H.findMarker(markers, "Stald"), nil) print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) os.exit(failures == 0 and 0 or 1)