Compare commits
4 Commits
v1.60.13.0
...
v1.60.14.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 4de0bfcb46 | |||
| 53346cc916 | |||
| 4e8e9aa350 | |||
| f8864e26c3 |
@@ -1 +1,5 @@
|
|||||||
*.zip
|
*.zip
|
||||||
|
docs/superpowers/
|
||||||
|
bench/
|
||||||
|
.superpowers/
|
||||||
|
tests/
|
||||||
|
|||||||
@@ -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 — "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
|
- **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".
|
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,
|
- **1.60.12** "Start alle" ophæver altid pausen: et flow på pause, hvis traktorer stadig kørte en gemt AD-tur,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ ADHitch.MARKER_PREFIXES = {
|
|||||||
}
|
}
|
||||||
ADHitch.TIMEOUT_REASON = {attach = "noAttachable", detach = "nothingToDetach", arrive = "notArrived"}
|
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.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.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.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
|
ADHitch.ACT_TIMEOUT_MS = 8000 -- så længe forsøges kobling efter ankomst
|
||||||
@@ -100,6 +101,14 @@ function ADHitch.findMarker(markers, name)
|
|||||||
return nil
|
return nil
|
||||||
end
|
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)
|
-- state = {adActive, distance, speed, arriveDistance?} (arriveDistance: udvidet, når traktoren har bakket hen til vognen)
|
||||||
local function arriveDistanceOf(state)
|
local function arriveDistanceOf(state)
|
||||||
return state.arriveDistance or ADHitch.ARRIVE_DISTANCE
|
return state.arriveDistance or ADHitch.ARRIVE_DISTANCE
|
||||||
@@ -525,7 +534,7 @@ function ADHitch.tick(adEnv, dt)
|
|||||||
if not succeeded then job.lastReason = detail end
|
if not succeeded then job.lastReason = detail end
|
||||||
end
|
end
|
||||||
local state = vehicleState(adEnv, vehicle, job.markerId)
|
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)
|
ADHitch.step(job, state, elapsed, succeeded)
|
||||||
if job.state == "done" then
|
if job.state == "done" then
|
||||||
finish(vehicle, job, true, detail, adEnv)
|
finish(vehicle, job, true, detail, adEnv)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||||
<modDesc descVersion="107">
|
<modDesc descVersion="107">
|
||||||
<author>masterdraco</author>
|
<author>masterdraco</author>
|
||||||
<version>1.60.13.0</version>
|
<version>1.60.14.0</version>
|
||||||
<title>
|
<title>
|
||||||
<en>AD Smart Pickup</en>
|
<en>AD Smart Pickup</en>
|
||||||
</title>
|
</title>
|
||||||
|
|||||||
@@ -44,20 +44,13 @@ Alt sættes op i menuen **Smart Pickup** i ESC-menuen.
|
|||||||
|
|
||||||
## Feedback
|
## 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`.
|
`Documents/My Games/FarmingSimulator2025/`, eller i det mindste linjerne der starter med `ADSmartPickup`.
|
||||||
|
|
||||||
## Udvikling
|
## Udvikling
|
||||||
|
|
||||||
- Mod-koden ligger i `FS25_ADSmartPickup/`, og testene ligger i `tests/` (ren Lua, kører uden spillet).
|
- Mod-koden ligger i `FS25_ADSmartPickup/`.
|
||||||
- Kør testene:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
for t in tests/test_*.lua; do luajit $t >/dev/null 2>&1 || echo FAIL $t; done
|
|
||||||
```
|
|
||||||
|
|
||||||
- Byg zip: `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .`
|
- 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) ·
|
**Docs:** [User manual](docs/USER_MANUAL.md) · [Danish manual](docs/BRUGERMANUAL.md) ·
|
||||||
[Changelog](CHANGELOG.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`.
|
`ADSmartPickup`.
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -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))
|
|
||||||
@@ -1116,7 +1116,7 @@ ADSmartPickup: F109 …
|
|||||||
|
|
||||||
Moden testes løbende, og fejlrapporter er meget velkomne.
|
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
|
2. Beskriv, hvad du gjorde, hvad du forventede, og hvad der skete. Nævn versionen (står i
|
||||||
`log.txt` og i mod-listen).
|
`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
|
3. Vedlæg `log.txt` fra `Documents/My Games/FarmingSimulator2025/log.txt`. Er filen for stor, så vedlæg
|
||||||
|
|||||||
@@ -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
@@ -1146,7 +1146,7 @@ ADSmartPickup: F109 …
|
|||||||
|
|
||||||
The mod is being tested continuously, and bug reports are very welcome.
|
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`
|
2. Describe what you did, what you expected, and what happened. Mention the version (shown in `log.txt`
|
||||||
and in the mod list).
|
and in the mod list).
|
||||||
3. Attach `log.txt` from `Documents/My Games/FarmingSimulator2025/log.txt`. If the file is too big,
|
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
@@ -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.
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
Reference in New Issue
Block a user