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