427 lines
17 KiB
Lua
427 lines
17 KiB
Lua
-- 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
|