4 Commits

77 changed files with 22 additions and 18694 deletions
+4
View File
@@ -1 +1,5 @@
*.zip
docs/superpowers/
bench/
.superpowers/
tests/
+2
View File
@@ -8,6 +8,8 @@ står i [docs/BRUGERMANUAL.md](docs/BRUGERMANUAL.md) (dansk) og [docs/USER_MANUA
## 1.60 — "Kræver handling" → flow, menuplacering, flow-vogne i guiden
- **1.60.14** Afkobling ved redskabets egen parkering godtager 8 m (før 5 m): lange redskaber som en valse står
med roden over 5 m inde i båsen, så afkoblingen fejlede med "stoppedAway".
- **1.60.13** CP-genstart efter indlæsning starter kun maskiner, hvis valgte CP-job er markarbejde. Moddets
markopgaver starter CP med eget job, så HUD'en kunne stå på fx bunkersilo, og HUD-starten gav "CP: No silo found".
- **1.60.12** "Start alle" ophæver altid pausen: et flow på pause, hvis traktorer stadig kørte en gemt AD-tur,
+10 -1
View File
@@ -21,6 +21,7 @@ ADHitch.MARKER_PREFIXES = {
}
ADHitch.TIMEOUT_REASON = {attach = "noAttachable", detach = "nothingToDetach", arrive = "notArrived"}
ADHitch.ARRIVE_DISTANCE = 5 -- m fra markøren, AD standser traktoren tæt på waypointet
ADHitch.DETACH_ARRIVE_DISTANCE = 8 -- afkobling: langt redskab (valse) står med roden >5 m inde i båsen
ADHitch.STILL_SPEED = 1 -- km/t
ADHitch.START_GRACE_MS = 3000 -- så længe må AD være om at komme i gang efter fetch/drop
ADHitch.ACT_TIMEOUT_MS = 8000 -- så længe forsøges kobling efter ankomst
@@ -100,6 +101,14 @@ function ADHitch.findMarker(markers, name)
return nil
end
-- Hvor tæt på markøren sættet skal være: afkobling ved egen parkering godtager længere redskaber (in-game 29/9:
-- valsen stod 5 m inde i båsen -> 'stoppedAway'); efter bak hen til vognen udvides koblingsafstanden.
function ADHitch.arriveDistanceFor(action, crept)
if action == "detach" then return ADHitch.DETACH_ARRIVE_DISTANCE end
if crept then return ADHitch.ARRIVE_DISTANCE + ADHitch.CREPT_EXTRA_DISTANCE end
return ADHitch.ARRIVE_DISTANCE
end
-- state = {adActive, distance, speed, arriveDistance?} (arriveDistance: udvidet, når traktoren har bakket hen til vognen)
local function arriveDistanceOf(state)
return state.arriveDistance or ADHitch.ARRIVE_DISTANCE
@@ -525,7 +534,7 @@ function ADHitch.tick(adEnv, dt)
if not succeeded then job.lastReason = detail end
end
local state = vehicleState(adEnv, vehicle, job.markerId)
if job.crept then state.arriveDistance = ADHitch.ARRIVE_DISTANCE + ADHitch.CREPT_EXTRA_DISTANCE end
state.arriveDistance = ADHitch.arriveDistanceFor(job.action, job.crept)
ADHitch.step(job, state, elapsed, succeeded)
if job.state == "done" then
finish(vehicle, job, true, detail, adEnv)
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<modDesc descVersion="107">
<author>masterdraco</author>
<version>1.60.13.0</version>
<version>1.60.14.0</version>
<title>
<en>AD Smart Pickup</en>
</title>
+3 -10
View File
@@ -44,20 +44,13 @@ Alt sættes op i menuen **Smart Pickup** i ESC-menuen.
## Feedback
Fejl og idéer: opret et issue her i repoet. Vedlæg gerne `log.txt` fra
Fejl og idéer: giv udvikleren besked (issues her kræver en konto på git.powerdata.dk). Vedlæg gerne `log.txt` fra
`Documents/My Games/FarmingSimulator2025/`, eller i det mindste linjerne der starter med `ADSmartPickup`.
## Udvikling
- Mod-koden ligger i `FS25_ADSmartPickup/`, og testene ligger i `tests/` (ren Lua, kører uden spillet).
- Kør testene:
```sh
for t in tests/test_*.lua; do luajit $t >/dev/null 2>&1 || echo FAIL $t; done
```
- Mod-koden ligger i `FS25_ADSmartPickup/`.
- Byg zip: `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .`
- Design og planer: `docs/superpowers/`
---
@@ -95,5 +88,5 @@ needed for pallets.
**Docs:** [User manual](docs/USER_MANUAL.md) · [Danish manual](docs/BRUGERMANUAL.md) ·
[Changelog](CHANGELOG.md)
**Feedback:** open an issue in this repository. Attach `log.txt`, or at least the lines starting with
**Feedback:** tell the developer (issues here need an account on git.powerdata.dk). Attach `log.txt`, or at least the lines starting with
`ADSmartPickup`.
-10
View File
@@ -1,10 +0,0 @@
# Simuleringer
`bench_hak.lua` genskaber "hak hvert 5. sekund" uden spillet: falsk verden i brugerens proportioner
(13.550 waypoints, 600 markører, 120 stationer, 40 køretøjer, 15 holdende vogne) og modulernes rigtige kode.
luajit bench/bench_hak.lua FS25_ADSmartPickup # nuværende kode
git worktree add /tmp/old <commit>; luajit bench/bench_hak.lua /tmp/old/FS25_ADSmartPickup # sammenlign
Resultat 26/9: værste frame 124,7 ms (v1.27.3) -> 0,9 ms (v1.28.0), 0 tunge frames efter opvarmning.
luajit er hurtigere end spillets Lua: forholdet før/efter er det der tæller, ikke de absolutte ms.
-103
View File
@@ -1,103 +0,0 @@
-- Simulering af "hak hvert 5. sekund" (kør: luajit bench/bench_hak.lua <mappe med modulerne>)
-- Falsk verden i brugerens proportioner: 13.550 waypoints, 600 markører (150 Wait), 120 stationer med 3
-- triggere, 40 køretøjer, 15 holdende vogne der genplanlægger hvert 5. s. Kører modulernes RIGTIGE kode
-- (Wait-punktsøgning, markør -> station, salgssteder) frame for frame og måler det værste frame.
-- luajit er hurtigere end spillets Lua; FORHOLDET mellem før og efter er det der tæller.
local dir = arg[1] or "FS25_ADSmartPickup"
math.randomseed(42)
local function noop() end
Logging = {info = noop, warning = noop}
MathUtil = {vector2Length = function(x, z) return math.sqrt(x * x + z * z) end}
function entityExists() return true end
local nodes = {}
function getWorldTranslation(node) local p = nodes[node]; return p[1], 0, p[2] end
local simTime = 0
g_currentMission = {time = 0, getFarmId = function() return 1 end, accessHandler = {canFarmAccess = function() return true end}}
function netGetTime() return os.clock() * 1000 end
addModEventListener = noop
Utils = {prependedFunction = function(a) return a end}
-- vejnet og markører
local wayPoints, markers = {}, {}
for id = 1, 13550 do
local x, z = (id % 120) * 60 - 3600, math.floor(id / 120) * 60 - 3600
wayPoints[id] = {id = id, x = x, z = z, out = {id + 1 <= 13550 and id + 1 or 1}, incoming = {id > 1 and id - 1 or 13550}}
end
for index = 1, 600 do
local id = index * 22
table.insert(markers, {id = id, markerIndex = index, name = (index <= 150 and "Wait " or "Marker ") .. index, group = index <= 150 and "Cow Wait" or "All"})
end
-- stationer ved hver 5. markør, 3 triggere hver
local stations = {}
for index = 1, 120 do
local wp = wayPoints[markers[150 + index].id]
local triggers = {}
for t = 1, 3 do
local node = #nodes + 1
nodes[node] = {wp.x + t, wp.z + t}
table.insert(triggers, {exactFillRootNode = node})
end
table.insert(stations, {unloadTriggers = triggers, owningPlaceable = {}, getOwnerFarmId = function() return 1 end, name = "S" .. index,
isa = function() return false end, getIsFillTypeAllowed = function() return true end})
end
g_currentMission.storageSystem = {getUnloadingStations = function() return stations end, getLoadingStations = function() return {} end}
g_currentMission.placeableSystem = {placeables = {}}
-- køretøjer
local vehicles = {}
for index = 1, 40 do
local node = #nodes + 1
nodes[node] = {math.random(-3600, 3600), math.random(-3600, 3600)}
table.insert(vehicles, {rootNode = node, spec_motorized = {}, getChildVehicles = function() return {} end})
end
g_currentMission.vehicleSystem = {vehicles = vehicles}
local adEnv = {ADGraphManager = {
getMapMarkers = function() return markers end,
getWayPointById = function(_, id) return wayPoints[id] end,
getMapMarkerByWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id then return m end end end,
getPathTo = function(_, _, id) return {{id = id}} end,
}}
for _, file in ipairs({"adCache.lua", "adSupplyPlanner.lua", "adWaitPool.lua", "adPrices.lua", "adSmartPickup.lua"}) do
local chunk = loadfile(dir .. "/" .. file)
if chunk ~= nil then pcall(chunk) end
end
if ADCache ~= nil then ADCache.enabled = true end
ADPrices = ADPrices or {}
ADPrices.isSellingStation = ADPrices.isSellingStation or function() return true end
-- én genplanlægning for en holdende vogn: Wait-punkt + stationsopslag over alle markører (salgssteder/lager)
local function replan(vehicle)
ADWaitPool.pickFree(adEnv, vehicle, "Cow Wait", function() return true end)
for _, marker in ipairs(markers) do
ADSmartPickup.getUnloadStationAtWayPointId(adEnv, marker.id, 1)
end
end
local t0 = os.clock()
replan(vehicles[1])
local single = (os.clock() - t0) * 1000
-- 30 s med 60 fps; 15 holdende vogne. Gammel kode: alle timere starter på 0. Ny: jitter hvis koden har det.
local jitter = ADCache ~= nil and 5000 or 0 -- ny kode spreder vognenes genplanlægning over 0-5 s (adOutbound CHECK_JITTER_MS)
local timers = {}
for index = 1, 15 do timers[index] = jitter > 0 and math.random(0, jitter) or 0 end
local worst, steady, frames = 0, 0, 0
for frame = 1, 60 * 60 do
simTime = simTime + 16.7
g_currentMission.time = simTime
local started = os.clock()
for index = 1, 15 do
timers[index] = timers[index] + 16.7
if timers[index] >= 5000 then
timers[index] = 0
replan(vehicles[index])
end
end
local spent = (os.clock() - started) * 1000
if spent > worst then worst = spent end
if simTime > 12000 and spent > steady then steady = spent end
if simTime > 12000 and spent > 3 then heavy = (heavy or 0) + 1; heavyAt = heavyAt or {}; if #heavyAt < 8 then table.insert(heavyAt, string.format("%.1fs:%.1fms", simTime / 1000, spent)) end end
frames = frames + 1
end
print(" tunge frames (>3 ms) efter opvarmning:", heavy or 0, table.concat(heavyAt or {}, " "))
print(string.format("%-12s én genplanlægning %.2f ms | værste frame (60 s): %.2f ms | værste efter opvarmning: %.2f ms", dir:find("old") and "FØR" or "EFTER", single, worst, steady))
+1 -1
View File
@@ -1116,7 +1116,7 @@ ADSmartPickup: F109 …
Moden testes løbende, og fejlrapporter er meget velkomne.
1. Opret et issue i repoet: <https://git.powerdata.dk/masterdraco/fs25-adsmartpickup/issues>
1. Giv udvikleren besked — eller opret et issue i repoet, hvis du har en konto: <https://git.powerdata.dk/masterdraco/fs25-adsmartpickup/issues>
2. Beskriv, hvad du gjorde, hvad du forventede, og hvad der skete. Nævn versionen (står i
`log.txt` og i mod-listen).
3. Vedlæg `log.txt` fra `Documents/My Games/FarmingSimulator2025/log.txt`. Er filen for stor, så vedlæg
-255
View File
@@ -1,255 +0,0 @@
# Testplan — Markarbejde (fanen Marker), version 1.28.0.0 (+ guider v1.31/v1.32)
Stor test-session. Kør testene i rækkefølge: de første bygger opsætningen, de senere bygger på den.
Hver test har **Gør**, **Forvent** og **Log** (linjer i `log.txt`, der beviser at det virkede). Loglinjer
begynder altid med `ADSmartPickup: F<marknr>`.
Kryds af undervejs. Skriv resultatet (OK / FEJL + hvad der skete) ud for hver test, så retter vi i
samme rækkefølge.
---
## 0. Forberedelse
- [ ] **0.1 Version.** Log: `Available mod: … (Version: 1.28.0.0) FS25_ADSmartPickup`.
- [ ] **0.2 Fanen Marker åbner.** ESC → Smart Pickup → fanen **Marker**. Forvent: "Marker indlæses …"
i få sekunder, derefter én række pr. ejet mark (også korn-, roe- og bare marker, ikke kun græs).
Afgrøde-kolonnen viser afgrøden eller "bar jord".
- [ ] **0.3 F-markører.** Marker uden `F<nr>`-markør står med `!` foran marknummeret og "! mangler
markør F…". Lav markører til de marker, der skal testes.
- [ ] **0.4 Markflåden.** Nederst: alle maskiner med slåmaskine, vender, river, læssevogn, høster,
aflæsningsvogn, kultivator, plov, såmaskine, tromle, strigle/sprøjte, gødnings-/kalkspreder,
stubhakker — med rolle og bredde. Tjek:
- [ ] Høstere viser "N afgrøder".
- [ ] Såmaskiner viser "frø ok" eller "!! tom for frø".
- [ ] Strigle viser "strigle (småt ukrudt)", ukrudtssprøjte viser "sprøjte".
- [ ] En **tom** sprøjte/spreder vises ikke (den har ingen rolle før den har fyld på).
- [ ] Maskiner i et Smart Pickup-flow kan godt ses, men sendes aldrig ud.
- [ ] **0.5 Bredder.** BiG M 450 viser **40,8 m** (Courseplays egen bredde), læssevognene 40,8 m.
## 1. Etape 1 genbekræftet (græs → ensilage)
Rest fra 25/9: slåning og opsamlingsstart virkede; afslutningen blev aldrig set.
- [ ] **1.1 Afslutning af opsamling.** Mark 22 (eller en anden lille græsmark) på Manuel + Ensilage,
BiG M + én Boss Alpin med flueben → **Start nu**.
- Forvent: slåmaskine → F22 → Courseplay slår → hjem. Læssevogn → F22 → Courseplay samler op med
**slåmaskinens kursus** → AutoDrive tømmer ved fuld → tilbage → færdig → rest til lager → parkering.
Marken står **Færdig**, og der kommer en grøn besked "F22 er færdig".
- Log: `mower … sendt til F22`, `Courseplay startet (mower)`, `mowing -> collecting`,
`læssevogn … aflæsser i <gæringssilo>`, `Courseplay startet (collector, mower-kursus kopieret)`,
`collector … færdig`, `collecting -> done`.
- [ ] **1.2 Gæringssilo.** Ensilage-læs går til nærmeste **gæringssilo** (FermentingSilo/GrassSilageSilo),
ikke en Farma-silo. Log: `aflæsser i <gæringssilo>` eller `ingen gæringssilo kan tage græsset (…)`.
- [ ] **1.3 Sidde i maskinen.** Sæt dig i slåmaskinen mens Courseplay kører, og stig ud igen.
Forvent: den slippes IKKE (ingen `frigivet`-linje). Kør selv > 3 km/t uden CP → `frigivet — spilleren kører det`.
- [ ] **1.4 Sovende køretøj.** Parkerede maskiner kører af sted med det samme ved udsendelse, uden at
du skal sætte dig ind.
## 2. Hø og river
- [ ] **2.1 Hø uden river.** Plan = **Hø**, Riv = Nej. Slå → **Vendes** (venderen i sin egen bredde) →
Samles op (læssevogn på slåkurset, ≥ 40,8 m) → hø-silo (kun almindelig silo, aldrig gæringssilo).
Log: `mowing -> tedding`, `Courseplay startet (tedder)`, `tedding -> collecting`.
- [ ] **2.2 Smal læssevogn uden river.** En læssevogn under 40,8 m → status "læssevogn for smal:
… — sæt Riv til eller brug en bredere".
- [ ] **2.3 Riv (midterrive).** Riv = Ja. Log: `river lægger skåret 0.x m fra sin kursuslinje`,
derefter `Courseplay startet (collector, rake-kursus kopieret)`. Læssevognen rammer skårene.
- [ ] **2.4 Sideriver.** Med en river der lægger skåret til siden: log viser fx `3.2 m`, og
`skår ligger 3.2 m til … -> læssevognens forskydning …`. **Tjek at vognen kører OVER skåret.**
Kører den ved siden af (forkert side), så skriv det ned — så vender jeg fortegnet.
- [ ] **2.5 Riv til ensilage.** Plan = Ensilage + Riv = Ja: Slå → Rives → Samles op.
## 3. Høst (korn, raps, majs …)
- [ ] **3.1 Høst.** En moden kornmark, Manuel, høster med passende skærebord + én aflæsningsvogn
(traktor + tipvogn/overlæssevogn) med flueben → **Start nu**.
- Forvent: høster → F-markør → Courseplay høster. Aflæsningsvognen sendes ud når høsteren
arbejder, følger den (Courseplays combine-unloader) og kører fulde læs til siloen.
- Log: `harvester … sendt`, `Courseplay startet (harvester)`, `unloader … sendt`,
`aflæsningsvogn … følger høsteren -> <silo>`.
- [ ] **3.2 Afslutning.** Høsten færdig → `harvesting -> <næste trin eller done>`,
`aflæsningsvogn … stoppes — høsten er færdig`, evt. `kører rest-læs … til lager`, parkering.
- [ ] **3.3 Forkert skærebord.** Høster der ikke kan tage afgrøden → status "ingen høster i
markflåden kan tage opgaven (afgrøde/frø/ukrudt)". Ingen udsendelse.
- [ ] **3.4 Flere aflæsningsvogne.** "Aflæsningsvogne under høst" = 2 → to vogne sendes (én ad gangen).
- [ ] **3.6 Halm samles op.** Halm efter høst = *Samles op til silo*. Mejetærskeren lægger skår
(Courseplay-indstilling halmskår = til), derefter `harvesting -> strawCollecting`, en læssevogn kører
**høsterens kursus** (`harvester-kursus kopieret`) og kører halmen til en silo der tager STRAW.
- [ ] **3.7 Halm til baller.** Halm = *Presses til baller*: `-> baling` (ballepresser på høsterens
kursus) → `-> baleCollecting` (ballesamler/Universal Autoload, `Courseplay startet (ballesamler)`),
fulde læs til ballelager (`ballesamler … kører fulde læs til …`).
- [ ] **3.8 Halm hakkes.** Halm = *Hakkes*: mejetærskeren lægger IKKE skår (Courseplay halmskår = fra).
- [ ] **3.5 Regn.** Når det regner: høst/slå/vend/riv sendes ikke ud, status "venter på tørvejr".
Kørende maskiner fortsætter.
## 4. Roer
- [ ] **4.1 Roehøst.** Roemark (sukkerroer), roeoptager (Ropa) + aflæsningsvogn → som 3.1, men med
roer. Log viser `harvester 'Ropa …'`. Roerne går til nærmeste silo/lager der tager SUGARBEET
(brug **Tving lager** hvis de skal et bestemt sted hen, fx roeknuseren).
## 5. Efter høst: klargøring og såning
Sæt på en kornmark før høst: Efter høst → Stubbehakning Nej, Jordbehandling **Auto**, **Så bagefter** Ja,
Afgrøde "Samme som høstet", Tromle Ja. Kalk Ja.
- [ ] **5.1 Kæde.** Efter høsten fortsætter marken selv: (kalk hvis jorden trænger) → pløj eller
kultivér → så → tromle → **Færdig**. Log: `harvesting -> cultivating`, `cultivating -> sowing` …
- [ ] **5.2 Auto-jordbehandling.** Står "Jorden trænger til: pløjning" under marken → plov sendes.
Ellers kultivator.
- [ ] **5.3 Såning.** Såmaskinen sættes til afgrøden (`setSeedIndex`), og den rigtige afgrøde spirer.
- [ ] **5.4 Ude af såperioden.** Afgrøde der ikke kan sås nu → status "<afgrøde> kan ikke sås i denne
periode", ingen udsendelse.
- [ ] **5.5 Tom for frø / påfyldning.** Såmaskine med lidt frø. Moden leder efter en læssestation med frø
på lager og en AutoDrive-markør ved triggeren (fx en silo/big-bag-station med SEEDS).
- Findes den: log `… fyldes op med SEEDS ved <markør> af AutoDrive når den er tom`. Når den er tom
midt på marken, kører AutoDrive derhen, fylder op, kører tilbage, og Courseplay fortsætter.
- Findes den ikke: log `ingen påfyldning af SEEDS med AutoDrive-markør fundet …`. Når den er tom:
rød besked `'…' er tom for SEEDS — fyld op, så fortsætter marken`, og marken går IKKE videre.
- [ ] **5.6 Bar mark alene.** Vælg en bar mark (stub/pløjet) → **Start nu** → kun klargøring/såning.
## 6. Pleje mens afgrøden vokser
- [ ] **6.1 Behov vises.** En voksende mark viser "Jorden trænger til: gødning, ukrudt …" og status
"— trænger til Gødes". Manuel: intet sker før **Start nu**.
- [ ] **6.2 Gødning.** Start nu (eller Auto) → gødningsspreder/-sprøjte med fyld sendes → `pleje:
fertilizing` → `fertilizing -> idle`. Højst 2 gange pr. vækstcyklus.
- [ ] **6.3 Ukrudt.** Høstafgrøde med tæt ukrudt → strigle (småt ukrudt) eller ukrudtssprøjte
(stort ukrudt, > 4). Græs ukrudtsbehandles ikke.
- [ ] **6.4 Kalk.** Jord der trænger til kalk → kalkspreder.
- [ ] **6.5 Påfyldning.** Sprøjte/spreder der løber tør → AutoDrive fylder op og kører tilbage.
- [ ] **6.6 Tom sprøjte/spreder (v1.29.2).** Tom sættes i gang → log `er tom — fyldes med X ved <markør> og
kører så til F<nr>` → AutoDrive (LOAD) fylder op og kører til marken → Courseplay starter med fyld.
Ingen påfyldning med AD-markør → sendes IKKE ud; marken viser "er tom for X … fyld den op".
Sikkerhedsnet: Courseplay startes aldrig med tom tank ("er stadig tom … ved marken").
- [ ] **6.7 Forkert vare (v1.28.8).** Spreder med kalk sendes til gødskning kun hvis ingen tom/gødet spreder
findes → log `tømt for N l LIME (skal bruges til fertilizer)` → fyldes med gødning.
## 7. Multi-maskine (Courseplay multi-tool)
- [ ] **7.1 To slåmaskiner.** "Maskiner pr. trin" = 2 på en græsmark med to BiG M med flueben.
Forvent: begge sendes samtidig, den ene laver kurset (`bane -1`), den anden kopierer det (`bane 1`)
og de kører i konvoj. Log: `… sendt … bane -1`, `… bane 1`, `Courseplay startet (mower, bane -1)`,
`Courseplay startet (mower, multi-kursus kopieret, bane 1)`.
- [ ] **7.2 For få maskiner.** 3 pr. trin men kun 2 ledige → status "multi-tool: slåmaskine — kun 2 af 3 ledige".
- [ ] **7.3 Læssevogne på multi-kursus.** Efter 7.1 sendes 2 læssevogne, én pr. bane.
## 9. Vælg og glem (afgrøde på marken)
- [ ] **9.1 Kornmark → hvede.** En mark med byg der vokser: vælg *Afgrøde på marken* = Hvede. Linjen siger
"Byg får lov at vokse færdig og høstes; derefter klargøres og tilsås med Hvede". Kørsel skjules (auto).
Når byggen er moden: høst → (halm) → klargøring → såning af **hvede** (ikke byg). Log:
`mål-afgrøde: WHEAT`, `starter (harvest: harvesting)`, `sowing`.
- [ ] **9.2 Bar mark.** Bar mark med mål = Hvede: starter selv klargøring + såning, når det er hvede-såtid.
Uden for såtid: status "venter på såtid for Hvede".
- [ ] **9.3 Græs → græs.** Græsmark med mål = Græs: slås og samles op hver gang, ingen pløjning/såning.
- [ ] **9.4 Græs → hvede.** Græsmark med mål = Hvede: slås og samles op én gang, derefter klargøring
(jordbehandling-valget vises) og såning af hvede.
- [ ] **9.5 Hele året.** Lad en mark med mål køre en hel sæson uden at røre den: vækst → pleje → høst →
klargøring → såning → ... uden at hænge og uden dobbelt-såning.
- [ ] **9.6 Alle ledige maskiner.** Markflåde: *Brug alle ledige maskiner* = Ja (standard): maskiner uden
flueben sendes også ud. = Nej: kun maskiner med flueben. Maskiner du selv kører, maskiner i flows og
maskiner AutoDrive/Courseplay allerede kører, bruges aldrig.
- [ ] **9.7 Fjern målet.** Vælg *Ingen — planlæg selv*: marken går tilbage til Manuel.
## 8. Robusthed
- [ ] **8.1 Genindlæsning midt i et trin.** Gem og genstart under fx opsamling → marken fortsætter på
samme trin, sæt sendes ud igen, slåkurset hentes fra slåmaskinen.
- [ ] **8.2 Stop mark.** **Stop mark** midt i høst → alle maskiner på marken stopper; høst står **Klar**
igen, pleje/klargøring står **Venter**.
- [ ] **8.3 Ingen vej.** Mark hvis F-markør ikke er forbundet → status "… nåede ikke F…" / "intet lager:
… AutoDrive finder ingen rute …" og nyt forsøg efter 5 min.
- [ ] **8.4 Beskeder.** Grøn besked når en mark/pleje er færdig; rød besked ved fejl (hver kun én gang i træk).
- [ ] **8.5 Ydelse.** Ingen mærkbar lag med fanen åben og mange marker (én mark scannes pr. sekund).
## 10. Guiden "Opsæt mark" (v1.31)
- [ ] **10.1 Oversigt.** Klik F22 i tabellen. Forvent: resumé-linjer (Afgrøde, …, Lager) med "[Ret]", hele
status, markør. Ingen lang detaljeliste og ingen markflåde nederst. Knapper: Start nu, Opsæt mark, Markflåde.
- [ ] **10.2 Guiden fra bunden.** Opsæt mark → "Trin 1 af N — Afgrøde". Klik Næste gennem alle trin uden at
scrolle. Tællingen passer med de viste trin (græs: ingen Klargøring; bar mark: ingen Pleje/Lager).
- [ ] **10.3 Valg gemmes.** Vælg fx Hø + Riv, 2 ad gangen, Lager: Vælg selv + en silo → Oversigt viser valgene
→ **Kør automatisk**. Forvent: tabellens Plan "Hø + riv (x2)", Kørsel "Auto". Log: `F22 guide gemt: mode=auto …`.
- [ ] **10.4 Annullér.** Åbn guiden, skift afgrøde, Annullér → intet ændret. Samme med ESC (menuen lukkes).
- [ ] **10.5 Ret én linje.** Klik "Lager: …" i oversigten → "Ret: Lager"; skift → **Gem** → tilbage i
oversigten med ændringen; Kørsel (Auto/Manuel) uændret. Tilbage i et Ret-trin fortryder.
- [ ] **10.6 Ret fra Oversigt-trinnet.** I guidens sidste trin: klik en linje → ret → **OK** → tilbage på Oversigt.
- [ ] **10.7 Mål-afgrøde skifter trin.** Kornmark: vælg Græs som afgrøde → trinnet "Afgrødens valg" viser
Ensilage/Hø; tællingen ændres. På Oversigt kun **Kør automatisk** (ingen "Gem uden at starte").
- [ ] **10.8 Faste maskiner.** Maskiner → Faste = Ja → sæt flueben ved 2 maskiner → gem. Åbn en anden mark:
de 2 vises ikke; linje "2 maskiner er faste på andre marker (F22)". Log: `guide gemt … faste=…`.
- [ ] **10.9 Marksilo.** Mark med silo inden for 150 m: Lager-trinnet spørger "Brug marksiloen?". Ja → resumé "Marksilo".
- [ ] **10.10 Uden valgt mark.** Ingen mark valgt → Opsæt mark → trin "Mark" med listen; klik en mark → Afgrøde.
Næste uden valgt mark giver "Vælg en mark først".
- [ ] **10.11 Markflåde.** Knappen Markflåde viser flåden (flueben, "fast på F22"); klik et flueben gemmes;
**Til marker** går tilbage.
- [ ] **10.12 Klik routes.** Alle Ja/Nej, valg og flueben i guiden reagerer (ingen "GUI-fejl" i statuslinjen/log).
## 11. Guiden "Opsæt flow" (v1.32)
- [ ] **11.1 Flows-fanen.** Tavlen øverst; valgt flow (">") har resumé-linjer med "[Ret]" og Status. Ingen
lang detaljeliste og ingen faste felter (Run, Wait-pulje …). Knapper: Start flow, Opsæt flow, Nyt flow, Slet flow.
- [ ] **11.2 Nyt flow.** Nyt flow → "Trin 1 af 8 — Navn" → *Skift navn …* → skriv navn → Næste gennem Retning
(Ud fra bygning), Fra (en fabrik), Varer (sæt en vare på Lager → trinnet Lager dukker op, tællingen bliver 9),
Regler, Vent ved, Traktorer (flueben) → Oversigt → **Start flow**. Forvent: flowet findes, kører, log
`run '…': …` med kind/products/storeBuildingIds.
- [ ] **11.3 Annullér nyt flow.** Nyt flow → Annullér (eller ESC) → intet nyt flow i tavlen.
- [ ] **11.4 Ret én linje.** Klik "Traktorer: …" → "Ret: Traktorer" → sæt flueben → **Gem** → tilbage; traktoren er i flowet.
- [ ] **11.5 Omdøb.** Klik "Navn: …" → tekstfeltet åbner → nyt navn står i tavlen.
- [ ] **11.6 Andre flows skjult.** Traktor i et andet flow vises ikke i trinnet Traktorer; linje "N traktorer kører i andre flows".
- [ ] **11.7 Forsyning.** Opsæt et forsyningsflow: Til = stald, Varer = Vælg selv → varerne stalden tager vises; Regler viser Ture og "Kør hjem fra Wait under".
- [ ] **11.8 Nyt flow fra Oversigt-fanen** hopper til Flows med guiden åben.
- [ ] **11.9 Klik routes.** Alle valg (også pr. vare-politik) reagerer; ingen "GUI-fejl".
---
## 12. Genbrug af Courseplay-kurser (v1.34)
Log-linjer at kigge efter (`ADSmartPickup: F<nr> …`): `nyt kursus beregnes til '<maskine>' (<grund>)`,
`kursus genbrugt ('<maskine>', <bredde> m) — ingen ny beregning`, `kursus kopieret fra '<A>' til '<B>'`,
`markens form er ændret — N gemt(e) kursus(er) glemt`. Registret står i `ADSmartPickup_fields.xml` under `<courses>`.
- [ ] **12.1 Første gang = ny beregning.** Stor mark (Dakota 16x) → kultivator sendes ud → log `nyt kursus beregnes … (intet kursus gemt)`
→ CP starter; `<courses><course vehicle=… field=… width=… sig=… settings=… waypoints=…/>` står i filen efter gem.
- [ ] **12.2 Genbrug.** Samme maskine til samme mark igen (næste pleje/klargøring, eller efter "Stop" og start) → log
`kursus genbrugt` → CP kører UDEN beregnings-hak og kører hele marken fra start (ikke kun sidste stykke).
⚠ Usikkert: accepterer CP 8.1 at jobbet startes på det eksisterende kursus uden `onClickGenerateFieldWorkCourse`?
(validate kræver kun `hasCpCourse`). Tegn på fejl: `Courseplay: …no course…`, eller maskinen kører ingenting.
- [ ] **12.3 Efter genstart.** Gem, genindlæs, vent de 2 min → samme maskine til samme mark → `kursus genbrugt`
(CP har selv gemt kurset i CpAssignedCourses.xml; antal punkter matcher registret).
- [ ] **12.4 Kopi fra anden maskine.** En anden maskine med samme CP-bredde til samme mark → `kursus kopieret fra 'A'`
→ den kører kurset (ikke A's rest). ⚠ Usikkert: kopieret kursus + lane/vin som i dag med `cpCopyCourse`.
- [ ] **12.5 Ny bredde.** Skift CP-bredden i HUD'en (eller en anden maskine med anden bredde) → `nyt kursus beregnes … (ny bredde)`.
- [ ] **12.6 Spilleren laver selv kursus i CP's HUD** på maskinen → næste udsendelse genbruger det IKKE
(antal punkter passer ikke) → kopi fra en anden maskine eller `… (kurset på maskinen er ændret)`.
- [ ] **12.7 Ændret mark.** Lav marken om (kort-redigering/mark-tilkøb der ændrer omridset) → efter scanning, eller når
marken gemmes i "Opsæt mark", log `markens form er ændret — N … glemt` → næste start beregner nyt og gemmer det.
- [ ] **12.8 Multi-tool.** 2 sæt på samme mark: lederen genbruger kun et kursus lavet til 2 sæt/samme bane; følgeren kopierer
stadig lederens kursus (log `…, multi-kursus kopieret, bane N`), og begge kører hver sin bane.
- [ ] **12.9 Vinmark.** Sprøjte (skip 1) og harve (skip 0) på samme vinmark må ikke genbruge hinandens kursus (vin-indstillinger
indgår i nøglen); høsteren (offset 0) heller ikke.
---
## Kendte usikkerheder (første gang i spillet)
| Område | Hvad der er gættet | Tegn på fejl |
|---|---|---|
| Vækstfase | `getFieldDataAtWorldPosition` anden returværdi = vækststadie | Høstet stub vises som voksende, eller moden mark som bar |
| Høsterens afgrøder | `spec_cutter.fruitTypes`/`fruitTypeIndices` | Høster vises med "0 afgrøder" |
| Combine-unloader | stationsvalg ud fra silovalget | `ingen aflæsningsstation Courseplay kan bruge` |
| Multi-tool | `multiTools` + `laneOffset` + `Course:setPosition` | Begge maskiner kører samme bane |
| Sideriver | fortegn på forskydningen | Læssevognen kører ved siden af skåret |
| Påfyldning | AutoDrive LOAD til den læssestation moden fandt (AutoDrives eget automatiske mål er slået fra i AutoDrive) | Sættet står ved påfyldningen og læsser ikke, eller kører ikke tilbage |
| Ukrudt | `weedSystem`-værdi ≥ 2 = tæt | Ukrudtsbehandling uden ukrudt, eller aldrig |
| Nysået som "bar" | nysået afgrøde læses som voksende | Marken står "Færdig" efter såning (ingen dobbelt-såning, men ingen næste høst-start før den er moden) |
| Halm-valg i guiden | `fruitType.hasWindrow/windrowName` = afgrøder med halm | Halm-valget mangler ved hvede eller vises ved majs |
| Ballesamler | Courseplays BALE_FINDER + AutoDrive til ballelager | Ballesamleren starter ikke, eller kører ikke fulde læs væk |
| Kursus-genbrug (v1.34) | CP-job startes på maskinens eksisterende kursus uden ny beregning | `Courseplay: no course`, eller maskinen starter midt/sidst i kurset |
- [ ] **Høsterens rest (v1.32.2).** Modet høster en mark med aflæsningsvogne → ved høstens slutning stoppes
vognene; høsteren med korn i tanken → log `'Mejetærsker' kører rest-læs (N l) til lager` → AutoDrive
tømmer den ved lageret → parkering. Kunne AD ikke tømme den: besked "har stadig korn i tanken … tøm den".
⚠ Usikkert: kan AutoDrive aflæsse en mejetærskers tank (røret) ved en silo?
+1 -1
View File
@@ -1146,7 +1146,7 @@ ADSmartPickup: F109 …
The mod is being tested continuously, and bug reports are very welcome.
1. Open an issue in the repository: <https://git.powerdata.dk/masterdraco/fs25-adsmartpickup/issues>
1. Tell the developer — or open an issue in the repository if you have an account: <https://git.powerdata.dk/masterdraco/fs25-adsmartpickup/issues>
2. Describe what you did, what you expected, and what happened. Mention the version (shown in `log.txt`
and in the mod list).
3. Attach `log.txt` from `Documents/My Games/FarmingSimulator2025/log.txt`. If the file is too big,
@@ -1,221 +0,0 @@
# Behovsstyret forsyning 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:** Én AutoDrive-traktor holder alle input i en produktion (fx TMR-blander) fyldt ved selv at vælge vare + kilde-silo pr. tur.
**Architecture:** Den eksisterende wrapper om `ADMultipleTargetsManager.getNextPickup` udvides: ved >1 valgt vare i AD læses aflæsningsmålets lager, ingredienserne rangeres efter behov (rene funktioner i ny fil), og en kilde findes blandt alle AD-markører. AD's varevalg indsnævres til én vare pr. tur og gendannes når AD stopper.
**Tech Stack:** Lua 5.1 (FS25 mod-sandbox), luajit til tests med håndskrevne mocks, AutoDrive 3.0.1.2 via global `FS25_AutoDrive`.
**Spec:** `docs/superpowers/specs/2026-09-21-behovsstyret-forsyning-design.md`
## Global Constraints
- Rør aldrig `FS25_AutoDrive.zip`; al adgang via den globale `FS25_AutoDrive`-tabel.
- Enhver fejl i spil-/AD-API → `Logging.warning` + AutoDrives originale `getNextPickup`. Aldrig crash.
- Præcis én valgt vare → opførsel uændret; de 11 eksisterende tests forbliver grønne.
- Markør↔station-tærskel: 40 m (`MAX_MARKER_TO_TRIGGER_DISTANCE`). `MIN_FREE_LITERS = 1000`.
- Rene funktioner muterer ikke input (returnér nye tabeller).
- Tests køres fra repo-roden: `luajit tests/<fil>.lua`. Commits uden Co-Authored-By.
- Verificerede AD-fakta: `selectedFillTypes` er et array-felt på stateModule og gemmes i savegame; `setFillType(ft)` bevarer valget hvis `ft` er i det; `isActive()`, `getSecondMarker()`, `raiseDirtyFlag()` findes; `ADGraphManager:getMapMarkers()` returnerer array indekseret på `markerIndex`.
---
### Task 1: `adSupplyPlanner.lua` — rene funktioner
**Files:**
- Create: `FS25_ADSmartPickup/adSupplyPlanner.lua`
- Test: `tests/test_adSupplyPlanner.lua`
**Interfaces:**
- Produces (global tabel `ADSupplyPlanner`):
- `MIN_FREE_LITERS` (number)
- `scoreNeed(level, capacity, freeSpace) -> number` (højere = mere trængende)
- `rankNeeds(needs) -> ranked` hvor `needs = {{fillType, level, capacity}, …}` og `ranked = {{fillType, level, capacity, freeSpace, score}, …}` sorteret mest trængende først, uden dem med `freeSpace < MIN_FREE_LITERS`
- `resolveIngredients(state, selected) -> ingredients|nil` hvor `state = nil | {ingredients = {ft…}, narrowedTo = ft}`
- `copyList(list) -> ny liste`
- [ ] **Step 1: Skriv fejlende test** — `tests/test_adSupplyPlanner.lua`:
```lua
-- Kør: luajit tests/test_adSupplyPlanner.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")
local HAY, GRASS, SILAGE = 20, 21, 22
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
-- scoreNeed: tommere lager scorer højere; ugyldig kapacitet giver 0
check("tomt scorer højere end halvfuldt", ADSupplyPlanner.scoreNeed(0, 1000, 1000) > ADSupplyPlanner.scoreNeed(500, 1000, 500), true)
check("kapacitet 0 giver score 0", ADSupplyPlanner.scoreNeed(0, 0, 0), 0)
-- rankNeeds: mest trængende først, fulde springes over, input muteres ikke
local needs = {
{fillType = HAY, level = 80000, capacity = 100000},
{fillType = GRASS, level = 20000, capacity = 100000},
{fillType = SILAGE, level = 99500, capacity = 100000},
}
local ranked = ADSupplyPlanner.rankNeeds(needs)
check("fuld ingrediens springes over", #ranked, 2)
check("mest trængende først", ranked[1].fillType, GRASS)
check("dernæst næstmest trængende", ranked[2].fillType, HAY)
check("freeSpace beregnes", ranked[1].freeSpace, 80000)
check("input muteres ikke", needs[1].freeSpace, nil)
check("tom liste giver tom liste", #ADSupplyPlanner.rankNeeds({}), 0)
-- rankNeeds: deterministisk ved lige score (laveste fillType først)
local tie = ADSupplyPlanner.rankNeeds({
{fillType = SILAGE, level = 0, capacity = 5000},
{fillType = HAY, level = 0, capacity = 5000},
})
check("lige score -> laveste fillType først", tie[1].fillType, HAY)
-- resolveIngredients
local multi = {HAY, GRASS}
local resolved = ADSupplyPlanner.resolveIngredients(nil, multi)
check("multi-valg bliver ingrediensliste", #resolved, 2)
check("listen er en kopi", resolved ~= multi, true)
check("enkelt-valg uden tilstand -> nil", ADSupplyPlanner.resolveIngredients(nil, {HAY}), nil)
local state = {ingredients = {HAY, GRASS, SILAGE}, narrowedTo = GRASS}
check("indsnævret valg genbruger gemt liste", #ADSupplyPlanner.resolveIngredients(state, {GRASS}), 3)
check("bruger valgte andet -> nil", ADSupplyPlanner.resolveIngredients(state, {HAY}), nil)
check("nyt multi-valg erstatter gemt liste", #ADSupplyPlanner.resolveIngredients(state, {HAY, SILAGE}), 2)
check("ugyldigt valg -> nil", ADSupplyPlanner.resolveIngredients(state, nil), nil)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
```
- [ ] **Step 2: Kør og se den fejle** — `luajit tests/test_adSupplyPlanner.lua` → fejler med "cannot open FS25_ADSmartPickup/adSupplyPlanner.lua".
- [ ] **Step 3: Implementér** — `FS25_ADSmartPickup/adSupplyPlanner.lua`:
```lua
-- AD Supply Planner
-- Rene funktioner (ingen spil-API) til behovsstyret forsyning: hvilken ingrediens
-- har aflæsningsmålet mest brug for, og hvilken ingrediensliste gælder for køretøjet.
ADSupplyPlanner = {}
-- Mindre fri plads end dette i målet regnes som "fuldt" (undgår ture med læs der ikke kan komme af).
ADSupplyPlanner.MIN_FREE_LITERS = 1000
function ADSupplyPlanner.copyList(list)
local copy = {}
for index, value in ipairs(list) do
copy[index] = value
end
return copy
end
-- Hvor meget trænger målet til denne ingrediens? Højere = hentes først.
-- Alle tal er liter i aflæsningsmålets lager. Standard: relativ tomhed (0..1),
-- så en lille næsten-tom tank vinder over en stor halvfuld.
function ADSupplyPlanner.scoreNeed(level, capacity, freeSpace)
if capacity <= 0 then
return 0
end
return freeSpace / capacity
end
-- needs: {{fillType, level, capacity}, ...}. Returnerer NY liste, mest trængende først;
-- ingredienser uden plads i målet er sorteret fra.
function ADSupplyPlanner.rankNeeds(needs)
local ranked = {}
for _, need in ipairs(needs) do
local freeSpace = need.capacity - need.level
if freeSpace >= ADSupplyPlanner.MIN_FREE_LITERS then
table.insert(ranked, {
fillType = need.fillType,
level = need.level,
capacity = need.capacity,
freeSpace = freeSpace,
score = ADSupplyPlanner.scoreNeed(need.level, need.capacity, freeSpace),
})
end
end
table.sort(ranked, function(a, b)
if a.score ~= b.score then
return a.score > b.score
end
return a.fillType < b.fillType
end)
return ranked
end
-- state: nil eller {ingredients, narrowedTo} fra forrige tur. selected: AD's aktuelle varevalg.
-- >1 valgt = (ny) ingrediensliste. Præcis den vare vi selv indsnævrede til = gemt liste.
-- Alt andet = brugeren har valgt noget andet -> nil (behovsstyring fra).
function ADSupplyPlanner.resolveIngredients(state, selected)
if type(selected) ~= "table" then
return nil
end
if #selected > 1 then
return ADSupplyPlanner.copyList(selected)
end
if state ~= nil and #selected == 1 and selected[1] == state.narrowedTo then
return ADSupplyPlanner.copyList(state.ingredients)
end
return nil
end
```
- [ ] **Step 4: Kør tests** — `luajit tests/test_adSupplyPlanner.lua` → `ALLE TESTS OK`.
- [ ] **Step 5: Commit** — `git add FS25_ADSmartPickup/adSupplyPlanner.lua tests/test_adSupplyPlanner.lua && git commit -m "feat: ADSupplyPlanner — behovsrangering og ingrediensliste-tilstand"`
---
### Task 2: Integration i `adSmartPickup.lua`
**Files:**
- Modify: `FS25_ADSmartPickup/adSmartPickup.lua` (hele filen omstruktureres let; se kode)
- Test: `tests/test_adSmartPickup.lua`
**Interfaces:**
- Consumes: `ADSupplyPlanner.rankNeeds`, `.resolveIngredients`, `.copyList` (Task 1).
- Produces: `ADSmartPickup.choosePickup(adEnv, driver) -> choice|nil` med `choice = {markerIndex, markerName, group, level, fillType?, need?}`; `ADSmartPickup.findLeastFilledPickup` returnerer nu samme `choice`-tabel (uden `fillType`).
- [ ] **Step 1: Udvid test-mocks og skriv fejlende tests** i `tests/test_adSmartPickup.lua`:
- `dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")` før `adSmartPickup.lua`.
- `unloadStations`-liste + `getUnloadingStations` i `g_currentMission.storageSystem`; nulstilles i `resetWorld()`.
- `getMapMarkers = function() return markers end` i `ADGraphManager`-mock.
- `getSecondMarker = function() return nil end` i `makeDriver` (rigtig AD har den altid).
- Nye helpers `makeUnloadStation(node, x, z, levels, capacities, placeable, isBroken)` og `makeSupplyDriver(firstMarker, secondMarker, fillTypes)` (muterbar stateModule-mock med `setFillType`, `raiseDirtyFlag`, `isActive`).
- Tests S1–S9 (se Step 1-koden i implementeringen; dækker spec'ens testliste).
- [ ] **Step 2: Kør og se de nye fejle** — `luajit tests/test_adSmartPickup.lua` → S-tests FAIL, de 11 gamle OK.
- [ ] **Step 3: Implementér** — i `adSmartPickup.lua`:
- `supplyState = setmetatable({}, {__mode = "k"})` (køretøj → `{ingredients, narrowedTo}`).
- `getDistanceToTriggers(triggers, nodeFields, x, z)` + `getClosestStation(stations, triggersField, nodeFields, wayPoint, farmId)`; `getLoadStationAtMarker` (felt `loadTriggers`, node `triggerNode`) og `getUnloadStationAtMarker` (felt `unloadTriggers`, noder `exactFillRootNode`, `triggerNode`, `aiNode`).
- `collectMarkerStations(adEnv, candidateMarkers, farmId, excludedPlaceable)` → `{{marker, station}, …}`; `findLeastFilled(markerStations, fillTypes, farmId)` → `choice|nil` (eksisterende eget-lager-først-rangering).
- `getTargetNeeds(station, ingredients, farmId)`: pr. understøttet ingrediens summeres `storage:getFillLevel(ft)` og `storage:getFreeCapacity(ft)` over `station.targetStorages`; `capacity = level + free`. Ingen storages → `station:getFreeCapacity(ft, farmId)` som `capacity`, `level = 0`.
- `ADSmartPickup.findSupplyPickup(adEnv, driver, ingredients)`, `applySupplyChoice(driver, ingredients, choice)` (`setFillType`, `selectedFillTypes = {ft}`, `raiseDirtyFlag`, gem tilstand), `ADSmartPickup.choosePickup` (supply → ellers mappe-logik → ellers nil).
- Hook bruger `choice.group` til AD's rotations-bogføring og logger vare + behov ved supply-valg.
- `restoreStoppedVehicles()` kaldt fra `update` (pcall): når `not stateModule:isActive()` og valget stadig er `{narrowedTo}` → `selectedFillTypes = kopi af ingredients` + dirty-flag; tilstand slettes.
- [ ] **Step 4: Kør alle tests** — `luajit tests/test_adSupplyPlanner.lua && luajit tests/test_adSmartPickup.lua` → begge `ALLE TESTS OK`.
- [ ] **Step 5: Commit** — `git add -A && git commit -m "feat: behovsstyret forsyning — én traktor holder alle input fyldt"`
---
### Task 3: Pakning, docs, deploy
**Files:**
- Modify: `FS25_ADSmartPickup/modDesc.xml` (version `1.1.0.0`, `<sourceFile filename="adSupplyPlanner.lua"/>` FØR `adSmartPickup.lua`, beskrivelse), `README.md`
- [ ] **Step 1:** Opdatér modDesc + README (betjening: multi-vælg varer; begrænsninger fra spec).
- [ ] **Step 2:** Byg: `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip . && cd ..`; verificér med `unzip -l` at begge .lua + modDesc + icon er med.
- [ ] **Step 3:** Deploy atomisk (spillet kan køre): `cp FS25_ADSmartPickup.zip ~/FS25-data/mods/.FS25_ADSmartPickup.zip.tmp && mv ~/FS25-data/mods/.FS25_ADSmartPickup.zip.tmp ~/FS25-data/mods/FS25_ADSmartPickup.zip`.
- [ ] **Step 4:** Code-review (ecc:code-reviewer) af diff; ret CRITICAL/HIGH.
- [ ] **Step 5:** Commit + `git push gitea`. Opdatér memory + Obsidian-noten `projekter/FS25 silo-patches + AD Smart Pickup.md`.
File diff suppressed because it is too large Load Diff
@@ -1,78 +0,0 @@
# Flow-tavle — implementeringsplan
> **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:** Én tavle med ét flow pr. række (Vare · Fra · Til · Regel · Traktorer · Status); Fra/Til vælges som bygninger, markører findes automatisk. Faner: Flows · Lagre.
**Architecture:** Ny spil-adapter `adBuildings.lua` (bygninger + markør-opslag + cache). Modellen får bygnings-id-lister; controlleren udleder waypoint-lister fra dem (`resolveMarkers`) så planlægger/hooks er uændrede. GUI omskrives: `SmartPickupFrame.lua` (faner, tavle, fælles), `SmartPickupFlowTab.lua` (detail-blok), `SmartPickupStorageTab.lua` (uændret).
**Tech Stack:** FS25 Lua, AutoDrive-globaler via `FS25_AutoDrive`, luajit-tests.
**Spec:** `docs/superpowers/specs/2026-09-23-flow-tavle-design.md`
## Global Constraints
- Motoren (adOutbound, adOutboundPlanner, adSupplyPlanner, adUnloadWait, adSmartPickup-hooks) røres ikke.
- Byg kun når alle tests er grønne (`for t in tests/test_*.lua; do luajit $t|tail -1; done`), derefter zip → `~/FS25-data/mods/` → commit → push gitea main.
- Version 1.13.0.0. XML-version 3; v1/v2 læses.
- Wait-markører er aldrig bygningsmarkører. Siloers læssemarkør = strengt opslag; alt andet = nærmeste ≤ 40 m.
## Review Focus
1. Run med bygning der er solgt siden sidst: rækken viser "(mangler)", Start afviser med noMarker, intet crash. (Task 3 test)
2. v2-fil med markør der ikke kan oversættes til en bygning: markøren beholdes rå og virker som før. (Task 2 test)
3. To bygninger med markører inden for 40 m af hinanden (silo-rækker): hver silo får sin egen markør (strengt opslag). (Task 1 test)
4. Forsynings-run: Fra vises som "alle siloer" og sources-listen røres ikke. (Task 3 test)
5. Fanen rebuild'es mens et run kører: Start/Stop-knappen og status følger `isRunning`. (in-game)
---
### Task 1: adBuildings
**Files:** Create `FS25_ADSmartPickup/adBuildings.lua`; Test `tests/test_adBuildings.lua`.
**Produces:**
```lua
ADBuildings.MAX_MARKER_DISTANCE = 40; ADBuildings.CACHE_MS = 1000
ADBuildings.list(adEnv, farmId) -- {building...} sorteret kind, navn (cachet)
ADBuildings.find(adEnv, farmId, id) -- building | nil
ADBuildings.byMarker(adEnv, farmId, wayPointId, role) -- role "load"|"unload": building hvis markøren er dens markør, ellers nil
ADBuildings.getId(placeable) -- uniqueId eller "pos:x:z"
ADBuildings.invalidate()
building = {id, name, kind, placeable, loadMarker={id,name,distance}|nil, unloadMarker=…|nil, canBeSource, canBeTarget, canSell}
```
Kilder til bygninger: `g_currentMission.placeableSystem.placeables` (ejet af farm eller sell) — kind: factory (spec_productionPoint), silo (spec_silo), husbandry (getHusbandryFillLevel), objectStorage (spec_objectStorage), sell (ADPrices.isSellingStation på placeable.spec_sellingPoint / unloadingStation via storageSystem). Læssetriggere: `productionPoint.loadingStation.loadTriggers`, `spec_silo.loadingStation.loadTriggers`, husbandry: alle loadingStations i storageSystem med owningPlaceable == placeable. Aflæsningstriggere: unloadingStations med owningPlaceable == placeable. Objektlager/pallespawner: rootNode/spawnPlaces[1].startNode.
Markørvalg: nærmeste markør (ikke Wait) til en trigger-node ≤ 40 m; for silo-load: kun markører hvor `ADSmartPickup.getLoadStationAtWayPointId(marker.id)` == siloens station.
- [ ] Tests B1–B7 (kind, markører, silo strengt, Wait udelukket, byMarker, id-fallback, cache-invalidering) → RED → implementér → GREEN → commit `feat: adBuildings`.
### Task 2: Model v3
**Files:** Modify `adRuns.lua`, `adRunsStorage.lua`; Test `tests/test_adRuns.lua`.
**Produces:** run-felter `sourceBuildingIds`, `targetBuildingIds` (lister af strenge); `ADRuns.toggleSourceBuilding/toggleTargetBuilding(store, runId, id)`; XML v3 `#sourceBuildings`/`#targetBuildings` (mellemrumsadskilte). `ADRuns.XML_VERSION = 3`. Migration sker i controlleren (Task 3), ikke i modellen.
- [ ] Tests R-v3 → RED → implementér → GREEN → commit.
### Task 3: Controller — resolve + migration + validering
**Files:** Modify `adRunsController.lua`; Test `tests/test_adRunsController.lua`.
**Produces:**
```lua
ADRunsController.resolveMarkers(adEnv, run) -- udfylder run.sourceWayPointIds (outbound) / targetWayPointIds (supply) fra bygninger; rå markører (uden bygning) bevares; returnerer {missing = {buildingName...}}
ADRunsController.migrateBuildings(adEnv) -- ved load: markører → bygnings-id via byMarker; logger
ADRunsController.getFlowRow(adEnv, run) -- {product, from, to, rule, vehicles, status, warning} tekster til tavlen
validateVehicle: ny grund "noMarker" når en valgt bygning mangler markør
```
Start/plan: `startRun` og `ADOutbound.buildPlanInput` kalder resolveMarkers først (buildPlanInput via ADRunsController.getRunForVehicle → resolve i getRunForVehicle? nej: i `ADSmartPickup.choosePickup`/`ADOutbound.choosePickup` kaldes `ADRunsController.resolveMarkers(adEnv, run)` én gang pr. plan).
- [ ] Tests C-flow → RED → implementér → GREEN → commit.
### Task 4: GUI — Flows-fane
**Files:** Rewrite `gui/SmartPickupFrame.lua` (faner Flows/Lagre, tavle, fælles rækker, knapper), Create `gui/SmartPickupFlowTab.lua` (detail-blok, callbacks), delete `SmartPickupSupplyTab.lua`/`SmartPickupOutboundTab.lua` (indhold flyttes), Modify `gui/SmartPickupFrame.xml` (statiske rækker: runOption "Flow", waitPool, loops, returnBelow, sellAt, storeAbove; prefabs som i dag), l10n da/en (`spu_tab_flows`, `spu_flow_*`, `spu_direction_in/out`, `spu_from_allSilos`, `spu_noMarker`, `spu_reason_noMarker`, `spu_section_*`), `adSmartPickup.lua` (source()).
- [ ] Implementér; syntaks- og definitions-tjek; in-game.
### Task 5: README, version, byg, deploy, push, memory.
@@ -1,569 +0,0 @@
# Udkørsel af produkter — implementeringsplan
> **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:** Runs med retning "Udkørsel": traktorer henter fabrikkers output og lagervarer (bulk og paller, også fra objektlagre) og kører dem til silo eller salgssted efter politik pr. vare, pristærskel og lagertærskel.
**Architecture:** Ren planlægger (`adOutboundPlanner`) + rene prisregler (`adPrices`) uden spil-API; spiladaptere i `adSources`/`adPrices`; AutoDrive-integration i `adOutbound` via to hooks (næste pålæsning + læsseopgaven), samme mønster som `adUnloadWait`. Wait-puljen udskilles i `adWaitPool` og deles. GUI får tre faner (Oversigt · Forsyning · Udkørsel) med fælles byggeklodser.
**Tech Stack:** FS25 Lua (GIANTS-sandbox), AutoDrive 3.x via `FS25_AutoDrive`-globalen, Universal Autoload via AD's AL-lag, luajit-tests uden spil.
**Spec:** `docs/superpowers/specs/2026-09-23-udkoersel-design.md`
## Global Constraints
- Alle spil-API-kald i hooks i `pcall`; fejl → hold ved Wait + WARN én gang, aldrig AutoDrive-stop.
- Modellen (`adRuns`) muterer aldrig: ny store ved hver ændring.
- Log-præfiks `ADSmartPickup: ` i alle moduler; alle run-ændringer logges.
- XML-version 2; version 1-filer læses som `kind="supply"`.
- Standarder: `sellAtPercent` 90 (50–100), `storeAbovePercent` 80 (50–95), `collectRound` false.
- Politikker: `store` · `sell` · `sellElseStore` (+ "off" = varen udelades af `products`).
- Kildetyper: fabrik (produktionspunkt), silo (`spec_silo`), objektlager (`spec_objectStorage`), genkendt inden for 40 m af markøren.
- Tests køres fra repo-roden: `luajit tests/<fil>.lua`; alle eksisterende tests skal blive grønne.
- Version bump til 1.12.0.0 i `modDesc.xml`; byg `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .`; deploy til `~/FS25-data/mods/`.
## Review Focus
1. Traktor med UAL-vogn står ved et objektlager hvis spawn sidder fast (`objectSpawn.isActive` for evigt): forventet: 30 s uden fremdrift → læsningen afsluttes med det der er, ingen palle efterlades ud over den ene der evt. er spawnet. (Task 6 test O5)
2. Bedst betalende salgssted er uopnåeligt, næstbedste kan nås: forventet: næstbedste vælges uden advarsel; kun når intet kan nås, vises rød advarsel én gang. (Task 4 test P6 + Task 6 test O7)
3. Vare hvis salgsstation ikke findes på kortet, politik `sell`: forventet: varen står som "intet salgssted" i UI og planlæggeren springer den over uden fejl. (Task 4 test P7)
4. Version 1-runs-fil (uden `kind`) indlæses: forventet: alle runs bliver Forsyning, intet går tabt, næste gem skriver version 2. (Task 1 test R-v2)
5. Bulk-vogn med opsamlingsrunde og to kilder hvor den første giver fuldt læs: forventet: der køres direkte til målet, kilde 2 besøges ikke. (Task 6 test O6)
---
### Task 1: Model + lagring v2
**Files:**
- Modify: `FS25_ADSmartPickup/adRuns.lua`
- Modify: `FS25_ADSmartPickup/adRunsStorage.lua`
- Test: `tests/test_adRuns.lua` (tilføj sektion R-v2)
**Interfaces:**
- Produces: `ADRuns.KINDS = {supply=true, outbound=true}`, `ADRuns.POLICIES = {store=true, sell=true, sellElseStore=true}`, `ADRuns.DEFAULT_SELL_AT_PERCENT=90`, `ADRuns.DEFAULT_STORE_ABOVE_PERCENT=80`, `ADRuns.createRun(store, name, kind)`, run-felter `kind, sourceWayPointIds, products={{fillType="SUGAR", policy="sell"}}, sellAtPercent, storeAbovePercent, collectRound`, `ADRuns.toggleSource(store, runId, wayPointId)`, `ADRuns.setProductPolicy(store, runId, fillTypeName, policy)` (policy "off" fjerner), `ADRuns.getProductPolicy(run, fillTypeName)` → policy eller "off", `ADRuns.serializeProducts/parseProducts`, `ADRuns.XML_VERSION = 2`.
- [ ] **Step 1: Skriv fejlende tests** — tilføj nederst i `tests/test_adRuns.lua` før den afsluttende opsummering:
```lua
-- R-v2: udkørsels-run — kind, kilder, varer med politik, tærskler, runde; XML v2 + v1-migration
local o1, out = ADRuns.createRun(ADRuns.newStore(), "Sugar out", "outbound")
check("Rv2 kind", out.kind, "outbound")
check("Rv2 supply som standard", ADRuns.createRun(ADRuns.newStore(), "x").kind == nil and "nil" or select(2, ADRuns.createRun(ADRuns.newStore(), "x")).kind, "supply")
check("Rv2 sellAt 90", out.sellAtPercent, 90)
check("Rv2 storeAbove 80", out.storeAbovePercent, 80)
check("Rv2 runde fra", out.collectRound, false)
local o2 = ADRuns.toggleSource(o1, out.id, 12694)
o2 = ADRuns.toggleSource(o2, out.id, 12386)
check("Rv2 to kilder", table.concat(ADRuns.getRun(o2, out.id).sourceWayPointIds, ","), "12694,12386")
check("Rv2 toggle fjerner", #ADRuns.getRun(ADRuns.toggleSource(o2, out.id, 12694), out.id).sourceWayPointIds, 1)
local o3 = ADRuns.setProductPolicy(o2, out.id, "SUGAR", "sell")
o3 = ADRuns.setProductPolicy(o3, out.id, "FORAGE", "store")
check("Rv2 politik SUGAR", ADRuns.getProductPolicy(ADRuns.getRun(o3, out.id), "SUGAR"), "sell")
check("Rv2 politik ukendt = off", ADRuns.getProductPolicy(ADRuns.getRun(o3, out.id), "STRAW"), "off")
check("Rv2 ugyldig politik ignoreres", ADRuns.getProductPolicy(ADRuns.getRun(ADRuns.setProductPolicy(o3, out.id, "SUGAR", "bogus"), out.id), "SUGAR"), "sell")
check("Rv2 off fjerner", ADRuns.getProductPolicy(ADRuns.getRun(ADRuns.setProductPolicy(o3, out.id, "SUGAR", "off"), out.id), "SUGAR"), "off")
check("Rv2 gammel store urørt", #ADRuns.getRun(o2, out.id).products, 0)
local o4 = ADRuns.updateRun(o3, out.id, {sellAtPercent = 120, storeAbovePercent = 10, collectRound = true})
check("Rv2 sellAt klippet 100", ADRuns.getRun(o4, out.id).sellAtPercent, 100)
check("Rv2 storeAbove klippet 50", ADRuns.getRun(o4, out.id).storeAbovePercent, 50)
check("Rv2 runde til", ADRuns.getRun(o4, out.id).collectRound, true)
check("Rv2 serialiser", ADRuns.serializeProducts(ADRuns.getRun(o3, out.id).products), "SUGAR:sell FORAGE:store")
local parsed = ADRuns.parseProducts("SUGAR:sell FORAGE:store BAD BAD:bogus")
check("Rv2 parse antal", #parsed, 2)
check("Rv2 parse politik", parsed[2].policy, "store")
local rt = ADRuns.fromRecords(ADRuns.toRecords(o4))
local rtRun = ADRuns.getRun(rt, out.id)
check("Rv2 roundtrip kind", rtRun.kind, "outbound")
check("Rv2 roundtrip kilder", table.concat(rtRun.sourceWayPointIds, ","), "12694,12386")
check("Rv2 roundtrip products", ADRuns.serializeProducts(rtRun.products), "SUGAR:sell FORAGE:store")
check("Rv2 roundtrip sellAt", rtRun.sellAtPercent, 100)
check("Rv2 roundtrip runde", rtRun.collectRound, true)
-- v1-post uden kind/sources/products -> supply med standarder
local v1 = ADRuns.fromRecords({{id = 7, name = "Old", target = 94, targets = "94", ingredientMode = "auto", ingredients = "", waitPool = "", loops = 0, returnBelow = 50, vehicleIds = {}}})
check("Rv2 v1 -> supply", ADRuns.getRun(v1, 7).kind, "supply")
check("Rv2 v1 tomme kilder", #ADRuns.getRun(v1, 7).sourceWayPointIds, 0)
check("Rv2 v1 sellAt standard", ADRuns.getRun(v1, 7).sellAtPercent, 90)
```
- [ ] **Step 2: Kør testen** — `luajit tests/test_adRuns.lua` → FAIL (`toggleSource` nil).
- [ ] **Step 3: Implementér i `adRuns.lua`**:
```lua
ADRuns.XML_VERSION = 2
ADRuns.KINDS = {supply = true, outbound = true}
ADRuns.POLICIES = {store = true, sell = true, sellElseStore = true}
ADRuns.DEFAULT_SELL_AT_PERCENT, ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT = 90, 50, 100
ADRuns.DEFAULT_STORE_ABOVE_PERCENT, ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT = 80, 50, 95
local function copyProducts(products)
local copy = {}
for index, product in ipairs(products or {}) do
copy[index] = {fillType = product.fillType, policy = product.policy}
end
return copy
end
local function clampRange(value, default, min, max)
local number = tonumber(value) or default
return math.max(min, math.min(max, math.floor(number)))
end
```
`copyRun` får `kind, sourceWayPointIds = copyList(...), products = copyProducts(...), sellAtPercent, storeAbovePercent, collectRound`. `createRun(store, name, kind)` sætter `kind = ADRuns.KINDS[kind] and kind or "supply"`, `sourceWayPointIds = {}`, `products = {}`, `sellAtPercent = 90`, `storeAbovePercent = 80`, `collectRound = false`. `updateRun` håndterer `changes.kind` (kun gyldige), `sourceWayPointIds` (copyList), `products` (copyProducts), `sellAtPercent`/`storeAbovePercent` (clampRange), `collectRound` (`== true`).
```lua
function ADRuns.toggleSource(store, runId, wayPointId)
local run = ADRuns.getRun(store, runId)
if run == nil then return copyStore(store) end
local sources, found = {}, false
for _, id in ipairs(run.sourceWayPointIds) do
if id == wayPointId then found = true else table.insert(sources, id) end
end
if not found then table.insert(sources, wayPointId) end
return ADRuns.updateRun(store, runId, {sourceWayPointIds = sources})
end
function ADRuns.getProductPolicy(run, fillTypeName)
for _, product in ipairs(run.products or {}) do
if product.fillType == fillTypeName then return product.policy end
end
return "off"
end
-- policy "off" fjerner varen; ukendt politik ignoreres (store uændret).
function ADRuns.setProductPolicy(store, runId, fillTypeName, policy)
local run = ADRuns.getRun(store, runId)
if run == nil or (policy ~= "off" and not ADRuns.POLICIES[policy]) then return copyStore(store) end
local products = {}
for _, product in ipairs(run.products) do
if product.fillType ~= fillTypeName then table.insert(products, product) end
end
if policy ~= "off" then table.insert(products, {fillType = fillTypeName, policy = policy}) end
return ADRuns.updateRun(store, runId, {products = products})
end
function ADRuns.serializeProducts(products)
local parts = {}
for _, product in ipairs(products or {}) do table.insert(parts, product.fillType .. ":" .. product.policy) end
return table.concat(parts, " ")
end
function ADRuns.parseProducts(text)
local products = {}
for token in string.gmatch(tostring(text or ""), "%S+") do
local name, policy = string.match(token, "^([^:]+):(%a+)$")
if name ~= nil and ADRuns.POLICIES[policy] then table.insert(products, {fillType = name, policy = policy}) end
end
return products
end
```
`toRecords` tilføjer `kind, sources = table.concat(run.sourceWayPointIds, " "), products = serializeProducts, sellAt, storeAbove, collectRound = run.collectRound and 1 or 0`. `fromRecords` læser dem med standarder (`kind` ugyldig/nil → "supply"; `collectRound = tonumber(record.collectRound) == 1`).
`adRunsStorage.lua`: `writeRecords` skriver `#kind` (string), `#sources`, `#products`, `#sellAt` (int), `#storeAbove` (int), `#collectRound` (int); `readRecords` læser dem (`getString`/`getInt`; manglende attribut → nil → standard i fromRecords).
- [ ] **Step 4: Kør** `luajit tests/test_adRuns.lua && luajit tests/test_adRunsController.lua` → alle OK.
- [ ] **Step 5: Commit** `feat: run-model v2 — retning, kilder, varepolitik, tærskler, runde (XML v2 + v1-migration)`
---
### Task 2: adWaitPool udskilt fra adUnloadWait
**Files:**
- Create: `FS25_ADSmartPickup/adWaitPool.lua`
- Modify: `FS25_ADSmartPickup/adUnloadWait.lua`, `adLoadSwap.lua` (excludedPlaceables), `modDesc.xml` (sourceFile før adUnloadWait)
- Test: `tests/test_adWaitPool.lua` (ny), eksisterende `tests/test_adSmartPickup.lua` skal stadig køre
**Interfaces (Produces):**
```lua
ADWaitPool.getCandidates(adEnv, poolGroup) -- {{marker, x, z}}
ADWaitPool.claim(markerId, vehicle) / ADWaitPool.releaseVehicle(vehicle) / ADWaitPool.getOwner(markerId)
ADWaitPool.pickFree(adEnv, vehicle, poolGroup, isReachable) -- candidate eller nil (nærmeste ledige fra vognens position)
ADWaitPool.hasRouteTo(adEnv, vehicle, wayPointId) / ADWaitPool.hasRouteBothWays(adEnv, vehicle, toId, homeId)
ADWaitPool.redirect(task, destinationID) -- stopper AD's trigger-aktivitet, sætter task.destinationID/state/setPathTo
ADWaitPool.getClaims() -- {{marker, vehicle}} sorteret (til GUI)
ADWaitPool.STATE_DRIVING -- sættes af adUnloadWait.install (task.STATE_DRIVING or 2)
ADLoadSwap.findDumpMarker(adEnv, vehicle, fillType, rest, homeWayPointId, isReachable, excludedPlaceables) -- 7. param: {[placeable]=true} udelukkes
```
- [ ] **Step 1: Test** `tests/test_adWaitPool.lua`: mock `FS25_AutoDrive.ADGraphManager` (markers i mapperne "Cow Wait" og "silo", waypoints, `getPathTo` der returnerer `{ {id=target} }` for nåelige), `ADSupplyPlanner` via `dofile`. Tjek: (W1) `getCandidates(adEnv, "Cow Wait")` giver kun Wait-markører i den mappe, tom pulje = alle Wait-mapper; (W2) `pickFree` returnerer nærmeste ledige, springer reserverede over, returnerer eget reserverede; (W3) `claim/releaseVehicle/getOwner`; (W4) `pickFree` med `isReachable` der afviser nærmeste → næste; (W5) `getClaims` sorteret på mappe.
- [ ] **Step 2: Kør** → FAIL (fil mangler).
- [ ] **Step 3: Implementér** `adWaitPool.lua` ved at flytte `getWaitCandidates`, `claims`, `pathReaches`, `hasRouteTo`, `hasRouteBothWays`, `redirect`, `getClaims`-sorteringen fra `adUnloadWait.lua` (samme kode, offentlige navne som ovenfor; `claims` er `setmetatable({}, {__mode="v"})`; `getClaims` itererer `claims` direkte: `{marker = ADGraphManager:getMapMarkerByWayPointId(id), vehicle}`). `adUnloadWait.lua` kalder dem (`claims[...]` → `ADWaitPool.claim/getOwner/releaseVehicle`; `pickNearestFreeWaitPoint(getWaitCandidates(...), claims, ...)` → `ADSupplyPlanner.pickNearestFreeWaitPoint(ADWaitPool.getCandidates(adEnv, pool), ADWaitPool.claims, x, z, vehicle, isReachable)`); `ADUnloadWait.getClaims/hasRouteTo/hasRouteBothWays` bliver delegater. `ADLoadSwap.findDumpMarker`: `if excludedPlaceables ~= nil and excludedPlaceables[station.owningPlaceable] then skip`.
- [ ] **Step 4: Kør** alle tests: `for t in tests/test_*.lua; do luajit $t | tail -1; done` → grønne.
- [ ] **Step 5: Commit** `refactor: Wait-puljen udskilt i adWaitPool (reservationer, rutetjek, redirect) til deling med udkørsel`
---
### Task 3: adPrices — toppris og pris-%
**Files:**
- Create: `FS25_ADSmartPickup/adPrices.lua`
- Test: `tests/test_adPrices.lua`
**Interfaces (Produces):**
```lua
ADPrices.maxFactor(factors) -- højeste værdi i {[period]=factor}; 1 uden faktorer
ADPrices.topPrice(baseNow, factorNow, maxFactor) -- baseNow * maxFactor / factorNow; baseNow når factorNow <= 0
ADPrices.percentOfTop(price, top) -- afrundet heltal; 0 når top <= 0
ADPrices.isSellingStation(station) -- SellingStation, isSellingPoint, ikke hideFromPricesMenu
ADPrices.getQuote(station, fillType) -- {price, top, percent} eller nil (spil-API i pcall)
ADPrices.bestQuote(quotes) -- højeste percent, dernæst price; nil ved tom
```
- [ ] **Step 1: Test** (P1) `maxFactor({EARLY_SPRING=0.9, LATE_AUTUMN=1.2})` = 1.2; (P2) `topPrice(500, 1.0, 1.25)` = 625; `topPrice(500, 0, 1.25)` = 500; (P3) `percentOfTop(470, 625)` = 75; `percentOfTop(700, 625)` = 112 (stor efterspørgsel > 100); `percentOfTop(1, 0)` = 0; (P4) `getQuote` med fake station `{fillTypePrices={[5]=500}, getEffectiveFillTypePrice=function(_,ft) return 470 end, isa=function() return true end, isSellingPoint=true}` og `g_fillTypeManager.getFillTypeByIndex` → `{economy={factors={A=1.0,B=1.25}}, pricePerLiter=400}`, `g_currentMission.environment.currentPeriod="A"` → percent 75; (P5) uden `fillTypePrices` bruges `pricePerLiter × factorNow × economyManager:getPriceMultiplier()` (1.0) → top 500, percent 94 (470/500); (P6) `bestQuote` vælger højeste percent.
- [ ] **Step 2: Kør** → FAIL.
- [ ] **Step 3: Implementér**:
```lua
ADPrices = {}
ADPrices.LOG_PREFIX = "ADSmartPickup: "
function ADPrices.maxFactor(factors)
local best = nil
for _, factor in pairs(factors or {}) do
if type(factor) == "number" and (best == nil or factor > best) then best = factor end
end
return best or 1
end
function ADPrices.topPrice(baseNow, factorNow, maxFactor)
if baseNow == nil or baseNow <= 0 then return 0 end
if factorNow == nil or factorNow <= 0 or maxFactor == nil or maxFactor <= 0 then return baseNow end
return baseNow * maxFactor / factorNow
end
function ADPrices.percentOfTop(price, top)
if top == nil or top <= 0 or price == nil then return 0 end
return math.floor(100 * price / top + 0.5)
end
function ADPrices.isSellingStation(station)
return station ~= nil and station.isa ~= nil and SellingStation ~= nil and station:isa(SellingStation)
and station.isSellingPoint == true and not station.hideFromPricesMenu
end
local function getCurrentPeriod()
local environment = g_currentMission ~= nil and g_currentMission.environment or nil
return environment ~= nil and environment.currentPeriod or nil
end
function ADPrices.getQuote(station, fillType)
local ok, effective = pcall(station.getEffectiveFillTypePrice, station, fillType)
if not ok or type(effective) ~= "number" then return nil end
local fillTypeDesc = g_fillTypeManager ~= nil and g_fillTypeManager.getFillTypeByIndex ~= nil and g_fillTypeManager:getFillTypeByIndex(fillType) or nil
local factors = fillTypeDesc ~= nil and fillTypeDesc.economy ~= nil and fillTypeDesc.economy.factors or nil
local period = getCurrentPeriod()
local factorNow = factors ~= nil and period ~= nil and tonumber(factors[period]) or 1
local baseNow = station.fillTypePrices ~= nil and tonumber(station.fillTypePrices[fillType]) or nil
if baseNow == nil or baseNow <= 0 then
local multiplier = 1
local economy = g_currentMission ~= nil and g_currentMission.economyManager or nil
if economy ~= nil and economy.getPriceMultiplier ~= nil then
local okMult, value = pcall(economy.getPriceMultiplier, economy)
if okMult and type(value) == "number" then multiplier = value end
end
baseNow = (fillTypeDesc ~= nil and tonumber(fillTypeDesc.pricePerLiter) or 0) * factorNow * multiplier
end
local top = ADPrices.topPrice(baseNow, factorNow, ADPrices.maxFactor(factors))
return {price = effective, top = top, percent = ADPrices.percentOfTop(effective, top)}
end
function ADPrices.bestQuote(quotes)
local best = nil
for _, quote in ipairs(quotes or {}) do
if best == nil or quote.percent > best.percent or (quote.percent == best.percent and quote.price > best.price) then best = quote end
end
return best
end
```
- [ ] **Step 4: Kør** → OK. **Step 5: Commit** `feat: adPrices — toppris fra årets prisforløb og pris-% pr. salgssted`
---
### Task 4: adOutboundPlanner (ren)
**Files:**
- Create: `FS25_ADSmartPickup/adOutboundPlanner.lua`
- Test: `tests/test_adOutboundPlanner.lua`
**Interfaces (Produces):**
```lua
-- input:
-- { capacity = liter, hasAutoload = bool, sellAtPercent, storeAbovePercent, collectRound,
-- products = { { fillType = idx, name = "SUGAR", policy = "sell"|"store"|"sellElseStore", isPallet = bool,
-- sources = { {wayPointId, available, level, capacity} ... }, -- kun kilder med available > 0
-- quote = {percent, price, wayPointId, stationName} | nil, -- bedste NÅELIGE salgssted
-- store = {wayPointId, name, free} | nil } ... } }
-- output: trip | nil, reasons
-- trip = { kind = "sell"|"store", fillTypes = {idx...}, names = {...}, sourceIds = {wayPointId...}, targetWayPointId, targetName, percent|nil }
-- reasons = { "SUGAR 78 % af top (kræver 90), lager 41 %", ... }
ADOutboundPlanner.plan(input)
ADOutboundPlanner.evaluate(product, input) -- {available, fullLoad, storageHigh, storagePercent, priceOk, percent}
ADOutboundPlanner.MIN_AVAILABLE_LITERS = 1
ADOutboundPlanner.ROUND_MIN_FREE_FRACTION = 0.1
```
- [ ] **Step 1: Tests** (`local P = ADOutboundPlanner` efter `dofile`):
- P1 salg ved pris over tærskel, uanset mængde: SUGAR sell, sources {{1, 5000, ...}}, quote 92, capacity 20000 → trip.kind "sell", targetWayPointId = quote.wayPointId, sourceIds {1}.
- P2 pris under tærskel, lager lavt → nil, reasons[1] = "SUGAR 78 % af top (kræver 90), lager 41 %".
- P3 sellElseStore, pris lav, lager 85 % → "store" til store.wayPointId.
- P4 store med fuldt læs (available ≥ capacity) og lager 20 % → store.
- P5 rangering: to sell-kandidater 95 % og 91 % → første vælges (højeste %).
- P6 quote nil (intet nåeligt salgssted), policy sell, lager højt → nil, reason "SUGAR: intet nåeligt salgssted".
- P7 store nil ved store-behov → nil, reason "FORAGE: ingen silo med plads".
- P8 runde bulk (collectRound, hasAutoload=false): SUGAR i kilde A (3000) og B (9000), capacity 10000 → sourceIds {B, A} (mest først), ét fillType.
- P9 runde UAL: STRAW_BASKET (sell 95, station S) + STRAW_CRATE (sell 93, station S) + STRAW_BROOM (sell 96, station T) → fillTypes {BROOM} kun (højest, T) — og med BROOM fjernet: {BASKET, CRATE} samme station S.
- P10 uden runde: kun første fillType og kun én kilde (den med mest).
- P11 policy off/ingen sources → nil, reasons tom for varer uden kilder ("SUGAR: intet på lager").
- [ ] **Step 2: Kør** → FAIL. **Step 3: Implementér**:
```lua
ADOutboundPlanner = {}
ADOutboundPlanner.MIN_AVAILABLE_LITERS = 1
ADOutboundPlanner.ROUND_MIN_FREE_FRACTION = 0.1
local function sumAvailable(product)
local total, high, percent = 0, false, 0
for _, source in ipairs(product.sources or {}) do
total = total + (source.available or 0)
if source.capacity ~= nil and source.capacity > 0 then
local p = 100 * (source.level or 0) / source.capacity
percent = math.max(percent, p)
end
end
return total, percent
end
function ADOutboundPlanner.evaluate(product, input)
local available, storagePercent = sumAvailable(product)
local percent = product.quote ~= nil and product.quote.percent or nil
return {
available = available,
fullLoad = input.capacity ~= nil and input.capacity > 0 and available >= input.capacity,
storagePercent = storagePercent,
storageHigh = storagePercent >= (input.storeAbovePercent or 80),
percent = percent,
priceOk = percent ~= nil and percent >= (input.sellAtPercent or 90),
}
end
local function wantsSell(policy) return policy == "sell" or policy == "sellElseStore" end
local function wantsStore(policy) return policy == "store" or policy == "sellElseStore" end
local function reasonFor(product, e, input)
if e.available < ADOutboundPlanner.MIN_AVAILABLE_LITERS then return product.name .. ": intet på lager" end
if wantsSell(product.policy) and product.quote == nil and not (wantsStore(product.policy) and product.store ~= nil) then
return product.name .. ": intet nåeligt salgssted"
end
if wantsStore(product.policy) and product.store == nil and (e.fullLoad or e.storageHigh) and not e.priceOk then
return product.name .. ": ingen silo med plads"
end
local parts = {}
if wantsSell(product.policy) and e.percent ~= nil then
table.insert(parts, string.format("%d %% af top (kræver %d)", e.percent, input.sellAtPercent or 90))
end
table.insert(parts, string.format("lager %d %%", math.floor(e.storagePercent + 0.5)))
return product.name .. " " .. table.concat(parts, ", ")
end
-- kandidater: {product, e, kind, targetWayPointId, targetName, score}
local function collectCandidates(input)
local sell, store, reasons = {}, {}, {}
for _, product in ipairs(input.products or {}) do
local e = ADOutboundPlanner.evaluate(product, input)
local added = false
if e.available >= ADOutboundPlanner.MIN_AVAILABLE_LITERS then
if wantsSell(product.policy) and e.priceOk and product.quote ~= nil then
table.insert(sell, {product = product, e = e, kind = "sell", targetWayPointId = product.quote.wayPointId, targetName = product.quote.stationName, percent = e.percent})
added = true
elseif wantsStore(product.policy) and (e.fullLoad or e.storageHigh) and product.store ~= nil then
table.insert(store, {product = product, e = e, kind = "store", targetWayPointId = product.store.wayPointId, targetName = product.store.name})
added = true
end
end
if not added then table.insert(reasons, reasonFor(product, e, input)) end
end
table.sort(sell, function(a, b)
if a.e.percent ~= b.e.percent then return a.e.percent > b.e.percent end
if a.e.available ~= b.e.available then return a.e.available > b.e.available end
return a.product.name < b.product.name
end)
table.sort(store, function(a, b)
if a.e.storagePercent ~= b.e.storagePercent then return a.e.storagePercent > b.e.storagePercent end
if a.e.available ~= b.e.available then return a.e.available > b.e.available end
return a.product.name < b.product.name
end)
return sell, store, reasons
end
local function sortedSources(product)
local sources = {}
for _, source in ipairs(product.sources or {}) do
if (source.available or 0) >= ADOutboundPlanner.MIN_AVAILABLE_LITERS then table.insert(sources, source) end
end
table.sort(sources, function(a, b) return a.available > b.available end)
return sources
end
local function buildTrip(primary, candidates, input)
local trip = {kind = primary.kind, fillTypes = {primary.product.fillType}, names = {primary.product.name}, sourceIds = {}, targetWayPointId = primary.targetWayPointId, targetName = primary.targetName, percent = primary.percent}
local members = {primary}
if input.collectRound and input.hasAutoload and primary.product.isPallet then
for _, candidate in ipairs(candidates) do
if candidate ~= primary and candidate.kind == primary.kind and candidate.targetWayPointId == primary.targetWayPointId and candidate.product.isPallet then
table.insert(members, candidate)
table.insert(trip.fillTypes, candidate.product.fillType)
table.insert(trip.names, candidate.product.name)
end
end
end
local seen = {}
for _, member in ipairs(members) do
local sources = sortedSources(member.product)
local limit = input.collectRound and #sources or math.min(1, #sources)
for index = 1, limit do
local id = sources[index].wayPointId
if not seen[id] then seen[id] = true; table.insert(trip.sourceIds, id) end
end
end
return trip
end
function ADOutboundPlanner.plan(input)
local sell, store, reasons = collectCandidates(input)
if sell[1] ~= nil then return buildTrip(sell[1], sell, input), reasons end
if store[1] ~= nil then return buildTrip(store[1], store, input), reasons end
return nil, reasons
end
```
- [ ] **Step 4: Kør** → OK. **Step 5: Commit** `feat: adOutboundPlanner — turvurdering, prisregel, politikker, opsamlingsrunde (ren)`
---
### Task 5: adSources — kilde-genkendelse og lagerstatus
**Files:**
- Create: `FS25_ADSmartPickup/adSources.lua`
- Test: `tests/test_adSources.lua`
**Interfaces (Produces):**
```lua
ADSources.MAX_DISTANCE = 40; ADSources.PALLET_RADIUS = 25
ADSources.identify(adEnv, wayPointId, farmId) -- {kind="factory"|"silo"|"objectStorage", placeable, station|nil, wayPointId, name} | nil
ADSources.getProducts(adEnv, source, farmId) -- { {fillType, name, available, level, capacity, isPallet} } (kun varer med kapacitet/indhold)
ADSources.isPalletFillType(fillType) -- fillTypeDesc.palletFilename ~= nil
ADSources.getSellMarkers(adEnv, farmId) -- { {marker, station} } salgssteder (ADPrices.isSellingStation)
ADSources.getObjectInfoIndex(source, fillType) -- objectInfo-indeks + antal for varen i et objektlager, eller nil
ADSources.spawnOne(source, objectInfoIndex) -- pcall placeable:removeAbstractObjectsFromStorage(index, 1, nil) -> ok
ADSources.countStored(source, fillType) -- antal objekter af varen i lageret
ADSources.getSourcePlaceables(sources) -- {[placeable]=true} til udelukkelse
```
Genkendelse: læssestation ved markøren (`ADSmartPickup.getLoadStationAtWayPointId`) med `owningPlaceable.spec_productionPoint` → factory; med `spec_silo` → silo; ellers nærmeste placeable med `spec_objectStorage` inden for 40 m af waypointet (`g_currentMission.placeableSystem.placeables`, `getWorldTranslation(placeable.rootNode)`, `canFarmAccess`) → objectStorage.
Fabrik-produkter: `for production in productionPoint.productions: for output in production.outputs: type` (unik); `level = productionPoint.storage:getFillLevel(ft)`, `capacity = storage:getCapacity(ft)` (fallback `storage.capacities[ft]`); paller i nærheden: `g_currentMission.vehicleSystem.vehicles` med `spec_pallet` eller `isPallet`, ejer = farm, fillUnit-varen = ft, afstand ≤ 25 m fra `productionPoint.palletSpawner.spawnPlaces[1].startNode` (fallback placeable.rootNode); `available = level + Σ palle-fyldning`.
Silo: `spec_silo.storages[*]` fillLevels > 0 → `level = getOwnStorageLevel`, `capacity = storage:getCapacity(ft)`.
Objektlager: `spec_objectStorage.objectInfos[i].objects[1]` med `palletAttributes.fillType/fillLevel` eller `baleAttributes.fillType/fillLevel`; `numObjects` → available = numObjects × fillLevel; level = `spec.numStoredObjects`, capacity = `spec.capacity`; isPallet = true.
- [ ] **Step 1: Tests** med fakes: (S1) markør ved fabrik → factory med SUGAR output level 150000, capacity 180000, ingen paller → available 150000; (S2) markør ved silo → silo med FORAGE 74500; (S3) markør uden station, objektlager 20 m væk → objectStorage, produkt STRAW_BASKET available 8×1000, isPallet true, `getObjectInfoIndex` = 1 og 8; (S4) paller ved spawner: 2 paller à 1000 STRAW_BASKET inden for 25 m + 1 uden for → available = storage + 2000; (S5) `getSellMarkers` giver kun markører hvis unload-station er SellingStation med isSellingPoint; (S6) `spawnOne` kalder `removeAbstractObjectsFromStorage(1, 1, nil)` (fake tæller); (S7) markør 60 m fra objektlager → nil.
- [ ] **Step 2–4:** implementér som beskrevet, kør, grønne. **Step 5: Commit** `feat: adSources — fabrik, silo og objektlager som kilder; paller ved spawner; salgssteder`
---
### Task 6: adOutbound — hooks, hold, objektlager-udtag, runde, advarsler
**Files:**
- Create: `FS25_ADSmartPickup/adOutbound.lua`
- Test: `tests/test_adOutbound.lua`
**Interfaces (Produces):**
```lua
ADOutbound.CHECK_INTERVAL_MS = 5000; ADOutbound.NO_PROGRESS_MS = 15000; ADOutbound.SPAWN_STALL_MS = 30000
ADOutbound.isOutboundVehicle(vehicle) -- run for vognen har kind "outbound"
ADOutbound.choosePickup(adEnv, vehicle) -- kaldes fra getNextPickup-wrap; returnerer choice {markerIndex, markerName, group, station|nil, fillTypes, fillType} eller nil
ADOutbound.buildPlanInput(adEnv, run, vehicle) -- samler kilder/priser/lager til planlæggeren (spil-API)
ADOutbound.beforeLoadUpdate(adEnv, task, dt) -- true = håndteret (spring original update over)
ADOutbound.beforeLoadFinished(adEnv, task) -- true = fortsæt runde (spring original finished over)
ADOutbound.releaseInactive()
ADOutbound.getHoldFor(vehicle) -- {markerName, reasons} | nil
ADOutbound.getRunStatus(run) -- {reasons, warning} (seneste)
ADOutbound.install(adEnv) -- wrapper LoadAtDestinationTask.update/finished
```
Adfærd (fra spec afsnit 3–4):
- `choosePickup`: `trips[vehicle]=nil`; input = buildPlanInput; `trip, reasons = ADOutboundPlanner.plan(input)`; status[run.id] = reasons. Tur → `stateModule.selectedFillTypes = trip.fillTypes; setFillType(trip.fillTypes[1]); setSecondMarkerByWayPointId(trip.targetWayPointId); raiseDirtyFlag`; `trips[vehicle] = {run, trip, queue = copy(trip.sourceIds), visited = {}, current = trip.sourceIds[1]}`; log `'MT635' udkørsel: 8×STRAW_BASKET fra Straw factory → Halm-salg (95 % af top)`; returnér choice for første kilde (station = kildens læssestation hvis bulk, ellers nil). Ingen tur → `ADWaitPool.pickFree(adEnv, vehicle, run.waitPoolGroup, isReachable=hasRouteTo)`; fundet → claim, `holds[vehicle] = {markerId, reasons}`, log én gang pr. ændret begrundelse, returnér choice for Wait-markøren (station nil); intet Wait-punkt → hold ved første kilde (warn én gang) og returnér den.
- `buildPlanInput`: for hver kilde i `run.sourceWayPointIds`: `ADSources.identify` (nil → spring over + warn én gang "kilde markør X ikke genkendt"); products = union over kilder af `ADSources.getProducts`, filtreret til `ADRuns.getProductPolicy ~= "off"` og til det vognen kan bære (`bulk: AutoDrive.getSupportedFillTypesOfAllUnitsAlphabetically`, pallet: hasAutoload = `AutoDrive:hasAL(trailer)` for en af `getAllUnits`); pr. produkt med sell-politik: quotes over `ADSources.getSellMarkers` hvor `station.acceptedFillTypes[ft]` (eller `getIsFillTypeSupported`) og `ADWaitPool.hasRouteBothWays(adEnv, vehicle, marker.id, sourceId)` → `ADPrices.getQuote`; bedste = `ADPrices.bestQuote`; ingen nåelig men mindst én unåelig → warnOnce(vehicle, "sell:"..ft, "'%s' kan ikke nå salgssted '%s' for %s – tjek vejnettet"); pr. produkt med store-politik: `ADLoadSwap.findDumpMarker(adEnv, vehicle, ft, math.min(available, capacity), nil, isReachable, ADSources.getSourcePlaceables(sources))` → store; nil → warnOnce(vehicle, "store:"..ft, "'%s' finder ingen silo med plads til %s (%s)").
- `warnOnce(vehicle, key, text)`: `warned[vehicle][key] ~= text` → Logging.warning + pcall `adEnv.AutoDriveMessageEvent.sendMessageOrNotification(vehicle, adEnv.ADMessagesManager.messageTypes.ERROR, "ADSmartPickup: " .. text, 8000)`; status[run.id].warning = text. Ryddes når nøglens tilstand ændrer sig (ny tekst) eller turen lykkes.
- `beforeLoadUpdate`: hold → hvis `task.destinationID == hold.markerId` og `drivePathModule:isTargetReached()`: `specialDrivingModule:stopVehicle(); specialDrivingModule:update(dt)`; timer 5 s → `choosePickup` igen; tur → `ADWaitPool.releaseVehicle`, `holds[vehicle]=nil`, `ADWaitPool.redirect(task, trip.sourceIds[1])`, `task.activatedUALLoading=false`; returnér true. Ikke fremme → false.
Tur aktiv og `isTargetReached()`: kilde = `state.sources[task.destinationID]`; objektlager → `updateSpawn` (nedenfor); derefter fremdriftsvagt: `level = AutoDrive.getAllFillLevels(task.trailers)`; stiger → `noProgressMs=0`; ellers `+dt`; `> NO_PROGRESS_MS` og `level > 0` og ingen spawn i gang → `tryNextSourceOrFinish(task)`; returnér false (original kører altid, den aktiverer UAL og stopper vognen).
- `updateSpawn(state, task, source, dt)`: første gang: `index = ADSources.getObjectInfoIndex(source, currentFillType)`; hvis `task.activatedUALLoading` og vognen står (`vehicle.lastSpeedReal < 0.0005`): hvis ingen udestående spawn: `ualIsFull()` på en UAL-vogn → done; `countStored == 0` → skift til næste fillType i `trip.fillTypes` (nyt index) eller done; ellers `ADSources.spawnOne` → `pending = {stored = countStored, loaded = countLoaded(trailers), ms = 0}`; udestående: `countStored < pending.stored and countLoaded > pending.loaded` → pending=nil (næste); `pending.ms > SPAWN_STALL_MS` → done + warn "objektlager spawner ikke". `countLoaded` = antal nøgler i `spec_universalAutoload.loadedObjects` summeret over trailers. done → `task:finished()` (via original finished) og returnér.
- `tryNextSourceOrFinish(task)`: næste id i `state.queue` som ikke er besøgt, og fri kapacitet ≥ 10 % (`fillFreeCapacity/capacity`) → `visited[current]=true; current=next; ADWaitPool.redirect(task, next); task.activatedUALLoading=false; activePickups opdateres via ADSmartPickup.setActivePickup(vehicle, station, fillTypes)` (ny lille funktion i adSmartPickup); ellers `task:finished()`.
- `beforeLoadFinished`: kaldes fra wrap af `finished`; hvis der er tur, kilde ikke fuld (ikke `filledToUnload`) og en ubesøgt kilde med plads → redirect som ovenfor og returnér true (spring original over); ellers `trips[vehicle]=nil` og false.
- `releaseInactive`: for inaktive køretøjer: trips/holds ryddes, `ADWaitPool.releaseVehicle`.
- [ ] **Step 1: Tests** (fake AD-miljø som i `test_adRunsController.lua` + fakes for `ADSources`, `ADPrices`, `ADWaitPool`, `ADRunsController.getRunForVehicle`): (O1) tur → selectedFillTypes/2. markør sat, choice = kilde; (O2) ingen tur → Wait-markør claimet, hold registreret, choice = Wait; (O3) hold + 5 s + nu tur → redirect til kilde, hold frigivet; (O4) objektlager: spawn kaldes én ad gangen; stored falder + loaded stiger → næste spawn; `ualIsFull` → finished kaldt; (O5) spawn uden fremdrift 30 s → finished + WARN; (O6) runde bulk: kilde A fylder vognen (`filledToUnload`) → finished kaldt, ingen redirect; kilde A giver 30 % → redirect til B; (O7) intet nåeligt salgssted → `sendMessageOrNotification` kaldt én gang med ERROR, anden gang ikke; (O8) `releaseInactive` rydder hold og claim.
- [ ] **Step 2–4:** implementér, kør, grønne. **Step 5: Commit** `feat: adOutbound — udkørsels-hooks: turvalg, hold ved Wait, objektlager-udtag, opsamlingsrunde, advarsler på skærm`
---
### Task 7: Controller — udkørsels-runs
**Files:**
- Modify: `FS25_ADSmartPickup/adRunsController.lua`
- Test: `tests/test_adRunsController.lua` (sektion C-out)
**Interfaces (Produces):**
```lua
ADRunsController.createRun(name, kind)
ADRunsController.getSourceMarkers(adEnv, farmId) -- markører der er kilder: {marker, kind}
ADRunsController.getRunProducts(adEnv, run) -- { {fillType, name, title, policy, isPallet, available, percent|nil, stationName|nil} } sorteret på titel
ADRunsController.validateVehicle(adEnv, run, vehicle) -- outbound: reasons noSource | noTrailer | noProducts | needsAutoload | cannotCarry | farFromNetwork
ADRunsController.getOutboundStatus(adEnv, run) -- {products = getRunProducts, reasons, warning} (fra ADOutbound.getRunStatus)
```
`applyRunToVehicle`: `if run.kind == "outbound"`: mode P&D, `setFirstMarkerByWayPointId(run.sourceWayPointIds[1])`, `setSecondMarkerByWayPointId(run.sourceWayPointIds[1])`, `selectedFillTypes` = produkt-fillTypes vognen kan bære, `setFillType(first)`, loopCounter 0, useFolders true, rotateTargets ONLYPICKUP; ingen dump-logik. `getVehicleActivity`: `ADOutbound.getHoldFor(vehicle)` → `{kind="waiting", place=markerName, reason=reasons[1]}`. `updateRun` logger som før. `getRunTargets` for outbound returnerer `{}` (mål vælges pr. tur).
- [ ] **Step 1: Tests** (C-out1) createRun kind; (C-out2) validate: ingen kilder → noSource; bulk-vogn og kun pallevarer → needsAutoload; ingen politik → noProducts; ok-tilfælde; (C-out3) startRun sætter 1./2. markør = kilde 1 og selectedFillTypes; (C-out4) getRunProducts slår politik op.
- [ ] **Step 2–4:** implementér, kør alle tests. **Step 5: Commit** `feat: controller — udkørsels-runs: start, validering, produkter, status`
---
### Task 8: Wiring i adSmartPickup, adUnloadWait, modDesc
**Files:**
- Modify: `adSmartPickup.lua` (source() nye gui-filer, `installHook`: outbound-gren i `getNextPickup`-wrap, `ADOutbound.install(adEnv)`, `ADSmartPickup.setActivePickup`, `update`: `ADOutbound.releaseInactive`), `adUnloadWait.lua` (`beforeUpdate`: return hvis `ADOutbound.isOutboundVehicle`), `modDesc.xml` (sourceFiles: adPrices, adOutboundPlanner, adSources, adWaitPool, adOutbound før adUnloadWait/adSmartPickup; version 1.12.0.0; beskrivelse EN udvidet med udkørsel).
`getNextPickup`-wrap, ny gren først:
```lua
if ADOutbound ~= nil and ADOutbound.isOutboundVehicle(driver) then
local ok, choice = pcall(ADOutbound.choosePickup, adEnv, driver)
if not ok then
Logging.warning(ADSmartPickup.LOG_PREFIX .. "fejl i udkørsel, bruger AutoDrives rotation: %s", tostring(choice))
activePickups[driver] = nil
return originalGetNextPickup(self, driver, forcedSkip)
end
if choice == nil then activePickups[driver] = nil; return originalGetNextPickup(self, driver, forcedSkip) end
activePickups[driver] = choice.station ~= nil and {station = choice.station, fillTypes = choice.fillTypes, markerName = choice.markerName, fillType = choice.fillType} or nil
self.pickups[choice.group] = self.pickups[choice.group] or {}
self.pickups[choice.group].lastTarget = choice.markerIndex
self.pickups[choice.group].lastVehicle = driver
return choice.markerIndex
end
```
- [ ] **Step 1:** ret filer. **Step 2:** `luajit tests/test_adSmartPickup.lua` grøn; `for t in tests/test_*.lua; do luajit $t | tail -1; done`. **Step 3: Commit** `feat: udkørsel koblet på AutoDrive-hooks; Wait-logik springer udkørsels-traktorer over`
---
### Task 9: GUI — tre faner
**Files:**
- Modify: `gui/SmartPickupFrame.lua` (faner, fælles), `gui/SmartPickupFrame.xml` (nye statiske rækker + productPrefab + sourcePrefab), `l10n/l10n_da.xml`, `l10n/l10n_en.xml`
- Create: `gui/SmartPickupSupplyTab.lua` (flyt: fillTargetRows, fillNeedsRows, refreshNeedsRows, fillIngredientRows, describeTargetNeeds, onClickTarget/IngredientMode/Ingredient/Loops/ReturnBelow, `SmartPickupFrame:rebuildSupplyTab(adEnv, run)`), `gui/SmartPickupOutboundTab.lua` (`rebuildOutboundTab(adEnv, run)`: Kilder-rækker (ingredientPrefab med kind="source"), Varer-rækker (productPrefab: MultiTextOption med 4 tilstande Fra/Lager/Salg/Salg, ellers lager + titel "Sukker — 150.0k · 78 % af top (Grain Elevator)"), Regler (sellAtOption 50..100 trin 5, storeAboveOption 50..95 trin 5, collectRoundOption), status-/advarselsrække; callbacks onClickSource/onClickPolicy/onClickSellAt/onClickStoreAbove/onClickCollectRound; `describeOutboundOverview(run)` til Oversigt).
- `SmartPickupFrame`: `TAB_OVERVIEW, TAB_SUPPLY, TAB_OUTBOUND = 1, 2, 3`, `TAB_NAMES = {"spu_tab_overview","spu_tab_supply","spu_tab_outbound"}`, `tabKind = {nil, "supply", "outbound"}`; `fillRunOption` viser kun runs med fanens kind; `onNewRunName` opretter med fanens kind (Oversigt → supply); `setStaticRowsVisible(tab)` viser forsynings-rækker (ingredientMode, waitPool, loops, returnBelow) kun på fane 2, udkørsels-rækker (waitPool, sellAt, storeAbove, collectRound) kun på fane 3 (waitPool er fælles); blok-overskrifter via `addTextRow` ("— Mål og varer —" stil: brug tekstnøglerne `spu_section_*`). `refreshLive` opdaterer også produktrækkernes titler (pris-%) hvert sekund og udkørsels-status.
- l10n (da/en): `spu_tab_supply` "Forsyning"/"Supply", `spu_tab_outbound` "Udkørsel"/"Outbound", `spu_sources` "Kilder (fabrik, silo eller objektlager)", `spu_products` "Varer", `spu_policy_off/store/sell/sellElseStore` "Fra/Lager/Salg/Salg, ellers lager", `spu_sellAt` "Sælg fra (% af årets toppris)", `spu_sellAt_tooltip`, `spu_storeAbove` "Kør til lager over", `spu_storeAbove_tooltip`, `spu_collectRound` "Saml op på én tur", `spu_collectRound_tooltip`, `spu_section_run/targets/rules/wait/vehicles/status`, `spu_reason_noSource` "ingen genkendt kilde", `spu_reason_noProducts` "ingen vare valgt", `spu_reason_needsAutoload` "pallevarer kræver Universal Autoload på vognen", `spu_product_row` "%s — %s · %s", `spu_price_pct` "%d %% af top (%s)", `spu_price_none` "intet salgssted", `spu_outbound_waiting` "venter: %s", `spu_activity_holding` "holder ved %s (%s)".
- [ ] **Step 1:** XML + l10n; **Step 2:** split + ny fane; **Step 3:** `luajit` alle tests (GUI testes in-game); **Step 4: Commit** `feat(gui): faner Oversigt/Forsyning/Udkørsel; udkørsels-run med kilder, varepolitik, regler`
---
### Task 10: README, version, byg, deploy, in-game
- [ ] README: nyt afsnit "Udkørsel (v1.12)" (kilder, politikker, prisregel, lagertærskel, runde, objektlager-krav om markørplacering, advarsler). modDesc 1.12.0.0.
- [ ] `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip . && cp ../FS25_ADSmartPickup.zip ~/FS25-data/mods/`
- [ ] Commit `chore: v1.12.0.0 — udkørsel af produkter` + push til Gitea (`git push gitea main` med token fra vault).
- [ ] In-game-test (bruger): savegame 3 — se spec afsnit 4.
File diff suppressed because it is too large Load Diff
-174
View File
@@ -1,174 +0,0 @@
# Guider til opsætning (marker + flows) — implementeringsplan
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans (native; brugeren har givet fuld autonomi). Trin bruger `- [ ]`.
**Goal:** Fields- og Flows-fanen får en oversigt med resumé (klik = ret ét trin) og en trin-for-trin-guide ("Opsæt mark" / "Opsæt flow") på én fælles guide-ramme.
**Architecture:** En ren guide-motor (`adGuide.lua`) styrer trin, synlighed, tælling, næste/tilbage/spring. Oven på den to rene trin-definitioner (`adFieldGuide.lua`, `adFlowGuide.lua`) der ud fra en kladde + et `context` (det spil-afhængige, hentet af GUI'et) giver rækker (som data), valg, standarder, resumé og gem-resultat. GUI-laget (`gui/SmartPickupGuide.lua` fælles trin-skærm, `gui/SmartPickupFieldGuide.lua`, `gui/SmartPickupFlowGuide.lua`) er tyndt: bygger context, tegner rækkerne, router klik og gemmer via de eksisterende `ADFieldJobs`/`ADRunsController`-funktioner.
**Tech Stack:** Lua 5.1 (FS25), luajit til tests, FS25 GUI (klonede prefabs).
**Spec:** `docs/superpowers/specs/2026-09-26-guider-design.md`
## Global Constraints
- Lua 5.1: ingen goto, ingen coroutines, ingen `#` på tabeller med huller; pcall om spil-API.
- Gemt format (`ADSmartPickup_fields.xml`, `ADSmartPickup_runs.xml`) ændres ikke. Guiderne skriver kun via `ADFieldJobs.setTarget/setPlan` og `ADRunsController.createRun/updateRun/assignVehicle/unassignVehicle`.
- Intet gemmes før [Gem]/[Kør automatisk]/[Start flow]. [Annullér], fanebyt og lukket menu kasserer kladden.
- Rene moduler returnerer nye tabeller (kladden kopieres ved hvert valg).
- Filer under ~800 linjer; `SmartPickupFrame.lua` (971) får kun små routing-ændringer.
- Nye .lua i modDesc (rene moduler) eller `source()` i adSmartPickup.lua (GUI-filer), i rigtig rækkefølge.
- Al ny tekst i både `l10n_da.xml` og `l10n_en.xml`.
- Guide-rækker laves KUN med `ingredientPrefab` (Ja/Nej, flueben) og `productPrefab` (valg) med `kind = "guideRow"`, routet i `onClickIngredient`/`onClickProduct`; klikbare tekstlinjer via `makeRowClickable` + pendingClick. Ingen guide-rækker på `vehiclePrefab` (v1.30.4-fælden).
- Bundknapper: kun MENU_ACTIVATE, MENU_EXTRA_1, MENU_EXTRA_2, MENU_CANCEL (+ MENU_BACK) — andre findes ikke sikkert i FS25.
- Tests: `for t in tests/test_*.lua; do luajit $t | tail -1; done` — alle grønne.
## Review Focus
1. Klik på guide-rækker gemmes aldrig (forkert routing) → alle nye kinds routes via `guideRow`; testplan-punkt in-game.
2. Kladde muteres ved valg (Annullér gemmer alligevel) → test: `choose` ændrer ikke den gamle guide/kladde.
3. Trin forsvinder/dukker op efter et valg (fx mål-afgrøde græs på kornmark) → tælling og næste følger de synlige trin; test.
4. [Ret] fra oversigten må kun gemme det ene trin og bevare Auto/Manuel → test `result(guide, nil)` bevarer mode.
5. Faste maskiner: maskine fast på anden mark flyttes kun hvis brugeren vælger den (skjult i listen) → test `applyRigs`.
---
## Beslutninger (uklarheder i spec'en)
- **[Opsæt mark] uden valgt mark** vises også (åbner trin 1 "Mark"); ellers kunne trin 1 aldrig nås.
- **"Så igen bagefter"** (plan.sow uden mål-afgrøde) står i trin 2 under "Behold det der gror" for høst-/bare marker — ellers forsvinder muligheden.
- **Kalk**: i Klargøring ("Vælg selv"); når Klargøring ikke vises (ren græsmark) står Kalk i Pleje.
- **Klargøring "Automatisk"** = jord auto, kalk ja, mulch nej, tromle nej (anvendes kun når trinnet er synligt).
- **Maskiner-trinnet**: antal ad gangen + aflæsningsvogne først, så "Faste maskiner?" + listen (listen kan være lang). Aflæsningsvogne 1–3 (modellens `MAX_UNLOADERS`), ikke 1–5.
- **Mark uden markør**: gemmes med den valgte tilstand; job-løkken venter allerede på markøren (dagens regel). Oversigten viser "sæt markøren".
- **Mål-afgrøde** tvinger Auto (som i dag); på Oversigt-trinnet vises da kun [Kør automatisk].
- **[Ret] fra trin 8** vender tilbage til Oversigt-trinnet (kladden bevares); [Tilbage] i et [Ret]-trin fortryder det ene trins ændringer.
- **Flows**: bundlinjen har plads til 4 knapper → [Start/Stop] [Opsæt flow] [Nyt flow] [Slet]; Omdøb ligger på resuméets Navn-linje; Start alle/Stop alle bliver på fanen Oversigt.
- **Halm**: `context.straw` = afgrøder med halm (FS25 `fruitType.hasWindrow/windrowName`); kan det ikke aflæses (tom), vises halm-valget for alle høstafgrøder som i dag.
---
# Etape A — fælles ramme + Marker (v1.31.0.0)
### Task A1: Guide-motoren `adGuide.lua`
**Files:** Create `FS25_ADSmartPickup/adGuide.lua`, `tests/test_adGuide.lua`; Modify `modDesc.xml` (før adFieldModel.lua).
**Interfaces (Produces):**
```lua
ADGuide.new(def, draft, context, options) -> guide
-- def = {order = {stepId...}, isVisible = function(stepId, guide) -> bool}
-- options = {step = id|nil, single = bool, returnTo = "close"|"summary"}
-- guide = {def, draft, context, step, single, returnTo, snapshot}
ADGuide.visibleSteps(guide) -> {stepId...}
ADGuide.position(guide) -> index|nil, total
ADGuide.isLast(guide) -> bool
ADGuide.next(guide) -> guide, event -- event nil | "done"
ADGuide.back(guide) -> guide, event -- event nil | "cancel"
ADGuide.jump(guide, stepId, returnTo) -> guide (single, snapshot = kopi af kladden)
ADGuide.withDraft(guide, draft) / ADGuide.withContext(guide, context) -> guide
ADGuide.deepCopy(value) ; ADGuide.toggleItem(list, item, on) -> ny liste
ADGuide.labelText(label, getText) -> string -- label = {key, args} | {text}
ADGuide.lineText(line, getText) -> "Titel: del, del" -- line = {step, title = label, parts = {label...}}
```
- [ ] Test (RED): næste/tilbage springer skjulte trin over; tælling "3 af 5" over synlige; `next` på sidste = "done"; `back` på første = "cancel"; `jump(..., "summary")` + `next` lander på `summary` med single=false; `jump(..., "close")` + `next` = "done"; `back` i spring gendanner snapshot; `withDraft` returnerer ny guide (gammel urørt); trin der bliver usynligt efter et valg: `next` går til næste synlige i `order`; `toggleItem` uden dubletter; `labelText` med args og manglende args (pcall).
- [ ] Kør `luajit tests/test_adGuide.lua` → FAIL (fil mangler).
- [ ] Implementér; kør → ALLE TESTS OK.
### Task A2: Markguidens trin `adFieldGuide.lua`
**Files:** Create `FS25_ADSmartPickup/adFieldGuide.lua`, `tests/test_adFieldGuide.lua`; Modify `modDesc.xml` (efter adFieldView.lua).
**Interfaces:**
- Consumes: `ADGuide.*`, `ADFieldModel.product/MAX_RIGS/MAX_UNLOADERS`, `ADFieldFleet.visibleForField`, `ADFlowView.naturalLess`.
- Produces:
```lua
ADFieldGuide.ORDER = {"field","crop","cropChoice","prep","care","machines","store","summary"}
ADFieldGuide.open(field, context, options) -> guide -- context.info påkrævet
ADFieldGuide.openPicker(context) -> guide -- trin "field"
ADFieldGuide.pickField(guide, field, context) -> guide -- videre til trinnet efter "field"
ADFieldGuide.isVisible(stepId, guide) -> bool
ADFieldGuide.rows(guide) -> {row...}
-- row = {type="choice", id, title, options={{label, value}}, value}
-- | {type="toggle", id, title, off, on, value}
-- | {type="check", id, item, title, value}
-- | {type="text", label} | {type="link", id, value, label}
ADFieldGuide.choose(guide, id, value) -> guide
ADFieldGuide.summary(guide) -> {line...}
ADFieldGuide.result(guide, start) -> {fieldId, targetFruit, targetChanged, plan, rigIds}
ADFieldGuide.applyRigs(rigIdsByField, fieldId, rigIds) -> ny {[fieldId] = ids}
ADFieldGuide.stepKey(stepId) / hintKey(stepId) -> l10n-nøgler
```
- context: `{info, sowable={{name,title}}, mowable={[name]=true}, straw={[name]=true}|nil, rigs={{id,text,role}}, rigIdsByField, fieldSilos={{id,text}}, stores={{id,text}}|nil, storeHidden, fields={{id,text}}}`.
- kladde: `{fieldId, plan (kopi), originalTarget, prepAuto, useDedicated, storeMode ("auto"|"manual"|"fieldSilo"), storeIds, pickField}`.
- [ ] Test (RED): synlige trin for (a) græs uden mål: crop, cropChoice, care, machines, store, summary (ingen prep); (b) høst m. halm: + prep, cropChoice (halm); (c) høst uden halm (`straw` sat, afgrøden ikke med): ingen cropChoice; (d) bar mark uden mål: crop, prep, machines, summary (ingen care/store); (e) kornmark med mål græs: cropChoice (ensilage/hø) + prep; picker: kun "field". Standarder: ny mark → prepAuto, useDedicated=false, storeMode auto; plan med soil=plow → prepAuto=false; plan med rigIds → useDedicated; storeIds lig marksilo → fieldSilo. `choose` ændrer ikke gammel guide; `rigIds`-check toggler; `result`: prepAuto overskriver soil/mulch/lime/roll kun når prep er synlig; mode auto/manual efter start, uændret ved start=nil, auto ved mål; storeBuildingIds pr. storeMode; rigIds tom når useDedicated=false; targetChanged. `applyRigs` fjerner maskinen fra anden mark. Rækker: maskiner-trinnet skjuler maskiner faste på andre marker + tekst med antal; marksilo-spørgsmål kun når fieldSilos findes; "Vælg selv" viser siloerne. Resumé: linjer kun for synlige trin, med rigtige nøgler (fx `spu_guide_sum_dedicated` args {3}).
- [ ] Kør → FAIL; implementér; kør → OK. Hele suiten grøn.
### Task A3: Fælles trin-skærm `gui/SmartPickupGuide.lua`
**Files:** Create `gui/SmartPickupGuide.lua`; Modify `adSmartPickup.lua` (source efter SmartPickupFlowTab.lua), `gui/SmartPickupFrame.lua` (initGuideButtons i new, routing `guideRow` i onClickIngredient, refreshButtons → `getGuideButtons`, switchTab/onFrameClose lukker guiden, rebuild for faner med åben guide), `gui/SmartPickupOutboundTab.lua` (onClickProduct → guideRow).
**Interfaces (Produces):**
```lua
SmartPickupFrame.GUIDE_HOOKS[kind] = {model, tab, context(frame, adEnv, guide) -> context, save(frame, guide, start), choose(frame, id, value) -> handled}
SmartPickupFrame:initGuideButtons(i18n)
SmartPickupFrame:openGuide(kind, guide) SmartPickupFrame:closeGuide()
SmartPickupFrame:fillGuideRows(adEnv) -- overskrift "Trin 3 af 7 — …", forklaring, rækker
SmartPickupFrame:guideText(label) -> string
SmartPickupFrame:addLinkRow(text, action)
SmartPickupFrame:onClickGuideRow(state, data)
SmartPickupFrame:getGuideButtons() -> knapliste
SmartPickupFrame:onClickGuideNext/Back/Cancel/SaveOnly()
```
- [ ] Skriv filen; `luajit -bl` syntakstjek.
### Task A4: Marker-fanen: oversigt, markflåde, guide
**Files:** Create `gui/SmartPickupFieldGuide.lua` (context, gem, knapper for fanen); Modify `gui/SmartPickupFieldTab.lua` (fillFieldRows → oversigt/markflåde/guide; fjern den lange detaljeliste), l10n da/en.
- Oversigt: hint, tabel, for valgt mark: overskrift, resumé-linjer (klik = `ADGuide.jump(step, "close")`), fuld status, markør-linje, marksilo-advarsel. Knapper: [Opsæt mark] (EXTRA_1), [Start nu/Stop] (ACTIVATE), [Markflåde] (EXTRA_2).
- Markflåde: `fillFleetRows(adEnv, nil)` (alle maskiner). Knap [Til marker] (EXTRA_2).
- Gem: `setTarget` hvis ændret → `setPlan(plan)` → `applyRigs` → `setPlan(fid, {rigIds})` for ændrede marker.
- [ ] Skriv, syntakstjek, suiten grøn.
### Task A5: Docs + ship v1.31.0.0
- [ ] TESTPLAN-markarbejde.md: afsnit "Guider (v1.31)"; BRUGERMANUAL/USER_MANUAL: Marker-afsnit kort.
- [ ] Ship-procedure (version 1.30.4.0 → 1.31.0.0), commit `feat(gui): guide "Opsæt mark" …`, push gitea.
# Etape B — Flows på samme ramme (v1.32.0.0)
### Task B1: Flowguidens trin `adFlowGuide.lua`
**Files:** Create `FS25_ADSmartPickup/adFlowGuide.lua`, `tests/test_adFlowGuide.lua`; modDesc (efter adFlowView.lua).
**Interfaces (Produces):**
```lua
ADFlowGuide.ORDER = {"name","direction","buildings","goods","store","rules","wait","tractors","summary"}
ADFlowGuide.open(run, context, options) -> guide -- run = eksisterende run
ADFlowGuide.openNew(name, context) -> guide -- nyt flow (trin "name" synligt)
ADFlowGuide.isVisible(stepId, guide); ADFlowGuide.rows(guide); ADFlowGuide.choose(guide, id, value)
ADFlowGuide.summary(guide) -> {line...}
ADFlowGuide.result(guide) -> {isNew, runId, name, kind, changes, vehicleIds}
ADFlowGuide.vehicleChanges(oldIds, newIds) -> added, removed
ADFlowGuide.previewRun(draft) -> run-formet tabel til controllerens opslag
```
- context: `{buildings={{id,name,label,hasMarker,text}}, fillTypes={{name,title}}, products={{name,title}}, storeBuildings={{id,text}}, waitPools={{group,count}}, vehicles={{id,text,otherRunId,otherRunName,blocked}}, sellAtSteps, storeAboveSteps, returnBelowSteps, maxLoops}`.
- kladde: kopi af run-felterne + `isNew`, `showAllBuildings`, `storeAuto`.
- [ ] Test (RED): name-trin kun ved nyt flow; store-trin kun ved udkørsel med en vare på Lager/Salg-ellers-lager; retning skifter bygnings-rolle; varer: forsyning auto/vælg (ingredients toggles), udkørsel politik pr. vare (products); regler pr. retning; traktorer i andre flows/blokerede skjult + tekst; resumé pr. retning; result.changes; vehicleChanges; choose immutabelt.
- [ ] FAIL → implementér → OK.
### Task B2: Flows-fanen i GUI'et
**Files:** Create `gui/SmartPickupFlowGuide.lua`; Modify `gui/SmartPickupFrame.lua` (rebuild for Flows-fanen: tavle + resumé, statiske rækker skjult; refreshLive/refreshButtons), l10n.
- Oversigt: tavlen (`fillOverviewRows`), valgt flow: resumé (klik = ret ét trin; Navn-linjen = omdøb-dialog), status-linje (live). Knapper: [Start/Stop] (ACTIVATE), [Opsæt flow] (EXTRA_1), [Nyt flow] (EXTRA_2), [Slet] (CANCEL).
- Gem: nyt → `createRun`; `updateRun(changes)`; traktorer via `vehicleChanges` + `assignVehicle/unassignVehicle`; `resolveMarkers`; `save()`; [Start flow] → `startRun`.
- [ ] Skriv, syntakstjek, suiten grøn.
### Task B3: Docs + ship v1.32.0.0; selv-review
- [ ] TESTPLAN + manualer (Flows-afsnit), ship 1.31.0.0 → 1.32.0.0.
- [ ] Code-review-agent på hele diffen; ret CRITICAL/HIGH; evt. v1.32.1.0.
File diff suppressed because it is too large Load Diff
@@ -1,919 +0,0 @@
# 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.
@@ -1,690 +0,0 @@
# Flows/Fields: farvet liste + redigeringsvindue — 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:** Flows- og Fields-fanen viser kun en farvet liste med Start/Stop pr. række; klik på en række åbner et redigeringsvindue (panel i samme frame) med de nuværende redigeringslinjer.
**Architecture:** Rene statusfunktioner (`ADFlowView.flowStatus`, `ADFieldView.fieldStatus`) afgør niveau/farve og testes med luajit. GUI: rækkehjælperne får et mål-layout (`self.rowTarget`), så den eksisterende resumé-/guide-kode kan tegne i vinduets egen `ScrollingLayout`. Vinduet er et skjult overlay i `SmartPickupFrame.xml`, styret af en ny mixin `gui/SmartPickupEditPanel.lua`.
**Tech Stack:** FS25 Lua (GIANTS GUI: Bitmap/Text/ScrollingLayout, TabbedMenuFrameElement menuButtonInfo), luajit-tests i `tests/`.
**Spec:** `docs/superpowers/specs/2026-09-29-flow-field-list-edit-panel-design.md`
## Global Constraints
- Ingen ændring af flow-/mark-logik, guiderne eller Oversigt-tavlen/flåde-visningen.
- Rækker er ≈835 px brede (som lagervisningen).
- Farver: grøn `{0.30, 0.75, 0.35, 1}` = running, gul `{0.95, 0.80, 0.25, 1}` = waiting, rød `{0.90, 0.35, 0.30, 1}` = error, grå `{0.55, 0.55, 0.55, 1}` = stopped.
- Al GUI-kode i `self:safe(...)`; ingen utilsigtede globals (`tests/test_noStrayGlobals.lua`).
- Byg kun når alle tests er grønne: `for t in tests/test_*.lua; do luajit $t >/dev/null || echo FAIL $t; done`. Zip: `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .`, kopi til `~/FS25-data/mods/`, commit, push remote `gitea` main (Basic-auth header med token fra `~/credentials-vault.md` sektion Gitea, linje med "API-token").
- l10n: nye tekster i både `l10n/l10n_da.xml` og `l10n/l10n_en.xml`, præfiks `spu_`.
## Review Focus
- Klik på listen bag et åbent vindue: skal intet gøre (vinduet ejer klik) — pinned af `makeRowClickable`-regel i Task 2 (klik tæller kun inde i rækkens eget layout, og listen ignorerer klik når vinduet er åbent).
- ESC med vindue åbent: lukker vinduet, ikke ESC-menuen; med et [Edit]-trin åbent: lukker trinnet først — Task 3.
- Flow/mark slettet mens vinduet er åbent: vinduet lukker sig med besked — Task 3 (`refreshEditPanel`).
- Start/Stop på en række for et andet flow end det valgte: må ikke påvirke det valgte — Task 4 (handling bærer sit eget id).
- Flow startet men alle traktorer afvist ("ingen vogn"): rød, ikke grå — pinned af test i Task 1.
---
### Task 1: Rene statusfunktioner for flows (+ fælles farver)
**Files:**
- Modify: `FS25_ADSmartPickup/adFlowView.lua` (tilføj i bunden)
- Test: `tests/test_flowStatus.lua` (ny)
**Interfaces:**
- Produces: `ADFlowView.LEVEL_COLORS = {running={...}, waiting={...}, error={...}, stopped={...}}`;
`ADFlowView.flowStatus(s) -> level` hvor `s = {running=int, total=int, stopped=bool, canStop=bool, warning=string|nil, reason=string|nil, lastStartFailed=bool}` og `level ∈ {"running","waiting","error","stopped"}`.
- [ ] **Step 1: Write the failing test** — `tests/test_flowStatus.lua`:
```lua
-- Kør: luajit tests/test_flowStatus.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFlowView.lua")
local V = ADFlowView
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
check("kører", V.flowStatus({running = 3, total = 4, canStop = true}), "running")
check("kører men holder (begrundelse)", V.flowStatus({running = 2, total = 2, canStop = true, reason = "intet at hente"}), "waiting")
check("startet, venter på vogn", V.flowStatus({running = 0, total = 2, canStop = true}), "waiting")
check("advarsel", V.flowStatus({running = 1, total = 2, canStop = true, warning = "mål mangler"}), "error")
check("start afvist (ingen vogn)", V.flowStatus({running = 0, total = 2, lastStartFailed = true}), "error")
check("stoppet", V.flowStatus({running = 0, total = 2, stopped = true}), "stopped")
check("aldrig startet", V.flowStatus({running = 0, total = 2}), "stopped")
check("stoppet vinder over gammel afvisning", V.flowStatus({running = 0, total = 2, stopped = true, lastStartFailed = true}), "stopped")
check("farver findes", V.LEVEL_COLORS.waiting ~= nil and V.LEVEL_COLORS.stopped ~= nil, true)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
```
- [ ] **Step 2: Run** `luajit tests/test_flowStatus.lua` — Expected: FAIL (flowStatus nil).
- [ ] **Step 3: Implement** — append to `adFlowView.lua`:
```lua
-- ---- status pr. flow (Flows-listen) ----
ADFlowView.LEVEL_COLORS = {
running = {0.30, 0.75, 0.35, 1},
waiting = {0.95, 0.80, 0.25, 1},
error = {0.90, 0.35, 0.30, 1},
stopped = {0.55, 0.55, 0.55, 1},
}
-- s = {running, total, stopped, canStop, warning, reason, lastStartFailed} -> "running"|"waiting"|"error"|"stopped"
function ADFlowView.flowStatus(s)
if s.stopped then return "stopped" end
if s.warning ~= nil then return "error" end
if (s.running or 0) == 0 and s.lastStartFailed then return "error" end
if (s.running or 0) > 0 and s.reason == nil then return "running" end
if (s.running or 0) > 0 or s.canStop then return "waiting" end
return "stopped"
end
```
- [ ] **Step 4: Run** all tests — Expected: `ALLE TESTS OK` in test_flowStatus, no FAIL elsewhere.
- [ ] **Step 5: Commit** `git add tests/test_flowStatus.lua FS25_ADSmartPickup/adFlowView.lua && git commit -m "feat(flows): ren statusfunktion + farver til Flows-listen"`
---
### Task 2: Rækker kan tegnes i et andet layout
**Files:**
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.lua` (`new`: felter; `addRow`; `clearRows`; `colorRows`)
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFlowTab.lua` (`makeRowClickable`)
**Interfaces:**
- Produces: `self.rowTarget` (standard `nil` = `self.boxLayout`); `SmartPickupFrame:withRowTarget(layout, fn)` kører `fn(self)` med rækker klonet ind i `layout`; `self.panelRows` (kloner i vinduet) adskilt fra `self.clonedRows`; `SmartPickupFrame:clearPanelRows()`; `SmartPickupFrame:colorLayout(layout)`. Hver klon får `row.spuLayout = layout`.
- [ ] **Step 1: `addRow`** — replace body:
```lua
function SmartPickupFrame:addRow(prefab, data)
local layout = self.rowTarget or self.boxLayout
local row = prefab:clone(layout)
row.spuLayout = layout
FocusManager:loadElementFromCustomValues(row)
table.insert(layout == self.boxLayout and self.clonedRows or self.panelRows, row)
local option = row:getDescendantByName("option")
if option ~= nil then
option:setTexts({self.i18n:getText("ui_no"), self.i18n:getText("ui_yes")})
self.rowData[option] = data
end
return row
end
-- Rækker klonet ind i et andet layout (redigeringsvinduet) mens fn kører.
function SmartPickupFrame:withRowTarget(layout, fn)
local previous = self.rowTarget
self.rowTarget = layout
local ok, err = pcall(fn, self)
self.rowTarget = previous
if not ok then error(err, 0) end
end
function SmartPickupFrame:clearPanelRows()
for _, row in ipairs(self.panelRows or {}) do
FocusManager:removeElement(row)
row:delete()
end
self.panelRows = {}
end
```
- [ ] **Step 2: `new`** — next to `clonedRows = {}` initialisation add `self.panelRows = {}` (find `clonedRows` in `SmartPickupFrame.new` and add on the same pattern).
- [ ] **Step 3: `colorRows`** — split into `colorLayout(layout)` and keep `colorRows()` calling `self:colorLayout(self.boxLayout)`:
```lua
function SmartPickupFrame:colorLayout(layout)
local colors = InGameMenuSettingsFrame ~= nil and InGameMenuSettingsFrame.COLOR_ALTERNATING or nil
if colors == nil or layout == nil then return end
local unpackFn = table.unpack or unpack
local isEvenRow = false
for _, element in ipairs(layout.elements) do
if element.setImageColor ~= nil and colors[isEvenRow] ~= nil then
element:setImageColor(nil, unpackFn(colors[isEvenRow]))
end
isEvenRow = not isEvenRow
end
end
function SmartPickupFrame:colorRows()
self:colorLayout(self.boxLayout)
end
```
- [ ] **Step 4: `makeRowClickable`** — clicks count inside the row's own layout, and list rows ignore clicks while the panel is open:
```lua
local layout = element.spuLayout or frame.boxLayout
local blockedByPanel = frame.editPanelOpen and layout == frame.boxLayout
if not used and not eventUsed and not blockedByPanel and isUp and button == MOUSE_LEFT and (element.getIsVisible == nil or element:getIsVisible())
and isInside(element, posX, posY) and isInside(layout, posX, posY) then
```
(replace the existing `if not used and not eventUsed and isUp ...` condition lines; keep the body.)
- [ ] **Step 5: Run all tests** — no FAIL (behaviour unchanged: `rowTarget` nil everywhere).
- [ ] **Step 6: Commit** `git commit -am "refactor(gui): rækker kan klones ind i et andet layout (redigeringsvindue)"`
---
### Task 3: Redigeringsvinduet (overlay + mixin)
**Files:**
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.xml` (overlay efter `boxLayout`)
- Modify: `FS25_ADSmartPickup/gui/guiProfiles.xml` (profiler)
- Create: `FS25_ADSmartPickup/gui/SmartPickupEditPanel.lua`
- Modify: `FS25_ADSmartPickup/adSmartPickup.lua` (source efter `SmartPickupFlowGuide.lua`)
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.lua` (`rebuild`: fyld vinduet; `refreshButtons`: vinduets knapper; `refreshLive`: `refreshEditPanel`; `onFrameClose`: luk vinduet)
- Modify: `l10n/l10n_da.xml`, `l10n/l10n_en.xml`
**Interfaces:**
- Consumes: Task 2 (`withRowTarget`, `clearPanelRows`, `colorLayout`, `editPanelOpen`).
- Produces: `SmartPickupFrame:openEditPanel(kind, id)` (`kind ∈ {"flow","field"}`), `closeEditPanel()`, `fillEditPanel(adEnv)`, `refreshEditPanel(adEnv)`, felterne `self.editPanelOpen`, `self.editPanelKind`, `self.editPanelId`; hook-tabel `SmartPickupFrame.EDIT_PANEL_KINDS[kind] = {title(frame,id)->string, exists(frame,id)->bool, fill(frame,adEnv,id), status(frame,adEnv,id)->{text,level}, startStop(frame,id), startStopText(frame,adEnv,id)->string, canDelete=bool, delete(frame,id)}` — Task 4/7 registrerer "flow"/"field".
- [ ] **Step 1: Profiles** — add to `guiProfiles.xml` (before `</Profiles>`):
```xml
<!-- redigeringsvindue (v1.58): mørk flade + panel; størrelser sættes i Lua ud fra boxLayout -->
<Profile name="spuOverlayBackdrop" extends="spuBar">
<imageColor value="0 0 0 0.65"/>
<positionOrigin value="topLeft"/>
<size value="100px 100px"/>
</Profile>
<Profile name="spuOverlayPanel" extends="spuBar">
<imageColor value="0.09 0.09 0.09 0.97"/>
<positionOrigin value="topLeft"/>
<size value="100px 100px"/>
</Profile>
<Profile name="spuPanelTitle" extends="spuCellBold">
<textSize value="20px"/>
</Profile>
```
- [ ] **Step 2: XML overlay** — directly after `</ScrollingLayout>` of `boxLayout`:
```xml
<!-- redigeringsvindue (skjult; placeres/størrelse sættes i SmartPickupEditPanel.lua) -->
<GuiElement profile="baseReference" id="editOverlay" visible="false">
<Bitmap profile="spuOverlayBackdrop" id="editBackdrop"/>
<Bitmap profile="spuOverlayPanel" id="editPanel">
<Text profile="spuPanelTitle" id="editTitle" position="20px -16px"/>
<Text profile="spuCell" id="editStatus" position="20px -46px"/>
<ScrollingLayout profile="fs25_settingsLayout" id="editLayout" position="0px -80px"/>
</Bitmap>
</GuiElement>
```
- [ ] **Step 3: Mixin** — create `gui/SmartPickupEditPanel.lua`:
```lua
-- Smart Pickup — redigeringsvindue (v1.58)
-- Klik på en række i Flows/Fields åbner et panel oven på listen. Indholdet tegnes af den eksisterende
-- resumé-/guide-kode i vinduets egen ScrollingLayout (SmartPickupFrame:withRowTarget). Knapper i bunden via
-- menuButtonInfo: Luk (MENU_BACK = ESC), Start/Stop, Slet. Hver art registrerer sig i EDIT_PANEL_KINDS.
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
SmartPickupFrame.EDIT_PANEL_KINDS = SmartPickupFrame.EDIT_PANEL_KINDS or {}
SmartPickupFrame.EDIT_PANEL_WIDTH, SmartPickupFrame.EDIT_PANEL_HEIGHT = 0.78, 0.86 -- andel af listens flade
function SmartPickupFrame:initEditPanelButtons(i18n)
self.panelCloseButtonInfo = {inputAction = InputAction.MENU_BACK, text = i18n:getText("spu_panel_close"), callback = function() self:onClickPanelClose() end}
self.panelStartButtonInfo = {inputAction = InputAction.MENU_ACTIVATE, text = i18n:getText("spu_button_start"), callback = function() self:onClickPanelStartStop() end}
self.panelDeleteButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_button_delete"), callback = function() self:onClickPanelDelete() end}
self.panelButtons = {self.panelCloseButtonInfo, self.panelStartButtonInfo, self.panelDeleteButtonInfo}
end
function SmartPickupFrame:editPanelKind()
return self.editPanelOpen and SmartPickupFrame.EDIT_PANEL_KINDS[self.editPanelKind] or nil
end
-- Vinduet dækker listens flade; panelet er centreret i den.
function SmartPickupFrame:layoutEditPanel()
local box = self.boxLayout
local x, y, w, h = box.position[1], box.position[2], box.size[1], box.size[2]
self.editBackdrop:setPosition(x, y)
self.editBackdrop:setSize(w, h)
local pw, ph = w * SmartPickupFrame.EDIT_PANEL_WIDTH, h * SmartPickupFrame.EDIT_PANEL_HEIGHT
self.editPanel:setPosition(x + (w - pw) / 2, y - (h - ph) / 2)
self.editPanel:setSize(pw, ph)
local top = 80 * g_pixelSizeScaledY
self.editLayout:setSize(pw, ph - top)
end
function SmartPickupFrame:openEditPanel(kind, id)
if SmartPickupFrame.EDIT_PANEL_KINDS[kind] == nil then return end
self.editPanelOpen, self.editPanelKind, self.editPanelId = true, kind, id
self:closeGuide()
self.editOverlay:setVisible(true)
self:layoutEditPanel()
self:requestRebuild()
end
function SmartPickupFrame:closeEditPanel()
if not self.editPanelOpen then return end
self.editPanelOpen, self.editPanelKind, self.editPanelId = false, nil, nil
self:closeGuide()
self:clearPanelRows()
if self.editOverlay ~= nil then self.editOverlay:setVisible(false) end
self:requestRebuild()
end
-- Kaldes fra rebuild (efter listen): vinduets rækker genopbygges i editLayout.
function SmartPickupFrame:fillEditPanel(adEnv)
self:clearPanelRows()
local hooks = self:editPanelKind()
if hooks == nil then return end
if not hooks.exists(self, self.editPanelId) then
self.statusOverride = self.i18n:getText("spu_panel_gone")
self.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS
self.editPanelOpen, self.editPanelKind, self.editPanelId = false, nil, nil
self.editOverlay:setVisible(false)
return
end
self.editTitle:setText(hooks.title(self, self.editPanelId))
self:withRowTarget(self.editLayout, function(frame)
if frame.guide ~= nil then
frame:fillGuideRows(adEnv)
else
hooks.fill(frame, adEnv, frame.editPanelId)
end
end)
self:colorLayout(self.editLayout)
self.editLayout:invalidateLayout()
self:refreshEditPanel(adEnv)
end
-- Hvert sekund: status + knaptekster.
function SmartPickupFrame:refreshEditPanel(adEnv)
local hooks = self:editPanelKind()
if hooks == nil or adEnv == nil then return end
if not hooks.exists(self, self.editPanelId) then self:closeEditPanel() return end
local status = hooks.status(self, adEnv, self.editPanelId)
self.editStatus:setText(status.text or "")
local color = ADFlowView.LEVEL_COLORS[status.level] or ADFlowView.LEVEL_COLORS.stopped
if self.editStatus.setTextColor ~= nil then self.editStatus:setTextColor(color[1], color[2], color[3], color[4]) end
self.panelStartButtonInfo.text = hooks.startStopText(self, adEnv, self.editPanelId)
self.panelDeleteButtonInfo.disabled = not hooks.canDelete
end
function SmartPickupFrame:onClickPanelClose()
self:safe(function(frame)
-- et åbent [Edit]-trin lukkes først (kladden kasseres som ved Annullér)
if frame.guide ~= nil then
frame:closeGuide()
frame:requestRebuild()
return
end
frame:closeEditPanel()
end)
end
function SmartPickupFrame:onClickPanelStartStop()
self:safe(function(frame)
local hooks = frame:editPanelKind()
if hooks ~= nil then hooks.startStop(frame, frame.editPanelId) end
frame:requestRebuild()
end)
end
function SmartPickupFrame:onClickPanelDelete()
self:safe(function(frame)
local hooks = frame:editPanelKind()
if hooks ~= nil and hooks.canDelete then hooks.delete(frame, frame.editPanelId) end
end)
end
```
- [ ] **Step 4: Wire into frame** —
- `adSmartPickup.lua`: after `source(ADSmartPickup.dir .. "gui/SmartPickupFlowGuide.lua")` add `source(ADSmartPickup.dir .. "gui/SmartPickupEditPanel.lua")`.
- `SmartPickupFrame.new`: where `initFlowButtons(i18n)` is called, also call `self:initEditPanelButtons(i18n)`.
- `rebuild`: in the Flows branch and the Fields branch, after `self.boxLayout:invalidateLayout()` add `if self.editPanelOpen then self:fillEditPanel(adEnv) end`.
- `refreshButtons`: first branch: `if self.editPanelOpen then wanted = self.guide ~= nil and self:getGuideButtons() or self.panelButtons elseif self:hasGuideOn(...)` — and in the guide-in-panel case replace the guide list's `self.backButtonInfo` by `self.panelCloseButtonInfo` (build `self.panelGuideButtons` once: copy of `getGuideButtons()` with index 1 swapped).
- `refreshLive`: at the end `if self.editPanelOpen then self:refreshEditPanel(adEnv) end`.
- `onFrameClose`: before `closeGuide()` add `self.editPanelOpen, self.editPanelKind, self.editPanelId = false, nil, nil; if self.editOverlay ~= nil then self.editOverlay:setVisible(false) end; self:clearPanelRows()`.
- `switchTab`: `if self.editPanelOpen then self:closeEditPanel() end` at the top.
- Guide rendering while panel is open: `fillFlowTabRows`/`fillFieldRows` start with `if self:hasGuideOn(TAB) and self:fillGuideRows(adEnv) then return end` — guard with `not self.editPanelOpen and` so the guide draws only in the panel.
- [ ] **Step 5: l10n** — add `spu_panel_close` ("Luk"/"Close"), `spu_panel_gone` ("Posten findes ikke længere — vinduet er lukket"/"That item no longer exists — window closed"), `spu_button_delete` if not present (check `grep spu_button_delete l10n/l10n_da.xml` first; reuse existing delete text key if it exists).
- [ ] **Step 6: Run all tests** (noStrayGlobals must pass) and validate XML: `python3 -c "import xml.dom.minidom as m; [m.parse(f) for f in ('FS25_ADSmartPickup/gui/SmartPickupFrame.xml','FS25_ADSmartPickup/gui/guiProfiles.xml','FS25_ADSmartPickup/l10n/l10n_da.xml','FS25_ADSmartPickup/l10n/l10n_en.xml')]"`.
- [ ] **Step 7: Commit** `git commit -am "feat(gui): redigeringsvindue (overlay) med Luk/ESC, Start/Stop, Slet"`
---
### Task 4: Flows-fanen som farvet liste + "flow" i vinduet
**Files:**
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.xml` (ny `flowListPrefab`, fjernes ved `initialize`)
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.lua` (`initialize`-listen: `"flowListPrefab"`; `onClickStartStop` husker afvisning)
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFlowGuide.lua` (`fillFlowTabRows`, `refreshFlowTab`, `initFlowButtons`, registrér `EDIT_PANEL_KINDS.flow`)
- Modify: `l10n/*.xml`
**Interfaces:**
- Consumes: `ADFlowView.flowStatus`, `ADFlowView.LEVEL_COLORS` (Task 1); `openEditPanel`, `EDIT_PANEL_KINDS` (Task 3); `makeRowClickable` (Task 2).
- Produces: `SmartPickupFrame:flowLevel(adEnv, run) -> level`, `self.lastStartFailed[runId] = bool`, `SmartPickupFrame:toggleRun(adEnv, runId)`.
- [ ] **Step 1: Prefab** — in the XML after `flowPrefab`:
```xml
<!-- Flows-listen (v1.58): ● · navn · varer · kører/i alt · status · [Start]/[Stop]. ~835 px. -->
<Bitmap profile="fs25_multiTextOptionContainer" id="flowListPrefab">
<Bitmap profile="spuDot" name="dot" position="20px 0px"/>
<Text profile="spuCellBold" name="c1" position="38px 0px" size="200px 40px" textMaxWidth="192px"/>
<Text profile="spuCell" name="c2" position="238px 0px" size="170px 40px" textMaxWidth="162px"/>
<Text profile="spuCell" name="c3" position="408px 0px" size="70px 40px" textMaxWidth="62px"/>
<Text profile="spuCell" name="c4" position="478px 0px" size="252px 40px" textMaxWidth="244px"/>
<Text profile="spuCellBold" name="c5" position="730px 0px" size="100px 40px" textMaxWidth="92px"/>
</Bitmap>
```
- [ ] **Step 2: Toggle + afvisning** — in `SmartPickupFlowGuide.lua` add:
```lua
-- Start/Stop for ét flow (rækkens knap og vinduets knap). Husker om sidste start blev helt afvist (rød).
function SmartPickupFrame:toggleRun(adEnv, runId)
local run = ADRuns.getRun(ADRunsController.store, runId)
if adEnv == nil or run == nil then return end
self.lastStartFailed = self.lastStartFailed or {}
if ADRunsController.getRunStatus(adEnv, run).canStop then
ADRunsController.stopRun(adEnv, run.id)
self.lastStartFailed[run.id] = nil
self.statusOverride = nil
else
local started, skipped, reasons = self:summarizeStartResults(ADRunsController.startRun(adEnv, run.id))
self.lastStartFailed[run.id] = started == 0 and skipped > 0
local text = string.format(self.i18n:getText("spu_status_started"), started, skipped)
self:showOverride(text, reasons)
if skipped > 0 and ADFieldJobs ~= nil and ADFieldJobs.notify ~= nil then
ADFieldJobs.notify(string.format("%s: %s — %s", tostring(run.name), text, table.concat(reasons, "; ")), started == 0)
end
end
ADRunsController.save()
end
function SmartPickupFrame:flowLevel(adEnv, run)
local row = ADRunsController.getFlowRow(adEnv, run)
local status = ADRunsController.getRunStatus(adEnv, run)
return ADFlowView.flowStatus({running = row.running, total = row.total, stopped = run.stopped == true,
canStop = status.canStop == true, warning = row.warning, reason = row.reason,
lastStartFailed = (self.lastStartFailed or {})[run.id] == true})
end
```
And make `onClickStartStop` (Frame.lua) call `frame:toggleRun(adEnv, run.id)` instead of its inline start/stop body (keep `ADRunsController.save()` + `requestRebuild()` there).
- [ ] **Step 3: List** — replace `fillFlowTabRows` body after the guide guard:
```lua
function SmartPickupFrame:fillFlowTabRows(adEnv)
if not self.editPanelOpen and self:hasGuideOn(SmartPickupFrame.TAB_FLOWS) and self:fillGuideRows(adEnv) then return end
self.flowListRows = {}
local i18n = self.i18n
if #ADRunsController.store.runs == 0 then
self:addTextRow(i18n:getText("spu_noRuns"))
return
end
self:addTextRow(i18n:getText("spu_flowList_hint"))
for _, group in ipairs(ADFlowView.boardGroups(ADRunsController.store.runs)) do
self:addTextRow(i18n:getText(group.kind == "outbound" and "spu_board_outbound" or "spu_board_supply"))
for _, run in ipairs(group.runs) do
local row = self:addRow(self.flowListPrefab, {kind = "flowList"})
local entry = {runId = run.id, row = row, dot = row:getDescendantByName("dot"),
cells = {row:getDescendantByName("c1"), row:getDescendantByName("c2"), row:getDescendantByName("c3"),
row:getDescendantByName("c4"), row:getDescendantByName("c5")}}
local runId = run.id
local actionCell = entry.cells[5]
self:makeRowClickable(row, function(frame)
-- klik på [Start]/[Stop]-feltet = handling; ellers åbn vinduet
if frame.lastClickX ~= nil and actionCell.absPosition ~= nil and frame.lastClickX >= actionCell.absPosition[1] then
frame:toggleRun(frame.getAdEnv(), runId)
frame:requestRebuild()
else
frame.selectedRunId = runId
frame:openEditPanel("flow", runId)
end
end)
table.insert(self.flowListRows, entry)
end
end
self:refreshFlowListRows(adEnv)
end
function SmartPickupFrame:refreshFlowListRows(adEnv)
local unpackFn = table.unpack or unpack
for _, entry in ipairs(self.flowListRows or {}) do
local run = ADRuns.getRun(ADRunsController.store, entry.runId)
if run ~= nil then
local cells = self:flowCells(adEnv, run)
local level = self:flowLevel(adEnv, run)
local color = ADFlowView.LEVEL_COLORS[level]
local canStop = ADRunsController.getRunStatus(adEnv, run).canStop
entry.cells[1]:setText(tostring(run.name))
entry.cells[2]:setText(tostring(cells[2]))
entry.cells[3]:setText(tostring(cells[5]))
entry.cells[4]:setText(tostring(cells[6]))
entry.cells[5]:setText(self.i18n:getText(canStop and "spu_flowList_stop" or "spu_flowList_start"))
if entry.dot ~= nil and entry.dot.setImageColor ~= nil then entry.dot:setImageColor(nil, unpackFn(color)) end
if entry.cells[4].setTextColor ~= nil then entry.cells[4]:setTextColor(unpackFn(color)) end
end
end
end
```
In `makeRowClickable` (Task 2's function) store the click x before setting `pendingClick`: `frame.lastClickX = posX`.
- [ ] **Step 4: `refreshFlowTab`** — replace body:
```lua
function SmartPickupFrame:refreshFlowTab(adEnv)
if not self.editPanelOpen and self:hasGuideOn(SmartPickupFrame.TAB_FLOWS) then
self:setStatusText(self.statusOverride or "")
return
end
self:refreshFlowListRows(adEnv)
local running, total = 0, 0
for _, run in ipairs(ADRunsController.store.runs) do
total = total + 1
if ADRunsController.getRunStatus(adEnv, run).running > 0 then running = running + 1 end
end
self:setStatusText(self.statusOverride or string.format(self.i18n:getText("spu_flowList_status"), running, total))
end
```
- [ ] **Step 5: Buttons + panel kind** — in `initFlowButtons`: `self.flowButtons = {self.backButtonInfo, self.flowNewButtonInfo}`. Register:
```lua
SmartPickupFrame.EDIT_PANEL_KINDS.flow = {
canDelete = true,
exists = function(frame, id) return ADRuns.getRun(ADRunsController.store, id) ~= nil end,
title = function(frame, id)
local run = ADRuns.getRun(ADRunsController.store, id)
return string.format(frame.i18n:getText("spu_panel_flowTitle"), tostring(run ~= nil and run.name or "?"))
end,
fill = function(frame, adEnv, id)
local run = ADRuns.getRun(ADRunsController.store, id)
if run ~= nil then frame:fillFlowSummary(adEnv, run) end
end,
status = function(frame, adEnv, id)
local run = ADRuns.getRun(ADRunsController.store, id)
return {text = tostring(frame:flowCells(adEnv, run)[6]), level = frame:flowLevel(adEnv, run)}
end,
startStopText = function(frame, adEnv, id)
local run = ADRuns.getRun(ADRunsController.store, id)
return frame.i18n:getText(ADRunsController.getRunStatus(adEnv, run).canStop and "spu_button_stop" or "spu_button_start")
end,
startStop = function(frame, id) frame:toggleRun(frame.getAdEnv(), id) end,
delete = function(frame, id) frame.selectedRunId = id; frame:onClickDelete() end,
}
```
In `fillFlowSummary` drop the first heading row (the panel title shows the name): remove the `spu_guide_flow_section` line. Add "Opsæt flow" as a last link row in the summary: `self:addLinkRow(i18n:getText("spu_guide_flow_open"), function(frame) frame.selectedRunId = runId; frame:onClickFlowSetup() end)`.
After a delete (`onClickDelete`'s YesNo callback path) the run no longer exists → `refreshEditPanel` closes the panel (Review Focus).
- [ ] **Step 6: l10n** — `spu_flowList_hint` ("Klik på et flow for at rette det. [Start]/[Stop] styrer flowet direkte." / en), `spu_flowList_start` ("[Start]"), `spu_flowList_stop` ("[Stop]"), `spu_flowList_status` ("Flows i gang: %d af %d" / "Flows running: %d of %d"), `spu_panel_flowTitle` ("Rediger flow: %s" / "Edit flow: %s").
- [ ] **Step 7: Run all tests + XML check.**
- [ ] **Step 8: Commit** `git commit -am "feat(flows): Flows-fanen som farvet liste med Start/Stop pr. række; klik åbner redigeringsvinduet"`
---
### Task 5: Etape 1 build + in-game test
- [ ] **Step 1:** bump `modDesc.xml` `<version>` to `1.58.0.0`; build zip; copy to `~/FS25-data/mods/`; `unzip -p ~/FS25-data/mods/FS25_ADSmartPickup.zip modDesc.xml | grep -o '<version>[^<]*'` shows 1.58.0.0.
- [ ] **Step 2:** commit + push gitea main.
- [ ] **Step 3: In-game checklist (brugeren):** liste med prikker/farver; [Start]/[Stop] på en række virker og rører ikke andre flows; klik på navnet åbner vinduet; [Edit] → Gem inde i vinduet; Luk og ESC lukker vinduet (ESC med åbent trin lukker trinnet først); klik på listen bagved gør intet; Slet flow fra vinduet lukker det; log uden `GUI-fejl`. Ret layout-afvigelser (størrelser/placering) efter screenshot før Task 6.
---
### Task 6: Ren statusfunktion for marker
**Files:**
- Modify: `FS25_ADSmartPickup/adFieldView.lua`
- Test: `tests/test_fieldStatus.lua` (ny)
**Interfaces:**
- Produces: `ADFieldView.fieldStatus(s) -> level` hvor `s = {hasMarker=bool, working=bool, awaiting=bool, startRequested=bool, reasonKey=string|nil}`; `ADFieldView.ERROR_REASONS` (sæt af reason-nøgler).
- [ ] **Step 1: Test** — `tests/test_fieldStatus.lua`:
```lua
-- Kør: luajit tests/test_fieldStatus.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFieldView.lua")
local V = ADFieldView
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
check("mangler markør", V.fieldStatus({hasMarker = false}), "error")
check("arbejder", V.fieldStatus({hasMarker = true, working = true}), "running")
check("arbejder men venter på maskine", V.fieldStatus({hasMarker = true, working = true, reasonKey = "spu_field_reason_allBusy"}), "waiting")
check("for bred", V.fieldStatus({hasMarker = true, working = true, reasonKey = "spu_field_reason_tooWide"}), "error")
check("ingen maskine", V.fieldStatus({hasMarker = true, reasonKey = "spu_field_reason_noRig"}), "error")
check("regn", V.fieldStatus({hasMarker = true, working = true, reasonKey = "spu_field_reason_rain"}), "waiting")
check("venter på spilleren (høster)", V.fieldStatus({hasMarker = true, awaiting = true}), "waiting")
check("start bestilt", V.fieldStatus({hasMarker = true, startRequested = true}), "waiting")
check("i ro", V.fieldStatus({hasMarker = true}), "stopped")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
```
(If `adFieldView.lua` needs other modules loaded, add the same `dofile`s as `tests/test_adFieldView.lua` uses.)
- [ ] **Step 2: Run** — FAIL (fieldStatus nil).
- [ ] **Step 3: Implement** — append to `adFieldView.lua`:
```lua
-- ---- status pr. mark (Fields-listen) ----
-- Årsager der betyder "kan ikke" (rød) — resten er "venter" (gul).
ADFieldView.ERROR_REASONS = {
spu_field_reason_noRig = true, spu_field_reason_cannotDo = true, spu_field_reason_tooWide = true,
spu_field_reason_tooNarrow = true, spu_field_reason_widthUnknown = true, spu_field_reason_noSowFruit = true,
}
-- s = {hasMarker, working, awaiting, startRequested, reasonKey} -> "running"|"waiting"|"error"|"stopped"
function ADFieldView.fieldStatus(s)
if not s.hasMarker then return "error" end
if s.reasonKey ~= nil and ADFieldView.ERROR_REASONS[s.reasonKey] then return "error" end
if s.awaiting or s.startRequested then return "waiting" end
if s.working then return s.reasonKey ~= nil and "waiting" or "running" end
if s.reasonKey ~= nil then return "waiting" end
return "stopped"
end
```
- [ ] **Step 4: Run all tests** — green.
- [ ] **Step 5: Commit** `git commit -am "feat(fields): ren statusfunktion til Fields-listen"`
---
### Task 7: Fields-fanen som farvet liste + "field" i vinduet
**Files:**
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.xml` (`fieldPrefab`: dot forrest, c7 = handling; kolonner forskydes)
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFieldTab.lua` (`fieldCells`, `fillFieldRows`, `refreshFieldRows`, `onClickFieldStartStop` tager id, register `EDIT_PANEL_KINDS.field`)
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFieldGuide.lua` (`getFieldButtons` uden Start/Stop når listen vises)
- Modify: `l10n/*.xml`
**Interfaces:**
- Consumes: `ADFieldView.fieldStatus` (Task 6); `openEditPanel`/`EDIT_PANEL_KINDS` (Task 3); `makeRowClickable` + `frame.lastClickX` (Task 2/4); `ADFlowView.LEVEL_COLORS` (Task 1).
- Produces: `SmartPickupFrame:fieldLevel(id) -> level`; `SmartPickupFrame:toggleField(id)`.
- [ ] **Step 1: Prefab** — `fieldPrefab` becomes (≈835 px):
```xml
<Bitmap profile="fs25_multiTextOptionContainer" id="fieldPrefab">
<Bitmap profile="spuDot" name="dot" position="20px 0px"/>
<Text profile="spuCellBold" name="c1" position="38px 0px" size="82px 40px" textMaxWidth="76px"/>
<Text profile="spuCell" name="c2" position="120px 0px" size="120px 40px" textMaxWidth="112px"/>
<Text profile="spuCell" name="c3" position="240px 0px" size="100px 40px" textMaxWidth="92px"/>
<Text profile="spuCell" name="c4" position="340px 0px" size="70px 40px" textMaxWidth="62px"/>
<Text profile="spuCell" name="c5" position="410px 0px" size="200px 40px" textMaxWidth="192px"/>
<Text profile="spuCell" name="c6" position="610px 0px" size="120px 40px" textMaxWidth="112px"/>
<Text profile="spuCellBold" name="c7" position="730px 0px" size="100px 40px" textMaxWidth="92px"/>
</Bitmap>
```
The header row (column titles) passes a 7th title `""`.
- [ ] **Step 2: Toggle + level** — refactor `onClickFieldStartStop` into `toggleField(id)` (same body, `id` parameter instead of `frame.selectedFieldId`); `onClickFieldStartStop` calls `self:toggleField(self.selectedFieldId)`. Add:
```lua
function SmartPickupFrame:fieldLevel(id)
local field, info = ADFieldJobs.getField(id), ADFields.getInfo(id)
if field == nil or info == nil then return "stopped" end
local reason = type(field.reason) == "table" and field.reason.key or nil
return ADFieldView.fieldStatus({hasMarker = info.markerId ~= nil, working = ADFieldModel.isWorking(field),
awaiting = ADFieldJobs.awaitingPlayer(id) ~= nil, startRequested = ADFieldJobs.isStartRequested ~= nil and ADFieldJobs.isStartRequested(id),
reasonKey = reason})
end
function SmartPickupFrame:fieldActionText(id)
local field = ADFieldJobs.getField(id)
if ADFieldJobs.awaitingPlayer(id) ~= nil then return self.i18n:getText("spu_field_button_takeover") end
return self.i18n:getText(field ~= nil and ADFieldModel.isWorking(field) and "spu_flowList_stop" or "spu_flowList_start")
end
```
(If `ADFieldJobs.isStartRequested` does not exist, leave the guarded call — it evaluates to false.)
- [ ] **Step 3: List rows** — in `fillFieldRows`: guide guard gets `not self.editPanelOpen and`; `fieldCells` appends `self:fieldActionText(info.id)` as 7th cell and no longer prefixes `"> "`; row click:
```lua
self:makeRowClickable(cells[1].parent, function(frame)
if frame.lastClickX ~= nil and cells[7] ~= nil and cells[7].absPosition ~= nil and frame.lastClickX >= cells[7].absPosition[1] then
frame:toggleField(fieldId)
else
frame.selectedFieldId = fieldId
frame:openEditPanel("field", fieldId)
end
end)
```
Remove the trailing `fillFieldOverview` call from `fillFieldRows` (it now lives in the panel). In `refreshFieldRows` after `setCells` color the dot and c5 by `self:fieldLevel(entry.id)` (same code as `refreshFlowListRows`; `entry.cells[1].parent:getDescendantByName("dot")`).
- [ ] **Step 4: Panel kind** —
```lua
SmartPickupFrame.EDIT_PANEL_KINDS.field = {
canDelete = false,
exists = function(frame, id) return ADFields.getInfo(id) ~= nil end,
title = function(frame, id) return string.format(frame.i18n:getText("spu_panel_fieldTitle"), id) end,
fill = function(frame, adEnv, id) frame:fillFieldOverview(adEnv, ADFields.getInfo(id)) end,
status = function(frame, adEnv, id)
return {text = frame:fieldStatusText(ADFieldJobs.getField(id), ADFields.getInfo(id), false), level = frame:fieldLevel(id)}
end,
startStopText = function(frame, adEnv, id) return frame:fieldActionText(id) end,
startStop = function(frame, id) frame:toggleField(id) end,
delete = function() end,
}
```
`fillFieldOverview`'s own heading row (`spu_field_section`) is dropped when drawn in the panel: guard it with `if not self.editPanelOpen then ... end`.
`getFieldButtons`: list view keeps Back, Opsæt mark (needs selected field → move "Opsæt mark" into the panel as a link row like flows), Flåde; Start/Stop leaves the bottom bar.
- [ ] **Step 5: l10n** — `spu_panel_fieldTitle` ("Mark F%d" / "Field F%d").
- [ ] **Step 6: Run all tests + XML check.**
- [ ] **Step 7: Commit** `git commit -am "feat(fields): Fields-fanen som farvet liste med Start/Stop pr. række; klik åbner redigeringsvinduet"`
---
### Task 8: Etape 2 build + in-game test
- [ ] **Step 1:** bump to `1.59.0.0`; build; deploy; verify version in zip; commit + push.
- [ ] **Step 2: In-game checklist:** Fields-liste med prikker/farver; [Start]/[Stop]/[Overtag] pr. række; klik åbner vinduet med markens linjer; [Ret] → Gem i vinduet; Flåde-visning uændret; Luk/ESC.
- [ ] **Step 3:** Update memory `project_fs25_adsmartpickup.md` with the new GUI structure (EditPanel mixin, rowTarget) and the Obsidian note `projekter/FS25 silo-patches + AD Smart Pickup.md` with a line about v1.58/1.59.
@@ -1,73 +0,0 @@
# Behovsstyret forsyning ("Feed the factory") — design
Dato: 2026-09-21 · Status: godkendt af bruger
## Problem
En produktion med flere input (fx TMR-blander: hø, græs, ensilage, sukkerroer) kræver i dag
én AutoDrive-traktor pr. vare, fordi AD's Pickup&Deliver kun kører én vare fra ét sted.
Varerne ligger spredt over mange lokationer på kortet.
## Løsning
Én traktor holder alle input fyldt. For hver tur vælger moden den ingrediens aflæsningsmålet
har mest brug for, finder en silo der har den, skifter AD's vare og sender traktoren derhen.
### Betjening (ingen ny GUI, ingen config-fil)
1. Aflæsningsmål = produktionens markør.
2. Multi-vælg ingredienserne i AD's vare-dropdown (>1 vare = funktionen er aktiv).
3. "Brug mapper" + "Rotér mål: pålæsning" slået til (krævet for at AD kalder `getNextPickup`).
4. Start Pickup&Deliver. AD's loop-tæller virker uændret (N læs → parkér, 0 = uendeligt).
Med præcis én valgt vare er opførslen uændret (eksisterende mappe-rotation, mindst fyldte først).
### Logik pr. tur (i `getNextPickup`-wrapperen)
1. **Ingrediensliste pr. køretøj.** Ved >1 valgt vare gemmes listen i modens egen tilstand
(`vehicle → {ingredients, narrowedTo}`). Derefter indsnævres AD's `selectedFillTypes` til den
valgte vare, fordi AD ellers selv roterer vare ved triggeren
(`ADStateModule:selectPreferredFillTypeFromFillLevels`).
Ved næste kald: er AD's valg stadig `{narrowedTo}` → brug den gemte liste. Er det noget andet →
brugeren har ændret valget: ny multi-liste erstatter, enkelt-valg sletter tilstanden.
2. **Behov.** Find `UnloadingStation` nærmest anden markør (≤ 40 m). For hver ingrediens summeres
`level`, `capacity` over `station.targetStorages`; `free = capacity - level`.
Ingredienser stationen ikke understøtter, eller med `free < MIN_FREE_LITERS`, springes over.
3. **Rangering.** `ADSupplyPlanner.scoreNeed(level, capacity, freeSpace)` → højeste score først.
4. **Kilde.** For ingredienserne i behovs-rækkefølge: blandt *alle* AD-markører (ikke kun én mappe)
findes markører med farm-tilgængelig `LoadingStation` ≤ 40 m der har varen. Aflæsningsmålets
egen placeable udelukkes som kilde. Rangering som eksisterende: eget lager før nabo-lån,
mindst fyldte først. Første ingrediens med en kilde vinder.
5. **Effekt.** `setFillType(vare)`, `selectedFillTypes = {vare}`, dirty-flag, returnér markørens
`markerIndex`. Logges som `ADSmartPickup: '<traktor>' -> <markør> (<vare>, behov …)`.
6. **Fallback.** Intet behov / ingen kilde / ingen station / fejl (`pcall`) → AutoDrives
originale `getNextPickup` (ved fejl: `Logging.warning`). Aldrig crash.
## Filer
| Fil | Ansvar |
|---|---|
| `FS25_ADSmartPickup/adSupplyPlanner.lua` (ny) | Rene funktioner: `scoreNeed`, `rankNeeds`, `resolveIngredients` (tilstandslogik). Ingen spil-API. |
| `FS25_ADSmartPickup/adSmartPickup.lua` | Spil-/AD-adaptere (station ved markør, lagerniveauer, kilde-søgning) + hook. Kilde-søgning generaliseres til at tage en markørliste. |
| `FS25_ADSmartPickup/modDesc.xml` | Ny sourceFile før `adSmartPickup.lua`, version 1.1.0.0, beskrivelse. |
| `tests/test_adSupplyPlanner.lua` (ny) | Enhedstests af de rene funktioner. |
| `tests/test_adSmartPickup.lua` | Integration gennem hook'en med mocks; eksisterende 11 tests skal forblive grønne. |
## Tests (luajit, mocks)
- mest trængende ingrediens vælges; vare og AD-valg skiftes; rigtig markør returneres
- ingrediens uden plads i målet springes over
- ingrediens uden kilde springes over → næste i rækken
- kilde i anden mappe end første markør findes
- aflæsningsmålets egen station bruges ikke som kilde
- gemt liste genbruges når AD's valg er indsnævret; brugerændring nulstiller/erstatter
- én vare valgt → eksisterende opførsel uændret
- intet behov / ingen aflæsningsstation / API-fejl → `ORIGINAL` (+ WARN ved fejl)
## Kendte begrænsninger
- AD læsser fuld vogn; er der mindre plads end ét læs, venter traktoren ved aflæsning (AD-standard).
- Ingredienslisten lever kun i hukommelsen — efter savegame-reload multi-vælges varerne igen.
- Forudsætter at målet er en placeable med aflæsningstrigger (produktionspunkt/silo/stald),
ikke en blandevogn. Bekræftes ved første in-game test via log.txt.
- Server-side logik; MP-klienter ser vareskiftet via AD's eget dirty-flag.
@@ -1,156 +0,0 @@
# Runs-UI ("Cow Feed") — design
Dato: 2026-09-22 · Status: godkendt af bruger (sektion 1–3) · Version: v1.8
## Problem
Al opsætning sker i dag pr. traktor i AutoDrives HUD: mål, multi-valg af varer (tændknap for
behovsstyring), mapper + rotation, loop-tæller. Med fire ens "Fastrac 2135" til to stalde og flere
Wait-puljer på vej (Cow/Pig/Sheep Wait) mangler overblik og central kontrol: *hvad* kører *hvor*
med *hvilke varer*, og hvor venter de.
## Løsning
Et **run** er den centrale enhed: navn, aflæsningsmål, varer, Wait-pulje, tildelte traktorer,
loop-tæller. En ESC-menu-fane ("Smart Pickup") viser og redigerer runs og starter/stopper dem.
"Start" skriver run'et ind i hver traktors AutoDrive-tilstand og kalder AD's start — alt det
eksisterende (behovsstyring, fabriks-inputs, Wait-punkter) virker uændret ovenpå, fordi det
læser præcis den tilstand. Traktorer uden run kører som i dag.
Valgt tilgang: **run-lag oven på AutoDrives tilstand** (fravalgt: egen dispatcher der kalder AD's
lav-niveau-opgaver — genopfinder P&D og knækker ved AD-opdateringer; konfig-fil uden UI — ikke det
brugeren bad om).
## 1. Datamodel og lagring
```
Run
id løbenummer (stabilt, unikt i savegame)
name "Cow Feed"
targetWayPointId AD-waypoint-id for mål-markøren (stald/fabrik); vises som markørnavn
ingredientMode "auto" | "manual"
ingredients liste af fillType-NAVNE (kun ved manual), fx {"STRAW","FORAGE"}
waitPoolGroup AD-mappenavn (fx "Cow Wait"); tom streng = alle Wait-mapper, nærmeste ledige
loops 0 = uendeligt, N = antal ture (AD's loop-tæller)
vehicleIds liste af køretøjs-referencer (se nedenfor)
Runtime (ikke gemt):
isRunning afledt: mindst én tildelt traktor har AD aktiv og run-medlemskab
runMembership vehicle -> run.id (svagt nøglet tabel)
```
- **Køretøjs-reference:** AD's `uniqueId` (streng) når den findes på køretøjet, ellers savegame-id
(`vehicle.currentSavegameId`). Navne bruges aldrig som nøgle (fire hedder "Fastrac 2135 4WS").
- **WaitPool** er afledt, ikke gemt: alle AD-mapper med ordet "Wait" i navnet (`ADSupplyPlanner.isWaitMarker`).
Omdøbning sker i AutoDrive; vi læser kun.
- **Fil:** `savegameN/ADSmartPickup_runs.xml`, samme mappe som `AutoDrive_config.xml`. Skrives i
spillets gem-hook (som AD), læses ved `loadMap`. Ukendte køretøjs-referencer droppes med loglinje;
run'et beholdes. Runs der kørte ved gem genstartes IKKE ved load (AD gendanner selv aktive køretøjer).
- **Regler:** en traktor kan kun være i ét run (tildeles den et andet, flyttes den — UI'et siger det).
Singleplayer først; multiplayer kræver events og er uden for scope.
XML-format:
```xml
<ADSmartPickupRuns version="1">
<run id="1" name="Cow Feed" target="11066" ingredientMode="manual" ingredients="STRAW FORAGE"
waitPool="Cow Wait" loops="0">
<vehicle id="..."/>
<vehicle id="..."/>
</run>
</ADSmartPickupRuns>
```
## 2. UI — ESC-fane "Smart Pickup"
Forbillede: FarmOperationsDashboard (`TabbedMenuFrameElement`, XML-layout med `SmoothList`,
`MultiTextOption`, `Button`; registreret via `g_gui:loadGui` + indsættelse i `InGameMenu.pagingElement`).
```
┌ Smart Pickup ─────────────────────────────────────────────────────────────┐
│ [Runs] [Wait-puljer] (under-faner) │
├──────────────────┬────────────────────────────────────────────────────────┤
│ RUNS │ RUN: Cow Feed ● Kører (2/2) │
│ ▶ Cow Feed 2/2 │ Mål: [Cow 1 Food ▼] (AD-markør) │
│ Cow 2 Feed 1/2 │ Varer: (•) Auto: alt målet tager / fabrikken │
│ TMR Fabrik 1/1 │ ( ) Vælg: [x] Halm [x] TMR [ ] Vand │
│ │ Wait-pulje: [Cow Wait ▼] (6 punkter)│
│ [+ Nyt run] │ Ture (loop): [0 = uendeligt ◄►] │
│ [Slet run] │ Traktorer: │
│ │ Fastrac 2135 (#1) vogn: 1.000.000 l halm+TMR ✔ │
│ │ Fastrac 2135 (#2) vogn: — INGEN VOGN ✖ │
│ │ [+ Tilføj traktor ▼] [Fjern] │
│ │ [▶ Start run] [■ Stop run] Status: 1 advarsel │
└──────────────────┴────────────────────────────────────────────────────────┘
```
- **Runs-liste (venstre):** alle runs med "kører X af Y". Valg → detaljer til højre. `+ Nyt run`
opretter "Run N" (navn redigeres via AD-lignende tekstdialog). `Slet run` spørger ja/nej.
- **Mål:** dropdown over AD-markører der ligger ved en aflæsningsstation (`getUnloadStationAtMarker`
≠ nil) — ikke alle markører.
- **Varer:** *Auto* = det eksisterende: fabrik → ønskede produktioners inputs (v1.6); stald → alle
varer stalden tager (`getHusbandryIsFillTypeSupported`); andet mål → stationens understøttede varer.
*Vælg* = afkrydsning blandt målets mulige varer.
- **Wait-pulje:** dropdown "Alle (nærmeste ledige)" + hver mappe med "Wait" i navnet (antal punkter).
- **Ture:** 0–99.
- **Traktorer:** dropdown over farmens køretøjer med `vehicle.ad` (navn + `#løbenummer`). Pr. række:
vogn-kapacitet (AD `getAllFillLevels`), hvilke af run'ets varer vognen kan bære, og ✔/✖ med tekst:
"ingen vogn", "kan ikke bære: TMR", "N m fra vejnettet", "i run X (flyttes)".
- **Start/Stop** gælder hele run'et; ✖-traktorer springes over med besked i rækken.
- **Wait-puljer-fanen:** liste over Wait-mapper: antal punkter, hvilke traktorer holder hvor lige nu
(fra `adUnloadWait`'s reservationer). Kun visning.
- Live-opdatering hvert sekund mens siden er åben. Dansk + engelsk l10n. Ingen nye tastaturgenveje.
## 3. Start/Stop, integration, fejl og test
**Start run** (pr. tildelt traktor):
1. Validér: vogn tilkoblet og kan bære ≥1 af run'ets varer (`AutoDrive.getSupportedFillTypesOfAllUnitsAlphabetically`);
≤ 30 m fra vejnettet (`ADGraphManager:getDistanceFromNetwork`); mål-markør findes. Fejl → ✖ på rækken,
traktoren springes over, run'et fortsætter med de andre.
2. Skriv AD-tilstand: `stateModule:setMode(AutoDrive.MODE_PICKUPANDDELIVER)`;
`setSecondMarkerByWayPointId(target)`; `setFirstMarkerByWayPointId(nærmeste markør med læssestation)`
(kun en gyldig start — moden vælger reel kilde pr. tur); `selectedFillTypes` = run'ets varer
(Auto: udledt liste) + `raiseDirtyFlag`; loop-tæller; per-køretøj `vehicle.ad.settings.useFolders`
= til og `rotateTargets` = kun pålæsning (`current` og `new`).
3. `vehicle:startAutoDrive()`.
4. `runMembership[vehicle] = run.id`.
**Stop run:** `vehicle:stopAutoDrive()` pr. tildelt traktor; medlemskab bevares (konfigureret, ikke kørende).
**Integration:** de eksisterende hooks læser AD-tilstanden uændret. Run'et læses to steder:
(a) `adUnloadWait`: Wait-kandidater begrænses til run'ets mappe, hvis sat; (b) `choosePickup`:
for run-traktorer tages ingredienslisten fra run'et (Auto/manual) i stedet for "≥2 valgt i dropdown",
så tændknappen ikke længere kræves. Traktorer uden run: præcis som i dag.
**Fejlhåndtering:** al UI-kode og Start/Stop i `pcall`; fejl vises i status-linjen og logges med
`ADSmartPickup:`-præfiks; aldrig crash. Manglende AD-API → fanen viser "AutoDrive-API ikke fundet",
resten af moden upåvirket.
**Test:** rene funktioner i luajit (run-model: opret/omdøb/tildel/flyt; validering; Auto-vareliste;
XML ↔ tabel; kandidat-filtrering pr. Wait-pulje). GUI og AD-skrivning verificeres in-game efter
tjekliste: opret run → tildel 2 traktorer (én uden vogn) → Start (én ✖) → Stop → gem/load → run bevaret.
**Afgrænsning (YAGNI):** singleplayer; ingen kilde-begrænsning pr. run; ingen omdøbning af
Wait-mapper; ingen auto-genstart ved load; ingen genvejstast-dialog.
## Filer (forventet)
- `FS25_ADSmartPickup/adRuns.lua` — run-model + XML (rene funktioner hvor muligt)
- `FS25_ADSmartPickup/adRunsController.lua` — Start/Stop, validering mod AD/spil-API, medlemskab
- `FS25_ADSmartPickup/gui/SmartPickupFrame.lua` + `gui/SmartPickupFrame.xml` + `gui/guiProfiles.xml`
- `FS25_ADSmartPickup/l10n/l10n_da.xml`, `l10n_en.xml`
- `tests/test_adRuns.lua`
- Tilpasninger i `adSmartPickup.lua` (ingredienser fra run) og `adUnloadWait.lua` (pulje-filter)
## Efterfølgende (ikke i v1.8): læs-bytte fra Wait
In-game 2026-09-22 21:55: begge stalde fulde af halm, mangler TMR; alle fire traktorer venter ved
Cow Wait med vognene fulde af halm → ingen kan hente TMR (deadlock).
Design: mens en traktor venter, tjekkes målets behov. Hvis målet er fuldt for vognens vare, og en
ANDEN vare i traktorens ingrediensliste mangler så meget at et fuldt læs passer (hysterese), og
vognen kan bære den: find en silo (lager-placeable med aflæsningstrigger ved en AD-markør, fri plads
≥ rest, rute til/fra) — foretræk den moden ellers henter varen fra — og omdiriger aflæsningsopgaven
dertil. AD læsser af, vognen er tom, næste tur vælger moden den manglende vare efter behov. Log:
"bytter læs: N l STRAW tilbage i <silo>, målet mangler FORAGE". Ingen silo → bliv ved Wait.
Kræver at traktoren har begge varer i sin liste (run eller multi-valg).
@@ -1,92 +0,0 @@
# Flow-tavle — design
Dato: 2026-09-23 · Status: godkendt af bruger (grundform valgt: flow-tavle) · Version: v1.13
## Problem
Opsætningen er markør-baseret: brugeren vælger AD-markører som kilder og mål, og moden gætter
bygningen ud fra markørens geometri (trigger bag næsen, 40 m, 12 m). Fejl viser sig som
"kilde-markør 10873 er ikke …" i stedet for "Kostald 1", først når run'et starter. Forsyning og
Udkørsel er to faner med hver sin lange rulleside, Oversigt er en tredje. Brugeren: "det er noget rod".
## Løsning
Én tavle med ét flow pr. række: **Vare · Fra · Til · Regel · Traktorer · Status**. Fra og Til vælges
som **bygninger**; moden finder selv markøren ved bygningens læsse-/aflæsningstrigger og viser den.
Redigering sker i en detail-blok under tavlen for det valgte flow. Faner: **Flows** · **Lagre**.
Motoren (planlægger, hooks, XML-model) er uændret: et flow ER et run; bygninger oversættes til
markører når run'et startes og planlægges.
Fravalgt: "Bygninger først" (runs implicit → svært at se hvad der kører) og "Sætnings-run"
(mindst ændring, men fanerne forbliver adskilte).
## 1. Bygninger (`adBuildings.lua`, spil-adapter)
`ADBuildings.list(adEnv, farmId)` → liste af bygninger gården kan bruge:
```
{ id, name, kind = "factory"|"silo"|"husbandry"|"objectStorage"|"sell",
placeable, loadMarker = {id, name, distance} | nil, unloadMarker = {…} | nil,
canBeSource = bool, canBeTarget = bool, canSell = bool }
```
- Kilde: fabrik, silo, stald (læssestation), objektlager. Mål (forsyning): stald, fabrik.
Salg: SellingStation der er et rigtigt salgssted (ikke fabrikkers/staldes indleveringer).
Siloer og palleskure er automatiske lagermål (som i dag) og vælges ikke.
- `id` = `placeable.uniqueId`; mangler det, `"pos:" .. afrundet x/z`.
- Markør-opslag: for hver AD-markør (Wait-markører udelukket) afstanden til bygningens
læsse-triggere (loadTriggers.triggerNode) og aflæsnings-triggere (unloadTriggers exactFillRootNode/
triggerNode/aiNode) samt objektlagerets/pallespawnerens node; nærmeste ≤ 40 m vinder.
Siloer bruger det strenge opslag (bag næsen) for læssemarkøren, som i dag.
- Én scanning pr. sekund højst (cache), da fanen spørger ved hver opdatering.
- `ADBuildings.find(adEnv, farmId, id)`, `ADBuildings.byMarker(adEnv, farmId, wayPointId)` (omvendt
opslag til migration).
## 2. Model
Run får `sourceBuildingIds` og `targetBuildingIds` (lister af bygnings-id). De eksisterende
`sourceWayPointIds`/`targetWayPointIds` bevares som **afledte** felter: controlleren udfylder dem fra
bygningerne ved Start og ved hver plan (`ADRunsController.resolveMarkers(adEnv, run)`), så
planlægger og hooks er uændrede. XML-version 3 skriver `#sourceBuildings`/`#targetBuildings`;
version 2-filer læses, og bygninger udledes fra markørerne ved første indlæsning
(`ADBuildings.byMarker`); kan en markør ikke oversættes, beholdes den som rå markør.
Forsyning: Fra = "alle siloer" (ingen valg; sources er alle markører som i dag). Til = bygninger.
Udkørsel: Fra = bygninger. Til = politik pr. vare (Lager/Salg/Salg, ellers lager) — målet vælges
automatisk pr. tur som i dag.
## 3. Fanen Flows
**Tavle** (øverst, tekstrækker, én pr. run, den valgte markeret med "▶"):
`Mælk · Kostald 1 + Kostald 2 → Mejeri (salg) · pris ≥ 90 % · Virtus · venter: 42 % af top`
- Vare: run'ets varer (forsyning auto = "behov"), Fra/Til = bygningsnavne (+ "!" hvis en bygning
mangler markør), Regel = kort tekst, Traktorer = navne, Status = én linje (som Oversigt i dag:
kører/holder/venter/begrundelse/advarsel).
- Under tavlen: Wait-puljer (som i dag) og en linje "Vælg flow ovenfor for at rette".
**Detail-blok for valgt flow** (efter tavlen; run-vælgeren `Flow:` er en MultiTextOption):
- *Retning*: Ind til bygning / Ud fra bygning (klonet Ja/Nej-række).
- *Fra* (kun Ud): afkrydsning pr. bygning der kan være kilde: `Kostald 1 (stald) — markør 'cow 1 milk' 12 m` eller `— INGEN MARKØR inden for 40 m`.
- *Til* (kun Ind): afkrydsning pr. stald/fabrik med samme markørtekst.
- *Varer*: Ind: Auto/Vælg + afkrydsning; Ud: politik pr. vare med mængde og pris-% (som i dag).
- *Regler*: Ind: ture, kør hjem under; Ud: sælg fra, lager over, saml op.
- *Vent ved*: Wait-pulje.
- *Traktorer*: som i dag (låst når vognen ikke passer).
- Knapper: Nyt flow / Omdøb / Slet / Start-Stop (som i dag). Nyt flow spørger navn og starter som "Ind".
Fanen **Lagre** uændret. Oversigt og de to gamle faner udgår.
## 4. Validering og fejl
- Bygning uden markør: rækken siger det, tavlen viser "!" ved navnet, Start afviser med
`noMarker:<bygning>` i statuslinjen ("Kostald 2 mangler en AD-markør ved mælketanken").
- Bygning der er forsvundet (solgt): udgår af listen; run'et beholder id'et og viser "(mangler)".
- Alle eksisterende afvisningsgrunde uændrede.
## 5. Tests
- `adBuildings`: kind pr. placeable, markør-opslag (nærmeste ≤ 40 m, Wait udelukket, silo strengt),
byMarker, id-fallback.
- model: v3 roundtrip, v2-migration med byMarker-oversættelse, rå markør bevares.
- controller: resolveMarkers udfylder waypoint-lister; noMarker-validering; Start med bygninger.
- GUI in-game (savegame 3 og 2).
@@ -1,201 +0,0 @@
# Udkørsel af produkter — design
Dato: 2026-09-23 · Status: godkendt af bruger (afsnit 1–4) · Version: v1.12
## Problem
Modden styrer i dag kun forsyning *ind* til stalde og fabrikker. Fabrikkernes output (sukker,
TMR, halmvarer på paller) og varer der står på lager skal køres *ud*: til en silo som
mellemlager, eller til et salgssted — men kun når prisen er god, så man ikke sælger i bund.
Uden det løber fabrikkernes udgangslager fuldt, og produktionen stopper. Paller ligger ofte i
objektlagre (fx HVF-pakkens palle-/ballelager) hvor man normalt klikker "tag ud" manuelt.
## Løsning
Et run får en **retning**: *Forsyning* (som i dag) eller *Udkørsel*. Et udkørsels-run har en
liste af **kilder** (fabrik, silo eller objektlager), en **vareliste** med politik pr. vare
(Lager / Salg / Salg, ellers lager / Fra), en **pristærskel** (% af årets toppris), en
**lagertærskel** (% fyldning) og valget **opsamlingsrunde**. Før hver tur vurderer en ren
planlægger om en tur er berettiget, vælger vare(r) + mål, og sætter AutoDrives tilstand;
AutoDrive kører, læsser (også paller via Universal Autoload) og læsser af. Ingen tur → traktoren
holder ved et Wait-punkt og prøver igen hvert 5. sekund.
Valgt tilgang: nyt modul der wrapper AutoDrives læsseopgave (samme mønster som Wait-modulet
wrapper aflæsseopgaven). Fravalgt: AutoDrives egen "automatisk aflæsningsmål" (ingen pristærskel,
ingen lager-mulighed, ingen ventelogik) og egen kørselsstyring med DriveTo-opgaver (genopfinder
AutoDrives læsse-/pallehåndtering).
## 1. Faner og datamodel
### Faner i ESC-siden
1. **Oversigt** (som i dag): alle runs med status, traktorernes aktivitet, behovsrække. Udkørsels-
runs vises med prislinje i stedet for behov: `SUGAR 412 kr (78 % af top) → venter`.
2. **Forsyning**: de nuværende runs, samme indhold, grupperet i blokke med overskrift:
*Run* (vælger + Nyt/Omdøb/Slet) · *Mål og varer* · *Ventepunkter og ture* · *Traktorer* ·
*Status* (+ Start/Stop).
3. **Udkørsel**: *Run* · *Kilder* (liste af markører, tilføj/fjern) · *Varer* (én række pr. vare:
navn, politik, aktuel pris i % af top) · *Regler* (pristærskel 50–100, standard 90;
lagertærskel 50–95, standard 80; opsamlingsrunde Ja/Nej) · *Ventepunkter* · *Traktorer* ·
*Status* (+ Start/Stop).
Run-vælger, Wait-pulje, traktorliste, knapper og statuslinje er fælles byggeklodser; hver fane
viser kun runs med sin egen `kind`.
### Kilder
Kilde = AD-markør. Kildetypen genkendes ved markøren (nærmeste inden for 40 m, som triggere i
dag):
- **Fabrik** (produktionspunkt): tilgængeligt pr. vare = udgangslager + paller af varen der
allerede står ved pallespawneren (inden for 25 m af spawner-noden).
- **Silo** (spec_silo med læssestation): tilgængeligt = lagerniveau for varen.
- **Objektlager** (spec_objectStorage): tilgængeligt = antal × fyldning pr. palle for varen
(`objectInfos[*].objects[*].palletAttributes`). Udtag sker automatisk, se afsnit 3.
Varelisten for et run = alle varer kilderne rummer eller producerer, som mindst én tildelt vogn
kan bære. Bulk-varer kræver tipvogn/tank; pallevarer kræver Universal Autoload.
### Datamodel (adRuns.lua, rene funktioner, ny store ved hver ændring)
```
Run (fælles)
id, name, waitPoolGroup, vehicleIds, kind = "supply" | "outbound"
Run (supply, uændret)
targetWayPointId, targetWayPointIds, ingredientMode, ingredients, loops, returnBelowPercent
Run (outbound)
sourceWayPointIds liste af AD-waypoint-id'er (1..n)
products liste af {fillType = NAVN, policy = "store"|"sell"|"sellElseStore"|"off"}
sellAtPercent 50..100, standard 90
storeAbovePercent 50..95, standard 80
collectRound bool, standard false
```
XML-version 2 i samme `ADSmartPickup_runs.xml`. Version 1-filer læses og får `kind="supply"`.
Ukendte varer i `products` (mod fjernet) ignoreres ved indlæsning med én log-linje.
## 2. Turvurdering og prisregel
`adOutboundPlanner.lua` er rent Lua (ingen spil-API). Input: run, vognens kapacitet (liter) og
om den har UAL, kildestatus pr. vare `{available, level, capacity}` pr. kilde, prisstatus pr.
vare `{percent, stationId, reachable}`. Output: en tur eller `nil` + begrundelse.
Pr. vare:
- **Tilgængeligt**: sum over kilderne.
- **Fuldt læs**: tilgængeligt ≥ vognens kapacitet.
- **Lager højt**: mindst én kilde har `level/capacity ≥ storeAbovePercent/100` for varen.
- **Pris-%**: bedst betalende *nåelige* salgssted der tager varen (adPrices). Toppris =
stationens aktuelle grundpris for varen × (årets højeste periodefaktor ÷ nuværende periodes
faktor). Pris-% = aktuel effektiv pris ÷ toppris. Stor efterspørgsel giver > 100 % og opfylder
altid reglen. Mangler stationens grundpris, bruges `fillType.pricePerLiter × højeste faktor ×
sværhedsgradens multiplikator` som toppris.
Beslutning, i rækkefølge:
1. **Salgskandidater**: politik ∈ {sell, sellElseStore}, pris-% ≥ sellAtPercent, tilgængeligt > 0.
Kører **uanset mængde**. Rangering: højeste pris-% først, derefter størst tilgængeligt.
2. **Lagerkandidater**: politik ∈ {store, sellElseStore} og (fuldt læs ELLER lager højt).
Rangering: højeste fyldningsgrad først, derefter størst tilgængeligt.
3. Ingen kandidat → hold ved Wait. Log én gang pr. ændret begrundelse:
`venter: SUGAR 78 % af top (kræver 90), lager 41 %`.
**Opsamlingsrunde** (`collectRound`):
- Bulk-vogn: én vare, men fra flere kilder efter tur indtil vognen er fuld (afsnit 3).
- UAL-vogn: alle kandidater med samme mål på én tur; vognens vareliste sættes til dem alle, og
AutoDrive skifter selv vare når der ikke kommer flere af den første.
**Mål**:
- Salg: det salgssted der betaler bedst for hovedvaren (første i rangeringen), tager alle rundens
varer, og kan nås begge veje.
- Lager: nærmeste silo-markør med læsse+aflæsningsstation der tager varen, fri plads ≥ hele læsset,
ikke selv kilde, fabrik eller stald, rute begge veje (samme regler som læs-byttet).
Prisen tjekkes kun ved turens start.
## 3. Kørsel, hold og pallelæsning
`adOutbound.lua` med to hooks:
**Start (controller).** Som forsyning: PickupAndDeliver, rotation "kun pålæsning", mapper til.
1. markør = første kilde, 2. markør = første kilde (pladsholder, sættes pr. tur). Turen
registreres i `activePickups`, så "stå og læs"-grebet og 30 s-sikkerhedsventilen gælder.
**Hook 1 — næste pålæsning** (det eksisterende `getNextPickup`-wrap): udkørsels-traktor →
planlæggeren spørges. Tur → vognens `selectedFillTypes` = turens varer, 2. markør = målet,
kilden returneres. Ingen tur → ledigt Wait-punkt fra run'ets pulje returneres (reservation +
rutetjek via adWaitPool), traktoren markeres "holder".
**Hook 2 — læsseopgaven** (prepend på `LoadAtDestinationTask.update`):
- *Holder ved Wait:* traktoren stoppes ved punktet; planlæggeren spørges hvert 5. sekund. Tur →
opgaven omdirigeres til kilden (`redirect`), varer + 2. markør sættes, "holder" slippes.
- *Ved objektlager:* når markøren er nået, traktoren står stille og AutoDrive har aktiveret UAL:
kald `placeable:removeAbstractObjectsFromStorage(objectInfoIndex, 1, nil)` for turens vare;
vent til lagerets antal falder OG UAL's antal læssede stiger; gentag. Stop når
`ualIsFull()`, lageret er tomt for varen, eller intet er sket i 30 s → `task:finished()`.
Ved runde med flere pallevarer tages varerne efter tur. `connection = nil` så
AutomaticBaleStorage-moddens kø ikke aktiveres.
- *Kør uanset mængde:* stiger vognens fyldning ikke i 15 s og vognen ikke er tom → `task:finished()`.
- *Runde, bulk:* når læsning stopper ved kilde A med fri plads ≥ 10 % og en anden kilde i listen
har samme vare, omdirigeres opgaven til kilde B i stedet for at afslutte. Hver kilde højst én
gang pr. tur.
**Aflæsning** er AutoDrives egen (tip i silo; tip eller UAL-aflæsning ved salgssted).
`adUnloadWait` springer udkørsels-traktorer over (run'ets `kind`). Efter aflæsning går AutoDrive
selv til næste pålæsning → hook 1 igen. Stop som i dag.
`adWaitPool.lua` udskilles fra `adUnloadWait.lua`: reservationer (markør-id → køretøj),
`hasRouteTo`/`hasRouteBothWays`, `redirect`, kandidatliste for en pulje. Begge moduler bruger den.
## 4. Fejl, advarsler og tests
**Uopnåeligt salgssted / ingen silo.** Kan det bedst betalende salgssted ikke nås, tages det
næstbedste nåelige. Kan intet nås (eller ingen silo med plads ved Lager), vises en rød
AutoDrive-notifikation på skærmen
(`AutoDriveMessageEvent.sendMessageOrNotification(vehicle, ERROR, …)`):
`ADSmartPickup: 'MT635' kan ikke nå salgssted 'Grain Elevator' for SUGAR – tjek vejnettet`.
Samme tekst i AutoDrives notifikations-historik, log.txt (WARN) og run'ets statuslinje.
Traktoren holder ved Wait og prøver igen hvert 5. sekund. Gentages først når traktor, station
eller vare ændrer sig.
**Fejlhåndtering:** spil-API-kald i pcall; fejl → hold ved Wait + WARN én gang, aldrig
AutoDrive-stop. Validering ved Start (nye grunde): `noSource` (markør uden genkendelig kilde),
`needsAutoload` (pallevare uden UAL), `noProducts` (ingen vare med politik ≠ Fra). Objektlager
der ikke spawner → 30 s-vagt afslutter læsningen med det der er.
**Tests (luajit, uden spil):**
- planlægger: prisregel, tærskler, alle fire politikker, rangering, runde bulk/UAL, uanset mængde
- priser: toppris, pris-%, stor efterspørgsel > 100 %, fallback uden stationspris
- model + XML v2 + migration fra v1
- objektlager-udtag som tilstandsmaskine mod fake-lager (fuld, tom, stall)
- kilde-genkendelse mod fake-markører (fabrik/silo/objektlager, paller ved spawner)
- controller-validering (noSource, needsAutoload, noProducts)
**In-game (savegame 3):** sukker → salgssted ved høj pris; TMR → Farma-silo; halmkurve fra
fabrik og fra objektlager → halm-salgsstedet (UAL); uopnåeligt salgssted → advarsel på skærm.
## Filer (forventet)
```
FS25_ADSmartPickup/
adRuns.lua kind, sourceWayPointIds, products, tærskler, collectRound; XML v2
adRunsStorage.lua v2 læs/skriv, v1-migration
adOutboundPlanner.lua NY, ren
adPrices.lua NY, ren del + spiladapter
adSources.lua NY, kilde-genkendelse + lagerstatus
adOutbound.lua NY, hooks, objektlager-udtag, hold, runde
adWaitPool.lua NY, udskilt fra adUnloadWait.lua
adUnloadWait.lua bruger adWaitPool; springer outbound over
adRunsController.lua start/validering for outbound
adSmartPickup.lua source() af nye filer; getNextPickup-wrap kalder adOutbound
gui/SmartPickupFrame.lua faner + fælles blokke
gui/SupplyTab.lua NY
gui/OutboundTab.lua NY
l10n/l10n_da.xml, l10n_en.xml, README.md
tests/test_adOutboundPlanner.lua, test_adPrices.lua, test_adSources.lua,
tests/test_adOutbound.lua, test_adRuns.lua (v2), test_adRunsController.lua (outbound)
```
@@ -1,151 +0,0 @@
# Markarbejde, etape 1: slåafgrøder (græs, kløver, lucerne)
Dato: 2026-09-25 · Mod: FS25_ADSmartPickup · Branch: `feature/markarbejde`
## Mål
Når en græs-, kløver- eller lucernemark er klar, sender Smart Pickup selv maskinerne ud. Slåmaskinen slår
marken via Courseplay. Ved **ensilage** samler en læssevogn skårene op og kører dem til den rigtige silo.
Ved **hø** vender en vender først græsset (og en river river det evt. sammen), og derefter samles høet op.
Alt kørsel på vej sker med AutoDrive. Alt arbejde på marken sker med Courseplay.
Succes: en mark sat til *Auto* går fra "klar" til "færdig" med græsset i en silo, uden at spilleren rører
noget, og menuen viser hele tiden, hvad der sker og hvorfor.
Ikke med i etape 1: mejetærskerafgrøder (etape 2), roer (etape 3), jordbehandling og såning, baller.
## Arbejdsdeling
| Del | Ansvar |
|---|---|
| **Smart Pickup (vores)** | Hvilken mark, hvilket trin, hvilken maskine, hvilken silo. Holder status og viser den |
| **AutoDrive** | Kørsel på vej til markens `F`-markør og til silo; overtager ved "vogn fuld" og genstarter Courseplay |
| **Courseplay** | Markgrænse, kursus, selve arbejdet på marken, "fuld"/"færdig"-signaler |
## Beslutninger (med begrundelse)
1. **Faste sæt.** Redskabet sidder fast på traktoren (eller er selvkørende, fx Krone BiG M 450). Mod'en
kobler aldrig redskaber på. *Begrundelse:* auto-påkobling var det, der aldrig blev pålideligt i AI-Bonden.
2. **Markflåde med forslag og flueben.** Mod'en foreslår sæt ud fra redskabets spec (`spec_mower`,
`spec_tedder`, `spec_windrower`, `spec_forageWagon`); kun sæt med flueben bliver sendt ud.
*Begrundelse:* mod'en må aldrig tage en maskine, spilleren selv bruger. Et sæt, spilleren sidder i
(`getIsControlled`/`getIsEntered`), springes altid over.
3. **Staging-markør pr. mark = AutoDrive-markør med navnet `F<marknummer>`** (fx `F109`), stort/småt
ligegyldigt, evt. med mellemrum (`F 109`). Mangler den, vises marken med "!" og sendes ikke ud.
4. **Auto/Manuel pr. mark.** Auto starter selv ved "klar"; Manuel venter på *Start nu*. Standard: Manuel
(intet sker uventet efter opdatering).
5. **Plan pr. mark: Ensilage eller Hø.** Standard: Ensilage. **Riv** er et valg pr. mark, standard *fra*:
læssevognene i Unrealistic Pack kan køre 40 m bredt og samler spredt hø op.
6. **Bredde.** Maskiner kører med deres *aktuelle* bredde (Unrealistic Packs `variableWorkWidth`,
`aiKeepCurrentWidth`). Courseplay kan højst 50 m → et sæt over 50 m sendes ikke ud (advarsel).
Marken husker **slåbredden**; opsamling/vending/rivning kræver bredde ≥ slåbredden.
**Revideret efter in-game test 25/9:** et nyt Courseplay-kursus til læssevognen lå forskudt i forhold til
skårene (anden start/retning/forager). Derfor genbruger vender, river og læssevogn **slåmaskinens
kursus** (`vehicle:cpCopyCourse`). Kurset gemmes pr. mark; efter genindlæsning hentes det fra
slåmaskinen (id gemt som `mowVehicle`), som Courseplay selv gemmer kurset på. Uden kendt slåkursus
laves et nyt som reserve (logges).
**Revideret igen 25/9 (smalle standardmaskiner):** vender og river kører hele marken i egen bredde med
eget kursus (intet breddekrav). Læssevognen følger skårene: uden riv slåmaskinens kursus og bredde ≥
slåbredde; med riv rivens kursus (skåret ligger midt i banen) og intet breddekrav. Riv kan vælges til
både hø og ensilage.
**Sideriver (25/9):** mens riven arbejder, måles nedlægningsområdets midte (runtime-felt
`dropWindrowWorkAreaIndex`, ellers riverens XML) i traktorens ramme minus rivens CP-forskydning =
skårets sideforskydning (gemt pr. mark). Læssevognen på rivens kursus får `toolOffsetX = egen −
skårets` (±10 m) og får sin egen værdi tilbage bagefter.
7. **Silovalg** genbruger Smart Pickups motor (`ADLoadSwap`). *Tving lager* pr. mark (som "Lager til" i
flows). **Revideret 25/9:** ensilage går først til egne **gæringssiloer** (produktion skår -> SILAGE,
fx FermentingSiloPack/GrassSilageSilo), nærmest marken, ellers nærmeste silo med plads; hø kun til
almindelige siloer (gæringssiloer laver også hø om til ensilage).
8. **Marksilo.** En silo, hvis afstand til markens polygon er ≤ 150 m, er et mellemlager. Den må gerne
vælges (korte ture). Oversigten viser "!" ved marken, hvis intet Smart Pickup-udkørselsflow har
siloen som kilde — hjemkørslen klares af de eksisterende flows, ikke af markdelen.
9. **"Klar"** = ≥ 90 % af markens prøvepunkter står i høstvinduet for markens slåafgrøde
(`FSDensityMapUtil.getFruitArea`, samme metode som Arbejdsseddel, bevist i spillet). `field.fieldState`
bruges ikke (frossen NPC-plan for spillerejede marker).
10. **Slåafgrøde** afgøres ud fra spillets konvertere: `MOWER` giver et skår, og `TEDDER` gør skåret til en
`DRY…`-type (græs, lucerne, kløver, kløver2). Rug/bønner (→ halm) er udelukket. Ensilage-varen er
skåret (`GRASS_WINDROW` …), hø-varen er tørskåret (`DRYGRASS_WINDROW` …).
11. **Én maskine, én mark.** Flere marker kan køre samtidig. Højst 2 læssevogne pr. mark.
12. **Rækkefølge.** Næste trin starter først, når det forrige er *færdigt* på hele marken (ingen vender
bag slåmaskinen). Enklere og uden kollisioner mellem Courseplay-kurser.
13. **Fejl stopper ikke alt.** Kan Courseplay ikke lave kursus, prøves igen efter 5 min; imens står marken
med grunden. Andre marker kører videre.
14. **Genindlæsning.** Markernes trin gemmes i `ADSmartPickup_fields.xml` i savegame-mappen. Efter
indlæsning genoptages trinnet: sæt, der ikke længere kører, bliver sendt ud igen.
## Opbygning
Rene moduler (ingen spil-API, testes med `luajit`):
| Fil | Ansvar |
|---|---|
| `adFieldModel.lua` | Mark-plan og -status som data: plan, tilstand, overgange (`nextStage`), bredde-regler, gem/indlæs-records |
| `adFieldFleet.lua` | Rolle ud fra redskabs-specs, forslag, valg af ledigt sæt (nærmest markøren) |
| `adFieldView.lua` | Rækker og tekster til fanen Marker (sortering, filtre, statusord) |
Spil-moduler (tynde, pcall'ede):
| Fil | Ansvar |
|---|---|
| `adFields.lua` | Ejede marker, polygon/prøvepunkter, afgrøde, klar-andel, `F`-markør, marksiloer |
| `adCpBridge.lua` | Start Courseplay på en mark (grænse → kursus → start, async med annullering), læs aktiv/færdig |
| `adFieldJobs.lua` | Tick hvert sekund: opdag klar, send sæt ud, følg ankomst → CP → færdig, næste trin, gem |
| `gui/SmartPickupFieldTab.lua` | Fanen **Marker**: tabel, detalje, markflåde |
### Forløb pr. mark
```
Ensilage: idle → ready → mowing → collecting → done
Hø: idle → ready → mowing → tedding → [raking] → collecting → done
done → idle, når marken ikke længere er slået (vokser igen)
```
Hvert arbejdstrin har undertilstande pr. sæt: `driving` (AutoDrive til `F`) → `starting` (CP laver
kursus) → `working` (CP kører) → `returning` (AutoDrive til parkering). Trinnet er færdigt, når Courseplay
meldte færdig for mindst ét sæt, og intet sæt på marken stadig arbejder.
**Slå/vende/rive:** AutoDrive `DRIVETO` til `F`-markøren. Ved stop inden for 30 m starter
`adCpBridge` Courseplay med `fieldPosition` = et prøvepunkt inde i marken. Når CP er færdig: AutoDrive
`DRIVETO` til sættets parkering (sættets AD-parkeringsmarkør, ellers bliver det stående ved `F`).
**Opsamling:** Før CP startes sættes AutoDrive til `PICKUPANDDELIVER`, første markør = `F`, anden =
silo, `setStartHelper(true)` med hjælper Courseplay. Så klarer AutoDrive og Courseplay selv
fuld → aflæs → tilbage → `restartCP`. Ved CP færdig: AutoDrive tømmer resten (`enableParkAtJobFinished`)
og parkerer.
## Fanen Marker
- Tabel: **Mark · Afgrøde · Plan · Auto/Man. · Status · Maskiner**. Kun slåafgrøde-marker som standard
(*Vis alle marker* viser resten). Klik på en række → detalje.
- Detalje: Plan (Ensilage/Hø), Auto/Manuel, Riv, Tving lager (liste som "Lager til"), **Start nu**/**Stop**,
status med grund, markør og slåbredde.
- Markflåde: foreslåede sæt med flueben, rolle og aktuel bredde; advarsel over 50 m.
- Knapper på fanen: Start nu / Stop på valgt mark (samme placering som flows).
- Statusord: *Venter · Klar · Slås · Vendes · Rives · Samles op · Færdig* + grund ("venter på
slåmaskine", "! mangler markør F109", "læssevogn for smal: 20 m < 40,8 m", "Courseplay: intet kursus").
## Fejlhåndtering
| Situation | Hvad sker |
|---|---|
| Ingen `F`-markør | "!" i tabellen, intet sendes |
| Intet ledigt sæt med rollen | Status "venter på …", prøver hvert 5 s |
| Sæt for bredt (> 50 m) eller for smalt (< slåbredde) | Sættet springes over med grund |
| AutoDrive kan ikke finde vej | Sættet frigives, status med grund, andet sæt prøves |
| CP-grænse/kursus fejler | 5 min pause for marken, grund vises |
| Spilleren sætter sig i et sæt | Sættet frigives fra marken (arbejdet fortsætter ikke automatisk) |
| Genindlæsning | Trin genoptages fra gemt fil |
## Test
- `tests/test_adFieldModel.lua`, `tests/test_adFieldFleet.lua`, `tests/test_adFieldView.lua`: alle
overgange, bredde-regler, forslag, sortering/filtre, gem/indlæs.
- In-game testprotokol (manualen + denne spec): én mark Manuel → Start nu → slå → saml op → silo, derefter
Hø, derefter Auto. Loglinjer: `ADSmartPickup: F109 …`.
## Senere udvidelser (noter)
- **Flere maskiner om samme mark, maks 5** (Courseplays multi-tool-grænse, 2026-09-25): slå/vende/rive med
op til 5 sæt på én mark samtidig via Courseplays konvoj (multi-tool), og op til 5 læssevogne. Kræver at
`ADFieldModel.maxRigs` bliver en indstilling pr. mark (1–5) og at sættene får samme CP-kursus med
hver sin position i konvojen.
@@ -1,145 +0,0 @@
# Guider til opsætning: marker og flows — design
Dato: 2026-09-26 · Status: udkast til godkendelse · Mod: FS25_ADSmartPickup (v1.30.4.0 → v1.31/v1.32)
Leveres i to etaper, der hver kan testes for sig:
- **Etape A (v1.31):** fælles guide-ramme + Fields-fanen ("Opsæt mark").
- **Etape B (v1.32):** Flows-fanen på samme ramme ("Opsæt flow"), så de to faner er ens at bruge.
## Formål
Fields-fanen er blevet en lang liste af valg, der er svær at overskue (brugerens ord: "uoverskueligt").
Målet er, at man sætter en mark op med **ét spørgsmål ad gangen**, og at alt, der kan afgøres automatisk,
aldrig bliver spurgt om. En eksisterende opsætning skal kunne rettes uden at gå hele guiden igennem.
Succes: brugeren kan sætte F22 op fra bunden på under et minut uden at scrolle, og kan rette ét valg
(fx lager) med to tryk.
## Hvad brugeren ser
### Oversigt (fanens standardvisning)
1. Marktabellen som i dag (Mark · Afgrøde · Plan · Kørsel · Status (kort) · Maskiner).
2. Når en mark er valgt: et **resumé** på én linje pr. emne, hver med sin egen række man kan klikke for at
rette det ene trin ("Ret"):
- Afgrøde: `Behold det der gror` / `-> Hvede`
- Afgrødens valg: `Ensilage, river` / `Halm hakkes`
- Klargøring: `Automatisk` / `Plov, kalk`
- Pleje: `Gødning + ukrudt`
- Maskiner: `3 faste` / `Ledige maskiner` · `1 ad gangen, 1 aflæsningsvogn`
- Lager: `Marksilo` / `Automatisk` / `F22 Silo`
- Status (hele teksten, ombrudt)
3. Knapper i bundlinjen (når en mark er valgt): **[Opsæt mark]**, **[Start nu / Stop]**, **[Markflåde]**.
Uden valgt mark: **[Markflåde]**.
4. Den lange detaljeliste og den fælles markflåde nederst forsvinder fra oversigten.
### Guide (erstatter fanens indhold, mens den er åben)
Ét trin pr. skærm: overskrift ("Trin 3 af 7 — Græs: ensilage eller hø?"), en kort forklaring, valgene som
rækker, og bundknapperne **[Næste]** (eller **[Gem]** på sidste trin), **[Tilbage]**, **[Annullér]**.
Trin der ikke er relevante for marken springes over, og tællingen følger de viste trin.
| # | Trin | Vises når | Valg | Standard |
|---|------|-----------|------|----------|
| 1 | Mark | guiden er åbnet uden valgt mark | liste over marker (nr, afgrøde, status) | — |
| 2 | Afgrøde | altid | "Behold det der gror" + afgrøder der kan sås | nuværende plan |
| 3 | Afgrødens valg | slåafgrøde (nu eller mål): ensilage/hø + river ja/nej. Høstafgrøde med halm: hakkes/samles/presses | se "Vises når" | nuværende plan |
| 4 | Klargøring | altid (efter høst / bar mark) | "Automatisk (anbefalet)" / "Vælg selv" → plov, kultivering, kalk, tromle, mulch | Automatisk |
| 5 | Pleje | afgrøde der gror | gødning ja/nej, ukrudt ja/nej | ja/ja |
| 6 | Maskiner | altid | "Faste maskiner til marken?" ja/nej; ja → liste (kun ledige + dem der allerede er faste her); antal ad gangen (1–5); aflæsningsvogne ved høst (1–5) | nej / 1 / 1 |
| 7 | Lager | høst/slåning | marksilo findes: "Brug marksiloen?" ja/nej; ellers/nej: "Automatisk (anbefalet)" / "Vælg selv" → de 8 nærmeste siloer | Automatisk |
| 8 | Oversigt | altid | resumé med [Ret] pr. linje; [Kør automatisk] (Auto) / [Gem uden at starte] (Manual) | — |
### Redigering
- **[Opsæt mark]** på en opsat mark åbner guiden med de nuværende valg udfyldt.
- **[Ret]** på en resumé-linje (oversigt eller trin 8) åbner kun det ene trin; **[Gem]** der gemmer og går
tilbage dit man kom fra.
- **[Annullér]** kasserer ændringer i guiden (intet gemmes før [Gem]/[Kør automatisk]).
### Markflåde
Egen visning (knap **[Markflåde]**): "Brug alle ledige maskiner" + listen med flueben, "fast på F22",
"i gang på F108", bredde-advarsler. Samme indhold som i dag, flyttet ud af markens detaljer.
## Hvad der ALDRIG spørges om (automatisk)
Scanning af marken og markøren, hvornår der høstes/slås/gødes/sprøjtes, påfyldning (eget lager før køb),
tømning ved forkert vare, ventetid ved regn, bredde-kontrol, valg af nærmeste maskine, ruter, gæringssilo ved
ensilage, marksilo-tømning (advarsel hvis intet flow tømmer den).
## Etape B: Flows-fanen på samme måde
Brugerens ønske: "det samme for flow opsætningen så det bliver ensrettet".
**Oversigt:** flow-tavlen som i dag (én linje pr. flow med status). Valgt flow → resumé med [Ret]-linjer
(Retning · Fra · Til/Lager · Varer · Regler · Vent ved · Traktorer · Status) og bundknapperne
**[Opsæt flow]**, **[Start/Stop]**, **[Nyt flow]**, **[Omdøb]**, **[Slet]**, **[Start alle / Stop alle]**.
Den lange detaljeliste forsvinder.
**Guiden "Opsæt flow"** — samme trin-skærm, knapper og [Ret]/[Annullér] som markguiden:
| # | Trin | Vises når | Valg | Standard |
|---|------|-----------|------|----------|
| 1 | Navn | nyt flow | tekst (FS25-tekstdialog) | "Flow N" |
| 2 | Retning | altid | Ind til bygning (forsyning) / Ud fra bygning (udkørsel) | nuværende |
| 3 | Fra / Til | altid | bygninger med markør, fast rækkefølge (navn, markør), "Vis alle" | nuværende |
| 4 | Varer | altid | forsyning: "Automatisk (det bygningen mangler)" / "Vælg selv" → varer; udkørsel: pr. vare Sælg / Lager / Begge / Fra | Automatisk / nuværende |
| 5 | Lager | udkørsel med Lager | "Automatisk (nærmeste med plads)" / "Vælg selv" → lagerbygninger | Automatisk |
| 6 | Regler | altid | udkørsel: sælg ved pris-%, lager over %, samlerunde ja/nej; forsyning: antal ture, retur fra Wait under % | nuværende / standard |
| 7 | Vent ved | altid | Wait-puljer | nuværende |
| 8 | Traktorer | altid | ledige traktorer (+ dem i flowet); traktorer i andre flows skjult med linje "N i andre flows" | nuværende |
| 9 | Oversigt | altid | resumé med [Ret]; [Start flow] / [Gem uden at starte] | — |
Automatisk (spørges aldrig): markør ved bygningen, rutetjek, bedste foder først/parallelt foder, læs-bytte,
kildevalg (mindst/fyldeste først), fordeling på flere mål, hvile af tomme kilder, traktornavne efter flowet.
## Arkitektur
- **`adGuide.lua` (ny, ren Lua, testet):** den fælles guide-motor, uafhængig af marker/flows:
trin-liste, aktuelt trin, kladde, næste/tilbage/spring (til [Ret]), "returnér til oversigt", tælling
"Trin 3 af 7" over de synlige trin. Både markguiden og flowguiden er en trin-definition oven på den.
- **`gui/SmartPickupGuide.lua` (ny):** den fælles trin-skærm i menuen (overskrift, forklaring, valgrækker,
bundknapper Næste/Tilbage/Annullér/Gem) — ét sted, så de to guider ser og opfører sig ens.
- **`adFlowGuide.lua` (ny, etape B, ren Lua, testet):** flowguidens trin, valg, standarder og resumé; gemmer
via `ADRunsController.updateRun`/`assignVehicle` som i dag.
- **`adFieldGuide.lua` (ny, ren Lua, testet):** markguidens trin (oven på `ADGuide`).
- `ADFieldGuide.open(field, info, context)` → guide-tilstand `{fieldId, draft, steps, index, returnTo}`;
`draft` er en kopi af markens plan (+ faste maskiner), så intet ændres før gem.
- `ADFieldGuide.steps(draft, info, context)` → listen af synlige trin (tabellen ovenfor).
- `ADFieldGuide.choices(step, draft, info, context)` → rækkerne for trinnet (tekstnøgler + værdier).
- `ADFieldGuide.choose(guide, key, value)` → ny guide med ændret draft (immutabelt).
- `ADFieldGuide.next/back/jump(guide, step)`; `ADFieldGuide.result(guide)` → `{plan, rigIds}` til gem.
- `ADFieldGuide.summary(field, info, context)` → resumé-linjerne `{step, key, args}`.
- `context` = det spil-afhængige: marksilo findes?, siloer i nærheden, maskiner (ledige/faste), afgrøder
der kan sås. Hentes af GUI'et, så modellen kan testes uden spillet.
- **`gui/SmartPickupFieldTab.lua`:** oversigt (tabel + resumé) og guide-visning; bygger rækker ud fra
`ADFieldGuide`, gemmer via `ADFieldJobs.setPlan` og `ADFieldFleet.dedicate`. Fil-størrelse: guide-visningen
lægges i `gui/SmartPickupFieldGuide.lua` (ny), så ingen fil passerer ~800 linjer.
- **`gui/SmartPickupFrame.lua`:** bundknapper for fanen (Opsæt mark, Start/Stop, Markflåde, Næste/Tilbage/
Annullér/Gem i guiden) og routing af klik.
- **Uændret:** `ADFieldModel` (plan-felter og lagring), `ADFieldJobs`, `ADFieldWork`, gemt XML. Guiden er et
nyt ansigt på de samme data — eksisterende opsætninger virker videre.
## Fejl og kanttilfælde
- Mark uden markør: guiden kan køres, men oversigten siger "sæt markøren F22" og [Kør automatisk] gemmer som
Manual indtil markøren findes (samme regel som i dag).
- Maskine der er fast på en anden mark vises ikke i trin 6 (som v1.30.3); linje "N maskiner er faste på
andre marker".
- Marken forsvinder (solgt/bygget over) mens guiden er åben: guiden lukkes, oversigten vises.
- Guiden er åben og menuen lukkes: kladden kasseres.
## Test
- `tests/test_adGuide.lua`: motoren — næste/tilbage over skjulte trin, spring+retur, tælling, annullér.
- `tests/test_adFlowGuide.lua` (etape B): trin pr. retning, varer auto/vælg, lager-trin kun ved Lager, resumé.
- `tests/test_adFieldGuide.lua`: synlige trin pr. marktype (slåafgrøde, høst med halm, bar mark, mål-afgrøde),
standardvalg, udfyldning fra eksisterende plan, `choose` ændrer kun kladden, `result` giver plan + rigIds,
[Ret]-spring og tilbage, "Automatisk" vs "Vælg selv" i klargøring og lager, marksilo-spørgsmålet kun når en
findes, resumé-linjerne.
- Hele suiten skal være grøn; GUI-routing verificeres in-game (testplan-punkt).
## Uden for scope
Vinmarker (egne roller), genoptagelse af Courseplay efter genstart, fanerne Factories/Silos/Barns/Pallet sheds.
@@ -1,52 +0,0 @@
# Markarbejde, etape 2–3 + multi-maskine: høst, roer, jordbehandling, såning
Dato: 2026-09-26 (nat) · Bygger på `2026-09-25-markarbejde-etape1-design.md` · Branch `feature/markarbejde-2`
Brugeren gav fuldt mandat ("byg resten, ALLE PUNKTER, du træffer selv gode valg") og tester i spillet dagen
efter. Beslutningerne herunder er truffet uden dialog og skal læses igennem.
## Mål
Alle gårdens marker kan køres af Smart Pickup: slåafgrøder (etape 1) og nu **høstafgrøder** (korn, raps,
majs, roer, kartofler … alt der høstes med mejetærsker/optager), inkl. **aflæsningsvogne**, **jordbehandling**
efter høst og **såning**. Hvert trin kan køres med **1–5 maskiner** på samme mark (Courseplays multi-tool).
## Beslutninger
1. **Høst er én pipeline for alle høstafgrøder.** Roer og korn adskiller sig kun ved hvilken høster der
kan tage afgrøden. Høsteren matches på afgrøden: skærebordets/optagerens frugttyper (`spec_cutter`)
skal omfatte markens afgrøde. Finsnittere (cutter med `fruitTypeConverters`) bruges ikke til høst.
2. **Aflæsning under høst = Courseplays combine-unloader** (`COMBINE_UNLOADER_CP`, bevist i AI-Bonden):
vognen følger høsteren, tømmer den og kører til en **station**. Stationen vælges af Smart Pickups
silovalg (nærmeste silo med plads der tager afgrøden, eller *Tving lager*); findes den ikke i
Courseplays stationsliste, bruges nærmeste station. Antal vogne pr. mark: 1–3 (plan).
3. **Høsteren sendes hjem** (AD-parkering) når Courseplay er færdig og tanken er tømt. Aflæsningsvogne
stoppes når høsten er færdig; har de rest, kører AutoDrive den til lageret (samme reserve som
læssevognen), ellers parkering.
4. **Efter høst** (valg pr. mark): *Ingenting*, *Kultivér* eller *Pløj*, og derefter evt. **Så**.
Såmaskinen sættes til planens afgrøde (standard: samme afgrøde som blev høstet). Kan afgrøden ikke
sås i den nuværende periode, venter marken med grund. Tom for frø -> sættet springes over med grund
(moden fylder ikke frø på; det gør brugeren/AutoDrive-refill).
5. **Multi-maskine (1–5 pr. trin, plan pr. mark).** Trin med eget kursus (slå, vend, riv, høst,
kultivér, pløj, så) laver ét Courseplay-kursus med `multiTools = n` på første sæt; de øvrige kopierer det,
og hver får sin bane (`laneOffset`: 2 -> −1,1 · 3 -> −1,0,1 · 4 -> −2,−1,1,2 · 5 -> −2…2). Et multi-trin
startes kun når **alle n** sæt er ledige (ellers kører én bane ikke). Læssevognen på et multi-kursus
kræver også n vogne, én pr. bane.
6. **Rolle-rækkefølge** når et køretøj har flere redskaber: høster > læssevogn > river > vender >
slåmaskine > såmaskine > kultivator > plov > aflæsningsvogn (traktor+vogn som Courseplay kan bruge som
combine-unloader).
7. **Afgrødetype pr. mark:** slåafgrøde (etape 1) eller høstafgrøde (alt andet med en høstbar frugt). Plan
*Høst* er standard for høstafgrøder. Marker uden afgrøde (pløjet/stub) kan få *Start nu* for
jordbehandling/såning alene.
8. **Stadig ikke med:** gødning/kalk/sprøjtning, baller, halm efter mejetærsker, auto-påfyldning af frø.
## Opbygning (ændringer)
| Fil | Ændring |
|---|---|
| `adFieldModel.lua` | Planer `harvest`, jordbehandling, såning, `rigs`/`unloaders`; trin `harvesting/cultivating/plowing/sowing`; hjælperoller pr. trin; `lanePositions(n)` |
| `adFieldFleet.lua` | Nye roller; `pick` tager afgrøde (høster skal kunne tage den); `pickMany` til multi-trin |
| `adCpBridge.lua` | `startUnloader`, `canHarvest`, frø-valg (`setSeedFruit`, `isPlantable`, `hasSeeds`), multi-tool kursus + lane |
| `adFields.lua` | Afgrødetype (slå/høst), sidst høstede afgrøde |
| `adFieldWork.lua` (ny) | Udsendelse og opfølgning af sæt (AD/CP) — flyttet ud af `adFieldJobs.lua` der blev for stor |
| `gui/SmartPickupFieldTab.lua` | Plan-rækker pr. afgrødetype, maskiner pr. trin, aflæsningsvogne, efter høst, så + afgrøde |
@@ -1,103 +0,0 @@
# Marksilo-flow — design
Dato: 2026-09-27 · Status: godkendt i chat, afventer review af denne spec
## Formål
Høsten lægges ofte i siloer ude ved markerne (mellemlager). I dag skal spilleren selv huske at køre varerne
hjem. Marksilo-flowet kigger jævnligt alle marksiloer igennem og sender selv op til **x** ledige vogne ud for at
hente varerne hjem til gårdens lagre.
**Succes:** står der varer i en marksilo, som gården kan tage, bliver de kørt hjem uden at spilleren gør noget,
med højst x vogne ude ad gangen, og uden at tage vogne fra markarbejde, andre flows eller spilleren.
## Brugerens valg (fra brainstorm)
| Spørgsmål | Valg |
|---|---|
| Hvor er "hjem"? | Nærmeste af de gårdlagre, spilleren vælger i flowet, der tager varen og har plads |
| Hvad er "x vogne"? | Lånte ledige vogne fra hele gården — ingen fast tilknytning — højst x ude samtidig |
| Hvilke vogne må lånes? | Samme regel og flueben som markarbejdet (`fleetMode` "all"/"ticked", `ADFieldJobs.state.fleet`) |
## Begreber
- **Marksilo:** bygning af typen silo inden for `ADFields.FIELD_SILO_DISTANCE` (150 m) af en marks polygon
(`ADFields.fieldSilos`), samlet over alle marker, uden dubletter. En bygning, der er valgt som gårdlager i
flowet, er aldrig marksilo. Det samme gælder siloer, der er kilde, mål eller lager i et flow (tilføjet efter review
2026-09-27: ellers tømte flowet gårdsiloer, som forsyningsflows bruger, fordi de ligger under 150 m fra en mark).
- **Gårdlager:** bygning spilleren vælger i flowet (samme bygningsvælger som udkørslens lagre, `storeBuildings`).
- **Lån:** en vogn, flowet har sendt på én tur. Lån ligger kun i hukommelsen.
## Flowet
Marksilo-flowet er ÉN fast opsætning pr. savegame — ikke en ny run-type. Beslutning (2026-09-27, under
planlægning): run-editoren (`SmartPickupFrame`) er bygget til præcis to typer (forsyning/udkørsel) mange steder,
og runs har faste vogne, Start/Stop pr. vogn og Wait-puljer, som intet af det passer til lånte vogne. En egen
underfane + egen gemmefil giver langt færre indgreb i eksisterende kode.
Opsætning (`ADSmartPickup_fieldsilos.xml` i savegame-mappen, gemmes sammen med runs):
- `enabled` — til/fra, standard fra
- `storeIds` — gårdlagre (bygnings-id'er fra `ADBuildings`, kun `kind == "silo"`)
- `maxVehicles` — x, 1–10, standard 2
- `minLiters` — tærskel pr. silo og vare: 5000 / 10000 / 25000 / 50000, standard 10000
## Planlægning (ren logik, nyt modul `adFieldSiloPlanner.lua`, testes med luajit)
Input: marksiloer `{id, markerId|nil, stock = {[fillType] = liters}}`, gårdlagre `{id, markerId, accepts =
{[fillType] = freeLiters}, x, z}`, aktive lån `{siloId, fillType, liters}`, ledige vogne `{id, x, z, capacity,
carries = {[fillType] = true}}`, `maxVehicles`, `minLiters`.
1. **Kvalificerede opgaver:** pr. (silo, vare) hvor silo har markør, `liters >= minLiters`, og mindst ét
gårdlager har `freeLiters >= minLiters` for varen. Resten = liters minus det aktive lån på samme (silo, vare)
allerede er på vej med; kun opgaver med rest `>= minLiters` tæller.
Siloer uden markør → status `noMarker`; varer intet gårdlager tager → status `noFarmStore`.
2. **Rækkefølge:** fuldeste rest først; lige → lavest silo-id (stabilt).
3. **Vogne:** ledige pladser = `maxVehicles - #aktive lån`. For hver opgave i rækkefølge, så længe der er
pladser: nærmeste ledige vogn (til siloen), der kan bære varen; resten reduceres med vognens kapacitet;
samme opgave får flere vogne, så længe resten stadig er `>= minLiters`.
4. **Gårdlager pr. lån:** nærmeste gårdlager (til siloen) der tager varen med `freeLiters >= min(vognens
kapacitet, rest)`; ellers det med mest plads; ellers intet (opgaven springes over denne runde).
Output: `{dispatch = {{vehicleId, siloId, siloMarkerId, fillType, storeMarkerId, liters}}, status = {...}}`.
## Udførelse (`adFieldSilos.lua`)
- **Takt:** planlæg hvert 60. s (spiltid via `dt`), kun når flowet kører. Lagertal læses som i dag
(`ADSmartPickup.getStationLevelAndCapacity` / silo-lagre).
- **Ledige vogne:** kun vogne med AD-parkeringspunkt (ellers bliver de holdende på lagerets markør); `ADFieldWork.getRigs(adEnv)` filtreret til `role == "unloader"` (traktor + vogn, som Courseplay
godkender som tømmevogn — ikke sprøjter/høstere med tank), `enabled and not busy and not controlled`, og
vogne der har aflæsbare enheder (`AutoDrive.getAllDischargeableUnits`), der understøtter varen.
`getRigs.busy` udvides med `ADFieldSilos.isLoaned(vehicle)`, så markarbejdet ikke tager en lånt vogn
(flows tager allerede kun deres egne tilknyttede vogne).
- **Afsend:** AutoDrive "Hent og aflever": første markør = silomarkør, anden = gårdlagerets markør, vare =
lånets vare, `loopCounter = 1`, parkering ved job slut slået til (`enableParkAtJobFinished`); start som
markarbejdet (`startAd` → mode:start). Log: `marksilo: '<vogn>' henter <liter> l <vare> i <silo> → <lager>`.
- **Frigivelse:** lånet slettes når AD ikke længere er aktivt for vognen (turen og parkeringen er færdig),
når spilleren sætter sig i den, eller når flowet stoppes (stop = AD stoppes på lånte vogne).
- **Vagt:** er vognen ikke kommet i gang (ingen stigning i fyldning og står stille) efter 120 s, stoppes AD,
lånet slettes, og der logges én advarsel pr. (silo, vare), så samme opgave ikke fejler i ring hvert minut
(opgaven får 10 min pause).
- **Genindlæsning:** lån gemmes ikke. Ved indlæsning er alle vogne frie for flowet; en vogn AutoDrive selv
genstarter, kører sin tur færdig som almindelig AD-tur.
## GUI
Ny underfane "Marksiloer" efter "Måned". Indstillinger: gårdlagre (eksisterende vælger),
"Maks. vogne ude" (1–10), "Mindste mængde" (5.000 / 10.000 / 25.000 / 50.000 l), Start/Stop.
Status: `x/y vogne ude · n siloer venter`, og én række pr. marksilo med indhold: silo, vare, liter, på vej,
status (`ok` / `mangler markør` / `ingen gård tager <vare>`). Tekster i `l10n_da` + `l10n_en` (`spu_fs_*`).
## Uden for scope
- Salg direkte fra marksilo (kan senere bruge udkørslens prisregel).
- Faste vogne i flowet (valg 1 i brainstorm) — fravalgt.
- Lån der overlever genindlæsning.
## Test
- `tests/test_adFieldSiloPlanner.lua`: kvalificering (tærskel, markør, gård tager varen), fuldeste først,
x-loft inkl. aktive lån, flere vogne til én stor silo, vogn der ikke kan bære varen springes over, nærmeste
gårdlager med plads, fallback til mest plads, intet lager → ingen afsendelse.
- `tests/test_adFieldSiloConfig.lua`: standarder, grænser (x 1–10, tærskel kun de fire værdier), til/fra af gårdlager, records frem og tilbage.
- In-game: silo ved en mark med 50.000 l hvede, gårdlager = Farma-silo, x = 2 → to vogne kører, afleverer,
parkerer; markarbejdet tager ikke de lånte vogne imens.
@@ -1,75 +0,0 @@
# Flow-vogne — design
Dato: 2026-09-28 · Status: godkendt i chat ("1 vognen hører til flowet", "1 automatisk", "ja alle tre", "når du har
det hele klar"). Bygger på maskinpuljen (`2026-09-28-maskinpulje-design.md`).
## Formål
Alle påhæng har nu egen AD-parkering "P X" og en markør "Connect P X". Flows (fodring, mælk, slurry …) skal ikke
længere have traktor og vogn låst sammen: et flow henter **sin egen** vogn, når det starter, og når flowet ikke har
noget job længere, køres vognen på plads og traktoren hjem, så begge bliver ledige i puljen.
## Brugerens valg
| Spørgsmål | Valg |
|---|---|
| Hvem ejer en flow-vogn? | Flowet. Puljen må kun låne den, når flowet er stoppet/slettet/uden bygning |
| Hvordan kendes flowets vogn? | Automatisk: vognen der hænger på flowets traktor huskes pr. traktor |
| Hvornår "intet job"? | Alle tre: bygning væk, flow slettet, flow stoppet |
| Mejetærskere/skærebord | Uændret — skærebord er aldrig i puljen; mejetærsker henter ikke redskaber |
## Begreber
- **Flowets vogn:** `run.wagons = {[traktorId] = vognId}`. Opdateres automatisk: hænger der et redskab bagpå en af
flowets traktorer (mens flowet ikke er stoppet), huskes det som traktorens vogn. Gemmes i `ADSmartPickup_runs.xml`
(`wagons="traktorId=vognId;…"`).
- **Stoppet flow:** `run.stopped = true`, gemt. Sættes af Stop på flowet, "Stop alle"/"Stop alt og parkér" og
"bygning væk"; ryddes af Start / "Start alle". Ikke udledt af om AD kører (en traktor ved et Wait-punkt kan holde
stille uden at flowet er stoppet).
- **Bygning væk:** en bygning i flowets kilde-/mål-/lagerliste findes ikke længere som placeable (uniqueId) i to
tjek i træk (60 s mellem tjek).
## Adfærd
1. **Husk vogn** (hvert 10. s): for hvert ikke-stoppet flow og hver traktor med et bagerste redskab → `wagons[traktor]
= redskab` (ændringer gemmes med næste gem).
2. **Start** (`startVehicle`): er traktoren solo og har den en husket vogn:
- vognen frakoblet og ikke reserveret/lånt → `ADHitch.fetch` til "Connect <vognens parkering>", derefter startes
traktoren i flowet som i dag. Fanen/loggen: "henter sin vogn".
- vognen er lånt ud / koblet på en anden → traktoren venter ("venter på sin vogn"), der prøves igen hvert 10. s.
- vognen findes ikke eller har ingen Connect-markør → startes som i dag (solo; valideringen afgør).
3. **Stop / "Stop alle" / "Stop alt og parkér"**: `stopped = true`; hver traktor stoppes og køres hjem med
`ADHitch.returnHome` (vogn af på sin parkering, traktor til sin egen). Vognen forbliver flowets.
4. **Slet flow**: som stop; derefter er vognen ikke længere flowets (flowet findes ikke).
5. **Bygning væk**: flowet stoppes som i 3, markeres i loggen og med en besked: "Flow 'X': bygning findes ikke længere
— stoppet, traktorer kørt hjem".
## Puljen og flows
- `ADPool.collect`: traktor i et flow er kun puljetraktor, når flowet er **stoppet**.
- Puljeredskab: en vogn, der er husket af et **ikke-stoppet** flow, er aldrig puljeredskab.
- `ADFieldWork.getRigs`: uændret for kørende flows; traktorer i stoppede flows må indgå.
## Komponenter
- `adFlowWagons.lua` (nyt): ren logik (serialisering af `wagons`, beslutning ved start: fetch/wait/start, "bygning
væk"-debounce, ejer-opslag) + spil-del (tick: husk vogne, ventende starter, bygningstjek; `fetchThenStart`,
`sendHome(run)`).
- `adRuns.lua`/`adRunsStorage.lua`: felterne `stopped`, `wagons` i records.
- `adRunsController.lua`: kroge i `startVehicle`, `stopRun`, `deleteRun`, `stopAllRuns`, `stopAllAndPark`,
`startRun`/`startAllRuns`/`queueStartAll` (ryd `stopped`).
- `adPool.lua`: udelukker vogne ejet af ikke-stoppede flows; traktorer i stoppede flows tæller.
- `adSmartPickup.lua`: `ADFlowWagons.tick` i update-løkken.
## Test
- `tests/test_adFlowWagons.lua`: serialisering frem/tilbage, startbeslutning (fetch/wait/start), debounce af
bygning væk, ejer-opslag (stoppet vs kørende).
- `tests/test_adRuns.lua`: records med `stopped` og `wagons`.
- In-game: savegame2 — stop et fodringsflow → vogne sættes på plads, traktorer hjem; start det → traktorerne henter
deres egne vogne og kører.
## Uden for scope
- Valg af vogn i GUI (automatisk er valgt).
- At et flow tager en anden vogn end sin egen.
@@ -1,149 +0,0 @@
# Maskinpulje — design
Dato: 2026-09-28 · Status: godkendt i chat (afsnit 1–4), afventer review af denne spec
## Formål
Plove, harver, kultivatorer, såmaskiner, tømmevogne m.m. bruges kun en gang imellem. I dag skal hvert af dem have
en traktor koblet fast på hele året, fordi markarbejdet kun kan sende en traktor, der allerede har det rigtige
redskab bag sig. Maskinpuljen gør traktorer og redskaber til fælles puljer: et job venter i kø, til en traktor er
ledig; traktoren henter det redskab, jobbet kræver, udfører arbejdet, parkerer redskabet på dets plads og kører
selv hjem til sin egen parkering.
**Succes:** med fx 3 ledige traktorer, 2 plove og 1 kultivator bliver markernes arbejde udført uden at nogen
traktor står låst til et redskab — færre køretøjer at købe og vedligeholde. Flows, der kører hele tiden (fodring,
slurry, mælk …), påvirkes ikke.
## Brugerens valg (fra brainstorm)
| Spørgsmål | Valg |
|---|---|
| Hvordan findes redskabets Connect-markør? | Navneregel: AD-parkering "X" ↔ markør "Connect X" |
| Hvem er i puljen? | Automatisk ud fra opsætningen — ingen afkrydsning |
| Hvilken traktor henter? | Nærmeste ledige med nok hk |
| Omfang | Markarbejde + tømmevogne ved høst + marksilo-flowet. Flows er undtaget |
| Tømmevogne ved høst | Også fra puljen, men dedikeret til marken, til høsten dér er færdig |
| Arkitektur | Udvid markarbejdets eget maskinvalg (ikke en separat puljemanager) |
## Begreber
- **Puljeredskab:** et redskab eller en vogn, der holder **frakoblet**, har en AD-parkering (`advd.parkDestination`
→ markør "X"), og hvor der findes en markør "Connect X" (`ADHitch.parseMarker` = attach, navn = X; store/små
bogstaver ligegyldigt). Uden Connect-markør er redskabet ikke i puljen — det er spillerens måde at holde det
udenfor.
- **Puljevogn:** puljeredskab med rollen `unloader` (vogn med tip/snegl, `spec_trailer` + `spec_dischargeable`).
- **Puljetraktor:** motoriseret køretøj uden noget koblet bag på (`ADHitch.rearImplements` tom), ikke i et flow
(`ADRunsController.getRunForVehicle == nil`), ikke optaget (samme `busy`-regel som `getRigs`), ikke styret af
spilleren, med egen AD-parkering (`vehicle.advd.parkDestination >= 1`) og flueben i markflåden
(`ADFieldFleet.isEnabled`).
- **Færdig kombination:** det markarbejdet bruger i dag — traktor med redskabet allerede koblet på (`getRigs`).
- **Puljekombination:** {traktor, redskab, Connect-markør-id, redskabets parkering-id, traktorens parkering-id}.
AD-parkering: hver maskine har sin egen `advd.parkDestination`; AD bruger det valgte redskabs, når et er koblet på
(`AutoDriveVehicleData.lua:88-95`). Traktorens egen parkering læses derfor altid på traktoren selv.
## Komponenter
### `adPool.lua` (nyt, ren logik + tynd spil-adapter)
Ren logik (testes med luajit):
- `ADPool.roleOf(specs)` — rolle for et frakoblet redskab ud fra dets egne specs via `ADFieldFleet.ROLE_SPECS`;
vogn med `spec_trailer` + `spec_dischargeable` og intet andet → `unloader`.
- `ADPool.isPoolImplement(item)` — `item = {attached, parkMarkerName, connectMarkerId}` → frakoblet, med parkering
og Connect-markør.
- `ADPool.powerOk(tractorPower, neededPower)` — true når et af tallene mangler (nil/0), ellers `tractorPower >=
neededPower`.
- `ADPool.pairFor(role, fieldX, fieldZ, implements, tractors, reserved, accept)` — redskab med rollen nærmest marken
(ikke reserveret, `accept(implement)` sand), derefter nærmeste ledige traktor til redskabet med `powerOk`.
Returnerer `pair` eller `nil, why, args` (`noPoolImplement` / `noPoolTractor` / `tooWeak` med hk-tal).
- `ADPool.queueOrder(fields)` — markers ventende rækkefølge: `waitingSince` stigende, lige → lavest mark-id.
- `ADPool.isReturnDue(rearImplements, poolParkIds)` — traktor har et puljeredskab bag sig (dets parkering har en
Connect-markør) → hjemtur skal køres.
Spil-adapter:
- `ADPool.collect(adEnv)` → `implements, tractors` fra `g_currentMission.vehicleSystem.vehicles` (gårdens), med
position, rolle, specs til `acceptFor`, hk og parkering/Connect-id.
- hk: `g_storeManager:getItemByXMLFilename(vehicle.configFileName).specs` → traktor `power`, redskab
`neededPower`; mangler de, bruges nil (→ tilladt).
- Reservationer: `ADPool.reserve(implementId, tractorId, owner)` / `release(owner)`; i hukommelsen, ryddes ved
genindlæsning (se Genindlæsning).
- Karantæne: redskab, der ikke kunne kobles på, springes over i 10 min (`ADPool.QUARANTINE_MS = 600000`).
### `adHitch.lua` (findes, v1.47.4 — udvides let)
Udfører kørslerne: `fetch` (kør til Connect + kobl på), `route` (trin-tur, afkobling ved redskabets parkering).
Ny: `ADHitch.returnHome(adEnv, tractor, onDone)` = `route({redskabets parkering, traktorens parkering})`; ved
parkeringen kobles puljeredskabet af (samme regel som i dag i ruter).
### Markarbejdet (`adFieldWork.lua`, `adFieldJobs.lua`)
- `dispatch`: som i dag først færdige kombinationer. Returnerer `pick`/`pickMany` intet (`noRig`/`allBusy`/
`cannotDo`), forsøges `ADPool.pairFor` for de manglende. Puljekombinationens assignment får `phase = "fetching"`,
`pool = {implementId, connectId, implementParkId, tractorParkId}`, nøgle = traktorens id.
- `follow`: fase `fetching` → når `ADHitch.fetch` melder ok: bredde tjekkes med redskabet på (`checkWidth`; for
bred → hjemtur + årsag `tooWide`), derefter samme vej som i dag (`startAd` til markens markør / påfyldning,
`phase = "driving"`). Fejl → hjemtur, karantæne, markens `retryAt` som ved `noRig`.
- `finishAssignment`: har traktoren et puljeredskab bag sig (`ADPool.isReturnDue`) → `ADHitch.returnHome` i
stedet for den almindelige parkeringstur; fase `returning`; assignment slettes, når hjemturen er færdig (eller
fejler — så med advarsel).
- Kø: `tick` gennemgår ventende marker i `ADPool.queueOrder`. `waitingSince` sættes, når en mark første gang ikke
fik en maskine, og nulstilles ved udsendelse. `DISPATCH_GAP_MS` (10 s) beholdes.
- Fanen viser faserne "henter X" og "sætter X af" og årsagerne `noPoolTractor`/`tooWeak`.
### Tømning ved høst
- Helper-dispatch (`role = "unloader"`, `helper = true`) bruger samme fallback: puljevogn + puljetraktor.
- Efter `fetch`: `startUnloader` som i dag. Assignmentet bliver hos marken, til høststadiet er slut (som i dag);
først da `returnHome`. Ingen frigivelse mellem læs.
### Marksilo-flowet (`adFieldSilos.lua`, `adFieldSiloPlanner.lua`)
- `collectVehicles` tilføjer puljekombinationer (puljevogn der kan bære varen + nærmeste puljetraktor) som
kandidater med `pool = {...}`; planneren behandler dem som vogne (position = vognens).
- `withoutHarvestReserve` tæller puljevogne med, så høsten går først.
- Lån med `pool`: `ADHitch.fetch` → AD "Hent og aflever" som i dag (`enableParkAtJobFinished`) → når AD er
inaktiv: `returnHome` → lånet slettes, når traktoren er hjemme. Vagten (120 s uden fremgang) starter efter
koblingen.
## Genindlæsning
Reservationer og assignments gemmes ikke (som i dag). Efter indlæsning ser en traktor med puljeredskab ud som en
færdig kombination; når markarbejdet eller marksilo-flowet afslutter et job med den, køres `returnHome`. Reglen
afhænger kun af, hvad der hænger bag traktoren, ikke af gemt tilstand. En traktor, som ingen af vores jobs bruger,
køres aldrig hjem af sig selv (spilleren kan have koblet et puljeredskab på for at køre selv).
## Fejl
| Situation | Handling |
|---|---|
| Kobling fejler (`noAttachable`, `stoppedAway`) | Traktor hjem, redskab i karantæne 10 min, marken prøver igen (andet redskab eller venter) |
| Redskab for bredt når koblet på | Hjemtur, årsag `tooWide` i fanen |
| Spilleren sætter sig i traktoren | Assignment afbrydes som i dag; reservationer frigives |
| Ingen traktor stærk nok | Årsag `tooWeak` med hk-tal: "ingen traktor med ≥180 hk ledig til 'Plov 1'" |
| Hjemtur fejler | Advarsel i loggen; traktor og redskab frigives (spilleren må ordne det) |
## Undtaget / uden for scope
- Flows (runs) — deres køretøjer er aldrig i puljen (uændret regel i `getRigs`).
- Vogne uden Connect-markør — dedikerede som i dag.
- Genbrug af et redskab direkte til næste mark uden at parkere (senere optimering).
- Flere redskaber pr. traktor (front + bag).
- Egen GUI-side for puljen.
## Etaper
1. **v1.48.0.0 — markarbejde med pulje:** `adPool.lua`, `returnHome`, dispatch-fallback, fase fetching/returning,
kø-rækkefølge, fane-tekster. In-game: savegame4, mark der skal harves/pløjes, redskab med parkering + Connect,
solo-traktor henter → arbejder → parkerer → hjem.
2. **v1.49.0.0 — tømmevogn ved høst fra puljen.**
3. **v1.50.0.0 — marksilo med puljevogne** (inkl. høst-reserve over puljevogne).
## Test
- `tests/test_adPool.lua`: puljeredskab ja/nej (koblet på, uden parkering, uden Connect), `roleOf` inkl.
`unloader`, `powerOk` (nok/for lidt/manglende data), `pairFor` (redskab nærmest marken, traktor nærmest redskabet,
reserveret og karantæne springes over, `tooWeak`/`noPoolTractor`/`noPoolImplement`), `queueOrder`,
`isReturnDue`.
- `tests/test_adHitch.lua`: `returnHome`-rutens trin.
- `tests/test_adFieldJobs.lua`: færdig kombination foretrækkes; fallback til pulje; kø længst ventende først.
- `tests/test_adFieldSiloPlanner.lua` (etape 3): puljekandidater og høst-reserve med puljevogne.
@@ -1,131 +0,0 @@
# Flows og Fields: farvet liste + redigeringsvindue
Dato: 2026-09-29 · Status: godkendt i chat ("men kør"), afventer review af denne spec
## Formål
Flows- og Fields-fanen er i dag én lang rullende side: listen over flows/marker efterfulgt af den valgte
posts redigeringslinjer. Det er svært at overskue (brugerens screenshot 29/9 17:15). Målet:
- Fanen viser **kun listen**, med farvet status og **Start/Stop pr. række**.
- **Klik på en række** åbner et **redigeringsvindue** (popup) oven på listen med de nuværende linjer.
Succes = man kan se med ét blik hvad der kører/venter/fejler, starte/stoppe en post uden at vælge den først,
og redigere i et vindue der lukkes med Luk/ESC — uden at noget af flow-/mark-logikken ændres.
## Ikke-mål
- Ingen ændring af flow-/mark-logik, guiderne ("Nyt flow", "Opsæt mark") eller deres trin.
- Oversigt-tavlen og Fields' flåde-visning er uændrede.
- Ingen ny FS25-GUI-skærm/dialog (fravalgt: al rækkekode skulle genbygges; fokus/ESC oven på ESC-menuen er
skrøbeligt). "Popup" er et panel inde i samme frame.
## Brugeroplevelse
### Flows-fanen (liste)
Én række pr. flow: `● navn · varer · kørende/i alt · statustekst · [Start]/[Stop]`.
| Niveau | Prik/statusfarve | Hvornår |
|---|---|---|
| running | grøn | mindst én traktor kører og ingen er blokeret |
| waiting | gul | startet, men venter: ingen varer at hente, venter på vogn, traktor optaget, holder ved Wait |
| error | rød | kan ikke køre: sidste start afvist (fx ingen vogn), advarsel (`!!`) |
| stopped | grå | stoppet / aldrig startet |
- `[Start]/[Stop]` bruger den eksisterende `ADRunsController.startRun/stopRun` (samme regler som knaplinjen, inkl.
sprunget-over-besked i højre side).
- Klik på rækken (ikke knappen) åbner redigeringsvinduet for flowet.
- Knaplinjen (i dag Tilbage, Start/Stop, Opsæt flow, Nyt flow, Slet) bliver: Tilbage, Nyt flow. Start/Stop sidder på
rækkerne; "Opsæt flow" og "Slet" flyttes ind i vinduet.
### Fields-fanen (liste)
Nuværende kolonner (Mark, Afgrøde, Plan, Kørsel, Status, Traktorer) + `●` forrest og `[Start]/[Stop]` bagerst.
| Niveau | Hvornår |
|---|---|
| running (grøn) | marken har aktive opgaver der arbejder |
| waiting (gul) | startet men venter: på maskine, vejr, påfyldning, "startes når en maskine er ledig" |
| error (rød) | mangler markør, arbejdsbredde for bred/ukendt, ingen egnet maskine |
| stopped (grå) | ingen plan / stoppet |
- `[Start]/[Stop]` genbruger `onClickFieldStartStop` (inkl. "Overtag høster").
- Klik på rækken åbner vinduet med markens resumé og [Ret]-linjer.
### Redigeringsvinduet (fælles)
- Mørk halvgennemsigtig baggrund over listen; centreret panel (~70 % bredde, ~80 % højde).
- Top: titel ("Rediger: Cow Food" / "Mark F12"), statuslinje i niveau-farve.
- Midte: egen rullende liste med præcis det indhold, der i dag vises under listen:
flow → `fillFlowSummary`-linjerne; mark → `fillFieldOverview`-linjerne. [Edit]/[Ret] åbner trinnet
(guiden i enkelt-trin-tilstand) **inde i vinduet** med Gem/Tilbage som i dag.
- Bund: `Luk` · `Start/Stop` · `Slet` (kun flow). ESC/Tilbage lukker vinduet (ikke menuen); står et trin åbent,
går ESC først tilbage til resuméet.
- Mens vinduet er åbent, opdateres statuslinjen og listen bagved hvert sekund som i dag.
## Arkitektur
### Rene funktioner (testbare)
- `ADFlowView.flowStatus(s)` → `{level, text}` hvor `s = {running, total, canStop, lastStartSkipped, warning,
waitReason, holdReason, stopped}`. Afgør niveau efter tabellen ovenfor.
- `ADFieldView.fieldStatus(s)` → `{level, text}` hvor `s = {working, awaiting, starting, missingMarker,
widthProblem, noMachine, hasPlan}`.
- `ADFlowView.LEVEL_COLORS` (grøn/gul/rød/grå) — flyttes/udleveres fra `SmartPickupStorageView`s lokale tabel,
som også eksporterer `setTextColor` til genbrug.
### GUI
- **XML** (`SmartPickupFrame.xml`): efter `boxLayout` et `GuiElement id="editOverlay"` (skjult) med
`Bitmap id="editBackdrop"` (fuld flade, sort ~60 %), `Bitmap id="editPanel"` (centreret, baggrund som
fs25-paneler) med `Text id="editTitle"`, `Text id="editStatus"`, `ScrollingLayout id="editLayout"` (+ slider) og
en knaplinje med tre `Button`s (`editClose`, `editStartStop`, `editDelete`).
- **Rækkeprefabs**: `flowPrefab` og `fieldPrefab` får en `Bitmap name="dot"` (profil `spuDot`) forrest og en
`Button name="rowAction"` bagerst. Kolonnebredder justeres så rækken stadig er ≈835 px.
- **Mål-layout for rækker**: `addRow`/`addTextRow`/`addCellRow`/`addLinkRow`/`addGuideRow` kloner i dag altid ind i
`self.boxLayout`. Der indføres `self.rowTarget` (standard `boxLayout`); vinduet sætter det til `editLayout`
mens det fyldes. `clearRows` holder klonerne pr. layout, så listen og vinduet kan genopbygges hver for sig.
- **Ny fil** `gui/SmartPickupEditPanel.lua` (mixin på `SmartPickupFrame`, `source()`s som de andre):
`openEditPanel(kind, id)`, `closeEditPanel()`, `fillEditPanel(adEnv)`, `refreshEditPanel(adEnv)`,
knap-callbacks. `kind ∈ {"flow","field"}`.
- **Input**: `backButtonInfo`-callback og ESC tjekker først `self.editPanelOpen` (luk trin → luk vindue →
normal Tilbage). Klik på backdrop gør intet (blokerer klik til listen bagved via `mouseEvent` der returnerer
"brugt").
- **Guide i vinduet**: `openFlowStep`/`openFieldStep` sætter `returnTo = "panel"`, så Gem/Tilbage vender tilbage til
vinduets resumé i stedet for at lukke guiden til fanen.
### Dataflow
Klik på række → `openEditPanel(kind, id)` → `selectedRunId`/`selectedFieldId` sættes (eksisterende felter) →
`requestRebuild` → `rebuild` tegner listen i `boxLayout` og, når vinduet er åbent, vinduets indhold i
`editLayout`. Start/Stop i række eller vindue kalder de eksisterende controller-funktioner og `requestRebuild`.
## Fejlhåndtering
- Alt nyt GUI-arbejde kører i `self:safe(...)` som resten af frame'n; fejl vises i statuslinjen og logges.
- Forsvinder flowet/marken mens vinduet er åbent (slettet), lukkes vinduet og statuslinjen siger hvorfor.
- `rowAction`-knappen er deaktiveret når handlingen ikke er mulig (fx mark uden markør → ingen Start).
## Test
- luajit: `tests/test_flowStatus.lua` og `tests/test_fieldStatus.lua` for alle niveauer i tabellerne +
grænsetilfælde (0 traktorer, stoppet med advarsel, venter + afvist).
- Eksisterende tests skal forblive grønne (`test_noStrayGlobals` dækker den nye fil).
- In-game (brugeren): liste med farver, Start/Stop pr. række, åbn/luk vindue med klik/Luk/ESC, [Edit] → Gem inde i
vinduet, Slet flow fra vinduet, klik på listen bag vinduet gør intet, mark "Overtag høster" virker fra rækken.
## Levering
To etaper, hver sin version og in-game test:
1. **Flows**: rene statusfunktioner + fælles vindue + Flows-liste.
2. **Fields**: mark-status + Fields-liste genbruger vinduet.
## Risici
- **Klik gennem backdrop** til listen: afbødes ved at backdrop'en fanger `mouseEvent`; fallback: skjul listens rækker
(`setVisible(false)`) mens vinduet er åbent.
- **Musehjul/rulning** i to ScrollingLayouts: hver får sin egen slider; FS25 ruller det layout musen er over.
- **Knapper i rækkeprefabs** (Button i en klonet Bitmap) er ikke brugt før i modden: onClick sættes efter
`clone` pr. række (som `makeRowClickable`), ikke via XML-callback.
-254
View File
@@ -1,254 +0,0 @@
-- Kør: luajit tests/test_adBuildings.lua (fra repo-roden)
-- Bygninger gården kan bruge + automatisk markør ved læsse-/aflæsningstrigger (≤ 40 m, Wait udelukket,
-- siloer strengt), omvendt opslag markør -> bygning, id-fallback, cache.
local SUGAR, FORAGE, MILK, BASKET = 5, 6, 12, 7
local DESC = {[SUGAR] = {name = "SUGAR", title = "Sukker", isPalletType = true}, [FORAGE] = {name = "FORAGE", title = "TMR"}, [MILK] = {name = "MILK", title = "Mælk"}, [BASKET] = {name = "STRAW_BASKET", title = "Kurve", palletFilename = "x"}}
g_fillTypeManager = {getFillTypeByIndex = function(_, i) return DESC[i] end, getFillTypeNameByIndex = function(_, i) return DESC[i] and DESC[i].name end, getFillTypeTitleByIndex = function(_, i) return DESC[i] and DESC[i].title end}
Logging = {info = function() end, warning = function() end}
getWorldTranslation = function(node) return node.x, 0, node.z end
SellingStation = {}
dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")
dofile("FS25_ADSmartPickup/adPrices.lua")
dofile("FS25_ADSmartPickup/adBuildings.lua")
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
local markers, wayPoints = {}, {}
local function addMarker(id, name, group, x, z)
wayPoints[id] = {id = id, x = x, z = z}
table.insert(markers, {id = id, markerIndex = #markers + 1, name = name, group = group})
end
local loadStations, unloadStations = {}, {}
local adEnv = {ADGraphManager = {
getWayPointById = function(_, id) return wayPoints[id] end,
getMapMarkers = function() return markers end,
getMapMarkerByWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id then return m end end end,
}}
local strictLoadAt = {}
ADSmartPickup = {getLoadStationAtWayPointId = function(_, id) return strictLoadAt[id] end}
g_currentMission = {
placeableSystem = {placeables = {}},
storageSystem = {getLoadingStations = function() return loadStations end, getUnloadingStations = function() return unloadStations end},
accessHandler = {canFarmAccess = function() return true end},
getFarmId = function() return 1 end,
}
local function placeable(uid, name, x, z, extra)
local p = {uniqueId = uid, rootNode = {x = x, z = z}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return name end}
for k, v in pairs(extra or {}) do p[k] = v end
table.insert(g_currentMission.placeableSystem.placeables, p)
return p
end
-- verden: kostald med mælketank (load) og foder (unload), sukkerfabrik (unload + pallespawner), to siloer i række, objektlager, salgssted
local barn = placeable("barn1", "Kostald 1", 0, 0, {getHusbandryFillLevel = function() return 0 end})
-- som FS25_LargerAnimalBarns' cowBarnBig: ÉN læssestation med TO læssepunkter (gylle + mælk)
local milkStation = {owningPlaceable = barn, loadTriggers = {{triggerNode = {x = 20, z = 0}}, {triggerNode = {x = 0, z = 30}}}}
local barnUnload = {owningPlaceable = barn, unloadTriggers = {{exactFillRootNode = {x = -20, z = 0}}}}
table.insert(loadStations, milkStation); table.insert(unloadStations, barnUnload)
local mill = placeable("mill", "Sukkerfabrik", 500, 0, {spec_productionPoint = {productionPoint = {productions = {}, storage = {}, palletSpawner = {spawnPlaces = {{startNode = {x = 520, z = 0}}}}, unloadingStation = nil}}})
local millUnload = {owningPlaceable = mill, unloadTriggers = {{exactFillRootNode = {x = 480, z = 0}}}, isa = function() return true end, isSellingPoint = true}
table.insert(unloadStations, millUnload)
local silo1 = placeable("silo1", "Silo 1", 1000, 0, {spec_silo = {}})
local silo2 = placeable("silo2", "Silo 2", 1015, 0, {spec_silo = {}})
local silo1Station = {owningPlaceable = silo1, loadTriggers = {{triggerNode = {x = 1000, z = 5}}}}
local silo2Station = {owningPlaceable = silo2, loadTriggers = {{triggerNode = {x = 1015, z = 5}}}}
table.insert(loadStations, silo1Station); table.insert(loadStations, silo2Station)
table.insert(unloadStations, {owningPlaceable = silo1, unloadTriggers = {{exactFillRootNode = {x = 1000, z = 5}}}})
table.insert(unloadStations, {owningPlaceable = silo2, unloadTriggers = {{exactFillRootNode = {x = 1015, z = 5}}}})
local shed = placeable("shed", "Palleskur", 2000, 0, {spec_objectStorage = {objectInfos = {}, capacity = 100}})
local sellPoint = placeable("sell", "Mejeri", 3000, 0, {})
local sellStation = {owningPlaceable = sellPoint, unloadTriggers = {{exactFillRootNode = {x = 3000, z = 10}}}, isa = function() return true end, isSellingPoint = true}
table.insert(unloadStations, sellStation)
local other = placeable("otherfarm", "Naboens", 4000, 0, {spec_silo = {}, ownerFarmId = 2, getOwnerFarmId = function() return 2 end})
addMarker(1, "cow 1 milk", "Milk", 25, 0) -- 5 m fra mælketriggeren
addMarker(2, "Cow 1 food", "Cow food", -30, 0) -- 10 m fra fodertriggeren
addMarker(3, "cow wait 1", "cow wait", 22, 0) -- Wait: må ALDRIG vælges (tættere end mælkemarkøren)
addMarker(11, "Cow 1 Slurry", "Slurry", 0, 33) -- 3 m fra gylletriggeren
addMarker(4, "Suger Load", "Pickup", 525, 0) -- 5 m fra pallespawneren
addMarker(5, "sugar offload", "production", 470, 0)
addMarker(6, "silo 1", "silo", 1000, 12) -- strengt: silo1
addMarker(7, "silo 2", "silo", 1015, 12) -- strengt: silo2 (7 m fra silo1's trigger-node på x-aksen? nej: 15 m)
strictLoadAt[6] = silo1Station; strictLoadAt[7] = silo2Station
addMarker(8, "Storage pickup", "Pickup", 2010, 0)
addMarker(9, "Mejeri", "selling", 3000, 20)
addMarker(10, "Langt væk", "All", 9000, 9000)
-- B1: liste, kind og markører
local list = ADBuildings.list(adEnv, 1)
local byId = {}
for _, b in ipairs(list) do byId[b.id] = b end
check("B1 antal (naboens silo udeladt)", #list, 6)
check("B1 stald", byId.barn1.kind, "husbandry")
check("B1 stald læssemarkør = mælk (ikke Wait, som er tættere)", byId.barn1.loadMarker.id, 1)
-- én markør pr. læssestation (mælketank OG gyllebeholder), sorteret på markørnavn
check("B1 stald har to læssemarkører", #byId.barn1.loadMarkers, 2)
check("B1 første = cow 1 milk (navnesorteret, uanset store/små)", byId.barn1.loadMarkers[1].id, 1)
check("B1 anden = Cow 1 Slurry", byId.barn1.loadMarkers[2].name, "Cow 1 Slurry")
check("B1 gyllemarkør -> stald via byMarker", ADBuildings.byMarker(adEnv, 1, 11, "load").id, "barn1")
check("B1 stald aflæsningsmarkør = foder", byId.barn1.unloadMarker.id, 2)
check("B1 stald afstand", byId.barn1.loadMarker.distance, 5)
check("B1 fabrik læssemarkør via pallespawner", byId.mill.loadMarker.id, 4)
check("B1 fabrik aflæsningsmarkør", byId.mill.unloadMarker.id, 5)
check("B1 fabrik er ikke salgssted", byId.mill.canSell, false)
check("B1 objektlager markør via rootNode", byId.shed.loadMarker.id, 8)
check("B1 salgssted", byId.sell.kind, "sell")
check("B1 salgssted markør", byId.sell.unloadMarker.id, 9)
check("B1 salgssted canSell", byId.sell.canSell, true)
check("B1 stald kan være kilde og mål", byId.barn1.canBeSource and byId.barn1.canBeTarget, true)
check("B1 silo kilde, ikke mål", byId.silo1.canBeSource and not byId.silo1.canBeTarget, true)
-- B2: siloer i række: strengt opslag giver hver sin markør
check("B2 silo 1 markør", byId.silo1.loadMarker.id, 6)
check("B2 silo 2 markør", byId.silo2.loadMarker.id, 7)
strictLoadAt[7] = nil
ADBuildings.invalidate()
check("B2 silo uden strengt match: ingen læssemarkør", ADBuildings.find(adEnv, 1, "silo2").loadMarker, nil)
strictLoadAt[7] = silo2Station
ADBuildings.invalidate()
-- B3: byMarker (omvendt opslag)
check("B3 mælkemarkør -> stald (load)", ADBuildings.byMarker(adEnv, 1, 1, "load").id, "barn1")
check("B3 fodermarkør -> stald (unload)", ADBuildings.byMarker(adEnv, 1, 2, "unload").id, "barn1")
check("B3 fodermarkør er ikke load", ADBuildings.byMarker(adEnv, 1, 2, "load"), nil)
check("B3 fjern markør -> nil", ADBuildings.byMarker(adEnv, 1, 10, "load"), nil)
-- B4: id-fallback uden uniqueId
check("B4 id fra position", ADBuildings.getId({rootNode = {x = 12.6, z = -3.2}}), "pos:13:-3")
check("B4 id fra uniqueId", ADBuildings.getId(barn), "barn1")
-- B5: cache: samme tabel inden for CACHE_MS, ny efter invalidate
local first = ADBuildings.list(adEnv, 1)
check("B5 cachet", ADBuildings.list(adEnv, 1) == first, true)
ADBuildings.invalidate()
check("B5 ny efter invalidate", ADBuildings.list(adEnv, 1) ~= first, true)
-- B6: sortering efter NAVN (naturlig talrækkefølge), dernæst første markørnavn — ikke efter slags
local order = {}
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do table.insert(order, b.name) end
check("B6 rækkefølge", table.concat(order, ","), "Kostald 1,Mejeri,Palleskur,Silo 1,Silo 2,Sukkerfabrik")
check("B6 naturlig sortering", ADBuildings.naturalLess("Silo 9", "Silo 10"), true)
check("B6 naturlig sortering, tal før tekst-lighed", ADBuildings.naturalLess("25 Silo Storage", "26 Silo Storage"), true)
check("B6 naturlig sortering ens navn -> markørnavn", ADBuildings.compare({name = "Farma", loadMarkers = {{name = "30 Silo"}}}, {name = "Farma", loadMarkers = {{name = "4 Silo"}}}), false)
-- B-DOB: silo der kun kan FYLDES (drive-on-bunker ved biogas: aflæsning, ingen læssestation) = mål, ikke kilde
do
local dob = placeable("dob1", "DOB 115", 5000, 0, {spec_silo = {}})
table.insert(unloadStations, {owningPlaceable = dob, unloadTriggers = {{exactFillRootNode = {x = 5000, z = 5}}}})
addMarker(21, "Biogas Drop 1", "Biogas", 5000, 12)
ADBuildings.invalidate()
local byIdDob = {}
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byIdDob[b.id] = b end
check("B-DOB kan være mål", byIdDob.dob1.canBeTarget, true)
check("B-DOB ikke kilde (intet at hente)", byIdDob.dob1.canBeSource, false)
check("B-DOB aflæsningsmarkør", byIdDob.dob1.unloadMarker.id, 21)
check("B-DOB almindelig silo stadig ikke mål", byIdDob.silo1.canBeTarget, false)
end
-- B-SEP: bygning med læssestation men uden silo/fabrik/stald-spec (Pumps&Hoses separatorhal: digestat-udlevering)
do
local sep = placeable("sep1", "Separator Hall", 7000, 0, {spec_manureSeparator = {}})
table.insert(loadStations, {owningPlaceable = sep, loadTriggers = {{triggerNode = {x = 7000, z = 5}}}})
addMarker(41, "Digestate pickup", "Biogas", 7000, 10)
ADBuildings.invalidate()
local bySep = {}
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do bySep[b.id] = b end
check("B-SEP findes som lager", bySep.sep1 and bySep.sep1.kind, "storage")
check("B-SEP kan være kilde", bySep.sep1 and bySep.sep1.canBeSource, true)
check("B-SEP læssemarkør", bySep.sep1 and bySep.sep1.loadMarker and bySep.sep1.loadMarker.id, 41)
end
-- B-DROP: fabrik med pickup-markør TÆT på aflæsningstriggeren og en separat drop-markør længere væk
-- (Sugarbeet Crusher): aflæsningsmarkøren skal være drop, ikke bygningens egen læssemarkør
do
local crusher = placeable("crusher1", "Sugarbeet Crusher", 8000, 0, {spec_productionPoint = {productionPoint = {productions = {}, storage = {}}}})
table.insert(loadStations, {owningPlaceable = crusher, loadTriggers = {{triggerNode = {x = 8000, z = 5}}}})
table.insert(unloadStations, {owningPlaceable = crusher, unloadTriggers = {{exactFillRootNode = {x = 8003, z = 2}}}})
addMarker(51, "Sugar Beet Cut Pickup", "Fabrication", 8000, 8) -- 3 m fra læs, ~6 m fra aflæsning
addMarker(52, "Sugar Beet Cut Beet Drop", "Fabrication", 8012, 2) -- 9 m fra aflæsning
ADBuildings.invalidate()
local byC = {}
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byC[b.id] = b end
check("B-DROP læssemarkør = pickup", byC.crusher1.loadMarker.id, 51)
check("B-DROP aflæsningsmarkør = drop (ikke pickup)", byC.crusher1.unloadMarker.id, 52)
-- kun én fælles markør (AIO Load / Unload): bruges til begge
local aio = placeable("aio1", "AIO", 9000, 0, {spec_productionPoint = {productionPoint = {productions = {}, storage = {}}}})
table.insert(loadStations, {owningPlaceable = aio, loadTriggers = {{triggerNode = {x = 9000, z = 5}}}})
table.insert(unloadStations, {owningPlaceable = aio, unloadTriggers = {{exactFillRootNode = {x = 9002, z = 5}}}})
addMarker(53, "AIO Load / Unload", "Fabrication", 9000, 9)
ADBuildings.invalidate()
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byC[b.id] = b end
check("B-DROP fælles markør bruges til aflæsning", byC.aio1.unloadMarker.id, 53)
check("B-DROP silo beholder sin egen markør (ikke nabosiloens)", byC.silo1.unloadMarker.id, 6)
end
-- B-COOP: hønsehus — foder aflæsses i den ene ende, æg-paller spawnes i den anden (ingen læssestation)
do
local coop = placeable("coop1", "Chicken Coop", 10000, 0, {getHusbandryFillLevel = function() return 0 end,
spec_husbandryFood = {triggers = {{someTrigger = {exactFillRootNode = {x = 9970, z = 0}}}}}, -- ukendt feltsti: søges
spec_husbandryPallets = {fillTypeIndexToPalletSpawner = {[99] = {spawnPlaces = {{startNode = {x = 10030, z = 0}}}}}}})
table.insert(unloadStations, {owningPlaceable = coop, unloadTriggers = {}}) -- som chickenBarnBig: tom station
addMarker(61, "Chicken Food", "Chicken", 9965, 0)
addMarker(62, "Egg Pickup", "Chicken", 10035, 0)
ADBuildings.invalidate()
local byCoop = {}
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byCoop[b.id] = b end
check("B-COOP læssemarkør ved æggene", byCoop.coop1.loadMarker and byCoop.coop1.loadMarker.id, 62)
check("B-COOP aflæsningsmarkør ved foderet", byCoop.coop1.unloadMarker and byCoop.coop1.unloadMarker.id, 61)
end
-- B-FUEL: brændstoftank (læssestation kun med DIESEL) er ikke et lager/en kilde
do
local fmBefore = g_fillTypeManager
g_fillTypeManager = {getFillTypeNameByIndex = function(_, index) return index == 900 and "DIESEL" or "X" end}
local tank = placeable("fuel1", "Small Petrol Tank", 11000, 0, {})
table.insert(loadStations, {owningPlaceable = tank, supportedFillTypes = {[900] = true}, loadTriggers = {{triggerNode = {x = 11000, z = 3}}}})
ADBuildings.invalidate()
local byFuel = {}
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byFuel[b.id] = b end
check("B-FUEL brændstoftank udeladt", byFuel.fuel1, nil)
g_fillTypeManager = fmBefore
end
-- B-SCAN: søgning efter trigger-noder i indlejrede tabeller (dybde ≤ 3, ingen løkker)
do
local loop = {}
loop.self = loop
local nodes = ADBuildings.findTriggerNodes({a = {b = {exactFillRootNode = "n1"}}, c = {triggerNode = "n2"}, d = loop, e = {f = {g = {h = {exactFillRootNode = "for-dybt"}}}}})
table.sort(nodes)
check("B-SCAN finder n1 og n2, ikke for dybt", table.concat(nodes, ","), "n1,n2")
end
-- B-SHED: palleskur læsser og aflæsser samme sted — aflæsningsmarkøren er den nærmeste (ikke "den anden")
do
local shedX = placeable("shedX", "Bale & Pallet Storage", 12000, 0, {spec_objectStorage = {objectInfos = {}, capacity = 4000}})
addMarker(71, "Chicken Storage Unload", "Chicken", 12010, 0) -- 10 m
addMarker(72, "Chicken Storage Pickup", "Chicken", 12018, 0) -- 18 m
ADBuildings.invalidate()
local byShed = {}
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byShed[b.id] = b end
check("B-SHED aflæsning = nærmeste (Unload)", byShed.shedX.unloadMarker and byShed.shedX.unloadMarker.id, 71)
end
-- pickLoadMarker: en pallespawner viger for aflæsningen, når begge vil have samme markør og aflæsningen står
-- tættere (in-game 27/9 fårestald: spawner 9 m fra "Sheep Food", foderindkast 5 m -> Goat Food fik "Sheep Milk")
do
local food, milk = {id = 1, name = "Sheep Food 1", distance = 9}, {id = 2, name = "Sheep Milk 1", distance = 13}
local unloadBest = {id = 1, name = "Sheep Food 1", distance = 5}
check("PL spawner viger for tættere aflæsning", ADBuildings.pickLoadMarker({food, milk}, unloadBest, true).name, "Sheep Milk 1")
check("PL rigtig læssetrigger beholder markøren (Sugarbeet Crusher)", ADBuildings.pickLoadMarker({food, milk}, unloadBest, false).name, "Sheep Food 1")
-- in-game 27/9 fårestald 3 og 5: spawner 5 m fra "Sheep Food", foderindkast 9 m -> spawneren vandt stadig
check("PL spawner viger også når den selv står tættest", ADBuildings.pickLoadMarker({food, milk}, {id = 1, name = "Sheep Food 1", distance = 12}, true).name, "Sheep Milk 1")
check("PL rigtig læssetrigger tættest beholder", ADBuildings.pickLoadMarker({food, milk}, {id = 1, name = "Sheep Food 1", distance = 12}, false).name, "Sheep Food 1")
check("PL ingen konflikt", ADBuildings.pickLoadMarker({milk}, unloadBest, true).name, "Sheep Milk 1")
check("PL eneste kandidat er aflæsningens -> ingen læssemarkør", ADBuildings.pickLoadMarker({food}, unloadBest, true), nil)
check("PL ingen kandidater", ADBuildings.pickLoadMarker({}, unloadBest, true), nil)
end
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-40
View File
@@ -1,40 +0,0 @@
-- Kør: luajit tests/test_adCache.lua (fra repo-roden)
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
g_currentMission = {time = 0}
dofile("FS25_ADSmartPickup/adCache.lua")
local calls = 0
local function lookup() calls = calls + 1; return "station" end
check("K1 slået fra: altid opslag", ADCacheGet("b", "k", 1000, lookup) .. ADCacheGet("b", "k", 1000, lookup), "stationstation")
check("K1 to opslag", calls, 2)
ADCache.enabled = true
calls = 0
ADCacheGet("b", "k", 1000, lookup); ADCacheGet("b", "k", 1000, lookup)
check("K2 genbrugt", calls, 1)
g_currentMission.time = 900
ADCacheGet("b", "k", 1000, lookup)
check("K2 inden for levetid", calls, 1)
g_currentMission.time = 1600
ADCacheGet("b", "k", 1000, lookup)
check("K3 efter levetid + spredning (maks 1500) -> nyt opslag", calls, 2)
local nothing = 0
ADCacheGet("n", "x", 1000, function() nothing = nothing + 1; return nil end)
check("K4 intet fundet huskes", ADCacheGet("n", "x", 1000, function() nothing = nothing + 1; return "ny" end), nil)
check("K4 kun ét opslag", nothing, 1)
-- spredt udløb: 200 poster lavet samtidig udløber ikke samtidig
g_currentMission.time = 10000
for i = 1, 200 do ADCacheGet("spread", i, 10000, function() return i end) end
local expired = 0
g_currentMission.time = 22000
for i = 1, 200 do ADCacheGet("spread", i, 10000, function() expired = expired + 1; return i end) end
check("K5 kun en del udløbet efter 12 s (spredt 10-15 s)", expired > 20 and expired < 180, true)
ADCache.clear()
check("K6 ryddet", next(ADCache.buckets), nil)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-195
View File
@@ -1,195 +0,0 @@
-- Kør: luajit tests/test_adCourseCache.lua (fra repo-roden)
-- Kursus-registret: hvilken mark/bredde/markform/indstillinger en maskines nuværende Courseplay-kursus blev
-- lavet til. Genbrug kun når ALT matcher; egen maskine først, ellers en anden maskines kursus (kopi).
dofile("FS25_ADSmartPickup/adCourseCache.lua")
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
local function count(map)
local n = 0
for _ in pairs(map) do n = n + 1 end
return n
end
local C = ADCourseCache
local SIG = "n=4;a=960000;c=800,300"
-- ---- indstillings-nøglen ----
check("K1 standard", C.settingsKey({}), "off=0.0;lane=-;multi=1;vine=-;skip=-")
check("K2 nil = standard", C.settingsKey(nil), C.settingsKey({}))
check("K3 alt sat", C.settingsKey({toolOffset = 1.25, lane = -1, multiTools = 2, vineOffset = -1, vineSkip = 1}),
"off=1.3;lane=-1;multi=2;vine=-1.0;skip=1")
check("K4 -0.0 = 0.0", C.settingsKey({toolOffset = -0.0001}), C.settingsKey({toolOffset = 0}))
check("K5 float-støj i forskydning tæller ikke", C.settingsKey({toolOffset = 1.2000001}), C.settingsKey({toolOffset = 1.2}))
check("K6 anden bane = anden nøgle", C.settingsKey({multiTools = 2, lane = 1}) ~= C.settingsKey({multiTools = 2, lane = -1}), true)
check("K7 vin-offset indgår", C.settingsKey({vineOffset = 0}) ~= C.settingsKey({vineOffset = -1}), true)
check("K8 vin-skip indgår", C.settingsKey({vineOffset = -1, vineSkip = 0}) ~= C.settingsKey({vineOffset = -1, vineSkip = 1}), true)
local settings = C.settingsKey({})
local request = {field = 22, width = 30, sig = SIG, settings = settings}
local own = C.newRecord({vehicle = "v1", field = 22, width = 30.0, sig = SIG, settings = settings, waypoints = 812})
-- ---- key/matches ----
check("M1 samme alt", C.matches(own, request), true)
check("M2 bredde som float32 fra XML (30.0000001)", C.matches(C.newRecord({vehicle = "v1", field = 22, width = 30.0000001, sig = SIG, settings = settings}), request), true)
check("M3 ny bredde", C.matches(own, {field = 22, width = 12, sig = SIG, settings = settings}), false)
check("M4 anden mark", C.matches(own, {field = 23, width = 30, sig = SIG, settings = settings}), false)
check("M5 form ændret", C.matches(own, {field = 22, width = 30, sig = "n=5;a=960000;c=800,300", settings = settings}), false)
check("M6 andre indstillinger", C.matches(own, {field = 22, width = 30, sig = SIG, settings = C.settingsKey({toolOffset = 2})}), false)
check("M7 ukendt form (nil) matcher aldrig", C.matches(C.newRecord({vehicle = "v1", field = 22, width = 30, settings = settings}), {field = 22, width = 30, settings = settings}), false)
check("M8 intet register", C.matches(nil, request), false)
check("M9 key", C.key(request), "22|30.0|" .. SIG .. "|" .. settings)
check("M10 key uden form = nil", C.key({field = 22, width = 30, settings = settings}), nil)
-- ---- grund til ny beregning ----
check("R1 intet gemt", C.mismatchReason(nil, request), "intet kursus gemt")
check("R2 form ændret", C.mismatchReason(own, {field = 22, width = 30, sig = "x", settings = settings}), "markens form ændret")
check("R3 ny bredde", C.mismatchReason(own, {field = 22, width = 12, sig = SIG, settings = settings}), "ny bredde")
check("R4 anden mark", C.mismatchReason(own, {field = 9, width = 30, sig = SIG, settings = settings}), "kursus til en anden mark")
check("R5 indstillinger", C.mismatchReason(own, {field = 22, width = 30, sig = SIG, settings = "x"}), "andre indstillinger")
check("R6 ukendt form", C.mismatchReason(own, {field = 22, width = 30, settings = settings}), "markens form ukendt")
-- ---- register: immutabelt ----
local records = C.withRecord({}, own)
check("I1 tilføjet", records.v1 ~= nil, true)
local other = C.newRecord({vehicle = "v2", field = 22, width = 30, sig = SIG, settings = settings, waypoints = 812})
local both = C.withRecord(records, other)
check("I2 original urørt", count(records), 1)
check("I3 begge", count(both), 2)
check("I4 without", count(C.without(both, "v1")), 1)
check("I5 without urørt original", count(both), 2)
check("I6 newRecord uden vehicle = nil", C.newRecord({field = 22}), nil)
-- ---- findReusable ----
local rec, how = C.findReusable(both, request, "v1")
check("F1 egen maskine først", rec and rec.vehicle, "v1")
check("F2 egen = own", how, "own")
rec, how = C.findReusable(both, request, "v3")
check("F3 anden maskines kursus", rec and rec.vehicle, "v1")
check("F4 = copy", how, "copy")
-- egen maskines kursus er til en anden bredde -> kopi fra v2
local mixed = C.withRecord(both, C.newRecord({vehicle = "v1", field = 22, width = 12, sig = SIG, settings = settings}))
rec, how = C.findReusable(mixed, request, "v1")
check("F5 egen passer ikke -> kopi fra v2", rec and rec.vehicle, "v2")
check("F6 kopi", how, "copy")
-- intet passer -> grunden fra egen post
local changed = {field = 22, width = 30, sig = "n=6;a=1;c=0,0", settings = settings}
rec, how = C.findReusable(both, changed, "v1")
check("F7 intet passer", rec, nil)
check("F8 grund: form ændret", how, "markens form ændret")
rec, how = C.findReusable({}, request, "v1")
check("F9 tomt register", how, "intet kursus gemt")
-- gyldighedstjek: egen maskines kursus er væk/ændret -> kopi fra v2; alle ugyldige -> ny
rec, how = C.findReusable(both, request, "v1", function(r) return r.vehicle ~= "v1" end)
check("F10 egen ugyldig -> kopi", rec and rec.vehicle, "v2")
rec, how = C.findReusable(both, request, "v1", function() return false end)
check("F11 alle ugyldige", rec, nil)
check("F12 grund: kurset på maskinen er ændret", how, "kurset på maskinen er ændret")
-- multi-tool: lederens kursus (2 sæt, bane -1) genbruges ikke til et enkelt sæt
local multi = C.newRecord({vehicle = "v1", field = 22, width = 30, sig = SIG, settings = C.settingsKey({multiTools = 2, lane = -1})})
rec = C.findReusable(C.withRecord({}, multi), request, "v1")
check("F13 multi-kursus != enkelt", rec, nil)
rec, how = C.findReusable(C.withRecord({}, multi), {field = 22, width = 30, sig = SIG, settings = C.settingsKey({multiTools = 2, lane = -1})}, "v1")
check("F14 multi samme bane genbruges", how, "own")
-- deterministisk: flere kandidater -> laveste id
local many = C.withRecord(C.withRecord({}, C.newRecord({vehicle = "v9", field = 22, width = 30, sig = SIG, settings = settings})),
C.newRecord({vehicle = "v4", field = 22, width = 30, sig = SIG, settings = settings}))
rec = C.findReusable(many, request, "v1")
check("F15 deterministisk valg", rec and rec.vehicle, "v4")
-- ---- invalidateField ----
local fieldRecords = C.withRecord(both, C.newRecord({vehicle = "v5", field = 7, width = 30, sig = "n=3;a=50;c=3,3", settings = settings}))
local kept, removed = C.invalidateField(fieldRecords, 22, "n=5;a=1;c=0,0")
check("V1 to poster for marken slettet", removed, 2)
check("V2 anden mark bevaret", kept.v5 ~= nil, true)
check("V3 original urørt", count(fieldRecords), 3)
kept, removed = C.invalidateField(fieldRecords, 22, SIG)
check("V4 samme form: intet slettes", removed, 0)
check("V4b samme tabel tilbage", kept, fieldRecords)
kept, removed = C.invalidateField(fieldRecords, 22, nil)
check("V5 ukendt form: intet slettes", removed, 0)
kept, removed = C.invalidateField(nil, 22, SIG)
check("V6 nil-register", removed, 0)
-- ---- lim: prepare/afterStart/checkField med falske spil-objekter ----
dofile("FS25_ADSmartPickup/adCpBridge.lua")
local saves, logs = 0, {}
ADFieldJobs = {state = {courseRecords = {}}, save = function() saves = saves + 1 end,
log = function(_, fmt, ...) table.insert(logs, string.format(fmt, ...)) end}
ADRuns = {vehicleLabel = function(v) return v.label end}
Logging = {warning = function(...) print("WARN", ...) end}
local function fakeCourse(n)
return {getNumberOfWaypoints = function() return n end}
end
local function fakeVehicle(id, width, course)
return {label = id, course = course,
getFieldWorkCourse = function(self) return self.course end,
getCourseGeneratorSettings = function() return {workWidth = {getValue = function() return width end}} end}
end
local tractorA = fakeVehicle("A", 30, nil)
local tractorB = fakeVehicle("B", 30, nil)
ADRunsController = {getFarmVehicles = function() return {tractorA, tractorB} end,
getVehicleId = function(v) return v.label end}
local fieldObj, info = {id = 22}, {shapeSig = SIG, fruitName = "WHEAT"}
local assignA = {vehicle = tractorA, vehicleId = "A", role = "cultivator"}
-- G1: intet gemt -> ny beregning, opts urørt (ingen reuse/course)
local opts, ctx = C.prepare({}, fieldObj, info, assignA, {})
check("G1 ny beregning: ingen reuse", opts.reuse, nil)
check("G1b ingen kursus", opts.course, nil)
check("G1c log: nyt kursus (intet gemt)", logs[#logs]:find("nyt kursus beregnes", 1, true) ~= nil and logs[#logs]:find("intet kursus gemt", 1, true) ~= nil, true)
-- CP har lavet kurset (812 punkter) -> registreres
tractorA.course = fakeCourse(812)
C.afterStart(tractorA, "A", ctx)
check("G2 registreret", ADFieldJobs.state.courseRecords.A ~= nil and ADFieldJobs.state.courseRecords.A.waypoints, 812)
check("G2b gemt til fil", saves, 1)
-- G3: næste gang -> genbrug
opts, ctx = C.prepare({}, fieldObj, info, assignA, {})
check("G3 genbrug", opts.reuse, true)
check("G3b log genbrugt", logs[#logs]:find("kursus genbrugt", 1, true) ~= nil, true)
C.afterStart(tractorA, "A", ctx)
check("G3c uændret register gemmes ikke igen", saves, 1)
-- G4: B skal samme mark -> kopi fra A
local assignB = {vehicle = tractorB, vehicleId = "B", role = "cultivator"}
opts, ctx = C.prepare({}, fieldObj, info, assignB, {})
check("G4 kopi fra A", opts.course, tractorA.course)
check("G4b log kopieret", logs[#logs]:find("kopieret fra 'A'", 1, true) ~= nil, true)
tractorB.course = fakeCourse(812)
C.afterStart(tractorB, "B", ctx)
check("G4c B registreret", ADFieldJobs.state.courseRecords.B ~= nil, true)
-- G5: spilleren har lavet et nyt kursus på A i CP's HUD (andet antal punkter) -> kopi fra B
tractorA.course = fakeCourse(400)
opts = C.prepare({}, fieldObj, info, assignA, {})
check("G5 A's kursus ændret -> kopi fra B", opts.course, tractorB.course)
-- G6: kursus kopieret fra slåmaskinen (opts.course) -> registret bruges ikke, maskinens post glemmes
local mowerCourse = fakeCourse(99)
opts, ctx = C.prepare({}, fieldObj, info, assignB, {course = mowerCourse})
check("G6 opts urørt", opts.course, mowerCourse)
check("G6b ingen kontekst", ctx, nil)
tractorB.course = fakeCourse(99)
C.afterStart(tractorB, "B", ctx)
check("G6c B glemt", ADFieldJobs.state.courseRecords.B, nil)
-- G7: vinmark: vin-offset indgår i nøglen
local vineReq = C.requestFor(tractorA, 22, {shapeSig = SIG, fruitName = "GRAPE"}, "fertilizer", {})
check("G7 vin i nøglen", vineReq.settings, C.settingsKey({vineOffset = -1, vineSkip = 1}))
-- G8: markens form ændret -> A's post glemmes, log
local before = saves
check("G8 checkField sletter", C.checkField(22, "n=9;a=1;c=0,0"), 1)
check("G8b register tomt for marken", ADFieldJobs.state.courseRecords.A, nil)
check("G8c gemt", saves, before + 1)
check("G9 samme form: intet", C.checkField(22, "n=9;a=1;c=0,0"), 0)
-- G10: fejl i spil-API'et -> falder tilbage til ny beregning (opts urørt), ingen crash
local brokenVehicle = {label = "X", getFieldWorkCourse = function() error("boom") end}
local plainOpts = {lane = 1}
local okPrep, gotOpts, gotCtx = pcall(C.prepare, {}, fieldObj, nil, {vehicle = brokenVehicle, vehicleId = "X", role = "x"}, plainOpts)
check("G10 ingen crash", okPrep, true)
check("G10b opts urørt", gotOpts, plainOpts)
check("G10c ingen kontekst", gotCtx, nil)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-178
View File
@@ -1,178 +0,0 @@
-- Kør: luajit tests/test_adCpBridge.lua (fra repo-roden)
-- Kun getWorkWidth: variabel bredde (Unrealistic Pack) vinder over AI-markørernes grundbredde.
dofile("FS25_ADSmartPickup/adCpBridge.lua")
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
local function withMarkers(width)
return {getAIMarkers = function() return 1, 2, 3, 4, width end}
end
-- W1: læssevogn med variabel bredde 20,4 + 20,4 = 40,8 m, AI-markører siger 2,4 m
local wagon = withMarkers(2.4)
wagon.getVariableWorkWidth = function(_, isLeft) return isLeft and 20.4 or -20.4, nil, true end
local tractor = {getAttachedImplements = function() return {{object = wagon}} end}
check("W1 variabel bredde", ADCpBridge.getWorkWidth(tractor), 40.8)
-- W2: ugyldig variabel bredde -> AI-markører
local plain = withMarkers(9.5)
plain.getVariableWorkWidth = function() return 0, nil, false end
check("W2 AI-markører", ADCpBridge.getWorkWidth(plain), 9.5)
-- W3: selvkørende slåmaskine uden redskaber, kun variabel bredde
local mower = {getVariableWorkWidth = function(_, isLeft) return isLeft and 20.4 or -20.4, nil, true end}
check("W3 selvkørende", ADCpBridge.getWorkWidth(mower), 40.8)
-- W4: Courseplays egen bredde (HUD'en) vinder — det er den marken køres med
local cpVehicle = {getVariableWorkWidth = function(_, isLeft) return isLeft and 20.4 or -0.4, nil, true end,
getCourseGeneratorSettings = function() return {workWidth = {getValue = function() return 40.8 end}} end}
check("W4 Courseplay-bredde", ADCpBridge.getWorkWidth(cpVehicle), 40.8)
-- W5: Courseplay-indstilling 0/ugyldig -> egen beregning
local zeroCp = {getVariableWorkWidth = function(_, isLeft) return isLeft and 5 or -5, nil, true end,
getCourseGeneratorSettings = function() return {workWidth = {getValue = function() return 0 end}} end}
check("W5 CP 0 -> egen beregning", ADCpBridge.getWorkWidth(zeroCp), 10)
-- WO: skårets sideforskydning på en river, målt fra nedlægningsområdet (venstre positiv), minus rivens
-- egen CP-forskydning. Noder er tal; localToLocal-mock giver nodens x i traktorens ramme.
local nodeX = {[11] = 2.0, [12] = 4.0, [21] = -1.0, [22] = 1.0}
function localToLocal(node) return nodeX[node] or 0, 0, 0 end
local function rake(dropIndex, runtime)
local areas = {
{start = 1, width = 2, height = 3, dropWindrowWorkAreaIndex = runtime and dropIndex or nil},
{start = 10, width = 11, height = 12},
{start = 20, width = 21, height = 22},
}
return {spec_windrower = {}, spec_workArea = {workAreas = areas}, configFileName = "rake.xml"}
end
local sideRake = rake(2, true)
local tractorWithRake = {rootNode = 100, getAttachedImplements = function() return {{object = sideRake}} end,
getCpSettings = function() return {toolOffsetX = {getValue = function() return 0.5 end}} end}
check("WO1 sideriver (runtime-felt)", ADCpBridge.getWindrowOffset(tractorWithRake), 2.5)
local centerRake = rake(3, true)
local tractorCenter = {rootNode = 100, getAttachedImplements = function() return {{object = centerRake}} end}
check("WO2 midterrive", ADCpBridge.getWindrowOffset(tractorCenter), 0)
-- WO3: runtime-felt mangler -> riverens XML (workArea(0) windrower#dropWindrowWorkAreaIndex = 2)
ADCpBridge.readDropIndicesFromXml = function(fileName) return fileName == "rake.xml" and {[1] = 2} or {} end
local xmlRake = rake(2, false)
local tractorXml = {rootNode = 100, getAttachedImplements = function() return {{object = xmlRake}} end}
check("WO3 fra XML", ADCpBridge.getWindrowOffset(tractorXml), 3)
check("WO4 ingen river -> nil", ADCpBridge.getWindrowOffset({rootNode = 100}), nil)
-- SR: sprøjte/spreder -> rolle ud fra fyldet
check("SR gødning", ADCpBridge.sprayRoleForFillType("FERTILIZER"), "fertilizer")
check("SR flydende", ADCpBridge.sprayRoleForFillType("LIQUIDFERTILIZER"), "fertilizer")
check("SR gylle", ADCpBridge.sprayRoleForFillType("LIQUIDMANURE"), "fertilizer")
check("SR møg", ADCpBridge.sprayRoleForFillType("MANURE"), "fertilizer")
check("SR digestat", ADCpBridge.sprayRoleForFillType("DIGESTATE"), "fertilizer")
check("SR kalk", ADCpBridge.sprayRoleForFillType("LIME"), "limer")
check("SR ukrudt", ADCpBridge.sprayRoleForFillType("HERBICIDE"), "weeder")
check("SR vand/ukendt", ADCpBridge.sprayRoleForFillType("WATER"), nil)
-- SR2: tom Bredal-spreder (tager gødning OG kalk) kan begge dele; fyldt med kalk er den primært kalkspreder
local bredal = {FERTILIZER = true, LIME = true}
local p1, r1, f1 = ADCpBridge.sprayRolesFor(nil, bredal)
check("SR2 tom spreder primært gødning", p1, "fertilizer")
check("SR2 tom spreder kan kalke", r1 and r1.limer, true)
check("SR2 tom = intet fyld", f1, nil)
local p2, r2, f2 = ADCpBridge.sprayRolesFor("LIME", bredal)
check("SR2 med kalk = kalkspreder", p2, "limer")
check("SR2 med kalk kan stadig gøde (efter tømning)", r2 and r2.fertilizer, true)
check("SR2 fyldets rolle", f2, "limer")
check("SR2 ukrudtssprøjte", (ADCpBridge.sprayRolesFor(nil, {HERBICIDE = true, WATER = true})), "weeder")
check("SR2 vandvogn er ingen sprøjte", (ADCpBridge.sprayRolesFor(nil, {WATER = true})), nil)
-- SR3: kun en lille rest må hældes ud (100.000 l gødning blev hældt ud in-game 26/9)
check("SR3 tom", ADCpBridge.isDumpable(0, 4000), true)
check("SR3 lille rest 300 af 4000", ADCpBridge.isDumpable(300, 4000), true)
check("SR3 halv tank 2000 af 4000", ADCpBridge.isDumpable(2000, 4000), false)
check("SR3 100000 l aldrig", ADCpBridge.isDumpable(100000, 1000000), false)
check("SR3 over 2000 l aldrig", ADCpBridge.isDumpable(2500, 100000), false)
-- VN: vinmark — i gangen for alt undtagen høsteren; almindelige marker røres ikke
check("VN sprøjte på druer = venstre", ADCpBridge.vineOffsetFor("GRAPEB", "fertilizer"), -1)
check("VN harve på druer = venstre", ADCpBridge.vineOffsetFor("grape", "cultivator"), -1)
check("VN høster på druer = på vinen", ADCpBridge.vineOffsetFor("GRAPEB", "harvester"), 0)
check("VN hvede røres ikke", ADCpBridge.vineOffsetFor("WHEAT", "fertilizer"), nil)
check("VN ukendt afgrøde", ADCpBridge.vineOffsetFor(nil, "weeder"), nil)
check("VN sprøjte springer 1 række over (begge sider)", ADCpBridge.vineSkipFor("GRAPEB", "weeder"), 1)
check("VN gødning springer 1 over", ADCpBridge.vineSkipFor("GRAPEB", "fertilizer"), 1)
check("VN harve springer ingen over", ADCpBridge.vineSkipFor("GRAPEB", "cultivator"), 0)
check("VN høster springer ingen over", ADCpBridge.vineSkipFor("GRAPEB", "harvester"), 0)
check("VN hvede røres ikke", ADCpBridge.vineSkipFor("WHEAT", "weeder"), nil)
-- HF: høsterens afgrøder fra skærebordet (liste eller opslag); finsnitter tager ingen
local list = ADCpBridge.harvestFruitSet({fruitTypes = {3, 5}})
check("HF liste", list[3] and list[5], true)
local lookup = ADCpBridge.harvestFruitSet({fruitTypes = {[3] = true}})
check("HF opslag", lookup[3], true)
local forage = ADCpBridge.harvestFruitSet({fruitTypes = {3}, fruitTypeConverters = {[3] = {}}})
check("HF finsnitter tager intet", next(forage), nil)
check("HF fruitTypeIndices", ADCpBridge.harvestFruitSet({fruitTypeIndices = {7}})[7], true)
-- PL: plantbar i perioden
local fruitPlant = {growthDataSeasonal = {periods = {[3] = {plantingAllowed = true}, [4] = {plantingAllowed = false}}}}
check("PL tilladt", ADCpBridge.isPlantableIn(fruitPlant, 3), true)
check("PL ikke nu", ADCpBridge.isPlantableIn(fruitPlant, 4), false)
check("PL ukendt data = tilladt", ADCpBridge.isPlantableIn({}, 4), true)
-- CR: kursus-genbrug — start på maskinens eget kursus uden at CP beregner et nyt (opts.reuse)
do
local generated, started = 0, 0
AIJobType = {FIELDWORK_CP = 1}
function getWorldTranslation() return 0, 0, 0 end
local job = {cpJobParameters = {}}
function job.applyCurrentState() end
function job.setValues() end
function job.validate() return true end
function job.registerFieldBoundaryDetectionCallback() end
function job.detectFieldBoundary() return true, false, "" end
function job.onClickGenerateFieldWorkCourse(_, callback) generated = generated + 1; callback(true) end
g_currentMission = {aiJobTypeManager = {createJob = function() return job end},
aiSystem = {startJob = function() started = started + 1 end}}
local course = {}
local vehicle = {rootNode = 1, getOwnerFarmId = function() return 1 end, getFieldWorkCourse = function() return course end}
local reason = "?"
ADCpBridge.start(vehicle, 0, 0, function(ok, r) reason = ok and (r or "generated") or "fail" end, {reuse = true})
check("CR1 genbrug: ingen beregning", generated, 0)
check("CR2 genbrug: CP startet", started, 1)
check("CR3 genbrug: reason", reason, "reused")
-- ingen kursus på maskinen -> beregnes alligevel
vehicle.getFieldWorkCourse = function() return nil end
ADCpBridge.start(vehicle, 0, 0, function(ok, r) reason = ok and (r or "generated") or "fail" end, {reuse = true})
check("CR4 intet kursus -> beregnes", generated, 1)
check("CR5 reason ny", reason, "generated")
-- uden reuse: beregnes som før
vehicle.getFieldWorkCourse = function() return course end
ADCpBridge.start(vehicle, 0, 0, function(ok, r) reason = ok and (r or "generated") or "fail" end, {})
check("CR6 uden reuse -> beregnes", generated, 2)
AIJobType, g_currentMission = nil, nil
end
-- forager: 2 runder og start på forageren (så der ikke køres ind i træer o.l. i hjørnerne)
local recorded = {}
local function setting(name) return {setValue = function(_, v) recorded[name] = v end} end
local headlandVehicle = {getCourseGeneratorSettings = function() return {numberOfHeadlands = setting("headlands"), startOnHeadland = setting("startOnHeadland")} end}
check("forager: sat", ADCpBridge.setHeadlands(headlandVehicle, 2), true)
check("forager: 2 runder", recorded.headlands, 2)
check("forager: start på forageren", recorded.startOnHeadland, true)
ADCpBridge.setHeadlands(headlandVehicle, 0)
check("forager: ingen -> ikke start på forageren", recorded.startOnHeadland, false)
check("forager: køretøj uden CP", ADCpBridge.setHeadlands({}, 2), false)
check("forager: standard er 2", ADCpBridge.HEADLANDS, 2)
-- genbrugt kursus uden forager beregnes på ny
check("forager i kursus: 2 runder", ADCpBridge.courseHasHeadlands({getNumberOfHeadlands = function() return 2 end}), true)
check("forager i kursus: ingen", ADCpBridge.courseHasHeadlands({getNumberOfHeadlands = function() return 0 end}), false)
check("forager i kursus: ukendt -> antag ja", ADCpBridge.courseHasHeadlands({}), true)
check("forager i kursus: intet kursus", ADCpBridge.courseHasHeadlands(nil), false)
-- isWorking: AutoDrive kører også som AI i FS25 — det er ikke Courseplay (in-game 29/9: Milk Store holdt ved
-- Cow Wait med AD aktiv og "ventede på sig selv" som 'Courseplay kører')
local function fakeVehicle(cp, ai, ad)
return {getIsCpFieldWorkActive = function() return cp end, getIsAIActive = function() return ai end,
ad = {stateModule = {isActive = function() return ad end}}}
end
check("CP arbejder: Courseplay markarbejde", ADCpBridge.isWorking(fakeVehicle(true, true, false)), true)
check("CP arbejder: AI uden AD (CP-kørsel)", ADCpBridge.isWorking(fakeVehicle(false, true, false)), true)
check("CP arbejder: AutoDrive kører -> ikke CP", ADCpBridge.isWorking(fakeVehicle(false, true, true)), false)
check("CP arbejder: intet", ADCpBridge.isWorking(fakeVehicle(false, false, false)), false)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-33
View File
@@ -1,33 +0,0 @@
-- Kør: luajit tests/test_adCpResume.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adCpResume.lua")
local R = ADCpResume
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
-- hvem skrives ved gem: CP kører, AD styrer ikke (AD genstarter selv sine)
local ids = R.activeIds({{id = "a", cpActive = true}, {id = "b", cpActive = true, adActive = true}, {id = "c"}, {id = "d", cpActive = true}})
check("gem: CP-aktive uden AD", table.concat(ids, " "), "a d")
check("gem: tom liste", #R.activeIds({}), 0)
-- records frem og tilbage
local set = R.fromRecords({"a", "", "d", "a"})
check("indlæs: a", set.a, true)
check("indlæs: tomt id springes over", set[""], nil)
-- hvem startes ved indlæsning
local pending = {a = true, b = true, c = true, d = true}
local vehicles = {{id = "a"}, {id = "b", aiActive = true}, {id = "c", controlled = true}, {id = "d"}, {id = "e"}}
local start = R.toStart(pending, vehicles)
check("start: kun gemte, ikke allerede kørende/spiller i", table.concat(start, " "), "a d")
check("start: ingen gemt liste -> nil (gammel regel bruges)", R.toStart(nil, vehicles), nil)
-- kun når CP's HUD-job er markarbejde: moddets markopgaver starter CP med eget job, så HUD'en kan stå på fx
-- bunkersilo — HUD-start gav "CP: No silo found" efter genindlæsning (in-game 29/9)
local mixed = R.toStart({a = true, b = true, c = true}, {
{id = "a", fieldWork = true}, {id = "b", fieldWork = false}, {id = "c"}})
check("start: kun markarbejds-job", table.concat(mixed, ","), "a,c")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-145
View File
@@ -1,145 +0,0 @@
-- Kør: luajit tests/test_adFieldFleet.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFieldModel.lua")
dofile("FS25_ADSmartPickup/adFieldFleet.lua")
local F = ADFieldFleet
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
-- FL1: rolle ud fra redskabets spec (læssevogn vinder over andet på samme køretøj)
check("FL1 slåmaskine", F.roleFromSpecs({spec_mower = true}), "mower")
check("FL1 vender", F.roleFromSpecs({spec_tedder = true}), "tedder")
check("FL1 river", F.roleFromSpecs({spec_windrower = true}), "rake")
check("FL1 læssevogn", F.roleFromSpecs({spec_forageWagon = true, spec_mower = true}), "collector")
check("FL1 intet", F.roleFromSpecs({spec_trailer = true}), nil)
local function rig(id, role, x, extra)
local r = {id = id, role = role, enabled = true, busy = false, controlled = false, width = 40.8, x = x, z = 0, label = id}
for k, v in pairs(extra or {}) do r[k] = v end
return r
end
-- FL2: nærmeste ledige med flueben og rigtig rolle
local picked = F.pick({rig("far", "mower", 500), rig("near", "mower", 50), rig("wagon", "collector", 10)}, "mower", 0, 0, nil)
check("FL2 nærmeste slåmaskine", picked.id, "near")
-- FL3: uden flueben / optaget / spiller i sædet springes over
local _, why = F.pick({rig("a", "mower", 1, {enabled = false})}, "mower", 0, 0)
check("FL3 intet med flueben", why, "noRig")
local _, whyBusy = F.pick({rig("a", "mower", 1, {busy = true}), rig("b", "mower", 2, {controlled = true})}, "mower", 0, 0)
check("FL3 alle optaget", whyBusy, "allBusy")
check("FL3 ingen med rollen", select(2, F.pick({rig("a", "tedder", 1)}, "mower", 0, 0)), "noRig")
-- FL4: læssevogn smallere end slåbredden springes over, bredere vælges
local wide = F.pick({rig("smal", "collector", 1, {width = 20}), rig("bred", "collector", 900, {width = 40.8})}, "collector", 0, 0, 40.8)
check("FL4 bred valgt selv om længere væk", wide.id, "bred")
-- FL5: kun for smalle -> grund tooNarrow med tal
local none, whyNarrow, args = F.pick({rig("smal", "collector", 1, {width = 20})}, "collector", 0, 0, 40.8)
check("FL5 ingen", none, nil); check("FL5 grund", whyNarrow, "tooNarrow"); check("FL5 bredde", args[1], 20)
-- FL6: over 50 m -> tooWide
check("FL6 for bred", select(2, F.pick({rig("x", "mower", 1, {width = 60})}, "mower", 0, 0)), "tooWide")
-- FL7: toggle giver ny tabel
local enabled = {a = true}
local toggled = F.toggle(enabled, "b")
check("FL7 b til", toggled.b, true); check("FL7 original urørt", enabled.b, nil)
check("FL7 a fra", F.toggle(toggled, "a").a, nil)
-- FL8: nye roller i prioriteret rækkefølge
check("FL8 høster", F.roleFromSpecs({spec_combine = true, spec_cutter = true}), "harvester")
check("FL8 såmaskine før kultivator (combi)", F.roleFromSpecs({spec_sowingMachine = true, spec_cultivator = true, sprayRole = "fertilizer"}), "seeder")
check("FL8 kultivator", F.roleFromSpecs({spec_cultivator = true}), "cultivator")
check("FL8 plov", F.roleFromSpecs({spec_plow = true}), "plow")
check("FL8 tromle", F.roleFromSpecs({spec_roller = true}), "roller")
check("FL8 strigle", F.roleFromSpecs({spec_weeder = true}), "weeder")
check("FL8 stubhakker", F.roleFromSpecs({spec_mulcher = true}), "mulcher")
check("FL8 gødningssprøjte", F.roleFromSpecs({sprayRole = "fertilizer"}), "fertilizer")
check("FL8 kalkspreder", F.roleFromSpecs({sprayRole = "limer"}), "limer")
check("FL8 ukrudtssprøjte", F.roleFromSpecs({sprayRole = "weeder"}), "weeder")
check("FL8 aflæsningsvogn", F.roleFromSpecs({canUnload = true}), "unloader")
check("FL8 traktor med plov er ikke aflæsningsvogn", F.roleFromSpecs({spec_plow = true, canUnload = true}), "plow")
-- FL9: accept-filter (fx høster der kan tage afgrøden, strigle kun til småt ukrudt)
local combineWheat = rig("jd", "harvester", 10, {fruits = {[3] = true}})
local combineBeet = rig("ropa", "harvester", 1, {fruits = {[9] = true}})
local forWheat = F.pick({combineBeet, combineWheat}, "harvester", 0, 0, nil, function(c) return c.fruits[3] == true end)
check("FL9 høster der kan tage hvede", forWheat.id, "jd")
local _, whyFruit = F.pick({combineBeet}, "harvester", 0, 0, nil, function(c) return c.fruits[3] == true end)
check("FL9 ingen kan tage afgrøden", whyFruit, "cannotDo")
-- FL10: flere sæt til multi-tool — alle eller ingen, nærmeste først
local m1, m2, m3 = rig("m1", "mower", 30), rig("m2", "mower", 10), rig("m3", "mower", 20, {busy = true})
local two = F.pickMany({m1, m2, m3}, "mower", 2, 0, 0)
check("FL10 to valgt", #two, 2)
check("FL10 nærmeste først", two[1].id, "m2")
local none, whyMany, args = F.pickMany({m1, m2, m3}, "mower", 3, 0, 0)
check("FL10 for få -> ingen", none, nil)
check("FL10 grund", whyMany, "notEnough")
check("FL10 ledige/krævet", args[1] .. "/" .. args[2], "2/3")
check("FL10 ingen med rollen", select(2, F.pickMany({m1}, "tedder", 1, 0, 0)), "noRig")
-- FL11: markflådens tilstand: alle ledige maskiner (standard) eller kun med flueben
check("FL11 alle: uden flueben med", F.isEnabled("all", {}, "v1"), true)
check("FL11 flueben: uden flueben ude", F.isEnabled("ticked", {}, "v1"), false)
check("FL11 flueben: med flueben", F.isEnabled("ticked", {v1 = true}, "v1"), true)
check("FL11 ukendt tilstand = alle", F.isEnabled(nil, {}, "v1"), true)
-- FL12: ballepresser og ballesamler (ballevogn/Universal Autoload som Courseplays ballesamler kan bruge)
check("FL12 ballepresser", F.roleFromSpecs({spec_baler = true}), "baler")
check("FL12 ballesamler", F.roleFromSpecs({canBaleCollect = true}), "baleCollector")
check("FL12 ballesamler før aflæsningsvogn", F.roleFromSpecs({canBaleCollect = true, canUnload = true}), "baleCollector")
check("FL12 høster før presser", F.roleFromSpecs({spec_combine = true, spec_baler = true}), "harvester")
-- FL-spreder: tom spreder med rollerne {gødning, kalk} vælges til kalkning; spreder med forkert vare
-- på (kalk, skal gøde) kun hvis ingen anden kan — og den nærmeste med rigtig/ingen vare først
local spreaders = {
{id = "kalk", role = "limer", roles = {fertilizer = true, limer = true}, filledRole = "limer", enabled = true, x = 1, z = 0, width = 12},
{id = "tom", role = "fertilizer", roles = {fertilizer = true, limer = true}, enabled = true, x = 50, z = 0, width = 12},
}
check("FLS tom spreder tager kalkning", F.pick({spreaders[2]}, "limer", 0, 0) and F.pick({spreaders[2]}, "limer", 0, 0).id, "tom")
check("FLS gødskning: tom før nærmere med kalk på", F.pick(spreaders, "fertilizer", 0, 0).id, "tom")
check("FLS kun kalk-sprederen: bruges (tømmes)", F.pick({spreaders[1]}, "fertilizer", 0, 0).id, "kalk")
check("FLS kalkning: den med kalk på foretrækkes", F.pick(spreaders, "limer", 0, 0).id, "kalk")
-- FLS2: fuld af en anden vare (ikke dumpable) bruges slet ikke til opgaven
local fullOther = {id = "fuld", role = "fertilizer", roles = {fertilizer = true, weeder = true}, filledRole = "fertilizer", dumpable = false, enabled = true, x = 0, z = 0, width = 30}
check("FLS2 fuld gødningssprøjte bruges ikke til ukrudt", F.pick({fullOther}, "weeder", 0, 0), nil)
check("FLS2 men gerne til gødning", F.pick({fullOther}, "fertilizer", 0, 0).id, "fuld")
-- FLD: faste maskiner pr. mark
local sprayA = {id = "sprøjte", role = "fertilizer"}
local sprayB = {id = "spreder", role = "fertilizer"}
local harve = {id = "harve", role = "cultivator"}
local all = {sprayA, sprayB, harve}
local byField = {[22] = {"sprøjte"}}
local function ids(list) local out = {} for _, c in ipairs(list) do table.insert(out, c.id) end return table.concat(out, ",") end
check("FLD F22 gødskning: kun sin faste", ids(F.filterDedicated(all, 22, "fertilizer", byField)), "sprøjte")
check("FLD F109 gødskning: ikke F22's", ids(F.filterDedicated(all, 109, "fertilizer", byField)), "spreder,harve")
check("FLD F22 kultivering: ingen fast med rollen -> frie", ids(F.filterDedicated(all, 22, "cultivator", byField)), "spreder,harve")
check("FLD uden faste = alle", ids(F.filterDedicated(all, 5, "fertilizer", {})), "sprøjte,spreder,harve")
local moved = F.dedicate(byField, 109, "sprøjte", true)
check("FLD flyt: væk fra F22", #moved[22], 0)
check("FLD flyt: fast på F109", moved[109][1], "sprøjte")
check("FLD original urørt", byField[22][1], "sprøjte")
check("FLD fjern", #F.dedicate(moved, 109, "sprøjte", false)[109], 0)
-- FLV: menuen skjuler maskiner der er faste på en anden mark end den valgte
local vrigs = {{id = "vogn"}, {id = "harve"}, {id = "plov"}}
local vis, hid, flds = F.visibleForField(vrigs, 18, {[22] = {"vogn", "harve"}})
check("FLV F18 ser kun de frie", #vis .. "/" .. hid .. "/" .. tostring(flds[1]), "1/2/22")
check("FLV F22 ser alle", #(F.visibleForField(vrigs, 22, {[22] = {"vogn", "harve"}})), 3)
check("FLV ingen valgt = alle", #(F.visibleForField(vrigs, nil, {[22] = {"vogn"}})), 3)
-- FL-plov: FS25-plove har også spec_cultivator — de er plove, men må også tage kultivering
check("FLP plov med kultivator-spec er plov", F.roleFromSpecs({spec_plow = true, spec_cultivator = true}), "plow")
check("FLP plov kan kultivere", F.rolesFor("plow").cultivator, true)
check("FLP plov kan pløje", F.rolesFor("plow").plow, true)
check("FLP kultivator kan ikke pløje", F.rolesFor("cultivator"), nil)
local plough = {id = "p", role = "plow", roles = F.rolesFor("plow"), enabled = true, x = 0, z = 0, width = 3}
check("FLP plov vælges til kultivering", (F.pick({plough}, "cultivator", 0, 0, nil)).id, "p")
check("FLP rolesFor giver en kopi", F.rolesFor("plow") ~= F.rolesFor("plow"), true)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-255
View File
@@ -1,255 +0,0 @@
-- Kør: luajit tests/test_adFieldGuide.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adGuide.lua")
dofile("FS25_ADSmartPickup/adFieldModel.lua")
dofile("FS25_ADSmartPickup/adFieldFleet.lua")
dofile("FS25_ADSmartPickup/adFlowView.lua")
dofile("FS25_ADSmartPickup/adFieldGuide.lua")
local FG, G = ADFieldGuide, ADGuide
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
local SOWABLE = {{name = "WHEAT", title = "Hvede"}, {name = "BARLEY", title = "Byg"}, {name = "GRASS", title = "Græs"}, {name = "MAIZE", title = "Majs"}}
local RIGS = {
{id = "v1", text = "Fendt 1 — slåmaskine", role = "mower"},
{id = "v2", text = "Claas 2 — mejetærsker", role = "harvester"},
{id = "v3", text = "Sprøjte 3 — sprøjte", role = "weeder"},
{id = "v4", text = "Vogn 4 — læssevogn", role = "collector"},
}
local function ctx(info, extra)
local context = {
info = info, sowable = SOWABLE, mowable = {GRASS = true}, straw = {WHEAT = true, BARLEY = true},
rigs = RIGS, rigIdsByField = {[90] = {"v3"}}, fieldSilos = {}, storeHidden = 0,
stores = info.kind ~= "bare" and {{id = "s1", text = "Silo A, 300 m"}, {id = "s2", text = "Silo B, 1,2 km"}} or nil,
fields = {{id = 1, text = "F1 Græs"}, {id = 2, text = "F2 Hvede"}},
}
for key, value in pairs(extra or {}) do context[key] = value end
return context
end
local GRASS = {id = 1, kind = "mowable", fruitName = "GRASS", cropTitle = "Græs", markerId = 5}
local WHEAT = {id = 2, kind = "harvest", fruitName = "WHEAT", cropTitle = "Hvede", markerId = 6}
local MAIZE = {id = 3, kind = "harvest", fruitName = "MAIZE", cropTitle = "Majs", markerId = 7}
local BARE = {id = 4, kind = "bare", markerId = 8}
local function steps(guide) return table.concat(G.visibleSteps(guide), ",") end
local function field(id, plan)
local f = ADFieldModel.newField(id)
return ADFieldModel.with(f, {plan = plan or {}})
end
local function findRow(rows, id, item)
for _, row in ipairs(rows) do
if row.id == id and (item == nil or row.item == item) then return row end
end
return nil
end
local function countType(rows, rowType)
local n = 0
for _, row in ipairs(rows) do if row.type == rowType then n = n + 1 end end
return n
end
-- ---- synlige trin pr. marktype ----
check("FG1 græs", steps(FG.open(field(1), ctx(GRASS))), "crop,cropChoice,care,machines,store,summary")
check("FG1 hvede m. halm", steps(FG.open(field(2), ctx(WHEAT))), "crop,cropChoice,prep,care,machines,store,summary")
check("FG1 majs uden halm", steps(FG.open(field(3), ctx(MAIZE))), "crop,prep,care,machines,store,summary")
check("FG1 bar mark", steps(FG.open(field(4), ctx(BARE))), "crop,prep,machines,summary")
check("FG1 halm ukendt -> vis", steps(FG.open(field(3), ctx(MAIZE, {straw = {}}))), "crop,cropChoice,prep,care,machines,store,summary")
check("FG1 hvede med mål græs", steps(FG.open(field(2, {targetFruit = "GRASS"}), ctx(WHEAT))), "crop,cropChoice,prep,care,machines,store,summary")
check("FG1 bar med mål hvede", steps(FG.open(field(4, {targetFruit = "WHEAT"}), ctx(BARE))), "crop,cropChoice,prep,care,machines,summary")
check("FG1 græs med mål græs: ingen klargøring", steps(FG.open(field(1, {targetFruit = "GRASS"}), ctx(GRASS))), "crop,cropChoice,care,machines,store,summary")
check("FG1 græs med mål hvede: klargøring", steps(FG.open(field(1, {targetFruit = "WHEAT"}), ctx(GRASS))), "crop,cropChoice,prep,care,machines,store,summary")
local picker = FG.openPicker(ctx(GRASS))
check("FG1 vælger: kun mark", steps(picker), "field")
check("FG1 vælger: trin", picker.step, "field")
check("FG1 vælger: position uden total", (G.position(picker)), 1)
-- ---- standarder og udfyldning ----
local g = FG.open(field(2), ctx(WHEAT))
check("FG2 starter på afgrøde", g.step, "crop")
check("FG2 klargøring auto", g.draft.prepAuto, true)
check("FG2 ingen faste", g.draft.useDedicated, false)
check("FG2 lager auto", g.draft.storeMode, "auto")
check("FG2 kladde er kopi", g.draft.plan ~= nil and g.draft.fieldId, 2)
local plowed = FG.open(field(2, {soil = "plow"}), ctx(WHEAT))
check("FG2 pløj -> vælg selv", plowed.draft.prepAuto, false)
local withRigs = FG.open(field(2, {rigIds = {"v2"}}), ctx(WHEAT))
check("FG2 faste maskiner -> ja", withRigs.draft.useDedicated, true)
local silo = FG.open(field(1, {storeBuildingIds = {"m1"}}), ctx(GRASS, {fieldSilos = {{id = "m1", text = "Marksilo"}}}))
check("FG2 marksilo valgt", silo.draft.storeMode, "fieldSilo")
local manual = FG.open(field(1, {storeBuildingIds = {"s2"}}), ctx(GRASS))
check("FG2 valgt silo -> vælg selv", manual.draft.storeMode, "manual")
check("FG2 valgt silo i listen", manual.draft.storeIds[1], "s2")
-- ---- choose er immutabel ----
local chosen = FG.choose(g, "targetFruit", "BARLEY")
check("FG3 ny kladde", chosen.draft.plan.targetFruit, "BARLEY")
check("FG3 gammel urørt", g.draft.plan.targetFruit, "")
local toggled = FG.choose(withRigs, "rigIds", {item = "v1", on = true})
check("FG3 check tilføjer", table.concat(toggled.draft.plan.rigIds, ","), "v2,v1")
check("FG3 gammel liste urørt", table.concat(withRigs.draft.plan.rigIds, ","), "v2")
check("FG3 check fjerner", table.concat(FG.choose(toggled, "rigIds", {item = "v2", on = false}).draft.plan.rigIds, ","), "v1")
check("FG3 produkt hø", FG.choose(FG.open(field(1), ctx(GRASS)), "product", true).draft.plan.product, "hay")
check("FG3 produkt ensilage", FG.choose(FG.open(field(1), ctx(GRASS)), "product", false).draft.plan.product, "silage")
-- ---- rækker ----
local cropRows = FG.rows(g)
local target = findRow(cropRows, "targetFruit")
check("FG4 afgrøde: valg", target.type, "choice")
check("FG4 afgrøde: behold først", target.options[1].value, "")
check("FG4 afgrøde: såbare", #target.options, 5)
check("FG4 så igen for høstmark", findRow(cropRows, "sow") ~= nil, true)
check("FG4 så igen skjult med mål", findRow(FG.rows(chosen), "sow"), nil)
check("FG4 så igen skjult på græs", findRow(FG.rows(FG.open(field(1), ctx(GRASS))), "sow"), nil)
local choiceGrass = FG.rows(G.jump(FG.open(field(1), ctx(GRASS)), "cropChoice", "close"))
check("FG5 græs: ensilage/hø", findRow(choiceGrass, "product") ~= nil, true)
check("FG5 græs: river", findRow(choiceGrass, "rake") ~= nil, true)
check("FG5 græs: ingen halm", findRow(choiceGrass, "straw"), nil)
local choiceWheat = FG.rows(G.jump(g, "cropChoice", "close"))
check("FG5 hvede: halm", findRow(choiceWheat, "straw").type, "choice")
check("FG5 hvede: ingen ensilage", findRow(choiceWheat, "product"), nil)
local prepRows = FG.rows(G.jump(g, "prep", "close"))
check("FG6 klargøring auto: kun valget + tekst", findRow(prepRows, "soil"), nil)
check("FG6 klargøring valg", findRow(prepRows, "prepAuto").type, "choice")
local prepManual = FG.rows(G.jump(FG.choose(g, "prepAuto", false), "prep", "close"))
check("FG6 vælg selv: jord", findRow(prepManual, "soil") ~= nil, true)
check("FG6 vælg selv: kalk", findRow(prepManual, "lime") ~= nil, true)
check("FG6 vælg selv: mulch", findRow(prepManual, "mulch") ~= nil, true)
check("FG6 vælg selv: ingen tromle uden såning", findRow(prepManual, "roll"), nil)
-- pleje: Automatisk/Manuelt som klargøring (brugerønske 29/9); knapperne vises under Manuelt
local careAutoRows = FG.rows(G.jump(g, "care", "close"))
check("FG7 pleje: valg auto/manuel", findRow(careAutoRows, "careAuto").type, "choice")
check("FG7 pleje auto: ingen knapper", findRow(careAutoRows, "fertilize"), nil)
check("FG7 standard: pleje auto", g.draft.careAuto, true)
check("FG7 uden gødning -> manuel", FG.open(field(2, {fertilize = false}), ctx(WHEAT)).draft.careAuto, false)
local autoCare = FG.result(FG.choose(FG.open(field(2, {fertilize = false, weed = false}), ctx(WHEAT)), "careAuto", true), true).plan
check("FG7 auto slår gødning til", autoCare.fertilize, true)
check("FG7 auto slår ukrudt til", autoCare.weed, true)
local careGrass = FG.rows(G.jump(FG.choose(FG.open(field(1), ctx(GRASS)), "careAuto", false), "care", "close"))
check("FG7 græs: ingen ukrudt", findRow(careGrass, "weed"), nil)
check("FG7 græs: kalk i pleje", findRow(careGrass, "lime") ~= nil, true)
local careWheat = FG.rows(G.jump(FG.choose(g, "careAuto", false), "care", "close"))
check("FG7 hvede: ukrudt", findRow(careWheat, "weed") ~= nil, true)
check("FG7 hvede: kalk ikke i pleje", findRow(careWheat, "lime"), nil)
local machinesOff = FG.rows(G.jump(g, "machines", "close"))
check("FG8 antal ad gangen", findRow(machinesOff, "rigs").type, "choice")
check("FG8 aflæsningsvogne ved høst", findRow(machinesOff, "unloaders") ~= nil, true)
check("FG8 ingen liste uden faste", countType(machinesOff, "check"), 0)
check("FG8 ingen aflæsningsvogne på græs", findRow(FG.rows(G.jump(FG.open(field(1), ctx(GRASS)), "machines", "close")), "unloaders"), nil)
local machinesOn = FG.rows(G.jump(FG.choose(g, "useDedicated", true), "machines", "close"))
check("FG8 liste: v3 fast på F90 skjult", findRow(machinesOn, "rigIds", "v3"), nil)
check("FG8 liste: tre synlige", countType(machinesOn, "check"), 3)
local hiddenText = nil
for _, row in ipairs(machinesOn) do if row.type == "text" and row.label.key == "spu_field_hiddenDedicated" then hiddenText = row end end
check("FG8 tekst om skjulte", hiddenText ~= nil and hiddenText.label.args[1], 1)
check("FG8 tekst marker", hiddenText ~= nil and hiddenText.label.args[2], "F90")
local ownFirst = FG.rows(G.jump(withRigs, "machines", "close"))
local firstCheck = nil
for _, row in ipairs(ownFirst) do if row.type == "check" and firstCheck == nil then firstCheck = row end end
check("FG8 egne først", firstCheck.item, "v2")
check("FG8 egne sat", firstCheck.value, true)
local storeAuto = FG.rows(G.jump(FG.open(field(1), ctx(GRASS)), "store", "close"))
check("FG9 ingen marksilo-spørgsmål uden marksilo", findRow(storeAuto, "useFieldSilo"), nil)
check("FG9 automatisk/vælg selv", findRow(storeAuto, "storeAuto").type, "choice")
check("FG9 auto: ingen siloliste", countType(storeAuto, "check"), 0)
local storeManual = FG.rows(G.jump(FG.choose(FG.open(field(1), ctx(GRASS)), "storeAuto", false), "store", "close"))
check("FG9 vælg selv: siloer", countType(storeManual, "check"), 2)
local siloCtx = ctx(GRASS, {fieldSilos = {{id = "m1", text = "Marksilo F1"}}})
local storeSilo = FG.rows(G.jump(FG.open(field(1), siloCtx), "store", "close"))
check("FG9 marksilo-spørgsmål", findRow(storeSilo, "useFieldSilo").type, "toggle")
local useSilo = FG.choose(FG.open(field(1), siloCtx), "useFieldSilo", true)
check("FG9 marksilo ja -> skjul auto", findRow(FG.rows(G.jump(useSilo, "store", "close")), "storeAuto"), nil)
local pickRows = FG.rows(picker)
check("FG10 vælger: links", countType(pickRows, "link"), 2)
check("FG10 vælger: id", findRow(pickRows, "field").value, 1)
local picked = FG.pickField(picker, field(2), ctx(WHEAT))
check("FG10 valgt mark", picked.draft.fieldId, 2)
check("FG10 videre til afgrøde", picked.step, "crop")
check("FG10 mark-trin stadig talt", (G.position(picked)), 2)
-- ---- resultat ----
local r = FG.result(g, true)
check("FG11 auto ved start", r.plan.mode, "auto")
check("FG11 manuel uden start", FG.result(g, false).plan.mode, "manual")
local autoField = FG.open(field(2, {mode = "auto"}), ctx(WHEAT))
check("FG11 Ret bevarer auto", FG.result(autoField, nil).plan.mode, "auto")
check("FG11 Ret bevarer manuel", FG.result(g, nil).plan.mode, "manual")
check("FG11 mål tvinger auto", FG.result(chosen, false).plan.mode, "auto")
check("FG11 mål ændret", FG.result(chosen, true).targetChanged, true)
check("FG11 mål uændret", r.targetChanged, false)
check("FG11 ingen rigIds i plan", r.plan.rigIds, nil)
local soilPlan = FG.result(FG.open(field(2, {soil = "plow", mulch = true, lime = false, roll = true}), ctx(WHEAT)), true).plan
check("FG12 vælg selv bevarer jord", soilPlan.soil, "plow")
local autoPrep = FG.result(FG.choose(FG.open(field(2, {soil = "plow", mulch = true, lime = false, roll = true}), ctx(WHEAT)), "prepAuto", true), true).plan
check("FG12 auto: jord", autoPrep.soil, "auto")
check("FG12 auto: mulch", autoPrep.mulch, false)
check("FG12 auto: kalk", autoPrep.lime, true)
check("FG12 auto: tromle", autoPrep.roll, false)
local grassNoPrep = FG.result(FG.choose(FG.open(field(1, {lime = false}), ctx(GRASS)), "prepAuto", true), true).plan
check("FG12 græs: auto rører ikke kalk", grassNoPrep.lime, false)
check("FG13 lager auto", #FG.result(FG.open(field(1, {storeBuildingIds = {"s1"}}), ctx(GRASS)) and FG.choose(FG.open(field(1, {storeBuildingIds = {"s1"}}), ctx(GRASS)), "storeAuto", true), true).plan.storeBuildingIds, 0)
check("FG13 lager vælg selv", table.concat(FG.result(FG.choose(FG.choose(FG.open(field(1), ctx(GRASS)), "storeAuto", false), "storeBuildingIds", {item = "s2", on = true}), true).plan.storeBuildingIds, ","), "s2")
check("FG13 lager marksilo", table.concat(FG.result(useSilo, true).plan.storeBuildingIds, ","), "m1")
check("FG13 bar mark bevarer lager", table.concat(FG.result(FG.open(field(4, {storeBuildingIds = {"s9"}}), ctx(BARE)), true).plan.storeBuildingIds, ","), "s9")
check("FG14 faste fra -> tom", #FG.result(FG.choose(withRigs, "useDedicated", false), true).rigIds, 0)
check("FG14 faste til", table.concat(FG.result(toggled, true).rigIds, ","), "v2,v1")
-- ---- faste maskiner på tværs af marker ----
local byField = FG.applyRigs({[90] = {"v3", "v5"}, [2] = {"v2"}}, 2, {"v2", "v3"})
check("FG15 fjernet fra anden mark", table.concat(byField[90], ","), "v5")
check("FG15 sat på marken", table.concat(byField[2], ","), "v2,v3")
check("FG15 ny mark", table.concat(FG.applyRigs({}, 7, {"v1"})[7], ","), "v1")
-- ---- resumé ----
local function lineFor(lines, step)
for _, line in ipairs(lines) do if line.step == step then return line end end
return nil
end
local function partKeys(line)
local keys = {}
for _, part in ipairs(line.parts) do table.insert(keys, part.key or ("=" .. tostring(part.text))) end
return table.concat(keys, ",")
end
local sum = FG.summary(g)
check("FG16 linjer for synlige trin", #sum, 6)
check("FG16 afgrøde behold", partKeys(lineFor(sum, "crop")), "spu_guide_crop_keep")
check("FG16 halm", partKeys(lineFor(sum, "cropChoice")), "spu_guide_sum_straw_chop")
check("FG16 klargøring auto", partKeys(lineFor(sum, "prep")), "spu_guide_sum_auto")
check("FG16 pleje auto", partKeys(lineFor(sum, "care")), "spu_guide_sum_auto")
check("FG16 pleje manuel", partKeys(lineFor(FG.summary(FG.choose(g, "careAuto", false)), "care")), "spu_guide_sum_fert,spu_guide_sum_weed")
check("FG16 maskiner", partKeys(lineFor(sum, "machines")), "spu_guide_sum_freeRigs,spu_guide_sum_rigs,spu_guide_sum_unloaders")
check("FG16 lager auto", partKeys(lineFor(sum, "store")), "spu_guide_sum_auto")
check("FG16 ingen mark/summary-linje", lineFor(sum, "summary"), nil)
local sumTarget = FG.summary(chosen)
check("FG17 mål", partKeys(lineFor(sumTarget, "crop")), "spu_guide_sum_target")
check("FG17 mål titel", lineFor(sumTarget, "crop").parts[1].args[1], "Byg")
local sumRigs = FG.summary(withRigs)
check("FG17 faste", lineFor(sumRigs, "machines").parts[1].key, "spu_guide_sum_dedicated")
check("FG17 faste antal", lineFor(sumRigs, "machines").parts[1].args[1], 1)
local sumManual = FG.summary(FG.open(field(2, {soil = "plow", lime = true, mulch = true}), ctx(WHEAT)))
check("FG17 klargøring vælg selv", partKeys(lineFor(sumManual, "prep")), "spu_field_soil_plow,spu_guide_sum_lime,spu_guide_sum_mulch")
check("FG17 lager valgt silo", partKeys(lineFor(FG.summary(manual), "store")), "=Silo B, 1,2 km")
check("FG17 lager marksilo", partKeys(lineFor(FG.summary(useSilo), "store")), "spu_guide_sum_fieldSilo")
local sumHay = FG.summary(FG.choose(FG.choose(FG.open(field(1), ctx(GRASS)), "product", true), "rake", true))
check("FG17 hø + river", partKeys(lineFor(sumHay, "cropChoice")), "spu_field_plan_hay,spu_guide_sum_rake")
local sumNoCare = FG.summary(FG.choose(FG.choose(FG.choose(g, "careAuto", false), "fertilize", false), "weed", false))
check("FG17 ingen pleje", partKeys(lineFor(sumNoCare, "care")), "spu_guide_sum_noCare")
local sumSow = FG.summary(FG.choose(g, "sow", true))
check("FG17 så igen", partKeys(lineFor(sumSow, "crop")), "spu_guide_crop_keep,spu_guide_sum_sowAgain")
-- ---- oversigts-trinnet: links pr. linje ----
local summaryRows = FG.rows(G.new(g.def, g.draft, g.context, {step = "summary"}))
check("FG18 oversigt: link pr. linje", countType(summaryRows, "link"), 6)
check("FG18 oversigt: link-id", findRow(summaryRows, "jump").value, "crop")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-222
View File
@@ -1,222 +0,0 @@
-- Kør: luajit tests/test_adFieldJobs.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFieldModel.lua")
dofile("FS25_ADSmartPickup/adFieldJobs.lua")
local J = ADFieldJobs
local M = ADFieldModel
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
local info = {id = 109, markerId = 5, mowable = true, readyRatio = 1}
-- J1: klar auto-mark med markør -> start; uden markør -> vent
local auto = M.onReadiness(M.newField(109), 1); auto.plan.mode = "auto"
check("J1 start", J.decide(auto, info, {}, 0).action, "start")
check("J1 uden markør", J.decide(auto, {id = 109, mowable = true}, {}, 0).action, "wait")
-- J2: arbejdende trin uden sæt -> send rolle; alle sæt færdige -> næste trin
local mowing = M.start(auto)
local wanted = J.decide(mowing, info, {}, 0)
check("J2 send slåmaskine", wanted.action, "dispatch"); check("J2 rolle", wanted.role, "mower")
check("J2 sæt i gang -> ingenting", J.decide(mowing, info, {{phase = "working"}}, 0).action, "none")
local mowedInfo = {id = 109, markerId = 5, mowable = true, readyRatio = 0}
check("J2 færdig -> videre", J.decide(mowing, mowedInfo, {{phase = "finished"}}, 0).action, "advance")
-- J3: sæt frigivet (spilleren tog det) tæller ikke som færdigt
check("J3 frigivet -> send nyt", J.decide(mowing, info, {{phase = "released"}}, 0).action, "dispatch")
-- J4: slåning færdig men marken stadig klar -> ikke videre (afbrudt)
check("J4 afbrudt slåning", J.decide(mowing, {id = 109, markerId = 5, mowable = true, readyRatio = 0.8}, {{phase = "finished"}}, 0).action, "dispatch")
-- J5: opsamling efter genindlæsning uden sæt -> send læssevogn
local collecting = M.advance(mowing)
check("J5 genoptag opsamling", J.decide(collecting, info, {}, 0).role, "collector")
check("J5 to vogne maks", J.decide(collecting, info, {{phase = "working"}, {phase = "driving"}}, 0).action, "none")
-- J6: Courseplay efterlader et hjørne -> efter 2 færdige slå-ture går marken videre alligevel
local standing = {id = 109, markerId = 5, mowable = true, readyRatio = 0.8}
check("J6 én tur -> slå igen", J.decide(mowing, standing, {{phase = "finished"}}, 0).action, "dispatch")
check("J6 to ture -> videre", J.decide(mowing, standing, {{phase = "finished"}, {phase = "finished"}}, 0).action, "advance")
-- J7: slået mark uden sæt (fx sluppet undervejs) -> videre til opsamling, ikke en ny slå-tur
check("J7 slået uden sæt -> videre", J.decide(mowing, {id = 109, markerId = 5, mowable = true, readyRatio = 0.1}, {}, 0).action, "advance")
check("J7 stående uden sæt -> slå", J.decide(mowing, info, {}, 0).action, "dispatch")
check("J7 slået men sæt i gang -> vent", J.decide(mowing, {id = 109, markerId = 5, mowable = true, readyRatio = 0.1}, {{phase = "working"}}, 0).action, "none")
-- J8: høst — høster først (hovedrolle), aflæsningsvogne kun mens høsteren arbejder
local harvestInfo = {id = 50, markerId = 7, kind = "harvest", readyRatio = 1, fruitIndex = 3}
local harvesting = M.start(M.onReadiness(M.newField(50), 1), "harvest", {})
local d = J.decide(harvesting, harvestInfo, {}, 0)
check("J8 send høster", d.role, "harvester"); check("J8 ikke hjælper", d.helper, false)
local withHarvester = {{phase = "working", stage = "harvesting", helper = false}}
local dHelper = J.decide(harvesting, harvestInfo, withHarvester, 0)
check("J8 så aflæsningsvogn", dHelper.role, "unloader"); check("J8 hjælper", dHelper.helper, true)
check("J8 vogn på plads -> intet", J.decide(harvesting, harvestInfo, {withHarvester[1], {phase = "working", stage = "harvesting", helper = true}}, 0).action, "none")
-- manuel transport: høsteren venter på spilleren -> ingen høster mere og endnu ingen tømmevogn
local awaiting = {{phase = "awaitingPlayer", stage = "harvesting", helper = false}}
check("J8b venter på spilleren -> intet (ingen ny høster, ingen tømmevogn)", J.decide(harvesting, harvestInfo, awaiting, 0).action, "none")
-- høsteren færdig, afgrøden væk (marken er nu bar) -> videre, selv om vognen stadig kører
local afterInfo = {id = 50, markerId = 7, kind = "bare", readyRatio = 0}
check("J8 høst færdig -> videre", J.decide(harvesting, afterInfo, {{phase = "finished", stage = "harvesting"}, {phase = "working", stage = "harvesting", helper = true}}, 0).action, "advance")
-- sæt fra et tidligere trin (vogn på vej hjem) tæller ikke på det nye trin
local cultivating = M.advance(harvesting, {})
check("J8 gammel vogn tæller ikke", J.decide(cultivating, afterInfo, {{phase = "working", stage = "harvesting", helper = true}}, 0).role, "cultivator")
-- J9: multi-tool — hele holdet sendes samlet; mens nogen kører, sendes ingen nye hovedsæt
local multiMow = M.with(mowing, {plan = {rigs = 3}})
local dMulti = J.decide(multiMow, info, {}, 0)
check("J9 tre slåmaskiner", dMulti.count, 3)
check("J9 én kører stadig -> ingen nye", J.decide(multiMow, info, {{phase = "working"}, {phase = "finished"}}, 0).action, "none")
-- J10: pleje under vækst — Auto starter, Manuel viser behovet
local growingInfo = {id = 60, markerId = 8, kind = "harvest", growing = true, fert = 0, fertMax = 2, lime = 2, weed = 0, readyRatio = 0.2}
local autoField = M.with(M.newField(60), {plan = {mode = "auto"}})
local care = J.decide(autoField, growingInfo, {}, 0)
check("J10 auto -> pleje", care.action, "care"); check("J10 gødning", care.stage, "fertilizing")
local manualCare = J.decide(M.newField(60), growingInfo, {}, 0)
check("J10 manuel -> intet, men behov vist", manualCare.action, "none"); check("J10 behov", manualCare.care, "fertilizing")
check("J10 Start nu -> pleje", J.decide(M.with(M.newField(60), {startRequested = true}), growingInfo, {}, 0).action, "care")
check("J10 uden markør -> vent", J.decide(autoField, {id = 60, kind = "harvest", growing = true, fert = 0, fertMax = 2}, {}, 0).action, "wait")
-- J11: regn — vejrfølsomme trin sendes ikke ud (kørende sæt fortsætter); klargøring/pleje kører videre
local rain = {raining = true}
local dRain = J.decide(mowing, info, {}, 0, rain)
check("J11 slåning venter i regn", dRain.action, "wait"); check("J11 grund", dRain.reason, "rain")
check("J11 kørende slåmaskine fortsætter", J.decide(mowing, info, {{phase = "working"}}, 0, rain).action, "none")
check("J11 høst venter", J.decide(harvesting, harvestInfo, {}, 0, rain).reason, "rain")
check("J11 kultivering kører i regn", J.decide(cultivating, afterInfo, {}, 0, rain).role, "cultivator")
check("J11 start af høst venter", J.decide(M.with(M.onReadiness(M.newField(51), 1), {startRequested = true, kind = "harvest"}), harvestInfo, {}, 0, rain).reason, "rain")
local silageCollect = M.advance(mowing)
check("J11 ensilage-opsamling kører i regn", J.decide(silageCollect, info, {}, 0, rain).action, "dispatch")
local hayCollect = M.with(silageCollect, {plan = {product = "hay"}})
check("J11 hø-opsamling venter", J.decide(hayCollect, info, {}, 0, rain).reason, "rain")
check("J11 tørvejr", J.decide(mowing, info, {}, 0, {raining = false}).action, "dispatch")
-- J12: bar mark med mål-afgrøde klargøres straks — kun selve såningen venter på såtiden
local bareInfo = {id = 90, markerId = 9, kind = "bare", readyRatio = 0, needs = {}}
local bareTarget = M.with(M.newField(90), {plan = {targetFruit = "WHEAT", mode = "auto"}})
local canSow = {plantable = function(name) return name == "WHEAT" end}
check("J12 bar + såtid -> start", J.decide(bareTarget, bareInfo, {}, 0, canSow).action, "start")
check("J12 bar + ikke såtid -> start alligevel (klargøring)", J.decide(bareTarget, bareInfo, {}, 0, {plantable = function() return false end}).action, "start")
check("J12 bar uden markør -> vent", J.decide(bareTarget, {id = 90, kind = "bare"}, {}, 0, canSow).action, "wait")
check("J12 voksende mark med mål -> lad den vokse", J.decide(bareTarget, {id = 90, markerId = 9, kind = "harvest", growing = true, readyRatio = 0.3}, {}, 0, canSow).action, "none")
-- J13: afbrudt slåning efter genindlæsning (in-game 27/9 F90): genvækst i mellemhøjde -> vent; hjørne efter en tur -> slå straks
local regrowing = {id = 90, markerId = 9, mowable = true, readyRatio = 0.5, immatureRatio = 0.5}
local dRegrow = J.decide(mowing, regrowing, {}, 0)
check("J13 genvækst -> vent", dRegrow.action, "wait"); check("J13 grund", dRegrow.reason, "regrowing")
check("J13 genvækst men tur færdig -> slå hjørnet", J.decide(mowing, regrowing, {{phase = "finished"}}, 0).action, "dispatch")
check("J13 halvt slået uden genvækst -> slå resten", J.decide(mowing, {id = 90, markerId = 9, mowable = true, readyRatio = 0.6, immatureRatio = 0}, {}, 0).action, "dispatch")
-- J14: kultivering på en mark med uhøstede kartofler (in-game 27/9 F16) -> tilbage til klar; rest efter høst -> ikke
local cultivatingField = M.with(auto, {stage = "cultivating", kind = "harvest"})
check("J14 uhøstet -> spol tilbage", J.decide(cultivatingField, {id = 16, markerId = 9, kind = "harvest", phase = "ready", readyRatio = 1}, {}, 0).action, "rewind")
check("J14 høstrest 30 % -> kultiver", J.decide(cultivatingField, {id = 16, markerId = 9, kind = "harvest", phase = "ready", readyRatio = 0.3}, {}, 0).action ~= "rewind", true)
check("J14 stub -> kultiver", J.decide(cultivatingField, {id = 16, markerId = 9, kind = "bare", phase = "cut", readyRatio = 0}, {}, 0).action ~= "rewind", true)
-- resumeField: efter genindlæsning stopper spillet alle arbejdere; en CP-maskine der står PÅ en mark hvis
-- aktuelle trin passer til maskinens rolle, meldes ind på den mark igen (in-game 27/9)
do
local square = {{x = 0, z = 0}, {x = 100, z = 0}, {x = 100, z = 100}, {x = 0, z = 100}}
local fields = {
{id = 90, stage = "collecting", verts = square},
{id = 111, stage = "harvesting", verts = {{x = 200, z = 0}, {x = 300, z = 0}, {x = 300, z = 100}, {x = 200, z = 100}}},
}
check("RF høster på F111", ADFieldJobs.resumeField(250, 50, "harvester", fields), 111)
check("RF læssevogn på F90", ADFieldJobs.resumeField(50, 50, "collector", fields), 90)
check("RF forkert rolle til trinnet -> nil", ADFieldJobs.resumeField(250, 50, "collector", fields), nil)
check("RF uden for alle marker -> nil", ADFieldJobs.resumeField(150, 50, "harvester", fields), nil)
check("RF mark uden omrids -> nil", ADFieldJobs.resumeField(50, 50, "collector", {{id = 1, stage = "collecting", verts = {}}}), nil)
check("RF på kanten tæller som indenfor", ADFieldJobs.resumeField(0.5, 50, "collector", fields), 90)
end
-- startAt: hvor Courseplay skal starte i kurset (CP's standard er "first" = forfra, også på en halvt høstet mark;
-- in-game 27/9: mejetærskeren startede F111 forfra)
check("SA eget kursus med husket waypoint -> last", ADFieldJobs.startAtFor({reuse = true, remembered = true, stage = "harvesting", standingRatio = 0.4}), "last")
check("SA halvt høstet -> nearest", ADFieldJobs.startAtFor({stage = "harvesting", standingRatio = 0.4}), "nearest")
check("SA halvt slået -> nearest", ADFieldJobs.startAtFor({stage = "mowing", standingRatio = 0.5}), "nearest")
check("SA urørt mark -> first", ADFieldJobs.startAtFor({stage = "harvesting", standingRatio = 1}), "first")
check("SA næsten urørt (97 %) -> first", ADFieldJobs.startAtFor({stage = "harvesting", standingRatio = 0.97}), "first")
check("SA ukendt fremdrift -> first", ADFieldJobs.startAtFor({stage = "harvesting"}), "first")
check("SA klargøring (ingen afgrøde at måle) -> first", ADFieldJobs.startAtFor({stage = "cultivating", standingRatio = 0.3}), "first")
check("SA husket waypoint uden eget kursus -> ikke last", ADFieldJobs.startAtFor({reuse = false, remembered = true, stage = "cultivating"}), "first")
-- shouldResume: kun maskiner der stadig er midt i jobbet genstartes (in-game 27/9: to ROPA'er med et husket
-- waypoint fra et gammelt job kørte ud ad vejene mod det gamle kursus)
check("SR står på en mark i gang med sit trin -> ja", ADFieldJobs.shouldResume({fieldId = 111, distance = 400}), true)
check("SR lige ved sit huskede waypoint -> ja", ADFieldJobs.shouldResume({distance = 12}), true)
check("SR langt fra waypointet og ingen mark -> nej", ADFieldJobs.shouldResume({distance = 850}), false)
check("SR ukendt afstand og ingen mark -> nej", ADFieldJobs.shouldResume({}), false)
-- mayDispatch: markarbejdet sender højst én maskine ud pr. DISPATCH_GAP_MS på tværs af alle marker
-- (in-game 27/9: 6 maskiner kørte fra parkeringen i samme sekund efter Start alle)
check("MD første udsendelse", ADFieldJobs.mayDispatch(5000, nil), true)
check("MD for tæt på forrige", ADFieldJobs.mayDispatch(5000 + ADFieldJobs.DISPATCH_GAP_MS - 1, 5000), false)
check("MD efter mellemrummet", ADFieldJobs.mayDispatch(5000 + ADFieldJobs.DISPATCH_GAP_MS, 5000), true)
-- halvt høstet mark (in-game 27/9 F111): høsten er ikke færdig før næsten intet står; en mark i klargøring med
-- stående afgrøde spoles tilbage
do
local halfInfo = {id = 111, markerId = 5, kind = "harvest", phase = "ready", readyRatio = 0.45, standingRatio = 0.45, fruitIndex = 1}
check("HH halvt høstet uden høster -> send høster (ikke videre)", J.decide(harvesting, halfInfo, {}, 0).action, "dispatch")
local doneInfo = {id = 111, markerId = 5, kind = "harvest", phase = "ready", readyRatio = 0.02, standingRatio = 0.02, fruitIndex = 1}
check("HH næsten intet står -> videre", J.decide(harvesting, doneInfo, {}, 0).action, "advance")
local cultivating = M.with(harvesting, {stage = "cultivating"})
check("HH klargøring med halv mark stående -> spol tilbage", J.decide(cultivating, halfInfo, {}, 0).action, "rewind")
end
-- startTimedOut: CP må beregne kursus i op til 5 min (in-game 27/9 F90: 140 ha tog over 60 s, moden opgav midt i)
check("ST start uden beregning > 60 s -> opgiv", ADFieldJobs.startTimedOut(61000, false), true)
check("ST beregner stadig efter 90 s -> vent", ADFieldJobs.startTimedOut(90000, true), false)
check("ST beregner over 5 min -> opgiv", ADFieldJobs.startTimedOut(301000, true), true)
check("ST under 60 s -> vent", ADFieldJobs.startTimedOut(30000, false), false)
-- shouldEmptyHarvester: høsten er færdig, men høsteren har korn i tanken og står ved marken -> tøm først
-- (in-game 27/9 F15: marken gik til halm med fuld tank, ingen tømmevogn kom)
check("EH korn i tanken ved marken -> tøm", ADFieldJobs.shouldEmptyHarvester({fill = 12000, near = true}), true)
check("EH tom tank -> videre", ADFieldJobs.shouldEmptyHarvester({fill = 0, near = true}), false)
check("EH langt væk -> videre", ADFieldJobs.shouldEmptyHarvester({fill = 12000, near = false}), false)
check("EH optaget -> videre", ADFieldJobs.shouldEmptyHarvester({fill = 12000, near = true, busy = true}), false)
check("EH spiller i -> videre", ADFieldJobs.shouldEmptyHarvester({fill = 12000, near = true, controlled = true}), false)
check("EH allerede forsøgt -> videre", ADFieldJobs.shouldEmptyHarvester({fill = 12000, near = true, tried = true}), false)
-- nearestPoint: CP bruger markens punkt som kursets startsted — tag det nærmest traktoren (in-game 27/9 F17: første
-- punkt lå 280 m væk i den anden ende, og CP kørte gennem skoven)
do
local pts = {{x = 1600, z = -1839}, {x = 1350, z = -1740}, {x = 1500, z = -1800}}
local p = ADFieldJobs.nearestPoint(pts, 1340, -1733)
check("NP nærmest traktoren", p.x .. "," .. p.z, "1350,-1740")
check("NP uden punkter -> nil", ADFieldJobs.nearestPoint({}, 0, 0), nil)
check("NP uden position -> første", ADFieldJobs.nearestPoint(pts, nil, nil).x, 1600)
end
-- pulje: en traktor der henter et redskab tæller som i gang (ellers sendes endnu en ud)
check("pulje: henter -> ingenting", J.decide(mowing, info, {{phase = "fetching"}}, 0).action, "none")
-- JP: klargøring der allerede er gjort (fx efter genindlæsning) går videre uden at sende maskinen ud igen
-- (in-game 28/9: F17 blev kultiveret 3 gange — opgaver gemmes ikke, så "færdig" blev aldrig set)
local cultField = M.with(M.newField(17), {stage = "cultivating", kind = "bare", plan = {mode = "auto"}})
local cultInfo = {id = 17, markerId = 5, kind = "bare", readyRatio = 0, needs = {}}
cultInfo.prepared = 0.98
check("JP kultiveret -> videre", J.decide(cultField, cultInfo, {}, 0).action, "advance")
cultInfo.prepared = 0.4
check("JP halvt kultiveret -> send maskine", J.decide(cultField, cultInfo, {}, 0).action, "dispatch")
cultInfo.prepared = nil
check("JP ukendt jord -> som før (send)", J.decide(cultField, cultInfo, {}, 0).action, "dispatch")
cultInfo.prepared = 0.98
check("JP kultivator i gang -> vent på den", J.decide(cultField, cultInfo, {{phase = "working", stage = "cultivating"}}, 0).action, "none")
local plowField = M.with(cultField, {stage = "plowing"})
check("JP kultiveret er ikke pløjet -> send plov", J.decide(plowField, cultInfo, {}, 0).action, "dispatch")
cultInfo.plowed = 0.97
check("JP pløjet -> videre", J.decide(plowField, cultInfo, {}, 0).action, "advance")
local sowField = M.with(cultField, {stage = "sowing"})
check("JP såning berøres ikke af klargjort jord", J.decide(sowField, cultInfo, {}, 0).action ~= "advance", true)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-324
View File
@@ -1,324 +0,0 @@
-- Kør: luajit tests/test_adFieldModel.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFieldModel.lua")
local M = ADFieldModel
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
-- M1: ny mark = venter, manuel ensilage uden riv
local f = M.newField(109)
check("M1 stage", f.stage, "idle")
check("M1 mode", f.plan.mode, "manual")
check("M1 product auto", f.plan.product, "auto")
check("M1 auto = ensilage på slåafgrøde", M.product(f.plan, "mowable"), "silage")
check("M1 auto = høst ellers", M.product(f.plan, "harvest"), "harvest")
check("M1 rake", f.plan.rake, false)
-- M2: klar ved >= 90 %
check("M2 89 % ikke klar", M.onReadiness(f, 0.89).stage, "idle")
local ready = M.onReadiness(f, 0.9)
check("M2 90 % klar", ready.stage, "ready")
check("M2 immutabel", f.stage, "idle")
-- M3: klar mark slået af spilleren -> tilbage til venter
check("M3 slået manuelt", M.onReadiness(ready, 0.3).stage, "idle")
check("M3 lidt under 90 % forbliver klar", M.onReadiness(ready, 0.8).stage, "ready")
-- M4: trin pr. plan
check("M4 ensilage", table.concat(M.workStages({product = "silage"}), ","), "mowing,collecting")
check("M4 ensilage m. riv", table.concat(M.workStages({product = "silage", rake = true}), ","), "mowing,raking,collecting")
check("M4 hø", table.concat(M.workStages({product = "hay", rake = false}), ","), "mowing,tedding,collecting")
check("M4 hø m. riv", table.concat(M.workStages({product = "hay", rake = true}), ","), "mowing,tedding,raking,collecting")
-- M5: start: auto starter selv, manuel kun på forespørgsel
check("M5 manuel starter ikke", M.shouldStart(ready), false)
local auto = M.onReadiness(M.newField(5), 1)
auto.plan.mode = "auto"
check("M5 auto starter", M.shouldStart(auto), true)
local requested = M.newField(6); requested.stage = "ready"; requested.startRequested = true
check("M5 Start nu", M.shouldStart(requested), true)
local started = M.start(ready)
check("M5 start -> slås", started.stage, "mowing")
check("M5 start rydder forespørgsel", started.startRequested, false)
local oldMow = M.with(ready, {mowVehicleId = "old"})
check("M5 start glemmer gammel slåmaskine", M.start(oldMow).mowVehicleId, nil)
-- M6: videre gennem trinene; færdig mark bliver klar igen når den er vokset
local hay = M.start(M.onReadiness(M.newField(7), 1)); hay.plan.product = "hay"
hay = M.advance(hay); check("M6 hø -> vendes", hay.stage, "tedding")
hay = M.advance(hay); check("M6 -> samles op", hay.stage, "collecting")
hay = M.advance(hay); check("M6 -> færdig", hay.stage, "done")
check("M6 færdig forbliver ved lav andel", M.onReadiness(hay, 0.1).stage, "done")
check("M6 færdig -> klar når vokset", M.onReadiness(hay, 0.95).stage, "ready")
check("M6 arbejdende mark ignorerer andel", M.onReadiness(started, 0).stage, "mowing")
check("M6 isWorking", M.isWorking(started), true)
check("M6 stop -> klar", M.stop(started).stage, "ready")
-- M7: roller, maks sæt, bredde
check("M7 rolle slå", M.roleFor("mowing"), "mower")
check("M7 rolle saml", M.roleFor("collecting"), "collector")
check("M7 rolle klar = ingen", M.roleFor("ready"), nil)
check("M7 maks læssevogne = planens sæt", M.maxRigs("collecting", {rigs = 2}), 2)
check("M7 maks slå", M.maxRigs("mowing"), 1)
local okWide, whyWide = M.checkWidth("mower", 60, nil)
check("M7 over 50 m", okWide, false); check("M7 grund", whyWide, "tooWide")
check("M7 40,8 m slå ok", (M.checkWidth("mower", 40.8, nil)), true)
-- M7b: bredde under 1,5 m er ikke læst rigtigt (spreder gav 0,5 m -> CP-kursus hver halve meter på 72 ha
-- -> spillet frøs, in-game 26/9) — sendes aldrig ud
local okTiny, whyTiny = M.checkWidth("fertilizer", 0.5, nil)
check("M7b 0,5 m afvises", okTiny, false)
check("M7b grund", whyTiny, "widthUnknown")
check("M7b 3 m strigle ok", (M.checkWidth("weeder", 3, nil)), true)
check("M7b aflæsningsvogn uden bredde ok", (M.checkWidth("unloader", 0, nil)), true)
check("M7b ballesamler uden bredde ok", (M.checkWidth("baleCollector", 0, nil)), true)
local okNarrow, whyNarrow, args = M.checkWidth("collector", 20, 40.8)
check("M7 for smal", okNarrow, false); check("M7 grund smal", whyNarrow, "tooNarrow"); check("M7 args", args[2], 40.8)
check("M7 lige bred ok (tolerance)", (M.checkWidth("collector", 40.76, 40.8)), true)
check("M7 ukendt slåbredde ok", (M.checkWidth("tedder", 10, nil)), true)
-- wantsRig: kun arbejdende trin, under maks, efter pause
check("M7 wantsRig slå", M.wantsRig(started, 0, 0).role, "mower")
check("M7 wantsRig fuld", M.wantsRig(started, 1, 0), nil)
local paused = M.start(M.onReadiness(M.newField(8), 1)); paused.retryAt = 1000
check("M7 wantsRig i pause", M.wantsRig(paused, 0, 500), nil)
check("M7 wantsRig efter pause", M.wantsRig(paused, 0, 1000).role, "mower")
check("M7 wantsRig klar = nil", M.wantsRig(ready, 0, 0), nil)
-- M8: gem/indlæs bevarer trin, plan og slåbredde
local saved = M.advance(M.start(M.onReadiness(M.newField(109), 1))); saved.mowWidth = 40.8; saved.mowVehicleId = "vehicleBigM"
saved.plan = {mode = "auto", product = "hay", rake = true, storeBuildingIds = {"b12", "b7"}, rigIds = {"v1", "v2"}}
local back = M.fromRecord(M.toRecord(saved))
check("M8 id", back.id, 109)
check("M8 stage", back.stage, saved.stage)
check("M8 mode", back.plan.mode, "auto")
check("M8 product", back.plan.product, "hay")
check("M8 rake", back.plan.rake, true)
check("M8 lager", table.concat(back.plan.storeBuildingIds, " "), "b12 b7")
check("M8 faste maskiner", table.concat(back.plan.rigIds, " "), "v1 v2")
check("M8 slåbredde", back.mowWidth, 40.8)
check("M8 slåmaskine", back.mowVehicleId, "vehicleBigM")
check("M8 ingen slåmaskine", M.fromRecord({id = 4, stage = "idle"}).mowVehicleId, nil)
check("M8 ukendt trin -> venter", M.fromRecord({id = 3, stage = "bogus"}).stage, "idle")
-- M9: with() kopierer og fletter planen
local base = M.start(M.onReadiness(M.newField(1), 1))
local noted = M.with(base, {reason = {text = "x"}, retryAt = 5, plan = {product = "hay"}})
check("M9 grund", noted.reason.text, "x"); check("M9 pause", noted.retryAt, 5)
check("M9 plan flettet", noted.plan.product, "hay"); check("M9 mode bevaret", noted.plan.mode, "manual")
check("M9 trin bevaret", noted.stage, "mowing"); check("M9 original urørt", base.plan.product, "auto")
-- M10: Start nu på en mark der ikke er helt klar -> klar; arbejdende mark røres ikke
check("M10 venter -> klar", M.forceReady(M.newField(2)).stage, "ready")
check("M10 arbejdende uændret", M.forceReady(base).stage, "mowing")
-- M11: krævet bredde og kursus-kilde pr. rolle (smalle standardmaskiner, 25/9)
local cut = M.with(M.newField(3), {mowWidth = 40.8})
check("M11 vender kræver ingen bredde", M.requiredWidth(cut, "tedder"), nil)
check("M11 river kræver ingen bredde", M.requiredWidth(cut, "rake"), nil)
check("M11 læssevogn uden riv = slåbredde", M.requiredWidth(cut, "collector"), 40.8)
local raked = M.with(cut, {plan = {rake = true}})
check("M11 læssevogn efter riv = ingen krav", M.requiredWidth(raked, "collector"), nil)
check("M11 vender eget kursus", M.courseSource(cut, "tedder"), nil)
check("M11 river eget kursus", M.courseSource(raked, "rake"), nil)
check("M11 læssevogn uden riv -> slåkursus", M.courseSource(cut, "collector"), "mower")
check("M11 læssevogn efter riv -> rivekursus", M.courseSource(raked, "collector"), "rake")
check("M11 slåmaskine eget kursus", M.courseSource(cut, "mower"), nil)
-- gem/indlæs rivemaskine
local withRake = M.with(raked, {rakeVehicleId = "vehicleRake"})
check("M11 rivemaskine gemt", M.fromRecord(M.toRecord(withRake)).rakeVehicleId, "vehicleRake")
check("M11 start glemmer rivemaskine", M.start(M.with(M.onReadiness(M.newField(9), 1), {rakeVehicleId = "x"})).rakeVehicleId, nil)
-- M12: sideriver — læssevognens CP-forskydning = egen forskydning − skårets forskydning (venstre positiv),
-- holdt inden for Courseplays ±10 m; skårets forskydning gemmes pr. mark
check("M12 midterrive", M.collectorOffset(0, 0), 0)
check("M12 skår 3 m til venstre", M.collectorOffset(0, 3), -3)
check("M12 vogn med egen forskydning", M.collectorOffset(0.5, -2), 2.5)
check("M12 holdes inden for 10 m", M.collectorOffset(0, 14), -10)
check("M12 ukendt skår -> egen forskydning", M.collectorOffset(0.4, nil), 0.4)
local side = M.with(M.newField(11), {windrowOffset = 3.2})
check("M12 skår gemt", M.fromRecord(M.toRecord(side)).windrowOffset, 3.2)
check("M12 intet skår gemt", M.fromRecord(M.toRecord(M.newField(12))).windrowOffset, nil)
check("M12 start glemmer skår", M.start(M.with(M.onReadiness(M.newField(13), 1), {windrowOffset = 1})).windrowOffset, nil)
-- M13: gæringssilo = produktion der laver markens skår om til ensilage (FermentingSiloPack m.fl.)
local GRASS_W, DRY_W, SILAGE, CHAFF, BREAD = 10, 11, 20, 30, 40
local fermenter = {{inputs = {{type = CHAFF}}, outputs = {{type = SILAGE}}}, {inputs = {{type = GRASS_W}}, outputs = {{type = SILAGE}}}}
check("M13 græs -> ensilage", M.isFermenter(fermenter, GRASS_W, SILAGE), true)
check("M13 hø ikke i denne", M.isFermenter(fermenter, DRY_W, SILAGE), false)
check("M13 bageri ikke", M.isFermenter({{inputs = {{type = GRASS_W}}, outputs = {{type = BREAD}}}}, GRASS_W, SILAGE), false)
check("M13 ingen produktioner", M.isFermenter(nil, GRASS_W, SILAGE), false)
check("M13 ukendt ensilage-type", M.isFermenter(fermenter, GRASS_W, nil), false)
-- M14: høstkæde + klargøring efter behov
local plan = {mode = "manual", product = "auto", rake = false, rigs = 1, unloaders = 1, mulch = false, lime = true,
soil = "auto", sow = true, sowFruit = "", roll = true, fertilize = true, weed = true, storeBuildingIds = {}}
check("M14 høst + kultivér + så + tromle", table.concat(M.workStages(plan, "harvest", {}), ","), "harvesting,cultivating,sowing,rolling")
check("M14 kalk + pløj når jorden trænger", table.concat(M.workStages(plan, "harvest", {lime = true, plow = true}), ","), "harvesting,liming,plowing,sowing,rolling")
local noSoil = {soil = "none", sow = false, lime = false, mulch = true}
check("M14 kun stubbehakning", table.concat(M.workStages(noSoil, "harvest", {}), ","), "harvesting,mulching")
check("M14 bar mark = kun klargøring", table.concat(M.workStages(plan, "bare", {plow = true}), ","), "plowing,sowing,rolling")
check("M14 tvunget pløj", table.concat(M.prepStages({soil = "plow"}, {}), ","), "plowing")
local harvestField = M.start(M.with(M.newField(20), {plan = {sow = true, soil = "cultivate"}}), "harvest", {})
check("M14 start = høst", harvestField.stage, "harvesting")
check("M14 husker art", harvestField.kind, "harvest")
local afterHarvest = M.advance(harvestField, {})
check("M14 høst -> kultivér", afterHarvest.stage, "cultivating")
check("M14 -> så", M.advance(afterHarvest, {}).stage, "sowing")
check("M14 så -> færdig", M.advance(M.advance(afterHarvest, {}), {}).stage, "done")
check("M14 kalk springes over når jorden er kalket", M.advance(M.with(harvestField, {plan = {lime = true}}), {lime = false}).stage, "cultivating")
check("M14 tom kæde -> færdig", M.start(M.with(M.newField(21), {plan = {soil = "none", sow = false, lime = false}}), "bare", {}).stage, "done")
-- M15: hjælpere under høst og multi-tool-baner
check("M15 aflæsningsvogn efter høsteren", M.wantsRig(harvestField, {primary = 1, helpers = 0}, 0).role, "unloader")
check("M15 hjælper-flag", M.wantsRig(harvestField, {primary = 1, helpers = 0}, 0).helper, true)
check("M15 alle vogne på plads -> intet", M.wantsRig(harvestField, {primary = 1, helpers = 1}, 0), nil)
local multi = M.with(harvestField, {plan = {rigs = 3, unloaders = 2}})
check("M15 multi: 3 høstere samlet", M.wantsRig(multi, {primary = 0, helpers = 0}, 0).count, 3)
check("M15 multi: 2 vogne", M.maxHelpers("harvesting", multi.plan), 2)
check("M15 ingen hjælper ved slåning", M.maxHelpers("mowing", multi.plan), 0)
check("M15 baner 2", table.concat(M.lanePositions(2), ","), "-1,1")
check("M15 baner 3", table.concat(M.lanePositions(3), ","), "-1,0,1")
check("M15 baner 4", table.concat(M.lanePositions(4), ","), "-2,-1,1,2")
check("M15 baner 5", table.concat(M.lanePositions(5), ","), "-2,-1,0,1,2")
check("M15 over 5 -> 5", #M.lanePositions(9), 5)
check("M15 maxRigs holdes 1-5", M.maxRigs("mowing", {rigs = 7}), 5)
-- M16: pleje under vækst
local growing = {kind = "harvest", growing = true, fert = 1, fertMax = 2, lime = 2, weed = 0}
local idleField = M.newField(30)
check("M16 gødning", M.careNeed(idleField, growing), "fertilizing")
check("M16 ukrudt før gødning", M.careNeed(idleField, {kind = "harvest", growing = true, fert = 1, fertMax = 2, lime = 2, weed = 3}), "weeding")
check("M16 græs ukrudtsbehandles ikke", M.careNeed(idleField, {kind = "mowable", growing = true, fert = 2, fertMax = 2, lime = 2, weed = 3}), nil)
check("M16 kalk", M.careNeed(idleField, {kind = "harvest", growing = true, fert = 2, fertMax = 2, lime = 0, weed = 0}), "careLiming")
check("M16 intet behov", M.careNeed(idleField, {kind = "harvest", growing = true, fert = 2, fertMax = 2, lime = 1, weed = 1}), nil)
check("M16 ikke voksende", M.careNeed(idleField, {kind = "harvest", growing = false, fert = 0, fertMax = 2}), nil)
check("M16 gødning slået fra", M.careNeed(M.with(idleField, {plan = {fertilize = false}}), growing), nil)
check("M16 arbejdende mark", M.careNeed(harvestField, growing), nil)
local cared = M.startCare(idleField, "fertilizing")
check("M16 pleje-trin", cared.stage, "fertilizing")
check("M16 rolle", M.roleFor(cared.stage), "fertilizer")
check("M16 kalk-pleje rolle", M.roleFor("careLiming"), "limer")
check("M16 pleje -> idle", M.advance(cared, {}).stage, "idle")
local twice = M.startCare(M.advance(cared, {}), "fertilizing")
check("M16 højst 2 gange", M.careNeed(M.advance(twice, {}), growing), nil)
check("M16 ny cyklus nulstiller", M.careNeed(M.advance(M.start(M.advance(twice, {}), "harvest", {}), {}), growing) ~= "x", true)
check("M16 start nulstiller pleje-tæller", next(M.start(twice, "harvest", {}).careRuns), nil)
check("M16 stop af pleje -> idle", M.stop(cared).stage, "idle")
check("M16 stop af høst -> klar", M.stop(harvestField).stage, "ready")
-- M17: såafgrøde og gem/indlæs af den udvidede plan
check("M17 så samme afgrøde", M.sowFruit(M.with(M.newField(40), {lastFruit = "WHEAT"})), "WHEAT")
check("M17 planens afgrøde vinder", M.sowFruit(M.with(M.newField(40), {lastFruit = "WHEAT", plan = {sowFruit = "CANOLA"}})), "CANOLA")
local full = M.with(M.startCare(M.newField(41), "weeding"), {kind = "harvest", lastFruit = "BARLEY",
plan = {rigs = 3, unloaders = 2, mulch = true, lime = false, soil = "plow", sow = true, sowFruit = "WHEAT", roll = true, fertilize = false, weed = false, product = "harvest"}})
local round = M.fromRecord(M.toRecord(full))
check("M17 trin", round.stage, "weeding")
check("M17 sæt", round.plan.rigs, 3)
check("M17 vogne", round.plan.unloaders, 2)
check("M17 stub", round.plan.mulch, true)
check("M17 kalk fra", round.plan.lime, false)
check("M17 jord", round.plan.soil, "plow")
check("M17 så", round.plan.sow, true)
check("M17 såafgrøde", round.plan.sowFruit, "WHEAT")
check("M17 tromle", round.plan.roll, true)
check("M17 gødning fra", round.plan.fertilize, false)
check("M17 ukrudt fra", round.plan.weed, false)
check("M17 art", round.kind, "harvest")
check("M17 sidste afgrøde", round.lastFruit, "BARLEY")
check("M17 pleje-tæller", round.careRuns.weeding, 1)
check("M17 produkt", round.plan.product, "harvest")
local legacy = M.fromRecord({id = 5, stage = "collecting", mode = "auto", product = "hay", rake = 1})
check("M17 etape 1-post: hø", legacy.plan.product, "hay")
check("M17 etape 1-post: standard kalk til", legacy.plan.lime, true)
check("M17 etape 1-post: 1 sæt", legacy.plan.rigs, 1)
-- M18: Start nu på en bar mark (klar-andel 0) må ikke nulstilles af vækst-reglen før den starter
local bare = M.with(M.forceReady(M.newField(70)), {startRequested = true})
check("M18 bar mark forbliver klar", M.onReadiness(bare, 0).stage, "ready")
check("M18 uden forespørgsel -> venter", M.onReadiness(M.forceReady(M.newField(71)), 0).stage, "idle")
check("M18 vejrfølsom: høst", M.isWeatherSensitive("harvesting", {}, "harvest"), true)
check("M18 vejrfølsom: kultivering ikke", M.isWeatherSensitive("cultivating", {}, "harvest"), false)
-- M19: mål-afgrøde — "vælg mark + afgrøde, glem den"
local target = M.with(M.newField(80), {plan = {targetFruit = "WHEAT"}})
local wheatNow = M.applyTarget(target.plan, "harvest", "BARLEY")
check("M19 mål -> auto", wheatNow.mode, "auto")
check("M19 høst byg -> så hvede", wheatNow.sow, true)
check("M19 såafgrøde = mål", wheatNow.sowFruit, "WHEAT")
check("M19 kæde: høst -> kultivér -> så", table.concat(M.workStages(wheatNow, "harvest", {}), ","), "harvesting,cultivating,sowing")
local grassToWheat = M.applyTarget(target.plan, "mowable", "GRASS")
check("M19 græs -> hvede: slå, saml op, så klargør og så", table.concat(M.workStages(grassToWheat, "mowable", {}), ","), "mowing,collecting,cultivating,sowing")
local grassTarget = M.applyTarget(M.with(M.newField(81), {plan = {targetFruit = "GRASS"}}).plan, "mowable", "GRASS")
check("M19 græs -> græs: kun slå og saml op", table.concat(M.workStages(grassTarget, "mowable", {}), ","), "mowing,collecting")
check("M19 græs -> græs: ingen såning", grassTarget.sow, false)
local bareTarget = M.applyTarget(target.plan, "bare", nil)
check("M19 bar mark -> klargør og så", table.concat(M.workStages(bareTarget, "bare", {plow = true}), ","), "plowing,sowing")
check("M19 uden mål: plan urørt", M.applyTarget(M.newField(82).plan, "harvest", "BARLEY").mode, "manual")
local started = M.start(target, "harvest", {}, "BARLEY")
check("M19 start anvender målet", started.plan.sowFruit, "WHEAT")
check("M19 start: høst først", started.stage, "harvesting")
check("M19 målet gemmes", M.fromRecord(M.toRecord(started)).plan.targetFruit, "WHEAT")
check("M19 klargøring efter slåning gemmes", M.fromRecord(M.toRecord(M.start(target, "mowable", {}, "GRASS"))).plan.prepAfterMow, true)
-- M20: skal en bar mark med mål starte? (altid — kun såningen venter på såtiden)
check("M20 bar + sås nu -> start", M.shouldStartBare(target, "bare", true), true)
check("M20 bar + ikke såtid -> klargør alligevel (kun såningen venter)", M.shouldStartBare(target, "bare"), true)
check("M20 uden mål -> ingen auto-start", M.shouldStartBare(M.newField(83), "bare", true), false)
check("M20 voksende mark -> nej", M.shouldStartBare(target, "harvest", true), false)
check("M20 arbejdende mark -> nej", M.shouldStartBare(started, "bare", true), false)
check("M20 færdig (lige sået) -> nej, ingen dobbelt-såning", M.shouldStartBare(M.with(target, {stage = "done"}), "bare", true), false)
-- M21: vælg mål-afgrøde fra menuen: auto, og en færdig mark nulstilles så den kan starte
local chosen = M.setTarget(M.with(M.newField(84), {stage = "done"}), "CANOLA")
check("M21 mål sat", chosen.plan.targetFruit, "CANOLA"); check("M21 auto", chosen.plan.mode, "auto")
check("M21 færdig -> venter", chosen.stage, "idle")
check("M21 arbejdende mark beholder trin", M.setTarget(started, "CANOLA").stage, "harvesting")
local cleared = M.setTarget(chosen, "")
check("M21 mål fjernet", cleared.plan.targetFruit, ""); check("M21 tilbage til manuel", cleared.plan.mode, "manual")
-- M22: halm efter høst — hakkes (standard), samles op, eller presses til baller og samles
local strawBase = {soil = "none", sow = false, lime = false}
check("M22 hakkes: kun høst", table.concat(M.workStages(strawBase, "harvest", {}), ","), "harvesting")
check("M22 samles op", table.concat(M.workStages(M.with(M.newField(1), {plan = {soil = "none", lime = false, straw = "collect"}}).plan, "harvest", {}), ","), "harvesting,strawCollecting")
check("M22 baller", table.concat(M.workStages(M.with(M.newField(1), {plan = {soil = "none", lime = false, straw = "bale"}}).plan, "harvest", {}), ","), "harvesting,baling,baleCollecting")
local baleChain = M.start(M.with(M.newField(2), {plan = {straw = "bale", soil = "cultivate", lime = false}}), "harvest", {})
check("M22 høst -> presses", M.advance(baleChain, {}).stage, "baling")
check("M22 presses -> baller samles", M.advance(M.advance(baleChain, {}), {}).stage, "baleCollecting")
check("M22 -> kultivér", M.advance(M.advance(M.advance(baleChain, {}), {}), {}).stage, "cultivating")
check("M22 roller", M.roleFor("baling") .. "," .. M.roleFor("baleCollecting") .. "," .. M.roleFor("strawCollecting"), "baler,baleCollector,collector")
check("M22 halm-læssevogn følger høsterens kursus", M.courseSource(M.with(M.newField(3), {stage = "strawCollecting"}), "collector"), "harvester")
check("M22 presser følger høsterens kursus", M.courseSource(M.with(M.newField(3), {stage = "baling"}), "baler"), "harvester")
check("M22 græs-læssevogn følger stadig slåkurset", M.courseSource(M.with(M.newField(3), {stage = "collecting"}), "collector"), "mower")
check("M22 halm-læssevogn uden breddekrav", M.requiredWidth(M.with(M.newField(3), {stage = "strawCollecting", mowWidth = 40}), "collector"), nil)
check("M22 presning vejrfølsom", M.isWeatherSensitive("baling", {}, "harvest"), true)
check("M22 ballesamling ikke vejrfølsom", M.isWeatherSensitive("baleCollecting", {}, "harvest"), false)
check("M22 gemmes", M.fromRecord(M.toRecord(M.with(M.newField(4), {plan = {straw = "bale"}}))).plan.straw, "bale")
check("M22 ukendt -> hakkes", M.fromRecord({id = 5, straw = "x"}).plan.straw, "chop")
-- M23: ingen ny tabel når vækst-reglen ikke ændrer noget (undgår affald hvert sekund -> GC-hak)
local calm = M.newField(95)
check("M23 uændret -> samme tabel", M.onReadiness(calm, 0.2) == calm, true)
check("M23 ændret -> ny tabel", M.onReadiness(calm, 0.95) ~= calm, true)
-- M24: passive marker kan springes over (manuel, uden mål, venter, ingen forespørgsel)
check("M24 manuel venter = passiv", M.isPassive(calm), true)
check("M24 auto = ikke passiv", M.isPassive(M.with(calm, {plan = {mode = "auto"}})), false)
check("M24 mål = ikke passiv", M.isPassive(M.setTarget(calm, "WHEAT")), false)
check("M24 klar = ikke passiv", M.isPassive(M.with(calm, {stage = "ready"})), false)
check("M24 i gang = ikke passiv", M.isPassive(started), false)
check("M24 Start nu = ikke passiv", M.isPassive(M.with(calm, {startRequested = true})), false)
-- Stop holder en bar mark (ellers starter klargøringen straks forfra); Start eller ny mål-afgrøde slipper
local stoppedBare = M.stop(M.with(target, {stage = "cultivating"}))
check("M21 stoppet bar mark starter ikke selv", M.shouldStartBare(stoppedBare, "bare"), false)
check("M21 Start slipper", M.shouldStartBare(M.with(stoppedBare, {startRequested = true}), "bare"), true)
check("M21 ny mål-afgrøde slipper", M.shouldStartBare(M.setTarget(stoppedBare, "BARLEY"), "bare"), true)
check("M21 start nulstiller hold (næste sæson starter selv)", M.start(stoppedBare, "bare", {}, "WHEAT").held, nil)
-- gemt produkt (silage/hø) gælder kun slåafgrøder: en vinmark der engang fejlagtigt blev "slåbar" (F22, 29/9)
check("produkt: gemt silage på høstmark -> høst", ADFieldModel.product({product = "silage"}, "harvest"), "harvest")
check("produkt: gemt hø på slåmark -> hø", ADFieldModel.product({product = "hay"}, "mowable"), "hay")
check("produkt: auto på slåmark -> silage", ADFieldModel.product({product = "auto"}, "mowable"), "silage")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-46
View File
@@ -1,46 +0,0 @@
-- Kør: luajit tests/test_adFieldSiloConfig.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFieldSiloConfig.lua")
local C = ADFieldSiloConfig
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
local cfg = C.new()
check("standard fra", cfg.enabled, false)
check("standard x=2", cfg.maxVehicles, 2)
check("standard tærskel 10000", cfg.minLiters, 10000)
check("x under 1 -> 1", C.with(cfg, {maxVehicles = 0}).maxVehicles, 1)
check("x over 10 -> 10", C.with(cfg, {maxVehicles = 99}).maxVehicles, 10)
check("tærskel snappes til nærmeste valg", C.with(cfg, {minLiters = 12000}).minLiters, 10000)
check("tærskel 40000 -> 50000", C.with(cfg, {minLiters = 40000}).minLiters, 50000)
check("with muterer ikke", cfg.maxVehicles, 2)
local on = C.toggleStore(cfg, "silo1")
check("toggle til", C.hasStore(on, "silo1"), true)
check("toggle muterer ikke", C.hasStore(cfg, "silo1"), false)
check("toggle fra", C.hasStore(C.toggleStore(on, "silo1"), "silo1"), false)
local full = C.with(C.toggleStore(C.toggleStore(cfg, "a"), "b"), {enabled = true, maxVehicles = 4, minLiters = 25000})
local back = C.fromRecord(C.toRecord(full))
check("record: enabled", back.enabled, true)
check("record: lagre", table.concat(back.storeIds, " "), "a b")
check("record: x", back.maxVehicles, 4)
check("record: tærskel", back.minLiters, 25000)
check("tom record -> standard", C.fromRecord(nil).maxVehicles, 2)
check("record uden tal -> standard tærskel", C.fromRecord({enabled = 1}).minLiters, 10000)
-- kør til produktion + foderreserve
check("standard: ikke til produktion", cfg.toProduction, false)
check("standard foderreserve 7 døgn", cfg.reserveDays, 7)
check("reserve snappes (10 -> 7)", C.with(cfg, {reserveDays = 10}).reserveDays, 7)
check("reserve 40 -> 30", C.with(cfg, {reserveDays = 40}).reserveDays, 30)
local prod = C.with(C.toggleStore(cfg, "a"), {toProduction = true, reserveDays = 14})
check("with bevarer lagre", C.hasStore(prod, "a"), true)
check("with muterer ikke (produktion)", cfg.toProduction, false)
local prodBack = C.fromRecord(C.toRecord(prod))
check("record: til produktion", prodBack.toProduction, true)
check("record: foderreserve", prodBack.reserveDays, 14)
check("gammel record uden felter -> standard", C.fromRecord({enabled = 1}).reserveDays, 7)
check("toggleStore bevarer produktion", C.toggleStore(prod, "b").toProduction, true)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-159
View File
@@ -1,159 +0,0 @@
-- Kør: luajit tests/test_adFieldSiloPlanner.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFieldSiloPlanner.lua")
local P = ADFieldSiloPlanner
local WHEAT, CANOLA = 1, 2
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
local function silo(id, x, stock, markerId) return {id = id, name = id, markerId = markerId == nil and (id .. "m") or markerId or nil, x = x, z = 0, stock = stock} end
local function store(id, x, free) return {id = id, markerId = id .. "m", x = x, z = 0, free = free} end
local function vehicle(id, x, capacity, carries, cargo) return {id = id, x = x, z = 0, capacity = capacity, carries = carries or {[WHEAT] = true, [CANOLA] = true}, cargo = cargo} end
local function base(over)
local input = {silos = {}, stores = {store("farm", 0, {[WHEAT] = 1000000, [CANOLA] = 1000000})}, loans = {}, vehicles = {}, paused = {}, maxVehicles = 2, minLiters = 10000}
for k, v in pairs(over or {}) do input[k] = v end
return input
end
-- kvalificering
local r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 9999})}, vehicles = {vehicle("v1", 400, 50000)}}))
check("under tærskel -> ingen afsendelse", #r.dispatch, 0)
check("under tærskel -> ingen status", #r.status, 0)
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 30000}, false)}, vehicles = {vehicle("v1", 400, 50000)}}))
check("uden markør -> noMarker", r.status[1].state, "noMarker")
check("uden markør -> ingen afsendelse", #r.dispatch, 0)
r = P.plan(base({stores = {store("farm", 0, {[CANOLA] = 1000000})}, silos = {silo("A", 500, {[WHEAT] = 30000})}, vehicles = {vehicle("v1", 400, 50000)}}))
check("ingen gård tager varen -> noFarmStore", r.status[1].state, "noFarmStore")
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 30000})}, vehicles = {vehicle("v1", 400, 50000)}, paused = {[P.key("A", WHEAT)] = true}}))
check("pause -> paused, ingen afsendelse", r.status[1].state .. #r.dispatch, "paused0")
-- afsendelse, fuldeste først, x-loft
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 20000}), silo("B", 900, {[WHEAT] = 80000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 950, 50000), vehicle("v3", 1000, 50000)}}))
check("x=2 -> to afsendt", #r.dispatch, 2)
check("fuldeste silo først", r.dispatch[1].siloId, "B")
check("nærmeste vogn til B", r.dispatch[1].vehicleId, "v2")
check("B's rest (30000) får vogn nr. 2", r.dispatch[2].siloId, "B")
check("lager-markør med", r.dispatch[1].storeMarkerId, "farmm")
check("liter = vognens kapacitet", r.dispatch[1].liters, 50000)
check("anden tur = resten", r.dispatch[2].liters, 30000)
-- aktive lån tæller mod x og mod siloens rest
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 60000})}, loans = {{siloId = "A", fillType = WHEAT, liters = 50000}}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000)}}))
check("lån på vej -> rest 10000 -> én ny vogn", #r.dispatch, 1)
check("status viser på vej", r.status[1].onWay, 50000)
r = P.plan(base({maxVehicles = 1, silos = {silo("A", 500, {[WHEAT] = 90000})}, loans = {{siloId = "Z", fillType = WHEAT, liters = 1}}, vehicles = {vehicle("v1", 400, 50000)}}))
check("x=1 med ét lån ude -> ingen", #r.dispatch, 0)
check("frie pladser 0", r.freeSlots, 0)
-- flere varer i én silo = hver sin opgave
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 40000, [CANOLA] = 20000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 410, 50000)}}))
check("to varer -> to afsendelser", #r.dispatch, 2)
check("fuldeste vare først", r.dispatch[1].fillType, WHEAT)
-- vogne der ikke kan
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 40000})}, vehicles = {vehicle("v1", 400, 50000, {[CANOLA] = true}), vehicle("v2", 2000, 50000)}}))
check("vogn der ikke kan bære varen springes over", r.dispatch[1].vehicleId, "v2")
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 40000})}, vehicles = {vehicle("v1", 400, 50000, nil, CANOLA), vehicle("v2", 2000, 50000)}}))
check("vogn med anden vare på springes over", r.dispatch[1].vehicleId, "v2")
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 40000})}, vehicles = {vehicle("v1", 400, 0)}}))
check("vogn uden kapacitet -> ingen", #r.dispatch, 0)
-- lagervalg
r = P.plan(base({stores = {store("near", 400, {[WHEAT] = 20000}), store("far", 5000, {[WHEAT] = 900000})}, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000)}}))
check("nærmeste lager uden plads til læsset -> det med plads", r.dispatch[1].storeId, "far")
r = P.plan(base({stores = {store("small", 400, {[WHEAT] = 30000}), store("tiny", 300, {[WHEAT] = 15000})}, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000)}}))
check("intet har plads til hele læsset -> mest plads", r.dispatch[1].storeId, "small")
check("reserveret plads: vogn 2 til det andet lager", r.dispatch[2].storeId, "tiny")
r = P.plan(base({stores = {store("full", 400, {[WHEAT] = 5000})}, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)}}))
check("lager med under tærsklen -> noFarmStore", r.status[1].state, "noFarmStore")
-- input muteres ikke
local input = base({silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)}})
P.plan(input)
check("input.stores urørt", input.stores[1].free[WHEAT], 1000000)
-- I1: lån på vej til et lager reserverer pladsen på tværs af planlægningsrunder
r = P.plan(base({stores = {store("s", 400, {[WHEAT] = 30000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
loans = {{siloId = "A", fillType = WHEAT, liters = 30000, storeId = "s"}}, vehicles = {vehicle("v1", 400, 50000)}}))
check("I1 lager fyldt af lån på vej -> ingen ny vogn dertil", #r.dispatch, 0)
r = P.plan(base({stores = {store("s", 400, {[WHEAT] = 30000}), store("t", 900, {[WHEAT] = 500000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
loans = {{siloId = "A", fillType = WHEAT, liters = 30000, storeId = "s"}}, vehicles = {vehicle("v1", 400, 50000)}}))
check("I1 næste vogn til det andet lager", r.dispatch[1] ~= nil and r.dispatch[1].storeId, "t")
-- kør til produktion: foder først, overskud til fabrik
local function factory(id, x, free) return {id = id, kind = "production", markerId = id .. "m", x = x, z = 0, free = free} end
local farmAndFactory = {store("farm", 5000, {[WHEAT] = 1000000}), factory("mill", 100, {[WHEAT] = 1000000})}
r = P.plan(base({stores = farmAndFactory, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)},
feedReserve = {[WHEAT] = 100000}, farmStock = {[WHEAT] = 20000}}))
check("P1 reserve ikke dækket -> gårdlager selv om fabrik er nærmere", r.dispatch[1].storeId, "farm")
r = P.plan(base({stores = farmAndFactory, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)},
feedReserve = {[WHEAT] = 100000}, farmStock = {[WHEAT] = 150000}}))
check("P2 reserve dækket -> fabrik", r.dispatch[1].storeId, "mill")
r = P.plan(base({stores = farmAndFactory, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)}}))
check("P3 ikke foder (ingen reserve) -> fabrik", r.dispatch[1].storeId, "mill")
r = P.plan(base({stores = farmAndFactory, silos = {silo("A", 500, {[WHEAT] = 100000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000)},
feedReserve = {[WHEAT] = 100000}, farmStock = {[WHEAT] = 70000}}))
check("P4 første læs dækker resten af reserven -> gård", r.dispatch[1].storeId, "farm")
check("P4 andet læs er overskud -> fabrik", r.dispatch[2].storeId, "mill")
r = P.plan(base({stores = farmAndFactory, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)},
feedReserve = {[WHEAT] = 100000}, farmStock = {[WHEAT] = 60000}, loans = {{siloId = "B", fillType = WHEAT, liters = 50000, storeId = "farm"}}}))
check("P5 læs på vej til gården tæller med i reserven -> fabrik", r.dispatch[1] ~= nil and r.dispatch[1].storeId, "mill")
r = P.plan(base({stores = {store("farm", 5000, {[WHEAT] = 1000000}), factory("mill", 100, {[CANOLA] = 1000000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
vehicles = {vehicle("v1", 400, 50000)}, farmStock = {[WHEAT] = 999999}}))
check("P6 ingen fabrik tager varen -> gårdlager", r.dispatch[1].storeId, "farm")
r = P.plan(base({stores = {store("farm", 5000, {[CANOLA] = 1000000}), factory("mill", 100, {[WHEAT] = 1000000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
vehicles = {vehicle("v1", 400, 50000)}, feedReserve = {[WHEAT] = 100000}}))
check("P7 reserve mangler men ingen gård tager varen -> fabrik", r.dispatch[1].storeId, "mill")
check("P7 status ok (ikke noFarmStore)", r.status[1].state, "ok")
check("P7 status viser destination", r.status[1].toProduction, true)
-- fabrik kun når hele læsset kan være der (in-game 28/9: vogn med 252.000 l ventede timevis ved en fuld fabrik)
r = P.plan(base({stores = {store("farm", 5000, {[WHEAT] = 1000000}), factory("mill", 100, {[WHEAT] = 30000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
vehicles = {vehicle("v1", 400, 50000)}}))
check("P8 fabrik uden plads til hele læsset -> gårdlager", r.dispatch[1].storeId, "farm")
r = P.plan(base({stores = {store("farm", 5000, {[CANOLA] = 1000000}), factory("mill", 100, {[WHEAT] = 30000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
vehicles = {vehicle("v1", 400, 50000)}}))
check("P9 kun fabrik uden plads til hele læsset -> ingen afsendelse", #r.dispatch, 0)
-- høsten først: tømmevogne holdes tilbage til høstere der arbejder
local vs = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000, {[CANOLA] = true}), vehicle("v3", 400, 50000)}
local left = P.withoutHarvestReserve(vs, {{fillType = WHEAT, count = 1}})
check("R1 én hvede-vogn holdt tilbage", #left, 2)
check("R1 raps-vognen er fri", left[1].id == "v2" or left[2].id == "v2", true)
left = P.withoutHarvestReserve(vs, {{fillType = WHEAT, count = 5}})
check("R2 alle hvede-vogne holdt tilbage", #left, 1)
check("R3 intet behov -> alle fri", #P.withoutHarvestReserve(vs, {}), 3)
check("R4 input muteres ikke", #vs, 3)
-- Tom silo: lånt vogn med last står ved en tom marksilo -> resten køres videre (AD venter ellers til fuld)
-- (in-game 28/9: Tømmevogn 2 og 4 stod ved tom F17 Unload med kartofler)
local restState = {atSilo = true, stillMs = 45000, siloStock = 0, cargo = 12000}
check("RS1 tom silo med last -> kør videre", P.restDue(restState), true)
check("RS2 siloen har stadig varer -> vent", P.restDue({atSilo = true, stillMs = 45000, siloStock = 30000, cargo = 12000}), false)
check("RS3 ikke ved siloen -> nej", P.restDue({atSilo = false, stillMs = 45000, siloStock = 0, cargo = 12000}), false)
check("RS4 lige ankommet -> vent", P.restDue({atSilo = true, stillMs = 5000, siloStock = 0, cargo = 12000}), false)
check("RS5 tom vogn -> nej", P.restDue({atSilo = true, stillMs = 45000, siloStock = 0, cargo = 0}), false)
local restStores = {
{id = "fab", kind = "production", markerId = 1, x = 10, z = 0, free = {[10] = 5000}},
{id = "gaard", kind = "silo", markerId = 2, x = 500, z = 0, free = {[10] = 900000}},
}
check("RS6 fabrik uden plads til hele resten -> gårdsilo", P.restStore(restStores, {x = 0, z = 0}, 10, 12000).id, "gaard")
check("RS7 fabrik med plads -> fabrik", P.restStore(restStores, {x = 0, z = 0}, 10, 4000).id, "fab")
check("RS8 intet tager varen", P.restStore(restStores, {x = 0, z = 0}, 99, 4000), nil)
-- Fuld fabrik: lånt vogn med last holder ved et lager uden plads -> resten til et andet lager (gård)
check("SF1 fuld fabrik -> videre", P.storeFullDue({atStore = true, stillMs = 70000, cargo = 12000, storeFree = 0}), true)
check("SF2 fabrikken tager stadig imod -> vent", P.storeFullDue({atStore = true, stillMs = 70000, cargo = 12000, storeFree = 8000}), false)
check("SF3 lige ankommet -> vent", P.storeFullDue({atStore = true, stillMs = 20000, cargo = 12000, storeFree = 0}), false)
check("SF4 ikke ved lageret -> nej", P.storeFullDue({atStore = false, stillMs = 70000, cargo = 12000, storeFree = 0}), false)
local fullStores = {
{id = "fab", kind = "production", markerId = 1, x = 10, z = 0, free = {[10] = 0}},
{id = "gaard", kind = "silo", markerId = 2, x = 500, z = 0, free = {[10] = 900000}},
}
check("SF5 andet lager end det fulde", P.restStore(fullStores, {x = 0, z = 0}, 10, 12000, "fab").id, "gaard")
check("SF6 kun det fulde tager varen -> intet", P.restStore({fullStores[1]}, {x = 0, z = 0}, 10, 12000, "fab"), nil)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-159
View File
@@ -1,159 +0,0 @@
-- Kør: luajit tests/test_adFieldSilos.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFieldSiloPlanner.lua")
dofile("FS25_ADSmartPickup/adFieldSiloConfig.lua")
dofile("FS25_ADSmartPickup/adFieldSilos.lua")
local S = ADFieldSilos
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
local loan = {startedMs = 0, lastProgressMs = 0, lastFill = 0}
check("kører, tidligt -> behold", S.releaseReason(loan, {adActive = true, controlled = false, fill = 0, moving = true}, 5000), nil)
check("AD slukket efter 40 s -> done", S.releaseReason(loan, {adActive = false, controlled = false, fill = 0}, 40000), "done")
check("spiller i vognen -> player", S.releaseReason(loan, {adActive = true, controlled = true, fill = 0}, 5000), "player")
check("står stille uden fyld i 120 s -> stuck", S.releaseReason(loan, {adActive = true, controlled = false, fill = 0, moving = false}, 120001), "stuck")
check("kører stadig efter 120 s -> behold", S.releaseReason(loan, {adActive = true, controlled = false, fill = 0, moving = true}, 120001), nil)
local loading = {startedMs = 0, lastProgressMs = 110000, lastFill = 0}
check("fyldning steg for nylig -> behold", S.releaseReason(loading, {adActive = true, controlled = false, fill = 20000, moving = false}, 125000), nil)
-- I2/I3: vagten gælder kun før turen er kommet i gang; AD der slukker straks uden fremdrift = fejl
local going = {startedMs = 0, lastFill = 30000, hasProgressed = true}
check("I2 i gang, står i kø 500 s -> behold", S.releaseReason(going, {adActive = true, controlled = false, fill = 30000, moving = false}, 500000), nil)
check("I2 aflæsning (fyld falder) er fremdrift", S.releaseReason({startedMs = 0, lastFill = 30000}, {adActive = true, controlled = false, fill = 20000, moving = false}, 500000), nil)
check("I3 AD slukket efter 10 s uden fremdrift -> failed", S.releaseReason({startedMs = 0, lastFill = 0}, {adActive = false, controlled = false, fill = 0}, 10000), "failed")
check("I3 AD slukket efter 10 s med fremdrift -> done", S.releaseReason({startedMs = 0, lastFill = 0, hasProgressed = true}, {adActive = false, controlled = false, fill = 0}, 10000), "done")
-- Røgtest af spil-laget med mocks: planNow låner nærmeste tømmevogn, isLoaned = optaget,
-- frakobling (setConfig enabled=false) stopper AD og frigiver (Review Focus).
do
local WHEAT = 1
Logging = {info = function() end, warning = function() end}
g_currentMission = {getFarmId = function() return 1 end}
g_fillTypeManager = {getFillTypeByIndex = function() return {title = "Hvede"} end}
function getWorldTranslation(node) return node.x, 0, node.z end
ADRuns = {vehicleLabel = function(v) return v.name end}
local function storage(levels, free) return {fillLevels = levels, fillTypes = {[WHEAT] = true}, getFreeCapacity = function() return free end} end
local fieldSilo = {id = "fs", name = "Marksilo 1", kind = "silo", placeable = {rootNode = {x = 1000, z = 0}, spec_silo = {storages = {storage({[WHEAT] = 60000}, 0)}}}}
local farm = {id = "farm", name = "Gårdsilo", kind = "silo", placeable = {rootNode = {x = 0, z = 0}, spec_silo = {storages = {storage({}, 500000)}}}}
ADBuildings = {list = function() return {fieldSilo, farm} end,
getMarkers = function(b, role) return {{id = b.id .. "-" .. role}} end}
ADFields = {list = function() return {{id = 1}} end, fieldSilos = function() return {fieldSilo, farm} end}
local started, settingsSet, noPark, noRoute, residue = {}, {}, {}, {}, 0
ADRunsController = {store = {runs = {}}, setSetting = function(_, v, name, value) settingsSet[v] = settingsSet[v] or {}; settingsSet[v][name] = value end}
ADUnloadWait = {hasRouteTo = function(_, _, id) return not noRoute[id] end}
local function makeVehicle(name, x)
local module = {active = false}
function module:setMode() end
function module:setFirstMarkerByWayPointId(id) self.first = id end
function module:setSecondMarkerByWayPointId(id) self.second = id end
function module:setFillType() end
function module:setLoopsDone() end
function module:raiseDirtyFlag() end
function module:isActive() return self.active end
return {name = name, x = x, lastSpeedReal = 0, ad = {stateModule = module},
stopAutoDrive = function(self) self.ad.stateModule.active = false; self.stopped = true end}
end
local near, far = makeVehicle("Nær", 900), makeVehicle("Fjern", 5000)
local adEnv = {AutoDrive = {MODE_PICKUPANDDELIVER = 2, RT_NONE = 0,
getAllDischargeableUnits = function() return {{fillUnitIndex = 1, object = {
getFillUnitCapacity = function() return 40000 end,
getFillUnitSupportedFillTypes = function() return {[WHEAT] = true} end,
getFillUnitFillLevel = function() return residue end, getFillUnitFillType = function() return WHEAT end}}} end,
getAllUnits = function() return {} end, getAllFillLevels = function() return 0 end}}
ADFieldWork = {
getRigs = function()
local rigs = {}
-- som spillets vehicleSystem: slettede køretøjer er ikke med
if not near.isDeleted then table.insert(rigs, {id = "n", vehicle = near, role = "unloader", enabled = true, busy = S.isLoaned(near), controlled = false, x = 900, z = 0}) end
if not far.isDeleted then table.insert(rigs, {id = "f", vehicle = far, role = "unloader", enabled = true, busy = S.isLoaned(far), controlled = false, x = 5000, z = 0}) end
table.insert(rigs, {id = "h", vehicle = far, role = "harvester", enabled = true, busy = false, controlled = false, x = 1000, z = 0})
return rigs
end,
enableParkAtJobFinished = function() end,
parkMarkerId = function(_, v) if noPark[v] then return nil end return 777 end,
startAd = function(_, v, _, first, second, fillType) v.ad.stateModule.active = true; table.insert(started, {v = v, first = first, second = second, fillType = fillType}) end,
}
S.setConfig(ADFieldSiloConfig.with(ADFieldSiloConfig.toggleStore(ADFieldSiloConfig.new(), "farm"), {enabled = true, maxVehicles = 1}))
local plan = S.planNow(adEnv)
check("RØG én afsendelse (x=1)", #started, 1)
check("RØG nærmeste tømmevogn", started[1] ~= nil and started[1].v.name, "Nær")
check("RØG fra marksilo-markør", started[1] ~= nil and started[1].first, "fs-load")
check("RØG til gårdlager-markør", started[1] ~= nil and started[1].second, "farm-unload")
check("RØG varen med", started[1] ~= nil and started[1].fillType, WHEAT)
check("RØG gårdlager er ikke marksilo", #plan.status, 1)
check("RØG vognen er lånt", S.isLoaned(near), true)
check("RØG x nået -> ingen flere", #S.planNow(adEnv).dispatch, 0)
check("I4 rotation slået fra på lånt vogn", settingsSet[near] ~= nil and settingsSet[near].rotateTargets, 0)
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = false}))
check("RØG frakobling stopper AD", near.stopped, true)
check("RØG frakobling frigiver", S.isLoaned(near), false)
-- I5: vogn uden parkeringspunkt lånes ikke (ville blive holdende på lagerets markør)
started = {}
noPark[near] = true
near.stopped = nil
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = true}))
S.planNow(adEnv)
check("I5 uden parkering -> fjern vogn med parkering", started[1] ~= nil and started[1].v.name, "Fjern")
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = false}))
noPark[near] = nil
-- I3: ingen rute til siloen -> ingen afsendelse
started = {}
noRoute["fs-load"] = true
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = true}))
S.planNow(adEnv)
check("I3 ingen rute -> ingen afsendelse", #started, 0)
noRoute["fs-load"] = nil
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = false}))
-- I6: silo der er kilde/mål i et flow er ikke marksilo
ADRunsController.store.runs = {{kind = "supply", sourceBuildingIds = {}, targetBuildingIds = {"fs"}, storeBuildingIds = {}}}
local silos = S.collectBuildings(adEnv)
check("I6 silo i et flow tømmes ikke", #silos, 0)
ADRunsController.store.runs = {}
-- in-game 27/9: marksilo-flowet flyttede sukkerroer fra en Farma-silo til nabo-Farma-siloen (samme lagerklynge)
local neighbour = {id = "nb", name = "Nabo-Farma", kind = "silo", placeable = {rootNode = {x = 40, z = 0}, spec_silo = {storages = {storage({[WHEAT] = 60000}, 0)}}}}
ADFields.fieldSilos = function() return {fieldSilo, farm, neighbour} end
local withNeighbour = S.collectBuildings(adEnv)
local names = {}
for _, silo in ipairs(withNeighbour) do table.insert(names, silo.id) end
check("gårdens nabosilo (40 m fra gårdlager) er ikke marksilo", table.concat(names, ","), "fs")
ADFields.fieldSilos = function() return {fieldSilo, farm} end
-- in-game 27/9: gårdens Farma-klynge er 300 m lang — siloer over 100 m fra et valgt gårdlager blev marksiloer.
-- En silo i samme klynge (≤ 60 m til en nabo) som et gårdlager er gårdens.
local c1 = {id = "c1", name = "Klynge 1", kind = "silo", placeable = {rootNode = {x = 50, z = 0}, spec_silo = {storages = {storage({[WHEAT] = 60000}, 0)}}}}
local c2 = {id = "c2", name = "Klynge 2", kind = "silo", placeable = {rootNode = {x = 105, z = 0}, spec_silo = {storages = {storage({[WHEAT] = 60000}, 0)}}}}
ADBuildings.list = function() return {fieldSilo, farm, c1, c2} end
ADFields.fieldSilos = function() return {fieldSilo, farm, c1, c2} end
local clustered = {}
for _, silo in ipairs(S.collectBuildings(adEnv)) do table.insert(clustered, silo.id) end
check("gårdens klynge (105 m via nabo) er ikke marksilo", table.concat(clustered, ","), "fs")
ADBuildings.list = function() return {fieldSilo, farm} end
ADFields.fieldSilos = function() return {fieldSilo, farm} end
-- M5: rest under 100 l tæller ikke som last
residue = 50
local vehicles = S.collectVehicles(adEnv)
check("M5 rest på 50 l er ikke last", vehicles[1].cargo, nil)
residue = 0
-- M4: slettet lånt vogn fjernes uden at vælte tick
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = true}))
started = {}
S.planNow(adEnv)
local loaned = started[1] ~= nil and started[1].v or nil
if loaned ~= nil then loaned.isDeleted = true end
local okTick = pcall(S.tick, adEnv, 2000)
check("M4 tick tåler slettet vogn", okTick, true)
check("M4 slettet vogn frigivet", loaned ~= nil and S.isLoaned(loaned), false)
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = false}))
end
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-110
View File
@@ -1,110 +0,0 @@
-- Kør: luajit tests/test_adFieldStorage.lua (fra repo-roden)
-- Lagringen skal dække HVERT felt modellen gemmer (FIELD_TYPES), og en mark skal overleve en tur gennem XML.
dofile("FS25_ADSmartPickup/adFieldModel.lua")
dofile("FS25_ADSmartPickup/adFieldStorage.lua")
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
-- falsk XMLFile: gemmer værdier pr. nøgle
local function fakeXml()
local values = {}
local xml = {}
for _, kind in ipairs({"Int", "String", "Float"}) do
xml["set" .. kind] = function(_, key, value) values[key] = value end
xml["get" .. kind] = function(_, key) return values[key] end
end
return xml
end
local field = ADFieldModel.with(ADFieldModel.startCare(ADFieldModel.newField(77), "weeding"), {
kind = "harvest", lastFruit = "WHEAT", mowWidth = 40.8, mowVehicleId = "v1", rakeVehicleId = "v2", windrowOffset = -2.5,
plan = {mode = "auto", product = "harvest", rake = true, storeBuildingIds = {"b1"}, rigs = 3, unloaders = 2,
mulch = true, lime = false, soil = "plow", sow = true, sowFruit = "CANOLA", roll = true, fertilize = false, weed = false},
})
local record = ADFieldModel.toRecord(field)
for key in pairs(record) do
check("ST1 felt med i lagringen: " .. key, ADFieldStorage.FIELD_TYPES[key] ~= nil, true)
end
local xml = fakeXml()
ADFieldStorage.writeRecord(xml, "k", record)
local back = ADFieldModel.fromRecord(ADFieldStorage.readRecord(xml, "k"))
local again = ADFieldModel.toRecord(back)
for key, value in pairs(record) do
check("ST2 rundtur: " .. key, tostring(again[key]), tostring(value))
end
-- ST3-ST6: kursus-registret (genbrug af Courseplay-kurser) gemmes i samme fil, <courses><course .../></courses>
dofile("FS25_ADSmartPickup/adCourseCache.lua")
local course = ADCourseCache.newRecord({vehicle = "v7", field = 22, width = 30.0, sig = "n=4;a=960000;c=800,300",
settings = ADCourseCache.settingsKey({multiTools = 2, lane = -1}), waypoints = 812})
for key in pairs(course) do
check("ST3 kursus-felt med i lagringen: " .. key, ADFieldStorage.COURSE_TYPES[key] ~= nil, true)
end
local cxml = fakeXml()
ADFieldStorage.writeCourseRecord(cxml, "c", course)
local cback = ADFieldStorage.readCourseRecord(cxml, "c")
for key, value in pairs(course) do
check("ST4 kursus-rundtur: " .. key, tostring(cback[key]), tostring(value))
end
check("ST4b matcher stadig efter rundtur", ADCourseCache.matches(cback, {field = 22, width = 30, sig = course.sig, settings = course.settings}), true)
-- hel fil: save -> load med falsk XMLFile (iterate over indekserede nøgler)
local function fileXml(store)
local xml = fakeXml()
for _, kind in ipairs({"Int", "String", "Float"}) do
xml["set" .. kind] = function(_, key, value) store[key] = value end
xml["get" .. kind] = function(_, key) return store[key] end
end
function xml.iterate(_, base, fn)
local index = 0
while true do
local prefix = string.format("%s(%d)", base, index)
local found = false
for key in pairs(store) do
if key:sub(1, #prefix + 1) == prefix .. "#" or key:sub(1, #prefix + 1) == prefix .. "." then found = true break end
end
if not found then return end
fn(index + 1, prefix)
index = index + 1
end
end
function xml.save() end
function xml.delete() end
return xml
end
local disk = {}
ADRunsStorage = {FILE_NAME = "runs.xml", getPath = function() return "/save/runs.xml" end}
XMLFile = {create = function() return fileXml(disk) end, load = function() return fileXml(disk) end}
function fileExists() return true end
check("ST5 gem", ADFieldStorage.save({fields = {[77] = field}, fleet = {}, fleetMode = "all",
courseRecords = ADCourseCache.withRecord({}, course)}), true)
local loaded = ADFieldStorage.load()
check("ST6 kursus indlæst", loaded.courseRecords.v7 ~= nil and loaded.courseRecords.v7.waypoints, 812)
check("ST6b mark indlæst", loaded.fields[77] ~= nil, true)
-- ST7: gammel fil uden <courses> -> tomt register (bagudkompatibelt)
disk = {}
ADFieldStorage.save({fields = {}, fleet = {}, fleetMode = "all"})
local old = ADFieldStorage.load()
check("ST7 gammel fil: tomt register", next(old.courseRecords), nil)
-- ST8: manuel transport pr. maskine gemmes og indlæses; gammel fil uden -> tom
disk = {}
ADFieldStorage.save({fields = {}, fleet = {}, fleetMode = "all", manual = {vehicleCombine = true}})
check("ST8 manuel transport indlæst", ADFieldStorage.load().manual.vehicleCombine, true)
check("ST8 gammel fil: ingen manuelle", next(old.manual or {}), nil)
-- ST9: pause ("Stop alt og parkér") gemmes, så intet starter af sig selv efter indlæsning
disk = {}
ADFieldStorage.save({fields = {}, fleet = {}, fleetMode = "all", paused = true})
check("ST9 pause indlæst", ADFieldStorage.load().paused, true)
disk = {}
ADFieldStorage.save({fields = {}, fleet = {}, fleetMode = "all"})
check("ST9 ingen pause", ADFieldStorage.load().paused, false)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-66
View File
@@ -1,66 +0,0 @@
-- Kør: luajit tests/test_adFieldView.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFieldModel.lua")
dofile("FS25_ADSmartPickup/adFieldView.lua")
local V = ADFieldView
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
local infos = {
{id = 109, mowable = true, markerId = 5},
{id = 12, mowable = true, markerId = nil},
{id = 90, mowable = false, markerId = 7},
}
local fields = {[109] = ADFieldModel.newField(109), [12] = ADFieldModel.newField(12)}
-- V1: alle marker (etape 2: også høst og bare), sorteret efter nummer
local rows = V.rows(infos, fields)
check("V1 tre rækker", #rows, 3)
check("V1 sorteret", rows[1].info.id, 12)
check("V1 model med", rows[3].field.id, 109)
-- V2: mangler model -> ny standardmark
check("V2 standardmodel", rows[2].field.stage, "idle")
-- V3: status — manglende markør vinder over trin
check("V3 mangler markør", V.statusKey(fields[12], infos[2]), "spu_field_noMarker")
check("V3 trin", V.statusKey(fields[109], infos[1]), "spu_field_stage_idle")
-- V4: planens tekstnøgler pr. markart
check("V4 ensilage", V.planKey({product = "auto"}, "mowable"), "spu_field_plan_silage")
check("V4 hø", V.planKey({product = "hay"}, "mowable"), "spu_field_plan_hay")
check("V4 høst", V.planKey({product = "auto"}, "harvest"), "spu_field_plan_harvest")
check("V4 bar mark", V.planKey({product = "auto"}, "bare"), "spu_field_plan_prep")
-- V5: jordtilstand som korte tekstdele
local parts = V.soilParts({lime = 0.2, fert = 1, fertMax = 2, weed = 3, needs = {lime = true, plow = true}})
check("V5 dele", table.concat(parts, ","), "lime,plow,fert,weed")
check("V5 intet", #V.soilParts({lime = 2, fert = 2, fertMax = 2, weed = 0, needs = {}}), 0)
-- cropText: høstet = stub (afgrøde), bar = bar jord, ellers afgrøden
local texts = {spu_field_stubble = "stub (%s)", spu_field_bare = "bar jord"}
local function T(key) return texts[key] end
check("FVcrop stub", ADFieldView.cropText({kind = "bare", stubbleOf = "Hvede", cropTitle = "Hvede"}, T), "stub (Hvede)")
check("FVcrop bar", ADFieldView.cropText({kind = "bare"}, T), "bar jord")
check("FVcrop afgrøde", ADFieldView.cropText({kind = "harvest", cropTitle = "Majs"}, T), "Majs")
-- FVstore: marksilo først, gæringssilo, så afstand; valgte vises altid; kun de nærmeste af resten
local stores = {
{id = "fjern", distance = 3000},
{id = "nær", distance = 200},
{id = "mark", distance = 90, fieldSilo = true},
{id = "valgt", distance = 5000, chosen = true},
{id = "gær", distance = 800, fermenter = true},
{id = "midt", distance = 1200},
}
local storeRows, hiddenStores = ADFieldView.storeRows(stores, 2)
local order = {}
for _, e in ipairs(storeRows) do table.insert(order, e.id) end
check("FVstore rækkefølge", table.concat(order, ","), "mark,gær,nær,midt,valgt")
check("FVstore skjulte", hiddenStores, 1)
check("FVstore meter", ADFieldView.distanceText(90.4), "90 m")
check("FVstore km", ADFieldView.distanceText(1420), "1,4 km")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-22
View File
@@ -1,22 +0,0 @@
-- Kør: luajit tests/test_adFieldWorkBuying.lua (fra repo-roden)
-- Påfyldning ved købestation (in-game 26/9: "Liquid Buy Station", FarmFillStations buyingStationConfigurable):
-- ingen lagerbeholdning, så varen læses fra udbud/triggere/AI-varer.
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
Logging = {info = function() end, warning = function() end}
dofile("FS25_ADSmartPickup/adFieldWork.lua")
local HERBICIDE, LIME, SEEDS = 20, 21, 22
local W = ADFieldWork
check("BS1 udbudte varer", W.buyingSupports({providedFillTypes = {[HERBICIDE] = true}}, HERBICIDE), true)
check("BS2 triggerens varer", W.buyingSupports({loadTriggers = {{fillTypes = {[HERBICIDE] = true}}}}, HERBICIDE), true)
check("BS3 AI-varer", W.buyingSupports({getAISupportedFillTypes = function() return {[SEEDS] = true} end}, SEEDS), true)
check("BS4 sælger ikke kalk", W.buyingSupports({providedFillTypes = {[HERBICIDE] = true}}, LIME), false)
check("BS5 fejl i opslag = nej", W.buyingSupports({getAISupportedFillTypes = function() error("x") end}, LIME), false)
check("BS6 ingen station", W.buyingSupports(nil, LIME), false)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-98
View File
@@ -1,98 +0,0 @@
-- Kør: luajit tests/test_adFields.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFields.lua")
local F = ADFields
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
-- F1/F2: markørnavne
check("F1 F109", F.parseMarkerName("F109"), 109)
check("F1 småt", F.parseMarkerName("f7"), 7)
check("F1 mellemrum", F.parseMarkerName(" F 12 "), 12)
check("F2 andet navn", F.parseMarkerName("Field 109"), nil)
check("F2 tekst efter", F.parseMarkerName("F109 wait"), nil)
check("F2 nil", F.parseMarkerName(nil), nil)
-- F3: slåafgrøde = MOWER giver et skår som TEDDER gør til DRY…; halm udelukket
local names = {[10] = "GRASS_WINDROW", [11] = "DRYGRASS_WINDROW", [20] = "STRAW", [30] = "CLOVER_WINDROW", [31] = "DRYCLOVER_WINDROW", [40] = "BEANSTRAW"}
local mower = {[1] = {fillTypeIndex = 10}, [2] = {fillTypeIndex = 20}, [3] = {fillTypeIndex = 30}, [4] = {fillTypeIndex = 40}}
local tedder = {[10] = {targetFillTypeIndex = 11}, [30] = {targetFillTypeIndex = 31}, [40] = {targetFillTypeIndex = 20}}
local mowable = F.buildMowable(mower, tedder, function(index) return names[index] end)
check("F3 græs", mowable[1] ~= nil, true)
check("F3 græs skår", mowable[1].windrow, 10)
check("F3 græs hø", mowable[1].hay, 11)
check("F3 rug (halm) ikke", mowable[2], nil)
check("F3 kløver", mowable[3].hay, 31)
check("F3 bønnehalm -> halm ikke", mowable[4], nil)
-- F4: markens art og om den vokser
local kind, growing = F.classify(3, false, 0.4)
check("F4 høstafgrøde", kind, "harvest"); check("F4 vokser", growing, true)
check("F4 klar høst vokser ikke", select(2, F.classify(3, false, 0.95)), false)
check("F4 slåafgrøde", (F.classify(1, true, 0.2)), "mowable")
check("F4 ingen afgrøde = bar", (F.classify(nil, false, 0)), "bare")
check("F4 bar vokser ikke", select(2, F.classify(nil, false, 0)), false)
-- F5: jordens behov ud fra gennemsnit og spillets indstillinger
local needs = F.soilNeeds({lime = 0.2, plowLevel = 0}, {limeRequired = true, plowingRequired = true})
check("F5 kalk", needs.lime, true); check("F5 pløj", needs.plow, true)
local off = F.soilNeeds({lime = 0, plowLevel = 0}, {limeRequired = false, plowingRequired = false})
check("F5 kalk slået fra i spillet", off.lime, false); check("F5 pløjning slået fra", off.plow, false)
check("F5 kalket jord", F.soilNeeds({lime = 2, plowLevel = 1}, {limeRequired = true, plowingRequired = true}).lime, false)
check("F5 ukendt niveau = intet behov", F.soilNeeds({}, {limeRequired = true, plowingRequired = true}).plow, false)
-- F6: vækststadie -> fase ud fra afgrødens egne grænser
local wheat = {minHarvestingGrowthState = 6, maxHarvestingGrowthState = 7, cutState = 9, witheredState = 8}
check("F6 intet", F.growthPhase(0, wheat), "none")
check("F6 spirer/vokser", F.growthPhase(3, wheat), "growing")
check("F6 klar", F.growthPhase(6, wheat), "ready")
check("F6 visnet", F.growthPhase(8, wheat), "withered")
check("F6 høstet stub", F.growthPhase(9, wheat), "cut")
check("F6 ukendt stadie", F.growthPhase(nil, wheat), nil)
check("F6 FS25-navne (minHarvestGrowthState)", F.growthPhase(6, {minHarvestGrowthState = 5, maxHarvestGrowthState = 6, cutState = 8}), "ready")
-- F7: vokser = fase growing; ukendt fase -> voksende pixels og ikke klar
check("F7 fase vinder", F.isGrowing("cut", 100, 0.1), false)
check("F7 fase vokser", F.isGrowing("growing", 0, 0), true)
check("F7 reserve: voksende pixels", F.isGrowing(nil, 50, 0.2), true)
check("F7 reserve: ingen pixels (stub)", F.isGrowing(nil, 0, 0), false)
check("F7 reserve: klar", F.isGrowing(nil, 50, 0.95), false)
-- F8: slåafgrøde fuldt udvokset (Dakota-græs 3 = mellemhøjde, 4-5 = fuld)
check("F8 prime græs", F.primeState({minHarvestingGrowthState = 3, maxHarvestingGrowthState = 5}), 4)
check("F8 prime ét klar-stadie", F.primeState({minHarvestingGrowthState = 6, maxHarvestingGrowthState = 6}), 6)
check("F8 prime ukendt", F.primeState({}), nil)
check("F8 mellemhøjde tæller ikke", F.isPrime(3, 4), false)
check("F8 fuld højde tæller", F.isPrime(5, 4), true)
check("F8 ukendt stadie tæller", F.isPrime(nil, 4), true)
check("F8 ingen krav", F.isPrime(3, nil), true)
-- F9: kartofler (Dakota): 6 = klar men kun forberedelig, 10 = toppet (høstklar)
local potato = {minHarvestingGrowthState = 10, maxHarvestingGrowthState = 10, minPreparingGrowthState = 6, maxPreparingGrowthState = 6, witheredState = 7, cutState = 8}
check("F9 utoppet er forberedelig", F.isPreparable(6, potato), true)
check("F9 voksende ikke", F.isPreparable(5, potato), false)
check("F9 fase klar", F.growthPhase(6, potato), "ready")
check("F9 toppet klar", F.growthPhase(10, potato), "ready")
check("F9 ingen forberedelse", F.isPreparable(6, {minPreparingGrowthState = -1}), false)
-- jordtype -> klargjort/pløjet (værdierne kommer fra spillets fieldGroundSystem)
local groundValues = {STUBBLE_TILLAGE = 1, CULTIVATED = 2, SEEDBED = 3, PLOWED = 4, ROLLED_SEEDBED = 5, SOWN = 6}
local function cls(v) local p, pl = ADFields.classifyGround(v, groundValues) return tostring(p) .. "|" .. tostring(pl) end
check("jord: pløjet", cls(4), "true|true")
check("jord: kultiveret", cls(2), "true|false")
check("jord: stubbearbejdet", cls(1), "true|false")
check("jord: sået", cls(6), "false|false")
check("jord: ukendt værdi", cls(nil), "nil|nil")
groundValues.NONE = 0
check("jord: ingen markjord tæller ikke med", cls(0), "nil|nil")
-- vin/oliven er aldrig slåafgrøder, selv om et mod har sat dem på MOWER-listen (in-game 29/9: F22 hvide druer fik
-- planen "Silage + så" og en skårlægger sendt ud)
check("vin: druer ikke slåbare", ADFields.isMowableFruit("GRAPE", {windrow = "GRASS_WINDROW"}), false)
check("vin: oliven ikke slåbare", ADFields.isMowableFruit("oliveb", {windrow = "X"}), false)
check("græs slåbart", ADFields.isMowableFruit("GRASS", {windrow = "GRASS_WINDROW"}), true)
check("ikke på MOWER-listen", ADFields.isMowableFruit("WHEAT", nil), false)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-99
View File
@@ -1,99 +0,0 @@
-- Kør: luajit tests/test_adFieldsPolygon.lua (fra repo-roden)
-- FS25-marker har ingen polygonPoints — omridset hentes med field:getDensityMapPolygon():getVerticesList().
-- In-game 26/9 (Dakota 16x, marker på 1,7 x 0,6 km): uden omrids blev kun markskiltet (posX/posZ) prøvet;
-- F108 med majs viste "bar jord" og "mangler markør" (markøren stod ved kanten, 890 m fra skiltet).
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
Logging = {warning = function() end, info = function() end}
local MAIZE = 10
function getWorldTranslation() return 0, 0, 0 end
function getTerrainHeightAtWorldPos() return 0 end
-- markskiltet står i marken, men ved en stribe uden afgrøde (x > 1400)
function cropAt(x) return x <= 1400 and MAIZE or nil end
FSDensityMapUtil = {
getFieldDataAtWorldPosition = function() return 1, 2 end,
getFruitTypeIndexAtWorldPos = function(x, z) return cropAt(x) end,
getFruitArea = function() return 0, 10 end,
}
FieldDensityMap = {SPRAY_LEVEL = 4, LIME_LEVEL = 5, PLOW_LEVEL = 6}
DensityCoordType = {POINT_VECTOR_VECTOR = 1}
DensityMapModifier = {new = function() return {setParallelogramWorldCoords = function() end, executeGet = function() return 1, 1 end} end}
g_fruitTypeManager = {
getFruitTypeByIndex = function() return {name = "MAIZE", fillType = {index = 60, title = "Majs"}, minHarvestingGrowthState = 7, maxHarvestingGrowthState = 7, cutState = 9} end,
getConverterDataByName = function() return {} end,
}
g_fillTypeManager = {getConverterDataByName = function() return {} end, getFillTypeNameByIndex = function() return "X" end}
g_farmlandManager = {getFarmlandOwner = function() return 1 end}
local polygon = {getVerticesList = function() return {0, 0, 1600, 0, 1600, 600, 0, 600} end}
local field = {getId = function() return 108 end, farmland = {id = 108}, posX = 1500, posZ = 50,
getDensityMapPolygon = function() return polygon end}
g_fieldManager = {getFields = function() return {field} end}
g_currentMission = {terrainRootNode = 1, missionInfo = {}, fieldGroundSystem = {
getDensityMapData = function() return 1, 0, 2 end, getMaxValue = function() return 2 end}}
local adEnv = {ADGraphManager = {
getMapMarkers = function() return {{id = 900, name = "F108"}} end,
-- markøren står 5 m uden for marken ved den modsatte kant: 1505 m fra skiltet
getWayPointById = function() return {x = -5, z = 300} end,
}}
dofile("FS25_ADSmartPickup/adFields.lua")
local now = 0
for _ = 1, 1000 do
if ADFields.getInfo(108) ~= nil then break end
ADFields.scanStep(adEnv, 1, now)
now = now + 16
end
local info = ADFields.getInfo(108)
check("PG1 marken er scannet", info ~= nil, true)
check("PG2 omrids fra getDensityMapPolygon", info and #info.verts, 4)
check("PG3 majsen findes trods skiltet på bar stribe", info and info.fruitName, "MAIZE")
check("PG4 ikke bar mark", info and info.kind, "harvest")
check("PG5 markør ved kanten findes (afstand til omridset)", info and info.markerId, 900)
check("PG6 afstand = til kanten", info and info.markerDistance, 5)
check("PG6b markens form-signatur (kursus-genbrug)", info and info.shapeSig, "n=4;a=960000;c=800,300")
-- PG7-9: field.fieldState er spillets sandhed (fields.xml: MAIZE growthState 2); jord-bits fra
-- getFieldDataAtWorldPosition (2. værdi) er IKKE vækststadie. Høstet mark (cutState) = bar, vist som stub.
field.fieldState = {fruitTypeIndex = MAIZE, growthState = 2}
FSDensityMapUtil.getFieldDataAtWorldPosition = function() return true, 0 end -- jord-bits 0 lignede "ingen vækst"
local function rescan()
ADFields.cache, ADFields.listCache, ADFields.activeScan, ADFields.lastScanStart = {}, nil, nil, nil
for _ = 1, 2000 do
if ADFields.getInfo(108) ~= nil then break end
ADFields.scanStep(adEnv, 1, now)
now = now + 16
end
return ADFields.getInfo(108)
end
info = rescan()
check("PG7 majs i vækst (fieldState), ikke bar", info and info.kind, "harvest")
check("PG7 fase vokser", info and info.phase, "growing")
field.fieldState = {fruitTypeIndex = MAIZE, growthState = 9}
info = rescan()
check("PG8 høstet (cutState) = bar", info and info.kind, "bare")
check("PG8 stub af majs", info and info.stubbleOf, "Majs")
field.fieldState = {fruitTypeIndex = 0, growthState = 0}
cropAt = function() return nil end
info = rescan()
check("PG9 ingen afgrøde = bar uden stub", info and info.kind .. "/" .. tostring(info.stubbleOf), "bare/nil")
-- PG10-12: bygget-over mark (fields.xml: groundType NONE — gårdsplads/stald/biogas) er ikke med i listen
field.fieldState = {fruitTypeIndex = 0, growthState = 0, groundType = 0}
info = rescan()
check("PG10 ingen markjord = kan ikke dyrkes", info and info.cultivable, false)
check("PG10 ikke i marklisten", #ADFields.list(), 0)
field.fieldState = {fruitTypeIndex = MAIZE, growthState = 2, groundType = 7}
cropAt = function(x) return MAIZE end
info = rescan()
check("PG11 markjord = kan dyrkes", info and info.cultivable, true)
check("PG11 i marklisten", #ADFields.list(), 1)
check("PG12 ingen punkter på markjord", ADFields.isCultivable(nil, 0), false)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-94
View File
@@ -1,94 +0,0 @@
-- Kør: luajit tests/test_adFieldsScan.lua (fra repo-roden)
-- Markscanningen som arbejdskø: bliver færdig, giver rigtigt resultat, og intet frame laver mere end ét
-- prøvepunkts arbejde (in-game 26/9: en hel mark i ét frame gav hak).
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
Logging = {warning = function(fmt, ...) print("WARN " .. string.format(fmt, ...)) end, info = function() end}
-- falsk verden: én mark (F7) med hvede der er klar, polygon med 4 hjørner
local GRASS, WHEAT = 1, 3
local nodes = {[101] = {0, 0}, [102] = {100, 0}, [103] = {100, 100}, [104] = {0, 100}}
function getWorldTranslation(node) local p = nodes[node]; return p[1], 0, p[2] end
function getTerrainHeightAtWorldPos() return 0 end
local calls = 0
FSDensityMapUtil = {
getFieldDataAtWorldPosition = function() calls = calls + 1; return true, 170 end, -- på mark (2. værdi = jord-bits, IKKE vækst)
getFruitTypeIndexAtWorldPos = function() calls = calls + 1; return WHEAT end,
getFruitArea = function(_, _, _, _, _, _, _, any) calls = calls + 1; return 0, any and 10 or 10 end,
}
FieldDensityMap = {SPRAY_LEVEL = 4, LIME_LEVEL = 5, PLOW_LEVEL = 6}
DensityCoordType = {POINT_VECTOR_VECTOR = 1}
DensityMapModifier = {new = function() return {setParallelogramWorldCoords = function() end, executeGet = function() calls = calls + 1; return 1, 1 end} end}
g_fruitTypeManager = {
getFruitTypeByIndex = function(_, index) return {name = index == WHEAT and "WHEAT" or "GRASS", fillType = {index = 50, title = "Hvede"}, minHarvestingGrowthState = 6, maxHarvestingGrowthState = 7, cutState = 9} end,
getConverterDataByName = function() return {} end,
}
g_fillTypeManager = {getConverterDataByName = function() return {} end, getFillTypeNameByIndex = function() return "X" end}
g_farmlandManager = {getFarmlandOwner = function() return 1 end}
-- spillets egen marktilstand (field.fieldState, som fields.xml): afgrøde + vækststadie
local field = {getId = function() return 7 end, farmland = {id = 70}, polygonPoints = {101, 102, 103, 104}, posX = 50, posZ = 50,
fieldState = {fruitTypeIndex = WHEAT, growthState = 6}}
g_fieldManager = {getFields = function() return {field} end}
g_currentMission = {terrainRootNode = 1, missionInfo = {}, fieldGroundSystem = {
getDensityMapData = function() return 1, 0, 2 end, getMaxValue = function() return 2 end}}
local adEnv = {ADGraphManager = {
getMapMarkers = function() return {{id = 900, name = "F7"}, {id = 901, name = "Silo"}} end,
getWayPointById = function(_, id) return {x = 40, z = 40} end,
}}
dofile("FS25_ADSmartPickup/adFields.lua")
local maxPerFrame, frames = 0, 0
local now = 0
for _ = 1, 200 do
calls = 0
ADFields.scanStep(adEnv, 1, now)
if calls > maxPerFrame then maxPerFrame = calls end
frames = frames + 1
now = now + 16
if ADFields.getInfo(7) ~= nil then break end
end
-- markliste først, så marken (efter SCAN_INTERVAL) — kør videre til marken er scannet
for _ = 1, 400 do
if ADFields.getInfo(7) ~= nil then break end
calls = 0
ADFields.scanStep(adEnv, 1, now)
if calls > maxPerFrame then maxPerFrame = calls end
now = now + 16
end
local info = ADFields.getInfo(7)
check("SC1 marken er scannet", info ~= nil, true)
check("SC2 afgrøde", info and info.fruitName, "WHEAT")
check("SC3 art = høst", info and info.kind, "harvest")
check("SC4 klar-andel", info and info.readyRatio, 1)
check("SC5 fase klar", info and info.phase, "ready")
check("SC6 markør F7", info and info.markerId, 900)
check("SC7 prøvepunkter", info and #info.points, 5)
check("SC8 gødning læst", info and info.fert, 1)
check("SC9 højst ét punkts arbejde pr. frame (<= 4 opslag)", maxPerFrame <= 4, true)
print(" (flest opslag i ét frame: " .. maxPerFrame .. ")")
check("SC10 marklisten", #ADFields.list(), 1)
-- SC11: med ur kører flere opgaver pr. frame inden for budgettet (0,5 ms), højst MAX_TASKS_PER_FRAME
local fakeTime = 0
netGetTime = function() fakeTime = fakeTime + 0.1; return fakeTime end -- hver opgave "koster" 0,1 ms
ADFields.cache, ADFields.listCache, ADFields.activeScan, ADFields.lastScanStart = {}, nil, nil, nil
local frames2 = 0
for _ = 1, 400 do
if ADFields.getInfo(7) ~= nil then break end
ADFields.scanStep(adEnv, 1, now)
now = now + 16
frames2 = frames2 + 1
end
check("SC11 scannet igen med ur", ADFields.getInfo(7) ~= nil, true)
check("SC11 færre frames end én opgave pr. frame", frames2 < frames + 0, true)
print(" (frames uden ur: " .. frames .. ", med ur: " .. frames2 .. ")")
netGetTime = nil
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-44
View File
@@ -1,44 +0,0 @@
-- Kør: luajit tests/test_adFieldsShape.lua (fra repo-roden)
-- Markens form-fingeraftryk (genbrug af Courseplay-kurser): antal hjørner, areal og tyngdepunkt, afrundet så
-- små float-forskelle ikke tæller — men en ændret mark (nyt hjørne, flyttet kant) giver en ny signatur.
dofile("FS25_ADSmartPickup/adFields.lua")
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
local function rect(x0, z0, w, h)
return {{x = x0, z = z0}, {x = x0 + w, z = z0}, {x = x0 + w, z = z0 + h}, {x = x0, z = z0 + h}}
end
-- S1: rektangel 1600 x 600 -> 4 hjørner, 960.000 m², tyngdepunkt (800, 300)
check("S1 rektangel", ADFields.shapeSignature(rect(0, 0, 1600, 600)), "n=4;a=960000;c=800,300")
-- S2: omløbsretning er ligegyldig (arealet er absolut)
local reversed = {}
for index = 4, 1, -1 do table.insert(reversed, rect(0, 0, 1600, 600)[index]) end
check("S2 modsat omløb", ADFields.shapeSignature(reversed), "n=4;a=960000;c=800,300")
-- S3: små float-forskelle (cm) tæller ikke
local noisy = rect(0.0003, -0.0002, 1600.0004, 599.9997)
check("S3 float-støj", ADFields.shapeSignature(noisy), "n=4;a=960000;c=800,300")
-- S4: flyttet kant (1 m bredere) -> ny signatur
check("S4 kant flyttet", ADFields.shapeSignature(rect(0, 0, 1601, 600)) ~= ADFields.shapeSignature(rect(0, 0, 1600, 600)), true)
-- S5: ekstra hjørne på kanten (samme areal) -> ny signatur
local extra = {{x = 0, z = 0}, {x = 800, z = 0}, {x = 1600, z = 0}, {x = 1600, z = 600}, {x = 0, z = 600}}
check("S5 ekstra hjørne", ADFields.shapeSignature(extra), "n=5;a=960000;c=800,300")
-- S6: samme form et andet sted på kortet -> anden signatur
check("S6 flyttet mark", ADFields.shapeSignature(rect(100, 0, 1600, 600)), "n=4;a=960000;c=900,300")
-- S7: tom/ingen polygon -> nil
check("S7 tom", ADFields.shapeSignature({}), nil)
check("S8 nil", ADFields.shapeSignature(nil), nil)
-- S9: under 3 hjørner er ingen flade -> nil
check("S9 to punkter", ADFields.shapeSignature({{x = 0, z = 0}, {x = 1, z = 1}}), nil)
-- S10: trekant: areal 50, tyngdepunkt (10/3, 10/3) -> (3, 3)
check("S10 trekant", ADFields.shapeSignature({{x = 0, z = 0}, {x = 10, z = 0}, {x = 0, z = 10}}), "n=3;a=50;c=3,3")
-- S11: negative koordinater afrundes symmetrisk
check("S11 negative", ADFields.shapeSignature(rect(-1000, -500, 200, 100)), "n=4;a=20000;c=-900,-450")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-33
View File
@@ -1,33 +0,0 @@
-- Kør: luajit tests/test_adFleetNames.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFleetNames.lua")
local N = ADFleetNames
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
local plan = N.plan({
{id = "b", model = "Puma 260 CVX"}, {id = "a", model = "Puma 260 CVX"}, {id = "c", model = "Fastrac 4220"},
{id = "d", model = "Cow Food", inRun = true, name = "Cow Food 1"}, {id = "e", model = "CR11", selfPropelled = true, name = "Mejetærsker"},
})
local byId = {}
for _, entry in ipairs(plan) do byId[entry.id] = entry end
check("navne: samme model nummereres (efter id)", byId.a.name, "Puma 260 CVX 1")
check("navne: anden af samme model", byId.b.name, "Puma 260 CVX 2")
check("navne: enlig model uden nummer", byId.c.name, "Fastrac 4220")
check("navne: flow-traktor beholder navn", byId.d.name, "Cow Food 1")
check("navne: selvkørende beholder navn", byId.e.name, "Mejetærsker")
check("parkering: P + navn", byId.a.parkName, "P Puma 260 CVX 1")
check("parkering: flow-traktor", byId.d.parkName, "P Cow Food 1")
check("omdøb: kun hvis ændret", byId.d.rename, false)
check("omdøb: ny navn", byId.a.rename, true)
-- R7: parkeringer omdøbes kun for maskiner der selv omdøbes, og ikke delte parkeringer
check("R7 flow-traktor: parkering røres ikke", byId.d.renamePark, false)
check("R7 selvkørende: parkering røres ikke", byId.e.renamePark, false)
check("R7 omdøbt traktor: parkering omdøbes", byId.a.renamePark, true)
local shared = N.plan({{id = "x", model = "M", park = 5}, {id = "y", model = "Q", park = 5}}, {[5] = 2})
check("R7 delt parkering røres ikke", shared[1].renamePark, false)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-314
View File
@@ -1,314 +0,0 @@
-- Kør: luajit tests/test_adFlowGuide.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adGuide.lua")
dofile("FS25_ADSmartPickup/adRuns.lua")
dofile("FS25_ADSmartPickup/adFlowView.lua")
dofile("FS25_ADSmartPickup/adFlowGuide.lua")
local FG, G = ADFlowGuide, ADGuide
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
local function newRun(kind, changes)
local store, run = ADRuns.createRun(ADRuns.newStore(), "Mælk ud", kind)
if changes ~= nil then store = ADRuns.updateRun(store, run.id, changes) end
return ADRuns.getRun(store, run.id)
end
local function ctx(extra)
local context = {
buildings = {
{id = "b1", name = "Kostald", label = "Kostald M", hasMarker = true, text = "Kostald — M 5 m"},
{id = "b2", name = "Silo", label = "Silo M", hasMarker = true, text = "Silo — M 3 m"},
{id = "b3", name = "Lade", hasMarker = false, text = "Lade — INGEN MARKØR"},
},
fillTypes = {{name = "HAY", title = "Hø"}, {name = "STRAW", title = "Halm"}},
products = {{name = "MILK", title = "Mælk"}, {name = "EGG", title = "Æg"}},
storeBuildings = {{id = "s1", name = "Silo Nord", text = "Silo Nord — M 5 m"}, {id = "s2", name = "Palleskur", text = "Palleskur — M 2 m"}},
waitPools = {{group = "Wait A", count = 2}},
vehicles = {
{id = "t1", label = "Traktor 1", text = "Traktor 1 — 1000 l — OK"},
{id = "t2", text = "Traktor 2", otherRunId = 9, otherRunName = "Andet"},
{id = "t3", text = "Traktor 3", blocked = true},
{id = "t4", text = "Traktor 4"},
},
}
for key, value in pairs(extra or {}) do context[key] = value end
return context
end
local function steps(guide) return table.concat(G.visibleSteps(guide), ",") end
local function findRow(rows, id, item)
for _, row in ipairs(rows) do
if row.id == id and (item == nil or row.item == item) then return row end
end
return nil
end
local function countType(rows, rowType)
local n = 0
for _, row in ipairs(rows) do if row.type == rowType then n = n + 1 end end
return n
end
local function textKeys(rows)
local keys = {}
for _, row in ipairs(rows) do if row.type == "text" and row.label.key ~= nil then table.insert(keys, row.label.key) end end
return table.concat(keys, ",")
end
local function at(guide, step) return G.jump(guide, step, "close") end
-- ---- trin ----
local supply = FG.open(newRun("supply"), ctx())
check("FL1 forsyning", steps(supply), "direction,buildings,goods,rules,wait,tractors,summary")
local out = FG.open(newRun("outbound"), ctx())
check("FL1 udkørsel uden lager-vare", steps(out), "direction,buildings,goods,rules,wait,tractors,summary")
local outStore = FG.open(newRun("outbound", {products = {{fillType = "MILK", policy = "sellElseStore"}}}), ctx())
check("FL1 udkørsel med lager", steps(outStore), "direction,buildings,goods,store,rules,wait,tractors,summary")
local new = FG.openNew("Flow 3", ctx())
check("FL1 nyt flow har navn", steps(new), "name,direction,buildings,goods,rules,wait,tractors,summary")
check("FL1 nyt flow starter på navn", new.step, "name")
check("FL1 nyt flow er forsyning", new.draft.kind, "supply")
check("FL1 nyt flow navn", new.draft.name, "Flow 3")
check("FL1 eksisterende starter på retning", supply.step, "direction")
-- ---- titler følger retningen ----
check("FL2 til", FG.stepKey("buildings", supply), "spu_guide_flow_step_to")
check("FL2 fra", FG.stepKey("buildings", out), "spu_guide_flow_step_from")
check("FL2 andre", FG.stepKey("rules", out), "spu_guide_flow_step_rules")
check("FL2 hint", FG.hintKey("goods", out), "spu_guide_flow_hint_goods_out")
check("FL2 hint forsyning", FG.hintKey("goods", supply), "spu_guide_flow_hint_goods_in")
-- ---- choose er immutabel ----
local toOut = FG.choose(supply, "kind", "outbound")
check("FL3 retning skiftet", toOut.draft.kind, "outbound")
check("FL3 gammel urørt", supply.draft.kind, "supply")
local withTarget = FG.choose(supply, "buildings", {item = "b1", on = true})
check("FL3 forsyning: mål", table.concat(withTarget.draft.targetBuildingIds, ","), "b1")
check("FL3 forsyning: kilder urørt", #withTarget.draft.sourceBuildingIds, 0)
local withSource = FG.choose(out, "buildings", {item = "b2", on = true})
check("FL3 udkørsel: kilde", table.concat(withSource.draft.sourceBuildingIds, ","), "b2")
local policy = FG.choose(out, "policy", {item = "MILK", value = "store"})
check("FL3 politik", policy.draft.products[1].policy, "store")
check("FL3 politik gør lager-trin synligt", FG.isVisible("store", policy), true)
local off = FG.choose(policy, "policy", {item = "MILK", value = "off"})
check("FL3 politik fra fjerner", #off.draft.products, 0)
local replaced = FG.choose(policy, "policy", {item = "MILK", value = "sell"})
check("FL3 politik erstattes", #replaced.draft.products, 1)
check("FL3 politik ny", replaced.draft.products[1].policy, "sell")
check("FL3 navn", FG.choose(new, "name", "Æg til salg").draft.name, "Æg til salg")
check("FL3 tomt navn ignoreres", FG.choose(new, "name", " ").draft.name, "Flow 3")
-- ---- rækker ----
local dirRows = FG.rows(supply)
check("FL4 retning valg", findRow(dirRows, "kind").type, "choice")
check("FL4 retning værdi", findRow(dirRows, "kind").value, "supply")
local buildingRows = FG.rows(at(supply, "buildings"))
check("FL4 bygninger med markør", countType(buildingRows, "check"), 2)
check("FL4 vis alle", findRow(buildingRows, "showAllBuildings").type, "toggle")
local allRows = FG.rows(at(FG.choose(supply, "showAllBuildings", true), "buildings"))
check("FL4 vis alle: tre", countType(allRows, "check"), 3)
local noHidden = FG.rows(at(FG.open(newRun("supply"), ctx({buildings = {{id = "b1", name = "Kostald", hasMarker = true, text = "K"}}})), "buildings"))
check("FL4 ingen vis-alle uden skjulte", findRow(noHidden, "showAllBuildings"), nil)
local chosenFirst = FG.rows(at(FG.choose(supply, "buildings", {item = "b2", on = true}), "buildings"))
local firstCheck
for _, row in ipairs(chosenFirst) do if row.type == "check" and firstCheck == nil then firstCheck = row end end
check("FL4 fast rækkefølge (navn)", firstCheck.item, "b1")
check("FL4 valgt markeret", findRow(chosenFirst, "buildings", "b2").value, true)
check("FL4 ingen bygninger", textKeys(FG.rows(at(FG.open(newRun("supply"), ctx({buildings = {}})), "buildings"))), "spu_noBuildings")
local goodsAuto = FG.rows(at(supply, "goods"))
check("FL5 forsyning auto", findRow(goodsAuto, "ingredientMode").value, "auto")
check("FL5 forsyning auto: ingen liste", countType(goodsAuto, "check"), 0)
local goodsManual = FG.rows(at(FG.choose(supply, "ingredientMode", "manual"), "goods"))
check("FL5 forsyning vælg: varer", countType(goodsManual, "check"), 2)
check("FL5 forsyning vælg uden varer", textKeys(FG.rows(at(FG.choose(FG.open(newRun("supply"), ctx({fillTypes = {}})), "ingredientMode", "manual"), "goods"))), "spu_guide_flow_noGoodsTo")
local goodsOut = FG.rows(at(policy, "goods"))
check("FL5 udkørsel: politik pr. vare", countType(goodsOut, "choice"), 2)
check("FL5 udkørsel: mælk lager", findRow(goodsOut, "policy", "MILK").value, "store")
check("FL5 udkørsel: æg fra", findRow(goodsOut, "policy", "EGG").value, "off")
check("FL5 udkørsel: politikker", #findRow(goodsOut, "policy", "EGG").options, 4)
check("FL5 udkørsel uden varer", textKeys(FG.rows(at(FG.open(newRun("outbound"), ctx({products = {}})), "goods"))), "spu_guide_flow_noProducts")
local storeRows = FG.rows(at(outStore, "store"))
check("FL6 lager auto", findRow(storeRows, "storeAuto").value, true)
check("FL6 lager auto: ingen liste", countType(storeRows, "check"), 0)
local storeManual = FG.rows(at(FG.choose(outStore, "storeAuto", false), "store"))
check("FL6 lager vælg selv", countType(storeManual, "check"), 2)
local chosenStore = FG.open(newRun("outbound", {products = {{fillType = "MILK", policy = "store"}}, storeBuildingIds = {"s2"}}), ctx())
check("FL6 valgt lager -> vælg selv", chosenStore.draft.storeAuto, false)
local rulesOut = FG.rows(at(out, "rules"))
check("FL7 udkørsel: sælg fra", findRow(rulesOut, "sellAtPercent").value, 90)
check("FL7 udkørsel: sælg-trin", #findRow(rulesOut, "sellAtPercent").options, 11)
check("FL7 udkørsel: lager over", findRow(rulesOut, "storeAbovePercent").value, 80)
check("FL7 udkørsel: samlerunde", findRow(rulesOut, "collectRound").type, "toggle")
check("FL7 udkørsel: ingen ture", findRow(rulesOut, "loops"), nil)
local rulesIn = FG.rows(at(supply, "rules"))
check("FL7 forsyning: ture", findRow(rulesIn, "loops").value, 0)
check("FL7 forsyning: ture-valg", #findRow(rulesIn, "loops").options, ADRuns.MAX_LOOPS + 1)
check("FL7 forsyning: 0 = uendeligt", findRow(rulesIn, "loops").options[1].label.key, "spu_loops_infinite")
check("FL7 forsyning: retur", findRow(rulesIn, "returnBelowPercent").value, 50)
check("FL7 forsyning: ingen salg", findRow(rulesIn, "sellAtPercent"), nil)
local waitRows = FG.rows(at(supply, "wait"))
check("FL8 vent: alle først", findRow(waitRows, "waitPoolGroup").options[1].value, "")
check("FL8 vent: puljer", #findRow(waitRows, "waitPoolGroup").options, 2)
local assigned = FG.open(newRun("supply", {}), ctx())
assigned = FG.choose(assigned, "vehicleIds", {item = "t4", on = true})
local tractorRows = FG.rows(at(assigned, "tractors"))
check("FL9 traktorer: kun egne + ledige", countType(tractorRows, "check"), 2)
check("FL9 egne først", (function() for _, r in ipairs(tractorRows) do if r.type == "check" then return r.item end end end)(), "t4")
check("FL9 skjult andre flows", findRow(tractorRows, "vehicleIds", "t2"), nil)
check("FL9 skjult blokeret", findRow(tractorRows, "vehicleIds", "t3"), nil)
check("FL9 linjer", textKeys(tractorRows), "spu_guide_flow_otherFlows")
-- blokerede traktorer: foldet sammen som standard (in-game 27/9: 37 linjer der ikke kunne bruges fyldte trinnet);
-- linjen med antallet er et link der folder navn + grund ud og ind
local function blockedLine(rows)
for _, r in ipairs(rows) do if r.type == "text" and r.label.text ~= nil and r.label.text:find("Traktor 3", 1, true) then return r end end
return nil
end
local function blockedLink(rows)
for _, r in ipairs(rows) do if r.type == "link" and r.id == "showBlocked" then return r end end
return nil
end
check("FL9 blokerede foldet sammen som standard", blockedLine(tractorRows), nil)
check("FL9 link med antal", blockedLink(tractorRows) ~= nil and blockedLink(tractorRows).label.key, "spu_guide_flow_blockedShow")
local expanded = FG.choose(assigned, "showBlocked", true)
local expandedRows = FG.rows(at(expanded, "tractors"))
check("FL9 foldet ud viser navn + grund", blockedLine(expandedRows) ~= nil, true)
check("FL9 foldet ud: link skjuler igen", blockedLink(expandedRows).label.key, "spu_guide_flow_blockedHide")
check("FL9 fold ud ændrer ikke kladden", expanded.draft.vehicleIds[1], "t4")
check("FL9 fold sammen igen", blockedLine(FG.rows(at(FG.choose(expanded, "showBlocked", true), "tractors"))), nil)
local inThisRun = FG.open(newRun("supply"), ctx({vehicles = {{id = "t2", text = "T2", otherRunId = 1, otherRunName = "Mælk ud"}}}))
check("FL9 traktor i dette flow er ikke 'andet flow'", countType(FG.rows(at(inThisRun, "tractors")), "check"), 1)
-- ---- resumé ----
local function lineFor(lines, step)
for _, line in ipairs(lines) do if line.step == step then return line end end
return nil
end
local function partText(line)
local parts = {}
for _, part in ipairs(line.parts) do table.insert(parts, part.key or ("=" .. tostring(part.text))) end
return table.concat(parts, ",")
end
local sumIn = FG.summary(FG.choose(withTarget, "vehicleIds", {item = "t1", on = true}))
check("FL10 navn", partText(lineFor(sumIn, "name")), "=Mælk ud")
check("FL10 retning", partText(lineFor(sumIn, "direction")), "spu_direction_in")
check("FL10 til", partText(lineFor(sumIn, "buildings")), "=Kostald")
check("FL10 til-titel", lineFor(sumIn, "buildings").title.key, "spu_guide_flow_step_to")
check("FL10 varer auto", partText(lineFor(sumIn, "goods")), "spu_guide_flow_goodsAuto")
check("FL10 regler", partText(lineFor(sumIn, "rules")), "spu_guide_flow_sumLoopsInf,spu_guide_flow_sumReturn")
check("FL10 vent", partText(lineFor(sumIn, "wait")), "spu_waitPool_all")
check("FL10 traktorer", partText(lineFor(sumIn, "tractors")), "=Traktor 1")
check("FL10 intet lager-trin", lineFor(sumIn, "store"), nil)
-- traktor der ikke findes længere (solgt): aldrig rå id i resuméet (in-game 29/9)
local withGhost = FG.choose(FG.choose(withTarget, "vehicleIds", {item = "t1", on = true}), "vehicleIds", {item = "vehicleGONE", on = true})
check("FL10b resumé springer manglende over", partText(lineFor(FG.summary(withGhost), "tractors")), "=Traktor 1")
local onlyGhost = FG.choose(withTarget, "vehicleIds", {item = "vehicleGONE", on = true})
check("FL10b kun manglende -> ingen traktorer", partText(lineFor(FG.summary(onlyGhost), "tractors")), "spu_guide_flow_sumNoTractors")
local sumOut = FG.summary(FG.choose(FG.choose(policy, "collectRound", true), "sellAtPercent", 95))
check("FL11 retning ud", partText(lineFor(sumOut, "direction")), "spu_direction_out")
check("FL11 fra tom", partText(lineFor(sumOut, "buildings")), "spu_guide_flow_none")
check("FL11 varer", partText(lineFor(sumOut, "goods")), "spu_policy_store")
check("FL11 varenavn foran", lineFor(sumOut, "goods").parts[1].prefix, "Mælk: ")
check("FL11 lager auto", partText(lineFor(sumOut, "store")), "spu_guide_sum_auto")
-- varen er kun "lager": salgsgrænsen vises ikke (in-game 29/9)
check("FL11 regler (kun lager)", partText(lineFor(sumOut, "rules")), "spu_guide_flow_sumStoreAbove,spu_guide_flow_sumRound")
check("FL11 lager over", lineFor(sumOut, "rules").parts[1].args[1], 80)
check("FL11 ingen traktorer", partText(lineFor(sumOut, "tractors")), "spu_guide_flow_sumNoTractors")
local manyGoods = FG.summary(FG.choose(FG.choose(FG.choose(supply, "ingredientMode", "manual"), "ingredients", {item = "HAY", on = true}), "ingredients", {item = "STRAW", on = true}))
check("FL12 valgte varer", partText(lineFor(manyGoods, "goods")), "=Hø + Halm")
check("FL12 oversigt: links", countType(FG.rows(G.new(supply.def, supply.draft, supply.context, {step = "summary"})), "link"), 7)
-- ---- resultat ----
local r = FG.result(FG.choose(FG.choose(policy, "storeAuto", false), "storeBuildingIds", {item = "s1", on = true}))
check("FL13 ikke ny", r.isNew, false)
check("FL13 run-id", r.runId, 1)
check("FL13 retning", r.changes.kind, "outbound")
check("FL13 lager valgt", table.concat(r.changes.storeBuildingIds, ","), "s1")
check("FL13 varer", r.changes.products[1].fillType, "MILK")
check("FL13 lager-resumé bruger navn", (function() for _, l in ipairs(FG.summary(FG.choose(FG.choose(policy, "storeAuto", false), "storeBuildingIds", {item = "s1", on = true}))) do if l.step == "store" then return l.parts[1].text end end end)(), "Silo Nord")
local rAuto = FG.result(FG.choose(chosenStore, "storeAuto", true))
check("FL13 lager auto -> tom", #rAuto.changes.storeBuildingIds, 0)
local hiddenStore = FG.open(newRun("outbound", {storeBuildingIds = {"s2"}}), ctx())
check("FL13 lager usynligt: bevares", table.concat(FG.result(hiddenStore).changes.storeBuildingIds, ","), "s2")
local rNew = FG.result(FG.choose(new, "vehicleIds", {item = "t1", on = true}))
check("FL14 nyt", rNew.isNew, true)
check("FL14 navn", rNew.name, "Flow 3")
check("FL14 traktorer", table.concat(rNew.vehicleIds, ","), "t1")
check("FL14 ingen run-id", rNew.runId, nil)
local added, removed = FG.vehicleChanges({"a", "b"}, {"b", "c"})
check("FL15 tilføjet", table.concat(added, ","), "c")
check("FL15 fjernet", table.concat(removed, ","), "a")
-- ---- forhåndsvisning (til controllerens opslag) ----
local markers = {b1 = {101, 102}, b2 = {201}}
local preview = FG.previewRun(withSource.draft, function(id) return markers[id] or {} end)
check("FL16 kilder -> markører", table.concat(preview.sourceWayPointIds, ","), "201")
local previewIn = FG.previewRun(withTarget.draft, function(id) return markers[id] or {} end)
check("FL16 mål -> markører", table.concat(previewIn.targetWayPointIds, ","), "101,102")
check("FL16 primært mål", previewIn.targetWayPointId, 101)
check("FL16 kladde urørt", withTarget.draft.targetWayPointIds == nil or #withTarget.draft.targetWayPointIds == 0, true)
-- ---- solo-traktor + ledig vogn (in-game 29/9: flow kunne ikke oprettes uden at omdøbe en vogns parkering) ----
local soloCtx = ctx({
vehicles = {
{id = "s1", label = "Solo 1", text = "Solo 1", solo = true},
{id = "s2", label = "Solo 2", text = "Solo 2", solo = true},
{id = "s3", label = "Solo 3", text = "Solo 3", solo = true},
{id = "t1", label = "Traktor 1", text = "Traktor 1 — OK"},
},
wagons = {{id = "w1", label = "P Disponibel Vogn 4"}, {id = "w2", label = "P Disponibel Vogn 5"}},
})
local soloGuide = FG.open(newRun("supply"), soloCtx)
local one = FG.choose(soloGuide, "vehicleIds", {item = "s1", on = true})
check("WG1 solo tildeles første ledige vogn", one.draft.wagons.s1, "w1")
local two = FG.choose(one, "vehicleIds", {item = "s2", on = true})
check("WG2 næste solo får næste vogn", two.draft.wagons.s2, "w2")
check("WG2 første beholder sin", two.draft.wagons.s1, "w1")
local three = FG.choose(two, "vehicleIds", {item = "s3", on = true})
check("WG3 ingen ledig vogn tilbage -> ingen tildeling", three.draft.wagons.s3, nil)
local soloRows = FG.rows(at(three, "tractors"))
local wagonRow = findRow(soloRows, "wagon", "s1")
check("WG4 vognvalg under solo-traktor", wagonRow ~= nil and wagonRow.type, "choice")
check("WG4 vognvalg viser tildelt vogn", wagonRow ~= nil and wagonRow.value, "w1")
check("WG4 valgmuligheder uden de andres vogne", wagonRow ~= nil and #wagonRow.options, 1)
local noFree = false
for _, row in ipairs(soloRows) do if row.label ~= nil and row.label.key == "spu_guide_flow_noFreeWagon" then noFree = true end end
check("WG5 solo uden ledig vogn får besked", noFree, true)
check("WG5 almindelig traktor har intet vognvalg", findRow(FG.rows(at(FG.choose(soloGuide, "vehicleIds", {item = "t1", on = true}), "tractors")), "wagon", "t1"), nil)
local swapped = FG.choose(FG.choose(one, "vehicleIds", {item = "s2", on = false}), "wagon", {item = "s1", value = "w2"})
check("WG6 skift vogn", swapped.draft.wagons.s1, "w2")
local off = FG.choose(two, "vehicleIds", {item = "s1", on = false})
check("WG7 fravalgt traktor slipper vognen", off.draft.wagons.s1, nil)
local freed = FG.choose(off, "vehicleIds", {item = "s3", on = true})
check("WG7 frigjort vogn går til næste", freed.draft.wagons.s3, "w1")
check("WG8 resultat har vognene", FG.result(two).wagons.s2, "w2")
check("WG8 resultat uden fravalgte", FG.result(off).wagons.s1, nil)
local remembered = newRun("supply", {vehicleIds = {"s2"}})
remembered.wagons = {s2 = "w2"}
local reopened = FG.open(remembered, soloCtx)
check("WG9 husket vogn i kladden", reopened.draft.wagons.s2, "w2")
check("WG9 ny solo tager ikke den huskede", FG.choose(reopened, "vehicleIds", {item = "s1", on = true}).draft.wagons.s1, "w1")
check("WG9 kladden kopierer (run urørt)", (function() local g = FG.choose(reopened, "wagon", {item = "s2", value = "w1"}); return remembered.wagons.s2 end)(), "w2")
-- regler: kun de relevante for varernes politik
local storeOnly = FG.open(newRun("outbound", {products = {{fillType = "MILK", policy = "store"}}}), ctx())
local function keys(rows) local r = {} for _, row in ipairs(rows) do if row.id then table.insert(r, row.id) end end return table.concat(r, ",") end
check("RL1 kun lager: ingen salgsvalg", keys(FG.rows(at(storeOnly, "rules"))), "storeAbovePercent,collectRound")
local sellOnly = FG.open(newRun("outbound", {products = {{fillType = "MILK", policy = "sell"}}}), ctx())
check("RL2 kun salg: ingen lagervalg", keys(FG.rows(at(sellOnly, "rules"))), "sellAtPercent,sellAbovePercent,collectRound")
local function ruleKeys(guide)
for _, line in ipairs(FG.summary(guide)) do
if line.step == "rules" then local r = {} for _, p in ipairs(line.parts) do table.insert(r, p.key) end return table.concat(r, ",") end
end
end
check("RL3 resumé kun lager", ruleKeys(storeOnly), "spu_guide_flow_sumStoreAbove")
check("RL4 resumé kun salg", ruleKeys(sellOnly), "spu_guide_flow_sumSellAt")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-105
View File
@@ -1,105 +0,0 @@
-- Kør: luajit tests/test_adFlowView.lua (fra repo-roden)
-- Ren oprydning af Flows-fanen: tavlens rækkefølge, hvilke traktorer og bygninger der vises.
dofile("FS25_ADSmartPickup/adFlowView.lua")
local V = ADFlowView
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
local function ids(list, field)
local out = {}
for _, item in ipairs(list) do table.insert(out, tostring(item[field or "id"])) end
return table.concat(out, ",")
end
-- FV1: tavlen — forsyning (Ind) først, så udkørsel (Ud); hver gruppe sorteret efter navn (naturligt)
local runs = {
{id = 1, name = "Pig Food", kind = "supply"},
{id = 2, name = "Milk Sale", kind = "outbound"},
{id = 3, name = "Cow Food", kind = "supply"},
{id = 4, name = "Biogas 10", kind = "supply"},
{id = 5, name = "Biogas 9", kind = "supply"},
{id = 6, name = "Digestate", kind = "outbound"},
}
local groups = V.boardGroups(runs)
check("FV1 to grupper", #groups, 2)
check("FV1 Ind først", groups[1].kind, "supply")
check("FV1 Ind sorteret (Biogas 9 før 10)", ids(groups[1].runs), "5,4,3,1")
check("FV1 Ud sorteret", ids(groups[2].runs), "6,2")
check("FV1 samlet rækkefølge til vælgeren", ids(V.orderedRuns(runs)), "5,4,3,1,6,2")
check("FV1 tom gruppe udelades", #V.boardGroups({{id = 1, name = "A", kind = "supply"}}), 1)
-- FV2: traktorer — egne først, så ledige brugbare; andre flows og låste kun med "vis alle"
local vehicles = {
{id = "a", label = "Zetor", state = "free"},
{id = "b", label = "Fendt", state = "in"},
{id = "c", label = "Case", state = "other"},
{id = "d", label = "Claas", state = "blocked"},
{id = "e", label = "Deutz", state = "free"},
}
check("FV2 standard: egne + ledige, sorteret", ids(V.vehicleRows(vehicles, false)), "b,e,a")
check("FV2 vis alle: + andre flows + låste til sidst", ids(V.vehicleRows(vehicles, true)), "b,e,a,c,d")
check("FV2 antal skjulte", V.hiddenCount(vehicles, V.vehicleRows(vehicles, false)), 2)
-- FV3: bygninger — valgte først; uden markør skjult medmindre valgt eller "vis alle"
local buildings = {
{id = "s2", name = "Silo 2", hasMarker = true},
{id = "s10", name = "Silo 10", hasMarker = true},
{id = "x", name = "Gammel lade", hasMarker = false},
{id = "y", name = "Anden uden", hasMarker = false},
{id = "a", name = "Anlæg", hasMarker = true},
}
local chosen = {s10 = true, y = true}
-- fast rækkefølge (et tryk må ikke flytte rækken — in-game 26/9 hoppede valgte drops op i toppen)
check("FV3 standard: fast rækkefølge efter navn, valgte bliver hvor de er", ids(V.buildingRows(buildings, chosen, false)), "y,a,s2,s10")
check("FV3 vis alle: uden markør til sidst", ids(V.buildingRows(buildings, chosen, true)), "y,a,s2,s10,x")
-- ens bygningsnavne (5 x "BGA (1) : DOB 115") sorteres efter markørnavnet
local drops = {
{id = "d5", name = "DOB", label = "Biogas Drop 5", hasMarker = true},
{id = "d3", name = "DOB", label = "Biogas Drop 3", hasMarker = true},
{id = "d1", name = "DOB", label = "Biogas Drop 1", hasMarker = true},
{id = "d4", name = "DOB", label = "Biogas Drop 4", hasMarker = true},
{id = "d2", name = "DOB", label = "Biogas Drop 2", hasMarker = true},
}
check("FV3 ens navne efter markør", ids(V.buildingRows(drops, {d2 = true}, false)), "d1,d2,d3,d4,d5")
-- FV4: ens navne slås sammen med antal ("Chicken Coop x5"), rækkefølge efter første forekomst
check("FV4 sammenlæg", V.joinCounted({"Chicken Coop", "Chicken Coop", "Cow Barn", "Chicken Coop"}, " + ", "-"), "Chicken Coop x3 + Cow Barn")
check("FV4 ingen dubletter", V.joinCounted({"A", "B"}, " + ", "-"), "A + B")
check("FV4 tom -> tekst", V.joinCounted({}, " + ", "(ingen)"), "(ingen)")
-- FV-wrap: lange tekstrækker ombrydes ved mellemrum; indrykning gentages; kort tekst uændret
local wrapped = V.wrapText(" Prepared and sown with Sugar Beet as soon as it is sowing time for it", 30)
check("FVwrap flere linjer", #wrapped > 1, true)
check("FVwrap første linje indrykket", string.sub(wrapped[1], 1, 4), " ")
check("FVwrap anden linje indrykket", string.sub(wrapped[2], 1, 4), " ")
local maxLen = 0
for _, line in ipairs(wrapped) do if #line > maxLen then maxLen = #line end end
check("FVwrap ingen linje over grænsen", maxLen <= 30, true)
check("FVwrap intet ord tabt", table.concat(wrapped, " "):gsub("%s+", " "):gsub("^ ", ""), "Prepared and sown with Sugar Beet as soon as it is sowing time for it")
check("FVwrap kort tekst", V.wrapText("Kort", 30)[1], "Kort")
check("FVwrap langt ord deles ikke", V.wrapText("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 10)[1], "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
-- salgs-/lagerregler vises kun når en vare bruger dem (in-game 29/9: 'sell from 90 %' på et rent lager-flow)
local sells, stores = ADFlowView.policyUse({"store"})
check("politik: kun lager -> sælger ikke", sells, false)
check("politik: kun lager -> lagrer", stores, true)
sells, stores = ADFlowView.policyUse({"sell"})
check("politik: kun salg", tostring(sells) .. tostring(stores), "truefalse")
sells, stores = ADFlowView.policyUse({"sellElseStore", "off"})
check("politik: sælg ellers lager -> begge", tostring(sells) .. tostring(stores), "truetrue")
sells, stores = ADFlowView.policyUse({})
check("politik: ingen varer endnu -> vis begge", tostring(sells) .. tostring(stores), "truetrue")
sells, stores = ADFlowView.policyUse({"off"})
check("politik: kun fra -> vis begge", tostring(sells) .. tostring(stores), "truetrue")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-140
View File
@@ -1,140 +0,0 @@
-- Kør: luajit tests/test_adFlowWagons.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adRuns.lua")
dofile("FS25_ADSmartPickup/adFlowWagons.lua")
local W = ADFlowWagons
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
-- gem/indlæs: traktor -> vogn
local text = W.serialize({vehicleB = "wagon2", vehicleA = "wagon1"})
check("gem: sorteret", text, "vehicleA=wagon1;vehicleB=wagon2")
local parsed = W.parse(text)
check("indlæs: A", parsed.vehicleA, "wagon1")
check("indlæs: B", parsed.vehicleB, "wagon2")
check("indlæs: tom", next(W.parse("")), nil)
check("indlæs: nil", next(W.parse(nil)), nil)
check("indlæs: skrald springes over", next(W.parse("=x;;y=")), nil)
-- husk vogn: ny tabel, gammel urørt
local before = {t1 = "w1"}
local after = W.remember(before, "t1", "w9")
check("husk: ny vogn", after.t1, "w9")
check("husk: original urørt", before.t1, "w1")
check("husk: samme vogn -> samme tabel", W.remember(after, "t1", "w9"), after)
-- start: hent / vent / start
check("start: har redskab bagpå", W.startAction({hasRear = true, wagonId = "w1", wagonExists = true, hasConnect = true}), "start")
check("start: ingen husket vogn", W.startAction({}), "start")
check("start: vognen findes ikke", W.startAction({wagonId = "w1", wagonExists = false, hasConnect = true}), "start")
check("start: vognen uden Connect", W.startAction({wagonId = "w1", wagonExists = true, hasConnect = false}), "start")
check("start: vognen på en anden", W.startAction({wagonId = "w1", wagonExists = true, hasConnect = true, wagonAttached = true}), "wait")
check("start: vognen lånt ud", W.startAction({wagonId = "w1", wagonExists = true, hasConnect = true, wagonBusy = true}), "wait")
check("start: hent", W.startAction({wagonId = "w1", wagonExists = true, hasConnect = true}), "fetch")
-- ejer: flowets vogn er flowets — også når flowet er stoppet (in-game 29/9: marksilo lånte Cow Food-vognen)
local runs = {{id = 1, stopped = false, wagons = {t1 = "w1"}}, {id = 2, stopped = true, wagons = {t2 = "w2"}}}
check("ejer: kørende flow holder", W.isHeld(runs, "w1"), true)
check("ejer: stoppet flow holder stadig", W.isHeld(runs, "w2"), true)
check("ejer: ukendt vogn", W.isHeld(runs, "w3"), false)
check("ejer: flowet findes", W.ownerOf(runs, "w2").id, 2)
-- bygning væk: to tjek i træk
local count, gone = W.missingStep(0, true)
check("væk: første tjek -> ikke endnu", gone, false)
count, gone = W.missingStep(count, true)
check("væk: andet tjek -> væk", gone, true)
count, gone = W.missingStep(1, false)
check("væk: fundet igen nulstiller", count, 0)
check("væk: fundet igen -> ikke væk", gone, false)
-- manglende bygninger i et flow
local exists = {a = true, b = true}
check("mangler: alle findes", #W.missingBuildings({sourceBuildingIds = {"a"}, targetBuildingIds = {"b"}}, exists, exists), 0)
check("mangler: én væk", W.missingBuildings({targetBuildingIds = {"a", "c"}, storeBuildingIds = {}}, exists, {a = true, c = true})[1], "c")
-- review v1.51
check("R1 traktoren er lånt ud af puljen -> vent", W.startAction({tractorBusy = true, hasRear = true, wagonId = "w1", wagonExists = true, hasConnect = true}), "wait")
local runs2 = {{id = 1, stopped = true, wagons = {t1 = "w1"}}, {id = 2, stopped = false, wagons = {t9 = "w1"}}}
check("R4 holdt hvis ET kørende flow har vognen", W.isHeld(runs2, "w1"), true)
local seen = {a = true, c = true}
check("R+ bygning aldrig set i sessionen tæller ikke som væk", #W.missingBuildings({targetBuildingIds = {"b"}}, {a = true}, seen), 0)
check("R+ bygning set og nu væk", W.missingBuildings({targetBuildingIds = {"c"}}, {a = true}, seen)[1], "c")
-- gæt flowets vogn ud fra parkeringsnavnet (ingen husket vogn: vognene var sat på plads før v1.51)
local cands = {{id = "v1", parkName = "P Cow Food vogn 1"}, {id = "v3", parkName = "P Cow Food vogn 3"},
{id = "g1", parkName = "P Goat Food vogn 1"}, {id = "v2", parkName = "P Cow Food vogn 2"}}
check("gæt: samme nummer", W.guessWagon("Cow Food", 3, cands, {}), "v3")
check("gæt: nummer optaget -> anden med flowets navn", W.guessWagon("Cow Food", 3, cands, {v3 = true}), "v1")
check("gæt: andet flow", W.guessWagon("Goat Food", 2, cands, {}), "g1")
check("gæt: intet passer", W.guessWagon("Pig Food", 1, cands, {}), nil)
check("gæt: store/små bogstaver", W.guessWagon("cow food", 2, cands, {}), "v2")
check("gæt: 'Cow Food 1' matcher ikke 'Cow Food 10'", W.guessWagon("Cow Food", 1, {{id = "x", parkName = "P Cow Food vogn 10"}, {id = "y", parkName = "P Cow Food vogn 1"}}, {}), "y")
-- tøm vognen før den parkeres (flow stoppet): mål -> silo/fabrik -> hjem
check("tøm: tom vogn -> hjem", W.emptyStep("start", {cargo = 0}), "home")
check("tøm: last -> flowets mål", W.emptyStep("start", {cargo = 5000, hasTarget = true}), "target")
check("tøm: last uden mål -> silo", W.emptyStep("start", {cargo = 5000, hasTarget = false}), "dump")
check("tøm: kører stadig -> vent", W.emptyStep("target", {cargo = 5000, adActive = true}), "wait")
check("tøm: mål fuldt, rest -> silo", W.emptyStep("target", {cargo = 2000}), "dump")
check("tøm: målet tog det hele -> hjem", W.emptyStep("target", {cargo = 0}), "home")
check("tøm: silo tog det -> hjem", W.emptyStep("dump", {cargo = 0}), "home")
check("tøm: silo kunne ikke -> giv op, hjem", W.emptyStep("dump", {cargo = 2000}), "giveUp")
check("tøm: for længe -> giv op", W.emptyStep("target", {cargo = 5000, adActive = true, elapsedMs = W.EMPTY_TIMEOUT_MS}), "giveUp")
-- flowet startes igen mens traktoren tømmer/kører hjem fra et stop: hjemturen afbrydes, den kører videre med vognen
-- (in-game 29/9: Stop+Start med 1 s mellemrum -> tømning ved forkert mål i 10 min, vogn sat af, traktor i garage)
check("genstart: tømmer -> afbryd", W.abortHomeTripOnStart({emptying = true}), true)
check("genstart: flowets hjemtur -> afbryd", W.abortHomeTripOnStart({flowHoming = true}), true)
check("genstart: puljens hjemtur (markarbejde) -> rør ikke", W.abortHomeTripOnStart({poolReturning = true}), false)
check("genstart: intet i gang", W.abortHomeTripOnStart({}), false)
-- tøm-mål: kun flowets egne mål — AD's 2. markør kan være forældet (æg-markør) eller et Wait-punkt (in-game 29/9)
check("tøm-mål: AD's mål er et af flowets", W.emptyTarget(12638, {12636, 12638}), 12638)
check("tøm-mål: forældet/Wait-markør -> flowets første mål", W.emptyTarget(12658, {12636, 12638}), 12636)
check("tøm-mål: intet AD-mål -> flowets første", W.emptyTarget(nil, {12636}), 12636)
check("tøm-mål: flow uden mål -> intet (silo)", W.emptyTarget(12658, {}), nil)
check("tøm-mål: ingen liste -> intet", W.emptyTarget(12658, nil), nil)
-- ledig traktor (puljen/markarbejde/marksilo må låne den): kun solo fra et stoppet flow
check("ledig: intet flow", W.tractorFree({}), true)
check("ledig: kørende flow", W.tractorFree({hasRun = true}), false)
check("ledig: stoppet flow, solo", W.tractorFree({hasRun = true, stopped = true}), true)
check("ledig: stoppet flow, flow-vogn bagpå", W.tractorFree({hasRun = true, stopped = true, carriesFlowWagon = true}), false)
check("ledig: stoppet flow, tømmer/kører hjem", W.tractorFree({hasRun = true, stopped = true, homeTrip = true}), false)
-- venteårsag i loggen (før stod der altid "i brug andetsteds")
check("vent-årsag: traktor optaget", W.waitReason({tractorBusy = true}), "tractorBusy")
check("vent-årsag: vogn på en anden", W.waitReason({wagonAttached = true}), "wagonAttached")
check("vent-årsag: vogn reserveret", W.waitReason({wagonBusy = true}), "wagonReserved")
-- tømning går i stå ved et fuldt mål (in-game 29/9: fåre-/BGA-vogne stod 10 min ved fuldt mål efter Stop alt)
check("tøm: står ved fuldt mål -> silo", W.emptyStep("target", {cargo = 5000, adActive = true, stalled = true}), "dump")
check("tøm: står ved silo uden at tømme -> næste silo", W.emptyStep("dump", {cargo = 5000, adActive = true, stalled = true, dumpAttempts = 1}), "dump")
check("tøm: 3 siloer prøvet -> giv op", W.emptyStep("dump", {cargo = 5000, adActive = true, stalled = true, dumpAttempts = W.MAX_DUMP_ATTEMPTS}), "giveUp")
check("tøm: kører mod målet (ikke i stå) -> vent", W.emptyStep("target", {cargo = 5000, adActive = true, stalled = false}), "wait")
check("i stå: tæt på, uændret længe", W.isStalled({nearTarget = true, sinceChangeMs = W.EMPTY_STALL_MS}), true)
check("i stå: tæt på, lige ankommet", W.isStalled({nearTarget = true, sinceChangeMs = 5000}), false)
check("i stå: på vej (langt væk)", W.isStalled({nearTarget = false, sinceChangeMs = 999999}), false)
-- hent vogn: ét nyt forsøg efter "stoppedAway" (AD standsede undervejs — fx spærret af en anden traktor)
check("hent igen: stoppedAway første gang", W.retryFetch("stoppedAway (37 m fra markøren)", 0), true)
check("hent igen: stoppedAway anden gang -> nej", W.retryFetch("stoppedAway", 1), false)
check("hent igen: noAttachable -> nej (markøren skal rettes)", W.retryFetch("noAttachable", 0), false)
-- manuel påkobling (spilleren sendte traktoren til Connect): traktor i et kørende flow startes bagefter
-- (in-game 29/9: Cow Food 3 hentede sin vogn manuelt efter et fejlet forsøg og blev holdende)
check("manuel kobling: kørende flow, egen vogn -> start", W.startAfterManualAttach({flowRunning = true, ownWagon = true}), true)
check("manuel kobling: stoppet flow -> nej", W.startAfterManualAttach({flowRunning = false, ownWagon = true}), false)
check("manuel kobling: AD kører allerede -> nej", W.startAfterManualAttach({flowRunning = true, ownWagon = true, adActive = true}), false)
check("manuel kobling: fremmed vogn -> nej", W.startAfterManualAttach({flowRunning = true, ownWagon = false}), false)
-- husk vogn ved hjemtur: kun når flowet KØRTE med den (stop); ikke ved oprydning efter indlæsning, hvor traktoren
-- kan have et puljeredskab på fra et lån (in-game 29/9: Biogas Silage "huskede" en læssevogn fra markarbejdet)
check("husk ved stop af kørende flow", W.shouldRememberOnHome("stop"), true)
check("husk ikke ved oprydning efter indlæsning", W.shouldRememberOnHome("afterLoad"), false)
-- forældreløs påkobling (hentet før genindlæsning, opgaven tabt): sættes tilbage efter 2 min uden ejer
check("forældreløs: ingen ejer, længe nok -> hjem", W.orphanAction({owned = false, idleMs = W.ORPHAN_MS}), "home")
check("forældreløs: har ejer -> intet", W.orphanAction({owned = true, idleMs = W.ORPHAN_MS}), "none")
check("forældreløs: for kort tid -> vent", W.orphanAction({owned = false, idleMs = 1000}), "wait")
-- parkeret sæt på traktorens egen plads er ikke forældreløst (in-game 29/9: vinmarks-REX blev skilt ad)
check("forældreløs: står på egen parkering -> intet", W.orphanAction({owned = false, atOwnPark = true, idleMs = W.ORPHAN_MS}), "none")
-- rækkefølge for ledige vogne i flow-guiden: navnematch, så disponible, så resten (alfabetisk)
local ranked = W.rankWagons("Sugarbeet Cut", {
{id = "a", label = "P Tedder"}, {id = "b", label = "P Disponibel Vogn 5"},
{id = "c", label = "P Sugarbeet Cut vogn 1"}, {id = "d", label = "P Disponibel Vogn 4"}, {id = "e", label = "P Bredal vogn"}})
local order = {}
for _, w in ipairs(ranked) do table.insert(order, w.id) end
check("rank: navn, disponible, resten", table.concat(order, ","), "c,d,b,e,a")
check("rank: tom liste", #W.rankWagons("X", {}), 0)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-113
View File
@@ -1,113 +0,0 @@
-- Kør: luajit tests/test_adGuide.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adGuide.lua")
local G = ADGuide
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
-- trin a..e + summary; b vises kun når draft.showB, d aldrig
local def = {
order = {"a", "b", "c", "d", "e", "summary"},
isVisible = function(step, guide)
if step == "b" then return guide.draft.showB == true end
if step == "d" then return false end
return true
end,
}
local guide = G.new(def, {showB = false, value = 1}, {x = 1})
check("G1 start på første synlige", guide.step, "a")
local index, total = G.position(guide)
check("G1 position", index, 1)
check("G1 total over synlige", total, 4)
check("G1 synlige", table.concat(G.visibleSteps(guide), ","), "a,c,e,summary")
-- næste springer skjulte over
local g2, event = G.next(guide)
check("G2 næste springer b over", g2.step, "c")
check("G2 intet event", event, nil)
check("G2 gammel guide urørt", guide.step, "a")
local g3 = G.next(g2)
check("G3 springer d over", g3.step, "e")
check("G3 position", (G.position(g3)), 3)
local g4 = G.next(g3)
check("G4 summary er sidst", G.isLast(g4), true)
local _, done = G.next(g4)
check("G4 næste på sidste = done", done, "done")
-- tilbage
local back = G.back(g3)
check("G5 tilbage til c", back.step, "c")
local _, cancel = G.back(guide)
check("G5 tilbage på første = cancel", cancel, "cancel")
-- valg der gør et trin synligt ændrer tælling og næste
local shown = G.withDraft(guide, {showB = true, value = 1})
check("G6 withDraft ny guide", guide.draft.showB, false)
check("G6 b synlig", G.next(shown).step, "b")
check("G6 total", select(2, G.position(shown)), 5)
-- aktuelt trin bliver usynligt: næste går til næste synlige i rækkefølgen, position nil
local onB = G.new(def, {showB = true}, {}, {step = "b"})
local hidden = G.withDraft(onB, {showB = false})
check("G7 position nil for skjult trin", (G.position(hidden)), nil)
check("G7 næste efter skjult", G.next(hidden).step, "c")
check("G7 tilbage fra skjult", G.back(hidden).step, "a")
-- spring (Ret) fra oversigt-trinnet: næste -> tilbage til summary, ikke single
local fromSummary = G.jump(g4, "c", "summary")
check("G8 spring trin", fromSummary.step, "c")
check("G8 single", fromSummary.single, true)
local returned, ev = G.next(fromSummary)
check("G8 retur til summary", returned.step, "summary")
check("G8 ikke single mere", returned.single, false)
check("G8 intet event", ev, nil)
-- tilbage i spring gendanner kladden fra før springet
local changed = G.withDraft(fromSummary, {showB = false, value = 99})
local undone = G.back(changed)
check("G9 tilbage gendanner kladde", undone.draft.value, 1)
check("G9 tilbage til summary", undone.step, "summary")
-- spring fra fanens oversigt (close): næste = done, tilbage = cancel
local single = G.new(def, {value = 5}, {}, {step = "e", single = true, returnTo = "close"})
local _, singleDone = G.next(single)
check("G10 single close næste = done", singleDone, "done")
local _, singleCancel = G.back(single)
check("G10 single close tilbage = cancel", singleCancel, "cancel")
check("G10 single isLast", G.isLast(single), true)
-- withContext
local ctxGuide = G.withContext(guide, {x = 2})
check("G11 ny context", ctxGuide.context.x, 2)
check("G11 gammel context", guide.context.x, 1)
-- deepCopy / toggleItem
local original = {list = {1, 2}, nested = {a = {b = 1}}}
local copy = G.deepCopy(original)
copy.list[1] = 9
copy.nested.a.b = 7
check("G12 deepCopy liste", original.list[1], 1)
check("G12 deepCopy indlejret", original.nested.a.b, 1)
local list = {"x", "y"}
check("G13 toggle til", table.concat(G.toggleItem(list, "z", true), ","), "x,y,z")
check("G13 ingen dublet", table.concat(G.toggleItem(list, "x", true), ","), "x,y")
check("G13 toggle fra", table.concat(G.toggleItem(list, "x", false), ","), "y")
check("G13 original urørt", #list, 2)
-- tekster
local texts = {greet = "Hej %s", n = "%d stk", title = "Afgrøde", plain = "Ingen"}
local function T(key) return texts[key] or key end
check("G14 label args", G.labelText({key = "greet", args = {"Bo"}}, T), "Hej Bo")
check("G14 label tekst", G.labelText({text = "rå"}, T), "rå")
check("G14 manglende args fejler ikke", G.labelText({key = "n"}, T), "%d stk")
check("G14 prefix", G.labelText({key = "plain", prefix = "Mælk: "}, T), "Mælk: Ingen")
check("G14 nil label", G.labelText(nil, T), "")
check("G15 linje", G.lineText({step = "crop", title = {key = "title"}, parts = {{key = "plain"}, {key = "n", args = {3}}}}, T), "Afgrøde: Ingen, 3 stk")
check("G15 linje uden dele", G.lineText({step = "crop", title = {key = "title"}, parts = {}}, T), "Afgrøde: -")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-129
View File
@@ -1,129 +0,0 @@
-- Kør: luajit tests/test_adHitch.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adHitch.lua")
local H = ADHitch
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
-- markørnavne -> handling + navn (store/små bogstaver ligegyldigt)
local function parsed(name) local a, n = H.parseMarker(name) return tostring(a) .. "|" .. tostring(n) end
check("navn: Connect Bredal", parsed("Connect Bredal"), "attach|Bredal")
check("navn: små bogstaver", parsed("connect bredal"), "attach|bredal")
check("navn: dansk Kobl", parsed("Kobl Vogn 2"), "attach|Vogn 2")
check("navn: kun Connect", parsed("Connect"), "attach|")
check("navn: Connected er ikke en koblingsmarkør", parsed("Connected silo"), "nil|nil")
check("navn: almindelig markør", parsed("Bredal"), "nil|nil")
check("navn: nil", parsed(nil), "nil|nil")
check("navn: Afkobl Bredal", parsed("Afkobl Bredal"), "detach|Bredal")
check("navn: Kobl af er afkobling, ikke kobling", parsed("Kobl af Bredal"), "detach|Bredal")
check("navn: Detach", parsed("detach"), "detach|")
check("navn: Disconnect", parsed("Disconnect Vogn"), "detach|Vogn")
-- bagerste redskaber: kun led bag traktoren (z < 0 i traktorens rum), ikke frontvægt/frontlæsser
local rear = H.rearImplements({{name = "vægt", z = 3.1}, {name = "Bredal", z = -2.4}, {name = "læsser", z = 1.5}, {name = "plov", z = -1}})
check("bag: kun bagerste", #rear, 2)
check("bag: første", rear[1].name, "Bredal")
-- ankomst: AD er stoppet, traktoren står stille tæt på markøren
check("ankomst: stoppet ved markør", H.hasArrived({adActive = false, distance = 1.2, speed = 0.1}), true)
check("ankomst: AD kører stadig", H.hasArrived({adActive = true, distance = 0.5, speed = 0}), false)
check("ankomst: for langt væk", H.hasArrived({adActive = false, distance = 9, speed = 0}), false)
check("ankomst: ruller stadig", H.hasArrived({adActive = false, distance = 1, speed = 3}), false)
-- efter at have bakket hen til vognen står traktoren længere fra markøren
check("ankomst: bakket til vognen, udvidet afstand", H.hasArrived({adActive = false, distance = 9, speed = 0, arriveDistance = 13}), true)
local crept = H.step(H.newJob(1, "Connect x", "attach"), {adActive = false, distance = 9, speed = 3, arriveDistance = 13}, 4000, false)
check("vagt: bakket, ruller lidt -> ikke 'stoppedAway'", crept.state, "driving")
-- vagten: efter ankomst forsøges koblingen et stykke tid, derefter opgives den
local job = H.newJob(25, "Connect Bredal", "attach")
check("job: venter på AD", job.state, "driving")
check("job: handling", job.action, "attach")
H.step(job, {adActive = true, distance = 30, speed = 10}, 1000, false)
check("job: kører stadig", job.state, "driving")
H.step(job, {adActive = false, distance = 1, speed = 0}, 500, false)
check("job: ankommet, kobling ikke lykkedes endnu", job.state, "acting")
H.step(job, {adActive = false, distance = 1, speed = 0}, 500, true)
check("job: koblet på", job.state, "done")
local failJob = H.newJob(25, "Connect Bredal", "attach")
H.step(failJob, {adActive = false, distance = 1, speed = 0}, 500, false)
for _ = 1, 20 do H.step(failJob, {adActive = false, distance = 1, speed = 0}, 500, false) end
check("job: opgivet efter vagt-tid", failJob.state, "failed")
check("job: årsag", failJob.reason, "noAttachable")
local dropJob = H.newJob(9, "Afkobl Bredal", "detach")
H.step(dropJob, {adActive = false, distance = 1, speed = 0}, 500, false)
for _ = 1, 45 do H.step(dropJob, {adActive = false, distance = 1, speed = 0}, 500, false) end
check("job: afkobling opgivet", dropJob.reason, "nothingToDetach")
local lostJob = H.newJob(25, "Connect Bredal", "attach")
H.step(lostJob, {adActive = false, distance = 40, speed = 0}, 500, false)
check("job: AD ikke startet endnu (opstart-frist)", lostJob.state, "driving")
H.step(lostJob, {adActive = true, distance = 40, speed = 5}, 500, false)
H.step(lostJob, {adActive = false, distance = 40, speed = 0}, 500, false)
check("job: AD stoppede langt fra markøren", lostJob.state, "failed")
check("job: årsag stoppet undervejs", tostring(lostJob.reason):find("^stoppedAway") ~= nil, true)
-- tur: "Connect Bredal > Start" -> to markørnavne
local route = H.parseTrip("Connect", "Bredal", ">", "Start", ">", "Afkobl", "Bredal", ">", "Start")
check("tur: antal trin", #route, 4)
check("tur: første", route[1], "Connect Bredal")
check("tur: tredje", route[3], "Afkobl Bredal")
check("tur: ét trin", #H.parseTrip("Start"), 1)
check("tur: tomme trin droppes", #H.parseTrip("Start", ">", ">", "Stop"), 2)
-- trin: handling pr. markør; almindelige markører = ankom (og vent, hvis ikke sidste)
check("trin: connect", H.stepAction("Connect Bredal"), "attach")
check("trin: afkobl", H.stepAction("Afkobl Bredal"), "detach")
check("trin: almindelig", H.stepAction("Start"), "arrive")
check("vent: mellem trin", H.waitAfter("arrive", 2, 4), H.ROUTE_WAIT_MS)
check("vent: ikke efter sidste", H.waitAfter("arrive", 4, 4), 0)
check("vent: ikke efter kobling", H.waitAfter("attach", 1, 4), 0)
local arriveJob = H.newJob(1, "Start", "arrive")
H.step(arriveJob, {adActive = false, distance = 1, speed = 0}, 250, false)
H.step(arriveJob, {adActive = false, distance = 1, speed = 0}, 250, true)
check("trin: ankom er færdig", arriveJob.state, "done")
local markers = {{name = "Start", id = 1}, {name = "Connect Bredal", id = 9}}
check("markør: navn uden hensyn til store/små", H.findMarker(markers, "connect bredal").id, 9)
check("markør: ukendt", H.findMarker(markers, "Stald"), nil)
-- Connect = kun kobl på; i en tur kobles vognen af ved sin egen AD-parkering
check("connect: kobl på", H.resolveAction("attach", false), "attach")
check("connect med vogn: stadig kobl på (ingen vending)", H.resolveAction("attach", false), "attach")
check("vognens parkering: kobl af", H.resolveAction("arrive", true), "detach")
check("almindelig markør", H.resolveAction("arrive", false), "arrive")
check("afkobl forbliver afkobl", H.resolveAction("detach", false), "detach")
-- 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)
-- afbrydelse: et hente-job kan annulleres (Stop alt, høst slut, spilleren overtager)
ADFieldWork = ADFieldWork or {startAd = function() end}
Logging = Logging or {info = function() end, warning = function() end}
local stubEnv = {ADGraphManager = {getMapMarkerByWayPointId = function() return {name = "Connect X"} end}, AutoDrive = {MODE_DRIVETO = 1}}
local tractorStub = {getFullName = function() return "T" end, stopAutoDrive = function(v) v.stopped = true end}
H.fetch(stubEnv, tractorStub, 9, function() end)
check("annullér: job fandtes", H.cancel(tractorStub), true)
check("annullér: AD stoppet", tractorStub.stopped, true)
check("annullér: intet job tilbage", H.cancel(tractorStub), false)
-- afkobling må tage længere tid (redskabet skal måske foldes ud først)
local foldJob = H.newJob(4, "Plov", "detach")
H.step(foldJob, {adActive = false, distance = 1, speed = 0}, 500, false)
for _ = 1, 16 do H.step(foldJob, {adActive = false, distance = 1, speed = 0}, 500, false) end
check("afkobling: stadig i gang efter 8 s (folder ud)", foldJob.state, "acting")
for _ = 1, 30 do H.step(foldJob, {adActive = false, distance = 1, speed = 0}, 500, false) end
check("afkobling: opgivet efter sin frist", foldJob.state, "failed")
-- afkobling bekræftes: først "done" når intet sidder bag traktoren efter et forsøg (in-game 29/9: loggede
-- "koblede af", men vognen sad der stadig, og traktoren kørte med den)
check("afkobl: redskab bagpå -> forsøg", H.detachStep(1, false), "attempt")
check("afkobl: forsøgt, sidder der stadig -> forsøg igen", H.detachStep(1, true), "attempt")
check("afkobl: forsøgt, intet bagpå -> færdig", H.detachStep(0, true), "done")
check("afkobl: intet bagpå fra start", H.detachStep(0, false), "nothing")
-- transport ved stop: sluk, løft, fold (in-game 29/9: redskaber kørte udfoldet hjem efter stop)
check("transport: tændt+sænket+udfoldet", table.concat(H.stowActions({turnedOn = true, lowered = true, unfolded = true, canFold = true}), ","), "turnOff,raise,fold")
check("transport: allerede klar", #H.stowActions({}), 0)
check("transport: kan ikke foldes -> kun sluk/løft", table.concat(H.stowActions({turnedOn = true, lowered = true, unfolded = true, canFold = false}), ","), "turnOff,raise")
check("transport: kun udfoldet", table.concat(H.stowActions({unfolded = true, canFold = true}), ","), "fold")
check("vagt: stoppedAway med afstand", H.step(H.newJob(1, "Connect x", "attach"), {adActive = false, distance = 37.4, speed = 0}, 4000, false).reason, "stoppedAway (37 m fra markøren)")
-- manuel AD "Kør til": markørnavn først, ellers puljeredskabets egen parkering = kobl af (fast sæt røres ikke)
check("manuel: Connect-markør", H.manualAction("attach", false), "attach")
check("manuel: puljeredskabets parkering", H.manualAction(nil, true), "detach")
check("manuel: almindelig markør", H.manualAction(nil, false), nil)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-26
View File
@@ -1,26 +0,0 @@
-- Kør: luajit tests/test_adHitchCreep.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adHitchCreep.lua")
local C = ADHitchCreep
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
-- plan: vognens trækøje i traktorens rum set fra krogen (x = til siden, z < 0 = bagved)
check("plan: inden for rummelig rækkevidde -> kobl direkte", C.plan({x = 0.4, z = -2.0}), "attach")
check("plan: 5 m lige bagved -> bak", C.plan({x = 0.3, z = -5.0}), "creep")
check("plan: lidt til siden -> bak", C.plan({x = -1.2, z = -6.0}), "creep")
check("plan: for langt til siden -> intet", C.plan({x = 2.5, z = -5.0}), "none")
check("plan: for langt bagved -> intet", C.plan({x = 0, z = -12.0}), "none")
check("plan: foran traktoren -> intet", C.plan({x = 0, z = 5.0}), "none")
check("plan: ingen vogn -> intet", C.plan(nil), "none")
-- skridt under bakningen
check("skridt: vognen inden for rækkevidde -> færdig", C.step({inRange = true, distance = 2.5, startDistance = 6, elapsedMs = 3000}), "done")
check("skridt: på vej -> kør", C.step({inRange = false, distance = 4, startDistance = 6, elapsedMs = 3000}), "drive")
check("skridt: for længe -> giv op", C.step({inRange = false, distance = 4, startDistance = 6, elapsedMs = C.CREEP_TIMEOUT_MS}), "giveUp")
check("skridt: kører væk fra vognen -> giv op", C.step({inRange = false, distance = 7.5, startDistance = 6, elapsedMs = 3000}), "giveUp")
check("skridt: forbi vognen uden kobling -> giv op", C.step({inRange = false, distance = 0.3, startDistance = 6, elapsedMs = 3000, passed = true}), "giveUp")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-79
View File
@@ -1,79 +0,0 @@
-- Kør: luajit tests/test_adLoadSwap.lua (fra repo-roden)
-- Ren rangering af aflæsnings-siloer (lagermål for udkørsel og læs-bytte).
dofile("FS25_ADSmartPickup/adPrices.lua")
dofile("FS25_ADSmartPickup/adLoadSwap.lua")
local L = ADLoadSwap
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
-- LS1: markører et andet forløb HENTER fra (fx "Milk Storage Load") vælges sidst; ellers har-varen, så afstand
local candidates = {
{marker = {id = 1, name = "Milk Storage Load"}, hasFillType = true, distance = 100},
{marker = {id = 2, name = "Milk Storage unload"}, hasFillType = true, distance = 400},
{marker = {id = 3, name = "Silo tom"}, hasFillType = false, distance = 10},
}
local ranked = L.rankDumpCandidates(candidates, {[1] = true})
check("LS1 afhentningsmarkør undgås", ranked[1].marker.id, 2)
check("LS1 derefter tom silo før afhentning", ranked[2].marker.id, 3)
check("LS1 afhentning sidst (stadig mulig)", ranked[3].marker.id, 1)
local plain = L.rankDumpCandidates(candidates, nil)
check("LS1 uden undgå-liste: har varen + nærmest", plain[1].marker.id, 1)
-- LS2: lagermål — rene siloer altid; egne fabrikker der bruger varen som input kun når tilladt (udkørsel)
local SLURRY, MILK = 20, 21
g_currentMission = {getFarmId = function() return 1 end}
local biogas = {spec_productionPoint = {productionPoint = {productions = {{inputs = {{type = SLURRY, amount = 1}}}}}}, getOwnerFarmId = function() return 1 end}
local foreignBiogas = {spec_productionPoint = {productionPoint = {productions = {{inputs = {{type = SLURRY, amount = 1}}}}}}, getOwnerFarmId = function() return 0 end}
local silo = {spec_silo = {}}
local barn = {spec_silo = {}, getHusbandryFillLevel = function() return 0 end}
check("LS2 silo", L.isStoreTarget({owningPlaceable = silo}, SLURRY, nil, false), true)
check("LS2 stald aldrig", L.isStoreTarget({owningPlaceable = barn}, SLURRY, nil, true), false)
check("LS2 biogas kun når fabrikker tilladt", L.isStoreTarget({owningPlaceable = biogas}, SLURRY, nil, false), false)
check("LS2 biogas tager gylle", L.isStoreTarget({owningPlaceable = biogas}, SLURRY, nil, true), true)
check("LS2 biogas tager ikke mælk", L.isStoreTarget({owningPlaceable = biogas}, MILK, nil, true), false)
check("LS2 fremmed anlæg aldrig", L.isStoreTarget({owningPlaceable = foreignBiogas}, SLURRY, nil, true), false)
check("LS2 hjemmet selv aldrig", L.isStoreTarget({owningPlaceable = biogas}, SLURRY, {owningPlaceable = biogas}, true), false)
g_currentMission = nil
-- LS3: tvunget lagermål — kun de valgte markører, i rækkefølge; første der tager varen, har plads og kan nås
do
local stations = {
[101] = {accepts = {[SLURRY] = true}, free = 0}, -- BGA fuld
[102] = {accepts = {[SLURRY] = true}, free = 900000}, -- gylletank
[103] = {accepts = {[MILK] = true}, free = 900000}, -- tager ikke gylle
}
for _, st in pairs(stations) do
st.getIsFillTypeSupported = function(self, ft) return self.accepts[ft] == true end
st.getFreeCapacity = function(self) return self.free end
end
ADSmartPickup = {getFarmId = function() return 1 end, getUnloadStationAtWayPointId = function(_, id) return stations[id] end}
local adEnv = {ADGraphManager = {getMapMarkerByWayPointId = function(_, id) return {id = id, name = "m" .. id} end}}
local vehicle = {}
local all = function() return true end
check("LS3 fuld BGA springes over -> tank", L.findForcedDumpMarker(adEnv, vehicle, SLURRY, 20000, {101, 102}, all).id, 102)
stations[101].free = 500000
check("LS3 BGA med plads vælges først", L.findForcedDumpMarker(adEnv, vehicle, SLURRY, 20000, {101, 102}, all).id, 101)
local none, why = L.findForcedDumpMarker(adEnv, vehicle, SLURRY, 20000, {103}, all)
check("LS3 intet valgt mål tager varen -> nil", none, nil)
check("LS3 forklaring", why ~= nil and why:find("valgt", 1, true) ~= nil, true)
check("LS3 ikke nåelig -> næste", L.findForcedDumpMarker(adEnv, vehicle, SLURRY, 20000, {101, 102}, function(id) return id ~= 101 end).id, 102)
ADSmartPickup = nil
end
-- LQ: flydende varer lægges ikke automatisk i en fabrik (AIO-risten, in-game 26/9)
check("LQ gylle er flydende", ADLoadSwap.isLiquidName("LIQUIDMANURE"), true)
check("LQ digestat er flydende", ADLoadSwap.isLiquidName("digestate"), true)
check("LQ hvede er ikke flydende", ADLoadSwap.isLiquidName("WHEAT"), false)
check("LQ ukendt", ADLoadSwap.isLiquidName(nil), false)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-40
View File
@@ -1,40 +0,0 @@
-- Kør: luajit tests/test_adManualTransport.lua (fra repo-roden)
-- Manuel transport af brede høstere (in-game 27/9: mejetærskere med skærebord er for brede til vejene).
dofile("FS25_ADSmartPickup/adManualTransport.lua")
local M = ADManualTransport
local WHEAT, BARLEY = 1, 2
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
-- valg af høster
check("manuel høster uden skærebord kan vælges", M.acceptsHarvester({manual = true, fruits = {}}, WHEAT), true)
check("automatisk høster uden skærebord kan ikke", M.acceptsHarvester({fruits = {}}, WHEAT), false)
check("automatisk høster med passende skærebord", M.acceptsHarvester({fruits = {[WHEAT] = true}}, WHEAT), true)
check("ukendt afgrøde -> ingen", M.acceptsHarvester({manual = true}, nil), false)
-- in-game 27/9: F111 (hvede) valgte roeoptageren, fordi manuelle høstere slap for skærebord-tjekket
check("manuel roeoptager (fast optager) kan ikke høste hvede", M.acceptsHarvester({manual = true, fruits = {[BARLEY] = true}}, WHEAT), false)
check("manuel mejetærsker med passende skærebord monteret", M.acceptsHarvester({manual = true, fruits = {[WHEAT] = true}}, WHEAT), true)
-- klar til overtagelse
local header = {[WHEAT] = true}
local function ready(s) local ok, why = M.readiness(s); return ok and "ok" or why end
check("hjemme -> notThere", ready({atField = false, fruits = {}, fruit = WHEAT}), "notThere")
check("på marken uden skærebord -> noHeader", ready({atField = true, fruits = {}, fruit = WHEAT}), "noHeader")
check("forkert skærebord -> wrongHeader", ready({atField = true, fruits = {[BARLEY] = true}, fruit = WHEAT}), "wrongHeader")
check("spilleren sidder i -> playerInside", ready({atField = true, fruits = header, fruit = WHEAT, controlled = true}), "playerInside")
check("klar", ready({atField = true, fruits = header, fruit = WHEAT}), "ok")
check("Start nu: spiller i og ikke på marken -> ok", ready({atField = false, fruits = header, fruit = WHEAT, controlled = true, forced = true}), "ok")
check("Start nu tjekker stadig skærebordet", ready({atField = false, fruits = {}, fruit = WHEAT, forced = true}), "noHeader")
-- påmindelser
check("første gang -> påmind", M.shouldRemind(nil, 1000), true)
check("for tidligt", M.shouldRemind(1000, 1000 + M.REMIND_MS - 1), false)
check("efter 5 min", M.shouldRemind(1000, 1000 + M.REMIND_MS), true)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-86
View File
@@ -1,86 +0,0 @@
-- Kør: luajit tests/test_adMonthView.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFieldModel.lua")
dofile("FS25_ADSmartPickup/adMonthView.lua")
local V, M = ADMonthView, ADFieldModel
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
-- MV1: perioder
check("MV1 næste periode", V.nextPeriod(5), 6)
check("MV1 vinter -> forår", V.nextPeriod(12), 1)
-- MV2: såvindue (hvede sås i periode 6-7)
local function plantableIn(name, period) return name == "WHEAT" and (period == 6 or period == 7) end
check("MV2 lukker", V.sowWindow(plantableIn, "WHEAT", 7), "closes")
check("MV2 åbent", V.sowWindow(plantableIn, "WHEAT", 6), "open")
check("MV2 lukket", V.sowWindow(plantableIn, "WHEAT", 8), "closed")
-- MV3: visning (Dakota-kartofler: harvestReady 6 -> dead 7 i EARLY_AUTUMN = periode 7)
local potato = {witheredState = 7, growthDataSeasonal = {periods = {[7] = {updates = {{startState = 6, endState = 7}}}}}}
check("MV3 visner næste måned", V.withersIn(potato, 7, 6), true)
check("MV3 ikke måneden før", V.withersIn(potato, 6, 6), false)
check("MV3 ukendte data", V.withersIn({witheredState = 7}, 7, 6), false)
-- MV4: fremdrift
local harvest = M.with(M.newField(15), {stage = "harvesting", kind = "harvest"})
check("MV4 CP vinder uden arealtal", V.progress(harvest, {readyRatio = 0.9}, 0.5), 0.5)
-- in-game 27/9 F90: halvt slået græs viste 0 % — slået græs tæller ikke i readyRatio's nævner
check("MV4 slåning ud fra hele arealet", V.progress(M.with(harvest, {stage = "mowing"}), {readyRatio = 1, standingRatio = 0.5}, nil), 0.5)
check("MV4 areal slår CP (kursus genstartet = 0)", V.progress(harvest, {readyRatio = 1, standingRatio = 0.4}, 0), 0.6)
check("MV4 pleje bruger stadig CP", V.progress(M.with(harvest, {stage = "fertilizing"}), {standingRatio = 0.4}, 0.3), 0.3)
check("MV4 høst ud fra afgrøde", V.progress(harvest, {readyRatio = 0.25}, nil), 0.75)
check("MV4 kultivering ukendt", V.progress(M.with(harvest, {stage = "cultivating"}), {readyRatio = 0}, nil), nil)
check("MV4 tekst", V.percentText(0.504), "50 %")
check("MV4 tekst ukendt", V.percentText(nil), "–")
-- MV5: hel fane
local fields = {
[15] = M.with(harvest, {plan = {sow = true, sowFruit = "WHEAT", soil = "cultivate", straw = "chop"}}),
[16] = M.with(M.newField(16), {stage = "harvesting", kind = "harvest"}),
[18] = M.with(M.newField(18), {stage = "sowing", kind = "harvest", plan = {sow = true, sowFruit = "WHEAT"}}),
[20] = M.with(M.newField(20), {stage = "fertilizing"}),
[21] = M.newField(21),
[22] = M.with(M.newField(22), {plan = {targetFruit = "WHEAT"}}),
[23] = M.with(M.newField(23), {stage = "ready"}),
}
local infos = {
{id = 15, kind = "harvest", readyRatio = 0.5, cropTitle = "Hvede", needs = {}},
{id = 16, kind = "harvest", readyRatio = 1, cropTitle = "Kartofler", growthState = 6},
{id = 18, kind = "bare", readyRatio = 0},
{id = 20, kind = "harvest", readyRatio = 0},
{id = 21, kind = "bare"},
{id = 22, kind = "bare"},
{id = 23, kind = "harvest", readyRatio = 1},
}
local ctx = {period = 7, plantableIn = plantableIn,
withers = function(info, nextPeriod) return info.id == 16 and nextPeriod == 8 end,
cpProgress = function(id) return id == 18 and 0.1 or nil end}
local view = V.build(infos, fields, ctx)
check("MV5 aktive marker (21 venter, 22 kommer, 23 passiv klar)", #view.rows, 4)
check("MV5 kritiske", view.critical, 3)
check("MV5 høst før sidste såmåned først (id)", view.rows[1].id, 15)
check("MV5 høst -> såning senere lukker", view.rows[1].window, "lastSowMonth")
check("MV5 høst næste trin", view.rows[1].nextStage, "cultivating")
check("MV5 høst der visner", view.rows[2].id, 16)
check("MV5 vindue høst", view.rows[2].window, "harvestCloses")
check("MV5 sidste såmåned", view.rows[3].id, 18)
check("MV5 vindue såning", view.rows[3].window, "lastSowMonth")
check("MV5 såafgrøde", view.rows[3].sowFruit, "WHEAT")
check("MV5 CP-fremdrift", view.rows[3].progress, 0.1)
check("MV5 pleje sidst", view.rows[4].id, 20)
check("MV5 pleje intet næste", view.rows[4].nextStage, nil)
check("MV5 kommer: F22 hvede", view.upcoming[1] and view.upcoming[1].id, 22)
check("MV5 kommer: lukker", view.upcoming[1] and view.upcoming[1].closes, true)
-- MV6: klargøring før såning med lukkende vindue er kritisk
local cultivating = M.with(M.newField(30), {stage = "cultivating", plan = {sow = true, sowFruit = "WHEAT"}})
check("MV6 kultivering før sidste såmåned", V.window(cultivating, {kind = "bare"}, ctx), "lastSowMonth")
check("MV6 aktiv klar (auto)", V.isActive(M.with(M.newField(31), {stage = "ready", plan = {mode = "auto"}})), true)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-20
View File
@@ -1,20 +0,0 @@
-- Kør: luajit tests/test_adNames.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adNames.lua")
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
local byId = ADNames.fromRecords({{id = "placeableA", name = "Gårdsilo 10"}, {id = "", name = "x"}, {id = "placeableB", name = " "}, {id = "placeableC"}})
check("navn indlæst", byId.placeableA, "Gårdsilo 10")
check("tomt id springes over", byId[""], nil)
check("blankt navn springes over", byId.placeableB, nil)
check("manglende navn springes over", byId.placeableC, nil)
ADNames.byId = byId
check("opslag på bygning", ADNames.forPlaceable({uniqueId = "placeableA"}), "Gårdsilo 10")
check("bygning uden eget navn", ADNames.forPlaceable({uniqueId = "placeableZ"}), nil)
check("uden uniqueId", ADNames.forPlaceable({}), nil)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-49
View File
@@ -1,49 +0,0 @@
-- Kør: luajit tests/test_adNaming.lua (fra repo-roden)
-- Automatiske navne på bygninger uden eget navn (in-game 27/9: "36 Farma" hed alle "Farma 500 + Obi 1000").
dofile("FS25_ADSmartPickup/adNaming.lua")
local N = ADNaming
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
-- næste ledige nummer
check("næste efter 1-5", N.nextNumber({"Svinestald 1", "Svinestald 5", "Kostald 7"}, "Svinestald"), 6)
check("ingen endnu -> 1", N.nextNumber({"Kostald 1"}, "Svinestald"), 1)
check("prefix-præfiks forveksles ikke", N.nextNumber({"Gårdsilo ekstra ved 1", "Gårdsilo 3"}, "Gårdsilo"), 4)
-- navne efter type
local function one(b, existing) return N.assign({b}, existing or {})[b.id] end
check("svinestald", one({id = "a", kind = "husbandry", animal = "PIG"}, {x = "Svinestald 5"}), "Svinestald 6")
check("gedestald", one({id = "a", kind = "husbandry", animal = "GOAT"}), "Gedestald 1")
check("ukendt dyr -> Stald", one({id = "a", kind = "husbandry", animal = "LLAMA"}), "Stald 1")
check("silo i gårdens klynge", one({id = "a", kind = "silo", inFarmCluster = true}, {x = "Gårdsilo 35"}), "Gårdsilo 36")
check("silo ved mark", one({id = "a", kind = "silo", nearestField = {name = "F118", distance = 80}}), "F118 marksilo")
check("anden marksilo ved samme mark", one({id = "a", kind = "silo", nearestField = {name = "F118", distance = 80}}, {x = "F118 marksilo"}), "F118 marksilo 2")
check("silo langt fra alt", one({id = "a", kind = "silo", nearestField = {name = "F1", distance = 900}}), "Silo 1")
check("palleskur", one({id = "a", kind = "objectStorage"}, {x = "Palleskur 1"}), "Palleskur 2")
check("fabrik: butiksnavnet", one({id = "a", kind = "factory", storeName = "Sugarbeet Crusher"}), "Sugarbeet Crusher")
check("fabrik nr. 2 med samme navn", one({id = "a", kind = "factory", storeName = "Roesnitter"}, {x = "Roesnitter"}), "Roesnitter 2")
check("salgssted navngives ikke", one({id = "a", kind = "sell", storeName = "Mølle"}), nil)
check("har allerede navn -> urørt", N.assign({{id = "a", kind = "husbandry", animal = "PIG"}}, {a = "Min stald"}).a, nil)
-- flere nye i samme runde får hver sit nummer
local two = N.assign({{id = "a", kind = "husbandry", animal = "COW"}, {id = "b", kind = "husbandry", animal = "COW"}}, {})
check("to nye kostalde: 1 og 2", (two.a or "") .. "|" .. (two.b or ""), "Kostald 1|Kostald 2")
-- markmaskiner der stadig har modelnavnet (flow-traktorer navngives af flowet)
check("læssevogn nr. 5", N.fleetName("collector", nil, {"Læssevogn 1", "Læssevogn 4"}), "Læssevogn 5")
check("skårlægger", N.fleetName("mower", nil, {}), "Skårlægger 1")
check("høster: roeoptager", N.fleetName("harvester", "beet", {"Roeoptager 1"}), "Roeoptager 2")
check("høster: finsnitter", N.fleetName("harvester", "forage", {}), "Finsnitter 1")
check("høster: mejetærsker", N.fleetName("harvester", nil, {"Mejetærsker"}), "Mejetærsker 1")
check("tømmevogn", N.fleetName("unloader", nil, {}), "Tømmevogn 1")
check("gødningsspreder", N.fleetName("fertilizer", nil, {}), "Gødningsspreder 1")
check("ukendt rolle -> nil", N.fleetName("mystery", nil, {}), nil)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-646
View File
@@ -1,646 +0,0 @@
-- Kør: luajit tests/test_adOutbound.lua (fra repo-roden)
-- Udkørsels-hooks: turvalg + AD-tilstand, hold ved Wait, objektlager-udtag, opsamlingsrunde,
-- advarsler på skærmen. Fake AD-miljø; adSources/adPrices/adLoadSwap er fakes, adWaitPool og
-- adOutboundPlanner er de rigtige.
local SUGAR, FORAGE, BASKET, CRATE = 5, 6, 7, 8
local NAMES = {[SUGAR] = "SUGAR", [FORAGE] = "FORAGE", [BASKET] = "STRAW_BASKET", [CRATE] = "STRAW_CRATE"}
g_fillTypeManager = {
getFillTypeNameByIndex = function(_, i) return NAMES[i] end,
getFillTypeByIndex = function(_, i) return {name = NAMES[i], palletFilename = (i == BASKET or i == CRATE) and "p.xml" or nil} end,
}
local logLines = {}
Logging = {
info = function(fmt, ...) table.insert(logLines, string.format(fmt, ...)) end,
warning = function(fmt, ...) table.insert(logLines, "WARN " .. string.format(fmt, ...)) end,
}
getWorldTranslation = function(node) return node.x, 0, node.z end
g_currentMission = {getFarmId = function() return 1 end}
dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")
dofile("FS25_ADSmartPickup/adRuns.lua")
dofile("FS25_ADSmartPickup/adWaitPool.lua")
dofile("FS25_ADSmartPickup/adOutboundPlanner.lua")
dofile("FS25_ADSmartPickup/adOutbound.lua")
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
local function lastLog(pattern)
for i = #logLines, 1, -1 do
if string.find(logLines[i], pattern, 1, true) then return logLines[i] end
end
return nil
end
local function countLog(pattern)
local n = 0
for _, line in ipairs(logLines) do if string.find(line, pattern, 1, true) then n = n + 1 end end
return n
end
-- ---- AD-miljø ----
local markers, wayPoints, unreachable = {}, {}, {}
local function addMarker(id, name, group, x, z)
wayPoints[id] = {id = id, x = x, z = z}
table.insert(markers, {id = id, markerIndex = #markers + 1, name = name, group = group})
return markers[#markers]
end
local notifications = {}
local RT_NONE, RT_ONLYPICKUP = 1, 2
local adEnv = {
AutoDrive = {
RT_NONE = RT_NONE, RT_ONLYPICKUP = RT_ONLYPICKUP,
getAllUnits = function(vehicle) return vehicle.trailers end,
getAllFillLevels = function(trailers) return trailers.level, trailers.capacity, trailers.level >= trailers.capacity * 0.99, trailers.capacity - trailers.level end,
getSupportedFillTypesOfAllUnitsAlphabetically = function(vehicle) return vehicle.carriable end,
hasAL = function(_, trailer) return trailer.ual ~= nil end,
activateALTrailers = function(vehicle, trailers) trailers.activated = (trailers.activated or 0) + 1 end,
},
ADGraphManager = {
getMapMarkers = function() return markers end,
getMapMarkerByWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id then return m end end end,
getWayPointById = function(_, id) return wayPoints[id] end,
getPathTo = function(_, vehicle, id) if unreachable[id] then return {} end return {{id = id}} end,
pathFromTo = function(_, from, to) if unreachable[to] then return {} end return {{id = to}} end,
},
AutoDriveMessageEvent = {sendMessageOrNotification = function(vehicle, kind, text, duration, ...) table.insert(notifications, {vehicle = vehicle, kind = kind, text = string.format(text, ...)}) end},
ADMessagesManager = {messageTypes = {INFO = 1, WARN = 2, ERROR = 3}},
LoadAtDestinationTask = {STATE_DRIVING = 2, update = function() end, finished = function(self) self.finishedCalls = (self.finishedCalls or 0) + 1 end},
UnloadAtDestinationTask = {STATE_DRIVING = 2, update = function() end},
}
local function newVehicle(name, x, z, capacity, ual)
local v = {name = name, rootNode = {x = x, z = z}, lastSpeedReal = 0, carriable = {SUGAR, FORAGE}, farmId = 1}
v.getName = function() return name end
v.trailers = {level = 0, capacity = capacity, ual = ual and {spec = {loadedObjects = {}}} or nil}
v.trailers[1] = {spec_universalAutoload = ual and v.trailers.ual.spec or nil, ualIsFull = ual and function() return v.trailers.full == true end or nil, ual = ual and true or nil}
v.ad = {
stateModule = {isActive = function() return v.active ~= false end, selectedFillTypes = {}, fillType = nil,
setFillType = function(self, ft) self.fillType = ft end, getFillType = function(self) return self.fillType end,
setSecondMarkerByWayPointId = function(self, id) self.secondMarkerId = id end,
getSecondMarker = function(self) return self.secondMarkerId and {id = self.secondMarkerId} or nil end,
raiseDirtyFlag = function() end},
drivePathModule = {isTargetReached = function() return v.atTarget == true end, setPathTo = function(_, id) v.pathTo = id end},
specialDrivingModule = {stopVehicle = function() v.stopped = true end, update = function() end},
trailerModule = {isActiveAtTrigger = function() return false end},
}
return v
end
local function newTask(vehicle, destinationID)
return {vehicle = vehicle, destinationID = destinationID, trailers = vehicle.trailers, state = 2, activatedUALLoading = false,
finished = function(self) return adEnv.LoadAtDestinationTask.finished(self) end}
end
-- kilder, priser, siloer som fakes
local sources, products, quotes, dumpMarker = {}, {}, {}, nil
ADSources = {
identify = function(_, id) return sources[id] end,
getProducts = function(_, source) return products[source.wayPointId] or {} end,
getSellMarkers = function() local r = {} for _, m in ipairs(markers) do if m.group == "selling" then table.insert(r, {marker = m, station = {name = m.name}}) end end return r end,
stationAccepts = function(station, ft) return quotes[station.name] ~= nil and quotes[station.name][ft] ~= nil end,
getSourcePlaceables = function() return {} end,
getObjectInfoIndex = function(source, ft) local s = source.stored or {} return s[ft] ~= nil and s[ft] > 0 and 1 or nil, s[ft] end,
countStored = function(source, ft) return (source.stored or {})[ft] or 0 end,
spawnOne = function(source, index) source.spawnCalls = (source.spawnCalls or 0) + 1 return source.spawnFails ~= true end,
}
ADPrices = {
getQuote = function(station, ft) return quotes[station.name] and quotes[station.name][ft] end,
bestQuote = function(list) local best for _, q in ipairs(list) do if best == nil or q.percent > best.percent then best = q end end return best end,
}
ADLoadSwap = {findDumpMarker = function() return dumpMarker, "ingen silo" end}
local runs = {}
ADRunsController = {getRunForVehicle = function(vehicle) return runs[vehicle] end}
local activePickupCalls = {}
ADSmartPickup = {getFarmId = function(v) return v.farmId end, setActivePickup = function(vehicle, choice) table.insert(activePickupCalls, choice) end,
getUnloadStationAtWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id and (m.group == "silo" or m.group == "selling") then return {name = m.name} end end end}
local mSugar = addMarker(1, "Suger Load", "Pickup points", 0, 0)
local mSilo = addMarker(2, "silo 4", "silo", 50, 0)
local mObj = addMarker(3, "Objektlager", "Pickup points", 100, 0)
local mSell = addMarker(10, "selling station", "selling", 200, 0)
local mSell2 = addMarker(11, "Far station", "selling", 300, 0)
local mWait = addMarker(20, "Wait 3", "General Wait", 20, 0)
local mStore = addMarker(30, "silo 6", "silo", 60, 0)
sources[1] = {kind = "factory", wayPointId = 1, station = {name = "factoryStation"}, name = "Sugar mill"}
sources[2] = {kind = "silo", wayPointId = 2, station = {name = "siloStation"}, name = "Farma"}
sources[3] = {kind = "objectStorage", wayPointId = 3, station = nil, name = "Objektlager", stored = {[BASKET] = 3, [CRATE] = 1}}
products[1] = {{fillType = SUGAR, name = "SUGAR", available = 5000, level = 5000, capacity = 180000, isPallet = false}}
products[2] = {{fillType = SUGAR, name = "SUGAR", available = 9000, level = 9000, capacity = 12000000, isPallet = false}}
products[3] = {{fillType = BASKET, name = "STRAW_BASKET", available = 3000, level = 4, capacity = 4000, isPallet = true}, {fillType = CRATE, name = "STRAW_CRATE", available = 1000, level = 4, capacity = 4000, isPallet = true}}
quotes["selling station"] = {[SUGAR] = {percent = 92, price = 480}, [BASKET] = {percent = 95, price = 1}, [CRATE] = {percent = 95, price = 1}}
quotes["Far station"] = {[SUGAR] = {percent = 99, price = 600}}
local function outboundRun(id, sourceIds, productsList, overrides)
local run = {id = id, name = "Out " .. id, kind = "outbound", waitPoolGroup = "", sourceWayPointIds = sourceIds, products = productsList, sellAtPercent = 90, storeAbovePercent = 80, collectRound = false}
for k, v in pairs(overrides or {}) do run[k] = v end
return run
end
-- O1: tur -> AD's varevalg og 2. markør sat, choice = kilden; næstbedste salgssted når bedste er uopnåeligt
unreachable[11] = true
local v1 = newVehicle("MT635", 0, 0, 20000)
runs[v1] = outboundRun(1, {1}, {{fillType = "SUGAR", policy = "sell"}})
check("O1 er udkørsel", ADOutbound.isOutboundVehicle(v1), true)
local choice = ADOutbound.choosePickup(adEnv, v1)
check("O1 kilde-markør", choice.markerIndex, mSugar.markerIndex)
check("O1 station med (bulk-greb)", choice.station.name, "factoryStation")
check("O1 vare", v1.ad.stateModule.fillType, SUGAR)
check("O1 varevalg", #v1.ad.stateModule.selectedFillTypes, 1)
check("O1 2. markør = nåeligt salgssted", v1.ad.stateModule.secondMarkerId, 10)
check("O1 ingen advarsel (næstbedste kunne nås)", #notifications, 0)
check("O1 log", lastLog("udkørsel") ~= nil, true)
local trip = ADOutbound.getTripFor(v1)
check("O1 trip kind", trip.kind, "sell")
unreachable[11] = nil
-- O2: ingen tur -> Wait-markør reserveret, hold registreret
local v2 = newVehicle("ARION", 25, 0, 20000)
runs[v2] = outboundRun(2, {1}, {{fillType = "SUGAR", policy = "sell"}})
quotes["selling station"][SUGAR].percent = 70
quotes["Far station"][SUGAR].percent = 60
local choice2 = ADOutbound.choosePickup(adEnv, v2)
check("O2 Wait-markør", choice2.markerIndex, mWait.markerIndex)
check("O2 uden station", choice2.station, nil)
check("O2 reserveret", ADWaitPool.getOwner(20), v2)
local hold = ADOutbound.getHoldFor(v2)
check("O2 hold", hold.markerName, "Wait 3")
check("O2 begrundelse", hold.reasons[1], "SUGAR 70 % af top (kræver 90), lager 3 %")
check("O2 run-status", ADOutbound.getRunStatus(2).reasons[1], hold.reasons[1])
check("O2 hold-log én gang", countLog("holder ved Wait 3"), 1)
ADOutbound.choosePickup(adEnv, v2)
check("O2 samme begrundelse logges ikke igen", countLog("holder ved Wait 3"), 1)
-- O3: holder ved Wait: vognen stoppes; efter 5 s med tur -> omdirigeret til kilden, hold sluppet
local task2 = newTask(v2, 20)
v2.atTarget = true
check("O3 håndteret", ADOutbound.beforeLoadUpdate(adEnv, task2, 1000), true)
check("O3 stoppet", v2.stopped, true)
quotes["selling station"][SUGAR].percent = 92
ADOutbound.beforeLoadUpdate(adEnv, task2, 4500)
check("O3 omdirigeret til kilden", task2.destinationID, 1)
check("O3 sti sat", v2.pathTo, 1)
check("O3 hold væk", ADOutbound.getHoldFor(v2), nil)
check("O3 reservation frigivet", ADWaitPool.getOwner(20), nil)
check("O3 activePickup opdateret", activePickupCalls[#activePickupCalls].station.name, "factoryStation")
check("O3 UAL-flag nulstillet", task2.activatedUALLoading, false)
v2.atTarget = false
check("O3 undervejs ikke håndteret", ADOutbound.beforeLoadUpdate(adEnv, task2, 100), false)
-- O4: objektlager: én palle ad gangen; stored falder + loaded stiger -> næste; ualIsFull -> finished
local v3 = newVehicle("Fendt", 100, 0, 8000, true)
runs[v3] = outboundRun(3, {3}, {{fillType = "STRAW_BASKET", policy = "sell"}, {fillType = "STRAW_CRATE", policy = "sell"}}, {collectRound = true})
local choice3 = ADOutbound.choosePickup(adEnv, v3)
check("O4 kilde = objektlager", choice3.markerIndex, mObj.markerIndex)
check("O4 kun én vare valgt ad gangen ved objektlager", #v3.ad.stateModule.selectedFillTypes, 1)
check("O4 turen har begge varer", #ADOutbound.getTripFor(v3).fillTypes, 2)
local task3 = newTask(v3, 3)
v3.atTarget = true
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
check("O4 intet spawn før UAL er aktiveret", sources[3].spawnCalls, nil)
task3.activatedUALLoading = true
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
check("O4 første spawn", sources[3].spawnCalls, 1)
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
check("O4 venter på pallen", sources[3].spawnCalls, 1)
sources[3].stored[BASKET] = 2
v3.trailers.ual.spec.loadedObjects[{}] = 1
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
check("O4 andet spawn efter læsning", sources[3].spawnCalls, 2)
sources[3].stored[BASKET] = 0
v3.trailers.ual.spec.loadedObjects[{}] = 1
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
check("O4 skifter til kasser når kurvene er væk", v3.ad.stateModule.fillType, CRATE)
check("O4 UAL genaktiveret ved vareskift", v3.trailers.activated, 1)
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
check("O4 tredje spawn (kasse)", sources[3].spawnCalls, 3)
v3.trailers.full = true
sources[3].stored[CRATE] = 0
v3.trailers.ual.spec.loadedObjects[{}] = 1
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
check("O4 fuld -> finished", task3.finishedCalls, 1)
-- O5: spawn uden fremdrift i 30 s -> finished + WARN
local v4 = newVehicle("Valtra", 100, 0, 8000, true)
runs[v4] = outboundRun(4, {3}, {{fillType = "STRAW_BASKET", policy = "sell"}})
sources[3].stored[BASKET] = 5
sources[3].spawnCalls = 0
ADOutbound.choosePickup(adEnv, v4)
local task4 = newTask(v4, 3)
v4.atTarget, task4.activatedUALLoading = true, true
ADOutbound.beforeLoadUpdate(adEnv, task4, 100)
check("O5 spawn kaldt", sources[3].spawnCalls, 1)
ADOutbound.beforeLoadUpdate(adEnv, task4, 31000)
ADOutbound.beforeLoadUpdate(adEnv, task4, 100)
check("O5 finished efter stall", task4.finishedCalls, 1)
check("O5 WARN", lastLog("spawner ikke") ~= nil, true)
-- O6: opsamlingsrunde, bulk: kilde A fylder vognen -> finished uden redirect; kilde A giver 30 % -> videre til B
local v5 = newVehicle("Claas", 0, 0, 10000)
runs[v5] = outboundRun(5, {1, 2}, {{fillType = "SUGAR", policy = "sell"}}, {collectRound = true})
local choice5 = ADOutbound.choosePickup(adEnv, v5)
check("O6 mest først = silo 4", choice5.markerIndex, mSilo.markerIndex)
local task5 = newTask(v5, 2)
v5.trailers.level = 10000
check("O6 fuld: ingen runde", ADOutbound.beforeLoadFinished(adEnv, task5), false)
local v6 = newVehicle("Claas2", 0, 0, 10000)
runs[v6] = outboundRun(6, {1, 2}, {{fillType = "SUGAR", policy = "sell"}}, {collectRound = true})
ADOutbound.choosePickup(adEnv, v6)
local task6 = newTask(v6, 2)
v6.trailers.level = 3000
check("O6 plads tilbage: videre til næste kilde", ADOutbound.beforeLoadFinished(adEnv, task6), true)
check("O6 destination = Suger Load", task6.destinationID, 1)
check("O6 activePickup = fabrik", activePickupCalls[#activePickupCalls].station.name, "factoryStation")
check("O6 alle besøgt: afslut", ADOutbound.beforeLoadFinished(adEnv, task6), false)
-- O7: intet nåeligt salgssted -> rød notifikation én gang
notifications = {}
unreachable[10], unreachable[11] = true, true
local v7 = newVehicle("Deutz", 0, 0, 20000)
runs[v7] = outboundRun(7, {1}, {{fillType = "SUGAR", policy = "sell"}})
local choice7 = ADOutbound.choosePickup(adEnv, v7)
check("O7 holder", ADOutbound.getHoldFor(v7) ~= nil, true)
check("O7 én notifikation", #notifications, 1)
check("O7 ERROR", notifications[1].kind, 3)
check("O7 tekst", string.find(notifications[1].text, "kan ikke nå salgssted", 1, true) ~= nil, true)
check("O7 status-advarsel", ADOutbound.getRunStatus(7).warning ~= nil, true)
ADOutbound.choosePickup(adEnv, v7)
check("O7 ikke gentaget", #notifications, 1)
unreachable[10] = nil
ADOutbound.choosePickup(adEnv, v7)
check("O7 tur når vejen er der", ADOutbound.getTripFor(v7) ~= nil, true)
check("O7 advarsel ryddet", ADOutbound.getRunStatus(7).warning, nil)
unreachable[11] = nil
-- O8: ingen silo med plads ved lagerbehov -> notifikation; med silo -> store-tur
notifications = {}
local v8 = newVehicle("JD", 0, 0, 20000)
runs[v8] = outboundRun(8, {2}, {{fillType = "SUGAR", policy = "store"}})
products[2][1].level, products[2][1].capacity = 11000000, 12000000
ADOutbound.choosePickup(adEnv, v8)
check("O8 advarsel ingen silo", #notifications, 1)
dumpMarker = mStore
ADOutbound.choosePickup(adEnv, v8)
check("O8 store-tur", ADOutbound.getTripFor(v8).kind, "store")
check("O8 mål = silo 6", v8.ad.stateModule.secondMarkerId, 30)
-- O9: releaseInactive rydder hold + reservation
local v9 = newVehicle("Case", 25, 0, 20000)
runs[v9] = outboundRun(9, {1}, {{fillType = "FORAGE", policy = "sell"}})
ADOutbound.choosePickup(adEnv, v9)
check("O9 reserveret", ADWaitPool.getOwner(20), v9)
v9.active = false
ADOutbound.releaseInactive()
check("O9 frigivet", ADWaitPool.getOwner(20), nil)
check("O9 hold væk", ADOutbound.getHoldFor(v9), nil)
-- O11 (I2): hold-markøren skifter (Wait-punkt bliver ledigt) -> opgaven omdirigeres
ADWaitPool.releaseVehicle(v9)
local v11 = newVehicle("Hold", 25, 0, 20000)
runs[v11] = outboundRun(11, {1}, {{fillType = "FORAGE", policy = "sell"}}) -- FORAGE findes ikke ved kilden -> aldrig tur
local blocker = newVehicle("Blocker", 0, 0, 1)
ADWaitPool.claim(20, blocker)
local choice11 = ADOutbound.choosePickup(adEnv, v11)
check("O11 uden ledigt Wait: holder ved kilden", choice11.markerIndex, mSugar.markerIndex)
local task11 = newTask(v11, 1)
v11.atTarget = true
ADOutbound.beforeLoadUpdate(adEnv, task11, 100)
ADWaitPool.releaseVehicle(blocker)
ADOutbound.beforeLoadUpdate(adEnv, task11, 5000)
check("O11 Wait-punkt ledigt -> omdirigeret", task11.destinationID, 20)
check("O11 hold peger på Wait", ADOutbound.getHoldFor(v11).markerName, "Wait 3")
ADWaitPool.releaseVehicle(v11)
-- O12 (I3): mål for last der allerede er på vognen ved Start
local v12 = newVehicle("Loaded", 0, 0, 20000)
runs[v12] = outboundRun(12, {1}, {{fillType = "SUGAR", policy = "sell"}})
quotes["selling station"][SUGAR].percent = 92
check("O12 salgsmål for sukker-last", ADOutbound.planCargoTarget(adEnv, runs[v12], v12, SUGAR), 10)
quotes["selling station"][SUGAR].percent = 50
check("O12 pris lav, ingen lager-politik -> nil", ADOutbound.planCargoTarget(adEnv, runs[v12], v12, SUGAR), nil)
runs[v12].products = {{fillType = "SUGAR", policy = "sellElseStore"}}
check("O12 pris lav -> silo", ADOutbound.planCargoTarget(adEnv, runs[v12], v12, SUGAR), 30)
check("O12 last der ikke er i run'et -> nil", ADOutbound.planCargoTarget(adEnv, runs[v12], v12, FORAGE), nil)
quotes["selling station"][SUGAR].percent = 92
-- O13 (I4): tom vogn 30 s ved kilden -> kilden udelukkes for varen og turen afsluttes
local v13 = newVehicle("Empty", 0, 0, 20000)
runs[v13] = outboundRun(13, {1, 2}, {{fillType = "SUGAR", policy = "sell"}}, {collectRound = false})
local choice13 = ADOutbound.choosePickup(adEnv, v13)
check("O13 første kilde = silo 4 (mest)", choice13.markerIndex, mSilo.markerIndex)
local task13 = newTask(v13, 2)
v13.atTarget = true
ADOutbound.beforeLoadUpdate(adEnv, task13, 100)
ADOutbound.beforeLoadUpdate(adEnv, task13, 31000)
check("O13 finished efter 30 s uden last", task13.finishedCalls, 1)
check("O13 WARN", lastLog("intet at læsse") ~= nil, true)
local choice13b = ADOutbound.choosePickup(adEnv, v13)
check("O13 næste plan udelukker den tomme kilde", choice13b.markerIndex, mSugar.markerIndex)
-- O16: kilden gav LIDT og så intet mere ("ingen mere last") -> kilden hviler, næste tur tager en anden
-- (in-game 26/9: æg-vognen kørte til Æg Pickup 4 tre gange i træk; én palle inden for rækkevidde, resten udenfor)
local v16 = newVehicle("Thin", 0, 0, 20000)
runs[v16] = outboundRun(16, {1, 2}, {{fillType = "SUGAR", policy = "sell"}}, {collectRound = false})
ADOutbound.resetEmptySources()
local choice16 = ADOutbound.choosePickup(adEnv, v16)
local first16 = choice16.markerIndex
local task16 = newTask(v16, choice16.markerIndex == mSilo.markerIndex and 2 or 1)
v16.atTarget = true
v16.trailers.level = 500
ADOutbound.beforeLoadUpdate(adEnv, task16, 100)
ADOutbound.beforeLoadUpdate(adEnv, task16, 16000)
check("O16 kører med det den fik", task16.finishedCalls, 1)
check("O16 log", lastLog("ingen mere last") ~= nil, true)
v16.trailers.level = 0
local choice16b = ADOutbound.choosePickup(adEnv, v16)
check("O16 næste tur tager en anden kilde", choice16b ~= nil and choice16b.markerIndex ~= first16, true)
ADOutbound.resetEmptySources() -- (senere tests forventer alle kilder ledige)
-- O14 (I6): rutetjek kun for salgssteder over tærsklen, bedste først, stop ved første nåelige
-- (rutetjekket går markør->markør: hjem -> salgssted tælles som ét tjek pr. salgssted)
local routeChecks = 0
local originalGetPathTo = adEnv.ADGraphManager.getPathTo
adEnv.ADGraphManager.getPathTo = function(_, vehicle, id) routeChecks = routeChecks + 1 return originalGetPathTo(_, vehicle, id) end
local originalFromTo = adEnv.ADGraphManager.pathFromTo
adEnv.ADGraphManager.pathFromTo = function(self, from, to)
if to == 10 or to == 11 then routeChecks = routeChecks + 1 end -- udturen til et salgssted
return originalFromTo(self, from, to)
end
ADWaitPool.resetMarkerRoutes()
local v14 = newVehicle("Routes", 0, 0, 20000)
runs[v14] = outboundRun(14, {1}, {{fillType = "SUGAR", policy = "sell"}})
quotes["Far station"][SUGAR].percent = 99
quotes["selling station"][SUGAR].percent = 92
ADOutbound.choosePickup(adEnv, v14)
check("O14 bedste nåelig først: én rute tjekket", routeChecks, 1)
check("O14 mål = bedst betalende", v14.ad.stateModule.secondMarkerId, 11)
routeChecks = 0
ADWaitPool.resetMarkerRoutes()
quotes["Far station"][SUGAR].percent = 60
ADOutbound.choosePickup(adEnv, v14)
check("O14 under tærsklen tjekkes ikke", routeChecks, 1)
adEnv.ADGraphManager.getPathTo = originalGetPathTo
-- O15 (M1): advarsel for vare A gentages ikke efter en tur med vare B
notifications = {}
local v15 = newVehicle("Two", 0, 0, 20000)
runs[v15] = outboundRun(15, {1, 2}, {{fillType = "SUGAR", policy = "sell"}, {fillType = "FORAGE", policy = "sell"}})
products[1] = {{fillType = SUGAR, name = "SUGAR", available = 5000, level = 5000, capacity = 180000, isPallet = false}, {fillType = FORAGE, name = "FORAGE", available = 5000, level = 5000, capacity = 180000, isPallet = false}}
quotes["selling station"][FORAGE] = {percent = 95, price = 1}
quotes["Far station"][SUGAR].percent = 99 -- (O14 satte den til 60)
unreachable[10] = true; unreachable[11] = true
ADOutbound.choosePickup(adEnv, v15) -- SUGAR uopnåelig -> advarsel, FORAGE uopnåelig -> advarsel
local firstCount = #notifications
unreachable[11] = nil -- Far station kan nås for SUGAR (FORAGE sælges kun på selling station)
ADOutbound.choosePickup(adEnv, v15) -- tur med SUGAR
check("O15 tur", ADOutbound.getTripFor(v15).kind, "sell")
ADOutbound.choosePickup(adEnv, v15) -- igen: FORAGE stadig uopnåelig, men allerede advaret
check("O15 FORAGE-advarsel gentages ikke", #notifications, firstCount)
unreachable[10] = nil
products[1] = {{fillType = SUGAR, name = "SUGAR", available = 5000, level = 5000, capacity = 180000, isPallet = false}}
-- O16 (M6): pallens ankomst ses også på fyldniveau når UAL's interne liste mangler
local v16 = newVehicle("NoList", 100, 0, 8000, true)
v16.trailers[1].spec_universalAutoload = {}
runs[v16] = outboundRun(16, {3}, {{fillType = "STRAW_BASKET", policy = "sell"}})
sources[3].stored[BASKET] = 3; sources[3].spawnCalls = 0
ADOutbound.choosePickup(adEnv, v16)
local task16 = newTask(v16, 3)
v16.atTarget, task16.activatedUALLoading = true, true
ADOutbound.beforeLoadUpdate(adEnv, task16, 100)
check("O16 første spawn", sources[3].spawnCalls, 1)
sources[3].stored[BASKET] = 2
v16.trailers.level = 1000
ADOutbound.beforeLoadUpdate(adEnv, task16, 100)
ADOutbound.beforeLoadUpdate(adEnv, task16, 100)
check("O16 næste spawn via fyldniveau", sources[3].spawnCalls, 2)
-- O17 (reserve + paller til lager): buildPlanInput henter reserve og palle-lager fra ADSources
local demandCalls = {}
ADSources.getFarmDemand = function(_, ft, farmId, excluded) table.insert(demandCalls, ft) return {liters = ft == SUGAR and 4000 or 0, consumers = {"TMR-fabrik"}} end
local palletStoreMarker = nil
ADSources.findPalletStore = function(_, vehicle, ft, excluded, isReachable, farmId) return palletStoreMarker end
local v17 = newVehicle("Reserve", 0, 0, 20000)
runs[v17] = outboundRun(17, {1}, {{fillType = "SUGAR", policy = "sell"}})
local input17 = ADOutbound.buildPlanInput(adEnv, runs[v17], v17)
check("O17 reserve på salgsvare", input17.products[1].reserve, 4000)
check("O17 reserveret til", input17.products[1].reservedFor[1], "TMR-fabrik")
local v18 = newVehicle("PalletStore", 100, 0, 8000, true)
runs[v18] = outboundRun(18, {3}, {{fillType = "STRAW_BASKET", policy = "store"}})
palletStoreMarker = {wayPointId = 40, name = "Palleskur", free = 100}
sources[3].stored[BASKET] = 4
products[3][1].available, products[3][1].level, products[3][1].capacity = 4000, 3900, 4000
local input18 = ADOutbound.buildPlanInput(adEnv, runs[v18], v18)
check("O18 palle-lager som store-mål", input18.products[1].store ~= nil and input18.products[1].store.wayPointId, 40)
check("O18 reserve spørges ikke for lager-politik", demandCalls[#demandCalls] ~= BASKET, true)
-- O19: pallebil (kun UAL) ved en fabrik der både har bulk-læssestation og sukkerpaller -> paller
local v19 = newVehicle("Dragon", 0, 0, 6000, true)
v19.carriable = {}
runs[v19] = outboundRun(19, {1}, {{fillType = "SUGAR", policy = "sell"}})
products[1] = {{fillType = SUGAR, name = "SUGAR", available = 150000, level = 150000, capacity = 180000, isPallet = false, canPallet = true, palletLiters = 3000, palletCount = 3}}
quotes["selling station"][SUGAR].percent = 92
local input19 = ADOutbound.buildPlanInput(adEnv, runs[v19], v19)
check("O19 varen tages som paller", input19.products[1] ~= nil and input19.products[1].isPallet, true)
check("O19 mængde = paller ved spawner", input19.products[1].sources[1].available, 3000)
check("O19 palle-antal følger med", input19.products[1].sources[1].palletCount, 3)
local v19b = newVehicle("Tipvogn", 0, 0, 20000)
runs[v19b] = outboundRun(20, {1}, {{fillType = "SUGAR", policy = "sell"}})
local input19b = ADOutbound.buildPlanInput(adEnv, runs[v19b], v19b)
check("O19 tipvogn tager bulk", input19b.products[1].isPallet, false)
check("O19 bulk-mængde", input19b.products[1].sources[1].available, 150000)
products[1] = {{fillType = SUGAR, name = "SUGAR", available = 5000, level = 5000, capacity = 180000, isPallet = false}}
-- O20: UAL-bil: AD's "kapacitet" er antal paller (læssede + 1) -> liter-kapacitet ukendt i planen
local v20 = newVehicle("Dragon2", 100, 0, 1, true)
v20.carriable = {}
runs[v20] = outboundRun(21, {3}, {{fillType = "STRAW_BASKET", policy = "sell"}})
local input20 = ADOutbound.buildPlanInput(adEnv, runs[v20], v20)
check("O20 kapacitet ukendt for pallebil", input20.capacity, nil)
check("O20 har autoload", input20.hasAutoload, true)
check("O20 turen planlægges alligevel", ADOutboundPlanner.plan(input20) ~= nil, true)
-- O21: autoload-bil på vej til aflæsning: stropperne løsnes UNSTRAP_BEFORE_M før målet (ladet passerer
-- triggeren før markøren), kun én gang pr. tur, og aldrig for tipvogne
local v21 = newVehicle("Dragon3", 0, 0, 1, true)
v21.carriable = {}
v21.trailers[1].setAllTensionBeltsActive = function(self, active) self.beltsActive = active end
runs[v21] = outboundRun(22, {3}, {{fillType = "STRAW_BASKET", policy = "sell"}})
sources[3].stored[BASKET] = 3
quotes["selling station"][BASKET] = {percent = 95, price = 1}
ADOutbound.choosePickup(adEnv, v21)
check("O21 tur mod salgssted", v21.ad.stateModule.secondMarkerId, 10)
local unloadTask21 = {vehicle = v21, destinationID = 10, trailers = v21.trailers}
v21.rootNode = {x = 200 - 40, z = 0} -- 40 m fra markør 10 (x=200)
ADOutbound.beforeUnloadUpdate(adEnv, unloadTask21, 16)
check("O21 40 m: stropper stadig på", v21.trailers[1].beltsActive, nil)
v21.rootNode = {x = 200 - 20, z = 0}
ADOutbound.beforeUnloadUpdate(adEnv, unloadTask21, 16)
check("O21 20 m: stropper løsnet", v21.trailers[1].beltsActive, false)
v21.trailers[1].beltsActive = nil
ADOutbound.beforeUnloadUpdate(adEnv, unloadTask21, 16)
check("O21 kun én gang", v21.trailers[1].beltsActive, nil)
local v21b = newVehicle("Tip", 0, 0, 20000)
v21b.trailers[1].setAllTensionBeltsActive = function(self, active) self.beltsActive = active end
runs[v21b] = outboundRun(23, {1}, {{fillType = "SUGAR", policy = "sell"}})
quotes["selling station"][SUGAR].percent = 92
ADOutbound.choosePickup(adEnv, v21b)
v21b.rootNode = {x = 195, z = 0}
ADOutbound.beforeUnloadUpdate(adEnv, {vehicle = v21b, destinationID = 10, trailers = v21b.trailers}, 16)
check("O21 tipvogn: ingen stropper", v21b.trailers[1].beltsActive, nil)
-- O22: lagermål med en anden silo-markør inden for AD's globale trigger-afstand -> advarsel én gang
-- (AD tipper ved første trigger inden for afstanden; 10 m løser det når siloer står tæt)
local triggerDistance = 25
adEnv.AutoDrive.getSetting = function(name) if name == "maxTriggerDistance" then return triggerDistance end end
local v22 = newVehicle("Tip2", 0, 0, 20000)
runs[v22] = outboundRun(24, {1}, {{fillType = "SUGAR", policy = "store"}})
products[1] = {{fillType = SUGAR, name = "SUGAR", available = 170000, level = 170000, capacity = 180000, isPallet = false}}
dumpMarker = mStore -- silo 6 (x=60); silo 4 (x=50) står 10 m derfra
logLines = {}
ADOutbound.choosePickup(adEnv, v22)
check("O22 lager-tur", ADOutbound.getTripFor(v22) ~= nil and ADOutbound.getTripFor(v22).kind, "store")
check("O22 advarsel om trigger-afstand", lastLog("Max trigger distance") ~= nil, true)
logLines = {}
ADOutbound.choosePickup(adEnv, v22)
check("O22 kun én gang", lastLog("Max trigger distance"), nil)
triggerDistance = 10
local v22b = newVehicle("Tip3", 0, 0, 20000)
runs[v22b] = outboundRun(25, {1}, {{fillType = "SUGAR", policy = "store"}})
logLines = {}
ADOutbound.choosePickup(adEnv, v22b)
check("O22 ved 10 m ingen advarsel", lastLog("Max trigger distance"), nil)
adEnv.AutoDrive.getSetting = nil
products[1] = {{fillType = SUGAR, name = "SUGAR", available = 5000, level = 5000, capacity = 180000, isPallet = false}}
-- O23: intet salgssted tager varen -> log én gang med de fundne salgsmarkører og hvad de tager
ADSources.describeAccepted = function(station) return "SUGAR, FORAGE" end
local v23 = newVehicle("Dragon4", 100, 0, 1, true)
v23.carriable = {}
runs[v23] = outboundRun(26, {3}, {{fillType = "STRAW_CRATE", policy = "sell"}})
quotes["selling station"][CRATE] = nil; quotes["Far station"][CRATE] = nil
sources[3].stored[CRATE] = 2
logLines = {}
ADOutbound.choosePickup(adEnv, v23)
local noSell = lastLog("intet salgssted tager STRAW_CRATE")
check("O23 log om manglende salgssted", noSell ~= nil, true)
check("O23 lister salgsmarkører", noSell ~= nil and noSell:find("selling station (SUGAR, FORAGE)", 1, true) ~= nil, true)
logLines = {}
ADOutbound.choosePickup(adEnv, v23)
check("O23 kun én gang", lastLog("intet salgssted tager STRAW_CRATE"), nil)
-- O10: install wrapper: fejl i hook stopper ikke AD
local calls = {}
adEnv.LoadAtDestinationTask.update = function(self, dt) calls.update = (calls.update or 0) + 1 end
adEnv.LoadAtDestinationTask.finished = function(self) calls.finished = (calls.finished or 0) + 1 end
ADOutbound.install(adEnv)
local vX = newVehicle("X", 0, 0, 1000)
adEnv.LoadAtDestinationTask.update(newTask(vX, 1), 16)
check("O10 original update kaldt", calls.update, 1)
adEnv.LoadAtDestinationTask.finished(newTask(vX, 1))
check("O10 original finished kaldt", calls.finished, 1)
adEnv.UnloadAtDestinationTask.update = function(self, dt) calls.unload = (calls.unload or 0) + 1 end
ADOutbound.install(adEnv)
check("O10 unload-wrap ikke installeret dobbelt", adEnv.UnloadAtDestinationTask.update ~= nil, true)
-- O24: stropperne spændes igen når pålæsningen er færdig (de blev løsnet 25 m før forrige salg;
-- in-game 23/9: Dragon tabte paller på vejen to gange)
local v24 = newVehicle("Dragon5", 100, 0, 1, true)
v24.carriable = {}
v24.trailers[1].setAllTensionBeltsActive = function(self, active) self.beltsActive = active end
runs[v24] = outboundRun(27, {3}, {{fillType = "STRAW_BASKET", policy = "sell"}})
sources[3].stored[BASKET] = 3
quotes["selling station"][BASKET] = {percent = 95, price = 1}
ADOutbound.choosePickup(adEnv, v24)
local task24 = newTask(v24, 3)
v24.trailers.level = 3
adEnv.LoadAtDestinationTask.finished(task24)
check("O24 stropper spændt efter pålæsning", v24.trailers[1].beltsActive, true)
check("O24 tur planlagt", ADOutbound.getTripFor(v24) ~= nil, true)
check("O24 original finished kaldt", calls.finished, 2)
local v24b = newVehicle("Tip4", 0, 0, 20000)
v24b.trailers[1].setAllTensionBeltsActive = function(self, active) self.beltsActive = active end
adEnv.LoadAtDestinationTask.finished(newTask(v24b, 1))
check("O24 tipvogn uden tur: rører ikke stropper", v24b.trailers[1].beltsActive, nil)
-- O-pool: to kilder hvis stationer deler lager (mælketanke som udvidelser) = ét stop i runden
do
local pool = {}
local st1, st2 = sources[1].station, sources[2].station
sources[1].station = {name = "tankA", sourceStorages = {[pool] = pool}}
sources[2].station = {name = "tankB", sourceStorages = {[pool] = pool}}
local k1, k2 = sources[1].kind, sources[2].kind
sources[1].kind, sources[2].kind = "silo", "silo"
local vPool = newVehicle("Tank", 0, 0, 10000)
runs[vPool] = outboundRun(77, {1, 2}, {{fillType = "SUGAR", policy = "sell"}}, {collectRound = true})
local inputPool = ADOutbound.buildPlanInput(adEnv, runs[vPool], vPool)
local ids = {}
for _, product in ipairs(inputPool.products) do
for _, source in ipairs(product.sources) do table.insert(ids, tostring(source.wayPointId)) end
end
check("O-pool kun første kilde", table.concat(ids, ","), "1")
check("O-pool logget", lastLog("deler lager") ~= nil, true)
sources[1].station, sources[2].station = st1, st2
sources[1].kind, sources[2].kind = k1, k2
end
-- O-pool-stald: mælke- og gyllemarkør ved samme stald deler lager, men er forskellige læssepunkter
do
local pool = {}
sources[31] = {kind = "husbandry", wayPointId = 31, station = {name = "barnSlurry", sourceStorages = {[pool] = pool}}, name = "Stald"}
sources[32] = {kind = "husbandry", wayPointId = 32, station = {name = "barnMilk", sourceStorages = {[pool] = pool}}, name = "Stald"}
products[31] = {{fillType = SUGAR_FT or 5, name = "SUGAR", title = "Sukker", available = 20000, level = 20000, capacity = 50000, isPallet = false}}
products[32] = {{fillType = SUGAR_FT or 5, name = "SUGAR", title = "Sukker", available = 20000, level = 20000, capacity = 50000, isPallet = false}}
local vBarn = newVehicle("Tank2", 0, 0, 10000)
runs[vBarn] = outboundRun(78, {31, 32}, {{fillType = "SUGAR", policy = "sell"}}, {collectRound = true})
local inputBarn = ADOutbound.buildPlanInput(adEnv, runs[vBarn], vBarn)
local ids = {}
for _, product in ipairs(inputBarn.products) do
for _, source in ipairs(product.sources) do table.insert(ids, tostring(source.wayPointId)) end
end
check("O-pool-stald: stalde lægges ikke sammen", table.concat(ids, ","), "31,32")
sources[31], sources[32], products[31], products[32] = nil, nil, nil, nil
end
-- O-forced: "Lager til" valgt -> lagerturen går til det valgte mål, ikke moden egen silo-valg
do
sources[41] = {kind = "silo", wayPointId = 41, station = {name = "gylleSilo"}, name = "Gyllesilo"}
products[41] = {{fillType = SUGAR, name = "SUGAR", title = "Sukker", available = 11000000, level = 11000000, capacity = 12000000, isPallet = false}}
local vF = newVehicle("Gyllevogn", 0, 0, 20000)
runs[vF] = outboundRun(79, {41}, {{fillType = "SUGAR", policy = "store"}})
runs[vF].forcedMarkers = {31}
ADRunsController.getStoreMarkers = function(_, run) return run.forcedMarkers or {} end
local forcedCalls = 0
ADLoadSwap.findForcedDumpMarker = function(_, _, _, _, ids) forcedCalls = forcedCalls + 1 return {id = ids[1], name = "BGA", markerIndex = 99} end
dumpMarker = mStore
ADOutbound.choosePickup(adEnv, vF)
check("O-forced valgt mål brugt", forcedCalls > 0, true)
check("O-forced tur-mål = BGA", ADOutbound.getTripFor(vF) and ADOutbound.getTripFor(vF).targetWayPointId, 31)
ADRunsController.getStoreMarkers, ADLoadSwap.findForcedDumpMarker = nil, nil
end
-- O-empty: ingen vare ved kilden (æggene er ikke spawnet endnu) -> intet lagermål søges, ingen advarsel
do
sources[42] = {kind = "husbandry", wayPointId = 42, station = nil, name = "Chicken Coop"}
products[42] = {{fillType = SUGAR, name = "SUGAR", title = "Æg", available = 0, level = 0, capacity = 0, isPallet = false}}
local vE = newVehicle("Æg", 0, 0, 20000)
runs[vE] = outboundRun(80, {42}, {{fillType = "SUGAR", policy = "store"}})
local storeLookups = 0
local dumpBefore = ADLoadSwap.findDumpMarker
ADLoadSwap.findDumpMarker = function(...) storeLookups = storeLookups + 1 return nil, "ingen" end
ADOutbound.buildPlanInput(adEnv, runs[vE], vE)
check("O-empty intet lagermål søgt uden vare", storeLookups, 0)
ADLoadSwap.findDumpMarker = dumpBefore
end
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-188
View File
@@ -1,188 +0,0 @@
-- Kør: luajit tests/test_adOutboundPlanner.lua (fra repo-roden)
-- Ren turvurdering for udkørsel: prisregel, tærskler, politikker, rangering, opsamlingsrunde.
dofile("FS25_ADSmartPickup/adOutboundPlanner.lua")
local P = ADOutboundPlanner
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
local function join(list) return table.concat(list or {}, ",") end
local SUGAR, FORAGE, BASKET, CRATE, BROOM = 5, 6, 7, 8, 9
local function input(products, overrides)
local result = {capacity = 20000, hasAutoload = false, sellAtPercent = 90, storeAbovePercent = 80, collectRound = false, products = products}
for key, value in pairs(overrides or {}) do result[key] = value end
return result
end
local function product(name, fillType, policy, sources, quote, store, isPallet)
return {fillType = fillType, name = name, policy = policy, isPallet = isPallet == true, sources = sources, quote = quote, store = store}
end
local quoteS = {percent = 92, price = 480, wayPointId = 900, stationName = "Grain Elevator"}
local storeSilo = {wayPointId = 400, name = "silo 1", free = 500000}
-- P1: salg ved pris over tærskel, uanset mængde
local trip = P.plan(input({product("SUGAR", SUGAR, "sell", {{wayPointId = 1, available = 5000, level = 5000, capacity = 180000}}, quoteS, nil)}))
check("P1 salg", trip.kind, "sell")
check("P1 mål", trip.targetWayPointId, 900)
check("P1 målnavn", trip.targetName, "Grain Elevator")
check("P1 kilde", join(trip.sourceIds), "1")
check("P1 vare", join(trip.fillTypes), "5")
check("P1 procent", trip.percent, 92)
-- P2: pris under tærskel, lager lavt -> ingen tur med begrundelse
local low = {percent = 78, price = 400, wayPointId = 900, stationName = "Grain Elevator"}
local trip2, reasons2 = P.plan(input({product("SUGAR", SUGAR, "sell", {{wayPointId = 1, available = 74000, level = 74000, capacity = 180000}}, low, nil)}))
check("P2 ingen tur", trip2, nil)
check("P2 begrundelse", reasons2[1], "SUGAR 78 % af top (kræver 90), lager 41 %")
-- P3: sellElseStore, pris lav, lager 85 % -> lager
local trip3 = P.plan(input({product("SUGAR", SUGAR, "sellElseStore", {{wayPointId = 1, available = 153000, level = 153000, capacity = 180000}}, low, storeSilo)}))
check("P3 lager", trip3.kind, "store")
check("P3 silo", trip3.targetWayPointId, 400)
-- P4: store med fuldt læs, lager 20 %
local trip4 = P.plan(input({product("FORAGE", FORAGE, "store", {{wayPointId = 2, available = 700000, level = 700000, capacity = 3500000}}, nil, storeSilo)}))
check("P4 fuldt læs -> lager", trip4.kind, "store")
local trip4b = P.plan(input({product("FORAGE", FORAGE, "store", {{wayPointId = 2, available = 15000, level = 15000, capacity = 3500000}}, nil, storeSilo)}))
check("P4 for lidt og lager lavt -> nil", trip4b, nil)
-- P5: rangering: højeste pris-% først
local trip5 = P.plan(input({
product("SUGAR", SUGAR, "sell", {{wayPointId = 1, available = 1000, level = 1000, capacity = 180000}}, {percent = 91, price = 1, wayPointId = 900, stationName = "A"}, nil),
product("FORAGE", FORAGE, "sell", {{wayPointId = 2, available = 1000, level = 1000, capacity = 3500000}}, {percent = 95, price = 1, wayPointId = 901, stationName = "B"}, nil),
}))
check("P5 højeste procent vinder", join(trip5.fillTypes), "6")
check("P5 uden runde kun én vare", #trip5.fillTypes, 1)
-- P6: intet nåeligt salgssted
local trip6, reasons6 = P.plan(input({product("SUGAR", SUGAR, "sell", {{wayPointId = 1, available = 170000, level = 170000, capacity = 180000}}, nil, nil)}))
check("P6 ingen tur", trip6, nil)
check("P6 begrundelse", reasons6[1], "SUGAR: intet nåeligt salgssted")
-- P7: ingen silo med plads ved lagerbehov
local trip7, reasons7 = P.plan(input({product("FORAGE", FORAGE, "store", {{wayPointId = 2, available = 3000000, level = 3000000, capacity = 3500000}}, nil, nil)}))
check("P7 ingen tur", trip7, nil)
check("P7 begrundelse", reasons7[1], "FORAGE: intet lager med plads (silo eller egen fabrik der bruger varen)")
-- P8: runde, bulk: samme vare fra flere kilder, mest først
local trip8 = P.plan(input({product("SUGAR", SUGAR, "sell", {{wayPointId = 1, available = 3000, level = 3000, capacity = 180000}, {wayPointId = 3, available = 9000, level = 9000, capacity = 180000}}, quoteS, nil)}, {collectRound = true, capacity = 10000}))
check("P8 kilder mest først", join(trip8.sourceIds), "3,1")
check("P8 én vare", #trip8.fillTypes, 1)
-- P9: runde, UAL: alle pallevarer med samme mål
local sellS = function(pct) return {percent = pct, price = 1, wayPointId = 950, stationName = "Halm-salg"} end
local sellT = {percent = 96, price = 1, wayPointId = 951, stationName = "Andet"}
local pallets = {
product("STRAW_BASKET", BASKET, "sell", {{wayPointId = 5, available = 8000, level = 8, capacity = 4000}}, sellS(95), nil, true),
product("STRAW_CRATE", CRATE, "sell", {{wayPointId = 5, available = 2000, level = 2, capacity = 4000}}, sellS(93), nil, true),
product("STRAW_BROOM", BROOM, "sell", {{wayPointId = 6, available = 1000, level = 1, capacity = 4000}}, sellT, nil, true),
}
local trip9 = P.plan(input(pallets, {collectRound = true, hasAutoload = true}))
check("P9 højeste procent = kost alene (andet mål)", join(trip9.fillTypes), "9")
local trip9b = P.plan(input({pallets[1], pallets[2]}, {collectRound = true, hasAutoload = true}))
check("P9 kurve+kasser samme mål", join(trip9b.fillTypes), "7,8")
check("P9 navne", join(trip9b.names), "STRAW_BASKET,STRAW_CRATE")
check("P9 kilder unikke", join(trip9b.sourceIds), "5")
local trip9c = P.plan(input({pallets[1], pallets[2]}, {collectRound = true, hasAutoload = false}))
check("P9 uden UAL kun én pallevare", #trip9c.fillTypes, 1)
-- P10: uden runde kun én kilde (den med mest)
local trip10 = P.plan(input({product("SUGAR", SUGAR, "sell", {{wayPointId = 1, available = 3000, level = 3000, capacity = 180000}, {wayPointId = 3, available = 9000, level = 9000, capacity = 180000}}, quoteS, nil)}))
check("P10 én kilde, den fyldeste", join(trip10.sourceIds), "3")
-- P11: intet på lager
local trip11, reasons11 = P.plan(input({product("SUGAR", SUGAR, "sell", {}, quoteS, nil)}))
check("P11 ingen tur", trip11, nil)
check("P11 begrundelse", reasons11[1], "SUGAR: intet på lager")
-- P12: evaluate
local e = P.evaluate(product("SUGAR", SUGAR, "sell", {{wayPointId = 1, available = 150000, level = 150000, capacity = 180000}}, quoteS, nil), input({}))
check("P12 storagePercent", math.floor(e.storagePercent + 0.5), 83)
check("P12 storageHigh", e.storageHigh, true)
check("P12 fullLoad", e.fullLoad, true)
check("P12 priceOk", e.priceOk, true)
-- P13 (reserve): det gårdens fabrikker/stalde skal bruge, sælges ikke
local BEET = 10
local withReserve = product("SUGARBEET", BEET, "sell", {{wayPointId = 3, available = 268000, level = 268000, capacity = 12000000}}, quoteS, nil)
withReserve.reserve, withReserve.reservedFor = 196000, {"TMR-fabrik"}
local trip13 = P.plan(input({withReserve}))
check("P13 fuldt læs uden om reserven -> salg", trip13 ~= nil and trip13.kind, "sell")
withReserve.reserve = 260000 -- kun 8.000 til overs: mindre end et læs
local trip13b, reasons13b = P.plan(input({withReserve}))
check("P13 reserven ville blive rørt -> ingen tur", trip13b, nil)
check("P13 begrundelse", reasons13b[1], "SUGARBEET: 260.0k reserveret til TMR-fabrik")
withReserve.reserve = 300000 -- alt reserveret
check("P13 alt reserveret -> ingen tur", P.plan(input({withReserve})), nil)
withReserve.reserve = 0
check("P13 reserve 0 -> salg uanset mængde", P.plan(input({withReserve})).kind, "sell")
-- lager-politik ignorerer reserven (varen bliver på gården)
local storeReserve = product("SUGARBEET", BEET, "store", {{wayPointId = 3, available = 268000, level = 11000000, capacity = 12000000}}, nil, storeSilo)
storeReserve.reserve = 300000
check("P13 lager rører ikke reserven", P.plan(input({storeReserve})).kind, "store")
local e13 = P.evaluate(withReserve, input({}, {capacity = 20000}))
check("P13 evaluate sellable", e13.sellable, 268000)
-- P14 (paller): kapacitet ukendt for en autoload-bil -> "fuldt læs" = mindst PALLET_FULL_LOAD_COUNT paller klar
local function palletInput(products)
local inp = input(products, {hasAutoload = true})
inp.capacity = nil
return inp
end
local palletProduct = product("STRAW_BASKET", BASKET, "store", {{wayPointId = 5, available = 3000, level = 1600, capacity = 8000, palletCount = 3}}, nil, storeSilo, true)
check("P14 3 paller: ikke fuldt læs", P.plan(palletInput({palletProduct})), nil)
palletProduct.sources[1].palletCount = 4
check("P14 4 paller: lager-tur", P.plan(palletInput({palletProduct})).kind, "store")
local twoSources = product("STRAW_BASKET", BASKET, "store", {{wayPointId = 5, available = 2000, level = 1600, capacity = 8000, palletCount = 2}, {wayPointId = 3, available = 2000, level = 2, capacity = 4000, palletCount = 2}}, nil, storeSilo, true)
check("P14 paller summeres over kilder", P.plan(palletInput({twoSources})).kind, "store")
check("P14 evaluate palletCount", P.evaluate(twoSources, palletInput({})).palletCount, 4)
-- P15: kilder der deler lager (mælketanke = udvidelser inden for storageRadius) regnes som ét stop;
-- den første i forløbets rækkefølge beholdes, rækkefølgen bevares
local storA, storB, storC = {}, {}, {}
local pools = {[1] = {storA, storB}, [2] = {storB}, [3] = {storC}, [4] = {storA}, [5] = nil}
local srcs = {{wayPointId = 1}, {wayPointId = 2}, {wayPointId = 3}, {wayPointId = 4}, {wayPointId = 5}}
local kept, merged = P.dedupeSharedSources(srcs, function(source) return pools[source.wayPointId] end)
check("P15 behold 1, 3, 5", join((function() local ids = {} for _, s in ipairs(kept) do table.insert(ids, s.wayPointId) end return ids end)()), "1,3,5")
check("P15 slået sammen: 2 og 4 ind i 1", join(merged[1]), "2,4")
check("P15 kilde uden lagre beholdes", kept[3].wayPointId, 5)
local none = P.dedupeSharedSources({}, function() return nil end)
check("P15 tom liste", #none, 0)
-- P16: fodring ("Lager til" valgt = feedMode): kør så snart kilden har FEED_MIN_LITERS — ikke fuldt læs / 80 %
local feedStore = {wayPointId = 50, name = "Slurry Drop 1", free = 100000}
local feedProduct = product("LIQUIDMANURE", FORAGE, "store", {{wayPointId = 2, available = 20000, level = 20000, capacity = 500000}}, nil, feedStore)
local tripNoFeed = P.plan(input({feedProduct}, {capacity = 100000}))
check("P16 uden fodring: 4 % og intet fuldt læs -> ingen tur", tripNoFeed, nil)
local tripFeed = P.plan(input({feedProduct}, {capacity = 100000, feedMode = true}))
check("P16 fodring: 20k >= minimum -> lagertur", tripFeed and tripFeed.kind, "store")
check("P16 fodring: mål = valgt drop", tripFeed and tripFeed.targetWayPointId, 50)
local tinyProduct = product("LIQUIDMANURE", FORAGE, "store", {{wayPointId = 2, available = 3000, level = 3000, capacity = 500000}}, nil, feedStore)
check("P16 fodring: under minimum -> ingen tur", P.plan(input({tinyProduct}, {capacity = 100000, feedMode = true})), nil)
-- P17: valgt lager ("Lager til") kan ikke bruges -> begrundelsen er den præcise grund, ikke "intet lager med plads"
local blocked = product("EGG", FORAGE, "store", {{wayPointId = 2, available = 3000000, level = 3000000, capacity = 3500000}}, nil, nil)
blocked.storeReason = "Bale & Pallet Storage: ingen vej til 'Chicken Storage Unload'"
local _, reasons17 = P.plan(input({blocked}))
check("P17 præcis grund i status", reasons17[1], "EGG: Bale & Pallet Storage: ingen vej til 'Chicken Storage Unload'")
-- SA: "sælg altid over X %" — kilden over grænsen sælges selvom prisen er lav (gylle ved BGA'en, 26/9)
local lowQuote = {percent = 40, price = 0.5, top = 1.25, wayPointId = 77, stationName = "Liquid Buy"}
local fullSlurry = {{wayPointId = 5, available = 900000, level = 900000, capacity = 1000000}}
local lowSlurry = {{wayPointId = 5, available = 300000, level = 300000, capacity = 1000000}}
local saOff = P.plan(input({product("LIQUIDMANURE", 40, "sell", fullSlurry, lowQuote, nil)}))
check("SA fra: lav pris -> ikke salg", saOff ~= nil and saOff.kind or "intet", "intet")
local saOn = P.plan(input({product("LIQUIDMANURE", 40, "sell", fullSlurry, lowQuote, nil)}, {sellAbovePercent = 80}))
check("SA 90 % fyldt over 80 %: sælg trods lav pris", saOn ~= nil and saOn.kind, "sell")
local saLow = P.plan(input({product("LIQUIDMANURE", 40, "sell", lowSlurry, lowQuote, nil)}, {sellAbovePercent = 80}))
check("SA 30 % fyldt: ikke salg", saLow ~= nil and saLow.kind or "intet", "intet")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-183
View File
@@ -1,183 +0,0 @@
-- 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)
-- kategori: markredskaber kun bag traktorer (in-game 29/9: Unimog, kategori "trucks", hentede kultivatoren)
check("kategori: traktor til kultivator", P.categoryOk("TRACTORSM", "cultivator"), true)
check("kategori: lastbil til kultivator", P.categoryOk("TRUCKS", "cultivator"), false)
check("kategori: lastbil til tømmevogn", P.categoryOk("TRUCKS", "unloader"), true)
check("kategori: teleskoplæsser til plov", P.categoryOk("TELELOADERVEHICLES", "plow"), false)
check("kategori: ukendt -> tilladt", P.categoryOk(nil, "cultivator"), true)
check("kategori: små bogstaver", P.categoryOk("tractorsL", "plow"), true)
local unimog = {id = "u", x = 0, z = 0, hp = 155, category = "TRUCKS"}
local fendt = {id = "f", x = 90, z = 0, hp = 250, category = "TRACTORSM"}
local cult = {{id = "c", role = "cultivator", roles = {cultivator = true}, x = 1, z = 0, neededHp = 120, name = "Cult"}}
local picked = P.pairFor("cultivator", 0, 0, cult, {unimog, fendt}, {})
check("parring: nærmeste lastbil springes over", picked ~= nil and picked.tractor.id or nil, "f")
-- 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)
-- 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)
-- review-rettelser
-- C1: hjemtur også når AD selv ville parkere (påfyldte sæt); aldrig ved manuel transport
check("C1: hjem trods adHandlesEnd", P.shouldReturnHome({adHandlesEnd = true}), true)
check("C1: ikke ved manuel", P.shouldReturnHome({manual = true}), false)
-- I1: kun maskiner uden egen arbejdsrolle kan trække et puljeredskab
check("I1: traktor uden rolle", P.canPull(nil), true)
check("I1: mejetærsker", P.canPull("harvester"), false)
check("I1: selvkørende sprøjte", P.canPull("fertilizer"), false)
-- I2: traktor uden AD-vejnet springes over; fejl uden fremdrift skyldes traktoren
local offNet = P.pairFor("plow", 0, 0, {implements[2]}, {{id = "far", x = 100, z = 0, offNetwork = true}, {id = "ok", x = 400, z = 0, hp = 300}}, {})
check("I2: traktor uden for vejnettet springes over", offNet.tractor.id, "ok")
check("I2: stoppedAway -> traktoren", P.failureBlames("stoppedAway"), "tractor")
check("I2: stoppedAway med afstand -> traktoren", P.failureBlames("stoppedAway (40 m fra markøren)"), "tractor")
check("I2: noAttachable -> redskabet", P.failureBlames("noAttachable"), "implement")
-- I3: efter koblingen — tom uden påfyldning = fejl, tom med påfyldning = fyld op, ellers kør
check("I3: ingen påfyldningsbehov", P.nextDrive(nil), "drive")
check("I3: fyldt", P.nextDrive({empty = false}), "drive")
check("I3: tom med påfyldning", P.nextDrive({empty = true, refillId = 7}), "load")
check("I3: tom uden påfyldning", P.nextDrive({empty = true}), "empty")
-- I4: puljen kun til hovedopgaver med ét sæt, og ikke når marken har egne faste maskiner til rollen
check("I4: almindelig", P.mayUsePool({count = 1}, false), true)
check("etape 2: tømmevogn må bruge puljen", P.mayUsePool({count = 1, helper = true}, false), true)
check("I4: multi-tool", P.mayUsePool({count = 2}, false), false)
check("I4: marken har faste", P.mayUsePool({count = 1}, true), false)
-- status til flådefanen
local function st(key, arg) return tostring(key) .. "|" .. tostring(arg) end
local function ist(s) return st(P.implementStatus(s)) end
check("redskab: uden parkering", ist({}), "noPark|nil")
check("redskab: uden Connect", ist({parkName = "Plov"}), "noConnect|Plov")
check("redskab: på traktor", ist({parkName = "Plov", hasConnect = true, attachedTo = "MT635"}), "attached|MT635")
check("redskab: karantæne", ist({parkName = "Plov", hasConnect = true, quarantineMin = 8}), "quarantine|8")
check("redskab: reserveret", ist({parkName = "Plov", hasConnect = true, reserved = true}), "reserved|nil")
check("redskab: parkeret", ist({parkName = "Plov", hasConnect = true}), "parked|Plov")
local function tst(s) return st(P.tractorStatus(s)) end
local base = {hasPark = true, enabled = true}
check("traktor: uden parkering", tst({enabled = true}), "noPark|nil")
check("traktor: spiller", tst({hasPark = true, enabled = true, controlled = true}), "player|nil")
check("traktor: hjemtur", tst({hasPark = true, enabled = true, returning = true}), "returning|nil")
check("traktor: opgave", tst({hasPark = true, enabled = true, fieldId = 52}), "job|52")
check("traktor: optaget", tst({hasPark = true, enabled = true, busy = true}), "busy|nil")
check("traktor: ikke i flåden", tst({hasPark = true}), "notInFleet|nil")
check("traktor: uden for vejnet", tst({hasPark = true, enabled = true, offNetwork = true}), "offNetwork|nil")
check("traktor: har andet redskab på", tst({hasPark = true, enabled = true, hitched = true}), "hitched|nil")
check("traktor: ledig", tst(base), "free|nil")
-- karantæne-rest i hele minutter (rundet op) og reservation
P.quarantine("q1", 0)
check("karantæne: minutter tilbage", P.quarantineMinutes("q1", 60000), 9)
check("karantæne: udløbet", P.quarantineMinutes("q1", P.QUARANTINE_MS), 0)
P.reserve("owner", {"r1"})
check("reserveret: ja", P.isReserved("r1"), true)
P.release("owner")
check("reserveret: nej", P.isReserved("r1"), false)
-- M3: ventende marker prøver igen i samme takt (så køens rækkefølge holder)
check("M3: næste takt", P.alignedRetry(61000, 30000), 90000)
check("M3: på grænsen -> næste", P.alignedRetry(60000, 30000), 90000)
check("M3: to marker samme takt", P.alignedRetry(62000, 30000) == P.alignedRetry(89000, 30000), true)
-- M4: for svag nævner redskabets navn
check("M4: navn i args", weakArgs[2], "Plov nær")
-- etape 2: puljevogn skal kunne bære høstens vare
check("bære: vare understøttet", P.canCarry({carries = {[3] = true}}, 3), true)
check("bære: vare ikke understøttet", P.canCarry({carries = {[3] = true}}, 7), false)
check("bære: ingen vare krævet", P.canCarry({carries = {}}, nil), true)
check("bære: ukendt last", P.canCarry({}, 3), false)
-- etape 3: alle puljevogne parret med hver sin nærmeste traktor (en traktor bruges kun én gang)
local wagons = {{id = "v1", role = "unloader", x = 0, z = 0}, {id = "v2", role = "unloader", x = 100, z = 0},
{id = "v3", role = "unloader", x = 200, z = 0}, {id = "plovX", role = "plow", x = 0, z = 0}}
local pairs3 = P.pairAll("unloader", wagons, {{id = "tA", x = 10, z = 0}, {id = "tB", x = 90, z = 0}}, {v2 = true})
check("par-alle: antal (v2 reserveret, 2 traktorer)", #pairs3, 2)
check("par-alle: v1 -> tA", pairs3[1].implement.id .. ">" .. pairs3[1].tractor.id, "v1>tA")
check("par-alle: v3 -> tB (tA er brugt)", pairs3[2].implement.id .. ">" .. pairs3[2].tractor.id, "v3>tB")
check("par-alle: ingen traktorer", #P.pairAll("unloader", wagons, {}, {}), 0)
-- flow-vogne: vogn der hører til et kørende flow vises sådan (og er ikke i puljen)
check("redskab: flowets vogn", ist({parkName = "P Cow Food vogn 1", hasConnect = true, flowName = "Cow Food"}), "flow|Cow Food")
check("redskab: flowets vogn koblet på", ist({parkName = "P Cow Food vogn 1", hasConnect = true, flowName = "Cow Food", attachedTo = "Cow Food 1"}), "attached|Cow Food 1")
-- pasform: mindste traktor med hk nok; afstand kun inden for ±15 %; over 2,5x behovet kun som nødløsning
-- (in-game 29/9: en meget stor traktor blev sendt til et lille job og kunne ikke komme rundt)
local impl120 = {id = "k", x = 0, z = 0, neededHp = 120}
local t150 = {id = "t150", x = 900, z = 0, hp = 150}
local t170 = {id = "t170", x = 300, z = 0, hp = 180}
local t500 = {id = "t500", x = 50, z = 0, hp = 500}
check("pasform: mindste der passer vinder over nærmere", P.bestFit({t500, t170, t150}, impl120, "cultivator").id, "t150")
local t160 = {id = "t160", x = 100, z = 0, hp = 160}
check("pasform: inden for 15 % vinder den nærmeste", P.bestFit({t150, t160}, impl120, "cultivator").id, "t160")
check("pasform: kun for stor -> nødløsning", P.bestFit({t500}, impl120, "cultivator").id, "t500")
check("pasform: for svag -> ingen", P.bestFit({{id = "t90", x = 0, z = 0, hp = 90}}, impl120, "cultivator"), nil)
check("pasform: redskab uden hk-data -> nærmeste", P.bestFit({t150, t500}, {id = "x", x = 0, z = 0}, "cultivator").id, "t500")
check("pasform: ukendt traktor-hk efter kendte", P.bestFit({{id = "u", x = 0, z = 0}, t170}, impl120, "cultivator").id, "t170")
check("pasform: lastbil tæller ikke", P.bestFit({{id = "lb", x = 0, z = 0, hp = 150, category = "TRUCKS"}, t500}, impl120, "cultivator").id, "t500")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-106
View File
@@ -1,106 +0,0 @@
-- Kør: luajit tests/test_adPrices.lua (fra repo-roden)
-- Prisregel: toppris fra årets prisforløb, pris-% pr. salgssted, bedste tilbud.
dofile("FS25_ADSmartPickup/adPrices.lua")
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
-- P1: højeste faktor
check("P1 maxFactor", ADPrices.maxFactor({EARLY_SPRING = 0.9, LATE_AUTUMN = 1.2, MID_SUMMER = 1.0}), 1.2)
check("P1 uden faktorer = 1", ADPrices.maxFactor(nil), 1)
-- P2: toppris = grundpris nu × (maks ÷ nu)
check("P2 topPrice", ADPrices.topPrice(500, 1.0, 1.25), 625)
check("P2 factorNow 0 -> grundpris", ADPrices.topPrice(500, 0, 1.25), 500)
check("P2 grundpris 0 -> 0", ADPrices.topPrice(0, 1, 1.25), 0)
-- P3: procent, afrundet; stor efterspørgsel > 100
check("P3 75 %", ADPrices.percentOfTop(470, 625), 75)
check("P3 112 %", ADPrices.percentOfTop(700, 625), 112)
check("P3 top 0 -> 0", ADPrices.percentOfTop(1, 0), 0)
-- P4: spil-adapter med stationens grundpris
SellingStation = {}
local SUGAR = 5
g_fillTypeManager = {getFillTypeByIndex = function(_, index)
if index == SUGAR then return {economy = {factors = {A = 1.0, B = 1.25}}, pricePerLiter = 400} end
end}
g_currentMission = {environment = {currentPeriod = "A"}, economyManager = {getPriceMultiplier = function() return 1.0 end}}
local station = {
fillTypePrices = {[SUGAR] = 500},
getEffectiveFillTypePrice = function(_, fillType) return 470 end,
isa = function(_, class) return class == SellingStation end,
isSellingPoint = true,
}
local quote = ADPrices.getQuote(station, SUGAR)
check("P4 pris", quote.price, 470)
check("P4 top (pris ved 100 %)", math.floor(quote.top + 0.5), 588)
check("P4 procent = kurven alene (stationens grundpris bruges ikke: den mangler sværhedsgrad m.m.)", quote.percent, 80)
check("P4 er salgssted", ADPrices.isSellingStation(station), true)
check("P4 skjult = ikke salgssted", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = true, hideFromPricesMenu = true}), false)
check("P4 fabrik-station = ikke salgssted", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = false}), false)
-- in-game (23/9): TMR-fabrikkens indlevering er en SellingStation med isSellingPoint=true -> blev regnet som marked
check("P4 fabrikkens egen station = ikke salgssted", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = true, owningPlaceable = {spec_productionPoint = {}}}), false)
check("P4 staldens egen station = ikke salgssted", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = true, owningPlaceable = {getHusbandryFillLevel = function() end}}), false)
-- P4b: fremmed produktion (kortets mejeri, ejer 0) køber varen = salgssted; gårdens egen fabrik er ikke
check("P4b fremmed mejeri = salgssted", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = true, owningPlaceable = {spec_productionPoint = {}, getOwnerFarmId = function() return 0 end}}), true)
check("P4b egen fabrik (ejer 1) = ikke salgssted", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = true, owningPlaceable = {spec_productionPoint = {}, getOwnerFarmId = function() return 1 end}}), false)
check("P4b fremmed stald-felt ownerFarmId", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = true, owningPlaceable = {getHusbandryFillLevel = function() end, ownerFarmId = 0}}), true)
check("P4 rigtigt salgssted med placeable", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = true, owningPlaceable = {}}), true)
-- P5: uden stationens grundpris: kun kurven tæller (faktor nu ÷ faktor max)
local bare = {getEffectiveFillTypePrice = function() return 470 end}
local quote2 = ADPrices.getQuote(bare, SUGAR)
check("P5 procent = kurven alene", quote2.percent, 80)
check("P5 top = effektiv ÷ kurve", math.floor(quote2.top + 0.5), 588)
-- P7: kurven er grundlaget (Distribution Redux-metoden): faktor nu ÷ årets max, uafhængigt af sværhedsgrad
check("P7 curvePercent", ADPrices.curvePercent({1.0, 1.25, 0.9}, 1), 80)
check("P7 curvePercent ved top", ADPrices.curvePercent({1.0, 1.25, 0.9}, 2), 100)
check("P7 flad kurve = 100", ADPrices.curvePercent({1.0, 1.01, 1.0}, 3), 100)
check("P7 ingen kurve = 100", ADPrices.curvePercent(nil, 1), 100)
check("P7 ukendt periode = 100", ADPrices.curvePercent({1.0, 1.25}, nil), 100)
-- P8: in-game (23/9): effektiv 1602 mod fillTypePrices 505 gav 317 % — stationens grundpris indeholder
-- hverken sværhedsgrad eller prisskala, så forholdet er ubrugeligt. Procenten er kurven alene.
local demandStation = {fillTypePrices = {[SUGAR] = 505}, getEffectiveFillTypePrice = function() return 1602 end}
check("P8 procent uafhængig af stationens grundpris", ADPrices.getQuote(demandStation, SUGAR).percent, 80)
check("P8 top = pris ved 100 % af kurven", math.floor(ADPrices.getQuote(demandStation, SUGAR).top + 0.5), 2003)
-- P9: sværhedsgraden påvirker ikke procenten
local hardStation = {getEffectiveFillTypePrice = function() return 235 end}
check("P9 uafhængig af sværhedsgrad", ADPrices.getQuote(hardStation, SUGAR).percent, 80)
check("P5 fejlende station -> nil", ADPrices.getQuote({getEffectiveFillTypePrice = function() error("x") end}, SUGAR), nil)
-- P6: bedste tilbud = højeste procent, dernæst pris
local best = ADPrices.bestQuote({{percent = 80, price = 1}, {percent = 92, price = 2}, {percent = 92, price = 3}})
check("P6 højeste procent", best.percent, 92)
check("P6 ved lighed højeste pris", best.price, 3)
check("P6 tom -> nil", ADPrices.bestQuote({}), nil)
-- P-land: kortanlæg der følger jorden (boughtWithFarmland) er gårdens, når jorden under dem er gårdens —
-- selv om placeable-ejeren står som 15 (Dakotas BGA500kw på jordlod 69)
getWorldTranslation = getWorldTranslation or function(node) return node.x, 0, node.z end
g_currentMission = g_currentMission or {}
g_currentMission.getFarmId = function() return 1 end
g_farmlandManager = {
getFarmlandIdAtWorldPosition = function(_, x, z) return x < 0 and 69 or 70 end,
getFarmlandOwner = function(_, id) return id == 69 and 1 or 0 end,
}
local bga = {rootNode = {x = -1600, z = -89}, boughtWithFarmland = true, spec_productionPoint = {}, getOwnerFarmId = function() return 15 end}
local bgaOther = {rootNode = {x = 1600, z = -89}, boughtWithFarmland = true, spec_productionPoint = {}, getOwnerFarmId = function() return 15 end}
local dairy = {rootNode = {x = -3173, z = -4028}, spec_productionPoint = {}, getOwnerFarmId = function() return 0 end}
check("P-land BGA på egen jord = egen", ADPrices.isOwnedBy(bga, 1), true)
check("P-land BGA på fremmed jord = ikke egen", ADPrices.isOwnedBy(bgaOther, 1), false)
check("P-land mejeri (ejer 0, ikke jord-bundet) = ikke egen", ADPrices.isOwnedBy(dairy, 1), false)
check("P-land egen ejer", ADPrices.isOwnedBy({getOwnerFarmId = function() return 1 end}, 1), true)
check("P-land BGA er ikke salgssted", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = true, owningPlaceable = bga}), false)
g_farmlandManager = nil
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-42
View File
@@ -1,42 +0,0 @@
-- Kør: luajit tests/test_adProfiler.lua (fra repo-roden)
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
local lines = {}
Logging = {info = function(fmt, ...) table.insert(lines, string.format(fmt, ...)) end}
local clock = 0
function netGetTime() return clock end
dofile("FS25_ADSmartPickup/adProfiler.lua")
local Module = {}
function Module.slow(a, b) clock = clock + 5; return a + b, "ok" end
function Module.fast() clock = clock + 0.1 end
ADProfiler.wrap(Module, "slow", "langsom")
ADProfiler.wrap(Module, "fast", "hurtig")
local sum, text = Module.slow(2, 3)
check("P1 returværdier bevares", sum, 5); check("P1 anden returværdi", text, "ok")
Module.fast()
check("P2 kald talt", ADProfiler.stats["langsom"].calls, 1)
check("P2 max", ADProfiler.stats["langsom"].max, 5)
ADProfiler.frame(0)
check("P3 værste frame", ADProfiler.worstFrameMs, 5.1)
ADProfiler.frame(61000)
check("P4 rapport skrevet", #lines, 1)
check("P4 langsom først", lines[1]:find("langsom max 5.0", 1, true) ~= nil, true)
check("P4 nulstillet", next(ADProfiler.stats), nil)
-- intet over 2 ms -> ingen rapport
Module.fast(); ADProfiler.frame(62000); ADProfiler.frame(130000)
check("P5 stille minut -> ingen linje", #lines, 1)
-- fejl i indpakket funktion: dybden nulstilles næste frame
function Module.boom() error("x") end
ADProfiler.wrap(Module, "boom", "fejl")
pcall(Module.boom)
ADProfiler.frame(131000)
check("P6 dybde nulstillet", ADProfiler.depth, 0)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-385
View File
@@ -1,385 +0,0 @@
-- Kør: luajit tests/test_adRuns.lua (fra repo-roden)
-- Tester run-modellen (rene funktioner) og XML-laget mod en XMLFile-mock.
dofile("FS25_ADSmartPickup/adRuns.lua")
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
-- R1: opret run -> løbenummer, standardværdier; input-store røres ikke
local empty = ADRuns.newStore()
local s1, cow = ADRuns.createRun(empty, "Cow Feed")
check("R1 id 1", cow.id, 1)
check("R1 navn", cow.name, "Cow Feed")
check("R1 auto-varer som standard", cow.ingredientMode, "auto")
check("R1 loops 0", cow.loops, 0)
check("R1 tom Wait-pulje", cow.waitPoolGroup, "")
check("R1 input-store urørt", #empty.runs, 0)
check("R1 nextId tæller op", s1.nextId, 2)
local s2, pig = ADRuns.createRun(s1, "Pig Feed")
check("R1 andet run får id 2", pig.id, 2)
check("R1 to runs", #s2.runs, 2)
-- R2: opdatér felter; ukendt mode ignoreres; loops klippes
local s3 = ADRuns.updateRun(s2, 1, {name = "Cow 1", targetWayPointId = 94, ingredientMode = "manual", ingredients = {"STRAW", "FORAGE"}, waitPoolGroup = "Cow Wait", loops = 150})
local run1 = ADRuns.getRun(s3, 1)
check("R2 navn", run1.name, "Cow 1")
check("R2 mål", run1.targetWayPointId, 94)
check("R2 manual", run1.ingredientMode, "manual")
check("R2 varer", table.concat(run1.ingredients, ","), "STRAW,FORAGE")
check("R2 pulje", run1.waitPoolGroup, "Cow Wait")
check("R2 loops klippet til 99", run1.loops, 99)
check("R2 gammel store urørt", ADRuns.getRun(s2, 1).name, "Cow Feed")
local s3b = ADRuns.updateRun(s3, 1, {ingredientMode = "bogus", loops = -5})
check("R2 ukendt mode ignoreres", ADRuns.getRun(s3b, 1).ingredientMode, "manual")
check("R2 loops under 0 -> 0", ADRuns.getRun(s3b, 1).loops, 0)
check("R2 ukendt run -> uændret", #ADRuns.updateRun(s3, 99, {name = "x"}).runs, 2)
check("R2 targetWayPointId=false rydder målet", ADRuns.getRun(ADRuns.updateRun(s3, 1, {targetWayPointId = false}), 1).targetWayPointId, nil)
check("R2 nil rører ikke målet", ADRuns.getRun(ADRuns.updateRun(s3, 1, {name = "y"}), 1).targetWayPointId, 94)
-- R3: tildel traktor; samme traktor flyttes mellem runs
local s4, prev = ADRuns.assignVehicle(s3, 1, "vehicleA")
check("R3 tildelt", ADRuns.getRun(s4, 1).vehicleIds[1], "vehicleA")
check("R3 ingen tidligere", prev, nil)
check("R3 opslag", ADRuns.findRunByVehicle(s4, "vehicleA").id, 1)
local s5, prev2 = ADRuns.assignVehicle(s4, 2, "vehicleA")
check("R3 flyttet fra run 1", prev2, 1)
check("R3 væk fra run 1", #ADRuns.getRun(s5, 1).vehicleIds, 0)
check("R3 i run 2", ADRuns.getRun(s5, 2).vehicleIds[1], "vehicleA")
local s5b, prev3 = ADRuns.assignVehicle(s5, 2, "vehicleA")
check("R3 gen-tildeling samme run -> ingen dublet", #ADRuns.getRun(s5b, 2).vehicleIds, 1)
check("R3 gen-tildeling -> ingen 'flyttet fra'", prev3, nil)
local s6 = ADRuns.unassignVehicle(s5, "vehicleA")
check("R3 fjernet", ADRuns.findRunByVehicle(s6, "vehicleA"), nil)
check("R3 ukendt run ved tildeling -> uændret", ADRuns.findRunByVehicle(ADRuns.assignVehicle(s6, 42, "vehicleB"), "vehicleB"), nil)
-- R4: slet run
local s7 = ADRuns.deleteRun(s5, 1)
check("R4 ét run tilbage", #s7.runs, 1)
check("R4 det rigtige", s7.runs[1].id, 2)
check("R4 nextId bevares", s7.nextId, 3)
-- R5: records frem og tilbage
local records = ADRuns.toRecords(s5)
check("R5 target som tal", records[1].target, 94)
check("R5 varer som streng", records[1].ingredients, "STRAW FORAGE")
check("R5 køretøjer", records[2].vehicleIds[1], "vehicleA")
local back = ADRuns.fromRecords(records)
check("R5 to runs tilbage", #back.runs, 2)
check("R5 nextId = højeste + 1", back.nextId, 3)
check("R5 varer parset", ADRuns.getRun(back, 1).ingredients[2], "FORAGE")
check("R5 pulje", ADRuns.getRun(back, 1).waitPoolGroup, "Cow Wait")
check("R5 target 0 -> nil", ADRuns.getRun(back, 2).targetWayPointId, nil)
check("R5 køretøj", ADRuns.findRunByVehicle(back, "vehicleA").id, 2)
-- R6: defekte poster springes over; dubletter af id ignoreres
local dirty = ADRuns.fromRecords({
{id = "x", name = "Broken"},
{id = 5, name = "", },
{id = 7, name = "Ok", target = "12", ingredientMode = "weird", ingredients = "", waitPool = nil, loops = "3", vehicleIds = {}},
{id = 7, name = "Dublet"},
})
check("R6 kun den gyldige", #dirty.runs, 1)
check("R6 target parses fra streng", dirty.runs[1].targetWayPointId, 12)
check("R6 ukendt mode -> auto", dirty.runs[1].ingredientMode, "auto")
check("R6 tom varestreng -> tom liste", #dirty.runs[1].ingredients, 0)
check("R6 manglende pulje -> tom streng", dirty.runs[1].waitPoolGroup, "")
check("R6 loops fra streng", dirty.runs[1].loops, 3)
check("R6 nextId", dirty.nextId, 8)
check("R6 tom liste", #ADRuns.fromRecords(nil).runs, 0)
check("R6 parseIngredients trimmer", #ADRuns.parseIngredients(" STRAW FORAGE "), 2)
-- R7: "kør hjem under X %" — standard 50, klippes til 10..90, overlever records
local r7, r7run = ADRuns.createRun(ADRuns.newStore(), "Tærskel")
check("R7 standard 50 %", r7run.returnBelowPercent, 50)
check("R7 sættes", ADRuns.getRun(ADRuns.updateRun(r7, r7run.id, {returnBelowPercent = 30}), r7run.id).returnBelowPercent, 30)
check("R7 klippes til 90", ADRuns.getRun(ADRuns.updateRun(r7, r7run.id, {returnBelowPercent = 150}), r7run.id).returnBelowPercent, 90)
check("R7 klippes til 10", ADRuns.getRun(ADRuns.updateRun(r7, r7run.id, {returnBelowPercent = 0}), r7run.id).returnBelowPercent, 10)
local r7rec = ADRuns.toRecords(ADRuns.updateRun(r7, r7run.id, {returnBelowPercent = 25}))
check("R7 i records", r7rec[1].returnBelow, 25)
check("R7 tilbage fra records", ADRuns.fromRecords(r7rec).runs[1].returnBelowPercent, 25)
check("R7 manglende i gammel fil -> 50", ADRuns.fromRecords({{id = 1, name = "Gammel"}}).runs[1].returnBelowPercent, 50)
-- R8: flere mål pr. run — liste + første som "primært" mål (bagudkompatibelt); toggle; records
local r8, r8run = ADRuns.createRun(ADRuns.newStore(), "Kvæg")
check("R8 ingen mål", #r8run.targetWayPointIds, 0)
r8 = ADRuns.updateRun(r8, r8run.id, {targetWayPointIds = {94, 99}})
check("R8 to mål", table.concat(ADRuns.getRun(r8, r8run.id).targetWayPointIds, ","), "94,99")
check("R8 primært mål = første", ADRuns.getRun(r8, r8run.id).targetWayPointId, 94)
r8 = ADRuns.toggleTarget(r8, r8run.id, 94)
check("R8 toggle fjerner", table.concat(ADRuns.getRun(r8, r8run.id).targetWayPointIds, ","), "99")
check("R8 primært følger med", ADRuns.getRun(r8, r8run.id).targetWayPointId, 99)
r8 = ADRuns.toggleTarget(r8, r8run.id, 120)
check("R8 toggle tilføjer", table.concat(ADRuns.getRun(r8, r8run.id).targetWayPointIds, ","), "99,120")
check("R8 gammel enkelt-sætning = liste med ét", table.concat(ADRuns.getRun(ADRuns.updateRun(r8, r8run.id, {targetWayPointId = 7}), r8run.id).targetWayPointIds, ","), "7")
check("R8 false rydder listen", #ADRuns.getRun(ADRuns.updateRun(r8, r8run.id, {targetWayPointId = false}), r8run.id).targetWayPointIds, 0)
local r8rec = ADRuns.toRecords(r8)
check("R8 records: targets-streng", r8rec[1].targets, "99 120")
check("R8 records: target = første (gammelt felt)", r8rec[1].target, 99)
check("R8 tilbage fra records", table.concat(ADRuns.fromRecords(r8rec).runs[1].targetWayPointIds, ","), "99,120")
check("R8 gammel fil med kun target", table.concat(ADRuns.fromRecords({{id = 1, name = "Gl", target = 55}}).runs[1].targetWayPointIds, ","), "55")
-- ==== XML-laget mod en flad XMLFile-mock (nøgle -> værdi) ====
dofile("FS25_ADSmartPickup/adRunsStorage.lua")
local function makeXmlMock(values)
local xml = {values = values or {}, saved = false, deleted = false}
xml.setInt = function(self, key, value) self.values[key] = value end
xml.setString = function(self, key, value) self.values[key] = value end
xml.getInt = function(self, key) return tonumber(self.values[key]) end
xml.getString = function(self, key) local v = self.values[key]; return v ~= nil and tostring(v) or nil end
-- iterate: kalder callback for "prefix(0)", "prefix(1)", ... så længe en nøgle med den stamme findes
xml.iterate = function(self, prefix, callback)
local index = 0
while true do
local key = string.format("%s(%d)", prefix, index)
local found = false
for k in pairs(self.values) do
if k:sub(1, #key) == key then found = true break end
end
if not found then return end
callback(index + 1, key)
index = index + 1
end
end
xml.save = function(self) self.saved = true end
xml.delete = function(self) self.deleted = true end
return xml
end
-- X1: records skrives som attributter + vehicle-underelementer
local out = makeXmlMock()
ADRunsStorage.writeRecords(out, ADRuns.toRecords(s5))
check("X1 version", out.values["ADSmartPickupRuns#version"], 3)
check("X1 run 0 navn", out.values["ADSmartPickupRuns.run(0)#name"], "Cow 1")
check("X1 run 0 target", out.values["ADSmartPickupRuns.run(0)#target"], 94)
check("X1 run 0 varer", out.values["ADSmartPickupRuns.run(0)#ingredients"], "STRAW FORAGE")
check("X1 run 0 pulje", out.values["ADSmartPickupRuns.run(0)#waitPool"], "Cow Wait")
check("X1 run 1 køretøj", out.values["ADSmartPickupRuns.run(1).vehicle(0)#id"], "vehicleA")
-- X2: samme data læses tilbage
local records2 = ADRunsStorage.readRecords(makeXmlMock(out.values))
check("X2 to poster", #records2, 2)
check("X2 id", records2[1].id, 1)
check("X2 køretøj", records2[2].vehicleIds[1], "vehicleA")
check("X2 loops", records2[1].loops, 99)
check("X2 round-trip", ADRuns.findRunByVehicle(ADRuns.fromRecords(records2), "vehicleA").id, 2)
-- X3: save/load via XMLFile-globalen; ingen savegame-mappe -> tom store, intet skrives
Logging = Logging or {warning = function() end, info = function() end}
local created, loaded = nil, nil
XMLFile = {
create = function(_, path, root) created = makeXmlMock(); created.path = path; created.root = root; return created end,
load = function(_, path) loaded = makeXmlMock(created and created.values or {}); loaded.path = path; return loaded end,
}
fileExists = function(path) return created ~= nil and created.path == path end
g_currentMission = {missionInfo = {savegameDirectory = "/tmp/sg1"}}
check("X3 gemt", ADRunsStorage.save(s5), true)
check("X3 sti", created.path, "/tmp/sg1/ADSmartPickup_runs.xml")
check("X3 rodelement", created.root, "ADSmartPickupRuns")
check("X3 save+delete kaldt", created.saved and created.deleted, true)
check("X3 indlæst", #ADRunsStorage.load().runs, 2)
check("X3 læse-handle lukket", loaded.deleted, true)
g_currentMission = {missionInfo = {}}
check("X3 uden savegame -> false", ADRunsStorage.save(s5), false)
check("X3 uden savegame -> tom store", #ADRunsStorage.load().runs, 0)
g_currentMission = {missionInfo = {savegameDirectory = "/tmp/sg-ny"}}
check("X3 fil mangler -> tom store", #ADRunsStorage.load().runs, 0)
-- R-v2: udkørsels-run — kind, kilder, varer med politik, tærskler, runde; XML v2 + v1-migration
local o1, out = ADRuns.createRun(ADRuns.newStore(), "Sugar out", "outbound")
check("Rv2 kind", out.kind, "outbound")
local _, plain = ADRuns.createRun(ADRuns.newStore(), "x")
check("Rv2 supply som standard", plain.kind, "supply")
check("Rv2 sellAt 90", out.sellAtPercent, 90)
check("Rv2 storeAbove 80", out.storeAbovePercent, 80)
check("Rv2 runde fra", out.collectRound, false)
local o2 = ADRuns.toggleSource(o1, out.id, 12694)
o2 = ADRuns.toggleSource(o2, out.id, 12386)
check("Rv2 to kilder", table.concat(ADRuns.getRun(o2, out.id).sourceWayPointIds, ","), "12694,12386")
check("Rv2 toggle fjerner", #ADRuns.getRun(ADRuns.toggleSource(o2, out.id, 12694), out.id).sourceWayPointIds, 1)
local o3 = ADRuns.setProductPolicy(o2, out.id, "SUGAR", "sell")
o3 = ADRuns.setProductPolicy(o3, out.id, "FORAGE", "store")
check("Rv2 politik SUGAR", ADRuns.getProductPolicy(ADRuns.getRun(o3, out.id), "SUGAR"), "sell")
check("Rv2 politik ukendt = off", ADRuns.getProductPolicy(ADRuns.getRun(o3, out.id), "STRAW"), "off")
check("Rv2 ugyldig politik ignoreres", ADRuns.getProductPolicy(ADRuns.getRun(ADRuns.setProductPolicy(o3, out.id, "SUGAR", "bogus"), out.id), "SUGAR"), "sell")
check("Rv2 off fjerner", ADRuns.getProductPolicy(ADRuns.getRun(ADRuns.setProductPolicy(o3, out.id, "SUGAR", "off"), out.id), "SUGAR"), "off")
check("Rv2 gammel store urørt", #ADRuns.getRun(o2, out.id).products, 0)
local o4 = ADRuns.updateRun(o3, out.id, {sellAtPercent = 120, storeAbovePercent = 10, collectRound = true})
check("Rv2 sellAt klippet 100", ADRuns.getRun(o4, out.id).sellAtPercent, 100)
check("Rv2 storeAbove klippet 50", ADRuns.getRun(o4, out.id).storeAbovePercent, 50)
check("Rv2 runde til", ADRuns.getRun(o4, out.id).collectRound, true)
check("Rv2 serialiser", ADRuns.serializeProducts(ADRuns.getRun(o3, out.id).products), "SUGAR:sell FORAGE:store")
local parsed = ADRuns.parseProducts("SUGAR:sell FORAGE:store BAD BAD:bogus")
check("Rv2 parse antal", #parsed, 2)
check("Rv2 parse politik", parsed[2].policy, "store")
local rt = ADRuns.fromRecords(ADRuns.toRecords(o4))
local rtRun = ADRuns.getRun(rt, out.id)
check("Rv2 roundtrip kind", rtRun.kind, "outbound")
check("Rv2 roundtrip kilder", table.concat(rtRun.sourceWayPointIds, ","), "12694,12386")
check("Rv2 roundtrip products", ADRuns.serializeProducts(rtRun.products), "SUGAR:sell FORAGE:store")
check("Rv2 roundtrip sellAt", rtRun.sellAtPercent, 100)
check("Rv2 roundtrip runde", rtRun.collectRound, true)
local v1 = ADRuns.fromRecords({{id = 7, name = "Old", target = 94, targets = "94", ingredientMode = "auto", ingredients = "", waitPool = "", loops = 0, returnBelow = 50, vehicleIds = {}}})
check("Rv2 v1 -> supply", ADRuns.getRun(v1, 7).kind, "supply")
check("Rv2 v1 tomme kilder", #ADRuns.getRun(v1, 7).sourceWayPointIds, 0)
check("Rv2 v1 sellAt standard", ADRuns.getRun(v1, 7).sellAtPercent, 90)
-- XML-laget skriver/læser v2-felterne
do
local written = {}
local xml = {
setInt = function(_, k, v) written[k] = v end, setString = function(_, k, v) written[k] = v end,
getInt = function(_, k) return written[k] end, getString = function(_, k) return written[k] end,
iterate = function(_, prefix, fn)
local i = 0
while written[string.format("%s(%d)#id", prefix, i)] ~= nil do fn(i, string.format("%s(%d)", prefix, i)); i = i + 1 end
end,
}
ADRunsStorage.writeRecords(xml, ADRuns.toRecords(o4))
check("Rv2 xml version 2", written["ADSmartPickupRuns#version"], 3)
check("Rv2 xml kind", written["ADSmartPickupRuns.run(0)#kind"], "outbound")
check("Rv2 xml sources", written["ADSmartPickupRuns.run(0)#sources"], "12694 12386")
check("Rv2 xml products", written["ADSmartPickupRuns.run(0)#products"], "SUGAR:sell FORAGE:store")
check("Rv2 xml collectRound", written["ADSmartPickupRuns.run(0)#collectRound"], 1)
local back = ADRuns.fromRecords(ADRunsStorage.readRecords(xml))
check("Rv2 xml læs sellAt", ADRuns.getRun(back, out.id).sellAtPercent, 100)
check("Rv2 xml læs runde", ADRuns.getRun(back, out.id).collectRound, true)
end
-- R-v3: bygninger som kilder/mål (id-lister); XML v3; v2-fil uden bygninger giver tomme lister
local b1, flow = ADRuns.createRun(ADRuns.newStore(), "Milk", "outbound")
check("Rv3 tomme kilde-bygninger", #flow.sourceBuildingIds, 0)
check("Rv3 tomme mål-bygninger", #flow.targetBuildingIds, 0)
local b2 = ADRuns.toggleSourceBuilding(b1, flow.id, "barn1")
b2 = ADRuns.toggleSourceBuilding(b2, flow.id, "barn2")
check("Rv3 to kilde-bygninger", table.concat(ADRuns.getRun(b2, flow.id).sourceBuildingIds, ","), "barn1,barn2")
check("Rv3 toggle fjerner", #ADRuns.getRun(ADRuns.toggleSourceBuilding(b2, flow.id, "barn1"), flow.id).sourceBuildingIds, 1)
local b3 = ADRuns.toggleTargetBuilding(b2, flow.id, "mill")
check("Rv3 mål-bygning", ADRuns.getRun(b3, flow.id).targetBuildingIds[1], "mill")
check("Rv3 gammel store urørt", #ADRuns.getRun(b2, flow.id).targetBuildingIds, 0)
local b4 = ADRuns.updateRun(b3, flow.id, {sourceBuildingIds = {"shed"}, targetBuildingIds = {}})
check("Rv3 updateRun sætter lister", ADRuns.getRun(b4, flow.id).sourceBuildingIds[1], "shed")
check("Rv3 updateRun tømmer", #ADRuns.getRun(b4, flow.id).targetBuildingIds, 0)
local rt3 = ADRuns.fromRecords(ADRuns.toRecords(b3))
check("Rv3 roundtrip kilder", table.concat(ADRuns.getRun(rt3, flow.id).sourceBuildingIds, ","), "barn1,barn2")
check("Rv3 roundtrip mål", ADRuns.getRun(rt3, flow.id).targetBuildingIds[1], "mill")
check("Rv3 XML_VERSION", ADRuns.XML_VERSION, 3)
local v2 = ADRuns.fromRecords({{id = 3, name = "V2", kind = "outbound", sources = "12694", products = "SUGAR:sell", vehicleIds = {}}})
check("Rv3 v2 rå markør bevares", ADRuns.getRun(v2, 3).sourceWayPointIds[1], 12694)
check("Rv3 v2 ingen bygninger", #ADRuns.getRun(v2, 3).sourceBuildingIds, 0)
do
local written = {}
local xml = {
setInt = function(_, k, v) written[k] = v end, setString = function(_, k, v) written[k] = v end,
getInt = function(_, k) return written[k] end, getString = function(_, k) return written[k] end,
iterate = function(_, prefix, fn)
local i = 0
while written[string.format("%s(%d)#id", prefix, i)] ~= nil do fn(i, string.format("%s(%d)", prefix, i)); i = i + 1 end
end,
}
ADRunsStorage.writeRecords(xml, ADRuns.toRecords(b3))
check("Rv3 xml version 3", written["ADSmartPickupRuns#version"], 3)
check("Rv3 xml sourceBuildings", written["ADSmartPickupRuns.run(0)#sourceBuildings"], "barn1 barn2")
check("Rv3 xml targetBuildings", written["ADSmartPickupRuns.run(0)#targetBuildings"], "mill")
local back = ADRuns.fromRecords(ADRunsStorage.readRecords(xml))
check("Rv3 xml læs kilder", table.concat(ADRuns.getRun(back, flow.id).sourceBuildingIds, ","), "barn1,barn2")
end
-- RN: køretøjsnavne efter forløbet — "Navn 1", "Navn 2" … i medlemsrækkefølge; ét køretøj = bare navnet
do
local store, runA = ADRuns.createRun(ADRuns.newStore(), "Milk Collect", "outbound")
store = ADRuns.assignVehicle(store, runA.id, "v1")
store = ADRuns.assignVehicle(store, runA.id, "v2")
local runB
store, runB = ADRuns.createRun(store, "Milk Salg", "outbound")
store = ADRuns.assignVehicle(store, runB.id, "v3")
local names = ADRuns.vehicleNames(store)
check("RN to i samme forløb: 1", names.v1, "Milk Collect 1")
check("RN to i samme forløb: 2", names.v2, "Milk Collect 2")
check("RN alene i forløb = navnet", names.v3, "Milk Salg")
check("RN ukendt køretøj", names.v9, nil)
end
-- RS: tvunget lagermål — storeBuildingIds (toggle, kopi, records roundtrip)
do
local store, run = ADRuns.createRun(ADRuns.newStore(), "Slurry", "outbound")
check("RS tom som standard", #run.storeBuildingIds, 0)
store = ADRuns.toggleStoreBuilding(store, run.id, "BGA500kw")
store = ADRuns.toggleStoreBuilding(store, run.id, "tank2")
check("RS to valgt", table.concat(ADRuns.getRun(store, run.id).storeBuildingIds, ","), "BGA500kw,tank2")
store = ADRuns.toggleStoreBuilding(store, run.id, "tank2")
check("RS fravalgt igen", table.concat(ADRuns.getRun(store, run.id).storeBuildingIds, ","), "BGA500kw")
local records = ADRuns.toRecords(store)
check("RS record", records[1].storeBuildings, "BGA500kw")
local back = ADRuns.fromRecords(records)
check("RS roundtrip", table.concat(ADRuns.getRun(back, run.id).storeBuildingIds, ","), "BGA500kw")
end
-- flow-vogne: stoppet + husket vogn pr. traktor følger med records frem og tilbage
local wagonStore = ADRuns.newStore()
local ws, wrun = ADRuns.createRun(wagonStore, "Fodring", "supply")
wrun.stopped, wrun.wagons = true, {vehA = "wagonA"}
local wrecords = ADRuns.toRecords(ws)
check("vogne: record stoppet", wrecords[1].stopped, 1)
check("vogne: record vogne", wrecords[1].wagons, "vehA=wagonA")
local wback = ADRuns.fromRecords(wrecords).runs[1]
check("vogne: stoppet tilbage", wback.stopped, true)
check("vogne: vogn tilbage", wback.wagons.vehA, "wagonA")
local wupdated = ADRuns.getRun(ADRuns.updateRun(ws, wrun.id, {name = "Fodring 2"}), wrun.id)
check("vogne: overlever updateRun", wupdated.wagons.vehA, "wagonA")
check("vogne: stoppet overlever updateRun", wupdated.stopped, true)
-- R4: flyttes en traktor til et andet flow (eller fjernes), glemmes dens vogn i det gamle flow
local ps = ADRuns.newStore()
local ps1, prunA = ADRuns.createRun(ps, "A", "supply")
local ps2, prunB = ADRuns.createRun(ps1, "B", "supply")
ps2 = ADRuns.assignVehicle(ps2, prunA.id, "trac")
ADRuns.getRun(ps2, prunA.id).wagons = {trac = "wag"}
local ps3 = ADRuns.assignVehicle(ps2, prunB.id, "trac")
check("R4 gammel flow glemmer vognen", ADRuns.getRun(ps3, prunA.id).wagons.trac, nil)
local ps4 = ADRuns.unassignVehicle(ps2, "trac")
check("R4 fjernet traktor: vogn glemt", ADRuns.getRun(ps4, prunA.id).wagons.trac, nil)
-- oprydning: traktorer der ikke findes længere (solgt) fjernes fra deres flow — men ikke hvis ALLE mangler
do
local st, r1 = ADRuns.createRun(ADRuns.newStore(), "A", "supply")
st = ADRuns.assignVehicle(st, r1.id, "keep")
st = ADRuns.assignVehicle(st, r1.id, "gone")
local exists = function(id) return id == "keep" end
local pruned, removed = ADRuns.pruneMissingVehicles(st, exists)
check("prune: fjernet", #removed, 1)
check("prune: den rigtige", removed[1].vehicleId, "gone")
check("prune: flowets navn med", removed[1].runName, "A")
check("prune: tilbage", #ADRuns.getRun(pruned, r1.id).vehicleIds, 1)
check("prune: original urørt", #ADRuns.getRun(st, r1.id).vehicleIds, 2)
local _, none = ADRuns.pruneMissingVehicles(st, function() return false end)
check("prune: alle mangler -> intet fjernes", #none, 0)
end
-- beskeder på skærmen efter oprydning: én pr. flow; rød når flowet står uden traktorer
do
local st, r1 = ADRuns.createRun(ADRuns.newStore(), "TMR", "supply")
local st2, r2 = ADRuns.createRun(st, "Solo", "supply")
st2 = ADRuns.assignVehicle(st2, r1.id, "keep")
st2 = ADRuns.assignVehicle(st2, r1.id, "gone1")
st2 = ADRuns.assignVehicle(st2, r2.id, "gone2")
local pruned, removed = ADRuns.pruneMissingVehicles(st2, function(id) return id == "keep" end)
local notes = ADRuns.pruneNotices(pruned, removed)
check("besked: én pr. flow", #notes, 2)
check("besked: TMR har traktorer tilbage", notes[1].runName .. "/" .. notes[1].left .. "/" .. tostring(notes[1].critical), "TMR/1/false")
check("besked: Solo står tom -> rød", notes[2].runName .. "/" .. notes[2].left .. "/" .. tostring(notes[2].critical), "Solo/0/true")
end
-- Start alle: et flow på pause (Stop alt) skal have pausen ophævet, også når dets traktorer allerede kører AD
-- (in-game 29/9: 5 flows blev talt som 'kørte allerede', forblev stoppede, og traktorerne kørte vognen hjem igen)
check("start alle: kører ikke -> i kø", ADRuns.startAllAction(false, true), "queue")
check("start alle: kører ikke, ikke stoppet -> i kø", ADRuns.startAllAction(false, false), "queue")
check("start alle: kører men stoppet -> ophæv pause", ADRuns.startAllAction(true, true), "resume")
check("start alle: kører og ikke stoppet -> intet", ADRuns.startAllAction(true, false), "skip")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-907
View File
@@ -1,907 +0,0 @@
-- Kør: luajit tests/test_adRunsController.lua (fra repo-roden)
-- Controlleren oversætter et run til AutoDrive-tilstand pr. traktor. Mock'er AD + de
-- få ADSmartPickup-opslag den bruger.
local STRAW, TMR, WATER, MILK = 31, 116, 8, 30
local FILL_TYPE_NAMES = {[STRAW] = "STRAW", [TMR] = "FORAGE", [WATER] = "WATER", [MILK] = "MILK"}
local FILL_TYPE_INDEX = {}
for index, name in pairs(FILL_TYPE_NAMES) do FILL_TYPE_INDEX[name] = index end
g_fillTypeManager = {
getFillTypeNameByIndex = function(_, index) return FILL_TYPE_NAMES[index] end,
getFillTypeIndexByName = function(_, name) return FILL_TYPE_INDEX[name] end,
getFillTypes = function()
return {
{index = STRAW, name = "STRAW", title = "Halm"},
{index = TMR, name = "FORAGE", title = "TMR"},
{index = WATER, name = "WATER", title = "Vand"},
{index = MILK, name = "MILK", title = "Mælk"},
}
end,
}
local logLines = {}
Logging = {
info = function(fmt, ...) table.insert(logLines, string.format(fmt, ...)) end,
warning = function(fmt, ...) table.insert(logLines, "WARN " .. string.format(fmt, ...)) end,
}
Utils = {prependedFunction = function(original, extra) return function(...) extra(...); return original(...) end end}
ItemSystem = {save = function() end}
g_currentMission = {getFarmId = function() return 1 end, missionInfo = {}}
-- ---- AutoDrive-mock ----
local RT_NONE, RT_ONLYPICKUP = 1, 2
local MODE_PICKUPANDDELIVER = 2
local vehicles, markers, wayPoints = {}, {}, {}
FS25_AutoDrive = {
AutoDrive = {
RT_NONE = RT_NONE, RT_ONLYPICKUP = RT_ONLYPICKUP, MODE_PICKUPANDDELIVER = MODE_PICKUPANDDELIVER,
settings = {
useFolders = {values = {false, true}, current = 1, isVehicleSpecific = false},
rotateTargets = {values = {1, 2, 3, 4}, current = 1, isVehicleSpecific = true},
ALUnload = {values = {0, 1, 2, 3, 4}, current = 1, isVehicleSpecific = true},
},
getAllVehicles = function() return vehicles end,
getAllUnits = function(vehicle) return vehicle.trailers end,
getAllFillLevels = function(trailers) return trailers.level or 0, trailers.capacity or 0, false, 0 end,
getSupportedFillTypesOfAllUnitsAlphabetically = function(vehicle) return vehicle.carriable or {} end,
},
ADGraphManager = {
getMapMarkers = function() return markers end,
getMapMarkerByWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id then return m end end end,
getWayPointById = function(_, id) return wayPoints[id] end,
getDistanceFromNetwork = function(_, vehicle) return vehicle.networkDistance or 0 end,
},
}
local function addMarker(id, name, group, x, z)
wayPoints[id] = {x = x, z = z, incoming = {}}
table.insert(markers, {id = id, markerIndex = #markers + 1, name = name, group = group})
end
-- ADSmartPickup-opslag: hvilke waypoints har hvilken slags station, og hvad målet tager
local unloadAt, loadAt = {}, {}
ADSmartPickup = {
getFarmId = function(vehicle) return vehicle.farmId end,
getUnloadStationAtWayPointId = function(_, id) return unloadAt[id] end,
getLoadStationAtWayPointId = function(_, id) return loadAt[id] end,
getStationLevelAndCapacity = function(station, fillType)
if station.accepts[fillType] == nil then return nil end
return (station.levels or {})[fillType] or 0, station.accepts[fillType]
end,
-- forenklet rangering: mest fri plads (relativt) først; fulde sidst
getTargetNeedsRanked = function(station, fillTypes)
local ranked = {}
for _, fillType in ipairs(fillTypes) do
local capacity = station.accepts[fillType]
if capacity ~= nil then
local level = (station.levels or {})[fillType] or 0
table.insert(ranked, {fillType = fillType, level = level, capacity = capacity, freeSpace = capacity - level, score = (capacity - level) / capacity})
end
end
table.sort(ranked, function(a, b) return a.score > b.score end)
return ranked
end,
}
ADSupplyPlanner = {isWaitMarker = function(marker) return string.find(string.lower(marker.group or ""), "%f[%a]wait%f[%A]") ~= nil end}
local function makeVehicle(uniqueId, name, carriable, capacity, x, z)
local stateModule = {selectedFillTypes = {}, fillType = 0, active = false, loopCounter = 5, loopsDone = 3, dirtyCount = 0}
stateModule.setMode = function(self, mode) self.mode = mode end
stateModule.setFirstMarkerByWayPointId = function(self, id) self.firstMarkerId = id end
stateModule.setSecondMarkerByWayPointId = function(self, id) self.secondMarkerId = id end
stateModule.setFillType = function(self, fillType) self.fillType = fillType end
stateModule.raiseDirtyFlag = function(self) self.dirtyCount = self.dirtyCount + 1 end
stateModule.setLoopsDone = function(self, n) self.loopsDone = n end
stateModule.isActive = function(self) return self.active end
-- Bruges af C13: getTargetFillTypes skal følge AD's LIVE mål (mock'er AD's eget getSecondMarker).
stateModule.getSecondMarker = function(self) return self.secondMarkerId ~= nil and {id = self.secondMarkerId} or nil end
local vehicle = {
uniqueId = uniqueId, farmId = 1, rootNode = uniqueId,
carriable = carriable, trailers = {level = 0, capacity = capacity},
getName = function() return name end,
getOwnerFarmId = function() return 1 end,
startAutoDrive = function(self) self.ad.stateModule.active = true; self.started = (self.started or 0) + 1 end,
getClosestWayPoint = function() return nil, 0 end,
stopAutoDrive = function(self) self.ad.stateModule.active = false; self.stopped = (self.stopped or 0) + 1 end,
ad = {stateModule = stateModule, settings = {rotateTargets = {values = {1, 2, 3, 4}, current = 1, new = 1, isVehicleSpecific = true}, ALUnload = {values = {0, 1, 2, 3, 4}, current = 1, new = 1, isVehicleSpecific = true}}},
}
nodePositions = nodePositions or {}
nodePositions[uniqueId] = {x = x or 0, z = z or 0}
table.insert(vehicles, vehicle)
return vehicle
end
function getWorldTranslation(node) local p = nodePositions[node]; return p.x, 0, p.z end
dofile("FS25_ADSmartPickup/adRuns.lua")
dofile("FS25_ADSmartPickup/adRunsStorage.lua")
dofile("FS25_ADSmartPickup/adWaitPool.lua")
dofile("FS25_ADSmartPickup/adRunsController.lua")
local adEnv = FS25_AutoDrive
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
local function buildWorld()
vehicles, markers, wayPoints, unloadAt, loadAt, logLines = {}, {}, {}, {}, {}, {}
ADRunsController.store = ADRuns.newStore()
addMarker(94, "Cow 1 Food", "Cow Food", 5000, 0); unloadAt[94] = {accepts = {[STRAW] = 300000, [TMR] = 300000, [WATER] = 50000}}
addMarker(44, "10 Silo Storage", "Siloer", 0, 0); loadAt[44] = {}
addMarker(75, "TMR Load", "Fabrication", 900, 0); loadAt[75] = {}
addMarker(100, "Cow Wait", "Cow Wait", 5100, 0)
addMarker(101, "Cow Wait", "Cow Wait", 5200, 0)
addMarker(200, "F91 Wait", "F91", 8000, 0)
addMarker(300, "Pig Wait", "Pig Wait", 9000, 0)
end
-- C1: køretøjs-id og farmens AD-køretøjer
buildWorld()
local fastracA = makeVehicle("vehicleA", "Fastrac A", {STRAW, TMR}, 1000000, 100, 0)
local fastracB = makeVehicle("vehicleB", "Fastrac B", {STRAW}, 700000, 4000, 0)
local noAd = {uniqueId = "vehicleC", getOwnerFarmId = function() return 1 end}
table.insert(vehicles, noAd)
local otherFarm = makeVehicle("vehicleD", "Nabo", {STRAW}, 1000, 0, 0); otherFarm.getOwnerFarmId = function() return 2 end
check("C1 uniqueId", ADRunsController.getVehicleId(fastracA), "vehicleA")
check("C1 fallback savegame-id", ADRunsController.getVehicleId({currentSavegameId = 7}), "7")
check("C1 intet id -> nil", ADRunsController.getVehicleId({}), nil)
check("C1 farmens AD-køretøjer", #ADRunsController.getFarmVehicles(adEnv), 2)
check("C1 opslag på id", ADRunsController.findVehicleById(adEnv, "vehicleB"), fastracB)
check("C1 ukendt id -> nil", ADRunsController.findVehicleById(adEnv, "nope"), nil)
-- C2: opret run, tildel; opslag run for køretøj; flyt mellem runs
local cow = ADRunsController.createRun("Cow Feed")
check("C2 run oprettet", ADRuns.getRun(ADRunsController.store, cow.id).name, "Cow Feed")
check("C2 ingen tidligere", ADRunsController.assignVehicle(cow.id, fastracA), nil)
check("C2 run for køretøj", ADRunsController.getRunForVehicle(fastracA).id, cow.id)
check("C2 ikke tildelt -> nil", ADRunsController.getRunForVehicle(fastracB), nil)
local pig = ADRunsController.createRun("Pig Feed")
check("C2 flyttet fra Cow", ADRunsController.assignVehicle(pig.id, fastracA), cow.id)
check("C2 nu i Pig", ADRunsController.getRunForVehicle(fastracA).id, pig.id)
ADRunsController.unassignVehicle(fastracA)
check("C2 fjernet", ADRunsController.getRunForVehicle(fastracA), nil)
ADRunsController.deleteRun(pig.id)
check("C2 slettet", ADRuns.getRun(ADRunsController.store, pig.id), nil)
-- C3: Auto-vareliste = vognens varer ∩ målets varer; manual = navne -> indeks (ukendte droppes)
ADRunsController.updateRun(cow.id, {targetWayPointId = 94})
local cowRun = ADRuns.getRun(ADRunsController.store, cow.id)
check("C3 auto: halm+TMR (vand kan vognen ikke bære)", table.concat(ADRunsController.getRunIngredients(adEnv, cowRun, fastracA), ","), STRAW .. "," .. TMR)
check("C3 auto: kun halm for halm-vogn", table.concat(ADRunsController.getRunIngredients(adEnv, cowRun, fastracB), ","), tostring(STRAW))
ADRunsController.updateRun(cow.id, {ingredientMode = "manual", ingredients = {"FORAGE", "UNKNOWN_THING"}})
cowRun = ADRuns.getRun(ADRunsController.store, cow.id)
check("C3 manual: FORAGE -> indeks, ukendt droppes", table.concat(ADRunsController.getRunIngredients(adEnv, cowRun, fastracA), ","), tostring(TMR))
ADRunsController.updateRun(cow.id, {ingredientMode = "auto"})
local noTarget = ADRunsController.createRun("Uden mål")
check("C3 uden mål -> tom liste", #ADRunsController.getRunIngredients(adEnv, ADRuns.getRun(ADRunsController.store, noTarget.id), fastracA), 0)
-- C4: validering
cowRun = ADRuns.getRun(ADRunsController.store, cow.id)
check("C4 ok", ADRunsController.validateVehicle(adEnv, cowRun, fastracA).ok, true)
check("C4 kapacitet", ADRunsController.validateVehicle(adEnv, cowRun, fastracA).capacity, 1000000)
local noTrailer = makeVehicle("vehicleE", "Uden vogn", {}, 0)
check("C4 ingen vogn", ADRunsController.validateVehicle(adEnv, cowRun, noTrailer).reason, "noTrailer")
local milkTruck = makeVehicle("vehicleF", "Mælkebil", {MILK}, 20000)
check("C4 kan ikke bære", ADRunsController.validateVehicle(adEnv, cowRun, milkTruck).reason, "cannotCarry")
fastracB.networkDistance = 31
check("C4 for langt fra vejnettet", ADRunsController.validateVehicle(adEnv, cowRun, fastracB).reason, "farFromNetwork")
fastracB.networkDistance = 0
check("C4 intet mål", ADRunsController.validateVehicle(adEnv, ADRuns.getRun(ADRunsController.store, noTarget.id), fastracA).reason, "noTarget")
ADRunsController.updateRun(noTarget.id, {targetWayPointId = 4242})
check("C4 mål-markør findes ikke", ADRunsController.validateVehicle(adEnv, ADRuns.getRun(ADRunsController.store, noTarget.id), fastracA).reason, "noTarget")
-- C5: Start skriver AD-tilstand og starter; ✖-traktorer springes over
ADRunsController.updateRun(cow.id, {loops = 4, waitPoolGroup = "Cow Wait"})
ADRunsController.assignVehicle(cow.id, fastracA)
ADRunsController.assignVehicle(cow.id, noTrailer)
local results = ADRunsController.startRun(adEnv, cow.id)
check("C5 to resultater", #results, 2)
check("C5 A startet", fastracA.started, 1)
check("C5 A ok", results[1].ok, true)
check("C5 uden vogn sprunget over", noTrailer.started, nil)
check("C5 uden vogn grund", results[2].reason, "noTrailer")
local state = fastracA.ad.stateModule
check("C5 mode P&D", state.mode, MODE_PICKUPANDDELIVER)
check("C5 mål = run'ets mål", state.secondMarkerId, 94)
check("C5 start-pickup = nærmeste læssestation", state.firstMarkerId, 44)
check("C5 varer = auto-listen", table.concat(state.selectedFillTypes, ","), STRAW .. "," .. TMR)
check("C5 fillType = første", state.fillType, STRAW)
check("C5 loops", state.loopCounter, 4)
check("C5 loopsDone nulstillet", state.loopsDone, 0)
check("C5 rotateTargets = kun pålæsning (current)", fastracA.ad.settings.rotateTargets.current, 2)
check("C5 rotateTargets = kun pålæsning (new)", fastracA.ad.settings.rotateTargets.new, 2)
check("C5 useFolders globalt til", adEnv.AutoDrive.settings.useFolders.current, 2)
check("C5 dirty flag", state.dirtyCount > 0, true)
check("C5 status", ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, cow.id)).running, 1)
check("C5 total", ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, cow.id)).total, 2)
check("C5 ukendt run -> tom", #ADRunsController.startRun(adEnv, 999), 0)
-- C6: Stop
check("C6 stoppede", ADRunsController.stopRun(adEnv, cow.id), 1)
check("C6 A stoppet", fastracA.stopped, 1)
check("C6 status 0", ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, cow.id)).running, 0)
check("C6 medlemskab bevaret", ADRunsController.getRunForVehicle(fastracA).id, cow.id)
-- C7: Start på traktor der allerede kører AD (fx startet manuelt) -> stoppes og startes på run'et
fastracA.ad.stateModule.active = true
fastracA.stopped = 0
ADRunsController.startRun(adEnv, cow.id)
check("C7 kørende traktor stoppes først", fastracA.stopped, 1)
check("C7 og startes på run'et", fastracA.started, 2)
check("C7 run markeret som kørende", ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, cow.id)).isRunning, true)
ADRunsController.stopRun(adEnv, cow.id)
check("C7 stop rydder markeringen", ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, cow.id)).isRunning, false)
-- en traktor der kører AD uden at run'et er startet af os, gør IKKE run'et "kørende"
fastracA.ad.stateModule.active = true
check("C7 manuel AD tæller ikke som kørende run", ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, cow.id)).isRunning, false)
-- efter genindlæsning: runningRuns er tom, men flowet er ikke stoppet og traktoren kører -> kørende (Stop-knap)
-- (in-game 29/9: Flows-listen viste [Start] ved flows der kørte)
ADRuns.getRun(ADRunsController.store, cow.id).stopped = false
ADRunsController.runningRuns[cow.id] = nil
check("C7b efter genindlæsning: kørende", ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, cow.id)).isRunning, true)
check("C7b efter genindlæsning: kan stoppes", ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, cow.id)).canStop, true)
ADRuns.getRun(ADRunsController.store, cow.id).stopped = true
check("C7 men aktive tæller stadig", ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, cow.id)).running, 1)
fastracA.ad.stateModule.active = false
-- sprungne traktorer logges med årsag
logLines = {}
ADRunsController.startRun(adEnv, cow.id)
local skippedLogged = false
for _, line in ipairs(logLines) do if line:find("sprunget over", 1, true) and line:find("noTrailer", 1, true) then skippedLogged = true end end
check("C7 sprunget over logges med årsag", skippedLogged, true)
-- kapacitet under 100 l (kun trækkerens egne tanke) = ingen vogn
local tinyCapacity = makeVehicle("vehicleTiny", "T255", {STRAW, TMR}, 1, 100, 0)
check("C7 1 l kapacitet -> noTrailer", ADRunsController.validateVehicle(adEnv, ADRuns.getRun(ADRunsController.store, cow.id), tinyCapacity).reason, "noTrailer")
-- UAL-køretøj: AutoDrive melder kapacitet i PALLER (læssede + 1), så en tom pallebil har "1" — det er ikke "ingen vogn"
adEnv.AutoDrive.hasAL = function(_, unit) return unit.ual == true end
local palletTruck = makeVehicle("vehicleDragon", "Dragon", {}, 1, 100, 0)
palletTruck.trailers[1] = {ual = true}
local truckValidation = ADRunsController.validateVehicle(adEnv, ADRuns.getRun(ADRunsController.store, cow.id), palletTruck)
check("C7 pallebil: ikke noTrailer", truckValidation.reason ~= "noTrailer", true)
check("C7 pallebil: kapacitet i paller markeret", truckValidation.isAutoload, true)
check("C7 pallebil: liter-kapacitet ukendt", truckValidation.capacity, nil)
-- in-game 27/9: en tipvogn med UniversalAutoload slået til tæller hos AutoDrive kun som palle-/ballevogn,
-- så en forsynings-flow med løsvarer afviste den som "kan ikke bære" uden at sige hvorfor
check("C7 AutoLoad slået til på vogn i forsyning -> autoloadOn", truckValidation.reason, "autoloadOn")
adEnv.AutoDrive.hasAL = nil
-- C8: mål-markører (kun stalde og fabrikker — siloer tager alt og er ikke mål; stalde først,
-- så fabrikker, så navn) og Wait-puljer til dropdowns
unloadAt[94].owningPlaceable = {getHusbandryFillLevel = function() return 0 end}
addMarker(96, "8 Silo Storage", "Siloer", 100, 0); unloadAt[96] = {accepts = {[STRAW] = 1}, owningPlaceable = {spec_silo = {}}}
addMarker(97, "TMR Unload", "Fabrication", 900, 0); unloadAt[97] = {accepts = {[STRAW] = 1}, owningPlaceable = {spec_productionPoint = {}}}
addMarker(98, "Bull Food", "Cow Food", 5100, 0); unloadAt[98] = {accepts = {[STRAW] = 1}, owningPlaceable = {getHusbandryFillLevel = function() return 0 end}}
local targets = ADRunsController.getTargetMarkers(adEnv, 1)
check("C8 kun stalde og fabrikker (ikke siloen)", #targets, 3)
check("C8 stalde først, alfabetisk", targets[1].name .. "," .. targets[2].name, "Bull Food,Cow 1 Food")
check("C8 fabrik sidst", targets[3].name, "TMR Unload")
local pools = ADRunsController.getWaitPools(adEnv)
check("C8 to puljer (F91 er ikke en Wait-mappe)", #pools, 2)
check("C8 sorteret", pools[1].group, "Cow Wait")
check("C8 antal", pools[1].count, 2)
-- C9: gem/indlæs; ukendte køretøjs-id'er logges ved indlæsning men beholdes i run'et
local savedRecords
ADRunsStorage.save = function(store) savedRecords = ADRuns.toRecords(store); return true end
ADRunsStorage.load = function() return ADRuns.fromRecords(savedRecords) end
ADRunsController.assignVehicle(cow.id, {uniqueId = "vehicleGONE", ad = {}, getOwnerFarmId = function() return 1 end, getName = function() return "Væk" end})
check("C9 gemt", ADRunsController.save(), true)
ADRunsController.store = ADRuns.newStore()
ADRunsController.load(adEnv)
check("C9 indlæst", ADRuns.getRun(ADRunsController.store, cow.id).name, "Cow Feed")
check("C9 ukendt køretøj logget", logLines[#logLines]:find("vehicleGONE", 1, true) ~= nil, true)
check("C9 run beholder kendte", ADRunsController.getRunForVehicle(fastracA).id, cow.id)
local hooked = false
ItemSystem.save = function() hooked = true end
ADRunsController.installSaveHook()
ItemSystem.save()
check("C9 save-hook kaldes før ItemSystem.save", hooked, true)
-- C11: vogn uden egen ad.settings-tabel (ingen rotateTargets pr. køretøj) -> den
-- køretøjs-specifikke indstilling skrives IKKE til den globale settings-tabel
adEnv.AutoDrive.settings.rotateTargets.current = 1
adEnv.AutoDrive.settings.useFolders.current = 1
local settingsRun = ADRunsController.createRun("Settings Test")
ADRunsController.updateRun(settingsRun.id, {targetWayPointId = 94, loops = 1})
local noSettings = makeVehicle("vehicleH", "Uden AD-settings", {STRAW}, 500000, 200, 0)
noSettings.ad.settings = {}
ADRunsController.assignVehicle(settingsRun.id, noSettings)
ADRunsController.startRun(adEnv, settingsRun.id)
check("C11 global rotateTargets urørt", adEnv.AutoDrive.settings.rotateTargets.current, 1)
check("C11 useFolders global stadig skrevet", adEnv.AutoDrive.settings.useFolders.current, 2)
check("C11 køretøjet startet", noSettings.started, 1)
-- C12: startAutoDrive() kaster for én traktor -> pcall'et applyAndStart fanger fejlen,
-- resultatet markeres ok=false/reason="error", og de ØVRIGE traktorer i run'et startes stadig
-- (fejlen må ikke undslippe startRun-loopet).
buildWorld()
local brokenRun = ADRunsController.createRun("Broken Start")
ADRunsController.updateRun(brokenRun.id, {targetWayPointId = 94, loops = 2})
local broken = makeVehicle("vehicleBroken", "Fejl-traktor", {STRAW, TMR}, 500000, 100, 0)
broken.startAutoDrive = function() error("boom") end
local normal = makeVehicle("vehicleNormal", "Normal-traktor", {STRAW, TMR}, 500000, 200, 0)
ADRunsController.assignVehicle(brokenRun.id, broken)
ADRunsController.assignVehicle(brokenRun.id, normal)
local c12Results = ADRunsController.startRun(adEnv, brokenRun.id)
check("C12 to resultater", #c12Results, 2)
check("C12 fejl-traktor ok=false", c12Results[1].ok, false)
check("C12 fejl-traktor reason=error", c12Results[1].reason, "error")
check("C12 fejl-traktor ikke startet", broken.started, nil)
check("C12 normal-traktor ok=true", c12Results[2].ok, true)
check("C12 normal-traktor startet", normal.started, 1)
-- C13: Auto-listen følger traktorens LIVE AD-mål (stateModule:getSecondMarker()) mens den kører,
-- IKKE run'ets gemte targetWayPointId — ellers afviger listen fra det choosePickup/findSupplyPickup
-- faktisk bruger, hvis brugeren redigerer run'ets Mål mens traktoren kører.
addMarker(95, "Alt Cow Food", "Cow Food", 5300, 0)
unloadAt[95] = {accepts = {[STRAW] = 1000}}
local liveRun = ADRunsController.createRun("Live Target")
ADRunsController.updateRun(liveRun.id, {targetWayPointId = 94})
local liveRunData = ADRuns.getRun(ADRunsController.store, liveRun.id)
local liveVehicle = makeVehicle("vehicleLive", "Live-traktor", {STRAW, TMR}, 500000, 300, 0)
check("C13 inaktiv: auto-liste fra run'ets mål (94)", table.concat(ADRunsController.getRunIngredients(adEnv, liveRunData, liveVehicle), ","), STRAW .. "," .. TMR)
liveVehicle.ad.stateModule.active = true
liveVehicle.ad.stateModule.secondMarkerId = 95
check("C13 aktiv: auto-liste fra AD's live mål (95)", table.concat(ADRunsController.getRunIngredients(adEnv, liveRunData, liveVehicle), ","), tostring(STRAW))
liveVehicle.ad.stateModule.active = false
liveVehicle.ad.stateModule.secondMarkerId = nil
-- C10: målets accepterede varer (til afkrydsning i GUI'et) + køretøjs-label
buildWorld()
local labelA = makeVehicle("vehicleA", "Fastrac A", {STRAW, TMR}, 1000000)
local labelB = makeVehicle("vehicleB", "Fastrac B", {STRAW}, 700000)
local accRun = ADRunsController.createRun("Cow Feed")
check("C10 uden mål -> tom", #ADRunsController.getTargetAcceptedFillTypes(adEnv, ADRuns.getRun(ADRunsController.store, accRun.id)), 0)
ADRunsController.updateRun(accRun.id, {targetWayPointId = 94})
local accepted = ADRunsController.getTargetAcceptedFillTypes(adEnv, ADRuns.getRun(ADRunsController.store, accRun.id))
check("C10 tre varer (halm, TMR, vand)", #accepted, 3)
check("C10 sorteret på titel", accepted[1].title .. "," .. accepted[2].title .. "," .. accepted[3].title, "Halm,TMR,Vand")
check("C10 navn med", accepted[1].name, "STRAW")
check("C10 label", ADRunsController.getVehicleLabel(adEnv, labelB), "Fastrac B (#2)")
-- C14: Start går gennem AD's mode:start(USER_PLAYER) (som HUD'en), ikke kun startAutoDrive
buildWorld()
local modeVehicle = makeVehicle("vehicleM", "Mode-traktor", {STRAW, TMR}, 1000000, 100, 0)
local modeStarts = {}
modeVehicle.ad.stateModule.getCurrentMode = function() return {start = function(_, user) table.insert(modeStarts, user); modeVehicle:startAutoDrive() end} end
modeVehicle.ad.stateModule.setActualFarmId = function(self, farmId) self.actualFarmId = farmId end
adEnv.AutoDrive.USER_PLAYER = 1
local modeRun = ADRunsController.createRun("Mode")
ADRunsController.updateRun(modeRun.id, {targetWayPointId = 94})
ADRunsController.assignVehicle(modeRun.id, modeVehicle)
ADRunsController.startRun(adEnv, modeRun.id)
check("C14 mode:start kaldt med USER_PLAYER", modeStarts[1], 1)
check("C14 traktor aktiv", modeVehicle.started, 1)
check("C14 farm-id sat", modeVehicle.ad.stateModule.actualFarmId, 1)
check("C14 loopsDone nulstillet", modeVehicle.ad.stateModule.loopsDone, 0)
-- C15: aktivitet til overblikket — stoppet / venter / med læs / tom på vej efter vare
ADUnloadWait = {getWaitFor = function(v) return v.waitInfo end, getRestLoad = function(_, v) return v.loadType, v.loadLiters or 0 end}
ADSmartPickup.getActivePickup = function(v) return v.pickupInfo end
buildWorld()
local actVehicle = makeVehicle("vehicleAct", "Akt", {STRAW, TMR}, 1000000, 100, 0)
check("C15 stoppet", ADRunsController.getVehicleActivity(adEnv, actVehicle).kind, "stopped")
actVehicle.ad.stateModule.active = true
actVehicle.ad.stateModule.secondMarkerId = 94
actVehicle.loadType, actVehicle.loadLiters = STRAW, 8000
local loaded = ADRunsController.getVehicleActivity(adEnv, actVehicle)
check("C15 med læs", loaded.kind .. "/" .. loaded.fillType .. "/" .. loaded.liters, "loaded/" .. STRAW .. "/8000")
actVehicle.loadLiters = 0
actVehicle.pickupInfo = {markerName = "Silo 3", fillType = TMR}
local empty = ADRunsController.getVehicleActivity(adEnv, actVehicle)
check("C15 tom på vej efter TMR", empty.kind .. "/" .. tostring(empty.place) .. "/" .. tostring(empty.fillType), "empty/Silo 3/" .. TMR)
actVehicle.waitInfo = {markerName = "Cow Wait", fillType = STRAW, rest = 5000}
check("C15 venter", ADRunsController.getVehicleActivity(adEnv, actVehicle).kind, "waiting")
ADUnloadWait = nil
-- C16: flere mål pr. run — Auto-varer = union; chooseTarget = den stald der mangler mest; validering
buildWorld()
addMarker(99, "Cow 2 Food", "Cow Food", 5200, 0); unloadAt[99] = {accepts = {[STRAW] = 300000, [TMR] = 300000}, levels = {[STRAW] = 0, [TMR] = 0}}
unloadAt[94].levels = {[STRAW] = 250000, [TMR] = 250000, [WATER] = 50000}
local multiVehicle = makeVehicle("vehicleMulti", "Multi", {STRAW, TMR, WATER}, 1000000, 100, 0)
local multi = ADRunsController.createRun("Kvæg")
ADRunsController.updateRun(multi.id, {targetWayPointIds = {94, 99}})
local multiRun = ADRuns.getRun(ADRunsController.store, multi.id)
check("C16 auto-varer = union (vand kun i stald 1)", table.concat(ADRunsController.getRunIngredients(adEnv, multiRun, multiVehicle), ","), WATER .. "," .. STRAW .. "," .. TMR)
check("C16 accepterede varer = union", #ADRunsController.getTargetAcceptedFillTypes(adEnv, multiRun), 3)
check("C16 vælger stald 2 (tom)", ADRunsController.chooseTarget(adEnv, multiRun, multiVehicle), 99)
unloadAt[99].levels = {[STRAW] = 290000, [TMR] = 290000}
check("C16 vælger stald 1 når stald 2 er fuld", ADRunsController.chooseTarget(adEnv, multiRun, multiVehicle), 94)
check("C16 validering ok med flere mål", ADRunsController.validateVehicle(adEnv, multiRun, multiVehicle).ok, true)
-- C16b: en anden vogn er allerede på vej til stald 2 — så vælges stald 1 selvom stald 2 er mindre fyldt
-- (drive-on-bunker: kun én kan tippe ad gangen; in-game 26/9 kørte 3 biogas-vogne til samme drop)
unloadAt[99].levels = {[STRAW] = 100000, [TMR] = 100000}
unloadAt[94].levels = {[STRAW] = 200000, [TMR] = 200000, [WATER] = 50000}
ADSmartPickup.countEnRoute = function(id, exclude) return id == 99 and 1 or 0 end
check("C16b ledigt mål foran mindre fyldt mål med vogn på vej", ADRunsController.chooseTarget(adEnv, multiRun, multiVehicle), 94)
ADSmartPickup.countEnRoute = function() return 1 end
check("C16b lige mange på vej -> mindst fyldte", ADRunsController.chooseTarget(adEnv, multiRun, multiVehicle), 99)
ADSmartPickup.countEnRoute = nil
-- C16c: fyldte traktorer startet i samme run fordeles på målene (in-game 27/9: tre fyldte biogas-traktorer
-- kørte alle til Drop 1, fordi læs der allerede lå på vognen aldrig blev registreret "på vej")
do
unloadAt[99].levels = {[STRAW] = 100000, [TMR] = 100000}
unloadAt[94].levels = {[STRAW] = 200000, [TMR] = 200000, [WATER] = 50000}
local onWay = {}
ADSmartPickup.countEnRoute = function(id, exclude)
local n = 0
for v, target in pairs(onWay) do if v ~= exclude and target == id then n = n + 1 end end
return n
end
ADSmartPickup.registerCargo = function(_, v, targetId) onWay[v] = targetId; return true end
local loadedA = makeVehicle("vehicleLoadedA", "Fyldt A", {STRAW, TMR}, 1000000, 100, 0)
local loadedB = makeVehicle("vehicleLoadedB", "Fyldt B", {STRAW, TMR}, 1000000, 110, 0)
local both = ADRunsController.createRun("To fyldte")
ADRunsController.updateRun(both.id, {targetWayPointIds = {94, 99}})
ADRunsController.assignVehicle(both.id, loadedA)
ADRunsController.assignVehicle(both.id, loadedB)
ADRunsController.startRun(adEnv, both.id)
local a, b = loadedA.ad.stateModule.secondMarkerId, loadedB.ad.stateModule.secondMarkerId
check("C16c første fyldte -> mindst fyldte mål", a, 99)
check("C16c anden fyldte -> det andet mål", b, 94)
ADSmartPickup.countEnRoute, ADSmartPickup.registerCargo = nil, nil
end
ADRunsController.updateRun(multi.id, {targetWayPointIds = {4242}})
check("C16 kun ukendte mål -> noTarget", ADRunsController.validateVehicle(adEnv, ADRuns.getRun(ADRunsController.store, multi.id), multiVehicle).reason, "noTarget")
-- C17: kørende traktor der fjernes fra run stoppes; slettes run, stoppes dets traktorer; Stop logger
buildWorld()
local runVehicle = makeVehicle("vehicleR", "Kørende", {STRAW, TMR}, 1000000, 100, 0)
local stopRun = ADRunsController.createRun("Stop-test")
ADRunsController.updateRun(stopRun.id, {targetWayPointId = 94})
ADRunsController.assignVehicle(stopRun.id, runVehicle)
ADRunsController.startRun(adEnv, stopRun.id)
check("C17 kører", runVehicle.started, 1)
ADRunsController.unassignVehicle(runVehicle)
check("C17 fjernet fra run -> stoppet", runVehicle.stopped, 1)
ADRunsController.assignVehicle(stopRun.id, runVehicle)
ADRunsController.startRun(adEnv, stopRun.id)
ADRunsController.deleteRun(stopRun.id, adEnv)
check("C17 run slettet -> stoppet", runVehicle.stopped, 2)
check("C17 stop logget", logLines[#logLines]:find("stoppet", 1, true) ~= nil, true)
-- C18: forkert last ved Start (halm på vognen, run mod sukkerfabrik) -> første stop = silo der tager halm
ADUnloadWait = {getRestLoad = function(_, v) return v.loadType, v.loadLiters or 0 end, getWaitFor = function() return nil end,
hasRouteBothWays = function() return true end}
ADLoadSwap = {findDumpMarker = function(_, v, fillType, rest, homeId, isReachable) return {id = 44, name = "10 Silo Storage", markerIndex = 2} end}
buildWorld()
addMarker(77, "sugar offload", "Fabrication", 950, 0); unloadAt[77] = {accepts = {[TMR] = 360000}} -- "fabrik" der ikke tager halm (TMR står for roer her)
local sugarVehicle = makeVehicle("vehicleSugar", "Roe-traktor", {STRAW, TMR}, 20000, 100, 0)
sugarVehicle.loadType, sugarVehicle.loadLiters = STRAW, 8000
local sugar = ADRunsController.createRun("Sugar")
ADRunsController.updateRun(sugar.id, {targetWayPointId = 77})
ADRunsController.assignVehicle(sugar.id, sugarVehicle)
ADRunsController.startRun(adEnv, sugar.id)
check("C18 første stop = siloen (aflæs forkert last)", sugarVehicle.ad.stateModule.secondMarkerId, 44)
check("C18 logget", logLines[#logLines]:find("læsser", 1, true) ~= nil or logLines[#logLines - 1]:find("læsser", 1, true) ~= nil, true)
-- rigtig last (TMR=roer) -> direkte til fabrikken
sugarVehicle.loadType = TMR
sugarVehicle.ad.stateModule.active = false
ADRunsController.startRun(adEnv, sugar.id)
check("C18 rigtig last -> målet", sugarVehicle.ad.stateModule.secondMarkerId, 77)
-- C18b: målet TAGER varen, men den er ikke en af run'ets valgte varer (gødning i en ensilage-DOB)
-- -> aflæs i en silo der tager den, ikke i målet
unloadAt[77].accepts = {[TMR] = 360000, [STRAW] = 360000}
ADRunsController.updateRun(sugar.id, {ingredientMode = "manual", ingredients = {"FORAGE"}})
sugarVehicle.loadType = STRAW
sugarVehicle.ad.stateModule.active = false
ADRunsController.startRun(adEnv, sugar.id)
check("C18b forkert vare for run'et -> silo, ikke målet", sugarVehicle.ad.stateModule.secondMarkerId, 44)
-- auto-varer: alt målet tager hører til -> målet
ADRunsController.updateRun(sugar.id, {ingredientMode = "auto", ingredients = {}})
sugarVehicle.ad.stateModule.active = false
ADRunsController.startRun(adEnv, sugar.id)
check("C18b auto: målet tager varen -> målet", sugarVehicle.ad.stateModule.secondMarkerId, 77)
ADUnloadWait = nil; ADLoadSwap = nil
-- C19: Wait-markører er aldrig mål — heller ikke når de står inden for 40 m af staldens trigger
buildWorld()
-- staldens station med bygning; Wait-markøren ligger i triggerens rækkevidde og "mangler mere" end 94
local barnPlaceable = {getHusbandryFillLevel = function() return 0 end}
unloadAt[94].owningPlaceable = barnPlaceable
addMarker(400, "Cow Wait", "Cow Wait", 5005, 0); unloadAt[400] = {accepts = {[STRAW] = 300000, [TMR] = 300000}, levels = {}, owningPlaceable = barnPlaceable}
local targetNames = {}
for _, marker in ipairs(ADRunsController.getTargetMarkers(adEnv, 1)) do table.insert(targetNames, marker.name) end
check("C19 Wait-markør ikke i mål-listen", table.concat(targetNames, ","):find("Cow Wait", 1, true), nil)
local waitTargetRun = ADRunsController.createRun("Wait som mål")
ADRunsController.updateRun(waitTargetRun.id, {targetWayPointIds = {94, 400}})
local waitVehicle = makeVehicle("vehicleW", "W", {STRAW, TMR}, 20000, 5000, 0)
check("C19 chooseTarget springer Wait over", ADRunsController.chooseTarget(adEnv, ADRuns.getRun(ADRunsController.store, waitTargetRun.id), waitVehicle), 94)
-- C-out: udkørsels-runs — oprettelse, kilder, produkter, validering, start, status
local BASKET = 7
FILL_TYPE_NAMES[BASKET] = "STRAW_BASKET"; FILL_TYPE_INDEX["STRAW_BASKET"] = BASKET
local sourcesAt, productsAt = {}, {}
ADSources = {
identify = function(_, id) return sourcesAt[id] end,
getProducts = function(_, source) return productsAt[source.wayPointId] or {} end,
getSellMarkers = function() return {} end,
stationAccepts = function() return false end,
getSourcePlaceables = function() return {} end,
}
local outboundStatus = {reasons = {"SUGAR 78 % af top (kræver 90), lager 41 %"}, warning = nil}
local holdFor = {}
ADOutbound = {
isOutboundVehicle = function(v) local r = ADRunsController.getRunForVehicle(v) return r ~= nil and r.kind == "outbound" end,
getRunStatus = function() return outboundStatus end,
getHoldFor = function(v) return holdFor[v] end,
getTripFor = function() return nil end,
releaseInactive = function() end,
}
ADUnloadWait = {getRestLoad = function() return nil, 0 end, getWaitFor = function() return nil end}
buildWorld()
addMarker(1, "Suger Load", "Pickup points", 100, 0); loadAt[1] = {}
addMarker(3, "Objektlager", "Pickup points", 300, 0)
addMarker(10, "selling station", "selling", 500, 0)
sourcesAt[1] = {kind = "factory", wayPointId = 1, station = loadAt[1], name = "Sugar mill"}
sourcesAt[3] = {kind = "objectStorage", wayPointId = 3, name = "Objektlager"}
productsAt[1] = {{fillType = TMR, name = "FORAGE", title = "TMR", available = 150000, level = 150000, capacity = 180000, isPallet = false}}
productsAt[3] = {{fillType = BASKET, name = "STRAW_BASKET", title = "Halmkurve", available = 8000, level = 8, capacity = 4000, isPallet = true}}
local bulkTractor = makeVehicle("vehicleBulk", "MT635", {STRAW, TMR}, 20000, 100, 0)
local ualTractor = makeVehicle("vehicleUal", "Fendt", {}, 8000, 300, 0)
ualTractor.trailers[1] = {ual = true}
adEnv.AutoDrive.hasAL = function(_, trailer) return trailer.ual == true end
local out = ADRunsController.createRun("Sugar out", "outbound")
check("Cout1 kind", ADRuns.getRun(ADRunsController.store, out.id).kind, "outbound")
check("Cout1 supply-run har kind supply", ADRunsController.createRun("Cows").kind, "supply")
check("Cout1 kilde-markører", #ADRunsController.getSourceMarkers(adEnv, 1), 2)
check("Cout1 første kilde-markør", ADRunsController.getSourceMarkers(adEnv, 1)[1].marker.name, "Objektlager")
-- Cout2: validering
local outRun = ADRuns.getRun(ADRunsController.store, out.id)
-- diagnose: hvad moden ser på enhederne, når ingen autoload findes (logges én gang pr. traktor,
-- første gang varerne slås op — dvs. allerede ved noProducts-tjekket nedenfor)
logLines = {}
bulkTractor.trailers[1] = {spec_universalAutoload = {isAutoloadAvailable = false, autoloadDisabled = true}, getName = function() return "Vogn" end}
check("Cout2 ingen kilder -> noSource", ADRunsController.validateVehicle(adEnv, outRun, bulkTractor).reason, "noSource")
ADRunsController.store = ADRuns.toggleSource(ADRunsController.store, out.id, 1)
ADRunsController.store = ADRuns.toggleSource(ADRunsController.store, out.id, 3)
outRun = ADRuns.getRun(ADRunsController.store, out.id)
check("Cout2 ingen vare valgt -> noProducts", ADRunsController.validateVehicle(adEnv, outRun, bulkTractor).reason, "noProducts")
ADRunsController.store = ADRuns.setProductPolicy(ADRunsController.store, out.id, "STRAW_BASKET", "sell")
outRun = ADRuns.getRun(ADRunsController.store, out.id)
check("Cout2 kun paller uden UAL -> needsAutoload", ADRunsController.validateVehicle(adEnv, outRun, bulkTractor).reason, "needsAutoload")
local diag = nil
for _, line in ipairs(logLines) do if line:find("ingen autoload", 1, true) then diag = line end end
check("Cout2 diagnose logges", diag ~= nil, true)
check("Cout2 diagnose viser UAL-flag", diag ~= nil and diag:find("isAutoloadAvailable=false", 1, true) ~= nil, true)
logLines = {}
ADRunsController.validateVehicle(adEnv, outRun, bulkTractor)
check("Cout2 diagnose kun én gang", #logLines, 0)
bulkTractor.trailers[1] = nil
check("Cout2 UAL-vogn ok", ADRunsController.validateVehicle(adEnv, outRun, ualTractor).ok, true)
ADRunsController.store = ADRuns.setProductPolicy(ADRunsController.store, out.id, "FORAGE", "store")
outRun = ADRuns.getRun(ADRunsController.store, out.id)
check("Cout2 bulk-vogn ok med TMR", ADRunsController.validateVehicle(adEnv, outRun, bulkTractor).ok, true)
local strawOnly = makeVehicle("vehicleStraw", "Halm", {STRAW}, 20000, 100, 0)
check("Cout2 kan ikke bære -> cannotCarry", ADRunsController.validateVehicle(adEnv, outRun, strawOnly).reason, "cannotCarry")
-- Cout3: start sætter mode, 1./2. markør = første kilde, varevalg = det vognen kan bære af run'ets varer
ADRunsController.assignVehicle(out.id, bulkTractor)
ADRunsController.assignVehicle(out.id, ualTractor)
local results = ADRunsController.startRun(adEnv, out.id)
check("Cout3 to startet", #results, 2)
check("Cout3 mode P&D", bulkTractor.ad.stateModule.mode, MODE_PICKUPANDDELIVER)
check("Cout3 1. markør = første kilde", bulkTractor.ad.stateModule.firstMarkerId, 1)
check("Cout3 2. markør = første kilde (pladsholder)", bulkTractor.ad.stateModule.secondMarkerId, 1)
check("Cout3 bulk-vogn: kun TMR", table.concat(bulkTractor.ad.stateModule.selectedFillTypes, ","), tostring(TMR))
check("Cout3 UAL-vogn: kurve", table.concat(ualTractor.ad.stateModule.selectedFillTypes, ","), tostring(BASKET))
check("Cout3 rotation kun pålæsning", bulkTractor.ad.settings.rotateTargets.current, 2)
check("Cout3 startet", bulkTractor.started, 1)
-- AD's "AL unload" er slået FRA som standard (values[1] = 0): autoload-vognen ville aldrig læsse af
-- ved salgsstedet. Ved Start sættes den til "center" hvis den er fra; et brugervalg (left/right/behind) røres ikke.
check("Cout3 ALUnload sat til center for autoload-bil", ualTractor.ad.settings.ALUnload.current, 2)
check("Cout3 ALUnload rører ikke tipvogn", bulkTractor.ad.settings.ALUnload.current, 1)
ualTractor.ad.settings.ALUnload.current = 4
ualTractor.ad.stateModule.active = false
ADRunsController.startRun(adEnv, out.id)
check("Cout3 ALUnload brugervalg bevares", ualTractor.ad.settings.ALUnload.current, 4)
-- Cout4: produkter til fanen (politik slået op, sorteret på titel) og status
local list = ADRunsController.getRunProducts(adEnv, outRun)
check("Cout4 to produkter", #list, 2)
check("Cout4 første = Halmkurve", list[1].title, "Halmkurve")
check("Cout4 politik", list[1].policy, "sell")
check("Cout4 TMR lager", list[2].policy, "store")
check("Cout4 available", list[2].available, 150000)
local st = ADRunsController.getOutboundStatus(adEnv, outRun)
check("Cout4 status-begrundelse", st.reasons[1], "SUGAR 78 % af top (kræver 90), lager 41 %")
holdFor[bulkTractor] = {markerName = "Wait 3", reasons = {"SUGAR 78 % af top (kræver 90), lager 41 %"}}
local activity = ADRunsController.getVehicleActivity(adEnv, bulkTractor)
check("Cout4 aktivitet = holder", activity.kind, "holding")
check("Cout4 aktivitet sted", activity.place, "Wait 3")
check("Cout4 udkørsel har ingen mål", #ADRunsController.getRunTargets(outRun), 0)
-- Cout5 (I2): Start kræver et Wait-punkt i puljen; Cout6 (I3): last på vognen ved Start får eget mål
ADSupplyPlanner.isWaitMarker = function(marker) return string.find(string.lower(marker.group or ""), "%f[%a]wait%f[%A]") ~= nil end
local noWait = ADRunsController.createRun("No wait", "outbound")
ADRunsController.store = ADRuns.toggleSource(ADRunsController.store, noWait.id, 1)
ADRunsController.store = ADRuns.setProductPolicy(ADRunsController.store, noWait.id, "FORAGE", "store")
ADRunsController.store = ADRuns.updateRun(ADRunsController.store, noWait.id, {waitPoolGroup = "Pig Wait"})
ADRunsController.store = ADRuns.updateRun(ADRunsController.store, noWait.id, {waitPoolGroup = "Findes ikke"})
local noWaitRun = ADRuns.getRun(ADRunsController.store, noWait.id)
check("Cout5 ingen Wait-punkt i puljen -> noWaitPoint", ADRunsController.validateVehicle(adEnv, noWaitRun, bulkTractor).reason, "noWaitPoint")
ADRunsController.store = ADRuns.updateRun(ADRunsController.store, noWait.id, {waitPoolGroup = "Cow Wait"})
noWaitRun = ADRuns.getRun(ADRunsController.store, noWait.id)
check("Cout5 Wait-punkt findes -> ok", ADRunsController.validateVehicle(adEnv, noWaitRun, bulkTractor).ok, true)
ADUnloadWait.getRestLoad = function(_, v) return v.loadType, v.loadLiters or 0 end
ADOutbound.planCargoTarget = function(adEnv, run, vehicle, fillType) return fillType == TMR and 10 or nil end
bulkTractor.loadType, bulkTractor.loadLiters = TMR, 8000
ADRunsController.assignVehicle(noWait.id, bulkTractor)
bulkTractor.ad.stateModule.active = false
ADRunsController.startRun(adEnv, noWait.id)
check("Cout6 last på vognen -> 2. markør = lastens mål", bulkTractor.ad.stateModule.secondMarkerId, 10)
bulkTractor.loadType = STRAW
check("Cout6 last uden mål -> cargoNoTarget", ADRunsController.validateVehicle(adEnv, noWaitRun, bulkTractor).reason, "cargoNoTarget")
bulkTractor.loadType = nil
ADUnloadWait = nil; ADOutbound = nil; ADSources = nil
-- C-flow: bygninger -> markører (resolveMarkers), migration af gamle markør-runs, noMarker, flow-rækker
local buildings = {
{id = "barn1", name = "Kostald 1", kind = "husbandry", loadMarker = {id = 10, name = "cow 1 milk", distance = 5}, loadMarkers = {{id = 10, name = "cow 1 milk", distance = 5}, {id = 11, name = "cow 1 slurry", distance = 3}}, unloadMarker = {id = 94, name = "Cow 1 Food", distance = 8}, canBeSource = true, canBeTarget = true, canSell = false},
{id = "shed", name = "Palleskur", kind = "objectStorage", loadMarker = nil, unloadMarker = nil, canBeSource = true, canBeTarget = false, canSell = false},
{id = "mill", name = "Sukkerfabrik", kind = "factory", loadMarker = {id = 12694, name = "Suger Load", distance = 3}, unloadMarker = {id = 77, name = "sugar offload", distance = 4}, canBeSource = true, canBeTarget = true, canSell = false},
}
ADBuildings = {
list = function() return buildings end,
find = function(_, _, id) for _, b in ipairs(buildings) do if b.id == id then return b end end end,
byMarker = function(_, _, wayPointId, role)
for _, b in ipairs(buildings) do
if role == "unload" then
if b.unloadMarker ~= nil and b.unloadMarker.id == wayPointId then return b end
else
for _, m in ipairs(b.loadMarkers or {b.loadMarker}) do if m ~= nil and m.id == wayPointId then return b end end
end
end
end,
}
ADSources = {identify = function(_, id) return (id == 10 or id == 12694 or id == 555) and {kind = "x", wayPointId = id, name = "k"} or nil end, getProducts = function() return {} end, getSellMarkers = function() return {} end, stationAccepts = function() return false end, getSourcePlaceables = function() return {} end}
ADOutbound = {isOutboundVehicle = function() return false end, getRunStatus = function() return {reasons = {"SUGAR 78 % af top"}} end, getHoldFor = function() return nil end, getTripFor = function() return nil end, releaseInactive = function() end}
buildWorld()
addMarker(10, "cow 1 milk", "Milk", 100, 0); loadAt[10] = {}
addMarker(555, "raa markoer", "Pickup", 200, 0); loadAt[555] = {}
addMarker(77, "sugar offload", "production", 950, 0); unloadAt[77] = {accepts = {[TMR] = 360000}}
-- CF1: udkørsel: bygninger + rå markør -> sourceWayPointIds; manglende markør rapporteres
local flowOut = ADRunsController.createRun("Milk", "outbound")
ADRunsController.updateRun(flowOut.id, {sourceBuildingIds = {"barn1", "shed"}, sourceWayPointIds = {555}})
local resolved = ADRunsController.resolveMarkers(adEnv, ADRuns.getRun(ADRunsController.store, flowOut.id))
check("CF1 kildemarkører = ALLE bygningens læssemarkører + rå", table.concat(ADRuns.getRun(ADRunsController.store, flowOut.id).sourceWayPointIds, ","), "10,11,555")
check("CF1 manglende markør", resolved.missing[1], "Palleskur")
-- en markør der HØRER til en bygning som ikke længere er valgt, fjernes (den er ikke rå)
ADRunsController.updateRun(flowOut.id, {sourceWayPointIds = {12694, 555}})
ADRunsController.resolveMarkers(adEnv, ADRuns.getRun(ADRunsController.store, flowOut.id))
check("CF1 fravalgt bygnings markør fjernes", table.concat(ADRuns.getRun(ADRunsController.store, flowOut.id).sourceWayPointIds, ","), "10,11,555")
-- CF1b: gamle rå markører ryddes: ikke-kilde (identify = nil) og markør der hører til en valgt bygning
ADSources.identify = function(_, id)
if id == 10 or id == 12694 or id == 555 then return {kind = "x", wayPointId = id, name = "k"} end
if id == 556 then return {kind = "husbandry", wayPointId = id, name = "Kostald 1", placeable = "barn1-placeable"} end
return nil
end
buildings[1].placeable = "barn1-placeable"
ADRunsController.updateRun(flowOut.id, {sourceBuildingIds = {"barn1"}, sourceWayPointIds = {10, 11, 555, 556, 999}})
logLines = {}
ADRunsController.resolveMarkers(adEnv, ADRuns.getRun(ADRunsController.store, flowOut.id))
check("CF1b rå ikke-kilde og dublet-bygning fjernet", table.concat(ADRuns.getRun(ADRunsController.store, flowOut.id).sourceWayPointIds, ","), "10,11,555")
local pruneLogged = false
for _, line in ipairs(logLines) do if line:find("gamle markører fjernet", 1, true) then pruneLogged = true end end
check("CF1b logget", pruneLogged, true)
-- CF1c: et gammelt kilde-id der ikke længere er en AD-MARKØR (AutoDrive har omnummereret waypoints)
-- fjernes, selv om punktet ligger ved en kilde — ellers kører traktoren til et tilfældigt vejpunkt
local identifyBefore = ADSources.identify
ADSources.identify = function(adEnv_, id, farmId)
if id == 10873 then return {kind = "husbandry", wayPointId = id, name = "Anden stald", placeable = "anden-stald"} end
return identifyBefore(adEnv_, id, farmId)
end
ADRunsController.updateRun(flowOut.id, {sourceBuildingIds = {"barn1"}, sourceWayPointIds = {10, 11, 555, 10873}})
ADRunsController.resolveMarkers(adEnv, ADRuns.getRun(ADRunsController.store, flowOut.id))
check("CF1c vejpunkt uden markør fjernet", table.concat(ADRuns.getRun(ADRunsController.store, flowOut.id).sourceWayPointIds, ","), "10,11,555")
ADSources.identify = identifyBefore
-- CF2: forsyning: mål-bygninger -> targetWayPointIds + targetWayPointId
local flowIn = ADRunsController.createRun("Foder", "supply")
ADRunsController.updateRun(flowIn.id, {targetBuildingIds = {"barn1", "mill"}})
ADRunsController.resolveMarkers(adEnv, ADRuns.getRun(ADRunsController.store, flowIn.id))
check("CF2 målmarkører", table.concat(ADRunsController.getRunTargets(ADRuns.getRun(ADRunsController.store, flowIn.id)), ","), "94,77")
check("CF2 primært mål", ADRuns.getRun(ADRunsController.store, flowIn.id).targetWayPointId, 94)
-- CF2b: gamle rå mål der ikke er en markør (slettet i AD) eller hører til en valgt bygning, ryddes
addMarker(78, "Anden stald food", "Cow Food", 3000, 0); unloadAt[78] = {accepts = {[TMR] = 1000}, owningPlaceable = "anden-stald"}
unloadAt[11062] = {accepts = {[TMR] = 1000}, owningPlaceable = "anden-stald"} -- waypoint uden markør
ADRunsController.updateRun(flowIn.id, {targetWayPointIds = {94, 77, 11062, 78}})
ADRunsController.resolveMarkers(adEnv, ADRuns.getRun(ADRunsController.store, flowIn.id))
check("CF2b ikke-markør fjernet, rå markør med station beholdt", table.concat(ADRunsController.getRunTargets(ADRuns.getRun(ADRunsController.store, flowIn.id)), ","), "94,77,78")
ADRunsController.updateRun(flowIn.id, {targetWayPointIds = {94, 77}})
-- CF3: migration: gamle markør-runs får bygninger
local oldOut = ADRunsController.createRun("Gammel ud", "outbound")
ADRunsController.updateRun(oldOut.id, {sourceWayPointIds = {12694, 555}})
local oldIn = ADRunsController.createRun("Gammel ind", "supply")
ADRunsController.updateRun(oldIn.id, {targetWayPointIds = {94}})
ADRunsController.migrateBuildings(adEnv)
check("CF3 ud: markør -> bygning", ADRuns.getRun(ADRunsController.store, oldOut.id).sourceBuildingIds[1], "mill")
check("CF3 ud: rå markør uden bygning beholdes", table.concat(ADRuns.getRun(ADRunsController.store, oldOut.id).sourceWayPointIds, ","), "12694,555")
check("CF3 ind: markør -> bygning", ADRuns.getRun(ADRunsController.store, oldIn.id).targetBuildingIds[1], "barn1")
ADRunsController.migrateBuildings(adEnv)
check("CF3 migration er idempotent", #ADRuns.getRun(ADRunsController.store, oldOut.id).sourceBuildingIds, 1)
-- CF4: validering: valgt bygning uden markør -> noMarker
local cfVehicle = makeVehicle("vehicleCF", "CF", {STRAW, TMR}, 20000, 100, 0)
ADRunsController.updateRun(flowOut.id, {sourceBuildingIds = {"shed"}, sourceWayPointIds = {}})
local cfValidation = ADRunsController.validateVehicle(adEnv, ADRuns.getRun(ADRunsController.store, flowOut.id), cfVehicle)
check("CF4 noMarker", cfValidation.reason, "noMarker")
check("CF4 hvilken bygning", cfValidation.missingBuilding, "Palleskur")
-- CF4b: ÉN bygning uden markør blandt flere med markør -> start alligevel, advar om den ene
ADRunsController.updateRun(flowOut.id, {sourceBuildingIds = {"barn1", "shed"}, sourceWayPointIds = {}})
logLines = {}
local cfValidation2 = ADRunsController.validateVehicle(adEnv, ADRuns.getRun(ADRunsController.store, flowOut.id), cfVehicle)
check("CF4b ikke afvist pga. markør", cfValidation2.reason ~= "noMarker", true)
local warned = false
for _, line in ipairs(logLines) do if line:find("Palleskur", 1, true) and line:find("springes over", 1, true) then warned = true end end
check("CF4b advarsel nævner bygningen", warned, true)
-- CF5: flow-række til tavlen
ADRunsController.updateRun(flowOut.id, {sourceBuildingIds = {"barn1"}, products = {{fillType = "SUGAR", policy = "sell"}, {fillType = "FORAGE", policy = "store"}}, collectRound = true})
ADRunsController.assignVehicle(flowOut.id, cfVehicle)
local row = ADRunsController.getFlowRow(adEnv, ADRuns.getRun(ADRunsController.store, flowOut.id))
check("CF5 vare-navne", table.concat(row.productNames, ","), "SUGAR,FORAGE")
check("CF5 fra", row.fromNames[1], "Kostald 1")
check("CF5 politikker", table.concat(row.policies, ","), "sell,store")
check("CF5 traktor", row.vehicleNames[1], "CF")
check("CF5 begrundelse", row.reason, "SUGAR 78 % af top")
check("CF5 runde", row.collectRound, true)
ADRunsController.updateRun(flowOut.id, {sourceBuildingIds = {"barn1", "shed"}})
check("CF5 manglende bygning markeres", ADRunsController.getFlowRow(adEnv, ADRuns.getRun(ADRunsController.store, flowOut.id)).missing[1], "Palleskur")
local rowIn = ADRunsController.getFlowRow(adEnv, ADRuns.getRun(ADRunsController.store, flowIn.id))
check("CF5 forsyning fra = alle siloer", rowIn.fromAllSilos, true)
check("CF5 forsyning til", table.concat(rowIn.toNames, ","), "Kostald 1,Sukkerfabrik")
check("CF5 forsyning auto-varer", rowIn.productAuto, true)
-- SB: "Lager til" — valgte bygninger -> deres aflæsningsmarkører (i valgt rækkefølge; ukendte udelades)
do
local sbRun = ADRunsController.createRun("Gylle", "outbound")
ADRunsController.updateRun(sbRun.id, {storeBuildingIds = {"mill", "findes-ikke", "barn1"}})
check("SB markører", table.concat(ADRunsController.getStoreMarkers(adEnv, ADRuns.getRun(ADRunsController.store, sbRun.id)), ","), "77,94")
ADRunsController.updateRun(sbRun.id, {storeBuildingIds = {}})
check("SB tom = ingen tvang", #ADRunsController.getStoreMarkers(adEnv, ADRuns.getRun(ADRunsController.store, sbRun.id)), 0)
end
-- CS: "Lager til" viser kun bygninger der kan tage flowets lagervarer (æg-paller: kun palleskure der tager æg)
do
local EGG, SILAGE = 501, 502
local shed = {id = "shed1", kind = "objectStorage", placeable = {takesEggs = true}}
local shedNo = {id = "shed2", kind = "objectStorage", placeable = {takesEggs = false}}
local silo = {id = "silo9", kind = "silo", placeable = {}, unloadMarker = {id = 901}}
unloadAt[901] = {accepts = {[SILAGE] = 100000}, getIsFillTypeSupported = function(_, ft) return ft == SILAGE end}
local accBefore, storeBefore = ADSources.objectStorageAccepts, ADLoadSwap and ADLoadSwap.isStoreTarget
ADSources.objectStorageAccepts = function(placeable, ft) return ft == EGG and placeable.takesEggs end
ADLoadSwap = ADLoadSwap or {}
ADLoadSwap.isStoreTarget = function(station, ft) return station.getIsFillTypeSupported(station, ft) end
local eggs = {{fillType = EGG, isPallet = true}}
check("CS palleskur der tager æg", ADRunsController.canStoreAt(adEnv, shed, eggs), true)
check("CS palleskur der ikke tager æg", ADRunsController.canStoreAt(adEnv, shedNo, eggs), false)
check("CS silo kan ikke tage æg-paller", ADRunsController.canStoreAt(adEnv, silo, eggs), false)
check("CS silo tager ensilage", ADRunsController.canStoreAt(adEnv, silo, {{fillType = SILAGE, isPallet = false}}), true)
ADSources.objectStorageAccepts = accBefore
ADLoadSwap.isStoreTarget = storeBefore
end
ADBuildings = nil; ADSources = nil; ADOutbound = nil
-- RC-navne: køretøjer får AD-førernavn efter forløbet; ude af forløb -> modelnavnet igen
do
local named = makeVehicle("vehicleNamed", "Virtus 135", {MILK or 1}, 20000, 0, 0)
named.ad.stateModule.driverName = "Virtus 135"
named.ad.stateModule.setName = function(self, n) self.driverName = n end
named.ad.stateModule.getName = function(self) return self.driverName end
local run = ADRunsController.createRun("Mælk ind", "outbound")
ADRunsController.assignVehicle(run.id, named)
ADRunsController.applyVehicleNames(adEnv)
check("RC-navne sat efter forløb", named.ad.stateModule.driverName, "Mælk ind")
check("RC-navne label bruger førernavn", ADRuns.vehicleLabel(named), "Mælk ind")
ADRunsController.store = ADRuns.unassignVehicle(ADRunsController.store, "vehicleNamed")
ADRunsController.applyVehicleNames(adEnv)
check("RC-navne ude af forløb -> modelnavn", named.ad.stateModule.driverName, "Virtus 135")
check("RC-navne label uden AD-navn", ADRuns.vehicleLabel({getName = function() return "Fendt" end}), "Fendt")
end
-- PM: afhentningsmarkører fra udkørsels-forløb (lagerture undgår dem som aflæsningssted)
do
local sale = ADRunsController.createRun("Salg-test", "outbound")
ADRunsController.updateRun(sale.id, {sourceWayPointIds = {4711, 4712}})
local supply = ADRunsController.createRun("Forsyning-test", "supply")
ADRunsController.updateRun(supply.id, {sourceWayPointIds = {9999}})
local pickups = ADRunsController.getPickupMarkers()
check("PM udkørsels-kilde med", pickups[4711], true)
check("PM forsyning tæller ikke", pickups[9999], nil)
end
-- SA: Start/Stop alle flows — kørende flows røres ikke ved Start alle; Stop alle stopper alt aktivt
do
buildWorld()
local idle = makeVehicle("vehicleAllA", "Alle A", {STRAW, TMR}, 1000000, 100, 0)
local busy = makeVehicle("vehicleAllB", "Alle B", {STRAW, TMR}, 1000000, 100, 0)
local runA = ADRunsController.createRun("Alle-A")
ADRunsController.updateRun(runA.id, {targetWayPointId = 94})
ADRunsController.assignVehicle(runA.id, idle)
local runB = ADRunsController.createRun("Alle-B")
ADRunsController.updateRun(runB.id, {targetWayPointId = 94})
ADRunsController.assignVehicle(runB.id, busy)
ADRunsController.createRun("Alle-tom") -- ingen traktorer: tæller ikke med
ADRunsController.startRun(adEnv, runB.id)
local busyStarts = busy.started
local summary = ADRunsController.startAllRuns(adEnv)
check("SA idle startet", idle.started, 1)
check("SA kørende flow ikke genstartet", busy.started, busyStarts)
check("SA mindst ét flow startet", summary.runsStarted >= 1, true)
check("SA kørende flow talt", summary.alreadyRunning >= 1, true)
check("SA traktorer startet tæller idle", summary.vehiclesStarted >= 1, true)
check("SA resultater med", type(summary.results), "table")
check("SA noget aktivt", ADRunsController.hasActiveRuns(adEnv), true)
local stopped = ADRunsController.stopAllRuns(adEnv)
check("SA stop alle stopper begge", stopped >= 2, true)
check("SA idle stoppet", idle.ad.stateModule.active, false)
check("SA busy stoppet", busy.ad.stateModule.active, false)
check("SA intet aktivt bagefter", ADRunsController.hasActiveRuns(adEnv), false)
check("SA isRunning ryddet", ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, runA.id)).isRunning, false)
end
-- C20: Start alle i kø — én traktor ad gangen med STAGGER_MS mellemrum (in-game 27/9: ellers kører de
-- sammen på parkeringspladsen); pause ophæves
do
buildWorld()
local q1 = makeVehicle("vehicleQ1", "Q1", {STRAW, TMR}, 1000000, 100, 0)
local q2 = makeVehicle("vehicleQ2", "Q2", {STRAW, TMR}, 1000000, 110, 0)
local q3 = makeVehicle("vehicleQ3", "Q3", {}, 0, 120, 0) -- ingen vogn -> springes over, ikke i kø
local qrun = ADRunsController.createRun("Kø")
ADRunsController.updateRun(qrun.id, {targetWayPointIds = {94}})
ADRunsController.assignVehicle(qrun.id, q1)
ADRunsController.assignVehicle(qrun.id, q2)
ADRunsController.assignVehicle(qrun.id, q3)
local summary = ADRunsController.queueStartAll(adEnv)
check("C20 to i kø", summary.queued, 2)
check("C20 en sprunget over", #summary.skipped, 1)
check("C20 intet startet før første tick", (q1.started or 0) + (q2.started or 0), 0)
ADRunsController.processStartQueue(adEnv, 0)
check("C20 første startes straks", (q1.started or 0) + (q2.started or 0), 1)
ADRunsController.processStartQueue(adEnv, ADRunsController.STAGGER_MS - 1)
check("C20 næste venter mellemrummet", (q1.started or 0) + (q2.started or 0), 1)
ADRunsController.processStartQueue(adEnv, 1)
check("C20 næste efter mellemrummet", (q1.started or 0) + (q2.started or 0), 2)
check("C20 kø tom", #ADRunsController.startQueue, 0)
check("C20 run markeret kørende", ADRunsController.runningRuns[qrun.id], true)
end
-- C21: Stop alt og parkér — stopper flow-traktorerne, sender dem til AD-parkering, sætter pause
do
buildWorld()
local p1 = makeVehicle("vehicleP1", "P1", {STRAW, TMR}, 1000000, 100, 0)
local p2 = makeVehicle("vehicleP2", "P2", {STRAW, TMR}, 1000000, 110, 0)
local prun = ADRunsController.createRun("Park")
ADRunsController.updateRun(prun.id, {targetWayPointIds = {94}})
ADRunsController.assignVehicle(prun.id, p1)
ADRunsController.assignVehicle(prun.id, p2)
ADRunsController.startRun(adEnv, prun.id)
local parked, paused = {}, nil
local oldWork, oldJobs, oldSilos = ADFieldWork, ADFieldJobs, ADFieldSilos
ADFieldWork = {
parkMarkerId = function(_, v) return v == p1 and 700 or nil end,
startAd = function(_, v, mode, first) parked[v] = first end,
stopAllAssignments = function() return {} end,
}
ADFieldJobs = {setPaused = function(value) paused = value end}
ADFieldSilos = {stopAll = function() end}
ADRunsController.queueStartAll(adEnv)
local summary = ADRunsController.stopAllAndPark(adEnv)
check("C21 begge stoppet", (p1.stopped or 0) >= 1 and (p2.stopped or 0) >= 1, true)
check("C21 P1 kører til parkering", parked[p1], 700)
check("C21 P2 uden parkering bliver stående", parked[p2], nil)
check("C21 tæller parkerede", summary.parked, 1)
check("C21 nævner dem uden parkering", summary.noPark[1], "P2")
check("C21 pause sat", paused, true)
check("C21 startkø tømt", #ADRunsController.startQueue, 0)
check("C21 run ikke længere kørende", ADRunsController.runningRuns[prun.id], nil)
ADFieldWork, ADFieldJobs, ADFieldSilos = oldWork, oldJobs, oldSilos
end
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
File diff suppressed because it is too large Load Diff
-596
View File
@@ -1,596 +0,0 @@
-- Kør: luajit tests/test_adSources.lua (fra repo-roden)
-- Kilde-genkendelse ved en AD-markør (fabrik / silo / objektlager), lagerstatus pr. vare,
-- paller ved fabrikkens spawner, salgssteder, objektlager-udtag.
local SUGAR, FORAGE, BASKET, CRATE = 5, 6, 7, 8
local DESC = {
[SUGAR] = {index = SUGAR, name = "SUGAR", title = "Sukker", isPalletType = true},
[FORAGE] = {index = FORAGE, name = "FORAGE", title = "TMR"},
[BASKET] = {index = BASKET, name = "STRAW_BASKET", title = "Halmkurve", palletFilename = "basketPallet.xml"},
[CRATE] = {index = CRATE, name = "STRAW_CRATE", title = "Halmkasser", palletFilename = "cratePallet.xml"},
}
g_fillTypeManager = {
getFillTypeByIndex = function(_, index) return DESC[index] end,
getFillTypeNameByIndex = function(_, index) return DESC[index] and DESC[index].name end,
getFillTypeTitleByIndex = function(_, index) return DESC[index] and DESC[index].title end,
}
Logging = {info = function() end, warning = function() end}
getWorldTranslation = function(node) return node.x, 0, node.z end
SellingStation = {}
dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")
dofile("FS25_ADSmartPickup/adPrices.lua")
dofile("FS25_ADSmartPickup/adStorageStats.lua")
dofile("FS25_ADSmartPickup/adSources.lua")
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
-- ---- fakes ----
local wayPoints, markers = {}, {}
local function addMarker(id, name, x, z)
wayPoints[id] = {id = id, x = x, z = z}
table.insert(markers, {id = id, markerIndex = #markers + 1, name = name, group = "g"})
end
local adEnv = {ADGraphManager = {
getWayPointById = function(_, id) return wayPoints[id] end,
getMapMarkers = function() return markers end,
getMapMarkerByWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id then return m end end end,
}}
local function storageOf(levels, capacities)
return {
fillLevels = levels,
getFillLevel = function(_, ft) return levels[ft] or 0 end,
getCapacity = function(_, ft) return capacities[ft] or 0 end,
}
end
local sugarStorage = storageOf({[SUGAR] = 150000, [BASKET] = 0}, {[SUGAR] = 180000, [BASKET] = 8000})
local factory = {
rootNode = {x = 100, z = 100}, getName = function() return "Sugar mill" end,
spec_productionPoint = {productionPoint = {
productions = {{outputs = {{type = SUGAR, amount = 1}}}, {outputs = {{type = BASKET, amount = 1}}}},
storage = sugarStorage,
palletSpawner = {spawnPlaces = {{startNode = {x = 110, z = 100}}}},
}},
}
local factoryStation = {owningPlaceable = factory, supportedFillTypes = {[SUGAR] = true}}
local silo = {rootNode = {x = 200, z = 200}, spec_silo = {storages = {storageOf({[FORAGE] = 74500}, {[FORAGE] = 12000000})}}, getName = function() return "Farma" end}
local siloStation = {owningPlaceable = silo}
local spawnCalls = {}
local objectStorage = {
rootNode = {x = 300, z = 300}, ownerFarmId = 1, getName = function() return "Objektlager" end,
spec_objectStorage = {
objectInfos = {
{objects = {{palletAttributes = {fillType = BASKET, fillLevel = 1000}}}, numObjects = 8},
{objects = {{baleAttributes = {fillType = FORAGE, fillLevel = 4000}}}, numObjects = 2},
},
numStoredObjects = 10, capacity = 4000,
},
removeAbstractObjectsFromStorage = function(self, index, amount, connection) table.insert(spawnCalls, {index, amount, connection}) end,
}
local sellStation = {owningPlaceable = {rootNode = {x = 500, z = 500}}, isa = function(_, c) return c == SellingStation end, isSellingPoint = true, acceptedFillTypes = {[SUGAR] = true}, getName = function() return "Grain Elevator" end}
local factoryUnload = {owningPlaceable = factory, isa = function() return true end, isSellingPoint = false}
addMarker(1, "Suger Load", 100, 90)
addMarker(2, "silo 2", 200, 190)
addMarker(3, "Objektlager", 300, 280) -- 20 m fra lageret
addMarker(4, "Langt væk", 300, 240) -- 60 m fra lageret
addMarker(5, "selling station", 500, 490)
addMarker(6, "sugar offload", 100, 80)
local loadAt = {[1] = factoryStation, [2] = siloStation}
local unloadAt = {[5] = sellStation, [6] = factoryUnload}
ADSmartPickup = {
getLoadStationAtWayPointId = function(_, id) return loadAt[id] end,
getUnloadStationAtWayPointId = function(_, id) return unloadAt[id] end,
getOwnStorageLevel = function(station, fillTypes)
local total = 0
for _, storage in pairs(station.owningPlaceable.spec_silo.storages) do
for _, ft in ipairs(fillTypes) do total = total + storage:getFillLevel(ft) end
end
return total
end,
}
local pallets = {}
local function addPallet(fillType, level, x, z, farmId)
table.insert(pallets, {spec_pallet = {}, rootNode = {x = x, z = z}, getOwnerFarmId = function() return farmId or 1 end,
getFillUnitFillType = function() return fillType end, getFillUnitFillLevel = function() return level end})
end
g_currentMission = {
placeableSystem = {placeables = {factory, silo, objectStorage}},
accessHandler = {canFarmAccess = function(_, farmId, placeable) return true end},
vehicleSystem = {vehicles = pallets},
}
-- S1: fabrik
local source1 = ADSources.identify(adEnv, 1, 1)
check("S1 fabrik", source1.kind, "factory")
check("S1 navn", source1.name, "Sugar mill")
local products1 = ADSources.getProducts(adEnv, source1, 1)
check("S1 to produkter", #products1, 2)
check("S1 SUGAR available", products1[1].available, 150000)
check("S1 SUGAR level", products1[1].level, 150000)
check("S1 SUGAR capacity", products1[1].capacity, 180000)
check("S1 SUGAR bulk", products1[1].isPallet, false)
check("S1 SUGAR kan også stå på paller", products1[1].canPallet, true)
check("S1 SUGAR paller ved spawner (ingen endnu)", products1[1].palletLiters, 0)
check("S1 TMR-lignende bulk kan ikke paller", products1[2].canPallet, true)
check("S1 BASKET er palle", products1[2].isPallet, true)
check("S1 BASKET tomt", products1[2].available, 0)
-- S2: silo
local source2 = ADSources.identify(adEnv, 2, 1)
check("S2 silo", source2.kind, "silo")
local products2 = ADSources.getProducts(adEnv, source2, 1)
check("S2 én vare", #products2, 1)
check("S2 FORAGE", products2[1].fillType, FORAGE)
check("S2 level", products2[1].level, 74500)
check("S2 capacity", products2[1].capacity, 12000000)
-- S3: objektlager 20 m fra markøren
local source3 = ADSources.identify(adEnv, 3, 1)
check("S3 objektlager", source3.kind, "objectStorage")
local products3 = ADSources.getProducts(adEnv, source3, 1)
check("S3 to varer", #products3, 2)
check("S3 kurve available", products3[1].available, 8000)
check("S3 kurve er palle", products3[1].isPallet, true)
check("S3 level = antal objekter", products3[1].level, 10)
check("S3 capacity", products3[1].capacity, 4000)
check("S3 baller tæller som palle", products3[2].isPallet, true)
local index, count = ADSources.getObjectInfoIndex(source3, BASKET)
check("S3 objectInfo-indeks", index, 1)
check("S3 antal", count, 8)
check("S3 countStored", ADSources.countStored(source3, FORAGE), 2)
check("S3 ukendt vare", ADSources.getObjectInfoIndex(source3, SUGAR), nil)
-- S4: paller ved fabrikkens spawner tæller med (inden for 25 m af spawn-noden)
addPallet(BASKET, 1000, 112, 100)
addPallet(BASKET, 1000, 115, 105)
addPallet(BASKET, 1000, 160, 100) -- 50 m væk: tæller ikke
addPallet(BASKET, 1000, 112, 102, 2) -- anden farm: tæller ikke
local products4 = ADSources.getProducts(adEnv, source1, 1)
check("S4 paller ved spawner", products4[2].available, 2000)
check("S4 antal paller ved spawner", products4[2].palletCount, 2)
check("S3 antal paller i objektlager", products3[1].palletCount, 8)
check("S4 bulk uændret", products4[1].available, 150000)
addPallet(SUGAR, 2000, 111, 101)
local products4b = ADSources.getProducts(adEnv, source1, 1)
check("S4 sukkerpaller tælles separat", products4b[1].palletLiters, 2000)
check("S4 bulk-mængde uden paller", products4b[1].available, 150000)
-- S5: salgssteder = kun markører hvis aflæsningsstation er et rigtigt salgssted
local sells = ADSources.getSellMarkers(adEnv, 1)
check("S5 ét salgssted", #sells, 1)
check("S5 markør", sells[1].marker.id, 5)
check("S5 tager SUGAR", ADSources.stationAccepts(sells[1].station, SUGAR), true)
check("S5 tager ikke FORAGE", ADSources.stationAccepts(sells[1].station, FORAGE), false)
-- palletriggerens varer ligger i supportedFillTypes (det AutoDrive bruger), ikke i acceptedFillTypes (kun bulk-trigger)
local palletSell = {acceptedFillTypes = {[SUGAR] = true}, supportedFillTypes = {[SUGAR] = true, [BASKET] = true}}
check("S5 pallevare via supportedFillTypes", ADSources.stationAccepts(palletSell, BASKET), true)
check("S5 ukendt vare stadig nej", ADSources.stationAccepts(palletSell, FORAGE), false)
check("S5 acceptedNames", ADSources.describeAccepted(palletSell), "STRAW_BASKET, SUGAR")
-- S6: spawnOne kalder spillets udtag med 1 objekt og uden klientforbindelse
check("S6 spawn ok", ADSources.spawnOne(source3, 1), true)
check("S6 indeks", spawnCalls[1][1], 1)
check("S6 antal", spawnCalls[1][2], 1)
check("S6 connection nil", spawnCalls[1][3], nil)
check("S6 fejl -> false", ADSources.spawnOne({kind = "objectStorage", placeable = {removeAbstractObjectsFromStorage = function() error("x") end}}, 1), false)
-- S7: markør uden station og >40 m fra objektlager -> nil
check("S7 ingen kilde", ADSources.identify(adEnv, 4, 1), nil)
check("S7 ukendt waypoint", ADSources.identify(adEnv, 99, 1), nil)
-- S8: hjælpere
check("S8 isPalletFillType palle", ADSources.isPalletFillType(BASKET), true)
check("S8 isPalletFillType palletype uden fil", ADSources.isPalletFillType(SUGAR), true)
check("S8 isPalletFillType bulk", ADSources.isPalletFillType(FORAGE), false)
-- C1: palle/bulk afgøres pr. kilde: fabrik med læssestation der tager varen = bulk; silo = altid bulk; objektlager = palle
check("S8 sukker ved fabrik med læssestation = bulk", ADSources.isPalletAt(source1, SUGAR), false)
check("S8 kurve ved fabrik (station tager dem ikke) = palle", ADSources.isPalletAt(source1, BASKET), true)
check("S8 silo altid bulk", ADSources.isPalletAt(source2, SUGAR), false)
check("S8 objektlager altid palle", ADSources.isPalletAt(source3, FORAGE), true)
-- S9 (I1): ren pallefabrik uden læssestation genkendes via afstand til spawner/rootNode
local basketFactory = {
rootNode = {x = 600, z = 600}, getName = function() return "Straw factory" end,
spec_productionPoint = {productionPoint = {productions = {{outputs = {{type = BASKET, amount = 1}}}}, storage = storageOf({[BASKET] = 0}, {[BASKET] = 8000}), palletSpawner = {spawnPlaces = {{startNode = {x = 610, z = 600}}}}}},
}
table.insert(g_currentMission.placeableSystem.placeables, basketFactory)
addMarker(7, "Straw factory pickup", 605, 590)
local source9 = ADSources.identify(adEnv, 7, 1)
check("S9 pallefabrik uden station = factory", source9.kind, "factory")
check("S9 station nil", source9.station, nil)
local products9 = ADSources.getProducts(adEnv, source9, 1)
check("S9 kurve er palle", products9[1].isPallet, true)
check("S9 fabrik foretrækkes frem for objektlager i nærheden", ADSources.identify(adEnv, 7, 1).placeable, basketFactory)
-- S9b: fabrik fundet på afstand, men produktionspunktet HAR en læssestation (sukkerfabrik hvor markøren
-- ikke rammer triggeren): stationen hentes fra produktionspunktet, og sukker er bulk, ikke paller
local mill2Storage = storageOf({[SUGAR] = 1000}, {[SUGAR] = 180000})
local mill2 = {
rootNode = {x = 1200, z = 1200}, getName = function() return "Sugar mill 2" end,
spec_productionPoint = {productionPoint = {productions = {{outputs = {{type = SUGAR, amount = 1}}}}, storage = mill2Storage,
loadingStation = {supportedFillTypes = {[SUGAR] = true}}}},
}
table.insert(g_currentMission.placeableSystem.placeables, mill2)
addMarker(9, "Mill 2 load", 1210, 1190)
local source9b = ADSources.identify(adEnv, 9, 1)
check("S9b fabrik", source9b.kind, "factory")
check("S9b station fra produktionspunktet", source9b.station, mill2.spec_productionPoint.productionPoint.loadingStation)
check("S9b sukker = bulk", ADSources.isPalletAt(source9b, SUGAR), false)
check("S9b produkt bulk", ADSources.getProducts(adEnv, source9b, 1)[1].isPallet, false)
local excluded = ADSources.getSourcePlaceables({source1, source2})
check("S8 udelukkede placeables", excluded[factory] and excluded[silo], true)
-- S10 (paller til lager): objektlager med plads der tager varen; nærmeste nåelige fra traktoren
objectStorage.getObjectStorageSupportsFillType = function(self, ft) return ft == BASKET or ft == FORAGE end
check("S10 tager kurve", ADSources.objectStorageAccepts(objectStorage, BASKET), true)
check("S10 tager ikke sukker", ADSources.objectStorageAccepts(objectStorage, SUGAR), false)
check("S10 fri pladser", ADSources.objectStorageFreeSlots(objectStorage), 3990)
local fullShed = {rootNode = {x = 700, z = 700}, spec_objectStorage = {capacity = 10, numStoredObjects = 10, objectInfos = {}}, getName = function() return "Fuldt skur" end}
table.insert(g_currentMission.placeableSystem.placeables, fullShed)
addMarker(8, "Fuldt skur", 700, 690)
check("S10 fuldt skur 0 pladser", ADSources.objectStorageFreeSlots(fullShed), 0)
local truck = {rootNode = {x = 650, z = 650}}
local store = ADSources.findPalletStore(adEnv, truck, BASKET, {}, function() return true end, 1)
check("S10 nærmeste med plads = Objektlager (skuret er fuldt)", store.wayPointId, 3)
check("S10 navn", store.name, "Objektlager")
check("S10 udelukket kilde -> nil", ADSources.findPalletStore(adEnv, truck, BASKET, {[objectStorage] = true}, function() return true end, 1), nil)
check("S10 uopnåelig -> nil", ADSources.findPalletStore(adEnv, truck, BASKET, {}, function() return false end, 1), nil)
check("S10 sukker: intet skur tager det", ADSources.findPalletStore(adEnv, truck, SUGAR, {}, function() return true end, 1), nil)
-- S11 (reserve): gårdens forbrug det næste døgn af en vare, minus hvad forbrugerne allerede har
local BEET, STRAW, WATER = 9, 10, 11
DESC[BEET] = {index = BEET, name = "SUGARBEET", title = "Roer"}; DESC[STRAW] = {index = STRAW, name = "STRAW", title = "Halm"}; DESC[WATER] = {index = WATER, name = "WATER", title = "Vand"}
local tmrStorage = storageOf({[BEET] = 20000, [FORAGE] = 500000}, {[BEET] = 2500000, [FORAGE] = 3500000})
local tmrFactory = {
rootNode = {x = 800, z = 800}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "TMR-fabrik" end,
spec_productionPoint = {productionPoint = {
productions = {{id = "tmr3", cyclesPerMinute = 200 / 60, inputs = {{type = BEET, amount = 45}}, outputs = {{type = FORAGE, amount = 240}}}},
storage = tmrStorage,
getIsProductionEnabled = function(self, id) return id == "tmr3" end,
}},
}
local cowBarn = {
rootNode = {x = 900, z = 900}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Kostald" end,
spec_husbandryStraw = {inputLitersPerHour = 500}, spec_husbandryWater = {litersPerHour = 100, automaticWaterSupply = true},
getHusbandryIsFillTypeSupported = function(self, ft) return ft == STRAW or ft == WATER end,
getHusbandryFillLevel = function(self, ft) return ft == STRAW and 2000 or 0 end,
getHusbandryFreeCapacity = function(self, ft) return ft == STRAW and 19375 or 0 end,
}
local otherFarmBarn = {rootNode = {x = 950, z = 950}, ownerFarmId = 2, getOwnerFarmId = function() return 2 end, spec_husbandryStraw = {inputLitersPerHour = 500}, getHusbandryIsFillTypeSupported = function() return true end, getHusbandryFillLevel = function() return 0 end, getHusbandryFreeCapacity = function() return 99999 end}
table.insert(g_currentMission.placeableSystem.placeables, tmrFactory)
table.insert(g_currentMission.placeableSystem.placeables, cowBarn)
table.insert(g_currentMission.placeableSystem.placeables, otherFarmBarn)
local demand = ADSources.getFarmDemand(adEnv, BEET, 1, {})
-- 45 l × (200/60) pr. min × 1440 min = 216.000 l/døgn; har 20.000 -> 196.000
check("S11 roer: døgnforbrug minus beholdning", demand.liters, 196000)
check("S11 forbruger-navn", demand.consumers[1], "TMR-fabrik")
local strawDemand = ADSources.getFarmDemand(adEnv, STRAW, 1, {})
-- 500 l/t × 24 = 12.000; har 2.000 -> 10.000 (under fri plads 19.375)
check("S11 halm: stald", strawDemand.liters, 10000)
check("S11 anden farm tæller ikke", #strawDemand.consumers, 1)
check("S11 automatisk vand = intet behov", ADSources.getFarmDemand(adEnv, WATER, 1, {}).liters, 0)
check("S11 output er ikke behov", ADSources.getFarmDemand(adEnv, FORAGE, 1, {}).liters, 0)
check("S11 udelukket forbruger", ADSources.getFarmDemand(adEnv, BEET, 1, {[tmrFactory] = true}).liters, 0)
tmrStorage.fillLevels[BEET] = 300000
check("S11 nok på lager -> 0", ADSources.getFarmDemand(adEnv, BEET, 1, {}).liters, 0)
tmrStorage.fillLevels[BEET] = 20000
tmrFactory.spec_productionPoint.productionPoint.getIsProductionEnabled = function() return false end
check("S11 slukket produktion -> 0", ADSources.getFarmDemand(adEnv, BEET, 1, {}).liters, 0)
-- S11b: staldens OUTPUT (mælk, gylle) er aldrig et behov — kun varer i foderkrybben er foder
local MILK_OUT = 13
local feedBarn = {
rootNode = {x = 960, z = 960}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Malkestald" end,
spec_husbandryFood = {litersPerHour = 2000, fillLevels = {[FORAGE] = 1000}},
getHusbandryIsFillTypeSupported = function(self, ft) return ft == MILK_OUT or ft == FORAGE end,
getHusbandryFillLevel = function(self, ft) return ft == MILK_OUT and 200000 or ft == FORAGE and 1000 or 0 end,
getHusbandryFreeCapacity = function(self, ft) return 9999999 end,
}
table.insert(g_currentMission.placeableSystem.placeables, feedBarn)
check("S11b mælk er ikke behov", ADSources.getFarmDemand(adEnv, MILK_OUT, 1, {}).liters, 0)
check("S11b foder er behov (2000 l/t × 24 − 1000)", ADSources.getFarmDemand(adEnv, FORAGE, 1, {}).liters, 47000)
-- tom krybbe: foderet kendes fra dyretypens foderdefinition
feedBarn.spec_husbandryFood.fillLevels = {}
feedBarn.getHusbandryFillLevel = function(self, ft) return ft == MILK_OUT and 200000 or 0 end
g_currentMission.animalFoodSystem = {getAnimalFood = function() return {groups = {{fillTypes = {FORAGE}}}} end}
check("S11b tom krybbe: foder stadig behov", ADSources.getFarmDemand(adEnv, FORAGE, 1, {}).liters, 48000)
check("S11b tom krybbe: mælk stadig ikke behov", ADSources.getFarmDemand(adEnv, MILK_OUT, 1, {}).liters, 0)
g_currentMission.animalFoodSystem = nil
table.remove(g_currentMission.placeableSystem.placeables)
-- S12 (inventar): alt gården har liggende, pr. bygning, kun bygninger med indhold
factory.ownerFarmId = 1; silo.ownerFarmId = 1; objectStorage.ownerFarmId = 1
local inventory = ADSources.getFarmInventory(adEnv, 1)
local byName = {}
for _, entry in ipairs(inventory) do byName[entry.name] = entry end
check("S12 sukkerfabrik med", byName["Sugar mill"] ~= nil, true)
check("S12 fabrik-kind", byName["Sugar mill"].kind, "factory")
check("S12 fabrik sukker", byName["Sugar mill"].entries[1].level, 150000)
check("S12 fabrik sukker er output", byName["Sugar mill"].entries[1].isOutput, true)
local beetEntry
for _, e in ipairs(byName["TMR-fabrik"].entries) do if e.fillType == BEET then beetEntry = e end end
check("S12 TMR-fabrik roer er input", beetEntry.isOutput, false)
check("S12 outputs først", byName["TMR-fabrik"].entries[1].isOutput, true)
check("S12 silo", byName["Farma"].entries[1].fillType, FORAGE)
check("S12 objektlager kurve som antal", byName["Objektlager"].entries[1].count, 8)
-- 10 af 4000 pladser optaget -> kurvene har 8 + 3990 ledige pladser; liter-kapacitet = pladser x 1000 l
check("S12 objektlager pladser = egne + ledige", byName["Objektlager"].entries[1].slots, 3998)
check("S12 objektlager kapacitet i liter", byName["Objektlager"].entries[1].capacity, 3998000)
check("S12 stald halm", byName["Kostald"].entries[1].level, 2000)
check("S12 stald kapacitet = niveau + fri", byName["Kostald"].entries[1].capacity, 21375)
check("S12 tomt skur udeladt", byName["Fuldt skur"], nil)
check("S12 anden farm udeladt", #inventory, 5)
-- S13: Wait-markører er hverken kilder eller lagermål, selv om et objektlager står inden for 40 m
-- (in-game 23/9: 'cow wait 1' blev valgt som palle-lager -> Dragon holdt ved Wait med stropperne af)
wayPoints[20] = {id = 20, x = 300, z = 275}
table.insert(markers, {id = 20, markerIndex = #markers + 1, name = "cow wait 1", group = "cow wait"})
check("S13 Wait-markør er ikke en kilde", ADSources.identify(adEnv, 20, 1), nil)
local truck2 = {rootNode = {x = 300, z = 270}}
local store13 = ADSources.findPalletStore(adEnv, truck2, BASKET, {}, function() return true end, 1)
check("S13 palle-lager = objektlagerets egen markør, ikke Wait", store13.wayPointId, 3)
-- S14: stald som kilde — markør ved staldens læssestation (mælketank/gylle); varer = det stationen udleverer
local MILK = 12
DESC[MILK] = {index = MILK, name = "MILK", title = "Mælk"}
local barnLevels = {[MILK] = 4200, [STRAW] = 2000}
local milkBarn = {
rootNode = {x = 1500, z = 1500}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Kostald" end,
getHusbandryFillLevel = function(self, ft) return barnLevels[ft] or 0 end,
getHusbandryCapacity = function(self, ft) return ft == MILK and 25000 or ft == STRAW and 21375 or 0 end,
getHusbandryFreeCapacity = function(self, ft) return ft == MILK and 20800 or 0 end,
getHusbandryIsFillTypeSupported = function(self, ft) return ft == MILK or ft == STRAW end,
}
local milkStation = {owningPlaceable = milkBarn, supportedFillTypes = {[MILK] = true}}
addMarker(10, "cow 1 milk", 1500, 1490); loadAt[10] = milkStation
local source14 = ADSources.identify(adEnv, 10, 1)
check("S14 stald", source14.kind, "husbandry")
check("S14 navn", source14.name, "Kostald")
local products14 = ADSources.getProducts(adEnv, source14, 1)
check("S14 én vare (kun det tanken udleverer)", #products14, 1)
check("S14 mælk", products14[1].fillType, MILK)
check("S14 niveau", products14[1].level, 4200)
check("S14 kapacitet", products14[1].capacity, 25000)
check("S14 bulk", products14[1].isPallet, false)
check("S14 isPalletAt stald = bulk", ADSources.isPalletAt(source14, MILK), false)
check("S14 kilde-markør i listen", ADRunsController ~= nil and true or true, true)
-- S14b: mælketank-udvidelse (siloExtension) — læssestationen trækker fra stald + tank; niveauet skal
-- være stationens samlede mængde, ikke kun staldens egen beholder
milkStation.getAllFillLevels = function(self, farmId) return {[MILK] = 1275 + 136912}, 10025000 end
local products14b = ADSources.getProducts(adEnv, source14, 1)
check("S14b stald + udvidelsestank", products14b[1].level, 138187)
check("S14b tilgængeligt = samlet", products14b[1].available, 138187)
milkStation.getAllFillLevels = nil
-- S14c: mælketank (silo med udvidelses-lager) — tanken selv er tom, puljen via læssestationen har mælken
local tank = {spec_silo = {storages = {{fillLevels = {}, getCapacity = function() return 10000000 end}}}}
local tankSource = {kind = "silo", placeable = tank, wayPointId = 999, station = {getAllFillLevels = function() return {[MILK] = 500000}, 70000000 end}}
local tankProducts = ADSources.getProducts(adEnv, tankSource, 1)
check("S14c tom tank viser puljens mælk", tankProducts[1] and tankProducts[1].level, 500000)
check("S14c kapacitet = puljens", tankProducts[1] and tankProducts[1].capacity, 70000000)
-- S14d: generelt lager (separatorhal): kilde via læssestationen, varer = stationens niveauer
local DIGESTATE = 30
DESC[DIGESTATE] = {index = DIGESTATE, name = "DIGESTATE", title = "Digestat"}
local separator = {rootNode = {x = 7000, z = 0}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Separator Hall" end, spec_manureSeparator = {}}
local sepStation = {owningPlaceable = separator, supportedFillTypes = {[DIGESTATE] = true}, getAllFillLevels = function() return {[DIGESTATE] = 42000}, 200000 end}
addMarker(77, "Digestate pickup", 7000, 10); loadAt[77] = sepStation
local sepSource = ADSources.identify(adEnv, 77, 1)
check("S14d separator = lager-kilde", sepSource and sepSource.kind, "storage")
local sepProducts = sepSource and ADSources.getProducts(adEnv, sepSource, 1) or {}
check("S14d digestat fra stationen", sepProducts[1] and sepProducts[1].fillType, DIGESTATE)
check("S14d mængde", sepProducts[1] and sepProducts[1].level, 42000)
-- S14e: hønsehus — æg kommer som PALLER fra staldens egen pallespawner (spec_husbandryPallets), ingen
-- læssestation ved æggene: markøren ved spawneren er en staldkilde, æggene er pallevarer
local EGGS = 31
DESC[EGGS] = {index = EGGS, name = "EGG", title = "Æg", isPallet = true}
local coopSpawner = {spawnPlaces = {{startNode = {x = 8000, z = 0}}}}
local coop = {rootNode = {x = 8040, z = 0}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Chicken Coop" end,
getHusbandryFillLevel = function() return 0 end,
spec_husbandryPallets = {fillTypeIndexToPalletSpawner = {[EGGS] = coopSpawner}, activeFillTypes = {EGGS}, fillLevels = {[EGGS] = 0}, capacities = {[EGGS] = 5000}}}
table.insert(g_currentMission.placeableSystem.placeables, coop)
addPallet(EGGS, 1000, 8003, 2); addPallet(EGGS, 1000, 8006, 2); addPallet(EGGS, 800, 8009, 2)
addMarker(78, "Egg Pickup", 8000, 12)
local coopSource = ADSources.identify(adEnv, 78, 1)
check("S14e hønsehus-kilde via pallespawner", coopSource and coopSource.kind, "husbandry")
local coopProducts = coopSource and ADSources.getProducts(adEnv, coopSource, 1) or {}
local eggs = nil
for _, item in ipairs(coopProducts) do if item.fillType == EGGS then eggs = item end end
check("S14e æg som pallevare", eggs and eggs.isPallet, true)
check("S14e æg på paller ved spawneren", eggs and eggs.available, 2800)
check("S14e tre paller", eggs and eggs.palletCount, 3)
check("S14e isPalletAt æg", coopSource and ADSources.isPalletAt(coopSource, EGGS), true)
-- S14f: hønsehuse på række 17 m fra hinanden (inden for PALLET_RADIUS) — en palle tæller kun for den
-- NÆRMESTE spawner, ellers "ser" et tomt hus naboernes æg og vognen kører forgæves derhen
local coop2Spawner = {spawnPlaces = {{startNode = {x = 8020, z = 0}}}}
local coop2 = {rootNode = {x = 8060, z = 0}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Chicken Coop" end,
getHusbandryFillLevel = function() return 0 end,
spec_husbandryPallets = {fillTypeIndexToPalletSpawner = {[EGGS] = coop2Spawner}, activeFillTypes = {EGGS}, fillLevels = {[EGGS] = 0}, capacities = {[EGGS] = 5000}}}
local emptyCoopSpawner = {spawnPlaces = {{startNode = {x = 8037, z = 0}}}}
local emptyCoop = {rootNode = {x = 8074, z = 0}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Chicken Coop" end,
getHusbandryFillLevel = function() return 0 end,
spec_husbandryPallets = {fillTypeIndexToPalletSpawner = {[EGGS] = emptyCoopSpawner}, activeFillTypes = {EGGS}, fillLevels = {[EGGS] = 0}, capacities = {[EGGS] = 5000}}}
table.insert(g_currentMission.placeableSystem.placeables, coop2)
table.insert(g_currentMission.placeableSystem.placeables, emptyCoop)
addPallet(EGGS, 1000, 8021, 2); addPallet(EGGS, 1000, 8023, 2)
local function eggsAt(placeable)
for _, item in ipairs(ADSources.getProducts(adEnv, {kind = "husbandry", placeable = placeable, station = nil}, 1)) do
if item.fillType == EGGS then return item end
end
end
check("S14f første hus kun egne paller", eggsAt(coop).available, 2800)
check("S14f nabohus kun egne paller", eggsAt(coop2).available, 2000)
check("S14f tomt hus ser ikke naboens æg", eggsAt(emptyCoop).available, 0)
check("S14f tomt hus 0 paller", eggsAt(emptyCoop).palletCount, 0)
table.remove(g_currentMission.placeableSystem.placeables)
table.remove(g_currentMission.placeableSystem.placeables)
-- S14g: spawnerens område er ALLE rækker fra start til slut — ikke kun første rækkes start. Hus A's
-- række 2 slutter tæt på hus B's start; en palle for enden af A's række 2 er stadig A's.
local rowA = {spawnPlaces = {{startNode = {x = 9000, z = 0}, endNode = {x = 9008, z = 0}}, {startNode = {x = 9000, z = 3}, endNode = {x = 9008, z = 3}}}}
local rowB = {spawnPlaces = {{startNode = {x = 9014, z = 3}, endNode = {x = 9022, z = 3}}}}
local function rowCoop(spawner, x)
return {rootNode = {x = x, z = -10}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Chicken Coop" end,
getHusbandryFillLevel = function() return 0 end,
spec_husbandryPallets = {fillTypeIndexToPalletSpawner = {[EGGS] = spawner}, activeFillTypes = {EGGS}, fillLevels = {[EGGS] = 0}, capacities = {[EGGS] = 5000}}}
end
local coopA, coopB = rowCoop(rowA, 9004), rowCoop(rowB, 9018)
table.insert(g_currentMission.placeableSystem.placeables, coopA)
table.insert(g_currentMission.placeableSystem.placeables, coopB)
addPallet(EGGS, 1000, 9008, 3) -- enden af A's række 2: 6 m til B's start, 8 m til A's første start
check("S14g palle for enden af egen række 2 er husets egen", eggsAt(coopA).available, 1000)
check("S14g naboen får den ikke", eggsAt(coopB).available, 0)
-- PlacementUtil-form (startX/startZ + dirX/dirZ*width) i stedet for noder
local placeC = {spawnPlaces = {{startX = 9000, startZ = 50, dirX = 1, dirZ = 0, width = 8}}}
local coopC = rowCoop(placeC, 9004)
table.insert(g_currentMission.placeableSystem.placeables, coopC)
addPallet(EGGS, 700, 9007, 51)
check("S14g PlacementUtil-plads tæller", eggsAt(coopC).available, 700)
table.remove(g_currentMission.placeableSystem.placeables)
table.remove(g_currentMission.placeableSystem.placeables)
table.remove(g_currentMission.placeableSystem.placeables)
-- S15: forklaring af en markør til advarslen (hvad opslaget ser)
check("S15 forklaring stald", ADSources.explainMarker(adEnv, 10, 1), "læssestation ved markøren hører til 'Kostald' (stald)")
check("S15 forklaring ingen station", ADSources.explainMarker(adEnv, 4, 1), "ingen læssestation ved markøren, og ingen fabrik/objektlager inden for 40 m")
loadAt[11] = {owningPlaceable = nil}
wayPoints[11] = {id = 11, x = 2000, z = 2000}; table.insert(markers, {id = 11, markerIndex = #markers + 1, name = "løs", group = "g"})
check("S15 station uden placeable", ADSources.explainMarker(adEnv, 11, 1), "læssestation ved markøren uden bygning (owningPlaceable = nil)")
-- S16: strengt opslag fejler, løst opslag (40 m, uanset retning) finder mælketanken -> stald; en SILO
-- accepteres aldrig løst (tætte rækker -> nabosilo)
local looseAt = {}
ADSmartPickup.getLoadStationNearWayPointId = function(_, id, farmId, maxDistance) return looseAt[id] end
addMarker(12, "cow 2 milk", 1600, 1600)
looseAt[12] = milkStation
check("S16 stald via løst opslag", ADSources.identify(adEnv, 12, 1).kind, "husbandry")
addMarker(13, "silo løst", 1700, 1700)
looseAt[13] = siloStation
check("S16 silo aldrig via løst opslag", ADSources.identify(adEnv, 13, 1), nil)
check("S16 forklaring nævner den løse station", ADSources.explainMarker(adEnv, 13, 1):find("silo", 1, true) ~= nil, true)
-- S16b: "Æg Pickup 1" står 12 m fra hønsehusets palleområde og 23 m fra svinestaldens gyllestation;
-- det nærmeste vinder (in-game 26/9: Pickup 1+2 blev genkendt som Pigsty og æggene aldrig hentet)
local looseDistance = {}
ADSmartPickup.getLoadStationNearWayPointId = function(_, id, farmId, maxDistance) return looseAt[id], looseDistance[id] end
local pigStation = {owningPlaceable = cowBarn}
addMarker(14, "Æg Pickup 1", 8000, 12)
looseAt[14], looseDistance[14] = pigStation, 23
local eggSource = ADSources.identify(adEnv, 14, 1)
check("S16b hønsehuset vinder over fjernere gyllestation", eggSource and eggSource.placeable, coop)
addMarker(15, "Slurry ved hønsehus", 8000, 30)
looseAt[15], looseDistance[15] = pigStation, 5
check("S16b nærmere gyllestation vinder stadig", ADSources.identify(adEnv, 15, 1).placeable, cowBarn)
-- S17: stald med ÉN station og TO læssepunkter (mælk i den ene ende, gylle i den anden): hver markør
-- giver kun de varer dens nærmeste læssepunkt udleverer (in-game 24/9, LargerAnimalBarns cowBarnBig)
local SLURRY = 13
DESC[SLURRY] = {index = SLURRY, name = "LIQUIDMANURE", title = "Gylle"}
barnLevels[SLURRY] = 9000
local twoEndBarn = {
rootNode = {x = 2500, z = 2500}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Stor kostald" end,
getHusbandryFillLevel = function(self, ft) return barnLevels[ft] or 0 end,
getHusbandryCapacity = function() return 25000 end,
}
local twoEndStation = {owningPlaceable = twoEndBarn, supportedFillTypes = {[MILK] = true, [SLURRY] = true},
loadTriggers = {{triggerNode = {x = 2500, z = 2470}, fillTypes = {[MILK] = true}}, {triggerNode = {x = 2500, z = 2530}, fillTypes = {[SLURRY] = true}}}}
addMarker(40, "Cow Milk", 2500, 2465); loadAt[40] = twoEndStation
addMarker(41, "Cow Slurry", 2500, 2535); loadAt[41] = twoEndStation
local milkEnd = ADSources.getProducts(adEnv, ADSources.identify(adEnv, 40, 1), 1)
check("S17 mælke-enden: én vare", #milkEnd, 1)
check("S17 mælke-enden: mælk", milkEnd[1].fillType, MILK)
local slurryEnd = ADSources.getProducts(adEnv, ADSources.identify(adEnv, 41, 1), 1)
check("S17 gylle-enden: én vare", #slurryEnd, 1)
check("S17 gylle-enden: gylle", slurryEnd[1].fillType, SLURRY)
-- læssepunkt uden fillTypes-tabel: stationens varer som før
twoEndStation.loadTriggers[1].fillTypes = nil
check("S17 uden punkt-varer: stationens varer", #ADSources.getProducts(adEnv, ADSources.identify(adEnv, 40, 1), 1), 2)
-- S18: gårdens forbrug og produktion pr. døgn (fabrikker + stalde) og bedste markedspris
tmrFactory.spec_productionPoint.productionPoint.getIsProductionEnabled = function(self, id) return id == "tmr3" end
tmrFactory.spec_productionPoint.productionPoint.productions[1].outputs = {{type = FORAGE, amount = 240}}
cowBarn.spec_husbandryWater.automaticWaterSupply = false
local rates = ADSources.getFarmRates(adEnv, 1)
-- TMR-fabrik: 45 l roer × 200/60 cyklus/min × 1440 = 216.000 l/døgn; TMR ud 240 × 200/60 × 1440 = 1.152.000
check("S18 roer forbrug", math.floor(rates[BEET].consumption + 0.5), 216000)
check("S18 TMR produktion", math.floor(rates[FORAGE].production + 0.5), 1152000)
check("S18 halm i stald 500 l/t", rates[STRAW].consumption, 12000)
check("S18 vand 100 l/t", rates[WATER].consumption, 2400)
check("S18 anden farm tæller ikke", rates[STRAW].consumption, 12000)
-- pris: bedste salgssted der tager varen; top = prisen ved 100 % af kurven
g_currentMission.storageSystem = {getUnloadingStations = function() return {sellStation, {isa = function() return true end, isSellingPoint = true, owningPlaceable = {}, acceptedFillTypes = {[SUGAR] = true}, getEffectiveFillTypePrice = function() return 2.0 end}} end}
sellStation.getEffectiveFillTypePrice = function() return 1.5 end
g_currentMission.environment = {currentPeriod = 1}
DESC[SUGAR].economy = {factors = {0.8, 1.0}}
local quote = ADSources.getMarketQuote(SUGAR)
check("S18 bedste pris", quote.price, 2.0)
check("S18 top ved 80 % af kurven", quote.top, 2.5)
check("S18 intet salgssted -> nil", ADSources.getMarketQuote(FORAGE), nil)
-- S19: staldenes produktion pr. døgn fra dyrenes output-kurver (liter pr. dyr pr. døgn efter alder)
local function curve(value) return {get = function(_, age) return age >= 12 and value or 0 end} end
g_currentMission.animalSystem = {getSubTypeByIndex = function(_, index)
if index == 1 then return {output = {milk = {fillType = MILK, curve = curve(360)}, liquidManure = curve(500)}} end
end}
g_fillTypeManager.getFillTypeIndexByName = function(_, name) for index, desc in pairs(DESC) do if desc.name == name then return index end end end
local producingBarn = {
rootNode = {x = 5000, z = 5000}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Malkestald" end,
getHusbandryFillLevel = function() return 0 end,
getClusters = function() return {
{getSubTypeIndex = function() return 1 end, getAge = function() return 24 end, getNumAnimals = function() return 10 end},
{getSubTypeIndex = function() return 1 end, getAge = function() return 6 end, getNumAnimals = function() return 5 end}, -- kalve: 0
} end,
}
table.insert(g_currentMission.placeableSystem.placeables, producingBarn)
local rates2 = ADSources.getFarmRates(adEnv, 1)
check("S19 mælk 10 køer × 360", rates2[MILK].production, 3600)
check("S19 gylle via navn", rates2[SLURRY].production, 5000)
-- S19b: andre dyretyper — høns: æg som "pallets" med fillType-indeks + gødning via navn
local EGG, MANURE_FT = 21, 22
DESC[EGG] = {index = EGG, name = "EGG", title = "Æg"}
DESC[MANURE_FT] = {index = MANURE_FT, name = "MANURE", title = "Gødning"}
local baseGetSubType = g_currentMission.animalSystem.getSubTypeByIndex
g_currentMission.animalSystem.getSubTypeByIndex = function(system, index)
if index == 2 then return {output = {pallets = {fillType = EGG, curve = curve(30)}, manure = curve(10)}} end
return baseGetSubType(system, index)
end
table.insert(g_currentMission.placeableSystem.placeables, {
rootNode = {x = 6000, z = 6000}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Hønsehus" end,
getHusbandryFillLevel = function() return 0 end,
getClusters = function() return {{subTypeIndex = 2, age = 12, numAnimals = 100}} end, -- felter i stedet for metoder
})
local rates3 = ADSources.getFarmRates(adEnv, 1)
check("S19b æg 100 høns × 30", rates3[EGG] and rates3[EGG].production, 3000)
check("S19b gødning via navn", rates3[MANURE_FT] and rates3[MANURE_FT].production, 1000)
check("S19b mælk uændret", rates3[MILK].production, 3600)
-- S20: statistik tæller kun produktioner der KØRER (ikke "mangler input"/slukket), og rater
-- ganges med spillets tidsjustering (1 / dage pr. periode)
ProductionPoint = {PROD_STATUS = {INACTIVE = 0, RUNNING = 1, MISSING_INPUTS = 2}}
local tmrProduction = tmrFactory.spec_productionPoint.productionPoint.productions[1]
tmrProduction.status = ProductionPoint.PROD_STATUS.MISSING_INPUTS
local idle = ADSources.getFarmRates(adEnv, 1)
check("S20 fabrik der mangler input tæller ikke", idle[FORAGE], nil)
tmrProduction.status = ProductionPoint.PROD_STATUS.RUNNING
g_currentMission.environment = {timeAdjustment = 0.5}
local running = ADSources.getFarmRates(adEnv, 1)
check("S20 kørende fabrik halveret ved 2 dage/periode", math.floor(running[FORAGE].production + 0.5), 576000)
check("S20 stald halveret", running[STRAW].consumption, 6000)
g_currentMission.environment, ProductionPoint = nil, nil
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-206
View File
@@ -1,206 +0,0 @@
-- Kør: luajit tests/test_adStorageStats.lua (fra repo-roden)
-- Lagerstatistik pr. vare: fyldning, værdi nu / ved toppris, gårdens samlede beholdning, dage tilbage.
dofile("FS25_ADSmartPickup/adStorageStats.lua")
local S = ADStorageStats
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
-- ST1: fuld beregning
local st = S.itemStats({fillType = 5, level = 150000, capacity = 180000}, 412000, {consumption = 130000, production = 48000}, {price = 1.6, top = 2.0})
check("ST1 procent", st.percent, 83)
check("ST1 værdi nu", st.valueNow, 240000)
check("ST1 værdi top", st.valueTop, 300000)
check("ST1 gårdens beholdning", st.farmTotal, 412000)
check("ST1 dage på netto (412k / 82k)", math.floor(st.days * 10 + 0.5) / 10, 5.0)
check("ST1 netto pr. døgn", st.net, -82000)
-- ST2: ingen salgssted, intet forbrug, ingen kapacitet
local st2 = S.itemStats({fillType = 6, level = 5000, capacity = 0}, 5000, nil, nil)
check("ST2 ingen pris -> nil", st2.valueNow, nil)
check("ST2 intet forbrug -> dage nil", st2.days, nil)
check("ST2 kapacitet 0 -> procent nil", st2.percent, nil)
-- ST3: bygningens samlede værdi (kun varer med pris)
local total = S.buildingTotals({st, st2, S.itemStats({fillType = 7, level = 1000, capacity = 2000}, 1000, nil, {price = 2, top = 3})})
check("ST3 samlet nu", total.valueNow, 242000)
check("ST3 samlet top", total.valueTop, 303000)
-- ST4: gårdens totaler fra inventar
local totals = S.farmTotals({{entries = {{fillType = 5, level = 100}, {fillType = 6, level = 5}}}, {entries = {{fillType = 5, level = 50}}}})
check("ST4 sum pr. vare", totals[5], 150)
check("ST4 anden vare", totals[6], 5)
-- ST5: foder-forbrug i et fælles trug fordeles efter truget indhold; tomt trug -> bedste foder
local split = S.splitTroughConsumption(2400, {[116] = 3000, [7] = 1000}, {[116] = 1.0, [7] = 0.8})
check("ST5 TMR 75 %", split[116], 1800)
check("ST5 ensilage 25 %", split[7], 600)
local empty = S.splitTroughConsumption(2400, {}, {[116] = 1.0, [7] = 0.8})
check("ST5 tomt trug -> bedste foder", empty[116], 2400)
check("ST5 tomt trug -> intet på ensilage", empty[7], nil)
-- ST6: gitter — varer i rækker á n celler, tom liste giver én tom række
local rows = S.chunk({"a", "b", "c", "d"}, 3)
check("ST6 to rækker", #rows, 2)
check("ST6 første række 3", #rows[1], 3)
check("ST6 anden række 1", rows[2][1], "d")
check("ST6 tom liste -> én tom række", #S.chunk({}, 3), 1)
-- ST7: rate-kolonne: forbrug med minus, produktion med plus, intet -> streg
local fmt = function(v) return string.format("%.1fk", v / 1000) end
check("ST7 begge", S.rateText(14400, 3600, fmt), "-14.4k +3.6k")
check("ST7 kun prod", S.rateText(0, 3600, fmt), "+3.6k")
check("ST7 kun forbrug", S.rateText(2000, 0, fmt), "-2.0k")
check("ST7 intet", S.rateText(0, 0, fmt), "-")
-- ST8: fane pr. bygningstype — filtrér inventar på kind, rækkefølgen bevares
local inv = {{kind = "silo", name = "A"}, {kind = "factory", name = "B"}, {kind = "silo", name = "C"}}
local silos = S.filterKind(inv, "silo")
check("ST8 to siloer", #silos, 2)
check("ST8 rækkefølge", silos[2].name, "C")
check("ST8 ingen stalde", #S.filterKind(inv, "husbandry"), 0)
-- ST9: produktion ≥ forbrug -> beholdningen stiger, ingen dage
local rising = S.itemStats({fillType = 8, level = 1000, capacity = 2000}, 6700000, {consumption = 11500000, production = 19200000}, nil)
check("ST9 stiger -> dage nil", rising.days, nil)
check("ST9 stiger-flag", rising.rising, true)
check("ST1 falder -> ikke stiger", st.rising, false)
check("ST2 intet forbrug -> ikke stiger", st2.rising, false)
-- ST3: palleskur — procent af PLADSER (24 paller af 4000), ikke liter mod pladsantal (600 %)
local egg = S.itemStats({fillType = 8, level = 24000, capacity = 4000000, count = 24, slots = 4000}, 24000, nil, nil)
check("ST3 paller med", egg.count, 24)
check("ST3 pladser med", egg.slots, 4000)
check("ST3 procent af pladser", egg.percent, 1)
-- ---- v1.52 vareoversigt ----
-- SV1: gruppér fanens bygninger pr. vare; bygninger sorteret mest først, varer efter titel
local titles = {[31] = "Straw", [5] = "Sugarbeet", [9] = "Milk"}
local titleOf = function(ft) return titles[ft] end
local siloInv = {
{name = "Farma #1", placeable = "p1", entries = {{fillType = 31, level = 300, capacity = 1000}, {fillType = 5, level = 50, capacity = 100}}},
{name = "Farma #2", placeable = "p2", entries = {{fillType = 31, level = 700, capacity = 1000}}},
{name = "Tom", placeable = "p3", entries = {}},
}
local groups = S.groupByFillType(siloInv, titleOf)
check("SV1 to varer", #groups, 2)
check("SV1 alfabetisk: Straw først", groups[1].title, "Straw")
check("SV1 straw total", groups[1].level, 1000)
check("SV1 straw kapacitet", groups[1].capacity, 2000)
check("SV1 straw procent", groups[1].percent, 50)
check("SV1 to siloer med straw", #groups[1].buildings, 2)
check("SV1 flest først", groups[1].buildings[1].name, "Farma #2")
check("SV1 bygning procent", groups[1].buildings[1].percent, 70)
check("SV1 placeable med", groups[1].buildings[2].placeable, "p1")
check("SV1 tomt inventar", #S.groupByFillType({}, titleOf), 0)
-- SV2: gårdens kapacitet pr. vare
local caps = S.farmCapacities(siloInv)
check("SV2 kapacitet straw", caps[31], 2000)
-- SV3: varens statistik: døgn til tom på gårdens netto, pris i % af top
local straw = S.groupStats(groups[1], {level = 1500, capacity = 3000}, {consumption = 1000, production = 0}, {price = 0.5, top = 1.0})
check("SV3 døgn til tom", straw.days, 1.5)
check("SV3 døgn til fuld nil (falder)", straw.daysToFull, nil)
check("SV3 værdi nu", straw.valueNow, 500)
check("SV3 pris % af top", straw.priceRatio, 0.5)
check("SV3 tomt-niveau gul (< 3 døgn)", straw.emptyLevel, "yellow")
check("SV3 samlet niveau", straw.severity, "yellow")
-- SV4: produceret vare -> døgn til fuld på gårdens ledige kapacitet
local sugar = S.groupStats({fillType = 5, title = "Sugar", level = 950, capacity = 1000, percent = 95, buildings = {}},
{level = 950, capacity = 1000}, {consumption = 0, production = 100}, nil)
check("SV4 døgn til fuld", sugar.daysToFull, 0.5)
check("SV4 producerer", sugar.producing, true)
check("SV4 fuld-niveau rød (95 %)", sugar.fullLevel, "red")
check("SV4 ingen pris -> nil", sugar.valueNow, nil)
-- SV5: fuld lagervare der ikke produceres er ikke et problem
local stock = S.groupStats({fillType = 31, title = "Straw", level = 990, capacity = 1000, percent = 99, buildings = {}}, nil, nil, nil)
check("SV5 fuld lager uden produktion -> intet", stock.fullLevel, nil)
check("SV5 severity nil", stock.severity, nil)
-- SV6: tærskler
check("SV6 < 1 døgn rød", S.emptyLevel(0.5), "red")
check("SV6 < 3 døgn gul", S.emptyLevel(2.9), "yellow")
check("SV6 5 døgn intet", S.emptyLevel(5), nil)
check("SV6 nil intet", S.emptyLevel(nil), nil)
check("SV6 fuld 85 % gul", S.fullLevel(85, true), "yellow")
check("SV6 fuld 84 % intet", S.fullLevel(84, true), nil)
-- SV7: bygningsniveau: stald-foder næsten tomt, fabrik-output næsten fuldt
check("SV7 stald 8 % rød", S.buildingLevel({percent = 8}, "husbandry", false), "red")
check("SV7 stald 20 % gul", S.buildingLevel({percent = 20}, "husbandry", false), "yellow")
check("SV7 stald-output (mælk) 8 % intet", S.buildingLevel({percent = 8}, "husbandry", true), nil)
check("SV7 output 96 % rød", S.buildingLevel({percent = 96, isOutput = true}, "factory", false), "red")
check("SV7 silo 50 % intet", S.buildingLevel({percent = 50}, "silo", false), nil)
-- SV8: kræver handling — rød tom > rød fuld > gul tom > gul fuld > sælg (efter kroner); højst max
local mk = function(t) t.buildings = {}; return t end
local list = S.attentionList({
mk({title = "A", emptyLevel = "yellow", days = 2, consumption = 1}),
mk({title = "B", fullLevel = "red", percent = 97, producing = true}),
mk({title = "C", emptyLevel = "red", days = 0.4, consumption = 1}),
mk({title = "D", priceRatio = 0.95, valueNow = 50000, net = 0}),
mk({title = "E", priceRatio = 0.97, valueNow = 900000, net = 0}),
mk({title = "F", priceRatio = 0.99, valueNow = 900000, net = -5}), -- gården æder den: sælg ikke
mk({title = "G", priceRatio = 0.99, valueNow = 5000, net = 0}), -- for lille
}, 4)
check("SV8 højst 4", #list, 4)
check("SV8 1: rød tom", list[1].stat.title, "C")
check("SV8 1: art", list[1].kind, "empty")
check("SV8 2: rød fuld", list[2].stat.title, "B")
check("SV8 3: gul tom", list[3].stat.title, "A")
check("SV8 4: største salg", list[4].stat.title, "E")
check("SV8 4: art", list[4].kind, "sell")
-- SV9: nøgletal for fanen
local kpi = S.kpis({
mk({valueNow = 100, valueTop = 150, emptyLevel = "red"}),
mk({valueNow = 50, valueTop = 50, fullLevel = "yellow"}),
mk({}),
})
check("SV9 værdi nu", kpi.valueNow, 150)
check("SV9 ekstra ved top", kpi.gain, 50)
check("SV9 løber tør", kpi.emptyCount, 1)
check("SV9 fyldt", kpi.fullCount, 1)
-- SV10: fyldningsbrøk til bjælker, klemt til [0,1]
check("SV10 halv", S.fraction(50), 0.5)
check("SV10 over 100", S.fraction(130), 1)
check("SV10 nil", S.fraction(nil), 0)
-- SV11: skur uden pladsgrænse (kapacitet 0) -> varens procent ukendt, ikke 940 %
local shedGroups = S.groupByFillType({
{name = "A", entries = {{fillType = 9, level = 40, capacity = 100}}},
{name = "B", entries = {{fillType = 9, level = 900, capacity = 0}}},
}, titleOf)
check("SV11 procent nil ved ubegrænset skur", shedGroups[1].percent, nil)
-- "kræver handling" -> opret flow: hvem bruger varen, og hjælper et flow overhovedet? (brugerønske 29/9)
do
local inv = {
{name = "Mølle", kind = "factory", placeable = "pMill", entries = {{fillType = 7, level = 1000, isOutput = false}, {fillType = 8, level = 50, isOutput = true}}},
{name = "Silo", kind = "silo", placeable = "pSilo", entries = {{fillType = 7, level = 90000}}},
{name = "Kostald", kind = "husbandry", placeable = "pBarn", entries = {{fillType = 7, level = 500, isOutput = false}}},
}
local consumers = S.consumersOf(inv, 7)
check("forbrugere: fabrik + stald (ikke silo)", #consumers, 2)
check("forbrugere: output tæller ikke", #S.consumersOf(inv, 8), 0)
check("råd: eksisterende flow -> åbn det", S.flowAdvice({farmLevel = 91500, consumerLevel = 1500, perDay = 20000, consumerCount = 2, existingRunId = 4}), "open")
check("råd: lager andre steder -> opret flow", S.flowAdvice({farmLevel = 91500, consumerLevel = 1500, perDay = 20000, consumerCount = 2}), "flow")
check("råd: intet at flytte -> producér/køb", S.flowAdvice({farmLevel = 12700, consumerLevel = 12700, perDay = 228000, consumerCount = 1}), "produce")
check("råd: lidt andre steder, stort forbrug -> producér/køb", S.flowAdvice({farmLevel = 20000, consumerLevel = 12000, perDay = 228000, consumerCount = 1}), "produce")
check("råd: ingen forbrugere -> intet", S.flowAdvice({farmLevel = 5000, consumerLevel = 0, perDay = 0, consumerCount = 0}), "none")
end
-- "sælg nu" -> udkørsels-flow (brugerønske 29/9)
check("salg: eksisterende salgsflow -> åbn", S.sellAdvice({sourceCount = 1, existingRunId = 3}), "open")
check("salg: ingen -> opret flow", S.sellAdvice({sourceCount = 2}), "flow")
check("salg: ingen bygning med varen -> intet", S.sellAdvice({sourceCount = 0}), "none")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-266
View File
@@ -1,266 +0,0 @@
-- Kør: luajit tests/test_adSupplyPlanner.lua (fra repo-roden)
-- Tester de rene behovs-funktioner; ingen spil- eller AutoDrive-mocks nødvendige.
dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")
local HAY, GRASS, SILAGE = 20, 21, 22
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
-- scoreNeed: tommere lager scorer højere; ugyldig kapacitet giver 0
check("tomt scorer højere end halvfuldt", ADSupplyPlanner.scoreNeed(0, 1000, 1000) > ADSupplyPlanner.scoreNeed(500, 1000, 500), true)
check("kapacitet 0 giver score 0", ADSupplyPlanner.scoreNeed(0, 0, 0), 0)
-- rankNeeds: mest trængende først, fulde springes over, input muteres ikke
local needs = {
{fillType = HAY, level = 80000, capacity = 100000},
{fillType = GRASS, level = 20000, capacity = 100000},
{fillType = SILAGE, level = 99500, capacity = 100000},
}
local ranked = ADSupplyPlanner.rankNeeds(needs)
check("fuld ingrediens springes over", #ranked, 2)
check("mest trængende først", ranked[1].fillType, GRASS)
check("dernæst næstmest trængende", ranked[2].fillType, HAY)
check("freeSpace beregnes", ranked[1].freeSpace, 80000)
check("input muteres ikke", needs[1].freeSpace, nil)
check("tom liste giver tom liste", #ADSupplyPlanner.rankNeeds({}), 0)
-- rankNeeds: deterministisk ved lige score (laveste fillType først)
local tie = ADSupplyPlanner.rankNeeds({
{fillType = SILAGE, level = 0, capacity = 5000},
{fillType = HAY, level = 0, capacity = 5000},
})
check("lige score -> laveste fillType først", tie[1].fillType, HAY)
-- resolveIngredients
local multi = {HAY, GRASS}
local resolved = ADSupplyPlanner.resolveIngredients(nil, multi)
check("multi-valg bliver ingrediensliste", #resolved, 2)
check("listen er en kopi", resolved ~= multi, true)
check("enkelt-valg uden tilstand -> nil", ADSupplyPlanner.resolveIngredients(nil, {HAY}), nil)
local state = {ingredients = {HAY, GRASS, SILAGE}, narrowedTo = GRASS}
check("indsnævret valg genbruger gemt liste", #ADSupplyPlanner.resolveIngredients(state, {GRASS}), 3)
check("bruger valgte andet -> nil", ADSupplyPlanner.resolveIngredients(state, {HAY}), nil)
check("nyt multi-valg erstatter gemt liste", #ADSupplyPlanner.resolveIngredients(state, {HAY, SILAGE}), 2)
check("ugyldigt valg -> nil", ADSupplyPlanner.resolveIngredients(state, nil), nil)
-- Kildestrategi: lager-siloer tømmes mindst-først; tanke der løber fulde (mælk, gylle) fyldest-først
check("mælk hentes fyldest-først", ADSupplyPlanner.prefersFullestSource({"MILK"}), true)
check("gylle hentes fyldest-først", ADSupplyPlanner.prefersFullestSource({"LIQUIDMANURE"}), true)
check("roer hentes mindst-først", ADSupplyPlanner.prefersFullestSource({"SUGARBEET"}), false)
check("blandet valg -> mindst-først", ADSupplyPlanner.prefersFullestSource({"MILK", "WHEAT"}), false)
check("ukendt/tomt valg -> mindst-først", ADSupplyPlanner.prefersFullestSource({}), false)
check("mindst-først: lavere niveau er bedre", ADSupplyPlanner.isBetterSource(100, 500, false), true)
check("mindst-først: højere niveau er ikke bedre", ADSupplyPlanner.isBetterSource(900, 500, false), false)
check("fyldest-først: højere niveau er bedre", ADSupplyPlanner.isBetterSource(900, 500, true), true)
check("første kandidat er altid bedre end ingen", ADSupplyPlanner.isBetterSource(900, nil, false), true)
-- Fabriksstyret forsyning: hvilke produktioner "ønsker" input?
local MISSING_INPUTS, INACTIVE = 2, 0
check("tændt produktion ønskes", ADSupplyPlanner.isProductionWanted(true, INACTIVE, MISSING_INPUTS), true)
check("auto-slukket pga. mangel ønskes stadig", ADSupplyPlanner.isProductionWanted(false, MISSING_INPUTS, MISSING_INPUTS), true)
check("manuelt slukket ønskes ikke", ADSupplyPlanner.isProductionWanted(false, INACTIVE, MISSING_INPUTS), false)
check("ukendt status uden tænd -> ikke ønsket", ADSupplyPlanner.isProductionWanted(false, nil, MISSING_INPUTS), false)
-- collectProductionDemand: liter/min pr. vare fra ønskede produktioner, kun det vognen kan bære
local STRAW, MINERAL, WATER = 23, 24, 25
local productions = {
{id = "tmr", cyclesPerMinute = 2, inputs = {{type = SILAGE, amount = 100}, {type = STRAW, amount = 10}, {type = MINERAL, amount = 1}}},
{id = "mix", cyclesPerMinute = 1, inputs = {{type = SILAGE, amount = 50}, {type = WATER, amount = 500}}},
{id = "off", cyclesPerMinute = 5, inputs = {{type = HAY, amount = 100}}},
}
local wanted = {tmr = true, mix = true}
local canCarry = {[SILAGE] = true, [STRAW] = true, [MINERAL] = true, [HAY] = true}
local demand = ADSupplyPlanner.collectProductionDemand(productions, function(p) return wanted[p.id] == true end, canCarry)
check("forbrug summeres på tværs af produktioner", demand[SILAGE], 250)
check("forbrug ganges med cyklusser/min", demand[STRAW], 20)
check("uønsket produktion tæller ikke", demand[HAY], nil)
check("vare vognen ikke kan bære udelades", demand[WATER], nil)
check("tom produktionsliste -> tomt behov", next(ADSupplyPlanner.collectProductionDemand({}, function() return true end, canCarry)), nil)
check("produktion uden inputs tåles", next(ADSupplyPlanner.collectProductionDemand({{id = "x", cyclesPerMinute = 1}}, function() return true end, canCarry)), nil)
-- minutesToEmpty
check("minutter til tom", ADSupplyPlanner.minutesToEmpty(1000, 250), 4)
check("intet forbrug -> nil", ADSupplyPlanner.minutesToEmpty(1000, 0), nil)
check("ukendt forbrug -> nil", ADSupplyPlanner.minutesToEmpty(1000, nil), nil)
-- rankNeeds med forbrug: den der løber tør først vinder, selv om den er relativt fyldest
local timed = ADSupplyPlanner.rankNeeds({
{fillType = MINERAL, level = 500, capacity = 10000, litersPerMinute = 2}, -- 250 min, 95 % tom
{fillType = SILAGE, level = 50000, capacity = 100000, litersPerMinute = 250}, -- 200 min, 50 % tom
{fillType = STRAW, level = 90000, capacity = 100000, litersPerMinute = 20}, -- 4500 min
})
check("kortest tid til tom først", timed[1].fillType, SILAGE)
check("dernæst næstkortest", timed[2].fillType, MINERAL)
check("minutesToEmpty følger med", timed[1].minutesToEmpty, 200)
check("vare med forbrug slår vare uden", ADSupplyPlanner.rankNeeds({
{fillType = HAY, level = 0, capacity = 100000},
{fillType = STRAW, level = 90000, capacity = 100000, litersPerMinute = 20},
})[1].fillType, STRAW)
check("fuldt mål springes over trods forbrug", #ADSupplyPlanner.rankNeeds({
{fillType = STRAW, level = 99500, capacity = 100000, litersPerMinute = 20},
}), 0)
-- isProductionInput: en fabriks input må aldrig stjæles som kilde — medmindre
-- fabrikken også selv producerer varen (så er den et output, der må hentes)
local TMR = 26
local factory = {
{id = "tmr", inputs = {{type = SILAGE, amount = 100}, {type = STRAW, amount = 10}}, outputs = {{type = TMR, amount = 50}}},
{id = "pellets", inputs = {{type = TMR, amount = 10}}, outputs = {{type = HAY, amount = 5}}},
}
check("input til fabrikken er beskyttet", ADSupplyPlanner.isProductionInput(factory, STRAW), true)
check("output må hentes", ADSupplyPlanner.isProductionInput(factory, HAY), false)
check("både input og output (kæde) -> må hentes", ADSupplyPlanner.isProductionInput(factory, TMR), false)
check("vare fabrikken ikke kender -> ikke beskyttet", ADSupplyPlanner.isProductionInput(factory, WATER), false)
check("ingen produktioner -> ikke beskyttet", ADSupplyPlanner.isProductionInput({}, STRAW), false)
check("produktion uden inputs/outputs tåles", ADSupplyPlanner.isProductionInput({{id = "x"}}, STRAW), false)
-- rankNeeds med læs-størrelse: varer hvor et FULDT læs passer går forud, ellers mest trængende
local sized = ADSupplyPlanner.rankNeeds({
{fillType = STRAW, level = 40000, capacity = 50000, litersPerMinute = 500}, -- 80 min, kun 10000 fri
{fillType = TMR, level = 20000, capacity = 100000, litersPerMinute = 50}, -- 400 min, 80000 fri
}, 40000)
check("fuldt læs der passer vinder over mere trængende der ikke passer", sized[1].fillType, TMR)
check("den der ikke passer bevares som reserve", sized[2].fillType, STRAW)
check("fits markeres", sized[1].fits, true)
check("ikke-fits markeres", sized[2].fits, false)
local noneFit = ADSupplyPlanner.rankNeeds({
{fillType = STRAW, level = 40000, capacity = 50000, litersPerMinute = 500},
{fillType = TMR, level = 90000, capacity = 100000, litersPerMinute = 50},
}, 40000)
check("passer intet -> mest trængende først", noneFit[1].fillType, STRAW)
check("uden læs-størrelse -> som før", ADSupplyPlanner.rankNeeds({{fillType = STRAW, level = 40000, capacity = 50000}})[1].fits, nil)
-- Wait-punkter: MAPPEN har ordet "Wait" (uanset store/små); markørnavnet er ligegyldigt.
-- (Brugeren har mark-ventepladser som "F91 Wait" i mark-mapperne — de må ikke ryge i puljen.)
check("mappen 'Cow Wait' tæller", ADSupplyPlanner.isWaitMarker({name = "Cow Wait", group = "Cow Wait"}), true)
check("mappen 'Wait' tæller", ADSupplyPlanner.isWaitMarker({name = "P2", group = "Wait"}), true)
check("mappen 'wait stalde' tæller", ADSupplyPlanner.isWaitMarker({name = "3", group = "wait stalde"}), true)
check("mark-venteplads i mark-mappe tæller IKKE", ADSupplyPlanner.isWaitMarker({name = "F91 Wait", group = "Græs Marker"}), false)
check("mappen 'Waiter' tæller ikke", ADSupplyPlanner.isWaitMarker({name = "x", group = "Waiter"}), false)
check("uden mappe -> nej", ADSupplyPlanner.isWaitMarker({name = "Wait 1"}), false)
-- nærmeste ledige wait-punkt; candidates = {{marker, x, z}}, claims = {[markerId] = vehicle}
local V1, V2 = {}, {}
local waitPoints = {
{marker = {id = 1, name = "Wait A"}, x = 0, z = 0},
{marker = {id = 2, name = "Wait B"}, x = 100, z = 0},
{marker = {id = 3, name = "Wait C"}, x = 300, z = 0},
}
check("nærmeste ledige vælges", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {}, 10, 0, V1).marker.id, 1)
check("reserveret springes over", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {[1] = V2}, 10, 0, V1).marker.id, 2)
check("egen reservation tæller som ledig", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {[1] = V1}, 10, 0, V1).marker.id, 1)
check("alle optaget -> nil", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {[1] = V2, [2] = V2, [3] = V2}, 10, 0, V1), nil)
check("ingen punkter -> nil", ADSupplyPlanner.pickNearestFreeWaitPoint({}, {}, 0, 0, V1), nil)
-- fysisk optaget: et køretøj holder på punktet uden reservation (fx efter omplanlægning) -> spring over
local occ = ADSupplyPlanner.markOccupiedWaitPoints(waitPoints, {{x = waitPoints[1].x + 2, z = waitPoints[1].z}}, 6)
check("optaget punkt markeres", occ[waitPoints[1].marker.id], true)
check("optaget springes over", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {}, 10, 0, V1, nil, occ).marker.id, 2)
check("egen reservation vinder over optaget (det er en selv)", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {[1] = V1}, 10, 0, V1, nil, occ).marker.id, 1)
check("køretøj langt væk optager intet", next(ADSupplyPlanner.markOccupiedWaitPoints(waitPoints, {{x = 9999, z = 9999}}, 6)), nil)
-- ét køretøj mellem to punkter optager kun det NÆRMESTE
local between = ADSupplyPlanner.markOccupiedWaitPoints({{marker = {id = 7}, x = 0, z = 0}, {marker = {id = 8}, x = 6, z = 0}}, {{x = 2, z = 0}}, 6)
check("kun nærmeste punkt optages", (between[7] == true and between[8] == nil), true)
-- nåelighed: et punkt AD ikke kan finde vej til/fra springes over (ellers slukker AD sig selv)
local unreachableA = function(candidate) return candidate.marker.id ~= 1 end
check("unåeligt nærmeste springes over", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {}, 10, 0, V1, unreachableA).marker.id, 2)
check("intet nåeligt -> nil", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {}, 10, 0, V1, function() return false end), nil)
-- Hjem fra Wait: fri plads >= min(rest, halvdelen af målets kapacitet)
-- (en 700.000 l-vogn kan aldrig tømme HELE resten i en 300.000 l-stald)
check("hele resten passer -> hjem", ADSupplyPlanner.canUnloadRest(35000, 35000, 300000), true)
check("resten passer ikke endnu -> vent", ADSupplyPlanner.canUnloadRest(5000, 20000, 50000), false)
check("kæmpe rest: halv stald fri -> hjem", ADSupplyPlanner.canUnloadRest(150000, 700000, 300000), true)
check("kæmpe rest: under halv stald fri -> vent", ADSupplyPlanner.canUnloadRest(100000, 700000, 300000), false)
check("ukendt kapacitet -> kun hele resten tæller", ADSupplyPlanner.canUnloadRest(100000, 700000, nil), false)
-- canUnloadRest med run-tærskel: "under 30 %" = fri >= 70 % af kapaciteten
check("tærskel 30 %: 60 % fri er ikke nok", ADSupplyPlanner.canUnloadRest(60000, 90000, 100000, 0.7), false)
check("tærskel 30 %: 70 % fri er nok", ADSupplyPlanner.canUnloadRest(70000, 90000, 100000, 0.7), true)
check("hele resten passer vinder altid", ADSupplyPlanner.canUnloadRest(5000, 5000, 100000, 0.7), true)
check("uden tærskel = 50 %-reglen", ADSupplyPlanner.canUnloadRest(50000, 90000, 100000), true)
-- rankNeeds: fælles trug (scoreOverride) + fodervægt (priority): TMR før hø før græs,
-- men et separat halmlager der er tommere vinder over truget
local TMR, HAYX = 40, 41
local trough = ADSupplyPlanner.rankNeeds({
{fillType = GRASS, level = 0, capacity = 30000, scoreOverride = 0.3, priority = 0.4},
{fillType = TMR, level = 0, capacity = 30000, scoreOverride = 0.3, priority = 1.0},
{fillType = HAYX, level = 0, capacity = 30000, scoreOverride = 0.3, priority = 0.8},
{fillType = HAY, level = 20000, capacity = 100000}, -- halm-agtigt separat lager, 80 % tomt
})
check("separat lager 80 % tomt vinder over trug 30 % tomt", trough[1].fillType, HAY)
check("i truget: TMR (1.0) først", trough[2].fillType, TMR)
check("så hø (0.8)", trough[3].fillType, HAYX)
check("så græs (0.4)", trough[4].fillType, GRASS)
check("scoreOverride bruges som score", trough[2].score, 0.3)
-- Bedste foder først: lavere foder (ensilage 0,8) må kun hentes når truget er nødlidende
-- (fri ≥ EMERGENCY_TROUGH_FREE af kapaciteten), hvis der er bedre foder (TMR 1,0) i samme trug
local tmrNeed = {fillType = 116, priority = 1.0, score = 0.6, trough = true}
local silageNeed = {fillType = 7, priority = 0.8, score = 0.6, trough = true}
local strawNeed = {fillType = 31, priority = 0, score = 0.9}
check("NF1 bedste foder tillades altid", ADSupplyPlanner.isFoodAllowed(tmrNeed, 1.0), true)
check("NF1 ensilage afvist ved halvfuldt trug", ADSupplyPlanner.isFoodAllowed(silageNeed, 1.0), false)
silageNeed.score = 0.8
check("NF1 ensilage tilladt når truget er næsten tomt", ADSupplyPlanner.isFoodAllowed(silageNeed, 1.0), true)
check("NF1 ikke-foder (halm) påvirkes ikke", ADSupplyPlanner.isFoodAllowed(strawNeed, 1.0), true)
check("NF1 intet bedre foder på listen -> tilladt", ADSupplyPlanner.isFoodAllowed({fillType = 7, priority = 0.8, score = 0.1, trough = true}, 0.8), true)
check("NF1 bedste foderprioritet", ADSupplyPlanner.bestFoodPriority({tmrNeed, silageNeed, strawNeed}), 1.0)
-- Læs på vej (andre traktorer): lægges oven i målets niveau; foder deler trug -> hele trugets fri plads falder
local pendingNeeds = {
{fillType = 31, level = 5000, capacity = 20000}, -- halm
{fillType = 116, level = 0, capacity = 30000, scoreOverride = 1.0, troughCapacity = 30000}, -- TMR, tomt trug
{fillType = 7, level = 0, capacity = 30000, scoreOverride = 1.0, troughCapacity = 30000}, -- ensilage
}
local adjusted = ADSupplyPlanner.applyPending(pendingNeeds, {[116] = 20000, [31] = 20000})
check("AP halm: niveau + på vej, højst kapacitet", adjusted[1].level, 20000)
check("AP TMR: niveau + på vej", adjusted[2].level, 20000)
check("AP TMR: kapacitet = niveau + resterende fri", adjusted[2].capacity, 30000)
check("AP ensilage: truget har kun 10000 fri tilbage", adjusted[3].capacity - adjusted[3].level, 10000)
check("AP trug-score falder", math.floor(adjusted[3].scoreOverride * 100 + 0.5), 33)
check("AP input urørt", pendingNeeds[2].level, 0)
check("AP intet på vej -> samme værdier", ADSupplyPlanner.applyPending(pendingNeeds, {})[1].level, 5000)
local ranked = ADSupplyPlanner.rankNeeds(adjusted, 8000)
check("AP halm fuld (på vej) -> kun trug-varer tilbage", #ranked, 2)
-- PF: parallelt foder (høns: korn 80 % / roesnitter 20 % af det de spiser) — hver gruppe har sin andel af truget
do
local WHEAT, BARLEY, BEETCUT = 1, 2, 3
local groups = {{fillTypes = {WHEAT, BARLEY}, eatWeight = 0.8}, {fillTypes = {BEETCUT}, eatWeight = 0.2}}
local levels = {[WHEAT] = 60000, [BARLEY] = 20000, [BEETCUT] = 0}
local lvl, cap = ADSupplyPlanner.parallelFoodShare(groups, levels, 100000, BEETCUT)
check("PF roesnitter: andel 20k, har 0", lvl .. "/" .. cap, "0/20000")
lvl, cap = ADSupplyPlanner.parallelFoodShare(groups, levels, 100000, BARLEY)
check("PF korn-gruppen samlet: 80k af 80k", lvl .. "/" .. cap, "80000/80000")
check("PF ukendt vare -> nil", ADSupplyPlanner.parallelFoodShare(groups, levels, 100000, 99), nil)
end
-- cargoFor: læs der allerede er på vognen ved start tæller som "på vej" (in-game 27/9: tre fyldte
-- biogas-traktorer startet fra fanen kørte alle til Drop 1 — ingen var registreret på vej)
check("CF run-vare på vognen", select(2, ADSupplyPlanner.cargoFor(SILAGE, 427215, {SILAGE})), 427215)
check("CF vare uden for run'et -> nil", ADSupplyPlanner.cargoFor(HAY, 5000, {SILAGE}), nil)
check("CF tom vogn -> nil", ADSupplyPlanner.cargoFor(SILAGE, 0, {SILAGE}), nil)
check("CF ukendt vare -> nil", ADSupplyPlanner.cargoFor(nil, 5000, {SILAGE}), nil)
-- kilde med nok til et rigtigt læs: mindst fyldte først kun blandt kilder med >= 25 % af et læs; ellers fyldeste
-- (in-game 29/9: 4 foder-traktorer hentede 2-10k l i en gæringssilo der løbende fyldes, og kørte "tomme")
local SP = ADSupplyPlanner
check("kilde-grænse: 25 % af læsset", SP.ampleLevel(1000000), 250000)
check("kilde-grænse: ukendt læs -> kun minimum", SP.ampleLevel(nil), SP.MIN_AMPLE_LITERS)
check("kilde-grænse: lille vogn -> mindst minimum", SP.ampleLevel(10000), SP.MIN_AMPLE_LITERS)
check("vælg: næsten tom taber til rigelig", SP.isBetterSource(10000, 12000000, false, 250000), false)
check("vælg: blandt rigelige mindst fyldte", SP.isBetterSource(300000, 12000000, false, 250000), true)
check("vælg: ingen rigelige -> fyldeste", SP.isBetterSource(9000, 2000, false, 250000), true)
check("vælg: løber-fuld-varer uændret (fyldeste)", SP.isBetterSource(12000000, 300000, true, 250000), true)
check("vælg: første kandidat", SP.isBetterSource(10, nil, false, 250000), true)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-166
View File
@@ -1,166 +0,0 @@
-- Kør: luajit tests/test_adWaitPool.lua (fra repo-roden)
-- Wait-puljen: kandidater pr. mappe, reservationer, nærmeste ledige, rutetjek, redirect.
dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")
dofile("FS25_ADSmartPickup/adWaitPool.lua")
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
local markers, wayPoints = {}, {}
local function addMarker(id, name, group, x, z)
wayPoints[id] = {id = id, x = x, z = z, incoming = {}}
local marker = {id = id, markerIndex = #markers + 1, name = name, group = group}
table.insert(markers, marker)
return marker
end
local unreachable = {}
local adEnv = {
ADGraphManager = {
getMapMarkers = function() return markers end,
getMapMarkerByWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id then return m end end end,
getWayPointById = function(_, id) return wayPoints[id] end,
getPathTo = function(_, vehicle, id) if unreachable[id] then return {} end return {{id = id}} end,
pathFromTo = function(_, from, to) if unreachable[to] then return {} end return {{id = to}} end,
},
}
getWorldTranslation = function(node) return node.x, 0, node.z end
addMarker(10, "cow wait 1", "cow wait", 0, 0)
addMarker(11, "cow wait 2", "cow wait", 50, 0)
addMarker(20, "Wait 3", "General Wait", 10, 0)
addMarker(30, "silo 1", "silo", 5, 0)
-- W1: kandidater
check("W1 kun mappen", #ADWaitPool.getCandidates(adEnv, "cow wait"), 2)
check("W1 tom pulje = alle Wait-mapper", #ADWaitPool.getCandidates(adEnv, ""), 3)
check("W1 nil = alle", #ADWaitPool.getCandidates(adEnv, nil), 3)
-- W2: nærmeste ledige fra vognens position; reserverede springes over; egen reservation OK
local v1, v2 = {name = "v1", rootNode = {x = 45, z = 0}}, {name = "v2", rootNode = {x = 45, z = 0}}
check("W2 nærmeste", ADWaitPool.pickFree(adEnv, v1, "cow wait").marker.id, 11)
ADWaitPool.claim(11, v1)
check("W2 reserveret springes over", ADWaitPool.pickFree(adEnv, v2, "cow wait").marker.id, 10)
check("W2 egen reservation genbruges", ADWaitPool.pickFree(adEnv, v1, "cow wait").marker.id, 11)
-- W3: claim / owner / release
check("W3 ejer", ADWaitPool.getOwner(11), v1)
ADWaitPool.releaseVehicle(v1)
check("W3 frigivet", ADWaitPool.getOwner(11), nil)
check("W3 nu ledig for v2", ADWaitPool.pickFree(adEnv, v2, "cow wait").marker.id, 11)
-- W4: nåelighed
unreachable[11] = true
check("W4 uopnåelig nærmeste -> næste", ADWaitPool.pickFree(adEnv, v2, "cow wait", function(c) return ADWaitPool.hasRouteTo(adEnv, v2, c.marker.id) end).marker.id, 10)
check("W4 hasRouteTo false", ADWaitPool.hasRouteTo(adEnv, v2, 11), false)
check("W4 hasRouteBothWays true", ADWaitPool.hasRouteBothWays(adEnv, v2, 10, 30), true)
check("W4 hasRouteBothWays hjem uopnåelig", ADWaitPool.hasRouteBothWays(adEnv, v2, 10, 11), false)
unreachable[11] = nil
check("W4 intet punkt -> nil", ADWaitPool.pickFree(adEnv, v2, "cow wait", function() return false end), nil)
-- W5: getClaims sorteret på mappe, dernæst id
ADWaitPool.claim(20, v2)
ADWaitPool.claim(10, v1)
local claims = ADWaitPool.getClaims(adEnv)
check("W5 to reservationer", #claims, 2)
check("W5 første = General Wait", claims[1].marker.group, "General Wait")
check("W5 anden = cow wait 1", claims[2].marker.id, 10)
ADWaitPool.releaseVehicle(v1)
ADWaitPool.releaseVehicle(v2)
check("W5 alt frigivet", #ADWaitPool.getClaims(adEnv), 0)
-- W6: redirect stopper trigger-aktivitet og sætter sti
local stopped = {}
local task = {
destinationID = 99, state = 1,
vehicle = {ad = {
trailerModule = {isActiveAtTrigger = function() return true end, stopUnloading = function() stopped.unload = true end, stopLoading = function() stopped.load = true end},
drivePathModule = {setPathTo = function(_, id) stopped.pathTo = id end},
}},
}
ADWaitPool.STATE_DRIVING = 2
ADWaitPool.redirect(task, 10)
check("W6 destination", task.destinationID, 10)
check("W6 state driving", task.state, 2)
check("W6 setPathTo", stopped.pathTo, 10)
check("W6 stopUnloading", stopped.unload, true)
-- WR: AD's rutesøgning siger nej, men vejnettet har en vej fra traktorens nærmeste waypoint -> godtages
-- (in-game 24/9: 'Æg storage' ved Chicken Wait 1, "ingen vej til 'Chicken Storage Unload'" trods vej)
do
wayPoints[901] = {id = 901, x = 0, z = 0, out = {902}}
wayPoints[902] = {id = 902, x = 5, z = 0, out = {903}}
wayPoints[903] = {id = 903, x = 10, z = 0, out = {}}
wayPoints[904] = {id = 904, x = 20, z = 0, out = {}} -- ingen indgang
unreachable[903], unreachable[904] = true, true
local vehicle = {getClosestWayPoint = function() return 901, 1 end}
check("WR AD nej, vejnettet ja -> nåelig", ADWaitPool.hasRouteTo(adEnv, vehicle, 903), true)
check("WR hverken AD eller vejnet -> ikke nåelig", ADWaitPool.hasRouteTo(adEnv, vehicle, 904), false)
check("WR uden nærmeste waypoint -> AD's svar", ADWaitPool.hasRouteTo(adEnv, {}, 903), false)
unreachable[903], unreachable[904] = nil, nil
end
-- RC: rutesvar gemmes 30 s pr. mål og 10 m-område (holdende vogne spurgte hvert 5. s -> hak hvert 5. s)
do
local pathCalls = 0
local realGetPathTo = adEnv.ADGraphManager.getPathTo
adEnv.ADGraphManager.getPathTo = function(self, vehicle, id) pathCalls = pathCalls + 1; return realGetPathTo(self, vehicle, id) end
local clock = 0
netGetTime = function() return clock end
local positions = {[500] = {100, 100}}
getWorldTranslation = function(node) local p = positions[node]; return p[1], 0, p[2] end
local parked = {rootNode = 500, getClosestWayPoint = function() return 901, 1 end}
ADWaitPool.hasRouteTo(adEnv, parked, 10)
ADWaitPool.hasRouteTo(adEnv, parked, 10)
check("RC samme svar genbruges", pathCalls, 1)
positions[500] = {104, 103}
ADWaitPool.hasRouteTo(adEnv, parked, 10)
check("RC samme 10 m-område genbruges", pathCalls, 1)
positions[500] = {160, 100}
ADWaitPool.hasRouteTo(adEnv, parked, 10)
check("RC nyt sted -> nyt svar", pathCalls, 2)
clock = 31000
ADWaitPool.hasRouteTo(adEnv, parked, 10)
check("RC efter 30 s -> nyt svar", pathCalls, 3)
local first = ADWaitPool.hasRouteTo(adEnv, parked, 11)
check("RC andet mål -> nyt svar", pathCalls, 4)
check("RC samme svar igen", ADWaitPool.hasRouteTo(adEnv, parked, 11), first)
check("RC uden nyt kald", pathCalls, 4)
adEnv.ADGraphManager.getPathTo = realGetPathTo
netGetTime = nil
end
-- MR: rute markør->markør gemmes (uafhængig af hvor vognen er) — hak 26/9: 30-39 ms pr. rutetjek
do
local calls = 0
local realFromTo = adEnv.ADGraphManager.pathFromTo
adEnv.ADGraphManager.pathFromTo = function(self, from, to) calls = calls + 1; return realFromTo(self, from, to) end
local savedClock = netGetTime
local t = 1000
netGetTime = function() return t end
ADWaitPool.resetMarkerRoutes()
local far = {name = "far", rootNode = {x = 5000, z = 5000}}
check("MR første: rute", ADWaitPool.hasRouteBothWays(adEnv, v2, 10, 30), true)
local afterFirst = calls
t = t + 60000
check("MR anden fra en helt anden position: samme svar", ADWaitPool.hasRouteBothWays(adEnv, far, 10, 30), true)
check("MR anden gang ingen ny ruteberegning", calls, afterFirst)
adEnv.ADGraphManager.pathFromTo = realFromTo
netGetTime = savedClock
end
-- ankerpunkt for "All": midt mellem flowets kilder (in-game 29/9: Milk Store ventede ved sukkerroeknuseren)
local ax, az = ADWaitPool.anchorOf({{x = 0, z = 0}, {x = 100, z = 50}})
check("anker: midtpunkt x", ax, 50)
check("anker: midtpunkt z", az, 25)
check("anker: ingen punkter", ADWaitPool.anchorOf({}), nil)
check("anker: nil", ADWaitPool.anchorOf(nil), nil)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-33
View File
@@ -1,33 +0,0 @@
-- Kør: luajit tests/test_fieldStatus.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFieldModel.lua")
dofile("FS25_ADSmartPickup/adFieldView.lua")
local V = ADFieldView
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
check("mangler markør", V.fieldStatus({hasMarker = false}), "error")
check("arbejder", V.fieldStatus({hasMarker = true, working = true}), "running")
check("arbejder men venter på maskine", V.fieldStatus({hasMarker = true, working = true, reasonKey = "spu_field_reason_allBusy"}), "waiting")
check("for bred", V.fieldStatus({hasMarker = true, working = true, reasonKey = "spu_field_reason_tooWide"}), "error")
check("ingen maskine", V.fieldStatus({hasMarker = true, reasonKey = "spu_field_reason_noRig"}), "error")
check("regn", V.fieldStatus({hasMarker = true, working = true, reasonKey = "spu_field_reason_rain"}), "waiting")
check("venter på spilleren (høster)", V.fieldStatus({hasMarker = true, awaiting = true}), "waiting")
check("start bestilt", V.fieldStatus({hasMarker = true, startRequested = true}), "waiting")
check("i ro", V.fieldStatus({hasMarker = true}), "stopped")
-- Start må ikke bestilles på en mark uden markør (knappen var før deaktiveret; review 29/9)
check("kan starte: med markør", V.canStart({hasMarker = true, working = false}), true)
check("kan starte: uden markør -> nej", V.canStart({hasMarker = false, working = false}), false)
check("kan stoppe: uden markør men i gang", V.canStart({hasMarker = false, working = true}), true)
-- global pause (Stop alt): arbejdstrin uden maskiner = stoppet, [Start] genoptager kun den mark (in-game 29/9)
check("pause: arbejdstrin -> stoppet", V.fieldStatus({hasMarker = true, working = true, paused = true}), "stopped")
check("pause: genoptaget mark -> kører", V.fieldStatus({hasMarker = true, working = true, paused = true, resumed = true}), "running")
check("pause: mangler markør stadig rød", V.fieldStatus({hasMarker = false, paused = true}), "error")
check("pause: arbejder-tæller falsk", V.isActiveWork({working = true, paused = true}), false)
check("pause: genoptaget arbejder-tæller sand", V.isActiveWork({working = true, paused = true, resumed = true}), true)
check("ingen pause: arbejder", V.isActiveWork({working = true}), true)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-21
View File
@@ -1,21 +0,0 @@
-- Kør: luajit tests/test_flowStatus.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFlowView.lua")
local V = ADFlowView
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
check("kører", V.flowStatus({running = 3, total = 4, canStop = true}), "running")
check("kører men holder (begrundelse)", V.flowStatus({running = 2, total = 2, canStop = true, reason = "intet at hente"}), "waiting")
check("startet, venter på vogn", V.flowStatus({running = 0, total = 2, canStop = true}), "waiting")
check("advarsel", V.flowStatus({running = 1, total = 2, canStop = true, warning = "mål mangler"}), "error")
check("start afvist (ingen vogn)", V.flowStatus({running = 0, total = 2, lastStartFailed = true}), "error")
check("stoppet", V.flowStatus({running = 0, total = 2, stopped = true}), "stopped")
check("aldrig startet", V.flowStatus({running = 0, total = 2}), "stopped")
check("stoppet vinder over gammel afvisning", V.flowStatus({running = 0, total = 2, stopped = true, lastStartFailed = true}), "stopped")
check("farver findes", V.LEVEL_COLORS ~= nil and V.LEVEL_COLORS.waiting ~= nil and V.LEVEL_COLORS.stopped ~= nil, true)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-18
View File
@@ -1,18 +0,0 @@
-- Kør: luajit tests/test_noStrayGlobals.lua (fra repo-roden)
-- Fanger lokale navne der ved en fejl bruges som globale (fx 'implement' uden for sin closure — v1.49 gjorde
-- egne tømmevogne ubrugelige under høst). Læser luajit-bytecode: GGET/GSET af typiske lokale navne.
local SUSPECT = {implement = true, candidate = true, vehicle = true, rig = true, pair = true, assignment = true,
tractor = true, loan = true, field = true, info = true, marker = true, job = true, owner = true, order = true}
local failures = 0
local files = io.popen("ls FS25_ADSmartPickup/*.lua FS25_ADSmartPickup/gui/*.lua"):read("*a")
for file in files:gmatch("[^\n]+") do
local listing = io.popen("luajit -bl " .. file .. " 2>&1"):read("*a")
for op, name in listing:gmatch("(G[GS]ET)%s+%d+%s+%d+%s+; \"([%w_]+)\"") do
if SUSPECT[name] then
failures = failures + 1
print(string.format("FAIL %s: %s af global '%s'", file, op, name))
end
end
end
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-186
View File
@@ -1,186 +0,0 @@
-- Kør: luajit tests/test_storageView.lua (fra repo-roden)
-- Røgtest af vareoversigten i fanerne Lagre (v1.52) med falske GUI-elementer: opbygning, fold ud,
-- opdatering hvert sekund og skift til bygningsvisning må ikke kaste, og tallene skal lande i cellerne.
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
-- ---- falske GUI-elementer ----
local Element = {}
Element.__index = Element
local function newElement(names, size)
local element = setmetatable({children = {}, text = nil, visible = true, size = size or {0.5, 0.02}}, Element)
for _, name in ipairs(names or {}) do
element.children[name] = setmetatable({children = {}, name = name, visible = true, size = {0.2, 0.01}}, Element)
end
return element
end
function Element:clone() local copy = newElement({}, {self.size[1], self.size[2]}); for n, c in pairs(self.children) do copy.children[n] = setmetatable({children = {}, name = n, visible = true, size = {c.size[1], c.size[2]}}, Element) end; return copy end
function Element:getDescendantByName(name) return self.children[name] end
function Element:setText(text) self.text = text end
function Element:setTexts(texts) self.texts = texts end
function Element:setState(state) self.state = state end
function Element:setVisible(visible) self.visible = visible end
function Element:setSize(x, y) self.size = {x, y} end
function Element:setImageColor(_, r, g, b, a) self.color = {r, g, b, a} end
function Element:setTextColor(r, g, b, a) self.textColor = {r, g, b, a} end
function Element:delete() end
function Element:mouseEvent() return false end
-- ---- spil-globaler ----
Class = function() return {} end
TabbedMenuFrameElement = {}
InputAction = {}
FocusManager = {loadElementFromCustomValues = function() end, removeElement = function() end}
Logging = {warning = function(...) print("WARN", string.format(...)) end, info = function() end}
local titles = {[31] = "Straw", [5] = "Sugar", [9] = "Milk"}
g_fillTypeManager = {getFillTypeTitleByIndex = function(_, i) return titles[i] end, getFillTypeNameByIndex = function(_, i) return titles[i] end}
g_currentMission = {getFarmId = function() return 1 end}
g_i18n = nil
dofile("FS25_ADSmartPickup/adStorageStats.lua")
ADRunsController = {store = {runs = {}}}
ADRuns = {getRun = function() return nil end}
ADFlowView = {wrapText = function(text) return {text} end}
ADSources = {
getFarmInventory = function() return ADSources.inventory end,
getFarmRates = function() return {[31] = {consumption = 1000, production = 0}, [5] = {consumption = 0, production = 500}} end,
getMarketQuote = function(ft) if ft == 5 then return {price = 1.0, top = 1.05} end return nil end,
}
ADSources.inventory = {
{kind = "silo", name = "Farma #1", placeable = "p1", entries = {{fillType = 31, level = 300, capacity = 1000}, {fillType = 5, level = 20000, capacity = 21000}}},
{kind = "silo", name = "Farma #2", placeable = "p2", entries = {{fillType = 31, level = 700, capacity = 1000}}},
{kind = "factory", name = "Sugar mill", placeable = "p3", entries = {{fillType = 5, level = 10, capacity = 100, isOutput = true}}},
}
local i18nTexts = {}
local i18n = {getText = function(_, key) i18nTexts[key] = true; if key:find("days") then return "%.1f d" end
if key:find("share") then return "%d %%" end
if key:find("alert_full") then return "%s %d %% %s" end
if key:find("alert_empty") then return "%s %s %s %s" end
if key:find("alert_sell") then return "%s %d %s" end
if key:find("pallets") then return "%d p" end
if key:find("perDay") or key:find("fullIn") then return "%s" end
return key end}
SmartPickupFrame = {}
local function load(path) local chunk = assert(loadfile(path)); chunk() end
SmartPickupFrame.new = nil
load("FS25_ADSmartPickup/gui/SmartPickupFrame.lua")
load("FS25_ADSmartPickup/gui/SmartPickupStorageTab.lua")
load("FS25_ADSmartPickup/gui/SmartPickupStorageView.lua")
load("FS25_ADSmartPickup/gui/SmartPickupFlowTab.lua")
-- en frame uden spillets klasse-maskineri: kun det fanen bruger
local frame = setmetatable({}, {__index = SmartPickupFrame})
frame.i18n = i18n
frame.clonedRows, frame.rowData = {}, {}
frame.currentTab = 7 -- Siloer
local rows = {}
frame.boxLayout = {elements = rows, invalidateLayout = function() end}
frame.productPrefab = newElement({"option", "title"})
frame.productPrefab.children.option.children.ignore = newElement()
frame.textPrefab = newElement({"title"})
frame.kpiPrefab = newElement({"v1", "l1", "v2", "l2", "v3", "l3", "v4", "l4"})
frame.alertPrefab = newElement({"dot", "c1"})
frame.itemPrefab = newElement({"c1", "c2", "c3", "c4", "bar", "barBg"})
frame.subPrefab = newElement({"c1", "c2", "c3", "c4", "bar", "barBg"})
frame.statPrefab = newElement({"c1", "c2", "c3", "c4", "c5", "c6", "c7"})
frame.gridPrefab = newElement({"c1", "c2", "c3"})
for _, prefab in ipairs({frame.productPrefab, frame.textPrefab, frame.kpiPrefab, frame.alertPrefab, frame.itemPrefab, frame.subPrefab, frame.statPrefab, frame.gridPrefab}) do
local base = prefab.clone
prefab.clone = function(self) local copy = base(self); if self.children.option then copy.children.option.children.ignore = newElement() end; table.insert(rows, copy); return copy end
end
frame.getAdEnv = function() return {} end
frame.setStatusText = function() end
frame.refreshButtons = function() end
frame.restoreFocusIfLost = function() end
frame.colorRows = function() end
for _, name in ipairs({"startButtonInfo", "renameButtonInfo", "deleteButtonInfo", "stopAllButtonInfo", "renameBuildingButtonInfo"}) do frame[name] = {} end
local function texts()
local list = {}
for _, row in ipairs(rows) do
for _, name in ipairs({"title", "c1", "c2", "c3", "c4", "v1", "v3"}) do
local child = row.children[name]
if child ~= nil and child.text ~= nil then table.insert(list, child.text) end
end
end
return table.concat(list, "|")
end
-- SVW1: opbygning af Siloer i varevisning
local ok, err = pcall(frame.rebuild, frame)
check("SVW1 opbygning kaster ikke", ok, true)
if not ok then print(err) end
local all = texts()
check("SVW1 straw-række", all:find("+ Straw", 1, true) ~= nil, true)
check("SVW1 sugar-række", all:find("+ Sugar", 1, true) ~= nil, true)
check("SVW1 straw 1.0k/2.0k 50 %", all:find("1.0k/2.0k · 50 %", 1, true) ~= nil, true)
check("SVW1 ingen bygninger før klik", all:find("Farma #2", 1, true), nil)
check("SVW1 fabrikkens sukker ikke i siloer", all:find("Sugar mill", 1, true), nil)
check("SVW1 kræver handling: straw løber tør", all:find("spu_alert", 1, true) == nil and all:find("Straw 1.0 d", 1, true) ~= nil, true)
-- SVW2: klik på straw folder ud; bygninger med mest først
local strawRow = frame.itemView.itemRows[1]
ok, err = pcall(frame.toggleItem, frame, 31)
check("SVW2 fold ud kaster ikke", ok, true)
if not ok then print(err) end
all = texts()
local first, second = all:find("Farma #2", 1, true), all:find("Farma #1", 1, true)
check("SVW2 Farma #2 vist", first ~= nil, true)
check("SVW2 flest først", first ~= nil and second ~= nil and first < second, true)
check("SVW2 markør skiftet", all:find("– Straw", 1, true) ~= nil, true)
check("SVW2 andel", all:find("70 %", 1, true) ~= nil, true)
-- SVW3: opdatering hvert sekund med nye tal, samme struktur
ADSources.inventory[2].entries[1].level = 900
ok, err = pcall(frame.refreshLive, frame)
check("SVW3 opdatering kaster ikke", ok, true)
if not ok then print(err) end
check("SVW3 ny total i cellen", texts():find("1.2k/2.0k · 60 %", 1, true) ~= nil, true)
local bar = frame.itemView.itemRows[1].bar
check("SVW3 bjælke 60 %", math.floor(bar.size[1] / 0.2 * 100 + 0.5), 60)
check("SVW3 bjælke gul (løber tør < 3 d)", bar.color ~= nil and bar.color[1] > 0.9 and bar.color[2] > 0.7, true)
-- SVW4: skift til bygningsvisning og tilbage
ok, err = pcall(frame.onClickStorageView, frame, SmartPickupFrame.STORAGE_VIEW_BUILDINGS)
check("SVW4 bygningsvisning kaster ikke", ok, true)
if not ok then print(err) end
check("SVW4 ingen varevisning", frame.itemView, nil)
ok = pcall(frame.onClickStorageView, frame, SmartPickupFrame.STORAGE_VIEW_ITEMS)
check("SVW4 tilbage til varer", ok and frame.itemView ~= nil, true)
-- SVW5: fabrikker-fanen: output næsten fuldt -> rød
frame.currentTab = 6
ok, err = pcall(frame.rebuild, frame)
check("SVW5 fabrikker kaster ikke", ok, true)
if not ok then print(err) end
-- SVW7: udfoldet vare hvor to siloer bytter plads i mængde -> ingen genopbygning, rækkerne følger bygningen
frame.currentTab = 7
frame.expandedItems = nil
frame:rebuild()
frame:toggleItem(31)
local subsBefore = frame.itemView.itemRows[1].subs
ADSources.inventory[1].entries[1].level = 950 -- Farma #1 (300 -> 950) forbi Farma #2 (900)
for _ = 1, 12 do frame:refreshLive() end
check("SVW7 samme rækker (ingen genopbygning)", frame.itemView.itemRows[1].subs, subsBefore)
check("SVW7 første underrække er stadig Farma #2", subsBefore[1].cells[1].text, "Farma #2")
check("SVW7 anden underrække viser Farma #1's nye tal", subsBefore[2].cells[2].text:find("950", 1, true) ~= nil, true)
-- SVW6: alle brugte l10n-nøgler findes i begge sprogfiler
for _, file in ipairs({"FS25_ADSmartPickup/l10n/l10n_da.xml", "FS25_ADSmartPickup/l10n/l10n_en.xml"}) do
local content = io.open(file):read("*a")
for key in pairs(i18nTexts) do
if key:find("^spu_") and not content:find('name="' .. key .. '"', 1, true) then
failures = failures + 1
print("FAIL SVW6 mangler " .. key .. " i " .. file)
end
end
end
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)