Files
fs25-adsmartpickup/FS25_ADSmartPickup/adFieldModel.lua

427 lines
17 KiB
Lua
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- 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",
}
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,
}
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)
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(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")
return stages
end
-- Krævet bredde for et sæt: kun læssevognen uden riv skal dække slåmaskinens baner.
function ADFieldModel.requiredWidth(field, role)
if role == "collector" and not field.plan.rake 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)
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.
function ADFieldModel.onReadiness(field, ratio)
local copy = copyField(field)
if (field.stage == "idle" or field.stage == "done") and ratio >= ADFieldModel.READY_RATIO then
copy.stage = "ready"
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)
copy.stage = "idle"
copy.startRequested = false
end
return copy
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.windrowOffset = nil
copy.careRuns = {}
end
-- Første trin i kæden. En tom kæde (bar mark uden jordbehandling eller såning) -> færdig.
function ADFieldModel.start(field, kind, needs)
local copy = copyField(field)
local stages = ADFieldModel.workStages(field.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", "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}
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.
function ADFieldModel.checkWidth(role, width, requiredWidth)
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, " "),
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),
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 "",
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
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)
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.windrowOffset = tonumber(record.windrowOffset)
return field
end