-- AD Field Model -- Markarbejdets regler som ren data (ingen spil-API): en marks plan og trin, hvornår den er klar, -- hvilket trin der følger, hvor mange sæt et trin må have, pleje-behov og bredde-reglerne. Alle -- funktioner returnerer nye tabeller; job-løkken (adFieldJobs/adFieldWork) og GUI'et kalder kun herind. -- -- Markens art (kind): "mowable" (græs/kløver/lucerne), "harvest" (alt andet der høstes), "bare" (ingen -- afgrøde: stub, pløjet, harvet). Kæder: -- slåafgrøde: mowing -> [tedding] -> [raking] -> collecting -- høst: harvesting -> klargøring -- klargøring: [mulching] -> [liming] -> [plowing|cultivating] -> [sowing] -> [rolling] -- Pleje under vækst (enkelt trin, derefter tilbage til idle): fertilizing, weeding, liming. ADFieldModel = {} ADFieldModel.READY_RATIO = 0.9 ADFieldModel.CUT_RATIO = 0.5 ADFieldModel.CP_MAX_WIDTH = 50 ADFieldModel.WIDTH_TOLERANCE = 0.05 ADFieldModel.RETRY_MS = 300000 ADFieldModel.CP_MAX_TOOL_OFFSET = 10 ADFieldModel.MAX_RIGS = 5 ADFieldModel.MAX_UNLOADERS = 3 ADFieldModel.WEED_MIN = 2 -- weedState 1 = spredt, 2+ = tæt (AI-Bonde/Arbejdsseddel) ADFieldModel.WEED_MECHANICAL_MAX = 4 -- større ukrudt kræver sprøjte, strigle kan ikke ADFieldModel.MAX_CARE_RUNS = 2 -- samme pleje højst to gange pr. vækstcyklus (undgår løkker) ADFieldModel.ROLES = { mowing = "mower", tedding = "tedder", raking = "rake", collecting = "collector", harvesting = "harvester", mulching = "mulcher", liming = "limer", plowing = "plow", cultivating = "cultivator", sowing = "seeder", rolling = "roller", weeding = "weeder", fertilizing = "fertilizer", strawCollecting = "collector", baling = "baler", baleCollecting = "baleCollector", } ADFieldModel.HELPERS = {harvesting = "unloader"} ADFieldModel.CARE_STAGES = {fertilizing = true, weeding = true, careLiming = true} -- trin med afgrøde der skal være væk bagefter (ellers sendes maskinen ud igen) ADFieldModel.CROP_STAGES = {mowing = true, harvesting = true} local KNOWN_STAGES = {idle = true, ready = true, done = true} for stage in pairs(ADFieldModel.ROLES) do KNOWN_STAGES[stage] = true end KNOWN_STAGES.careLiming = true local DEFAULT_PLAN = { mode = "manual", product = "auto", rake = false, rigs = 1, unloaders = 1, mulch = false, lime = true, soil = "auto", sow = false, sowFruit = "", roll = false, fertilize = true, weed = true, targetFruit = "", prepAfterMow = false, straw = "chop", } local STRAW_VALUES = {chop = true, collect = true, bale = true} -- halm-trin efter høst (mejetærskeren lægger skår når halmen skal bruges) local STRAW_STAGES = {collect = {"strawCollecting"}, bale = {"baling", "baleCollecting"}} local SOIL_VALUES = {auto = true, cultivate = true, plow = true, none = true} local PRODUCTS = {auto = true, silage = true, hay = true, harvest = true} local function copyList(list) local copy = {} for index, value in ipairs(list or {}) do copy[index] = value end return copy end local function copyPlan(plan) local copy = {} for key, value in pairs(DEFAULT_PLAN) do copy[key] = value end for key, value in pairs(plan or {}) do copy[key] = value end copy.storeBuildingIds = copyList(plan ~= nil and plan.storeBuildingIds or nil) -- faste maskiner (køretøjs-id'er) til marken; en fast maskine bruges kun her copy.rigIds = copyList(plan ~= nil and plan.rigIds or nil) return copy end local function copyField(field) local copy = {} for key, value in pairs(field) do copy[key] = value end copy.plan = copyPlan(field.plan) local care = {} for key, value in pairs(field.careRuns or {}) do care[key] = value end copy.careRuns = care return copy end local function clamp(value, low, high) return math.max(low, math.min(high, math.floor(tonumber(value) or low))) end function ADFieldModel.newField(id) return { id = id, stage = "idle", mowWidth = nil, retryAt = 0, reason = nil, startRequested = false, careRuns = {}, plan = copyPlan(nil), } end -- Plan "auto": slåafgrøder -> ensilage, alt andet -> høst. function ADFieldModel.product(plan, kind) if plan.product ~= nil and plan.product ~= "auto" then return plan.product end return kind == "mowable" and "silage" or "harvest" end -- Klargøringskæden efter høst (eller på en bar mark). needs = {lime = bool, plow = bool} fra marken. function ADFieldModel.prepStages(plan, needs) needs = needs or {} local stages = {} if plan.mulch then table.insert(stages, "mulching") end if plan.lime and needs.lime then table.insert(stages, "liming") end local soil = plan.soil or "auto" if soil == "plow" or (soil == "auto" and needs.plow) then table.insert(stages, "plowing") elseif soil == "cultivate" or soil == "auto" then table.insert(stages, "cultivating") end if plan.sow then table.insert(stages, "sowing") if plan.roll then table.insert(stages, "rolling") end end return stages end -- Hele kæden for markens art. kind nil = slåafgrøde (bagudkompatibelt med etape 1). function ADFieldModel.workStages(plan, kind, needs) kind = kind or "mowable" if kind == "bare" then return ADFieldModel.prepStages(plan, needs) end local product = ADFieldModel.product(plan, kind) if product == "harvest" then local stages = {"harvesting"} for _, stage in ipairs(STRAW_STAGES[plan.straw or "chop"] or {}) do table.insert(stages, stage) end for _, stage in ipairs(ADFieldModel.prepStages(plan, needs)) do table.insert(stages, stage) end return stages end local stages = {"mowing"} if product == "hay" then table.insert(stages, "tedding") end if plan.rake then table.insert(stages, "raking") end table.insert(stages, "collecting") -- mål-afgrøde på en slåmark med en anden afgrøde: efter sidste slæt klargøres og sås målet if plan.prepAfterMow then for _, stage in ipairs(ADFieldModel.prepStages(plan, needs)) do table.insert(stages, stage) end end return stages end -- Mål-afgrøde ("vælg mark + afgrøde, glem den"): planen udfyldes ud fra hvad der står på marken nu. -- Står målet der allerede som slåafgrøde (græs -> græs): bare slå igen og igen. Ellers: høst/slå det der -- står, klargør og så målet. Kører altid automatisk. Uden mål returneres planen uændret. function ADFieldModel.applyTarget(plan, kind, currentFruit) local copy = copyPlan(plan) if copy.targetFruit == nil or copy.targetFruit == "" then return copy end copy.mode = "auto" copy.sowFruit = copy.targetFruit if kind == "mowable" then copy.prepAfterMow = currentFruit ~= copy.targetFruit copy.sow = copy.prepAfterMow else copy.prepAfterMow = false copy.sow = true end return copy end -- Vælg mål-afgrøde fra menuen ("" = ingen). Med mål kører marken automatisk; en færdig/klar mark sættes -- til venter, så den kan starte forfra. Uden mål: tilbage til manuel. function ADFieldModel.setTarget(field, fruitName) local copy = copyField(field) copy.plan.targetFruit = fruitName or "" copy.plan.mode = copy.plan.targetFruit ~= "" and "auto" or "manual" if not ADFieldModel.isWorking(field) and field.stage == "done" then copy.stage = "idle" end return copy end -- Bar mark med mål-afgrøde starter selv klargøring + såning med det samme: klargøringen (kalk, plov, -- kultivator) kører uanset årstid, og kun såningen venter på såtiden (preflight: notPlantable). Med mange -- marker nås det ellers ikke inden såvinduet lukker (brugerønske 28/9). function ADFieldModel.shouldStartBare(field, kind) if field.plan.targetFruit == nil or field.plan.targetFruit == "" then return false end -- kun fra idle: efter egen såning står marken "done" (nysået kan læses som bar) — ingen dobbelt-såning return kind == "bare" and field.stage == "idle" end -- Krævet bredde for et sæt: kun læssevognen uden riv skal dække slåmaskinens baner (halmskår ligger i -- høsterens spor — der er breddekravet ligegyldigt). function ADFieldModel.requiredWidth(field, role) if role == "collector" and not field.plan.rake and field.stage ~= "strawCollecting" then return field.mowWidth end return nil end -- Hvilket tidligere kursus sættet skal køre: læssevognen følger skårene; de andre laver deres eget. function ADFieldModel.courseSource(field, role) -- halm: læssevogn og presser kører høsterens kursus (skåret ligger i mejetærskerens spor) if field.stage == "strawCollecting" or role == "baler" then return "harvester" end if role ~= "collector" then return nil end return field.plan.rake and "rake" or "mower" end function ADFieldModel.roleFor(stage) if stage == "careLiming" then return "limer" end return ADFieldModel.ROLES[stage] end function ADFieldModel.helperFor(stage) return ADFieldModel.HELPERS[stage] end function ADFieldModel.isWorking(field) return ADFieldModel.roleFor(field.stage) ~= nil end function ADFieldModel.isCare(stage) return ADFieldModel.CARE_STAGES[stage] == true end -- Antal sæt pr. trin (multi-tool 1–5) og aflæsningsvogne under høst (1–3). function ADFieldModel.maxRigs(stage, plan) return clamp(plan ~= nil and plan.rigs or 1, 1, ADFieldModel.MAX_RIGS) end function ADFieldModel.maxHelpers(stage, plan) if ADFieldModel.helperFor(stage) == nil then return 0 end return clamp(plan ~= nil and plan.unloaders or 1, 1, ADFieldModel.MAX_UNLOADERS) end -- Courseplays multi-tool-baner (laneOffset) for n sæt: 2 -> -1,1 · 3 -> -1,0,1 · 4 -> -2,-1,1,2 · 5 -> -2..2 function ADFieldModel.lanePositions(n) local lanes = {[1] = {0}, [2] = {-1, 1}, [3] = {-1, 0, 1}, [4] = {-2, -1, 1, 2}, [5] = {-2, -1, 0, 1, 2}} return copyList(lanes[clamp(n, 1, 5)]) end -- Venter/færdig -> klar ved fuld vækst; klar -> venter hvis afgrøden blev taget uden os. -- Uændret -> samme tabel (kaldes for hver mark hvert sekund; nye kopier giver affald og GC-hak). function ADFieldModel.onReadiness(field, ratio) if (field.stage == "idle" or field.stage == "done") and ratio >= ADFieldModel.READY_RATIO then local copy = copyField(field) copy.stage = "ready" return copy elseif field.stage == "ready" and ratio < ADFieldModel.CUT_RATIO and not field.startRequested then -- (en Start nu-forespørgsel holder marken klar: bare marker har klar-andel 0) local copy = copyField(field) copy.stage = "idle" copy.startRequested = false return copy end return field end -- Passiv mark: manuel, uden mål, venter/færdig, ingen Start nu — der sker intet uden brugeren. function ADFieldModel.isPassive(field) local plan = field.plan return plan.mode ~= "auto" and (plan.targetFruit == nil or plan.targetFruit == "") and not field.startRequested and (field.stage == "idle" or field.stage == "done") end function ADFieldModel.shouldStart(field) return field.stage == "ready" and (field.plan.mode == "auto" or field.startRequested == true) end -- Start nu fra menuen på en mark der ikke er nået 90 %: den behandles som klar. function ADFieldModel.forceReady(field) if field.stage == "idle" or field.stage == "done" then return ADFieldModel.with(field, {stage = "ready"}) end return field end local function freshCycle(copy) copy.startRequested = false copy.retryAt = 0 copy.reason = nil copy.mowWidth = nil copy.mowVehicleId = nil copy.rakeVehicleId = nil copy.harvestVehicleId = nil copy.windrowOffset = nil copy.careRuns = {} end -- Første trin i kæden. En tom kæde (bar mark uden jordbehandling eller såning) -> færdig. -- currentFruit (valgfri): afgrøden på marken nu; med mål-afgrøde udfyldes planen ud fra den. function ADFieldModel.start(field, kind, needs, currentFruit) local copy = copyField(field) copy.plan = ADFieldModel.applyTarget(copy.plan, kind, currentFruit) local stages = ADFieldModel.workStages(copy.plan, kind, needs) copy.stage = stages[1] or "done" copy.kind = kind or "mowable" freshCycle(copy) return copy end -- Næste trin i kæden (behov genberegnes, så fx kalk springes over hvis jorden allerede er kalket). -- Pleje-trin går tilbage til idle. function ADFieldModel.advance(field, needs) local copy = copyField(field) copy.retryAt = 0 copy.reason = nil if ADFieldModel.isCare(field.stage) then copy.stage = "idle" return copy end local stages = ADFieldModel.workStages(field.plan, field.kind, needs) copy.stage = "done" local passed = false local order = {"mowing", "tedding", "raking", "collecting", "harvesting", "strawCollecting", "baling", "baleCollecting", "mulching", "liming", "plowing", "cultivating", "sowing", "rolling"} local rank = {} for index, stage in ipairs(order) do rank[stage] = index end for _, stage in ipairs(stages) do if not passed and (rank[stage] or 0) > (rank[field.stage] or 0) then copy.stage = stage passed = true end end return copy end -- Stop fra menuen: arbejdet afbrydes; marken står klar (hovedkæde) eller venter (pleje/klargøring). function ADFieldModel.stop(field) local copy = copyField(field) if ADFieldModel.CROP_STAGES[field.stage] or field.stage == "tedding" or field.stage == "raking" or field.stage == "collecting" then copy.stage = "ready" elseif ADFieldModel.isWorking(field) then copy.stage = "idle" end copy.startRequested = false copy.reason = nil return copy end -- Vejrfølsomme trin: afgrøde og skår skal være tørre (høst, slåning, vending, rivning, hø-opsamling). -- Jordbehandling, såning, pleje og ensilage-opsamling kan køre i regn. local WEATHER_STAGES = {harvesting = true, mowing = true, tedding = true, raking = true, strawCollecting = true, baling = true} function ADFieldModel.isWeatherSensitive(stage, plan, kind) if WEATHER_STAGES[stage] then return true end return stage == "collecting" and ADFieldModel.product(plan, kind or "mowable") == "hay" end -- Pleje under vækst: hvilket pleje-trin marken trænger til nu (eller nil). info fra adFields: -- {kind, growing, fert, fertMax, lime, weed}. Højst MAX_CARE_RUNS pr. pleje pr. cyklus. function ADFieldModel.careNeed(field, info) if not (field.stage == "idle" or field.stage == "done") or info == nil or not info.growing then return nil end local runs = field.careRuns or {} local function allowed(stage) return (runs[stage] or 0) < ADFieldModel.MAX_CARE_RUNS end if field.plan.weed and info.kind == "harvest" and (info.weed or 0) >= ADFieldModel.WEED_MIN and allowed("weeding") then return "weeding" end if field.plan.fertilize and info.fert ~= nil and info.fertMax ~= nil and info.fert < info.fertMax and allowed("fertilizing") then return "fertilizing" end if field.plan.lime and info.lime ~= nil and info.lime < 1 and allowed("careLiming") then return "careLiming" end return nil end -- Start et pleje-trin (tæller kørslen, så samme pleje ikke gentages i det uendelige). function ADFieldModel.startCare(field, stage) local copy = copyField(field) copy.careRuns[stage] = (copy.careRuns[stage] or 0) + 1 copy.stage = stage copy.startRequested = false copy.retryAt = 0 copy.reason = nil return copy end -- Et sæt må ikke over Courseplays grænse; efter-trin skal dække mindst den krævede bredde. -- Under dette er bredden ikke læst rigtigt (spreder uden AI-markører gav 0,5 m): Courseplay laver så et -- kursus med rækker hver halve meter — på en 72 ha mark fryser spillet (in-game 26/9, F108). ADFieldModel.MIN_WORK_WIDTH = 1.5 -- roller uden eget kursus (følger høsteren / samler baller): ingen arbejdsbredde ADFieldModel.NO_WIDTH_ROLES = {unloader = true, baleCollector = true} function ADFieldModel.checkWidth(role, width, requiredWidth) if width < ADFieldModel.MIN_WORK_WIDTH and not ADFieldModel.NO_WIDTH_ROLES[role] then return false, "widthUnknown", {width} end if width > ADFieldModel.CP_MAX_WIDTH + ADFieldModel.WIDTH_TOLERANCE then return false, "tooWide", {width, ADFieldModel.CP_MAX_WIDTH} end if role ~= "mower" and requiredWidth ~= nil and width + ADFieldModel.WIDTH_TOLERANCE < requiredWidth then return false, "tooNarrow", {width, requiredWidth} end return true end -- Hvilken rolle trinet mangler: {role, count, helper}. Hovedrollen sendes samlet (multi-tool kræver alle -- baner), hjælpere (aflæsningsvogne) en ad gangen. counts = {primary = aktive, helpers = aktive}. function ADFieldModel.wantsRig(field, counts, now) if type(counts) == "number" then counts = {primary = counts, helpers = 0} end local role = ADFieldModel.roleFor(field.stage) if role == nil or (field.retryAt or 0) > now then return nil end local wanted = ADFieldModel.maxRigs(field.stage, field.plan) if (counts.primary or 0) < wanted then return {role = role, count = wanted - (counts.primary or 0), helper = false} end local helper = ADFieldModel.helperFor(field.stage) if helper ~= nil and (counts.helpers or 0) < ADFieldModel.maxHelpers(field.stage, field.plan) then return {role = helper, count = 1, helper = true} end return nil end -- Hvilken afgrøde der skal sås: planens valg, ellers den afgrøde der blev høstet. function ADFieldModel.sowFruit(field) if field.plan.sowFruit ~= nil and field.plan.sowFruit ~= "" then return field.plan.sowFruit end return field.lastFruit end function ADFieldModel.with(field, changes) local copy = copyField(field) for key, value in pairs(changes) do if key == "plan" then for planKey, planValue in pairs(value) do copy.plan[planKey] = planValue end elseif value == false and (key == "reason") then copy[key] = nil else copy[key] = value end end return copy end -- Læssevognens Courseplay-forskydning når den kører rivens kursus (se etape 1-spec). function ADFieldModel.collectorOffset(ownOffset, windrowOffset) local offset = (ownOffset or 0) - (windrowOffset or 0) return math.max(-ADFieldModel.CP_MAX_TOOL_OFFSET, math.min(ADFieldModel.CP_MAX_TOOL_OFFSET, offset)) end -- Gæringssilo: en produktion der har markens skår som input og ensilage som output. function ADFieldModel.isFermenter(productions, windrowFillType, silageFillType) if silageFillType == nil then return false end for _, production in ipairs(productions or {}) do local takesWindrow, makesSilage = false, false for _, input in ipairs(production.inputs or {}) do if input.type == windrowFillType then takesWindrow = true end end for _, output in ipairs(production.outputs or {}) do if output.type == silageFillType then makesSilage = true end end if takesWindrow and makesSilage then return true end end return false end local function flag(value) return value and 1 or 0 end function ADFieldModel.toRecord(field) local plan = field.plan local care = {} for stage, runs in pairs(field.careRuns or {}) do table.insert(care, stage .. "=" .. tostring(runs)) end table.sort(care) return { id = field.id, stage = field.stage, mode = plan.mode, product = plan.product, rake = flag(plan.rake), store = table.concat(plan.storeBuildingIds, " "), dedicated = table.concat(plan.rigIds or {}, " "), rigs = plan.rigs, unloaders = plan.unloaders, mulch = flag(plan.mulch), lime = flag(plan.lime), soil = plan.soil, sow = flag(plan.sow), sowFruit = plan.sowFruit or "", roll = flag(plan.roll), fertilize = flag(plan.fertilize), weed = flag(plan.weed), targetFruit = plan.targetFruit or "", prepAfterMow = flag(plan.prepAfterMow), straw = plan.straw or "chop", kind = field.kind or "", lastFruit = field.lastFruit or "", care = table.concat(care, " "), mowWidth = field.mowWidth or 0, mowVehicle = field.mowVehicleId or "", rakeVehicle = field.rakeVehicleId or "", harvestVehicle = field.harvestVehicleId or "", windrowOffset = field.windrowOffset ~= nil and string.format("%.2f", field.windrowOffset) or "", } end local function readFlag(value, default) if value == nil then return default end return value == 1 or value == true end local function readText(value) return value ~= nil and value ~= "" and value or nil end function ADFieldModel.fromRecord(record) local field = ADFieldModel.newField(record.id) local plan = field.plan field.stage = KNOWN_STAGES[record.stage] and record.stage or "idle" plan.mode = record.mode == "auto" and "auto" or "manual" -- etape 1 gemte "silage"/"hay"; ukendt -> auto plan.product = PRODUCTS[record.product] and record.product or "auto" plan.rake = readFlag(record.rake, false) for id in string.gmatch(record.store or "", "%S+") do table.insert(plan.storeBuildingIds, id) end for id in string.gmatch(record.dedicated or "", "%S+") do table.insert(plan.rigIds, id) end plan.rigs = clamp(record.rigs or 1, 1, ADFieldModel.MAX_RIGS) plan.unloaders = clamp(record.unloaders or 1, 1, ADFieldModel.MAX_UNLOADERS) plan.mulch = readFlag(record.mulch, false) plan.lime = readFlag(record.lime, true) plan.soil = SOIL_VALUES[record.soil] and record.soil or "auto" plan.sow = readFlag(record.sow, false) plan.sowFruit = record.sowFruit or "" plan.roll = readFlag(record.roll, false) plan.fertilize = readFlag(record.fertilize, true) plan.weed = readFlag(record.weed, true) plan.targetFruit = record.targetFruit or "" plan.prepAfterMow = readFlag(record.prepAfterMow, false) plan.straw = STRAW_VALUES[record.straw] and record.straw or "chop" field.kind = readText(record.kind) field.lastFruit = readText(record.lastFruit) for stage, runs in string.gmatch(record.care or "", "(%w+)=(%d+)") do field.careRuns[stage] = tonumber(runs) end field.mowWidth = (record.mowWidth or 0) > 0 and record.mowWidth or nil field.mowVehicleId = readText(record.mowVehicle) field.rakeVehicleId = readText(record.rakeVehicle) field.harvestVehicleId = readText(record.harvestVehicle) field.windrowOffset = tonumber(record.windrowOffset) return field end