Files
fs25-adsmartpickup/FS25_ADSmartPickup/gui/SmartPickupFrame.lua
T

970 lines
42 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- Smart Pickup — ESC-fane
-- Viser og redigerer runs og starter/stopper dem. Al logik ligger i ADRunsController;
-- her oversættes kun mellem GUI-elementer og run-felter. Alt kører i pcall så en GUI-fejl
-- aldrig tager spillet med sig.
SmartPickupFrame = {}
SmartPickupFrame._mt = Class(SmartPickupFrame, TabbedMenuFrameElement)
SmartPickupFrame.REFRESH_INTERVAL_MS = 1000
SmartPickupFrame.STATUS_OVERRIDE_TICKS = 6
SmartPickupFrame.MAX_STATUS_REASONS = 3
SmartPickupFrame.LOG_PREFIX = "ADSmartPickup: "
SmartPickupFrame.STATE_OFF, SmartPickupFrame.STATE_ON = 1, 2
SmartPickupFrame.TAB_OVERVIEW, SmartPickupFrame.TAB_FLOWS, SmartPickupFrame.TAB_FIELDS, SmartPickupFrame.TAB_MONTH, SmartPickupFrame.TAB_FIELDSILOS = 1, 2, 3, 4, 5
-- Måned-fanen genopbygges hvert N. sekund (ingen klikbare rækker, så fokus går ikke tabt)
SmartPickupFrame.MONTH_REBUILD_TICKS = 5
-- Marksilo-fanen har klikbare valg: genopbygges sjældnere, så fokus ikke rykker midt i et valg
SmartPickupFrame.FIELDSILO_REBUILD_TICKS = 30
-- Kæder-fanen (gui/SmartPickupChainTab.lua): klikbare forslag/pause, så genopbygning sjældent (som Marksiloer)
SmartPickupFrame.TAB_CHAINS = 10
SmartPickupFrame.CHAIN_REBUILD_TICKS = 30
-- Lagerfaner: én pr. bygningstype (indeks -> kind i ADSources.getFarmInventory)
SmartPickupFrame.TAB_NAMES = {"spu_tab_overview", "spu_tab_flows", "spu_tab_fields", "spu_tab_month", "spu_tab_fieldsilos", "spu_tab_factories", "spu_tab_silos", "spu_tab_barns", "spu_tab_sheds", "spu_tab_chains"}
SmartPickupFrame.TAB_KINDS = {[6] = "factory", [7] = "silo", [8] = "husbandry", [9] = "objectStorage"}
function SmartPickupFrame.getTabKind(tab)
return SmartPickupFrame.TAB_KINDS[tab]
end
SmartPickupFrame.POLICIES = {"off", "store", "sell", "sellElseStore"}
-- Afvisningsgrunde der handler om VOGNEN (ikke run'ets opsætning): traktoren kan ikke vælges.
SmartPickupFrame.VEHICLE_BLOCKING_REASONS = {noTrailer = true, cannotCarry = true, needsAutoload = true, autoloadOn = true}
function SmartPickupFrame.new(i18n, getAdEnv)
local self = SmartPickupFrame:superClass().new(nil, SmartPickupFrame._mt)
self.name = "SmartPickupFrame"
self.i18n = i18n
self.getAdEnv = getAdEnv
self.selectedRunId = nil
self.runIds, self.targetIds, self.poolGroups = {}, {}, {}
self.rowData = {} -- element -> {kind = "ingredient"|"vehicle", ...}
self.clonedRows = {}
self.panelRows = {}
self.refreshTimer = 0
self.statusOverride = nil
self.statusRow = nil -- Text-elementet i den klonede statusrække
self.needsRows = {}
self.pendingRebuild = false
self.frameOpen = false
self.focusLost = false
self.currentTab = SmartPickupFrame.TAB_OVERVIEW
self.subCategoryTabs = {}
self.backButtonInfo = {inputAction = InputAction.MENU_BACK}
self.startButtonInfo = {inputAction = InputAction.MENU_ACTIVATE, text = i18n:getText("spu_button_start"), callback = function() self:onClickStartStop() end}
self.newButtonInfo = {inputAction = InputAction.MENU_EXTRA_1, text = i18n:getText("spu_button_new"), callback = function() self:onClickNew() end}
self.renameButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_button_rename"), callback = function() self:onClickRename() end}
self.deleteButtonInfo = {inputAction = InputAction.MENU_CANCEL, text = i18n:getText("spu_button_delete"), callback = function() self:onClickDelete() end}
-- Oversigt: Start/Stop alle i stedet for knapperne der virker på ét valgt flow.
self.startAllButtonInfo = {inputAction = InputAction.MENU_ACTIVATE, text = i18n:getText("spu_button_startAll"), callback = function() self:onClickStartAll() end}
self.stopAllButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_button_stopAll"), callback = function() self:onClickStopAll() end}
self.monthButtons = {self.backButtonInfo}
-- bygningsfanerne (Siloer/Stalde/Fabrikker/Palleskure): omdøb den valgte bygning
self.renameBuildingButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_button_renameBuilding"), callback = function() self:onClickRenameBuilding() end}
self.storageButtons = {self.backButtonInfo, self.renameBuildingButtonInfo}
self.flowButtons = {self.backButtonInfo, self.startButtonInfo, self.newButtonInfo, self.renameButtonInfo, self.deleteButtonInfo}
-- Stop alt og parkér: flows + markarbejde + marksilo stoppes, køretøjer til parkering, pause til Start alle
self.stopParkButtonInfo = {inputAction = InputAction.MENU_CANCEL, text = i18n:getText("spu_button_stopPark"), callback = function() self:onClickStopAndPark() end}
self.overviewButtons = {self.backButtonInfo, self.startAllButtonInfo, self.stopAllButtonInfo, self.stopParkButtonInfo, self.newButtonInfo}
-- Marker: Start nu / Stop for den valgte mark
-- Marker: [Start nu/Stop], [Opsæt mark], [Markflåde] (gui/SmartPickupFieldGuide.lua); guidens knapper
self:initFieldButtons(i18n)
self:initGuideButtons(i18n)
-- Flows: [Start/Stop] [Opsæt flow] [Nyt flow] [Slet] (gui/SmartPickupFlowGuide.lua)
self:initFlowButtons(i18n)
self:initEditPanelButtons(i18n)
-- Egen reference til listen: fallback'en i refreshButtons skal kunne sende den igen.
self.menuButtonInfo = self.overviewButtons
self:setMenuButtonInfo(self.menuButtonInfo)
return self
end
function SmartPickupFrame:delete()
SmartPickupFrame:superClass().delete(self)
end
function SmartPickupFrame:copyAttributes(src)
SmartPickupFrame:superClass().copyAttributes(self, src)
self.i18n, self.getAdEnv = src.i18n, src.getAdEnv
end
-- Prefabs skal ud af layoutet før de klones, ellers vises de selv som rækker.
function SmartPickupFrame:initialize()
for _, name in ipairs({"ingredientPrefab", "vehiclePrefab", "textPrefab", "productPrefab", "statPrefab", "gridPrefab", "flowPrefab", "flowListPrefab", "fieldPrefab", "fieldListPrefab", "monthPrefab", "kpiPrefab", "alertPrefab", "itemPrefab", "subPrefab"}) do
local prefab = self[name]
if prefab ~= nil then
prefab:unlinkElement()
FocusManager:removeElement(prefab)
end
end
if self.selectorPrefab ~= nil and self.subCategoryBox ~= nil and self.subCategoryPaging ~= nil then
self.selectorPrefab:unlinkElement()
FocusManager:removeElement(self.selectorPrefab)
for index, key in ipairs(SmartPickupFrame.TAB_NAMES) do
self.subCategoryPaging:addText(tostring(index))
local tab = self.selectorPrefab:clone(self.subCategoryBox)
FocusManager:loadElementFromCustomValues(tab)
tab:setText(self.i18n:getText(key))
local background = tab:getDescendantByName("background")
if background ~= nil then
background:setSize(tab.size[1], tab.size[2])
end
local tabIndex = index
tab.onClickCallback = function() self:switchTab(tabIndex) end
self.subCategoryTabs[index] = tab
end
self.subCategoryBox:invalidateLayout()
self.subCategoryPaging:setSize(self.subCategoryBox.maxFlowSize + 140 * g_pixelSizeScaledX)
end
local loopTexts = {self.i18n:getText("spu_loops_infinite")}
for loops = 1, ADRuns.MAX_LOOPS do
table.insert(loopTexts, tostring(loops))
end
self.loopsOption:setTexts(loopTexts)
self.returnBelowSteps = {}
local percentTexts = {}
for percent = ADRuns.MIN_RETURN_BELOW_PERCENT, ADRuns.MAX_RETURN_BELOW_PERCENT, 10 do
table.insert(self.returnBelowSteps, percent)
table.insert(percentTexts, string.format("%d %%", percent))
end
self.returnBelowOption:setTexts(percentTexts)
self.sellAtSteps, self.storeAboveSteps = {}, {}
local sellTexts, storeTexts = {}, {}
for percent = ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT, 5 do
table.insert(self.sellAtSteps, percent)
table.insert(sellTexts, string.format("%d %%", percent))
end
for percent = ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT, 5 do
table.insert(self.storeAboveSteps, percent)
table.insert(storeTexts, string.format("%d %%", percent))
end
if self.sellAtOption ~= nil then self.sellAtOption:setTexts(sellTexts) end
if self.storeAboveOption ~= nil then self.storeAboveOption:setTexts(storeTexts) end
end
-- Indeks i en trin-liste for en procent (største trin <= værdien).
function SmartPickupFrame.getStepIndex(steps, value, default)
local index = 1
for position, percent in ipairs(steps or {}) do
if percent <= (value or default) then
index = position
end
end
return index
end
function SmartPickupFrame:getReturnBelowIndex(run)
local index = 1
for position, percent in ipairs(self.returnBelowSteps or {}) do
if percent <= (run.returnBelowPercent or ADRuns.DEFAULT_RETURN_BELOW_PERCENT) then
index = position
end
end
return index
end
-- FocusManager har skiftet API mellem FS-versioner; her hentes det fokuserede element
-- så blødt som muligt, fordi det kun bruges til at opdage at fokus lige er slettet.
function SmartPickupFrame.getFocusedElement()
if FocusManager == nil then
return nil
end
if FocusManager.getFocusedElement ~= nil then
local ok, element = pcall(FocusManager.getFocusedElement, FocusManager)
if ok then
return element
end
end
local data = FocusManager.currentFocusData
if type(data) == "table" then
return data.focusElement
end
return nil
end
function SmartPickupFrame.isDescendantOf(element, ancestor)
while element ~= nil do
if element == ancestor then
return true
end
element = element.parent
end
return false
end
-- rebuild sletter de rækker der kan have fokus (fx vare-rækkerne når Varer skifter til
-- Auto); uden dette står fokus på et slettet element og tastatur/controller er død.
function SmartPickupFrame:restoreFocusIfLost()
if self.focusLost and self.frameOpen and self.boxLayout ~= nil then
FocusManager:setFocus(self.boxLayout)
end
self.focusLost = false
end
function SmartPickupFrame:onFrameOpen()
SmartPickupFrame:superClass().onFrameOpen(self)
self.statusOverride = nil
self.frameOpen = true
self.pendingRebuild = false
self:safe(self.rebuild)
-- Som AutoDrive's egne sider: fokus på selve listen, ikke på en enkelt række.
self:safe(function(frame)
FocusManager:setFocus(frame.boxLayout)
end)
end
function SmartPickupFrame:onFrameClose()
self.frameOpen = false
-- et åbent redigeringsvindue og en åben guide kasseres (intet er gemt før [Gem])
self:hideEditPanel()
self:closeGuide()
SmartPickupFrame:superClass().onFrameClose(self)
end
-- Genopbygning sker her og aldrig inde i et klik-callback: sletter man elementet midt i
-- dets egen klik-håndtering, hopper værdien frem og tilbage og fokus ryger.
function SmartPickupFrame:update(dt)
SmartPickupFrame:superClass().update(self, dt)
if self.pendingClick ~= nil then
local action = self.pendingClick
self.pendingClick = nil
self:safe(action)
elseif self.pendingRebuild then
self.pendingRebuild = false
self:safe(self.rebuild)
end
self.refreshTimer = self.refreshTimer + dt
if self.refreshTimer >= SmartPickupFrame.REFRESH_INTERVAL_MS then
self.refreshTimer = 0
self:safe(self.refreshLive)
end
end
function SmartPickupFrame:safe(fn, ...)
local ok, err = pcall(fn, self, ...)
if not ok then
Logging.warning(SmartPickupFrame.LOG_PREFIX .. "GUI-fejl: %s", tostring(err))
self.statusOverride = string.format(self.i18n:getText("spu_status_error"), tostring(err))
self:setStatusText(self.statusOverride)
end
end
function SmartPickupFrame:setStatusText(text)
if self.statusRow ~= nil then
self.statusRow:setText(text)
end
end
-- ESC-menuen kalder ikke altid update() på siden, så genopbygningen sker straks. De
-- elementer der udløser den (Run/Mål/Varer-tilstand, knapperne) er statiske og slettes
-- ikke af rebuild, så det er sikkert; pendingRebuild er kun bagstopper.
function SmartPickupFrame:requestRebuild()
self.pendingRebuild = false
self:safe(self.rebuild)
end
function SmartPickupFrame:switchTab(tabIndex)
if self.editPanelOpen then
self:closeGuide()
self:hideEditPanel()
end
if self.guide ~= nil and not self:hasGuideOn(tabIndex) then self:closeGuide() end
self.currentTab = tabIndex
for index, tab in ipairs(self.subCategoryTabs) do
tab:setSelected(index == tabIndex)
end
if self.subCategoryPaging ~= nil then
self.subCategoryPaging:setState(tabIndex, false)
end
self:requestRebuild()
self:safe(function(frame)
FocusManager:setFocus(frame.boxLayout)
end)
end
function SmartPickupFrame:onClickTab(state)
self:switchTab(state)
end
-- De faste rækker: Flow-vælger + Wait-pulje for alle flows; Ture/kør hjem for forsyning; Sælg fra/
-- Kør til lager for udkørsel. Fanen Lagre viser ingen.
function SmartPickupFrame:setStaticRowsVisible(tab, run)
-- Flows-fanen bruger tavle + resumé + guide (v1.32); de faste XML-rækker vises ikke længere
local isFlows = false
local hasRun = isFlows and run ~= nil
local isSupply, isOutbound = hasRun and run.kind ~= "outbound", hasRun and run.kind == "outbound"
local visibility = {
{self.runOption, isFlows},
{self.waitPoolOption, hasRun},
{self.loopsOption, isSupply}, {self.returnBelowOption, isSupply},
{self.sellAtOption, isOutbound}, {self.storeAboveOption, isOutbound},
}
for _, entry in ipairs(visibility) do
local option = entry[1]
if option ~= nil and option.parent ~= nil then
option.parent:setVisible(entry[2])
end
end
end
-- Samme rækkefølge som tavlen: Ind til bygning først, så Ud fra bygning, hver efter navn.
function SmartPickupFrame:getRunsForTab()
if ADFlowView ~= nil then
return ADFlowView.orderedRuns(ADRunsController.store.runs)
end
return ADRunsController.store.runs
end
function SmartPickupFrame:getRun()
return self.selectedRunId ~= nil and ADRuns.getRun(ADRunsController.store, self.selectedRunId) or nil
end
-- ---- opbygning ----
-- addRow melder hver klonet række ind i FocusManager; uden afmelding her ville
-- FocusManager beholde slettede rækker og fokus-navigationen gå i stå.
function SmartPickupFrame:clearRows()
local focused = SmartPickupFrame.getFocusedElement()
for _, row in ipairs(self.clonedRows) do
if focused ~= nil and SmartPickupFrame.isDescendantOf(focused, row) then
self.focusLost = true
end
FocusManager:removeElement(row)
row:delete()
end
self.clonedRows, self.rowData = {}, {}
self.statusRow = nil
self.needsRows = {}
self.overviewRows = {}
self.productRows, self.outboundStatusRows = {}, {}
self.storageRows = {}
self.storageDetailRows = {}
self.itemView = nil
self.boardRows = {}
self.ingredientModeRowOption, self.collectRoundRowOption, self.directionRowOption = nil, nil, nil
end
-- Ja/Nej-række som klon af ingredientPrefab (faste BinaryOption-elementer fra XML'en viser kun
-- første valg grønt i FS25; klonede rækker virker begge veje). texts = {venstre, højre}.
function SmartPickupFrame:addToggleRow(kind, title, texts, isOn)
local row = self:addRow(self.ingredientPrefab, {kind = kind})
local option = row:getDescendantByName("option")
option:setTexts(texts)
option:setState(isOn and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
row:getDescendantByName("title"):setText(title)
return option
end
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()
local focused = SmartPickupFrame.getFocusedElement()
for _, row in ipairs(self.panelRows or {}) do
if focused ~= nil and SmartPickupFrame.isDescendantOf(focused, row) then
self.focusLost = true
end
FocusManager:removeElement(row)
row:delete()
end
self.panelRows = {}
end
-- Tekstrække uden valgmuligheder: sektionstitler, Wait-pulje-linjer og statuslinjen.
-- Tegn pr. tekstrække før teksten ombrydes (rækken afkorter med "..." ved ca. 70 tegn).
SmartPickupFrame.TEXT_ROW_CHARS = 90
-- Lange tekster fordeles på flere rækker i stedet for at blive afkortet. Returnerer første rækkes titel.
function SmartPickupFrame:addTextRow(text)
local first = nil
for _, line in ipairs(ADFlowView.wrapText(text, SmartPickupFrame.TEXT_ROW_CHARS)) do
local title = self:addRow(self.textPrefab, {kind = "text"}):getDescendantByName("title")
title:setText(line)
first = first or title
end
return first
end
-- AutoDrive farver hver række i onCreate; her sker det efter hver genopbygning,
-- fordi rækkerne først findes på det tidspunkt.
function SmartPickupFrame:colorRows()
self:colorLayout(self.boxLayout)
end
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:rebuild()
local adEnv = self.getAdEnv()
local store = ADRunsController.store
if self.selectedRunId == nil or ADRuns.getRun(store, self.selectedRunId) == nil then
self.selectedRunId = store.runs[1] ~= nil and store.runs[1].id or nil
end
self:clearRows()
local run = self:getRun()
self:setStaticRowsVisible(self.currentTab, run)
if SmartPickupFrame.getTabKind(self.currentTab) ~= nil then
if adEnv ~= nil then
self:fillStorageRows(adEnv)
end
self.statusRow = self:addTextRow("")
self:colorRows()
self.boxLayout:invalidateLayout()
self:restoreFocusIfLost()
self:refreshLive()
return
end
if self.currentTab == SmartPickupFrame.TAB_FIELDS then
if adEnv ~= nil then
self:fillFieldRows(adEnv)
end
self.statusRow = self:addTextRow("")
self:colorRows()
self.boxLayout:invalidateLayout()
if self.editPanelOpen then self:fillEditPanel(adEnv) end
self:refreshLive()
self:restoreFocusIfLost()
return
end
if self.currentTab == SmartPickupFrame.TAB_FIELDSILOS then
if adEnv ~= nil then
self:fillFieldSiloRows(adEnv)
end
self.monthRebuildTicks = SmartPickupFrame.FIELDSILO_REBUILD_TICKS
self:colorRows()
self.boxLayout:invalidateLayout()
self:refreshButtons()
self:restoreFocusIfLost()
return
end
if self.currentTab == SmartPickupFrame.TAB_CHAINS then
if adEnv ~= nil then
self:fillChainRows(adEnv)
end
self.statusRow = self:addTextRow(self.statusOverride or "")
self.monthRebuildTicks = SmartPickupFrame.CHAIN_REBUILD_TICKS
self:colorRows()
self.boxLayout:invalidateLayout()
self:refreshButtons()
self:restoreFocusIfLost()
return
end
if self.currentTab == SmartPickupFrame.TAB_MONTH then
if adEnv ~= nil then
self:fillMonthRows(adEnv)
end
self.monthRebuildTicks = SmartPickupFrame.MONTH_REBUILD_TICKS
self:colorRows()
self.boxLayout:invalidateLayout()
self:refreshButtons()
self:restoreFocusIfLost()
return
end
if self.currentTab == SmartPickupFrame.TAB_OVERVIEW then
if adEnv ~= nil then
self:fillOverviewRows(adEnv)
end
self.statusRow = self:addTextRow("")
self:addTextRow(self.i18n:getText("spu_waitPools"))
self:fillPoolRows(adEnv)
self:colorRows()
self.boxLayout:invalidateLayout()
self:refreshLive()
self:restoreFocusIfLost()
return
end
if adEnv ~= nil then
self:fillFlowTabRows(adEnv)
end
self.statusRow = self:addTextRow("")
self:colorRows()
self.boxLayout:invalidateLayout()
if self.editPanelOpen then self:fillEditPanel(adEnv) end
self:refreshLive()
self:restoreFocusIfLost()
end
-- Overblik: "Cow 1 food: TMR 18.0k/27.8k · Silage 3.0k/…" (mest trængende først)
function SmartPickupFrame:describeTargetLevels(target)
if #target.levels == 0 then
return string.format("%s: %s", target.name, self.i18n:getText("spu_needs_none"))
end
return string.format("%s: %s", target.name, self:formatLevels(target.levels))
end
-- Felterne følger modellen: når et klik ikke er nået ind i run'et, springer feltet tilbage,
-- og loggen ("run '…': waitPoolGroup=…") viser om klikket blev registreret.
function SmartPickupFrame:syncOptionsFromModel(adEnv, run)
local function sync(option, wantedIndex)
if option ~= nil and wantedIndex ~= nil and SmartPickupFrame.getOptionState(option) ~= wantedIndex then
option:setState(wantedIndex, false)
end
end
local poolIndex = 1
for index, group in ipairs(self.poolGroups) do
if group == run.waitPoolGroup then poolIndex = index end
end
sync(self.waitPoolOption, poolIndex)
sync(self.directionRowOption, run.kind == "outbound" and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF)
if run.kind == "outbound" then
sync(self.sellAtOption, SmartPickupFrame.getStepIndex(self.sellAtSteps, run.sellAtPercent, ADRuns.DEFAULT_SELL_AT_PERCENT))
sync(self.storeAboveOption, SmartPickupFrame.getStepIndex(self.storeAboveSteps, run.storeAbovePercent, ADRuns.DEFAULT_STORE_ABOVE_PERCENT))
sync(self.collectRoundRowOption, run.collectRound and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF)
return
end
sync(self.ingredientModeRowOption, run.ingredientMode == "manual" and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF)
sync(self.loopsOption, run.loops + 1)
sync(self.returnBelowOption, self:getReturnBelowIndex(run))
end
-- "Silage 0/33.750 l · Straw 21.375/21.375 l" — mest trængende først.
-- Kompakte tal, så 4–5 varer kan stå på én række: 18.0k/27.8k
function SmartPickupFrame.formatCompact(liters)
if liters >= 1000000 then
return string.format("%.1fM", liters / 1000000)
elseif liters >= 1000 then
return string.format("%.1fk", liters / 1000)
end
return string.format("%d", liters)
end
function SmartPickupFrame:formatLevels(levels)
local parts = {}
for _, need in ipairs(levels) do
local title = g_fillTypeManager.getFillTypeTitleByIndex ~= nil and g_fillTypeManager:getFillTypeTitleByIndex(need.fillType) or g_fillTypeManager:getFillTypeNameByIndex(need.fillType)
table.insert(parts, string.format("%s %s/%s", tostring(title), SmartPickupFrame.formatCompact(need.level), SmartPickupFrame.formatCompact(need.capacity)))
end
return table.concat(parts, " · ")
end
-- ---- overblik (øverst): ét afsnit pr. run — status, traktorernes aktivitet, målets niveauer ----
function SmartPickupFrame:describeActivity(adEnv, vehicle)
local activity = ADRunsController.getVehicleActivity(adEnv, vehicle)
local label = ADRunsController.getVehicleLabel(adEnv, vehicle)
local fillTypeName = activity.fillType ~= nil and (g_fillTypeManager.getFillTypeTitleByIndex ~= nil and g_fillTypeManager:getFillTypeTitleByIndex(activity.fillType) or g_fillTypeManager:getFillTypeNameByIndex(activity.fillType)) or "?"
local place = tostring(activity.place or "?")
if activity.kind == "stopped" then
return string.format("%s: %s", label, self.i18n:getText("spu_activity_stopped"))
elseif activity.kind == "waiting" then
return string.format("%s: %s", label, string.format(self.i18n:getText("spu_activity_waiting"), place, g_i18n:formatNumber(activity.liters or 0, 0), fillTypeName))
elseif activity.kind == "loaded" then
return string.format("%s: %s", label, string.format(self.i18n:getText("spu_activity_loaded"), g_i18n:formatNumber(activity.liters or 0, 0), fillTypeName, place))
elseif activity.kind == "holding" then
return string.format("%s: %s", label, string.format(self.i18n:getText("spu_activity_holding"), place, tostring(activity.reason or "")))
end
if activity.fillType ~= nil then
return string.format("%s: %s", label, string.format(self.i18n:getText("spu_activity_empty"), place, fillTypeName))
end
return string.format("%s: %s", label, string.format(self.i18n:getText("spu_activity_driving"), place))
end
function SmartPickupFrame:fillPoolRows(adEnv)
if adEnv == nil then
return
end
local occupied = {}
for _, claim in ipairs(ADUnloadWait.getClaims()) do
occupied[claim.marker.group] = occupied[claim.marker.group] or {}
-- kun løbenummeret ("#31") — fulde navne klippes af i rækken
local label = ADRunsController.getVehicleLabel(adEnv, claim.vehicle)
table.insert(occupied[claim.marker.group], string.match(label, "%(#(%d+)%)") and ("#" .. string.match(label, "%(#(%d+)%)")) or label)
end
for _, pool in ipairs(ADRunsController.getWaitPools(adEnv)) do
local names = occupied[pool.group] or {}
local suffix = #names > 0 and (" (" .. table.concat(names, ", ") .. ")") or ""
self:addTextRow(string.format(self.i18n:getText("spu_waitPoolRow"), pool.group, pool.count, #names, suffix))
end
end
-- BinaryOption har :getState() i FS25, men feltet .state i ældre GUI-versioner.
function SmartPickupFrame.getOptionState(option)
return option.getState ~= nil and option:getState() or option.state
end
-- setMenuButtonInfoDirty findes i FS22/25's TabbedMenuFrameElement; mangler den,
-- sendes listen igen. Kaldes hvert sekund, så den må aldrig kaste.
function SmartPickupFrame:refreshButtons()
local wanted = self.flowButtons
if self.editPanelOpen then
wanted = self.guide ~= nil and self:panelGuideButtons() or self:getPanelButtons()
elseif self:hasGuideOn(self.currentTab) then
wanted = self:getGuideButtons()
elseif self.currentTab == SmartPickupFrame.TAB_OVERVIEW then
wanted = self.overviewButtons
elseif self.currentTab == SmartPickupFrame.TAB_FIELDS then
wanted = self:getFieldButtons()
elseif self.currentTab == SmartPickupFrame.TAB_MONTH or self.currentTab == SmartPickupFrame.TAB_FIELDSILOS
or self.currentTab == SmartPickupFrame.TAB_CHAINS then
wanted = self.monthButtons
elseif SmartPickupFrame.getTabKind(self.currentTab) ~= nil then
wanted = self.storageButtons
self.renameBuildingButtonInfo.disabled = self.storageDetailPlaceable == nil
end
if self.menuButtonInfo ~= wanted then
self.menuButtonInfo = wanted
self:setMenuButtonInfo(wanted)
end
if self.setMenuButtonInfoDirty ~= nil then
self:setMenuButtonInfoDirty()
else
self:setMenuButtonInfo(self.menuButtonInfo)
end
end
-- Status + Start/Stop-knappens tekst, kaldes hvert sekund.
function SmartPickupFrame:refreshLive()
local adEnv = self.getAdEnv()
local run = self:getRun()
if adEnv == nil then
self:setStatusText(self.i18n:getText("spu_status_noAd"))
return
end
if self.currentTab == SmartPickupFrame.TAB_MONTH or self.currentTab == SmartPickupFrame.TAB_FIELDSILOS
or self.currentTab == SmartPickupFrame.TAB_CHAINS then
if self.currentTab == SmartPickupFrame.TAB_CHAINS then
-- "forslaget gælder ikke længere" står STATUS_OVERRIDE_TICKS sekunder
self.statusOverrideTicks = (self.statusOverrideTicks or 0) - 1
if self.statusOverrideTicks <= 0 then self.statusOverride = nil end
self:setStatusText(self.statusOverride or "")
end
self.monthRebuildTicks = (self.monthRebuildTicks or 0) - 1
if self.monthRebuildTicks <= 0 then self:requestRebuild() end
self:refreshButtons()
return
end
local isOverview = self.currentTab == SmartPickupFrame.TAB_OVERVIEW
if isOverview then
self:refreshOverview(adEnv)
end
if self.editPanelOpen then self:refreshEditPanel(adEnv) end
if self.currentTab == SmartPickupFrame.TAB_FIELDS then
self:refreshFieldRows(adEnv)
self.statusOverrideTicks = (self.statusOverrideTicks or 0) - 1
if self.statusOverrideTicks <= 0 then self.statusOverride = nil end
self:refreshButtons()
return
end
if self.currentTab == SmartPickupFrame.TAB_FLOWS then
self:refreshFlowTab(adEnv)
self.statusOverrideTicks = (self.statusOverrideTicks or 0) - 1
if self.statusOverrideTicks <= 0 then self.statusOverride = nil end
self:refreshButtons()
return
end
self:refreshStorageRows(adEnv)
local isRunning = false
if isOverview then
local running, total, activeRuns = 0, 0, 0
for _, anyRun in ipairs(ADRunsController.store.runs) do
local status = ADRunsController.getRunStatus(adEnv, anyRun)
running, total = running + status.running, total + status.total
if status.running > 0 then activeRuns = activeRuns + 1 end
end
local statusText = string.format(self.i18n:getText("spu_status_overview"), running, total, activeRuns, #ADRunsController.store.runs)
local queued = #(ADRunsController.startQueue or {})
if queued > 0 then
local totalQueued = ADRunsController.startQueueTotal or queued
statusText = string.format(self.i18n:getText("spu_status_starting"), totalQueued - queued, totalQueued)
elseif ADFieldJobs ~= nil and ADFieldJobs.state ~= nil and ADFieldJobs.state.paused then
statusText = self.i18n:getText("spu_status_paused")
end
self:setStatusText(self.statusOverride or statusText)
self.stopAllButtonInfo.disabled = running == 0
elseif SmartPickupFrame.getTabKind(self.currentTab) ~= nil then
-- lagerfanerne: ingen flow-status (in-game 29/9: 'Stoppet (1 traktorer)' fra det sidst valgte flow)
self:setStatusText(self.statusOverride or "")
elseif run ~= nil then
self:syncOptionsFromModel(adEnv, run)
if run.kind == "outbound" then
self:refreshOutboundRows(adEnv, run)
else
self:refreshNeedsRows(adEnv, run)
end
local status = ADRunsController.getRunStatus(adEnv, run)
isRunning = status.canStop == true
local text = status.running > 0 and string.format(self.i18n:getText("spu_status_running"), status.running, status.total)
or string.format(self.i18n:getText("spu_status_stopped"), status.total)
self:setStatusText(self.statusOverride or text)
for option, data in pairs(self.rowData) do
-- kun run'ets egne traktorer opdateres live (alle valideres ved rebuild)
local isInRun = SmartPickupFrame.getOptionState(option) == SmartPickupFrame.STATE_ON
if data.kind == "vehicle" and data.row ~= nil and isInRun then
data.row:getDescendantByName("title"):setText(self:describeVehicle(adEnv, run, data.vehicle, true))
end
end
else
self:setStatusText(self.statusOverride or self.i18n:getText("spu_noRuns"))
end
-- Engangsbesked ("Startet 2, sprunget over 1: …", "flyttet fra ..."): står i
-- STATUS_OVERRIDE_TICKS opdateringer (sekunder), så man når at læse hvorfor.
self.statusOverrideTicks = (self.statusOverrideTicks or 0) - 1
if self.statusOverrideTicks <= 0 then
self.statusOverride = nil
end
self.startButtonInfo.text = self.i18n:getText(isRunning and "spu_button_stop" or "spu_button_start")
self.startButtonInfo.disabled = run == nil
self.renameButtonInfo.disabled = run == nil
self.deleteButtonInfo.disabled = run == nil
self:refreshButtons()
end
-- ---- callbacks fra XML (state = 1-baseret indeks; element = det klikkede element) ----
function SmartPickupFrame:onClickRun(state)
self.selectedRunId = self.runIds[state]
self.statusOverride = nil
self:requestRebuild()
end
function SmartPickupFrame:onClickIngredient(state, element)
-- guiderne og markflåden deler prefab (og dermed onClick) med vare-rækkerne
local data = self.rowData[element]
if data ~= nil and data.kind == "guideRow" then
return self:onClickGuideRow(state, data)
elseif data ~= nil and SmartPickupFrame.FIELDSILO_ROW_KINDS ~= nil and SmartPickupFrame.FIELDSILO_ROW_KINDS[data.kind] then
return self:onClickFieldSiloRow(state, data)
elseif data ~= nil and SmartPickupFrame.FIELD_ROW_KINDS[data.kind] then
return self:onClickFieldRow(state, data)
end
self:safe(function(frame)
local run, data = frame:getRun(), frame.rowData[element]
if run == nil or data == nil or data.kind ~= "ingredient" then return end
local names = {}
for _, name in ipairs(run.ingredients) do
if name ~= data.name then table.insert(names, name) end
end
if state == SmartPickupFrame.STATE_ON then
table.insert(names, data.name)
end
ADRunsController.updateRun(run.id, {ingredients = names})
end)
end
function SmartPickupFrame:onClickWaitPool(state)
self:safe(function(frame)
local run = frame:getRun()
if run == nil then return end
ADRunsController.updateRun(run.id, {waitPoolGroup = frame.poolGroups[state] or ""})
end)
end
function SmartPickupFrame:onClickVehicle(state, element)
local clicked = self.rowData[element]
-- markfanens maskinrækker (markflåden og markens faste maskiner) bruger samme række-type som flowets
-- traktorliste; de hører til markfanen (fieldRig blev før behandlet som flow-traktor og aldrig gemt)
if clicked ~= nil and clicked.kind == "fleetVehicle" then
return self:onClickFieldRow(state, clicked)
end
if clicked ~= nil and clicked.kind == "guideRow" then
return self:onClickGuideRow(state, clicked)
end
self:safe(function(frame)
local run, data = frame:getRun(), frame.rowData[element]
-- kun flowets traktorrækker (de har et køretøj); alt andet ignoreres
if run == nil or data == nil or data.locked or data.vehicle == nil then return end
if state == SmartPickupFrame.STATE_ON then
local previousRunId = ADRunsController.assignVehicle(run.id, data.vehicle)
if previousRunId ~= nil then
local previous = ADRuns.getRun(ADRunsController.store, previousRunId)
frame.statusOverride = string.format(frame.i18n:getText("spu_movedFrom"), previous ~= nil and previous.name or tostring(previousRunId))
frame.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS
end
else
ADRunsController.unassignVehicle(data.vehicle)
end
frame:refreshLive()
end)
end
-- ---- knapper i bundlinjen ----
function SmartPickupFrame:onClickStartStop()
self:safe(function(frame)
local adEnv, run = frame.getAdEnv(), frame:getRun()
if adEnv == nil or run == nil then return end
frame:toggleRun(adEnv, run.id)
frame:requestRebuild()
end)
end
-- Start-resultater -> antal startet/sprunget over + "Traktor: grund"-liste til statuslinjen.
function SmartPickupFrame:summarizeStartResults(results)
local started, skipped, reasons = 0, 0, {}
for _, result in ipairs(results) do
if result.ok then
started = started + 1
else
skipped = skipped + 1
local reasonText = self.i18n:getText("spu_reason_" .. tostring(result.reason))
if result.reason == "noMarker" and result.missingBuilding ~= nil then
reasonText = string.format(reasonText, tostring(result.missingBuilding))
end
table.insert(reasons, string.format("%s: %s", tostring(ADRuns.vehicleLabel(result.vehicle)), reasonText))
end
end
return started, skipped, reasons
end
function SmartPickupFrame:showOverride(text, reasons)
if reasons ~= nil and #reasons > 0 then
-- én statuslinje: højst MAX_STATUS_REASONS grunde, resten står i log.txt
local shown = {}
for index = 1, math.min(#reasons, SmartPickupFrame.MAX_STATUS_REASONS) do shown[index] = reasons[index] end
text = text .. " — " .. table.concat(shown, "; ")
if #reasons > #shown then text = text .. string.format(" (+%d)", #reasons - #shown) end
end
self.statusOverride = text
self.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS
self:setStatusText(text)
end
function SmartPickupFrame:onClickStartAll()
self:safe(function(frame)
local adEnv = frame.getAdEnv()
if adEnv == nil then return end
-- pausen (Stop alt og parkér) ophæves; traktorerne startes én ad gangen med mellemrum
if ADFieldJobs ~= nil and ADFieldJobs.state ~= nil and ADFieldJobs.state.paused then ADFieldJobs.setPaused(false) end
local summary = ADRunsController.queueStartAll(adEnv)
local _, skipped, reasons = frame:summarizeStartResults(summary.skipped)
Logging.info(SmartPickupFrame.LOG_PREFIX .. "Start alle: %d traktorer i kø (én hvert %d. sekund), %d flows kørte allerede, %d sprunget over",
summary.queued, math.floor(ADRunsController.STAGGER_MS / 1000), summary.alreadyRunning, skipped)
frame:showOverride(string.format(frame.i18n:getText("spu_status_queuedAll"), summary.queued, math.floor(ADRunsController.STAGGER_MS / 1000), summary.alreadyRunning, skipped), reasons)
ADRunsController.save()
frame:requestRebuild()
end)
end
function SmartPickupFrame:onClickStopAndPark()
self:safe(function(frame)
local adEnv = frame.getAdEnv()
if adEnv == nil then return end
local summary = ADRunsController.stopAllAndPark(adEnv)
local text = string.format(frame.i18n:getText("spu_status_stopParked"), summary.stopped, summary.parked)
local notes = {}
if #summary.noPark > 0 then table.insert(notes, string.format(frame.i18n:getText("spu_status_noPark"), table.concat(summary.noPark, ", "))) end
if #summary.manual > 0 then table.insert(notes, string.format(frame.i18n:getText("spu_status_fetchManual"), table.concat(summary.manual, ", "))) end
frame:showOverride(text, notes)
ADRunsController.save()
frame:requestRebuild()
end)
end
function SmartPickupFrame:onClickStopAll()
self:safe(function(frame)
local adEnv = frame.getAdEnv()
if adEnv == nil then return end
local stopped = ADRunsController.stopAllRuns(adEnv)
Logging.info(SmartPickupFrame.LOG_PREFIX .. "Stop alle: %d traktorer stoppet", stopped)
frame:showOverride(string.format(frame.i18n:getText("spu_status_stoppedAll"), stopped))
ADRunsController.save()
frame:requestRebuild()
end)
end
-- Klik på en række i Oversigt: vælg flowet og hop til fanen Flows (kaldes fra update, aldrig inde i
-- rækkens eget mouseEvent — rækken slettes af genopbygningen).
function SmartPickupFrame:openFlow(runId)
if ADRuns.getRun(ADRunsController.store, runId) == nil then return end
self.selectedRunId = runId
self.statusOverride = nil
self:switchTab(SmartPickupFrame.TAB_FLOWS)
end
-- Nyt flow: guiden "Opsæt flow" (gui/SmartPickupFlowGuide.lua) — intet oprettes før [Gem].
function SmartPickupFrame:onClickNew()
self:onClickFlowNew()
end
-- Omdøb den valgte bygning i en bygningsfane (gemmes i ADSmartPickup_names.xml; tomt = butiksnavnet igen)
function SmartPickupFrame:onClickRenameBuilding()
self:safe(function(frame)
local placeable = frame.storageDetailPlaceable
if placeable == nil then return end
frame.renamingBuildingId = ADBuildings.getId(placeable)
local current = ADNames.forPlaceable(placeable) or (placeable.getName ~= nil and placeable:getName()) or ""
TextInputDialog.show(SmartPickupFrame.onRenameBuildingResult, frame, tostring(current), frame.i18n:getText("spu_renameBuildingTitle"), nil, 40, g_i18n:getText("button_ok"))
end)
end
function SmartPickupFrame:onRenameBuildingResult(value, clickOk)
self:safe(function(frame)
if not clickOk or frame.renamingBuildingId == nil then return end
ADNames.set(frame.renamingBuildingId, value)
frame.renamingBuildingId = nil
frame:requestRebuild()
end)
end
function SmartPickupFrame:onClickRename()
-- Dialogen kan kaste (fx manglende i18n-nøgle) — fang det som resten af GUI'et, i stedet
-- for at lade fejlen undslippe klik-callbacken uden statuslinje-besked.
self:safe(function(frame)
local run = frame:getRun()
if run == nil then return end
TextInputDialog.show(SmartPickupFrame.onRenameResult, frame, run.name, frame.i18n:getText("spu_renameTitle"), nil, 30, g_i18n:getText("button_ok"))
end)
end
function SmartPickupFrame:onRenameResult(value, clickOk)
self:safe(function(frame)
local run = frame:getRun()
if clickOk and run ~= nil and value ~= nil and value:match("%S") then
ADRunsController.updateRun(run.id, {name = value})
frame:requestRebuild()
end
end)
end
function SmartPickupFrame:onClickDelete()
-- Samme begrundelse som onClickRename: dialog-opkaldet skal ikke kunne undslippe uden om safe().
self:safe(function(frame)
local run = frame:getRun()
if run == nil then return end
YesNoDialog.show(SmartPickupFrame.onDeleteResult, frame, string.format(frame.i18n:getText("spu_deleteConfirm"), run.name), frame.i18n:getText("spu_button_delete"))
end)
end
function SmartPickupFrame:onDeleteResult(yes)
self:safe(function(frame)
local run = frame:getRun()
if yes and run ~= nil then
ADRunsController.deleteRun(run.id, frame.getAdEnv())
frame.selectedRunId = nil
frame:requestRebuild()
end
end)
end