-- 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, = 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} -- 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. 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 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 -- Ligger (x, z) inden for omridset? (lige-ulige-reglen; kanten tæller som inde) local function 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 insidePolygon(x, z, field.verts) then return field.id end end return nil 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 -- 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) if ADFieldJobs.PREP_STAGES[field.stage] and info ~= nil and info.kind == "harvest" and info.phase == "ready" and (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 = 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 -- 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) 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 == "rewind" then ADFieldJobs.setField(ADFieldModel.with(field, {stage = "ready", retryAt = 0, reason = false})) 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 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 ~= 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 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 if 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 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