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

877 lines
38 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.LOG_PREFIX = "ADSmartPickup: "
SmartPickupFrame.STATE_OFF, SmartPickupFrame.STATE_ON = 1, 2
SmartPickupFrame.TAB_OVERVIEW, SmartPickupFrame.TAB_SUPPLY, SmartPickupFrame.TAB_OUTBOUND, SmartPickupFrame.TAB_STORAGE = 1, 2, 3, 4
SmartPickupFrame.TAB_NAMES = {"spu_tab_overview", "spu_tab_supply", "spu_tab_outbound", "spu_tab_storage"}
-- Hver fane viser kun runs af sin egen slags; Oversigt og Lagre opretter forsynings-runs.
SmartPickupFrame.TAB_KIND = {"supply", "supply", "outbound", "supply"}
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}
-- Egen reference til listen: fallback'en i refreshButtons skal kunne sende den igen.
self.menuButtonInfo = {self.backButtonInfo, self.startButtonInfo, self.newButtonInfo, self.renameButtonInfo, self.deleteButtonInfo}
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"}) 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
self.ingredientModeOption:setTexts({self.i18n:getText("spu_mode_auto"), self.i18n:getText("spu_mode_manual")})
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
if self.collectRoundOption ~= nil then self.collectRoundOption:setTexts({self.i18n:getText("ui_no"), self.i18n:getText("ui_yes")}) 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:getTabKind()
return SmartPickupFrame.TAB_KIND[self.currentTab] or "supply"
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.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 hører til hver sin fane: Run + Wait-pulje er fælles, Varer/Ture/tærskel er
-- Forsyning, Sælg fra/Kør til lager/Saml op er Udkørsel. Oversigt viser ingen.
function SmartPickupFrame:setStaticRowsVisible(tab)
local isSupply, isOutbound = tab == SmartPickupFrame.TAB_SUPPLY, tab == SmartPickupFrame.TAB_OUTBOUND
local visibility = {
{self.runOption, isSupply or isOutbound},
{self.waitPoolOption, isSupply or isOutbound},
{self.ingredientModeOption, isSupply}, {self.loopsOption, isSupply}, {self.returnBelowOption, isSupply},
{self.sellAtOption, isOutbound}, {self.storeAboveOption, isOutbound}, {self.collectRoundOption, 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
-- Run'et der vises: kun runs af fanens slags; skifter fanen, vælges fanens første run.
function SmartPickupFrame:getRunsForTab()
local kind, result = self:getTabKind(), {}
for _, run in ipairs(ADRunsController.store.runs) do
if (run.kind or "supply") == kind then
table.insert(result, run)
end
end
return result
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 = {}
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.
function SmartPickupFrame:addTextRow(text)
local title = self:addRow(self.textPrefab, {kind = "text"}):getDescendantByName("title")
title:setText(text)
return title
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
local tabRuns = self:getRunsForTab()
local selected = self.selectedRunId ~= nil and ADRuns.getRun(store, self.selectedRunId) or nil
if selected == nil or (selected.kind or "supply") ~= self:getTabKind() then
self.selectedRunId = tabRuns[1] ~= nil and tabRuns[1].id or nil
end
self:clearRows()
local isOverview = self.currentTab == SmartPickupFrame.TAB_OVERVIEW
self:setStaticRowsVisible(self.currentTab)
if self.currentTab == SmartPickupFrame.TAB_STORAGE 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 isOverview then
if adEnv ~= nil then
self:fillOverviewRows(adEnv)
end
self:addTextRow(self.i18n:getText("spu_waitPools"))
self:fillPoolRows(adEnv)
self.statusRow = self:addTextRow("")
self:colorRows()
self.boxLayout:invalidateLayout()
self:restoreFocusIfLost()
self:refreshLive()
return
end
self:fillRunOption(adEnv)
local run = self:getRun()
local hasRun = run ~= nil and adEnv ~= nil
for _, element in ipairs({self.ingredientModeOption, self.waitPoolOption, self.loopsOption, self.returnBelowOption, self.sellAtOption, self.storeAboveOption, self.collectRoundOption}) do
if element ~= nil then
element:setDisabled(not hasRun)
end
end
if hasRun then
self:fillWaitPoolOption(adEnv, run)
if self.currentTab == SmartPickupFrame.TAB_OUTBOUND then
self:rebuildOutboundTab(adEnv, run)
else
self:rebuildSupplyTab(adEnv, run)
end
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
function SmartPickupFrame:fillVehicleRows(adEnv, run)
local inRun = {}
for _, id in ipairs(run.vehicleIds) do
inRun[id] = true
end
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
local id = ADRunsController.getVehicleId(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(inRun[id] and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
-- Traktorer i et ANDET run kan ikke vælges her (én traktor = ét run); rækken viser hvor den er.
local otherRun = ADRunsController.getRunForVehicle(vehicle)
if otherRun ~= nil and otherRun.id ~= run.id then
option:setDisabled(true)
self.rowData[option].locked = true
title:setText(string.format("%s — %s", ADRunsController.getVehicleLabel(adEnv, vehicle), string.format(self.i18n:getText("spu_inRun"), tostring(otherRun.name))))
else
title:setText(self:describeVehicle(adEnv, run, vehicle, inRun[id]))
-- Vogn der ikke kan bære run'ets varer (ingen vogn / forkert vogn / paller uden UAL) kan ikke
-- vælges; er den allerede i run'et, kan den stadig fravælges.
if not inRun[id] and SmartPickupFrame.VEHICLE_BLOCKING_REASONS[ADRunsController.validateVehicle(adEnv, run, vehicle).reason] 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
table.insert(parts, "X " .. self.i18n:getText("spu_reason_" .. tostring(validation.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)
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.collectRoundOption, run.collectRound and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF)
return
end
sync(self.ingredientModeOption, 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
-- Rækkerne klones bagest og flyttes derefter forrest i layoutet.
function SmartPickupFrame:fillOverviewRows(adEnv)
local rows = {}
local function add(text)
local title = self:addTextRow(text)
table.insert(rows, title.parent)
return title
end
self.overviewRows = {}
for _, run in ipairs(ADRunsController.store.runs) do
local status = ADRunsController.getRunStatus(adEnv, run)
local statusText = 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)
local header = add(string.format("%s — %s", run.name, statusText))
local entry = {run = run, header = header, vehicles = {}, targets = {}}
for _, vehicleId in ipairs(run.vehicleIds) do
local vehicle = ADRunsController.findVehicleById(adEnv, vehicleId)
if vehicle ~= nil then
table.insert(entry.vehicles, {vehicle = vehicle, title = add(" " .. self:describeActivity(adEnv, vehicle))})
end
end
if run.kind == "outbound" then
entry.outbound = add(" " .. self:describeOutboundOverview(adEnv, run))
else
for _, target in ipairs(ADRunsController.getTargetsLevels(adEnv, run)) do
table.insert(entry.targets, {wayPointId = target.wayPointId, title = add(" " .. self:describeTargetLevels(target))})
end
end
table.insert(self.overviewRows, entry)
end
-- forrest i layoutet: fjern de klonede rækker bagfra og sæt dem ind i rækkefølge først
local elements = self.boxLayout.elements
for index = #rows, 1, -1 do
for position = #elements, 1, -1 do
if elements[position] == rows[index] then
table.remove(elements, position)
break
end
end
table.insert(elements, 1, rows[index])
end
end
-- Opdateres hvert sekund uden genopbygning (kun tekster).
function SmartPickupFrame:refreshOverview(adEnv)
for _, entry in ipairs(self.overviewRows or {}) do
local status = ADRunsController.getRunStatus(adEnv, entry.run)
local statusText = 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)
entry.header:setText(string.format("%s — %s", entry.run.name, statusText))
for _, item in ipairs(entry.vehicles) do
item.title:setText(" " .. self:describeActivity(adEnv, item.vehicle))
end
if entry.outbound ~= nil then
entry.outbound:setText(" " .. self:describeOutboundOverview(adEnv, entry.run))
end
local byId = {}
for _, target in ipairs(ADRunsController.getTargetsLevels(adEnv, entry.run)) do
byId[target.wayPointId] = target
end
for _, item in ipairs(entry.targets) do
if byId[item.wayPointId] ~= nil then
item.title:setText(" " .. self:describeTargetLevels(byId[item.wayPointId]))
end
end
end
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()
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
self:refreshOverview(adEnv)
self:refreshStorageRows(adEnv)
local isRunning = false
if 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)
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)
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 = 0, 0, {}
for _, result in ipairs(ADRunsController.startRun(adEnv, run.id)) do
if result.ok then
started = started + 1
else
skipped = skipped + 1
table.insert(reasons, string.format("%s: %s", tostring(result.vehicle:getName()), frame.i18n:getText("spu_reason_" .. tostring(result.reason))))
end
end
frame.statusOverride = string.format(frame.i18n:getText("spu_status_started"), started, skipped)
if #reasons > 0 then
frame.statusOverride = frame.statusOverride .. " — " .. table.concat(reasons, "; ")
end
frame.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS
end
ADRunsController.save()
frame:requestRebuild()
end)
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 kind = frame:getTabKind()
local run = ADRunsController.createRun(value, kind)
frame.selectedRunId = run.id
frame.statusOverride = nil
local tab = kind == "outbound" and SmartPickupFrame.TAB_OUTBOUND or SmartPickupFrame.TAB_SUPPLY
frame.currentTab = tab
frame:switchTab(tab)
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