diff --git a/FS25_ADSmartPickup/adFieldJobs.lua b/FS25_ADSmartPickup/adFieldJobs.lua index 8d3f452..36c2308 100644 --- a/FS25_ADSmartPickup/adFieldJobs.lua +++ b/FS25_ADSmartPickup/adFieldJobs.lua @@ -14,8 +14,8 @@ ADFieldJobs.PLAYER_DRIVING_KMH = 3 -- Courseplay kan efterlade hjørner på skæve marker: efter så mange færdige slå-ture går marken videre. ADFieldJobs.MAX_MOW_PASSES = 2 ADFieldJobs.state = {fields = {}, fleet = {}, assignments = {}} --- mark-id -> kopi af slåmaskinens Courseplay-kursus (kun i hukommelsen; efter genindlæsning hentes det --- igen fra slåmaskinen, som CP selv gemmer kurset på) +-- mark-id -> {mower = kursus, rake = kursus}: kopier af slåmaskinens/rivens Courseplay-kursus (kun i +-- hukommelsen; efter genindlæsning hentes de fra maskinen, som CP selv gemmer kurset på) ADFieldJobs.courses = {} ADFieldJobs.timer = 0 ADFieldJobs.now = 0 @@ -246,7 +246,7 @@ end local function dispatch(adEnv, field, info, role) local candidates = ADFieldJobs.getRigs(adEnv) - local rig, reason, args = ADFieldFleet.pick(candidates, role, info.x, info.z, field.mowWidth) + local rig, reason, args = ADFieldFleet.pick(candidates, role, info.x, info.z, ADFieldModel.requiredWidth(field, role)) if rig == nil then note(field.id, {reason = {key = "spu_field_reason_" .. reason, role = role, args = args}}) return @@ -258,18 +258,32 @@ local function dispatch(adEnv, field, info, role) note(field.id, {reason = false}) end --- Slåkurset for marken: gemt kopi, ellers slåmaskinens nuværende kursus (efter genindlæsning). -local function mowCourse(adEnv, field) - if ADFieldJobs.courses[field.id] ~= nil then return ADFieldJobs.courses[field.id] end - if field.mowVehicleId == nil then return nil end +-- Kurset som et trin skal genbruge ("mower"/"rake"): gemt kopi, ellers maskinens nuværende kursus +-- (efter genindlæsning). +local function sourceCourse(adEnv, field, source) + local saved = ADFieldJobs.courses[field.id] + if saved ~= nil and saved[source] ~= nil then return saved[source] end + local vehicleId = source == "rake" and field.rakeVehicleId or field.mowVehicleId + if vehicleId == nil then return nil end for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do - if ADRunsController.getVehicleId(vehicle) == field.mowVehicleId then + if ADRunsController.getVehicleId(vehicle) == vehicleId then return ADCpBridge.getCourse(vehicle) end end return nil end +local function rememberCourse(field, source, vehicle, vehicleId) + local course = ADCpBridge.getCourse(vehicle) + local ok, copy = false, nil + if course ~= nil and course.copy ~= nil then ok, copy = pcall(course.copy, course) end + ADFieldJobs.courses[field.id] = ADFieldJobs.courses[field.id] or {} + ADFieldJobs.courses[field.id][source] = ok and copy or course + if source == "rake" then + note(field.id, {rakeVehicleId = vehicleId}) + end +end + local function beginWork(adEnv, field, info, assignment) local vehicle = assignment.vehicle if assignment.role == "collector" then @@ -298,9 +312,10 @@ local function beginWork(adEnv, field, info, assignment) local point = info.points[1] or {x = info.x, z = info.z} assignment.phase = "starting" assignment.since = ADFieldJobs.now - local course = assignment.role ~= "mower" and mowCourse(adEnv, field) or nil - if assignment.role ~= "mower" and course == nil then - log(field.id, "intet slåkursus kendt — Courseplay laver et nyt kursus til %s", assignment.role) + local source = ADFieldModel.courseSource(field, assignment.role) + local course = source ~= nil and sourceCourse(adEnv, field, source) or nil + if source ~= nil and course == nil then + log(field.id, "intet %s-kursus kendt — Courseplay laver et nyt kursus til %s", source, assignment.role) end local handle, err = ADCpBridge.start(vehicle, point.x, point.z, function(ok, reason) if ok then @@ -308,11 +323,11 @@ local function beginWork(adEnv, field, info, assignment) assignment.since = ADFieldJobs.now if assignment.role == "mower" then note(field.id, {mowWidth = assignment.width, mowVehicleId = assignment.vehicleId}) - local mowed = ADCpBridge.getCourse(vehicle) - local copied = mowed ~= nil and mowed.copy ~= nil and select(2, pcall(mowed.copy, mowed)) or mowed - ADFieldJobs.courses[field.id] = type(copied) == "table" and copied or mowed + rememberCourse(field, "mower", vehicle, assignment.vehicleId) + elseif assignment.role == "rake" then + rememberCourse(field, "rake", vehicle, assignment.vehicleId) end - log(field.id, "Courseplay startet (%s%s)", assignment.role, reason == "copied" and ", slåkursus kopieret" or "") + log(field.id, "Courseplay startet (%s%s)", assignment.role, reason == "copied" and (", " .. tostring(source) .. "-kursus kopieret") or "") else release(assignment) note(field.id, {retryAt = ADFieldJobs.now + ADFieldModel.RETRY_MS, reason = {text = tostring(reason)}}) diff --git a/FS25_ADSmartPickup/adFieldModel.lua b/FS25_ADSmartPickup/adFieldModel.lua index 5516963..21aa0ed 100644 --- a/FS25_ADSmartPickup/adFieldModel.lua +++ b/FS25_ADSmartPickup/adFieldModel.lua @@ -38,14 +38,30 @@ function ADFieldModel.newField(id) } end +-- Hø vendes; Riv kan vælges til både hø og ensilage (samler skårene midt i rivens baner, så også +-- smalle standard-læssevogne kan tage dem). function ADFieldModel.workStages(plan) - if plan.product == "hay" then - if plan.rake then - return {"mowing", "tedding", "raking", "collecting"} - end - return {"mowing", "tedding", "collecting"} + local stages = {"mowing"} + if plan.product == "hay" then table.insert(stages, "tedding") end + if plan.rake then table.insert(stages, "raking") end + table.insert(stages, "collecting") + return stages +end + +-- Krævet bredde for et sæt: kun læssevognen uden riv skal dække slåmaskinens baner. Vender og river kører +-- hele marken i deres egen bredde; efter riv ligger skåret midt i banen, så vognens bredde er ligegyldig. +function ADFieldModel.requiredWidth(field, role) + if role == "collector" and not field.plan.rake then + return field.mowWidth end - return {"mowing", "collecting"} + return nil +end + +-- Hvilket tidligere kursus sættet skal køre: læssevognen følger skårene (rivens kursus, ellers +-- slåmaskinens); de andre laver deres eget i deres egen bredde. +function ADFieldModel.courseSource(field, role) + if role ~= "collector" then return nil end + return field.plan.rake and "rake" or "mower" end function ADFieldModel.roleFor(stage) @@ -93,6 +109,7 @@ function ADFieldModel.start(field) copy.reason = nil copy.mowWidth = nil copy.mowVehicleId = nil + copy.rakeVehicleId = nil return copy end @@ -158,6 +175,7 @@ function ADFieldModel.toRecord(field) rake = field.plan.rake and 1 or 0, store = table.concat(field.plan.storeBuildingIds, " "), mowWidth = field.mowWidth or 0, mowVehicle = field.mowVehicleId or "", + rakeVehicle = field.rakeVehicleId or "", } end @@ -170,5 +188,6 @@ function ADFieldModel.fromRecord(record) for id in string.gmatch(record.store or "", "%S+") do table.insert(field.plan.storeBuildingIds, id) end field.mowWidth = (record.mowWidth or 0) > 0 and record.mowWidth or nil field.mowVehicleId = record.mowVehicle ~= nil and record.mowVehicle ~= "" and record.mowVehicle or nil + field.rakeVehicleId = record.rakeVehicle ~= nil and record.rakeVehicle ~= "" and record.rakeVehicle or nil return field end diff --git a/FS25_ADSmartPickup/adFieldStorage.lua b/FS25_ADSmartPickup/adFieldStorage.lua index d0f24b3..9209aa0 100644 --- a/FS25_ADSmartPickup/adFieldStorage.lua +++ b/FS25_ADSmartPickup/adFieldStorage.lua @@ -29,6 +29,7 @@ function ADFieldStorage.save(state) xmlFile:setString(key .. "#store", record.store) xmlFile:setFloat(key .. "#mowWidth", record.mowWidth) xmlFile:setString(key .. "#mowVehicle", record.mowVehicle) + xmlFile:setString(key .. "#rakeVehicle", record.rakeVehicle) index = index + 1 end local vehicleIndex = 0 @@ -53,6 +54,7 @@ function ADFieldStorage.load() mode = xmlFile:getString(key .. "#mode"), product = xmlFile:getString(key .. "#product"), rake = xmlFile:getInt(key .. "#rake"), store = xmlFile:getString(key .. "#store"), mowWidth = xmlFile:getFloat(key .. "#mowWidth"), mowVehicle = xmlFile:getString(key .. "#mowVehicle"), + rakeVehicle = xmlFile:getString(key .. "#rakeVehicle"), }) if field.id ~= nil then result.fields[field.id] = field end end) diff --git a/FS25_ADSmartPickup/gui/SmartPickupFieldTab.lua b/FS25_ADSmartPickup/gui/SmartPickupFieldTab.lua index 2c6b5d0..5e25a29 100644 --- a/FS25_ADSmartPickup/gui/SmartPickupFieldTab.lua +++ b/FS25_ADSmartPickup/gui/SmartPickupFieldTab.lua @@ -36,7 +36,7 @@ end function SmartPickupFrame:fieldPlanText(field) local text = self.i18n:getText(field.plan.product == "hay" and "spu_field_plan_hay" or "spu_field_plan_silage") - if field.plan.product == "hay" and field.plan.rake then + if field.plan.rake then text = text .. " + " .. self.i18n:getText("spu_field_plan_rake") end return text @@ -102,9 +102,7 @@ function SmartPickupFrame:fillFieldDetail(adEnv, info) local isHay = field.plan.product == "hay" self:addToggleRow("fieldProduct", i18n:getText("spu_field_plan"), {i18n:getText("spu_field_plan_silage"), i18n:getText("spu_field_plan_hay")}, isHay) self:addToggleRow("fieldMode", i18n:getText("spu_field_mode"), {i18n:getText("spu_field_mode_manual"), i18n:getText("spu_field_mode_auto")}, field.plan.mode == "auto") - if isHay then - self:addToggleRow("fieldRake", i18n:getText("spu_field_rake"), {i18n:getText("ui_no"), i18n:getText("ui_yes")}, field.plan.rake == true) - end + self:addToggleRow("fieldRake", i18n:getText("spu_field_rake"), {i18n:getText("ui_no"), i18n:getText("ui_yes")}, field.plan.rake == true) if info.markerId ~= nil then self:addTextRow(string.format(i18n:getText("spu_field_marker"), tostring(info.markerName), info.markerDistance or 0)) else diff --git a/FS25_ADSmartPickup/l10n/l10n_da.xml b/FS25_ADSmartPickup/l10n/l10n_da.xml index 69882ec..8a0db00 100644 --- a/FS25_ADSmartPickup/l10n/l10n_da.xml +++ b/FS25_ADSmartPickup/l10n/l10n_da.xml @@ -53,7 +53,7 @@ - + @@ -74,7 +74,7 @@ - + diff --git a/FS25_ADSmartPickup/l10n/l10n_en.xml b/FS25_ADSmartPickup/l10n/l10n_en.xml index 5567613..0217b8b 100644 --- a/FS25_ADSmartPickup/l10n/l10n_en.xml +++ b/FS25_ADSmartPickup/l10n/l10n_en.xml @@ -53,7 +53,7 @@ - + @@ -74,7 +74,7 @@ - + diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml index bd86249..c367485 100644 --- a/FS25_ADSmartPickup/modDesc.xml +++ b/FS25_ADSmartPickup/modDesc.xml @@ -1,7 +1,7 @@ masterdraco - 1.22.0.0 + 1.23.0.0 <en>AD Smart Pickup</en> diff --git a/docs/BRUGERMANUAL.md b/docs/BRUGERMANUAL.md index a5dfcca..9002b90 100644 --- a/docs/BRUGERMANUAL.md +++ b/docs/BRUGERMANUAL.md @@ -1,6 +1,6 @@ # AD Smart Pickup — brugermanual -Version 1.22.0 · Farming Simulator 25 · tillæg til AutoDrive +Version 1.23.0 · Farming Simulator 25 · tillæg til AutoDrive *English version: [USER_MANUAL.md](USER_MANUAL.md)* @@ -531,7 +531,7 @@ hvilket trin og hvilken maskine. Kræver **Courseplay** ud over AutoDrive. | Plan | Trin | |---|---| -| **Ensilage** | Slås → Samles op → silo | +| **Ensilage** | Slås → (Rives) → Samles op → silo | | **Hø** | Slås → Vendes → (Rives) → Samles op → silo | ### Opsætning @@ -551,9 +551,12 @@ hvilket trin og hvilken maskine. Kræver **Courseplay** ud over AutoDrive. - Courseplay kan højst køre **50 m** bredt. Et sæt over 50 m sendes ikke ud (`!! over 50 m` i markflåden). -- Moden husker, hvor bredt marken blev **slået**, og **slåmaskinens Courseplay-kursus**. Vender, river og - læssevogn kører præcis samme kursus (kopieret), så de rammer skårene. De skal være **mindst lige så - brede** som slåmaskinen. Med Unrealistic Pack: sæt slåmaskinen og +- **Vender og river** kører hele marken i deres egen bredde med deres eget kursus. De må gerne være + smallere end slåmaskinen. +- **Læssevognen** skal ramme skårene og kører derfor et kopieret kursus: + - **uden Riv:** slåmaskinens kursus, og vognen skal være **mindst lige så bred som slåbredden**. + - **med Riv:** rivens kursus. Skåret ligger midt i hver rivebane, så vognens bredde er ligegyldig. +- Har du kun smalle standardvogne, så slå **Riv** til på marken (også ved ensilage). Med Unrealistic Pack: sæt slåmaskinen og læssevognene til samme bredde, fx 40,8 m. ### En mark @@ -564,7 +567,7 @@ Klik på en mark i tabellen for at vælge den. Under tabellen står: |---|---| | **Plan** | Ensilage eller Hø | | **Kørsel** | *Manuel*: marken venter på **Start nu**. *Auto*: marken starter selv, når den er klar | -| **Riv** | Kun ved hø: riv høet sammen før opsamling. Standard fra, fordi brede læssevogne samler spredt hø | +| **Riv** | Riv skårene sammen før opsamling (hø og ensilage). Standard fra. Slå til ved smalle læssevogne | | **Tving lager** | Vælg siloer. Uden valg bruges nærmeste silo med plads, der tager varen | En mark er **klar**, når mindst 90 % af den står i høstvinduet. Status viser andelen, fx `Venter 72 %`. diff --git a/docs/USER_MANUAL.md b/docs/USER_MANUAL.md index 09e2308..ef9ff9b 100644 --- a/docs/USER_MANUAL.md +++ b/docs/USER_MANUAL.md @@ -1,6 +1,6 @@ # AD Smart Pickup — user manual -Version 1.22.0 · Farming Simulator 25 · add-on for AutoDrive +Version 1.23.0 · Farming Simulator 25 · add-on for AutoDrive *Dansk version: [BRUGERMANUAL.md](BRUGERMANUAL.md)* @@ -537,7 +537,7 @@ which step and which machine. Requires **Courseplay** in addition to AutoDrive. | Plan | Steps | |---|---| -| **Silage** | Mow → Collect → silo | +| **Silage** | Mow → (Rake) → Collect → silo | | **Hay** | Mow → Ted → (Rake) → Collect → silo | ### Setup @@ -557,9 +557,13 @@ which step and which machine. Requires **Courseplay** in addition to AutoDrive. - Courseplay can work at most **50 m** wide. A rig over 50 m is not sent out (`!! over 50 m` in the fleet). -- The mod remembers how wide the field was **mowed**, and the **mower's Courseplay course**. Tedder, - rake and loading wagon drive exactly that course (copied), so they hit the swaths. They must be **at - least as wide** as the mower. With Unrealistic Pack: set the mower and the +- **Tedder and rake** work the whole field at their own width with their own course. They may be + narrower than the mower. +- **The loading wagon** must hit the swaths, so it drives a copied course: + - **without Rake:** the mower's course, and the wagon must be **at least as wide as the mowing width**. + - **with Rake:** the rake's course. The swath lies in the middle of each rake lane, so the wagon's width + does not matter. +- If you only have narrow standard wagons, turn **Rake** on for the field (also for silage). With Unrealistic Pack: set the mower and the loading wagons to the same width, such as 40.8 m. ### A field @@ -570,7 +574,7 @@ Click a field in the table to select it. Below the table: |---|---| | **Plan** | Silage or Hay | | **Mode** | *Manual*: the field waits for **Start now**. *Auto*: the field starts by itself when ready | -| **Rake** | Hay only: rake before collecting. Off by default, because wide loading wagons collect spread hay | +| **Rake** | Rake the swaths before collecting (hay and silage). Off by default. Turn on for narrow loading wagons | | **Force storage** | Pick silos. Without a choice the nearest silo with room that takes the goods is used | A field is **ready** when at least 90 % of it is in the harvest window. The status shows the share, diff --git a/docs/superpowers/specs/2026-09-25-markarbejde-etape1-design.md b/docs/superpowers/specs/2026-09-25-markarbejde-etape1-design.md index aa26c12..5f438f3 100644 --- a/docs/superpowers/specs/2026-09-25-markarbejde-etape1-design.md +++ b/docs/superpowers/specs/2026-09-25-markarbejde-etape1-design.md @@ -44,6 +44,10 @@ Ikke med i etape 1: mejetærskerafgrøder (etape 2), roer (etape 3), jordbehandl kursus** (`vehicle:cpCopyCourse`). Kurset gemmes pr. mark; efter genindlæsning hentes det fra slåmaskinen (id gemt som `mowVehicle`), som Courseplay selv gemmer kurset på. Uden kendt slåkursus laves et nyt som reserve (logges). + **Revideret igen 25/9 (smalle standardmaskiner):** vender og river kører hele marken i egen bredde med + eget kursus (intet breddekrav). Læssevognen følger skårene: uden riv slåmaskinens kursus og bredde ≥ + slåbredde; med riv rivens kursus (skåret ligger midt i banen) og intet breddekrav. Riv kan vælges til + både hø og ensilage. 7. **Silovalg** genbruger Smart Pickups motor (`ADLoadSwap`): nærmeste silo med plads, der tager varen, med vej fra `F`-markøren. *Tving lager* pr. mark (som "Lager til" i flows). Fabrikker er ikke mål. 8. **Marksilo.** En silo, hvis afstand til markens polygon er ≤ 150 m, er et mellemlager. Den må gerne diff --git a/tests/test_adFieldModel.lua b/tests/test_adFieldModel.lua index 0380e2a..0d9e21e 100644 --- a/tests/test_adFieldModel.lua +++ b/tests/test_adFieldModel.lua @@ -28,6 +28,7 @@ check("M3 lidt under 90 % forbliver klar", M.onReadiness(ready, 0.8).stage, "rea -- M4: trin pr. plan check("M4 ensilage", table.concat(M.workStages({product = "silage"}), ","), "mowing,collecting") +check("M4 ensilage m. riv", table.concat(M.workStages({product = "silage", rake = true}), ","), "mowing,raking,collecting") check("M4 hø", table.concat(M.workStages({product = "hay", rake = false}), ","), "mowing,tedding,collecting") check("M4 hø m. riv", table.concat(M.workStages({product = "hay", rake = true}), ","), "mowing,tedding,raking,collecting") @@ -103,5 +104,22 @@ check("M9 trin bevaret", noted.stage, "mowing"); check("M9 original urørt", bas check("M10 venter -> klar", M.forceReady(M.newField(2)).stage, "ready") check("M10 arbejdende uændret", M.forceReady(base).stage, "mowing") +-- M11: krævet bredde og kursus-kilde pr. rolle (smalle standardmaskiner, 25/9) +local cut = M.with(M.newField(3), {mowWidth = 40.8}) +check("M11 vender kræver ingen bredde", M.requiredWidth(cut, "tedder"), nil) +check("M11 river kræver ingen bredde", M.requiredWidth(cut, "rake"), nil) +check("M11 læssevogn uden riv = slåbredde", M.requiredWidth(cut, "collector"), 40.8) +local raked = M.with(cut, {plan = {rake = true}}) +check("M11 læssevogn efter riv = ingen krav", M.requiredWidth(raked, "collector"), nil) +check("M11 vender eget kursus", M.courseSource(cut, "tedder"), nil) +check("M11 river eget kursus", M.courseSource(raked, "rake"), nil) +check("M11 læssevogn uden riv -> slåkursus", M.courseSource(cut, "collector"), "mower") +check("M11 læssevogn efter riv -> rivekursus", M.courseSource(raked, "collector"), "rake") +check("M11 slåmaskine eget kursus", M.courseSource(cut, "mower"), nil) +-- gem/indlæs rivemaskine +local withRake = M.with(raked, {rakeVehicleId = "vehicleRake"}) +check("M11 rivemaskine gemt", M.fromRecord(M.toRecord(withRake)).rakeVehicleId, "vehicleRake") +check("M11 start glemmer rivemaskine", M.start(M.with(M.onReadiness(M.newField(9), 1), {rakeVehicleId = "x"})).rakeVehicleId, nil) + print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) os.exit(failures == 0 and 0 or 1)