feat: flere mål pr. run (afkrydsning; den stald der mangler mest får turen, AD's mål skiftes ved turstart) + under-faner Overblik/Runs (v1.11.0.0)

This commit is contained in:
2026-09-23 01:56:47 +02:00
parent 69a98984bb
commit bcd7de8344
12 changed files with 376 additions and 113 deletions
+42 -5
View File
@@ -25,6 +25,7 @@ local function copyRun(run)
id = run.id,
name = run.name,
targetWayPointId = run.targetWayPointId,
targetWayPointIds = copyList(run.targetWayPointIds),
ingredientMode = run.ingredientMode,
ingredients = copyList(run.ingredients),
waitPoolGroup = run.waitPoolGroup,
@@ -62,6 +63,7 @@ function ADRuns.createRun(store, name)
id = new.nextId,
name = name,
targetWayPointId = nil,
targetWayPointIds = {},
ingredientMode = "auto",
ingredients = {},
waitPoolGroup = "",
@@ -102,12 +104,16 @@ function ADRuns.updateRun(store, runId, changes)
return new
end
if changes.name ~= nil then run.name = changes.name end
-- false = ryd målet (nil i changes betyder "rør ikke")
if changes.targetWayPointId == false then
run.targetWayPointId = nil
-- Mål: liste (flere stalde pr. run); targetWayPointId = første = "primært" (bagudkompatibelt).
-- false = ryd målene (nil i changes betyder "rør ikke")
if changes.targetWayPointIds ~= nil then
run.targetWayPointIds = copyList(changes.targetWayPointIds)
elseif changes.targetWayPointId == false then
run.targetWayPointIds = {}
elseif changes.targetWayPointId ~= nil then
run.targetWayPointId = changes.targetWayPointId
run.targetWayPointIds = {changes.targetWayPointId}
end
run.targetWayPointId = run.targetWayPointIds[1]
if ADRuns.INGREDIENT_MODES[changes.ingredientMode] then run.ingredientMode = changes.ingredientMode end
if changes.ingredients ~= nil then run.ingredients = copyList(changes.ingredients) end
if changes.waitPoolGroup ~= nil then run.waitPoolGroup = changes.waitPoolGroup end
@@ -116,6 +122,27 @@ function ADRuns.updateRun(store, runId, changes)
return new
end
-- Tilføj/fjern et mål (afkrydsning i fanen).
function ADRuns.toggleTarget(store, runId, wayPointId)
local run = ADRuns.getRun(store, runId)
if run == nil then
return copyStore(store)
end
local targets = {}
local found = false
for _, id in ipairs(run.targetWayPointIds) do
if id == wayPointId then
found = true
else
table.insert(targets, id)
end
end
if not found then
table.insert(targets, wayPointId)
end
return ADRuns.updateRun(store, runId, {targetWayPointIds = targets})
end
function ADRuns.findRunByVehicle(store, vehicleId)
for _, run in ipairs(store.runs) do
for _, id in ipairs(run.vehicleIds) do
@@ -177,6 +204,7 @@ function ADRuns.toRecords(store)
id = run.id,
name = run.name,
target = run.targetWayPointId or 0,
targets = table.concat(run.targetWayPointIds, " "),
ingredientMode = run.ingredientMode,
ingredients = ADRuns.serializeIngredients(run.ingredients),
waitPool = run.waitPoolGroup,
@@ -196,10 +224,19 @@ function ADRuns.fromRecords(records)
local isValid = id ~= nil and id >= 1 and record.name ~= nil and record.name ~= ""
if isValid and ADRuns.getRun(store, id) == nil then
local target = tonumber(record.target)
local targets = {}
for token in string.gmatch(tostring(record.targets or ""), "%S+") do
local wayPointId = tonumber(token)
if wayPointId ~= nil and wayPointId > 0 then table.insert(targets, wayPointId) end
end
if #targets == 0 and target ~= nil and target > 0 then
targets = {target}
end
table.insert(store.runs, {
id = id,
name = record.name,
targetWayPointId = target ~= nil and target > 0 and target or nil,
targetWayPointId = targets[1],
targetWayPointIds = targets,
ingredientMode = ADRuns.INGREDIENT_MODES[record.ingredientMode] and record.ingredientMode or "auto",
ingredients = ADRuns.parseIngredients(record.ingredients),
waitPoolGroup = record.waitPool or "",
+86 -44
View File
@@ -120,29 +120,51 @@ end
-- Auto-listen: det vognen kan bære, som målet også tager. Er målet en fabrik, indsnævrer
-- findSupplyPickup selv til de ønskede produktioners inputs bagefter.
-- Run'ets mål som liste (flere stalde pr. run); gamle runs har kun targetWayPointId.
function ADRunsController.getRunTargets(run)
if run.targetWayPointIds ~= nil and #run.targetWayPointIds > 0 then
return run.targetWayPointIds
end
return run.targetWayPointId ~= nil and {run.targetWayPointId} or {}
end
local function getTargetStations(adEnv, run, farmId)
local stations = {}
for _, wayPointId in ipairs(ADRunsController.getRunTargets(run)) do
local station = ADSmartPickup.getUnloadStationAtWayPointId(adEnv, wayPointId, farmId)
if station ~= nil then
table.insert(stations, {wayPointId = wayPointId, station = station})
end
end
return stations
end
function ADRunsController.getTargetFillTypes(adEnv, run, vehicle)
local farmId = ADSmartPickup.getFarmId(vehicle)
-- En kørende traktor følger AD's eget mål (stateModule:getSecondMarker(), det samme
-- choosePickup/findSupplyPickup bruger) indtil næste Start — IKKE run'ets gemte
-- targetWayPointId. Redigerer brugeren run'ets Mål mens tildelte traktorer kører,
-- skal Auto-listen derfor følge det LIVE mål, ellers kan den blive tom og fejlagtigt
-- smide traktoren over i mappe-logikken midt i turen.
local wayPointId = run.targetWayPointId
-- choosePickup/findSupplyPickup bruger) indtil næste tur — IKKE run'ets gemte mål.
-- Redigerer brugeren run'ets Mål mens tildelte traktorer kører, skal Auto-listen derfor
-- følge det LIVE mål, ellers kan den blive tom og fejlagtigt smide traktoren over i
-- mappe-logikken midt i turen. Inaktiv: union over alle run'ets mål.
local stations = {}
local stateModule = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
if stateModule ~= nil and stateModule.isActive ~= nil and stateModule:isActive() and stateModule.getSecondMarker ~= nil then
local secondMarker = stateModule:getSecondMarker()
if secondMarker ~= nil and secondMarker.id ~= nil then
wayPointId = secondMarker.id
local station = secondMarker ~= nil and secondMarker.id ~= nil and ADSmartPickup.getUnloadStationAtWayPointId(adEnv, secondMarker.id, farmId) or nil
if station ~= nil then
stations = {{wayPointId = secondMarker.id, station = station}}
end
end
local target = wayPointId ~= nil and ADSmartPickup.getUnloadStationAtWayPointId(adEnv, wayPointId, farmId) or nil
if target == nil then
return {}
if #stations == 0 then
stations = getTargetStations(adEnv, run, farmId)
end
local result = {}
local result, seen = {}, {}
for _, fillType in ipairs(ADRunsController.getCarriableFillTypes(adEnv, vehicle)) do
if ADSmartPickup.getStationLevelAndCapacity(target, fillType, farmId) ~= nil then
table.insert(result, fillType)
for _, entry in ipairs(stations) do
if not seen[fillType] and ADSmartPickup.getStationLevelAndCapacity(entry.station, fillType, farmId) ~= nil then
seen[fillType] = true
table.insert(result, fillType)
end
end
end
table.sort(result)
@@ -165,25 +187,52 @@ end
-- Alle varer målet tager (uanset vogn) — afkrydsningslisten i GUI'et. {index, name, title}, sorteret på titel.
function ADRunsController.getTargetAcceptedFillTypes(adEnv, run)
if run.targetWayPointId == nil then
return {}
end
local farmId = g_currentMission:getFarmId()
local target = ADSmartPickup.getUnloadStationAtWayPointId(adEnv, run.targetWayPointId, farmId)
if target == nil then
local stations = getTargetStations(adEnv, run, farmId)
if #stations == 0 then
return {}
end
local fillTypes = g_fillTypeManager.getFillTypes ~= nil and g_fillTypeManager:getFillTypes() or g_fillTypeManager.fillTypes or {}
local result = {}
for _, fillType in ipairs(fillTypes) do
if fillType.index ~= nil and ADSmartPickup.getStationLevelAndCapacity(target, fillType.index, farmId) ~= nil then
table.insert(result, {index = fillType.index, name = fillType.name, title = fillType.title or fillType.name})
if fillType.index ~= nil then
for _, entry in ipairs(stations) do
if ADSmartPickup.getStationLevelAndCapacity(entry.station, fillType.index, farmId) ~= nil then
table.insert(result, {index = fillType.index, name = fillType.name, title = fillType.title or fillType.name})
break
end
end
end
end
table.sort(result, function(a, b) return tostring(a.title) < tostring(b.title) end)
return result
end
-- Flere mål: den stald der mangler mest lige nu (bedste rangerede behov pr. mål; et FULDT
-- læs der passer vinder). nil uden mål. Ét mål = det.
function ADRunsController.chooseTarget(adEnv, run, vehicle)
local targets = ADRunsController.getRunTargets(run)
if #targets <= 1 then
return targets[1]
end
local farmId = ADSmartPickup.getFarmId(vehicle)
local ingredients = ADRunsController.getRunIngredients(adEnv, run, vehicle)
local bestId, bestNeed = nil, nil
for _, entry in ipairs(getTargetStations(adEnv, run, farmId)) do
local ranked = ADSmartPickup.getTargetNeedsRanked(entry.station, ingredients, farmId)
local need = ranked[1]
if need ~= nil and need.freeSpace >= (ADSupplyPlanner.MIN_FREE_LITERS or 1000) then
local isBetter = bestNeed == nil
or (need.fits == true and bestNeed.fits ~= true)
or (need.fits == bestNeed.fits and need.score > bestNeed.score)
if isBetter then
bestId, bestNeed = entry.wayPointId, need
end
end
end
return bestId or targets[1]
end
-- Fire traktorer kan hedde det samme; løbenummeret er pladsen i farmens AD-køretøjsliste.
function ADRunsController.getVehicleLabel(adEnv, vehicle)
for index, candidate in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
@@ -220,7 +269,13 @@ end
function ADRunsController.validateVehicle(adEnv, run, vehicle)
local carriable = ADRunsController.getCarriableFillTypes(adEnv, vehicle)
local result = {ok = false, carriable = carriable, capacity = getLoadCapacity(adEnv, vehicle)}
if run.targetWayPointId == nil or adEnv.ADGraphManager:getMapMarkerByWayPointId(run.targetWayPointId) == nil then
local hasTarget = false
for _, wayPointId in ipairs(ADRunsController.getRunTargets(run)) do
if adEnv.ADGraphManager:getMapMarkerByWayPointId(wayPointId) ~= nil then
hasTarget = true
end
end
if not hasTarget then
result.reason = "noTarget"
return result
end
@@ -298,7 +353,7 @@ local function applyRunToVehicle(adEnv, run, vehicle, ingredients)
local stateModule = vehicle.ad.stateModule
local farmId = ADSmartPickup.getFarmId(vehicle)
stateModule:setMode(adEnv.AutoDrive.MODE_PICKUPANDDELIVER)
stateModule:setSecondMarkerByWayPointId(run.targetWayPointId)
stateModule:setSecondMarkerByWayPointId(ADRunsController.chooseTarget(adEnv, run, vehicle) or run.targetWayPointId)
local loadMarker = findNearestLoadMarker(adEnv, vehicle, farmId)
if loadMarker ~= nil then
stateModule:setFirstMarkerByWayPointId(loadMarker.id)
@@ -432,13 +487,10 @@ function ADRunsController.getVehicleActivity(adEnv, vehicle)
return {kind = "empty", place = first ~= nil and first.name or nil}
end
-- Målets niveauer for run'ets varer: {{fillType, level, capacity}, ...} mest trængende først.
function ADRunsController.getTargetLevels(adEnv, run)
-- Niveauer pr. mål for run'ets varer: {{wayPointId, name, levels = {{fillType, level, capacity, free}}}}
-- i samme rangering som valget bruger (trug som én trang, fodervægt som prioritet); fulde sidst.
function ADRunsController.getTargetsLevels(adEnv, run)
local farmId = g_currentMission:getFarmId()
local target = run.targetWayPointId ~= nil and ADSmartPickup.getUnloadStationAtWayPointId(adEnv, run.targetWayPointId, farmId) or nil
if target == nil then
return {}
end
local fillTypes = {}
if run.ingredientMode == "manual" then
for _, name in ipairs(run.ingredients) do
@@ -450,26 +502,16 @@ function ADRunsController.getTargetLevels(adEnv, run)
table.insert(fillTypes, fillType.index)
end
end
-- samme rangering som valget bruger (trug som én trang, fodervægt som prioritet); fulde sidst
if ADSmartPickup.getTargetNeedsRanked ~= nil then
local result = {}
for _, entry in ipairs(getTargetStations(adEnv, run, farmId)) do
local marker = adEnv.ADGraphManager:getMapMarkerByWayPointId(entry.wayPointId)
local levels = {}
for _, need in ipairs(ADSmartPickup.getTargetNeedsRanked(target, fillTypes, farmId)) do
for _, need in ipairs(ADSmartPickup.getTargetNeedsRanked(entry.station, fillTypes, farmId)) do
table.insert(levels, {fillType = need.fillType, level = need.level, capacity = need.capacity, free = need.freeSpace})
end
return levels
table.insert(result, {wayPointId = entry.wayPointId, name = marker ~= nil and tostring(marker.name) or tostring(entry.wayPointId), levels = levels})
end
local levels = {}
for _, fillType in ipairs(fillTypes) do
local level, capacity = ADSmartPickup.getStationLevelAndCapacity(target, fillType, farmId)
if level ~= nil then
table.insert(levels, {fillType = fillType, level = level, capacity = capacity, free = capacity - level})
end
end
table.sort(levels, function(a, b)
if a.free ~= b.free then return a.free > b.free end
return a.fillType < b.fillType
end)
return levels
return result
end
-- ---- opslag til dropdowns ----
+2
View File
@@ -30,6 +30,7 @@ function ADRunsStorage.writeRecords(xmlFile, records)
xmlFile:setInt(key .. "#id", record.id)
xmlFile:setString(key .. "#name", record.name)
xmlFile:setInt(key .. "#target", record.target)
xmlFile:setString(key .. "#targets", record.targets or "")
xmlFile:setString(key .. "#ingredientMode", record.ingredientMode)
xmlFile:setString(key .. "#ingredients", record.ingredients)
xmlFile:setString(key .. "#waitPool", record.waitPool)
@@ -48,6 +49,7 @@ function ADRunsStorage.readRecords(xmlFile)
id = xmlFile:getInt(key .. "#id"),
name = xmlFile:getString(key .. "#name"),
target = xmlFile:getInt(key .. "#target"),
targets = xmlFile:getString(key .. "#targets"),
ingredientMode = xmlFile:getString(key .. "#ingredientMode"),
ingredients = xmlFile:getString(key .. "#ingredients"),
waitPool = xmlFile:getString(key .. "#waitPool"),
+19
View File
@@ -611,8 +611,27 @@ function ADSmartPickup.getIngredientsFor(adEnv, driver)
return ADSupplyPlanner.resolveIngredients(supplyState[driver], driver.ad.stateModule:getSelectedFillTypes())
end
-- Run med flere mål: ved turens start sættes AD's aflæsningsmål til den stald der mangler
-- mest. AD læser først målet når vognen er læsset, så skiftet her gælder for hele turen.
local function chooseRunTarget(adEnv, driver)
local run = ADRunsController ~= nil and ADRunsController.getRunForVehicle(driver) or nil
if run == nil or ADRunsController.chooseTarget == nil or #ADRunsController.getRunTargets(run) < 2 then
return
end
local stateModule = driver.ad.stateModule
local targetId = ADRunsController.chooseTarget(adEnv, run, driver)
local current = stateModule.getSecondMarker ~= nil and stateModule:getSecondMarker() or nil
if targetId == nil or stateModule.setSecondMarkerByWayPointId == nil or (current ~= nil and current.id == targetId) then
return
end
stateModule:setSecondMarkerByWayPointId(targetId)
local marker = adEnv.ADGraphManager.getMapMarkerByWayPointId ~= nil and adEnv.ADGraphManager:getMapMarkerByWayPointId(targetId) or nil
Logging.info(ADSmartPickup.LOG_PREFIX .. "'%s' kører til %s (mangler mest af run'ets mål)", tostring(driver:getName()), marker ~= nil and tostring(marker.name) or tostring(targetId))
end
-- Samlet valg: behovsstyret med ingrediensliste, ellers mappe-logik. nil = AutoDrives original.
function ADSmartPickup.choosePickup(adEnv, driver)
chooseRunTarget(adEnv, driver)
local ingredients = ADSmartPickup.getIngredientsFor(adEnv, driver)
if ingredients == nil then
supplyState[driver] = nil
+135 -55
View File
@@ -9,6 +9,8 @@ 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"}
function SmartPickupFrame.new(i18n, getAdEnv)
local self = SmartPickupFrame:superClass().new(nil, SmartPickupFrame._mt)
@@ -22,10 +24,12 @@ function SmartPickupFrame.new(i18n, getAdEnv)
self.refreshTimer = 0
self.statusOverride = nil
self.statusRow = nil -- Text-elementet i den klonede statusrække
self.needsRow = nil
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}
@@ -55,6 +59,25 @@ function SmartPickupFrame:initialize()
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
@@ -173,6 +196,33 @@ function SmartPickupFrame:requestRebuild()
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 (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
if option ~= nil and option.parent ~= nil then
option.parent:setVisible(isVisible)
end
end
end
function SmartPickupFrame:getRun()
return self.selectedRunId ~= nil and ADRuns.getRun(ADRunsController.store, self.selectedRunId) or nil
end
@@ -192,7 +242,7 @@ function SmartPickupFrame:clearRows()
end
self.clonedRows, self.rowData = {}, {}
self.statusRow = nil
self.needsRow = nil
self.needsRows = {}
self.overviewRows = {}
end
@@ -239,30 +289,39 @@ function SmartPickupFrame:rebuild()
self.selectedRunId = store.runs[1] ~= nil and store.runs[1].id or nil
end
self:clearRows()
if adEnv ~= nil then
self:fillOverviewRows(adEnv)
local isOverview = self.currentTab == SmartPickupFrame.TAB_OVERVIEW
self:setStaticRowsVisible(not isOverview)
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.targetOption, self.ingredientModeOption, self.waitPoolOption, self.loopsOption, self.returnBelowOption}) do
for _, element in ipairs({self.ingredientModeOption, self.waitPoolOption, self.loopsOption, self.returnBelowOption}) do
element:setDisabled(not hasRun)
end
if hasRun then
self:fillTargetOption(adEnv, run)
self.ingredientModeOption:setState(run.ingredientMode == "manual" and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
self:fillIngredientRows(adEnv, run)
self.needsRow = self:addTextRow(self:describeNeeds(adEnv, run))
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"))
self:fillVehicleRows(adEnv, run)
end
if adEnv ~= nil then
self:addTextRow(self.i18n:getText("spu_waitPools"))
self:fillPoolRows(adEnv)
end
self.statusRow = self:addTextRow("")
self:colorRows()
self.boxLayout:invalidateLayout()
@@ -289,18 +348,49 @@ function SmartPickupFrame:fillRunOption(adEnv)
self.runOption:setDisabled(#self.runIds == 0)
end
function SmartPickupFrame:fillTargetOption(adEnv, run)
local texts, index = {self.i18n:getText("spu_noTarget")}, 1
self.targetIds = {false}
-- 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
table.insert(texts, tostring(marker.name))
table.insert(self.targetIds, marker.id)
if marker.id == run.targetWayPointId then
index = #texts
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
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
self.targetOption:setTexts(texts)
self.targetOption:setState(index, false)
end
function SmartPickupFrame:fillIngredientRows(adEnv, run)
@@ -404,11 +494,6 @@ function SmartPickupFrame:syncOptionsFromModel(adEnv, run)
option:setState(wantedIndex, false)
end
end
local targetIndex = 1
for index, id in ipairs(self.targetIds) do
if id == run.targetWayPointId then targetIndex = index end
end
sync(self.targetOption, targetIndex)
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
@@ -429,17 +514,6 @@ function SmartPickupFrame:formatLevels(levels)
return table.concat(parts, " · ")
end
function SmartPickupFrame:describeNeeds(adEnv, run)
if run.targetWayPointId == nil then
return self.i18n:getText("spu_needs_noTarget")
end
local levels = ADRunsController.getTargetLevels(adEnv, run)
if #levels == 0 then
return self.i18n:getText("spu_needs_none")
end
return string.format(self.i18n:getText("spu_needs"), self:formatLevels(levels))
end
-- ---- overblik (øverst): ét afsnit pr. run — status, traktorernes aktivitet, målets niveauer ----
function SmartPickupFrame:describeActivity(adEnv, vehicle)
@@ -469,24 +543,22 @@ function SmartPickupFrame:fillOverviewRows(adEnv)
return title
end
add(self.i18n:getText("spu_overview"))
local targetMarkerName = function(run)
local marker = run.targetWayPointId ~= nil and adEnv.ADGraphManager:getMapMarkerByWayPointId(run.targetWayPointId) or nil
return marker ~= nil and tostring(marker.name) or self.i18n:getText("spu_noTarget")
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 = {}, target = nil}
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
entry.target = add(string.format(" %s: %s", targetMarkerName(run), self:formatLevels(ADRunsController.getTargetLevels(adEnv, run))))
for _, target in ipairs(ADRunsController.getTargetsLevels(adEnv, run)) do
table.insert(entry.targets, {wayPointId = target.wayPointId, title = add(" " .. self:describeTargetNeeds(target))})
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
@@ -512,9 +584,14 @@ function SmartPickupFrame:refreshOverview(adEnv)
for _, item in ipairs(entry.vehicles) do
item.title:setText(" " .. self:describeActivity(adEnv, item.vehicle))
end
if entry.target ~= nil then
local marker = entry.run.targetWayPointId ~= nil and adEnv.ADGraphManager:getMapMarkerByWayPointId(entry.run.targetWayPointId) or nil
entry.target:setText(string.format(" %s: %s", marker ~= nil and tostring(marker.name) or self.i18n:getText("spu_noTarget"), self:formatLevels(ADRunsController.getTargetLevels(adEnv, entry.run))))
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:describeTargetNeeds(byId[item.wayPointId]))
end
end
end
end
@@ -564,9 +641,7 @@ function SmartPickupFrame:refreshLive()
local isRunning = false
if run ~= nil then
self:syncOptionsFromModel(adEnv, run)
if self.needsRow ~= nil then
self.needsRow:setText(self:describeNeeds(adEnv, run))
end
self:refreshNeedsRows(adEnv, run)
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)
@@ -600,12 +675,12 @@ function SmartPickupFrame:onClickRun(state)
self:requestRebuild()
end
function SmartPickupFrame:onClickTarget(state)
function SmartPickupFrame:onClickTarget(state, element)
self:safe(function(frame)
local run = frame:getRun()
if run == nil then return end
-- targetIds[1] er false = "(vælg mål)"; modellen tolker false som "ryd målet"
ADRunsController.updateRun(run.id, {targetWayPointId = frame.targetIds[state] or false})
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
@@ -620,9 +695,14 @@ function SmartPickupFrame:onClickIngredientMode(state)
end
function SmartPickupFrame:onClickIngredient(state, element)
-- mål-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)
end
self:safe(function(frame)
local run, data = frame:getRun(), frame.rowData[element]
if run == nil or data == nil then return end
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
+9 -7
View File
@@ -10,19 +10,21 @@
<!-- Samme opbygning som AutoDrive's egne ESC-sider: ScrollingLayout direkte i
menuContainer, alle rækker som direkte børn (ingen indlejrede layouts). -->
<ScrollingLayout profile="fs25_settingsLayout" id="boxLayout">
<!-- under-faner som basisspillets Priser-side (mønster fra FS25_FarmOperationsDashboard) -->
<BoxLayout profile="fs25_subCategorySelectorTabbedBox" id="subCategoryBox"/>
<ThreePartBitmap profile="fs25_lineSeparatorTopHighlighted" position="0px -57px"/>
<MultiTextOption profile="fs25_subCategorySelectorTabbed" id="subCategoryPaging" position="0px -10px" onClick="onClickTab"/>
<Button profile="fs25_subCategorySelectorTabbedTab" id="selectorPrefab">
<ThreePartBitmap profile="fs25_subCategorySelectorTabbedTabBg" name="background"/>
</Button>
<ScrollingLayout profile="fs25_settingsLayout" id="boxLayout" position="0px -90px" absoluteSizeOffset="0px 90px">
<Bitmap profile="fs25_multiTextOptionContainer">
<MultiTextOption profile="fs25_settingsMultiTextOption" onClick="onClickRun" id="runOption">
<Text profile="fs25_multiTextOptionTooltip" name="ignore" text="$l10n_spu_run_tooltip"/>
</MultiTextOption>
<Text profile="fs25_settingsMultiTextOptionTitle" text="$l10n_spu_run"/>
</Bitmap>
<Bitmap profile="fs25_multiTextOptionContainer">
<MultiTextOption profile="fs25_settingsMultiTextOption" onClick="onClickTarget" id="targetOption">
<Text profile="fs25_multiTextOptionTooltip" name="ignore" text="$l10n_spu_target_tooltip"/>
</MultiTextOption>
<Text profile="fs25_settingsMultiTextOptionTitle" text="$l10n_spu_target"/>
</Bitmap>
<Bitmap profile="fs25_multiTextOptionContainer">
<BinaryOption profile="fs25_settingsBinaryOption" onClick="onClickIngredientMode" id="ingredientModeOption">
<Text profile="fs25_multiTextOptionTooltip" name="ignore" text="$l10n_spu_ingredientMode_tooltip"/>
+3
View File
@@ -42,6 +42,9 @@
<text name="spu_renameTitle" text="Navn på run"/>
<text name="spu_deleteConfirm" text="Slet run '%s'? Traktorerne stoppes ikke."/>
<text name="spu_capacity" text="%s l"/>
<text name="spu_tab_overview" text="Overblik"/>
<text name="spu_tab_runs" text="Runs"/>
<text name="spu_targets" text="Mål (vælg én eller flere — den der mangler mest, får turen)"/>
<text name="spu_returnBelow" text="Kør hjem fra Wait under"/>
<text name="spu_returnBelow_tooltip" text="Ventende traktorer kører først hjem, når målet er faldet under denne andel (eller hele resten passer)."/>
<text name="spu_overview" text="Overblik"/>
+3
View File
@@ -42,6 +42,9 @@
<text name="spu_renameTitle" text="Run name"/>
<text name="spu_deleteConfirm" text="Delete run '%s'? Tractors are not stopped."/>
<text name="spu_capacity" text="%s l"/>
<text name="spu_tab_overview" text="Overview"/>
<text name="spu_tab_runs" text="Runs"/>
<text name="spu_targets" text="Targets (pick one or more — the neediest gets the trip)"/>
<text name="spu_returnBelow" text="Return from Wait below"/>
<text name="spu_returnBelow_tooltip" text="Waiting tractors return only once the target has dropped below this share (or the whole rest fits)."/>
<text name="spu_overview" text="Overview"/>
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<modDesc descVersion="107">
<author>masterdraco</author>
<version>1.10.3.0</version>
<version>1.11.0.0</version>
<title>
<en>AD Smart Pickup</en>
</title>
+19
View File
@@ -107,6 +107,25 @@ check("R7 i records", r7rec[1].returnBelow, 25)
check("R7 tilbage fra records", ADRuns.fromRecords(r7rec).runs[1].returnBelowPercent, 25)
check("R7 manglende i gammel fil -> 50", ADRuns.fromRecords({{id = 1, name = "Gammel"}}).runs[1].returnBelowPercent, 50)
-- R8: flere mål pr. run — liste + første som "primært" mål (bagudkompatibelt); toggle; records
local r8, r8run = ADRuns.createRun(ADRuns.newStore(), "Kvæg")
check("R8 ingen mål", #r8run.targetWayPointIds, 0)
r8 = ADRuns.updateRun(r8, r8run.id, {targetWayPointIds = {94, 99}})
check("R8 to mål", table.concat(ADRuns.getRun(r8, r8run.id).targetWayPointIds, ","), "94,99")
check("R8 primært mål = første", ADRuns.getRun(r8, r8run.id).targetWayPointId, 94)
r8 = ADRuns.toggleTarget(r8, r8run.id, 94)
check("R8 toggle fjerner", table.concat(ADRuns.getRun(r8, r8run.id).targetWayPointIds, ","), "99")
check("R8 primært følger med", ADRuns.getRun(r8, r8run.id).targetWayPointId, 99)
r8 = ADRuns.toggleTarget(r8, r8run.id, 120)
check("R8 toggle tilføjer", table.concat(ADRuns.getRun(r8, r8run.id).targetWayPointIds, ","), "99,120")
check("R8 gammel enkelt-sætning = liste med ét", table.concat(ADRuns.getRun(ADRuns.updateRun(r8, r8run.id, {targetWayPointId = 7}), r8run.id).targetWayPointIds, ","), "7")
check("R8 false rydder listen", #ADRuns.getRun(ADRuns.updateRun(r8, r8run.id, {targetWayPointId = false}), r8run.id).targetWayPointIds, 0)
local r8rec = ADRuns.toRecords(r8)
check("R8 records: targets-streng", r8rec[1].targets, "99 120")
check("R8 records: target = første (gammelt felt)", r8rec[1].target, 99)
check("R8 tilbage fra records", table.concat(ADRuns.fromRecords(r8rec).runs[1].targetWayPointIds, ","), "99,120")
check("R8 gammel fil med kun target", table.concat(ADRuns.fromRecords({{id = 1, name = "Gl", target = 55}}).runs[1].targetWayPointIds, ","), "55")
-- ==== XML-laget mod en flad XMLFile-mock (nøgle -> værdi) ====
dofile("FS25_ADSmartPickup/adRunsStorage.lua")
+31 -1
View File
@@ -64,7 +64,20 @@ ADSmartPickup = {
getLoadStationAtWayPointId = function(_, id) return loadAt[id] end,
getStationLevelAndCapacity = function(station, fillType)
if station.accepts[fillType] == nil then return nil end
return 0, station.accepts[fillType]
return (station.levels or {})[fillType] or 0, station.accepts[fillType]
end,
-- forenklet rangering: mest fri plads (relativt) først; fulde sidst
getTargetNeedsRanked = function(station, fillTypes)
local ranked = {}
for _, fillType in ipairs(fillTypes) do
local capacity = station.accepts[fillType]
if capacity ~= nil then
local level = (station.levels or {})[fillType] or 0
table.insert(ranked, {fillType = fillType, level = level, capacity = capacity, freeSpace = capacity - level, score = (capacity - level) / capacity})
end
end
table.sort(ranked, function(a, b) return a.score > b.score end)
return ranked
end,
}
ADSupplyPlanner = {isWaitMarker = function(marker) return string.find(string.lower(marker.group or ""), "%f[%a]wait%f[%A]") ~= nil end}
@@ -345,5 +358,22 @@ actVehicle.waitInfo = {markerName = "Cow Wait", fillType = STRAW, rest = 5000}
check("C15 venter", ADRunsController.getVehicleActivity(adEnv, actVehicle).kind, "waiting")
ADUnloadWait = nil
-- C16: flere mål pr. run — Auto-varer = union; chooseTarget = den stald der mangler mest; validering
buildWorld()
addMarker(99, "Cow 2 Food", "Cow Food", 5200, 0); unloadAt[99] = {accepts = {[STRAW] = 300000, [TMR] = 300000}, levels = {[STRAW] = 0, [TMR] = 0}}
unloadAt[94].levels = {[STRAW] = 250000, [TMR] = 250000, [WATER] = 50000}
local multiVehicle = makeVehicle("vehicleMulti", "Multi", {STRAW, TMR, WATER}, 1000000, 100, 0)
local multi = ADRunsController.createRun("Kvæg")
ADRunsController.updateRun(multi.id, {targetWayPointIds = {94, 99}})
local multiRun = ADRuns.getRun(ADRunsController.store, multi.id)
check("C16 auto-varer = union (vand kun i stald 1)", table.concat(ADRunsController.getRunIngredients(adEnv, multiRun, multiVehicle), ","), WATER .. "," .. STRAW .. "," .. TMR)
check("C16 accepterede varer = union", #ADRunsController.getTargetAcceptedFillTypes(adEnv, multiRun), 3)
check("C16 vælger stald 2 (tom)", ADRunsController.chooseTarget(adEnv, multiRun, multiVehicle), 99)
unloadAt[99].levels = {[STRAW] = 290000, [TMR] = 290000}
check("C16 vælger stald 1 når stald 2 er fuld", ADRunsController.chooseTarget(adEnv, multiRun, multiVehicle), 94)
check("C16 validering ok med flere mål", ADRunsController.validateVehicle(adEnv, multiRun, multiVehicle).ok, true)
ADRunsController.updateRun(multi.id, {targetWayPointIds = {4242}})
check("C16 kun ukendte mål -> noTarget", ADRunsController.validateVehicle(adEnv, ADRuns.getRun(ADRunsController.store, multi.id), multiVehicle).reason, "noTarget")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
+26
View File
@@ -119,6 +119,7 @@ FS25_AutoDrive = {
end,
getWayPointById = function(_, id) return wayPoints[id] end,
getMapMarkers = function() return markers end,
getMapMarkerByWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id then return m end end end,
-- ruter: alt nåeligt medmindre testen sætter unreachable[id] (til eller fra)
unreachable = {},
getPathTo = function(self, vehicle, id) if self.unreachable[id] then return {} end; return {{id = id}} end,
@@ -897,6 +898,31 @@ UnloadTask.update(tt, 16)
check("I5 hjem når målet er under 20 %", tt.destinationID, 30)
ADRunsController.store = ADRuns.newStore()
-- I6: run med to stalde — ved turstart vælges den stald der mangler mest, AD's mål skiftes, og
-- varen hentes efter DEN stalds behov
buildCowWorld()
addMarker(31, "Cow 2 Food", "Cow Food", 6000, 0)
local cow2Levels = {[STRAW] = 95000, [TMR] = 95000}
table.insert(unloadStations, makeUnloadStation(131, 6000, 10, cow2Levels, {[STRAW] = 100000, [TMR] = 100000}, {name = "kostald 2"}))
cowLevels[STRAW], cowLevels[TMR] = 90000, 20000 -- stald 1: TMR mangler
local i6 = makeSupplyDriver(markerByIndex(21), markerByIndex(31), {TMR, STRAW}) -- AD peger på stald 2 (fuld)
i6.uniqueId = "vehicleI6"; i6.carriable = {TMR, STRAW}; i6.trailers = {level = 0, capacity = 8000}
i6.ad.stateModule.setSecondMarkerByWayPointId = function(self, id) self.secondMarkerId = id end
i6.ad.stateModule.getSecondMarker = function(self) return markerByIndex(self.secondMarkerId or 31) end
testVehicles = {i6}
ADRunsController.store = ADRuns.newStore()
local twoBarns = ADRunsController.createRun("Kvæg")
ADRunsController.updateRun(twoBarns.id, {targetWayPointIds = {30, 31}})
ADRunsController.assignVehicle(twoBarns.id, i6)
check("I6 henter TMR til stald 1", manager:getNextPickup(i6, false), 20)
check("I6 AD's mål skiftet til stald 1", i6.ad.stateModule.secondMarkerId, 30)
check("I6 skiftet logges", countLogsSince(1, "kører til Cow 1 Food") >= 1, true)
cowLevels[STRAW], cowLevels[TMR] = 99000, 99000
cow2Levels[STRAW] = 10000 -- nu mangler stald 2 halm
check("I6 næste tur: halm til stald 2", manager:getNextPickup(i6, false), 21)
check("I6 mål skiftet til stald 2", i6.ad.stateModule.secondMarkerId, 31)
ADRunsController.store = ADRuns.newStore()
-- ==== E: eksponerede opslag (bruges af runs-controller og læs-bytte) ====
local eDriver = buildCowWorld()
check("E1 aflæsningsstation ved waypoint", ADSmartPickup.getUnloadStationAtWayPointId(FS25_AutoDrive, 30, 1), unloadStations[1])