Files
fs25-adsmartpickup/FS25_ADSmartPickup/adFieldModel.lua
T
2026-09-25 23:43:51 +02:00

222 lines
8.3 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, og bredde-reglerne. Alle funktioner
-- returnerer nye tabeller; job-løkken (adFieldJobs) og GUI'et kalder kun herind.
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.MAX_COLLECTORS = 2
ADFieldModel.CP_MAX_TOOL_OFFSET = 10
ADFieldModel.ROLES = {mowing = "mower", tedding = "tedder", raking = "rake", collecting = "collector"}
local KNOWN_STAGES = {idle = true, ready = true, mowing = true, tedding = true, raking = true, collecting = true, done = true}
local function copyList(list)
local copy = {}
for index, value in ipairs(list or {}) do copy[index] = value end
return copy
end
local function copyField(field)
local copy = {}
for key, value in pairs(field) do copy[key] = value end
copy.plan = {
mode = field.plan.mode, product = field.plan.product, rake = field.plan.rake,
storeBuildingIds = copyList(field.plan.storeBuildingIds),
}
return copy
end
function ADFieldModel.newField(id)
return {
id = id, stage = "idle", mowWidth = nil, retryAt = 0, reason = nil, startRequested = false,
plan = {mode = "manual", product = "silage", rake = false, storeBuildingIds = {}},
}
end
-- Hø vendes; Riv kan vælges til både hø og ensilage (samler skårene midt i rivens baner, så også
-- smalle standard-læssevogne kan tage dem).
function ADFieldModel.workStages(plan)
local stages = {"mowing"}
if plan.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. Vender og river kører
-- hele marken i deres egen bredde; efter riv ligger skåret midt i banen, så vognens bredde er ligegyldig.
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 (rivens kursus, ellers
-- slåmaskinens); de andre laver deres eget i deres egen bredde.
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)
return ADFieldModel.ROLES[stage]
end
function ADFieldModel.isWorking(field)
return ADFieldModel.ROLES[field.stage] ~= nil
end
function ADFieldModel.maxRigs(stage)
return stage == "collecting" and ADFieldModel.MAX_COLLECTORS or 1
end
-- Venter/færdig -> klar ved fuld vækst; klar -> venter hvis marken blev slået uden os.
-- Arbejdende marker styres af job-løkken, ikke af vækst.
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 then
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
function ADFieldModel.start(field)
local copy = copyField(field)
copy.stage = ADFieldModel.workStages(field.plan)[1]
copy.startRequested = false
copy.retryAt = 0
copy.reason = nil
copy.mowWidth = nil
copy.mowVehicleId = nil
copy.rakeVehicleId = nil
copy.windrowOffset = nil
return copy
end
function ADFieldModel.advance(field)
local copy = copyField(field)
local stages = ADFieldModel.workStages(field.plan)
copy.stage = "done"
for index, stage in ipairs(stages) do
if stage == field.stage and stages[index + 1] ~= nil then
copy.stage = stages[index + 1]
end
end
copy.retryAt = 0
copy.reason = nil
return copy
end
-- Stop fra menuen: arbejdet afbrydes; marken står klar igen (manuel start kræves).
function ADFieldModel.stop(field)
local copy = copyField(field)
if ADFieldModel.isWorking(field) then
copy.stage = "ready"
end
copy.startRequested = false
copy.reason = nil
return copy
end
-- Et sæt må ikke over Courseplays grænse; efter-trin skal dække mindst slåbredden.
function ADFieldModel.checkWidth(role, width, mowWidth)
if width > ADFieldModel.CP_MAX_WIDTH + ADFieldModel.WIDTH_TOLERANCE then
return false, "tooWide", {width, ADFieldModel.CP_MAX_WIDTH}
end
if role ~= "mower" and mowWidth ~= nil and width + ADFieldModel.WIDTH_TOLERANCE < mowWidth then
return false, "tooNarrow", {width, mowWidth}
end
return true
end
function ADFieldModel.wantsRig(field, assignedCount, now)
local role = ADFieldModel.roleFor(field.stage)
if role == nil or (field.retryAt or 0) > now or assignedCount >= ADFieldModel.maxRigs(field.stage) then
return nil
end
return role
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
else
copy[key] = value
end
end
return copy
end
-- Læssevognens Courseplay-forskydning når den kører rivens kursus: pickuppen skal ligge over skåret, der
-- ligger windrowOffset m til venstre (positiv) for rivens kursuslinje. Courseplay tillader ±10 m.
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 (FermentingSiloPack,
-- GrassSilageSilo m.fl. er produktioner, ikke siloer). productions = {{inputs = {{type}}, outputs = {{type}}}}.
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
function ADFieldModel.toRecord(field)
return {
id = field.id, stage = field.stage, mode = field.plan.mode, product = field.plan.product,
rake = field.plan.rake and 1 or 0, store = table.concat(field.plan.storeBuildingIds, " "),
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
function ADFieldModel.fromRecord(record)
local field = ADFieldModel.newField(record.id)
field.stage = KNOWN_STAGES[record.stage] and record.stage or "idle"
field.plan.mode = record.mode == "auto" and "auto" or "manual"
field.plan.product = record.product == "hay" and "hay" or "silage"
field.plan.rake = record.rake == 1 or record.rake == true
for id in string.gmatch(record.store or "", "%S+") do table.insert(field.plan.storeBuildingIds, id) end
field.mowWidth = (record.mowWidth or 0) > 0 and record.mowWidth or nil
field.mowVehicleId = record.mowVehicle ~= nil and record.mowVehicle ~= "" and record.mowVehicle or nil
field.rakeVehicleId = record.rakeVehicle ~= nil and record.rakeVehicle ~= "" and record.rakeVehicle or nil
field.windrowOffset = tonumber(record.windrowOffset)
return field
end