feat(gui): guiden "Opsæt mark" — markfanen får oversigt med resumé (klik på en linje = ret ét trin), trin-for-trin-guide på fælles guide-ramme (ADGuide/ADFieldGuide) og egen markflåde-visning (v1.31.0.0)

This commit is contained in:
2026-09-26 20:20:56 +02:00
parent 7cbd8fb878
commit 92af260a79
17 changed files with 1953 additions and 329 deletions
+471
View File
@@ -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
+162
View File
@@ -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
+2
View File
@@ -25,6 +25,8 @@ if ADSmartPickup.dir ~= nil and source ~= nil then
source(ADSmartPickup.dir .. "gui/SmartPickupStorageTab.lua") source(ADSmartPickup.dir .. "gui/SmartPickupStorageTab.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupFlowTab.lua") source(ADSmartPickup.dir .. "gui/SmartPickupFlowTab.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupFieldTab.lua") source(ADSmartPickup.dir .. "gui/SmartPickupFieldTab.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupGuide.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupFieldGuide.lua")
end end
-- Markøren er dér hvor trækkerens næse stopper; læssetriggeren ligger et -- Markøren er dér hvor trækkerens næse stopper; læssetriggeren ligger et
@@ -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
+41 -278
View File
@@ -1,16 +1,12 @@
-- Smart Pickup — fanen Marker -- Smart Pickup — fanen Marker
-- Tabel over alle gårdens marker (Mark · Afgrøde · Plan · Kørsel · Status · Maskiner); klik på en række -- Oversigt: tabel over alle gårdens marker (Mark · Afgrøde · Plan · Kørsel · Status · Maskiner); klik på en
-- viser markens detaljer under tabellen: plan efter markens art (slåafgrøde, høst, bar), maskiner pr. trin, -- række viser markens resumé (én linje pr. emne — klik på en linje for at rette netop det trin), hele
-- aflæsningsvogne, efter høst, såning, pleje, tving lager, jordtilstand. Nederst markflåden med flueben. -- statusteksten og markøren. [Opsæt mark] åbner guiden (gui/SmartPickupFieldGuide.lua + SmartPickupGuide.lua),
-- Al logik ligger i ADFieldJobs/ADFieldWork/ADFieldModel/ADFieldView. -- [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). -- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
SmartPickupFrame.FIELD_ROW_KINDS = { -- Markflådens rækker (vehiclePrefab/ingredientPrefab) routes til onClickFieldRow.
fieldProduct = true, fieldMode = true, fieldRake = true, fieldStore = true, fleetVehicle = true, SmartPickupFrame.FIELD_ROW_KINDS = {fleetVehicle = true, fleetMode = 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,
}
SmartPickupFrame.ROLE_KEYS = { SmartPickupFrame.ROLE_KEYS = {
mower = "spu_field_role_mower", tedder = "spu_field_role_tedder", rake = "spu_field_role_rake", 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", 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", weeder = "spu_field_role_weeder", fertilizer = "spu_field_role_fertilizer",
baler = "spu_field_role_baler", baleCollector = "spu_field_role_baleCollector", 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 ---- -- ---- tekster ----
@@ -95,18 +82,6 @@ function SmartPickupFrame:fieldStatusText(field, info, short)
return text return text
end 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) function SmartPickupFrame:fruitTitle(name)
local fruit = g_fruitTypeManager ~= nil and name ~= nil and name ~= "" and g_fruitTypeManager:getFruitTypeByName(name) or nil 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) return fruit ~= nil and fruit.fillType ~= nil and tostring(fruit.fillType.title) or tostring(name)
@@ -139,47 +114,15 @@ function SmartPickupFrame:fieldCells(row)
} }
end 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 ---- -- ---- opbygning ----
function SmartPickupFrame:fillFieldRows(adEnv) function SmartPickupFrame:fillFieldRows(adEnv)
self.fieldRows = {} 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 i18n = self.i18n
local infos = ADFields.list() local infos = ADFields.list()
if #infos == 0 then if #infos == 0 then
@@ -201,161 +144,48 @@ function SmartPickupFrame:fillFieldRows(adEnv)
end end
local selected = self.selectedFieldId ~= nil and ADFields.getInfo(self.selectedFieldId) or nil local selected = self.selectedFieldId ~= nil and ADFields.getInfo(self.selectedFieldId) or nil
if selected ~= nil then if selected ~= nil then
self:fillFieldDetail(adEnv, selected) self:fillFieldOverview(adEnv, selected)
end end
self:fillFleetRows(adEnv)
end end
-- lagervalget viser de nærmeste siloer (plus valgte, marksiloer og gæringssiloer) -- Den valgte marks resumé: én klikbar linje pr. emne ([Ret] åbner kun det trin), hele status, markør og
SmartPickupFrame.FIELD_STORE_ROWS = 8 -- advarsel om marksilo der ikke tømmes hjem.
function SmartPickupFrame:fillFieldOverview(adEnv, info)
function SmartPickupFrame:fillFieldDetail(adEnv, info)
local i18n = self.i18n local i18n = self.i18n
local field = ADFieldJobs.getField(info.id) 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) local crop = ADFieldView.cropText(info, function(key) return i18n:getText(key) end)
self:addTextRow(string.format(i18n:getText("spu_field_section"), info.id, crop)) 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))) 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 if info.markerId ~= nil then
self:addTextRow(string.format(i18n:getText("spu_field_marker"), tostring(info.markerName), info.markerDistance or 0)) self:addTextRow(string.format(i18n:getText("spu_field_marker"), tostring(info.markerName), info.markerDistance or 0))
else else
self:addTextRow(" " .. string.format(i18n:getText("spu_field_noMarker"), info.id)) self:addTextRow(" " .. string.format(i18n:getText("spu_guide_noMarker"), info.id))
end end
if field.mowWidth ~= nil then self:addFieldSiloWarnings(context.fieldSilos)
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)
end end
-- Tving lager: siloer der tager markens vare (skår/tørskår/høst); ved ensilage også gæringssiloer. -- Marksiloer skal tømmes hjem af et udkørselsflow.
function SmartPickupFrame:fillFieldStoreRows(adEnv, info, field) function SmartPickupFrame:addFieldSiloWarnings(fieldSilos)
local i18n = self.i18n for _, silo in ipairs(fieldSilos or {}) do
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
local emptied = false local emptied = false
for _, run in ipairs(ADRunsController.store.runs) do for _, run in ipairs(ADRunsController.store.runs) do
for _, sourceId in ipairs(run.kind == "outbound" and run.sourceBuildingIds or {}) 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
end end
if not emptied then 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 end
end end
@@ -387,54 +217,14 @@ function SmartPickupFrame:rigDetails(rig)
return #parts > 0 and (" — " .. table.concat(parts, " — ")) or "" return #parts > 0 and (" — " .. table.concat(parts, " — ")) or ""
end end
-- {[fieldId] = rigIds} for alle marker -- Markflåden (egen visning): alle maskiner med flueben, "fast på F22", "i gang på F108", bredde-advarsler.
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
function SmartPickupFrame:fillFleetRows(adEnv) function SmartPickupFrame:fillFleetRows(adEnv)
local i18n = self.i18n local i18n = self.i18n
self:addTextRow(i18n:getText("spu_field_section_fleet")) self:addTextRow(i18n:getText("spu_field_section_fleet"))
local useAll = ADFieldJobs.state.fleetMode ~= "ticked" local useAll = ADFieldJobs.state.fleetMode ~= "ticked"
self:addToggleRow("fleetMode", i18n:getText("spu_field_fleetAll"), {i18n:getText("ui_no"), i18n:getText("ui_yes")}, useAll) 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 = {}
local rigs, hiddenRigs, hiddenFields = ADFieldFleet.visibleForField(ADFieldJobs.getRigs(adEnv), self.selectedFieldId, rigIdsByField()) for _, rig in ipairs(ADFieldJobs.getRigs(adEnv)) do table.insert(rigs, rig) end
table.sort(rigs, function(a, b) table.sort(rigs, function(a, b)
if a.role ~= b.role then return a.role < b.role end if a.role ~= b.role then return a.role < b.role end
return ADFlowView.naturalLess(a.label, b.label) return ADFlowView.naturalLess(a.label, b.label)
@@ -448,12 +238,15 @@ function SmartPickupFrame:fillFleetRows(adEnv)
if #rigs == 0 then if #rigs == 0 then
self:addTextRow(" " .. i18n:getText("spu_field_noRigs")) self:addTextRow(" " .. i18n:getText("spu_field_noRigs"))
end end
self:addHiddenDedicatedRow(hiddenRigs, hiddenFields)
end end
-- ---- live ---- -- ---- live ----
function SmartPickupFrame:refreshFieldRows(adEnv) function SmartPickupFrame:refreshFieldRows(adEnv)
if self:hasGuideOn(SmartPickupFrame.TAB_FIELDS) or self.fieldView == "fleet" then
self:setStatusText(self.statusOverride or "")
return
end
local rowsById = {} local rowsById = {}
for _, row in ipairs(ADFieldView.rows(ADFields.list(), ADFieldJobs.state.fields)) do rowsById[row.info.id] = row end for _, row in ipairs(ADFieldView.rows(ADFields.list(), ADFieldJobs.state.fields)) do rowsById[row.info.id] = row end
local working, total = 0, 0 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) self.fieldStartButtonInfo.disabled = selected == nil or info == nil or (not isWorking and info.markerId == nil)
end end
-- ---- klik ---- -- ---- klik (markflåden) ----
function SmartPickupFrame:onClickFieldRow(state, data) function SmartPickupFrame:onClickFieldRow(state, data)
self:safe(function(frame) self:safe(function(frame)
local isOn = state == SmartPickupFrame.STATE_ON local isOn = state == SmartPickupFrame.STATE_ON
local id = frame.selectedFieldId
if data.kind == "fleetVehicle" then if data.kind == "fleetVehicle" then
ADFieldJobs.toggleFleet(data.vehicleId) ADFieldJobs.toggleFleet(data.vehicleId)
elseif data.kind == "fleetMode" then elseif data.kind == "fleetMode" then
ADFieldJobs.setFleetMode(isOn and "all" or "ticked") 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 end
frame:requestRebuild() frame:requestRebuild()
end) end)
+16 -5
View File
@@ -52,8 +52,9 @@ function SmartPickupFrame.new(i18n, getAdEnv)
self.flowButtons = {self.backButtonInfo, self.startButtonInfo, self.newButtonInfo, self.renameButtonInfo, self.deleteButtonInfo} self.flowButtons = {self.backButtonInfo, self.startButtonInfo, self.newButtonInfo, self.renameButtonInfo, self.deleteButtonInfo}
self.overviewButtons = {self.backButtonInfo, self.startAllButtonInfo, self.stopAllButtonInfo, self.newButtonInfo} self.overviewButtons = {self.backButtonInfo, self.startAllButtonInfo, self.stopAllButtonInfo, self.newButtonInfo}
-- Marker: Start nu / Stop for den valgte mark -- 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} -- Marker: [Start nu/Stop], [Opsæt mark], [Markflåde] (gui/SmartPickupFieldGuide.lua); guidens knapper
self.fieldButtons = {self.backButtonInfo, self.fieldStartButtonInfo} self:initFieldButtons(i18n)
self:initGuideButtons(i18n)
-- Egen reference til listen: fallback'en i refreshButtons skal kunne sende den igen. -- Egen reference til listen: fallback'en i refreshButtons skal kunne sende den igen.
self.menuButtonInfo = self.overviewButtons self.menuButtonInfo = self.overviewButtons
self:setMenuButtonInfo(self.menuButtonInfo) self:setMenuButtonInfo(self.menuButtonInfo)
@@ -197,6 +198,8 @@ end
function SmartPickupFrame:onFrameClose() function SmartPickupFrame:onFrameClose()
self.frameOpen = false self.frameOpen = false
-- en åben guide kasseres (intet er gemt før [Gem])
self:closeGuide()
SmartPickupFrame:superClass().onFrameClose(self) SmartPickupFrame:superClass().onFrameClose(self)
end end
@@ -243,6 +246,7 @@ function SmartPickupFrame:requestRebuild()
end end
function SmartPickupFrame:switchTab(tabIndex) function SmartPickupFrame:switchTab(tabIndex)
if self.guide ~= nil and not self:hasGuideOn(tabIndex) then self:closeGuide() end
self.currentTab = tabIndex self.currentTab = tabIndex
for index, tab in ipairs(self.subCategoryTabs) do for index, tab in ipairs(self.subCategoryTabs) do
tab:setSelected(index == tabIndex) tab:setSelected(index == tabIndex)
@@ -665,10 +669,12 @@ end
-- sendes listen igen. Kaldes hvert sekund, så den må aldrig kaste. -- sendes listen igen. Kaldes hvert sekund, så den må aldrig kaste.
function SmartPickupFrame:refreshButtons() function SmartPickupFrame:refreshButtons()
local wanted = self.flowButtons 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 wanted = self.overviewButtons
elseif self.currentTab == SmartPickupFrame.TAB_FIELDS then elseif self.currentTab == SmartPickupFrame.TAB_FIELDS then
wanted = self.fieldButtons wanted = self:getFieldButtons()
end end
if self.menuButtonInfo ~= wanted then if self.menuButtonInfo ~= wanted then
self.menuButtonInfo = wanted self.menuButtonInfo = wanted
@@ -757,7 +763,9 @@ end
function SmartPickupFrame:onClickIngredient(state, element) function SmartPickupFrame:onClickIngredient(state, element)
-- mål- og kilde-rækkerne deler prefab (og dermed onClick) med vare-rækkerne -- mål- og kilde-rækkerne deler prefab (og dermed onClick) med vare-rækkerne
local data = self.rowData[element] 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) return self:onClickTarget(state, element)
elseif data ~= nil and data.kind == "source" then elseif data ~= nil and data.kind == "source" then
return self:onClickSource(state, element) 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 if clicked ~= nil and (clicked.kind == "fleetVehicle" or clicked.kind == "fieldRig") then
return self:onClickFieldRow(state, clicked) return self:onClickFieldRow(state, clicked)
end end
if clicked ~= nil and clicked.kind == "guideRow" then
return self:onClickGuideRow(state, clicked)
end
self:safe(function(frame) self:safe(function(frame)
local run, data = frame:getRun(), frame.rowData[element] local run, data = frame:getRun(), frame.rowData[element]
-- kun flowets traktorrækker (de har et køretøj); alt andet ignoreres -- kun flowets traktorrækker (de har et køretøj); alt andet ignoreres
+229
View File
@@ -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
@@ -179,6 +179,9 @@ end
function SmartPickupFrame:onClickProduct(state, element) function SmartPickupFrame:onClickProduct(state, element)
local clicked = self.rowData[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 if clicked ~= nil and clicked.kind == "storageBuilding" then
return self:safe(function(frame) frame:onClickStorageBuilding(state) end) return self:safe(function(frame) frame:onClickStorageBuilding(state) end)
end end
+71 -1
View File
@@ -77,7 +77,7 @@
<text name="spu_field_mode_manual" text="Manuel"/> <text name="spu_field_mode_manual" text="Manuel"/>
<text name="spu_field_notMowable" text="ikke græs/kløver"/> <text name="spu_field_notMowable" text="ikke græs/kløver"/>
<text name="spu_field_scanning" text="Marker indlæses ... (én mark pr. sekund)"/> <text name="spu_field_scanning" text="Marker indlæses ... (én mark pr. sekund)"/>
<text name="spu_field_hint" text="Klik på en mark for detaljer, plan og faste maskiner."/> <text name="spu_field_hint" text="Klik på en mark for at se og rette dens opsætning."/>
<text name="spu_field_showAll" text="Vis alle marker (%d skjult)"/> <text name="spu_field_showAll" text="Vis alle marker (%d skjult)"/>
<text name="spu_col_field" text="Mark"/> <text name="spu_col_field" text="Mark"/>
<text name="spu_col_crop" text="Afgrøde"/> <text name="spu_col_crop" text="Afgrøde"/>
@@ -320,5 +320,75 @@
<text name="spu_col_lasts" text="Rækker til"/> <text name="spu_col_lasts" text="Rækker til"/>
<text name="spu_col_rate" text="Forbrug/prod. døgn"/> <text name="spu_col_rate" text="Forbrug/prod. døgn"/>
<text name="spu_storage_select_tooltip" text="Vælg bygningen tabellen viser."/> <text name="spu_storage_select_tooltip" text="Vælg bygningen tabellen viser."/>
<text name="spu_guide_next" text="Næste"/>
<text name="spu_guide_save" text="Gem"/>
<text name="spu_guide_ok" text="OK"/>
<text name="spu_guide_back" text="Tilbage"/>
<text name="spu_guide_cancel" text="Annullér"/>
<text name="spu_guide_saveOnly" text="Gem uden at starte"/>
<text name="spu_guide_runAuto" text="Kør automatisk"/>
<text name="spu_guide_stepOf" text="Trin %d af %d — %s"/>
<text name="spu_guide_stepOne" text="Trin %d — %s"/>
<text name="spu_guide_editStep" text="Ret: %s"/>
<text name="spu_guide_editSuffix" text=" [Ret]"/>
<text name="spu_guide_sum_hint" text="Tryk på en linje for at rette netop det."/>
<text name="spu_guide_auto" text="Automatisk (anbefalet)"/>
<text name="spu_guide_manual" text="Vælg selv"/>
<text name="spu_guide_closed" text="Guiden er lukket — marken/flowet findes ikke længere"/>
<text name="spu_guide_cannotAdvance" text="Vælg først"/>
<text name="spu_guide_pickField" text="Vælg en mark først"/>
<text name="spu_guide_saved" text="F%d gemt"/>
<text name="spu_guide_noMarker" text="! Sæt en AD-markør ved F%d — arbejdet venter på markøren"/>
<text name="spu_guide_field_open" text="Opsæt mark"/>
<text name="spu_guide_fleet_open" text="Markflåde"/>
<text name="spu_guide_fleet_close" text="Til marker"/>
<text name="spu_guide_field_title" text="— Opsæt mark F%d —"/>
<text name="spu_guide_field_titleNone" text="— Opsæt mark —"/>
<text name="spu_guide_field_step_field" text="Mark"/>
<text name="spu_guide_field_step_crop" text="Afgrøde"/>
<text name="spu_guide_field_step_cropChoice" text="Afgrødens valg"/>
<text name="spu_guide_field_step_prep" text="Klargøring"/>
<text name="spu_guide_field_step_care" text="Pleje"/>
<text name="spu_guide_field_step_machines" text="Maskiner"/>
<text name="spu_guide_field_step_store" text="Lager"/>
<text name="spu_guide_field_step_summary" text="Oversigt"/>
<text name="spu_guide_field_hint_field" text="Vælg den mark du vil sætte op."/>
<text name="spu_guide_field_hint_crop" text="Hvad skal gro på marken? 'Behold' kører videre med det der står nu."/>
<text name="spu_guide_field_hint_cropChoice" text="Hvad skal der ske med afgrøden ved slåning eller høst?"/>
<text name="spu_guide_field_hint_prep" text="Efter høst / på bar mark. Automatisk pløjer eller kultiverer og kalker efter behov."/>
<text name="spu_guide_field_hint_care" text="Mens afgrøden gror. Der gødes/sprøjtes kun når marken trænger."/>
<text name="spu_guide_field_hint_machines" text="Hvor mange ad gangen, og skal marken have sine egne faste maskiner?"/>
<text name="spu_guide_field_hint_store" text="Hvor skal det høstede/slåede hen?"/>
<text name="spu_guide_field_hint_summary" text="Tjek valgene. Tryk på en linje for at rette den."/>
<text name="spu_guide_crop" text="Afgrøde"/>
<text name="spu_guide_crop_keep" text="Behold det der gror"/>
<text name="spu_guide_crop_sowAgain" text="Så samme afgrøde igen efter høst"/>
<text name="spu_guide_prep" text="Klargøring"/>
<text name="spu_guide_prep_autoText" text=" Pløjer når jorden trænger, ellers kultiverer; kalker ved lav pH."/>
<text name="spu_guide_rigs" text="Maskiner ad gangen (multi-tool)"/>
<text name="spu_guide_dedicated" text="Faste maskiner til marken?"/>
<text name="spu_guide_useFieldSilo" text="Brug marksiloen?"/>
<text name="spu_guide_store" text="Lager"/>
<text name="spu_guide_store_autoText" text=" Nærmeste silo med plads (ensilage: gæringssilo)."/>
<text name="spu_guide_sum_target" text="-&gt; %s"/>
<text name="spu_guide_sum_sowAgain" text="så igen"/>
<text name="spu_guide_sum_rake" text="river"/>
<text name="spu_guide_sum_noRake" text="uden river"/>
<text name="spu_guide_sum_straw_chop" text="halm hakkes"/>
<text name="spu_guide_sum_straw_collect" text="halm samles"/>
<text name="spu_guide_sum_straw_bale" text="halm presses"/>
<text name="spu_guide_sum_auto" text="Automatisk"/>
<text name="spu_guide_sum_lime" text="kalk"/>
<text name="spu_guide_sum_mulch" text="mulch"/>
<text name="spu_guide_sum_roll" text="tromle"/>
<text name="spu_guide_sum_fert" text="gødning"/>
<text name="spu_guide_sum_weed" text="ukrudt"/>
<text name="spu_guide_sum_noCare" text="ingen pleje"/>
<text name="spu_guide_sum_dedicated" text="%d faste"/>
<text name="spu_guide_sum_freeRigs" text="ledige maskiner"/>
<text name="spu_guide_sum_rigs" text="%d ad gangen"/>
<text name="spu_guide_sum_unloaders" text="%d aflæsningsvogne"/>
<text name="spu_guide_sum_fieldSilo" text="Marksilo"/>
<text name="spu_guide_sum_stores" text="%d valgte siloer"/>
</texts> </texts>
</l10n> </l10n>
+71 -1
View File
@@ -77,7 +77,7 @@
<text name="spu_field_mode_manual" text="Manual"/> <text name="spu_field_mode_manual" text="Manual"/>
<text name="spu_field_notMowable" text="not grass/clover"/> <text name="spu_field_notMowable" text="not grass/clover"/>
<text name="spu_field_scanning" text="Loading fields ... (one field per second)"/> <text name="spu_field_scanning" text="Loading fields ... (one field per second)"/>
<text name="spu_field_hint" text="Click a field for details, plan and dedicated machines."/> <text name="spu_field_hint" text="Click a field to see and change its setup."/>
<text name="spu_field_showAll" text="Show all fields (%d hidden)"/> <text name="spu_field_showAll" text="Show all fields (%d hidden)"/>
<text name="spu_col_field" text="Field"/> <text name="spu_col_field" text="Field"/>
<text name="spu_col_crop" text="Crop"/> <text name="spu_col_crop" text="Crop"/>
@@ -320,5 +320,75 @@
<text name="spu_col_lasts" text="Lasts"/> <text name="spu_col_lasts" text="Lasts"/>
<text name="spu_col_rate" text="Use/prod. per day"/> <text name="spu_col_rate" text="Use/prod. per day"/>
<text name="spu_storage_select_tooltip" text="Choose the building shown in the table."/> <text name="spu_storage_select_tooltip" text="Choose the building shown in the table."/>
<text name="spu_guide_next" text="Next"/>
<text name="spu_guide_save" text="Save"/>
<text name="spu_guide_ok" text="OK"/>
<text name="spu_guide_back" text="Back"/>
<text name="spu_guide_cancel" text="Cancel"/>
<text name="spu_guide_saveOnly" text="Save without starting"/>
<text name="spu_guide_runAuto" text="Run automatically"/>
<text name="spu_guide_stepOf" text="Step %d of %d — %s"/>
<text name="spu_guide_stepOne" text="Step %d — %s"/>
<text name="spu_guide_editStep" text="Edit: %s"/>
<text name="spu_guide_editSuffix" text=" [Edit]"/>
<text name="spu_guide_sum_hint" text="Click a line to change just that."/>
<text name="spu_guide_auto" text="Automatic (recommended)"/>
<text name="spu_guide_manual" text="Choose myself"/>
<text name="spu_guide_closed" text="Guide closed — the field/flow no longer exists"/>
<text name="spu_guide_cannotAdvance" text="Choose first"/>
<text name="spu_guide_pickField" text="Pick a field first"/>
<text name="spu_guide_saved" text="F%d saved"/>
<text name="spu_guide_noMarker" text="! Place an AD marker at F%d — work waits for the marker"/>
<text name="spu_guide_field_open" text="Set up field"/>
<text name="spu_guide_fleet_open" text="Field fleet"/>
<text name="spu_guide_fleet_close" text="Back to fields"/>
<text name="spu_guide_field_title" text="— Set up field F%d —"/>
<text name="spu_guide_field_titleNone" text="— Set up field —"/>
<text name="spu_guide_field_step_field" text="Field"/>
<text name="spu_guide_field_step_crop" text="Crop"/>
<text name="spu_guide_field_step_cropChoice" text="Crop options"/>
<text name="spu_guide_field_step_prep" text="Preparation"/>
<text name="spu_guide_field_step_care" text="Crop care"/>
<text name="spu_guide_field_step_machines" text="Machines"/>
<text name="spu_guide_field_step_store" text="Storage"/>
<text name="spu_guide_field_step_summary" text="Summary"/>
<text name="spu_guide_field_hint_field" text="Pick the field you want to set up."/>
<text name="spu_guide_field_hint_crop" text="What should grow here? 'Keep' carries on with the current crop."/>
<text name="spu_guide_field_hint_cropChoice" text="What happens to the crop when mowing or harvesting?"/>
<text name="spu_guide_field_hint_prep" text="After harvest / on bare land. Automatic plows or cultivates and limes as needed."/>
<text name="spu_guide_field_hint_care" text="While the crop grows. Fertilizing/spraying only happens when needed."/>
<text name="spu_guide_field_hint_machines" text="How many at a time, and should the field have its own dedicated machines?"/>
<text name="spu_guide_field_hint_store" text="Where should the harvest go?"/>
<text name="spu_guide_field_hint_summary" text="Check your choices. Click a line to change it."/>
<text name="spu_guide_crop" text="Crop"/>
<text name="spu_guide_crop_keep" text="Keep what grows"/>
<text name="spu_guide_crop_sowAgain" text="Sow the same crop again after harvest"/>
<text name="spu_guide_prep" text="Preparation"/>
<text name="spu_guide_prep_autoText" text=" Plows when the soil needs it, otherwise cultivates; limes at low pH."/>
<text name="spu_guide_rigs" text="Machines at a time (multi-tool)"/>
<text name="spu_guide_dedicated" text="Dedicated machines for this field?"/>
<text name="spu_guide_useFieldSilo" text="Use the field silo?"/>
<text name="spu_guide_store" text="Storage"/>
<text name="spu_guide_store_autoText" text=" Nearest silo with room (silage: fermenting silo)."/>
<text name="spu_guide_sum_target" text="-&gt; %s"/>
<text name="spu_guide_sum_sowAgain" text="sow again"/>
<text name="spu_guide_sum_rake" text="rake"/>
<text name="spu_guide_sum_noRake" text="no rake"/>
<text name="spu_guide_sum_straw_chop" text="straw chopped"/>
<text name="spu_guide_sum_straw_collect" text="straw collected"/>
<text name="spu_guide_sum_straw_bale" text="straw baled"/>
<text name="spu_guide_sum_auto" text="Automatic"/>
<text name="spu_guide_sum_lime" text="lime"/>
<text name="spu_guide_sum_mulch" text="mulch"/>
<text name="spu_guide_sum_roll" text="roll"/>
<text name="spu_guide_sum_fert" text="fertilizer"/>
<text name="spu_guide_sum_weed" text="weeds"/>
<text name="spu_guide_sum_noCare" text="no care"/>
<text name="spu_guide_sum_dedicated" text="%d dedicated"/>
<text name="spu_guide_sum_freeRigs" text="free machines"/>
<text name="spu_guide_sum_rigs" text="%d at a time"/>
<text name="spu_guide_sum_unloaders" text="%d unloaders"/>
<text name="spu_guide_sum_fieldSilo" text="Field silo"/>
<text name="spu_guide_sum_stores" text="%d chosen silos"/>
</texts> </texts>
</l10n> </l10n>
+3 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?> <?xml version="1.0" encoding="utf-8" standalone="no"?>
<modDesc descVersion="107"> <modDesc descVersion="107">
<author>masterdraco</author> <author>masterdraco</author>
<version>1.30.4.0</version> <version>1.31.0.0</version>
<title> <title>
<en>AD Smart Pickup</en> <en>AD Smart Pickup</en>
</title> </title>
@@ -23,6 +23,7 @@
<sourceFile filename="adOutboundPlanner.lua"/> <sourceFile filename="adOutboundPlanner.lua"/>
<sourceFile filename="adStorageStats.lua"/> <sourceFile filename="adStorageStats.lua"/>
<sourceFile filename="adFlowView.lua"/> <sourceFile filename="adFlowView.lua"/>
<sourceFile filename="adGuide.lua"/>
<sourceFile filename="adSources.lua"/> <sourceFile filename="adSources.lua"/>
<sourceFile filename="adBuildings.lua"/> <sourceFile filename="adBuildings.lua"/>
<sourceFile filename="adOutbound.lua"/> <sourceFile filename="adOutbound.lua"/>
@@ -30,6 +31,7 @@
<sourceFile filename="adFieldModel.lua"/> <sourceFile filename="adFieldModel.lua"/>
<sourceFile filename="adFieldFleet.lua"/> <sourceFile filename="adFieldFleet.lua"/>
<sourceFile filename="adFieldView.lua"/> <sourceFile filename="adFieldView.lua"/>
<sourceFile filename="adFieldGuide.lua"/>
<sourceFile filename="adFields.lua"/> <sourceFile filename="adFields.lua"/>
<sourceFile filename="adCpBridge.lua"/> <sourceFile filename="adCpBridge.lua"/>
<sourceFile filename="adFieldStorage.lua"/> <sourceFile filename="adFieldStorage.lua"/>
+45 -21
View File
@@ -540,7 +540,7 @@ maskiner. Kræver **Courseplay** ud over AutoDrive.
### Vælg og glem: afgrøde på marken ### 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 - **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. 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. - **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 → … - 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 Med en afgrøde valgt kører marken automatisk. Du kan stadig vælge ensilage/hø, halm, antal maskiner,
stadig vælge ensilage/hø, halm, antal maskiner, jordbehandling og pleje. Vælg *Ingen — planlæg selv* for jordbehandling og pleje i de næste trin. Vælg *Behold det der gror* for at styre marken som før.
at styre marken manuelt som før.
### Opsætning ### 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 `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. 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å. 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 **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, 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å 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 ### 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 | | **Afgrøde** | *Behold det der gror* / *-> Hvede* |
| **Maskiner pr. trin** | 1–5 sæt samtidig (Courseplays multi-tool). Et trin starter først når alle sæt er ledige | | **Afgrødens valg** | *Ensilage, river* / *halm hakkes* |
| **Plan** (slåafgrøde) | Ensilage eller Hø | | **Klargøring** | *Automatisk* / *Pløj, kalk* |
| **Riv** (slåafgrøde) | Riv skårene sammen før opsamling | | **Pleje** | *gødning, ukrudt* |
| **Aflæsningsvogne** (høst) | 1–3 vogne der følger høsteren og kører til lager | | **Maskiner** | *3 faste* / *ledige maskiner*, *1 ad gangen*, *1 aflæsningsvogne* |
| **Halm efter høst** (høst) | Hakkes (spredes), Samles op til silo, eller Presses til baller, som en ballesamler kører til et ballelager | | **Lager** | *Marksilo* / *Automatisk* / en valgt silo |
| **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 |
Under valgene står **Jorden trænger til** (kalk, pløjning, gødning, ukrudt), markøren og evt. slåbredde og **Klik på en linje** for at rette netop det (fx lageret): kun det ene trin åbnes, og **Gem** gemmer og går
skår-forskydning. 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 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. marken vokser og trænger). På en bar mark starter den klargøring/såning. **Stop mark** stopper maskinerne.
+25 -1
View File
@@ -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. 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 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.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). - [ ] **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) ## 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 | | 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 | | 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) | | 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 | | Ballesamler | Courseplays BALE_FINDER + AutoDrive til ballelager | Ballesamleren starter ikke, eller kører ikke fulde læs væk |
+47 -21
View File
@@ -546,7 +546,7 @@ and which machines. Requires **Courseplay** in addition to AutoDrive.
### Select and forget: crop on the field ### 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 - **Whatever grows now** is left to finish, cared for (fertilizer, weeds, lime) and harvested or mowed when
ready. 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. - **Bare field:** prepared and sown as soon as it is sowing time.
- After that the cycle runs by itself: growth → care → harvest → preparation → sowing → … - 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. With a crop chosen, the field runs automatically. You can still choose silage/hay, straw, number of
You can still choose silage/hay, straw, number of machines, soil work and care. Choose *None — plan it machines, soil work and care in the following steps. Choose *Keep what grows* to run the field as before.
yourself* to run the field manually as before.
### Setup ### 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 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. 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. 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 **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 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 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 ### 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 | | **Crop** | *Keep what grows* / *-> Wheat* |
| **Machines per step** | 1–5 rigs at once (Courseplay multi-tool). A step starts only when all rigs are free | | **Crop options** | *Silage, rake* / *straw chopped* |
| **Plan** (mowable) | Silage or Hay | | **Preparation** | *Automatic* / *Plow, lime* |
| **Rake** (mowable) | Rake the swaths before collecting | | **Crop care** | *fertilizer, weeds* |
| **Unloading trailers** (harvest) | 1–3 trailers that follow the harvester and drive to storage | | **Machines** | *3 dedicated* / *free machines*, *1 at a time*, *1 unloaders* |
| **Straw after harvest** (harvest) | Chopped (spread), Collected to a silo, or Baled, and a bale collector takes the bales to a bale store | | **Storage** | *Field silo* / *Automatic* / a chosen silo |
| **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 |
Below the options: **The soil needs** (lime, plowing, fertilizer, weeds), the marker, and mowing width and **Click a line** to change just that (e.g. storage): only that step opens, and **Save** saves and returns.
swath offset when relevant. 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 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 care, when the field grows and needs it). On a bare field it starts preparation/sowing. **Stop field** stops
+174
View File
@@ -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.
+245
View File
@@ -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)
+112
View File
@@ -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)