feat(marker): Courseplay-bro — sprøjteroller, høsterens afgrøder, frø/plantbarhed, multi-tool-baner, combine-unloader
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user