333 lines
15 KiB
Lua
333 lines
15 KiB
Lua
-- AD Field Jobs
|
|
-- Markarbejdets job-løkke (hvert sekund): opdater markerne, start kæder og pleje, beslut hvilke sæt der
|
|
-- skal ud, og gå videre til næste trin. decide() er ren og testet; udsendelse og opfølgning af sættene
|
|
-- (AutoDrive/Courseplay) ligger i adFieldWork. Alt spil-nært er pcall'et pr. mark og pr. sæt.
|
|
|
|
ADFieldJobs = {}
|
|
|
|
ADFieldJobs.LOG_PREFIX = "ADSmartPickup: "
|
|
ADFieldJobs.TICK_MS = 1000
|
|
-- én mark scannes hvert andet sekund; passive marker (manuelle, uden mål) vurderes i ti portioner
|
|
ADFieldJobs.PASSIVE_SLICES = 10
|
|
-- tidsmåling: tager et tick mere end dette, skrives det i loggen (højst én gang pr. minut)
|
|
ADFieldJobs.SLOW_TICK_MS = 3
|
|
ADFieldJobs.tickCount = 0
|
|
-- Courseplay kan efterlade hjørner på skæve marker: efter så mange færdige ture pr. sæt går marken videre.
|
|
ADFieldJobs.MAX_PASSES = 2
|
|
-- fleetMode: "all" = alle ledige maskiner (standard, "vælg og glem"), "ticked" = kun med flueben
|
|
-- courseRecords: vehicleId -> post i kursus-registret (ADCourseCache)
|
|
ADFieldJobs.state = {fields = {}, fleet = {}, assignments = {}, fleetMode = "all", courseRecords = {}}
|
|
-- mark-id -> {mower = kursus, rake = kursus, <trin> = fælles multi-tool-kursus}: kopier (kun i hukommelsen;
|
|
-- efter genindlæsning hentes slå-/rivekurset fra maskinen, som CP selv gemmer kurset på)
|
|
ADFieldJobs.courses = {}
|
|
ADFieldJobs.finished = {}
|
|
-- starter halvt forskudt, så markarbejdet og menuens opdatering (begge hvert sekund) ikke rammer samme frame
|
|
ADFieldJobs.timer = 500
|
|
ADFieldJobs.now = 0
|
|
ADFieldJobs.loaded = false
|
|
|
|
ADFieldJobs.ACTIVE_PHASES = {driving = true, starting = true, waitingCourse = true, working = true}
|
|
|
|
-- ---- ren beslutning ----
|
|
|
|
-- Er afgrøden taget (trin med afgrøde)? Slåning: under 50 % står. Høst: marken er bar eller under 50 %.
|
|
local function cropGone(stage, info)
|
|
if info == nil then return false end
|
|
if stage == "harvesting" and info.kind == "bare" then return true end
|
|
return (info.readyRatio or 0) < ADFieldModel.CUT_RATIO
|
|
end
|
|
|
|
-- Beslutning for én mark. assignments = sæt på marken ({phase, stage, helper}); kun sæt på det aktuelle
|
|
-- trin tæller (en aflæsningsvogn på vej hjem fra høsten forstyrrer ikke kultiveringen).
|
|
-- env = {raining = bool}: vejrfølsomme trin (høst/slå/vend/riv/hø) sendes ikke ud i regn.
|
|
-- Svar: {action = "start"|"care"|"advance"|"dispatch"|"wait"|"none", role, count, helper, stage, care, reason}.
|
|
function ADFieldJobs.decide(field, info, assignments, now, env)
|
|
local hasMarker = info ~= nil and info.markerId ~= nil
|
|
local raining = env ~= nil and env.raining == true
|
|
local kind = field.kind or (info ~= nil and info.kind) or "mowable"
|
|
if field.stage == "ready" then
|
|
if not ADFieldModel.shouldStart(field) then return {action = "none"} end
|
|
if not hasMarker then return {action = "wait"} end
|
|
local first = ADFieldModel.workStages(field.plan, info.kind, info.needs)[1]
|
|
if raining and first ~= nil and ADFieldModel.isWeatherSensitive(first, field.plan, info.kind) then
|
|
return {action = "wait", reason = "rain"}
|
|
end
|
|
return {action = "start"}
|
|
end
|
|
-- mål-afgrøde på en bar mark: klargør og så så snart målet kan sås
|
|
if info ~= nil and info.kind == "bare" and field.plan.targetFruit ~= nil and field.plan.targetFruit ~= "" then
|
|
local plantable = env ~= nil and env.plantable ~= nil and env.plantable(field.plan.targetFruit) or false
|
|
if ADFieldModel.shouldStartBare(field, "bare", plantable) then
|
|
if not hasMarker then return {action = "wait"} end
|
|
return {action = "start"}
|
|
end
|
|
end
|
|
if not ADFieldModel.isWorking(field) then
|
|
local care = ADFieldModel.careNeed(field, info)
|
|
if care == nil then return {action = "none"} end
|
|
if field.plan.mode ~= "auto" and not field.startRequested then return {action = "none", care = care} end
|
|
if not hasMarker then return {action = "wait", care = care} end
|
|
return {action = "care", stage = care}
|
|
end
|
|
local active, finished, helpers = 0, 0, 0
|
|
for _, assignment in ipairs(assignments) do
|
|
if assignment.stage == nil or assignment.stage == field.stage then
|
|
local isActive = ADFieldJobs.ACTIVE_PHASES[assignment.phase]
|
|
if assignment.helper then
|
|
if isActive then helpers = helpers + 1 end
|
|
else
|
|
if isActive then active = active + 1 end
|
|
if assignment.phase == "finished" then finished = finished + 1 end
|
|
end
|
|
end
|
|
end
|
|
local isCropStage = ADFieldModel.CROP_STAGES[field.stage] == true
|
|
if active == 0 then
|
|
-- afgrøden er væk (sluppet undervejs, genindlæst, eller færdig): videre
|
|
if isCropStage and cropGone(field.stage, info) then return {action = "advance"} end
|
|
if finished > 0 then
|
|
local maxPasses = ADFieldJobs.MAX_PASSES * ADFieldModel.maxRigs(field.stage, field.plan)
|
|
if not isCropStage or finished >= maxPasses then return {action = "advance"} end
|
|
end
|
|
if not hasMarker then return {action = "wait"} end
|
|
if (field.retryAt or 0) > now then return {action = "none"} end
|
|
if raining and ADFieldModel.isWeatherSensitive(field.stage, field.plan, kind) then
|
|
return {action = "wait", reason = "rain"}
|
|
end
|
|
return {action = "dispatch", role = ADFieldModel.roleFor(field.stage), count = ADFieldModel.maxRigs(field.stage, field.plan), helper = false}
|
|
end
|
|
-- hovedsættene arbejder: fyld op med hjælpere (aflæsningsvogne) en ad gangen
|
|
local helperRole = ADFieldModel.helperFor(field.stage)
|
|
if helperRole ~= nil and helpers < ADFieldModel.maxHelpers(field.stage, field.plan) and hasMarker and (field.retryAt or 0) <= now then
|
|
return {action = "dispatch", role = helperRole, count = 1, helper = true}
|
|
end
|
|
return {action = "none"}
|
|
end
|
|
|
|
-- ---- tilstand ----
|
|
|
|
function ADFieldJobs.log(fieldId, fmt, ...)
|
|
Logging.info(ADFieldJobs.LOG_PREFIX .. "F%s " .. fmt, tostring(fieldId), ...)
|
|
end
|
|
|
|
function ADFieldJobs.getField(id)
|
|
return ADFieldJobs.state.fields[id] or ADFieldModel.newField(id)
|
|
end
|
|
|
|
function ADFieldJobs.setField(field)
|
|
ADFieldJobs.state.fields[field.id] = field
|
|
end
|
|
|
|
-- Ændr felter på marken (kopi via modellen).
|
|
function ADFieldJobs.note(fieldId, changes)
|
|
ADFieldJobs.setField(ADFieldModel.with(ADFieldJobs.getField(fieldId), changes))
|
|
end
|
|
|
|
function ADFieldJobs.assignmentsFor(fieldId)
|
|
local list = {}
|
|
for _, assignment in pairs(ADFieldJobs.state.assignments) do
|
|
if assignment.fieldId == fieldId then table.insert(list, assignment) end
|
|
end
|
|
return list
|
|
end
|
|
|
|
function ADFieldJobs.save()
|
|
local ok, err = pcall(ADFieldStorage.save, ADFieldJobs.state)
|
|
if not ok then Logging.warning(ADFieldJobs.LOG_PREFIX .. "marker kunne ikke gemmes: %s", tostring(err)) end
|
|
end
|
|
|
|
function ADFieldJobs.load()
|
|
local ok, loaded = pcall(ADFieldStorage.load)
|
|
if ok then
|
|
ADFieldJobs.state.fields, ADFieldJobs.state.fleet = loaded.fields, loaded.fleet
|
|
ADFieldJobs.state.fleetMode = loaded.fleetMode or "all"
|
|
ADFieldJobs.state.courseRecords = loaded.courseRecords or {}
|
|
else
|
|
Logging.warning(ADFieldJobs.LOG_PREFIX .. "marker kunne ikke indlæses: %s", tostring(loaded))
|
|
end
|
|
ADFieldJobs.loaded = true
|
|
end
|
|
|
|
function ADFieldJobs.setPlan(id, changes)
|
|
ADFieldJobs.note(id, {plan = changes})
|
|
ADFieldJobs.save()
|
|
end
|
|
|
|
function ADFieldJobs.requestStart(id)
|
|
ADFieldJobs.note(id, {startRequested = true})
|
|
end
|
|
|
|
function ADFieldJobs.forceReady(id)
|
|
ADFieldJobs.setField(ADFieldModel.forceReady(ADFieldJobs.getField(id)))
|
|
end
|
|
|
|
function ADFieldJobs.setTarget(id, fruitName)
|
|
ADFieldJobs.setField(ADFieldModel.setTarget(ADFieldJobs.getField(id), fruitName))
|
|
ADFieldJobs.log(id, "mål-afgrøde: %s", fruitName ~= "" and fruitName or "ingen")
|
|
ADFieldJobs.save()
|
|
end
|
|
|
|
function ADFieldJobs.setFleetMode(mode)
|
|
ADFieldJobs.state.fleetMode = mode
|
|
ADFieldJobs.save()
|
|
end
|
|
|
|
function ADFieldJobs.toggleFleet(vehicleId)
|
|
ADFieldJobs.state.fleet = ADFieldFleet.toggle(ADFieldJobs.state.fleet, vehicleId)
|
|
ADFieldJobs.save()
|
|
end
|
|
|
|
-- GUI'et henter flåde og gæringssiloer herfra (ligger i adFieldWork).
|
|
function ADFieldJobs.getRigs(adEnv) return ADFieldWork.getRigs(adEnv) end
|
|
function ADFieldJobs.getFermenters(...) return ADFieldWork.getFermenters(...) end
|
|
|
|
function ADFieldJobs.forgetFinished(fieldId)
|
|
local remaining = {}
|
|
for _, done in ipairs(ADFieldJobs.finished) do
|
|
if done.fieldId ~= fieldId then table.insert(remaining, done) end
|
|
end
|
|
ADFieldJobs.finished = remaining
|
|
end
|
|
|
|
local function finishedFor(fieldId)
|
|
local list = {}
|
|
for _, assignment in ipairs(ADFieldJobs.finished) do
|
|
if assignment.fieldId == fieldId then table.insert(list, assignment) end
|
|
end
|
|
return list
|
|
end
|
|
|
|
function ADFieldJobs.stopField(adEnv, id)
|
|
ADFieldWork.stopAll(adEnv, id)
|
|
ADFieldJobs.forgetFinished(id)
|
|
ADFieldJobs.setField(ADFieldModel.stop(ADFieldJobs.getField(id)))
|
|
ADFieldJobs.save()
|
|
ADFieldJobs.log(id, "stoppet fra menuen")
|
|
end
|
|
|
|
-- Besked i spillet (højre side). critical = rød. Samme tekst vises kun én gang i træk.
|
|
function ADFieldJobs.notify(text, critical)
|
|
if ADFieldJobs.lastNotice == text then return end
|
|
ADFieldJobs.lastNotice = text
|
|
pcall(function()
|
|
local kind = critical and FSBaseMission.INGAME_NOTIFICATION_CRITICAL or FSBaseMission.INGAME_NOTIFICATION_OK
|
|
g_currentMission:addIngameNotification(kind, "Smart Pickup: " .. text)
|
|
end)
|
|
end
|
|
|
|
-- Vejret lige nu (regn), læst en gang pr. tick.
|
|
function ADFieldJobs.environment()
|
|
local ok, raining = pcall(function()
|
|
local weather = g_currentMission.environment.weather
|
|
return weather ~= nil and weather.getIsRaining ~= nil and weather:getIsRaining() or false
|
|
end)
|
|
return {
|
|
raining = ok and raining == true,
|
|
-- kan afgrøden (navn) sås i den aktuelle periode?
|
|
plantable = function(name)
|
|
local fruit = g_fruitTypeManager ~= nil and g_fruitTypeManager:getFruitTypeByName(name) or nil
|
|
return fruit ~= nil and ADCpBridge.isPlantableNow(fruit.index)
|
|
end,
|
|
}
|
|
end
|
|
|
|
-- Seneste beslutning pr. mark (GUI viser pleje-behov og grunde).
|
|
ADFieldJobs.lastDecision = {}
|
|
|
|
local function stepField(adEnv, info, env)
|
|
local current = ADFieldJobs.getField(info.id)
|
|
local field = ADFieldModel.onReadiness(current, info.readyRatio or 0)
|
|
if field ~= current then ADFieldJobs.setField(field) end
|
|
local assignments = ADFieldJobs.assignmentsFor(info.id)
|
|
for _, done in ipairs(finishedFor(info.id)) do table.insert(assignments, done) end
|
|
local decision = ADFieldJobs.decide(field, info, assignments, ADFieldJobs.now, env)
|
|
ADFieldJobs.lastDecision[info.id] = decision
|
|
if decision.action == "start" then
|
|
local started = ADFieldModel.start(field, info.kind, info.needs, info.fruitName)
|
|
started.lastFruit = info.kind == "harvest" and info.fruitName or field.lastFruit
|
|
ADFieldJobs.setField(started)
|
|
ADFieldJobs.log(info.id, "starter (%s: %s)", tostring(info.kind), started.stage)
|
|
ADFieldJobs.save()
|
|
elseif decision.action == "care" then
|
|
ADFieldJobs.setField(ADFieldModel.startCare(field, decision.stage))
|
|
ADFieldJobs.log(info.id, "pleje: %s", decision.stage)
|
|
ADFieldJobs.save()
|
|
elseif decision.action == "advance" then
|
|
local nextField = ADFieldModel.advance(field, info.needs)
|
|
ADFieldJobs.setField(nextField)
|
|
ADFieldJobs.forgetFinished(info.id)
|
|
ADFieldWork.windDownHelpers(adEnv, info.id, field.stage)
|
|
ADFieldJobs.log(info.id, "%s -> %s", field.stage, nextField.stage)
|
|
if nextField.stage == "done" or (ADFieldModel.isCare(field.stage) and nextField.stage == "idle") then
|
|
ADFieldJobs.notify(string.format("F%d %s", info.id, g_i18n ~= nil and g_i18n:getText("spu_field_notice_done") or "done"), false)
|
|
end
|
|
ADFieldJobs.save()
|
|
elseif decision.action == "wait" and decision.reason == "rain" then
|
|
if field.reason == nil or field.reason.key ~= "spu_field_reason_rain" then
|
|
ADFieldJobs.note(info.id, {reason = {key = "spu_field_reason_rain"}})
|
|
end
|
|
elseif decision.action == "dispatch" then
|
|
ADFieldWork.dispatch(adEnv, field, info, decision)
|
|
end
|
|
end
|
|
|
|
-- Realtid i ms: netGetTime (bruges overalt i FS), ellers getTimeSec.
|
|
local function clockMs()
|
|
local ok, ms = pcall(function() return netGetTime() end)
|
|
if ok and type(ms) == "number" then return ms end
|
|
local okSec, seconds = pcall(function() return getTimeSec() end)
|
|
return okSec and type(seconds) == "number" and seconds * 1000 or nil
|
|
end
|
|
|
|
-- Et langsomt tick (> SLOW_TICK_MS) skrives i loggen med fordeling, højst én gang pr. minut.
|
|
local function reportSlow(total, scan, follow, steps, stepped)
|
|
if total == nil or total <= ADFieldJobs.SLOW_TICK_MS then return end
|
|
if ADFieldJobs.lastSlowReport ~= nil and ADFieldJobs.now - ADFieldJobs.lastSlowReport < 60000 then return end
|
|
ADFieldJobs.lastSlowReport = ADFieldJobs.now
|
|
Logging.info(ADFieldJobs.LOG_PREFIX .. "markarbejde-tick %.1f ms (scan %.1f, sæt %.1f, marker %.1f for %d marker)",
|
|
total, scan or 0, follow or 0, steps or 0, stepped)
|
|
end
|
|
|
|
ADFieldJobs.STARTUP_GRACE_MS = 120000
|
|
|
|
function ADFieldJobs.tick(adEnv, dt)
|
|
ADFieldJobs.now = ADFieldJobs.now + dt
|
|
ADFieldJobs.timer = ADFieldJobs.timer + dt
|
|
if ADFieldJobs.loaded then
|
|
-- markscanningen kører et lille stykke HVERT frame (arbejdskø), aldrig en hel mark i ét frame
|
|
local okScan, errScan = pcall(ADFields.scanStep, adEnv, g_currentMission:getFarmId(), ADFieldJobs.now)
|
|
if not okScan then Logging.warning(ADFieldJobs.LOG_PREFIX .. "markscanning: %s", tostring(errScan)) end
|
|
end
|
|
if ADFieldJobs.timer < ADFieldJobs.TICK_MS then return end
|
|
ADFieldJobs.timer = 0
|
|
ADFieldJobs.tickCount = ADFieldJobs.tickCount + 1
|
|
if not ADFieldJobs.loaded then ADFieldJobs.load() end
|
|
local t0 = clockMs()
|
|
local t1 = t0
|
|
for _, assignment in pairs(ADFieldJobs.state.assignments) do
|
|
local ok, err = pcall(ADFieldWork.follow, adEnv, assignment)
|
|
if not ok then
|
|
Logging.warning(ADFieldJobs.LOG_PREFIX .. "F%s opfølgning fejlede: %s", tostring(assignment.fieldId), tostring(err))
|
|
ADFieldWork.release(assignment)
|
|
end
|
|
end
|
|
local t2 = clockMs()
|
|
-- de første minutter efter indlæsning sendes intet ud: Courseplay/AutoDrive genoptager deres egne jobs
|
|
-- lidt efter start, og indtil da ligner de maskiner ledige (in-game 26/9: kultivatoren blev taget midt
|
|
-- i spillerens eget CP-job 45 s efter indlæsning)
|
|
if ADFieldJobs.now < ADFieldJobs.STARTUP_GRACE_MS then return end
|
|
local env = ADFieldJobs.environment()
|
|
local slice = ADFieldJobs.tickCount % ADFieldJobs.PASSIVE_SLICES
|
|
local stepped = 0
|
|
for index, info in ipairs(ADFields.list()) do
|
|
-- passive marker (der sker intet uden brugeren) vurderes kun i deres tiendedel af sekunderne
|
|
if not ADFieldModel.isPassive(ADFieldJobs.getField(info.id)) or index % ADFieldJobs.PASSIVE_SLICES == slice then
|
|
stepped = stepped + 1
|
|
local ok, err = pcall(stepField, adEnv, info, env)
|
|
if not ok then Logging.warning(ADFieldJobs.LOG_PREFIX .. "F%s fejl: %s", tostring(info.id), tostring(err)) end
|
|
end
|
|
end
|
|
local t3 = clockMs()
|
|
if t0 ~= nil and t3 ~= nil then reportSlow(t3 - t0, t1 - t0, t2 - t1, t3 - t2, stepped) end
|
|
end
|