From 02e355e70019b6ffa4b465a4c0d53bfc0b787542 Mon Sep 17 00:00:00 2001 From: masterdraco Date: Fri, 25 Sep 2026 23:54:45 +0200 Subject: [PATCH] =?UTF-8?q?feat(marker):=20Courseplay-bro=20=E2=80=94=20sp?= =?UTF-8?q?r=C3=B8jteroller,=20h=C3=B8sterens=20afgr=C3=B8der,=20fr=C3=B8/?= =?UTF-8?q?plantbarhed,=20multi-tool-baner,=20combine-unloader?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FS25_ADSmartPickup/adCpBridge.lua | 243 +++++++++++++++++++++++++++++- tests/test_adCpBridge.lua | 25 +++ 2 files changed, 261 insertions(+), 7 deletions(-) diff --git a/FS25_ADSmartPickup/adCpBridge.lua b/FS25_ADSmartPickup/adCpBridge.lua index 955e6af..5e6e6c9 100644 --- a/FS25_ADSmartPickup/adCpBridge.lua +++ b/FS25_ADSmartPickup/adCpBridge.lua @@ -5,7 +5,47 @@ 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() return AIJobType ~= nil and AIJobType.FIELDWORK_CP ~= nil @@ -22,16 +62,112 @@ local function collect(vehicle, list) return list 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) local specs = {} for _, object in ipairs(collect(vehicle)) do for _, name in ipairs(ADCpBridge.SPECS) do if object[name] ~= nil then specs[name] = true 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 return specs 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å -- grundbredden (læssevogn 2,4 m selv om den er sat til 40,8 m). local function variableWidth(object) @@ -143,9 +279,28 @@ function ADCpBridge.getCourse(vehicle) return ok and course or nil end --- course (valgfri): et færdigt kursus (slåmaskinens) der kopieres til køretøjet i stedet for at lave et --- nyt — så vender/river/læssevogn kører præcis slåmaskinens baner (in-game 25/9: nyt kursus lå forskudt). -function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone, course) +-- Multi-tool: sæt antal sæt på kursus-generatoren før kurset laves (1 = normalt kursus). +local function setMultiTools(vehicle, n) + 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 return nil, "Courseplay (FIELDWORK_CP) findes ikke" end @@ -165,6 +320,7 @@ function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone, course) if not handle.cancelled then onDone(ok, reason) end end local function startWithCourse() + setLane(vehicle, params, opts.lane) job:setValues() local ok, err = job:validate(farmId) if not ok then return finish(false, "Courseplay: " .. tostring(err)) end @@ -173,14 +329,15 @@ function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone, course) end local function generateAndStart() if handle.cancelled then return end - if course ~= nil and vehicle.cpCopyCourse ~= nil then - local copied, copyError = pcall(vehicle.cpCopyCourse, vehicle, course) + if opts.course ~= nil and vehicle.cpCopyCourse ~= nil then + local copied, copyError = pcall(vehicle.cpCopyCourse, vehicle, opts.course) if copied then handle.copiedCourse = true return startWithCourse() 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 + setMultiTools(vehicle, opts.multiTools or 1) job:onClickGenerateFieldWorkCourse(function(generated) if handle.cancelled then return 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 return handle 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 diff --git a/tests/test_adCpBridge.lua b/tests/test_adCpBridge.lua index 01cc37c..c99749f 100644 --- a/tests/test_adCpBridge.lua +++ b/tests/test_adCpBridge.lua @@ -61,5 +61,30 @@ local tractorXml = {rootNode = 100, getAttachedImplements = function() return {{ check("WO3 fra XML", ADCpBridge.getWindrowOffset(tractorXml), 3) 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")) os.exit(failures == 0 and 0 or 1)