diff --git a/FS25_ADSmartPickup/adFieldGuide.lua b/FS25_ADSmartPickup/adFieldGuide.lua
new file mode 100644
index 0000000..e3bdfa2
--- /dev/null
+++ b/FS25_ADSmartPickup/adFieldGuide.lua
@@ -0,0 +1,471 @@
+-- AD Field Guide
+-- "Opsæt mark": markguidens trin oven på ADGuide (ren Lua, ingen spil-API). Kladden er en kopi af
+-- markens plan (+ faste maskiner), så intet ændres før [Gem]. Alt spil-afhængigt kommer i `context`:
+-- {info, sowable = {{name, title}}, mowable = {[name] = true}, straw = {[name] = true} | nil,
+-- rigs = {{id, text, role}}, rigIdsByField = {[fieldId] = ids}, fieldSilos = {{id, text}},
+-- stores = {{id, text}} | nil (nil = markens vare kendes ikke), storeHidden, fields = {{id, text}}}
+-- Rækker er data: {type = "choice"|"toggle"|"check"|"text"|"link", id, title/label, ...}; GUI'et tegner dem.
+
+ADFieldGuide = {}
+
+ADFieldGuide.ORDER = {"field", "crop", "cropChoice", "prep", "care", "machines", "store", "summary"}
+ADFieldGuide.SOIL_VALUES = {"auto", "cultivate", "plow", "none"}
+ADFieldGuide.STRAW_VALUES = {"chop", "collect", "bale"}
+
+local function L(key, args) return {key = key, args = args} end
+local function T(text) return {text = text} end
+local YES_NO = {off = L("ui_no"), on = L("ui_yes")}
+
+-- ---- afledte egenskaber ----
+
+local function target(draft)
+ return draft.plan ~= nil and draft.plan.targetFruit or ""
+end
+
+local function isMowable(context, name)
+ return name ~= nil and name ~= "" and (context.mowable or {})[name] == true
+end
+
+-- Ukendt halm-tabel (tom/nil): alle høstafgrøder regnes med halm (som før guiden).
+local function hasStraw(context, name)
+ if context.straw == nil or next(context.straw) == nil then return true end
+ return name ~= nil and context.straw[name] == true
+end
+
+local function info(guide) return guide.context.info or {} end
+
+local function mowOptions(guide)
+ local t = target(guide.draft)
+ return info(guide).kind == "mowable" or isMowable(guide.context, t)
+end
+
+local function strawOptions(guide)
+ local t, i = target(guide.draft), info(guide)
+ if t ~= "" then
+ if isMowable(guide.context, t) then return false end
+ return hasStraw(guide.context, t)
+ end
+ return i.kind == "harvest" and hasStraw(guide.context, i.fruitName)
+end
+
+-- Der høstes (mejetærsker + aflæsningsvogne) nu eller senere.
+local function harvestInvolved(guide)
+ local t = target(guide.draft)
+ return info(guide).kind == "harvest" or (t ~= "" and not isMowable(guide.context, t))
+end
+
+local function prepVisible(guide)
+ local t, i = target(guide.draft), info(guide)
+ return i.kind ~= "mowable" or (t ~= "" and t ~= i.fruitName)
+end
+
+local function careVisible(guide)
+ return info(guide).kind ~= "bare" or target(guide.draft) ~= ""
+end
+
+-- Ukrudt giver kun mening på afgrøder der ikke slås (samme regel som før).
+local function weedVisible(guide)
+ local t = target(guide.draft)
+ if t ~= "" then return not isMowable(guide.context, t) end
+ return info(guide).kind ~= "mowable"
+end
+
+local VISIBLE = {
+ field = function(guide) return guide.draft.pickField == true end,
+ crop = function(guide) return guide.draft.fieldId ~= nil end,
+ cropChoice = function(guide) return guide.draft.fieldId ~= nil and (mowOptions(guide) or strawOptions(guide)) end,
+ prep = function(guide) return guide.draft.fieldId ~= nil and prepVisible(guide) end,
+ care = function(guide) return guide.draft.fieldId ~= nil and careVisible(guide) end,
+ machines = function(guide) return guide.draft.fieldId ~= nil end,
+ store = function(guide) return guide.draft.fieldId ~= nil and guide.context.stores ~= nil and info(guide).kind ~= "bare" end,
+ summary = function(guide) return guide.draft.fieldId ~= nil end,
+}
+
+function ADFieldGuide.isVisible(step, guide)
+ local rule = VISIBLE[step]
+ return rule ~= nil and rule(guide) == true
+end
+
+ADFieldGuide.DEF = {order = ADFieldGuide.ORDER, isVisible = ADFieldGuide.isVisible}
+
+function ADFieldGuide.stepKey(step) return "spu_guide_field_step_" .. tostring(step) end
+function ADFieldGuide.hintKey(step) return "spu_guide_field_hint_" .. tostring(step) end
+
+-- ---- kladde ----
+
+local function isPrepAuto(plan)
+ return (plan.soil or "auto") == "auto" and plan.mulch ~= true and plan.lime ~= false and plan.roll ~= true
+end
+
+local function storeModeOf(ids, fieldSilos)
+ if #ids == 0 then return "auto" end
+ local silo = {}
+ for _, entry in ipairs(fieldSilos or {}) do silo[entry.id] = true end
+ for _, id in ipairs(ids) do
+ if not silo[id] then return "manual" end
+ end
+ return "fieldSilo"
+end
+
+local function draftFrom(field, context)
+ local plan = ADGuide.deepCopy(field.plan or {})
+ plan.storeBuildingIds = plan.storeBuildingIds or {}
+ plan.rigIds = plan.rigIds or {}
+ local mode = storeModeOf(plan.storeBuildingIds, context.fieldSilos)
+ return {
+ fieldId = field.id, plan = plan, originalTarget = plan.targetFruit or "",
+ prepAuto = isPrepAuto(plan), useDedicated = #plan.rigIds > 0,
+ storeMode = mode, storeIds = mode == "manual" and ADGuide.deepCopy(plan.storeBuildingIds) or {},
+ }
+end
+
+function ADFieldGuide.open(field, context, options)
+ return ADGuide.new(ADFieldGuide.DEF, draftFrom(field, context), context, options)
+end
+
+-- Guiden åbnet uden valgt mark: trin 1 "Mark".
+function ADFieldGuide.openPicker(context)
+ return ADGuide.new(ADFieldGuide.DEF, {pickField = true}, context, {step = "field"})
+end
+
+-- Mark valgt i trin 1: kladden bygges fra markens plan; trin 1 tæller stadig med.
+function ADFieldGuide.pickField(guide, field, context)
+ local draft = draftFrom(field, context)
+ draft.pickField = true
+ local picked = ADGuide.withContext(ADGuide.withDraft(guide, draft), context)
+ return (ADGuide.next(picked))
+end
+
+-- ---- valg ----
+
+local PLAN_VALUES = {
+ targetFruit = true, sow = true, rake = true, mulch = true, lime = true, roll = true,
+ fertilize = true, weed = true, straw = true, soil = true, rigs = true, unloaders = true,
+}
+
+function ADFieldGuide.choose(guide, id, value)
+ local draft = ADGuide.deepCopy(guide.draft)
+ if PLAN_VALUES[id] then
+ draft.plan[id] = value
+ elseif id == "product" then
+ draft.plan.product = value and "hay" or "silage"
+ elseif id == "prepAuto" or id == "useDedicated" then
+ draft[id] = value == true
+ elseif id == "rigIds" then
+ draft.plan.rigIds = ADGuide.toggleItem(draft.plan.rigIds, value.item, value.on)
+ elseif id == "useFieldSilo" then
+ draft.storeMode = value and "fieldSilo" or "auto"
+ elseif id == "storeAuto" then
+ draft.storeMode = value and "auto" or "manual"
+ elseif id == "storeBuildingIds" then
+ draft.storeIds = ADGuide.toggleItem(draft.storeIds, value.item, value.on)
+ else
+ return guide
+ end
+ return ADGuide.withDraft(guide, draft)
+end
+
+-- ---- rækker pr. trin ----
+
+local function numberOptions(from, to)
+ local options = {}
+ for value = from, to do table.insert(options, {label = T(tostring(value)), value = value}) end
+ return options
+end
+
+local function choice(id, title, options, value)
+ return {type = "choice", id = id, title = title, options = options, value = value}
+end
+
+local function toggle(id, title, value, off, on)
+ return {type = "toggle", id = id, title = title, off = off or YES_NO.off, on = on or YES_NO.on, value = value == true}
+end
+
+local function rowsField(guide)
+ local rows = {}
+ for _, entry in ipairs(guide.context.fields or {}) do
+ table.insert(rows, {type = "link", id = "field", value = entry.id, label = T(entry.text)})
+ end
+ if #rows == 0 then table.insert(rows, {type = "text", label = L("spu_field_scanning")}) end
+ return rows
+end
+
+local function rowsCrop(guide)
+ local plan = guide.draft.plan
+ local options = {{label = L("spu_guide_crop_keep"), value = ""}}
+ for _, fruit in ipairs(guide.context.sowable or {}) do
+ table.insert(options, {label = T(fruit.title), value = fruit.name})
+ end
+ local rows = {choice("targetFruit", L("spu_guide_crop"), options, plan.targetFruit or "")}
+ if target(guide.draft) == "" and info(guide).kind ~= "mowable" then
+ table.insert(rows, toggle("sow", L("spu_guide_crop_sowAgain"), plan.sow))
+ end
+ return rows
+end
+
+local function rowsCropChoice(guide)
+ local plan, rows = guide.draft.plan, {}
+ if mowOptions(guide) then
+ table.insert(rows, toggle("product", L("spu_field_plan"), ADFieldModel.product(plan, "mowable") == "hay",
+ L("spu_field_plan_silage"), L("spu_field_plan_hay")))
+ table.insert(rows, toggle("rake", L("spu_field_rake"), plan.rake))
+ end
+ if strawOptions(guide) then
+ local options = {}
+ for _, value in ipairs(ADFieldGuide.STRAW_VALUES) do
+ table.insert(options, {label = L("spu_field_straw_" .. value), value = value})
+ end
+ table.insert(rows, choice("straw", L("spu_field_straw"), options, plan.straw or "chop"))
+ end
+ return rows
+end
+
+local function rowsPrep(guide)
+ local draft, plan = guide.draft, guide.draft.plan
+ local rows = {choice("prepAuto", L("spu_guide_prep"), {
+ {label = L("spu_guide_auto"), value = true}, {label = L("spu_guide_manual"), value = false}}, draft.prepAuto)}
+ if draft.prepAuto then
+ table.insert(rows, {type = "text", label = L("spu_guide_prep_autoText")})
+ return rows
+ end
+ local soil = {}
+ for _, value in ipairs(ADFieldGuide.SOIL_VALUES) do
+ table.insert(soil, {label = L("spu_field_soil_" .. value), value = value})
+ end
+ table.insert(rows, choice("soil", L("spu_field_soil"), soil, plan.soil or "auto"))
+ table.insert(rows, toggle("lime", L("spu_field_lime"), plan.lime ~= false))
+ table.insert(rows, toggle("mulch", L("spu_field_mulch"), plan.mulch))
+ if target(draft) ~= "" or plan.sow then
+ table.insert(rows, toggle("roll", L("spu_field_roll"), plan.roll))
+ end
+ return rows
+end
+
+local function rowsCare(guide)
+ local plan = guide.draft.plan
+ local rows = {toggle("fertilize", L("spu_field_fertilize"), plan.fertilize ~= false)}
+ if weedVisible(guide) then table.insert(rows, toggle("weed", L("spu_field_weed"), plan.weed ~= false)) end
+ if not prepVisible(guide) then table.insert(rows, toggle("lime", L("spu_field_lime"), plan.lime ~= false)) end
+ return rows
+end
+
+-- Faste maskiner: kun ledige og dem der allerede er faste her; faste på andre marker skjules med en linje.
+local function dedicatedRows(guide)
+ local draft, rows = guide.draft, {}
+ local byField = {}
+ for fieldId, ids in pairs(guide.context.rigIdsByField or {}) do byField[fieldId] = ids end
+ byField[draft.fieldId] = draft.plan.rigIds
+ local own = {}
+ for _, id in ipairs(draft.plan.rigIds) do own[id] = true end
+ local visible, hidden, fields = ADFieldFleet.visibleForField(guide.context.rigs or {}, draft.fieldId, byField)
+ local sorted = {}
+ for _, rig in ipairs(visible) do table.insert(sorted, rig) end
+ table.sort(sorted, function(a, b)
+ if (own[a.id] == true) ~= (own[b.id] == true) then return own[a.id] == true end
+ if (a.role or "") ~= (b.role or "") then return (a.role or "") < (b.role or "") end
+ return ADFlowView.naturalLess(tostring(a.text), tostring(b.text))
+ end)
+ for _, rig in ipairs(sorted) do
+ table.insert(rows, {type = "check", id = "rigIds", item = rig.id, title = T(rig.text), value = own[rig.id] == true})
+ end
+ if #sorted == 0 then table.insert(rows, {type = "text", label = L("spu_field_noRigs")}) end
+ if hidden > 0 then
+ local names = {}
+ for _, fieldId in ipairs(fields) do table.insert(names, "F" .. tostring(fieldId)) end
+ table.insert(rows, {type = "text", label = L("spu_field_hiddenDedicated", {hidden, table.concat(names, ", ")})})
+ end
+ return rows
+end
+
+local function rowsMachines(guide)
+ local draft, plan = guide.draft, guide.draft.plan
+ local rows = {choice("rigs", L("spu_guide_rigs"), numberOptions(1, ADFieldModel.MAX_RIGS), plan.rigs or 1)}
+ if harvestInvolved(guide) then
+ table.insert(rows, choice("unloaders", L("spu_field_unloaders"), numberOptions(1, ADFieldModel.MAX_UNLOADERS), plan.unloaders or 1))
+ end
+ table.insert(rows, toggle("useDedicated", L("spu_guide_dedicated"), draft.useDedicated))
+ if draft.useDedicated then
+ for _, row in ipairs(dedicatedRows(guide)) do table.insert(rows, row) end
+ end
+ return rows
+end
+
+local function rowsStore(guide)
+ local draft, context, rows = guide.draft, guide.context, {}
+ local silos = context.fieldSilos or {}
+ if #silos > 0 then
+ local names = {}
+ for _, silo in ipairs(silos) do table.insert(names, silo.text) end
+ table.insert(rows, toggle("useFieldSilo", L("spu_guide_useFieldSilo"), draft.storeMode == "fieldSilo"))
+ table.insert(rows, {type = "text", label = T(" " .. table.concat(names, ", "))})
+ if draft.storeMode == "fieldSilo" then return rows end
+ end
+ table.insert(rows, choice("storeAuto", L("spu_guide_store"), {
+ {label = L("spu_guide_auto"), value = true}, {label = L("spu_guide_manual"), value = false}}, draft.storeMode ~= "manual"))
+ if draft.storeMode ~= "manual" then
+ table.insert(rows, {type = "text", label = L("spu_guide_store_autoText")})
+ return rows
+ end
+ local chosen = {}
+ for _, id in ipairs(draft.storeIds) do chosen[id] = true end
+ for _, store in ipairs(context.stores or {}) do
+ table.insert(rows, {type = "check", id = "storeBuildingIds", item = store.id, title = T(store.text), value = chosen[store.id] == true})
+ end
+ if #(context.stores or {}) == 0 then table.insert(rows, {type = "text", label = L("spu_field_noStore")}) end
+ if (context.storeHidden or 0) > 0 then
+ table.insert(rows, {type = "text", label = L("spu_field_storeHidden", {context.storeHidden})})
+ end
+ return rows
+end
+
+local function rowsSummary(guide)
+ local rows = {}
+ for _, line in ipairs(ADFieldGuide.summary(guide)) do
+ table.insert(rows, {type = "link", id = "jump", value = line.step, line = line})
+ end
+ return rows
+end
+
+local ROWS = {
+ field = rowsField, crop = rowsCrop, cropChoice = rowsCropChoice, prep = rowsPrep, care = rowsCare,
+ machines = rowsMachines, store = rowsStore, summary = rowsSummary,
+}
+
+function ADFieldGuide.rows(guide)
+ local builder = ROWS[guide.step]
+ return builder ~= nil and builder(guide) or {}
+end
+
+-- ---- resumé ----
+
+local function fruitTitle(context, name)
+ for _, fruit in ipairs(context.sowable or {}) do
+ if fruit.name == name then return fruit.title end
+ end
+ return tostring(name)
+end
+
+local function storeText(guide)
+ local draft, context = guide.draft, guide.context
+ if draft.storeMode == "fieldSilo" then return {L("spu_guide_sum_fieldSilo")} end
+ if draft.storeMode ~= "manual" or #draft.storeIds == 0 then return {L("spu_guide_sum_auto")} end
+ local names = {}
+ for _, id in ipairs(draft.storeIds) do
+ for _, store in ipairs(context.stores or {}) do
+ if store.id == id then table.insert(names, store.text) end
+ end
+ end
+ if #names == 0 or #names > 2 or #names ~= #draft.storeIds then
+ return {L("spu_guide_sum_stores", {#draft.storeIds})}
+ end
+ return {T(table.concat(names, " + "))}
+end
+
+local SUMMARY = {
+ crop = function(guide)
+ local plan, t = guide.draft.plan, target(guide.draft)
+ if t ~= "" then return {L("spu_guide_sum_target", {fruitTitle(guide.context, t)})} end
+ local parts = {L("spu_guide_crop_keep")}
+ if plan.sow and info(guide).kind ~= "mowable" then table.insert(parts, L("spu_guide_sum_sowAgain")) end
+ return parts
+ end,
+ cropChoice = function(guide)
+ local plan, parts = guide.draft.plan, {}
+ if mowOptions(guide) then
+ table.insert(parts, L(ADFieldModel.product(plan, "mowable") == "hay" and "spu_field_plan_hay" or "spu_field_plan_silage"))
+ table.insert(parts, L(plan.rake and "spu_guide_sum_rake" or "spu_guide_sum_noRake"))
+ end
+ if strawOptions(guide) then table.insert(parts, L("spu_guide_sum_straw_" .. (plan.straw or "chop"))) end
+ return parts
+ end,
+ prep = function(guide)
+ local plan = guide.draft.plan
+ if guide.draft.prepAuto then return {L("spu_guide_sum_auto")} end
+ local parts = {L("spu_field_soil_" .. (plan.soil or "auto"))}
+ if plan.lime ~= false then table.insert(parts, L("spu_guide_sum_lime")) end
+ if plan.mulch then table.insert(parts, L("spu_guide_sum_mulch")) end
+ if plan.roll and (plan.sow or target(guide.draft) ~= "") then table.insert(parts, L("spu_guide_sum_roll")) end
+ return parts
+ end,
+ care = function(guide)
+ local plan, parts = guide.draft.plan, {}
+ if plan.fertilize ~= false then table.insert(parts, L("spu_guide_sum_fert")) end
+ if weedVisible(guide) and plan.weed ~= false then table.insert(parts, L("spu_guide_sum_weed")) end
+ if not prepVisible(guide) and plan.lime ~= false then table.insert(parts, L("spu_guide_sum_lime")) end
+ if #parts == 0 then parts = {L("spu_guide_sum_noCare")} end
+ return parts
+ end,
+ machines = function(guide)
+ local draft, plan = guide.draft, guide.draft.plan
+ local parts = {}
+ if draft.useDedicated and #plan.rigIds > 0 then
+ table.insert(parts, L("spu_guide_sum_dedicated", {#plan.rigIds}))
+ else
+ table.insert(parts, L("spu_guide_sum_freeRigs"))
+ end
+ table.insert(parts, L("spu_guide_sum_rigs", {plan.rigs or 1}))
+ if harvestInvolved(guide) then table.insert(parts, L("spu_guide_sum_unloaders", {plan.unloaders or 1})) end
+ return parts
+ end,
+ store = storeText,
+}
+
+-- Resumé-linjer {step, title, parts} for de synlige trin (ikke "Mark" og "Oversigt").
+function ADFieldGuide.summary(guide)
+ local lines = {}
+ for _, step in ipairs(ADGuide.visibleSteps(guide)) do
+ local build = SUMMARY[step]
+ if build ~= nil then
+ table.insert(lines, {step = step, title = L(ADFieldGuide.stepKey(step)), parts = build(guide)})
+ end
+ end
+ return lines
+end
+
+-- ---- gem ----
+
+-- start: true = [Kør automatisk], false = [Gem uden at starte], nil = [Ret] (tilstanden bevares).
+-- Mål-afgrøde kører altid automatisk (ADFieldModel.setTarget).
+function ADFieldGuide.result(guide, start)
+ local draft = guide.draft
+ local plan = ADGuide.deepCopy(draft.plan)
+ local t = plan.targetFruit or ""
+ if t ~= "" then
+ plan.mode = "auto"
+ elseif start ~= nil then
+ plan.mode = start and "auto" or "manual"
+ end
+ if draft.prepAuto and ADFieldGuide.isVisible("prep", guide) then
+ plan.soil, plan.mulch, plan.lime, plan.roll = "auto", false, true, false
+ end
+ if ADFieldGuide.isVisible("store", guide) then
+ if draft.storeMode == "fieldSilo" then
+ local ids = {}
+ for _, silo in ipairs(guide.context.fieldSilos or {}) do table.insert(ids, silo.id) end
+ plan.storeBuildingIds = ids
+ elseif draft.storeMode == "manual" then
+ plan.storeBuildingIds = ADGuide.deepCopy(draft.storeIds)
+ else
+ plan.storeBuildingIds = {}
+ end
+ end
+ local rigIds = draft.useDedicated and ADGuide.deepCopy(plan.rigIds) or {}
+ plan.rigIds = nil
+ return {fieldId = draft.fieldId, targetFruit = t, targetChanged = t ~= (draft.originalTarget or ""), plan = plan, rigIds = rigIds}
+end
+
+-- Faste maskiner for alle marker efter gem: marken får rigIds, og de fjernes fra andre marker.
+function ADFieldGuide.applyRigs(rigIdsByField, fieldId, rigIds)
+ local mine = {}
+ for _, id in ipairs(rigIds or {}) do mine[id] = true end
+ local result = {}
+ for otherId, ids in pairs(rigIdsByField or {}) do
+ if otherId ~= fieldId then
+ local copy = {}
+ for _, id in ipairs(ids or {}) do if not mine[id] then table.insert(copy, id) end end
+ result[otherId] = copy
+ end
+ end
+ result[fieldId] = ADGuide.deepCopy(rigIds or {})
+ return result
+end
diff --git a/FS25_ADSmartPickup/adGuide.lua b/FS25_ADSmartPickup/adGuide.lua
new file mode 100644
index 0000000..332ff36
--- /dev/null
+++ b/FS25_ADSmartPickup/adGuide.lua
@@ -0,0 +1,162 @@
+-- AD Guide
+-- Fælles guide-motor (ren Lua, ingen spil-API) for "Opsæt mark" og "Opsæt flow": ét trin ad gangen,
+-- trin der ikke er relevante springes over, tælling "Trin 3 af 7" over de synlige trin, og [Ret] der
+-- åbner ét enkelt trin. Motoren kender ikke marker eller flows; en trin-definition giver rækkefølgen og
+-- hvornår et trin vises:
+-- def = {order = {stepId, ...}, isVisible = function(stepId, guide) -> bool}
+-- Guiden er en tabel {def, draft, context, step, single, returnTo, snapshot}; alle funktioner returnerer
+-- en ny guide (kladden kopieres), så [Annullér] aldrig har ændret noget.
+
+ADGuide = {}
+
+-- Kopi af rene data (tabeller uden cykler); funktioner og tal/tekster deles.
+function ADGuide.deepCopy(value)
+ if type(value) ~= "table" then return value end
+ local copy = {}
+ for key, item in pairs(value) do copy[key] = ADGuide.deepCopy(item) end
+ return copy
+end
+
+-- Ny liste med item tilføjet (on) eller fjernet; aldrig dubletter.
+function ADGuide.toggleItem(list, item, on)
+ local result, found = {}, false
+ for _, value in ipairs(list or {}) do
+ if value == item then
+ found = true
+ if on then table.insert(result, value) end
+ else
+ table.insert(result, value)
+ end
+ end
+ if on and not found then table.insert(result, item) end
+ return result
+end
+
+local function clone(guide, changes)
+ local copy = {}
+ for key, value in pairs(guide) do copy[key] = value end
+ for key, value in pairs(changes or {}) do
+ if value == false and (key == "snapshot") then copy[key] = nil else copy[key] = value end
+ end
+ return copy
+end
+
+local function isVisible(guide, step)
+ return guide.def.isVisible == nil or guide.def.isVisible(step, guide) == true
+end
+
+function ADGuide.visibleSteps(guide)
+ local steps = {}
+ for _, step in ipairs(guide.def.order) do
+ if isVisible(guide, step) then table.insert(steps, step) end
+ end
+ return steps
+end
+
+local function orderIndex(guide, step)
+ for index, id in ipairs(guide.def.order) do
+ if id == step then return index end
+ end
+ return 0
+end
+
+-- Første synlige trin efter (dir = 1) eller før (dir = -1) det aktuelle i rækkefølgen.
+local function neighbour(guide, dir)
+ local order = guide.def.order
+ local index = orderIndex(guide, guide.step) + dir
+ while index >= 1 and index <= #order do
+ if isVisible(guide, order[index]) then return order[index] end
+ index = index + dir
+ end
+ return nil
+end
+
+-- options = {step = id, single = bool, returnTo = "close"|"summary"}
+function ADGuide.new(def, draft, context, options)
+ options = options or {}
+ local guide = {def = def, draft = draft, context = context, step = options.step,
+ single = options.single == true, returnTo = options.returnTo or "close"}
+ if guide.step == nil then
+ guide.step = ADGuide.visibleSteps(guide)[1] or def.order[1]
+ end
+ if guide.single then guide.snapshot = ADGuide.deepCopy(draft) end
+ return guide
+end
+
+-- index (nil hvis det aktuelle trin ikke er synligt), total synlige trin.
+function ADGuide.position(guide)
+ local steps = ADGuide.visibleSteps(guide)
+ local index = nil
+ for position, step in ipairs(steps) do
+ if step == guide.step then index = position end
+ end
+ return index, #steps
+end
+
+-- Sidste trin: ingen synlige efter, eller et [Ret]-trin der gemmer direkte.
+function ADGuide.isLast(guide)
+ if guide.single and guide.returnTo == "close" then return true end
+ if guide.single then return false end
+ return neighbour(guide, 1) == nil
+end
+
+-- Næste: event "done" når der gemmes (sidste trin, eller [Ret] fra fanens oversigt).
+function ADGuide.next(guide)
+ if guide.single then
+ if guide.returnTo == "summary" then
+ return clone(guide, {step = "summary", single = false, snapshot = false}), nil
+ end
+ return guide, "done"
+ end
+ local step = neighbour(guide, 1)
+ if step == nil then return guide, "done" end
+ return clone(guide, {step = step}), nil
+end
+
+-- Tilbage: event "cancel" når guiden lukkes (første trin, eller [Ret] fra fanens oversigt).
+-- I et [Ret]-trin fra guidens eget oversigts-trin fortrydes trinnets ændringer.
+function ADGuide.back(guide)
+ if guide.single then
+ if guide.returnTo == "summary" then
+ return clone(guide, {step = "summary", single = false, draft = guide.snapshot or guide.draft, snapshot = false}), nil
+ end
+ return guide, "cancel"
+ end
+ local step = neighbour(guide, -1)
+ if step == nil then return guide, "cancel" end
+ return clone(guide, {step = step}), nil
+end
+
+-- [Ret]: åbn kun ét trin. returnTo "summary" (guidens oversigt) eller "close" (gem og luk).
+function ADGuide.jump(guide, step, returnTo)
+ return clone(guide, {step = step, single = true, returnTo = returnTo or "close", snapshot = ADGuide.deepCopy(guide.draft)})
+end
+
+function ADGuide.withDraft(guide, draft)
+ return clone(guide, {draft = draft})
+end
+
+function ADGuide.withContext(guide, context)
+ return clone(guide, {context = context})
+end
+
+-- label = {key = "...", args = {...}} (oversættes) eller {text = "..."} (færdig tekst).
+function ADGuide.labelText(label, getText)
+ if type(label) ~= "table" then return "" end
+ if label.text ~= nil then return tostring(label.text) end
+ local template = getText(label.key)
+ if label.args == nil or #label.args == 0 then return tostring(template) end
+ local unpackFn = table.unpack or unpack
+ local ok, text = pcall(string.format, template, unpackFn(label.args))
+ return ok and text or tostring(template)
+end
+
+-- Resumé-linje {step, title, parts} -> "Afgrøde: Behold det der gror, så igen".
+function ADGuide.lineText(line, getText)
+ local parts = {}
+ for _, part in ipairs(line.parts or {}) do
+ local text = ADGuide.labelText(part, getText)
+ if text ~= "" then table.insert(parts, text) end
+ end
+ return ADGuide.labelText(line.title, getText) .. ": " .. (#parts > 0 and table.concat(parts, ", ") or "-")
+end
diff --git a/FS25_ADSmartPickup/adSmartPickup.lua b/FS25_ADSmartPickup/adSmartPickup.lua
index e026122..b636514 100644
--- a/FS25_ADSmartPickup/adSmartPickup.lua
+++ b/FS25_ADSmartPickup/adSmartPickup.lua
@@ -25,6 +25,8 @@ if ADSmartPickup.dir ~= nil and source ~= nil then
source(ADSmartPickup.dir .. "gui/SmartPickupStorageTab.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupFlowTab.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupFieldTab.lua")
+ source(ADSmartPickup.dir .. "gui/SmartPickupGuide.lua")
+ source(ADSmartPickup.dir .. "gui/SmartPickupFieldGuide.lua")
end
-- Markøren er dér hvor trækkerens næse stopper; læssetriggeren ligger et
diff --git a/FS25_ADSmartPickup/gui/SmartPickupFieldGuide.lua b/FS25_ADSmartPickup/gui/SmartPickupFieldGuide.lua
new file mode 100644
index 0000000..b8fad0f
--- /dev/null
+++ b/FS25_ADSmartPickup/gui/SmartPickupFieldGuide.lua
@@ -0,0 +1,236 @@
+-- Smart Pickup — guiden "Opsæt mark" og fanen Markers knapper
+-- Henter det spil-afhængige (afgrøder der kan sås, slåafgrøder, halm, maskiner, marksiloer, siloer i nærheden)
+-- som `context` til ADFieldGuide og gemmer guidens resultat via ADFieldJobs. Selve trin-skærmen er fælles
+-- (gui/SmartPickupGuide.lua). Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
+
+-- lagervalget viser de nærmeste siloer (plus valgte, marksiloer og gæringssiloer)
+SmartPickupFrame.FIELD_STORE_ROWS = 8
+
+function SmartPickupFrame:initFieldButtons(i18n)
+ self.fieldStartButtonInfo = {inputAction = InputAction.MENU_ACTIVATE, text = i18n:getText("spu_field_button_start"), callback = function() self:onClickFieldStartStop() end}
+ self.fieldSetupButtonInfo = {inputAction = InputAction.MENU_EXTRA_1, text = i18n:getText("spu_guide_field_open"), callback = function() self:onClickFieldSetup() end}
+ self.fieldFleetButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_guide_fleet_open"), callback = function() self:onClickFieldFleet(true) end}
+ self.fieldFleetCloseButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_guide_fleet_close"), callback = function() self:onClickFieldFleet(false) end}
+ self.fieldButtons = {self.backButtonInfo, self.fieldStartButtonInfo, self.fieldSetupButtonInfo, self.fieldFleetButtonInfo}
+ self.fieldFleetButtons = {self.backButtonInfo, self.fieldFleetCloseButtonInfo}
+end
+
+function SmartPickupFrame:getFieldButtons()
+ return self.fieldView == "fleet" and self.fieldFleetButtons or self.fieldButtons
+end
+
+-- ---- context til ADFieldGuide ----
+
+-- {[fieldId] = rigIds} for alle marker
+function SmartPickupFrame.rigIdsByField()
+ local byField = {}
+ for fieldId, field in pairs(ADFieldJobs.state.fields) do byField[fieldId] = field.plan ~= nil and field.plan.rigIds or {} end
+ return byField
+end
+
+local function fruitTypes()
+ return g_fruitTypeManager ~= nil and g_fruitTypeManager.fruitTypes or {}
+end
+
+-- Afgrøder der kan sås, slåafgrøder (navne) og afgrøder med halm (skår efter mejetærskeren).
+function SmartPickupFrame:fruitContext()
+ local sowable, straw, mowable = {}, {}, {}
+ for _, fruit in ipairs(fruitTypes()) do
+ if fruit.allowsSeeding ~= false and fruit.fillType ~= nil and fruit.name ~= nil then
+ table.insert(sowable, {name = fruit.name, title = tostring(fruit.fillType.title or fruit.name)})
+ end
+ local ok, hasStraw = pcall(function() return fruit.hasWindrow == true or fruit.windrowName ~= nil end)
+ if ok and hasStraw and fruit.name ~= nil then straw[fruit.name] = true end
+ end
+ if ADFields ~= nil and g_fruitTypeManager ~= nil then
+ for index in pairs(ADFields.getMowable()) do
+ local fruit = g_fruitTypeManager:getFruitTypeByIndex(index)
+ if fruit ~= nil and fruit.name ~= nil then mowable[fruit.name] = true end
+ end
+ end
+ return sowable, mowable, straw
+end
+
+-- Markens vare ud fra kladdens plan (ensilage -> skår, hø -> tørskår, høst -> afgrøden); nil på bar mark.
+local function fieldFillType(info, plan)
+ if info.kind == "mowable" then
+ return ADFieldModel.product(plan, "mowable") == "hay" and info.hay or info.windrow
+ end
+ if info.kind == "harvest" then return info.harvestFillType end
+ return nil
+end
+
+-- Siloer der tager markens vare: marksilo først, gæringssiloer, så de nærmeste (samme regel som før).
+function SmartPickupFrame:fieldStoreContext(adEnv, info, plan, chosenIds)
+ local fillType = fieldFillType(info, plan)
+ local farmId = g_currentMission:getFarmId()
+ local fieldSilos, silosById = {}, {}
+ for _, building in ipairs(ADFields.fieldSilos(adEnv, farmId, info)) do
+ local markers = ADBuildings.getMarkers ~= nil and ADBuildings.getMarkers(building, "unload") or {}
+ table.insert(fieldSilos, {id = building.id, name = tostring(building.name),
+ text = markers[1] ~= nil and tostring(markers[1].name) or tostring(building.name)})
+ silosById[building.id] = true
+ end
+ if fillType == nil then return nil, 0, fieldSilos end
+ local chosen = {}
+ for _, id in ipairs(chosenIds or {}) do chosen[id] = true end
+ local fermenterIds = {}
+ if info.kind == "mowable" and ADFieldModel.product(plan, "mowable") == "silage" then
+ for _, building in ipairs(ADFieldJobs.getFermenters(adEnv, farmId, fillType, info.x, info.z)) do fermenterIds[building.id] = true end
+ end
+ local entries = {}
+ for _, building in ipairs(ADBuildings.list(adEnv, farmId)) do
+ local isSilo = building.kind == "silo" and ADRunsController.canStoreAt(adEnv, building, {{fillType = fillType, isPallet = false}})
+ if chosen[building.id] or isSilo or fermenterIds[building.id] then
+ local distance = nil
+ local node = building.placeable ~= nil and building.placeable.rootNode or nil
+ if node ~= nil and info.x ~= nil then
+ local bx, _, bz = getWorldTranslation(node)
+ distance = math.sqrt((bx - info.x) ^ 2 + (bz - info.z) ^ 2)
+ end
+ table.insert(entries, {id = building.id, building = building, distance = distance, chosen = chosen[building.id] == true,
+ fieldSilo = silosById[building.id] == true, fermenter = fermenterIds[building.id] == true})
+ end
+ end
+ local shown, hidden = ADFieldView.storeRows(entries, SmartPickupFrame.FIELD_STORE_ROWS)
+ local stores = {}
+ for _, entry in ipairs(shown) do
+ local building = entry.building
+ local markers = ADBuildings.getMarkers ~= nil and ADBuildings.getMarkers(building, "unload") or {}
+ -- det der skelner rækkerne først (rækken afkorter bagfra): markør, afstand, art — så bygningens navn
+ local label = (markers[1] ~= nil and tostring(markers[1].name) or tostring(building.name)) .. ", " .. ADFieldView.distanceText(entry.distance)
+ if entry.fieldSilo then label = label .. " (" .. self.i18n:getText("spu_field_fieldSilo") .. ")" end
+ if entry.fermenter then label = label .. " (" .. self.i18n:getText("spu_field_fermenter") .. ")" end
+ if markers[1] ~= nil then label = label .. " — " .. tostring(building.name) end
+ table.insert(stores, {id = building.id, text = label})
+ end
+ return stores, hidden, fieldSilos
+end
+
+-- Markerne til trin 1 ("F22 — Hvede — Venter").
+function SmartPickupFrame:fieldPickList()
+ local fields = {}
+ for _, row in ipairs(ADFieldView.rows(ADFields.list(), ADFieldJobs.state.fields)) do
+ local crop = ADFieldView.cropText(row.info, function(key) return self.i18n:getText(key) end)
+ table.insert(fields, {id = row.info.id, text = string.format("F%d — %s — %s", row.info.id, crop, self:fieldStatusText(row.field, row.info, true))})
+ end
+ return fields
+end
+
+-- context for en mark (info) med kladdens plan; info nil = trin 1 uden valgt mark.
+function SmartPickupFrame:fieldGuideContext(adEnv, info, plan, chosenStoreIds)
+ local sowable, mowable, straw = self:fruitContext()
+ local context = {info = info or {}, sowable = sowable, mowable = mowable, straw = straw,
+ rigIdsByField = SmartPickupFrame.rigIdsByField(), fieldSilos = {}, storeHidden = 0, fields = self:fieldPickList(), rigs = {}}
+ for _, rig in ipairs(ADFieldJobs.getRigs(adEnv)) do
+ table.insert(context.rigs, {id = rig.id, role = rig.role, text = string.format("%s — %s", tostring(rig.label), self:roleText(rig.role))})
+ end
+ if info ~= nil then
+ context.stores, context.storeHidden, context.fieldSilos = self:fieldStoreContext(adEnv, info, plan or {}, chosenStoreIds)
+ end
+ return context
+end
+
+-- ---- hooks til den fælles guide-skærm ----
+
+local function chosenStoreIds(guide)
+ local ids = {}
+ for _, id in ipairs(guide.draft.storeIds or {}) do table.insert(ids, id) end
+ for _, id in ipairs(guide.draft.plan ~= nil and guide.draft.plan.storeBuildingIds or {}) do table.insert(ids, id) end
+ return ids
+end
+
+SmartPickupFrame.GUIDE_HOOKS = SmartPickupFrame.GUIDE_HOOKS or {}
+SmartPickupFrame.GUIDE_HOOKS.field = {
+ model = ADFieldGuide,
+ tab = SmartPickupFrame.TAB_FIELDS,
+ cannotAdvanceKey = "spu_guide_pickField",
+ heading = function(frame, guide)
+ if guide.draft.fieldId == nil then return frame.i18n:getText("spu_guide_field_titleNone") end
+ return string.format(frame.i18n:getText("spu_guide_field_title"), guide.draft.fieldId)
+ end,
+ context = function(frame, adEnv, guide)
+ local fieldId = guide.draft.fieldId
+ if fieldId == nil then return frame:fieldGuideContext(adEnv, nil) end
+ local info = ADFields.getInfo(fieldId)
+ -- marken er solgt/bygget over mens guiden var åben
+ if info == nil or info.cultivable == false then return nil end
+ return frame:fieldGuideContext(adEnv, info, guide.draft.plan, chosenStoreIds(guide))
+ end,
+ choose = function(frame, id, value)
+ if id ~= "field" then return false end
+ local info = ADFields.getInfo(value)
+ if info == nil then return true end
+ local field = ADFieldJobs.getField(value)
+ frame.selectedFieldId = value
+ frame.statusOverride = nil
+ frame.guide = ADFieldGuide.pickField(frame.guide, field, frame:fieldGuideContext(frame.getAdEnv(), info, field.plan, field.plan.storeBuildingIds))
+ return true
+ end,
+ canAdvance = function(guide) return guide.draft.fieldId ~= nil end,
+ startKey = function() return "spu_guide_runAuto" end,
+ -- mål-afgrøde kører altid automatisk: ingen "Gem uden at starte"
+ showSaveOnly = function(guide) return (guide.draft.plan.targetFruit or "") == "" end,
+ save = function(frame, guide, start) frame:saveFieldGuide(guide, start) end,
+ afterRows = function(frame, adEnv, guide)
+ if guide.step ~= "summary" or guide.draft.fieldId == nil then return end
+ local info = guide.context.info
+ if info ~= nil and info.markerId == nil then
+ frame:addTextRow(" " .. string.format(frame.i18n:getText("spu_guide_noMarker"), guide.draft.fieldId))
+ end
+ end,
+}
+
+-- Gem: mål-afgrøde (hvis ændret), planen, og faste maskiner (fjernes fra andre marker).
+function SmartPickupFrame:saveFieldGuide(guide, start)
+ local result = ADFieldGuide.result(guide, start)
+ if result.fieldId == nil then return end
+ if result.targetChanged then ADFieldJobs.setTarget(result.fieldId, result.targetFruit) end
+ ADFieldJobs.setPlan(result.fieldId, result.plan)
+ local byField = SmartPickupFrame.rigIdsByField()
+ for fieldId, ids in pairs(ADFieldGuide.applyRigs(byField, result.fieldId, result.rigIds)) do
+ if table.concat(byField[fieldId] or {}, " ") ~= table.concat(ids, " ") then
+ ADFieldJobs.setPlan(fieldId, {rigIds = ids})
+ end
+ end
+ ADFieldJobs.log(result.fieldId, "guide gemt: mode=%s mål=%s faste=%s lager=%s", tostring(result.plan.mode), result.targetFruit ~= "" and result.targetFruit or "-",
+ table.concat(result.rigIds, " "), table.concat(result.plan.storeBuildingIds or {}, " "))
+ self.selectedFieldId = result.fieldId
+ self.statusOverride = string.format(self.i18n:getText("spu_guide_saved"), result.fieldId)
+ self.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS
+end
+
+-- ---- knapper og [Ret] ----
+
+-- [Opsæt mark]: den valgte mark med de nuværende valg, ellers trin 1 (vælg mark).
+function SmartPickupFrame:onClickFieldSetup()
+ self:safe(function(frame)
+ local adEnv = frame.getAdEnv()
+ if adEnv == nil then return end
+ local info = frame.selectedFieldId ~= nil and ADFields.getInfo(frame.selectedFieldId) or nil
+ frame.fieldView = nil
+ if info == nil then
+ frame:openGuide("field", ADFieldGuide.openPicker(frame:fieldGuideContext(adEnv, nil)))
+ return
+ end
+ local field = ADFieldJobs.getField(info.id)
+ frame:openGuide("field", ADFieldGuide.open(field, frame:fieldGuideContext(adEnv, info, field.plan, field.plan.storeBuildingIds)))
+ end)
+end
+
+-- [Ret] på en resumé-linje i fanens oversigt: kun det ene trin; [Gem] gemmer og går tilbage.
+function SmartPickupFrame:openFieldStep(fieldId, step)
+ local adEnv = self.getAdEnv()
+ local info = ADFields.getInfo(fieldId)
+ if adEnv == nil or info == nil then return end
+ local field = ADFieldJobs.getField(fieldId)
+ local context = self:fieldGuideContext(adEnv, info, field.plan, field.plan.storeBuildingIds)
+ self:openGuide("field", ADFieldGuide.open(field, context, {step = step, single = true, returnTo = "close"}))
+end
+
+function SmartPickupFrame:onClickFieldFleet(show)
+ self:safe(function(frame)
+ frame.fieldView = show and "fleet" or nil
+ frame:requestRebuild()
+ end)
+end
diff --git a/FS25_ADSmartPickup/gui/SmartPickupFieldTab.lua b/FS25_ADSmartPickup/gui/SmartPickupFieldTab.lua
index 58b6b4b..8df465d 100644
--- a/FS25_ADSmartPickup/gui/SmartPickupFieldTab.lua
+++ b/FS25_ADSmartPickup/gui/SmartPickupFieldTab.lua
@@ -1,16 +1,12 @@
-- Smart Pickup — fanen Marker
--- Tabel over alle gårdens marker (Mark · Afgrøde · Plan · Kørsel · Status · Maskiner); klik på en række
--- viser markens detaljer under tabellen: plan efter markens art (slåafgrøde, høst, bar), maskiner pr. trin,
--- aflæsningsvogne, efter høst, såning, pleje, tving lager, jordtilstand. Nederst markflåden med flueben.
--- Al logik ligger i ADFieldJobs/ADFieldWork/ADFieldModel/ADFieldView.
+-- Oversigt: tabel over alle gårdens marker (Mark · Afgrøde · Plan · Kørsel · Status · Maskiner); klik på en
+-- række viser markens resumé (én linje pr. emne — klik på en linje for at rette netop det trin), hele
+-- statusteksten og markøren. [Opsæt mark] åbner guiden (gui/SmartPickupFieldGuide.lua + SmartPickupGuide.lua),
+-- [Markflåde] viser den fælles markflåde. Al logik ligger i ADFieldJobs/ADFieldModel/ADFieldView/ADFieldGuide.
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
-SmartPickupFrame.FIELD_ROW_KINDS = {
- fieldProduct = true, fieldMode = true, fieldRake = true, fieldStore = true, fleetVehicle = true,
- fieldRigs = true, fieldUnloaders = true, fieldMulch = true, fieldLime = true, fieldSoil = true,
- fieldSow = true, fieldSowFruit = true, fieldRoll = true, fieldFertilize = true, fieldWeed = true,
- fieldTarget = true, fleetMode = true, fieldStraw = true, fieldRig = true,
-}
+-- Markflådens rækker (vehiclePrefab/ingredientPrefab) routes til onClickFieldRow.
+SmartPickupFrame.FIELD_ROW_KINDS = {fleetVehicle = true, fleetMode = true}
SmartPickupFrame.ROLE_KEYS = {
mower = "spu_field_role_mower", tedder = "spu_field_role_tedder", rake = "spu_field_role_rake",
collector = "spu_field_role_collector", harvester = "spu_field_role_harvester", unloader = "spu_field_role_unloader",
@@ -19,15 +15,6 @@ SmartPickupFrame.ROLE_KEYS = {
weeder = "spu_field_role_weeder", fertilizer = "spu_field_role_fertilizer",
baler = "spu_field_role_baler", baleCollector = "spu_field_role_baleCollector",
}
-SmartPickupFrame.SOIL_VALUES = {"auto", "cultivate", "plow", "none"}
--- simple Ja/Nej-valg på planen: rækketype -> plan-felt
-SmartPickupFrame.PLAN_TOGGLES = {
- fieldRake = "rake", fieldMulch = "mulch", fieldLime = "lime", fieldSow = "sow", fieldRoll = "roll",
- fieldFertilize = "fertilize", fieldWeed = "weed",
-}
--- valg med flere muligheder: rækketype -> plan-felt
-SmartPickupFrame.PLAN_CHOICES = {fieldRigs = "rigs", fieldUnloaders = "unloaders", fieldSoil = "soil", fieldSowFruit = "sowFruit", fieldStraw = "straw"}
-SmartPickupFrame.STRAW_VALUES = {"chop", "collect", "bale"}
-- ---- tekster ----
@@ -95,18 +82,6 @@ function SmartPickupFrame:fieldStatusText(field, info, short)
return text
end
--- Hvad moden gør med marken frem mod mål-afgrøden (én linje under valget).
-function SmartPickupFrame:targetSummary(field, info)
- local target = self:fruitTitle(field.plan.targetFruit)
- if info.kind == "bare" then
- return string.format(self.i18n:getText("spu_field_target_bare"), target)
- end
- if info.fruitName == field.plan.targetFruit and info.kind == "mowable" then
- return string.format(self.i18n:getText("spu_field_target_keep"), target)
- end
- return string.format(self.i18n:getText("spu_field_target_switch"), tostring(info.cropTitle or "-"), target)
-end
-
function SmartPickupFrame:fruitTitle(name)
local fruit = g_fruitTypeManager ~= nil and name ~= nil and name ~= "" and g_fruitTypeManager:getFruitTypeByName(name) or nil
return fruit ~= nil and fruit.fillType ~= nil and tostring(fruit.fillType.title) or tostring(name)
@@ -139,47 +114,15 @@ function SmartPickupFrame:fieldCells(row)
}
end
--- ---- rækker ----
-
--- Valg med flere muligheder (klon af productPrefab): values[i] er værdien for texts[i].
-function SmartPickupFrame:addChoiceRow(kind, title, texts, values, current)
- local row = self:addRow(self.productPrefab, {kind = kind, values = values})
- local option = row:getDescendantByName("option")
- option:setTexts(texts)
- local index = 1
- for position, value in ipairs(values) do
- if value == current then index = position end
- end
- option:setState(index, false)
- row:getDescendantByName("title"):setText(title)
- return option
-end
-
-local function numberTexts(from, to)
- local texts, values = {}, {}
- for value = from, to do
- table.insert(texts, tostring(value))
- table.insert(values, value)
- end
- return texts, values
-end
-
--- Afgrøder der kan sås (første valg: samme som den høstede / ingen mål-afgrøde).
-function SmartPickupFrame:sowFruitChoices(firstKey)
- local texts, values = {self.i18n:getText(firstKey or "spu_field_sowSame")}, {""}
- for _, fruit in ipairs(g_fruitTypeManager ~= nil and g_fruitTypeManager.fruitTypes or {}) do
- if fruit.allowsSeeding ~= false and fruit.fillType ~= nil and fruit.name ~= nil then
- table.insert(texts, tostring(fruit.fillType.title or fruit.name))
- table.insert(values, fruit.name)
- end
- end
- return texts, values
-end
-
-- ---- opbygning ----
function SmartPickupFrame:fillFieldRows(adEnv)
self.fieldRows = {}
+ if self:hasGuideOn(SmartPickupFrame.TAB_FIELDS) and self:fillGuideRows(adEnv) then return end
+ if self.fieldView == "fleet" then
+ self:fillFleetRows(adEnv)
+ return
+ end
local i18n = self.i18n
local infos = ADFields.list()
if #infos == 0 then
@@ -201,161 +144,48 @@ function SmartPickupFrame:fillFieldRows(adEnv)
end
local selected = self.selectedFieldId ~= nil and ADFields.getInfo(self.selectedFieldId) or nil
if selected ~= nil then
- self:fillFieldDetail(adEnv, selected)
+ self:fillFieldOverview(adEnv, selected)
end
- self:fillFleetRows(adEnv)
end
--- lagervalget viser de nærmeste siloer (plus valgte, marksiloer og gæringssiloer)
-SmartPickupFrame.FIELD_STORE_ROWS = 8
-
-function SmartPickupFrame:fillFieldDetail(adEnv, info)
+-- Den valgte marks resumé: én klikbar linje pr. emne ([Ret] åbner kun det trin), hele status, markør og
+-- advarsel om marksilo der ikke tømmes hjem.
+function SmartPickupFrame:fillFieldOverview(adEnv, info)
local i18n = self.i18n
local field = ADFieldJobs.getField(info.id)
- local plan = field.plan
- local yesNo = {i18n:getText("ui_no"), i18n:getText("ui_yes")}
local crop = ADFieldView.cropText(info, function(key) return i18n:getText(key) end)
self:addTextRow(string.format(i18n:getText("spu_field_section"), info.id, crop))
- -- hele statusteksten (tabellens celle afkorter den)
+ self:addTextRow(" " .. i18n:getText("spu_guide_sum_hint"))
+ local context = self:fieldGuideContext(adEnv, info, field.plan, field.plan.storeBuildingIds)
+ local guide = ADFieldGuide.open(field, context)
+ local getText = function(key) return i18n:getText(key) end
+ local fieldId = info.id
+ for _, line in ipairs(ADFieldGuide.summary(guide)) do
+ local step = line.step
+ self:addLinkRow(" " .. ADGuide.lineText(line, getText) .. i18n:getText("spu_guide_editSuffix"), function(frame)
+ frame:openFieldStep(fieldId, step)
+ end)
+ end
self:addTextRow(" " .. string.format(i18n:getText("spu_field_statusFull"), self:fieldStatusText(field, info)))
- local targetTexts, targetValues = self:sowFruitChoices("spu_field_target_none")
- self:addChoiceRow("fieldTarget", i18n:getText("spu_field_target"), targetTexts, targetValues, plan.targetFruit or "")
- local hasTarget = plan.targetFruit ~= nil and plan.targetFruit ~= ""
- if hasTarget then
- self:addTextRow(" " .. self:targetSummary(field, info))
- else
- self:addToggleRow("fieldMode", i18n:getText("spu_field_mode"), {i18n:getText("spu_field_mode_manual"), i18n:getText("spu_field_mode_auto")}, plan.mode == "auto")
- end
- local rigTexts, rigValues = numberTexts(1, ADFieldModel.MAX_RIGS)
- self:addChoiceRow("fieldRigs", i18n:getText("spu_field_rigs"), rigTexts, rigValues, plan.rigs or 1)
- -- græs-valgene (ensilage/hø, riv) også når målet er en slåafgrøde (fx græs på en kornmark)
- local targetMowable = false
- if hasTarget and ADFields ~= nil and g_fruitTypeManager ~= nil then
- local targetFruit = g_fruitTypeManager:getFruitTypeByName(plan.targetFruit)
- targetMowable = targetFruit ~= nil and ADFields.getMowable()[targetFruit.index] ~= nil
- end
- if info.kind == "mowable" or targetMowable then
- local isHay = ADFieldModel.product(plan, "mowable") == "hay"
- self:addToggleRow("fieldProduct", i18n:getText("spu_field_plan"), {i18n:getText("spu_field_plan_silage"), i18n:getText("spu_field_plan_hay")}, isHay)
- self:addToggleRow("fieldRake", i18n:getText("spu_field_rake"), yesNo, plan.rake == true)
- end
- -- klargøring: høst-/bare marker, og græsmarker der skal skifte til en anden mål-afgrøde
- if info.kind ~= "mowable" or (hasTarget and info.fruitName ~= plan.targetFruit) then
- if info.kind == "harvest" then
- local unloaderTexts, unloaderValues = numberTexts(1, ADFieldModel.MAX_UNLOADERS)
- self:addChoiceRow("fieldUnloaders", i18n:getText("spu_field_unloaders"), unloaderTexts, unloaderValues, plan.unloaders or 1)
- local strawTexts = {}
- for _, value in ipairs(SmartPickupFrame.STRAW_VALUES) do table.insert(strawTexts, i18n:getText("spu_field_straw_" .. value)) end
- self:addChoiceRow("fieldStraw", i18n:getText("spu_field_straw"), strawTexts, SmartPickupFrame.STRAW_VALUES, plan.straw or "chop")
- end
- self:addTextRow(i18n:getText(info.kind == "harvest" and "spu_field_section_after" or "spu_field_section_prep"))
- self:addToggleRow("fieldMulch", i18n:getText("spu_field_mulch"), yesNo, plan.mulch == true)
- local soilTexts = {}
- for _, value in ipairs(SmartPickupFrame.SOIL_VALUES) do table.insert(soilTexts, i18n:getText("spu_field_soil_" .. value)) end
- self:addChoiceRow("fieldSoil", i18n:getText("spu_field_soil"), soilTexts, SmartPickupFrame.SOIL_VALUES, plan.soil or "auto")
- if not hasTarget then
- self:addToggleRow("fieldSow", i18n:getText("spu_field_sow"), yesNo, plan.sow == true)
- if plan.sow then
- local fruitTexts, fruitValues = self:sowFruitChoices()
- self:addChoiceRow("fieldSowFruit", i18n:getText("spu_field_sowFruit"), fruitTexts, fruitValues, plan.sowFruit or "")
- end
- end
- if hasTarget or plan.sow then
- self:addToggleRow("fieldRoll", i18n:getText("spu_field_roll"), yesNo, plan.roll == true)
- end
- end
- self:addTextRow(i18n:getText("spu_field_section_care"))
- self:addToggleRow("fieldFertilize", i18n:getText("spu_field_fertilize"), yesNo, plan.fertilize ~= false)
- self:addToggleRow("fieldLime", i18n:getText("spu_field_lime"), yesNo, plan.lime ~= false)
- if info.kind ~= "mowable" then
- self:addToggleRow("fieldWeed", i18n:getText("spu_field_weed"), yesNo, plan.weed ~= false)
- end
- local soil = self:soilText(info)
- self:addTextRow(" " .. string.format(i18n:getText("spu_field_soilState"), soil ~= "" and soil or i18n:getText("spu_field_soilOk")))
if info.markerId ~= nil then
self:addTextRow(string.format(i18n:getText("spu_field_marker"), tostring(info.markerName), info.markerDistance or 0))
else
- self:addTextRow(" " .. string.format(i18n:getText("spu_field_noMarker"), info.id))
+ self:addTextRow(" " .. string.format(i18n:getText("spu_guide_noMarker"), info.id))
end
- if field.mowWidth ~= nil then
- self:addTextRow(string.format(i18n:getText("spu_field_mowWidth"), field.mowWidth))
- end
- if field.plan.rake and field.windrowOffset ~= nil then
- self:addTextRow(string.format(i18n:getText("spu_field_windrowOffset"), math.abs(field.windrowOffset),
- i18n:getText(math.abs(field.windrowOffset) <= 0.05 and "spu_field_side_center" or (field.windrowOffset > 0 and "spu_field_side_left" or "spu_field_side_right"))))
- end
- self:fillFieldStoreRows(adEnv, info, field)
+ self:addFieldSiloWarnings(context.fieldSilos)
end
--- Tving lager: siloer der tager markens vare (skår/tørskår/høst); ved ensilage også gæringssiloer.
-function SmartPickupFrame:fillFieldStoreRows(adEnv, info, field)
- local i18n = self.i18n
- local product = ADFieldModel.product(field.plan, info.kind)
- local fillType
- if info.kind == "mowable" then
- fillType = product == "hay" and info.hay or info.windrow
- else
- fillType = info.harvestFillType
- end
- if fillType == nil then return end
- self:fillDedicatedRows(adEnv, field)
- self:addTextRow(i18n:getText("spu_field_section_store"))
- local chosen = {}
- for _, id in ipairs(field.plan.storeBuildingIds) do chosen[id] = true end
- local farmId = g_currentMission:getFarmId()
- local fieldSilos = {}
- for _, building in ipairs(ADFields.fieldSilos(adEnv, farmId, info)) do fieldSilos[building.id] = building end
- local fermenterIds = {}
- if info.kind == "mowable" and product == "silage" then
- for _, building in ipairs(ADFieldJobs.getFermenters(adEnv, farmId, fillType, info.x, info.z)) do fermenterIds[building.id] = true end
- end
- -- kandidater med markørnavn og afstand til marken (34 ens "Farma 500 + Obi 1000" kan ellers ikke skelnes)
- local entries = {}
- for _, building in ipairs(ADBuildings.list(adEnv, farmId)) do
- local isSilo = building.kind == "silo" and ADRunsController.canStoreAt(adEnv, building, {{fillType = fillType, isPallet = false}})
- if chosen[building.id] or isSilo or fermenterIds[building.id] then
- local distance = nil
- local node = building.placeable ~= nil and building.placeable.rootNode or nil
- if node ~= nil and info.x ~= nil then
- local bx, _, bz = getWorldTranslation(node)
- distance = math.sqrt((bx - info.x) ^ 2 + (bz - info.z) ^ 2)
- end
- table.insert(entries, {id = building.id, building = building, distance = distance, chosen = chosen[building.id] == true,
- fieldSilo = fieldSilos[building.id] ~= nil, fermenter = fermenterIds[building.id] == true})
- end
- end
- local storeRows, hiddenStores = ADFieldView.storeRows(entries, SmartPickupFrame.FIELD_STORE_ROWS)
- local count = #storeRows
- for _, entry in ipairs(storeRows) do
- local building = entry.building
- local row = self:addRow(self.ingredientPrefab, {kind = "fieldStore", buildingId = building.id})
- local markers = ADBuildings.getMarkers ~= nil and ADBuildings.getMarkers(building, "unload") or {}
- -- det der skelner rækkerne først (rækken afkorter bagfra): markør, afstand, art — så bygningens navn
- local label = (markers[1] ~= nil and tostring(markers[1].name) or tostring(building.name)) .. ", " .. ADFieldView.distanceText(entry.distance)
- if entry.fieldSilo then label = label .. " (" .. i18n:getText("spu_field_fieldSilo") .. ")" end
- if entry.fermenter then label = label .. " (" .. i18n:getText("spu_field_fermenter") .. ")" end
- if markers[1] ~= nil then label = label .. " — " .. tostring(building.name) end
- row:getDescendantByName("title"):setText(" " .. label)
- row:getDescendantByName("option"):setState(entry.chosen and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
- end
- if hiddenStores > 0 then
- self:addTextRow(" " .. string.format(i18n:getText("spu_field_storeHidden"), hiddenStores))
- end
- if count == 0 then
- self:addTextRow(" " .. i18n:getText("spu_field_noStore"))
- elseif #field.plan.storeBuildingIds == 0 then
- self:addTextRow(" " .. i18n:getText((info.kind == "mowable" and product == "silage") and "spu_field_storeAutoSilage" or "spu_field_storeAuto"))
- end
- -- marksiloer skal tømmes hjem af et udkørselsflow
- for id, building in pairs(fieldSilos) do
+-- Marksiloer skal tømmes hjem af et udkørselsflow.
+function SmartPickupFrame:addFieldSiloWarnings(fieldSilos)
+ for _, silo in ipairs(fieldSilos or {}) do
local emptied = false
for _, run in ipairs(ADRunsController.store.runs) do
for _, sourceId in ipairs(run.kind == "outbound" and run.sourceBuildingIds or {}) do
- if sourceId == id then emptied = true end
+ if sourceId == silo.id then emptied = true end
end
end
if not emptied then
- self:addTextRow(" " .. string.format(i18n:getText("spu_field_fieldSiloNotEmptied"), tostring(building.name)))
+ self:addTextRow(" " .. string.format(self.i18n:getText("spu_field_fieldSiloNotEmptied"), tostring(silo.name or silo.text)))
end
end
end
@@ -387,54 +217,14 @@ function SmartPickupFrame:rigDetails(rig)
return #parts > 0 and (" — " .. table.concat(parts, " — ")) or ""
end
--- {[fieldId] = rigIds} for alle marker
-local function rigIdsByField()
- local byField = {}
- for fieldId, field in pairs(ADFieldJobs.state.fields) do byField[fieldId] = field.plan ~= nil and field.plan.rigIds or {} end
- return byField
-end
-
--- "3 maskiner er faste på andre marker (F22, F90)"
-function SmartPickupFrame:addHiddenDedicatedRow(hidden, fields)
- if hidden <= 0 then return end
- local names = {}
- for _, fieldId in ipairs(fields) do table.insert(names, "F" .. tostring(fieldId)) end
- self:addTextRow(" " .. string.format(self.i18n:getText("spu_field_hiddenDedicated"), hidden, table.concat(names, ", ")))
-end
-
--- Faste maskiner til den valgte mark: en fast maskine bruges kun her, og marken venter på sin egen hellere
--- end at låne en anden med samme opgave. Maskiner der er faste på en anden mark er markeret.
-function SmartPickupFrame:fillDedicatedRows(adEnv, field)
- local i18n = self.i18n
- self:addTextRow(i18n:getText("spu_field_section_dedicated"))
- local owner = {}
- for fieldId, other in pairs(ADFieldJobs.state.fields) do
- for _, id in ipairs(other.plan ~= nil and other.plan.rigIds or {}) do owner[id] = fieldId end
- end
- local rigs, hiddenRigs, hiddenFields = ADFieldFleet.visibleForField(ADFieldJobs.getRigs(adEnv), field.id, rigIdsByField())
- table.sort(rigs, function(a, b)
- local ma, mb = owner[a.id] == field.id, owner[b.id] == field.id
- if ma ~= mb then return ma end
- if a.role ~= b.role then return a.role < b.role end
- return ADFlowView.naturalLess(a.label, b.label)
- end)
- for _, rig in ipairs(rigs) do
- local row = self:addRow(self.vehiclePrefab, {kind = "fieldRig", vehicleId = rig.id})
- local elsewhere = owner[rig.id] ~= nil and owner[rig.id] ~= field.id
- and (" — " .. string.format(i18n:getText("spu_field_dedicatedElsewhere"), tostring(owner[rig.id]))) or ""
- row:getDescendantByName("title"):setText(string.format(" %s — %s%s", tostring(rig.label), self:roleText(rig.role), elsewhere))
- row:getDescendantByName("option"):setState(owner[rig.id] == field.id and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
- end
- self:addHiddenDedicatedRow(hiddenRigs, hiddenFields)
-end
-
+-- Markflåden (egen visning): alle maskiner med flueben, "fast på F22", "i gang på F108", bredde-advarsler.
function SmartPickupFrame:fillFleetRows(adEnv)
local i18n = self.i18n
self:addTextRow(i18n:getText("spu_field_section_fleet"))
local useAll = ADFieldJobs.state.fleetMode ~= "ticked"
self:addToggleRow("fleetMode", i18n:getText("spu_field_fleetAll"), {i18n:getText("ui_no"), i18n:getText("ui_yes")}, useAll)
- -- valgt mark: maskiner der er faste på en anden mark skjules (de kan ikke bruges her)
- local rigs, hiddenRigs, hiddenFields = ADFieldFleet.visibleForField(ADFieldJobs.getRigs(adEnv), self.selectedFieldId, rigIdsByField())
+ local rigs = {}
+ for _, rig in ipairs(ADFieldJobs.getRigs(adEnv)) do table.insert(rigs, rig) end
table.sort(rigs, function(a, b)
if a.role ~= b.role then return a.role < b.role end
return ADFlowView.naturalLess(a.label, b.label)
@@ -448,12 +238,15 @@ function SmartPickupFrame:fillFleetRows(adEnv)
if #rigs == 0 then
self:addTextRow(" " .. i18n:getText("spu_field_noRigs"))
end
- self:addHiddenDedicatedRow(hiddenRigs, hiddenFields)
end
-- ---- live ----
function SmartPickupFrame:refreshFieldRows(adEnv)
+ if self:hasGuideOn(SmartPickupFrame.TAB_FIELDS) or self.fieldView == "fleet" then
+ self:setStatusText(self.statusOverride or "")
+ return
+ end
local rowsById = {}
for _, row in ipairs(ADFieldView.rows(ADFields.list(), ADFieldJobs.state.fields)) do rowsById[row.info.id] = row end
local working, total = 0, 0
@@ -473,45 +266,15 @@ function SmartPickupFrame:refreshFieldRows(adEnv)
self.fieldStartButtonInfo.disabled = selected == nil or info == nil or (not isWorking and info.markerId == nil)
end
--- ---- klik ----
+-- ---- klik (markflåden) ----
function SmartPickupFrame:onClickFieldRow(state, data)
self:safe(function(frame)
local isOn = state == SmartPickupFrame.STATE_ON
- local id = frame.selectedFieldId
if data.kind == "fleetVehicle" then
ADFieldJobs.toggleFleet(data.vehicleId)
elseif data.kind == "fleetMode" then
ADFieldJobs.setFleetMode(isOn and "all" or "ticked")
- elseif id == nil then
- return
- elseif data.kind == "fieldTarget" then
- ADFieldJobs.setTarget(id, data.values ~= nil and data.values[state] or "")
- elseif SmartPickupFrame.PLAN_CHOICES[data.kind] ~= nil then
- local value = data.values ~= nil and data.values[state] or nil
- if value ~= nil then ADFieldJobs.setPlan(id, {[SmartPickupFrame.PLAN_CHOICES[data.kind]] = value}) end
- elseif data.kind == "fieldProduct" then
- ADFieldJobs.setPlan(id, {product = isOn and "hay" or "silage"})
- elseif data.kind == "fieldMode" then
- ADFieldJobs.setPlan(id, {mode = isOn and "auto" or "manual"})
- elseif SmartPickupFrame.PLAN_TOGGLES[data.kind] ~= nil then
- ADFieldJobs.setPlan(id, {[SmartPickupFrame.PLAN_TOGGLES[data.kind]] = isOn})
- elseif data.kind == "fieldRig" then
- -- fast på denne mark (og fjernes fra en anden mark)
- local byField = {}
- for fieldId, other in pairs(ADFieldJobs.state.fields) do byField[fieldId] = other.plan ~= nil and other.plan.rigIds or {} end
- local updated = ADFieldFleet.dedicate(byField, id, data.vehicleId, isOn)
- for fieldId, ids in pairs(updated) do
- local before = table.concat(byField[fieldId] or {}, " ")
- if before ~= table.concat(ids, " ") then ADFieldJobs.setPlan(fieldId, {rigIds = ids}) end
- end
- elseif data.kind == "fieldStore" then
- local ids = {}
- for _, buildingId in ipairs(ADFieldJobs.getField(id).plan.storeBuildingIds) do
- if buildingId ~= data.buildingId then table.insert(ids, buildingId) end
- end
- if isOn then table.insert(ids, data.buildingId) end
- ADFieldJobs.setPlan(id, {storeBuildingIds = ids})
end
frame:requestRebuild()
end)
diff --git a/FS25_ADSmartPickup/gui/SmartPickupFrame.lua b/FS25_ADSmartPickup/gui/SmartPickupFrame.lua
index 6c57dae..60f60f0 100644
--- a/FS25_ADSmartPickup/gui/SmartPickupFrame.lua
+++ b/FS25_ADSmartPickup/gui/SmartPickupFrame.lua
@@ -52,8 +52,9 @@ function SmartPickupFrame.new(i18n, getAdEnv)
self.flowButtons = {self.backButtonInfo, self.startButtonInfo, self.newButtonInfo, self.renameButtonInfo, self.deleteButtonInfo}
self.overviewButtons = {self.backButtonInfo, self.startAllButtonInfo, self.stopAllButtonInfo, self.newButtonInfo}
-- Marker: Start nu / Stop for den valgte mark
- self.fieldStartButtonInfo = {inputAction = InputAction.MENU_ACTIVATE, text = i18n:getText("spu_field_button_start"), callback = function() self:onClickFieldStartStop() end}
- self.fieldButtons = {self.backButtonInfo, self.fieldStartButtonInfo}
+ -- Marker: [Start nu/Stop], [Opsæt mark], [Markflåde] (gui/SmartPickupFieldGuide.lua); guidens knapper
+ self:initFieldButtons(i18n)
+ self:initGuideButtons(i18n)
-- Egen reference til listen: fallback'en i refreshButtons skal kunne sende den igen.
self.menuButtonInfo = self.overviewButtons
self:setMenuButtonInfo(self.menuButtonInfo)
@@ -197,6 +198,8 @@ end
function SmartPickupFrame:onFrameClose()
self.frameOpen = false
+ -- en åben guide kasseres (intet er gemt før [Gem])
+ self:closeGuide()
SmartPickupFrame:superClass().onFrameClose(self)
end
@@ -243,6 +246,7 @@ function SmartPickupFrame:requestRebuild()
end
function SmartPickupFrame:switchTab(tabIndex)
+ if self.guide ~= nil and not self:hasGuideOn(tabIndex) then self:closeGuide() end
self.currentTab = tabIndex
for index, tab in ipairs(self.subCategoryTabs) do
tab:setSelected(index == tabIndex)
@@ -665,10 +669,12 @@ end
-- sendes listen igen. Kaldes hvert sekund, så den må aldrig kaste.
function SmartPickupFrame:refreshButtons()
local wanted = self.flowButtons
- if self.currentTab == SmartPickupFrame.TAB_OVERVIEW then
+ if self:hasGuideOn(self.currentTab) then
+ wanted = self:getGuideButtons()
+ elseif self.currentTab == SmartPickupFrame.TAB_OVERVIEW then
wanted = self.overviewButtons
elseif self.currentTab == SmartPickupFrame.TAB_FIELDS then
- wanted = self.fieldButtons
+ wanted = self:getFieldButtons()
end
if self.menuButtonInfo ~= wanted then
self.menuButtonInfo = wanted
@@ -757,7 +763,9 @@ end
function SmartPickupFrame:onClickIngredient(state, element)
-- mål- og kilde-rækkerne deler prefab (og dermed onClick) med vare-rækkerne
local data = self.rowData[element]
- if data ~= nil and data.kind == "target" then
+ if data ~= nil and data.kind == "guideRow" then
+ return self:onClickGuideRow(state, data)
+ elseif data ~= nil and data.kind == "target" then
return self:onClickTarget(state, element)
elseif data ~= nil and data.kind == "source" then
return self:onClickSource(state, element)
@@ -806,6 +814,9 @@ function SmartPickupFrame:onClickVehicle(state, element)
if clicked ~= nil and (clicked.kind == "fleetVehicle" or clicked.kind == "fieldRig") then
return self:onClickFieldRow(state, clicked)
end
+ if clicked ~= nil and clicked.kind == "guideRow" then
+ return self:onClickGuideRow(state, clicked)
+ end
self:safe(function(frame)
local run, data = frame:getRun(), frame.rowData[element]
-- kun flowets traktorrækker (de har et køretøj); alt andet ignoreres
diff --git a/FS25_ADSmartPickup/gui/SmartPickupGuide.lua b/FS25_ADSmartPickup/gui/SmartPickupGuide.lua
new file mode 100644
index 0000000..d0dbdc7
--- /dev/null
+++ b/FS25_ADSmartPickup/gui/SmartPickupGuide.lua
@@ -0,0 +1,229 @@
+-- Smart Pickup — fælles guide-skærm ("Opsæt mark" / "Opsæt flow")
+-- Ét trin ad gangen: overskrift, "Trin 3 af 7 — …", kort forklaring, valgene som rækker og bundknapperne
+-- Næste/Gem, Tilbage, Annullér (+ Gem uden at starte på oversigts-trinnet). Al logik ligger i ADGuide og
+-- trin-definitionerne (ADFieldGuide/ADFlowGuide); her tegnes kun rækkerne og klik routes.
+-- Hver guide registrerer sig i SmartPickupFrame.GUIDE_HOOKS[kind] = {
+-- model, tab, heading(frame, guide), context(frame, adEnv, guide) -> context|nil (nil = luk guiden),
+-- choose(frame, id, value) -> handled, save(frame, guide, start), startKey(guide), showSaveOnly(guide),
+-- canAdvance(guide), afterRows(frame, adEnv, guide) }
+-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
+
+SmartPickupFrame.GUIDE_HOOKS = SmartPickupFrame.GUIDE_HOOKS or {}
+
+function SmartPickupFrame:initGuideButtons(i18n)
+ self.guideNextButtonInfo = {inputAction = InputAction.MENU_ACTIVATE, text = i18n:getText("spu_guide_next"), callback = function() self:onClickGuideNext(true) end}
+ self.guideSaveOnlyButtonInfo = {inputAction = InputAction.MENU_EXTRA_1, text = i18n:getText("spu_guide_saveOnly"), callback = function() self:onClickGuideNext(false) end}
+ self.guidePrevButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_guide_back"), callback = function() self:onClickGuideBack() end}
+ self.guideCancelButtonInfo = {inputAction = InputAction.MENU_CANCEL, text = i18n:getText("spu_guide_cancel"), callback = function() self:onClickGuideCancel() end}
+ self.guideButtons = {self.backButtonInfo, self.guideNextButtonInfo, self.guidePrevButtonInfo, self.guideCancelButtonInfo}
+ self.guideSummaryButtons = {self.backButtonInfo, self.guideNextButtonInfo, self.guideSaveOnlyButtonInfo, self.guidePrevButtonInfo, self.guideCancelButtonInfo}
+end
+
+function SmartPickupFrame:guideHooks()
+ return self.guideKind ~= nil and SmartPickupFrame.GUIDE_HOOKS[self.guideKind] or nil
+end
+
+-- Er der en åben guide på fanen?
+function SmartPickupFrame:hasGuideOn(tab)
+ local hooks = self:guideHooks()
+ return self.guide ~= nil and hooks ~= nil and hooks.tab == tab
+end
+
+function SmartPickupFrame:openGuide(kind, guide)
+ self.guideKind, self.guide = kind, guide
+ self.statusOverride = nil
+ self:requestRebuild()
+end
+
+-- Kladden kasseres (Annullér, fanebyt, lukket menu).
+function SmartPickupFrame:closeGuide()
+ self.guideKind, self.guide = nil, nil
+end
+
+function SmartPickupFrame:guideText(label)
+ return ADGuide.labelText(label, function(key) return self.i18n:getText(key) end)
+end
+
+-- Klikbar tekstrække (ombrydes; alle linjer er klikbare). Handlingen køres fra update() (pendingClick).
+function SmartPickupFrame:addLinkRow(text, action)
+ local first = nil
+ for _, line in ipairs(ADFlowView.wrapText(text, SmartPickupFrame.TEXT_ROW_CHARS)) do
+ local row = self:addRow(self.textPrefab, {kind = "text"})
+ local title = row:getDescendantByName("title")
+ title:setText(line)
+ self:makeRowClickable(row, action)
+ first = first or title
+ end
+ return first
+end
+
+local function optionIndex(options, value)
+ for index, option in ipairs(options) do
+ if option.value == value then return index end
+ end
+ return 1
+end
+
+function SmartPickupFrame:addGuideRow(row)
+ local data = {kind = "guideRow", row = row}
+ if row.type == "choice" then
+ local element = self:addRow(self.productPrefab, data)
+ local option = element:getDescendantByName("option")
+ local texts = {}
+ for _, entry in ipairs(row.options) do table.insert(texts, self:guideText(entry.label)) end
+ option:setTexts(texts)
+ option:setState(optionIndex(row.options, row.value), false)
+ element:getDescendantByName("title"):setText(self:guideText(row.title))
+ elseif row.type == "toggle" or row.type == "check" then
+ local element = self:addRow(self.ingredientPrefab, data)
+ local option = element:getDescendantByName("option")
+ if row.type == "toggle" then option:setTexts({self:guideText(row.off), self:guideText(row.on)}) end
+ option:setState(row.value and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
+ element:getDescendantByName("title"):setText((row.type == "check" and " " or "") .. self:guideText(row.title))
+ elseif row.type == "link" then
+ local text = row.label ~= nil and self:guideText(row.label)
+ or (ADGuide.lineText(row.line, function(key) return self.i18n:getText(key) end) .. self.i18n:getText("spu_guide_editSuffix"))
+ self:addLinkRow(" " .. text, function(frame) frame:onGuideLink(row) end)
+ else
+ self:addTextRow(self:guideText(row.label))
+ end
+end
+
+-- Tegner den åbne guide. false = guiden er lukket (fx marken findes ikke længere) — fanen viser oversigten.
+function SmartPickupFrame:fillGuideRows(adEnv)
+ local hooks = self:guideHooks()
+ if hooks == nil or self.guide == nil then return false end
+ local context = hooks.context(self, adEnv, self.guide)
+ if context == nil then
+ self:closeGuide()
+ self.statusOverride = self.i18n:getText("spu_guide_closed")
+ self.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS
+ return false
+ end
+ self.guide = ADGuide.withContext(self.guide, context)
+ local guide, model = self.guide, hooks.model
+ self:addTextRow(hooks.heading(self, guide))
+ local stepTitle = self.i18n:getText(model.stepKey(guide.step))
+ local index, total = ADGuide.position(guide)
+ local header
+ if guide.single then
+ header = string.format(self.i18n:getText("spu_guide_editStep"), stepTitle)
+ elseif index ~= nil and total > 1 then
+ header = string.format(self.i18n:getText("spu_guide_stepOf"), index, total, stepTitle)
+ else
+ header = string.format(self.i18n:getText("spu_guide_stepOne"), index or 1, stepTitle)
+ end
+ self:addTextRow(header)
+ self:addTextRow(" " .. self.i18n:getText(model.hintKey(guide.step)))
+ for _, row in ipairs(model.rows(guide)) do
+ self:addGuideRow(row)
+ end
+ if hooks.afterRows ~= nil then hooks.afterRows(self, adEnv, guide) end
+ return true
+end
+
+-- ---- klik ----
+
+function SmartPickupFrame:applyGuideChoice(id, value)
+ local hooks = self:guideHooks()
+ if hooks == nil or self.guide == nil then return end
+ if hooks.choose ~= nil and hooks.choose(self, id, value) then return end
+ self.guide = hooks.model.choose(self.guide, id, value)
+end
+
+function SmartPickupFrame:onClickGuideRow(state, data)
+ self:safe(function(frame)
+ local row = data ~= nil and data.row or nil
+ if row == nil or frame.guide == nil then return end
+ local value
+ if row.type == "choice" then
+ local option = row.options[state]
+ if option == nil then return end
+ value = option.value
+ elseif row.type == "toggle" then
+ value = state == SmartPickupFrame.STATE_ON
+ elseif row.type == "check" then
+ value = {item = row.item, on = state == SmartPickupFrame.STATE_ON}
+ else
+ return
+ end
+ frame:applyGuideChoice(row.id, value)
+ frame:requestRebuild()
+ end)
+end
+
+-- Klikbare linjer: [Ret] på oversigts-trinnet åbner ét trin; andre links er valg (fx mark i trin 1).
+function SmartPickupFrame:onGuideLink(row)
+ if self.guide == nil then return end
+ if row.id == "jump" then
+ self.guide = ADGuide.jump(self.guide, row.value, "summary")
+ else
+ self:applyGuideChoice(row.id, row.value)
+ end
+ self:requestRebuild()
+end
+
+local function finish(frame, hooks, guide, start)
+ hooks.save(frame, guide, start)
+ frame:closeGuide()
+ frame:requestRebuild()
+end
+
+-- Næste / Gem / Kør automatisk (start = true) og Gem uden at starte (start = false).
+function SmartPickupFrame:onClickGuideNext(start)
+ self:safe(function(frame)
+ local hooks, guide = frame:guideHooks(), frame.guide
+ if hooks == nil or guide == nil then return end
+ if hooks.canAdvance ~= nil and not hooks.canAdvance(guide) then
+ frame:showOverride(frame.i18n:getText(hooks.cannotAdvanceKey or "spu_guide_cannotAdvance"))
+ return
+ end
+ if guide.single and guide.returnTo == "close" then
+ if start == false then return end
+ return finish(frame, hooks, guide, nil)
+ end
+ if guide.step == "summary" and not guide.single then
+ return finish(frame, hooks, guide, start)
+ end
+ if start == false then return end
+ local nextGuide, event = ADGuide.next(guide)
+ if event == "done" then return finish(frame, hooks, guide, true) end
+ frame.guide = nextGuide
+ frame:requestRebuild()
+ end)
+end
+
+function SmartPickupFrame:onClickGuideBack()
+ self:safe(function(frame)
+ if frame.guide == nil then return end
+ local previous, event = ADGuide.back(frame.guide)
+ if event == "cancel" then frame:closeGuide() else frame.guide = previous end
+ frame:requestRebuild()
+ end)
+end
+
+function SmartPickupFrame:onClickGuideCancel()
+ self:safe(function(frame)
+ frame:closeGuide()
+ frame:requestRebuild()
+ end)
+end
+
+-- Knapperne følger trinnet: Næste / OK (tilbage til oversigten) / Gem ([Ret] fra fanen) / Kør automatisk.
+function SmartPickupFrame:getGuideButtons()
+ local hooks, guide = self:guideHooks(), self.guide
+ local i18n = self.i18n
+ local isSummary = guide.step == "summary" and not guide.single
+ local key = "spu_guide_next"
+ if guide.single then
+ key = guide.returnTo == "close" and "spu_guide_save" or "spu_guide_ok"
+ elseif isSummary then
+ key = hooks.startKey ~= nil and hooks.startKey(guide) or "spu_guide_save"
+ end
+ self.guideNextButtonInfo.text = i18n:getText(key)
+ self.guideNextButtonInfo.disabled = hooks.canAdvance ~= nil and not hooks.canAdvance(guide)
+ if isSummary and (hooks.showSaveOnly == nil or hooks.showSaveOnly(guide)) then
+ return self.guideSummaryButtons
+ end
+ return self.guideButtons
+end
diff --git a/FS25_ADSmartPickup/gui/SmartPickupOutboundTab.lua b/FS25_ADSmartPickup/gui/SmartPickupOutboundTab.lua
index 0913490..ec8d700 100644
--- a/FS25_ADSmartPickup/gui/SmartPickupOutboundTab.lua
+++ b/FS25_ADSmartPickup/gui/SmartPickupOutboundTab.lua
@@ -179,6 +179,9 @@ end
function SmartPickupFrame:onClickProduct(state, element)
local clicked = self.rowData[element]
+ if clicked ~= nil and clicked.kind == "guideRow" then
+ return self:onClickGuideRow(state, clicked)
+ end
if clicked ~= nil and clicked.kind == "storageBuilding" then
return self:safe(function(frame) frame:onClickStorageBuilding(state) end)
end
diff --git a/FS25_ADSmartPickup/l10n/l10n_da.xml b/FS25_ADSmartPickup/l10n/l10n_da.xml
index f989284..db1d659 100644
--- a/FS25_ADSmartPickup/l10n/l10n_da.xml
+++ b/FS25_ADSmartPickup/l10n/l10n_da.xml
@@ -77,7 +77,7 @@
-
+
@@ -320,5 +320,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FS25_ADSmartPickup/l10n/l10n_en.xml b/FS25_ADSmartPickup/l10n/l10n_en.xml
index 4dca23c..59bfd69 100644
--- a/FS25_ADSmartPickup/l10n/l10n_en.xml
+++ b/FS25_ADSmartPickup/l10n/l10n_en.xml
@@ -77,7 +77,7 @@
-
+
@@ -320,5 +320,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml
index fa9179b..5133738 100644
--- a/FS25_ADSmartPickup/modDesc.xml
+++ b/FS25_ADSmartPickup/modDesc.xml
@@ -1,7 +1,7 @@
masterdraco
- 1.30.4.0
+ 1.31.0.0
AD Smart Pickup
@@ -23,6 +23,7 @@
+
@@ -30,6 +31,7 @@
+
diff --git a/docs/BRUGERMANUAL.md b/docs/BRUGERMANUAL.md
index c05cf3d..02e0cf4 100644
--- a/docs/BRUGERMANUAL.md
+++ b/docs/BRUGERMANUAL.md
@@ -540,7 +540,7 @@ maskiner. Kræver **Courseplay** ud over AutoDrive.
### Vælg og glem: afgrøde på marken
-Det nemmeste: vælg **Afgrøde på marken** (fx *Hvede*) øverst under marken. Så passer moden resten:
+Det nemmeste: vælg en afgrøde (fx *Hvede*) i guidens trin **Afgrøde** (Opsæt mark). Så passer moden resten:
- **Vokser der noget nu**, får det lov at vokse færdig, bliver passet (gødning, ukrudt, kalk) og høstet
eller slået, når det er klart.
@@ -550,9 +550,8 @@ Det nemmeste: vælg **Afgrøde på marken** (fx *Hvede*) øverst under marken. S
- **Bar mark:** klargøres og tilsås, så snart det er såtid.
- Derefter kører cyklussen af sig selv: vækst → pleje → høst → klargøring → såning → …
-Med en afgrøde valgt kører marken automatisk. Linjen under valget fortæller, hvad moden vil gøre. Du kan
-stadig vælge ensilage/hø, halm, antal maskiner, jordbehandling og pleje. Vælg *Ingen — planlæg selv* for
-at styre marken manuelt som før.
+Med en afgrøde valgt kører marken automatisk. Du kan stadig vælge ensilage/hø, halm, antal maskiner,
+jordbehandling og pleje i de næste trin. Vælg *Behold det der gror* for at styre marken som før.
### Opsætning
@@ -560,7 +559,7 @@ at styre marken manuelt som før.
`F109`. Maskinerne kører dertil ad vejen, før Courseplay tager over. Højst 400 m fra marken. Mangler
den, står marken med `!` og sendes ikke ud.
2. **Faste sæt.** Redskabet skal sidde på traktoren (eller være selvkørende). Moden kobler aldrig på.
-3. **Markflåde.** Nederst på fanen står alle køretøjer med et markredskab, med rolle og bredde. Med
+3. **Markflåde.** Knappen **Markflåde** viser alle køretøjer med et markredskab, med rolle og bredde. Med
**Brug alle ledige maskiner** = Ja (standard) finder moden selv maskinerne; med Nej bruges kun dem med
flueben. Køretøjer i et flow, køretøjer AutoDrive/Courseplay allerede kører,
og køretøjer du selv kører, bruges aldrig. En sprøjte/spreder har først en rolle, når den har fyld på
@@ -579,25 +578,50 @@ at styre marken manuelt som før.
### En mark
-Klik på en mark i tabellen. Under tabellen står:
+Klik på en mark i tabellen. Under tabellen står markens **resumé** — én linje pr. emne:
-| Valg | Betydning |
+| Linje | Eksempel |
|---|---|
-| **Kørsel** | *Manuel*: marken venter på **Start nu**. *Auto*: den starter selv, når den er klar eller trænger til pleje |
-| **Maskiner pr. trin** | 1–5 sæt samtidig (Courseplays multi-tool). Et trin starter først når alle sæt er ledige |
-| **Plan** (slåafgrøde) | Ensilage eller Hø |
-| **Riv** (slåafgrøde) | Riv skårene sammen før opsamling |
-| **Aflæsningsvogne** (høst) | 1–3 vogne der følger høsteren og kører til lager |
-| **Halm efter høst** (høst) | Hakkes (spredes), Samles op til silo, eller Presses til baller, som en ballesamler kører til et ballelager |
-| **Stubbehakning** | Hak stubben efter høst |
-| **Jordbehandling** | Auto (pløj når jorden trænger, ellers kultivér), Kultivér, Pløj eller Ingen |
-| **Så bagefter** + **Afgrøde at så** | Så efter jordbehandlingen. Standard: samme afgrøde som blev høstet |
-| **Tromle** | Tromle efter såning |
-| **Gød / Kalk / Bekæmp ukrudt** | Pleje mens afgrøden vokser, når jorden trænger (højst to gange pr. pleje pr. vækst) |
-| **Tving lager** | Vælg lagre. Uden valg: ensilage → nærmeste gæringssilo, ellers silo; hø og høst → nærmeste silo |
+| **Afgrøde** | *Behold det der gror* / *-> Hvede* |
+| **Afgrødens valg** | *Ensilage, river* / *halm hakkes* |
+| **Klargøring** | *Automatisk* / *Pløj, kalk* |
+| **Pleje** | *gødning, ukrudt* |
+| **Maskiner** | *3 faste* / *ledige maskiner*, *1 ad gangen*, *1 aflæsningsvogne* |
+| **Lager** | *Marksilo* / *Automatisk* / en valgt silo |
-Under valgene står **Jorden trænger til** (kalk, pløjning, gødning, ukrudt), markøren og evt. slåbredde og
-skår-forskydning.
+**Klik på en linje** for at rette netop det (fx lageret): kun det ene trin åbnes, og **Gem** gemmer og går
+tilbage. Under resuméet står hele statusteksten og markøren.
+
+Bundknapperne: **Start nu / Stop mark**, **Opsæt mark** og **Markflåde**.
+
+### Guiden "Opsæt mark"
+
+**Opsæt mark** sætter marken op med ét spørgsmål ad gangen ("Trin 3 af 7 — …"). Trin, der ikke passer
+til marken, springes over; en opsat mark åbner med de nuværende valg. Uden valgt mark vælger du først marken.
+
+| Trin | Vises når | Valg |
+|---|---|---|
+| **Afgrøde** | altid | *Behold det der gror* eller en afgrøde at så (mål-afgrøde, se ovenfor). Høst-/bar mark: *Så samme afgrøde igen efter høst* |
+| **Afgrødens valg** | slåafgrøde (nu eller som mål): Ensilage/Hø + Riv. Høstafgrøde med halm: Halm hakkes/samles/presses | |
+| **Klargøring** | efter høst / bar mark / skift af afgrøde | *Automatisk (anbefalet)* eller *Vælg selv*: jordbehandling, kalk, stubhakning, tromle |
+| **Pleje** | afgrøde der gror | Gødning, ukrudt (ikke på slåafgrøder); kalk her, når der ingen klargøring er |
+| **Maskiner** | altid | Antal ad gangen (1–5), aflæsningsvogne ved høst (1–3), **Faste maskiner til marken?** + listen |
+| **Lager** | høst/slåning | *Brug marksiloen?* (når der er en), ellers *Automatisk* eller *Vælg selv* blandt de 8 nærmeste |
+| **Oversigt** | altid | Resuméet; klik på en linje for at rette. **Kør automatisk** (Auto) eller **Gem uden at starte** (Manuel) |
+
+Knapperne: **Næste**, **Tilbage**, **Annullér**. Intet gemmes før **Kør automatisk** / **Gem uden at starte**
+(eller **Gem** ved ret af én linje). Lukkes menuen, eller skifter du fane, kasseres guiden. Med en
+mål-afgrøde kører marken altid automatisk. Mangler markøren, kan marken sættes op; arbejdet venter på markøren.
+
+**Faste maskiner:** en fast maskine bruges kun på denne mark, og marken venter på sin egen hellere end at
+låne en anden. Listen viser kun ledige maskiner og dem der allerede er faste her; maskiner der er faste på
+andre marker står som "N maskiner er faste på andre marker (F22)".
+
+**Markflåde** (knap): den fælles markflåde — *Brug alle ledige maskiner* og listen med flueben, "fast på
+F22", "i gang på F108" og bredde-advarsler. **Til marker** går tilbage.
+
+Moden spørger aldrig om: scanning, hvornår der høstes/slås/gødes/sprøjtes, påfyldning, tømning ved forkert
+vare, ventetid ved regn, bredde-kontrol, valg af nærmeste maskine, ruter og gæringssilo ved ensilage.
En mark er **klar**, når mindst 90 % står i høstvinduet. **Start nu** starter hele kæden (eller pleje, hvis
marken vokser og trænger). På en bar mark starter den klargøring/såning. **Stop mark** stopper maskinerne.
diff --git a/docs/TESTPLAN-markarbejde.md b/docs/TESTPLAN-markarbejde.md
index 91ea81a..846f91c 100644
--- a/docs/TESTPLAN-markarbejde.md
+++ b/docs/TESTPLAN-markarbejde.md
@@ -1,4 +1,4 @@
-# Testplan — Markarbejde (fanen Marker), version 1.28.0.0
+# Testplan — Markarbejde (fanen Marker), version 1.28.0.0 (+ guider v1.31/v1.32)
Stor test-session. Kør testene i rækkefølge: de første bygger opsætningen, de senere bygger på den.
Hver test har **Gør**, **Forvent** og **Log** (linjer i `log.txt`, der beviser at det virkede). Loglinjer
@@ -164,6 +164,29 @@ Afgrøde "Samme som høstet", Tromle Ja. Kalk Ja.
- [ ] **8.4 Beskeder.** Grøn besked når en mark/pleje er færdig; rød besked ved fejl (hver kun én gang i træk).
- [ ] **8.5 Ydelse.** Ingen mærkbar lag med fanen åben og mange marker (én mark scannes pr. sekund).
+## 10. Guiden "Opsæt mark" (v1.31)
+
+- [ ] **10.1 Oversigt.** Klik F22 i tabellen. Forvent: resumé-linjer (Afgrøde, …, Lager) med "[Ret]", hele
+ status, markør. Ingen lang detaljeliste og ingen markflåde nederst. Knapper: Start nu, Opsæt mark, Markflåde.
+- [ ] **10.2 Guiden fra bunden.** Opsæt mark → "Trin 1 af N — Afgrøde". Klik Næste gennem alle trin uden at
+ scrolle. Tællingen passer med de viste trin (græs: ingen Klargøring; bar mark: ingen Pleje/Lager).
+- [ ] **10.3 Valg gemmes.** Vælg fx Hø + Riv, 2 ad gangen, Lager: Vælg selv + en silo → Oversigt viser valgene
+ → **Kør automatisk**. Forvent: tabellens Plan "Hø + riv (x2)", Kørsel "Auto". Log: `F22 guide gemt: mode=auto …`.
+- [ ] **10.4 Annullér.** Åbn guiden, skift afgrøde, Annullér → intet ændret. Samme med ESC (menuen lukkes).
+- [ ] **10.5 Ret én linje.** Klik "Lager: …" i oversigten → "Ret: Lager"; skift → **Gem** → tilbage i
+ oversigten med ændringen; Kørsel (Auto/Manuel) uændret. Tilbage i et Ret-trin fortryder.
+- [ ] **10.6 Ret fra Oversigt-trinnet.** I guidens sidste trin: klik en linje → ret → **OK** → tilbage på Oversigt.
+- [ ] **10.7 Mål-afgrøde skifter trin.** Kornmark: vælg Græs som afgrøde → trinnet "Afgrødens valg" viser
+ Ensilage/Hø; tællingen ændres. På Oversigt kun **Kør automatisk** (ingen "Gem uden at starte").
+- [ ] **10.8 Faste maskiner.** Maskiner → Faste = Ja → sæt flueben ved 2 maskiner → gem. Åbn en anden mark:
+ de 2 vises ikke; linje "2 maskiner er faste på andre marker (F22)". Log: `guide gemt … faste=…`.
+- [ ] **10.9 Marksilo.** Mark med silo inden for 150 m: Lager-trinnet spørger "Brug marksiloen?". Ja → resumé "Marksilo".
+- [ ] **10.10 Uden valgt mark.** Ingen mark valgt → Opsæt mark → trin "Mark" med listen; klik en mark → Afgrøde.
+ Næste uden valgt mark giver "Vælg en mark først".
+- [ ] **10.11 Markflåde.** Knappen Markflåde viser flåden (flueben, "fast på F22"); klik et flueben gemmes;
+ **Til marker** går tilbage.
+- [ ] **10.12 Klik routes.** Alle Ja/Nej, valg og flueben i guiden reagerer (ingen "GUI-fejl" i statuslinjen/log).
+
---
## Kendte usikkerheder (første gang i spillet)
@@ -178,4 +201,5 @@ Afgrøde "Samme som høstet", Tromle Ja. Kalk Ja.
| Påfyldning | AutoDrive LOAD til den læssestation moden fandt (AutoDrives eget automatiske mål er slået fra i AutoDrive) | Sættet står ved påfyldningen og læsser ikke, eller kører ikke tilbage |
| Ukrudt | `weedSystem`-værdi ≥ 2 = tæt | Ukrudtsbehandling uden ukrudt, eller aldrig |
| Nysået som "bar" | nysået afgrøde læses som voksende | Marken står "Færdig" efter såning (ingen dobbelt-såning, men ingen næste høst-start før den er moden) |
+| Halm-valg i guiden | `fruitType.hasWindrow/windrowName` = afgrøder med halm | Halm-valget mangler ved hvede eller vises ved majs |
| Ballesamler | Courseplays BALE_FINDER + AutoDrive til ballelager | Ballesamleren starter ikke, eller kører ikke fulde læs væk |
diff --git a/docs/USER_MANUAL.md b/docs/USER_MANUAL.md
index 4bf80d1..c563ba9 100644
--- a/docs/USER_MANUAL.md
+++ b/docs/USER_MANUAL.md
@@ -546,7 +546,7 @@ and which machines. Requires **Courseplay** in addition to AutoDrive.
### Select and forget: crop on the field
-The easiest way: pick **Crop on the field** (such as *Wheat*) at the top of the field. The mod does the rest:
+The easiest way: pick a crop (such as *Wheat*) in the guide step **Crop** (Set up field). The mod does the rest:
- **Whatever grows now** is left to finish, cared for (fertilizer, weeds, lime) and harvested or mowed when
ready.
@@ -556,9 +556,8 @@ The easiest way: pick **Crop on the field** (such as *Wheat*) at the top of the
- **Bare field:** prepared and sown as soon as it is sowing time.
- After that the cycle runs by itself: growth → care → harvest → preparation → sowing → …
-With a crop chosen, the field runs automatically. The line below the choice tells you what the mod will do.
-You can still choose silage/hay, straw, number of machines, soil work and care. Choose *None — plan it
-yourself* to run the field manually as before.
+With a crop chosen, the field runs automatically. You can still choose silage/hay, straw, number of
+machines, soil work and care in the following steps. Choose *Keep what grows* to run the field as before.
### Setup
@@ -566,7 +565,7 @@ yourself* to run the field manually as before.
such as `F109`. Machines drive there on the road before Courseplay takes over. At most 400 m from the
field. Without it, the field shows `!` and nothing is sent.
2. **Fixed rigs.** The implement must be attached (or self-propelled). The mod never attaches.
-3. **Field fleet.** At the bottom are all vehicles with a field implement, with role and width. With
+3. **Field fleet.** The **Field fleet** button shows all vehicles with a field implement, with role and width. With
**Use all free machines** = Yes (default) the mod finds the machines itself; with No only ticked ones
are used. Vehicles in a flow, vehicles AutoDrive/Courseplay are already driving, and
vehicles you drive are never used. A sprayer/spreader only has a role once it holds product
@@ -585,25 +584,52 @@ yourself* to run the field manually as before.
### A field
-Click a field in the table. Below the table:
+Click a field in the table. Below the table is the field's **summary** — one line per topic:
-| Option | Meaning |
+| Line | Example |
|---|---|
-| **Mode** | *Manual*: the field waits for **Start now**. *Auto*: it starts by itself when ready or when it needs care |
-| **Machines per step** | 1–5 rigs at once (Courseplay multi-tool). A step starts only when all rigs are free |
-| **Plan** (mowable) | Silage or Hay |
-| **Rake** (mowable) | Rake the swaths before collecting |
-| **Unloading trailers** (harvest) | 1–3 trailers that follow the harvester and drive to storage |
-| **Straw after harvest** (harvest) | Chopped (spread), Collected to a silo, or Baled, and a bale collector takes the bales to a bale store |
-| **Mulch stubble** | Mulch the stubble after harvest |
-| **Soil work** | Auto (plow when the soil needs it, else cultivate), Cultivate, Plow or None |
-| **Sow afterwards** + **Crop to sow** | Sow after soil work. Default: the crop that was harvested |
-| **Roll** | Roll after sowing |
-| **Fertilize / Lime / Control weeds** | Care while the crop grows, when the soil needs it (at most twice per care per season) |
-| **Force storage** | Pick stores. Without a choice: silage → nearest fermenting silo, else silo; hay and harvest → nearest silo |
+| **Crop** | *Keep what grows* / *-> Wheat* |
+| **Crop options** | *Silage, rake* / *straw chopped* |
+| **Preparation** | *Automatic* / *Plow, lime* |
+| **Crop care** | *fertilizer, weeds* |
+| **Machines** | *3 dedicated* / *free machines*, *1 at a time*, *1 unloaders* |
+| **Storage** | *Field silo* / *Automatic* / a chosen silo |
-Below the options: **The soil needs** (lime, plowing, fertilizer, weeds), the marker, and mowing width and
-swath offset when relevant.
+**Click a line** to change just that (e.g. storage): only that step opens, and **Save** saves and returns.
+Below the summary are the full status and the marker.
+
+Buttons: **Start now / Stop field**, **Set up field** and **Field fleet**.
+
+### The "Set up field" guide
+
+**Set up field** sets the field up one question at a time ("Step 3 of 7 — …"). Steps that do not fit the
+field are skipped; a field already set up opens with its current choices. Without a selected field you pick
+the field first.
+
+| Step | Shown when | Choices |
+|---|---|---|
+| **Crop** | always | *Keep what grows* or a crop to sow (target crop, see above). Harvest/bare field: *Sow the same crop again after harvest* |
+| **Crop options** | mowable (now or as target): Silage/Hay + Rake. Harvest crop with straw: straw chopped/collected/baled | |
+| **Preparation** | after harvest / bare field / crop change | *Automatic (recommended)* or *Choose myself*: soil work, lime, mulch, roll |
+| **Crop care** | a growing crop | Fertilizer, weeds (not on mowables); lime here when there is no preparation |
+| **Machines** | always | At a time (1–5), unloaders when harvesting (1–3), **Dedicated machines for this field?** + the list |
+| **Storage** | harvest/mowing | *Use the field silo?* (when there is one), else *Automatic* or *Choose myself* among the 8 nearest |
+| **Summary** | always | The summary; click a line to change it. **Run automatically** (Auto) or **Save without starting** (Manual) |
+
+Buttons: **Next**, **Back**, **Cancel**. Nothing is saved before **Run automatically** / **Save without
+starting** (or **Save** when editing one line). Closing the menu or switching tab discards the guide. With a
+target crop the field always runs automatically. Without a marker the field can be set up; work waits for
+the marker.
+
+**Dedicated machines:** a dedicated machine only works this field, and the field waits for its own rather
+than borrowing another. The list only shows free machines and those already dedicated here; machines
+dedicated to other fields show as "N machines are dedicated to other fields (F22)".
+
+**Field fleet** (button): the shared fleet — *Use all free machines* and the ticked list, "dedicated to
+F22", "working on F108" and width warnings. **Back to fields** returns.
+
+The mod never asks about: scanning, when to harvest/mow/fertilize/spray, refilling, emptying wrong cargo,
+waiting for rain, width checks, choosing the nearest machine, routes, and fermenting silo for silage.
A field is **ready** when at least 90 % is in the harvest window. **Start now** starts the whole chain (or
care, when the field grows and needs it). On a bare field it starts preparation/sowing. **Stop field** stops
diff --git a/docs/superpowers/plans/2026-09-26-guider.md b/docs/superpowers/plans/2026-09-26-guider.md
new file mode 100644
index 0000000..069883d
--- /dev/null
+++ b/docs/superpowers/plans/2026-09-26-guider.md
@@ -0,0 +1,174 @@
+# Guider til opsætning (marker + flows) — implementeringsplan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans (native; brugeren har givet fuld autonomi). Trin bruger `- [ ]`.
+
+**Goal:** Fields- og Flows-fanen får en oversigt med resumé (klik = ret ét trin) og en trin-for-trin-guide ("Opsæt mark" / "Opsæt flow") på én fælles guide-ramme.
+
+**Architecture:** En ren guide-motor (`adGuide.lua`) styrer trin, synlighed, tælling, næste/tilbage/spring. Oven på den to rene trin-definitioner (`adFieldGuide.lua`, `adFlowGuide.lua`) der ud fra en kladde + et `context` (det spil-afhængige, hentet af GUI'et) giver rækker (som data), valg, standarder, resumé og gem-resultat. GUI-laget (`gui/SmartPickupGuide.lua` fælles trin-skærm, `gui/SmartPickupFieldGuide.lua`, `gui/SmartPickupFlowGuide.lua`) er tyndt: bygger context, tegner rækkerne, router klik og gemmer via de eksisterende `ADFieldJobs`/`ADRunsController`-funktioner.
+
+**Tech Stack:** Lua 5.1 (FS25), luajit til tests, FS25 GUI (klonede prefabs).
+
+**Spec:** `docs/superpowers/specs/2026-09-26-guider-design.md`
+
+## Global Constraints
+
+- Lua 5.1: ingen goto, ingen coroutines, ingen `#` på tabeller med huller; pcall om spil-API.
+- Gemt format (`ADSmartPickup_fields.xml`, `ADSmartPickup_runs.xml`) ændres ikke. Guiderne skriver kun via `ADFieldJobs.setTarget/setPlan` og `ADRunsController.createRun/updateRun/assignVehicle/unassignVehicle`.
+- Intet gemmes før [Gem]/[Kør automatisk]/[Start flow]. [Annullér], fanebyt og lukket menu kasserer kladden.
+- Rene moduler returnerer nye tabeller (kladden kopieres ved hvert valg).
+- Filer under ~800 linjer; `SmartPickupFrame.lua` (971) får kun små routing-ændringer.
+- Nye .lua i modDesc (rene moduler) eller `source()` i adSmartPickup.lua (GUI-filer), i rigtig rækkefølge.
+- Al ny tekst i både `l10n_da.xml` og `l10n_en.xml`.
+- Guide-rækker laves KUN med `ingredientPrefab` (Ja/Nej, flueben) og `productPrefab` (valg) med `kind = "guideRow"`, routet i `onClickIngredient`/`onClickProduct`; klikbare tekstlinjer via `makeRowClickable` + pendingClick. Ingen guide-rækker på `vehiclePrefab` (v1.30.4-fælden).
+- Bundknapper: kun MENU_ACTIVATE, MENU_EXTRA_1, MENU_EXTRA_2, MENU_CANCEL (+ MENU_BACK) — andre findes ikke sikkert i FS25.
+- Tests: `for t in tests/test_*.lua; do luajit $t | tail -1; done` — alle grønne.
+
+## Review Focus
+
+1. Klik på guide-rækker gemmes aldrig (forkert routing) → alle nye kinds routes via `guideRow`; testplan-punkt in-game.
+2. Kladde muteres ved valg (Annullér gemmer alligevel) → test: `choose` ændrer ikke den gamle guide/kladde.
+3. Trin forsvinder/dukker op efter et valg (fx mål-afgrøde græs på kornmark) → tælling og næste følger de synlige trin; test.
+4. [Ret] fra oversigten må kun gemme det ene trin og bevare Auto/Manuel → test `result(guide, nil)` bevarer mode.
+5. Faste maskiner: maskine fast på anden mark flyttes kun hvis brugeren vælger den (skjult i listen) → test `applyRigs`.
+
+---
+
+## Beslutninger (uklarheder i spec'en)
+
+- **[Opsæt mark] uden valgt mark** vises også (åbner trin 1 "Mark"); ellers kunne trin 1 aldrig nås.
+- **"Så igen bagefter"** (plan.sow uden mål-afgrøde) står i trin 2 under "Behold det der gror" for høst-/bare marker — ellers forsvinder muligheden.
+- **Kalk**: i Klargøring ("Vælg selv"); når Klargøring ikke vises (ren græsmark) står Kalk i Pleje.
+- **Klargøring "Automatisk"** = jord auto, kalk ja, mulch nej, tromle nej (anvendes kun når trinnet er synligt).
+- **Maskiner-trinnet**: antal ad gangen + aflæsningsvogne først, så "Faste maskiner?" + listen (listen kan være lang). Aflæsningsvogne 1–3 (modellens `MAX_UNLOADERS`), ikke 1–5.
+- **Mark uden markør**: gemmes med den valgte tilstand; job-løkken venter allerede på markøren (dagens regel). Oversigten viser "sæt markøren".
+- **Mål-afgrøde** tvinger Auto (som i dag); på Oversigt-trinnet vises da kun [Kør automatisk].
+- **[Ret] fra trin 8** vender tilbage til Oversigt-trinnet (kladden bevares); [Tilbage] i et [Ret]-trin fortryder det ene trins ændringer.
+- **Flows**: bundlinjen har plads til 4 knapper → [Start/Stop] [Opsæt flow] [Nyt flow] [Slet]; Omdøb ligger på resuméets Navn-linje; Start alle/Stop alle bliver på fanen Oversigt.
+- **Halm**: `context.straw` = afgrøder med halm (FS25 `fruitType.hasWindrow/windrowName`); kan det ikke aflæses (tom), vises halm-valget for alle høstafgrøder som i dag.
+
+---
+
+# Etape A — fælles ramme + Marker (v1.31.0.0)
+
+### Task A1: Guide-motoren `adGuide.lua`
+
+**Files:** Create `FS25_ADSmartPickup/adGuide.lua`, `tests/test_adGuide.lua`; Modify `modDesc.xml` (før adFieldModel.lua).
+
+**Interfaces (Produces):**
+```lua
+ADGuide.new(def, draft, context, options) -> guide
+-- def = {order = {stepId...}, isVisible = function(stepId, guide) -> bool}
+-- options = {step = id|nil, single = bool, returnTo = "close"|"summary"}
+-- guide = {def, draft, context, step, single, returnTo, snapshot}
+ADGuide.visibleSteps(guide) -> {stepId...}
+ADGuide.position(guide) -> index|nil, total
+ADGuide.isLast(guide) -> bool
+ADGuide.next(guide) -> guide, event -- event nil | "done"
+ADGuide.back(guide) -> guide, event -- event nil | "cancel"
+ADGuide.jump(guide, stepId, returnTo) -> guide (single, snapshot = kopi af kladden)
+ADGuide.withDraft(guide, draft) / ADGuide.withContext(guide, context) -> guide
+ADGuide.deepCopy(value) ; ADGuide.toggleItem(list, item, on) -> ny liste
+ADGuide.labelText(label, getText) -> string -- label = {key, args} | {text}
+ADGuide.lineText(line, getText) -> "Titel: del, del" -- line = {step, title = label, parts = {label...}}
+```
+
+- [ ] Test (RED): næste/tilbage springer skjulte trin over; tælling "3 af 5" over synlige; `next` på sidste = "done"; `back` på første = "cancel"; `jump(..., "summary")` + `next` lander på `summary` med single=false; `jump(..., "close")` + `next` = "done"; `back` i spring gendanner snapshot; `withDraft` returnerer ny guide (gammel urørt); trin der bliver usynligt efter et valg: `next` går til næste synlige i `order`; `toggleItem` uden dubletter; `labelText` med args og manglende args (pcall).
+- [ ] Kør `luajit tests/test_adGuide.lua` → FAIL (fil mangler).
+- [ ] Implementér; kør → ALLE TESTS OK.
+
+### Task A2: Markguidens trin `adFieldGuide.lua`
+
+**Files:** Create `FS25_ADSmartPickup/adFieldGuide.lua`, `tests/test_adFieldGuide.lua`; Modify `modDesc.xml` (efter adFieldView.lua).
+
+**Interfaces:**
+- Consumes: `ADGuide.*`, `ADFieldModel.product/MAX_RIGS/MAX_UNLOADERS`, `ADFieldFleet.visibleForField`, `ADFlowView.naturalLess`.
+- Produces:
+```lua
+ADFieldGuide.ORDER = {"field","crop","cropChoice","prep","care","machines","store","summary"}
+ADFieldGuide.open(field, context, options) -> guide -- context.info påkrævet
+ADFieldGuide.openPicker(context) -> guide -- trin "field"
+ADFieldGuide.pickField(guide, field, context) -> guide -- videre til trinnet efter "field"
+ADFieldGuide.isVisible(stepId, guide) -> bool
+ADFieldGuide.rows(guide) -> {row...}
+-- row = {type="choice", id, title, options={{label, value}}, value}
+-- | {type="toggle", id, title, off, on, value}
+-- | {type="check", id, item, title, value}
+-- | {type="text", label} | {type="link", id, value, label}
+ADFieldGuide.choose(guide, id, value) -> guide
+ADFieldGuide.summary(guide) -> {line...}
+ADFieldGuide.result(guide, start) -> {fieldId, targetFruit, targetChanged, plan, rigIds}
+ADFieldGuide.applyRigs(rigIdsByField, fieldId, rigIds) -> ny {[fieldId] = ids}
+ADFieldGuide.stepKey(stepId) / hintKey(stepId) -> l10n-nøgler
+```
+- context: `{info, sowable={{name,title}}, mowable={[name]=true}, straw={[name]=true}|nil, rigs={{id,text,role}}, rigIdsByField, fieldSilos={{id,text}}, stores={{id,text}}|nil, storeHidden, fields={{id,text}}}`.
+- kladde: `{fieldId, plan (kopi), originalTarget, prepAuto, useDedicated, storeMode ("auto"|"manual"|"fieldSilo"), storeIds, pickField}`.
+
+- [ ] Test (RED): synlige trin for (a) græs uden mål: crop, cropChoice, care, machines, store, summary (ingen prep); (b) høst m. halm: + prep, cropChoice (halm); (c) høst uden halm (`straw` sat, afgrøden ikke med): ingen cropChoice; (d) bar mark uden mål: crop, prep, machines, summary (ingen care/store); (e) kornmark med mål græs: cropChoice (ensilage/hø) + prep; picker: kun "field". Standarder: ny mark → prepAuto, useDedicated=false, storeMode auto; plan med soil=plow → prepAuto=false; plan med rigIds → useDedicated; storeIds lig marksilo → fieldSilo. `choose` ændrer ikke gammel guide; `rigIds`-check toggler; `result`: prepAuto overskriver soil/mulch/lime/roll kun når prep er synlig; mode auto/manual efter start, uændret ved start=nil, auto ved mål; storeBuildingIds pr. storeMode; rigIds tom når useDedicated=false; targetChanged. `applyRigs` fjerner maskinen fra anden mark. Rækker: maskiner-trinnet skjuler maskiner faste på andre marker + tekst med antal; marksilo-spørgsmål kun når fieldSilos findes; "Vælg selv" viser siloerne. Resumé: linjer kun for synlige trin, med rigtige nøgler (fx `spu_guide_sum_dedicated` args {3}).
+- [ ] Kør → FAIL; implementér; kør → OK. Hele suiten grøn.
+
+### Task A3: Fælles trin-skærm `gui/SmartPickupGuide.lua`
+
+**Files:** Create `gui/SmartPickupGuide.lua`; Modify `adSmartPickup.lua` (source efter SmartPickupFlowTab.lua), `gui/SmartPickupFrame.lua` (initGuideButtons i new, routing `guideRow` i onClickIngredient, refreshButtons → `getGuideButtons`, switchTab/onFrameClose lukker guiden, rebuild for faner med åben guide), `gui/SmartPickupOutboundTab.lua` (onClickProduct → guideRow).
+
+**Interfaces (Produces):**
+```lua
+SmartPickupFrame.GUIDE_HOOKS[kind] = {model, tab, context(frame, adEnv, guide) -> context, save(frame, guide, start), choose(frame, id, value) -> handled}
+SmartPickupFrame:initGuideButtons(i18n)
+SmartPickupFrame:openGuide(kind, guide) SmartPickupFrame:closeGuide()
+SmartPickupFrame:fillGuideRows(adEnv) -- overskrift "Trin 3 af 7 — …", forklaring, rækker
+SmartPickupFrame:guideText(label) -> string
+SmartPickupFrame:addLinkRow(text, action)
+SmartPickupFrame:onClickGuideRow(state, data)
+SmartPickupFrame:getGuideButtons() -> knapliste
+SmartPickupFrame:onClickGuideNext/Back/Cancel/SaveOnly()
+```
+- [ ] Skriv filen; `luajit -bl` syntakstjek.
+
+### Task A4: Marker-fanen: oversigt, markflåde, guide
+
+**Files:** Create `gui/SmartPickupFieldGuide.lua` (context, gem, knapper for fanen); Modify `gui/SmartPickupFieldTab.lua` (fillFieldRows → oversigt/markflåde/guide; fjern den lange detaljeliste), l10n da/en.
+
+- Oversigt: hint, tabel, for valgt mark: overskrift, resumé-linjer (klik = `ADGuide.jump(step, "close")`), fuld status, markør-linje, marksilo-advarsel. Knapper: [Opsæt mark] (EXTRA_1), [Start nu/Stop] (ACTIVATE), [Markflåde] (EXTRA_2).
+- Markflåde: `fillFleetRows(adEnv, nil)` (alle maskiner). Knap [Til marker] (EXTRA_2).
+- Gem: `setTarget` hvis ændret → `setPlan(plan)` → `applyRigs` → `setPlan(fid, {rigIds})` for ændrede marker.
+- [ ] Skriv, syntakstjek, suiten grøn.
+
+### Task A5: Docs + ship v1.31.0.0
+
+- [ ] TESTPLAN-markarbejde.md: afsnit "Guider (v1.31)"; BRUGERMANUAL/USER_MANUAL: Marker-afsnit kort.
+- [ ] Ship-procedure (version 1.30.4.0 → 1.31.0.0), commit `feat(gui): guide "Opsæt mark" …`, push gitea.
+
+# Etape B — Flows på samme ramme (v1.32.0.0)
+
+### Task B1: Flowguidens trin `adFlowGuide.lua`
+
+**Files:** Create `FS25_ADSmartPickup/adFlowGuide.lua`, `tests/test_adFlowGuide.lua`; modDesc (efter adFlowView.lua).
+
+**Interfaces (Produces):**
+```lua
+ADFlowGuide.ORDER = {"name","direction","buildings","goods","store","rules","wait","tractors","summary"}
+ADFlowGuide.open(run, context, options) -> guide -- run = eksisterende run
+ADFlowGuide.openNew(name, context) -> guide -- nyt flow (trin "name" synligt)
+ADFlowGuide.isVisible(stepId, guide); ADFlowGuide.rows(guide); ADFlowGuide.choose(guide, id, value)
+ADFlowGuide.summary(guide) -> {line...}
+ADFlowGuide.result(guide) -> {isNew, runId, name, kind, changes, vehicleIds}
+ADFlowGuide.vehicleChanges(oldIds, newIds) -> added, removed
+ADFlowGuide.previewRun(draft) -> run-formet tabel til controllerens opslag
+```
+- context: `{buildings={{id,name,label,hasMarker,text}}, fillTypes={{name,title}}, products={{name,title}}, storeBuildings={{id,text}}, waitPools={{group,count}}, vehicles={{id,text,otherRunId,otherRunName,blocked}}, sellAtSteps, storeAboveSteps, returnBelowSteps, maxLoops}`.
+- kladde: kopi af run-felterne + `isNew`, `showAllBuildings`, `storeAuto`.
+- [ ] Test (RED): name-trin kun ved nyt flow; store-trin kun ved udkørsel med en vare på Lager/Salg-ellers-lager; retning skifter bygnings-rolle; varer: forsyning auto/vælg (ingredients toggles), udkørsel politik pr. vare (products); regler pr. retning; traktorer i andre flows/blokerede skjult + tekst; resumé pr. retning; result.changes; vehicleChanges; choose immutabelt.
+- [ ] FAIL → implementér → OK.
+
+### Task B2: Flows-fanen i GUI'et
+
+**Files:** Create `gui/SmartPickupFlowGuide.lua`; Modify `gui/SmartPickupFrame.lua` (rebuild for Flows-fanen: tavle + resumé, statiske rækker skjult; refreshLive/refreshButtons), l10n.
+
+- Oversigt: tavlen (`fillOverviewRows`), valgt flow: resumé (klik = ret ét trin; Navn-linjen = omdøb-dialog), status-linje (live). Knapper: [Start/Stop] (ACTIVATE), [Opsæt flow] (EXTRA_1), [Nyt flow] (EXTRA_2), [Slet] (CANCEL).
+- Gem: nyt → `createRun`; `updateRun(changes)`; traktorer via `vehicleChanges` + `assignVehicle/unassignVehicle`; `resolveMarkers`; `save()`; [Start flow] → `startRun`.
+- [ ] Skriv, syntakstjek, suiten grøn.
+
+### Task B3: Docs + ship v1.32.0.0; selv-review
+
+- [ ] TESTPLAN + manualer (Flows-afsnit), ship 1.31.0.0 → 1.32.0.0.
+- [ ] Code-review-agent på hele diffen; ret CRITICAL/HIGH; evt. v1.32.1.0.
diff --git a/tests/test_adFieldGuide.lua b/tests/test_adFieldGuide.lua
new file mode 100644
index 0000000..4d40752
--- /dev/null
+++ b/tests/test_adFieldGuide.lua
@@ -0,0 +1,245 @@
+-- Kør: luajit tests/test_adFieldGuide.lua (fra repo-roden)
+dofile("FS25_ADSmartPickup/adGuide.lua")
+dofile("FS25_ADSmartPickup/adFieldModel.lua")
+dofile("FS25_ADSmartPickup/adFieldFleet.lua")
+dofile("FS25_ADSmartPickup/adFlowView.lua")
+dofile("FS25_ADSmartPickup/adFieldGuide.lua")
+local FG, G = ADFieldGuide, ADGuide
+local failures = 0
+local function check(name, actual, expected)
+ if actual == expected then print("OK " .. name) else
+ failures = failures + 1
+ print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
+ end
+end
+
+local SOWABLE = {{name = "WHEAT", title = "Hvede"}, {name = "BARLEY", title = "Byg"}, {name = "GRASS", title = "Græs"}, {name = "MAIZE", title = "Majs"}}
+local RIGS = {
+ {id = "v1", text = "Fendt 1 — slåmaskine", role = "mower"},
+ {id = "v2", text = "Claas 2 — mejetærsker", role = "harvester"},
+ {id = "v3", text = "Sprøjte 3 — sprøjte", role = "weeder"},
+ {id = "v4", text = "Vogn 4 — læssevogn", role = "collector"},
+}
+local function ctx(info, extra)
+ local context = {
+ info = info, sowable = SOWABLE, mowable = {GRASS = true}, straw = {WHEAT = true, BARLEY = true},
+ rigs = RIGS, rigIdsByField = {[90] = {"v3"}}, fieldSilos = {}, storeHidden = 0,
+ stores = info.kind ~= "bare" and {{id = "s1", text = "Silo A, 300 m"}, {id = "s2", text = "Silo B, 1,2 km"}} or nil,
+ fields = {{id = 1, text = "F1 Græs"}, {id = 2, text = "F2 Hvede"}},
+ }
+ for key, value in pairs(extra or {}) do context[key] = value end
+ return context
+end
+local GRASS = {id = 1, kind = "mowable", fruitName = "GRASS", cropTitle = "Græs", markerId = 5}
+local WHEAT = {id = 2, kind = "harvest", fruitName = "WHEAT", cropTitle = "Hvede", markerId = 6}
+local MAIZE = {id = 3, kind = "harvest", fruitName = "MAIZE", cropTitle = "Majs", markerId = 7}
+local BARE = {id = 4, kind = "bare", markerId = 8}
+
+local function steps(guide) return table.concat(G.visibleSteps(guide), ",") end
+local function field(id, plan)
+ local f = ADFieldModel.newField(id)
+ return ADFieldModel.with(f, {plan = plan or {}})
+end
+local function findRow(rows, id, item)
+ for _, row in ipairs(rows) do
+ if row.id == id and (item == nil or row.item == item) then return row end
+ end
+ return nil
+end
+local function countType(rows, rowType)
+ local n = 0
+ for _, row in ipairs(rows) do if row.type == rowType then n = n + 1 end end
+ return n
+end
+
+-- ---- synlige trin pr. marktype ----
+check("FG1 græs", steps(FG.open(field(1), ctx(GRASS))), "crop,cropChoice,care,machines,store,summary")
+check("FG1 hvede m. halm", steps(FG.open(field(2), ctx(WHEAT))), "crop,cropChoice,prep,care,machines,store,summary")
+check("FG1 majs uden halm", steps(FG.open(field(3), ctx(MAIZE))), "crop,prep,care,machines,store,summary")
+check("FG1 bar mark", steps(FG.open(field(4), ctx(BARE))), "crop,prep,machines,summary")
+check("FG1 halm ukendt -> vis", steps(FG.open(field(3), ctx(MAIZE, {straw = {}}))), "crop,cropChoice,prep,care,machines,store,summary")
+check("FG1 hvede med mål græs", steps(FG.open(field(2, {targetFruit = "GRASS"}), ctx(WHEAT))), "crop,cropChoice,prep,care,machines,store,summary")
+check("FG1 bar med mål hvede", steps(FG.open(field(4, {targetFruit = "WHEAT"}), ctx(BARE))), "crop,cropChoice,prep,care,machines,summary")
+check("FG1 græs med mål græs: ingen klargøring", steps(FG.open(field(1, {targetFruit = "GRASS"}), ctx(GRASS))), "crop,cropChoice,care,machines,store,summary")
+check("FG1 græs med mål hvede: klargøring", steps(FG.open(field(1, {targetFruit = "WHEAT"}), ctx(GRASS))), "crop,cropChoice,prep,care,machines,store,summary")
+local picker = FG.openPicker(ctx(GRASS))
+check("FG1 vælger: kun mark", steps(picker), "field")
+check("FG1 vælger: trin", picker.step, "field")
+check("FG1 vælger: position uden total", (G.position(picker)), 1)
+
+-- ---- standarder og udfyldning ----
+local g = FG.open(field(2), ctx(WHEAT))
+check("FG2 starter på afgrøde", g.step, "crop")
+check("FG2 klargøring auto", g.draft.prepAuto, true)
+check("FG2 ingen faste", g.draft.useDedicated, false)
+check("FG2 lager auto", g.draft.storeMode, "auto")
+check("FG2 kladde er kopi", g.draft.plan ~= nil and g.draft.fieldId, 2)
+local plowed = FG.open(field(2, {soil = "plow"}), ctx(WHEAT))
+check("FG2 pløj -> vælg selv", plowed.draft.prepAuto, false)
+local withRigs = FG.open(field(2, {rigIds = {"v2"}}), ctx(WHEAT))
+check("FG2 faste maskiner -> ja", withRigs.draft.useDedicated, true)
+local silo = FG.open(field(1, {storeBuildingIds = {"m1"}}), ctx(GRASS, {fieldSilos = {{id = "m1", text = "Marksilo"}}}))
+check("FG2 marksilo valgt", silo.draft.storeMode, "fieldSilo")
+local manual = FG.open(field(1, {storeBuildingIds = {"s2"}}), ctx(GRASS))
+check("FG2 valgt silo -> vælg selv", manual.draft.storeMode, "manual")
+check("FG2 valgt silo i listen", manual.draft.storeIds[1], "s2")
+
+-- ---- choose er immutabel ----
+local chosen = FG.choose(g, "targetFruit", "BARLEY")
+check("FG3 ny kladde", chosen.draft.plan.targetFruit, "BARLEY")
+check("FG3 gammel urørt", g.draft.plan.targetFruit, "")
+local toggled = FG.choose(withRigs, "rigIds", {item = "v1", on = true})
+check("FG3 check tilføjer", table.concat(toggled.draft.plan.rigIds, ","), "v2,v1")
+check("FG3 gammel liste urørt", table.concat(withRigs.draft.plan.rigIds, ","), "v2")
+check("FG3 check fjerner", table.concat(FG.choose(toggled, "rigIds", {item = "v2", on = false}).draft.plan.rigIds, ","), "v1")
+check("FG3 produkt hø", FG.choose(FG.open(field(1), ctx(GRASS)), "product", true).draft.plan.product, "hay")
+check("FG3 produkt ensilage", FG.choose(FG.open(field(1), ctx(GRASS)), "product", false).draft.plan.product, "silage")
+
+-- ---- rækker ----
+local cropRows = FG.rows(g)
+local target = findRow(cropRows, "targetFruit")
+check("FG4 afgrøde: valg", target.type, "choice")
+check("FG4 afgrøde: behold først", target.options[1].value, "")
+check("FG4 afgrøde: såbare", #target.options, 5)
+check("FG4 så igen for høstmark", findRow(cropRows, "sow") ~= nil, true)
+check("FG4 så igen skjult med mål", findRow(FG.rows(chosen), "sow"), nil)
+check("FG4 så igen skjult på græs", findRow(FG.rows(FG.open(field(1), ctx(GRASS))), "sow"), nil)
+
+local choiceGrass = FG.rows(G.jump(FG.open(field(1), ctx(GRASS)), "cropChoice", "close"))
+check("FG5 græs: ensilage/hø", findRow(choiceGrass, "product") ~= nil, true)
+check("FG5 græs: river", findRow(choiceGrass, "rake") ~= nil, true)
+check("FG5 græs: ingen halm", findRow(choiceGrass, "straw"), nil)
+local choiceWheat = FG.rows(G.jump(g, "cropChoice", "close"))
+check("FG5 hvede: halm", findRow(choiceWheat, "straw").type, "choice")
+check("FG5 hvede: ingen ensilage", findRow(choiceWheat, "product"), nil)
+
+local prepRows = FG.rows(G.jump(g, "prep", "close"))
+check("FG6 klargøring auto: kun valget + tekst", findRow(prepRows, "soil"), nil)
+check("FG6 klargøring valg", findRow(prepRows, "prepAuto").type, "choice")
+local prepManual = FG.rows(G.jump(FG.choose(g, "prepAuto", false), "prep", "close"))
+check("FG6 vælg selv: jord", findRow(prepManual, "soil") ~= nil, true)
+check("FG6 vælg selv: kalk", findRow(prepManual, "lime") ~= nil, true)
+check("FG6 vælg selv: mulch", findRow(prepManual, "mulch") ~= nil, true)
+check("FG6 vælg selv: ingen tromle uden såning", findRow(prepManual, "roll"), nil)
+
+local careGrass = FG.rows(G.jump(FG.open(field(1), ctx(GRASS)), "care", "close"))
+check("FG7 græs: ingen ukrudt", findRow(careGrass, "weed"), nil)
+check("FG7 græs: kalk i pleje", findRow(careGrass, "lime") ~= nil, true)
+local careWheat = FG.rows(G.jump(g, "care", "close"))
+check("FG7 hvede: ukrudt", findRow(careWheat, "weed") ~= nil, true)
+check("FG7 hvede: kalk ikke i pleje", findRow(careWheat, "lime"), nil)
+
+local machinesOff = FG.rows(G.jump(g, "machines", "close"))
+check("FG8 antal ad gangen", findRow(machinesOff, "rigs").type, "choice")
+check("FG8 aflæsningsvogne ved høst", findRow(machinesOff, "unloaders") ~= nil, true)
+check("FG8 ingen liste uden faste", countType(machinesOff, "check"), 0)
+check("FG8 ingen aflæsningsvogne på græs", findRow(FG.rows(G.jump(FG.open(field(1), ctx(GRASS)), "machines", "close")), "unloaders"), nil)
+local machinesOn = FG.rows(G.jump(FG.choose(g, "useDedicated", true), "machines", "close"))
+check("FG8 liste: v3 fast på F90 skjult", findRow(machinesOn, "rigIds", "v3"), nil)
+check("FG8 liste: tre synlige", countType(machinesOn, "check"), 3)
+local hiddenText = nil
+for _, row in ipairs(machinesOn) do if row.type == "text" and row.label.key == "spu_field_hiddenDedicated" then hiddenText = row end end
+check("FG8 tekst om skjulte", hiddenText ~= nil and hiddenText.label.args[1], 1)
+check("FG8 tekst marker", hiddenText ~= nil and hiddenText.label.args[2], "F90")
+local ownFirst = FG.rows(G.jump(withRigs, "machines", "close"))
+local firstCheck = nil
+for _, row in ipairs(ownFirst) do if row.type == "check" and firstCheck == nil then firstCheck = row end end
+check("FG8 egne først", firstCheck.item, "v2")
+check("FG8 egne sat", firstCheck.value, true)
+
+local storeAuto = FG.rows(G.jump(FG.open(field(1), ctx(GRASS)), "store", "close"))
+check("FG9 ingen marksilo-spørgsmål uden marksilo", findRow(storeAuto, "useFieldSilo"), nil)
+check("FG9 automatisk/vælg selv", findRow(storeAuto, "storeAuto").type, "choice")
+check("FG9 auto: ingen siloliste", countType(storeAuto, "check"), 0)
+local storeManual = FG.rows(G.jump(FG.choose(FG.open(field(1), ctx(GRASS)), "storeAuto", false), "store", "close"))
+check("FG9 vælg selv: siloer", countType(storeManual, "check"), 2)
+local siloCtx = ctx(GRASS, {fieldSilos = {{id = "m1", text = "Marksilo F1"}}})
+local storeSilo = FG.rows(G.jump(FG.open(field(1), siloCtx), "store", "close"))
+check("FG9 marksilo-spørgsmål", findRow(storeSilo, "useFieldSilo").type, "toggle")
+local useSilo = FG.choose(FG.open(field(1), siloCtx), "useFieldSilo", true)
+check("FG9 marksilo ja -> skjul auto", findRow(FG.rows(G.jump(useSilo, "store", "close")), "storeAuto"), nil)
+
+local pickRows = FG.rows(picker)
+check("FG10 vælger: links", countType(pickRows, "link"), 2)
+check("FG10 vælger: id", findRow(pickRows, "field").value, 1)
+local picked = FG.pickField(picker, field(2), ctx(WHEAT))
+check("FG10 valgt mark", picked.draft.fieldId, 2)
+check("FG10 videre til afgrøde", picked.step, "crop")
+check("FG10 mark-trin stadig talt", (G.position(picked)), 2)
+
+-- ---- resultat ----
+local r = FG.result(g, true)
+check("FG11 auto ved start", r.plan.mode, "auto")
+check("FG11 manuel uden start", FG.result(g, false).plan.mode, "manual")
+local autoField = FG.open(field(2, {mode = "auto"}), ctx(WHEAT))
+check("FG11 Ret bevarer auto", FG.result(autoField, nil).plan.mode, "auto")
+check("FG11 Ret bevarer manuel", FG.result(g, nil).plan.mode, "manual")
+check("FG11 mål tvinger auto", FG.result(chosen, false).plan.mode, "auto")
+check("FG11 mål ændret", FG.result(chosen, true).targetChanged, true)
+check("FG11 mål uændret", r.targetChanged, false)
+check("FG11 ingen rigIds i plan", r.plan.rigIds, nil)
+local soilPlan = FG.result(FG.open(field(2, {soil = "plow", mulch = true, lime = false, roll = true}), ctx(WHEAT)), true).plan
+check("FG12 vælg selv bevarer jord", soilPlan.soil, "plow")
+local autoPrep = FG.result(FG.choose(FG.open(field(2, {soil = "plow", mulch = true, lime = false, roll = true}), ctx(WHEAT)), "prepAuto", true), true).plan
+check("FG12 auto: jord", autoPrep.soil, "auto")
+check("FG12 auto: mulch", autoPrep.mulch, false)
+check("FG12 auto: kalk", autoPrep.lime, true)
+check("FG12 auto: tromle", autoPrep.roll, false)
+local grassNoPrep = FG.result(FG.choose(FG.open(field(1, {lime = false}), ctx(GRASS)), "prepAuto", true), true).plan
+check("FG12 græs: auto rører ikke kalk", grassNoPrep.lime, false)
+check("FG13 lager auto", #FG.result(FG.open(field(1, {storeBuildingIds = {"s1"}}), ctx(GRASS)) and FG.choose(FG.open(field(1, {storeBuildingIds = {"s1"}}), ctx(GRASS)), "storeAuto", true), true).plan.storeBuildingIds, 0)
+check("FG13 lager vælg selv", table.concat(FG.result(FG.choose(FG.choose(FG.open(field(1), ctx(GRASS)), "storeAuto", false), "storeBuildingIds", {item = "s2", on = true}), true).plan.storeBuildingIds, ","), "s2")
+check("FG13 lager marksilo", table.concat(FG.result(useSilo, true).plan.storeBuildingIds, ","), "m1")
+check("FG13 bar mark bevarer lager", table.concat(FG.result(FG.open(field(4, {storeBuildingIds = {"s9"}}), ctx(BARE)), true).plan.storeBuildingIds, ","), "s9")
+check("FG14 faste fra -> tom", #FG.result(FG.choose(withRigs, "useDedicated", false), true).rigIds, 0)
+check("FG14 faste til", table.concat(FG.result(toggled, true).rigIds, ","), "v2,v1")
+
+-- ---- faste maskiner på tværs af marker ----
+local byField = FG.applyRigs({[90] = {"v3", "v5"}, [2] = {"v2"}}, 2, {"v2", "v3"})
+check("FG15 fjernet fra anden mark", table.concat(byField[90], ","), "v5")
+check("FG15 sat på marken", table.concat(byField[2], ","), "v2,v3")
+check("FG15 ny mark", table.concat(FG.applyRigs({}, 7, {"v1"})[7], ","), "v1")
+
+-- ---- resumé ----
+local function lineFor(lines, step)
+ for _, line in ipairs(lines) do if line.step == step then return line end end
+ return nil
+end
+local function partKeys(line)
+ local keys = {}
+ for _, part in ipairs(line.parts) do table.insert(keys, part.key or ("=" .. tostring(part.text))) end
+ return table.concat(keys, ",")
+end
+local sum = FG.summary(g)
+check("FG16 linjer for synlige trin", #sum, 6)
+check("FG16 afgrøde behold", partKeys(lineFor(sum, "crop")), "spu_guide_crop_keep")
+check("FG16 halm", partKeys(lineFor(sum, "cropChoice")), "spu_guide_sum_straw_chop")
+check("FG16 klargøring auto", partKeys(lineFor(sum, "prep")), "spu_guide_sum_auto")
+check("FG16 pleje", partKeys(lineFor(sum, "care")), "spu_guide_sum_fert,spu_guide_sum_weed")
+check("FG16 maskiner", partKeys(lineFor(sum, "machines")), "spu_guide_sum_freeRigs,spu_guide_sum_rigs,spu_guide_sum_unloaders")
+check("FG16 lager auto", partKeys(lineFor(sum, "store")), "spu_guide_sum_auto")
+check("FG16 ingen mark/summary-linje", lineFor(sum, "summary"), nil)
+local sumTarget = FG.summary(chosen)
+check("FG17 mål", partKeys(lineFor(sumTarget, "crop")), "spu_guide_sum_target")
+check("FG17 mål titel", lineFor(sumTarget, "crop").parts[1].args[1], "Byg")
+local sumRigs = FG.summary(withRigs)
+check("FG17 faste", lineFor(sumRigs, "machines").parts[1].key, "spu_guide_sum_dedicated")
+check("FG17 faste antal", lineFor(sumRigs, "machines").parts[1].args[1], 1)
+local sumManual = FG.summary(FG.open(field(2, {soil = "plow", lime = true, mulch = true}), ctx(WHEAT)))
+check("FG17 klargøring vælg selv", partKeys(lineFor(sumManual, "prep")), "spu_field_soil_plow,spu_guide_sum_lime,spu_guide_sum_mulch")
+check("FG17 lager valgt silo", partKeys(lineFor(FG.summary(manual), "store")), "=Silo B, 1,2 km")
+check("FG17 lager marksilo", partKeys(lineFor(FG.summary(useSilo), "store")), "spu_guide_sum_fieldSilo")
+local sumHay = FG.summary(FG.choose(FG.choose(FG.open(field(1), ctx(GRASS)), "product", true), "rake", true))
+check("FG17 hø + river", partKeys(lineFor(sumHay, "cropChoice")), "spu_field_plan_hay,spu_guide_sum_rake")
+local sumNoCare = FG.summary(FG.choose(FG.choose(g, "fertilize", false), "weed", false))
+check("FG17 ingen pleje", partKeys(lineFor(sumNoCare, "care")), "spu_guide_sum_noCare")
+local sumSow = FG.summary(FG.choose(g, "sow", true))
+check("FG17 så igen", partKeys(lineFor(sumSow, "crop")), "spu_guide_crop_keep,spu_guide_sum_sowAgain")
+
+-- ---- oversigts-trinnet: links pr. linje ----
+local summaryRows = FG.rows(G.new(g.def, g.draft, g.context, {step = "summary"}))
+check("FG18 oversigt: link pr. linje", countType(summaryRows, "link"), 6)
+check("FG18 oversigt: link-id", findRow(summaryRows, "jump").value, "crop")
+
+print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
+os.exit(failures == 0 and 0 or 1)
diff --git a/tests/test_adGuide.lua b/tests/test_adGuide.lua
new file mode 100644
index 0000000..b90917c
--- /dev/null
+++ b/tests/test_adGuide.lua
@@ -0,0 +1,112 @@
+-- Kør: luajit tests/test_adGuide.lua (fra repo-roden)
+dofile("FS25_ADSmartPickup/adGuide.lua")
+local G = ADGuide
+local failures = 0
+local function check(name, actual, expected)
+ if actual == expected then print("OK " .. name) else
+ failures = failures + 1
+ print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
+ end
+end
+
+-- trin a..e + summary; b vises kun når draft.showB, d aldrig
+local def = {
+ order = {"a", "b", "c", "d", "e", "summary"},
+ isVisible = function(step, guide)
+ if step == "b" then return guide.draft.showB == true end
+ if step == "d" then return false end
+ return true
+ end,
+}
+
+local guide = G.new(def, {showB = false, value = 1}, {x = 1})
+check("G1 start på første synlige", guide.step, "a")
+local index, total = G.position(guide)
+check("G1 position", index, 1)
+check("G1 total over synlige", total, 4)
+check("G1 synlige", table.concat(G.visibleSteps(guide), ","), "a,c,e,summary")
+
+-- næste springer skjulte over
+local g2, event = G.next(guide)
+check("G2 næste springer b over", g2.step, "c")
+check("G2 intet event", event, nil)
+check("G2 gammel guide urørt", guide.step, "a")
+local g3 = G.next(g2)
+check("G3 springer d over", g3.step, "e")
+check("G3 position", (G.position(g3)), 3)
+local g4 = G.next(g3)
+check("G4 summary er sidst", G.isLast(g4), true)
+local _, done = G.next(g4)
+check("G4 næste på sidste = done", done, "done")
+
+-- tilbage
+local back = G.back(g3)
+check("G5 tilbage til c", back.step, "c")
+local _, cancel = G.back(guide)
+check("G5 tilbage på første = cancel", cancel, "cancel")
+
+-- valg der gør et trin synligt ændrer tælling og næste
+local shown = G.withDraft(guide, {showB = true, value = 1})
+check("G6 withDraft ny guide", guide.draft.showB, false)
+check("G6 b synlig", G.next(shown).step, "b")
+check("G6 total", select(2, G.position(shown)), 5)
+
+-- aktuelt trin bliver usynligt: næste går til næste synlige i rækkefølgen, position nil
+local onB = G.new(def, {showB = true}, {}, {step = "b"})
+local hidden = G.withDraft(onB, {showB = false})
+check("G7 position nil for skjult trin", (G.position(hidden)), nil)
+check("G7 næste efter skjult", G.next(hidden).step, "c")
+check("G7 tilbage fra skjult", G.back(hidden).step, "a")
+
+-- spring (Ret) fra oversigt-trinnet: næste -> tilbage til summary, ikke single
+local fromSummary = G.jump(g4, "c", "summary")
+check("G8 spring trin", fromSummary.step, "c")
+check("G8 single", fromSummary.single, true)
+local returned, ev = G.next(fromSummary)
+check("G8 retur til summary", returned.step, "summary")
+check("G8 ikke single mere", returned.single, false)
+check("G8 intet event", ev, nil)
+-- tilbage i spring gendanner kladden fra før springet
+local changed = G.withDraft(fromSummary, {showB = false, value = 99})
+local undone = G.back(changed)
+check("G9 tilbage gendanner kladde", undone.draft.value, 1)
+check("G9 tilbage til summary", undone.step, "summary")
+
+-- spring fra fanens oversigt (close): næste = done, tilbage = cancel
+local single = G.new(def, {value = 5}, {}, {step = "e", single = true, returnTo = "close"})
+local _, singleDone = G.next(single)
+check("G10 single close næste = done", singleDone, "done")
+local _, singleCancel = G.back(single)
+check("G10 single close tilbage = cancel", singleCancel, "cancel")
+check("G10 single isLast", G.isLast(single), true)
+
+-- withContext
+local ctxGuide = G.withContext(guide, {x = 2})
+check("G11 ny context", ctxGuide.context.x, 2)
+check("G11 gammel context", guide.context.x, 1)
+
+-- deepCopy / toggleItem
+local original = {list = {1, 2}, nested = {a = {b = 1}}}
+local copy = G.deepCopy(original)
+copy.list[1] = 9
+copy.nested.a.b = 7
+check("G12 deepCopy liste", original.list[1], 1)
+check("G12 deepCopy indlejret", original.nested.a.b, 1)
+local list = {"x", "y"}
+check("G13 toggle til", table.concat(G.toggleItem(list, "z", true), ","), "x,y,z")
+check("G13 ingen dublet", table.concat(G.toggleItem(list, "x", true), ","), "x,y")
+check("G13 toggle fra", table.concat(G.toggleItem(list, "x", false), ","), "y")
+check("G13 original urørt", #list, 2)
+
+-- tekster
+local texts = {greet = "Hej %s", n = "%d stk", title = "Afgrøde", plain = "Ingen"}
+local function T(key) return texts[key] or key end
+check("G14 label args", G.labelText({key = "greet", args = {"Bo"}}, T), "Hej Bo")
+check("G14 label tekst", G.labelText({text = "rå"}, T), "rå")
+check("G14 manglende args fejler ikke", G.labelText({key = "n"}, T), "%d stk")
+check("G14 nil label", G.labelText(nil, T), "")
+check("G15 linje", G.lineText({step = "crop", title = {key = "title"}, parts = {{key = "plain"}, {key = "n", args = {3}}}}, T), "Afgrøde: Ingen, 3 stk")
+check("G15 linje uden dele", G.lineText({step = "crop", title = {key = "title"}, parts = {}}, T), "Afgrøde: -")
+
+print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
+os.exit(failures == 0 and 0 or 1)