38 KiB
Flows/Fields: farvet liste + redigeringsvindue — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Flows- og Fields-fanen viser kun en farvet liste med Start/Stop pr. række; klik på en række åbner et redigeringsvindue (panel i samme frame) med de nuværende redigeringslinjer.
Architecture: Rene statusfunktioner (ADFlowView.flowStatus, ADFieldView.fieldStatus) afgør niveau/farve og testes med luajit. GUI: rækkehjælperne får et mål-layout (self.rowTarget), så den eksisterende resumé-/guide-kode kan tegne i vinduets egen ScrollingLayout. Vinduet er et skjult overlay i SmartPickupFrame.xml, styret af en ny mixin gui/SmartPickupEditPanel.lua.
Tech Stack: FS25 Lua (GIANTS GUI: Bitmap/Text/ScrollingLayout, TabbedMenuFrameElement menuButtonInfo), luajit-tests i tests/.
Spec: docs/superpowers/specs/2026-09-29-flow-field-list-edit-panel-design.md
Global Constraints
- Ingen ændring af flow-/mark-logik, guiderne eller Oversigt-tavlen/flåde-visningen.
- Rækker er ≈835 px brede (som lagervisningen).
- Farver: grøn
{0.30, 0.75, 0.35, 1}= running, gul{0.95, 0.80, 0.25, 1}= waiting, rød{0.90, 0.35, 0.30, 1}= error, grå{0.55, 0.55, 0.55, 1}= stopped. - Al GUI-kode i
self:safe(...); ingen utilsigtede globals (tests/test_noStrayGlobals.lua). - Byg kun når alle tests er grønne:
for t in tests/test_*.lua; do luajit $t >/dev/null || echo FAIL $t; done. Zip:cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip ., kopi til~/FS25-data/mods/, commit, push remotegiteamain (Basic-auth header med token fra~/credentials-vault.mdsektion Gitea, linje med "API-token"). - l10n: nye tekster i både
l10n/l10n_da.xmlogl10n/l10n_en.xml, præfiksspu_.
Review Focus
- Klik på listen bag et åbent vindue: skal intet gøre (vinduet ejer klik) — pinned af
makeRowClickable-regel i Task 2 (klik tæller kun inde i rækkens eget layout, og listen ignorerer klik når vinduet er åbent). - ESC med vindue åbent: lukker vinduet, ikke ESC-menuen; med et [Edit]-trin åbent: lukker trinnet først — Task 3.
- Flow/mark slettet mens vinduet er åbent: vinduet lukker sig med besked — Task 3 (
refreshEditPanel). - Start/Stop på en række for et andet flow end det valgte: må ikke påvirke det valgte — Task 4 (handling bærer sit eget id).
- Flow startet men alle traktorer afvist ("ingen vogn"): rød, ikke grå — pinned af test i Task 1.
Task 1: Rene statusfunktioner for flows (+ fælles farver)
Files:
- Modify:
FS25_ADSmartPickup/adFlowView.lua(tilføj i bunden) - Test:
tests/test_flowStatus.lua(ny)
Interfaces:
-
Produces:
ADFlowView.LEVEL_COLORS = {running={...}, waiting={...}, error={...}, stopped={...}};ADFlowView.flowStatus(s) -> levelhvors = {running=int, total=int, stopped=bool, canStop=bool, warning=string|nil, reason=string|nil, lastStartFailed=bool}oglevel ∈ {"running","waiting","error","stopped"}. -
Step 1: Write the failing test —
tests/test_flowStatus.lua:
-- Kør: luajit tests/test_flowStatus.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFlowView.lua")
local V = ADFlowView
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
check("kører", V.flowStatus({running = 3, total = 4, canStop = true}), "running")
check("kører men holder (begrundelse)", V.flowStatus({running = 2, total = 2, canStop = true, reason = "intet at hente"}), "waiting")
check("startet, venter på vogn", V.flowStatus({running = 0, total = 2, canStop = true}), "waiting")
check("advarsel", V.flowStatus({running = 1, total = 2, canStop = true, warning = "mål mangler"}), "error")
check("start afvist (ingen vogn)", V.flowStatus({running = 0, total = 2, lastStartFailed = true}), "error")
check("stoppet", V.flowStatus({running = 0, total = 2, stopped = true}), "stopped")
check("aldrig startet", V.flowStatus({running = 0, total = 2}), "stopped")
check("stoppet vinder over gammel afvisning", V.flowStatus({running = 0, total = 2, stopped = true, lastStartFailed = true}), "stopped")
check("farver findes", V.LEVEL_COLORS.waiting ~= nil and V.LEVEL_COLORS.stopped ~= nil, true)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-
Step 2: Run
luajit tests/test_flowStatus.lua— Expected: FAIL (flowStatus nil). -
Step 3: Implement — append to
adFlowView.lua:
-- ---- status pr. flow (Flows-listen) ----
ADFlowView.LEVEL_COLORS = {
running = {0.30, 0.75, 0.35, 1},
waiting = {0.95, 0.80, 0.25, 1},
error = {0.90, 0.35, 0.30, 1},
stopped = {0.55, 0.55, 0.55, 1},
}
-- s = {running, total, stopped, canStop, warning, reason, lastStartFailed} -> "running"|"waiting"|"error"|"stopped"
function ADFlowView.flowStatus(s)
if s.stopped then return "stopped" end
if s.warning ~= nil then return "error" end
if (s.running or 0) == 0 and s.lastStartFailed then return "error" end
if (s.running or 0) > 0 and s.reason == nil then return "running" end
if (s.running or 0) > 0 or s.canStop then return "waiting" end
return "stopped"
end
- Step 4: Run all tests — Expected:
ALLE TESTS OKin test_flowStatus, no FAIL elsewhere. - Step 5: Commit
git add tests/test_flowStatus.lua FS25_ADSmartPickup/adFlowView.lua && git commit -m "feat(flows): ren statusfunktion + farver til Flows-listen"
Task 2: Rækker kan tegnes i et andet layout
Files:
- Modify:
FS25_ADSmartPickup/gui/SmartPickupFrame.lua(new: felter;addRow;clearRows;colorRows) - Modify:
FS25_ADSmartPickup/gui/SmartPickupFlowTab.lua(makeRowClickable)
Interfaces:
-
Produces:
self.rowTarget(standardnil=self.boxLayout);SmartPickupFrame:withRowTarget(layout, fn)kørerfn(self)med rækker klonet ind ilayout;self.panelRows(kloner i vinduet) adskilt fraself.clonedRows;SmartPickupFrame:clearPanelRows();SmartPickupFrame:colorLayout(layout). Hver klon fårrow.spuLayout = layout. -
Step 1:
addRow— replace body:
function SmartPickupFrame:addRow(prefab, data)
local layout = self.rowTarget or self.boxLayout
local row = prefab:clone(layout)
row.spuLayout = layout
FocusManager:loadElementFromCustomValues(row)
table.insert(layout == self.boxLayout and self.clonedRows or self.panelRows, row)
local option = row:getDescendantByName("option")
if option ~= nil then
option:setTexts({self.i18n:getText("ui_no"), self.i18n:getText("ui_yes")})
self.rowData[option] = data
end
return row
end
-- Rækker klonet ind i et andet layout (redigeringsvinduet) mens fn kører.
function SmartPickupFrame:withRowTarget(layout, fn)
local previous = self.rowTarget
self.rowTarget = layout
local ok, err = pcall(fn, self)
self.rowTarget = previous
if not ok then error(err, 0) end
end
function SmartPickupFrame:clearPanelRows()
for _, row in ipairs(self.panelRows or {}) do
FocusManager:removeElement(row)
row:delete()
end
self.panelRows = {}
end
- Step 2:
new— next toclonedRows = {}initialisation addself.panelRows = {}(findclonedRowsinSmartPickupFrame.newand add on the same pattern). - Step 3:
colorRows— split intocolorLayout(layout)and keepcolorRows()callingself:colorLayout(self.boxLayout):
function SmartPickupFrame:colorLayout(layout)
local colors = InGameMenuSettingsFrame ~= nil and InGameMenuSettingsFrame.COLOR_ALTERNATING or nil
if colors == nil or layout == nil then return end
local unpackFn = table.unpack or unpack
local isEvenRow = false
for _, element in ipairs(layout.elements) do
if element.setImageColor ~= nil and colors[isEvenRow] ~= nil then
element:setImageColor(nil, unpackFn(colors[isEvenRow]))
end
isEvenRow = not isEvenRow
end
end
function SmartPickupFrame:colorRows()
self:colorLayout(self.boxLayout)
end
- Step 4:
makeRowClickable— clicks count inside the row's own layout, and list rows ignore clicks while the panel is open:
local layout = element.spuLayout or frame.boxLayout
local blockedByPanel = frame.editPanelOpen and layout == frame.boxLayout
if not used and not eventUsed and not blockedByPanel and isUp and button == MOUSE_LEFT and (element.getIsVisible == nil or element:getIsVisible())
and isInside(element, posX, posY) and isInside(layout, posX, posY) then
(replace the existing if not used and not eventUsed and isUp ... condition lines; keep the body.)
- Step 5: Run all tests — no FAIL (behaviour unchanged:
rowTargetnil everywhere). - Step 6: Commit
git commit -am "refactor(gui): rækker kan klones ind i et andet layout (redigeringsvindue)"
Task 3: Redigeringsvinduet (overlay + mixin)
Files:
- Modify:
FS25_ADSmartPickup/gui/SmartPickupFrame.xml(overlay efterboxLayout) - Modify:
FS25_ADSmartPickup/gui/guiProfiles.xml(profiler) - Create:
FS25_ADSmartPickup/gui/SmartPickupEditPanel.lua - Modify:
FS25_ADSmartPickup/adSmartPickup.lua(source efterSmartPickupFlowGuide.lua) - Modify:
FS25_ADSmartPickup/gui/SmartPickupFrame.lua(rebuild: fyld vinduet;refreshButtons: vinduets knapper;refreshLive:refreshEditPanel;onFrameClose: luk vinduet) - Modify:
l10n/l10n_da.xml,l10n/l10n_en.xml
Interfaces:
-
Consumes: Task 2 (
withRowTarget,clearPanelRows,colorLayout,editPanelOpen). -
Produces:
SmartPickupFrame:openEditPanel(kind, id)(kind ∈ {"flow","field"}),closeEditPanel(),fillEditPanel(adEnv),refreshEditPanel(adEnv), felterneself.editPanelOpen,self.editPanelKind,self.editPanelId; hook-tabelSmartPickupFrame.EDIT_PANEL_KINDS[kind] = {title(frame,id)->string, exists(frame,id)->bool, fill(frame,adEnv,id), status(frame,adEnv,id)->{text,level}, startStop(frame,id), startStopText(frame,adEnv,id)->string, canDelete=bool, delete(frame,id)}— Task 4/7 registrerer "flow"/"field". -
Step 1: Profiles — add to
guiProfiles.xml(before</Profiles>):
<!-- redigeringsvindue (v1.58): mørk flade + panel; størrelser sættes i Lua ud fra boxLayout -->
<Profile name="spuOverlayBackdrop" extends="spuBar">
<imageColor value="0 0 0 0.65"/>
<positionOrigin value="topLeft"/>
<size value="100px 100px"/>
</Profile>
<Profile name="spuOverlayPanel" extends="spuBar">
<imageColor value="0.09 0.09 0.09 0.97"/>
<positionOrigin value="topLeft"/>
<size value="100px 100px"/>
</Profile>
<Profile name="spuPanelTitle" extends="spuCellBold">
<textSize value="20px"/>
</Profile>
- Step 2: XML overlay — directly after
</ScrollingLayout>ofboxLayout:
<!-- redigeringsvindue (skjult; placeres/størrelse sættes i SmartPickupEditPanel.lua) -->
<GuiElement profile="baseReference" id="editOverlay" visible="false">
<Bitmap profile="spuOverlayBackdrop" id="editBackdrop"/>
<Bitmap profile="spuOverlayPanel" id="editPanel">
<Text profile="spuPanelTitle" id="editTitle" position="20px -16px"/>
<Text profile="spuCell" id="editStatus" position="20px -46px"/>
<ScrollingLayout profile="fs25_settingsLayout" id="editLayout" position="0px -80px"/>
</Bitmap>
</GuiElement>
- Step 3: Mixin — create
gui/SmartPickupEditPanel.lua:
-- Smart Pickup — redigeringsvindue (v1.58)
-- Klik på en række i Flows/Fields åbner et panel oven på listen. Indholdet tegnes af den eksisterende
-- resumé-/guide-kode i vinduets egen ScrollingLayout (SmartPickupFrame:withRowTarget). Knapper i bunden via
-- menuButtonInfo: Luk (MENU_BACK = ESC), Start/Stop, Slet. Hver art registrerer sig i EDIT_PANEL_KINDS.
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
SmartPickupFrame.EDIT_PANEL_KINDS = SmartPickupFrame.EDIT_PANEL_KINDS or {}
SmartPickupFrame.EDIT_PANEL_WIDTH, SmartPickupFrame.EDIT_PANEL_HEIGHT = 0.78, 0.86 -- andel af listens flade
function SmartPickupFrame:initEditPanelButtons(i18n)
self.panelCloseButtonInfo = {inputAction = InputAction.MENU_BACK, text = i18n:getText("spu_panel_close"), callback = function() self:onClickPanelClose() end}
self.panelStartButtonInfo = {inputAction = InputAction.MENU_ACTIVATE, text = i18n:getText("spu_button_start"), callback = function() self:onClickPanelStartStop() end}
self.panelDeleteButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_button_delete"), callback = function() self:onClickPanelDelete() end}
self.panelButtons = {self.panelCloseButtonInfo, self.panelStartButtonInfo, self.panelDeleteButtonInfo}
end
function SmartPickupFrame:editPanelKind()
return self.editPanelOpen and SmartPickupFrame.EDIT_PANEL_KINDS[self.editPanelKind] or nil
end
-- Vinduet dækker listens flade; panelet er centreret i den.
function SmartPickupFrame:layoutEditPanel()
local box = self.boxLayout
local x, y, w, h = box.position[1], box.position[2], box.size[1], box.size[2]
self.editBackdrop:setPosition(x, y)
self.editBackdrop:setSize(w, h)
local pw, ph = w * SmartPickupFrame.EDIT_PANEL_WIDTH, h * SmartPickupFrame.EDIT_PANEL_HEIGHT
self.editPanel:setPosition(x + (w - pw) / 2, y - (h - ph) / 2)
self.editPanel:setSize(pw, ph)
local top = 80 * g_pixelSizeScaledY
self.editLayout:setSize(pw, ph - top)
end
function SmartPickupFrame:openEditPanel(kind, id)
if SmartPickupFrame.EDIT_PANEL_KINDS[kind] == nil then return end
self.editPanelOpen, self.editPanelKind, self.editPanelId = true, kind, id
self:closeGuide()
self.editOverlay:setVisible(true)
self:layoutEditPanel()
self:requestRebuild()
end
function SmartPickupFrame:closeEditPanel()
if not self.editPanelOpen then return end
self.editPanelOpen, self.editPanelKind, self.editPanelId = false, nil, nil
self:closeGuide()
self:clearPanelRows()
if self.editOverlay ~= nil then self.editOverlay:setVisible(false) end
self:requestRebuild()
end
-- Kaldes fra rebuild (efter listen): vinduets rækker genopbygges i editLayout.
function SmartPickupFrame:fillEditPanel(adEnv)
self:clearPanelRows()
local hooks = self:editPanelKind()
if hooks == nil then return end
if not hooks.exists(self, self.editPanelId) then
self.statusOverride = self.i18n:getText("spu_panel_gone")
self.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS
self.editPanelOpen, self.editPanelKind, self.editPanelId = false, nil, nil
self.editOverlay:setVisible(false)
return
end
self.editTitle:setText(hooks.title(self, self.editPanelId))
self:withRowTarget(self.editLayout, function(frame)
if frame.guide ~= nil then
frame:fillGuideRows(adEnv)
else
hooks.fill(frame, adEnv, frame.editPanelId)
end
end)
self:colorLayout(self.editLayout)
self.editLayout:invalidateLayout()
self:refreshEditPanel(adEnv)
end
-- Hvert sekund: status + knaptekster.
function SmartPickupFrame:refreshEditPanel(adEnv)
local hooks = self:editPanelKind()
if hooks == nil or adEnv == nil then return end
if not hooks.exists(self, self.editPanelId) then self:closeEditPanel() return end
local status = hooks.status(self, adEnv, self.editPanelId)
self.editStatus:setText(status.text or "")
local color = ADFlowView.LEVEL_COLORS[status.level] or ADFlowView.LEVEL_COLORS.stopped
if self.editStatus.setTextColor ~= nil then self.editStatus:setTextColor(color[1], color[2], color[3], color[4]) end
self.panelStartButtonInfo.text = hooks.startStopText(self, adEnv, self.editPanelId)
self.panelDeleteButtonInfo.disabled = not hooks.canDelete
end
function SmartPickupFrame:onClickPanelClose()
self:safe(function(frame)
-- et åbent [Edit]-trin lukkes først (kladden kasseres som ved Annullér)
if frame.guide ~= nil then
frame:closeGuide()
frame:requestRebuild()
return
end
frame:closeEditPanel()
end)
end
function SmartPickupFrame:onClickPanelStartStop()
self:safe(function(frame)
local hooks = frame:editPanelKind()
if hooks ~= nil then hooks.startStop(frame, frame.editPanelId) end
frame:requestRebuild()
end)
end
function SmartPickupFrame:onClickPanelDelete()
self:safe(function(frame)
local hooks = frame:editPanelKind()
if hooks ~= nil and hooks.canDelete then hooks.delete(frame, frame.editPanelId) end
end)
end
- Step 4: Wire into frame —
adSmartPickup.lua: aftersource(ADSmartPickup.dir .. "gui/SmartPickupFlowGuide.lua")addsource(ADSmartPickup.dir .. "gui/SmartPickupEditPanel.lua").SmartPickupFrame.new: whereinitFlowButtons(i18n)is called, also callself:initEditPanelButtons(i18n).rebuild: in the Flows branch and the Fields branch, afterself.boxLayout:invalidateLayout()addif self.editPanelOpen then self:fillEditPanel(adEnv) end.refreshButtons: first branch:if self.editPanelOpen then wanted = self.guide ~= nil and self:getGuideButtons() or self.panelButtons elseif self:hasGuideOn(...)— and in the guide-in-panel case replace the guide list'sself.backButtonInfobyself.panelCloseButtonInfo(buildself.panelGuideButtonsonce: copy ofgetGuideButtons()with index 1 swapped).refreshLive: at the endif self.editPanelOpen then self:refreshEditPanel(adEnv) end.onFrameClose: beforecloseGuide()addself.editPanelOpen, self.editPanelKind, self.editPanelId = false, nil, nil; if self.editOverlay ~= nil then self.editOverlay:setVisible(false) end; self:clearPanelRows().switchTab:if self.editPanelOpen then self:closeEditPanel() endat the top.- Guide rendering while panel is open:
fillFlowTabRows/fillFieldRowsstart withif self:hasGuideOn(TAB) and self:fillGuideRows(adEnv) then return end— guard withnot self.editPanelOpen andso the guide draws only in the panel.
- Step 5: l10n — add
spu_panel_close("Luk"/"Close"),spu_panel_gone("Posten findes ikke længere — vinduet er lukket"/"That item no longer exists — window closed"),spu_button_deleteif not present (checkgrep spu_button_delete l10n/l10n_da.xmlfirst; reuse existing delete text key if it exists). - Step 6: Run all tests (noStrayGlobals must pass) and validate XML:
python3 -c "import xml.dom.minidom as m; [m.parse(f) for f in ('FS25_ADSmartPickup/gui/SmartPickupFrame.xml','FS25_ADSmartPickup/gui/guiProfiles.xml','FS25_ADSmartPickup/l10n/l10n_da.xml','FS25_ADSmartPickup/l10n/l10n_en.xml')]". - Step 7: Commit
git commit -am "feat(gui): redigeringsvindue (overlay) med Luk/ESC, Start/Stop, Slet"
Task 4: Flows-fanen som farvet liste + "flow" i vinduet
Files:
- Modify:
FS25_ADSmartPickup/gui/SmartPickupFrame.xml(nyflowListPrefab, fjernes vedinitialize) - Modify:
FS25_ADSmartPickup/gui/SmartPickupFrame.lua(initialize-listen:"flowListPrefab";onClickStartStophusker afvisning) - Modify:
FS25_ADSmartPickup/gui/SmartPickupFlowGuide.lua(fillFlowTabRows,refreshFlowTab,initFlowButtons, registrérEDIT_PANEL_KINDS.flow) - Modify:
l10n/*.xml
Interfaces:
-
Consumes:
ADFlowView.flowStatus,ADFlowView.LEVEL_COLORS(Task 1);openEditPanel,EDIT_PANEL_KINDS(Task 3);makeRowClickable(Task 2). -
Produces:
SmartPickupFrame:flowLevel(adEnv, run) -> level,self.lastStartFailed[runId] = bool,SmartPickupFrame:toggleRun(adEnv, runId). -
Step 1: Prefab — in the XML after
flowPrefab:
<!-- Flows-listen (v1.58): ● · navn · varer · kører/i alt · status · [Start]/[Stop]. ~835 px. -->
<Bitmap profile="fs25_multiTextOptionContainer" id="flowListPrefab">
<Bitmap profile="spuDot" name="dot" position="20px 0px"/>
<Text profile="spuCellBold" name="c1" position="38px 0px" size="200px 40px" textMaxWidth="192px"/>
<Text profile="spuCell" name="c2" position="238px 0px" size="170px 40px" textMaxWidth="162px"/>
<Text profile="spuCell" name="c3" position="408px 0px" size="70px 40px" textMaxWidth="62px"/>
<Text profile="spuCell" name="c4" position="478px 0px" size="252px 40px" textMaxWidth="244px"/>
<Text profile="spuCellBold" name="c5" position="730px 0px" size="100px 40px" textMaxWidth="92px"/>
</Bitmap>
- Step 2: Toggle + afvisning — in
SmartPickupFlowGuide.luaadd:
-- Start/Stop for ét flow (rækkens knap og vinduets knap). Husker om sidste start blev helt afvist (rød).
function SmartPickupFrame:toggleRun(adEnv, runId)
local run = ADRuns.getRun(ADRunsController.store, runId)
if adEnv == nil or run == nil then return end
self.lastStartFailed = self.lastStartFailed or {}
if ADRunsController.getRunStatus(adEnv, run).canStop then
ADRunsController.stopRun(adEnv, run.id)
self.lastStartFailed[run.id] = nil
self.statusOverride = nil
else
local started, skipped, reasons = self:summarizeStartResults(ADRunsController.startRun(adEnv, run.id))
self.lastStartFailed[run.id] = started == 0 and skipped > 0
local text = string.format(self.i18n:getText("spu_status_started"), started, skipped)
self:showOverride(text, reasons)
if skipped > 0 and ADFieldJobs ~= nil and ADFieldJobs.notify ~= nil then
ADFieldJobs.notify(string.format("%s: %s — %s", tostring(run.name), text, table.concat(reasons, "; ")), started == 0)
end
end
ADRunsController.save()
end
function SmartPickupFrame:flowLevel(adEnv, run)
local row = ADRunsController.getFlowRow(adEnv, run)
local status = ADRunsController.getRunStatus(adEnv, run)
return ADFlowView.flowStatus({running = row.running, total = row.total, stopped = run.stopped == true,
canStop = status.canStop == true, warning = row.warning, reason = row.reason,
lastStartFailed = (self.lastStartFailed or {})[run.id] == true})
end
And make onClickStartStop (Frame.lua) call frame:toggleRun(adEnv, run.id) instead of its inline start/stop body (keep ADRunsController.save() + requestRebuild() there).
- Step 3: List — replace
fillFlowTabRowsbody after the guide guard:
function SmartPickupFrame:fillFlowTabRows(adEnv)
if not self.editPanelOpen and self:hasGuideOn(SmartPickupFrame.TAB_FLOWS) and self:fillGuideRows(adEnv) then return end
self.flowListRows = {}
local i18n = self.i18n
if #ADRunsController.store.runs == 0 then
self:addTextRow(i18n:getText("spu_noRuns"))
return
end
self:addTextRow(i18n:getText("spu_flowList_hint"))
for _, group in ipairs(ADFlowView.boardGroups(ADRunsController.store.runs)) do
self:addTextRow(i18n:getText(group.kind == "outbound" and "spu_board_outbound" or "spu_board_supply"))
for _, run in ipairs(group.runs) do
local row = self:addRow(self.flowListPrefab, {kind = "flowList"})
local entry = {runId = run.id, row = row, dot = row:getDescendantByName("dot"),
cells = {row:getDescendantByName("c1"), row:getDescendantByName("c2"), row:getDescendantByName("c3"),
row:getDescendantByName("c4"), row:getDescendantByName("c5")}}
local runId = run.id
local actionCell = entry.cells[5]
self:makeRowClickable(row, function(frame)
-- klik på [Start]/[Stop]-feltet = handling; ellers åbn vinduet
if frame.lastClickX ~= nil and actionCell.absPosition ~= nil and frame.lastClickX >= actionCell.absPosition[1] then
frame:toggleRun(frame.getAdEnv(), runId)
frame:requestRebuild()
else
frame.selectedRunId = runId
frame:openEditPanel("flow", runId)
end
end)
table.insert(self.flowListRows, entry)
end
end
self:refreshFlowListRows(adEnv)
end
function SmartPickupFrame:refreshFlowListRows(adEnv)
local unpackFn = table.unpack or unpack
for _, entry in ipairs(self.flowListRows or {}) do
local run = ADRuns.getRun(ADRunsController.store, entry.runId)
if run ~= nil then
local cells = self:flowCells(adEnv, run)
local level = self:flowLevel(adEnv, run)
local color = ADFlowView.LEVEL_COLORS[level]
local canStop = ADRunsController.getRunStatus(adEnv, run).canStop
entry.cells[1]:setText(tostring(run.name))
entry.cells[2]:setText(tostring(cells[2]))
entry.cells[3]:setText(tostring(cells[5]))
entry.cells[4]:setText(tostring(cells[6]))
entry.cells[5]:setText(self.i18n:getText(canStop and "spu_flowList_stop" or "spu_flowList_start"))
if entry.dot ~= nil and entry.dot.setImageColor ~= nil then entry.dot:setImageColor(nil, unpackFn(color)) end
if entry.cells[4].setTextColor ~= nil then entry.cells[4]:setTextColor(unpackFn(color)) end
end
end
end
In makeRowClickable (Task 2's function) store the click x before setting pendingClick: frame.lastClickX = posX.
- Step 4:
refreshFlowTab— replace body:
function SmartPickupFrame:refreshFlowTab(adEnv)
if not self.editPanelOpen and self:hasGuideOn(SmartPickupFrame.TAB_FLOWS) then
self:setStatusText(self.statusOverride or "")
return
end
self:refreshFlowListRows(adEnv)
local running, total = 0, 0
for _, run in ipairs(ADRunsController.store.runs) do
total = total + 1
if ADRunsController.getRunStatus(adEnv, run).running > 0 then running = running + 1 end
end
self:setStatusText(self.statusOverride or string.format(self.i18n:getText("spu_flowList_status"), running, total))
end
- Step 5: Buttons + panel kind — in
initFlowButtons:self.flowButtons = {self.backButtonInfo, self.flowNewButtonInfo}. Register:
SmartPickupFrame.EDIT_PANEL_KINDS.flow = {
canDelete = true,
exists = function(frame, id) return ADRuns.getRun(ADRunsController.store, id) ~= nil end,
title = function(frame, id)
local run = ADRuns.getRun(ADRunsController.store, id)
return string.format(frame.i18n:getText("spu_panel_flowTitle"), tostring(run ~= nil and run.name or "?"))
end,
fill = function(frame, adEnv, id)
local run = ADRuns.getRun(ADRunsController.store, id)
if run ~= nil then frame:fillFlowSummary(adEnv, run) end
end,
status = function(frame, adEnv, id)
local run = ADRuns.getRun(ADRunsController.store, id)
return {text = tostring(frame:flowCells(adEnv, run)[6]), level = frame:flowLevel(adEnv, run)}
end,
startStopText = function(frame, adEnv, id)
local run = ADRuns.getRun(ADRunsController.store, id)
return frame.i18n:getText(ADRunsController.getRunStatus(adEnv, run).canStop and "spu_button_stop" or "spu_button_start")
end,
startStop = function(frame, id) frame:toggleRun(frame.getAdEnv(), id) end,
delete = function(frame, id) frame.selectedRunId = id; frame:onClickDelete() end,
}
In fillFlowSummary drop the first heading row (the panel title shows the name): remove the spu_guide_flow_section line. Add "Opsæt flow" as a last link row in the summary: self:addLinkRow(i18n:getText("spu_guide_flow_open"), function(frame) frame.selectedRunId = runId; frame:onClickFlowSetup() end).
After a delete (onClickDelete's YesNo callback path) the run no longer exists → refreshEditPanel closes the panel (Review Focus).
- Step 6: l10n —
spu_flowList_hint("Klik på et flow for at rette det. [Start]/[Stop] styrer flowet direkte." / en),spu_flowList_start("[Start]"),spu_flowList_stop("[Stop]"),spu_flowList_status("Flows i gang: %d af %d" / "Flows running: %d of %d"),spu_panel_flowTitle("Rediger flow: %s" / "Edit flow: %s"). - Step 7: Run all tests + XML check.
- Step 8: Commit
git commit -am "feat(flows): Flows-fanen som farvet liste med Start/Stop pr. række; klik åbner redigeringsvinduet"
Task 5: Etape 1 build + in-game test
- Step 1: bump
modDesc.xml<version>to1.58.0.0; build zip; copy to~/FS25-data/mods/;unzip -p ~/FS25-data/mods/FS25_ADSmartPickup.zip modDesc.xml | grep -o '<version>[^<]*'shows 1.58.0.0. - Step 2: commit + push gitea main.
- Step 3: In-game checklist (brugeren): liste med prikker/farver; [Start]/[Stop] på en række virker og rører ikke andre flows; klik på navnet åbner vinduet; [Edit] → Gem inde i vinduet; Luk og ESC lukker vinduet (ESC med åbent trin lukker trinnet først); klik på listen bagved gør intet; Slet flow fra vinduet lukker det; log uden
GUI-fejl. Ret layout-afvigelser (størrelser/placering) efter screenshot før Task 6.
Task 6: Ren statusfunktion for marker
Files:
- Modify:
FS25_ADSmartPickup/adFieldView.lua - Test:
tests/test_fieldStatus.lua(ny)
Interfaces:
-
Produces:
ADFieldView.fieldStatus(s) -> levelhvors = {hasMarker=bool, working=bool, awaiting=bool, startRequested=bool, reasonKey=string|nil};ADFieldView.ERROR_REASONS(sæt af reason-nøgler). -
Step 1: Test —
tests/test_fieldStatus.lua:
-- Kør: luajit tests/test_fieldStatus.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFieldView.lua")
local V = ADFieldView
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
check("mangler markør", V.fieldStatus({hasMarker = false}), "error")
check("arbejder", V.fieldStatus({hasMarker = true, working = true}), "running")
check("arbejder men venter på maskine", V.fieldStatus({hasMarker = true, working = true, reasonKey = "spu_field_reason_allBusy"}), "waiting")
check("for bred", V.fieldStatus({hasMarker = true, working = true, reasonKey = "spu_field_reason_tooWide"}), "error")
check("ingen maskine", V.fieldStatus({hasMarker = true, reasonKey = "spu_field_reason_noRig"}), "error")
check("regn", V.fieldStatus({hasMarker = true, working = true, reasonKey = "spu_field_reason_rain"}), "waiting")
check("venter på spilleren (høster)", V.fieldStatus({hasMarker = true, awaiting = true}), "waiting")
check("start bestilt", V.fieldStatus({hasMarker = true, startRequested = true}), "waiting")
check("i ro", V.fieldStatus({hasMarker = true}), "stopped")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
(If adFieldView.lua needs other modules loaded, add the same dofiles as tests/test_adFieldView.lua uses.)
- Step 2: Run — FAIL (fieldStatus nil).
- Step 3: Implement — append to
adFieldView.lua:
-- ---- status pr. mark (Fields-listen) ----
-- Årsager der betyder "kan ikke" (rød) — resten er "venter" (gul).
ADFieldView.ERROR_REASONS = {
spu_field_reason_noRig = true, spu_field_reason_cannotDo = true, spu_field_reason_tooWide = true,
spu_field_reason_tooNarrow = true, spu_field_reason_widthUnknown = true, spu_field_reason_noSowFruit = true,
}
-- s = {hasMarker, working, awaiting, startRequested, reasonKey} -> "running"|"waiting"|"error"|"stopped"
function ADFieldView.fieldStatus(s)
if not s.hasMarker then return "error" end
if s.reasonKey ~= nil and ADFieldView.ERROR_REASONS[s.reasonKey] then return "error" end
if s.awaiting or s.startRequested then return "waiting" end
if s.working then return s.reasonKey ~= nil and "waiting" or "running" end
if s.reasonKey ~= nil then return "waiting" end
return "stopped"
end
- Step 4: Run all tests — green.
- Step 5: Commit
git commit -am "feat(fields): ren statusfunktion til Fields-listen"
Task 7: Fields-fanen som farvet liste + "field" i vinduet
Files:
- Modify:
FS25_ADSmartPickup/gui/SmartPickupFrame.xml(fieldPrefab: dot forrest, c7 = handling; kolonner forskydes) - Modify:
FS25_ADSmartPickup/gui/SmartPickupFieldTab.lua(fieldCells,fillFieldRows,refreshFieldRows,onClickFieldStartStoptager id, registerEDIT_PANEL_KINDS.field) - Modify:
FS25_ADSmartPickup/gui/SmartPickupFieldGuide.lua(getFieldButtonsuden Start/Stop når listen vises) - Modify:
l10n/*.xml
Interfaces:
-
Consumes:
ADFieldView.fieldStatus(Task 6);openEditPanel/EDIT_PANEL_KINDS(Task 3);makeRowClickable+frame.lastClickX(Task 2/4);ADFlowView.LEVEL_COLORS(Task 1). -
Produces:
SmartPickupFrame:fieldLevel(id) -> level;SmartPickupFrame:toggleField(id). -
Step 1: Prefab —
fieldPrefabbecomes (≈835 px):
<Bitmap profile="fs25_multiTextOptionContainer" id="fieldPrefab">
<Bitmap profile="spuDot" name="dot" position="20px 0px"/>
<Text profile="spuCellBold" name="c1" position="38px 0px" size="82px 40px" textMaxWidth="76px"/>
<Text profile="spuCell" name="c2" position="120px 0px" size="120px 40px" textMaxWidth="112px"/>
<Text profile="spuCell" name="c3" position="240px 0px" size="100px 40px" textMaxWidth="92px"/>
<Text profile="spuCell" name="c4" position="340px 0px" size="70px 40px" textMaxWidth="62px"/>
<Text profile="spuCell" name="c5" position="410px 0px" size="200px 40px" textMaxWidth="192px"/>
<Text profile="spuCell" name="c6" position="610px 0px" size="120px 40px" textMaxWidth="112px"/>
<Text profile="spuCellBold" name="c7" position="730px 0px" size="100px 40px" textMaxWidth="92px"/>
</Bitmap>
The header row (column titles) passes a 7th title "".
- Step 2: Toggle + level — refactor
onClickFieldStartStopintotoggleField(id)(same body,idparameter instead offrame.selectedFieldId);onClickFieldStartStopcallsself:toggleField(self.selectedFieldId). Add:
function SmartPickupFrame:fieldLevel(id)
local field, info = ADFieldJobs.getField(id), ADFields.getInfo(id)
if field == nil or info == nil then return "stopped" end
local reason = type(field.reason) == "table" and field.reason.key or nil
return ADFieldView.fieldStatus({hasMarker = info.markerId ~= nil, working = ADFieldModel.isWorking(field),
awaiting = ADFieldJobs.awaitingPlayer(id) ~= nil, startRequested = ADFieldJobs.isStartRequested ~= nil and ADFieldJobs.isStartRequested(id),
reasonKey = reason})
end
function SmartPickupFrame:fieldActionText(id)
local field = ADFieldJobs.getField(id)
if ADFieldJobs.awaitingPlayer(id) ~= nil then return self.i18n:getText("spu_field_button_takeover") end
return self.i18n:getText(field ~= nil and ADFieldModel.isWorking(field) and "spu_flowList_stop" or "spu_flowList_start")
end
(If ADFieldJobs.isStartRequested does not exist, leave the guarded call — it evaluates to false.)
- Step 3: List rows — in
fillFieldRows: guide guard getsnot self.editPanelOpen and;fieldCellsappendsself:fieldActionText(info.id)as 7th cell and no longer prefixes"> "; row click:
self:makeRowClickable(cells[1].parent, function(frame)
if frame.lastClickX ~= nil and cells[7] ~= nil and cells[7].absPosition ~= nil and frame.lastClickX >= cells[7].absPosition[1] then
frame:toggleField(fieldId)
else
frame.selectedFieldId = fieldId
frame:openEditPanel("field", fieldId)
end
end)
Remove the trailing fillFieldOverview call from fillFieldRows (it now lives in the panel). In refreshFieldRows after setCells color the dot and c5 by self:fieldLevel(entry.id) (same code as refreshFlowListRows; entry.cells[1].parent:getDescendantByName("dot")).
- Step 4: Panel kind —
SmartPickupFrame.EDIT_PANEL_KINDS.field = {
canDelete = false,
exists = function(frame, id) return ADFields.getInfo(id) ~= nil end,
title = function(frame, id) return string.format(frame.i18n:getText("spu_panel_fieldTitle"), id) end,
fill = function(frame, adEnv, id) frame:fillFieldOverview(adEnv, ADFields.getInfo(id)) end,
status = function(frame, adEnv, id)
return {text = frame:fieldStatusText(ADFieldJobs.getField(id), ADFields.getInfo(id), false), level = frame:fieldLevel(id)}
end,
startStopText = function(frame, adEnv, id) return frame:fieldActionText(id) end,
startStop = function(frame, id) frame:toggleField(id) end,
delete = function() end,
}
fillFieldOverview's own heading row (spu_field_section) is dropped when drawn in the panel: guard it with if not self.editPanelOpen then ... end.
getFieldButtons: list view keeps Back, Opsæt mark (needs selected field → move "Opsæt mark" into the panel as a link row like flows), Flåde; Start/Stop leaves the bottom bar.
- Step 5: l10n —
spu_panel_fieldTitle("Mark F%d" / "Field F%d"). - Step 6: Run all tests + XML check.
- Step 7: Commit
git commit -am "feat(fields): Fields-fanen som farvet liste med Start/Stop pr. række; klik åbner redigeringsvinduet"
Task 8: Etape 2 build + in-game test
- Step 1: bump to
1.59.0.0; build; deploy; verify version in zip; commit + push. - Step 2: In-game checklist: Fields-liste med prikker/farver; [Start]/[Stop]/[Overtag] pr. række; klik åbner vinduet med markens linjer; [Ret] → Gem i vinduet; Flåde-visning uændret; Luk/ESC.
- Step 3: Update memory
project_fs25_adsmartpickup.mdwith the new GUI structure (EditPanel mixin, rowTarget) and the Obsidian noteprojekter/FS25 silo-patches + AD Smart Pickup.mdwith a line about v1.58/1.59.