feat(marker): Courseplay-bro — sprøjteroller, høsterens afgrøder, frø/plantbarhed, multi-tool-baner, combine-unloader

This commit is contained in:
2026-09-25 23:54:45 +02:00
parent 793352c4c4
commit 02e355e700
2 changed files with 261 additions and 7 deletions
+236 -7
View File
@@ -5,7 +5,47 @@
ADCpBridge = {} ADCpBridge = {}
ADCpBridge.SPECS = {"spec_mower", "spec_tedder", "spec_windrower", "spec_forageWagon"} ADCpBridge.SPECS = {"spec_mower", "spec_tedder", "spec_windrower", "spec_forageWagon", "spec_combine",
"spec_sowingMachine", "spec_cultivator", "spec_plow", "spec_roller", "spec_weeder", "spec_mulcher"}
-- Sprøjtens/sprederens fyld -> rolle (tom sprøjte har ingen rolle; den kan ikke sendes ud).
ADCpBridge.SPRAY_ROLES = {
FERTILIZER = "fertilizer", LIQUIDFERTILIZER = "fertilizer", MANURE = "fertilizer",
LIQUIDMANURE = "fertilizer", DIGESTATE = "fertilizer", LIME = "limer", HERBICIDE = "weeder",
}
function ADCpBridge.sprayRoleForFillType(name)
return ADCpBridge.SPRAY_ROLES[name]
end
-- Afgrøder et skærebord/optager kan høste (spec_cutter). Finsnittere (fruitTypeConverters) tager ingen:
-- de laver hakkelse, ikke korn/roer.
function ADCpBridge.harvestFruitSet(cutterSpec)
local fruits = {}
if cutterSpec == nil then return fruits end
if type(cutterSpec.fruitTypeConverters) == "table" and next(cutterSpec.fruitTypeConverters) ~= nil then
return fruits
end
-- (ikke ipairs over {a, b}: den stopper ved første nil)
for _, key in ipairs({"fruitTypes", "fruitTypeIndices"}) do
local source = cutterSpec[key]
if type(source) == "table" then
for key, value in pairs(source) do
if type(value) == "number" then fruits[value] = true
elseif value == true and type(key) == "number" then fruits[key] = true end
end
end
end
return fruits
end
-- Må afgrøden sås i perioden? (growthDataSeasonal.periods[p].plantingAllowed, bevist i AI-Bonden).
-- Ukendte data = tilladt (kort uden sæsondata).
function ADCpBridge.isPlantableIn(fruit, period)
local periods = fruit ~= nil and fruit.growthDataSeasonal ~= nil and fruit.growthDataSeasonal.periods or nil
local data = periods ~= nil and periods[period] or nil
if type(data) ~= "table" then return true end
return data.plantingAllowed == true
end
function ADCpBridge.isAvailable() function ADCpBridge.isAvailable()
return AIJobType ~= nil and AIJobType.FIELDWORK_CP ~= nil return AIJobType ~= nil and AIJobType.FIELDWORK_CP ~= nil
@@ -22,16 +62,112 @@ local function collect(vehicle, list)
return list return list
end end
-- Sprøjtens/sprederens rolle ud fra det fyld den har på (tom -> nil).
local function sprayRole(object)
if object.spec_sprayer == nil or object.getSprayerFillUnitIndex == nil then return nil end
local ok, role = pcall(function()
local unit = object:getSprayerFillUnitIndex()
if (object:getFillUnitFillLevel(unit) or 0) <= 0 then return nil end
local name = g_fillTypeManager:getFillTypeNameByIndex(object:getFillUnitFillType(unit))
return ADCpBridge.sprayRoleForFillType(name)
end)
return ok and role or nil
end
function ADCpBridge.getSpecs(vehicle) function ADCpBridge.getSpecs(vehicle)
local specs = {} local specs = {}
for _, object in ipairs(collect(vehicle)) do for _, object in ipairs(collect(vehicle)) do
for _, name in ipairs(ADCpBridge.SPECS) do for _, name in ipairs(ADCpBridge.SPECS) do
if object[name] ~= nil then specs[name] = true end if object[name] ~= nil then specs[name] = true end
end end
specs.sprayRole = specs.sprayRole or sprayRole(object)
end
if vehicle.getCanStartCpCombineUnloader ~= nil then
local ok, can = pcall(vehicle.getCanStartCpCombineUnloader, vehicle)
specs.canUnload = ok and can == true or nil
end end
return specs return specs
end end
-- Afgrøder høsteren kan tage (alle skærebord/optagere i kæden). nil = ikke en høster.
function ADCpBridge.getHarvestFruits(vehicle)
if vehicle.spec_combine == nil then return nil end
local fruits = {}
for _, object in ipairs(collect(vehicle)) do
for fruit in pairs(ADCpBridge.harvestFruitSet(object.spec_cutter)) do fruits[fruit] = true end
end
return fruits
end
-- Mekanisk strigle (kun småt ukrudt) frem for ukrudtssprøjte.
function ADCpBridge.isMechanicalWeeder(vehicle)
for _, object in ipairs(collect(vehicle)) do
if object.spec_weeder ~= nil then return true end
end
return false
end
local function seederOf(vehicle)
for _, object in ipairs(collect(vehicle)) do
if object.spec_sowingMachine ~= nil then return object end
end
return nil
end
-- Har såmaskinen frø på? (fyldenheder med SEEDS)
function ADCpBridge.hasSeeds(vehicle)
local seeder = seederOf(vehicle)
if seeder == nil then return false end
local ok, has = pcall(function()
local seedsIndex = g_fillTypeManager:getFillTypeIndexByName("SEEDS")
for index, unit in ipairs(seeder.spec_fillUnit ~= nil and seeder.spec_fillUnit.fillUnits or {}) do
if unit.fillType == seedsIndex and (unit.fillLevel or 0) > 0 then return true end
if unit.supportedFillTypes ~= nil and unit.supportedFillTypes[seedsIndex] and seeder:getFillUnitFillLevel(index) > 0 then return true end
end
return false
end)
return ok and has == true
end
-- Afgrøder såmaskinen kan så (spec_sowingMachine.seeds).
function ADCpBridge.getSeedFruits(vehicle)
local seeder = seederOf(vehicle)
local fruits = {}
local seeds = seeder ~= nil and seeder.spec_sowingMachine.seeds or nil
for _, fruit in pairs(seeds or {}) do fruits[fruit] = true end
return fruits
end
-- Sæt såmaskinens afgrøde (setSeedIndex(slot), bevist i AI-Bonden).
function ADCpBridge.setSeedFruit(vehicle, fruitIndex)
local seeder = seederOf(vehicle)
if seeder == nil then return false, "ingen såmaskine" end
for slot, fruit in pairs(seeder.spec_sowingMachine.seeds or {}) do
if fruit == fruitIndex then
if seeder.setSeedIndex ~= nil then
local ok, err = pcall(seeder.setSeedIndex, seeder, slot)
return ok, not ok and tostring(err) or nil
end
end
end
return false, "såmaskinen kan ikke så afgrøden"
end
function ADCpBridge.isPlantableNow(fruitIndex)
local fruit = g_fruitTypeManager:getFruitTypeByIndex(fruitIndex)
local environment = g_currentMission ~= nil and g_currentMission.environment or nil
return ADCpBridge.isPlantableIn(fruit, environment ~= nil and environment.currentPeriod or 0)
end
-- Stop køretøjets AI-job (Courseplay/hjælper).
function ADCpBridge.stop(vehicle)
pcall(function()
if vehicle.getIsAIActive ~= nil and vehicle:getIsAIActive() and vehicle.stopCurrentAIJob ~= nil then
vehicle:stopCurrentAIJob(AIMessageSuccessStoppedByUser.new())
end
end)
end
-- Variabel bredde (Unrealistic Pack m.fl.) først, som Courseplays WorkWidthUtil: AI-markørerne står på -- Variabel bredde (Unrealistic Pack m.fl.) først, som Courseplays WorkWidthUtil: AI-markørerne står på
-- grundbredden (læssevogn 2,4 m selv om den er sat til 40,8 m). -- grundbredden (læssevogn 2,4 m selv om den er sat til 40,8 m).
local function variableWidth(object) local function variableWidth(object)
@@ -143,9 +279,28 @@ function ADCpBridge.getCourse(vehicle)
return ok and course or nil return ok and course or nil
end end
-- course (valgfri): et færdigt kursus (slåmaskinens) der kopieres til køretøjet i stedet for at lave et -- Multi-tool: sæt antal sæt på kursus-generatoren før kurset laves (1 = normalt kursus).
-- nyt — så vender/river/læssevogn kører præcis slåmaskinens baner (in-game 25/9: nyt kursus lå forskudt). local function setMultiTools(vehicle, n)
function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone, course) pcall(function() vehicle:getCourseGeneratorSettings().multiTools:setValue(n or 1) end)
end
-- Sæt køretøjets bane på et multi-tool-kursus (samme som HUD'ens "Baneforskydning").
local function setLane(vehicle, params, lane)
if lane == nil then return end
pcall(function()
local course = vehicle:getFieldWorkCourse()
if course ~= nil and course.getMultiTools ~= nil and course:getMultiTools() > 1 then course:setPosition(lane) end
end)
if params ~= nil and params.laneOffset ~= nil then pcall(function() params.laneOffset:setValue(lane) end) end
end
-- Start Courseplay-markarbejde. opts (valgfri):
-- course = færdigt kursus der kopieres (slåmaskinens/rivens, eller trinnets fælles multi-tool-kursus)
-- multiTools = antal sæt når kurset laves her (multi-tool-leder)
-- lane = køretøjets bane på et multi-tool-kursus (laneOffset -2..2)
-- onDone(ok, reason) kaldes én gang; reason "copied" ved succes med kopieret kursus.
function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone, opts)
opts = opts or {}
if not ADCpBridge.isAvailable() then if not ADCpBridge.isAvailable() then
return nil, "Courseplay (FIELDWORK_CP) findes ikke" return nil, "Courseplay (FIELDWORK_CP) findes ikke"
end end
@@ -165,6 +320,7 @@ function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone, course)
if not handle.cancelled then onDone(ok, reason) end if not handle.cancelled then onDone(ok, reason) end
end end
local function startWithCourse() local function startWithCourse()
setLane(vehicle, params, opts.lane)
job:setValues() job:setValues()
local ok, err = job:validate(farmId) local ok, err = job:validate(farmId)
if not ok then return finish(false, "Courseplay: " .. tostring(err)) end if not ok then return finish(false, "Courseplay: " .. tostring(err)) end
@@ -173,14 +329,15 @@ function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone, course)
end end
local function generateAndStart() local function generateAndStart()
if handle.cancelled then return end if handle.cancelled then return end
if course ~= nil and vehicle.cpCopyCourse ~= nil then if opts.course ~= nil and vehicle.cpCopyCourse ~= nil then
local copied, copyError = pcall(vehicle.cpCopyCourse, vehicle, course) local copied, copyError = pcall(vehicle.cpCopyCourse, vehicle, opts.course)
if copied then if copied then
handle.copiedCourse = true handle.copiedCourse = true
return startWithCourse() return startWithCourse()
end end
Logging.warning("ADSmartPickup: kunne ikke kopiere slåkursus: %s — laver nyt", tostring(copyError)) Logging.warning("ADSmartPickup: kunne ikke kopiere kursus: %s — laver nyt", tostring(copyError))
end end
setMultiTools(vehicle, opts.multiTools or 1)
job:onClickGenerateFieldWorkCourse(function(generated) job:onClickGenerateFieldWorkCourse(function(generated)
if handle.cancelled then return end if handle.cancelled then return end
if not generated then return finish(false, "Courseplay kunne ikke lave kursus") end if not generated then return finish(false, "Courseplay kunne ikke lave kursus") end
@@ -203,3 +360,75 @@ function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone, course)
end end
return handle return handle
end end
-- Aflæsningsvogn under høst: Courseplays combine-unloader (bevist i AI-Bonden). Vognen venter ved
-- (waitX, waitZ) — markens F-markør — følger høsteren på marken og kører fuld til station. station =
-- den station Smart Pickups silovalg fandt; findes den ikke i Courseplays liste, bruges nærmeste.
function ADCpBridge.startUnloader(vehicle, fieldX, fieldZ, waitX, waitZ, station, onDone)
if AIJobType == nil or AIJobType.COMBINE_UNLOADER_CP == nil then
return nil, "Courseplay (COMBINE_UNLOADER_CP) findes ikke"
end
local farmId = vehicle:getOwnerFarmId()
local job = g_currentMission.aiJobTypeManager:createJob(AIJobType.COMBINE_UNLOADER_CP)
if job == nil then return nil, "kunne ikke oprette aflæsningsjob" end
pcall(function() job:applyCurrentState(vehicle, g_currentMission, farmId, false, false) end)
local params = job.cpJobParameters
if params ~= nil then
if params.fieldPosition ~= nil then pcall(function() params.fieldPosition:setPosition(fieldX, fieldZ) end) end
if params.startPosition ~= nil then pcall(function() params.startPosition:setPosition(waitX, waitZ) end) end
if params.useGiantsUnload ~= nil then pcall(function() params.useGiantsUnload:setValue(true) end) end
end
pcall(function() job:setValues() end)
local chosen = nil
local ok, stations = pcall(function() return job:getUnloadingStations() end)
if ok and type(stations) == "table" then
local best, bestDistance = nil, math.huge
for _, candidate in ipairs(stations) do
if station ~= nil and (candidate == station or candidate.owningPlaceable == station.owningPlaceable) then
best, bestDistance = candidate, -1
elseif bestDistance >= 0 then
local okPos, sx, sz = pcall(function()
local x, _, z = getWorldTranslation(candidate.owningPlaceable.rootNode)
return x, z
end)
local distance = okPos and ((sx - fieldX) ^ 2 + (sz - fieldZ) ^ 2) or math.huge
if distance < bestDistance then best, bestDistance = candidate, distance end
end
end
chosen = best
end
if chosen == nil or params == nil or params.unloadingStation == nil then
return nil, "ingen aflæsningsstation Courseplay kan bruge"
end
pcall(function() params.unloadingStation:setValue(NetworkUtil.getObjectId(chosen)) end)
pcall(function() job:setValues() end)
local handle = {cancelled = false, fired = false, finished = false, stationName = chosen.getName ~= nil and chosen:getName() or nil}
local function finish(success, reason)
if handle.finished then return end
handle.finished = true
if not handle.cancelled then onDone(success, reason) end
end
local function startJob()
if handle.cancelled then return end
local valid, err = job:validate(farmId)
if not valid then return finish(false, "Courseplay: " .. tostring(err)) end
g_currentMission.aiSystem:startJob(job, farmId)
finish(true)
end
handle.onBoundary = function(_, success, message)
if handle.fired then return end
handle.fired = true
if not success then return finish(false, "Courseplay fandt ikke markgrænsen: " .. tostring(message)) end
startJob()
end
if job.registerFieldBoundaryDetectionCallback ~= nil then
job:registerFieldBoundaryDetectionCallback(handle, handle.onBoundary)
end
local isValid, isRunning, message = job:detectFieldBoundary()
if isValid and not isRunning then
startJob()
elseif not isRunning then
return nil, "Courseplay: " .. tostring(message)
end
return handle
end
+25
View File
@@ -61,5 +61,30 @@ local tractorXml = {rootNode = 100, getAttachedImplements = function() return {{
check("WO3 fra XML", ADCpBridge.getWindrowOffset(tractorXml), 3) check("WO3 fra XML", ADCpBridge.getWindrowOffset(tractorXml), 3)
check("WO4 ingen river -> nil", ADCpBridge.getWindrowOffset({rootNode = 100}), nil) check("WO4 ingen river -> nil", ADCpBridge.getWindrowOffset({rootNode = 100}), nil)
-- SR: sprøjte/spreder -> rolle ud fra fyldet
check("SR gødning", ADCpBridge.sprayRoleForFillType("FERTILIZER"), "fertilizer")
check("SR flydende", ADCpBridge.sprayRoleForFillType("LIQUIDFERTILIZER"), "fertilizer")
check("SR gylle", ADCpBridge.sprayRoleForFillType("LIQUIDMANURE"), "fertilizer")
check("SR møg", ADCpBridge.sprayRoleForFillType("MANURE"), "fertilizer")
check("SR digestat", ADCpBridge.sprayRoleForFillType("DIGESTATE"), "fertilizer")
check("SR kalk", ADCpBridge.sprayRoleForFillType("LIME"), "limer")
check("SR ukrudt", ADCpBridge.sprayRoleForFillType("HERBICIDE"), "weeder")
check("SR vand/ukendt", ADCpBridge.sprayRoleForFillType("WATER"), nil)
-- HF: høsterens afgrøder fra skærebordet (liste eller opslag); finsnitter tager ingen
local list = ADCpBridge.harvestFruitSet({fruitTypes = {3, 5}})
check("HF liste", list[3] and list[5], true)
local lookup = ADCpBridge.harvestFruitSet({fruitTypes = {[3] = true}})
check("HF opslag", lookup[3], true)
local forage = ADCpBridge.harvestFruitSet({fruitTypes = {3}, fruitTypeConverters = {[3] = {}}})
check("HF finsnitter tager intet", next(forage), nil)
check("HF fruitTypeIndices", ADCpBridge.harvestFruitSet({fruitTypeIndices = {7}})[7], true)
-- PL: plantbar i perioden
local fruitPlant = {growthDataSeasonal = {periods = {[3] = {plantingAllowed = true}, [4] = {plantingAllowed = false}}}}
check("PL tilladt", ADCpBridge.isPlantableIn(fruitPlant, 3), true)
check("PL ikke nu", ADCpBridge.isPlantableIn(fruitPlant, 4), false)
check("PL ukendt data = tilladt", ADCpBridge.isPlantableIn({}, 4), 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)