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

969 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 = 1, 2, 3
-- Lagerfaner: én pr. bygningstype (indeks -> kind i ADSources.getFarmInventory)
SmartPickupFrame.TAB_NAMES = {"spu_tab_overview", "spu_tab_flows", "spu_tab_fields", "spu_tab_factories", "spu_tab_silos", "spu_tab_barns", "spu_tab_sheds"}
SmartPickupFrame.TAB_KINDS = {[4] = "factory", [5] = "silo", [6] = "husbandry", [7] = "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}
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.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.flowButtons = {self.backButtonInfo, self.startButtonInfo, self.newButtonInfo, self.renameButtonInfo, self.deleteButtonInfo}
self.overviewButtons = {self.backButtonInfo, self.startAllButtonInfo, self.stopAllButtonInfo, self.newButtonInfo}
-- Marker: Start nu / Stop for den valgte mark
self.fieldStartButtonInfo = {inputAction = InputAction.MENU_ACTIVATE, text = i18n:getText("spu_field_button_start"), callback = function() self:onClickFieldStartStop() end}
self.fieldButtons = {self.backButtonInfo, self.fieldStartButtonInfo}
-- 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", "fieldPrefab"}) 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
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)
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)
local isFlows = tab == SmartPickupFrame.TAB_FLOWS
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.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 row = prefab:clone(self.boxLayout)
FocusManager:loadElementFromCustomValues(row)
table.insert(self.clonedRows, 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
-- 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 = 66
-- 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()
local colors = InGameMenuSettingsFrame ~= nil and InGameMenuSettingsFrame.COLOR_ALTERNATING or nil
if colors == nil or self.boxLayout == nil then
return
end
local unpackFn = table.unpack or unpack
local isEvenRow = false
for _, element in ipairs(self.boxLayout.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()
self:refreshLive()
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
self:fillRunOption(adEnv)
local hasRun = run ~= nil and adEnv ~= nil
for _, element in ipairs({self.waitPoolOption, self.loopsOption, self.returnBelowOption, self.sellAtOption, self.storeAboveOption}) do
if element ~= nil then
element:setDisabled(not hasRun)
end
end
if hasRun then
self:fillWaitPoolOption(adEnv, run)
self:rebuildFlowDetail(adEnv, run)
self:addTextRow(self.i18n:getText("spu_section_vehicles"))
self:fillVehicleRows(adEnv, run)
self:addTextRow(self.i18n:getText("spu_section_status"))
end
self.statusRow = self:addTextRow("")
self:colorRows()
self.boxLayout:invalidateLayout()
self:refreshLive()
self:restoreFocusIfLost()
end
function SmartPickupFrame:fillRunOption(adEnv)
local texts, index = {}, 1
self.runIds = {}
for position, run in ipairs(self:getRunsForTab()) do
local status = adEnv ~= nil and ADRunsController.getRunStatus(adEnv, run) or {running = 0, total = #run.vehicleIds}
table.insert(texts, string.format("%s (%d/%d)", run.name, status.running, status.total))
table.insert(self.runIds, run.id)
if run.id == self.selectedRunId then
index = position
end
end
if #texts == 0 then
texts = {self.i18n:getText("spu_noRuns")}
end
self.runOption:setTexts(texts)
self.runOption:setState(index, false)
self.runOption:setDisabled(#self.runIds == 0)
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
function SmartPickupFrame:fillWaitPoolOption(adEnv, run)
local texts, index = {self.i18n:getText("spu_waitPool_all")}, 1
self.poolGroups = {""}
for _, pool in ipairs(ADRunsController.getWaitPools(adEnv)) do
table.insert(texts, string.format("%s (%d)", pool.group, pool.count))
table.insert(self.poolGroups, pool.group)
if pool.group == run.waitPoolGroup then
index = #texts
end
end
self.waitPoolOption:setTexts(texts)
self.waitPoolOption:setState(index, false)
end
-- Traktorer: som standard flowets egne og de ledige der kan bruges; "Vis alle" tilføjer traktorer i andre
-- flows og låste (vognen passer ikke) — de er tit mange og gør listen uoverskuelig.
function SmartPickupFrame:fillVehicleRows(adEnv, run)
local inRun = {}
for _, id in ipairs(run.vehicleIds) do
inRun[id] = true
end
local entries = {}
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
local id = ADRunsController.getVehicleId(vehicle)
local otherRun = ADRunsController.getRunForVehicle(vehicle)
local state = "free"
if inRun[id] then
state = "in"
elseif otherRun ~= nil and otherRun.id ~= run.id then
state = "other"
elseif SmartPickupFrame.VEHICLE_BLOCKING_REASONS[ADRunsController.validateVehicle(adEnv, run, vehicle).reason] then
state = "blocked"
end
table.insert(entries, {id = id, label = ADRunsController.getVehicleLabel(adEnv, vehicle), state = state, vehicle = vehicle, otherRun = otherRun})
end
local rows = ADFlowView.vehicleRows(entries, self.showAllVehicles == true)
local hidden = ADFlowView.hiddenCount(entries, rows)
if hidden > 0 or self.showAllVehicles then
self.showAllVehiclesOption = self:addToggleRow("showAllVehicles", string.format(self.i18n:getText("spu_showAllVehicles"), hidden), {self.i18n:getText("ui_no"), self.i18n:getText("ui_yes")}, self.showAllVehicles == true)
end
for _, entry in ipairs(rows) do
local vehicle = entry.vehicle
local row = self:addRow(self.vehiclePrefab, {kind = "vehicle", vehicle = vehicle, row = nil})
local option, title = row:getDescendantByName("option"), row:getDescendantByName("title")
self.rowData[option].row = row
option:setState(entry.state == "in" and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
if entry.state == "other" then
-- én traktor = ét flow; rækken viser hvor den er
option:setDisabled(true)
self.rowData[option].locked = true
title:setText(string.format("%s — %s", entry.label, string.format(self.i18n:getText("spu_inRun"), tostring(entry.otherRun.name))))
else
title:setText(self:describeVehicle(adEnv, run, vehicle, entry.state == "in"))
if entry.state == "blocked" then
option:setDisabled(true)
self.rowData[option].locked = true
end
end
end
end
-- "Fastrac 2135 4WS (#1) — 1.000.000 l — ✔" / "… — ✖ ingen vogn"; reservationsnote fra Start bevares.
-- "Fastrac 2135 4WS (#1) — 1.000.000 l — kan bære: Halm, TMR — ✔" / "… — ✖ ingen vogn".
-- Vises for ALLE traktorer, så man kan se om vognen passer, før man siger Ja.
function SmartPickupFrame:describeVehicle(adEnv, run, vehicle, isInRun)
local label = ADRunsController.getVehicleLabel(adEnv, vehicle)
local validation = ADRunsController.validateVehicle(adEnv, run, vehicle)
local parts = {label}
if validation.isAutoload and validation.capacity == nil then
table.insert(parts, self.i18n:getText("spu_capacity_pallets"))
elseif validation.capacity ~= nil then
table.insert(parts, string.format(self.i18n:getText("spu_capacity"), g_i18n:formatNumber(validation.capacity, 0)))
local carriable = self:describeCarriable(adEnv, run, vehicle, validation.carriable)
if carriable ~= nil then
table.insert(parts, carriable)
end
end
if validation.ok then
table.insert(parts, "OK")
else
local reason = self.i18n:getText("spu_reason_" .. tostring(validation.reason))
if validation.reason == "noMarker" and validation.missingBuilding ~= nil then
reason = string.format(reason, tostring(validation.missingBuilding))
end
table.insert(parts, "X " .. reason)
end
return table.concat(parts, " — ")
end
-- Hvilke af run'ets varer vognen kan bære (titler); nil når ingen.
function SmartPickupFrame:describeCarriable(adEnv, run, vehicle, carriable)
local canCarry = {}
for _, fillType in ipairs(carriable or {}) do
canCarry[fillType] = true
end
local titles = {}
for _, fillType in ipairs(ADRunsController.getRunIngredients(adEnv, run, vehicle)) do
if canCarry[fillType] then
local title = g_fillTypeManager.getFillTypeTitleByIndex ~= nil and g_fillTypeManager:getFillTypeTitleByIndex(fillType) or nil
table.insert(titles, tostring(title or g_fillTypeManager:getFillTypeNameByIndex(fillType)))
end
end
if #titles == 0 then
return nil
end
return string.format(self.i18n:getText("spu_canCarry"), table.concat(titles, ", "))
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.currentTab == SmartPickupFrame.TAB_OVERVIEW then
wanted = self.overviewButtons
elseif self.currentTab == SmartPickupFrame.TAB_FIELDS then
wanted = self.fieldButtons
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
local isOverview = self.currentTab == SmartPickupFrame.TAB_OVERVIEW
if isOverview then
self:refreshOverview(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
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
self:setStatusText(self.statusOverride or string.format(self.i18n:getText("spu_status_overview"), running, total, activeRuns, #ADRunsController.store.runs))
self.stopAllButtonInfo.disabled = running == 0
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.isRunning == 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)
-- mål- og kilde-rækkerne deler prefab (og dermed onClick) med vare-rækkerne
local data = self.rowData[element]
if data ~= nil and data.kind == "target" then
return self:onClickTarget(state, element)
elseif data ~= nil and data.kind == "source" then
return self:onClickSource(state, element)
elseif data ~= nil and data.kind == "ingredientMode" then
return self:onClickIngredientMode(state)
elseif data ~= nil and data.kind == "collectRound" then
return self:onClickCollectRound(state)
elseif data ~= nil and data.kind == "direction" then
return self:onClickDirection(state)
elseif data ~= nil and (data.kind == "showAllVehicles" or data.kind == "showAllBuildings") then
local isOn = state == SmartPickupFrame.STATE_ON
if data.kind == "showAllVehicles" then self.showAllVehicles = isOn else self.showAllBuildings = isOn end
self:requestRebuild()
return
elseif data ~= nil and SmartPickupFrame.FIELD_ROW_KINDS[data.kind] then
return self:onClickFieldRow(state, data)
elseif data ~= nil and (data.kind == "sourceBuilding" or data.kind == "targetBuilding" or data.kind == "storeBuilding") then
return self:onClickBuilding(state, element)
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]
if clicked ~= nil and clicked.kind == "fleetVehicle" then
return self:onClickFieldRow(state, clicked)
end
self:safe(function(frame)
local run, data = frame:getRun(), frame.rowData[element]
if run == nil or data == nil or data.locked 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
if ADRunsController.getRunStatus(adEnv, run).isRunning then
ADRunsController.stopRun(adEnv, run.id)
frame.statusOverride = nil
else
local started, skipped, reasons = frame:summarizeStartResults(ADRunsController.startRun(adEnv, run.id))
frame:showOverride(string.format(frame.i18n:getText("spu_status_started"), started, skipped), reasons)
end
ADRunsController.save()
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
local summary = ADRunsController.startAllRuns(adEnv)
local _, skipped, reasons = frame:summarizeStartResults(summary.results)
Logging.info(SmartPickupFrame.LOG_PREFIX .. "Start alle: %d flows startet (%d traktorer), %d kørte allerede, %d sprunget over",
summary.runsStarted, summary.vehiclesStarted, summary.alreadyRunning, skipped)
frame:showOverride(string.format(frame.i18n:getText("spu_status_startedAll"), summary.runsStarted, summary.vehiclesStarted, summary.alreadyRunning, skipped), reasons)
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 run: navnet vælges FØR run'et oprettes (annulleres dialogen, oprettes intet).
-- Forslaget tæller efter antal runs, ikke efter løbenummeret, så "Run 2" kan genbruges.
function SmartPickupFrame:onClickNew()
self:safe(function(frame)
local suggestion = string.format(frame.i18n:getText("spu_newRunName"), #ADRunsController.store.runs + 1)
TextInputDialog.show(SmartPickupFrame.onNewRunName, frame, suggestion, frame.i18n:getText("spu_renameTitle"), nil, 30, g_i18n:getText("button_ok"))
end)
end
function SmartPickupFrame:onNewRunName(value, clickOk)
self:safe(function(frame)
if not clickOk or value == nil or not value:match("%S") then
return
end
local run = ADRunsController.createRun(value, "supply")
frame.selectedRunId = run.id
frame.statusOverride = nil
frame.currentTab = SmartPickupFrame.TAB_FLOWS
frame:switchTab(SmartPickupFrame.TAB_FLOWS)
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