From 4c60fd555f440ba43e6c59a963cf4ad3415de992 Mon Sep 17 00:00:00 2001 From: masterdraco Date: Sat, 26 Sep 2026 18:09:12 +0200 Subject: [PATCH] =?UTF-8?q?feat(markarbejde):=20faste=20maskiner=20pr.=20m?= =?UTF-8?q?ark=20(bruges=20kun=20d=C3=A9r;=20marken=20venter=20p=C3=A5=20s?= =?UTF-8?q?in=20egen);=20fix:=20AD-p=C3=A5fyldning=20f=C3=A5r=20varen=20sa?= =?UTF-8?q?t=20(kom=20tom=20tilbage);=20AD=20der=20stopper=20undervejs=20s?= =?UTF-8?q?tartes=20igen=202x,=20ellers=20hjem=20til=20parkering=20(v1.30.?= =?UTF-8?q?0.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FS25_ADSmartPickup/adFieldFleet.lua | 37 +++++++++++++++++++ FS25_ADSmartPickup/adFieldModel.lua | 4 ++ FS25_ADSmartPickup/adFieldStorage.lua | 1 + FS25_ADSmartPickup/adFieldWork.lua | 30 +++++++++++++-- .../gui/SmartPickupFieldTab.lua | 37 ++++++++++++++++++- FS25_ADSmartPickup/l10n/l10n_da.xml | 2 + FS25_ADSmartPickup/l10n/l10n_en.xml | 2 + FS25_ADSmartPickup/modDesc.xml | 2 +- tests/test_adFieldFleet.lua | 17 +++++++++ tests/test_adFieldModel.lua | 3 +- 10 files changed, 129 insertions(+), 6 deletions(-) diff --git a/FS25_ADSmartPickup/adFieldFleet.lua b/FS25_ADSmartPickup/adFieldFleet.lua index abfa943..4908644 100644 --- a/FS25_ADSmartPickup/adFieldFleet.lua +++ b/FS25_ADSmartPickup/adFieldFleet.lua @@ -78,6 +78,43 @@ end -- Nærmeste brugbare sæt. Grund når intet passer: noRig (ingen med flueben og rollen), cannotDo (ingen kan -- tage opgaven, fx afgrøden), allBusy, tooWide/tooNarrow (bredden, med tal fra den første). -- accept (valgfri): candidate -> bool, fx "høsteren kan tage hvede". +-- Faste maskiner: rigIdsByField = {[fieldId] = {id, ...}}. En maskine der er fast på en ANDEN mark må +-- ikke bruges her. Har marken selv faste maskiner med rollen, bruges kun de (marken venter på sin egen); +-- ellers de frie. (in-game 26/9: sprøjten "til F22" blev sendt til F109.) +function ADFieldFleet.filterDedicated(candidates, fieldId, role, rigIdsByField) + local ownerOf, own = {}, {} + for otherId, ids in pairs(rigIdsByField or {}) do + for _, id in ipairs(ids or {}) do + ownerOf[id] = otherId + if otherId == fieldId then own[id] = true end + end + end + local ownWithRole = {} + for _, candidate in ipairs(candidates or {}) do + local hasRole = candidate.role == role or (candidate.roles ~= nil and candidate.roles[role]) + if own[candidate.id] and hasRole then table.insert(ownWithRole, candidate) end + end + if #ownWithRole > 0 then return ownWithRole end + local free = {} + for _, candidate in ipairs(candidates or {}) do + if ownerOf[candidate.id] == nil then table.insert(free, candidate) end + end + return free +end + +-- Gør en maskine fast på én mark (fjernes fra andre marker). Returnerer ny tabel {[fieldId] = ids}. +function ADFieldFleet.dedicate(rigIdsByField, fieldId, vehicleId, isOn) + local result = {} + for otherId, ids in pairs(rigIdsByField or {}) do + local copy = {} + for _, id in ipairs(ids or {}) do if id ~= vehicleId then table.insert(copy, id) end end + result[otherId] = copy + end + result[fieldId] = result[fieldId] or {} + if isOn then table.insert(result[fieldId], vehicleId) end + return result +end + function ADFieldFleet.pick(candidates, role, x, z, requiredWidth, accept) local usable, reason, args = sortCandidates(candidates, role, x, z, requiredWidth, accept) if usable[1] ~= nil then return usable[1].candidate end diff --git a/FS25_ADSmartPickup/adFieldModel.lua b/FS25_ADSmartPickup/adFieldModel.lua index 9fb3ca5..79be375 100644 --- a/FS25_ADSmartPickup/adFieldModel.lua +++ b/FS25_ADSmartPickup/adFieldModel.lua @@ -61,6 +61,8 @@ local function copyPlan(plan) for key, value in pairs(DEFAULT_PLAN) do copy[key] = value end for key, value in pairs(plan or {}) do copy[key] = value end copy.storeBuildingIds = copyList(plan ~= nil and plan.storeBuildingIds or nil) + -- faste maskiner (køretøjs-id'er) til marken; en fast maskine bruges kun her + copy.rigIds = copyList(plan ~= nil and plan.rigIds or nil) return copy end @@ -448,6 +450,7 @@ function ADFieldModel.toRecord(field) return { id = field.id, stage = field.stage, mode = plan.mode, product = plan.product, rake = flag(plan.rake), store = table.concat(plan.storeBuildingIds, " "), + dedicated = table.concat(plan.rigIds or {}, " "), rigs = plan.rigs, unloaders = plan.unloaders, mulch = flag(plan.mulch), lime = flag(plan.lime), soil = plan.soil, sow = flag(plan.sow), sowFruit = plan.sowFruit or "", roll = flag(plan.roll), fertilize = flag(plan.fertilize), weed = flag(plan.weed), @@ -479,6 +482,7 @@ function ADFieldModel.fromRecord(record) plan.product = PRODUCTS[record.product] and record.product or "auto" plan.rake = readFlag(record.rake, false) for id in string.gmatch(record.store or "", "%S+") do table.insert(plan.storeBuildingIds, id) end + for id in string.gmatch(record.dedicated or "", "%S+") do table.insert(plan.rigIds, id) end plan.rigs = clamp(record.rigs or 1, 1, ADFieldModel.MAX_RIGS) plan.unloaders = clamp(record.unloaders or 1, 1, ADFieldModel.MAX_UNLOADERS) plan.mulch = readFlag(record.mulch, false) diff --git a/FS25_ADSmartPickup/adFieldStorage.lua b/FS25_ADSmartPickup/adFieldStorage.lua index 9d7f2cd..d6cd50d 100644 --- a/FS25_ADSmartPickup/adFieldStorage.lua +++ b/FS25_ADSmartPickup/adFieldStorage.lua @@ -13,6 +13,7 @@ ADFieldStorage.FIELD_TYPES = { sowFruit = "String", roll = "Int", fertilize = "Int", weed = "Int", kind = "String", lastFruit = "String", care = "String", targetFruit = "String", prepAfterMow = "Int", straw = "String", mowWidth = "Float", mowVehicle = "String", rakeVehicle = "String", harvestVehicle = "String", windrowOffset = "String", + dedicated = "String", } function ADFieldStorage.getPath() diff --git a/FS25_ADSmartPickup/adFieldWork.lua b/FS25_ADSmartPickup/adFieldWork.lua index 9a513bc..e15fbbb 100644 --- a/FS25_ADSmartPickup/adFieldWork.lua +++ b/FS25_ADSmartPickup/adFieldWork.lua @@ -15,6 +15,8 @@ ADFieldWork.PLAYER_DRIVING_KMH = 3 ADFieldWork.WINDROW_MEASURE_MS = 10000 -- mangler der sæt, prøves igen efter så lang tid (ikke hvert sekund over hele flåden) ADFieldWork.NO_RIG_RETRY_MS = 30000 +-- AutoDrive der stopper før markøren startes igen så mange gange, før turen opgives +ADFieldWork.DRIVE_RETRIES = 2 ADFieldWork.REFILL_ROLES = {seeder = true, fertilizer = true, limer = true, weeder = true} local function log(...) ADFieldJobs.log(...) end @@ -57,12 +59,18 @@ local function helperCp(adEnv) return adEnv.ADStateModule ~= nil and adEnv.ADStateModule.HELPER_CP or 1 end -local function startAd(adEnv, vehicle, mode, firstId, secondId) +-- fillType (valgfri, LOAD): varen AutoDrive skal læsse — uden den kører AD til påfyldningen og hjem tom +-- (in-game 26/9: sprøjten kom tom tilbage fra Buying Station Multifruit) +local function startAd(adEnv, vehicle, mode, firstId, secondId, fillType) local module = stateModule(vehicle) stopAd(vehicle) module:setMode(mode) module:setFirstMarkerByWayPointId(firstId) if secondId ~= nil then module:setSecondMarkerByWayPointId(secondId) end + if fillType ~= nil then + module:setFillType(fillType) + module.selectedFillTypes = {fillType} + end module:setStartHelper(false) if module.setActualFarmId ~= nil then module:setActualFarmId(vehicle:getOwnerFarmId()) end module:raiseDirtyFlag() @@ -316,7 +324,10 @@ function ADFieldWork.dispatch(adEnv, field, info, decision) note(field.id, {retryAt = now() + ADFieldModel.RETRY_MS, reason = reason}) return end - local candidates = ADFieldWork.getRigs(adEnv) + -- faste maskiner: andre markers faste bruges ikke; har marken selv faste med rollen, kun dem + local rigIdsByField = {} + for fieldId, other in pairs(ADFieldJobs.state.fields) do rigIdsByField[fieldId] = other.plan ~= nil and other.plan.rigIds or nil end + local candidates = ADFieldFleet.filterDedicated(ADFieldWork.getRigs(adEnv), field.id, role, rigIdsByField) local required = ADFieldModel.requiredWidth(field, role) local accept = acceptFor(field, info, role) local rigs, why, args @@ -349,12 +360,14 @@ function ADFieldWork.dispatch(adEnv, field, info, decision) ADFieldJobs.state.assignments[rig.id] = assignment if refill ~= nil and refill.empty then -- AutoDrive LOAD: påfyldningen (2. markør), fyld op, videre til F-markøren (1.) — så Courseplay - startAd(adEnv, rig.vehicle, adEnv.AutoDrive.MODE_LOAD, info.markerId, refill.refillId) + startAd(adEnv, rig.vehicle, adEnv.AutoDrive.MODE_LOAD, info.markerId, refill.refillId, refill.fillType) + assignment.drive = {mode = adEnv.AutoDrive.MODE_LOAD, first = info.markerId, second = refill.refillId, fillType = refill.fillType} local marker = adEnv.ADGraphManager:getMapMarkerByWayPointId(refill.refillId) log(field.id, "%s '%s' er tom — fyldes med %s ved %s og kører så til %s", role, rig.label, fillTypeName(refill.fillType), tostring(marker ~= nil and marker.name or refill.refillId), tostring(info.markerName)) else startAd(adEnv, rig.vehicle, adEnv.AutoDrive.MODE_DRIVETO, info.markerId, nil) + assignment.drive = {mode = adEnv.AutoDrive.MODE_DRIVETO, first = info.markerId} log(field.id, "%s '%s' sendt til %s (%.1f m%s)", role, rig.label, tostring(info.markerName), rig.width or 0, assignment.lane ~= nil and string.format(", bane %d", assignment.lane) or "") end @@ -751,9 +764,20 @@ function ADFieldWork.follow(adEnv, assignment) local distance = wayPoint ~= nil and math.sqrt((wayPoint.x - x) ^ 2 + (wayPoint.z - z) ^ 2) or math.huge if distance <= ADFieldWork.ARRIVE_DISTANCE then beginWork(adEnv, field, info, assignment) + elseif assignment.drive ~= nil and (assignment.driveRetries or 0) < ADFieldWork.DRIVE_RETRIES then + -- AutoDrive stoppede undervejs (spilleren byggede, trafik, ...): kør videre i stedet for at opgive + assignment.driveRetries = (assignment.driveRetries or 0) + 1 + assignment.since = now() + local d = assignment.drive + startAd(adEnv, vehicle, d.mode, d.first, d.second, d.fillType) + log(assignment.fieldId, "'%s' stoppede %.0f m fra %s — AutoDrive startes igen (%d/%d)", ADRuns.vehicleLabel(vehicle), distance, + tostring(info ~= nil and info.markerName or "?"), assignment.driveRetries, ADFieldWork.DRIVE_RETRIES) else ADFieldWork.release(assignment) fail(assignment.fieldId, string.format("%s nåede ikke %s (%.0f m væk)", ADRuns.vehicleLabel(vehicle), tostring(info ~= nil and info.markerName or "?"), distance)) + -- ikke efterladt midt på vejen/ved en mark: hjem til parkeringen + local park = parkMarkerId(adEnv, vehicle) + if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end end end elseif assignment.phase == "waitingCourse" then diff --git a/FS25_ADSmartPickup/gui/SmartPickupFieldTab.lua b/FS25_ADSmartPickup/gui/SmartPickupFieldTab.lua index c4d8eef..46ba6a7 100644 --- a/FS25_ADSmartPickup/gui/SmartPickupFieldTab.lua +++ b/FS25_ADSmartPickup/gui/SmartPickupFieldTab.lua @@ -9,7 +9,7 @@ SmartPickupFrame.FIELD_ROW_KINDS = { fieldProduct = true, fieldMode = true, fieldRake = true, fieldStore = true, fleetVehicle = true, fieldRigs = true, fieldUnloaders = true, fieldMulch = true, fieldLime = true, fieldSoil = true, fieldSow = true, fieldSowFruit = true, fieldRoll = true, fieldFertilize = true, fieldWeed = true, - fieldTarget = true, fleetMode = true, fieldStraw = true, + fieldTarget = true, fleetMode = true, fieldStraw = true, fieldRig = true, } SmartPickupFrame.ROLE_KEYS = { mower = "spu_field_role_mower", tedder = "spu_field_role_tedder", rake = "spu_field_role_rake", @@ -277,6 +277,7 @@ function SmartPickupFrame:fillFieldStoreRows(adEnv, info, field) fillType = info.harvestFillType end if fillType == nil then return end + self:fillDedicatedRows(adEnv, field) self:addTextRow(i18n:getText("spu_field_section_store")) local chosen = {} for _, id in ipairs(field.plan.storeBuildingIds) do chosen[id] = true end @@ -358,6 +359,31 @@ function SmartPickupFrame:rigDetails(rig) return #parts > 0 and (" — " .. table.concat(parts, " — ")) or "" end +-- Faste maskiner til den valgte mark: en fast maskine bruges kun her, og marken venter på sin egen hellere +-- end at låne en anden med samme opgave. Maskiner der er faste på en anden mark er markeret. +function SmartPickupFrame:fillDedicatedRows(adEnv, field) + local i18n = self.i18n + self:addTextRow(i18n:getText("spu_field_section_dedicated")) + local owner = {} + for fieldId, other in pairs(ADFieldJobs.state.fields) do + for _, id in ipairs(other.plan ~= nil and other.plan.rigIds or {}) do owner[id] = fieldId end + end + local rigs = ADFieldJobs.getRigs(adEnv) + table.sort(rigs, function(a, b) + local ma, mb = owner[a.id] == field.id, owner[b.id] == field.id + if ma ~= mb then return ma end + if a.role ~= b.role then return a.role < b.role end + return ADFlowView.naturalLess(a.label, b.label) + end) + for _, rig in ipairs(rigs) do + local row = self:addRow(self.vehiclePrefab, {kind = "fieldRig", vehicleId = rig.id}) + local elsewhere = owner[rig.id] ~= nil and owner[rig.id] ~= field.id + and (" — " .. string.format(i18n:getText("spu_field_dedicatedElsewhere"), tostring(owner[rig.id]))) or "" + row:getDescendantByName("title"):setText(string.format(" %s — %s%s", tostring(rig.label), self:roleText(rig.role), elsewhere)) + row:getDescendantByName("option"):setState(owner[rig.id] == field.id and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false) + end +end + function SmartPickupFrame:fillFleetRows(adEnv) local i18n = self.i18n self:addTextRow(i18n:getText("spu_field_section_fleet")) @@ -424,6 +450,15 @@ function SmartPickupFrame:onClickFieldRow(state, data) ADFieldJobs.setPlan(id, {mode = isOn and "auto" or "manual"}) elseif SmartPickupFrame.PLAN_TOGGLES[data.kind] ~= nil then ADFieldJobs.setPlan(id, {[SmartPickupFrame.PLAN_TOGGLES[data.kind]] = isOn}) + elseif data.kind == "fieldRig" then + -- fast på denne mark (og fjernes fra en anden mark) + local byField = {} + for fieldId, other in pairs(ADFieldJobs.state.fields) do byField[fieldId] = other.plan ~= nil and other.plan.rigIds or {} end + local updated = ADFieldFleet.dedicate(byField, id, data.vehicleId, isOn) + for fieldId, ids in pairs(updated) do + local before = table.concat(byField[fieldId] or {}, " ") + if before ~= table.concat(ids, " ") then ADFieldJobs.setPlan(fieldId, {rigIds = ids}) end + end elseif data.kind == "fieldStore" then local ids = {} for _, buildingId in ipairs(ADFieldJobs.getField(id).plan.storeBuildingIds) do diff --git a/FS25_ADSmartPickup/l10n/l10n_da.xml b/FS25_ADSmartPickup/l10n/l10n_da.xml index 03c4caf..ebe82fc 100644 --- a/FS25_ADSmartPickup/l10n/l10n_da.xml +++ b/FS25_ADSmartPickup/l10n/l10n_da.xml @@ -80,6 +80,8 @@ + + diff --git a/FS25_ADSmartPickup/l10n/l10n_en.xml b/FS25_ADSmartPickup/l10n/l10n_en.xml index 38861ec..2cbae9f 100644 --- a/FS25_ADSmartPickup/l10n/l10n_en.xml +++ b/FS25_ADSmartPickup/l10n/l10n_en.xml @@ -80,6 +80,8 @@ + + diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml index 4b6982a..a5e4d7b 100644 --- a/FS25_ADSmartPickup/modDesc.xml +++ b/FS25_ADSmartPickup/modDesc.xml @@ -1,7 +1,7 @@ masterdraco - 1.29.6.0 + 1.30.0.0 <en>AD Smart Pickup</en> diff --git a/tests/test_adFieldFleet.lua b/tests/test_adFieldFleet.lua index 1ca86de..2f4382a 100644 --- a/tests/test_adFieldFleet.lua +++ b/tests/test_adFieldFleet.lua @@ -105,5 +105,22 @@ check("FLS gødskning: tom før nærmere med kalk på", F.pick(spreaders, "ferti check("FLS kun kalk-sprederen: bruges (tømmes)", F.pick({spreaders[1]}, "fertilizer", 0, 0).id, "kalk") check("FLS kalkning: den med kalk på foretrækkes", F.pick(spreaders, "limer", 0, 0).id, "kalk") +-- FLD: faste maskiner pr. mark +local sprayA = {id = "sprøjte", role = "fertilizer"} +local sprayB = {id = "spreder", role = "fertilizer"} +local harve = {id = "harve", role = "cultivator"} +local all = {sprayA, sprayB, harve} +local byField = {[22] = {"sprøjte"}} +local function ids(list) local out = {} for _, c in ipairs(list) do table.insert(out, c.id) end return table.concat(out, ",") end +check("FLD F22 gødskning: kun sin faste", ids(F.filterDedicated(all, 22, "fertilizer", byField)), "sprøjte") +check("FLD F109 gødskning: ikke F22's", ids(F.filterDedicated(all, 109, "fertilizer", byField)), "spreder,harve") +check("FLD F22 kultivering: ingen fast med rollen -> frie", ids(F.filterDedicated(all, 22, "cultivator", byField)), "spreder,harve") +check("FLD uden faste = alle", ids(F.filterDedicated(all, 5, "fertilizer", {})), "sprøjte,spreder,harve") +local moved = F.dedicate(byField, 109, "sprøjte", true) +check("FLD flyt: væk fra F22", #moved[22], 0) +check("FLD flyt: fast på F109", moved[109][1], "sprøjte") +check("FLD original urørt", byField[22][1], "sprøjte") +check("FLD fjern", #F.dedicate(moved, 109, "sprøjte", false)[109], 0) + print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) os.exit(failures == 0 and 0 or 1) diff --git a/tests/test_adFieldModel.lua b/tests/test_adFieldModel.lua index f6e8ad7..23d1f0d 100644 --- a/tests/test_adFieldModel.lua +++ b/tests/test_adFieldModel.lua @@ -90,7 +90,7 @@ check("M7 wantsRig klar = nil", M.wantsRig(ready, 0, 0), nil) -- M8: gem/indlæs bevarer trin, plan og slåbredde local saved = M.advance(M.start(M.onReadiness(M.newField(109), 1))); saved.mowWidth = 40.8; saved.mowVehicleId = "vehicleBigM" -saved.plan = {mode = "auto", product = "hay", rake = true, storeBuildingIds = {"b12", "b7"}} +saved.plan = {mode = "auto", product = "hay", rake = true, storeBuildingIds = {"b12", "b7"}, rigIds = {"v1", "v2"}} local back = M.fromRecord(M.toRecord(saved)) check("M8 id", back.id, 109) check("M8 stage", back.stage, saved.stage) @@ -98,6 +98,7 @@ check("M8 mode", back.plan.mode, "auto") check("M8 product", back.plan.product, "hay") check("M8 rake", back.plan.rake, true) check("M8 lager", table.concat(back.plan.storeBuildingIds, " "), "b12 b7") +check("M8 faste maskiner", table.concat(back.plan.rigIds, " "), "v1 v2") check("M8 slåbredde", back.mowWidth, 40.8) check("M8 slåmaskine", back.mowVehicleId, "vehicleBigM") check("M8 ingen slåmaskine", M.fromRecord({id = 4, stage = "idle"}).mowVehicleId, nil)