docs: maskinpulje etape 1 — implementeringsplan (v1.48.0.0)
This commit is contained in:
@@ -0,0 +1,919 @@
|
|||||||
|
# Maskinpulje etape 1 (v1.48.0.0) Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Markarbejdet kan sende en ledig solo-traktor, der henter et frakoblet puljeredskab (plov, kultivator,
|
||||||
|
såmaskine …), udfører arbejdet med Courseplay, sætter redskabet af på dets AD-parkering og kører hjem.
|
||||||
|
|
||||||
|
**Architecture:** Nyt modul `adPool.lua` (ren planner + tynd spil-adapter) finder puljeredskaber (frakoblet +
|
||||||
|
AD-parkering "X" + markør "Connect X") og puljetraktorer og parrer dem. `ADFieldWork.dispatch` bruger puljen som
|
||||||
|
fallback, når ingen færdig kombination er ledig; en ny fase `fetching` kører `ADHitch.fetch` og fortsætter så i
|
||||||
|
den eksisterende `driving`-fase. Når en opgave slutter, kører `ADHitch.returnHome` redskabet til dets parkering
|
||||||
|
(afkobling) og traktoren hjem. Marker, der venter, betjenes længst-ventende først.
|
||||||
|
|
||||||
|
**Tech Stack:** Lua 5.1 (FS25-scripting), luajit til tests, AutoDrive 3.0.1.2 og Courseplay 8.1.0.3 API'er.
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-09-28-maskinpulje-design.md`
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- Kun etape 1 (markarbejde). Tømmevogne ved høst (`decision.helper == true`) og marksilo-flowet bruger IKKE puljen i denne version.
|
||||||
|
- Puljeredskab = frakoblet + AD-parkering "X" (`advd.parkDestination >= 1`) + markør "Connect X" (store/små bogstaver ligegyldigt).
|
||||||
|
- Puljetraktor = motoriseret, intet koblet bag på, ikke i et flow (`ADRunsController.getRunForVehicle == nil`), ikke optaget, ikke styret af spilleren, egen AD-parkering, flueben i markflåden (`ADFieldFleet.isEnabled`).
|
||||||
|
- Færdige kombinationer foretrækkes altid; puljen kun når `pick`/`pickMany` intet giver.
|
||||||
|
- Redskab nærmest marken, derefter nærmeste traktor til redskabet med nok hk; manglende hk-data = tilladt.
|
||||||
|
- Karantæne for et redskab, der ikke kunne kobles på: 10 min (`ADPool.QUARANTINE_MS = 600000`).
|
||||||
|
- En traktor køres kun hjem, når et af mod'ens egne jobs slutter — aldrig af sig selv.
|
||||||
|
- Flows (runs) rører aldrig puljen.
|
||||||
|
- Kommentarer og logtekster på dansk, som resten af koden. Ingen Co-Authored-By i commits. Push kun til remote `gitea`.
|
||||||
|
- Tests køres fra repo-roden: `luajit tests/test_<navn>.lua` — output slutter med `ALLE TESTS OK`.
|
||||||
|
- Build: `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .`; deploy mens spillet kører: `cp` til `~/FS25-data/mods/.FS25_ADSmartPickup.new` og `mv` på plads.
|
||||||
|
|
||||||
|
## Review Focus
|
||||||
|
|
||||||
|
- **To marker vil have samme plov samtidig** → kun den første får den; den anden venter (reservation) — testet i Task 1 (`pairFor` springer reserverede over).
|
||||||
|
- **Redskab uden hk-data eller traktor uden motor-data** → tillades (intet går i stå) — testet i Task 1 (`powerOk` nil-tilfælde, `numberOf` af tabel-specs).
|
||||||
|
- **Traktor på hjemtur bliver valgt til et nyt job** → må ikke ske; den er optaget til den er hjemme — testet i Task 3 (`ADPool.isReturning`) og brugt i `getRigs`/`poolTractors`.
|
||||||
|
- **Spilleren kobler selv et puljeredskab på og kører** → traktoren køres ikke hjem af mod'en — dækket af, at `returnHome` kun kaldes fra `finishAssignment`/fejlveje (Task 5); ingen tick-regel.
|
||||||
|
- **Kobling fejler (trækøje står skævt)** → traktor hjem, redskab i karantæne, marken prøver igen — testet i Task 1 (karantæne i `pairFor`) og Task 4 (fejlvej).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: `adPool.lua` — ren planlægning
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `FS25_ADSmartPickup/adPool.lua`
|
||||||
|
- Test: `tests/test_adPool.lua`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `ADFieldFleet.roleFromSpecs(specs)` (adFieldFleet.lua:27), `ADFieldModel.checkWidth(role, width, requiredWidth)` (bruges via `accept`), `ADHitch.parseMarker(name)` (adHitch.lua).
|
||||||
|
- Produces:
|
||||||
|
- `ADPool.roleOf(specs) -> role|nil` (specs = `ADCpBridge.getSpecs`-tabel; `unloader` når `specs.spec_trailer and specs.spec_dischargeable` og ingen anden rolle).
|
||||||
|
- `ADPool.numberOf(value) -> number|nil`
|
||||||
|
- `ADPool.powerOk(tractorHp, neededHp) -> bool`
|
||||||
|
- `ADPool.connectFor(parkName, markers) -> marker|nil` (markers = `{{name, id}}`)
|
||||||
|
- `ADPool.pairFor(role, fieldX, fieldZ, implements, tractors, blocked, accept) -> pair | nil, why, args`
|
||||||
|
- implement = `{id, name, role, roles?, x, z, width, neededHp, connectId, parkId}`
|
||||||
|
- tractor = `{id, name, x, z, hp, parkId}`
|
||||||
|
- blocked = `{[id] = true}` (reserverede + karantæne, både redskab- og traktor-id)
|
||||||
|
- pair = `{implement = implement, tractor = tractor}`
|
||||||
|
- why ∈ `noPoolImplement` / `noPoolTractor` / `tooWeak` (args = `{neededHp}`)
|
||||||
|
- `ADPool.queueOrder(infos, waitingSince) -> infos (ny tabel)`
|
||||||
|
- `ADPool.reserve(owner, ids)`, `ADPool.release(owner)`, `ADPool.quarantine(id, nowMs)`, `ADPool.blocked(nowMs) -> {[id]=true}`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `tests/test_adPool.lua`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Kør: luajit tests/test_adPool.lua (fra repo-roden)
|
||||||
|
dofile("FS25_ADSmartPickup/adFieldFleet.lua")
|
||||||
|
dofile("FS25_ADSmartPickup/adHitch.lua")
|
||||||
|
dofile("FS25_ADSmartPickup/adPool.lua")
|
||||||
|
local P = ADPool
|
||||||
|
local failures = 0
|
||||||
|
local function check(name, actual, expected)
|
||||||
|
if actual == expected then print("OK " .. name) else
|
||||||
|
failures = failures + 1
|
||||||
|
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- rolle ud fra redskabets egne specs
|
||||||
|
check("rolle: plov", P.roleOf({spec_plow = true}), "plow")
|
||||||
|
check("rolle: kultivator", P.roleOf({spec_cultivator = true}), "cultivator")
|
||||||
|
check("rolle: vogn med tip", P.roleOf({spec_trailer = true, spec_dischargeable = true}), "unloader")
|
||||||
|
check("rolle: vogn der også er læssevogn er læssevogn", P.roleOf({spec_trailer = true, spec_dischargeable = true, spec_forageWagon = true}), "collector")
|
||||||
|
check("rolle: intet", P.roleOf({}), nil)
|
||||||
|
-- hk fra butiksdata: tal eller tabel (konfigurationer)
|
||||||
|
check("tal", P.numberOf(180), 180)
|
||||||
|
check("tabel: største", P.numberOf({base = 120, config = 150}), 150)
|
||||||
|
check("tom tabel", P.numberOf({}), nil)
|
||||||
|
check("nil", P.numberOf(nil), nil)
|
||||||
|
check("hk: nok", P.powerOk(200, 180), true)
|
||||||
|
check("hk: for lidt", P.powerOk(150, 180), false)
|
||||||
|
check("hk: redskab uden data", P.powerOk(150, nil), true)
|
||||||
|
check("hk: traktor uden data", P.powerOk(nil, 180), true)
|
||||||
|
-- Connect-markør til en parkering
|
||||||
|
local markers = {{name = "Start", id = 1}, {name = "Bredal", id = 4}, {name = "Connect Bredal", id = 9}, {name = "connect plov 1", id = 12}}
|
||||||
|
check("connect: Bredal", P.connectFor("Bredal", markers).id, 9)
|
||||||
|
check("connect: små bogstaver", P.connectFor("Plov 1", markers).id, 12)
|
||||||
|
check("connect: ingen", P.connectFor("Start", markers), nil)
|
||||||
|
check("connect: uden parkering", P.connectFor(nil, markers), nil)
|
||||||
|
-- parring: redskab nærmest marken, så traktor nærmest redskabet
|
||||||
|
local implements = {
|
||||||
|
{id = "plovFar", name = "Plov far", role = "plow", x = 500, z = 0, connectId = 20, parkId = 21},
|
||||||
|
{id = "plovNear", name = "Plov nær", role = "plow", x = 100, z = 0, neededHp = 180, connectId = 30, parkId = 31},
|
||||||
|
{id = "kult", name = "Kultivator", role = "cultivator", x = 0, z = 0, connectId = 40, parkId = 41},
|
||||||
|
}
|
||||||
|
local tractors = {
|
||||||
|
{id = "small", name = "Lille", x = 110, z = 0, hp = 120, parkId = 2},
|
||||||
|
{id = "big", name = "Stor", x = 300, z = 0, hp = 250, parkId = 3},
|
||||||
|
{id = "mid", name = "Mellem", x = 150, z = 0, hp = 190, parkId = 5},
|
||||||
|
}
|
||||||
|
local pair = P.pairFor("plow", 0, 0, implements, tractors, {})
|
||||||
|
check("par: nærmeste plov til marken", pair.implement.id, "plovNear")
|
||||||
|
check("par: nærmeste stærke traktor til ploven", pair.tractor.id, "mid")
|
||||||
|
local blockedPair = P.pairFor("plow", 0, 0, implements, tractors, {plovNear = true})
|
||||||
|
check("par: reserveret plov springes over", blockedPair.implement.id, "plovFar")
|
||||||
|
check("par: plov uden hk-krav tager nærmeste traktor", blockedPair.tractor.id, "big")
|
||||||
|
local none, why = P.pairFor("seeder", 0, 0, implements, tractors, {})
|
||||||
|
check("par: ingen såmaskine", none, nil); check("par: grund", why, "noPoolImplement")
|
||||||
|
local weak, weakWhy, weakArgs = P.pairFor("plow", 0, 0, {implements[2]}, {tractors[1]}, {})
|
||||||
|
check("par: for svag", weak, nil); check("par: grund tooWeak", weakWhy, "tooWeak"); check("par: hk i args", weakArgs[1], 180)
|
||||||
|
local noTractor, noTractorWhy = P.pairFor("plow", 0, 0, implements, {}, {})
|
||||||
|
check("par: ingen traktor", noTractor, nil); check("par: grund noPoolTractor", noTractorWhy, "noPoolTractor")
|
||||||
|
local rejected, rejectedWhy = P.pairFor("plow", 0, 0, implements, tractors, {}, function() return false end)
|
||||||
|
check("par: accept afviser alle", rejected, nil); check("par: grund", rejectedWhy, "noPoolImplement")
|
||||||
|
local multi = P.pairFor("fertilizer", 0, 0, {{id = "spreder", role = "fertilizer", roles = {fertilizer = true, liming = true}, x = 0, z = 0}}, tractors, {})
|
||||||
|
check("par: redskab med flere roller", multi.implement.id, "spreder")
|
||||||
|
-- reservationer og karantæne
|
||||||
|
P.reserve("F12", {"plovNear", "mid"})
|
||||||
|
check("reserveret: redskab", P.blocked(0).plovNear, true)
|
||||||
|
check("reserveret: traktor", P.blocked(0).mid, true)
|
||||||
|
P.release("F12")
|
||||||
|
check("frigivet", P.blocked(0).plovNear, nil)
|
||||||
|
P.quarantine("plovNear", 1000)
|
||||||
|
check("karantæne: aktiv", P.blocked(1000 + P.QUARANTINE_MS - 1).plovNear, true)
|
||||||
|
check("karantæne: udløbet", P.blocked(1000 + P.QUARANTINE_MS).plovNear, nil)
|
||||||
|
-- kø: længst ventende først, lige -> lavest mark-id, ikke-ventende bagerst i oprindelig rækkefølge
|
||||||
|
local infos = {{id = 5}, {id = 3}, {id = 9}, {id = 1}}
|
||||||
|
local ordered = P.queueOrder(infos, {[9] = 100, [3] = 50, [1] = 50})
|
||||||
|
check("kø: 1.", ordered[1].id, 1); check("kø: 2.", ordered[2].id, 3); check("kø: 3.", ordered[3].id, 9); check("kø: 4.", ordered[4].id, 5)
|
||||||
|
check("kø: rører ikke input", infos[1].id, 5)
|
||||||
|
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||||
|
os.exit(failures == 0 and 0 or 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `luajit tests/test_adPool.lua`
|
||||||
|
Expected: fejl `cannot open FS25_ADSmartPickup/adPool.lua`
|
||||||
|
|
||||||
|
- [ ] **Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
Create `FS25_ADSmartPickup/adPool.lua`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- AD Pool
|
||||||
|
-- Maskinpulje: plove, kultivatorer, såmaskiner … holder frakoblet på deres AD-parkering "X", og en ledig
|
||||||
|
-- solo-traktor henter dem via markøren "Connect X", når markarbejdet mangler en færdig kombination.
|
||||||
|
-- Ren logik øverst (testes med luajit), spil-adapter nederst.
|
||||||
|
|
||||||
|
ADPool = {}
|
||||||
|
ADPool.LOG_PREFIX = "ADSmartPickup: pulje: "
|
||||||
|
ADPool.QUARANTINE_MS = 600000 -- redskab der ikke kunne kobles på, springes over så længe
|
||||||
|
|
||||||
|
-- ---- ren logik ----
|
||||||
|
|
||||||
|
-- Rolle for et frakoblet redskab ud fra dets egne specs (samme prioritet som markflåden); en ren vogn med
|
||||||
|
-- tip/snegl er tømmevogn.
|
||||||
|
function ADPool.roleOf(specs)
|
||||||
|
local role = ADFieldFleet.roleFromSpecs(specs)
|
||||||
|
if role ~= nil and role ~= "unloader" then return role end
|
||||||
|
if specs.spec_trailer and specs.spec_dischargeable then return "unloader" end
|
||||||
|
return role
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Butiksdata er enten et tal eller en tabel pr. konfiguration: største tal, ellers nil.
|
||||||
|
function ADPool.numberOf(value)
|
||||||
|
if type(value) == "number" then return value end
|
||||||
|
if type(value) ~= "table" then return nil end
|
||||||
|
local best = nil
|
||||||
|
for _, entry in pairs(value) do
|
||||||
|
if type(entry) == "number" and (best == nil or entry > best) then best = entry end
|
||||||
|
end
|
||||||
|
return best
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Mangler et af tallene, tillades det (intet må gå i stå på manglende data).
|
||||||
|
function ADPool.powerOk(tractorHp, neededHp)
|
||||||
|
if tractorHp == nil or neededHp == nil or tractorHp <= 0 or neededHp <= 0 then return true end
|
||||||
|
return tractorHp >= neededHp
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Markøren "Connect <parkering>" for en parkering (navneregel).
|
||||||
|
function ADPool.connectFor(parkName, markers)
|
||||||
|
if type(parkName) ~= "string" then return nil end
|
||||||
|
local wanted = parkName:lower()
|
||||||
|
for _, marker in pairs(markers or {}) do
|
||||||
|
local action, target = ADHitch.parseMarker(marker.name)
|
||||||
|
if action == "attach" and target ~= nil and target:lower() == wanted then return marker end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function hasRole(item, role)
|
||||||
|
return item.role == role or (item.roles ~= nil and item.roles[role] == true)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function distanceSq(ax, az, bx, bz)
|
||||||
|
return (ax - bx) ^ 2 + (az - bz) ^ 2
|
||||||
|
end
|
||||||
|
|
||||||
|
local function nearest(items, x, z, keep)
|
||||||
|
local best, bestDistance = nil, math.huge
|
||||||
|
for _, item in ipairs(items) do
|
||||||
|
if keep(item) then
|
||||||
|
local distance = distanceSq(item.x, item.z, x, z)
|
||||||
|
if distance < bestDistance then best, bestDistance = item, distance end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return best
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Redskab med rollen nærmest marken, derefter nærmeste ledige traktor med nok hk til redskabet.
|
||||||
|
-- blocked = {[id] = true} (reserverede og redskaber i karantæne). accept (valgfri): implement -> bool.
|
||||||
|
function ADPool.pairFor(role, fieldX, fieldZ, implements, tractors, blocked, accept)
|
||||||
|
blocked = blocked or {}
|
||||||
|
local free = {}
|
||||||
|
for _, tractor in ipairs(tractors or {}) do
|
||||||
|
if not blocked[tractor.id] then table.insert(free, tractor) end
|
||||||
|
end
|
||||||
|
local candidates = {}
|
||||||
|
for _, implement in ipairs(implements or {}) do
|
||||||
|
if hasRole(implement, role) and not blocked[implement.id] and (accept == nil or accept(implement)) then
|
||||||
|
table.insert(candidates, implement)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #candidates == 0 then return nil, "noPoolImplement" end
|
||||||
|
if #free == 0 then return nil, "noPoolTractor" end
|
||||||
|
table.sort(candidates, function(a, b)
|
||||||
|
return distanceSq(a.x, a.z, fieldX, fieldZ) < distanceSq(b.x, b.z, fieldX, fieldZ)
|
||||||
|
end)
|
||||||
|
local weakest = nil
|
||||||
|
for _, implement in ipairs(candidates) do
|
||||||
|
local tractor = nearest(free, implement.x, implement.z, function(t) return ADPool.powerOk(t.hp, implement.neededHp) end)
|
||||||
|
if tractor ~= nil then return {implement = implement, tractor = tractor} end
|
||||||
|
if weakest == nil or (implement.neededHp or 0) < weakest then weakest = implement.neededHp end
|
||||||
|
end
|
||||||
|
return nil, "tooWeak", {weakest or 0}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Ventende marker først (længst ventende først, lige -> lavest id); resten bagefter i oprindelig rækkefølge.
|
||||||
|
function ADPool.queueOrder(infos, waitingSince)
|
||||||
|
local waiting, rest = {}, {}
|
||||||
|
for _, info in ipairs(infos or {}) do
|
||||||
|
if waitingSince ~= nil and waitingSince[info.id] ~= nil then table.insert(waiting, info) else table.insert(rest, info) end
|
||||||
|
end
|
||||||
|
table.sort(waiting, function(a, b)
|
||||||
|
local wa, wb = waitingSince[a.id], waitingSince[b.id]
|
||||||
|
if wa ~= wb then return wa < wb end
|
||||||
|
return a.id < b.id
|
||||||
|
end)
|
||||||
|
for _, info in ipairs(rest) do table.insert(waiting, info) end
|
||||||
|
return waiting
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Reservationer (i hukommelsen): owner -> {id, ...}. Karantæne: id -> udløbstid.
|
||||||
|
local reservations, quarantined = {}, {}
|
||||||
|
|
||||||
|
function ADPool.reserve(owner, ids)
|
||||||
|
reservations[owner] = ids
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADPool.release(owner)
|
||||||
|
reservations[owner] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADPool.quarantine(id, nowMs)
|
||||||
|
quarantined[id] = nowMs + ADPool.QUARANTINE_MS
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADPool.blocked(nowMs)
|
||||||
|
local result = {}
|
||||||
|
for _, ids in pairs(reservations) do
|
||||||
|
for _, id in ipairs(ids) do result[id] = true end
|
||||||
|
end
|
||||||
|
for id, untilMs in pairs(quarantined) do
|
||||||
|
if nowMs < untilMs then result[id] = true end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `luajit tests/test_adPool.lua`
|
||||||
|
Expected: sidste linje `ALLE TESTS OK`
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add FS25_ADSmartPickup/adPool.lua tests/test_adPool.lua
|
||||||
|
git commit -m "feat(pulje): ren planlægning — rolle, hk-tjek, Connect-navneregel, parring, kø, reservation/karantæne"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: `adPool.lua` — spil-adapter (indsamling af puljen)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `FS25_ADSmartPickup/adPool.lua` (tilføj nederst)
|
||||||
|
- Modify: `FS25_ADSmartPickup/modDesc.xml` (registrér `adPool.lua` efter `adHitch.lua`)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `ADCpBridge.getSpecs(vehicle)` (adCpBridge.lua:153), `ADCpBridge.getWorkWidth(vehicle)` (:363), `ADRunsController.getFarmVehicles(adEnv)`, `ADRunsController.getRunForVehicle(vehicle)`, `ADRunsController.getVehicleId(vehicle)`, `ADRuns.vehicleLabel(vehicle)`, `ADFieldFleet.isEnabled(mode, fleet, id)`, `ADFieldJobs.state`, `ADHitch.rearImplementsOf(vehicle)` (Task 3), `ADPool.isReturning(vehicle)` (Task 3), `adEnv.ADGraphManager:getMapMarkers()`, `:getMapMarkerById(index)`, `g_currentMission.vehicleSystem.vehicles`.
|
||||||
|
- Produces: `ADPool.collect(adEnv, isBusy) -> implements, tractors` (formater som Task 1; implement har også `.vehicle`, tractor har `.vehicle`); `isBusy(vehicle) -> bool` gives af kalderen (markarbejdets optaget-regel).
|
||||||
|
|
||||||
|
- [ ] **Step 1: Tilføj adapteren**
|
||||||
|
|
||||||
|
Tilføj nederst i `FS25_ADSmartPickup/adPool.lua`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- ---- spil ----
|
||||||
|
|
||||||
|
local function parkMarker(adEnv, vehicle)
|
||||||
|
local index = vehicle.advd ~= nil and vehicle.advd.parkDestination or nil
|
||||||
|
if type(index) ~= "number" or index < 1 then return nil end
|
||||||
|
return adEnv.ADGraphManager:getMapMarkerById(index)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function positionOf(vehicle)
|
||||||
|
local x, _, z = getWorldTranslation(vehicle.rootNode)
|
||||||
|
return x, z
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Butiksdata: redskabets krævede effekt (hk). specs indlæses først, når butikken har vist varen.
|
||||||
|
local function neededHpOf(vehicle)
|
||||||
|
if g_storeManager == nil or vehicle.configFileName == nil then return nil end
|
||||||
|
local item = g_storeManager:getItemByXMLFilename(vehicle.configFileName)
|
||||||
|
if item == nil then return nil end
|
||||||
|
if item.specs == nil and StoreItemUtil ~= nil and StoreItemUtil.loadSpecsFromXML ~= nil then
|
||||||
|
pcall(StoreItemUtil.loadSpecsFromXML, item)
|
||||||
|
end
|
||||||
|
return item.specs ~= nil and ADPool.numberOf(item.specs.neededPower) or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Traktorens effekt i hk: motorens topeffekt (kW) omregnet; ellers butiksdata.
|
||||||
|
local function tractorHpOf(vehicle)
|
||||||
|
local motor = vehicle.spec_motorized ~= nil and vehicle.spec_motorized.motor or nil
|
||||||
|
local kw = motor ~= nil and motor.peakMotorPower or nil
|
||||||
|
if type(kw) == "number" and kw > 0 then return kw * 1.35962 end
|
||||||
|
if g_storeManager == nil or vehicle.configFileName == nil then return nil end
|
||||||
|
local item = g_storeManager:getItemByXMLFilename(vehicle.configFileName)
|
||||||
|
return item ~= nil and item.specs ~= nil and ADPool.numberOf(item.specs.power) or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isAttached(vehicle)
|
||||||
|
return vehicle.getAttacherVehicle ~= nil and vehicle:getAttacherVehicle() ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Puljeredskaber (frakoblet, parkering "X", markør "Connect X") og puljetraktorer.
|
||||||
|
-- isBusy(vehicle): markarbejdets optaget-regel (opgave, AD/CP kører, lånt af marksilo, på hjemtur).
|
||||||
|
function ADPool.collect(adEnv, isBusy)
|
||||||
|
local markers = adEnv.ADGraphManager:getMapMarkers()
|
||||||
|
local farmId = g_currentMission:getFarmId()
|
||||||
|
local implements, tractors = {}, {}
|
||||||
|
-- spillets egen liste: frakoblede redskaber er ikke nødvendigvis i AutoDrive's køretøjsliste
|
||||||
|
for _, vehicle in ipairs(g_currentMission.vehicleSystem.vehicles) do
|
||||||
|
local owned = vehicle.getOwnerFarmId ~= nil and vehicle:getOwnerFarmId() == farmId
|
||||||
|
if owned and vehicle.spec_motorized == nil and vehicle.spec_attachable ~= nil and not isAttached(vehicle) then
|
||||||
|
local park = parkMarker(adEnv, vehicle)
|
||||||
|
local connect = park ~= nil and ADPool.connectFor(park.name, markers) or nil
|
||||||
|
local specs = connect ~= nil and ADCpBridge.getSpecs(vehicle) or nil
|
||||||
|
local role = specs ~= nil and ADPool.roleOf(specs) or nil
|
||||||
|
if role ~= nil then
|
||||||
|
local x, z = positionOf(vehicle)
|
||||||
|
table.insert(implements, {
|
||||||
|
id = ADRunsController.getVehicleId(vehicle), vehicle = vehicle, name = ADRuns.vehicleLabel(vehicle),
|
||||||
|
role = role, roles = specs.sprayRoles, x = x, z = z, width = ADCpBridge.getWorkWidth(vehicle),
|
||||||
|
neededHp = neededHpOf(vehicle), connectId = connect.id, parkId = park.id,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local state = ADFieldJobs.state
|
||||||
|
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||||
|
local id = ADRunsController.getVehicleId(vehicle)
|
||||||
|
local park = parkMarker(adEnv, vehicle)
|
||||||
|
local controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled()
|
||||||
|
if id ~= nil and park ~= nil and vehicle.spec_attacherJoints ~= nil and #ADHitch.rearImplementsOf(vehicle) == 0
|
||||||
|
and ADRunsController.getRunForVehicle(vehicle) == nil and not controlled and not isBusy(vehicle)
|
||||||
|
and not ADPool.isReturning(vehicle) and ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id) then
|
||||||
|
local x, z = positionOf(vehicle)
|
||||||
|
table.insert(tractors, {id = id, vehicle = vehicle, name = ADRuns.vehicleLabel(vehicle), x = x, z = z,
|
||||||
|
hp = tractorHpOf(vehicle), parkId = park.id})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return implements, tractors
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Registrér modulet**
|
||||||
|
|
||||||
|
I `FS25_ADSmartPickup/modDesc.xml` efter linjen `<sourceFile filename="adHitch.lua"/>`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<sourceFile filename="adPool.lua"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Syntakstjek og tests**
|
||||||
|
|
||||||
|
Run: `luajit -bl FS25_ADSmartPickup/adPool.lua >/dev/null && echo syntaks-ok && luajit tests/test_adPool.lua | tail -1`
|
||||||
|
Expected: `syntaks-ok` og `ALLE TESTS OK`
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add FS25_ADSmartPickup/adPool.lua FS25_ADSmartPickup/modDesc.xml
|
||||||
|
git commit -m "feat(pulje): spil-adapter — frakoblede redskaber med parkering+Connect og ledige solo-traktorer med hk"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: `adHitch.lua` — hjemtur og "på hjemtur"-status
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `FS25_ADSmartPickup/adHitch.lua`
|
||||||
|
- Modify: `FS25_ADSmartPickup/adPool.lua` (isReturning-register)
|
||||||
|
- Test: `tests/test_adHitch.lua`, `tests/test_adPool.lua`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `ADHitch.route(adEnv, vehicle, markerIds, onDone)` (findes), `attachedImplements`/`rearImplements` (findes i adHitch.lua).
|
||||||
|
- Produces:
|
||||||
|
- `ADHitch.homeRoute(implementParkIds, tractorParkId) -> {id, ...}` (ren)
|
||||||
|
- `ADHitch.rearImplementsOf(vehicle) -> {{object, z, name}}` (offentlig udgave af den lokale)
|
||||||
|
- `ADHitch.returnHome(adEnv, tractor, onDone) -> bool` (false = intet puljeredskab bag / ingen parkering)
|
||||||
|
- `ADPool.setReturning(vehicle, isOn)`, `ADPool.isReturning(vehicle) -> bool`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing tests**
|
||||||
|
|
||||||
|
Tilføj i `tests/test_adHitch.lua` før `print(failures == 0`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- hjemtur: hvert puljeredskabs parkering (afkobles der), så traktorens egen parkering
|
||||||
|
local home = H.homeRoute({4}, 2)
|
||||||
|
check("hjem: to trin", #home, 2); check("hjem: redskab først", home[1], 4); check("hjem: traktor sidst", home[2], 2)
|
||||||
|
check("hjem: uden redskab kun traktor", #H.homeRoute({}, 2), 1)
|
||||||
|
check("hjem: samme parkering to gange tæller én", #H.homeRoute({4, 4}, 2), 2)
|
||||||
|
check("hjem: uden traktorparkering", #H.homeRoute({4}, nil), 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
Tilføj i `tests/test_adPool.lua` før `print(failures == 0`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- på hjemtur: optaget indtil hjemme
|
||||||
|
local tractorObject = {}
|
||||||
|
check("hjemtur: ikke fra start", P.isReturning(tractorObject), false)
|
||||||
|
P.setReturning(tractorObject, true)
|
||||||
|
check("hjemtur: markeret", P.isReturning(tractorObject), true)
|
||||||
|
P.setReturning(tractorObject, false)
|
||||||
|
check("hjemtur: hjemme", P.isReturning(tractorObject), false)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `luajit tests/test_adHitch.lua | tail -3; luajit tests/test_adPool.lua | tail -3`
|
||||||
|
Expected: fejl `attempt to call field 'homeRoute' (a nil value)` og `attempt to call field 'isReturning'`
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement**
|
||||||
|
|
||||||
|
I `FS25_ADSmartPickup/adPool.lua`, efter `ADPool.blocked`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Traktorer på hjemtur (sætter redskab af, kører hjem): optaget for markarbejdet til de er hjemme.
|
||||||
|
local returning = setmetatable({}, {__mode = "k"})
|
||||||
|
|
||||||
|
function ADPool.setReturning(vehicle, isOn)
|
||||||
|
returning[vehicle] = isOn and true or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADPool.isReturning(vehicle)
|
||||||
|
return returning[vehicle] == true
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
I `FS25_ADSmartPickup/adHitch.lua`, i den rene del (efter `ADHitch.waitAfter`):
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Hjemtur: hvert puljeredskabs parkering (der sættes det af), så traktorens egen parkering.
|
||||||
|
function ADHitch.homeRoute(implementParkIds, tractorParkId)
|
||||||
|
local route, seen = {}, {}
|
||||||
|
for _, id in ipairs(implementParkIds or {}) do
|
||||||
|
if not seen[id] then seen[id] = true table.insert(route, id) end
|
||||||
|
end
|
||||||
|
if tractorParkId ~= nil then table.insert(route, tractorParkId) end
|
||||||
|
return route
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
I spil-delen af `adHitch.lua`, lige efter `local function isImplementPark(...) ... end`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function ADHitch.rearImplementsOf(vehicle)
|
||||||
|
return ADHitch.rearImplements(attachedImplements(vehicle))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function parkIdOf(adEnv, vehicle)
|
||||||
|
local index = vehicle.advd ~= nil and vehicle.advd.parkDestination or nil
|
||||||
|
local marker = type(index) == "number" and index >= 1 and adEnv.ADGraphManager:getMapMarkerById(index) or nil
|
||||||
|
return marker ~= nil and marker.id or nil
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Efter `function ADHitch.route(...) ... end`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Puljeredskaber bag traktoren sættes af på deres parkering, og traktoren kører hjem til sin egen.
|
||||||
|
-- Kun redskaber med en Connect-markør til deres parkering er puljeredskaber; andre bliver siddende.
|
||||||
|
function ADHitch.returnHome(adEnv, tractor, onDone)
|
||||||
|
local markers = adEnv.ADGraphManager:getMapMarkers()
|
||||||
|
local parkIds = {}
|
||||||
|
for _, implement in ipairs(ADHitch.rearImplementsOf(tractor)) do
|
||||||
|
local parkId = parkIdOf(adEnv, implement.object)
|
||||||
|
local park = parkId ~= nil and adEnv.ADGraphManager:getMapMarkerByWayPointId(parkId) or nil
|
||||||
|
if park ~= nil and ADPool.connectFor(park.name, markers) ~= nil then table.insert(parkIds, parkId) end
|
||||||
|
end
|
||||||
|
if #parkIds == 0 then return false end
|
||||||
|
local route = ADHitch.homeRoute(parkIds, parkIdOf(adEnv, tractor))
|
||||||
|
ADPool.setReturning(tractor, true)
|
||||||
|
ADHitch.route(adEnv, tractor, route, function(vehicle, ok, detail)
|
||||||
|
ADPool.setReturning(vehicle, false)
|
||||||
|
if onDone ~= nil then onDone(vehicle, ok, detail) end
|
||||||
|
end)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run tests to verify they pass**
|
||||||
|
|
||||||
|
Run: `luajit tests/test_adHitch.lua | tail -1; luajit tests/test_adPool.lua | tail -1; luajit -bl FS25_ADSmartPickup/adHitch.lua >/dev/null && echo syntaks-ok`
|
||||||
|
Expected: `ALLE TESTS OK` to gange og `syntaks-ok`
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add FS25_ADSmartPickup/adHitch.lua FS25_ADSmartPickup/adPool.lua tests/test_adHitch.lua tests/test_adPool.lua
|
||||||
|
git commit -m "feat(pulje): hjemtur — puljeredskaber sættes af på deres parkering, traktoren kører til sin egen; optaget imens"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Markarbejde — puljen som fallback og fasen `fetching`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `FS25_ADSmartPickup/adFieldWork.lua` (`getRigs` :132-170, `dispatch` :382-482, `follow` :1083-1143, `release` :278-283)
|
||||||
|
- Modify: `FS25_ADSmartPickup/l10n/l10n_da.xml`, `FS25_ADSmartPickup/l10n/l10n_en.xml`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `ADCpBridge.getSeedFruits/hasSeeds/isMechanicalWeeder(vehicle)`, `ADPool.collect(adEnv, isBusy)`, `ADPool.pairFor(...)`, `ADPool.blocked(nowMs)`, `ADPool.reserve/release/quarantine`, `ADPool.isReturning`, `ADHitch.fetch(adEnv, vehicle, markerId, onDone)`, `ADHitch.returnHome`.
|
||||||
|
- Produces: assignment-felter `pool = {implementId, implementName, connectId}`, `phase = "fetching"`, `hitch = {done=bool, ok=bool, detail=any}`; field-årsager `spu_field_reason_noPoolTractor`, `spu_field_reason_fetching` (tooWeak som fritekst).
|
||||||
|
|
||||||
|
- [ ] **Step 1: Optaget-regel genbrugelig + hjemtur tæller som optaget**
|
||||||
|
|
||||||
|
I `adFieldWork.lua` før `function ADFieldWork.getRigs`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Optaget for markarbejdet: vores egen opgave, AutoDrive/Courseplay kører den, lånt af marksilo-flowet,
|
||||||
|
-- eller på hjemtur fra puljen (sætter redskab af).
|
||||||
|
local function isBusyVehicle(vehicle, id)
|
||||||
|
return ADFieldJobs.state.assignments[id] ~= nil or isAdActive(vehicle) or ADCpBridge.isWorking(vehicle)
|
||||||
|
or (ADFieldSilos ~= nil and ADFieldSilos.isLoaned(vehicle)) or (ADPool ~= nil and ADPool.isReturning(vehicle))
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
I `getRigs` erstat `busy = state.assignments[id] ~= nil or isAdActive(vehicle) or ADCpBridge.isWorking(vehicle)\n or (ADFieldSilos ~= nil and ADFieldSilos.isLoaned(vehicle)),` med:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
busy = isBusyVehicle(vehicle, id),
|
||||||
|
```
|
||||||
|
|
||||||
|
og tilføj som sidste alternativ i `busyWhy`-kæden (før `or nil`):
|
||||||
|
|
||||||
|
```lua
|
||||||
|
or (ADPool ~= nil and ADPool.isReturning(vehicle) and "sætter redskab af") or nil,
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Fallback i dispatch**
|
||||||
|
|
||||||
|
I `ADFieldWork.dispatch`, lige efter `if rigs == nil then` og FØR `if #offNetwork > 0 ...`, indsæt:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- ingen færdig kombination: en ledig solo-traktor henter et puljeredskab (kun hovedopgaver, ikke tømmevogne)
|
||||||
|
if not decision.helper and (decision.count or 1) == 1 and ADPool ~= nil
|
||||||
|
and (why == "noRig" or why == "allBusy" or why == "cannotDo") then
|
||||||
|
local pooled, poolWhy, poolArgs = ADFieldWork.dispatchPool(adEnv, field, info, role, roleAccept)
|
||||||
|
if pooled then return end
|
||||||
|
if poolWhy == "tooWeak" or (poolWhy == "noPoolTractor" and why ~= "allBusy") then
|
||||||
|
why, args = poolWhy, poolArgs
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Tilføj før `function ADFieldWork.dispatch` en ny funktion:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Puljekombination: nærmeste puljeredskab til marken + nærmeste ledige traktor med nok hk. Traktoren kører til
|
||||||
|
-- "Connect X", kobler på og fortsætter så som en almindelig udsendelse (fase fetching -> driving).
|
||||||
|
function ADFieldWork.dispatchPool(adEnv, field, info, role, roleAccept)
|
||||||
|
local implements, tractors = ADPool.collect(adEnv, function(vehicle)
|
||||||
|
return isBusyVehicle(vehicle, ADRunsController.getVehicleId(vehicle))
|
||||||
|
end)
|
||||||
|
local accept = function(implement)
|
||||||
|
-- samme felter som getRigs giver sæt, så markens krav (frø, afgrøde, ukrudt) tjekkes på redskabet selv
|
||||||
|
local candidate = {role = implement.role, roles = implement.roles, vehicle = implement.vehicle}
|
||||||
|
if role == "seeder" then
|
||||||
|
candidate.seedFruits = ADCpBridge.getSeedFruits(implement.vehicle)
|
||||||
|
candidate.hasSeeds = ADCpBridge.hasSeeds(implement.vehicle)
|
||||||
|
end
|
||||||
|
if role == "weeder" then candidate.mechanical = ADCpBridge.isMechanicalWeeder(implement.vehicle) end
|
||||||
|
if roleAccept ~= nil and not roleAccept(candidate) then return false end
|
||||||
|
local required = ADFieldModel.requiredWidth(field, role)
|
||||||
|
return (implement.width or 0) <= 0 or ADFieldModel.checkWidth(role, implement.width, required)
|
||||||
|
end
|
||||||
|
local pair, why, args = ADPool.pairFor(role, info.x, info.z, implements, tractors, ADPool.blocked(now()), accept)
|
||||||
|
if pair == nil then return false, why, args end
|
||||||
|
local tractor, implement = pair.tractor, pair.implement
|
||||||
|
local assignment = {
|
||||||
|
vehicleId = tractor.id, vehicle = tractor.vehicle, fieldId = field.id, role = role, stage = field.stage,
|
||||||
|
helper = false, phase = "fetching", since = now(), width = implement.width, groupSize = 1, leader = true,
|
||||||
|
startX = tractor.x, startZ = tractor.z,
|
||||||
|
pool = {implementId = implement.id, implementName = implement.name, connectId = implement.connectId},
|
||||||
|
hitch = {done = false},
|
||||||
|
}
|
||||||
|
ADFieldJobs.state.assignments[tractor.id] = assignment
|
||||||
|
ADPool.reserve(assignment, {implement.id, tractor.id})
|
||||||
|
ADHitch.fetch(adEnv, tractor.vehicle, implement.connectId, function(_, ok, detail)
|
||||||
|
assignment.hitch = {done = true, ok = ok, detail = detail}
|
||||||
|
end)
|
||||||
|
note(field.id, {reason = {key = "spu_field_reason_fetching", name = implement.name}})
|
||||||
|
log(field.id, "%s: '%s' henter '%s' (%s)", role, tractor.name, implement.name, "pulje")
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Frigiv reservation ved release**
|
||||||
|
|
||||||
|
I `ADFieldWork.release`, før `ADFieldJobs.state.assignments[assignment.vehicleId] = nil`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
if ADPool ~= nil and assignment.pool ~= nil then ADPool.release(assignment) end
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Fasen fetching i follow**
|
||||||
|
|
||||||
|
I `ADFieldWork.follow`, efter linjen `wake(vehicle)` og før `if assignment.phase == "driving" then`, gør `if` til en kæde ved at indsætte:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
if assignment.phase == "fetching" then
|
||||||
|
local hitch = assignment.hitch
|
||||||
|
if not hitch.done then return end
|
||||||
|
ADPool.release(assignment)
|
||||||
|
if not hitch.ok then
|
||||||
|
-- koblingen fejlede: redskabet i karantæne, traktoren hjem, marken prøver igen
|
||||||
|
ADPool.quarantine(assignment.pool.implementId, now())
|
||||||
|
ADFieldWork.release(assignment)
|
||||||
|
note(assignment.fieldId, {retryAt = now() + ADFieldWork.NO_RIG_RETRY_MS,
|
||||||
|
reason = {text = string.format("kunne ikke koble '%s' på (%s) — prøver et andet om lidt",
|
||||||
|
tostring(assignment.pool.implementName), tostring(hitch.detail))}})
|
||||||
|
local park = parkMarkerId(adEnv, vehicle)
|
||||||
|
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- koblet på: bredden med redskabet på (for bred -> hjem igen)
|
||||||
|
local width = ADCpBridge.getWorkWidth(vehicle)
|
||||||
|
local okWidth, widthReason, widthArgs = ADFieldModel.checkWidth(assignment.role, width, ADFieldModel.requiredWidth(field, assignment.role))
|
||||||
|
if not okWidth then
|
||||||
|
ADFieldWork.release(assignment)
|
||||||
|
note(assignment.fieldId, {retryAt = now() + ADFieldModel.RETRY_MS, reason = {key = "spu_field_reason_" .. tostring(widthReason), role = assignment.role, args = widthArgs}})
|
||||||
|
ADHitch.returnHome(adEnv, vehicle)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
assignment.width, assignment.phase, assignment.since = width, "driving", now()
|
||||||
|
assignment.startX, assignment.startZ = vehiclePosition(vehicle)
|
||||||
|
startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, info.markerId, nil)
|
||||||
|
assignment.drive = {mode = adEnv.AutoDrive.MODE_DRIVETO, first = info.markerId}
|
||||||
|
note(assignment.fieldId, {reason = false})
|
||||||
|
log(assignment.fieldId, "%s '%s' har '%s' på — kører til %s (%.1f m)", assignment.role, ADRuns.vehicleLabel(vehicle),
|
||||||
|
tostring(assignment.pool.implementName), tostring(info.markerName), width or 0)
|
||||||
|
elseif assignment.phase == "driving" then
|
||||||
|
```
|
||||||
|
|
||||||
|
(og fjern det oprindelige `if assignment.phase == "driving" then`, som nu er `elseif`.)
|
||||||
|
|
||||||
|
Tilføj også `fetching` til spiller-reglen øverst i `follow`, så AD's kørsel under hentning ikke tolkes som spillerens: `steeredByAi` er allerede sand, mens AD kører; ingen ændring nødvendig.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Tekster**
|
||||||
|
|
||||||
|
`l10n/l10n_da.xml` (ved de andre `spu_field_reason_`):
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<text name="spu_field_reason_noPoolTractor" text="ingen ledig traktor til at hente et redskab til %s"/>
|
||||||
|
<text name="spu_field_reasonShort_noPoolTractor" text="venter på traktor"/>
|
||||||
|
<text name="spu_field_reason_fetching" text="henter '%s'"/>
|
||||||
|
<text name="spu_field_reasonShort_fetching" text="henter redskab"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
`l10n/l10n_en.xml`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<text name="spu_field_reason_noPoolTractor" text="no free tractor to fetch an implement for %s"/>
|
||||||
|
<text name="spu_field_reasonShort_noPoolTractor" text="waiting for tractor"/>
|
||||||
|
<text name="spu_field_reason_fetching" text="fetching '%s'"/>
|
||||||
|
<text name="spu_field_reasonShort_fetching" text="fetching implement"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
`tooWeak` bruger fritekst (hk står før rollen, og `fieldReasonText` sætter rollen først): i Step 2 erstattes linjen
|
||||||
|
`why, args = poolWhy, poolArgs` med:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
if poolWhy == "tooWeak" then
|
||||||
|
note(field.id, {reason = {text = string.format("ingen ledig traktor med ≥%.0f hk til %s-redskabet",
|
||||||
|
(poolArgs or {})[1] or 0, tostring(role))}, retryAt = now() + ADFieldWork.NO_RIG_RETRY_MS})
|
||||||
|
return
|
||||||
|
end
|
||||||
|
why, args = poolWhy, poolArgs
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Syntakstjek og alle tests**
|
||||||
|
|
||||||
|
Run: `luajit -bl FS25_ADSmartPickup/adFieldWork.lua >/dev/null && echo syntaks-ok; for f in tests/test_*.lua; do luajit $f >/dev/null 2>&1 || echo "FEJL $f"; done; echo færdig`
|
||||||
|
Expected: `syntaks-ok`, ingen `FEJL`-linjer, `færdig`
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add FS25_ADSmartPickup/adFieldWork.lua FS25_ADSmartPickup/l10n/l10n_da.xml FS25_ADSmartPickup/l10n/l10n_en.xml
|
||||||
|
git commit -m "feat(pulje): markarbejdet sender en solo-traktor efter et puljeredskab når ingen færdig kombination er ledig (fase fetching)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Markarbejde — hjemtur når opgaven slutter
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `FS25_ADSmartPickup/adFieldWork.lua` (`finishAssignment` :834-862, fejlvejen i `follow` "nåede ikke" :1115-1120)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `ADHitch.returnHome(adEnv, tractor, onDone) -> bool`.
|
||||||
|
- Produces: ingen nye navne.
|
||||||
|
|
||||||
|
- [ ] **Step 1: finishAssignment**
|
||||||
|
|
||||||
|
Erstat i `finishAssignment`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local park = not assignment.adHandlesEnd and not assignment.manual and parkMarkerId(adEnv, vehicle) or nil
|
||||||
|
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
||||||
|
```
|
||||||
|
|
||||||
|
med:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- puljeredskab bag traktoren: sæt det af på sin parkering, traktoren hjem (afhænger kun af hvad der hænger
|
||||||
|
-- bag, så det virker også efter genindlæsning); ellers som før
|
||||||
|
local pooled = not assignment.adHandlesEnd and not assignment.manual and ADHitch ~= nil
|
||||||
|
and ADHitch.returnHome(adEnv, vehicle, function(_, ok, detail)
|
||||||
|
if not ok then log(assignment.fieldId, "'%s' kom ikke hjem fra puljen: %s", ADRuns.vehicleLabel(vehicle), tostring(detail)) end
|
||||||
|
end)
|
||||||
|
local park = not pooled and not assignment.adHandlesEnd and not assignment.manual and parkMarkerId(adEnv, vehicle) or nil
|
||||||
|
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
||||||
|
if pooled then log(assignment.fieldId, "'%s' sætter redskabet af og kører hjem", ADRuns.vehicleLabel(vehicle)) end
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: "nåede ikke frem" og "tom" — også hjem med redskabet**
|
||||||
|
|
||||||
|
I `follow`, i grenen der slutter med
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local park = parkMarkerId(adEnv, vehicle)
|
||||||
|
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
||||||
|
```
|
||||||
|
|
||||||
|
erstat de to linjer med:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
if not (ADHitch ~= nil and ADHitch.returnHome(adEnv, vehicle)) then
|
||||||
|
local park = parkMarkerId(adEnv, vehicle)
|
||||||
|
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
I `finishAssignment`, i "tom såmaskine/sprøjte"-grenen efter `ADFieldWork.release(assignment)`, indsæt:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
if ADHitch ~= nil then ADHitch.returnHome(adEnv, vehicle) end
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Syntakstjek og alle tests**
|
||||||
|
|
||||||
|
Run: `luajit -bl FS25_ADSmartPickup/adFieldWork.lua >/dev/null && echo syntaks-ok; for f in tests/test_*.lua; do luajit $f >/dev/null 2>&1 || echo "FEJL $f"; done; echo færdig`
|
||||||
|
Expected: `syntaks-ok`, ingen `FEJL`, `færdig`
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add FS25_ADSmartPickup/adFieldWork.lua
|
||||||
|
git commit -m "feat(pulje): når opgaven slutter (eller fejler) sættes puljeredskabet af på sin parkering og traktoren kører hjem"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Kø — længst ventende mark først
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `FS25_ADSmartPickup/adFieldJobs.lua` (`stepField` dispatch-gren ~:421-426, `tick` løkke ~:486)
|
||||||
|
- Test: `tests/test_adPool.lua` (dækket af `queueOrder`-tests i Task 1)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `ADPool.queueOrder(infos, waitingSince)`.
|
||||||
|
- Produces: `ADFieldJobs.waitingSince = {[fieldId] = nowMs}`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Registrér ventetid**
|
||||||
|
|
||||||
|
I `adFieldJobs.lua` ved siden af `ADFieldJobs.lastDecision = {}`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Hvornår en mark begyndte at vente på en maskine (kø: længst ventende får næste ledige traktor).
|
||||||
|
ADFieldJobs.waitingSince = {}
|
||||||
|
```
|
||||||
|
|
||||||
|
I `stepField`, erstat dispatch-grenen:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local before = ADFieldJobs.assignmentCount()
|
||||||
|
ADFieldWork.dispatch(adEnv, field, info, decision)
|
||||||
|
if ADFieldJobs.assignmentCount() > before then ADFieldJobs.lastDispatchAt = ADFieldJobs.now end
|
||||||
|
```
|
||||||
|
|
||||||
|
med:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local before = ADFieldJobs.assignmentCount()
|
||||||
|
ADFieldWork.dispatch(adEnv, field, info, decision)
|
||||||
|
if ADFieldJobs.assignmentCount() > before then
|
||||||
|
ADFieldJobs.lastDispatchAt = ADFieldJobs.now
|
||||||
|
ADFieldJobs.waitingSince[info.id] = nil
|
||||||
|
elseif ADFieldJobs.waitingSince[info.id] == nil then
|
||||||
|
ADFieldJobs.waitingSince[info.id] = ADFieldJobs.now
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
I samme funktion, i grenen `decision.action == "advance"` efter `ADFieldJobs.setField(nextField)`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
ADFieldJobs.waitingSince[info.id] = nil
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Kørsel i kø-rækkefølge**
|
||||||
|
|
||||||
|
I `ADFieldJobs.tick` erstat `for index, info in ipairs(ADFields.list()) do` med:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local infos = ADFields.list()
|
||||||
|
if ADPool ~= nil then infos = ADPool.queueOrder(infos, ADFieldJobs.waitingSince) end
|
||||||
|
for index, info in ipairs(infos) do
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Tests**
|
||||||
|
|
||||||
|
Run: `for f in tests/test_*.lua; do luajit $f >/dev/null 2>&1 || echo "FEJL $f"; done; luajit -bl FS25_ADSmartPickup/adFieldJobs.lua >/dev/null && echo syntaks-ok`
|
||||||
|
Expected: ingen `FEJL`, `syntaks-ok`
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add FS25_ADSmartPickup/adFieldJobs.lua
|
||||||
|
git commit -m "feat(pulje): marker der venter på en maskine betjenes længst-ventende først"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: Version, build, deploy og in-game test
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `FS25_ADSmartPickup/modDesc.xml` (`<version>1.47.4.0</version>` → `1.48.0.0`)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Bump version, alle tests, build, deploy**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/fs25-adsmartpickup
|
||||||
|
sed -i 's#<version>1\.47\.4\.0</version>#<version>1.48.0.0</version>#' FS25_ADSmartPickup/modDesc.xml
|
||||||
|
for f in tests/test_*.lua; do luajit $f >/dev/null 2>&1 || echo "FEJL $f"; done
|
||||||
|
rm -f FS25_ADSmartPickup.zip; (cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .)
|
||||||
|
cp FS25_ADSmartPickup.zip ~/FS25-data/mods/.FS25_ADSmartPickup.new && mv ~/FS25-data/mods/.FS25_ADSmartPickup.new ~/FS25-data/mods/FS25_ADSmartPickup.zip
|
||||||
|
unzip -p ~/FS25-data/mods/FS25_ADSmartPickup.zip modDesc.xml | grep -m1 '<version>'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: ingen `FEJL`, `<version>1.48.0.0</version>`
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit og push**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add FS25_ADSmartPickup/modDesc.xml
|
||||||
|
git commit -m "chore: v1.48.0.0 — maskinpulje etape 1 (markarbejde)"
|
||||||
|
TOKEN=$(grep -A15 -i "^## Gitea" ~/credentials-vault.md | grep -oE "[a-f0-9]{40}" | head -1); git -c http.extraHeader="Authorization: Basic $(printf 'masterdraco:%s' "$TOKEN" | base64 -w0)" push -q gitea main
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: In-game test (savegame4, Golden Lands) — spilleren udfører, vi læser loggen**
|
||||||
|
|
||||||
|
Forudsætning: et redskab til markarbejde (fx kultivator/plov) med AD-parkering "X" og markør "Connect X" (bakkespor), Challengeren solo med egen AD-parkering, og en mark med markør der skal kultiveres/pløjes (plan auto eller Start).
|
||||||
|
|
||||||
|
Forventet i `log.txt` (grep `ADSmartPickup`):
|
||||||
|
1. `F<n>: cultivator: 'Challenger MT635' henter '<redskab>' (pulje)`
|
||||||
|
2. `kobling: 'Challenger MT635' koblede '<redskab>' på ved 'Connect X'`
|
||||||
|
3. `F<n>: cultivator 'Challenger MT635' har '<redskab>' på — kører til <markør>`
|
||||||
|
4. CP arbejder; `F<n>: cultivator '…' færdig`
|
||||||
|
5. `F<n>: '…' sætter redskabet af og kører hjem`, `kobling: … koblede '<redskab>' af ved 'X'`, `… er færdig med turen (2 trin)`
|
||||||
|
|
||||||
|
Fejlscenarie: flyt redskabet 3 m væk fra sin plads → `kunne ikke koble … på` efter 8 s, traktoren kører hjem, fanen viser årsagen, redskabet springes over i 10 min.
|
||||||
Reference in New Issue
Block a user