feat(gui): faner Oversigt/Forsyning/Udkørsel; udkørsels-run med kilder, varepolitik, regler og status

This commit is contained in:
2026-09-23 10:02:24 +02:00
parent cb115f9588
commit 337db16178
6 changed files with 508 additions and 126 deletions
+107 -124
View File
@@ -9,8 +9,11 @@ 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_RUNS = 1, 2
SmartPickupFrame.TAB_NAMES = {"spu_tab_overview", "spu_tab_runs"}
SmartPickupFrame.TAB_OVERVIEW, SmartPickupFrame.TAB_SUPPLY, SmartPickupFrame.TAB_OUTBOUND = 1, 2, 3
SmartPickupFrame.TAB_NAMES = {"spu_tab_overview", "spu_tab_supply", "spu_tab_outbound"}
-- Hver fane viser kun runs af sin egen slags; Oversigt opretter forsynings-runs.
SmartPickupFrame.TAB_KIND = {"supply", "supply", "outbound"}
SmartPickupFrame.POLICIES = {"off", "store", "sell", "sellElseStore"}
function SmartPickupFrame.new(i18n, getAdEnv)
local self = SmartPickupFrame:superClass().new(nil, SmartPickupFrame._mt)
@@ -52,7 +55,7 @@ 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"}) do
for _, name in ipairs({"ingredientPrefab", "vehiclePrefab", "textPrefab", "productPrefab"}) do
local prefab = self[name]
if prefab ~= nil then
prefab:unlinkElement()
@@ -91,6 +94,34 @@ function SmartPickupFrame:initialize()
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)
@@ -214,15 +245,35 @@ function SmartPickupFrame:onClickTab(state)
self:switchTab(state)
end
-- De faste rækker (Run, Varer, Wait-pulje, Ture, tærskel) hører til Runs-fanen.
function SmartPickupFrame:setStaticRowsVisible(isVisible)
for _, option in ipairs({self.runOption, self.ingredientModeOption, self.waitPoolOption, self.loopsOption, self.returnBelowOption}) do
-- 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(isVisible)
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
@@ -244,6 +295,7 @@ function SmartPickupFrame:clearRows()
self.statusRow = nil
self.needsRows = {}
self.overviewRows = {}
self.productRows, self.outboundStatusRows = {}, {}
end
function SmartPickupFrame:addRow(prefab, data)
@@ -285,12 +337,14 @@ 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
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(not isOverview)
self:setStaticRowsVisible(self.currentTab)
if isOverview then
if adEnv ~= nil then
self:fillOverviewRows(adEnv)
@@ -307,20 +361,21 @@ function SmartPickupFrame:rebuild()
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}) do
element:setDisabled(not hasRun)
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.ingredientModeOption:setState(run.ingredientMode == "manual" and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
self:fillWaitPoolOption(adEnv, run)
self.loopsOption:setState(run.loops + 1, false)
self.returnBelowOption:setState(self:getReturnBelowIndex(run), false)
self:addTextRow(self.i18n:getText("spu_targets"))
self:fillTargetRows(adEnv, run)
self:fillIngredientRows(adEnv, run)
self:fillNeedsRows(adEnv, run)
self:addTextRow(self.i18n:getText("spu_vehicles"))
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()
@@ -332,7 +387,7 @@ end
function SmartPickupFrame:fillRunOption(adEnv)
local texts, index = {}, 1
self.runIds = {}
for position, run in ipairs(ADRunsController.store.runs) do
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)
@@ -348,32 +403,6 @@ function SmartPickupFrame:fillRunOption(adEnv)
self.runOption:setDisabled(#self.runIds == 0)
end
-- Mål = afkrydsning (flere stalde/fabrikker pr. run). Ved hver tur vælges den der mangler mest.
function SmartPickupFrame:fillTargetRows(adEnv, run)
local chosen = {}
for _, id in ipairs(ADRunsController.getRunTargets(run)) do
chosen[id] = true
end
for _, marker in ipairs(ADRunsController.getTargetMarkers(adEnv, g_currentMission:getFarmId())) do
local row = self:addRow(self.ingredientPrefab, {kind = "target", wayPointId = marker.id})
row:getDescendantByName("title"):setText(" " .. tostring(marker.name))
row:getDescendantByName("option"):setState(chosen[marker.id] and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
end
end
-- Behovsrække pr. mål ("Cow 1 Food: Forage 0/33.750 l · …"), opdateres hvert sekund.
function SmartPickupFrame:fillNeedsRows(adEnv, run)
self.needsRows = {}
local targets = ADRunsController.getTargetsLevels(adEnv, run)
if #targets == 0 then
self:addTextRow(self.i18n:getText("spu_needs_noTarget"))
return
end
for _, target in ipairs(targets) do
table.insert(self.needsRows, {wayPointId = target.wayPointId, title = self:addTextRow(self:describeTargetNeeds(target))})
end
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
@@ -382,40 +411,6 @@ function SmartPickupFrame:describeTargetLevels(target)
return string.format("%s: %s", target.name, self:formatLevels(target.levels))
end
function SmartPickupFrame:describeTargetNeeds(target)
if #target.levels == 0 then
return string.format("%s: %s", target.name, self.i18n:getText("spu_needs_none"))
end
return string.format(self.i18n:getText("spu_needs"), target.name .. ": " .. self:formatLevels(target.levels))
end
function SmartPickupFrame:refreshNeedsRows(adEnv, run)
local byId = {}
for _, target in ipairs(ADRunsController.getTargetsLevels(adEnv, run)) do
byId[target.wayPointId] = target
end
for _, row in ipairs(self.needsRows or {}) do
if byId[row.wayPointId] ~= nil then
row.title:setText(self:describeTargetNeeds(byId[row.wayPointId]))
end
end
end
function SmartPickupFrame:fillIngredientRows(adEnv, run)
if run.ingredientMode ~= "manual" then
return
end
local chosen = {}
for _, name in ipairs(run.ingredients) do
chosen[name] = true
end
for _, fillType in ipairs(ADRunsController.getTargetAcceptedFillTypes(adEnv, run)) do
local row = self:addRow(self.ingredientPrefab, {kind = "ingredient", name = fillType.name})
row:getDescendantByName("title"):setText(tostring(fillType.title))
row:getDescendantByName("option"):setState(chosen[fillType.name] and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
end
end
function SmartPickupFrame:fillWaitPoolOption(adEnv, run)
local texts, index = {self.i18n:getText("spu_waitPool_all")}, 1
self.poolGroups = {""}
@@ -502,12 +497,18 @@ function SmartPickupFrame:syncOptionsFromModel(adEnv, run)
option:setState(wantedIndex, false)
end
end
sync(self.ingredientModeOption, run.ingredientMode == "manual" and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF)
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
@@ -545,6 +546,8 @@ function SmartPickupFrame:describeActivity(adEnv, vehicle)
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))
@@ -573,8 +576,12 @@ function SmartPickupFrame:fillOverviewRows(adEnv)
table.insert(entry.vehicles, {vehicle = vehicle, title = add(" " .. self:describeActivity(adEnv, vehicle))})
end
end
for _, target in ipairs(ADRunsController.getTargetsLevels(adEnv, run)) do
table.insert(entry.targets, {wayPointId = target.wayPointId, title = add(" " .. self:describeTargetLevels(target))})
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
@@ -601,6 +608,9 @@ function SmartPickupFrame:refreshOverview(adEnv)
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
@@ -658,7 +668,11 @@ function SmartPickupFrame:refreshLive()
local isRunning = false
if run ~= nil then
self:syncOptionsFromModel(adEnv, run)
self:refreshNeedsRows(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)
@@ -692,30 +706,13 @@ function SmartPickupFrame:onClickRun(state)
self:requestRebuild()
end
function SmartPickupFrame:onClickTarget(state, element)
self:safe(function(frame)
local run, data = frame:getRun(), frame.rowData[element]
if run == nil or data == nil then return end
ADRunsController.store = ADRuns.toggleTarget(ADRunsController.store, run.id, data.wayPointId)
Logging.info(SmartPickupFrame.LOG_PREFIX .. "run '%s': targets=%s", tostring(run.name), table.concat(ADRunsController.getRunTargets(ADRuns.getRun(ADRunsController.store, run.id)), " "))
frame:requestRebuild()
end)
end
function SmartPickupFrame:onClickIngredientMode(state)
self:safe(function(frame)
local run = frame:getRun()
if run == nil then return end
ADRunsController.updateRun(run.id, {ingredientMode = state == SmartPickupFrame.STATE_ON and "manual" or "auto"})
frame:requestRebuild()
end)
end
function SmartPickupFrame:onClickIngredient(state, element)
-- mål-rækkerne deler prefab (og dermed onClick) med vare-rækkerne
-- 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]
@@ -739,22 +736,6 @@ function SmartPickupFrame:onClickWaitPool(state)
end)
end
function SmartPickupFrame:onClickLoops(state)
self:safe(function(frame)
local run = frame:getRun()
if run == nil then return end
ADRunsController.updateRun(run.id, {loops = state - 1})
end)
end
function SmartPickupFrame:onClickReturnBelow(state)
self:safe(function(frame)
local run = frame:getRun()
if run == nil then return end
ADRunsController.updateRun(run.id, {returnBelowPercent = frame.returnBelowSteps[state] or ADRuns.DEFAULT_RETURN_BELOW_PERCENT})
end)
end
function SmartPickupFrame:onClickVehicle(state, element)
self:safe(function(frame)
local run, data = frame:getRun(), frame.rowData[element]
@@ -807,11 +788,13 @@ function SmartPickupFrame:onNewRunName(value, clickOk)
if not clickOk or value == nil or not value:match("%S") then
return
end
local run = ADRunsController.createRun(value)
local kind = frame:getTabKind()
local run = ADRunsController.createRun(value, kind)
frame.selectedRunId = run.id
frame.statusOverride = nil
frame.currentTab = SmartPickupFrame.TAB_RUNS
frame:switchTab(SmartPickupFrame.TAB_RUNS)
local tab = kind == "outbound" and SmartPickupFrame.TAB_OUTBOUND or SmartPickupFrame.TAB_SUPPLY
frame.currentTab = tab
frame:switchTab(tab)
end)
end