feat(marker): smalle standardmaskiner — vender/river i egen bredde, læssevogn følger rivens kursus; Riv også til ensilage (v1.23.0.0)

Brugerspørgsmål 25/9: få standardredskaber har 40,8 m. Vender og river kører hele marken i deres egen
bredde; efter riv ligger skåret midt i banen, så læssevognen kopierer rivens kursus uden breddekrav.
Uden riv kræver læssevognen stadig slåbredden (nu med hint 'sæt Riv til').
This commit is contained in:
2026-09-25 23:31:46 +02:00
parent 4c88118e56
commit ba88eb5f99
11 changed files with 105 additions and 42 deletions
+30 -15
View File
@@ -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)}})
+24 -5
View File
@@ -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"}
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
return {"mowing", "tedding", "collecting"}
-- 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
+2
View File
@@ -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)
@@ -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
if info.markerId ~= nil then
self:addTextRow(string.format(i18n:getText("spu_field_marker"), tostring(info.markerName), info.markerDistance or 0))
else
+2 -2
View File
@@ -53,7 +53,7 @@
<text name="spu_field_reason_noRig" text="ingen %s med flueben i markflåden"/>
<text name="spu_field_reason_allBusy" text="venter på ledig %s"/>
<text name="spu_field_reason_tooWide" text="%s for bred: %.1f m &gt; %.0f m (Courseplay)"/>
<text name="spu_field_reason_tooNarrow" text="%s for smal: %.1f m &lt; slåbredde %.1f m"/>
<text name="spu_field_reason_tooNarrow" text="%s for smal: %.1f m &lt; slåbredde %.1f m — sæt Riv til eller brug en bredere"/>
<text name="spu_field_role_mower" text="slåmaskine"/>
<text name="spu_field_role_tedder" text="vender"/>
<text name="spu_field_role_rake" text="river"/>
@@ -74,7 +74,7 @@
<text name="spu_field_section" text="— Mark F%d: %s —"/>
<text name="spu_field_plan" text="Plan"/>
<text name="spu_field_mode" text="Kørsel"/>
<text name="spu_field_rake" text="Riv høet sammen før opsamling"/>
<text name="spu_field_rake" text="Riv skårene sammen før opsamling (gør smalle læssevogne brugbare)"/>
<text name="spu_field_marker" text=" Markør %s, %.0f m fra marken"/>
<text name="spu_field_mowWidth" text=" Slået med %.1f m — opsamling skal være mindst lige så bred"/>
<text name="spu_field_section_store" text="— Tving lager (ellers nærmeste silo med plads) —"/>
+2 -2
View File
@@ -53,7 +53,7 @@
<text name="spu_field_reason_noRig" text="no %s ticked in the field fleet"/>
<text name="spu_field_reason_allBusy" text="waiting for a free %s"/>
<text name="spu_field_reason_tooWide" text="%s too wide: %.1f m &gt; %.0f m (Courseplay)"/>
<text name="spu_field_reason_tooNarrow" text="%s too narrow: %.1f m &lt; mowing width %.1f m"/>
<text name="spu_field_reason_tooNarrow" text="%s too narrow: %.1f m &lt; mowing width %.1f m — turn on Rake or use a wider one"/>
<text name="spu_field_role_mower" text="mower"/>
<text name="spu_field_role_tedder" text="tedder"/>
<text name="spu_field_role_rake" text="rake"/>
@@ -74,7 +74,7 @@
<text name="spu_field_section" text="— Field F%d: %s —"/>
<text name="spu_field_plan" text="Plan"/>
<text name="spu_field_mode" text="Mode"/>
<text name="spu_field_rake" text="Rake the hay before collecting"/>
<text name="spu_field_rake" text="Rake the swaths before collecting (lets narrow loading wagons collect)"/>
<text name="spu_field_marker" text=" Marker %s, %.0f m from the field"/>
<text name="spu_field_mowWidth" text=" Mowed at %.1f m — collecting must be at least as wide"/>
<text name="spu_field_section_store" text="— Force storage (else nearest silo with room) —"/>
+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.22.0.0</version>
<version>1.23.0.0</version>
<title>
<en>AD Smart Pickup</en>
</title>
+9 -6
View File
@@ -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 %`.
+10 -6
View File
@@ -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,
@@ -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
+18
View File
@@ -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)