Files
fs25-adsmartpickup/FS25_ADSmartPickup/adFieldJobs.lua
T

532 lines
26 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)
-- manual: {[vehicleId] = true} = høstere spilleren selv kører til marken (for brede til vejene)
ADFieldJobs.state = {fields = {}, fleet = {}, assignments = {}, fleetMode = "all", courseRecords = {}, manual = {}}
-- 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
-- awaitingPlayer: høster med manuel transport, spilleren kører den til marken (tæller som i gang)
ADFieldJobs.ACTIVE_PHASES = {fetching = true, driving = true, starting = true, waitingCourse = true, working = true, awaitingPlayer = true}
-- Andel græs i mellemhøjde (høstbart, men halv ydelse) der får en afbrudt slåning til at vente.
ADFieldJobs.REGROW_RATIO = 0.1
-- Trin efter høsten der ødelægger/ignorerer en stående afgrøde.
-- Klargøring der kan måles på jorden: trin -> info-felt (andel af marken). Kultivering er gjort når jorden er
-- klargjort (kultiveret/pløjet/såbed/stubbearbejdet); pløjning først når den er pløjet.
ADFieldJobs.GROUND_PREP_STAGES = {cultivating = "prepared", plowing = "plowed"}
ADFieldJobs.PREPARED_DONE_RATIO = 0.95
ADFieldJobs.PREP_STAGES = {strawCollecting = true, baling = true, baleCollecting = true, mulching = true, liming = true,
plowing = true, cultivating = true, sowing = true, rolling = true}
-- ---- ren beslutning ----
-- Er afgrøden taget (trin med afgrøde)? Slåning: under 50 % står. Høst: marken er bar, eller under 5 % af HELE
-- marken står (rester i hjørnerne). readyRatio regner kun med den afgrøde der er tilbage, så en halvt høstet mark
-- lignede "væk" (in-game 27/9 F111 gik til kultivering med halvdelen stående).
ADFieldJobs.HARVEST_DONE_RATIO = 0.05
local function cropGone(stage, info)
if info == nil then return false end
if stage == "harvesting" and info.kind == "bare" then return true end
if stage == "harvesting" and type(info.standingRatio) == "number" then
return info.standingRatio < ADFieldJobs.HARVEST_DONE_RATIO
end
return (info.readyRatio or 0) < ADFieldModel.CUT_RATIO
end
-- Ligger (x, z) inden for omridset? (lige-ulige-reglen; kanten tæller som inde)
function ADFieldJobs.insidePolygon(x, z, verts)
local inside, j = false, #verts
for i = 1, #verts do
local a, b = verts[i], verts[j]
if (a.z > z) ~= (b.z > z) and x <= (b.x - a.x) * (z - a.z) / (b.z - a.z) + a.x then inside = not inside end
j = i
end
return inside
end
-- Efter genindlæsning: marken en CP-maskine står på, hvis markens aktuelle trin er maskinens rolle (ellers nil).
-- fields = {{id, stage, verts = {{x, z}}}}
function ADFieldJobs.resumeField(x, z, role, fields)
for _, field in ipairs(fields or {}) do
if #(field.verts or {}) >= 3 and ADFieldModel.roleFor(field.stage) == role and ADFieldJobs.insidePolygon(x, z, field.verts) then
return field.id
end
end
return nil
end
-- Hvor Courseplay skal starte i kurset: "last" (eget kursus + husket waypoint = fortsæt præcis), "nearest"
-- (marken er allerede delvist høstet/slået), ellers "first". CP's eget standardvalg er "first" — også på en
-- halvt høstet mark (in-game 27/9: mejetærskeren startede F111 forfra).
-- s = {reuse, remembered, stage, standingRatio}
ADFieldJobs.PARTLY_DONE_RATIO = 0.95
function ADFieldJobs.startAtFor(s)
if s.reuse and s.remembered then return "last" end
local cropStage = s.stage == "harvesting" or s.stage == "mowing"
if cropStage and type(s.standingRatio) == "number" and s.standingRatio < ADFieldJobs.PARTLY_DONE_RATIO then return "nearest" end
return "first"
end
-- Skal en maskine med husket CP-waypoint genstartes efter indlæsning? Kun hvis den stadig er midt i jobbet: den
-- står på en mark der er i gang med dens trin (fieldId), eller den står lige ved sit huskede waypoint. Et husket
-- waypoint fra et gammelt job sendte ellers maskinen ud ad vejene (in-game 27/9: to ROPA'er). s = {fieldId, distance}
ADFieldJobs.RESUME_NEAR_M = 30
function ADFieldJobs.shouldResume(s)
if s.fieldId ~= nil then return true end
return type(s.distance) == "number" and s.distance <= ADFieldJobs.RESUME_NEAR_M
end
-- Højst én maskine sendt ud pr. DISPATCH_GAP_MS på tværs af alle marker (in-game 27/9: 6 maskiner kørte fra
-- parkeringen i samme sekund efter Start alle).
ADFieldJobs.DISPATCH_GAP_MS = 10000
function ADFieldJobs.mayDispatch(now, lastAt)
return lastAt == nil or now - lastAt >= ADFieldJobs.DISPATCH_GAP_MS
end
function ADFieldJobs.assignmentCount()
local count = 0
for _ in pairs(ADFieldJobs.state.assignments) do count = count + 1 end
return count
end
-- Har Courseplay-starten taget for lang tid? Mens CP beregner kursus (store marker), må det tage op til
-- COURSE_GENERATE_MS; ellers START_MS (in-game 27/9 F90: 140 ha tog over 60 s, og moden opgav midt i beregningen).
ADFieldJobs.START_MS = 60000
ADFieldJobs.COURSE_GENERATE_MS = 300000
function ADFieldJobs.startTimedOut(elapsed, generating)
return elapsed > (generating and ADFieldJobs.COURSE_GENERATE_MS or ADFieldJobs.START_MS)
end
-- Høsten er færdig, men høsteren har korn i tanken: tøm den på marken før marken går videre (én gang pr. mark).
-- s = {fill, near, busy, controlled, tried}
ADFieldJobs.emptyAttempts = {}
function ADFieldJobs.shouldEmptyHarvester(s)
return (s.fill or 0) > 0 and s.near == true and not s.busy and not s.controlled and not s.tried
end
-- Markens punkt nærmest (x, z) — CP bruger det som kursets startsted. Uden position: første punkt.
function ADFieldJobs.nearestPoint(points, x, z)
if points == nil or points[1] == nil then return nil end
if x == nil or z == nil then return points[1] end
local best, bestD = nil, nil
for _, point in ipairs(points) do
local d = (point.x - x) ^ 2 + (point.z - z) ^ 2
if bestD == nil or d < bestD then best, bestD = point, d end
end
return best
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 straks; såningen venter selv på såtiden
if info ~= nil and info.kind == "bare" and field.plan.targetFruit ~= nil and field.plan.targetFruit ~= "" then
if ADFieldModel.shouldStartBare(field, "bare") then
if not hasMarker then return {action = "wait"} end
return {action = "start"}
end
end
-- klargøring på en mark hvor afgrøden står uhøstet: tilbage til klar (in-game 27/9 F16: kartofler
-- blev regnet som høstet, og marken stod til kultivering med afgrøden i jorden)
local standing = info ~= nil and type(info.standingRatio) == "number" and info.standingRatio >= ADFieldJobs.HARVEST_DONE_RATIO
if ADFieldJobs.PREP_STAGES[field.stage] and info ~= nil and info.kind == "harvest" and info.phase == "ready"
and (standing or (info.readyRatio or 0) >= ADFieldModel.READY_RATIO) then
return {action = "rewind"}
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, awaiting = 0, 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 == "awaitingPlayer" then awaiting = awaiting + 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
-- klargøringen er allerede gjort (målt på jorden): videre — også efter genindlæsning, hvor
-- opgaven ikke er husket (in-game 28/9: F17 blev kultiveret 3 gange)
local groundKey = ADFieldJobs.GROUND_PREP_STAGES[field.stage]
if groundKey ~= nil and info ~= nil and type(info[groundKey]) == "number" and info[groundKey] >= ADFieldJobs.PREPARED_DONE_RATIO 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
-- slåning uden maskine på en halvt slået mark: vent til genvæksten er fuldt udvokset igen
-- (et hjørne Courseplay efterlod i samme omgang, finished > 0, slås straks)
if field.stage == "mowing" and finished == 0 and info ~= nil and (info.immatureRatio or 0) > ADFieldJobs.REGROW_RATIO then
return {action = "wait", reason = "regrowing"}
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)
-- tømmevogne først når en høster faktisk er i gang (ikke mens spilleren kører den derud)
if helperRole ~= nil and active > awaiting 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 {}
ADFieldJobs.state.manual = loaded.manual or {}
ADFieldJobs.state.paused = loaded.paused == true
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
-- "Start nu" på en mark hvor moden venter på spilleren (manuel transport): overtag uden at vente
ADFieldJobs.takeoverRequested = {}
function ADFieldJobs.requestTakeover(id)
ADFieldJobs.takeoverRequested[id] = true
end
-- Venter moden på at spilleren kører en høster til marken? (assignment eller nil)
function ADFieldJobs.awaitingPlayer(fieldId)
for _, assignment in pairs(ADFieldJobs.state.assignments) do
if assignment.fieldId == fieldId and assignment.phase == "awaitingPlayer" then return assignment end
end
return nil
end
-- Under global pause: kun denne mark genoptages (de andre venter på Start alle). nil = pause igen.
function ADFieldJobs.setFieldResumed(id, isOn)
ADFieldJobs.state.resumed = ADFieldJobs.state.resumed or {}
ADFieldJobs.state.resumed[id] = isOn ~= false or nil
end
function ADFieldJobs.isFieldResumed(id)
return (ADFieldJobs.state.resumed or {})[id] == true
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
-- Pause (Stop alt og parkér): intet nyt sendes ud, ingen genstart efter indlæsning. Gemmes.
function ADFieldJobs.setPaused(value)
ADFieldJobs.state.paused = value == true
ADFieldJobs.state.resumed = {}
ADFieldJobs.save()
Logging.info(ADFieldJobs.LOG_PREFIX .. "markarbejde %s", value and "på pause" or "genoptaget")
end
function ADFieldJobs.toggleManual(vehicleId)
ADFieldJobs.state.manual = ADFieldFleet.toggle(ADFieldJobs.state.manual or {}, vehicleId)
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 = {}
-- Hvornår en mark begyndte at vente på en maskine (kø: længst ventende får næste ledige traktor).
ADFieldJobs.waitingSince = {}
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 == "rewind" then
ADFieldJobs.setField(ADFieldModel.with(field, {stage = "ready", retryAt = 0, reason = false}))
ADFieldJobs.waitingSince[info.id] = nil
ADFieldWork.windDownHelpers(adEnv, info.id, field.stage)
ADFieldJobs.log(info.id, "%s -> ready: afgrøden står stadig uhøstet (%.0f %% klar)", field.stage, (info.readyRatio or 0) * 100)
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
-- høsteren har stadig korn i tanken: CP genstartes og venter ved kursets ende, tømmevognene kommer
-- (marken bliver på høst til tanken er tom) — in-game 27/9 F15
if field.stage == "harvesting" and ADFieldWork.tryEmptyHarvester ~= nil then
local okEmpty, emptying = pcall(ADFieldWork.tryEmptyHarvester, adEnv, field, info)
if okEmpty and emptying then return end
end
local nextField = ADFieldModel.advance(field, info.needs)
ADFieldJobs.setField(nextField)
ADFieldJobs.waitingSince[info.id] = nil
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 ~= nil then
local key = "spu_field_reason_" .. decision.reason
if field.reason == nil or field.reason.key ~= key then
ADFieldJobs.note(info.id, {reason = {key = key}})
end
elseif decision.action == "dispatch" then
-- én udsendelse ad gangen på tværs af markerne: marken prøver igen ved næste tick
if not ADFieldJobs.mayDispatch(ADFieldJobs.now, ADFieldJobs.lastDispatchAt) then return end
local before = ADFieldJobs.assignmentCount()
ADFieldWork.dispatch(adEnv, field, info, decision)
if ADFieldJobs.assignmentCount() > before then
ADFieldJobs.lastDispatchAt = ADFieldJobs.now
ADFieldJobs.waitingSince[info.id] = nil
elseif ADFieldJobs.waitingSince[info.id] == nil then
ADFieldJobs.waitingSince[info.id] = ADFieldJobs.now
end
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
-- Stop alt og parkér: intet sendes ud og intet genstartes, før spilleren trykker Start alle — undtagen
-- marker spilleren har genoptaget enkeltvis ([Start] i Fields-listen, ADFieldJobs.setFieldResumed)
local paused, resumed = ADFieldJobs.state.paused, ADFieldJobs.state.resumed or {}
if paused and next(resumed) == nil then return end
if not paused and not ADFieldJobs.resumedAfterLoad then
-- én gang, før noget nyt sendes ud: CP-maskiner der var midt i et job, fortsætter (og tæller som aktive)
ADFieldJobs.resumedAfterLoad = true
local okResume, errResume = pcall(ADFieldWork.resumeAfterLoad, adEnv)
if not okResume then Logging.warning(ADFieldJobs.LOG_PREFIX .. "genstart efter indlæsning fejlede: %s", tostring(errResume)) end
end
local env = ADFieldJobs.environment()
local slice = ADFieldJobs.tickCount % ADFieldJobs.PASSIVE_SLICES
local stepped = 0
local infos = ADFields.list()
if ADPool ~= nil then infos = ADPool.queueOrder(infos, ADFieldJobs.waitingSince) end
for index, info in ipairs(infos) do
-- passive marker (der sker intet uden brugeren) vurderes kun i deres tiendedel af sekunderne
if (not paused or resumed[info.id]) and (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