feat(kobling): Connect-markører — traktor der kører AD 'Kør til' en 'Connect <navn>'-markør kobler redskabet bag sig på ved ankomst; ADHitch.fetch til flows, konsol spuConnect (v1.46.0.0)

This commit is contained in:
2026-09-28 17:07:07 +02:00
parent 5a715bc3e9
commit fde3fd4399
4 changed files with 236 additions and 1 deletions
+182
View File
@@ -0,0 +1,182 @@
-- AD Hitch
-- Kobler redskaber på med AutoDrive. En markør der hedder "Connect <navn>" (eller "Kobl <navn>") 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
+6
View File
@@ -1272,6 +1272,12 @@ 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 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 ADStuckDiag ~= nil then pcall(ADStuckDiag.tick, dt) end
if ADRunsController ~= nil and ADRunsController.adoptLoadedThrottled ~= nil then if ADRunsController ~= nil and ADRunsController.adoptLoadedThrottled ~= nil then
pcall(ADRunsController.adoptLoadedThrottled, getAutoDriveEnv(), dt) pcall(ADRunsController.adoptLoadedThrottled, getAutoDriveEnv(), dt)
+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.45.1.0</version> <version>1.46.0.0</version>
<title> <title>
<en>AD Smart Pickup</en> <en>AD Smart Pickup</en>
</title> </title>
@@ -46,6 +46,7 @@
<sourceFile filename="adFieldSiloConfig.lua"/> <sourceFile filename="adFieldSiloConfig.lua"/>
<sourceFile filename="adFieldSilos.lua"/> <sourceFile filename="adFieldSilos.lua"/>
<sourceFile filename="adCpResume.lua"/> <sourceFile filename="adCpResume.lua"/>
<sourceFile filename="adHitch.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"/>
+46
View File
@@ -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 <navn>" (store/små bogstaver ligegyldigt), også dansk "Kobl <navn>"
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)