From bcd7de8344603d07edafc1a1b5cd584b40ffc54a Mon Sep 17 00:00:00 2001 From: masterdraco Date: Wed, 23 Sep 2026 01:56:47 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20flere=20m=C3=A5l=20pr.=20run=20(afkryds?= =?UTF-8?q?ning;=20den=20stald=20der=20mangler=20mest=20f=C3=A5r=20turen,?= =?UTF-8?q?=20AD's=20m=C3=A5l=20skiftes=20ved=20turstart)=20+=20under-fane?= =?UTF-8?q?r=20Overblik/Runs=20(v1.11.0.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FS25_ADSmartPickup/adRuns.lua | 47 ++++- FS25_ADSmartPickup/adRunsController.lua | 130 +++++++++----- FS25_ADSmartPickup/adRunsStorage.lua | 2 + FS25_ADSmartPickup/adSmartPickup.lua | 19 ++ FS25_ADSmartPickup/gui/SmartPickupFrame.lua | 190 ++++++++++++++------ FS25_ADSmartPickup/gui/SmartPickupFrame.xml | 16 +- FS25_ADSmartPickup/l10n/l10n_da.xml | 3 + FS25_ADSmartPickup/l10n/l10n_en.xml | 3 + FS25_ADSmartPickup/modDesc.xml | 2 +- tests/test_adRuns.lua | 19 ++ tests/test_adRunsController.lua | 32 +++- tests/test_adSmartPickup.lua | 26 +++ 12 files changed, 376 insertions(+), 113 deletions(-) diff --git a/FS25_ADSmartPickup/adRuns.lua b/FS25_ADSmartPickup/adRuns.lua index 1049b75..7794b3a 100644 --- a/FS25_ADSmartPickup/adRuns.lua +++ b/FS25_ADSmartPickup/adRuns.lua @@ -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 "", diff --git a/FS25_ADSmartPickup/adRunsController.lua b/FS25_ADSmartPickup/adRunsController.lua index 92c4a87..a48e5c7 100644 --- a/FS25_ADSmartPickup/adRunsController.lua +++ b/FS25_ADSmartPickup/adRunsController.lua @@ -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 ---- diff --git a/FS25_ADSmartPickup/adRunsStorage.lua b/FS25_ADSmartPickup/adRunsStorage.lua index b5bb783..7e167a2 100644 --- a/FS25_ADSmartPickup/adRunsStorage.lua +++ b/FS25_ADSmartPickup/adRunsStorage.lua @@ -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"), diff --git a/FS25_ADSmartPickup/adSmartPickup.lua b/FS25_ADSmartPickup/adSmartPickup.lua index d8ace0a..75b70ad 100644 --- a/FS25_ADSmartPickup/adSmartPickup.lua +++ b/FS25_ADSmartPickup/adSmartPickup.lua @@ -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 diff --git a/FS25_ADSmartPickup/gui/SmartPickupFrame.lua b/FS25_ADSmartPickup/gui/SmartPickupFrame.lua index 0d269bc..09db8bf 100644 --- a/FS25_ADSmartPickup/gui/SmartPickupFrame.lua +++ b/FS25_ADSmartPickup/gui/SmartPickupFrame.lua @@ -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 diff --git a/FS25_ADSmartPickup/gui/SmartPickupFrame.xml b/FS25_ADSmartPickup/gui/SmartPickupFrame.xml index 2e714b9..a10ce2a 100644 --- a/FS25_ADSmartPickup/gui/SmartPickupFrame.xml +++ b/FS25_ADSmartPickup/gui/SmartPickupFrame.xml @@ -10,19 +10,21 @@ - + + + + + + + - - - - - - diff --git a/FS25_ADSmartPickup/l10n/l10n_da.xml b/FS25_ADSmartPickup/l10n/l10n_da.xml index 18239bd..4110296 100644 --- a/FS25_ADSmartPickup/l10n/l10n_da.xml +++ b/FS25_ADSmartPickup/l10n/l10n_da.xml @@ -42,6 +42,9 @@ + + + diff --git a/FS25_ADSmartPickup/l10n/l10n_en.xml b/FS25_ADSmartPickup/l10n/l10n_en.xml index 8314503..797abca 100644 --- a/FS25_ADSmartPickup/l10n/l10n_en.xml +++ b/FS25_ADSmartPickup/l10n/l10n_en.xml @@ -42,6 +42,9 @@ + + + diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml index aab726e..17de29a 100644 --- a/FS25_ADSmartPickup/modDesc.xml +++ b/FS25_ADSmartPickup/modDesc.xml @@ -1,7 +1,7 @@ masterdraco - 1.10.3.0 + 1.11.0.0 <en>AD Smart Pickup</en> diff --git a/tests/test_adRuns.lua b/tests/test_adRuns.lua index dd7e175..c029923 100644 --- a/tests/test_adRuns.lua +++ b/tests/test_adRuns.lua @@ -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") diff --git a/tests/test_adRunsController.lua b/tests/test_adRunsController.lua index 7458e20..3a957df 100644 --- a/tests/test_adRunsController.lua +++ b/tests/test_adRunsController.lua @@ -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) diff --git a/tests/test_adSmartPickup.lua b/tests/test_adSmartPickup.lua index defa67c..ee5d497 100644 --- a/tests/test_adSmartPickup.lua +++ b/tests/test_adSmartPickup.lua @@ -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])