853 lines
36 KiB
Lua
853 lines
36 KiB
Lua
-- 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.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"}
|
||
|
||
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]))
|
||
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.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, "✔")
|
||
else
|
||
table.insert(parts, "✖ " .. 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.running > 0
|
||
local text = isRunning 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 ..."): vist nu, ryddet her,
|
||
-- så næste opdatering om et sekund viser den levende status igen.
|
||
self.statusOverride = nil
|
||
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))
|
||
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).running > 0 then
|
||
ADRunsController.stopRun(adEnv, run.id)
|
||
frame.statusOverride = nil
|
||
else
|
||
local started, skipped = 0, 0
|
||
for _, result in ipairs(ADRunsController.startRun(adEnv, run.id)) do
|
||
if result.ok then started = started + 1 else skipped = skipped + 1 end
|
||
end
|
||
frame.statusOverride = string.format(frame.i18n:getText("spu_status_started"), started, skipped)
|
||
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
|