chore: tests ud af det offentlige repo (kun lokalt); feedback uden krav om konto

This commit is contained in:
2026-09-29 21:32:19 +02:00
parent 4e8e9aa350
commit 53346cc916
51 changed files with 6 additions and 8932 deletions
+1
View File
@@ -2,3 +2,4 @@
docs/superpowers/
bench/
.superpowers/
tests/
+3 -9
View File
@@ -44,18 +44,12 @@ Alt sættes op i menuen **Smart Pickup** i ESC-menuen.
## Feedback
Fejl og idéer: opret et issue her i repoet. Vedlæg gerne `log.txt` fra
Fejl og idéer: giv udvikleren besked (issues her kræver en konto på git.powerdata.dk). Vedlæg gerne `log.txt` fra
`Documents/My Games/FarmingSimulator2025/`, eller i det mindste linjerne der starter med `ADSmartPickup`.
## Udvikling
- Mod-koden ligger i `FS25_ADSmartPickup/`, og testene ligger i `tests/` (ren Lua, kører uden spillet).
- Kør testene:
```sh
for t in tests/test_*.lua; do luajit $t >/dev/null 2>&1 || echo FAIL $t; done
```
- Mod-koden ligger i `FS25_ADSmartPickup/`.
- Byg zip: `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .`
---
@@ -94,5 +88,5 @@ needed for pallets.
**Docs:** [User manual](docs/USER_MANUAL.md) · [Danish manual](docs/BRUGERMANUAL.md) ·
[Changelog](CHANGELOG.md)
**Feedback:** open an issue in this repository. Attach `log.txt`, or at least the lines starting with
**Feedback:** tell the developer (issues here need an account on git.powerdata.dk). Attach `log.txt`, or at least the lines starting with
`ADSmartPickup`.
+1 -1
View File
@@ -1116,7 +1116,7 @@ ADSmartPickup: F109 …
Moden testes løbende, og fejlrapporter er meget velkomne.
1. Opret et issue i repoet: <https://git.powerdata.dk/masterdraco/fs25-adsmartpickup/issues>
1. Giv udvikleren besked — eller opret et issue i repoet, hvis du har en konto: <https://git.powerdata.dk/masterdraco/fs25-adsmartpickup/issues>
2. Beskriv, hvad du gjorde, hvad du forventede, og hvad der skete. Nævn versionen (står i
`log.txt` og i mod-listen).
3. Vedlæg `log.txt` fra `Documents/My Games/FarmingSimulator2025/log.txt`. Er filen for stor, så vedlæg
+1 -1
View File
@@ -1146,7 +1146,7 @@ ADSmartPickup: F109 …
The mod is being tested continuously, and bug reports are very welcome.
1. Open an issue in the repository: <https://git.powerdata.dk/masterdraco/fs25-adsmartpickup/issues>
1. Tell the developer — or open an issue in the repository if you have an account: <https://git.powerdata.dk/masterdraco/fs25-adsmartpickup/issues>
2. Describe what you did, what you expected, and what happened. Mention the version (shown in `log.txt`
and in the mod list).
3. Attach `log.txt` from `Documents/My Games/FarmingSimulator2025/log.txt`. If the file is too big,
-254
View File
@@ -1,254 +0,0 @@
-- Kør: luajit tests/test_adBuildings.lua (fra repo-roden)
-- Bygninger gården kan bruge + automatisk markør ved læsse-/aflæsningstrigger (≤ 40 m, Wait udelukket,
-- siloer strengt), omvendt opslag markør -> bygning, id-fallback, cache.
local SUGAR, FORAGE, MILK, BASKET = 5, 6, 12, 7
local DESC = {[SUGAR] = {name = "SUGAR", title = "Sukker", isPalletType = true}, [FORAGE] = {name = "FORAGE", title = "TMR"}, [MILK] = {name = "MILK", title = "Mælk"}, [BASKET] = {name = "STRAW_BASKET", title = "Kurve", palletFilename = "x"}}
g_fillTypeManager = {getFillTypeByIndex = function(_, i) return DESC[i] end, getFillTypeNameByIndex = function(_, i) return DESC[i] and DESC[i].name end, getFillTypeTitleByIndex = function(_, i) return DESC[i] and DESC[i].title end}
Logging = {info = function() end, warning = function() end}
getWorldTranslation = function(node) return node.x, 0, node.z end
SellingStation = {}
dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")
dofile("FS25_ADSmartPickup/adPrices.lua")
dofile("FS25_ADSmartPickup/adBuildings.lua")
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else failures = failures + 1; print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual))) end
end
local markers, wayPoints = {}, {}
local function addMarker(id, name, group, x, z)
wayPoints[id] = {id = id, x = x, z = z}
table.insert(markers, {id = id, markerIndex = #markers + 1, name = name, group = group})
end
local loadStations, unloadStations = {}, {}
local adEnv = {ADGraphManager = {
getWayPointById = function(_, id) return wayPoints[id] end,
getMapMarkers = function() return markers end,
getMapMarkerByWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id then return m end end end,
}}
local strictLoadAt = {}
ADSmartPickup = {getLoadStationAtWayPointId = function(_, id) return strictLoadAt[id] end}
g_currentMission = {
placeableSystem = {placeables = {}},
storageSystem = {getLoadingStations = function() return loadStations end, getUnloadingStations = function() return unloadStations end},
accessHandler = {canFarmAccess = function() return true end},
getFarmId = function() return 1 end,
}
local function placeable(uid, name, x, z, extra)
local p = {uniqueId = uid, rootNode = {x = x, z = z}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return name end}
for k, v in pairs(extra or {}) do p[k] = v end
table.insert(g_currentMission.placeableSystem.placeables, p)
return p
end
-- verden: kostald med mælketank (load) og foder (unload), sukkerfabrik (unload + pallespawner), to siloer i række, objektlager, salgssted
local barn = placeable("barn1", "Kostald 1", 0, 0, {getHusbandryFillLevel = function() return 0 end})
-- som FS25_LargerAnimalBarns' cowBarnBig: ÉN læssestation med TO læssepunkter (gylle + mælk)
local milkStation = {owningPlaceable = barn, loadTriggers = {{triggerNode = {x = 20, z = 0}}, {triggerNode = {x = 0, z = 30}}}}
local barnUnload = {owningPlaceable = barn, unloadTriggers = {{exactFillRootNode = {x = -20, z = 0}}}}
table.insert(loadStations, milkStation); table.insert(unloadStations, barnUnload)
local mill = placeable("mill", "Sukkerfabrik", 500, 0, {spec_productionPoint = {productionPoint = {productions = {}, storage = {}, palletSpawner = {spawnPlaces = {{startNode = {x = 520, z = 0}}}}, unloadingStation = nil}}})
local millUnload = {owningPlaceable = mill, unloadTriggers = {{exactFillRootNode = {x = 480, z = 0}}}, isa = function() return true end, isSellingPoint = true}
table.insert(unloadStations, millUnload)
local silo1 = placeable("silo1", "Silo 1", 1000, 0, {spec_silo = {}})
local silo2 = placeable("silo2", "Silo 2", 1015, 0, {spec_silo = {}})
local silo1Station = {owningPlaceable = silo1, loadTriggers = {{triggerNode = {x = 1000, z = 5}}}}
local silo2Station = {owningPlaceable = silo2, loadTriggers = {{triggerNode = {x = 1015, z = 5}}}}
table.insert(loadStations, silo1Station); table.insert(loadStations, silo2Station)
table.insert(unloadStations, {owningPlaceable = silo1, unloadTriggers = {{exactFillRootNode = {x = 1000, z = 5}}}})
table.insert(unloadStations, {owningPlaceable = silo2, unloadTriggers = {{exactFillRootNode = {x = 1015, z = 5}}}})
local shed = placeable("shed", "Palleskur", 2000, 0, {spec_objectStorage = {objectInfos = {}, capacity = 100}})
local sellPoint = placeable("sell", "Mejeri", 3000, 0, {})
local sellStation = {owningPlaceable = sellPoint, unloadTriggers = {{exactFillRootNode = {x = 3000, z = 10}}}, isa = function() return true end, isSellingPoint = true}
table.insert(unloadStations, sellStation)
local other = placeable("otherfarm", "Naboens", 4000, 0, {spec_silo = {}, ownerFarmId = 2, getOwnerFarmId = function() return 2 end})
addMarker(1, "cow 1 milk", "Milk", 25, 0) -- 5 m fra mælketriggeren
addMarker(2, "Cow 1 food", "Cow food", -30, 0) -- 10 m fra fodertriggeren
addMarker(3, "cow wait 1", "cow wait", 22, 0) -- Wait: må ALDRIG vælges (tættere end mælkemarkøren)
addMarker(11, "Cow 1 Slurry", "Slurry", 0, 33) -- 3 m fra gylletriggeren
addMarker(4, "Suger Load", "Pickup", 525, 0) -- 5 m fra pallespawneren
addMarker(5, "sugar offload", "production", 470, 0)
addMarker(6, "silo 1", "silo", 1000, 12) -- strengt: silo1
addMarker(7, "silo 2", "silo", 1015, 12) -- strengt: silo2 (7 m fra silo1's trigger-node på x-aksen? nej: 15 m)
strictLoadAt[6] = silo1Station; strictLoadAt[7] = silo2Station
addMarker(8, "Storage pickup", "Pickup", 2010, 0)
addMarker(9, "Mejeri", "selling", 3000, 20)
addMarker(10, "Langt væk", "All", 9000, 9000)
-- B1: liste, kind og markører
local list = ADBuildings.list(adEnv, 1)
local byId = {}
for _, b in ipairs(list) do byId[b.id] = b end
check("B1 antal (naboens silo udeladt)", #list, 6)
check("B1 stald", byId.barn1.kind, "husbandry")
check("B1 stald læssemarkør = mælk (ikke Wait, som er tættere)", byId.barn1.loadMarker.id, 1)
-- én markør pr. læssestation (mælketank OG gyllebeholder), sorteret på markørnavn
check("B1 stald har to læssemarkører", #byId.barn1.loadMarkers, 2)
check("B1 første = cow 1 milk (navnesorteret, uanset store/små)", byId.barn1.loadMarkers[1].id, 1)
check("B1 anden = Cow 1 Slurry", byId.barn1.loadMarkers[2].name, "Cow 1 Slurry")
check("B1 gyllemarkør -> stald via byMarker", ADBuildings.byMarker(adEnv, 1, 11, "load").id, "barn1")
check("B1 stald aflæsningsmarkør = foder", byId.barn1.unloadMarker.id, 2)
check("B1 stald afstand", byId.barn1.loadMarker.distance, 5)
check("B1 fabrik læssemarkør via pallespawner", byId.mill.loadMarker.id, 4)
check("B1 fabrik aflæsningsmarkør", byId.mill.unloadMarker.id, 5)
check("B1 fabrik er ikke salgssted", byId.mill.canSell, false)
check("B1 objektlager markør via rootNode", byId.shed.loadMarker.id, 8)
check("B1 salgssted", byId.sell.kind, "sell")
check("B1 salgssted markør", byId.sell.unloadMarker.id, 9)
check("B1 salgssted canSell", byId.sell.canSell, true)
check("B1 stald kan være kilde og mål", byId.barn1.canBeSource and byId.barn1.canBeTarget, true)
check("B1 silo kilde, ikke mål", byId.silo1.canBeSource and not byId.silo1.canBeTarget, true)
-- B2: siloer i række: strengt opslag giver hver sin markør
check("B2 silo 1 markør", byId.silo1.loadMarker.id, 6)
check("B2 silo 2 markør", byId.silo2.loadMarker.id, 7)
strictLoadAt[7] = nil
ADBuildings.invalidate()
check("B2 silo uden strengt match: ingen læssemarkør", ADBuildings.find(adEnv, 1, "silo2").loadMarker, nil)
strictLoadAt[7] = silo2Station
ADBuildings.invalidate()
-- B3: byMarker (omvendt opslag)
check("B3 mælkemarkør -> stald (load)", ADBuildings.byMarker(adEnv, 1, 1, "load").id, "barn1")
check("B3 fodermarkør -> stald (unload)", ADBuildings.byMarker(adEnv, 1, 2, "unload").id, "barn1")
check("B3 fodermarkør er ikke load", ADBuildings.byMarker(adEnv, 1, 2, "load"), nil)
check("B3 fjern markør -> nil", ADBuildings.byMarker(adEnv, 1, 10, "load"), nil)
-- B4: id-fallback uden uniqueId
check("B4 id fra position", ADBuildings.getId({rootNode = {x = 12.6, z = -3.2}}), "pos:13:-3")
check("B4 id fra uniqueId", ADBuildings.getId(barn), "barn1")
-- B5: cache: samme tabel inden for CACHE_MS, ny efter invalidate
local first = ADBuildings.list(adEnv, 1)
check("B5 cachet", ADBuildings.list(adEnv, 1) == first, true)
ADBuildings.invalidate()
check("B5 ny efter invalidate", ADBuildings.list(adEnv, 1) ~= first, true)
-- B6: sortering efter NAVN (naturlig talrækkefølge), dernæst første markørnavn — ikke efter slags
local order = {}
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do table.insert(order, b.name) end
check("B6 rækkefølge", table.concat(order, ","), "Kostald 1,Mejeri,Palleskur,Silo 1,Silo 2,Sukkerfabrik")
check("B6 naturlig sortering", ADBuildings.naturalLess("Silo 9", "Silo 10"), true)
check("B6 naturlig sortering, tal før tekst-lighed", ADBuildings.naturalLess("25 Silo Storage", "26 Silo Storage"), true)
check("B6 naturlig sortering ens navn -> markørnavn", ADBuildings.compare({name = "Farma", loadMarkers = {{name = "30 Silo"}}}, {name = "Farma", loadMarkers = {{name = "4 Silo"}}}), false)
-- B-DOB: silo der kun kan FYLDES (drive-on-bunker ved biogas: aflæsning, ingen læssestation) = mål, ikke kilde
do
local dob = placeable("dob1", "DOB 115", 5000, 0, {spec_silo = {}})
table.insert(unloadStations, {owningPlaceable = dob, unloadTriggers = {{exactFillRootNode = {x = 5000, z = 5}}}})
addMarker(21, "Biogas Drop 1", "Biogas", 5000, 12)
ADBuildings.invalidate()
local byIdDob = {}
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byIdDob[b.id] = b end
check("B-DOB kan være mål", byIdDob.dob1.canBeTarget, true)
check("B-DOB ikke kilde (intet at hente)", byIdDob.dob1.canBeSource, false)
check("B-DOB aflæsningsmarkør", byIdDob.dob1.unloadMarker.id, 21)
check("B-DOB almindelig silo stadig ikke mål", byIdDob.silo1.canBeTarget, false)
end
-- B-SEP: bygning med læssestation men uden silo/fabrik/stald-spec (Pumps&Hoses separatorhal: digestat-udlevering)
do
local sep = placeable("sep1", "Separator Hall", 7000, 0, {spec_manureSeparator = {}})
table.insert(loadStations, {owningPlaceable = sep, loadTriggers = {{triggerNode = {x = 7000, z = 5}}}})
addMarker(41, "Digestate pickup", "Biogas", 7000, 10)
ADBuildings.invalidate()
local bySep = {}
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do bySep[b.id] = b end
check("B-SEP findes som lager", bySep.sep1 and bySep.sep1.kind, "storage")
check("B-SEP kan være kilde", bySep.sep1 and bySep.sep1.canBeSource, true)
check("B-SEP læssemarkør", bySep.sep1 and bySep.sep1.loadMarker and bySep.sep1.loadMarker.id, 41)
end
-- B-DROP: fabrik med pickup-markør TÆT på aflæsningstriggeren og en separat drop-markør længere væk
-- (Sugarbeet Crusher): aflæsningsmarkøren skal være drop, ikke bygningens egen læssemarkør
do
local crusher = placeable("crusher1", "Sugarbeet Crusher", 8000, 0, {spec_productionPoint = {productionPoint = {productions = {}, storage = {}}}})
table.insert(loadStations, {owningPlaceable = crusher, loadTriggers = {{triggerNode = {x = 8000, z = 5}}}})
table.insert(unloadStations, {owningPlaceable = crusher, unloadTriggers = {{exactFillRootNode = {x = 8003, z = 2}}}})
addMarker(51, "Sugar Beet Cut Pickup", "Fabrication", 8000, 8) -- 3 m fra læs, ~6 m fra aflæsning
addMarker(52, "Sugar Beet Cut Beet Drop", "Fabrication", 8012, 2) -- 9 m fra aflæsning
ADBuildings.invalidate()
local byC = {}
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byC[b.id] = b end
check("B-DROP læssemarkør = pickup", byC.crusher1.loadMarker.id, 51)
check("B-DROP aflæsningsmarkør = drop (ikke pickup)", byC.crusher1.unloadMarker.id, 52)
-- kun én fælles markør (AIO Load / Unload): bruges til begge
local aio = placeable("aio1", "AIO", 9000, 0, {spec_productionPoint = {productionPoint = {productions = {}, storage = {}}}})
table.insert(loadStations, {owningPlaceable = aio, loadTriggers = {{triggerNode = {x = 9000, z = 5}}}})
table.insert(unloadStations, {owningPlaceable = aio, unloadTriggers = {{exactFillRootNode = {x = 9002, z = 5}}}})
addMarker(53, "AIO Load / Unload", "Fabrication", 9000, 9)
ADBuildings.invalidate()
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byC[b.id] = b end
check("B-DROP fælles markør bruges til aflæsning", byC.aio1.unloadMarker.id, 53)
check("B-DROP silo beholder sin egen markør (ikke nabosiloens)", byC.silo1.unloadMarker.id, 6)
end
-- B-COOP: hønsehus — foder aflæsses i den ene ende, æg-paller spawnes i den anden (ingen læssestation)
do
local coop = placeable("coop1", "Chicken Coop", 10000, 0, {getHusbandryFillLevel = function() return 0 end,
spec_husbandryFood = {triggers = {{someTrigger = {exactFillRootNode = {x = 9970, z = 0}}}}}, -- ukendt feltsti: søges
spec_husbandryPallets = {fillTypeIndexToPalletSpawner = {[99] = {spawnPlaces = {{startNode = {x = 10030, z = 0}}}}}}})
table.insert(unloadStations, {owningPlaceable = coop, unloadTriggers = {}}) -- som chickenBarnBig: tom station
addMarker(61, "Chicken Food", "Chicken", 9965, 0)
addMarker(62, "Egg Pickup", "Chicken", 10035, 0)
ADBuildings.invalidate()
local byCoop = {}
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byCoop[b.id] = b end
check("B-COOP læssemarkør ved æggene", byCoop.coop1.loadMarker and byCoop.coop1.loadMarker.id, 62)
check("B-COOP aflæsningsmarkør ved foderet", byCoop.coop1.unloadMarker and byCoop.coop1.unloadMarker.id, 61)
end
-- B-FUEL: brændstoftank (læssestation kun med DIESEL) er ikke et lager/en kilde
do
local fmBefore = g_fillTypeManager
g_fillTypeManager = {getFillTypeNameByIndex = function(_, index) return index == 900 and "DIESEL" or "X" end}
local tank = placeable("fuel1", "Small Petrol Tank", 11000, 0, {})
table.insert(loadStations, {owningPlaceable = tank, supportedFillTypes = {[900] = true}, loadTriggers = {{triggerNode = {x = 11000, z = 3}}}})
ADBuildings.invalidate()
local byFuel = {}
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byFuel[b.id] = b end
check("B-FUEL brændstoftank udeladt", byFuel.fuel1, nil)
g_fillTypeManager = fmBefore
end
-- B-SCAN: søgning efter trigger-noder i indlejrede tabeller (dybde ≤ 3, ingen løkker)
do
local loop = {}
loop.self = loop
local nodes = ADBuildings.findTriggerNodes({a = {b = {exactFillRootNode = "n1"}}, c = {triggerNode = "n2"}, d = loop, e = {f = {g = {h = {exactFillRootNode = "for-dybt"}}}}})
table.sort(nodes)
check("B-SCAN finder n1 og n2, ikke for dybt", table.concat(nodes, ","), "n1,n2")
end
-- B-SHED: palleskur læsser og aflæsser samme sted — aflæsningsmarkøren er den nærmeste (ikke "den anden")
do
local shedX = placeable("shedX", "Bale & Pallet Storage", 12000, 0, {spec_objectStorage = {objectInfos = {}, capacity = 4000}})
addMarker(71, "Chicken Storage Unload", "Chicken", 12010, 0) -- 10 m
addMarker(72, "Chicken Storage Pickup", "Chicken", 12018, 0) -- 18 m
ADBuildings.invalidate()
local byShed = {}
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byShed[b.id] = b end
check("B-SHED aflæsning = nærmeste (Unload)", byShed.shedX.unloadMarker and byShed.shedX.unloadMarker.id, 71)
end
-- pickLoadMarker: en pallespawner viger for aflæsningen, når begge vil have samme markør og aflæsningen står
-- tættere (in-game 27/9 fårestald: spawner 9 m fra "Sheep Food", foderindkast 5 m -> Goat Food fik "Sheep Milk")
do
local food, milk = {id = 1, name = "Sheep Food 1", distance = 9}, {id = 2, name = "Sheep Milk 1", distance = 13}
local unloadBest = {id = 1, name = "Sheep Food 1", distance = 5}
check("PL spawner viger for tættere aflæsning", ADBuildings.pickLoadMarker({food, milk}, unloadBest, true).name, "Sheep Milk 1")
check("PL rigtig læssetrigger beholder markøren (Sugarbeet Crusher)", ADBuildings.pickLoadMarker({food, milk}, unloadBest, false).name, "Sheep Food 1")
-- in-game 27/9 fårestald 3 og 5: spawner 5 m fra "Sheep Food", foderindkast 9 m -> spawneren vandt stadig
check("PL spawner viger også når den selv står tættest", ADBuildings.pickLoadMarker({food, milk}, {id = 1, name = "Sheep Food 1", distance = 12}, true).name, "Sheep Milk 1")
check("PL rigtig læssetrigger tættest beholder", ADBuildings.pickLoadMarker({food, milk}, {id = 1, name = "Sheep Food 1", distance = 12}, false).name, "Sheep Food 1")
check("PL ingen konflikt", ADBuildings.pickLoadMarker({milk}, unloadBest, true).name, "Sheep Milk 1")
check("PL eneste kandidat er aflæsningens -> ingen læssemarkør", ADBuildings.pickLoadMarker({food}, unloadBest, true), nil)
check("PL ingen kandidater", ADBuildings.pickLoadMarker({}, unloadBest, true), nil)
end
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-40
View File
@@ -1,40 +0,0 @@
-- Kør: luajit tests/test_adCache.lua (fra repo-roden)
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
g_currentMission = {time = 0}
dofile("FS25_ADSmartPickup/adCache.lua")
local calls = 0
local function lookup() calls = calls + 1; return "station" end
check("K1 slået fra: altid opslag", ADCacheGet("b", "k", 1000, lookup) .. ADCacheGet("b", "k", 1000, lookup), "stationstation")
check("K1 to opslag", calls, 2)
ADCache.enabled = true
calls = 0
ADCacheGet("b", "k", 1000, lookup); ADCacheGet("b", "k", 1000, lookup)
check("K2 genbrugt", calls, 1)
g_currentMission.time = 900
ADCacheGet("b", "k", 1000, lookup)
check("K2 inden for levetid", calls, 1)
g_currentMission.time = 1600
ADCacheGet("b", "k", 1000, lookup)
check("K3 efter levetid + spredning (maks 1500) -> nyt opslag", calls, 2)
local nothing = 0
ADCacheGet("n", "x", 1000, function() nothing = nothing + 1; return nil end)
check("K4 intet fundet huskes", ADCacheGet("n", "x", 1000, function() nothing = nothing + 1; return "ny" end), nil)
check("K4 kun ét opslag", nothing, 1)
-- spredt udløb: 200 poster lavet samtidig udløber ikke samtidig
g_currentMission.time = 10000
for i = 1, 200 do ADCacheGet("spread", i, 10000, function() return i end) end
local expired = 0
g_currentMission.time = 22000
for i = 1, 200 do ADCacheGet("spread", i, 10000, function() expired = expired + 1; return i end) end
check("K5 kun en del udløbet efter 12 s (spredt 10-15 s)", expired > 20 and expired < 180, true)
ADCache.clear()
check("K6 ryddet", next(ADCache.buckets), nil)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-195
View File
@@ -1,195 +0,0 @@
-- Kør: luajit tests/test_adCourseCache.lua (fra repo-roden)
-- Kursus-registret: hvilken mark/bredde/markform/indstillinger en maskines nuværende Courseplay-kursus blev
-- lavet til. Genbrug kun når ALT matcher; egen maskine først, ellers en anden maskines kursus (kopi).
dofile("FS25_ADSmartPickup/adCourseCache.lua")
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
local function count(map)
local n = 0
for _ in pairs(map) do n = n + 1 end
return n
end
local C = ADCourseCache
local SIG = "n=4;a=960000;c=800,300"
-- ---- indstillings-nøglen ----
check("K1 standard", C.settingsKey({}), "off=0.0;lane=-;multi=1;vine=-;skip=-")
check("K2 nil = standard", C.settingsKey(nil), C.settingsKey({}))
check("K3 alt sat", C.settingsKey({toolOffset = 1.25, lane = -1, multiTools = 2, vineOffset = -1, vineSkip = 1}),
"off=1.3;lane=-1;multi=2;vine=-1.0;skip=1")
check("K4 -0.0 = 0.0", C.settingsKey({toolOffset = -0.0001}), C.settingsKey({toolOffset = 0}))
check("K5 float-støj i forskydning tæller ikke", C.settingsKey({toolOffset = 1.2000001}), C.settingsKey({toolOffset = 1.2}))
check("K6 anden bane = anden nøgle", C.settingsKey({multiTools = 2, lane = 1}) ~= C.settingsKey({multiTools = 2, lane = -1}), true)
check("K7 vin-offset indgår", C.settingsKey({vineOffset = 0}) ~= C.settingsKey({vineOffset = -1}), true)
check("K8 vin-skip indgår", C.settingsKey({vineOffset = -1, vineSkip = 0}) ~= C.settingsKey({vineOffset = -1, vineSkip = 1}), true)
local settings = C.settingsKey({})
local request = {field = 22, width = 30, sig = SIG, settings = settings}
local own = C.newRecord({vehicle = "v1", field = 22, width = 30.0, sig = SIG, settings = settings, waypoints = 812})
-- ---- key/matches ----
check("M1 samme alt", C.matches(own, request), true)
check("M2 bredde som float32 fra XML (30.0000001)", C.matches(C.newRecord({vehicle = "v1", field = 22, width = 30.0000001, sig = SIG, settings = settings}), request), true)
check("M3 ny bredde", C.matches(own, {field = 22, width = 12, sig = SIG, settings = settings}), false)
check("M4 anden mark", C.matches(own, {field = 23, width = 30, sig = SIG, settings = settings}), false)
check("M5 form ændret", C.matches(own, {field = 22, width = 30, sig = "n=5;a=960000;c=800,300", settings = settings}), false)
check("M6 andre indstillinger", C.matches(own, {field = 22, width = 30, sig = SIG, settings = C.settingsKey({toolOffset = 2})}), false)
check("M7 ukendt form (nil) matcher aldrig", C.matches(C.newRecord({vehicle = "v1", field = 22, width = 30, settings = settings}), {field = 22, width = 30, settings = settings}), false)
check("M8 intet register", C.matches(nil, request), false)
check("M9 key", C.key(request), "22|30.0|" .. SIG .. "|" .. settings)
check("M10 key uden form = nil", C.key({field = 22, width = 30, settings = settings}), nil)
-- ---- grund til ny beregning ----
check("R1 intet gemt", C.mismatchReason(nil, request), "intet kursus gemt")
check("R2 form ændret", C.mismatchReason(own, {field = 22, width = 30, sig = "x", settings = settings}), "markens form ændret")
check("R3 ny bredde", C.mismatchReason(own, {field = 22, width = 12, sig = SIG, settings = settings}), "ny bredde")
check("R4 anden mark", C.mismatchReason(own, {field = 9, width = 30, sig = SIG, settings = settings}), "kursus til en anden mark")
check("R5 indstillinger", C.mismatchReason(own, {field = 22, width = 30, sig = SIG, settings = "x"}), "andre indstillinger")
check("R6 ukendt form", C.mismatchReason(own, {field = 22, width = 30, settings = settings}), "markens form ukendt")
-- ---- register: immutabelt ----
local records = C.withRecord({}, own)
check("I1 tilføjet", records.v1 ~= nil, true)
local other = C.newRecord({vehicle = "v2", field = 22, width = 30, sig = SIG, settings = settings, waypoints = 812})
local both = C.withRecord(records, other)
check("I2 original urørt", count(records), 1)
check("I3 begge", count(both), 2)
check("I4 without", count(C.without(both, "v1")), 1)
check("I5 without urørt original", count(both), 2)
check("I6 newRecord uden vehicle = nil", C.newRecord({field = 22}), nil)
-- ---- findReusable ----
local rec, how = C.findReusable(both, request, "v1")
check("F1 egen maskine først", rec and rec.vehicle, "v1")
check("F2 egen = own", how, "own")
rec, how = C.findReusable(both, request, "v3")
check("F3 anden maskines kursus", rec and rec.vehicle, "v1")
check("F4 = copy", how, "copy")
-- egen maskines kursus er til en anden bredde -> kopi fra v2
local mixed = C.withRecord(both, C.newRecord({vehicle = "v1", field = 22, width = 12, sig = SIG, settings = settings}))
rec, how = C.findReusable(mixed, request, "v1")
check("F5 egen passer ikke -> kopi fra v2", rec and rec.vehicle, "v2")
check("F6 kopi", how, "copy")
-- intet passer -> grunden fra egen post
local changed = {field = 22, width = 30, sig = "n=6;a=1;c=0,0", settings = settings}
rec, how = C.findReusable(both, changed, "v1")
check("F7 intet passer", rec, nil)
check("F8 grund: form ændret", how, "markens form ændret")
rec, how = C.findReusable({}, request, "v1")
check("F9 tomt register", how, "intet kursus gemt")
-- gyldighedstjek: egen maskines kursus er væk/ændret -> kopi fra v2; alle ugyldige -> ny
rec, how = C.findReusable(both, request, "v1", function(r) return r.vehicle ~= "v1" end)
check("F10 egen ugyldig -> kopi", rec and rec.vehicle, "v2")
rec, how = C.findReusable(both, request, "v1", function() return false end)
check("F11 alle ugyldige", rec, nil)
check("F12 grund: kurset på maskinen er ændret", how, "kurset på maskinen er ændret")
-- multi-tool: lederens kursus (2 sæt, bane -1) genbruges ikke til et enkelt sæt
local multi = C.newRecord({vehicle = "v1", field = 22, width = 30, sig = SIG, settings = C.settingsKey({multiTools = 2, lane = -1})})
rec = C.findReusable(C.withRecord({}, multi), request, "v1")
check("F13 multi-kursus != enkelt", rec, nil)
rec, how = C.findReusable(C.withRecord({}, multi), {field = 22, width = 30, sig = SIG, settings = C.settingsKey({multiTools = 2, lane = -1})}, "v1")
check("F14 multi samme bane genbruges", how, "own")
-- deterministisk: flere kandidater -> laveste id
local many = C.withRecord(C.withRecord({}, C.newRecord({vehicle = "v9", field = 22, width = 30, sig = SIG, settings = settings})),
C.newRecord({vehicle = "v4", field = 22, width = 30, sig = SIG, settings = settings}))
rec = C.findReusable(many, request, "v1")
check("F15 deterministisk valg", rec and rec.vehicle, "v4")
-- ---- invalidateField ----
local fieldRecords = C.withRecord(both, C.newRecord({vehicle = "v5", field = 7, width = 30, sig = "n=3;a=50;c=3,3", settings = settings}))
local kept, removed = C.invalidateField(fieldRecords, 22, "n=5;a=1;c=0,0")
check("V1 to poster for marken slettet", removed, 2)
check("V2 anden mark bevaret", kept.v5 ~= nil, true)
check("V3 original urørt", count(fieldRecords), 3)
kept, removed = C.invalidateField(fieldRecords, 22, SIG)
check("V4 samme form: intet slettes", removed, 0)
check("V4b samme tabel tilbage", kept, fieldRecords)
kept, removed = C.invalidateField(fieldRecords, 22, nil)
check("V5 ukendt form: intet slettes", removed, 0)
kept, removed = C.invalidateField(nil, 22, SIG)
check("V6 nil-register", removed, 0)
-- ---- lim: prepare/afterStart/checkField med falske spil-objekter ----
dofile("FS25_ADSmartPickup/adCpBridge.lua")
local saves, logs = 0, {}
ADFieldJobs = {state = {courseRecords = {}}, save = function() saves = saves + 1 end,
log = function(_, fmt, ...) table.insert(logs, string.format(fmt, ...)) end}
ADRuns = {vehicleLabel = function(v) return v.label end}
Logging = {warning = function(...) print("WARN", ...) end}
local function fakeCourse(n)
return {getNumberOfWaypoints = function() return n end}
end
local function fakeVehicle(id, width, course)
return {label = id, course = course,
getFieldWorkCourse = function(self) return self.course end,
getCourseGeneratorSettings = function() return {workWidth = {getValue = function() return width end}} end}
end
local tractorA = fakeVehicle("A", 30, nil)
local tractorB = fakeVehicle("B", 30, nil)
ADRunsController = {getFarmVehicles = function() return {tractorA, tractorB} end,
getVehicleId = function(v) return v.label end}
local fieldObj, info = {id = 22}, {shapeSig = SIG, fruitName = "WHEAT"}
local assignA = {vehicle = tractorA, vehicleId = "A", role = "cultivator"}
-- G1: intet gemt -> ny beregning, opts urørt (ingen reuse/course)
local opts, ctx = C.prepare({}, fieldObj, info, assignA, {})
check("G1 ny beregning: ingen reuse", opts.reuse, nil)
check("G1b ingen kursus", opts.course, nil)
check("G1c log: nyt kursus (intet gemt)", logs[#logs]:find("nyt kursus beregnes", 1, true) ~= nil and logs[#logs]:find("intet kursus gemt", 1, true) ~= nil, true)
-- CP har lavet kurset (812 punkter) -> registreres
tractorA.course = fakeCourse(812)
C.afterStart(tractorA, "A", ctx)
check("G2 registreret", ADFieldJobs.state.courseRecords.A ~= nil and ADFieldJobs.state.courseRecords.A.waypoints, 812)
check("G2b gemt til fil", saves, 1)
-- G3: næste gang -> genbrug
opts, ctx = C.prepare({}, fieldObj, info, assignA, {})
check("G3 genbrug", opts.reuse, true)
check("G3b log genbrugt", logs[#logs]:find("kursus genbrugt", 1, true) ~= nil, true)
C.afterStart(tractorA, "A", ctx)
check("G3c uændret register gemmes ikke igen", saves, 1)
-- G4: B skal samme mark -> kopi fra A
local assignB = {vehicle = tractorB, vehicleId = "B", role = "cultivator"}
opts, ctx = C.prepare({}, fieldObj, info, assignB, {})
check("G4 kopi fra A", opts.course, tractorA.course)
check("G4b log kopieret", logs[#logs]:find("kopieret fra 'A'", 1, true) ~= nil, true)
tractorB.course = fakeCourse(812)
C.afterStart(tractorB, "B", ctx)
check("G4c B registreret", ADFieldJobs.state.courseRecords.B ~= nil, true)
-- G5: spilleren har lavet et nyt kursus på A i CP's HUD (andet antal punkter) -> kopi fra B
tractorA.course = fakeCourse(400)
opts = C.prepare({}, fieldObj, info, assignA, {})
check("G5 A's kursus ændret -> kopi fra B", opts.course, tractorB.course)
-- G6: kursus kopieret fra slåmaskinen (opts.course) -> registret bruges ikke, maskinens post glemmes
local mowerCourse = fakeCourse(99)
opts, ctx = C.prepare({}, fieldObj, info, assignB, {course = mowerCourse})
check("G6 opts urørt", opts.course, mowerCourse)
check("G6b ingen kontekst", ctx, nil)
tractorB.course = fakeCourse(99)
C.afterStart(tractorB, "B", ctx)
check("G6c B glemt", ADFieldJobs.state.courseRecords.B, nil)
-- G7: vinmark: vin-offset indgår i nøglen
local vineReq = C.requestFor(tractorA, 22, {shapeSig = SIG, fruitName = "GRAPE"}, "fertilizer", {})
check("G7 vin i nøglen", vineReq.settings, C.settingsKey({vineOffset = -1, vineSkip = 1}))
-- G8: markens form ændret -> A's post glemmes, log
local before = saves
check("G8 checkField sletter", C.checkField(22, "n=9;a=1;c=0,0"), 1)
check("G8b register tomt for marken", ADFieldJobs.state.courseRecords.A, nil)
check("G8c gemt", saves, before + 1)
check("G9 samme form: intet", C.checkField(22, "n=9;a=1;c=0,0"), 0)
-- G10: fejl i spil-API'et -> falder tilbage til ny beregning (opts urørt), ingen crash
local brokenVehicle = {label = "X", getFieldWorkCourse = function() error("boom") end}
local plainOpts = {lane = 1}
local okPrep, gotOpts, gotCtx = pcall(C.prepare, {}, fieldObj, nil, {vehicle = brokenVehicle, vehicleId = "X", role = "x"}, plainOpts)
check("G10 ingen crash", okPrep, true)
check("G10b opts urørt", gotOpts, plainOpts)
check("G10c ingen kontekst", gotCtx, nil)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-178
View File
@@ -1,178 +0,0 @@
-- Kør: luajit tests/test_adCpBridge.lua (fra repo-roden)
-- Kun getWorkWidth: variabel bredde (Unrealistic Pack) vinder over AI-markørernes grundbredde.
dofile("FS25_ADSmartPickup/adCpBridge.lua")
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
local function withMarkers(width)
return {getAIMarkers = function() return 1, 2, 3, 4, width end}
end
-- W1: læssevogn med variabel bredde 20,4 + 20,4 = 40,8 m, AI-markører siger 2,4 m
local wagon = withMarkers(2.4)
wagon.getVariableWorkWidth = function(_, isLeft) return isLeft and 20.4 or -20.4, nil, true end
local tractor = {getAttachedImplements = function() return {{object = wagon}} end}
check("W1 variabel bredde", ADCpBridge.getWorkWidth(tractor), 40.8)
-- W2: ugyldig variabel bredde -> AI-markører
local plain = withMarkers(9.5)
plain.getVariableWorkWidth = function() return 0, nil, false end
check("W2 AI-markører", ADCpBridge.getWorkWidth(plain), 9.5)
-- W3: selvkørende slåmaskine uden redskaber, kun variabel bredde
local mower = {getVariableWorkWidth = function(_, isLeft) return isLeft and 20.4 or -20.4, nil, true end}
check("W3 selvkørende", ADCpBridge.getWorkWidth(mower), 40.8)
-- W4: Courseplays egen bredde (HUD'en) vinder — det er den marken køres med
local cpVehicle = {getVariableWorkWidth = function(_, isLeft) return isLeft and 20.4 or -0.4, nil, true end,
getCourseGeneratorSettings = function() return {workWidth = {getValue = function() return 40.8 end}} end}
check("W4 Courseplay-bredde", ADCpBridge.getWorkWidth(cpVehicle), 40.8)
-- W5: Courseplay-indstilling 0/ugyldig -> egen beregning
local zeroCp = {getVariableWorkWidth = function(_, isLeft) return isLeft and 5 or -5, nil, true end,
getCourseGeneratorSettings = function() return {workWidth = {getValue = function() return 0 end}} end}
check("W5 CP 0 -> egen beregning", ADCpBridge.getWorkWidth(zeroCp), 10)
-- WO: skårets sideforskydning på en river, målt fra nedlægningsområdet (venstre positiv), minus rivens
-- egen CP-forskydning. Noder er tal; localToLocal-mock giver nodens x i traktorens ramme.
local nodeX = {[11] = 2.0, [12] = 4.0, [21] = -1.0, [22] = 1.0}
function localToLocal(node) return nodeX[node] or 0, 0, 0 end
local function rake(dropIndex, runtime)
local areas = {
{start = 1, width = 2, height = 3, dropWindrowWorkAreaIndex = runtime and dropIndex or nil},
{start = 10, width = 11, height = 12},
{start = 20, width = 21, height = 22},
}
return {spec_windrower = {}, spec_workArea = {workAreas = areas}, configFileName = "rake.xml"}
end
local sideRake = rake(2, true)
local tractorWithRake = {rootNode = 100, getAttachedImplements = function() return {{object = sideRake}} end,
getCpSettings = function() return {toolOffsetX = {getValue = function() return 0.5 end}} end}
check("WO1 sideriver (runtime-felt)", ADCpBridge.getWindrowOffset(tractorWithRake), 2.5)
local centerRake = rake(3, true)
local tractorCenter = {rootNode = 100, getAttachedImplements = function() return {{object = centerRake}} end}
check("WO2 midterrive", ADCpBridge.getWindrowOffset(tractorCenter), 0)
-- WO3: runtime-felt mangler -> riverens XML (workArea(0) windrower#dropWindrowWorkAreaIndex = 2)
ADCpBridge.readDropIndicesFromXml = function(fileName) return fileName == "rake.xml" and {[1] = 2} or {} end
local xmlRake = rake(2, false)
local tractorXml = {rootNode = 100, getAttachedImplements = function() return {{object = xmlRake}} end}
check("WO3 fra XML", ADCpBridge.getWindrowOffset(tractorXml), 3)
check("WO4 ingen river -> nil", ADCpBridge.getWindrowOffset({rootNode = 100}), nil)
-- SR: sprøjte/spreder -> rolle ud fra fyldet
check("SR gødning", ADCpBridge.sprayRoleForFillType("FERTILIZER"), "fertilizer")
check("SR flydende", ADCpBridge.sprayRoleForFillType("LIQUIDFERTILIZER"), "fertilizer")
check("SR gylle", ADCpBridge.sprayRoleForFillType("LIQUIDMANURE"), "fertilizer")
check("SR møg", ADCpBridge.sprayRoleForFillType("MANURE"), "fertilizer")
check("SR digestat", ADCpBridge.sprayRoleForFillType("DIGESTATE"), "fertilizer")
check("SR kalk", ADCpBridge.sprayRoleForFillType("LIME"), "limer")
check("SR ukrudt", ADCpBridge.sprayRoleForFillType("HERBICIDE"), "weeder")
check("SR vand/ukendt", ADCpBridge.sprayRoleForFillType("WATER"), nil)
-- SR2: tom Bredal-spreder (tager gødning OG kalk) kan begge dele; fyldt med kalk er den primært kalkspreder
local bredal = {FERTILIZER = true, LIME = true}
local p1, r1, f1 = ADCpBridge.sprayRolesFor(nil, bredal)
check("SR2 tom spreder primært gødning", p1, "fertilizer")
check("SR2 tom spreder kan kalke", r1 and r1.limer, true)
check("SR2 tom = intet fyld", f1, nil)
local p2, r2, f2 = ADCpBridge.sprayRolesFor("LIME", bredal)
check("SR2 med kalk = kalkspreder", p2, "limer")
check("SR2 med kalk kan stadig gøde (efter tømning)", r2 and r2.fertilizer, true)
check("SR2 fyldets rolle", f2, "limer")
check("SR2 ukrudtssprøjte", (ADCpBridge.sprayRolesFor(nil, {HERBICIDE = true, WATER = true})), "weeder")
check("SR2 vandvogn er ingen sprøjte", (ADCpBridge.sprayRolesFor(nil, {WATER = true})), nil)
-- SR3: kun en lille rest må hældes ud (100.000 l gødning blev hældt ud in-game 26/9)
check("SR3 tom", ADCpBridge.isDumpable(0, 4000), true)
check("SR3 lille rest 300 af 4000", ADCpBridge.isDumpable(300, 4000), true)
check("SR3 halv tank 2000 af 4000", ADCpBridge.isDumpable(2000, 4000), false)
check("SR3 100000 l aldrig", ADCpBridge.isDumpable(100000, 1000000), false)
check("SR3 over 2000 l aldrig", ADCpBridge.isDumpable(2500, 100000), false)
-- VN: vinmark — i gangen for alt undtagen høsteren; almindelige marker røres ikke
check("VN sprøjte på druer = venstre", ADCpBridge.vineOffsetFor("GRAPEB", "fertilizer"), -1)
check("VN harve på druer = venstre", ADCpBridge.vineOffsetFor("grape", "cultivator"), -1)
check("VN høster på druer = på vinen", ADCpBridge.vineOffsetFor("GRAPEB", "harvester"), 0)
check("VN hvede røres ikke", ADCpBridge.vineOffsetFor("WHEAT", "fertilizer"), nil)
check("VN ukendt afgrøde", ADCpBridge.vineOffsetFor(nil, "weeder"), nil)
check("VN sprøjte springer 1 række over (begge sider)", ADCpBridge.vineSkipFor("GRAPEB", "weeder"), 1)
check("VN gødning springer 1 over", ADCpBridge.vineSkipFor("GRAPEB", "fertilizer"), 1)
check("VN harve springer ingen over", ADCpBridge.vineSkipFor("GRAPEB", "cultivator"), 0)
check("VN høster springer ingen over", ADCpBridge.vineSkipFor("GRAPEB", "harvester"), 0)
check("VN hvede røres ikke", ADCpBridge.vineSkipFor("WHEAT", "weeder"), nil)
-- HF: høsterens afgrøder fra skærebordet (liste eller opslag); finsnitter tager ingen
local list = ADCpBridge.harvestFruitSet({fruitTypes = {3, 5}})
check("HF liste", list[3] and list[5], true)
local lookup = ADCpBridge.harvestFruitSet({fruitTypes = {[3] = true}})
check("HF opslag", lookup[3], true)
local forage = ADCpBridge.harvestFruitSet({fruitTypes = {3}, fruitTypeConverters = {[3] = {}}})
check("HF finsnitter tager intet", next(forage), nil)
check("HF fruitTypeIndices", ADCpBridge.harvestFruitSet({fruitTypeIndices = {7}})[7], true)
-- PL: plantbar i perioden
local fruitPlant = {growthDataSeasonal = {periods = {[3] = {plantingAllowed = true}, [4] = {plantingAllowed = false}}}}
check("PL tilladt", ADCpBridge.isPlantableIn(fruitPlant, 3), true)
check("PL ikke nu", ADCpBridge.isPlantableIn(fruitPlant, 4), false)
check("PL ukendt data = tilladt", ADCpBridge.isPlantableIn({}, 4), true)
-- CR: kursus-genbrug — start på maskinens eget kursus uden at CP beregner et nyt (opts.reuse)
do
local generated, started = 0, 0
AIJobType = {FIELDWORK_CP = 1}
function getWorldTranslation() return 0, 0, 0 end
local job = {cpJobParameters = {}}
function job.applyCurrentState() end
function job.setValues() end
function job.validate() return true end
function job.registerFieldBoundaryDetectionCallback() end
function job.detectFieldBoundary() return true, false, "" end
function job.onClickGenerateFieldWorkCourse(_, callback) generated = generated + 1; callback(true) end
g_currentMission = {aiJobTypeManager = {createJob = function() return job end},
aiSystem = {startJob = function() started = started + 1 end}}
local course = {}
local vehicle = {rootNode = 1, getOwnerFarmId = function() return 1 end, getFieldWorkCourse = function() return course end}
local reason = "?"
ADCpBridge.start(vehicle, 0, 0, function(ok, r) reason = ok and (r or "generated") or "fail" end, {reuse = true})
check("CR1 genbrug: ingen beregning", generated, 0)
check("CR2 genbrug: CP startet", started, 1)
check("CR3 genbrug: reason", reason, "reused")
-- ingen kursus på maskinen -> beregnes alligevel
vehicle.getFieldWorkCourse = function() return nil end
ADCpBridge.start(vehicle, 0, 0, function(ok, r) reason = ok and (r or "generated") or "fail" end, {reuse = true})
check("CR4 intet kursus -> beregnes", generated, 1)
check("CR5 reason ny", reason, "generated")
-- uden reuse: beregnes som før
vehicle.getFieldWorkCourse = function() return course end
ADCpBridge.start(vehicle, 0, 0, function(ok, r) reason = ok and (r or "generated") or "fail" end, {})
check("CR6 uden reuse -> beregnes", generated, 2)
AIJobType, g_currentMission = nil, nil
end
-- forager: 2 runder og start på forageren (så der ikke køres ind i træer o.l. i hjørnerne)
local recorded = {}
local function setting(name) return {setValue = function(_, v) recorded[name] = v end} end
local headlandVehicle = {getCourseGeneratorSettings = function() return {numberOfHeadlands = setting("headlands"), startOnHeadland = setting("startOnHeadland")} end}
check("forager: sat", ADCpBridge.setHeadlands(headlandVehicle, 2), true)
check("forager: 2 runder", recorded.headlands, 2)
check("forager: start på forageren", recorded.startOnHeadland, true)
ADCpBridge.setHeadlands(headlandVehicle, 0)
check("forager: ingen -> ikke start på forageren", recorded.startOnHeadland, false)
check("forager: køretøj uden CP", ADCpBridge.setHeadlands({}, 2), false)
check("forager: standard er 2", ADCpBridge.HEADLANDS, 2)
-- genbrugt kursus uden forager beregnes på ny
check("forager i kursus: 2 runder", ADCpBridge.courseHasHeadlands({getNumberOfHeadlands = function() return 2 end}), true)
check("forager i kursus: ingen", ADCpBridge.courseHasHeadlands({getNumberOfHeadlands = function() return 0 end}), false)
check("forager i kursus: ukendt -> antag ja", ADCpBridge.courseHasHeadlands({}), true)
check("forager i kursus: intet kursus", ADCpBridge.courseHasHeadlands(nil), false)
-- isWorking: AutoDrive kører også som AI i FS25 — det er ikke Courseplay (in-game 29/9: Milk Store holdt ved
-- Cow Wait med AD aktiv og "ventede på sig selv" som 'Courseplay kører')
local function fakeVehicle(cp, ai, ad)
return {getIsCpFieldWorkActive = function() return cp end, getIsAIActive = function() return ai end,
ad = {stateModule = {isActive = function() return ad end}}}
end
check("CP arbejder: Courseplay markarbejde", ADCpBridge.isWorking(fakeVehicle(true, true, false)), true)
check("CP arbejder: AI uden AD (CP-kørsel)", ADCpBridge.isWorking(fakeVehicle(false, true, false)), true)
check("CP arbejder: AutoDrive kører -> ikke CP", ADCpBridge.isWorking(fakeVehicle(false, true, true)), false)
check("CP arbejder: intet", ADCpBridge.isWorking(fakeVehicle(false, false, false)), false)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-33
View File
@@ -1,33 +0,0 @@
-- Kør: luajit tests/test_adCpResume.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adCpResume.lua")
local R = ADCpResume
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
-- hvem skrives ved gem: CP kører, AD styrer ikke (AD genstarter selv sine)
local ids = R.activeIds({{id = "a", cpActive = true}, {id = "b", cpActive = true, adActive = true}, {id = "c"}, {id = "d", cpActive = true}})
check("gem: CP-aktive uden AD", table.concat(ids, " "), "a d")
check("gem: tom liste", #R.activeIds({}), 0)
-- records frem og tilbage
local set = R.fromRecords({"a", "", "d", "a"})
check("indlæs: a", set.a, true)
check("indlæs: tomt id springes over", set[""], nil)
-- hvem startes ved indlæsning
local pending = {a = true, b = true, c = true, d = true}
local vehicles = {{id = "a"}, {id = "b", aiActive = true}, {id = "c", controlled = true}, {id = "d"}, {id = "e"}}
local start = R.toStart(pending, vehicles)
check("start: kun gemte, ikke allerede kørende/spiller i", table.concat(start, " "), "a d")
check("start: ingen gemt liste -> nil (gammel regel bruges)", R.toStart(nil, vehicles), nil)
-- kun når CP's HUD-job er markarbejde: moddets markopgaver starter CP med eget job, så HUD'en kan stå på fx
-- bunkersilo — HUD-start gav "CP: No silo found" efter genindlæsning (in-game 29/9)
local mixed = R.toStart({a = true, b = true, c = true}, {
{id = "a", fieldWork = true}, {id = "b", fieldWork = false}, {id = "c"}})
check("start: kun markarbejds-job", table.concat(mixed, ","), "a,c")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-145
View File
@@ -1,145 +0,0 @@
-- Kør: luajit tests/test_adFieldFleet.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFieldModel.lua")
dofile("FS25_ADSmartPickup/adFieldFleet.lua")
local F = ADFieldFleet
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
-- FL1: rolle ud fra redskabets spec (læssevogn vinder over andet på samme køretøj)
check("FL1 slåmaskine", F.roleFromSpecs({spec_mower = true}), "mower")
check("FL1 vender", F.roleFromSpecs({spec_tedder = true}), "tedder")
check("FL1 river", F.roleFromSpecs({spec_windrower = true}), "rake")
check("FL1 læssevogn", F.roleFromSpecs({spec_forageWagon = true, spec_mower = true}), "collector")
check("FL1 intet", F.roleFromSpecs({spec_trailer = true}), nil)
local function rig(id, role, x, extra)
local r = {id = id, role = role, enabled = true, busy = false, controlled = false, width = 40.8, x = x, z = 0, label = id}
for k, v in pairs(extra or {}) do r[k] = v end
return r
end
-- FL2: nærmeste ledige med flueben og rigtig rolle
local picked = F.pick({rig("far", "mower", 500), rig("near", "mower", 50), rig("wagon", "collector", 10)}, "mower", 0, 0, nil)
check("FL2 nærmeste slåmaskine", picked.id, "near")
-- FL3: uden flueben / optaget / spiller i sædet springes over
local _, why = F.pick({rig("a", "mower", 1, {enabled = false})}, "mower", 0, 0)
check("FL3 intet med flueben", why, "noRig")
local _, whyBusy = F.pick({rig("a", "mower", 1, {busy = true}), rig("b", "mower", 2, {controlled = true})}, "mower", 0, 0)
check("FL3 alle optaget", whyBusy, "allBusy")
check("FL3 ingen med rollen", select(2, F.pick({rig("a", "tedder", 1)}, "mower", 0, 0)), "noRig")
-- FL4: læssevogn smallere end slåbredden springes over, bredere vælges
local wide = F.pick({rig("smal", "collector", 1, {width = 20}), rig("bred", "collector", 900, {width = 40.8})}, "collector", 0, 0, 40.8)
check("FL4 bred valgt selv om længere væk", wide.id, "bred")
-- FL5: kun for smalle -> grund tooNarrow med tal
local none, whyNarrow, args = F.pick({rig("smal", "collector", 1, {width = 20})}, "collector", 0, 0, 40.8)
check("FL5 ingen", none, nil); check("FL5 grund", whyNarrow, "tooNarrow"); check("FL5 bredde", args[1], 20)
-- FL6: over 50 m -> tooWide
check("FL6 for bred", select(2, F.pick({rig("x", "mower", 1, {width = 60})}, "mower", 0, 0)), "tooWide")
-- FL7: toggle giver ny tabel
local enabled = {a = true}
local toggled = F.toggle(enabled, "b")
check("FL7 b til", toggled.b, true); check("FL7 original urørt", enabled.b, nil)
check("FL7 a fra", F.toggle(toggled, "a").a, nil)
-- FL8: nye roller i prioriteret rækkefølge
check("FL8 høster", F.roleFromSpecs({spec_combine = true, spec_cutter = true}), "harvester")
check("FL8 såmaskine før kultivator (combi)", F.roleFromSpecs({spec_sowingMachine = true, spec_cultivator = true, sprayRole = "fertilizer"}), "seeder")
check("FL8 kultivator", F.roleFromSpecs({spec_cultivator = true}), "cultivator")
check("FL8 plov", F.roleFromSpecs({spec_plow = true}), "plow")
check("FL8 tromle", F.roleFromSpecs({spec_roller = true}), "roller")
check("FL8 strigle", F.roleFromSpecs({spec_weeder = true}), "weeder")
check("FL8 stubhakker", F.roleFromSpecs({spec_mulcher = true}), "mulcher")
check("FL8 gødningssprøjte", F.roleFromSpecs({sprayRole = "fertilizer"}), "fertilizer")
check("FL8 kalkspreder", F.roleFromSpecs({sprayRole = "limer"}), "limer")
check("FL8 ukrudtssprøjte", F.roleFromSpecs({sprayRole = "weeder"}), "weeder")
check("FL8 aflæsningsvogn", F.roleFromSpecs({canUnload = true}), "unloader")
check("FL8 traktor med plov er ikke aflæsningsvogn", F.roleFromSpecs({spec_plow = true, canUnload = true}), "plow")
-- FL9: accept-filter (fx høster der kan tage afgrøden, strigle kun til småt ukrudt)
local combineWheat = rig("jd", "harvester", 10, {fruits = {[3] = true}})
local combineBeet = rig("ropa", "harvester", 1, {fruits = {[9] = true}})
local forWheat = F.pick({combineBeet, combineWheat}, "harvester", 0, 0, nil, function(c) return c.fruits[3] == true end)
check("FL9 høster der kan tage hvede", forWheat.id, "jd")
local _, whyFruit = F.pick({combineBeet}, "harvester", 0, 0, nil, function(c) return c.fruits[3] == true end)
check("FL9 ingen kan tage afgrøden", whyFruit, "cannotDo")
-- FL10: flere sæt til multi-tool — alle eller ingen, nærmeste først
local m1, m2, m3 = rig("m1", "mower", 30), rig("m2", "mower", 10), rig("m3", "mower", 20, {busy = true})
local two = F.pickMany({m1, m2, m3}, "mower", 2, 0, 0)
check("FL10 to valgt", #two, 2)
check("FL10 nærmeste først", two[1].id, "m2")
local none, whyMany, args = F.pickMany({m1, m2, m3}, "mower", 3, 0, 0)
check("FL10 for få -> ingen", none, nil)
check("FL10 grund", whyMany, "notEnough")
check("FL10 ledige/krævet", args[1] .. "/" .. args[2], "2/3")
check("FL10 ingen med rollen", select(2, F.pickMany({m1}, "tedder", 1, 0, 0)), "noRig")
-- FL11: markflådens tilstand: alle ledige maskiner (standard) eller kun med flueben
check("FL11 alle: uden flueben med", F.isEnabled("all", {}, "v1"), true)
check("FL11 flueben: uden flueben ude", F.isEnabled("ticked", {}, "v1"), false)
check("FL11 flueben: med flueben", F.isEnabled("ticked", {v1 = true}, "v1"), true)
check("FL11 ukendt tilstand = alle", F.isEnabled(nil, {}, "v1"), true)
-- FL12: ballepresser og ballesamler (ballevogn/Universal Autoload som Courseplays ballesamler kan bruge)
check("FL12 ballepresser", F.roleFromSpecs({spec_baler = true}), "baler")
check("FL12 ballesamler", F.roleFromSpecs({canBaleCollect = true}), "baleCollector")
check("FL12 ballesamler før aflæsningsvogn", F.roleFromSpecs({canBaleCollect = true, canUnload = true}), "baleCollector")
check("FL12 høster før presser", F.roleFromSpecs({spec_combine = true, spec_baler = true}), "harvester")
-- FL-spreder: tom spreder med rollerne {gødning, kalk} vælges til kalkning; spreder med forkert vare
-- på (kalk, skal gøde) kun hvis ingen anden kan — og den nærmeste med rigtig/ingen vare først
local spreaders = {
{id = "kalk", role = "limer", roles = {fertilizer = true, limer = true}, filledRole = "limer", enabled = true, x = 1, z = 0, width = 12},
{id = "tom", role = "fertilizer", roles = {fertilizer = true, limer = true}, enabled = true, x = 50, z = 0, width = 12},
}
check("FLS tom spreder tager kalkning", F.pick({spreaders[2]}, "limer", 0, 0) and F.pick({spreaders[2]}, "limer", 0, 0).id, "tom")
check("FLS gødskning: tom før nærmere med kalk på", F.pick(spreaders, "fertilizer", 0, 0).id, "tom")
check("FLS kun kalk-sprederen: bruges (tømmes)", F.pick({spreaders[1]}, "fertilizer", 0, 0).id, "kalk")
check("FLS kalkning: den med kalk på foretrækkes", F.pick(spreaders, "limer", 0, 0).id, "kalk")
-- FLS2: fuld af en anden vare (ikke dumpable) bruges slet ikke til opgaven
local fullOther = {id = "fuld", role = "fertilizer", roles = {fertilizer = true, weeder = true}, filledRole = "fertilizer", dumpable = false, enabled = true, x = 0, z = 0, width = 30}
check("FLS2 fuld gødningssprøjte bruges ikke til ukrudt", F.pick({fullOther}, "weeder", 0, 0), nil)
check("FLS2 men gerne til gødning", F.pick({fullOther}, "fertilizer", 0, 0).id, "fuld")
-- FLD: faste maskiner pr. mark
local sprayA = {id = "sprøjte", role = "fertilizer"}
local sprayB = {id = "spreder", role = "fertilizer"}
local harve = {id = "harve", role = "cultivator"}
local all = {sprayA, sprayB, harve}
local byField = {[22] = {"sprøjte"}}
local function ids(list) local out = {} for _, c in ipairs(list) do table.insert(out, c.id) end return table.concat(out, ",") end
check("FLD F22 gødskning: kun sin faste", ids(F.filterDedicated(all, 22, "fertilizer", byField)), "sprøjte")
check("FLD F109 gødskning: ikke F22's", ids(F.filterDedicated(all, 109, "fertilizer", byField)), "spreder,harve")
check("FLD F22 kultivering: ingen fast med rollen -> frie", ids(F.filterDedicated(all, 22, "cultivator", byField)), "spreder,harve")
check("FLD uden faste = alle", ids(F.filterDedicated(all, 5, "fertilizer", {})), "sprøjte,spreder,harve")
local moved = F.dedicate(byField, 109, "sprøjte", true)
check("FLD flyt: væk fra F22", #moved[22], 0)
check("FLD flyt: fast på F109", moved[109][1], "sprøjte")
check("FLD original urørt", byField[22][1], "sprøjte")
check("FLD fjern", #F.dedicate(moved, 109, "sprøjte", false)[109], 0)
-- FLV: menuen skjuler maskiner der er faste på en anden mark end den valgte
local vrigs = {{id = "vogn"}, {id = "harve"}, {id = "plov"}}
local vis, hid, flds = F.visibleForField(vrigs, 18, {[22] = {"vogn", "harve"}})
check("FLV F18 ser kun de frie", #vis .. "/" .. hid .. "/" .. tostring(flds[1]), "1/2/22")
check("FLV F22 ser alle", #(F.visibleForField(vrigs, 22, {[22] = {"vogn", "harve"}})), 3)
check("FLV ingen valgt = alle", #(F.visibleForField(vrigs, nil, {[22] = {"vogn"}})), 3)
-- FL-plov: FS25-plove har også spec_cultivator — de er plove, men må også tage kultivering
check("FLP plov med kultivator-spec er plov", F.roleFromSpecs({spec_plow = true, spec_cultivator = true}), "plow")
check("FLP plov kan kultivere", F.rolesFor("plow").cultivator, true)
check("FLP plov kan pløje", F.rolesFor("plow").plow, true)
check("FLP kultivator kan ikke pløje", F.rolesFor("cultivator"), nil)
local plough = {id = "p", role = "plow", roles = F.rolesFor("plow"), enabled = true, x = 0, z = 0, width = 3}
check("FLP plov vælges til kultivering", (F.pick({plough}, "cultivator", 0, 0, nil)).id, "p")
check("FLP rolesFor giver en kopi", F.rolesFor("plow") ~= F.rolesFor("plow"), true)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-255
View File
@@ -1,255 +0,0 @@
-- Kør: luajit tests/test_adFieldGuide.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adGuide.lua")
dofile("FS25_ADSmartPickup/adFieldModel.lua")
dofile("FS25_ADSmartPickup/adFieldFleet.lua")
dofile("FS25_ADSmartPickup/adFlowView.lua")
dofile("FS25_ADSmartPickup/adFieldGuide.lua")
local FG, G = ADFieldGuide, ADGuide
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
local SOWABLE = {{name = "WHEAT", title = "Hvede"}, {name = "BARLEY", title = "Byg"}, {name = "GRASS", title = "Græs"}, {name = "MAIZE", title = "Majs"}}
local RIGS = {
{id = "v1", text = "Fendt 1 — slåmaskine", role = "mower"},
{id = "v2", text = "Claas 2 — mejetærsker", role = "harvester"},
{id = "v3", text = "Sprøjte 3 — sprøjte", role = "weeder"},
{id = "v4", text = "Vogn 4 — læssevogn", role = "collector"},
}
local function ctx(info, extra)
local context = {
info = info, sowable = SOWABLE, mowable = {GRASS = true}, straw = {WHEAT = true, BARLEY = true},
rigs = RIGS, rigIdsByField = {[90] = {"v3"}}, fieldSilos = {}, storeHidden = 0,
stores = info.kind ~= "bare" and {{id = "s1", text = "Silo A, 300 m"}, {id = "s2", text = "Silo B, 1,2 km"}} or nil,
fields = {{id = 1, text = "F1 Græs"}, {id = 2, text = "F2 Hvede"}},
}
for key, value in pairs(extra or {}) do context[key] = value end
return context
end
local GRASS = {id = 1, kind = "mowable", fruitName = "GRASS", cropTitle = "Græs", markerId = 5}
local WHEAT = {id = 2, kind = "harvest", fruitName = "WHEAT", cropTitle = "Hvede", markerId = 6}
local MAIZE = {id = 3, kind = "harvest", fruitName = "MAIZE", cropTitle = "Majs", markerId = 7}
local BARE = {id = 4, kind = "bare", markerId = 8}
local function steps(guide) return table.concat(G.visibleSteps(guide), ",") end
local function field(id, plan)
local f = ADFieldModel.newField(id)
return ADFieldModel.with(f, {plan = plan or {}})
end
local function findRow(rows, id, item)
for _, row in ipairs(rows) do
if row.id == id and (item == nil or row.item == item) then return row end
end
return nil
end
local function countType(rows, rowType)
local n = 0
for _, row in ipairs(rows) do if row.type == rowType then n = n + 1 end end
return n
end
-- ---- synlige trin pr. marktype ----
check("FG1 græs", steps(FG.open(field(1), ctx(GRASS))), "crop,cropChoice,care,machines,store,summary")
check("FG1 hvede m. halm", steps(FG.open(field(2), ctx(WHEAT))), "crop,cropChoice,prep,care,machines,store,summary")
check("FG1 majs uden halm", steps(FG.open(field(3), ctx(MAIZE))), "crop,prep,care,machines,store,summary")
check("FG1 bar mark", steps(FG.open(field(4), ctx(BARE))), "crop,prep,machines,summary")
check("FG1 halm ukendt -> vis", steps(FG.open(field(3), ctx(MAIZE, {straw = {}}))), "crop,cropChoice,prep,care,machines,store,summary")
check("FG1 hvede med mål græs", steps(FG.open(field(2, {targetFruit = "GRASS"}), ctx(WHEAT))), "crop,cropChoice,prep,care,machines,store,summary")
check("FG1 bar med mål hvede", steps(FG.open(field(4, {targetFruit = "WHEAT"}), ctx(BARE))), "crop,cropChoice,prep,care,machines,summary")
check("FG1 græs med mål græs: ingen klargøring", steps(FG.open(field(1, {targetFruit = "GRASS"}), ctx(GRASS))), "crop,cropChoice,care,machines,store,summary")
check("FG1 græs med mål hvede: klargøring", steps(FG.open(field(1, {targetFruit = "WHEAT"}), ctx(GRASS))), "crop,cropChoice,prep,care,machines,store,summary")
local picker = FG.openPicker(ctx(GRASS))
check("FG1 vælger: kun mark", steps(picker), "field")
check("FG1 vælger: trin", picker.step, "field")
check("FG1 vælger: position uden total", (G.position(picker)), 1)
-- ---- standarder og udfyldning ----
local g = FG.open(field(2), ctx(WHEAT))
check("FG2 starter på afgrøde", g.step, "crop")
check("FG2 klargøring auto", g.draft.prepAuto, true)
check("FG2 ingen faste", g.draft.useDedicated, false)
check("FG2 lager auto", g.draft.storeMode, "auto")
check("FG2 kladde er kopi", g.draft.plan ~= nil and g.draft.fieldId, 2)
local plowed = FG.open(field(2, {soil = "plow"}), ctx(WHEAT))
check("FG2 pløj -> vælg selv", plowed.draft.prepAuto, false)
local withRigs = FG.open(field(2, {rigIds = {"v2"}}), ctx(WHEAT))
check("FG2 faste maskiner -> ja", withRigs.draft.useDedicated, true)
local silo = FG.open(field(1, {storeBuildingIds = {"m1"}}), ctx(GRASS, {fieldSilos = {{id = "m1", text = "Marksilo"}}}))
check("FG2 marksilo valgt", silo.draft.storeMode, "fieldSilo")
local manual = FG.open(field(1, {storeBuildingIds = {"s2"}}), ctx(GRASS))
check("FG2 valgt silo -> vælg selv", manual.draft.storeMode, "manual")
check("FG2 valgt silo i listen", manual.draft.storeIds[1], "s2")
-- ---- choose er immutabel ----
local chosen = FG.choose(g, "targetFruit", "BARLEY")
check("FG3 ny kladde", chosen.draft.plan.targetFruit, "BARLEY")
check("FG3 gammel urørt", g.draft.plan.targetFruit, "")
local toggled = FG.choose(withRigs, "rigIds", {item = "v1", on = true})
check("FG3 check tilføjer", table.concat(toggled.draft.plan.rigIds, ","), "v2,v1")
check("FG3 gammel liste urørt", table.concat(withRigs.draft.plan.rigIds, ","), "v2")
check("FG3 check fjerner", table.concat(FG.choose(toggled, "rigIds", {item = "v2", on = false}).draft.plan.rigIds, ","), "v1")
check("FG3 produkt hø", FG.choose(FG.open(field(1), ctx(GRASS)), "product", true).draft.plan.product, "hay")
check("FG3 produkt ensilage", FG.choose(FG.open(field(1), ctx(GRASS)), "product", false).draft.plan.product, "silage")
-- ---- rækker ----
local cropRows = FG.rows(g)
local target = findRow(cropRows, "targetFruit")
check("FG4 afgrøde: valg", target.type, "choice")
check("FG4 afgrøde: behold først", target.options[1].value, "")
check("FG4 afgrøde: såbare", #target.options, 5)
check("FG4 så igen for høstmark", findRow(cropRows, "sow") ~= nil, true)
check("FG4 så igen skjult med mål", findRow(FG.rows(chosen), "sow"), nil)
check("FG4 så igen skjult på græs", findRow(FG.rows(FG.open(field(1), ctx(GRASS))), "sow"), nil)
local choiceGrass = FG.rows(G.jump(FG.open(field(1), ctx(GRASS)), "cropChoice", "close"))
check("FG5 græs: ensilage/hø", findRow(choiceGrass, "product") ~= nil, true)
check("FG5 græs: river", findRow(choiceGrass, "rake") ~= nil, true)
check("FG5 græs: ingen halm", findRow(choiceGrass, "straw"), nil)
local choiceWheat = FG.rows(G.jump(g, "cropChoice", "close"))
check("FG5 hvede: halm", findRow(choiceWheat, "straw").type, "choice")
check("FG5 hvede: ingen ensilage", findRow(choiceWheat, "product"), nil)
local prepRows = FG.rows(G.jump(g, "prep", "close"))
check("FG6 klargøring auto: kun valget + tekst", findRow(prepRows, "soil"), nil)
check("FG6 klargøring valg", findRow(prepRows, "prepAuto").type, "choice")
local prepManual = FG.rows(G.jump(FG.choose(g, "prepAuto", false), "prep", "close"))
check("FG6 vælg selv: jord", findRow(prepManual, "soil") ~= nil, true)
check("FG6 vælg selv: kalk", findRow(prepManual, "lime") ~= nil, true)
check("FG6 vælg selv: mulch", findRow(prepManual, "mulch") ~= nil, true)
check("FG6 vælg selv: ingen tromle uden såning", findRow(prepManual, "roll"), nil)
-- pleje: Automatisk/Manuelt som klargøring (brugerønske 29/9); knapperne vises under Manuelt
local careAutoRows = FG.rows(G.jump(g, "care", "close"))
check("FG7 pleje: valg auto/manuel", findRow(careAutoRows, "careAuto").type, "choice")
check("FG7 pleje auto: ingen knapper", findRow(careAutoRows, "fertilize"), nil)
check("FG7 standard: pleje auto", g.draft.careAuto, true)
check("FG7 uden gødning -> manuel", FG.open(field(2, {fertilize = false}), ctx(WHEAT)).draft.careAuto, false)
local autoCare = FG.result(FG.choose(FG.open(field(2, {fertilize = false, weed = false}), ctx(WHEAT)), "careAuto", true), true).plan
check("FG7 auto slår gødning til", autoCare.fertilize, true)
check("FG7 auto slår ukrudt til", autoCare.weed, true)
local careGrass = FG.rows(G.jump(FG.choose(FG.open(field(1), ctx(GRASS)), "careAuto", false), "care", "close"))
check("FG7 græs: ingen ukrudt", findRow(careGrass, "weed"), nil)
check("FG7 græs: kalk i pleje", findRow(careGrass, "lime") ~= nil, true)
local careWheat = FG.rows(G.jump(FG.choose(g, "careAuto", false), "care", "close"))
check("FG7 hvede: ukrudt", findRow(careWheat, "weed") ~= nil, true)
check("FG7 hvede: kalk ikke i pleje", findRow(careWheat, "lime"), nil)
local machinesOff = FG.rows(G.jump(g, "machines", "close"))
check("FG8 antal ad gangen", findRow(machinesOff, "rigs").type, "choice")
check("FG8 aflæsningsvogne ved høst", findRow(machinesOff, "unloaders") ~= nil, true)
check("FG8 ingen liste uden faste", countType(machinesOff, "check"), 0)
check("FG8 ingen aflæsningsvogne på græs", findRow(FG.rows(G.jump(FG.open(field(1), ctx(GRASS)), "machines", "close")), "unloaders"), nil)
local machinesOn = FG.rows(G.jump(FG.choose(g, "useDedicated", true), "machines", "close"))
check("FG8 liste: v3 fast på F90 skjult", findRow(machinesOn, "rigIds", "v3"), nil)
check("FG8 liste: tre synlige", countType(machinesOn, "check"), 3)
local hiddenText = nil
for _, row in ipairs(machinesOn) do if row.type == "text" and row.label.key == "spu_field_hiddenDedicated" then hiddenText = row end end
check("FG8 tekst om skjulte", hiddenText ~= nil and hiddenText.label.args[1], 1)
check("FG8 tekst marker", hiddenText ~= nil and hiddenText.label.args[2], "F90")
local ownFirst = FG.rows(G.jump(withRigs, "machines", "close"))
local firstCheck = nil
for _, row in ipairs(ownFirst) do if row.type == "check" and firstCheck == nil then firstCheck = row end end
check("FG8 egne først", firstCheck.item, "v2")
check("FG8 egne sat", firstCheck.value, true)
local storeAuto = FG.rows(G.jump(FG.open(field(1), ctx(GRASS)), "store", "close"))
check("FG9 ingen marksilo-spørgsmål uden marksilo", findRow(storeAuto, "useFieldSilo"), nil)
check("FG9 automatisk/vælg selv", findRow(storeAuto, "storeAuto").type, "choice")
check("FG9 auto: ingen siloliste", countType(storeAuto, "check"), 0)
local storeManual = FG.rows(G.jump(FG.choose(FG.open(field(1), ctx(GRASS)), "storeAuto", false), "store", "close"))
check("FG9 vælg selv: siloer", countType(storeManual, "check"), 2)
local siloCtx = ctx(GRASS, {fieldSilos = {{id = "m1", text = "Marksilo F1"}}})
local storeSilo = FG.rows(G.jump(FG.open(field(1), siloCtx), "store", "close"))
check("FG9 marksilo-spørgsmål", findRow(storeSilo, "useFieldSilo").type, "toggle")
local useSilo = FG.choose(FG.open(field(1), siloCtx), "useFieldSilo", true)
check("FG9 marksilo ja -> skjul auto", findRow(FG.rows(G.jump(useSilo, "store", "close")), "storeAuto"), nil)
local pickRows = FG.rows(picker)
check("FG10 vælger: links", countType(pickRows, "link"), 2)
check("FG10 vælger: id", findRow(pickRows, "field").value, 1)
local picked = FG.pickField(picker, field(2), ctx(WHEAT))
check("FG10 valgt mark", picked.draft.fieldId, 2)
check("FG10 videre til afgrøde", picked.step, "crop")
check("FG10 mark-trin stadig talt", (G.position(picked)), 2)
-- ---- resultat ----
local r = FG.result(g, true)
check("FG11 auto ved start", r.plan.mode, "auto")
check("FG11 manuel uden start", FG.result(g, false).plan.mode, "manual")
local autoField = FG.open(field(2, {mode = "auto"}), ctx(WHEAT))
check("FG11 Ret bevarer auto", FG.result(autoField, nil).plan.mode, "auto")
check("FG11 Ret bevarer manuel", FG.result(g, nil).plan.mode, "manual")
check("FG11 mål tvinger auto", FG.result(chosen, false).plan.mode, "auto")
check("FG11 mål ændret", FG.result(chosen, true).targetChanged, true)
check("FG11 mål uændret", r.targetChanged, false)
check("FG11 ingen rigIds i plan", r.plan.rigIds, nil)
local soilPlan = FG.result(FG.open(field(2, {soil = "plow", mulch = true, lime = false, roll = true}), ctx(WHEAT)), true).plan
check("FG12 vælg selv bevarer jord", soilPlan.soil, "plow")
local autoPrep = FG.result(FG.choose(FG.open(field(2, {soil = "plow", mulch = true, lime = false, roll = true}), ctx(WHEAT)), "prepAuto", true), true).plan
check("FG12 auto: jord", autoPrep.soil, "auto")
check("FG12 auto: mulch", autoPrep.mulch, false)
check("FG12 auto: kalk", autoPrep.lime, true)
check("FG12 auto: tromle", autoPrep.roll, false)
local grassNoPrep = FG.result(FG.choose(FG.open(field(1, {lime = false}), ctx(GRASS)), "prepAuto", true), true).plan
check("FG12 græs: auto rører ikke kalk", grassNoPrep.lime, false)
check("FG13 lager auto", #FG.result(FG.open(field(1, {storeBuildingIds = {"s1"}}), ctx(GRASS)) and FG.choose(FG.open(field(1, {storeBuildingIds = {"s1"}}), ctx(GRASS)), "storeAuto", true), true).plan.storeBuildingIds, 0)
check("FG13 lager vælg selv", table.concat(FG.result(FG.choose(FG.choose(FG.open(field(1), ctx(GRASS)), "storeAuto", false), "storeBuildingIds", {item = "s2", on = true}), true).plan.storeBuildingIds, ","), "s2")
check("FG13 lager marksilo", table.concat(FG.result(useSilo, true).plan.storeBuildingIds, ","), "m1")
check("FG13 bar mark bevarer lager", table.concat(FG.result(FG.open(field(4, {storeBuildingIds = {"s9"}}), ctx(BARE)), true).plan.storeBuildingIds, ","), "s9")
check("FG14 faste fra -> tom", #FG.result(FG.choose(withRigs, "useDedicated", false), true).rigIds, 0)
check("FG14 faste til", table.concat(FG.result(toggled, true).rigIds, ","), "v2,v1")
-- ---- faste maskiner på tværs af marker ----
local byField = FG.applyRigs({[90] = {"v3", "v5"}, [2] = {"v2"}}, 2, {"v2", "v3"})
check("FG15 fjernet fra anden mark", table.concat(byField[90], ","), "v5")
check("FG15 sat på marken", table.concat(byField[2], ","), "v2,v3")
check("FG15 ny mark", table.concat(FG.applyRigs({}, 7, {"v1"})[7], ","), "v1")
-- ---- resumé ----
local function lineFor(lines, step)
for _, line in ipairs(lines) do if line.step == step then return line end end
return nil
end
local function partKeys(line)
local keys = {}
for _, part in ipairs(line.parts) do table.insert(keys, part.key or ("=" .. tostring(part.text))) end
return table.concat(keys, ",")
end
local sum = FG.summary(g)
check("FG16 linjer for synlige trin", #sum, 6)
check("FG16 afgrøde behold", partKeys(lineFor(sum, "crop")), "spu_guide_crop_keep")
check("FG16 halm", partKeys(lineFor(sum, "cropChoice")), "spu_guide_sum_straw_chop")
check("FG16 klargøring auto", partKeys(lineFor(sum, "prep")), "spu_guide_sum_auto")
check("FG16 pleje auto", partKeys(lineFor(sum, "care")), "spu_guide_sum_auto")
check("FG16 pleje manuel", partKeys(lineFor(FG.summary(FG.choose(g, "careAuto", false)), "care")), "spu_guide_sum_fert,spu_guide_sum_weed")
check("FG16 maskiner", partKeys(lineFor(sum, "machines")), "spu_guide_sum_freeRigs,spu_guide_sum_rigs,spu_guide_sum_unloaders")
check("FG16 lager auto", partKeys(lineFor(sum, "store")), "spu_guide_sum_auto")
check("FG16 ingen mark/summary-linje", lineFor(sum, "summary"), nil)
local sumTarget = FG.summary(chosen)
check("FG17 mål", partKeys(lineFor(sumTarget, "crop")), "spu_guide_sum_target")
check("FG17 mål titel", lineFor(sumTarget, "crop").parts[1].args[1], "Byg")
local sumRigs = FG.summary(withRigs)
check("FG17 faste", lineFor(sumRigs, "machines").parts[1].key, "spu_guide_sum_dedicated")
check("FG17 faste antal", lineFor(sumRigs, "machines").parts[1].args[1], 1)
local sumManual = FG.summary(FG.open(field(2, {soil = "plow", lime = true, mulch = true}), ctx(WHEAT)))
check("FG17 klargøring vælg selv", partKeys(lineFor(sumManual, "prep")), "spu_field_soil_plow,spu_guide_sum_lime,spu_guide_sum_mulch")
check("FG17 lager valgt silo", partKeys(lineFor(FG.summary(manual), "store")), "=Silo B, 1,2 km")
check("FG17 lager marksilo", partKeys(lineFor(FG.summary(useSilo), "store")), "spu_guide_sum_fieldSilo")
local sumHay = FG.summary(FG.choose(FG.choose(FG.open(field(1), ctx(GRASS)), "product", true), "rake", true))
check("FG17 hø + river", partKeys(lineFor(sumHay, "cropChoice")), "spu_field_plan_hay,spu_guide_sum_rake")
local sumNoCare = FG.summary(FG.choose(FG.choose(FG.choose(g, "careAuto", false), "fertilize", false), "weed", false))
check("FG17 ingen pleje", partKeys(lineFor(sumNoCare, "care")), "spu_guide_sum_noCare")
local sumSow = FG.summary(FG.choose(g, "sow", true))
check("FG17 så igen", partKeys(lineFor(sumSow, "crop")), "spu_guide_crop_keep,spu_guide_sum_sowAgain")
-- ---- oversigts-trinnet: links pr. linje ----
local summaryRows = FG.rows(G.new(g.def, g.draft, g.context, {step = "summary"}))
check("FG18 oversigt: link pr. linje", countType(summaryRows, "link"), 6)
check("FG18 oversigt: link-id", findRow(summaryRows, "jump").value, "crop")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-222
View File
@@ -1,222 +0,0 @@
-- Kør: luajit tests/test_adFieldJobs.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFieldModel.lua")
dofile("FS25_ADSmartPickup/adFieldJobs.lua")
local J = ADFieldJobs
local M = ADFieldModel
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
local info = {id = 109, markerId = 5, mowable = true, readyRatio = 1}
-- J1: klar auto-mark med markør -> start; uden markør -> vent
local auto = M.onReadiness(M.newField(109), 1); auto.plan.mode = "auto"
check("J1 start", J.decide(auto, info, {}, 0).action, "start")
check("J1 uden markør", J.decide(auto, {id = 109, mowable = true}, {}, 0).action, "wait")
-- J2: arbejdende trin uden sæt -> send rolle; alle sæt færdige -> næste trin
local mowing = M.start(auto)
local wanted = J.decide(mowing, info, {}, 0)
check("J2 send slåmaskine", wanted.action, "dispatch"); check("J2 rolle", wanted.role, "mower")
check("J2 sæt i gang -> ingenting", J.decide(mowing, info, {{phase = "working"}}, 0).action, "none")
local mowedInfo = {id = 109, markerId = 5, mowable = true, readyRatio = 0}
check("J2 færdig -> videre", J.decide(mowing, mowedInfo, {{phase = "finished"}}, 0).action, "advance")
-- J3: sæt frigivet (spilleren tog det) tæller ikke som færdigt
check("J3 frigivet -> send nyt", J.decide(mowing, info, {{phase = "released"}}, 0).action, "dispatch")
-- J4: slåning færdig men marken stadig klar -> ikke videre (afbrudt)
check("J4 afbrudt slåning", J.decide(mowing, {id = 109, markerId = 5, mowable = true, readyRatio = 0.8}, {{phase = "finished"}}, 0).action, "dispatch")
-- J5: opsamling efter genindlæsning uden sæt -> send læssevogn
local collecting = M.advance(mowing)
check("J5 genoptag opsamling", J.decide(collecting, info, {}, 0).role, "collector")
check("J5 to vogne maks", J.decide(collecting, info, {{phase = "working"}, {phase = "driving"}}, 0).action, "none")
-- J6: Courseplay efterlader et hjørne -> efter 2 færdige slå-ture går marken videre alligevel
local standing = {id = 109, markerId = 5, mowable = true, readyRatio = 0.8}
check("J6 én tur -> slå igen", J.decide(mowing, standing, {{phase = "finished"}}, 0).action, "dispatch")
check("J6 to ture -> videre", J.decide(mowing, standing, {{phase = "finished"}, {phase = "finished"}}, 0).action, "advance")
-- J7: slået mark uden sæt (fx sluppet undervejs) -> videre til opsamling, ikke en ny slå-tur
check("J7 slået uden sæt -> videre", J.decide(mowing, {id = 109, markerId = 5, mowable = true, readyRatio = 0.1}, {}, 0).action, "advance")
check("J7 stående uden sæt -> slå", J.decide(mowing, info, {}, 0).action, "dispatch")
check("J7 slået men sæt i gang -> vent", J.decide(mowing, {id = 109, markerId = 5, mowable = true, readyRatio = 0.1}, {{phase = "working"}}, 0).action, "none")
-- J8: høst — høster først (hovedrolle), aflæsningsvogne kun mens høsteren arbejder
local harvestInfo = {id = 50, markerId = 7, kind = "harvest", readyRatio = 1, fruitIndex = 3}
local harvesting = M.start(M.onReadiness(M.newField(50), 1), "harvest", {})
local d = J.decide(harvesting, harvestInfo, {}, 0)
check("J8 send høster", d.role, "harvester"); check("J8 ikke hjælper", d.helper, false)
local withHarvester = {{phase = "working", stage = "harvesting", helper = false}}
local dHelper = J.decide(harvesting, harvestInfo, withHarvester, 0)
check("J8 så aflæsningsvogn", dHelper.role, "unloader"); check("J8 hjælper", dHelper.helper, true)
check("J8 vogn på plads -> intet", J.decide(harvesting, harvestInfo, {withHarvester[1], {phase = "working", stage = "harvesting", helper = true}}, 0).action, "none")
-- manuel transport: høsteren venter på spilleren -> ingen høster mere og endnu ingen tømmevogn
local awaiting = {{phase = "awaitingPlayer", stage = "harvesting", helper = false}}
check("J8b venter på spilleren -> intet (ingen ny høster, ingen tømmevogn)", J.decide(harvesting, harvestInfo, awaiting, 0).action, "none")
-- høsteren færdig, afgrøden væk (marken er nu bar) -> videre, selv om vognen stadig kører
local afterInfo = {id = 50, markerId = 7, kind = "bare", readyRatio = 0}
check("J8 høst færdig -> videre", J.decide(harvesting, afterInfo, {{phase = "finished", stage = "harvesting"}, {phase = "working", stage = "harvesting", helper = true}}, 0).action, "advance")
-- sæt fra et tidligere trin (vogn på vej hjem) tæller ikke på det nye trin
local cultivating = M.advance(harvesting, {})
check("J8 gammel vogn tæller ikke", J.decide(cultivating, afterInfo, {{phase = "working", stage = "harvesting", helper = true}}, 0).role, "cultivator")
-- J9: multi-tool — hele holdet sendes samlet; mens nogen kører, sendes ingen nye hovedsæt
local multiMow = M.with(mowing, {plan = {rigs = 3}})
local dMulti = J.decide(multiMow, info, {}, 0)
check("J9 tre slåmaskiner", dMulti.count, 3)
check("J9 én kører stadig -> ingen nye", J.decide(multiMow, info, {{phase = "working"}, {phase = "finished"}}, 0).action, "none")
-- J10: pleje under vækst — Auto starter, Manuel viser behovet
local growingInfo = {id = 60, markerId = 8, kind = "harvest", growing = true, fert = 0, fertMax = 2, lime = 2, weed = 0, readyRatio = 0.2}
local autoField = M.with(M.newField(60), {plan = {mode = "auto"}})
local care = J.decide(autoField, growingInfo, {}, 0)
check("J10 auto -> pleje", care.action, "care"); check("J10 gødning", care.stage, "fertilizing")
local manualCare = J.decide(M.newField(60), growingInfo, {}, 0)
check("J10 manuel -> intet, men behov vist", manualCare.action, "none"); check("J10 behov", manualCare.care, "fertilizing")
check("J10 Start nu -> pleje", J.decide(M.with(M.newField(60), {startRequested = true}), growingInfo, {}, 0).action, "care")
check("J10 uden markør -> vent", J.decide(autoField, {id = 60, kind = "harvest", growing = true, fert = 0, fertMax = 2}, {}, 0).action, "wait")
-- J11: regn — vejrfølsomme trin sendes ikke ud (kørende sæt fortsætter); klargøring/pleje kører videre
local rain = {raining = true}
local dRain = J.decide(mowing, info, {}, 0, rain)
check("J11 slåning venter i regn", dRain.action, "wait"); check("J11 grund", dRain.reason, "rain")
check("J11 kørende slåmaskine fortsætter", J.decide(mowing, info, {{phase = "working"}}, 0, rain).action, "none")
check("J11 høst venter", J.decide(harvesting, harvestInfo, {}, 0, rain).reason, "rain")
check("J11 kultivering kører i regn", J.decide(cultivating, afterInfo, {}, 0, rain).role, "cultivator")
check("J11 start af høst venter", J.decide(M.with(M.onReadiness(M.newField(51), 1), {startRequested = true, kind = "harvest"}), harvestInfo, {}, 0, rain).reason, "rain")
local silageCollect = M.advance(mowing)
check("J11 ensilage-opsamling kører i regn", J.decide(silageCollect, info, {}, 0, rain).action, "dispatch")
local hayCollect = M.with(silageCollect, {plan = {product = "hay"}})
check("J11 hø-opsamling venter", J.decide(hayCollect, info, {}, 0, rain).reason, "rain")
check("J11 tørvejr", J.decide(mowing, info, {}, 0, {raining = false}).action, "dispatch")
-- J12: bar mark med mål-afgrøde klargøres straks — kun selve såningen venter på såtiden
local bareInfo = {id = 90, markerId = 9, kind = "bare", readyRatio = 0, needs = {}}
local bareTarget = M.with(M.newField(90), {plan = {targetFruit = "WHEAT", mode = "auto"}})
local canSow = {plantable = function(name) return name == "WHEAT" end}
check("J12 bar + såtid -> start", J.decide(bareTarget, bareInfo, {}, 0, canSow).action, "start")
check("J12 bar + ikke såtid -> start alligevel (klargøring)", J.decide(bareTarget, bareInfo, {}, 0, {plantable = function() return false end}).action, "start")
check("J12 bar uden markør -> vent", J.decide(bareTarget, {id = 90, kind = "bare"}, {}, 0, canSow).action, "wait")
check("J12 voksende mark med mål -> lad den vokse", J.decide(bareTarget, {id = 90, markerId = 9, kind = "harvest", growing = true, readyRatio = 0.3}, {}, 0, canSow).action, "none")
-- J13: afbrudt slåning efter genindlæsning (in-game 27/9 F90): genvækst i mellemhøjde -> vent; hjørne efter en tur -> slå straks
local regrowing = {id = 90, markerId = 9, mowable = true, readyRatio = 0.5, immatureRatio = 0.5}
local dRegrow = J.decide(mowing, regrowing, {}, 0)
check("J13 genvækst -> vent", dRegrow.action, "wait"); check("J13 grund", dRegrow.reason, "regrowing")
check("J13 genvækst men tur færdig -> slå hjørnet", J.decide(mowing, regrowing, {{phase = "finished"}}, 0).action, "dispatch")
check("J13 halvt slået uden genvækst -> slå resten", J.decide(mowing, {id = 90, markerId = 9, mowable = true, readyRatio = 0.6, immatureRatio = 0}, {}, 0).action, "dispatch")
-- J14: kultivering på en mark med uhøstede kartofler (in-game 27/9 F16) -> tilbage til klar; rest efter høst -> ikke
local cultivatingField = M.with(auto, {stage = "cultivating", kind = "harvest"})
check("J14 uhøstet -> spol tilbage", J.decide(cultivatingField, {id = 16, markerId = 9, kind = "harvest", phase = "ready", readyRatio = 1}, {}, 0).action, "rewind")
check("J14 høstrest 30 % -> kultiver", J.decide(cultivatingField, {id = 16, markerId = 9, kind = "harvest", phase = "ready", readyRatio = 0.3}, {}, 0).action ~= "rewind", true)
check("J14 stub -> kultiver", J.decide(cultivatingField, {id = 16, markerId = 9, kind = "bare", phase = "cut", readyRatio = 0}, {}, 0).action ~= "rewind", true)
-- resumeField: efter genindlæsning stopper spillet alle arbejdere; en CP-maskine der står PÅ en mark hvis
-- aktuelle trin passer til maskinens rolle, meldes ind på den mark igen (in-game 27/9)
do
local square = {{x = 0, z = 0}, {x = 100, z = 0}, {x = 100, z = 100}, {x = 0, z = 100}}
local fields = {
{id = 90, stage = "collecting", verts = square},
{id = 111, stage = "harvesting", verts = {{x = 200, z = 0}, {x = 300, z = 0}, {x = 300, z = 100}, {x = 200, z = 100}}},
}
check("RF høster på F111", ADFieldJobs.resumeField(250, 50, "harvester", fields), 111)
check("RF læssevogn på F90", ADFieldJobs.resumeField(50, 50, "collector", fields), 90)
check("RF forkert rolle til trinnet -> nil", ADFieldJobs.resumeField(250, 50, "collector", fields), nil)
check("RF uden for alle marker -> nil", ADFieldJobs.resumeField(150, 50, "harvester", fields), nil)
check("RF mark uden omrids -> nil", ADFieldJobs.resumeField(50, 50, "collector", {{id = 1, stage = "collecting", verts = {}}}), nil)
check("RF på kanten tæller som indenfor", ADFieldJobs.resumeField(0.5, 50, "collector", fields), 90)
end
-- startAt: hvor Courseplay skal starte i kurset (CP's standard er "first" = forfra, også på en halvt høstet mark;
-- in-game 27/9: mejetærskeren startede F111 forfra)
check("SA eget kursus med husket waypoint -> last", ADFieldJobs.startAtFor({reuse = true, remembered = true, stage = "harvesting", standingRatio = 0.4}), "last")
check("SA halvt høstet -> nearest", ADFieldJobs.startAtFor({stage = "harvesting", standingRatio = 0.4}), "nearest")
check("SA halvt slået -> nearest", ADFieldJobs.startAtFor({stage = "mowing", standingRatio = 0.5}), "nearest")
check("SA urørt mark -> first", ADFieldJobs.startAtFor({stage = "harvesting", standingRatio = 1}), "first")
check("SA næsten urørt (97 %) -> first", ADFieldJobs.startAtFor({stage = "harvesting", standingRatio = 0.97}), "first")
check("SA ukendt fremdrift -> first", ADFieldJobs.startAtFor({stage = "harvesting"}), "first")
check("SA klargøring (ingen afgrøde at måle) -> first", ADFieldJobs.startAtFor({stage = "cultivating", standingRatio = 0.3}), "first")
check("SA husket waypoint uden eget kursus -> ikke last", ADFieldJobs.startAtFor({reuse = false, remembered = true, stage = "cultivating"}), "first")
-- shouldResume: kun maskiner der stadig er midt i jobbet genstartes (in-game 27/9: to ROPA'er med et husket
-- waypoint fra et gammelt job kørte ud ad vejene mod det gamle kursus)
check("SR står på en mark i gang med sit trin -> ja", ADFieldJobs.shouldResume({fieldId = 111, distance = 400}), true)
check("SR lige ved sit huskede waypoint -> ja", ADFieldJobs.shouldResume({distance = 12}), true)
check("SR langt fra waypointet og ingen mark -> nej", ADFieldJobs.shouldResume({distance = 850}), false)
check("SR ukendt afstand og ingen mark -> nej", ADFieldJobs.shouldResume({}), false)
-- mayDispatch: markarbejdet sender højst én maskine ud pr. DISPATCH_GAP_MS på tværs af alle marker
-- (in-game 27/9: 6 maskiner kørte fra parkeringen i samme sekund efter Start alle)
check("MD første udsendelse", ADFieldJobs.mayDispatch(5000, nil), true)
check("MD for tæt på forrige", ADFieldJobs.mayDispatch(5000 + ADFieldJobs.DISPATCH_GAP_MS - 1, 5000), false)
check("MD efter mellemrummet", ADFieldJobs.mayDispatch(5000 + ADFieldJobs.DISPATCH_GAP_MS, 5000), true)
-- halvt høstet mark (in-game 27/9 F111): høsten er ikke færdig før næsten intet står; en mark i klargøring med
-- stående afgrøde spoles tilbage
do
local halfInfo = {id = 111, markerId = 5, kind = "harvest", phase = "ready", readyRatio = 0.45, standingRatio = 0.45, fruitIndex = 1}
check("HH halvt høstet uden høster -> send høster (ikke videre)", J.decide(harvesting, halfInfo, {}, 0).action, "dispatch")
local doneInfo = {id = 111, markerId = 5, kind = "harvest", phase = "ready", readyRatio = 0.02, standingRatio = 0.02, fruitIndex = 1}
check("HH næsten intet står -> videre", J.decide(harvesting, doneInfo, {}, 0).action, "advance")
local cultivating = M.with(harvesting, {stage = "cultivating"})
check("HH klargøring med halv mark stående -> spol tilbage", J.decide(cultivating, halfInfo, {}, 0).action, "rewind")
end
-- startTimedOut: CP må beregne kursus i op til 5 min (in-game 27/9 F90: 140 ha tog over 60 s, moden opgav midt i)
check("ST start uden beregning > 60 s -> opgiv", ADFieldJobs.startTimedOut(61000, false), true)
check("ST beregner stadig efter 90 s -> vent", ADFieldJobs.startTimedOut(90000, true), false)
check("ST beregner over 5 min -> opgiv", ADFieldJobs.startTimedOut(301000, true), true)
check("ST under 60 s -> vent", ADFieldJobs.startTimedOut(30000, false), false)
-- shouldEmptyHarvester: høsten er færdig, men høsteren har korn i tanken og står ved marken -> tøm først
-- (in-game 27/9 F15: marken gik til halm med fuld tank, ingen tømmevogn kom)
check("EH korn i tanken ved marken -> tøm", ADFieldJobs.shouldEmptyHarvester({fill = 12000, near = true}), true)
check("EH tom tank -> videre", ADFieldJobs.shouldEmptyHarvester({fill = 0, near = true}), false)
check("EH langt væk -> videre", ADFieldJobs.shouldEmptyHarvester({fill = 12000, near = false}), false)
check("EH optaget -> videre", ADFieldJobs.shouldEmptyHarvester({fill = 12000, near = true, busy = true}), false)
check("EH spiller i -> videre", ADFieldJobs.shouldEmptyHarvester({fill = 12000, near = true, controlled = true}), false)
check("EH allerede forsøgt -> videre", ADFieldJobs.shouldEmptyHarvester({fill = 12000, near = true, tried = true}), false)
-- nearestPoint: CP bruger markens punkt som kursets startsted — tag det nærmest traktoren (in-game 27/9 F17: første
-- punkt lå 280 m væk i den anden ende, og CP kørte gennem skoven)
do
local pts = {{x = 1600, z = -1839}, {x = 1350, z = -1740}, {x = 1500, z = -1800}}
local p = ADFieldJobs.nearestPoint(pts, 1340, -1733)
check("NP nærmest traktoren", p.x .. "," .. p.z, "1350,-1740")
check("NP uden punkter -> nil", ADFieldJobs.nearestPoint({}, 0, 0), nil)
check("NP uden position -> første", ADFieldJobs.nearestPoint(pts, nil, nil).x, 1600)
end
-- pulje: en traktor der henter et redskab tæller som i gang (ellers sendes endnu en ud)
check("pulje: henter -> ingenting", J.decide(mowing, info, {{phase = "fetching"}}, 0).action, "none")
-- JP: klargøring der allerede er gjort (fx efter genindlæsning) går videre uden at sende maskinen ud igen
-- (in-game 28/9: F17 blev kultiveret 3 gange — opgaver gemmes ikke, så "færdig" blev aldrig set)
local cultField = M.with(M.newField(17), {stage = "cultivating", kind = "bare", plan = {mode = "auto"}})
local cultInfo = {id = 17, markerId = 5, kind = "bare", readyRatio = 0, needs = {}}
cultInfo.prepared = 0.98
check("JP kultiveret -> videre", J.decide(cultField, cultInfo, {}, 0).action, "advance")
cultInfo.prepared = 0.4
check("JP halvt kultiveret -> send maskine", J.decide(cultField, cultInfo, {}, 0).action, "dispatch")
cultInfo.prepared = nil
check("JP ukendt jord -> som før (send)", J.decide(cultField, cultInfo, {}, 0).action, "dispatch")
cultInfo.prepared = 0.98
check("JP kultivator i gang -> vent på den", J.decide(cultField, cultInfo, {{phase = "working", stage = "cultivating"}}, 0).action, "none")
local plowField = M.with(cultField, {stage = "plowing"})
check("JP kultiveret er ikke pløjet -> send plov", J.decide(plowField, cultInfo, {}, 0).action, "dispatch")
cultInfo.plowed = 0.97
check("JP pløjet -> videre", J.decide(plowField, cultInfo, {}, 0).action, "advance")
local sowField = M.with(cultField, {stage = "sowing"})
check("JP såning berøres ikke af klargjort jord", J.decide(sowField, cultInfo, {}, 0).action ~= "advance", true)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-324
View File
@@ -1,324 +0,0 @@
-- Kør: luajit tests/test_adFieldModel.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFieldModel.lua")
local M = ADFieldModel
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
-- M1: ny mark = venter, manuel ensilage uden riv
local f = M.newField(109)
check("M1 stage", f.stage, "idle")
check("M1 mode", f.plan.mode, "manual")
check("M1 product auto", f.plan.product, "auto")
check("M1 auto = ensilage på slåafgrøde", M.product(f.plan, "mowable"), "silage")
check("M1 auto = høst ellers", M.product(f.plan, "harvest"), "harvest")
check("M1 rake", f.plan.rake, false)
-- M2: klar ved >= 90 %
check("M2 89 % ikke klar", M.onReadiness(f, 0.89).stage, "idle")
local ready = M.onReadiness(f, 0.9)
check("M2 90 % klar", ready.stage, "ready")
check("M2 immutabel", f.stage, "idle")
-- M3: klar mark slået af spilleren -> tilbage til venter
check("M3 slået manuelt", M.onReadiness(ready, 0.3).stage, "idle")
check("M3 lidt under 90 % forbliver klar", M.onReadiness(ready, 0.8).stage, "ready")
-- M4: trin pr. plan
check("M4 ensilage", table.concat(M.workStages({product = "silage"}), ","), "mowing,collecting")
check("M4 ensilage m. riv", table.concat(M.workStages({product = "silage", rake = true}), ","), "mowing,raking,collecting")
check("M4 hø", table.concat(M.workStages({product = "hay", rake = false}), ","), "mowing,tedding,collecting")
check("M4 hø m. riv", table.concat(M.workStages({product = "hay", rake = true}), ","), "mowing,tedding,raking,collecting")
-- M5: start: auto starter selv, manuel kun på forespørgsel
check("M5 manuel starter ikke", M.shouldStart(ready), false)
local auto = M.onReadiness(M.newField(5), 1)
auto.plan.mode = "auto"
check("M5 auto starter", M.shouldStart(auto), true)
local requested = M.newField(6); requested.stage = "ready"; requested.startRequested = true
check("M5 Start nu", M.shouldStart(requested), true)
local started = M.start(ready)
check("M5 start -> slås", started.stage, "mowing")
check("M5 start rydder forespørgsel", started.startRequested, false)
local oldMow = M.with(ready, {mowVehicleId = "old"})
check("M5 start glemmer gammel slåmaskine", M.start(oldMow).mowVehicleId, nil)
-- M6: videre gennem trinene; færdig mark bliver klar igen når den er vokset
local hay = M.start(M.onReadiness(M.newField(7), 1)); hay.plan.product = "hay"
hay = M.advance(hay); check("M6 hø -> vendes", hay.stage, "tedding")
hay = M.advance(hay); check("M6 -> samles op", hay.stage, "collecting")
hay = M.advance(hay); check("M6 -> færdig", hay.stage, "done")
check("M6 færdig forbliver ved lav andel", M.onReadiness(hay, 0.1).stage, "done")
check("M6 færdig -> klar når vokset", M.onReadiness(hay, 0.95).stage, "ready")
check("M6 arbejdende mark ignorerer andel", M.onReadiness(started, 0).stage, "mowing")
check("M6 isWorking", M.isWorking(started), true)
check("M6 stop -> klar", M.stop(started).stage, "ready")
-- M7: roller, maks sæt, bredde
check("M7 rolle slå", M.roleFor("mowing"), "mower")
check("M7 rolle saml", M.roleFor("collecting"), "collector")
check("M7 rolle klar = ingen", M.roleFor("ready"), nil)
check("M7 maks læssevogne = planens sæt", M.maxRigs("collecting", {rigs = 2}), 2)
check("M7 maks slå", M.maxRigs("mowing"), 1)
local okWide, whyWide = M.checkWidth("mower", 60, nil)
check("M7 over 50 m", okWide, false); check("M7 grund", whyWide, "tooWide")
check("M7 40,8 m slå ok", (M.checkWidth("mower", 40.8, nil)), true)
-- M7b: bredde under 1,5 m er ikke læst rigtigt (spreder gav 0,5 m -> CP-kursus hver halve meter på 72 ha
-- -> spillet frøs, in-game 26/9) — sendes aldrig ud
local okTiny, whyTiny = M.checkWidth("fertilizer", 0.5, nil)
check("M7b 0,5 m afvises", okTiny, false)
check("M7b grund", whyTiny, "widthUnknown")
check("M7b 3 m strigle ok", (M.checkWidth("weeder", 3, nil)), true)
check("M7b aflæsningsvogn uden bredde ok", (M.checkWidth("unloader", 0, nil)), true)
check("M7b ballesamler uden bredde ok", (M.checkWidth("baleCollector", 0, nil)), true)
local okNarrow, whyNarrow, args = M.checkWidth("collector", 20, 40.8)
check("M7 for smal", okNarrow, false); check("M7 grund smal", whyNarrow, "tooNarrow"); check("M7 args", args[2], 40.8)
check("M7 lige bred ok (tolerance)", (M.checkWidth("collector", 40.76, 40.8)), true)
check("M7 ukendt slåbredde ok", (M.checkWidth("tedder", 10, nil)), true)
-- wantsRig: kun arbejdende trin, under maks, efter pause
check("M7 wantsRig slå", M.wantsRig(started, 0, 0).role, "mower")
check("M7 wantsRig fuld", M.wantsRig(started, 1, 0), nil)
local paused = M.start(M.onReadiness(M.newField(8), 1)); paused.retryAt = 1000
check("M7 wantsRig i pause", M.wantsRig(paused, 0, 500), nil)
check("M7 wantsRig efter pause", M.wantsRig(paused, 0, 1000).role, "mower")
check("M7 wantsRig klar = nil", M.wantsRig(ready, 0, 0), nil)
-- M8: gem/indlæs bevarer trin, plan og slåbredde
local saved = M.advance(M.start(M.onReadiness(M.newField(109), 1))); saved.mowWidth = 40.8; saved.mowVehicleId = "vehicleBigM"
saved.plan = {mode = "auto", product = "hay", rake = true, storeBuildingIds = {"b12", "b7"}, rigIds = {"v1", "v2"}}
local back = M.fromRecord(M.toRecord(saved))
check("M8 id", back.id, 109)
check("M8 stage", back.stage, saved.stage)
check("M8 mode", back.plan.mode, "auto")
check("M8 product", back.plan.product, "hay")
check("M8 rake", back.plan.rake, true)
check("M8 lager", table.concat(back.plan.storeBuildingIds, " "), "b12 b7")
check("M8 faste maskiner", table.concat(back.plan.rigIds, " "), "v1 v2")
check("M8 slåbredde", back.mowWidth, 40.8)
check("M8 slåmaskine", back.mowVehicleId, "vehicleBigM")
check("M8 ingen slåmaskine", M.fromRecord({id = 4, stage = "idle"}).mowVehicleId, nil)
check("M8 ukendt trin -> venter", M.fromRecord({id = 3, stage = "bogus"}).stage, "idle")
-- M9: with() kopierer og fletter planen
local base = M.start(M.onReadiness(M.newField(1), 1))
local noted = M.with(base, {reason = {text = "x"}, retryAt = 5, plan = {product = "hay"}})
check("M9 grund", noted.reason.text, "x"); check("M9 pause", noted.retryAt, 5)
check("M9 plan flettet", noted.plan.product, "hay"); check("M9 mode bevaret", noted.plan.mode, "manual")
check("M9 trin bevaret", noted.stage, "mowing"); check("M9 original urørt", base.plan.product, "auto")
-- M10: Start nu på en mark der ikke er helt klar -> klar; arbejdende mark røres ikke
check("M10 venter -> klar", M.forceReady(M.newField(2)).stage, "ready")
check("M10 arbejdende uændret", M.forceReady(base).stage, "mowing")
-- M11: krævet bredde og kursus-kilde pr. rolle (smalle standardmaskiner, 25/9)
local cut = M.with(M.newField(3), {mowWidth = 40.8})
check("M11 vender kræver ingen bredde", M.requiredWidth(cut, "tedder"), nil)
check("M11 river kræver ingen bredde", M.requiredWidth(cut, "rake"), nil)
check("M11 læssevogn uden riv = slåbredde", M.requiredWidth(cut, "collector"), 40.8)
local raked = M.with(cut, {plan = {rake = true}})
check("M11 læssevogn efter riv = ingen krav", M.requiredWidth(raked, "collector"), nil)
check("M11 vender eget kursus", M.courseSource(cut, "tedder"), nil)
check("M11 river eget kursus", M.courseSource(raked, "rake"), nil)
check("M11 læssevogn uden riv -> slåkursus", M.courseSource(cut, "collector"), "mower")
check("M11 læssevogn efter riv -> rivekursus", M.courseSource(raked, "collector"), "rake")
check("M11 slåmaskine eget kursus", M.courseSource(cut, "mower"), nil)
-- gem/indlæs rivemaskine
local withRake = M.with(raked, {rakeVehicleId = "vehicleRake"})
check("M11 rivemaskine gemt", M.fromRecord(M.toRecord(withRake)).rakeVehicleId, "vehicleRake")
check("M11 start glemmer rivemaskine", M.start(M.with(M.onReadiness(M.newField(9), 1), {rakeVehicleId = "x"})).rakeVehicleId, nil)
-- M12: sideriver — læssevognens CP-forskydning = egen forskydning − skårets forskydning (venstre positiv),
-- holdt inden for Courseplays ±10 m; skårets forskydning gemmes pr. mark
check("M12 midterrive", M.collectorOffset(0, 0), 0)
check("M12 skår 3 m til venstre", M.collectorOffset(0, 3), -3)
check("M12 vogn med egen forskydning", M.collectorOffset(0.5, -2), 2.5)
check("M12 holdes inden for 10 m", M.collectorOffset(0, 14), -10)
check("M12 ukendt skår -> egen forskydning", M.collectorOffset(0.4, nil), 0.4)
local side = M.with(M.newField(11), {windrowOffset = 3.2})
check("M12 skår gemt", M.fromRecord(M.toRecord(side)).windrowOffset, 3.2)
check("M12 intet skår gemt", M.fromRecord(M.toRecord(M.newField(12))).windrowOffset, nil)
check("M12 start glemmer skår", M.start(M.with(M.onReadiness(M.newField(13), 1), {windrowOffset = 1})).windrowOffset, nil)
-- M13: gæringssilo = produktion der laver markens skår om til ensilage (FermentingSiloPack m.fl.)
local GRASS_W, DRY_W, SILAGE, CHAFF, BREAD = 10, 11, 20, 30, 40
local fermenter = {{inputs = {{type = CHAFF}}, outputs = {{type = SILAGE}}}, {inputs = {{type = GRASS_W}}, outputs = {{type = SILAGE}}}}
check("M13 græs -> ensilage", M.isFermenter(fermenter, GRASS_W, SILAGE), true)
check("M13 hø ikke i denne", M.isFermenter(fermenter, DRY_W, SILAGE), false)
check("M13 bageri ikke", M.isFermenter({{inputs = {{type = GRASS_W}}, outputs = {{type = BREAD}}}}, GRASS_W, SILAGE), false)
check("M13 ingen produktioner", M.isFermenter(nil, GRASS_W, SILAGE), false)
check("M13 ukendt ensilage-type", M.isFermenter(fermenter, GRASS_W, nil), false)
-- M14: høstkæde + klargøring efter behov
local plan = {mode = "manual", product = "auto", rake = false, rigs = 1, unloaders = 1, mulch = false, lime = true,
soil = "auto", sow = true, sowFruit = "", roll = true, fertilize = true, weed = true, storeBuildingIds = {}}
check("M14 høst + kultivér + så + tromle", table.concat(M.workStages(plan, "harvest", {}), ","), "harvesting,cultivating,sowing,rolling")
check("M14 kalk + pløj når jorden trænger", table.concat(M.workStages(plan, "harvest", {lime = true, plow = true}), ","), "harvesting,liming,plowing,sowing,rolling")
local noSoil = {soil = "none", sow = false, lime = false, mulch = true}
check("M14 kun stubbehakning", table.concat(M.workStages(noSoil, "harvest", {}), ","), "harvesting,mulching")
check("M14 bar mark = kun klargøring", table.concat(M.workStages(plan, "bare", {plow = true}), ","), "plowing,sowing,rolling")
check("M14 tvunget pløj", table.concat(M.prepStages({soil = "plow"}, {}), ","), "plowing")
local harvestField = M.start(M.with(M.newField(20), {plan = {sow = true, soil = "cultivate"}}), "harvest", {})
check("M14 start = høst", harvestField.stage, "harvesting")
check("M14 husker art", harvestField.kind, "harvest")
local afterHarvest = M.advance(harvestField, {})
check("M14 høst -> kultivér", afterHarvest.stage, "cultivating")
check("M14 -> så", M.advance(afterHarvest, {}).stage, "sowing")
check("M14 så -> færdig", M.advance(M.advance(afterHarvest, {}), {}).stage, "done")
check("M14 kalk springes over når jorden er kalket", M.advance(M.with(harvestField, {plan = {lime = true}}), {lime = false}).stage, "cultivating")
check("M14 tom kæde -> færdig", M.start(M.with(M.newField(21), {plan = {soil = "none", sow = false, lime = false}}), "bare", {}).stage, "done")
-- M15: hjælpere under høst og multi-tool-baner
check("M15 aflæsningsvogn efter høsteren", M.wantsRig(harvestField, {primary = 1, helpers = 0}, 0).role, "unloader")
check("M15 hjælper-flag", M.wantsRig(harvestField, {primary = 1, helpers = 0}, 0).helper, true)
check("M15 alle vogne på plads -> intet", M.wantsRig(harvestField, {primary = 1, helpers = 1}, 0), nil)
local multi = M.with(harvestField, {plan = {rigs = 3, unloaders = 2}})
check("M15 multi: 3 høstere samlet", M.wantsRig(multi, {primary = 0, helpers = 0}, 0).count, 3)
check("M15 multi: 2 vogne", M.maxHelpers("harvesting", multi.plan), 2)
check("M15 ingen hjælper ved slåning", M.maxHelpers("mowing", multi.plan), 0)
check("M15 baner 2", table.concat(M.lanePositions(2), ","), "-1,1")
check("M15 baner 3", table.concat(M.lanePositions(3), ","), "-1,0,1")
check("M15 baner 4", table.concat(M.lanePositions(4), ","), "-2,-1,1,2")
check("M15 baner 5", table.concat(M.lanePositions(5), ","), "-2,-1,0,1,2")
check("M15 over 5 -> 5", #M.lanePositions(9), 5)
check("M15 maxRigs holdes 1-5", M.maxRigs("mowing", {rigs = 7}), 5)
-- M16: pleje under vækst
local growing = {kind = "harvest", growing = true, fert = 1, fertMax = 2, lime = 2, weed = 0}
local idleField = M.newField(30)
check("M16 gødning", M.careNeed(idleField, growing), "fertilizing")
check("M16 ukrudt før gødning", M.careNeed(idleField, {kind = "harvest", growing = true, fert = 1, fertMax = 2, lime = 2, weed = 3}), "weeding")
check("M16 græs ukrudtsbehandles ikke", M.careNeed(idleField, {kind = "mowable", growing = true, fert = 2, fertMax = 2, lime = 2, weed = 3}), nil)
check("M16 kalk", M.careNeed(idleField, {kind = "harvest", growing = true, fert = 2, fertMax = 2, lime = 0, weed = 0}), "careLiming")
check("M16 intet behov", M.careNeed(idleField, {kind = "harvest", growing = true, fert = 2, fertMax = 2, lime = 1, weed = 1}), nil)
check("M16 ikke voksende", M.careNeed(idleField, {kind = "harvest", growing = false, fert = 0, fertMax = 2}), nil)
check("M16 gødning slået fra", M.careNeed(M.with(idleField, {plan = {fertilize = false}}), growing), nil)
check("M16 arbejdende mark", M.careNeed(harvestField, growing), nil)
local cared = M.startCare(idleField, "fertilizing")
check("M16 pleje-trin", cared.stage, "fertilizing")
check("M16 rolle", M.roleFor(cared.stage), "fertilizer")
check("M16 kalk-pleje rolle", M.roleFor("careLiming"), "limer")
check("M16 pleje -> idle", M.advance(cared, {}).stage, "idle")
local twice = M.startCare(M.advance(cared, {}), "fertilizing")
check("M16 højst 2 gange", M.careNeed(M.advance(twice, {}), growing), nil)
check("M16 ny cyklus nulstiller", M.careNeed(M.advance(M.start(M.advance(twice, {}), "harvest", {}), {}), growing) ~= "x", true)
check("M16 start nulstiller pleje-tæller", next(M.start(twice, "harvest", {}).careRuns), nil)
check("M16 stop af pleje -> idle", M.stop(cared).stage, "idle")
check("M16 stop af høst -> klar", M.stop(harvestField).stage, "ready")
-- M17: såafgrøde og gem/indlæs af den udvidede plan
check("M17 så samme afgrøde", M.sowFruit(M.with(M.newField(40), {lastFruit = "WHEAT"})), "WHEAT")
check("M17 planens afgrøde vinder", M.sowFruit(M.with(M.newField(40), {lastFruit = "WHEAT", plan = {sowFruit = "CANOLA"}})), "CANOLA")
local full = M.with(M.startCare(M.newField(41), "weeding"), {kind = "harvest", lastFruit = "BARLEY",
plan = {rigs = 3, unloaders = 2, mulch = true, lime = false, soil = "plow", sow = true, sowFruit = "WHEAT", roll = true, fertilize = false, weed = false, product = "harvest"}})
local round = M.fromRecord(M.toRecord(full))
check("M17 trin", round.stage, "weeding")
check("M17 sæt", round.plan.rigs, 3)
check("M17 vogne", round.plan.unloaders, 2)
check("M17 stub", round.plan.mulch, true)
check("M17 kalk fra", round.plan.lime, false)
check("M17 jord", round.plan.soil, "plow")
check("M17 så", round.plan.sow, true)
check("M17 såafgrøde", round.plan.sowFruit, "WHEAT")
check("M17 tromle", round.plan.roll, true)
check("M17 gødning fra", round.plan.fertilize, false)
check("M17 ukrudt fra", round.plan.weed, false)
check("M17 art", round.kind, "harvest")
check("M17 sidste afgrøde", round.lastFruit, "BARLEY")
check("M17 pleje-tæller", round.careRuns.weeding, 1)
check("M17 produkt", round.plan.product, "harvest")
local legacy = M.fromRecord({id = 5, stage = "collecting", mode = "auto", product = "hay", rake = 1})
check("M17 etape 1-post: hø", legacy.plan.product, "hay")
check("M17 etape 1-post: standard kalk til", legacy.plan.lime, true)
check("M17 etape 1-post: 1 sæt", legacy.plan.rigs, 1)
-- M18: Start nu på en bar mark (klar-andel 0) må ikke nulstilles af vækst-reglen før den starter
local bare = M.with(M.forceReady(M.newField(70)), {startRequested = true})
check("M18 bar mark forbliver klar", M.onReadiness(bare, 0).stage, "ready")
check("M18 uden forespørgsel -> venter", M.onReadiness(M.forceReady(M.newField(71)), 0).stage, "idle")
check("M18 vejrfølsom: høst", M.isWeatherSensitive("harvesting", {}, "harvest"), true)
check("M18 vejrfølsom: kultivering ikke", M.isWeatherSensitive("cultivating", {}, "harvest"), false)
-- M19: mål-afgrøde — "vælg mark + afgrøde, glem den"
local target = M.with(M.newField(80), {plan = {targetFruit = "WHEAT"}})
local wheatNow = M.applyTarget(target.plan, "harvest", "BARLEY")
check("M19 mål -> auto", wheatNow.mode, "auto")
check("M19 høst byg -> så hvede", wheatNow.sow, true)
check("M19 såafgrøde = mål", wheatNow.sowFruit, "WHEAT")
check("M19 kæde: høst -> kultivér -> så", table.concat(M.workStages(wheatNow, "harvest", {}), ","), "harvesting,cultivating,sowing")
local grassToWheat = M.applyTarget(target.plan, "mowable", "GRASS")
check("M19 græs -> hvede: slå, saml op, så klargør og så", table.concat(M.workStages(grassToWheat, "mowable", {}), ","), "mowing,collecting,cultivating,sowing")
local grassTarget = M.applyTarget(M.with(M.newField(81), {plan = {targetFruit = "GRASS"}}).plan, "mowable", "GRASS")
check("M19 græs -> græs: kun slå og saml op", table.concat(M.workStages(grassTarget, "mowable", {}), ","), "mowing,collecting")
check("M19 græs -> græs: ingen såning", grassTarget.sow, false)
local bareTarget = M.applyTarget(target.plan, "bare", nil)
check("M19 bar mark -> klargør og så", table.concat(M.workStages(bareTarget, "bare", {plow = true}), ","), "plowing,sowing")
check("M19 uden mål: plan urørt", M.applyTarget(M.newField(82).plan, "harvest", "BARLEY").mode, "manual")
local started = M.start(target, "harvest", {}, "BARLEY")
check("M19 start anvender målet", started.plan.sowFruit, "WHEAT")
check("M19 start: høst først", started.stage, "harvesting")
check("M19 målet gemmes", M.fromRecord(M.toRecord(started)).plan.targetFruit, "WHEAT")
check("M19 klargøring efter slåning gemmes", M.fromRecord(M.toRecord(M.start(target, "mowable", {}, "GRASS"))).plan.prepAfterMow, true)
-- M20: skal en bar mark med mål starte? (altid — kun såningen venter på såtiden)
check("M20 bar + sås nu -> start", M.shouldStartBare(target, "bare", true), true)
check("M20 bar + ikke såtid -> klargør alligevel (kun såningen venter)", M.shouldStartBare(target, "bare"), true)
check("M20 uden mål -> ingen auto-start", M.shouldStartBare(M.newField(83), "bare", true), false)
check("M20 voksende mark -> nej", M.shouldStartBare(target, "harvest", true), false)
check("M20 arbejdende mark -> nej", M.shouldStartBare(started, "bare", true), false)
check("M20 færdig (lige sået) -> nej, ingen dobbelt-såning", M.shouldStartBare(M.with(target, {stage = "done"}), "bare", true), false)
-- M21: vælg mål-afgrøde fra menuen: auto, og en færdig mark nulstilles så den kan starte
local chosen = M.setTarget(M.with(M.newField(84), {stage = "done"}), "CANOLA")
check("M21 mål sat", chosen.plan.targetFruit, "CANOLA"); check("M21 auto", chosen.plan.mode, "auto")
check("M21 færdig -> venter", chosen.stage, "idle")
check("M21 arbejdende mark beholder trin", M.setTarget(started, "CANOLA").stage, "harvesting")
local cleared = M.setTarget(chosen, "")
check("M21 mål fjernet", cleared.plan.targetFruit, ""); check("M21 tilbage til manuel", cleared.plan.mode, "manual")
-- M22: halm efter høst — hakkes (standard), samles op, eller presses til baller og samles
local strawBase = {soil = "none", sow = false, lime = false}
check("M22 hakkes: kun høst", table.concat(M.workStages(strawBase, "harvest", {}), ","), "harvesting")
check("M22 samles op", table.concat(M.workStages(M.with(M.newField(1), {plan = {soil = "none", lime = false, straw = "collect"}}).plan, "harvest", {}), ","), "harvesting,strawCollecting")
check("M22 baller", table.concat(M.workStages(M.with(M.newField(1), {plan = {soil = "none", lime = false, straw = "bale"}}).plan, "harvest", {}), ","), "harvesting,baling,baleCollecting")
local baleChain = M.start(M.with(M.newField(2), {plan = {straw = "bale", soil = "cultivate", lime = false}}), "harvest", {})
check("M22 høst -> presses", M.advance(baleChain, {}).stage, "baling")
check("M22 presses -> baller samles", M.advance(M.advance(baleChain, {}), {}).stage, "baleCollecting")
check("M22 -> kultivér", M.advance(M.advance(M.advance(baleChain, {}), {}), {}).stage, "cultivating")
check("M22 roller", M.roleFor("baling") .. "," .. M.roleFor("baleCollecting") .. "," .. M.roleFor("strawCollecting"), "baler,baleCollector,collector")
check("M22 halm-læssevogn følger høsterens kursus", M.courseSource(M.with(M.newField(3), {stage = "strawCollecting"}), "collector"), "harvester")
check("M22 presser følger høsterens kursus", M.courseSource(M.with(M.newField(3), {stage = "baling"}), "baler"), "harvester")
check("M22 græs-læssevogn følger stadig slåkurset", M.courseSource(M.with(M.newField(3), {stage = "collecting"}), "collector"), "mower")
check("M22 halm-læssevogn uden breddekrav", M.requiredWidth(M.with(M.newField(3), {stage = "strawCollecting", mowWidth = 40}), "collector"), nil)
check("M22 presning vejrfølsom", M.isWeatherSensitive("baling", {}, "harvest"), true)
check("M22 ballesamling ikke vejrfølsom", M.isWeatherSensitive("baleCollecting", {}, "harvest"), false)
check("M22 gemmes", M.fromRecord(M.toRecord(M.with(M.newField(4), {plan = {straw = "bale"}}))).plan.straw, "bale")
check("M22 ukendt -> hakkes", M.fromRecord({id = 5, straw = "x"}).plan.straw, "chop")
-- M23: ingen ny tabel når vækst-reglen ikke ændrer noget (undgår affald hvert sekund -> GC-hak)
local calm = M.newField(95)
check("M23 uændret -> samme tabel", M.onReadiness(calm, 0.2) == calm, true)
check("M23 ændret -> ny tabel", M.onReadiness(calm, 0.95) ~= calm, true)
-- M24: passive marker kan springes over (manuel, uden mål, venter, ingen forespørgsel)
check("M24 manuel venter = passiv", M.isPassive(calm), true)
check("M24 auto = ikke passiv", M.isPassive(M.with(calm, {plan = {mode = "auto"}})), false)
check("M24 mål = ikke passiv", M.isPassive(M.setTarget(calm, "WHEAT")), false)
check("M24 klar = ikke passiv", M.isPassive(M.with(calm, {stage = "ready"})), false)
check("M24 i gang = ikke passiv", M.isPassive(started), false)
check("M24 Start nu = ikke passiv", M.isPassive(M.with(calm, {startRequested = true})), false)
-- Stop holder en bar mark (ellers starter klargøringen straks forfra); Start eller ny mål-afgrøde slipper
local stoppedBare = M.stop(M.with(target, {stage = "cultivating"}))
check("M21 stoppet bar mark starter ikke selv", M.shouldStartBare(stoppedBare, "bare"), false)
check("M21 Start slipper", M.shouldStartBare(M.with(stoppedBare, {startRequested = true}), "bare"), true)
check("M21 ny mål-afgrøde slipper", M.shouldStartBare(M.setTarget(stoppedBare, "BARLEY"), "bare"), true)
check("M21 start nulstiller hold (næste sæson starter selv)", M.start(stoppedBare, "bare", {}, "WHEAT").held, nil)
-- gemt produkt (silage/hø) gælder kun slåafgrøder: en vinmark der engang fejlagtigt blev "slåbar" (F22, 29/9)
check("produkt: gemt silage på høstmark -> høst", ADFieldModel.product({product = "silage"}, "harvest"), "harvest")
check("produkt: gemt hø på slåmark -> hø", ADFieldModel.product({product = "hay"}, "mowable"), "hay")
check("produkt: auto på slåmark -> silage", ADFieldModel.product({product = "auto"}, "mowable"), "silage")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-46
View File
@@ -1,46 +0,0 @@
-- Kør: luajit tests/test_adFieldSiloConfig.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFieldSiloConfig.lua")
local C = ADFieldSiloConfig
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
local cfg = C.new()
check("standard fra", cfg.enabled, false)
check("standard x=2", cfg.maxVehicles, 2)
check("standard tærskel 10000", cfg.minLiters, 10000)
check("x under 1 -> 1", C.with(cfg, {maxVehicles = 0}).maxVehicles, 1)
check("x over 10 -> 10", C.with(cfg, {maxVehicles = 99}).maxVehicles, 10)
check("tærskel snappes til nærmeste valg", C.with(cfg, {minLiters = 12000}).minLiters, 10000)
check("tærskel 40000 -> 50000", C.with(cfg, {minLiters = 40000}).minLiters, 50000)
check("with muterer ikke", cfg.maxVehicles, 2)
local on = C.toggleStore(cfg, "silo1")
check("toggle til", C.hasStore(on, "silo1"), true)
check("toggle muterer ikke", C.hasStore(cfg, "silo1"), false)
check("toggle fra", C.hasStore(C.toggleStore(on, "silo1"), "silo1"), false)
local full = C.with(C.toggleStore(C.toggleStore(cfg, "a"), "b"), {enabled = true, maxVehicles = 4, minLiters = 25000})
local back = C.fromRecord(C.toRecord(full))
check("record: enabled", back.enabled, true)
check("record: lagre", table.concat(back.storeIds, " "), "a b")
check("record: x", back.maxVehicles, 4)
check("record: tærskel", back.minLiters, 25000)
check("tom record -> standard", C.fromRecord(nil).maxVehicles, 2)
check("record uden tal -> standard tærskel", C.fromRecord({enabled = 1}).minLiters, 10000)
-- kør til produktion + foderreserve
check("standard: ikke til produktion", cfg.toProduction, false)
check("standard foderreserve 7 døgn", cfg.reserveDays, 7)
check("reserve snappes (10 -> 7)", C.with(cfg, {reserveDays = 10}).reserveDays, 7)
check("reserve 40 -> 30", C.with(cfg, {reserveDays = 40}).reserveDays, 30)
local prod = C.with(C.toggleStore(cfg, "a"), {toProduction = true, reserveDays = 14})
check("with bevarer lagre", C.hasStore(prod, "a"), true)
check("with muterer ikke (produktion)", cfg.toProduction, false)
local prodBack = C.fromRecord(C.toRecord(prod))
check("record: til produktion", prodBack.toProduction, true)
check("record: foderreserve", prodBack.reserveDays, 14)
check("gammel record uden felter -> standard", C.fromRecord({enabled = 1}).reserveDays, 7)
check("toggleStore bevarer produktion", C.toggleStore(prod, "b").toProduction, true)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-159
View File
@@ -1,159 +0,0 @@
-- Kør: luajit tests/test_adFieldSiloPlanner.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFieldSiloPlanner.lua")
local P = ADFieldSiloPlanner
local WHEAT, CANOLA = 1, 2
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
local function silo(id, x, stock, markerId) return {id = id, name = id, markerId = markerId == nil and (id .. "m") or markerId or nil, x = x, z = 0, stock = stock} end
local function store(id, x, free) return {id = id, markerId = id .. "m", x = x, z = 0, free = free} end
local function vehicle(id, x, capacity, carries, cargo) return {id = id, x = x, z = 0, capacity = capacity, carries = carries or {[WHEAT] = true, [CANOLA] = true}, cargo = cargo} end
local function base(over)
local input = {silos = {}, stores = {store("farm", 0, {[WHEAT] = 1000000, [CANOLA] = 1000000})}, loans = {}, vehicles = {}, paused = {}, maxVehicles = 2, minLiters = 10000}
for k, v in pairs(over or {}) do input[k] = v end
return input
end
-- kvalificering
local r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 9999})}, vehicles = {vehicle("v1", 400, 50000)}}))
check("under tærskel -> ingen afsendelse", #r.dispatch, 0)
check("under tærskel -> ingen status", #r.status, 0)
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 30000}, false)}, vehicles = {vehicle("v1", 400, 50000)}}))
check("uden markør -> noMarker", r.status[1].state, "noMarker")
check("uden markør -> ingen afsendelse", #r.dispatch, 0)
r = P.plan(base({stores = {store("farm", 0, {[CANOLA] = 1000000})}, silos = {silo("A", 500, {[WHEAT] = 30000})}, vehicles = {vehicle("v1", 400, 50000)}}))
check("ingen gård tager varen -> noFarmStore", r.status[1].state, "noFarmStore")
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 30000})}, vehicles = {vehicle("v1", 400, 50000)}, paused = {[P.key("A", WHEAT)] = true}}))
check("pause -> paused, ingen afsendelse", r.status[1].state .. #r.dispatch, "paused0")
-- afsendelse, fuldeste først, x-loft
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 20000}), silo("B", 900, {[WHEAT] = 80000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 950, 50000), vehicle("v3", 1000, 50000)}}))
check("x=2 -> to afsendt", #r.dispatch, 2)
check("fuldeste silo først", r.dispatch[1].siloId, "B")
check("nærmeste vogn til B", r.dispatch[1].vehicleId, "v2")
check("B's rest (30000) får vogn nr. 2", r.dispatch[2].siloId, "B")
check("lager-markør med", r.dispatch[1].storeMarkerId, "farmm")
check("liter = vognens kapacitet", r.dispatch[1].liters, 50000)
check("anden tur = resten", r.dispatch[2].liters, 30000)
-- aktive lån tæller mod x og mod siloens rest
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 60000})}, loans = {{siloId = "A", fillType = WHEAT, liters = 50000}}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000)}}))
check("lån på vej -> rest 10000 -> én ny vogn", #r.dispatch, 1)
check("status viser på vej", r.status[1].onWay, 50000)
r = P.plan(base({maxVehicles = 1, silos = {silo("A", 500, {[WHEAT] = 90000})}, loans = {{siloId = "Z", fillType = WHEAT, liters = 1}}, vehicles = {vehicle("v1", 400, 50000)}}))
check("x=1 med ét lån ude -> ingen", #r.dispatch, 0)
check("frie pladser 0", r.freeSlots, 0)
-- flere varer i én silo = hver sin opgave
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 40000, [CANOLA] = 20000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 410, 50000)}}))
check("to varer -> to afsendelser", #r.dispatch, 2)
check("fuldeste vare først", r.dispatch[1].fillType, WHEAT)
-- vogne der ikke kan
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 40000})}, vehicles = {vehicle("v1", 400, 50000, {[CANOLA] = true}), vehicle("v2", 2000, 50000)}}))
check("vogn der ikke kan bære varen springes over", r.dispatch[1].vehicleId, "v2")
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 40000})}, vehicles = {vehicle("v1", 400, 50000, nil, CANOLA), vehicle("v2", 2000, 50000)}}))
check("vogn med anden vare på springes over", r.dispatch[1].vehicleId, "v2")
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 40000})}, vehicles = {vehicle("v1", 400, 0)}}))
check("vogn uden kapacitet -> ingen", #r.dispatch, 0)
-- lagervalg
r = P.plan(base({stores = {store("near", 400, {[WHEAT] = 20000}), store("far", 5000, {[WHEAT] = 900000})}, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000)}}))
check("nærmeste lager uden plads til læsset -> det med plads", r.dispatch[1].storeId, "far")
r = P.plan(base({stores = {store("small", 400, {[WHEAT] = 30000}), store("tiny", 300, {[WHEAT] = 15000})}, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000)}}))
check("intet har plads til hele læsset -> mest plads", r.dispatch[1].storeId, "small")
check("reserveret plads: vogn 2 til det andet lager", r.dispatch[2].storeId, "tiny")
r = P.plan(base({stores = {store("full", 400, {[WHEAT] = 5000})}, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)}}))
check("lager med under tærsklen -> noFarmStore", r.status[1].state, "noFarmStore")
-- input muteres ikke
local input = base({silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)}})
P.plan(input)
check("input.stores urørt", input.stores[1].free[WHEAT], 1000000)
-- I1: lån på vej til et lager reserverer pladsen på tværs af planlægningsrunder
r = P.plan(base({stores = {store("s", 400, {[WHEAT] = 30000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
loans = {{siloId = "A", fillType = WHEAT, liters = 30000, storeId = "s"}}, vehicles = {vehicle("v1", 400, 50000)}}))
check("I1 lager fyldt af lån på vej -> ingen ny vogn dertil", #r.dispatch, 0)
r = P.plan(base({stores = {store("s", 400, {[WHEAT] = 30000}), store("t", 900, {[WHEAT] = 500000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
loans = {{siloId = "A", fillType = WHEAT, liters = 30000, storeId = "s"}}, vehicles = {vehicle("v1", 400, 50000)}}))
check("I1 næste vogn til det andet lager", r.dispatch[1] ~= nil and r.dispatch[1].storeId, "t")
-- kør til produktion: foder først, overskud til fabrik
local function factory(id, x, free) return {id = id, kind = "production", markerId = id .. "m", x = x, z = 0, free = free} end
local farmAndFactory = {store("farm", 5000, {[WHEAT] = 1000000}), factory("mill", 100, {[WHEAT] = 1000000})}
r = P.plan(base({stores = farmAndFactory, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)},
feedReserve = {[WHEAT] = 100000}, farmStock = {[WHEAT] = 20000}}))
check("P1 reserve ikke dækket -> gårdlager selv om fabrik er nærmere", r.dispatch[1].storeId, "farm")
r = P.plan(base({stores = farmAndFactory, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)},
feedReserve = {[WHEAT] = 100000}, farmStock = {[WHEAT] = 150000}}))
check("P2 reserve dækket -> fabrik", r.dispatch[1].storeId, "mill")
r = P.plan(base({stores = farmAndFactory, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)}}))
check("P3 ikke foder (ingen reserve) -> fabrik", r.dispatch[1].storeId, "mill")
r = P.plan(base({stores = farmAndFactory, silos = {silo("A", 500, {[WHEAT] = 100000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000)},
feedReserve = {[WHEAT] = 100000}, farmStock = {[WHEAT] = 70000}}))
check("P4 første læs dækker resten af reserven -> gård", r.dispatch[1].storeId, "farm")
check("P4 andet læs er overskud -> fabrik", r.dispatch[2].storeId, "mill")
r = P.plan(base({stores = farmAndFactory, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)},
feedReserve = {[WHEAT] = 100000}, farmStock = {[WHEAT] = 60000}, loans = {{siloId = "B", fillType = WHEAT, liters = 50000, storeId = "farm"}}}))
check("P5 læs på vej til gården tæller med i reserven -> fabrik", r.dispatch[1] ~= nil and r.dispatch[1].storeId, "mill")
r = P.plan(base({stores = {store("farm", 5000, {[WHEAT] = 1000000}), factory("mill", 100, {[CANOLA] = 1000000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
vehicles = {vehicle("v1", 400, 50000)}, farmStock = {[WHEAT] = 999999}}))
check("P6 ingen fabrik tager varen -> gårdlager", r.dispatch[1].storeId, "farm")
r = P.plan(base({stores = {store("farm", 5000, {[CANOLA] = 1000000}), factory("mill", 100, {[WHEAT] = 1000000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
vehicles = {vehicle("v1", 400, 50000)}, feedReserve = {[WHEAT] = 100000}}))
check("P7 reserve mangler men ingen gård tager varen -> fabrik", r.dispatch[1].storeId, "mill")
check("P7 status ok (ikke noFarmStore)", r.status[1].state, "ok")
check("P7 status viser destination", r.status[1].toProduction, true)
-- fabrik kun når hele læsset kan være der (in-game 28/9: vogn med 252.000 l ventede timevis ved en fuld fabrik)
r = P.plan(base({stores = {store("farm", 5000, {[WHEAT] = 1000000}), factory("mill", 100, {[WHEAT] = 30000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
vehicles = {vehicle("v1", 400, 50000)}}))
check("P8 fabrik uden plads til hele læsset -> gårdlager", r.dispatch[1].storeId, "farm")
r = P.plan(base({stores = {store("farm", 5000, {[CANOLA] = 1000000}), factory("mill", 100, {[WHEAT] = 30000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
vehicles = {vehicle("v1", 400, 50000)}}))
check("P9 kun fabrik uden plads til hele læsset -> ingen afsendelse", #r.dispatch, 0)
-- høsten først: tømmevogne holdes tilbage til høstere der arbejder
local vs = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000, {[CANOLA] = true}), vehicle("v3", 400, 50000)}
local left = P.withoutHarvestReserve(vs, {{fillType = WHEAT, count = 1}})
check("R1 én hvede-vogn holdt tilbage", #left, 2)
check("R1 raps-vognen er fri", left[1].id == "v2" or left[2].id == "v2", true)
left = P.withoutHarvestReserve(vs, {{fillType = WHEAT, count = 5}})
check("R2 alle hvede-vogne holdt tilbage", #left, 1)
check("R3 intet behov -> alle fri", #P.withoutHarvestReserve(vs, {}), 3)
check("R4 input muteres ikke", #vs, 3)
-- Tom silo: lånt vogn med last står ved en tom marksilo -> resten køres videre (AD venter ellers til fuld)
-- (in-game 28/9: Tømmevogn 2 og 4 stod ved tom F17 Unload med kartofler)
local restState = {atSilo = true, stillMs = 45000, siloStock = 0, cargo = 12000}
check("RS1 tom silo med last -> kør videre", P.restDue(restState), true)
check("RS2 siloen har stadig varer -> vent", P.restDue({atSilo = true, stillMs = 45000, siloStock = 30000, cargo = 12000}), false)
check("RS3 ikke ved siloen -> nej", P.restDue({atSilo = false, stillMs = 45000, siloStock = 0, cargo = 12000}), false)
check("RS4 lige ankommet -> vent", P.restDue({atSilo = true, stillMs = 5000, siloStock = 0, cargo = 12000}), false)
check("RS5 tom vogn -> nej", P.restDue({atSilo = true, stillMs = 45000, siloStock = 0, cargo = 0}), false)
local restStores = {
{id = "fab", kind = "production", markerId = 1, x = 10, z = 0, free = {[10] = 5000}},
{id = "gaard", kind = "silo", markerId = 2, x = 500, z = 0, free = {[10] = 900000}},
}
check("RS6 fabrik uden plads til hele resten -> gårdsilo", P.restStore(restStores, {x = 0, z = 0}, 10, 12000).id, "gaard")
check("RS7 fabrik med plads -> fabrik", P.restStore(restStores, {x = 0, z = 0}, 10, 4000).id, "fab")
check("RS8 intet tager varen", P.restStore(restStores, {x = 0, z = 0}, 99, 4000), nil)
-- Fuld fabrik: lånt vogn med last holder ved et lager uden plads -> resten til et andet lager (gård)
check("SF1 fuld fabrik -> videre", P.storeFullDue({atStore = true, stillMs = 70000, cargo = 12000, storeFree = 0}), true)
check("SF2 fabrikken tager stadig imod -> vent", P.storeFullDue({atStore = true, stillMs = 70000, cargo = 12000, storeFree = 8000}), false)
check("SF3 lige ankommet -> vent", P.storeFullDue({atStore = true, stillMs = 20000, cargo = 12000, storeFree = 0}), false)
check("SF4 ikke ved lageret -> nej", P.storeFullDue({atStore = false, stillMs = 70000, cargo = 12000, storeFree = 0}), false)
local fullStores = {
{id = "fab", kind = "production", markerId = 1, x = 10, z = 0, free = {[10] = 0}},
{id = "gaard", kind = "silo", markerId = 2, x = 500, z = 0, free = {[10] = 900000}},
}
check("SF5 andet lager end det fulde", P.restStore(fullStores, {x = 0, z = 0}, 10, 12000, "fab").id, "gaard")
check("SF6 kun det fulde tager varen -> intet", P.restStore({fullStores[1]}, {x = 0, z = 0}, 10, 12000, "fab"), nil)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-159
View File
@@ -1,159 +0,0 @@
-- Kør: luajit tests/test_adFieldSilos.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFieldSiloPlanner.lua")
dofile("FS25_ADSmartPickup/adFieldSiloConfig.lua")
dofile("FS25_ADSmartPickup/adFieldSilos.lua")
local S = ADFieldSilos
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
local loan = {startedMs = 0, lastProgressMs = 0, lastFill = 0}
check("kører, tidligt -> behold", S.releaseReason(loan, {adActive = true, controlled = false, fill = 0, moving = true}, 5000), nil)
check("AD slukket efter 40 s -> done", S.releaseReason(loan, {adActive = false, controlled = false, fill = 0}, 40000), "done")
check("spiller i vognen -> player", S.releaseReason(loan, {adActive = true, controlled = true, fill = 0}, 5000), "player")
check("står stille uden fyld i 120 s -> stuck", S.releaseReason(loan, {adActive = true, controlled = false, fill = 0, moving = false}, 120001), "stuck")
check("kører stadig efter 120 s -> behold", S.releaseReason(loan, {adActive = true, controlled = false, fill = 0, moving = true}, 120001), nil)
local loading = {startedMs = 0, lastProgressMs = 110000, lastFill = 0}
check("fyldning steg for nylig -> behold", S.releaseReason(loading, {adActive = true, controlled = false, fill = 20000, moving = false}, 125000), nil)
-- I2/I3: vagten gælder kun før turen er kommet i gang; AD der slukker straks uden fremdrift = fejl
local going = {startedMs = 0, lastFill = 30000, hasProgressed = true}
check("I2 i gang, står i kø 500 s -> behold", S.releaseReason(going, {adActive = true, controlled = false, fill = 30000, moving = false}, 500000), nil)
check("I2 aflæsning (fyld falder) er fremdrift", S.releaseReason({startedMs = 0, lastFill = 30000}, {adActive = true, controlled = false, fill = 20000, moving = false}, 500000), nil)
check("I3 AD slukket efter 10 s uden fremdrift -> failed", S.releaseReason({startedMs = 0, lastFill = 0}, {adActive = false, controlled = false, fill = 0}, 10000), "failed")
check("I3 AD slukket efter 10 s med fremdrift -> done", S.releaseReason({startedMs = 0, lastFill = 0, hasProgressed = true}, {adActive = false, controlled = false, fill = 0}, 10000), "done")
-- Røgtest af spil-laget med mocks: planNow låner nærmeste tømmevogn, isLoaned = optaget,
-- frakobling (setConfig enabled=false) stopper AD og frigiver (Review Focus).
do
local WHEAT = 1
Logging = {info = function() end, warning = function() end}
g_currentMission = {getFarmId = function() return 1 end}
g_fillTypeManager = {getFillTypeByIndex = function() return {title = "Hvede"} end}
function getWorldTranslation(node) return node.x, 0, node.z end
ADRuns = {vehicleLabel = function(v) return v.name end}
local function storage(levels, free) return {fillLevels = levels, fillTypes = {[WHEAT] = true}, getFreeCapacity = function() return free end} end
local fieldSilo = {id = "fs", name = "Marksilo 1", kind = "silo", placeable = {rootNode = {x = 1000, z = 0}, spec_silo = {storages = {storage({[WHEAT] = 60000}, 0)}}}}
local farm = {id = "farm", name = "Gårdsilo", kind = "silo", placeable = {rootNode = {x = 0, z = 0}, spec_silo = {storages = {storage({}, 500000)}}}}
ADBuildings = {list = function() return {fieldSilo, farm} end,
getMarkers = function(b, role) return {{id = b.id .. "-" .. role}} end}
ADFields = {list = function() return {{id = 1}} end, fieldSilos = function() return {fieldSilo, farm} end}
local started, settingsSet, noPark, noRoute, residue = {}, {}, {}, {}, 0
ADRunsController = {store = {runs = {}}, setSetting = function(_, v, name, value) settingsSet[v] = settingsSet[v] or {}; settingsSet[v][name] = value end}
ADUnloadWait = {hasRouteTo = function(_, _, id) return not noRoute[id] end}
local function makeVehicle(name, x)
local module = {active = false}
function module:setMode() end
function module:setFirstMarkerByWayPointId(id) self.first = id end
function module:setSecondMarkerByWayPointId(id) self.second = id end
function module:setFillType() end
function module:setLoopsDone() end
function module:raiseDirtyFlag() end
function module:isActive() return self.active end
return {name = name, x = x, lastSpeedReal = 0, ad = {stateModule = module},
stopAutoDrive = function(self) self.ad.stateModule.active = false; self.stopped = true end}
end
local near, far = makeVehicle("Nær", 900), makeVehicle("Fjern", 5000)
local adEnv = {AutoDrive = {MODE_PICKUPANDDELIVER = 2, RT_NONE = 0,
getAllDischargeableUnits = function() return {{fillUnitIndex = 1, object = {
getFillUnitCapacity = function() return 40000 end,
getFillUnitSupportedFillTypes = function() return {[WHEAT] = true} end,
getFillUnitFillLevel = function() return residue end, getFillUnitFillType = function() return WHEAT end}}} end,
getAllUnits = function() return {} end, getAllFillLevels = function() return 0 end}}
ADFieldWork = {
getRigs = function()
local rigs = {}
-- som spillets vehicleSystem: slettede køretøjer er ikke med
if not near.isDeleted then table.insert(rigs, {id = "n", vehicle = near, role = "unloader", enabled = true, busy = S.isLoaned(near), controlled = false, x = 900, z = 0}) end
if not far.isDeleted then table.insert(rigs, {id = "f", vehicle = far, role = "unloader", enabled = true, busy = S.isLoaned(far), controlled = false, x = 5000, z = 0}) end
table.insert(rigs, {id = "h", vehicle = far, role = "harvester", enabled = true, busy = false, controlled = false, x = 1000, z = 0})
return rigs
end,
enableParkAtJobFinished = function() end,
parkMarkerId = function(_, v) if noPark[v] then return nil end return 777 end,
startAd = function(_, v, _, first, second, fillType) v.ad.stateModule.active = true; table.insert(started, {v = v, first = first, second = second, fillType = fillType}) end,
}
S.setConfig(ADFieldSiloConfig.with(ADFieldSiloConfig.toggleStore(ADFieldSiloConfig.new(), "farm"), {enabled = true, maxVehicles = 1}))
local plan = S.planNow(adEnv)
check("RØG én afsendelse (x=1)", #started, 1)
check("RØG nærmeste tømmevogn", started[1] ~= nil and started[1].v.name, "Nær")
check("RØG fra marksilo-markør", started[1] ~= nil and started[1].first, "fs-load")
check("RØG til gårdlager-markør", started[1] ~= nil and started[1].second, "farm-unload")
check("RØG varen med", started[1] ~= nil and started[1].fillType, WHEAT)
check("RØG gårdlager er ikke marksilo", #plan.status, 1)
check("RØG vognen er lånt", S.isLoaned(near), true)
check("RØG x nået -> ingen flere", #S.planNow(adEnv).dispatch, 0)
check("I4 rotation slået fra på lånt vogn", settingsSet[near] ~= nil and settingsSet[near].rotateTargets, 0)
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = false}))
check("RØG frakobling stopper AD", near.stopped, true)
check("RØG frakobling frigiver", S.isLoaned(near), false)
-- I5: vogn uden parkeringspunkt lånes ikke (ville blive holdende på lagerets markør)
started = {}
noPark[near] = true
near.stopped = nil
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = true}))
S.planNow(adEnv)
check("I5 uden parkering -> fjern vogn med parkering", started[1] ~= nil and started[1].v.name, "Fjern")
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = false}))
noPark[near] = nil
-- I3: ingen rute til siloen -> ingen afsendelse
started = {}
noRoute["fs-load"] = true
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = true}))
S.planNow(adEnv)
check("I3 ingen rute -> ingen afsendelse", #started, 0)
noRoute["fs-load"] = nil
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = false}))
-- I6: silo der er kilde/mål i et flow er ikke marksilo
ADRunsController.store.runs = {{kind = "supply", sourceBuildingIds = {}, targetBuildingIds = {"fs"}, storeBuildingIds = {}}}
local silos = S.collectBuildings(adEnv)
check("I6 silo i et flow tømmes ikke", #silos, 0)
ADRunsController.store.runs = {}
-- in-game 27/9: marksilo-flowet flyttede sukkerroer fra en Farma-silo til nabo-Farma-siloen (samme lagerklynge)
local neighbour = {id = "nb", name = "Nabo-Farma", kind = "silo", placeable = {rootNode = {x = 40, z = 0}, spec_silo = {storages = {storage({[WHEAT] = 60000}, 0)}}}}
ADFields.fieldSilos = function() return {fieldSilo, farm, neighbour} end
local withNeighbour = S.collectBuildings(adEnv)
local names = {}
for _, silo in ipairs(withNeighbour) do table.insert(names, silo.id) end
check("gårdens nabosilo (40 m fra gårdlager) er ikke marksilo", table.concat(names, ","), "fs")
ADFields.fieldSilos = function() return {fieldSilo, farm} end
-- in-game 27/9: gårdens Farma-klynge er 300 m lang — siloer over 100 m fra et valgt gårdlager blev marksiloer.
-- En silo i samme klynge (≤ 60 m til en nabo) som et gårdlager er gårdens.
local c1 = {id = "c1", name = "Klynge 1", kind = "silo", placeable = {rootNode = {x = 50, z = 0}, spec_silo = {storages = {storage({[WHEAT] = 60000}, 0)}}}}
local c2 = {id = "c2", name = "Klynge 2", kind = "silo", placeable = {rootNode = {x = 105, z = 0}, spec_silo = {storages = {storage({[WHEAT] = 60000}, 0)}}}}
ADBuildings.list = function() return {fieldSilo, farm, c1, c2} end
ADFields.fieldSilos = function() return {fieldSilo, farm, c1, c2} end
local clustered = {}
for _, silo in ipairs(S.collectBuildings(adEnv)) do table.insert(clustered, silo.id) end
check("gårdens klynge (105 m via nabo) er ikke marksilo", table.concat(clustered, ","), "fs")
ADBuildings.list = function() return {fieldSilo, farm} end
ADFields.fieldSilos = function() return {fieldSilo, farm} end
-- M5: rest under 100 l tæller ikke som last
residue = 50
local vehicles = S.collectVehicles(adEnv)
check("M5 rest på 50 l er ikke last", vehicles[1].cargo, nil)
residue = 0
-- M4: slettet lånt vogn fjernes uden at vælte tick
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = true}))
started = {}
S.planNow(adEnv)
local loaned = started[1] ~= nil and started[1].v or nil
if loaned ~= nil then loaned.isDeleted = true end
local okTick = pcall(S.tick, adEnv, 2000)
check("M4 tick tåler slettet vogn", okTick, true)
check("M4 slettet vogn frigivet", loaned ~= nil and S.isLoaned(loaned), false)
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = false}))
end
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-110
View File
@@ -1,110 +0,0 @@
-- Kør: luajit tests/test_adFieldStorage.lua (fra repo-roden)
-- Lagringen skal dække HVERT felt modellen gemmer (FIELD_TYPES), og en mark skal overleve en tur gennem XML.
dofile("FS25_ADSmartPickup/adFieldModel.lua")
dofile("FS25_ADSmartPickup/adFieldStorage.lua")
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
-- falsk XMLFile: gemmer værdier pr. nøgle
local function fakeXml()
local values = {}
local xml = {}
for _, kind in ipairs({"Int", "String", "Float"}) do
xml["set" .. kind] = function(_, key, value) values[key] = value end
xml["get" .. kind] = function(_, key) return values[key] end
end
return xml
end
local field = ADFieldModel.with(ADFieldModel.startCare(ADFieldModel.newField(77), "weeding"), {
kind = "harvest", lastFruit = "WHEAT", mowWidth = 40.8, mowVehicleId = "v1", rakeVehicleId = "v2", windrowOffset = -2.5,
plan = {mode = "auto", product = "harvest", rake = true, storeBuildingIds = {"b1"}, rigs = 3, unloaders = 2,
mulch = true, lime = false, soil = "plow", sow = true, sowFruit = "CANOLA", roll = true, fertilize = false, weed = false},
})
local record = ADFieldModel.toRecord(field)
for key in pairs(record) do
check("ST1 felt med i lagringen: " .. key, ADFieldStorage.FIELD_TYPES[key] ~= nil, true)
end
local xml = fakeXml()
ADFieldStorage.writeRecord(xml, "k", record)
local back = ADFieldModel.fromRecord(ADFieldStorage.readRecord(xml, "k"))
local again = ADFieldModel.toRecord(back)
for key, value in pairs(record) do
check("ST2 rundtur: " .. key, tostring(again[key]), tostring(value))
end
-- ST3-ST6: kursus-registret (genbrug af Courseplay-kurser) gemmes i samme fil, <courses><course .../></courses>
dofile("FS25_ADSmartPickup/adCourseCache.lua")
local course = ADCourseCache.newRecord({vehicle = "v7", field = 22, width = 30.0, sig = "n=4;a=960000;c=800,300",
settings = ADCourseCache.settingsKey({multiTools = 2, lane = -1}), waypoints = 812})
for key in pairs(course) do
check("ST3 kursus-felt med i lagringen: " .. key, ADFieldStorage.COURSE_TYPES[key] ~= nil, true)
end
local cxml = fakeXml()
ADFieldStorage.writeCourseRecord(cxml, "c", course)
local cback = ADFieldStorage.readCourseRecord(cxml, "c")
for key, value in pairs(course) do
check("ST4 kursus-rundtur: " .. key, tostring(cback[key]), tostring(value))
end
check("ST4b matcher stadig efter rundtur", ADCourseCache.matches(cback, {field = 22, width = 30, sig = course.sig, settings = course.settings}), true)
-- hel fil: save -> load med falsk XMLFile (iterate over indekserede nøgler)
local function fileXml(store)
local xml = fakeXml()
for _, kind in ipairs({"Int", "String", "Float"}) do
xml["set" .. kind] = function(_, key, value) store[key] = value end
xml["get" .. kind] = function(_, key) return store[key] end
end
function xml.iterate(_, base, fn)
local index = 0
while true do
local prefix = string.format("%s(%d)", base, index)
local found = false
for key in pairs(store) do
if key:sub(1, #prefix + 1) == prefix .. "#" or key:sub(1, #prefix + 1) == prefix .. "." then found = true break end
end
if not found then return end
fn(index + 1, prefix)
index = index + 1
end
end
function xml.save() end
function xml.delete() end
return xml
end
local disk = {}
ADRunsStorage = {FILE_NAME = "runs.xml", getPath = function() return "/save/runs.xml" end}
XMLFile = {create = function() return fileXml(disk) end, load = function() return fileXml(disk) end}
function fileExists() return true end
check("ST5 gem", ADFieldStorage.save({fields = {[77] = field}, fleet = {}, fleetMode = "all",
courseRecords = ADCourseCache.withRecord({}, course)}), true)
local loaded = ADFieldStorage.load()
check("ST6 kursus indlæst", loaded.courseRecords.v7 ~= nil and loaded.courseRecords.v7.waypoints, 812)
check("ST6b mark indlæst", loaded.fields[77] ~= nil, true)
-- ST7: gammel fil uden <courses> -> tomt register (bagudkompatibelt)
disk = {}
ADFieldStorage.save({fields = {}, fleet = {}, fleetMode = "all"})
local old = ADFieldStorage.load()
check("ST7 gammel fil: tomt register", next(old.courseRecords), nil)
-- ST8: manuel transport pr. maskine gemmes og indlæses; gammel fil uden -> tom
disk = {}
ADFieldStorage.save({fields = {}, fleet = {}, fleetMode = "all", manual = {vehicleCombine = true}})
check("ST8 manuel transport indlæst", ADFieldStorage.load().manual.vehicleCombine, true)
check("ST8 gammel fil: ingen manuelle", next(old.manual or {}), nil)
-- ST9: pause ("Stop alt og parkér") gemmes, så intet starter af sig selv efter indlæsning
disk = {}
ADFieldStorage.save({fields = {}, fleet = {}, fleetMode = "all", paused = true})
check("ST9 pause indlæst", ADFieldStorage.load().paused, true)
disk = {}
ADFieldStorage.save({fields = {}, fleet = {}, fleetMode = "all"})
check("ST9 ingen pause", ADFieldStorage.load().paused, false)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-66
View File
@@ -1,66 +0,0 @@
-- Kør: luajit tests/test_adFieldView.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFieldModel.lua")
dofile("FS25_ADSmartPickup/adFieldView.lua")
local V = ADFieldView
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
local infos = {
{id = 109, mowable = true, markerId = 5},
{id = 12, mowable = true, markerId = nil},
{id = 90, mowable = false, markerId = 7},
}
local fields = {[109] = ADFieldModel.newField(109), [12] = ADFieldModel.newField(12)}
-- V1: alle marker (etape 2: også høst og bare), sorteret efter nummer
local rows = V.rows(infos, fields)
check("V1 tre rækker", #rows, 3)
check("V1 sorteret", rows[1].info.id, 12)
check("V1 model med", rows[3].field.id, 109)
-- V2: mangler model -> ny standardmark
check("V2 standardmodel", rows[2].field.stage, "idle")
-- V3: status — manglende markør vinder over trin
check("V3 mangler markør", V.statusKey(fields[12], infos[2]), "spu_field_noMarker")
check("V3 trin", V.statusKey(fields[109], infos[1]), "spu_field_stage_idle")
-- V4: planens tekstnøgler pr. markart
check("V4 ensilage", V.planKey({product = "auto"}, "mowable"), "spu_field_plan_silage")
check("V4 hø", V.planKey({product = "hay"}, "mowable"), "spu_field_plan_hay")
check("V4 høst", V.planKey({product = "auto"}, "harvest"), "spu_field_plan_harvest")
check("V4 bar mark", V.planKey({product = "auto"}, "bare"), "spu_field_plan_prep")
-- V5: jordtilstand som korte tekstdele
local parts = V.soilParts({lime = 0.2, fert = 1, fertMax = 2, weed = 3, needs = {lime = true, plow = true}})
check("V5 dele", table.concat(parts, ","), "lime,plow,fert,weed")
check("V5 intet", #V.soilParts({lime = 2, fert = 2, fertMax = 2, weed = 0, needs = {}}), 0)
-- cropText: høstet = stub (afgrøde), bar = bar jord, ellers afgrøden
local texts = {spu_field_stubble = "stub (%s)", spu_field_bare = "bar jord"}
local function T(key) return texts[key] end
check("FVcrop stub", ADFieldView.cropText({kind = "bare", stubbleOf = "Hvede", cropTitle = "Hvede"}, T), "stub (Hvede)")
check("FVcrop bar", ADFieldView.cropText({kind = "bare"}, T), "bar jord")
check("FVcrop afgrøde", ADFieldView.cropText({kind = "harvest", cropTitle = "Majs"}, T), "Majs")
-- FVstore: marksilo først, gæringssilo, så afstand; valgte vises altid; kun de nærmeste af resten
local stores = {
{id = "fjern", distance = 3000},
{id = "nær", distance = 200},
{id = "mark", distance = 90, fieldSilo = true},
{id = "valgt", distance = 5000, chosen = true},
{id = "gær", distance = 800, fermenter = true},
{id = "midt", distance = 1200},
}
local storeRows, hiddenStores = ADFieldView.storeRows(stores, 2)
local order = {}
for _, e in ipairs(storeRows) do table.insert(order, e.id) end
check("FVstore rækkefølge", table.concat(order, ","), "mark,gær,nær,midt,valgt")
check("FVstore skjulte", hiddenStores, 1)
check("FVstore meter", ADFieldView.distanceText(90.4), "90 m")
check("FVstore km", ADFieldView.distanceText(1420), "1,4 km")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-22
View File
@@ -1,22 +0,0 @@
-- Kør: luajit tests/test_adFieldWorkBuying.lua (fra repo-roden)
-- Påfyldning ved købestation (in-game 26/9: "Liquid Buy Station", FarmFillStations buyingStationConfigurable):
-- ingen lagerbeholdning, så varen læses fra udbud/triggere/AI-varer.
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
Logging = {info = function() end, warning = function() end}
dofile("FS25_ADSmartPickup/adFieldWork.lua")
local HERBICIDE, LIME, SEEDS = 20, 21, 22
local W = ADFieldWork
check("BS1 udbudte varer", W.buyingSupports({providedFillTypes = {[HERBICIDE] = true}}, HERBICIDE), true)
check("BS2 triggerens varer", W.buyingSupports({loadTriggers = {{fillTypes = {[HERBICIDE] = true}}}}, HERBICIDE), true)
check("BS3 AI-varer", W.buyingSupports({getAISupportedFillTypes = function() return {[SEEDS] = true} end}, SEEDS), true)
check("BS4 sælger ikke kalk", W.buyingSupports({providedFillTypes = {[HERBICIDE] = true}}, LIME), false)
check("BS5 fejl i opslag = nej", W.buyingSupports({getAISupportedFillTypes = function() error("x") end}, LIME), false)
check("BS6 ingen station", W.buyingSupports(nil, LIME), false)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-98
View File
@@ -1,98 +0,0 @@
-- Kør: luajit tests/test_adFields.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFields.lua")
local F = ADFields
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
-- F1/F2: markørnavne
check("F1 F109", F.parseMarkerName("F109"), 109)
check("F1 småt", F.parseMarkerName("f7"), 7)
check("F1 mellemrum", F.parseMarkerName(" F 12 "), 12)
check("F2 andet navn", F.parseMarkerName("Field 109"), nil)
check("F2 tekst efter", F.parseMarkerName("F109 wait"), nil)
check("F2 nil", F.parseMarkerName(nil), nil)
-- F3: slåafgrøde = MOWER giver et skår som TEDDER gør til DRY…; halm udelukket
local names = {[10] = "GRASS_WINDROW", [11] = "DRYGRASS_WINDROW", [20] = "STRAW", [30] = "CLOVER_WINDROW", [31] = "DRYCLOVER_WINDROW", [40] = "BEANSTRAW"}
local mower = {[1] = {fillTypeIndex = 10}, [2] = {fillTypeIndex = 20}, [3] = {fillTypeIndex = 30}, [4] = {fillTypeIndex = 40}}
local tedder = {[10] = {targetFillTypeIndex = 11}, [30] = {targetFillTypeIndex = 31}, [40] = {targetFillTypeIndex = 20}}
local mowable = F.buildMowable(mower, tedder, function(index) return names[index] end)
check("F3 græs", mowable[1] ~= nil, true)
check("F3 græs skår", mowable[1].windrow, 10)
check("F3 græs hø", mowable[1].hay, 11)
check("F3 rug (halm) ikke", mowable[2], nil)
check("F3 kløver", mowable[3].hay, 31)
check("F3 bønnehalm -> halm ikke", mowable[4], nil)
-- F4: markens art og om den vokser
local kind, growing = F.classify(3, false, 0.4)
check("F4 høstafgrøde", kind, "harvest"); check("F4 vokser", growing, true)
check("F4 klar høst vokser ikke", select(2, F.classify(3, false, 0.95)), false)
check("F4 slåafgrøde", (F.classify(1, true, 0.2)), "mowable")
check("F4 ingen afgrøde = bar", (F.classify(nil, false, 0)), "bare")
check("F4 bar vokser ikke", select(2, F.classify(nil, false, 0)), false)
-- F5: jordens behov ud fra gennemsnit og spillets indstillinger
local needs = F.soilNeeds({lime = 0.2, plowLevel = 0}, {limeRequired = true, plowingRequired = true})
check("F5 kalk", needs.lime, true); check("F5 pløj", needs.plow, true)
local off = F.soilNeeds({lime = 0, plowLevel = 0}, {limeRequired = false, plowingRequired = false})
check("F5 kalk slået fra i spillet", off.lime, false); check("F5 pløjning slået fra", off.plow, false)
check("F5 kalket jord", F.soilNeeds({lime = 2, plowLevel = 1}, {limeRequired = true, plowingRequired = true}).lime, false)
check("F5 ukendt niveau = intet behov", F.soilNeeds({}, {limeRequired = true, plowingRequired = true}).plow, false)
-- F6: vækststadie -> fase ud fra afgrødens egne grænser
local wheat = {minHarvestingGrowthState = 6, maxHarvestingGrowthState = 7, cutState = 9, witheredState = 8}
check("F6 intet", F.growthPhase(0, wheat), "none")
check("F6 spirer/vokser", F.growthPhase(3, wheat), "growing")
check("F6 klar", F.growthPhase(6, wheat), "ready")
check("F6 visnet", F.growthPhase(8, wheat), "withered")
check("F6 høstet stub", F.growthPhase(9, wheat), "cut")
check("F6 ukendt stadie", F.growthPhase(nil, wheat), nil)
check("F6 FS25-navne (minHarvestGrowthState)", F.growthPhase(6, {minHarvestGrowthState = 5, maxHarvestGrowthState = 6, cutState = 8}), "ready")
-- F7: vokser = fase growing; ukendt fase -> voksende pixels og ikke klar
check("F7 fase vinder", F.isGrowing("cut", 100, 0.1), false)
check("F7 fase vokser", F.isGrowing("growing", 0, 0), true)
check("F7 reserve: voksende pixels", F.isGrowing(nil, 50, 0.2), true)
check("F7 reserve: ingen pixels (stub)", F.isGrowing(nil, 0, 0), false)
check("F7 reserve: klar", F.isGrowing(nil, 50, 0.95), false)
-- F8: slåafgrøde fuldt udvokset (Dakota-græs 3 = mellemhøjde, 4-5 = fuld)
check("F8 prime græs", F.primeState({minHarvestingGrowthState = 3, maxHarvestingGrowthState = 5}), 4)
check("F8 prime ét klar-stadie", F.primeState({minHarvestingGrowthState = 6, maxHarvestingGrowthState = 6}), 6)
check("F8 prime ukendt", F.primeState({}), nil)
check("F8 mellemhøjde tæller ikke", F.isPrime(3, 4), false)
check("F8 fuld højde tæller", F.isPrime(5, 4), true)
check("F8 ukendt stadie tæller", F.isPrime(nil, 4), true)
check("F8 ingen krav", F.isPrime(3, nil), true)
-- F9: kartofler (Dakota): 6 = klar men kun forberedelig, 10 = toppet (høstklar)
local potato = {minHarvestingGrowthState = 10, maxHarvestingGrowthState = 10, minPreparingGrowthState = 6, maxPreparingGrowthState = 6, witheredState = 7, cutState = 8}
check("F9 utoppet er forberedelig", F.isPreparable(6, potato), true)
check("F9 voksende ikke", F.isPreparable(5, potato), false)
check("F9 fase klar", F.growthPhase(6, potato), "ready")
check("F9 toppet klar", F.growthPhase(10, potato), "ready")
check("F9 ingen forberedelse", F.isPreparable(6, {minPreparingGrowthState = -1}), false)
-- jordtype -> klargjort/pløjet (værdierne kommer fra spillets fieldGroundSystem)
local groundValues = {STUBBLE_TILLAGE = 1, CULTIVATED = 2, SEEDBED = 3, PLOWED = 4, ROLLED_SEEDBED = 5, SOWN = 6}
local function cls(v) local p, pl = ADFields.classifyGround(v, groundValues) return tostring(p) .. "|" .. tostring(pl) end
check("jord: pløjet", cls(4), "true|true")
check("jord: kultiveret", cls(2), "true|false")
check("jord: stubbearbejdet", cls(1), "true|false")
check("jord: sået", cls(6), "false|false")
check("jord: ukendt værdi", cls(nil), "nil|nil")
groundValues.NONE = 0
check("jord: ingen markjord tæller ikke med", cls(0), "nil|nil")
-- vin/oliven er aldrig slåafgrøder, selv om et mod har sat dem på MOWER-listen (in-game 29/9: F22 hvide druer fik
-- planen "Silage + så" og en skårlægger sendt ud)
check("vin: druer ikke slåbare", ADFields.isMowableFruit("GRAPE", {windrow = "GRASS_WINDROW"}), false)
check("vin: oliven ikke slåbare", ADFields.isMowableFruit("oliveb", {windrow = "X"}), false)
check("græs slåbart", ADFields.isMowableFruit("GRASS", {windrow = "GRASS_WINDROW"}), true)
check("ikke på MOWER-listen", ADFields.isMowableFruit("WHEAT", nil), false)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-99
View File
@@ -1,99 +0,0 @@
-- Kør: luajit tests/test_adFieldsPolygon.lua (fra repo-roden)
-- FS25-marker har ingen polygonPoints — omridset hentes med field:getDensityMapPolygon():getVerticesList().
-- In-game 26/9 (Dakota 16x, marker på 1,7 x 0,6 km): uden omrids blev kun markskiltet (posX/posZ) prøvet;
-- F108 med majs viste "bar jord" og "mangler markør" (markøren stod ved kanten, 890 m fra skiltet).
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
Logging = {warning = function() end, info = function() end}
local MAIZE = 10
function getWorldTranslation() return 0, 0, 0 end
function getTerrainHeightAtWorldPos() return 0 end
-- markskiltet står i marken, men ved en stribe uden afgrøde (x > 1400)
function cropAt(x) return x <= 1400 and MAIZE or nil end
FSDensityMapUtil = {
getFieldDataAtWorldPosition = function() return 1, 2 end,
getFruitTypeIndexAtWorldPos = function(x, z) return cropAt(x) end,
getFruitArea = function() return 0, 10 end,
}
FieldDensityMap = {SPRAY_LEVEL = 4, LIME_LEVEL = 5, PLOW_LEVEL = 6}
DensityCoordType = {POINT_VECTOR_VECTOR = 1}
DensityMapModifier = {new = function() return {setParallelogramWorldCoords = function() end, executeGet = function() return 1, 1 end} end}
g_fruitTypeManager = {
getFruitTypeByIndex = function() return {name = "MAIZE", fillType = {index = 60, title = "Majs"}, minHarvestingGrowthState = 7, maxHarvestingGrowthState = 7, cutState = 9} end,
getConverterDataByName = function() return {} end,
}
g_fillTypeManager = {getConverterDataByName = function() return {} end, getFillTypeNameByIndex = function() return "X" end}
g_farmlandManager = {getFarmlandOwner = function() return 1 end}
local polygon = {getVerticesList = function() return {0, 0, 1600, 0, 1600, 600, 0, 600} end}
local field = {getId = function() return 108 end, farmland = {id = 108}, posX = 1500, posZ = 50,
getDensityMapPolygon = function() return polygon end}
g_fieldManager = {getFields = function() return {field} end}
g_currentMission = {terrainRootNode = 1, missionInfo = {}, fieldGroundSystem = {
getDensityMapData = function() return 1, 0, 2 end, getMaxValue = function() return 2 end}}
local adEnv = {ADGraphManager = {
getMapMarkers = function() return {{id = 900, name = "F108"}} end,
-- markøren står 5 m uden for marken ved den modsatte kant: 1505 m fra skiltet
getWayPointById = function() return {x = -5, z = 300} end,
}}
dofile("FS25_ADSmartPickup/adFields.lua")
local now = 0
for _ = 1, 1000 do
if ADFields.getInfo(108) ~= nil then break end
ADFields.scanStep(adEnv, 1, now)
now = now + 16
end
local info = ADFields.getInfo(108)
check("PG1 marken er scannet", info ~= nil, true)
check("PG2 omrids fra getDensityMapPolygon", info and #info.verts, 4)
check("PG3 majsen findes trods skiltet på bar stribe", info and info.fruitName, "MAIZE")
check("PG4 ikke bar mark", info and info.kind, "harvest")
check("PG5 markør ved kanten findes (afstand til omridset)", info and info.markerId, 900)
check("PG6 afstand = til kanten", info and info.markerDistance, 5)
check("PG6b markens form-signatur (kursus-genbrug)", info and info.shapeSig, "n=4;a=960000;c=800,300")
-- PG7-9: field.fieldState er spillets sandhed (fields.xml: MAIZE growthState 2); jord-bits fra
-- getFieldDataAtWorldPosition (2. værdi) er IKKE vækststadie. Høstet mark (cutState) = bar, vist som stub.
field.fieldState = {fruitTypeIndex = MAIZE, growthState = 2}
FSDensityMapUtil.getFieldDataAtWorldPosition = function() return true, 0 end -- jord-bits 0 lignede "ingen vækst"
local function rescan()
ADFields.cache, ADFields.listCache, ADFields.activeScan, ADFields.lastScanStart = {}, nil, nil, nil
for _ = 1, 2000 do
if ADFields.getInfo(108) ~= nil then break end
ADFields.scanStep(adEnv, 1, now)
now = now + 16
end
return ADFields.getInfo(108)
end
info = rescan()
check("PG7 majs i vækst (fieldState), ikke bar", info and info.kind, "harvest")
check("PG7 fase vokser", info and info.phase, "growing")
field.fieldState = {fruitTypeIndex = MAIZE, growthState = 9}
info = rescan()
check("PG8 høstet (cutState) = bar", info and info.kind, "bare")
check("PG8 stub af majs", info and info.stubbleOf, "Majs")
field.fieldState = {fruitTypeIndex = 0, growthState = 0}
cropAt = function() return nil end
info = rescan()
check("PG9 ingen afgrøde = bar uden stub", info and info.kind .. "/" .. tostring(info.stubbleOf), "bare/nil")
-- PG10-12: bygget-over mark (fields.xml: groundType NONE — gårdsplads/stald/biogas) er ikke med i listen
field.fieldState = {fruitTypeIndex = 0, growthState = 0, groundType = 0}
info = rescan()
check("PG10 ingen markjord = kan ikke dyrkes", info and info.cultivable, false)
check("PG10 ikke i marklisten", #ADFields.list(), 0)
field.fieldState = {fruitTypeIndex = MAIZE, growthState = 2, groundType = 7}
cropAt = function(x) return MAIZE end
info = rescan()
check("PG11 markjord = kan dyrkes", info and info.cultivable, true)
check("PG11 i marklisten", #ADFields.list(), 1)
check("PG12 ingen punkter på markjord", ADFields.isCultivable(nil, 0), false)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-94
View File
@@ -1,94 +0,0 @@
-- Kør: luajit tests/test_adFieldsScan.lua (fra repo-roden)
-- Markscanningen som arbejdskø: bliver færdig, giver rigtigt resultat, og intet frame laver mere end ét
-- prøvepunkts arbejde (in-game 26/9: en hel mark i ét frame gav hak).
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
Logging = {warning = function(fmt, ...) print("WARN " .. string.format(fmt, ...)) end, info = function() end}
-- falsk verden: én mark (F7) med hvede der er klar, polygon med 4 hjørner
local GRASS, WHEAT = 1, 3
local nodes = {[101] = {0, 0}, [102] = {100, 0}, [103] = {100, 100}, [104] = {0, 100}}
function getWorldTranslation(node) local p = nodes[node]; return p[1], 0, p[2] end
function getTerrainHeightAtWorldPos() return 0 end
local calls = 0
FSDensityMapUtil = {
getFieldDataAtWorldPosition = function() calls = calls + 1; return true, 170 end, -- på mark (2. værdi = jord-bits, IKKE vækst)
getFruitTypeIndexAtWorldPos = function() calls = calls + 1; return WHEAT end,
getFruitArea = function(_, _, _, _, _, _, _, any) calls = calls + 1; return 0, any and 10 or 10 end,
}
FieldDensityMap = {SPRAY_LEVEL = 4, LIME_LEVEL = 5, PLOW_LEVEL = 6}
DensityCoordType = {POINT_VECTOR_VECTOR = 1}
DensityMapModifier = {new = function() return {setParallelogramWorldCoords = function() end, executeGet = function() calls = calls + 1; return 1, 1 end} end}
g_fruitTypeManager = {
getFruitTypeByIndex = function(_, index) return {name = index == WHEAT and "WHEAT" or "GRASS", fillType = {index = 50, title = "Hvede"}, minHarvestingGrowthState = 6, maxHarvestingGrowthState = 7, cutState = 9} end,
getConverterDataByName = function() return {} end,
}
g_fillTypeManager = {getConverterDataByName = function() return {} end, getFillTypeNameByIndex = function() return "X" end}
g_farmlandManager = {getFarmlandOwner = function() return 1 end}
-- spillets egen marktilstand (field.fieldState, som fields.xml): afgrøde + vækststadie
local field = {getId = function() return 7 end, farmland = {id = 70}, polygonPoints = {101, 102, 103, 104}, posX = 50, posZ = 50,
fieldState = {fruitTypeIndex = WHEAT, growthState = 6}}
g_fieldManager = {getFields = function() return {field} end}
g_currentMission = {terrainRootNode = 1, missionInfo = {}, fieldGroundSystem = {
getDensityMapData = function() return 1, 0, 2 end, getMaxValue = function() return 2 end}}
local adEnv = {ADGraphManager = {
getMapMarkers = function() return {{id = 900, name = "F7"}, {id = 901, name = "Silo"}} end,
getWayPointById = function(_, id) return {x = 40, z = 40} end,
}}
dofile("FS25_ADSmartPickup/adFields.lua")
local maxPerFrame, frames = 0, 0
local now = 0
for _ = 1, 200 do
calls = 0
ADFields.scanStep(adEnv, 1, now)
if calls > maxPerFrame then maxPerFrame = calls end
frames = frames + 1
now = now + 16
if ADFields.getInfo(7) ~= nil then break end
end
-- markliste først, så marken (efter SCAN_INTERVAL) — kør videre til marken er scannet
for _ = 1, 400 do
if ADFields.getInfo(7) ~= nil then break end
calls = 0
ADFields.scanStep(adEnv, 1, now)
if calls > maxPerFrame then maxPerFrame = calls end
now = now + 16
end
local info = ADFields.getInfo(7)
check("SC1 marken er scannet", info ~= nil, true)
check("SC2 afgrøde", info and info.fruitName, "WHEAT")
check("SC3 art = høst", info and info.kind, "harvest")
check("SC4 klar-andel", info and info.readyRatio, 1)
check("SC5 fase klar", info and info.phase, "ready")
check("SC6 markør F7", info and info.markerId, 900)
check("SC7 prøvepunkter", info and #info.points, 5)
check("SC8 gødning læst", info and info.fert, 1)
check("SC9 højst ét punkts arbejde pr. frame (<= 4 opslag)", maxPerFrame <= 4, true)
print(" (flest opslag i ét frame: " .. maxPerFrame .. ")")
check("SC10 marklisten", #ADFields.list(), 1)
-- SC11: med ur kører flere opgaver pr. frame inden for budgettet (0,5 ms), højst MAX_TASKS_PER_FRAME
local fakeTime = 0
netGetTime = function() fakeTime = fakeTime + 0.1; return fakeTime end -- hver opgave "koster" 0,1 ms
ADFields.cache, ADFields.listCache, ADFields.activeScan, ADFields.lastScanStart = {}, nil, nil, nil
local frames2 = 0
for _ = 1, 400 do
if ADFields.getInfo(7) ~= nil then break end
ADFields.scanStep(adEnv, 1, now)
now = now + 16
frames2 = frames2 + 1
end
check("SC11 scannet igen med ur", ADFields.getInfo(7) ~= nil, true)
check("SC11 færre frames end én opgave pr. frame", frames2 < frames + 0, true)
print(" (frames uden ur: " .. frames .. ", med ur: " .. frames2 .. ")")
netGetTime = nil
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-44
View File
@@ -1,44 +0,0 @@
-- Kør: luajit tests/test_adFieldsShape.lua (fra repo-roden)
-- Markens form-fingeraftryk (genbrug af Courseplay-kurser): antal hjørner, areal og tyngdepunkt, afrundet så
-- små float-forskelle ikke tæller — men en ændret mark (nyt hjørne, flyttet kant) giver en ny signatur.
dofile("FS25_ADSmartPickup/adFields.lua")
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
local function rect(x0, z0, w, h)
return {{x = x0, z = z0}, {x = x0 + w, z = z0}, {x = x0 + w, z = z0 + h}, {x = x0, z = z0 + h}}
end
-- S1: rektangel 1600 x 600 -> 4 hjørner, 960.000 m², tyngdepunkt (800, 300)
check("S1 rektangel", ADFields.shapeSignature(rect(0, 0, 1600, 600)), "n=4;a=960000;c=800,300")
-- S2: omløbsretning er ligegyldig (arealet er absolut)
local reversed = {}
for index = 4, 1, -1 do table.insert(reversed, rect(0, 0, 1600, 600)[index]) end
check("S2 modsat omløb", ADFields.shapeSignature(reversed), "n=4;a=960000;c=800,300")
-- S3: små float-forskelle (cm) tæller ikke
local noisy = rect(0.0003, -0.0002, 1600.0004, 599.9997)
check("S3 float-støj", ADFields.shapeSignature(noisy), "n=4;a=960000;c=800,300")
-- S4: flyttet kant (1 m bredere) -> ny signatur
check("S4 kant flyttet", ADFields.shapeSignature(rect(0, 0, 1601, 600)) ~= ADFields.shapeSignature(rect(0, 0, 1600, 600)), true)
-- S5: ekstra hjørne på kanten (samme areal) -> ny signatur
local extra = {{x = 0, z = 0}, {x = 800, z = 0}, {x = 1600, z = 0}, {x = 1600, z = 600}, {x = 0, z = 600}}
check("S5 ekstra hjørne", ADFields.shapeSignature(extra), "n=5;a=960000;c=800,300")
-- S6: samme form et andet sted på kortet -> anden signatur
check("S6 flyttet mark", ADFields.shapeSignature(rect(100, 0, 1600, 600)), "n=4;a=960000;c=900,300")
-- S7: tom/ingen polygon -> nil
check("S7 tom", ADFields.shapeSignature({}), nil)
check("S8 nil", ADFields.shapeSignature(nil), nil)
-- S9: under 3 hjørner er ingen flade -> nil
check("S9 to punkter", ADFields.shapeSignature({{x = 0, z = 0}, {x = 1, z = 1}}), nil)
-- S10: trekant: areal 50, tyngdepunkt (10/3, 10/3) -> (3, 3)
check("S10 trekant", ADFields.shapeSignature({{x = 0, z = 0}, {x = 10, z = 0}, {x = 0, z = 10}}), "n=3;a=50;c=3,3")
-- S11: negative koordinater afrundes symmetrisk
check("S11 negative", ADFields.shapeSignature(rect(-1000, -500, 200, 100)), "n=4;a=20000;c=-900,-450")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-33
View File
@@ -1,33 +0,0 @@
-- Kør: luajit tests/test_adFleetNames.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFleetNames.lua")
local N = ADFleetNames
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
local plan = N.plan({
{id = "b", model = "Puma 260 CVX"}, {id = "a", model = "Puma 260 CVX"}, {id = "c", model = "Fastrac 4220"},
{id = "d", model = "Cow Food", inRun = true, name = "Cow Food 1"}, {id = "e", model = "CR11", selfPropelled = true, name = "Mejetærsker"},
})
local byId = {}
for _, entry in ipairs(plan) do byId[entry.id] = entry end
check("navne: samme model nummereres (efter id)", byId.a.name, "Puma 260 CVX 1")
check("navne: anden af samme model", byId.b.name, "Puma 260 CVX 2")
check("navne: enlig model uden nummer", byId.c.name, "Fastrac 4220")
check("navne: flow-traktor beholder navn", byId.d.name, "Cow Food 1")
check("navne: selvkørende beholder navn", byId.e.name, "Mejetærsker")
check("parkering: P + navn", byId.a.parkName, "P Puma 260 CVX 1")
check("parkering: flow-traktor", byId.d.parkName, "P Cow Food 1")
check("omdøb: kun hvis ændret", byId.d.rename, false)
check("omdøb: ny navn", byId.a.rename, true)
-- R7: parkeringer omdøbes kun for maskiner der selv omdøbes, og ikke delte parkeringer
check("R7 flow-traktor: parkering røres ikke", byId.d.renamePark, false)
check("R7 selvkørende: parkering røres ikke", byId.e.renamePark, false)
check("R7 omdøbt traktor: parkering omdøbes", byId.a.renamePark, true)
local shared = N.plan({{id = "x", model = "M", park = 5}, {id = "y", model = "Q", park = 5}}, {[5] = 2})
check("R7 delt parkering røres ikke", shared[1].renamePark, false)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-314
View File
@@ -1,314 +0,0 @@
-- Kør: luajit tests/test_adFlowGuide.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adGuide.lua")
dofile("FS25_ADSmartPickup/adRuns.lua")
dofile("FS25_ADSmartPickup/adFlowView.lua")
dofile("FS25_ADSmartPickup/adFlowGuide.lua")
local FG, G = ADFlowGuide, ADGuide
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
local function newRun(kind, changes)
local store, run = ADRuns.createRun(ADRuns.newStore(), "Mælk ud", kind)
if changes ~= nil then store = ADRuns.updateRun(store, run.id, changes) end
return ADRuns.getRun(store, run.id)
end
local function ctx(extra)
local context = {
buildings = {
{id = "b1", name = "Kostald", label = "Kostald M", hasMarker = true, text = "Kostald — M 5 m"},
{id = "b2", name = "Silo", label = "Silo M", hasMarker = true, text = "Silo — M 3 m"},
{id = "b3", name = "Lade", hasMarker = false, text = "Lade — INGEN MARKØR"},
},
fillTypes = {{name = "HAY", title = "Hø"}, {name = "STRAW", title = "Halm"}},
products = {{name = "MILK", title = "Mælk"}, {name = "EGG", title = "Æg"}},
storeBuildings = {{id = "s1", name = "Silo Nord", text = "Silo Nord — M 5 m"}, {id = "s2", name = "Palleskur", text = "Palleskur — M 2 m"}},
waitPools = {{group = "Wait A", count = 2}},
vehicles = {
{id = "t1", label = "Traktor 1", text = "Traktor 1 — 1000 l — OK"},
{id = "t2", text = "Traktor 2", otherRunId = 9, otherRunName = "Andet"},
{id = "t3", text = "Traktor 3", blocked = true},
{id = "t4", text = "Traktor 4"},
},
}
for key, value in pairs(extra or {}) do context[key] = value end
return context
end
local function steps(guide) return table.concat(G.visibleSteps(guide), ",") end
local function findRow(rows, id, item)
for _, row in ipairs(rows) do
if row.id == id and (item == nil or row.item == item) then return row end
end
return nil
end
local function countType(rows, rowType)
local n = 0
for _, row in ipairs(rows) do if row.type == rowType then n = n + 1 end end
return n
end
local function textKeys(rows)
local keys = {}
for _, row in ipairs(rows) do if row.type == "text" and row.label.key ~= nil then table.insert(keys, row.label.key) end end
return table.concat(keys, ",")
end
local function at(guide, step) return G.jump(guide, step, "close") end
-- ---- trin ----
local supply = FG.open(newRun("supply"), ctx())
check("FL1 forsyning", steps(supply), "direction,buildings,goods,rules,wait,tractors,summary")
local out = FG.open(newRun("outbound"), ctx())
check("FL1 udkørsel uden lager-vare", steps(out), "direction,buildings,goods,rules,wait,tractors,summary")
local outStore = FG.open(newRun("outbound", {products = {{fillType = "MILK", policy = "sellElseStore"}}}), ctx())
check("FL1 udkørsel med lager", steps(outStore), "direction,buildings,goods,store,rules,wait,tractors,summary")
local new = FG.openNew("Flow 3", ctx())
check("FL1 nyt flow har navn", steps(new), "name,direction,buildings,goods,rules,wait,tractors,summary")
check("FL1 nyt flow starter på navn", new.step, "name")
check("FL1 nyt flow er forsyning", new.draft.kind, "supply")
check("FL1 nyt flow navn", new.draft.name, "Flow 3")
check("FL1 eksisterende starter på retning", supply.step, "direction")
-- ---- titler følger retningen ----
check("FL2 til", FG.stepKey("buildings", supply), "spu_guide_flow_step_to")
check("FL2 fra", FG.stepKey("buildings", out), "spu_guide_flow_step_from")
check("FL2 andre", FG.stepKey("rules", out), "spu_guide_flow_step_rules")
check("FL2 hint", FG.hintKey("goods", out), "spu_guide_flow_hint_goods_out")
check("FL2 hint forsyning", FG.hintKey("goods", supply), "spu_guide_flow_hint_goods_in")
-- ---- choose er immutabel ----
local toOut = FG.choose(supply, "kind", "outbound")
check("FL3 retning skiftet", toOut.draft.kind, "outbound")
check("FL3 gammel urørt", supply.draft.kind, "supply")
local withTarget = FG.choose(supply, "buildings", {item = "b1", on = true})
check("FL3 forsyning: mål", table.concat(withTarget.draft.targetBuildingIds, ","), "b1")
check("FL3 forsyning: kilder urørt", #withTarget.draft.sourceBuildingIds, 0)
local withSource = FG.choose(out, "buildings", {item = "b2", on = true})
check("FL3 udkørsel: kilde", table.concat(withSource.draft.sourceBuildingIds, ","), "b2")
local policy = FG.choose(out, "policy", {item = "MILK", value = "store"})
check("FL3 politik", policy.draft.products[1].policy, "store")
check("FL3 politik gør lager-trin synligt", FG.isVisible("store", policy), true)
local off = FG.choose(policy, "policy", {item = "MILK", value = "off"})
check("FL3 politik fra fjerner", #off.draft.products, 0)
local replaced = FG.choose(policy, "policy", {item = "MILK", value = "sell"})
check("FL3 politik erstattes", #replaced.draft.products, 1)
check("FL3 politik ny", replaced.draft.products[1].policy, "sell")
check("FL3 navn", FG.choose(new, "name", "Æg til salg").draft.name, "Æg til salg")
check("FL3 tomt navn ignoreres", FG.choose(new, "name", " ").draft.name, "Flow 3")
-- ---- rækker ----
local dirRows = FG.rows(supply)
check("FL4 retning valg", findRow(dirRows, "kind").type, "choice")
check("FL4 retning værdi", findRow(dirRows, "kind").value, "supply")
local buildingRows = FG.rows(at(supply, "buildings"))
check("FL4 bygninger med markør", countType(buildingRows, "check"), 2)
check("FL4 vis alle", findRow(buildingRows, "showAllBuildings").type, "toggle")
local allRows = FG.rows(at(FG.choose(supply, "showAllBuildings", true), "buildings"))
check("FL4 vis alle: tre", countType(allRows, "check"), 3)
local noHidden = FG.rows(at(FG.open(newRun("supply"), ctx({buildings = {{id = "b1", name = "Kostald", hasMarker = true, text = "K"}}})), "buildings"))
check("FL4 ingen vis-alle uden skjulte", findRow(noHidden, "showAllBuildings"), nil)
local chosenFirst = FG.rows(at(FG.choose(supply, "buildings", {item = "b2", on = true}), "buildings"))
local firstCheck
for _, row in ipairs(chosenFirst) do if row.type == "check" and firstCheck == nil then firstCheck = row end end
check("FL4 fast rækkefølge (navn)", firstCheck.item, "b1")
check("FL4 valgt markeret", findRow(chosenFirst, "buildings", "b2").value, true)
check("FL4 ingen bygninger", textKeys(FG.rows(at(FG.open(newRun("supply"), ctx({buildings = {}})), "buildings"))), "spu_noBuildings")
local goodsAuto = FG.rows(at(supply, "goods"))
check("FL5 forsyning auto", findRow(goodsAuto, "ingredientMode").value, "auto")
check("FL5 forsyning auto: ingen liste", countType(goodsAuto, "check"), 0)
local goodsManual = FG.rows(at(FG.choose(supply, "ingredientMode", "manual"), "goods"))
check("FL5 forsyning vælg: varer", countType(goodsManual, "check"), 2)
check("FL5 forsyning vælg uden varer", textKeys(FG.rows(at(FG.choose(FG.open(newRun("supply"), ctx({fillTypes = {}})), "ingredientMode", "manual"), "goods"))), "spu_guide_flow_noGoodsTo")
local goodsOut = FG.rows(at(policy, "goods"))
check("FL5 udkørsel: politik pr. vare", countType(goodsOut, "choice"), 2)
check("FL5 udkørsel: mælk lager", findRow(goodsOut, "policy", "MILK").value, "store")
check("FL5 udkørsel: æg fra", findRow(goodsOut, "policy", "EGG").value, "off")
check("FL5 udkørsel: politikker", #findRow(goodsOut, "policy", "EGG").options, 4)
check("FL5 udkørsel uden varer", textKeys(FG.rows(at(FG.open(newRun("outbound"), ctx({products = {}})), "goods"))), "spu_guide_flow_noProducts")
local storeRows = FG.rows(at(outStore, "store"))
check("FL6 lager auto", findRow(storeRows, "storeAuto").value, true)
check("FL6 lager auto: ingen liste", countType(storeRows, "check"), 0)
local storeManual = FG.rows(at(FG.choose(outStore, "storeAuto", false), "store"))
check("FL6 lager vælg selv", countType(storeManual, "check"), 2)
local chosenStore = FG.open(newRun("outbound", {products = {{fillType = "MILK", policy = "store"}}, storeBuildingIds = {"s2"}}), ctx())
check("FL6 valgt lager -> vælg selv", chosenStore.draft.storeAuto, false)
local rulesOut = FG.rows(at(out, "rules"))
check("FL7 udkørsel: sælg fra", findRow(rulesOut, "sellAtPercent").value, 90)
check("FL7 udkørsel: sælg-trin", #findRow(rulesOut, "sellAtPercent").options, 11)
check("FL7 udkørsel: lager over", findRow(rulesOut, "storeAbovePercent").value, 80)
check("FL7 udkørsel: samlerunde", findRow(rulesOut, "collectRound").type, "toggle")
check("FL7 udkørsel: ingen ture", findRow(rulesOut, "loops"), nil)
local rulesIn = FG.rows(at(supply, "rules"))
check("FL7 forsyning: ture", findRow(rulesIn, "loops").value, 0)
check("FL7 forsyning: ture-valg", #findRow(rulesIn, "loops").options, ADRuns.MAX_LOOPS + 1)
check("FL7 forsyning: 0 = uendeligt", findRow(rulesIn, "loops").options[1].label.key, "spu_loops_infinite")
check("FL7 forsyning: retur", findRow(rulesIn, "returnBelowPercent").value, 50)
check("FL7 forsyning: ingen salg", findRow(rulesIn, "sellAtPercent"), nil)
local waitRows = FG.rows(at(supply, "wait"))
check("FL8 vent: alle først", findRow(waitRows, "waitPoolGroup").options[1].value, "")
check("FL8 vent: puljer", #findRow(waitRows, "waitPoolGroup").options, 2)
local assigned = FG.open(newRun("supply", {}), ctx())
assigned = FG.choose(assigned, "vehicleIds", {item = "t4", on = true})
local tractorRows = FG.rows(at(assigned, "tractors"))
check("FL9 traktorer: kun egne + ledige", countType(tractorRows, "check"), 2)
check("FL9 egne først", (function() for _, r in ipairs(tractorRows) do if r.type == "check" then return r.item end end end)(), "t4")
check("FL9 skjult andre flows", findRow(tractorRows, "vehicleIds", "t2"), nil)
check("FL9 skjult blokeret", findRow(tractorRows, "vehicleIds", "t3"), nil)
check("FL9 linjer", textKeys(tractorRows), "spu_guide_flow_otherFlows")
-- blokerede traktorer: foldet sammen som standard (in-game 27/9: 37 linjer der ikke kunne bruges fyldte trinnet);
-- linjen med antallet er et link der folder navn + grund ud og ind
local function blockedLine(rows)
for _, r in ipairs(rows) do if r.type == "text" and r.label.text ~= nil and r.label.text:find("Traktor 3", 1, true) then return r end end
return nil
end
local function blockedLink(rows)
for _, r in ipairs(rows) do if r.type == "link" and r.id == "showBlocked" then return r end end
return nil
end
check("FL9 blokerede foldet sammen som standard", blockedLine(tractorRows), nil)
check("FL9 link med antal", blockedLink(tractorRows) ~= nil and blockedLink(tractorRows).label.key, "spu_guide_flow_blockedShow")
local expanded = FG.choose(assigned, "showBlocked", true)
local expandedRows = FG.rows(at(expanded, "tractors"))
check("FL9 foldet ud viser navn + grund", blockedLine(expandedRows) ~= nil, true)
check("FL9 foldet ud: link skjuler igen", blockedLink(expandedRows).label.key, "spu_guide_flow_blockedHide")
check("FL9 fold ud ændrer ikke kladden", expanded.draft.vehicleIds[1], "t4")
check("FL9 fold sammen igen", blockedLine(FG.rows(at(FG.choose(expanded, "showBlocked", true), "tractors"))), nil)
local inThisRun = FG.open(newRun("supply"), ctx({vehicles = {{id = "t2", text = "T2", otherRunId = 1, otherRunName = "Mælk ud"}}}))
check("FL9 traktor i dette flow er ikke 'andet flow'", countType(FG.rows(at(inThisRun, "tractors")), "check"), 1)
-- ---- resumé ----
local function lineFor(lines, step)
for _, line in ipairs(lines) do if line.step == step then return line end end
return nil
end
local function partText(line)
local parts = {}
for _, part in ipairs(line.parts) do table.insert(parts, part.key or ("=" .. tostring(part.text))) end
return table.concat(parts, ",")
end
local sumIn = FG.summary(FG.choose(withTarget, "vehicleIds", {item = "t1", on = true}))
check("FL10 navn", partText(lineFor(sumIn, "name")), "=Mælk ud")
check("FL10 retning", partText(lineFor(sumIn, "direction")), "spu_direction_in")
check("FL10 til", partText(lineFor(sumIn, "buildings")), "=Kostald")
check("FL10 til-titel", lineFor(sumIn, "buildings").title.key, "spu_guide_flow_step_to")
check("FL10 varer auto", partText(lineFor(sumIn, "goods")), "spu_guide_flow_goodsAuto")
check("FL10 regler", partText(lineFor(sumIn, "rules")), "spu_guide_flow_sumLoopsInf,spu_guide_flow_sumReturn")
check("FL10 vent", partText(lineFor(sumIn, "wait")), "spu_waitPool_all")
check("FL10 traktorer", partText(lineFor(sumIn, "tractors")), "=Traktor 1")
check("FL10 intet lager-trin", lineFor(sumIn, "store"), nil)
-- traktor der ikke findes længere (solgt): aldrig rå id i resuméet (in-game 29/9)
local withGhost = FG.choose(FG.choose(withTarget, "vehicleIds", {item = "t1", on = true}), "vehicleIds", {item = "vehicleGONE", on = true})
check("FL10b resumé springer manglende over", partText(lineFor(FG.summary(withGhost), "tractors")), "=Traktor 1")
local onlyGhost = FG.choose(withTarget, "vehicleIds", {item = "vehicleGONE", on = true})
check("FL10b kun manglende -> ingen traktorer", partText(lineFor(FG.summary(onlyGhost), "tractors")), "spu_guide_flow_sumNoTractors")
local sumOut = FG.summary(FG.choose(FG.choose(policy, "collectRound", true), "sellAtPercent", 95))
check("FL11 retning ud", partText(lineFor(sumOut, "direction")), "spu_direction_out")
check("FL11 fra tom", partText(lineFor(sumOut, "buildings")), "spu_guide_flow_none")
check("FL11 varer", partText(lineFor(sumOut, "goods")), "spu_policy_store")
check("FL11 varenavn foran", lineFor(sumOut, "goods").parts[1].prefix, "Mælk: ")
check("FL11 lager auto", partText(lineFor(sumOut, "store")), "spu_guide_sum_auto")
-- varen er kun "lager": salgsgrænsen vises ikke (in-game 29/9)
check("FL11 regler (kun lager)", partText(lineFor(sumOut, "rules")), "spu_guide_flow_sumStoreAbove,spu_guide_flow_sumRound")
check("FL11 lager over", lineFor(sumOut, "rules").parts[1].args[1], 80)
check("FL11 ingen traktorer", partText(lineFor(sumOut, "tractors")), "spu_guide_flow_sumNoTractors")
local manyGoods = FG.summary(FG.choose(FG.choose(FG.choose(supply, "ingredientMode", "manual"), "ingredients", {item = "HAY", on = true}), "ingredients", {item = "STRAW", on = true}))
check("FL12 valgte varer", partText(lineFor(manyGoods, "goods")), "=Hø + Halm")
check("FL12 oversigt: links", countType(FG.rows(G.new(supply.def, supply.draft, supply.context, {step = "summary"})), "link"), 7)
-- ---- resultat ----
local r = FG.result(FG.choose(FG.choose(policy, "storeAuto", false), "storeBuildingIds", {item = "s1", on = true}))
check("FL13 ikke ny", r.isNew, false)
check("FL13 run-id", r.runId, 1)
check("FL13 retning", r.changes.kind, "outbound")
check("FL13 lager valgt", table.concat(r.changes.storeBuildingIds, ","), "s1")
check("FL13 varer", r.changes.products[1].fillType, "MILK")
check("FL13 lager-resumé bruger navn", (function() for _, l in ipairs(FG.summary(FG.choose(FG.choose(policy, "storeAuto", false), "storeBuildingIds", {item = "s1", on = true}))) do if l.step == "store" then return l.parts[1].text end end end)(), "Silo Nord")
local rAuto = FG.result(FG.choose(chosenStore, "storeAuto", true))
check("FL13 lager auto -> tom", #rAuto.changes.storeBuildingIds, 0)
local hiddenStore = FG.open(newRun("outbound", {storeBuildingIds = {"s2"}}), ctx())
check("FL13 lager usynligt: bevares", table.concat(FG.result(hiddenStore).changes.storeBuildingIds, ","), "s2")
local rNew = FG.result(FG.choose(new, "vehicleIds", {item = "t1", on = true}))
check("FL14 nyt", rNew.isNew, true)
check("FL14 navn", rNew.name, "Flow 3")
check("FL14 traktorer", table.concat(rNew.vehicleIds, ","), "t1")
check("FL14 ingen run-id", rNew.runId, nil)
local added, removed = FG.vehicleChanges({"a", "b"}, {"b", "c"})
check("FL15 tilføjet", table.concat(added, ","), "c")
check("FL15 fjernet", table.concat(removed, ","), "a")
-- ---- forhåndsvisning (til controllerens opslag) ----
local markers = {b1 = {101, 102}, b2 = {201}}
local preview = FG.previewRun(withSource.draft, function(id) return markers[id] or {} end)
check("FL16 kilder -> markører", table.concat(preview.sourceWayPointIds, ","), "201")
local previewIn = FG.previewRun(withTarget.draft, function(id) return markers[id] or {} end)
check("FL16 mål -> markører", table.concat(previewIn.targetWayPointIds, ","), "101,102")
check("FL16 primært mål", previewIn.targetWayPointId, 101)
check("FL16 kladde urørt", withTarget.draft.targetWayPointIds == nil or #withTarget.draft.targetWayPointIds == 0, true)
-- ---- solo-traktor + ledig vogn (in-game 29/9: flow kunne ikke oprettes uden at omdøbe en vogns parkering) ----
local soloCtx = ctx({
vehicles = {
{id = "s1", label = "Solo 1", text = "Solo 1", solo = true},
{id = "s2", label = "Solo 2", text = "Solo 2", solo = true},
{id = "s3", label = "Solo 3", text = "Solo 3", solo = true},
{id = "t1", label = "Traktor 1", text = "Traktor 1 — OK"},
},
wagons = {{id = "w1", label = "P Disponibel Vogn 4"}, {id = "w2", label = "P Disponibel Vogn 5"}},
})
local soloGuide = FG.open(newRun("supply"), soloCtx)
local one = FG.choose(soloGuide, "vehicleIds", {item = "s1", on = true})
check("WG1 solo tildeles første ledige vogn", one.draft.wagons.s1, "w1")
local two = FG.choose(one, "vehicleIds", {item = "s2", on = true})
check("WG2 næste solo får næste vogn", two.draft.wagons.s2, "w2")
check("WG2 første beholder sin", two.draft.wagons.s1, "w1")
local three = FG.choose(two, "vehicleIds", {item = "s3", on = true})
check("WG3 ingen ledig vogn tilbage -> ingen tildeling", three.draft.wagons.s3, nil)
local soloRows = FG.rows(at(three, "tractors"))
local wagonRow = findRow(soloRows, "wagon", "s1")
check("WG4 vognvalg under solo-traktor", wagonRow ~= nil and wagonRow.type, "choice")
check("WG4 vognvalg viser tildelt vogn", wagonRow ~= nil and wagonRow.value, "w1")
check("WG4 valgmuligheder uden de andres vogne", wagonRow ~= nil and #wagonRow.options, 1)
local noFree = false
for _, row in ipairs(soloRows) do if row.label ~= nil and row.label.key == "spu_guide_flow_noFreeWagon" then noFree = true end end
check("WG5 solo uden ledig vogn får besked", noFree, true)
check("WG5 almindelig traktor har intet vognvalg", findRow(FG.rows(at(FG.choose(soloGuide, "vehicleIds", {item = "t1", on = true}), "tractors")), "wagon", "t1"), nil)
local swapped = FG.choose(FG.choose(one, "vehicleIds", {item = "s2", on = false}), "wagon", {item = "s1", value = "w2"})
check("WG6 skift vogn", swapped.draft.wagons.s1, "w2")
local off = FG.choose(two, "vehicleIds", {item = "s1", on = false})
check("WG7 fravalgt traktor slipper vognen", off.draft.wagons.s1, nil)
local freed = FG.choose(off, "vehicleIds", {item = "s3", on = true})
check("WG7 frigjort vogn går til næste", freed.draft.wagons.s3, "w1")
check("WG8 resultat har vognene", FG.result(two).wagons.s2, "w2")
check("WG8 resultat uden fravalgte", FG.result(off).wagons.s1, nil)
local remembered = newRun("supply", {vehicleIds = {"s2"}})
remembered.wagons = {s2 = "w2"}
local reopened = FG.open(remembered, soloCtx)
check("WG9 husket vogn i kladden", reopened.draft.wagons.s2, "w2")
check("WG9 ny solo tager ikke den huskede", FG.choose(reopened, "vehicleIds", {item = "s1", on = true}).draft.wagons.s1, "w1")
check("WG9 kladden kopierer (run urørt)", (function() local g = FG.choose(reopened, "wagon", {item = "s2", value = "w1"}); return remembered.wagons.s2 end)(), "w2")
-- regler: kun de relevante for varernes politik
local storeOnly = FG.open(newRun("outbound", {products = {{fillType = "MILK", policy = "store"}}}), ctx())
local function keys(rows) local r = {} for _, row in ipairs(rows) do if row.id then table.insert(r, row.id) end end return table.concat(r, ",") end
check("RL1 kun lager: ingen salgsvalg", keys(FG.rows(at(storeOnly, "rules"))), "storeAbovePercent,collectRound")
local sellOnly = FG.open(newRun("outbound", {products = {{fillType = "MILK", policy = "sell"}}}), ctx())
check("RL2 kun salg: ingen lagervalg", keys(FG.rows(at(sellOnly, "rules"))), "sellAtPercent,sellAbovePercent,collectRound")
local function ruleKeys(guide)
for _, line in ipairs(FG.summary(guide)) do
if line.step == "rules" then local r = {} for _, p in ipairs(line.parts) do table.insert(r, p.key) end return table.concat(r, ",") end
end
end
check("RL3 resumé kun lager", ruleKeys(storeOnly), "spu_guide_flow_sumStoreAbove")
check("RL4 resumé kun salg", ruleKeys(sellOnly), "spu_guide_flow_sumSellAt")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-105
View File
@@ -1,105 +0,0 @@
-- Kør: luajit tests/test_adFlowView.lua (fra repo-roden)
-- Ren oprydning af Flows-fanen: tavlens rækkefølge, hvilke traktorer og bygninger der vises.
dofile("FS25_ADSmartPickup/adFlowView.lua")
local V = ADFlowView
local failures = 0
local function check(name, actual, expected)
if actual == expected then
print("OK " .. name)
else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
local function ids(list, field)
local out = {}
for _, item in ipairs(list) do table.insert(out, tostring(item[field or "id"])) end
return table.concat(out, ",")
end
-- FV1: tavlen — forsyning (Ind) først, så udkørsel (Ud); hver gruppe sorteret efter navn (naturligt)
local runs = {
{id = 1, name = "Pig Food", kind = "supply"},
{id = 2, name = "Milk Sale", kind = "outbound"},
{id = 3, name = "Cow Food", kind = "supply"},
{id = 4, name = "Biogas 10", kind = "supply"},
{id = 5, name = "Biogas 9", kind = "supply"},
{id = 6, name = "Digestate", kind = "outbound"},
}
local groups = V.boardGroups(runs)
check("FV1 to grupper", #groups, 2)
check("FV1 Ind først", groups[1].kind, "supply")
check("FV1 Ind sorteret (Biogas 9 før 10)", ids(groups[1].runs), "5,4,3,1")
check("FV1 Ud sorteret", ids(groups[2].runs), "6,2")
check("FV1 samlet rækkefølge til vælgeren", ids(V.orderedRuns(runs)), "5,4,3,1,6,2")
check("FV1 tom gruppe udelades", #V.boardGroups({{id = 1, name = "A", kind = "supply"}}), 1)
-- FV2: traktorer — egne først, så ledige brugbare; andre flows og låste kun med "vis alle"
local vehicles = {
{id = "a", label = "Zetor", state = "free"},
{id = "b", label = "Fendt", state = "in"},
{id = "c", label = "Case", state = "other"},
{id = "d", label = "Claas", state = "blocked"},
{id = "e", label = "Deutz", state = "free"},
}
check("FV2 standard: egne + ledige, sorteret", ids(V.vehicleRows(vehicles, false)), "b,e,a")
check("FV2 vis alle: + andre flows + låste til sidst", ids(V.vehicleRows(vehicles, true)), "b,e,a,c,d")
check("FV2 antal skjulte", V.hiddenCount(vehicles, V.vehicleRows(vehicles, false)), 2)
-- FV3: bygninger — valgte først; uden markør skjult medmindre valgt eller "vis alle"
local buildings = {
{id = "s2", name = "Silo 2", hasMarker = true},
{id = "s10", name = "Silo 10", hasMarker = true},
{id = "x", name = "Gammel lade", hasMarker = false},
{id = "y", name = "Anden uden", hasMarker = false},
{id = "a", name = "Anlæg", hasMarker = true},
}
local chosen = {s10 = true, y = true}
-- fast rækkefølge (et tryk må ikke flytte rækken — in-game 26/9 hoppede valgte drops op i toppen)
check("FV3 standard: fast rækkefølge efter navn, valgte bliver hvor de er", ids(V.buildingRows(buildings, chosen, false)), "y,a,s2,s10")
check("FV3 vis alle: uden markør til sidst", ids(V.buildingRows(buildings, chosen, true)), "y,a,s2,s10,x")
-- ens bygningsnavne (5 x "BGA (1) : DOB 115") sorteres efter markørnavnet
local drops = {
{id = "d5", name = "DOB", label = "Biogas Drop 5", hasMarker = true},
{id = "d3", name = "DOB", label = "Biogas Drop 3", hasMarker = true},
{id = "d1", name = "DOB", label = "Biogas Drop 1", hasMarker = true},
{id = "d4", name = "DOB", label = "Biogas Drop 4", hasMarker = true},
{id = "d2", name = "DOB", label = "Biogas Drop 2", hasMarker = true},
}
check("FV3 ens navne efter markør", ids(V.buildingRows(drops, {d2 = true}, false)), "d1,d2,d3,d4,d5")
-- FV4: ens navne slås sammen med antal ("Chicken Coop x5"), rækkefølge efter første forekomst
check("FV4 sammenlæg", V.joinCounted({"Chicken Coop", "Chicken Coop", "Cow Barn", "Chicken Coop"}, " + ", "-"), "Chicken Coop x3 + Cow Barn")
check("FV4 ingen dubletter", V.joinCounted({"A", "B"}, " + ", "-"), "A + B")
check("FV4 tom -> tekst", V.joinCounted({}, " + ", "(ingen)"), "(ingen)")
-- FV-wrap: lange tekstrækker ombrydes ved mellemrum; indrykning gentages; kort tekst uændret
local wrapped = V.wrapText(" Prepared and sown with Sugar Beet as soon as it is sowing time for it", 30)
check("FVwrap flere linjer", #wrapped > 1, true)
check("FVwrap første linje indrykket", string.sub(wrapped[1], 1, 4), " ")
check("FVwrap anden linje indrykket", string.sub(wrapped[2], 1, 4), " ")
local maxLen = 0
for _, line in ipairs(wrapped) do if #line > maxLen then maxLen = #line end end
check("FVwrap ingen linje over grænsen", maxLen <= 30, true)
check("FVwrap intet ord tabt", table.concat(wrapped, " "):gsub("%s+", " "):gsub("^ ", ""), "Prepared and sown with Sugar Beet as soon as it is sowing time for it")
check("FVwrap kort tekst", V.wrapText("Kort", 30)[1], "Kort")
check("FVwrap langt ord deles ikke", V.wrapText("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 10)[1], "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
-- salgs-/lagerregler vises kun når en vare bruger dem (in-game 29/9: 'sell from 90 %' på et rent lager-flow)
local sells, stores = ADFlowView.policyUse({"store"})
check("politik: kun lager -> sælger ikke", sells, false)
check("politik: kun lager -> lagrer", stores, true)
sells, stores = ADFlowView.policyUse({"sell"})
check("politik: kun salg", tostring(sells) .. tostring(stores), "truefalse")
sells, stores = ADFlowView.policyUse({"sellElseStore", "off"})
check("politik: sælg ellers lager -> begge", tostring(sells) .. tostring(stores), "truetrue")
sells, stores = ADFlowView.policyUse({})
check("politik: ingen varer endnu -> vis begge", tostring(sells) .. tostring(stores), "truetrue")
sells, stores = ADFlowView.policyUse({"off"})
check("politik: kun fra -> vis begge", tostring(sells) .. tostring(stores), "truetrue")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-140
View File
@@ -1,140 +0,0 @@
-- Kør: luajit tests/test_adFlowWagons.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adRuns.lua")
dofile("FS25_ADSmartPickup/adFlowWagons.lua")
local W = ADFlowWagons
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
-- gem/indlæs: traktor -> vogn
local text = W.serialize({vehicleB = "wagon2", vehicleA = "wagon1"})
check("gem: sorteret", text, "vehicleA=wagon1;vehicleB=wagon2")
local parsed = W.parse(text)
check("indlæs: A", parsed.vehicleA, "wagon1")
check("indlæs: B", parsed.vehicleB, "wagon2")
check("indlæs: tom", next(W.parse("")), nil)
check("indlæs: nil", next(W.parse(nil)), nil)
check("indlæs: skrald springes over", next(W.parse("=x;;y=")), nil)
-- husk vogn: ny tabel, gammel urørt
local before = {t1 = "w1"}
local after = W.remember(before, "t1", "w9")
check("husk: ny vogn", after.t1, "w9")
check("husk: original urørt", before.t1, "w1")
check("husk: samme vogn -> samme tabel", W.remember(after, "t1", "w9"), after)
-- start: hent / vent / start
check("start: har redskab bagpå", W.startAction({hasRear = true, wagonId = "w1", wagonExists = true, hasConnect = true}), "start")
check("start: ingen husket vogn", W.startAction({}), "start")
check("start: vognen findes ikke", W.startAction({wagonId = "w1", wagonExists = false, hasConnect = true}), "start")
check("start: vognen uden Connect", W.startAction({wagonId = "w1", wagonExists = true, hasConnect = false}), "start")
check("start: vognen på en anden", W.startAction({wagonId = "w1", wagonExists = true, hasConnect = true, wagonAttached = true}), "wait")
check("start: vognen lånt ud", W.startAction({wagonId = "w1", wagonExists = true, hasConnect = true, wagonBusy = true}), "wait")
check("start: hent", W.startAction({wagonId = "w1", wagonExists = true, hasConnect = true}), "fetch")
-- ejer: flowets vogn er flowets — også når flowet er stoppet (in-game 29/9: marksilo lånte Cow Food-vognen)
local runs = {{id = 1, stopped = false, wagons = {t1 = "w1"}}, {id = 2, stopped = true, wagons = {t2 = "w2"}}}
check("ejer: kørende flow holder", W.isHeld(runs, "w1"), true)
check("ejer: stoppet flow holder stadig", W.isHeld(runs, "w2"), true)
check("ejer: ukendt vogn", W.isHeld(runs, "w3"), false)
check("ejer: flowet findes", W.ownerOf(runs, "w2").id, 2)
-- bygning væk: to tjek i træk
local count, gone = W.missingStep(0, true)
check("væk: første tjek -> ikke endnu", gone, false)
count, gone = W.missingStep(count, true)
check("væk: andet tjek -> væk", gone, true)
count, gone = W.missingStep(1, false)
check("væk: fundet igen nulstiller", count, 0)
check("væk: fundet igen -> ikke væk", gone, false)
-- manglende bygninger i et flow
local exists = {a = true, b = true}
check("mangler: alle findes", #W.missingBuildings({sourceBuildingIds = {"a"}, targetBuildingIds = {"b"}}, exists, exists), 0)
check("mangler: én væk", W.missingBuildings({targetBuildingIds = {"a", "c"}, storeBuildingIds = {}}, exists, {a = true, c = true})[1], "c")
-- review v1.51
check("R1 traktoren er lånt ud af puljen -> vent", W.startAction({tractorBusy = true, hasRear = true, wagonId = "w1", wagonExists = true, hasConnect = true}), "wait")
local runs2 = {{id = 1, stopped = true, wagons = {t1 = "w1"}}, {id = 2, stopped = false, wagons = {t9 = "w1"}}}
check("R4 holdt hvis ET kørende flow har vognen", W.isHeld(runs2, "w1"), true)
local seen = {a = true, c = true}
check("R+ bygning aldrig set i sessionen tæller ikke som væk", #W.missingBuildings({targetBuildingIds = {"b"}}, {a = true}, seen), 0)
check("R+ bygning set og nu væk", W.missingBuildings({targetBuildingIds = {"c"}}, {a = true}, seen)[1], "c")
-- gæt flowets vogn ud fra parkeringsnavnet (ingen husket vogn: vognene var sat på plads før v1.51)
local cands = {{id = "v1", parkName = "P Cow Food vogn 1"}, {id = "v3", parkName = "P Cow Food vogn 3"},
{id = "g1", parkName = "P Goat Food vogn 1"}, {id = "v2", parkName = "P Cow Food vogn 2"}}
check("gæt: samme nummer", W.guessWagon("Cow Food", 3, cands, {}), "v3")
check("gæt: nummer optaget -> anden med flowets navn", W.guessWagon("Cow Food", 3, cands, {v3 = true}), "v1")
check("gæt: andet flow", W.guessWagon("Goat Food", 2, cands, {}), "g1")
check("gæt: intet passer", W.guessWagon("Pig Food", 1, cands, {}), nil)
check("gæt: store/små bogstaver", W.guessWagon("cow food", 2, cands, {}), "v2")
check("gæt: 'Cow Food 1' matcher ikke 'Cow Food 10'", W.guessWagon("Cow Food", 1, {{id = "x", parkName = "P Cow Food vogn 10"}, {id = "y", parkName = "P Cow Food vogn 1"}}, {}), "y")
-- tøm vognen før den parkeres (flow stoppet): mål -> silo/fabrik -> hjem
check("tøm: tom vogn -> hjem", W.emptyStep("start", {cargo = 0}), "home")
check("tøm: last -> flowets mål", W.emptyStep("start", {cargo = 5000, hasTarget = true}), "target")
check("tøm: last uden mål -> silo", W.emptyStep("start", {cargo = 5000, hasTarget = false}), "dump")
check("tøm: kører stadig -> vent", W.emptyStep("target", {cargo = 5000, adActive = true}), "wait")
check("tøm: mål fuldt, rest -> silo", W.emptyStep("target", {cargo = 2000}), "dump")
check("tøm: målet tog det hele -> hjem", W.emptyStep("target", {cargo = 0}), "home")
check("tøm: silo tog det -> hjem", W.emptyStep("dump", {cargo = 0}), "home")
check("tøm: silo kunne ikke -> giv op, hjem", W.emptyStep("dump", {cargo = 2000}), "giveUp")
check("tøm: for længe -> giv op", W.emptyStep("target", {cargo = 5000, adActive = true, elapsedMs = W.EMPTY_TIMEOUT_MS}), "giveUp")
-- flowet startes igen mens traktoren tømmer/kører hjem fra et stop: hjemturen afbrydes, den kører videre med vognen
-- (in-game 29/9: Stop+Start med 1 s mellemrum -> tømning ved forkert mål i 10 min, vogn sat af, traktor i garage)
check("genstart: tømmer -> afbryd", W.abortHomeTripOnStart({emptying = true}), true)
check("genstart: flowets hjemtur -> afbryd", W.abortHomeTripOnStart({flowHoming = true}), true)
check("genstart: puljens hjemtur (markarbejde) -> rør ikke", W.abortHomeTripOnStart({poolReturning = true}), false)
check("genstart: intet i gang", W.abortHomeTripOnStart({}), false)
-- tøm-mål: kun flowets egne mål — AD's 2. markør kan være forældet (æg-markør) eller et Wait-punkt (in-game 29/9)
check("tøm-mål: AD's mål er et af flowets", W.emptyTarget(12638, {12636, 12638}), 12638)
check("tøm-mål: forældet/Wait-markør -> flowets første mål", W.emptyTarget(12658, {12636, 12638}), 12636)
check("tøm-mål: intet AD-mål -> flowets første", W.emptyTarget(nil, {12636}), 12636)
check("tøm-mål: flow uden mål -> intet (silo)", W.emptyTarget(12658, {}), nil)
check("tøm-mål: ingen liste -> intet", W.emptyTarget(12658, nil), nil)
-- ledig traktor (puljen/markarbejde/marksilo må låne den): kun solo fra et stoppet flow
check("ledig: intet flow", W.tractorFree({}), true)
check("ledig: kørende flow", W.tractorFree({hasRun = true}), false)
check("ledig: stoppet flow, solo", W.tractorFree({hasRun = true, stopped = true}), true)
check("ledig: stoppet flow, flow-vogn bagpå", W.tractorFree({hasRun = true, stopped = true, carriesFlowWagon = true}), false)
check("ledig: stoppet flow, tømmer/kører hjem", W.tractorFree({hasRun = true, stopped = true, homeTrip = true}), false)
-- venteårsag i loggen (før stod der altid "i brug andetsteds")
check("vent-årsag: traktor optaget", W.waitReason({tractorBusy = true}), "tractorBusy")
check("vent-årsag: vogn på en anden", W.waitReason({wagonAttached = true}), "wagonAttached")
check("vent-årsag: vogn reserveret", W.waitReason({wagonBusy = true}), "wagonReserved")
-- tømning går i stå ved et fuldt mål (in-game 29/9: fåre-/BGA-vogne stod 10 min ved fuldt mål efter Stop alt)
check("tøm: står ved fuldt mål -> silo", W.emptyStep("target", {cargo = 5000, adActive = true, stalled = true}), "dump")
check("tøm: står ved silo uden at tømme -> næste silo", W.emptyStep("dump", {cargo = 5000, adActive = true, stalled = true, dumpAttempts = 1}), "dump")
check("tøm: 3 siloer prøvet -> giv op", W.emptyStep("dump", {cargo = 5000, adActive = true, stalled = true, dumpAttempts = W.MAX_DUMP_ATTEMPTS}), "giveUp")
check("tøm: kører mod målet (ikke i stå) -> vent", W.emptyStep("target", {cargo = 5000, adActive = true, stalled = false}), "wait")
check("i stå: tæt på, uændret længe", W.isStalled({nearTarget = true, sinceChangeMs = W.EMPTY_STALL_MS}), true)
check("i stå: tæt på, lige ankommet", W.isStalled({nearTarget = true, sinceChangeMs = 5000}), false)
check("i stå: på vej (langt væk)", W.isStalled({nearTarget = false, sinceChangeMs = 999999}), false)
-- hent vogn: ét nyt forsøg efter "stoppedAway" (AD standsede undervejs — fx spærret af en anden traktor)
check("hent igen: stoppedAway første gang", W.retryFetch("stoppedAway (37 m fra markøren)", 0), true)
check("hent igen: stoppedAway anden gang -> nej", W.retryFetch("stoppedAway", 1), false)
check("hent igen: noAttachable -> nej (markøren skal rettes)", W.retryFetch("noAttachable", 0), false)
-- manuel påkobling (spilleren sendte traktoren til Connect): traktor i et kørende flow startes bagefter
-- (in-game 29/9: Cow Food 3 hentede sin vogn manuelt efter et fejlet forsøg og blev holdende)
check("manuel kobling: kørende flow, egen vogn -> start", W.startAfterManualAttach({flowRunning = true, ownWagon = true}), true)
check("manuel kobling: stoppet flow -> nej", W.startAfterManualAttach({flowRunning = false, ownWagon = true}), false)
check("manuel kobling: AD kører allerede -> nej", W.startAfterManualAttach({flowRunning = true, ownWagon = true, adActive = true}), false)
check("manuel kobling: fremmed vogn -> nej", W.startAfterManualAttach({flowRunning = true, ownWagon = false}), false)
-- husk vogn ved hjemtur: kun når flowet KØRTE med den (stop); ikke ved oprydning efter indlæsning, hvor traktoren
-- kan have et puljeredskab på fra et lån (in-game 29/9: Biogas Silage "huskede" en læssevogn fra markarbejdet)
check("husk ved stop af kørende flow", W.shouldRememberOnHome("stop"), true)
check("husk ikke ved oprydning efter indlæsning", W.shouldRememberOnHome("afterLoad"), false)
-- forældreløs påkobling (hentet før genindlæsning, opgaven tabt): sættes tilbage efter 2 min uden ejer
check("forældreløs: ingen ejer, længe nok -> hjem", W.orphanAction({owned = false, idleMs = W.ORPHAN_MS}), "home")
check("forældreløs: har ejer -> intet", W.orphanAction({owned = true, idleMs = W.ORPHAN_MS}), "none")
check("forældreløs: for kort tid -> vent", W.orphanAction({owned = false, idleMs = 1000}), "wait")
-- parkeret sæt på traktorens egen plads er ikke forældreløst (in-game 29/9: vinmarks-REX blev skilt ad)
check("forældreløs: står på egen parkering -> intet", W.orphanAction({owned = false, atOwnPark = true, idleMs = W.ORPHAN_MS}), "none")
-- rækkefølge for ledige vogne i flow-guiden: navnematch, så disponible, så resten (alfabetisk)
local ranked = W.rankWagons("Sugarbeet Cut", {
{id = "a", label = "P Tedder"}, {id = "b", label = "P Disponibel Vogn 5"},
{id = "c", label = "P Sugarbeet Cut vogn 1"}, {id = "d", label = "P Disponibel Vogn 4"}, {id = "e", label = "P Bredal vogn"}})
local order = {}
for _, w in ipairs(ranked) do table.insert(order, w.id) end
check("rank: navn, disponible, resten", table.concat(order, ","), "c,d,b,e,a")
check("rank: tom liste", #W.rankWagons("X", {}), 0)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-113
View File
@@ -1,113 +0,0 @@
-- Kør: luajit tests/test_adGuide.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adGuide.lua")
local G = ADGuide
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
-- trin a..e + summary; b vises kun når draft.showB, d aldrig
local def = {
order = {"a", "b", "c", "d", "e", "summary"},
isVisible = function(step, guide)
if step == "b" then return guide.draft.showB == true end
if step == "d" then return false end
return true
end,
}
local guide = G.new(def, {showB = false, value = 1}, {x = 1})
check("G1 start på første synlige", guide.step, "a")
local index, total = G.position(guide)
check("G1 position", index, 1)
check("G1 total over synlige", total, 4)
check("G1 synlige", table.concat(G.visibleSteps(guide), ","), "a,c,e,summary")
-- næste springer skjulte over
local g2, event = G.next(guide)
check("G2 næste springer b over", g2.step, "c")
check("G2 intet event", event, nil)
check("G2 gammel guide urørt", guide.step, "a")
local g3 = G.next(g2)
check("G3 springer d over", g3.step, "e")
check("G3 position", (G.position(g3)), 3)
local g4 = G.next(g3)
check("G4 summary er sidst", G.isLast(g4), true)
local _, done = G.next(g4)
check("G4 næste på sidste = done", done, "done")
-- tilbage
local back = G.back(g3)
check("G5 tilbage til c", back.step, "c")
local _, cancel = G.back(guide)
check("G5 tilbage på første = cancel", cancel, "cancel")
-- valg der gør et trin synligt ændrer tælling og næste
local shown = G.withDraft(guide, {showB = true, value = 1})
check("G6 withDraft ny guide", guide.draft.showB, false)
check("G6 b synlig", G.next(shown).step, "b")
check("G6 total", select(2, G.position(shown)), 5)
-- aktuelt trin bliver usynligt: næste går til næste synlige i rækkefølgen, position nil
local onB = G.new(def, {showB = true}, {}, {step = "b"})
local hidden = G.withDraft(onB, {showB = false})
check("G7 position nil for skjult trin", (G.position(hidden)), nil)
check("G7 næste efter skjult", G.next(hidden).step, "c")
check("G7 tilbage fra skjult", G.back(hidden).step, "a")
-- spring (Ret) fra oversigt-trinnet: næste -> tilbage til summary, ikke single
local fromSummary = G.jump(g4, "c", "summary")
check("G8 spring trin", fromSummary.step, "c")
check("G8 single", fromSummary.single, true)
local returned, ev = G.next(fromSummary)
check("G8 retur til summary", returned.step, "summary")
check("G8 ikke single mere", returned.single, false)
check("G8 intet event", ev, nil)
-- tilbage i spring gendanner kladden fra før springet
local changed = G.withDraft(fromSummary, {showB = false, value = 99})
local undone = G.back(changed)
check("G9 tilbage gendanner kladde", undone.draft.value, 1)
check("G9 tilbage til summary", undone.step, "summary")
-- spring fra fanens oversigt (close): næste = done, tilbage = cancel
local single = G.new(def, {value = 5}, {}, {step = "e", single = true, returnTo = "close"})
local _, singleDone = G.next(single)
check("G10 single close næste = done", singleDone, "done")
local _, singleCancel = G.back(single)
check("G10 single close tilbage = cancel", singleCancel, "cancel")
check("G10 single isLast", G.isLast(single), true)
-- withContext
local ctxGuide = G.withContext(guide, {x = 2})
check("G11 ny context", ctxGuide.context.x, 2)
check("G11 gammel context", guide.context.x, 1)
-- deepCopy / toggleItem
local original = {list = {1, 2}, nested = {a = {b = 1}}}
local copy = G.deepCopy(original)
copy.list[1] = 9
copy.nested.a.b = 7
check("G12 deepCopy liste", original.list[1], 1)
check("G12 deepCopy indlejret", original.nested.a.b, 1)
local list = {"x", "y"}
check("G13 toggle til", table.concat(G.toggleItem(list, "z", true), ","), "x,y,z")
check("G13 ingen dublet", table.concat(G.toggleItem(list, "x", true), ","), "x,y")
check("G13 toggle fra", table.concat(G.toggleItem(list, "x", false), ","), "y")
check("G13 original urørt", #list, 2)
-- tekster
local texts = {greet = "Hej %s", n = "%d stk", title = "Afgrøde", plain = "Ingen"}
local function T(key) return texts[key] or key end
check("G14 label args", G.labelText({key = "greet", args = {"Bo"}}, T), "Hej Bo")
check("G14 label tekst", G.labelText({text = "rå"}, T), "rå")
check("G14 manglende args fejler ikke", G.labelText({key = "n"}, T), "%d stk")
check("G14 prefix", G.labelText({key = "plain", prefix = "Mælk: "}, T), "Mælk: Ingen")
check("G14 nil label", G.labelText(nil, T), "")
check("G15 linje", G.lineText({step = "crop", title = {key = "title"}, parts = {{key = "plain"}, {key = "n", args = {3}}}}, T), "Afgrøde: Ingen, 3 stk")
check("G15 linje uden dele", G.lineText({step = "crop", title = {key = "title"}, parts = {}}, T), "Afgrøde: -")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-129
View File
@@ -1,129 +0,0 @@
-- Kør: luajit tests/test_adHitch.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adHitch.lua")
local H = ADHitch
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
-- markørnavne -> handling + navn (store/små bogstaver ligegyldigt)
local function parsed(name) local a, n = H.parseMarker(name) return tostring(a) .. "|" .. tostring(n) end
check("navn: Connect Bredal", parsed("Connect Bredal"), "attach|Bredal")
check("navn: små bogstaver", parsed("connect bredal"), "attach|bredal")
check("navn: dansk Kobl", parsed("Kobl Vogn 2"), "attach|Vogn 2")
check("navn: kun Connect", parsed("Connect"), "attach|")
check("navn: Connected er ikke en koblingsmarkør", parsed("Connected silo"), "nil|nil")
check("navn: almindelig markør", parsed("Bredal"), "nil|nil")
check("navn: nil", parsed(nil), "nil|nil")
check("navn: Afkobl Bredal", parsed("Afkobl Bredal"), "detach|Bredal")
check("navn: Kobl af er afkobling, ikke kobling", parsed("Kobl af Bredal"), "detach|Bredal")
check("navn: Detach", parsed("detach"), "detach|")
check("navn: Disconnect", parsed("Disconnect Vogn"), "detach|Vogn")
-- bagerste redskaber: kun led bag traktoren (z < 0 i traktorens rum), ikke frontvægt/frontlæsser
local rear = H.rearImplements({{name = "vægt", z = 3.1}, {name = "Bredal", z = -2.4}, {name = "læsser", z = 1.5}, {name = "plov", z = -1}})
check("bag: kun bagerste", #rear, 2)
check("bag: første", rear[1].name, "Bredal")
-- ankomst: AD er stoppet, traktoren står stille tæt på markøren
check("ankomst: stoppet ved markør", H.hasArrived({adActive = false, distance = 1.2, speed = 0.1}), true)
check("ankomst: AD kører stadig", H.hasArrived({adActive = true, distance = 0.5, speed = 0}), false)
check("ankomst: for langt væk", H.hasArrived({adActive = false, distance = 9, speed = 0}), false)
check("ankomst: ruller stadig", H.hasArrived({adActive = false, distance = 1, speed = 3}), false)
-- efter at have bakket hen til vognen står traktoren længere fra markøren
check("ankomst: bakket til vognen, udvidet afstand", H.hasArrived({adActive = false, distance = 9, speed = 0, arriveDistance = 13}), true)
local crept = H.step(H.newJob(1, "Connect x", "attach"), {adActive = false, distance = 9, speed = 3, arriveDistance = 13}, 4000, false)
check("vagt: bakket, ruller lidt -> ikke 'stoppedAway'", crept.state, "driving")
-- vagten: efter ankomst forsøges koblingen et stykke tid, derefter opgives den
local job = H.newJob(25, "Connect Bredal", "attach")
check("job: venter på AD", job.state, "driving")
check("job: handling", job.action, "attach")
H.step(job, {adActive = true, distance = 30, speed = 10}, 1000, false)
check("job: kører stadig", job.state, "driving")
H.step(job, {adActive = false, distance = 1, speed = 0}, 500, false)
check("job: ankommet, kobling ikke lykkedes endnu", job.state, "acting")
H.step(job, {adActive = false, distance = 1, speed = 0}, 500, true)
check("job: koblet på", job.state, "done")
local failJob = H.newJob(25, "Connect Bredal", "attach")
H.step(failJob, {adActive = false, distance = 1, speed = 0}, 500, false)
for _ = 1, 20 do H.step(failJob, {adActive = false, distance = 1, speed = 0}, 500, false) end
check("job: opgivet efter vagt-tid", failJob.state, "failed")
check("job: årsag", failJob.reason, "noAttachable")
local dropJob = H.newJob(9, "Afkobl Bredal", "detach")
H.step(dropJob, {adActive = false, distance = 1, speed = 0}, 500, false)
for _ = 1, 45 do H.step(dropJob, {adActive = false, distance = 1, speed = 0}, 500, false) end
check("job: afkobling opgivet", dropJob.reason, "nothingToDetach")
local lostJob = H.newJob(25, "Connect Bredal", "attach")
H.step(lostJob, {adActive = false, distance = 40, speed = 0}, 500, false)
check("job: AD ikke startet endnu (opstart-frist)", lostJob.state, "driving")
H.step(lostJob, {adActive = true, distance = 40, speed = 5}, 500, false)
H.step(lostJob, {adActive = false, distance = 40, speed = 0}, 500, false)
check("job: AD stoppede langt fra markøren", lostJob.state, "failed")
check("job: årsag stoppet undervejs", tostring(lostJob.reason):find("^stoppedAway") ~= nil, true)
-- tur: "Connect Bredal > Start" -> to markørnavne
local route = H.parseTrip("Connect", "Bredal", ">", "Start", ">", "Afkobl", "Bredal", ">", "Start")
check("tur: antal trin", #route, 4)
check("tur: første", route[1], "Connect Bredal")
check("tur: tredje", route[3], "Afkobl Bredal")
check("tur: ét trin", #H.parseTrip("Start"), 1)
check("tur: tomme trin droppes", #H.parseTrip("Start", ">", ">", "Stop"), 2)
-- trin: handling pr. markør; almindelige markører = ankom (og vent, hvis ikke sidste)
check("trin: connect", H.stepAction("Connect Bredal"), "attach")
check("trin: afkobl", H.stepAction("Afkobl Bredal"), "detach")
check("trin: almindelig", H.stepAction("Start"), "arrive")
check("vent: mellem trin", H.waitAfter("arrive", 2, 4), H.ROUTE_WAIT_MS)
check("vent: ikke efter sidste", H.waitAfter("arrive", 4, 4), 0)
check("vent: ikke efter kobling", H.waitAfter("attach", 1, 4), 0)
local arriveJob = H.newJob(1, "Start", "arrive")
H.step(arriveJob, {adActive = false, distance = 1, speed = 0}, 250, false)
H.step(arriveJob, {adActive = false, distance = 1, speed = 0}, 250, true)
check("trin: ankom er færdig", arriveJob.state, "done")
local markers = {{name = "Start", id = 1}, {name = "Connect Bredal", id = 9}}
check("markør: navn uden hensyn til store/små", H.findMarker(markers, "connect bredal").id, 9)
check("markør: ukendt", H.findMarker(markers, "Stald"), nil)
-- Connect = kun kobl på; i en tur kobles vognen af ved sin egen AD-parkering
check("connect: kobl på", H.resolveAction("attach", false), "attach")
check("connect med vogn: stadig kobl på (ingen vending)", H.resolveAction("attach", false), "attach")
check("vognens parkering: kobl af", H.resolveAction("arrive", true), "detach")
check("almindelig markør", H.resolveAction("arrive", false), "arrive")
check("afkobl forbliver afkobl", H.resolveAction("detach", false), "detach")
-- hjemtur: hvert puljeredskabs parkering (afkobles der), så traktorens egen parkering
local home = H.homeRoute({4}, 2)
check("hjem: to trin", #home, 2); check("hjem: redskab først", home[1], 4); check("hjem: traktor sidst", home[2], 2)
check("hjem: uden redskab kun traktor", #H.homeRoute({}, 2), 1)
check("hjem: samme parkering to gange tæller én", #H.homeRoute({4, 4}, 2), 2)
check("hjem: uden traktorparkering", #H.homeRoute({4}, nil), 1)
-- afbrydelse: et hente-job kan annulleres (Stop alt, høst slut, spilleren overtager)
ADFieldWork = ADFieldWork or {startAd = function() end}
Logging = Logging or {info = function() end, warning = function() end}
local stubEnv = {ADGraphManager = {getMapMarkerByWayPointId = function() return {name = "Connect X"} end}, AutoDrive = {MODE_DRIVETO = 1}}
local tractorStub = {getFullName = function() return "T" end, stopAutoDrive = function(v) v.stopped = true end}
H.fetch(stubEnv, tractorStub, 9, function() end)
check("annullér: job fandtes", H.cancel(tractorStub), true)
check("annullér: AD stoppet", tractorStub.stopped, true)
check("annullér: intet job tilbage", H.cancel(tractorStub), false)
-- afkobling må tage længere tid (redskabet skal måske foldes ud først)
local foldJob = H.newJob(4, "Plov", "detach")
H.step(foldJob, {adActive = false, distance = 1, speed = 0}, 500, false)
for _ = 1, 16 do H.step(foldJob, {adActive = false, distance = 1, speed = 0}, 500, false) end
check("afkobling: stadig i gang efter 8 s (folder ud)", foldJob.state, "acting")
for _ = 1, 30 do H.step(foldJob, {adActive = false, distance = 1, speed = 0}, 500, false) end
check("afkobling: opgivet efter sin frist", foldJob.state, "failed")
-- afkobling bekræftes: først "done" når intet sidder bag traktoren efter et forsøg (in-game 29/9: loggede
-- "koblede af", men vognen sad der stadig, og traktoren kørte med den)
check("afkobl: redskab bagpå -> forsøg", H.detachStep(1, false), "attempt")
check("afkobl: forsøgt, sidder der stadig -> forsøg igen", H.detachStep(1, true), "attempt")
check("afkobl: forsøgt, intet bagpå -> færdig", H.detachStep(0, true), "done")
check("afkobl: intet bagpå fra start", H.detachStep(0, false), "nothing")
-- transport ved stop: sluk, løft, fold (in-game 29/9: redskaber kørte udfoldet hjem efter stop)
check("transport: tændt+sænket+udfoldet", table.concat(H.stowActions({turnedOn = true, lowered = true, unfolded = true, canFold = true}), ","), "turnOff,raise,fold")
check("transport: allerede klar", #H.stowActions({}), 0)
check("transport: kan ikke foldes -> kun sluk/løft", table.concat(H.stowActions({turnedOn = true, lowered = true, unfolded = true, canFold = false}), ","), "turnOff,raise")
check("transport: kun udfoldet", table.concat(H.stowActions({unfolded = true, canFold = true}), ","), "fold")
check("vagt: stoppedAway med afstand", H.step(H.newJob(1, "Connect x", "attach"), {adActive = false, distance = 37.4, speed = 0}, 4000, false).reason, "stoppedAway (37 m fra markøren)")
-- manuel AD "Kør til": markørnavn først, ellers puljeredskabets egen parkering = kobl af (fast sæt røres ikke)
check("manuel: Connect-markør", H.manualAction("attach", false), "attach")
check("manuel: puljeredskabets parkering", H.manualAction(nil, true), "detach")
check("manuel: almindelig markør", H.manualAction(nil, false), nil)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-26
View File
@@ -1,26 +0,0 @@
-- Kør: luajit tests/test_adHitchCreep.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adHitchCreep.lua")
local C = ADHitchCreep
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
-- plan: vognens trækøje i traktorens rum set fra krogen (x = til siden, z < 0 = bagved)
check("plan: inden for rummelig rækkevidde -> kobl direkte", C.plan({x = 0.4, z = -2.0}), "attach")
check("plan: 5 m lige bagved -> bak", C.plan({x = 0.3, z = -5.0}), "creep")
check("plan: lidt til siden -> bak", C.plan({x = -1.2, z = -6.0}), "creep")
check("plan: for langt til siden -> intet", C.plan({x = 2.5, z = -5.0}), "none")
check("plan: for langt bagved -> intet", C.plan({x = 0, z = -12.0}), "none")
check("plan: foran traktoren -> intet", C.plan({x = 0, z = 5.0}), "none")
check("plan: ingen vogn -> intet", C.plan(nil), "none")
-- skridt under bakningen
check("skridt: vognen inden for rækkevidde -> færdig", C.step({inRange = true, distance = 2.5, startDistance = 6, elapsedMs = 3000}), "done")
check("skridt: på vej -> kør", C.step({inRange = false, distance = 4, startDistance = 6, elapsedMs = 3000}), "drive")
check("skridt: for længe -> giv op", C.step({inRange = false, distance = 4, startDistance = 6, elapsedMs = C.CREEP_TIMEOUT_MS}), "giveUp")
check("skridt: kører væk fra vognen -> giv op", C.step({inRange = false, distance = 7.5, startDistance = 6, elapsedMs = 3000}), "giveUp")
check("skridt: forbi vognen uden kobling -> giv op", C.step({inRange = false, distance = 0.3, startDistance = 6, elapsedMs = 3000, passed = true}), "giveUp")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-79
View File
@@ -1,79 +0,0 @@
-- Kør: luajit tests/test_adLoadSwap.lua (fra repo-roden)
-- Ren rangering af aflæsnings-siloer (lagermål for udkørsel og læs-bytte).
dofile("FS25_ADSmartPickup/adPrices.lua")
dofile("FS25_ADSmartPickup/adLoadSwap.lua")
local L = ADLoadSwap
local failures = 0
local function check(name, actual, expected)
if actual == expected then
print("OK " .. name)
else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
-- LS1: markører et andet forløb HENTER fra (fx "Milk Storage Load") vælges sidst; ellers har-varen, så afstand
local candidates = {
{marker = {id = 1, name = "Milk Storage Load"}, hasFillType = true, distance = 100},
{marker = {id = 2, name = "Milk Storage unload"}, hasFillType = true, distance = 400},
{marker = {id = 3, name = "Silo tom"}, hasFillType = false, distance = 10},
}
local ranked = L.rankDumpCandidates(candidates, {[1] = true})
check("LS1 afhentningsmarkør undgås", ranked[1].marker.id, 2)
check("LS1 derefter tom silo før afhentning", ranked[2].marker.id, 3)
check("LS1 afhentning sidst (stadig mulig)", ranked[3].marker.id, 1)
local plain = L.rankDumpCandidates(candidates, nil)
check("LS1 uden undgå-liste: har varen + nærmest", plain[1].marker.id, 1)
-- LS2: lagermål — rene siloer altid; egne fabrikker der bruger varen som input kun når tilladt (udkørsel)
local SLURRY, MILK = 20, 21
g_currentMission = {getFarmId = function() return 1 end}
local biogas = {spec_productionPoint = {productionPoint = {productions = {{inputs = {{type = SLURRY, amount = 1}}}}}}, getOwnerFarmId = function() return 1 end}
local foreignBiogas = {spec_productionPoint = {productionPoint = {productions = {{inputs = {{type = SLURRY, amount = 1}}}}}}, getOwnerFarmId = function() return 0 end}
local silo = {spec_silo = {}}
local barn = {spec_silo = {}, getHusbandryFillLevel = function() return 0 end}
check("LS2 silo", L.isStoreTarget({owningPlaceable = silo}, SLURRY, nil, false), true)
check("LS2 stald aldrig", L.isStoreTarget({owningPlaceable = barn}, SLURRY, nil, true), false)
check("LS2 biogas kun når fabrikker tilladt", L.isStoreTarget({owningPlaceable = biogas}, SLURRY, nil, false), false)
check("LS2 biogas tager gylle", L.isStoreTarget({owningPlaceable = biogas}, SLURRY, nil, true), true)
check("LS2 biogas tager ikke mælk", L.isStoreTarget({owningPlaceable = biogas}, MILK, nil, true), false)
check("LS2 fremmed anlæg aldrig", L.isStoreTarget({owningPlaceable = foreignBiogas}, SLURRY, nil, true), false)
check("LS2 hjemmet selv aldrig", L.isStoreTarget({owningPlaceable = biogas}, SLURRY, {owningPlaceable = biogas}, true), false)
g_currentMission = nil
-- LS3: tvunget lagermål — kun de valgte markører, i rækkefølge; første der tager varen, har plads og kan nås
do
local stations = {
[101] = {accepts = {[SLURRY] = true}, free = 0}, -- BGA fuld
[102] = {accepts = {[SLURRY] = true}, free = 900000}, -- gylletank
[103] = {accepts = {[MILK] = true}, free = 900000}, -- tager ikke gylle
}
for _, st in pairs(stations) do
st.getIsFillTypeSupported = function(self, ft) return self.accepts[ft] == true end
st.getFreeCapacity = function(self) return self.free end
end
ADSmartPickup = {getFarmId = function() return 1 end, getUnloadStationAtWayPointId = function(_, id) return stations[id] end}
local adEnv = {ADGraphManager = {getMapMarkerByWayPointId = function(_, id) return {id = id, name = "m" .. id} end}}
local vehicle = {}
local all = function() return true end
check("LS3 fuld BGA springes over -> tank", L.findForcedDumpMarker(adEnv, vehicle, SLURRY, 20000, {101, 102}, all).id, 102)
stations[101].free = 500000
check("LS3 BGA med plads vælges først", L.findForcedDumpMarker(adEnv, vehicle, SLURRY, 20000, {101, 102}, all).id, 101)
local none, why = L.findForcedDumpMarker(adEnv, vehicle, SLURRY, 20000, {103}, all)
check("LS3 intet valgt mål tager varen -> nil", none, nil)
check("LS3 forklaring", why ~= nil and why:find("valgt", 1, true) ~= nil, true)
check("LS3 ikke nåelig -> næste", L.findForcedDumpMarker(adEnv, vehicle, SLURRY, 20000, {101, 102}, function(id) return id ~= 101 end).id, 102)
ADSmartPickup = nil
end
-- LQ: flydende varer lægges ikke automatisk i en fabrik (AIO-risten, in-game 26/9)
check("LQ gylle er flydende", ADLoadSwap.isLiquidName("LIQUIDMANURE"), true)
check("LQ digestat er flydende", ADLoadSwap.isLiquidName("digestate"), true)
check("LQ hvede er ikke flydende", ADLoadSwap.isLiquidName("WHEAT"), false)
check("LQ ukendt", ADLoadSwap.isLiquidName(nil), false)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-40
View File
@@ -1,40 +0,0 @@
-- Kør: luajit tests/test_adManualTransport.lua (fra repo-roden)
-- Manuel transport af brede høstere (in-game 27/9: mejetærskere med skærebord er for brede til vejene).
dofile("FS25_ADSmartPickup/adManualTransport.lua")
local M = ADManualTransport
local WHEAT, BARLEY = 1, 2
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
-- valg af høster
check("manuel høster uden skærebord kan vælges", M.acceptsHarvester({manual = true, fruits = {}}, WHEAT), true)
check("automatisk høster uden skærebord kan ikke", M.acceptsHarvester({fruits = {}}, WHEAT), false)
check("automatisk høster med passende skærebord", M.acceptsHarvester({fruits = {[WHEAT] = true}}, WHEAT), true)
check("ukendt afgrøde -> ingen", M.acceptsHarvester({manual = true}, nil), false)
-- in-game 27/9: F111 (hvede) valgte roeoptageren, fordi manuelle høstere slap for skærebord-tjekket
check("manuel roeoptager (fast optager) kan ikke høste hvede", M.acceptsHarvester({manual = true, fruits = {[BARLEY] = true}}, WHEAT), false)
check("manuel mejetærsker med passende skærebord monteret", M.acceptsHarvester({manual = true, fruits = {[WHEAT] = true}}, WHEAT), true)
-- klar til overtagelse
local header = {[WHEAT] = true}
local function ready(s) local ok, why = M.readiness(s); return ok and "ok" or why end
check("hjemme -> notThere", ready({atField = false, fruits = {}, fruit = WHEAT}), "notThere")
check("på marken uden skærebord -> noHeader", ready({atField = true, fruits = {}, fruit = WHEAT}), "noHeader")
check("forkert skærebord -> wrongHeader", ready({atField = true, fruits = {[BARLEY] = true}, fruit = WHEAT}), "wrongHeader")
check("spilleren sidder i -> playerInside", ready({atField = true, fruits = header, fruit = WHEAT, controlled = true}), "playerInside")
check("klar", ready({atField = true, fruits = header, fruit = WHEAT}), "ok")
check("Start nu: spiller i og ikke på marken -> ok", ready({atField = false, fruits = header, fruit = WHEAT, controlled = true, forced = true}), "ok")
check("Start nu tjekker stadig skærebordet", ready({atField = false, fruits = {}, fruit = WHEAT, forced = true}), "noHeader")
-- påmindelser
check("første gang -> påmind", M.shouldRemind(nil, 1000), true)
check("for tidligt", M.shouldRemind(1000, 1000 + M.REMIND_MS - 1), false)
check("efter 5 min", M.shouldRemind(1000, 1000 + M.REMIND_MS), true)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-86
View File
@@ -1,86 +0,0 @@
-- Kør: luajit tests/test_adMonthView.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFieldModel.lua")
dofile("FS25_ADSmartPickup/adMonthView.lua")
local V, M = ADMonthView, ADFieldModel
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
-- MV1: perioder
check("MV1 næste periode", V.nextPeriod(5), 6)
check("MV1 vinter -> forår", V.nextPeriod(12), 1)
-- MV2: såvindue (hvede sås i periode 6-7)
local function plantableIn(name, period) return name == "WHEAT" and (period == 6 or period == 7) end
check("MV2 lukker", V.sowWindow(plantableIn, "WHEAT", 7), "closes")
check("MV2 åbent", V.sowWindow(plantableIn, "WHEAT", 6), "open")
check("MV2 lukket", V.sowWindow(plantableIn, "WHEAT", 8), "closed")
-- MV3: visning (Dakota-kartofler: harvestReady 6 -> dead 7 i EARLY_AUTUMN = periode 7)
local potato = {witheredState = 7, growthDataSeasonal = {periods = {[7] = {updates = {{startState = 6, endState = 7}}}}}}
check("MV3 visner næste måned", V.withersIn(potato, 7, 6), true)
check("MV3 ikke måneden før", V.withersIn(potato, 6, 6), false)
check("MV3 ukendte data", V.withersIn({witheredState = 7}, 7, 6), false)
-- MV4: fremdrift
local harvest = M.with(M.newField(15), {stage = "harvesting", kind = "harvest"})
check("MV4 CP vinder uden arealtal", V.progress(harvest, {readyRatio = 0.9}, 0.5), 0.5)
-- in-game 27/9 F90: halvt slået græs viste 0 % — slået græs tæller ikke i readyRatio's nævner
check("MV4 slåning ud fra hele arealet", V.progress(M.with(harvest, {stage = "mowing"}), {readyRatio = 1, standingRatio = 0.5}, nil), 0.5)
check("MV4 areal slår CP (kursus genstartet = 0)", V.progress(harvest, {readyRatio = 1, standingRatio = 0.4}, 0), 0.6)
check("MV4 pleje bruger stadig CP", V.progress(M.with(harvest, {stage = "fertilizing"}), {standingRatio = 0.4}, 0.3), 0.3)
check("MV4 høst ud fra afgrøde", V.progress(harvest, {readyRatio = 0.25}, nil), 0.75)
check("MV4 kultivering ukendt", V.progress(M.with(harvest, {stage = "cultivating"}), {readyRatio = 0}, nil), nil)
check("MV4 tekst", V.percentText(0.504), "50 %")
check("MV4 tekst ukendt", V.percentText(nil), "–")
-- MV5: hel fane
local fields = {
[15] = M.with(harvest, {plan = {sow = true, sowFruit = "WHEAT", soil = "cultivate", straw = "chop"}}),
[16] = M.with(M.newField(16), {stage = "harvesting", kind = "harvest"}),
[18] = M.with(M.newField(18), {stage = "sowing", kind = "harvest", plan = {sow = true, sowFruit = "WHEAT"}}),
[20] = M.with(M.newField(20), {stage = "fertilizing"}),
[21] = M.newField(21),
[22] = M.with(M.newField(22), {plan = {targetFruit = "WHEAT"}}),
[23] = M.with(M.newField(23), {stage = "ready"}),
}
local infos = {
{id = 15, kind = "harvest", readyRatio = 0.5, cropTitle = "Hvede", needs = {}},
{id = 16, kind = "harvest", readyRatio = 1, cropTitle = "Kartofler", growthState = 6},
{id = 18, kind = "bare", readyRatio = 0},
{id = 20, kind = "harvest", readyRatio = 0},
{id = 21, kind = "bare"},
{id = 22, kind = "bare"},
{id = 23, kind = "harvest", readyRatio = 1},
}
local ctx = {period = 7, plantableIn = plantableIn,
withers = function(info, nextPeriod) return info.id == 16 and nextPeriod == 8 end,
cpProgress = function(id) return id == 18 and 0.1 or nil end}
local view = V.build(infos, fields, ctx)
check("MV5 aktive marker (21 venter, 22 kommer, 23 passiv klar)", #view.rows, 4)
check("MV5 kritiske", view.critical, 3)
check("MV5 høst før sidste såmåned først (id)", view.rows[1].id, 15)
check("MV5 høst -> såning senere lukker", view.rows[1].window, "lastSowMonth")
check("MV5 høst næste trin", view.rows[1].nextStage, "cultivating")
check("MV5 høst der visner", view.rows[2].id, 16)
check("MV5 vindue høst", view.rows[2].window, "harvestCloses")
check("MV5 sidste såmåned", view.rows[3].id, 18)
check("MV5 vindue såning", view.rows[3].window, "lastSowMonth")
check("MV5 såafgrøde", view.rows[3].sowFruit, "WHEAT")
check("MV5 CP-fremdrift", view.rows[3].progress, 0.1)
check("MV5 pleje sidst", view.rows[4].id, 20)
check("MV5 pleje intet næste", view.rows[4].nextStage, nil)
check("MV5 kommer: F22 hvede", view.upcoming[1] and view.upcoming[1].id, 22)
check("MV5 kommer: lukker", view.upcoming[1] and view.upcoming[1].closes, true)
-- MV6: klargøring før såning med lukkende vindue er kritisk
local cultivating = M.with(M.newField(30), {stage = "cultivating", plan = {sow = true, sowFruit = "WHEAT"}})
check("MV6 kultivering før sidste såmåned", V.window(cultivating, {kind = "bare"}, ctx), "lastSowMonth")
check("MV6 aktiv klar (auto)", V.isActive(M.with(M.newField(31), {stage = "ready", plan = {mode = "auto"}})), true)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-20
View File
@@ -1,20 +0,0 @@
-- Kør: luajit tests/test_adNames.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adNames.lua")
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
local byId = ADNames.fromRecords({{id = "placeableA", name = "Gårdsilo 10"}, {id = "", name = "x"}, {id = "placeableB", name = " "}, {id = "placeableC"}})
check("navn indlæst", byId.placeableA, "Gårdsilo 10")
check("tomt id springes over", byId[""], nil)
check("blankt navn springes over", byId.placeableB, nil)
check("manglende navn springes over", byId.placeableC, nil)
ADNames.byId = byId
check("opslag på bygning", ADNames.forPlaceable({uniqueId = "placeableA"}), "Gårdsilo 10")
check("bygning uden eget navn", ADNames.forPlaceable({uniqueId = "placeableZ"}), nil)
check("uden uniqueId", ADNames.forPlaceable({}), nil)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-49
View File
@@ -1,49 +0,0 @@
-- Kør: luajit tests/test_adNaming.lua (fra repo-roden)
-- Automatiske navne på bygninger uden eget navn (in-game 27/9: "36 Farma" hed alle "Farma 500 + Obi 1000").
dofile("FS25_ADSmartPickup/adNaming.lua")
local N = ADNaming
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
-- næste ledige nummer
check("næste efter 1-5", N.nextNumber({"Svinestald 1", "Svinestald 5", "Kostald 7"}, "Svinestald"), 6)
check("ingen endnu -> 1", N.nextNumber({"Kostald 1"}, "Svinestald"), 1)
check("prefix-præfiks forveksles ikke", N.nextNumber({"Gårdsilo ekstra ved 1", "Gårdsilo 3"}, "Gårdsilo"), 4)
-- navne efter type
local function one(b, existing) return N.assign({b}, existing or {})[b.id] end
check("svinestald", one({id = "a", kind = "husbandry", animal = "PIG"}, {x = "Svinestald 5"}), "Svinestald 6")
check("gedestald", one({id = "a", kind = "husbandry", animal = "GOAT"}), "Gedestald 1")
check("ukendt dyr -> Stald", one({id = "a", kind = "husbandry", animal = "LLAMA"}), "Stald 1")
check("silo i gårdens klynge", one({id = "a", kind = "silo", inFarmCluster = true}, {x = "Gårdsilo 35"}), "Gårdsilo 36")
check("silo ved mark", one({id = "a", kind = "silo", nearestField = {name = "F118", distance = 80}}), "F118 marksilo")
check("anden marksilo ved samme mark", one({id = "a", kind = "silo", nearestField = {name = "F118", distance = 80}}, {x = "F118 marksilo"}), "F118 marksilo 2")
check("silo langt fra alt", one({id = "a", kind = "silo", nearestField = {name = "F1", distance = 900}}), "Silo 1")
check("palleskur", one({id = "a", kind = "objectStorage"}, {x = "Palleskur 1"}), "Palleskur 2")
check("fabrik: butiksnavnet", one({id = "a", kind = "factory", storeName = "Sugarbeet Crusher"}), "Sugarbeet Crusher")
check("fabrik nr. 2 med samme navn", one({id = "a", kind = "factory", storeName = "Roesnitter"}, {x = "Roesnitter"}), "Roesnitter 2")
check("salgssted navngives ikke", one({id = "a", kind = "sell", storeName = "Mølle"}), nil)
check("har allerede navn -> urørt", N.assign({{id = "a", kind = "husbandry", animal = "PIG"}}, {a = "Min stald"}).a, nil)
-- flere nye i samme runde får hver sit nummer
local two = N.assign({{id = "a", kind = "husbandry", animal = "COW"}, {id = "b", kind = "husbandry", animal = "COW"}}, {})
check("to nye kostalde: 1 og 2", (two.a or "") .. "|" .. (two.b or ""), "Kostald 1|Kostald 2")
-- markmaskiner der stadig har modelnavnet (flow-traktorer navngives af flowet)
check("læssevogn nr. 5", N.fleetName("collector", nil, {"Læssevogn 1", "Læssevogn 4"}), "Læssevogn 5")
check("skårlægger", N.fleetName("mower", nil, {}), "Skårlægger 1")
check("høster: roeoptager", N.fleetName("harvester", "beet", {"Roeoptager 1"}), "Roeoptager 2")
check("høster: finsnitter", N.fleetName("harvester", "forage", {}), "Finsnitter 1")
check("høster: mejetærsker", N.fleetName("harvester", nil, {"Mejetærsker"}), "Mejetærsker 1")
check("tømmevogn", N.fleetName("unloader", nil, {}), "Tømmevogn 1")
check("gødningsspreder", N.fleetName("fertilizer", nil, {}), "Gødningsspreder 1")
check("ukendt rolle -> nil", N.fleetName("mystery", nil, {}), nil)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-646
View File
@@ -1,646 +0,0 @@
-- Kør: luajit tests/test_adOutbound.lua (fra repo-roden)
-- Udkørsels-hooks: turvalg + AD-tilstand, hold ved Wait, objektlager-udtag, opsamlingsrunde,
-- advarsler på skærmen. Fake AD-miljø; adSources/adPrices/adLoadSwap er fakes, adWaitPool og
-- adOutboundPlanner er de rigtige.
local SUGAR, FORAGE, BASKET, CRATE = 5, 6, 7, 8
local NAMES = {[SUGAR] = "SUGAR", [FORAGE] = "FORAGE", [BASKET] = "STRAW_BASKET", [CRATE] = "STRAW_CRATE"}
g_fillTypeManager = {
getFillTypeNameByIndex = function(_, i) return NAMES[i] end,
getFillTypeByIndex = function(_, i) return {name = NAMES[i], palletFilename = (i == BASKET or i == CRATE) and "p.xml" or nil} end,
}
local logLines = {}
Logging = {
info = function(fmt, ...) table.insert(logLines, string.format(fmt, ...)) end,
warning = function(fmt, ...) table.insert(logLines, "WARN " .. string.format(fmt, ...)) end,
}
getWorldTranslation = function(node) return node.x, 0, node.z end
g_currentMission = {getFarmId = function() return 1 end}
dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")
dofile("FS25_ADSmartPickup/adRuns.lua")
dofile("FS25_ADSmartPickup/adWaitPool.lua")
dofile("FS25_ADSmartPickup/adOutboundPlanner.lua")
dofile("FS25_ADSmartPickup/adOutbound.lua")
local failures = 0
local function check(name, actual, expected)
if actual == expected then
print("OK " .. name)
else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
local function lastLog(pattern)
for i = #logLines, 1, -1 do
if string.find(logLines[i], pattern, 1, true) then return logLines[i] end
end
return nil
end
local function countLog(pattern)
local n = 0
for _, line in ipairs(logLines) do if string.find(line, pattern, 1, true) then n = n + 1 end end
return n
end
-- ---- AD-miljø ----
local markers, wayPoints, unreachable = {}, {}, {}
local function addMarker(id, name, group, x, z)
wayPoints[id] = {id = id, x = x, z = z}
table.insert(markers, {id = id, markerIndex = #markers + 1, name = name, group = group})
return markers[#markers]
end
local notifications = {}
local RT_NONE, RT_ONLYPICKUP = 1, 2
local adEnv = {
AutoDrive = {
RT_NONE = RT_NONE, RT_ONLYPICKUP = RT_ONLYPICKUP,
getAllUnits = function(vehicle) return vehicle.trailers end,
getAllFillLevels = function(trailers) return trailers.level, trailers.capacity, trailers.level >= trailers.capacity * 0.99, trailers.capacity - trailers.level end,
getSupportedFillTypesOfAllUnitsAlphabetically = function(vehicle) return vehicle.carriable end,
hasAL = function(_, trailer) return trailer.ual ~= nil end,
activateALTrailers = function(vehicle, trailers) trailers.activated = (trailers.activated or 0) + 1 end,
},
ADGraphManager = {
getMapMarkers = function() return markers end,
getMapMarkerByWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id then return m end end end,
getWayPointById = function(_, id) return wayPoints[id] end,
getPathTo = function(_, vehicle, id) if unreachable[id] then return {} end return {{id = id}} end,
pathFromTo = function(_, from, to) if unreachable[to] then return {} end return {{id = to}} end,
},
AutoDriveMessageEvent = {sendMessageOrNotification = function(vehicle, kind, text, duration, ...) table.insert(notifications, {vehicle = vehicle, kind = kind, text = string.format(text, ...)}) end},
ADMessagesManager = {messageTypes = {INFO = 1, WARN = 2, ERROR = 3}},
LoadAtDestinationTask = {STATE_DRIVING = 2, update = function() end, finished = function(self) self.finishedCalls = (self.finishedCalls or 0) + 1 end},
UnloadAtDestinationTask = {STATE_DRIVING = 2, update = function() end},
}
local function newVehicle(name, x, z, capacity, ual)
local v = {name = name, rootNode = {x = x, z = z}, lastSpeedReal = 0, carriable = {SUGAR, FORAGE}, farmId = 1}
v.getName = function() return name end
v.trailers = {level = 0, capacity = capacity, ual = ual and {spec = {loadedObjects = {}}} or nil}
v.trailers[1] = {spec_universalAutoload = ual and v.trailers.ual.spec or nil, ualIsFull = ual and function() return v.trailers.full == true end or nil, ual = ual and true or nil}
v.ad = {
stateModule = {isActive = function() return v.active ~= false end, selectedFillTypes = {}, fillType = nil,
setFillType = function(self, ft) self.fillType = ft end, getFillType = function(self) return self.fillType end,
setSecondMarkerByWayPointId = function(self, id) self.secondMarkerId = id end,
getSecondMarker = function(self) return self.secondMarkerId and {id = self.secondMarkerId} or nil end,
raiseDirtyFlag = function() end},
drivePathModule = {isTargetReached = function() return v.atTarget == true end, setPathTo = function(_, id) v.pathTo = id end},
specialDrivingModule = {stopVehicle = function() v.stopped = true end, update = function() end},
trailerModule = {isActiveAtTrigger = function() return false end},
}
return v
end
local function newTask(vehicle, destinationID)
return {vehicle = vehicle, destinationID = destinationID, trailers = vehicle.trailers, state = 2, activatedUALLoading = false,
finished = function(self) return adEnv.LoadAtDestinationTask.finished(self) end}
end
-- kilder, priser, siloer som fakes
local sources, products, quotes, dumpMarker = {}, {}, {}, nil
ADSources = {
identify = function(_, id) return sources[id] end,
getProducts = function(_, source) return products[source.wayPointId] or {} end,
getSellMarkers = function() local r = {} for _, m in ipairs(markers) do if m.group == "selling" then table.insert(r, {marker = m, station = {name = m.name}}) end end return r end,
stationAccepts = function(station, ft) return quotes[station.name] ~= nil and quotes[station.name][ft] ~= nil end,
getSourcePlaceables = function() return {} end,
getObjectInfoIndex = function(source, ft) local s = source.stored or {} return s[ft] ~= nil and s[ft] > 0 and 1 or nil, s[ft] end,
countStored = function(source, ft) return (source.stored or {})[ft] or 0 end,
spawnOne = function(source, index) source.spawnCalls = (source.spawnCalls or 0) + 1 return source.spawnFails ~= true end,
}
ADPrices = {
getQuote = function(station, ft) return quotes[station.name] and quotes[station.name][ft] end,
bestQuote = function(list) local best for _, q in ipairs(list) do if best == nil or q.percent > best.percent then best = q end end return best end,
}
ADLoadSwap = {findDumpMarker = function() return dumpMarker, "ingen silo" end}
local runs = {}
ADRunsController = {getRunForVehicle = function(vehicle) return runs[vehicle] end}
local activePickupCalls = {}
ADSmartPickup = {getFarmId = function(v) return v.farmId end, setActivePickup = function(vehicle, choice) table.insert(activePickupCalls, choice) end,
getUnloadStationAtWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id and (m.group == "silo" or m.group == "selling") then return {name = m.name} end end end}
local mSugar = addMarker(1, "Suger Load", "Pickup points", 0, 0)
local mSilo = addMarker(2, "silo 4", "silo", 50, 0)
local mObj = addMarker(3, "Objektlager", "Pickup points", 100, 0)
local mSell = addMarker(10, "selling station", "selling", 200, 0)
local mSell2 = addMarker(11, "Far station", "selling", 300, 0)
local mWait = addMarker(20, "Wait 3", "General Wait", 20, 0)
local mStore = addMarker(30, "silo 6", "silo", 60, 0)
sources[1] = {kind = "factory", wayPointId = 1, station = {name = "factoryStation"}, name = "Sugar mill"}
sources[2] = {kind = "silo", wayPointId = 2, station = {name = "siloStation"}, name = "Farma"}
sources[3] = {kind = "objectStorage", wayPointId = 3, station = nil, name = "Objektlager", stored = {[BASKET] = 3, [CRATE] = 1}}
products[1] = {{fillType = SUGAR, name = "SUGAR", available = 5000, level = 5000, capacity = 180000, isPallet = false}}
products[2] = {{fillType = SUGAR, name = "SUGAR", available = 9000, level = 9000, capacity = 12000000, isPallet = false}}
products[3] = {{fillType = BASKET, name = "STRAW_BASKET", available = 3000, level = 4, capacity = 4000, isPallet = true}, {fillType = CRATE, name = "STRAW_CRATE", available = 1000, level = 4, capacity = 4000, isPallet = true}}
quotes["selling station"] = {[SUGAR] = {percent = 92, price = 480}, [BASKET] = {percent = 95, price = 1}, [CRATE] = {percent = 95, price = 1}}
quotes["Far station"] = {[SUGAR] = {percent = 99, price = 600}}
local function outboundRun(id, sourceIds, productsList, overrides)
local run = {id = id, name = "Out " .. id, kind = "outbound", waitPoolGroup = "", sourceWayPointIds = sourceIds, products = productsList, sellAtPercent = 90, storeAbovePercent = 80, collectRound = false}
for k, v in pairs(overrides or {}) do run[k] = v end
return run
end
-- O1: tur -> AD's varevalg og 2. markør sat, choice = kilden; næstbedste salgssted når bedste er uopnåeligt
unreachable[11] = true
local v1 = newVehicle("MT635", 0, 0, 20000)
runs[v1] = outboundRun(1, {1}, {{fillType = "SUGAR", policy = "sell"}})
check("O1 er udkørsel", ADOutbound.isOutboundVehicle(v1), true)
local choice = ADOutbound.choosePickup(adEnv, v1)
check("O1 kilde-markør", choice.markerIndex, mSugar.markerIndex)
check("O1 station med (bulk-greb)", choice.station.name, "factoryStation")
check("O1 vare", v1.ad.stateModule.fillType, SUGAR)
check("O1 varevalg", #v1.ad.stateModule.selectedFillTypes, 1)
check("O1 2. markør = nåeligt salgssted", v1.ad.stateModule.secondMarkerId, 10)
check("O1 ingen advarsel (næstbedste kunne nås)", #notifications, 0)
check("O1 log", lastLog("udkørsel") ~= nil, true)
local trip = ADOutbound.getTripFor(v1)
check("O1 trip kind", trip.kind, "sell")
unreachable[11] = nil
-- O2: ingen tur -> Wait-markør reserveret, hold registreret
local v2 = newVehicle("ARION", 25, 0, 20000)
runs[v2] = outboundRun(2, {1}, {{fillType = "SUGAR", policy = "sell"}})
quotes["selling station"][SUGAR].percent = 70
quotes["Far station"][SUGAR].percent = 60
local choice2 = ADOutbound.choosePickup(adEnv, v2)
check("O2 Wait-markør", choice2.markerIndex, mWait.markerIndex)
check("O2 uden station", choice2.station, nil)
check("O2 reserveret", ADWaitPool.getOwner(20), v2)
local hold = ADOutbound.getHoldFor(v2)
check("O2 hold", hold.markerName, "Wait 3")
check("O2 begrundelse", hold.reasons[1], "SUGAR 70 % af top (kræver 90), lager 3 %")
check("O2 run-status", ADOutbound.getRunStatus(2).reasons[1], hold.reasons[1])
check("O2 hold-log én gang", countLog("holder ved Wait 3"), 1)
ADOutbound.choosePickup(adEnv, v2)
check("O2 samme begrundelse logges ikke igen", countLog("holder ved Wait 3"), 1)
-- O3: holder ved Wait: vognen stoppes; efter 5 s med tur -> omdirigeret til kilden, hold sluppet
local task2 = newTask(v2, 20)
v2.atTarget = true
check("O3 håndteret", ADOutbound.beforeLoadUpdate(adEnv, task2, 1000), true)
check("O3 stoppet", v2.stopped, true)
quotes["selling station"][SUGAR].percent = 92
ADOutbound.beforeLoadUpdate(adEnv, task2, 4500)
check("O3 omdirigeret til kilden", task2.destinationID, 1)
check("O3 sti sat", v2.pathTo, 1)
check("O3 hold væk", ADOutbound.getHoldFor(v2), nil)
check("O3 reservation frigivet", ADWaitPool.getOwner(20), nil)
check("O3 activePickup opdateret", activePickupCalls[#activePickupCalls].station.name, "factoryStation")
check("O3 UAL-flag nulstillet", task2.activatedUALLoading, false)
v2.atTarget = false
check("O3 undervejs ikke håndteret", ADOutbound.beforeLoadUpdate(adEnv, task2, 100), false)
-- O4: objektlager: én palle ad gangen; stored falder + loaded stiger -> næste; ualIsFull -> finished
local v3 = newVehicle("Fendt", 100, 0, 8000, true)
runs[v3] = outboundRun(3, {3}, {{fillType = "STRAW_BASKET", policy = "sell"}, {fillType = "STRAW_CRATE", policy = "sell"}}, {collectRound = true})
local choice3 = ADOutbound.choosePickup(adEnv, v3)
check("O4 kilde = objektlager", choice3.markerIndex, mObj.markerIndex)
check("O4 kun én vare valgt ad gangen ved objektlager", #v3.ad.stateModule.selectedFillTypes, 1)
check("O4 turen har begge varer", #ADOutbound.getTripFor(v3).fillTypes, 2)
local task3 = newTask(v3, 3)
v3.atTarget = true
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
check("O4 intet spawn før UAL er aktiveret", sources[3].spawnCalls, nil)
task3.activatedUALLoading = true
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
check("O4 første spawn", sources[3].spawnCalls, 1)
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
check("O4 venter på pallen", sources[3].spawnCalls, 1)
sources[3].stored[BASKET] = 2
v3.trailers.ual.spec.loadedObjects[{}] = 1
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
check("O4 andet spawn efter læsning", sources[3].spawnCalls, 2)
sources[3].stored[BASKET] = 0
v3.trailers.ual.spec.loadedObjects[{}] = 1
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
check("O4 skifter til kasser når kurvene er væk", v3.ad.stateModule.fillType, CRATE)
check("O4 UAL genaktiveret ved vareskift", v3.trailers.activated, 1)
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
check("O4 tredje spawn (kasse)", sources[3].spawnCalls, 3)
v3.trailers.full = true
sources[3].stored[CRATE] = 0
v3.trailers.ual.spec.loadedObjects[{}] = 1
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
check("O4 fuld -> finished", task3.finishedCalls, 1)
-- O5: spawn uden fremdrift i 30 s -> finished + WARN
local v4 = newVehicle("Valtra", 100, 0, 8000, true)
runs[v4] = outboundRun(4, {3}, {{fillType = "STRAW_BASKET", policy = "sell"}})
sources[3].stored[BASKET] = 5
sources[3].spawnCalls = 0
ADOutbound.choosePickup(adEnv, v4)
local task4 = newTask(v4, 3)
v4.atTarget, task4.activatedUALLoading = true, true
ADOutbound.beforeLoadUpdate(adEnv, task4, 100)
check("O5 spawn kaldt", sources[3].spawnCalls, 1)
ADOutbound.beforeLoadUpdate(adEnv, task4, 31000)
ADOutbound.beforeLoadUpdate(adEnv, task4, 100)
check("O5 finished efter stall", task4.finishedCalls, 1)
check("O5 WARN", lastLog("spawner ikke") ~= nil, true)
-- O6: opsamlingsrunde, bulk: kilde A fylder vognen -> finished uden redirect; kilde A giver 30 % -> videre til B
local v5 = newVehicle("Claas", 0, 0, 10000)
runs[v5] = outboundRun(5, {1, 2}, {{fillType = "SUGAR", policy = "sell"}}, {collectRound = true})
local choice5 = ADOutbound.choosePickup(adEnv, v5)
check("O6 mest først = silo 4", choice5.markerIndex, mSilo.markerIndex)
local task5 = newTask(v5, 2)
v5.trailers.level = 10000
check("O6 fuld: ingen runde", ADOutbound.beforeLoadFinished(adEnv, task5), false)
local v6 = newVehicle("Claas2", 0, 0, 10000)
runs[v6] = outboundRun(6, {1, 2}, {{fillType = "SUGAR", policy = "sell"}}, {collectRound = true})
ADOutbound.choosePickup(adEnv, v6)
local task6 = newTask(v6, 2)
v6.trailers.level = 3000
check("O6 plads tilbage: videre til næste kilde", ADOutbound.beforeLoadFinished(adEnv, task6), true)
check("O6 destination = Suger Load", task6.destinationID, 1)
check("O6 activePickup = fabrik", activePickupCalls[#activePickupCalls].station.name, "factoryStation")
check("O6 alle besøgt: afslut", ADOutbound.beforeLoadFinished(adEnv, task6), false)
-- O7: intet nåeligt salgssted -> rød notifikation én gang
notifications = {}
unreachable[10], unreachable[11] = true, true
local v7 = newVehicle("Deutz", 0, 0, 20000)
runs[v7] = outboundRun(7, {1}, {{fillType = "SUGAR", policy = "sell"}})
local choice7 = ADOutbound.choosePickup(adEnv, v7)
check("O7 holder", ADOutbound.getHoldFor(v7) ~= nil, true)
check("O7 én notifikation", #notifications, 1)
check("O7 ERROR", notifications[1].kind, 3)
check("O7 tekst", string.find(notifications[1].text, "kan ikke nå salgssted", 1, true) ~= nil, true)
check("O7 status-advarsel", ADOutbound.getRunStatus(7).warning ~= nil, true)
ADOutbound.choosePickup(adEnv, v7)
check("O7 ikke gentaget", #notifications, 1)
unreachable[10] = nil
ADOutbound.choosePickup(adEnv, v7)
check("O7 tur når vejen er der", ADOutbound.getTripFor(v7) ~= nil, true)
check("O7 advarsel ryddet", ADOutbound.getRunStatus(7).warning, nil)
unreachable[11] = nil
-- O8: ingen silo med plads ved lagerbehov -> notifikation; med silo -> store-tur
notifications = {}
local v8 = newVehicle("JD", 0, 0, 20000)
runs[v8] = outboundRun(8, {2}, {{fillType = "SUGAR", policy = "store"}})
products[2][1].level, products[2][1].capacity = 11000000, 12000000
ADOutbound.choosePickup(adEnv, v8)
check("O8 advarsel ingen silo", #notifications, 1)
dumpMarker = mStore
ADOutbound.choosePickup(adEnv, v8)
check("O8 store-tur", ADOutbound.getTripFor(v8).kind, "store")
check("O8 mål = silo 6", v8.ad.stateModule.secondMarkerId, 30)
-- O9: releaseInactive rydder hold + reservation
local v9 = newVehicle("Case", 25, 0, 20000)
runs[v9] = outboundRun(9, {1}, {{fillType = "FORAGE", policy = "sell"}})
ADOutbound.choosePickup(adEnv, v9)
check("O9 reserveret", ADWaitPool.getOwner(20), v9)
v9.active = false
ADOutbound.releaseInactive()
check("O9 frigivet", ADWaitPool.getOwner(20), nil)
check("O9 hold væk", ADOutbound.getHoldFor(v9), nil)
-- O11 (I2): hold-markøren skifter (Wait-punkt bliver ledigt) -> opgaven omdirigeres
ADWaitPool.releaseVehicle(v9)
local v11 = newVehicle("Hold", 25, 0, 20000)
runs[v11] = outboundRun(11, {1}, {{fillType = "FORAGE", policy = "sell"}}) -- FORAGE findes ikke ved kilden -> aldrig tur
local blocker = newVehicle("Blocker", 0, 0, 1)
ADWaitPool.claim(20, blocker)
local choice11 = ADOutbound.choosePickup(adEnv, v11)
check("O11 uden ledigt Wait: holder ved kilden", choice11.markerIndex, mSugar.markerIndex)
local task11 = newTask(v11, 1)
v11.atTarget = true
ADOutbound.beforeLoadUpdate(adEnv, task11, 100)
ADWaitPool.releaseVehicle(blocker)
ADOutbound.beforeLoadUpdate(adEnv, task11, 5000)
check("O11 Wait-punkt ledigt -> omdirigeret", task11.destinationID, 20)
check("O11 hold peger på Wait", ADOutbound.getHoldFor(v11).markerName, "Wait 3")
ADWaitPool.releaseVehicle(v11)
-- O12 (I3): mål for last der allerede er på vognen ved Start
local v12 = newVehicle("Loaded", 0, 0, 20000)
runs[v12] = outboundRun(12, {1}, {{fillType = "SUGAR", policy = "sell"}})
quotes["selling station"][SUGAR].percent = 92
check("O12 salgsmål for sukker-last", ADOutbound.planCargoTarget(adEnv, runs[v12], v12, SUGAR), 10)
quotes["selling station"][SUGAR].percent = 50
check("O12 pris lav, ingen lager-politik -> nil", ADOutbound.planCargoTarget(adEnv, runs[v12], v12, SUGAR), nil)
runs[v12].products = {{fillType = "SUGAR", policy = "sellElseStore"}}
check("O12 pris lav -> silo", ADOutbound.planCargoTarget(adEnv, runs[v12], v12, SUGAR), 30)
check("O12 last der ikke er i run'et -> nil", ADOutbound.planCargoTarget(adEnv, runs[v12], v12, FORAGE), nil)
quotes["selling station"][SUGAR].percent = 92
-- O13 (I4): tom vogn 30 s ved kilden -> kilden udelukkes for varen og turen afsluttes
local v13 = newVehicle("Empty", 0, 0, 20000)
runs[v13] = outboundRun(13, {1, 2}, {{fillType = "SUGAR", policy = "sell"}}, {collectRound = false})
local choice13 = ADOutbound.choosePickup(adEnv, v13)
check("O13 første kilde = silo 4 (mest)", choice13.markerIndex, mSilo.markerIndex)
local task13 = newTask(v13, 2)
v13.atTarget = true
ADOutbound.beforeLoadUpdate(adEnv, task13, 100)
ADOutbound.beforeLoadUpdate(adEnv, task13, 31000)
check("O13 finished efter 30 s uden last", task13.finishedCalls, 1)
check("O13 WARN", lastLog("intet at læsse") ~= nil, true)
local choice13b = ADOutbound.choosePickup(adEnv, v13)
check("O13 næste plan udelukker den tomme kilde", choice13b.markerIndex, mSugar.markerIndex)
-- O16: kilden gav LIDT og så intet mere ("ingen mere last") -> kilden hviler, næste tur tager en anden
-- (in-game 26/9: æg-vognen kørte til Æg Pickup 4 tre gange i træk; én palle inden for rækkevidde, resten udenfor)
local v16 = newVehicle("Thin", 0, 0, 20000)
runs[v16] = outboundRun(16, {1, 2}, {{fillType = "SUGAR", policy = "sell"}}, {collectRound = false})
ADOutbound.resetEmptySources()
local choice16 = ADOutbound.choosePickup(adEnv, v16)
local first16 = choice16.markerIndex
local task16 = newTask(v16, choice16.markerIndex == mSilo.markerIndex and 2 or 1)
v16.atTarget = true
v16.trailers.level = 500
ADOutbound.beforeLoadUpdate(adEnv, task16, 100)
ADOutbound.beforeLoadUpdate(adEnv, task16, 16000)
check("O16 kører med det den fik", task16.finishedCalls, 1)
check("O16 log", lastLog("ingen mere last") ~= nil, true)
v16.trailers.level = 0
local choice16b = ADOutbound.choosePickup(adEnv, v16)
check("O16 næste tur tager en anden kilde", choice16b ~= nil and choice16b.markerIndex ~= first16, true)
ADOutbound.resetEmptySources() -- (senere tests forventer alle kilder ledige)
-- O14 (I6): rutetjek kun for salgssteder over tærsklen, bedste først, stop ved første nåelige
-- (rutetjekket går markør->markør: hjem -> salgssted tælles som ét tjek pr. salgssted)
local routeChecks = 0
local originalGetPathTo = adEnv.ADGraphManager.getPathTo
adEnv.ADGraphManager.getPathTo = function(_, vehicle, id) routeChecks = routeChecks + 1 return originalGetPathTo(_, vehicle, id) end
local originalFromTo = adEnv.ADGraphManager.pathFromTo
adEnv.ADGraphManager.pathFromTo = function(self, from, to)
if to == 10 or to == 11 then routeChecks = routeChecks + 1 end -- udturen til et salgssted
return originalFromTo(self, from, to)
end
ADWaitPool.resetMarkerRoutes()
local v14 = newVehicle("Routes", 0, 0, 20000)
runs[v14] = outboundRun(14, {1}, {{fillType = "SUGAR", policy = "sell"}})
quotes["Far station"][SUGAR].percent = 99
quotes["selling station"][SUGAR].percent = 92
ADOutbound.choosePickup(adEnv, v14)
check("O14 bedste nåelig først: én rute tjekket", routeChecks, 1)
check("O14 mål = bedst betalende", v14.ad.stateModule.secondMarkerId, 11)
routeChecks = 0
ADWaitPool.resetMarkerRoutes()
quotes["Far station"][SUGAR].percent = 60
ADOutbound.choosePickup(adEnv, v14)
check("O14 under tærsklen tjekkes ikke", routeChecks, 1)
adEnv.ADGraphManager.getPathTo = originalGetPathTo
-- O15 (M1): advarsel for vare A gentages ikke efter en tur med vare B
notifications = {}
local v15 = newVehicle("Two", 0, 0, 20000)
runs[v15] = outboundRun(15, {1, 2}, {{fillType = "SUGAR", policy = "sell"}, {fillType = "FORAGE", policy = "sell"}})
products[1] = {{fillType = SUGAR, name = "SUGAR", available = 5000, level = 5000, capacity = 180000, isPallet = false}, {fillType = FORAGE, name = "FORAGE", available = 5000, level = 5000, capacity = 180000, isPallet = false}}
quotes["selling station"][FORAGE] = {percent = 95, price = 1}
quotes["Far station"][SUGAR].percent = 99 -- (O14 satte den til 60)
unreachable[10] = true; unreachable[11] = true
ADOutbound.choosePickup(adEnv, v15) -- SUGAR uopnåelig -> advarsel, FORAGE uopnåelig -> advarsel
local firstCount = #notifications
unreachable[11] = nil -- Far station kan nås for SUGAR (FORAGE sælges kun på selling station)
ADOutbound.choosePickup(adEnv, v15) -- tur med SUGAR
check("O15 tur", ADOutbound.getTripFor(v15).kind, "sell")
ADOutbound.choosePickup(adEnv, v15) -- igen: FORAGE stadig uopnåelig, men allerede advaret
check("O15 FORAGE-advarsel gentages ikke", #notifications, firstCount)
unreachable[10] = nil
products[1] = {{fillType = SUGAR, name = "SUGAR", available = 5000, level = 5000, capacity = 180000, isPallet = false}}
-- O16 (M6): pallens ankomst ses også på fyldniveau når UAL's interne liste mangler
local v16 = newVehicle("NoList", 100, 0, 8000, true)
v16.trailers[1].spec_universalAutoload = {}
runs[v16] = outboundRun(16, {3}, {{fillType = "STRAW_BASKET", policy = "sell"}})
sources[3].stored[BASKET] = 3; sources[3].spawnCalls = 0
ADOutbound.choosePickup(adEnv, v16)
local task16 = newTask(v16, 3)
v16.atTarget, task16.activatedUALLoading = true, true
ADOutbound.beforeLoadUpdate(adEnv, task16, 100)
check("O16 første spawn", sources[3].spawnCalls, 1)
sources[3].stored[BASKET] = 2
v16.trailers.level = 1000
ADOutbound.beforeLoadUpdate(adEnv, task16, 100)
ADOutbound.beforeLoadUpdate(adEnv, task16, 100)
check("O16 næste spawn via fyldniveau", sources[3].spawnCalls, 2)
-- O17 (reserve + paller til lager): buildPlanInput henter reserve og palle-lager fra ADSources
local demandCalls = {}
ADSources.getFarmDemand = function(_, ft, farmId, excluded) table.insert(demandCalls, ft) return {liters = ft == SUGAR and 4000 or 0, consumers = {"TMR-fabrik"}} end
local palletStoreMarker = nil
ADSources.findPalletStore = function(_, vehicle, ft, excluded, isReachable, farmId) return palletStoreMarker end
local v17 = newVehicle("Reserve", 0, 0, 20000)
runs[v17] = outboundRun(17, {1}, {{fillType = "SUGAR", policy = "sell"}})
local input17 = ADOutbound.buildPlanInput(adEnv, runs[v17], v17)
check("O17 reserve på salgsvare", input17.products[1].reserve, 4000)
check("O17 reserveret til", input17.products[1].reservedFor[1], "TMR-fabrik")
local v18 = newVehicle("PalletStore", 100, 0, 8000, true)
runs[v18] = outboundRun(18, {3}, {{fillType = "STRAW_BASKET", policy = "store"}})
palletStoreMarker = {wayPointId = 40, name = "Palleskur", free = 100}
sources[3].stored[BASKET] = 4
products[3][1].available, products[3][1].level, products[3][1].capacity = 4000, 3900, 4000
local input18 = ADOutbound.buildPlanInput(adEnv, runs[v18], v18)
check("O18 palle-lager som store-mål", input18.products[1].store ~= nil and input18.products[1].store.wayPointId, 40)
check("O18 reserve spørges ikke for lager-politik", demandCalls[#demandCalls] ~= BASKET, true)
-- O19: pallebil (kun UAL) ved en fabrik der både har bulk-læssestation og sukkerpaller -> paller
local v19 = newVehicle("Dragon", 0, 0, 6000, true)
v19.carriable = {}
runs[v19] = outboundRun(19, {1}, {{fillType = "SUGAR", policy = "sell"}})
products[1] = {{fillType = SUGAR, name = "SUGAR", available = 150000, level = 150000, capacity = 180000, isPallet = false, canPallet = true, palletLiters = 3000, palletCount = 3}}
quotes["selling station"][SUGAR].percent = 92
local input19 = ADOutbound.buildPlanInput(adEnv, runs[v19], v19)
check("O19 varen tages som paller", input19.products[1] ~= nil and input19.products[1].isPallet, true)
check("O19 mængde = paller ved spawner", input19.products[1].sources[1].available, 3000)
check("O19 palle-antal følger med", input19.products[1].sources[1].palletCount, 3)
local v19b = newVehicle("Tipvogn", 0, 0, 20000)
runs[v19b] = outboundRun(20, {1}, {{fillType = "SUGAR", policy = "sell"}})
local input19b = ADOutbound.buildPlanInput(adEnv, runs[v19b], v19b)
check("O19 tipvogn tager bulk", input19b.products[1].isPallet, false)
check("O19 bulk-mængde", input19b.products[1].sources[1].available, 150000)
products[1] = {{fillType = SUGAR, name = "SUGAR", available = 5000, level = 5000, capacity = 180000, isPallet = false}}
-- O20: UAL-bil: AD's "kapacitet" er antal paller (læssede + 1) -> liter-kapacitet ukendt i planen
local v20 = newVehicle("Dragon2", 100, 0, 1, true)
v20.carriable = {}
runs[v20] = outboundRun(21, {3}, {{fillType = "STRAW_BASKET", policy = "sell"}})
local input20 = ADOutbound.buildPlanInput(adEnv, runs[v20], v20)
check("O20 kapacitet ukendt for pallebil", input20.capacity, nil)
check("O20 har autoload", input20.hasAutoload, true)
check("O20 turen planlægges alligevel", ADOutboundPlanner.plan(input20) ~= nil, true)
-- O21: autoload-bil på vej til aflæsning: stropperne løsnes UNSTRAP_BEFORE_M før målet (ladet passerer
-- triggeren før markøren), kun én gang pr. tur, og aldrig for tipvogne
local v21 = newVehicle("Dragon3", 0, 0, 1, true)
v21.carriable = {}
v21.trailers[1].setAllTensionBeltsActive = function(self, active) self.beltsActive = active end
runs[v21] = outboundRun(22, {3}, {{fillType = "STRAW_BASKET", policy = "sell"}})
sources[3].stored[BASKET] = 3
quotes["selling station"][BASKET] = {percent = 95, price = 1}
ADOutbound.choosePickup(adEnv, v21)
check("O21 tur mod salgssted", v21.ad.stateModule.secondMarkerId, 10)
local unloadTask21 = {vehicle = v21, destinationID = 10, trailers = v21.trailers}
v21.rootNode = {x = 200 - 40, z = 0} -- 40 m fra markør 10 (x=200)
ADOutbound.beforeUnloadUpdate(adEnv, unloadTask21, 16)
check("O21 40 m: stropper stadig på", v21.trailers[1].beltsActive, nil)
v21.rootNode = {x = 200 - 20, z = 0}
ADOutbound.beforeUnloadUpdate(adEnv, unloadTask21, 16)
check("O21 20 m: stropper løsnet", v21.trailers[1].beltsActive, false)
v21.trailers[1].beltsActive = nil
ADOutbound.beforeUnloadUpdate(adEnv, unloadTask21, 16)
check("O21 kun én gang", v21.trailers[1].beltsActive, nil)
local v21b = newVehicle("Tip", 0, 0, 20000)
v21b.trailers[1].setAllTensionBeltsActive = function(self, active) self.beltsActive = active end
runs[v21b] = outboundRun(23, {1}, {{fillType = "SUGAR", policy = "sell"}})
quotes["selling station"][SUGAR].percent = 92
ADOutbound.choosePickup(adEnv, v21b)
v21b.rootNode = {x = 195, z = 0}
ADOutbound.beforeUnloadUpdate(adEnv, {vehicle = v21b, destinationID = 10, trailers = v21b.trailers}, 16)
check("O21 tipvogn: ingen stropper", v21b.trailers[1].beltsActive, nil)
-- O22: lagermål med en anden silo-markør inden for AD's globale trigger-afstand -> advarsel én gang
-- (AD tipper ved første trigger inden for afstanden; 10 m løser det når siloer står tæt)
local triggerDistance = 25
adEnv.AutoDrive.getSetting = function(name) if name == "maxTriggerDistance" then return triggerDistance end end
local v22 = newVehicle("Tip2", 0, 0, 20000)
runs[v22] = outboundRun(24, {1}, {{fillType = "SUGAR", policy = "store"}})
products[1] = {{fillType = SUGAR, name = "SUGAR", available = 170000, level = 170000, capacity = 180000, isPallet = false}}
dumpMarker = mStore -- silo 6 (x=60); silo 4 (x=50) står 10 m derfra
logLines = {}
ADOutbound.choosePickup(adEnv, v22)
check("O22 lager-tur", ADOutbound.getTripFor(v22) ~= nil and ADOutbound.getTripFor(v22).kind, "store")
check("O22 advarsel om trigger-afstand", lastLog("Max trigger distance") ~= nil, true)
logLines = {}
ADOutbound.choosePickup(adEnv, v22)
check("O22 kun én gang", lastLog("Max trigger distance"), nil)
triggerDistance = 10
local v22b = newVehicle("Tip3", 0, 0, 20000)
runs[v22b] = outboundRun(25, {1}, {{fillType = "SUGAR", policy = "store"}})
logLines = {}
ADOutbound.choosePickup(adEnv, v22b)
check("O22 ved 10 m ingen advarsel", lastLog("Max trigger distance"), nil)
adEnv.AutoDrive.getSetting = nil
products[1] = {{fillType = SUGAR, name = "SUGAR", available = 5000, level = 5000, capacity = 180000, isPallet = false}}
-- O23: intet salgssted tager varen -> log én gang med de fundne salgsmarkører og hvad de tager
ADSources.describeAccepted = function(station) return "SUGAR, FORAGE" end
local v23 = newVehicle("Dragon4", 100, 0, 1, true)
v23.carriable = {}
runs[v23] = outboundRun(26, {3}, {{fillType = "STRAW_CRATE", policy = "sell"}})
quotes["selling station"][CRATE] = nil; quotes["Far station"][CRATE] = nil
sources[3].stored[CRATE] = 2
logLines = {}
ADOutbound.choosePickup(adEnv, v23)
local noSell = lastLog("intet salgssted tager STRAW_CRATE")
check("O23 log om manglende salgssted", noSell ~= nil, true)
check("O23 lister salgsmarkører", noSell ~= nil and noSell:find("selling station (SUGAR, FORAGE)", 1, true) ~= nil, true)
logLines = {}
ADOutbound.choosePickup(adEnv, v23)
check("O23 kun én gang", lastLog("intet salgssted tager STRAW_CRATE"), nil)
-- O10: install wrapper: fejl i hook stopper ikke AD
local calls = {}
adEnv.LoadAtDestinationTask.update = function(self, dt) calls.update = (calls.update or 0) + 1 end
adEnv.LoadAtDestinationTask.finished = function(self) calls.finished = (calls.finished or 0) + 1 end
ADOutbound.install(adEnv)
local vX = newVehicle("X", 0, 0, 1000)
adEnv.LoadAtDestinationTask.update(newTask(vX, 1), 16)
check("O10 original update kaldt", calls.update, 1)
adEnv.LoadAtDestinationTask.finished(newTask(vX, 1))
check("O10 original finished kaldt", calls.finished, 1)
adEnv.UnloadAtDestinationTask.update = function(self, dt) calls.unload = (calls.unload or 0) + 1 end
ADOutbound.install(adEnv)
check("O10 unload-wrap ikke installeret dobbelt", adEnv.UnloadAtDestinationTask.update ~= nil, true)
-- O24: stropperne spændes igen når pålæsningen er færdig (de blev løsnet 25 m før forrige salg;
-- in-game 23/9: Dragon tabte paller på vejen to gange)
local v24 = newVehicle("Dragon5", 100, 0, 1, true)
v24.carriable = {}
v24.trailers[1].setAllTensionBeltsActive = function(self, active) self.beltsActive = active end
runs[v24] = outboundRun(27, {3}, {{fillType = "STRAW_BASKET", policy = "sell"}})
sources[3].stored[BASKET] = 3
quotes["selling station"][BASKET] = {percent = 95, price = 1}
ADOutbound.choosePickup(adEnv, v24)
local task24 = newTask(v24, 3)
v24.trailers.level = 3
adEnv.LoadAtDestinationTask.finished(task24)
check("O24 stropper spændt efter pålæsning", v24.trailers[1].beltsActive, true)
check("O24 tur planlagt", ADOutbound.getTripFor(v24) ~= nil, true)
check("O24 original finished kaldt", calls.finished, 2)
local v24b = newVehicle("Tip4", 0, 0, 20000)
v24b.trailers[1].setAllTensionBeltsActive = function(self, active) self.beltsActive = active end
adEnv.LoadAtDestinationTask.finished(newTask(v24b, 1))
check("O24 tipvogn uden tur: rører ikke stropper", v24b.trailers[1].beltsActive, nil)
-- O-pool: to kilder hvis stationer deler lager (mælketanke som udvidelser) = ét stop i runden
do
local pool = {}
local st1, st2 = sources[1].station, sources[2].station
sources[1].station = {name = "tankA", sourceStorages = {[pool] = pool}}
sources[2].station = {name = "tankB", sourceStorages = {[pool] = pool}}
local k1, k2 = sources[1].kind, sources[2].kind
sources[1].kind, sources[2].kind = "silo", "silo"
local vPool = newVehicle("Tank", 0, 0, 10000)
runs[vPool] = outboundRun(77, {1, 2}, {{fillType = "SUGAR", policy = "sell"}}, {collectRound = true})
local inputPool = ADOutbound.buildPlanInput(adEnv, runs[vPool], vPool)
local ids = {}
for _, product in ipairs(inputPool.products) do
for _, source in ipairs(product.sources) do table.insert(ids, tostring(source.wayPointId)) end
end
check("O-pool kun første kilde", table.concat(ids, ","), "1")
check("O-pool logget", lastLog("deler lager") ~= nil, true)
sources[1].station, sources[2].station = st1, st2
sources[1].kind, sources[2].kind = k1, k2
end
-- O-pool-stald: mælke- og gyllemarkør ved samme stald deler lager, men er forskellige læssepunkter
do
local pool = {}
sources[31] = {kind = "husbandry", wayPointId = 31, station = {name = "barnSlurry", sourceStorages = {[pool] = pool}}, name = "Stald"}
sources[32] = {kind = "husbandry", wayPointId = 32, station = {name = "barnMilk", sourceStorages = {[pool] = pool}}, name = "Stald"}
products[31] = {{fillType = SUGAR_FT or 5, name = "SUGAR", title = "Sukker", available = 20000, level = 20000, capacity = 50000, isPallet = false}}
products[32] = {{fillType = SUGAR_FT or 5, name = "SUGAR", title = "Sukker", available = 20000, level = 20000, capacity = 50000, isPallet = false}}
local vBarn = newVehicle("Tank2", 0, 0, 10000)
runs[vBarn] = outboundRun(78, {31, 32}, {{fillType = "SUGAR", policy = "sell"}}, {collectRound = true})
local inputBarn = ADOutbound.buildPlanInput(adEnv, runs[vBarn], vBarn)
local ids = {}
for _, product in ipairs(inputBarn.products) do
for _, source in ipairs(product.sources) do table.insert(ids, tostring(source.wayPointId)) end
end
check("O-pool-stald: stalde lægges ikke sammen", table.concat(ids, ","), "31,32")
sources[31], sources[32], products[31], products[32] = nil, nil, nil, nil
end
-- O-forced: "Lager til" valgt -> lagerturen går til det valgte mål, ikke moden egen silo-valg
do
sources[41] = {kind = "silo", wayPointId = 41, station = {name = "gylleSilo"}, name = "Gyllesilo"}
products[41] = {{fillType = SUGAR, name = "SUGAR", title = "Sukker", available = 11000000, level = 11000000, capacity = 12000000, isPallet = false}}
local vF = newVehicle("Gyllevogn", 0, 0, 20000)
runs[vF] = outboundRun(79, {41}, {{fillType = "SUGAR", policy = "store"}})
runs[vF].forcedMarkers = {31}
ADRunsController.getStoreMarkers = function(_, run) return run.forcedMarkers or {} end
local forcedCalls = 0
ADLoadSwap.findForcedDumpMarker = function(_, _, _, _, ids) forcedCalls = forcedCalls + 1 return {id = ids[1], name = "BGA", markerIndex = 99} end
dumpMarker = mStore
ADOutbound.choosePickup(adEnv, vF)
check("O-forced valgt mål brugt", forcedCalls > 0, true)
check("O-forced tur-mål = BGA", ADOutbound.getTripFor(vF) and ADOutbound.getTripFor(vF).targetWayPointId, 31)
ADRunsController.getStoreMarkers, ADLoadSwap.findForcedDumpMarker = nil, nil
end
-- O-empty: ingen vare ved kilden (æggene er ikke spawnet endnu) -> intet lagermål søges, ingen advarsel
do
sources[42] = {kind = "husbandry", wayPointId = 42, station = nil, name = "Chicken Coop"}
products[42] = {{fillType = SUGAR, name = "SUGAR", title = "Æg", available = 0, level = 0, capacity = 0, isPallet = false}}
local vE = newVehicle("Æg", 0, 0, 20000)
runs[vE] = outboundRun(80, {42}, {{fillType = "SUGAR", policy = "store"}})
local storeLookups = 0
local dumpBefore = ADLoadSwap.findDumpMarker
ADLoadSwap.findDumpMarker = function(...) storeLookups = storeLookups + 1 return nil, "ingen" end
ADOutbound.buildPlanInput(adEnv, runs[vE], vE)
check("O-empty intet lagermål søgt uden vare", storeLookups, 0)
ADLoadSwap.findDumpMarker = dumpBefore
end
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-188
View File
@@ -1,188 +0,0 @@
-- Kør: luajit tests/test_adOutboundPlanner.lua (fra repo-roden)
-- Ren turvurdering for udkørsel: prisregel, tærskler, politikker, rangering, opsamlingsrunde.
dofile("FS25_ADSmartPickup/adOutboundPlanner.lua")
local P = ADOutboundPlanner
local failures = 0
local function check(name, actual, expected)
if actual == expected then
print("OK " .. name)
else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
local function join(list) return table.concat(list or {}, ",") end
local SUGAR, FORAGE, BASKET, CRATE, BROOM = 5, 6, 7, 8, 9
local function input(products, overrides)
local result = {capacity = 20000, hasAutoload = false, sellAtPercent = 90, storeAbovePercent = 80, collectRound = false, products = products}
for key, value in pairs(overrides or {}) do result[key] = value end
return result
end
local function product(name, fillType, policy, sources, quote, store, isPallet)
return {fillType = fillType, name = name, policy = policy, isPallet = isPallet == true, sources = sources, quote = quote, store = store}
end
local quoteS = {percent = 92, price = 480, wayPointId = 900, stationName = "Grain Elevator"}
local storeSilo = {wayPointId = 400, name = "silo 1", free = 500000}
-- P1: salg ved pris over tærskel, uanset mængde
local trip = P.plan(input({product("SUGAR", SUGAR, "sell", {{wayPointId = 1, available = 5000, level = 5000, capacity = 180000}}, quoteS, nil)}))
check("P1 salg", trip.kind, "sell")
check("P1 mål", trip.targetWayPointId, 900)
check("P1 målnavn", trip.targetName, "Grain Elevator")
check("P1 kilde", join(trip.sourceIds), "1")
check("P1 vare", join(trip.fillTypes), "5")
check("P1 procent", trip.percent, 92)
-- P2: pris under tærskel, lager lavt -> ingen tur med begrundelse
local low = {percent = 78, price = 400, wayPointId = 900, stationName = "Grain Elevator"}
local trip2, reasons2 = P.plan(input({product("SUGAR", SUGAR, "sell", {{wayPointId = 1, available = 74000, level = 74000, capacity = 180000}}, low, nil)}))
check("P2 ingen tur", trip2, nil)
check("P2 begrundelse", reasons2[1], "SUGAR 78 % af top (kræver 90), lager 41 %")
-- P3: sellElseStore, pris lav, lager 85 % -> lager
local trip3 = P.plan(input({product("SUGAR", SUGAR, "sellElseStore", {{wayPointId = 1, available = 153000, level = 153000, capacity = 180000}}, low, storeSilo)}))
check("P3 lager", trip3.kind, "store")
check("P3 silo", trip3.targetWayPointId, 400)
-- P4: store med fuldt læs, lager 20 %
local trip4 = P.plan(input({product("FORAGE", FORAGE, "store", {{wayPointId = 2, available = 700000, level = 700000, capacity = 3500000}}, nil, storeSilo)}))
check("P4 fuldt læs -> lager", trip4.kind, "store")
local trip4b = P.plan(input({product("FORAGE", FORAGE, "store", {{wayPointId = 2, available = 15000, level = 15000, capacity = 3500000}}, nil, storeSilo)}))
check("P4 for lidt og lager lavt -> nil", trip4b, nil)
-- P5: rangering: højeste pris-% først
local trip5 = P.plan(input({
product("SUGAR", SUGAR, "sell", {{wayPointId = 1, available = 1000, level = 1000, capacity = 180000}}, {percent = 91, price = 1, wayPointId = 900, stationName = "A"}, nil),
product("FORAGE", FORAGE, "sell", {{wayPointId = 2, available = 1000, level = 1000, capacity = 3500000}}, {percent = 95, price = 1, wayPointId = 901, stationName = "B"}, nil),
}))
check("P5 højeste procent vinder", join(trip5.fillTypes), "6")
check("P5 uden runde kun én vare", #trip5.fillTypes, 1)
-- P6: intet nåeligt salgssted
local trip6, reasons6 = P.plan(input({product("SUGAR", SUGAR, "sell", {{wayPointId = 1, available = 170000, level = 170000, capacity = 180000}}, nil, nil)}))
check("P6 ingen tur", trip6, nil)
check("P6 begrundelse", reasons6[1], "SUGAR: intet nåeligt salgssted")
-- P7: ingen silo med plads ved lagerbehov
local trip7, reasons7 = P.plan(input({product("FORAGE", FORAGE, "store", {{wayPointId = 2, available = 3000000, level = 3000000, capacity = 3500000}}, nil, nil)}))
check("P7 ingen tur", trip7, nil)
check("P7 begrundelse", reasons7[1], "FORAGE: intet lager med plads (silo eller egen fabrik der bruger varen)")
-- P8: runde, bulk: samme vare fra flere kilder, mest først
local trip8 = P.plan(input({product("SUGAR", SUGAR, "sell", {{wayPointId = 1, available = 3000, level = 3000, capacity = 180000}, {wayPointId = 3, available = 9000, level = 9000, capacity = 180000}}, quoteS, nil)}, {collectRound = true, capacity = 10000}))
check("P8 kilder mest først", join(trip8.sourceIds), "3,1")
check("P8 én vare", #trip8.fillTypes, 1)
-- P9: runde, UAL: alle pallevarer med samme mål
local sellS = function(pct) return {percent = pct, price = 1, wayPointId = 950, stationName = "Halm-salg"} end
local sellT = {percent = 96, price = 1, wayPointId = 951, stationName = "Andet"}
local pallets = {
product("STRAW_BASKET", BASKET, "sell", {{wayPointId = 5, available = 8000, level = 8, capacity = 4000}}, sellS(95), nil, true),
product("STRAW_CRATE", CRATE, "sell", {{wayPointId = 5, available = 2000, level = 2, capacity = 4000}}, sellS(93), nil, true),
product("STRAW_BROOM", BROOM, "sell", {{wayPointId = 6, available = 1000, level = 1, capacity = 4000}}, sellT, nil, true),
}
local trip9 = P.plan(input(pallets, {collectRound = true, hasAutoload = true}))
check("P9 højeste procent = kost alene (andet mål)", join(trip9.fillTypes), "9")
local trip9b = P.plan(input({pallets[1], pallets[2]}, {collectRound = true, hasAutoload = true}))
check("P9 kurve+kasser samme mål", join(trip9b.fillTypes), "7,8")
check("P9 navne", join(trip9b.names), "STRAW_BASKET,STRAW_CRATE")
check("P9 kilder unikke", join(trip9b.sourceIds), "5")
local trip9c = P.plan(input({pallets[1], pallets[2]}, {collectRound = true, hasAutoload = false}))
check("P9 uden UAL kun én pallevare", #trip9c.fillTypes, 1)
-- P10: uden runde kun én kilde (den med mest)
local trip10 = P.plan(input({product("SUGAR", SUGAR, "sell", {{wayPointId = 1, available = 3000, level = 3000, capacity = 180000}, {wayPointId = 3, available = 9000, level = 9000, capacity = 180000}}, quoteS, nil)}))
check("P10 én kilde, den fyldeste", join(trip10.sourceIds), "3")
-- P11: intet på lager
local trip11, reasons11 = P.plan(input({product("SUGAR", SUGAR, "sell", {}, quoteS, nil)}))
check("P11 ingen tur", trip11, nil)
check("P11 begrundelse", reasons11[1], "SUGAR: intet på lager")
-- P12: evaluate
local e = P.evaluate(product("SUGAR", SUGAR, "sell", {{wayPointId = 1, available = 150000, level = 150000, capacity = 180000}}, quoteS, nil), input({}))
check("P12 storagePercent", math.floor(e.storagePercent + 0.5), 83)
check("P12 storageHigh", e.storageHigh, true)
check("P12 fullLoad", e.fullLoad, true)
check("P12 priceOk", e.priceOk, true)
-- P13 (reserve): det gårdens fabrikker/stalde skal bruge, sælges ikke
local BEET = 10
local withReserve = product("SUGARBEET", BEET, "sell", {{wayPointId = 3, available = 268000, level = 268000, capacity = 12000000}}, quoteS, nil)
withReserve.reserve, withReserve.reservedFor = 196000, {"TMR-fabrik"}
local trip13 = P.plan(input({withReserve}))
check("P13 fuldt læs uden om reserven -> salg", trip13 ~= nil and trip13.kind, "sell")
withReserve.reserve = 260000 -- kun 8.000 til overs: mindre end et læs
local trip13b, reasons13b = P.plan(input({withReserve}))
check("P13 reserven ville blive rørt -> ingen tur", trip13b, nil)
check("P13 begrundelse", reasons13b[1], "SUGARBEET: 260.0k reserveret til TMR-fabrik")
withReserve.reserve = 300000 -- alt reserveret
check("P13 alt reserveret -> ingen tur", P.plan(input({withReserve})), nil)
withReserve.reserve = 0
check("P13 reserve 0 -> salg uanset mængde", P.plan(input({withReserve})).kind, "sell")
-- lager-politik ignorerer reserven (varen bliver på gården)
local storeReserve = product("SUGARBEET", BEET, "store", {{wayPointId = 3, available = 268000, level = 11000000, capacity = 12000000}}, nil, storeSilo)
storeReserve.reserve = 300000
check("P13 lager rører ikke reserven", P.plan(input({storeReserve})).kind, "store")
local e13 = P.evaluate(withReserve, input({}, {capacity = 20000}))
check("P13 evaluate sellable", e13.sellable, 268000)
-- P14 (paller): kapacitet ukendt for en autoload-bil -> "fuldt læs" = mindst PALLET_FULL_LOAD_COUNT paller klar
local function palletInput(products)
local inp = input(products, {hasAutoload = true})
inp.capacity = nil
return inp
end
local palletProduct = product("STRAW_BASKET", BASKET, "store", {{wayPointId = 5, available = 3000, level = 1600, capacity = 8000, palletCount = 3}}, nil, storeSilo, true)
check("P14 3 paller: ikke fuldt læs", P.plan(palletInput({palletProduct})), nil)
palletProduct.sources[1].palletCount = 4
check("P14 4 paller: lager-tur", P.plan(palletInput({palletProduct})).kind, "store")
local twoSources = product("STRAW_BASKET", BASKET, "store", {{wayPointId = 5, available = 2000, level = 1600, capacity = 8000, palletCount = 2}, {wayPointId = 3, available = 2000, level = 2, capacity = 4000, palletCount = 2}}, nil, storeSilo, true)
check("P14 paller summeres over kilder", P.plan(palletInput({twoSources})).kind, "store")
check("P14 evaluate palletCount", P.evaluate(twoSources, palletInput({})).palletCount, 4)
-- P15: kilder der deler lager (mælketanke = udvidelser inden for storageRadius) regnes som ét stop;
-- den første i forløbets rækkefølge beholdes, rækkefølgen bevares
local storA, storB, storC = {}, {}, {}
local pools = {[1] = {storA, storB}, [2] = {storB}, [3] = {storC}, [4] = {storA}, [5] = nil}
local srcs = {{wayPointId = 1}, {wayPointId = 2}, {wayPointId = 3}, {wayPointId = 4}, {wayPointId = 5}}
local kept, merged = P.dedupeSharedSources(srcs, function(source) return pools[source.wayPointId] end)
check("P15 behold 1, 3, 5", join((function() local ids = {} for _, s in ipairs(kept) do table.insert(ids, s.wayPointId) end return ids end)()), "1,3,5")
check("P15 slået sammen: 2 og 4 ind i 1", join(merged[1]), "2,4")
check("P15 kilde uden lagre beholdes", kept[3].wayPointId, 5)
local none = P.dedupeSharedSources({}, function() return nil end)
check("P15 tom liste", #none, 0)
-- P16: fodring ("Lager til" valgt = feedMode): kør så snart kilden har FEED_MIN_LITERS — ikke fuldt læs / 80 %
local feedStore = {wayPointId = 50, name = "Slurry Drop 1", free = 100000}
local feedProduct = product("LIQUIDMANURE", FORAGE, "store", {{wayPointId = 2, available = 20000, level = 20000, capacity = 500000}}, nil, feedStore)
local tripNoFeed = P.plan(input({feedProduct}, {capacity = 100000}))
check("P16 uden fodring: 4 % og intet fuldt læs -> ingen tur", tripNoFeed, nil)
local tripFeed = P.plan(input({feedProduct}, {capacity = 100000, feedMode = true}))
check("P16 fodring: 20k >= minimum -> lagertur", tripFeed and tripFeed.kind, "store")
check("P16 fodring: mål = valgt drop", tripFeed and tripFeed.targetWayPointId, 50)
local tinyProduct = product("LIQUIDMANURE", FORAGE, "store", {{wayPointId = 2, available = 3000, level = 3000, capacity = 500000}}, nil, feedStore)
check("P16 fodring: under minimum -> ingen tur", P.plan(input({tinyProduct}, {capacity = 100000, feedMode = true})), nil)
-- P17: valgt lager ("Lager til") kan ikke bruges -> begrundelsen er den præcise grund, ikke "intet lager med plads"
local blocked = product("EGG", FORAGE, "store", {{wayPointId = 2, available = 3000000, level = 3000000, capacity = 3500000}}, nil, nil)
blocked.storeReason = "Bale & Pallet Storage: ingen vej til 'Chicken Storage Unload'"
local _, reasons17 = P.plan(input({blocked}))
check("P17 præcis grund i status", reasons17[1], "EGG: Bale & Pallet Storage: ingen vej til 'Chicken Storage Unload'")
-- SA: "sælg altid over X %" — kilden over grænsen sælges selvom prisen er lav (gylle ved BGA'en, 26/9)
local lowQuote = {percent = 40, price = 0.5, top = 1.25, wayPointId = 77, stationName = "Liquid Buy"}
local fullSlurry = {{wayPointId = 5, available = 900000, level = 900000, capacity = 1000000}}
local lowSlurry = {{wayPointId = 5, available = 300000, level = 300000, capacity = 1000000}}
local saOff = P.plan(input({product("LIQUIDMANURE", 40, "sell", fullSlurry, lowQuote, nil)}))
check("SA fra: lav pris -> ikke salg", saOff ~= nil and saOff.kind or "intet", "intet")
local saOn = P.plan(input({product("LIQUIDMANURE", 40, "sell", fullSlurry, lowQuote, nil)}, {sellAbovePercent = 80}))
check("SA 90 % fyldt over 80 %: sælg trods lav pris", saOn ~= nil and saOn.kind, "sell")
local saLow = P.plan(input({product("LIQUIDMANURE", 40, "sell", lowSlurry, lowQuote, nil)}, {sellAbovePercent = 80}))
check("SA 30 % fyldt: ikke salg", saLow ~= nil and saLow.kind or "intet", "intet")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-183
View File
@@ -1,183 +0,0 @@
-- Kør: luajit tests/test_adPool.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFieldFleet.lua")
dofile("FS25_ADSmartPickup/adHitch.lua")
dofile("FS25_ADSmartPickup/adPool.lua")
local P = ADPool
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
-- rolle ud fra redskabets egne specs
check("rolle: plov", P.roleOf({spec_plow = true}), "plow")
check("rolle: kultivator", P.roleOf({spec_cultivator = true}), "cultivator")
check("rolle: vogn med tip", P.roleOf({spec_trailer = true, spec_dischargeable = true}), "unloader")
check("rolle: vogn der også er læssevogn er læssevogn", P.roleOf({spec_trailer = true, spec_dischargeable = true, spec_forageWagon = true}), "collector")
check("rolle: intet", P.roleOf({}), nil)
-- hk fra butiksdata: tal eller tabel (konfigurationer)
check("tal", P.numberOf(180), 180)
check("tabel: største", P.numberOf({base = 120, config = 150}), 150)
check("tom tabel", P.numberOf({}), nil)
check("nil", P.numberOf(nil), nil)
check("hk: nok", P.powerOk(200, 180), true)
check("hk: for lidt", P.powerOk(150, 180), false)
check("hk: redskab uden data", P.powerOk(150, nil), true)
check("hk: traktor uden data", P.powerOk(nil, 180), true)
-- kategori: markredskaber kun bag traktorer (in-game 29/9: Unimog, kategori "trucks", hentede kultivatoren)
check("kategori: traktor til kultivator", P.categoryOk("TRACTORSM", "cultivator"), true)
check("kategori: lastbil til kultivator", P.categoryOk("TRUCKS", "cultivator"), false)
check("kategori: lastbil til tømmevogn", P.categoryOk("TRUCKS", "unloader"), true)
check("kategori: teleskoplæsser til plov", P.categoryOk("TELELOADERVEHICLES", "plow"), false)
check("kategori: ukendt -> tilladt", P.categoryOk(nil, "cultivator"), true)
check("kategori: små bogstaver", P.categoryOk("tractorsL", "plow"), true)
local unimog = {id = "u", x = 0, z = 0, hp = 155, category = "TRUCKS"}
local fendt = {id = "f", x = 90, z = 0, hp = 250, category = "TRACTORSM"}
local cult = {{id = "c", role = "cultivator", roles = {cultivator = true}, x = 1, z = 0, neededHp = 120, name = "Cult"}}
local picked = P.pairFor("cultivator", 0, 0, cult, {unimog, fendt}, {})
check("parring: nærmeste lastbil springes over", picked ~= nil and picked.tractor.id or nil, "f")
-- Connect-markør til en parkering
local markers = {{name = "Start", id = 1}, {name = "Bredal", id = 4}, {name = "Connect Bredal", id = 9}, {name = "connect plov 1", id = 12}}
check("connect: Bredal", P.connectFor("Bredal", markers).id, 9)
check("connect: små bogstaver", P.connectFor("Plov 1", markers).id, 12)
check("connect: ingen", P.connectFor("Start", markers), nil)
check("connect: uden parkering", P.connectFor(nil, markers), nil)
-- parring: redskab nærmest marken, så traktor nærmest redskabet
local implements = {
{id = "plovFar", name = "Plov far", role = "plow", x = 500, z = 0, connectId = 20, parkId = 21},
{id = "plovNear", name = "Plov nær", role = "plow", x = 100, z = 0, neededHp = 180, connectId = 30, parkId = 31},
{id = "kult", name = "Kultivator", role = "cultivator", x = 0, z = 0, connectId = 40, parkId = 41},
}
local tractors = {
{id = "small", name = "Lille", x = 110, z = 0, hp = 120, parkId = 2},
{id = "big", name = "Stor", x = 300, z = 0, hp = 250, parkId = 3},
{id = "mid", name = "Mellem", x = 150, z = 0, hp = 190, parkId = 5},
}
local pair = P.pairFor("plow", 0, 0, implements, tractors, {})
check("par: nærmeste plov til marken", pair.implement.id, "plovNear")
check("par: nærmeste stærke traktor til ploven", pair.tractor.id, "mid")
local blockedPair = P.pairFor("plow", 0, 0, implements, tractors, {plovNear = true})
check("par: reserveret plov springes over", blockedPair.implement.id, "plovFar")
check("par: plov uden hk-krav tager nærmeste traktor", blockedPair.tractor.id, "big")
local none, why = P.pairFor("seeder", 0, 0, implements, tractors, {})
check("par: ingen såmaskine", none, nil); check("par: grund", why, "noPoolImplement")
local weak, weakWhy, weakArgs = P.pairFor("plow", 0, 0, {implements[2]}, {tractors[1]}, {})
check("par: for svag", weak, nil); check("par: grund tooWeak", weakWhy, "tooWeak"); check("par: hk i args", weakArgs[1], 180)
local noTractor, noTractorWhy = P.pairFor("plow", 0, 0, implements, {}, {})
check("par: ingen traktor", noTractor, nil); check("par: grund noPoolTractor", noTractorWhy, "noPoolTractor")
local rejected, rejectedWhy = P.pairFor("plow", 0, 0, implements, tractors, {}, function() return false end)
check("par: accept afviser alle", rejected, nil); check("par: grund", rejectedWhy, "noPoolImplement")
local multi = P.pairFor("fertilizer", 0, 0, {{id = "spreder", role = "fertilizer", roles = {fertilizer = true, liming = true}, x = 0, z = 0}}, tractors, {})
check("par: redskab med flere roller", multi.implement.id, "spreder")
-- reservationer og karantæne
P.reserve("F12", {"plovNear", "mid"})
check("reserveret: redskab", P.blocked(0).plovNear, true)
check("reserveret: traktor", P.blocked(0).mid, true)
P.release("F12")
check("frigivet", P.blocked(0).plovNear, nil)
P.quarantine("plovNear", 1000)
check("karantæne: aktiv", P.blocked(1000 + P.QUARANTINE_MS - 1).plovNear, true)
check("karantæne: udløbet", P.blocked(1000 + P.QUARANTINE_MS).plovNear, nil)
-- kø: længst ventende først, lige -> lavest mark-id, ikke-ventende bagerst i oprindelig rækkefølge
local infos = {{id = 5}, {id = 3}, {id = 9}, {id = 1}}
local ordered = P.queueOrder(infos, {[9] = 100, [3] = 50, [1] = 50})
check("kø: 1.", ordered[1].id, 1); check("kø: 2.", ordered[2].id, 3); check("kø: 3.", ordered[3].id, 9); check("kø: 4.", ordered[4].id, 5)
check("kø: rører ikke input", infos[1].id, 5)
-- på hjemtur: optaget indtil hjemme
local tractorObject = {}
check("hjemtur: ikke fra start", P.isReturning(tractorObject), false)
P.setReturning(tractorObject, true)
check("hjemtur: markeret", P.isReturning(tractorObject), true)
P.setReturning(tractorObject, false)
check("hjemtur: hjemme", P.isReturning(tractorObject), false)
-- review-rettelser
-- C1: hjemtur også når AD selv ville parkere (påfyldte sæt); aldrig ved manuel transport
check("C1: hjem trods adHandlesEnd", P.shouldReturnHome({adHandlesEnd = true}), true)
check("C1: ikke ved manuel", P.shouldReturnHome({manual = true}), false)
-- I1: kun maskiner uden egen arbejdsrolle kan trække et puljeredskab
check("I1: traktor uden rolle", P.canPull(nil), true)
check("I1: mejetærsker", P.canPull("harvester"), false)
check("I1: selvkørende sprøjte", P.canPull("fertilizer"), false)
-- I2: traktor uden AD-vejnet springes over; fejl uden fremdrift skyldes traktoren
local offNet = P.pairFor("plow", 0, 0, {implements[2]}, {{id = "far", x = 100, z = 0, offNetwork = true}, {id = "ok", x = 400, z = 0, hp = 300}}, {})
check("I2: traktor uden for vejnettet springes over", offNet.tractor.id, "ok")
check("I2: stoppedAway -> traktoren", P.failureBlames("stoppedAway"), "tractor")
check("I2: stoppedAway med afstand -> traktoren", P.failureBlames("stoppedAway (40 m fra markøren)"), "tractor")
check("I2: noAttachable -> redskabet", P.failureBlames("noAttachable"), "implement")
-- I3: efter koblingen — tom uden påfyldning = fejl, tom med påfyldning = fyld op, ellers kør
check("I3: ingen påfyldningsbehov", P.nextDrive(nil), "drive")
check("I3: fyldt", P.nextDrive({empty = false}), "drive")
check("I3: tom med påfyldning", P.nextDrive({empty = true, refillId = 7}), "load")
check("I3: tom uden påfyldning", P.nextDrive({empty = true}), "empty")
-- I4: puljen kun til hovedopgaver med ét sæt, og ikke når marken har egne faste maskiner til rollen
check("I4: almindelig", P.mayUsePool({count = 1}, false), true)
check("etape 2: tømmevogn må bruge puljen", P.mayUsePool({count = 1, helper = true}, false), true)
check("I4: multi-tool", P.mayUsePool({count = 2}, false), false)
check("I4: marken har faste", P.mayUsePool({count = 1}, true), false)
-- status til flådefanen
local function st(key, arg) return tostring(key) .. "|" .. tostring(arg) end
local function ist(s) return st(P.implementStatus(s)) end
check("redskab: uden parkering", ist({}), "noPark|nil")
check("redskab: uden Connect", ist({parkName = "Plov"}), "noConnect|Plov")
check("redskab: på traktor", ist({parkName = "Plov", hasConnect = true, attachedTo = "MT635"}), "attached|MT635")
check("redskab: karantæne", ist({parkName = "Plov", hasConnect = true, quarantineMin = 8}), "quarantine|8")
check("redskab: reserveret", ist({parkName = "Plov", hasConnect = true, reserved = true}), "reserved|nil")
check("redskab: parkeret", ist({parkName = "Plov", hasConnect = true}), "parked|Plov")
local function tst(s) return st(P.tractorStatus(s)) end
local base = {hasPark = true, enabled = true}
check("traktor: uden parkering", tst({enabled = true}), "noPark|nil")
check("traktor: spiller", tst({hasPark = true, enabled = true, controlled = true}), "player|nil")
check("traktor: hjemtur", tst({hasPark = true, enabled = true, returning = true}), "returning|nil")
check("traktor: opgave", tst({hasPark = true, enabled = true, fieldId = 52}), "job|52")
check("traktor: optaget", tst({hasPark = true, enabled = true, busy = true}), "busy|nil")
check("traktor: ikke i flåden", tst({hasPark = true}), "notInFleet|nil")
check("traktor: uden for vejnet", tst({hasPark = true, enabled = true, offNetwork = true}), "offNetwork|nil")
check("traktor: har andet redskab på", tst({hasPark = true, enabled = true, hitched = true}), "hitched|nil")
check("traktor: ledig", tst(base), "free|nil")
-- karantæne-rest i hele minutter (rundet op) og reservation
P.quarantine("q1", 0)
check("karantæne: minutter tilbage", P.quarantineMinutes("q1", 60000), 9)
check("karantæne: udløbet", P.quarantineMinutes("q1", P.QUARANTINE_MS), 0)
P.reserve("owner", {"r1"})
check("reserveret: ja", P.isReserved("r1"), true)
P.release("owner")
check("reserveret: nej", P.isReserved("r1"), false)
-- M3: ventende marker prøver igen i samme takt (så køens rækkefølge holder)
check("M3: næste takt", P.alignedRetry(61000, 30000), 90000)
check("M3: på grænsen -> næste", P.alignedRetry(60000, 30000), 90000)
check("M3: to marker samme takt", P.alignedRetry(62000, 30000) == P.alignedRetry(89000, 30000), true)
-- M4: for svag nævner redskabets navn
check("M4: navn i args", weakArgs[2], "Plov nær")
-- etape 2: puljevogn skal kunne bære høstens vare
check("bære: vare understøttet", P.canCarry({carries = {[3] = true}}, 3), true)
check("bære: vare ikke understøttet", P.canCarry({carries = {[3] = true}}, 7), false)
check("bære: ingen vare krævet", P.canCarry({carries = {}}, nil), true)
check("bære: ukendt last", P.canCarry({}, 3), false)
-- etape 3: alle puljevogne parret med hver sin nærmeste traktor (en traktor bruges kun én gang)
local wagons = {{id = "v1", role = "unloader", x = 0, z = 0}, {id = "v2", role = "unloader", x = 100, z = 0},
{id = "v3", role = "unloader", x = 200, z = 0}, {id = "plovX", role = "plow", x = 0, z = 0}}
local pairs3 = P.pairAll("unloader", wagons, {{id = "tA", x = 10, z = 0}, {id = "tB", x = 90, z = 0}}, {v2 = true})
check("par-alle: antal (v2 reserveret, 2 traktorer)", #pairs3, 2)
check("par-alle: v1 -> tA", pairs3[1].implement.id .. ">" .. pairs3[1].tractor.id, "v1>tA")
check("par-alle: v3 -> tB (tA er brugt)", pairs3[2].implement.id .. ">" .. pairs3[2].tractor.id, "v3>tB")
check("par-alle: ingen traktorer", #P.pairAll("unloader", wagons, {}, {}), 0)
-- flow-vogne: vogn der hører til et kørende flow vises sådan (og er ikke i puljen)
check("redskab: flowets vogn", ist({parkName = "P Cow Food vogn 1", hasConnect = true, flowName = "Cow Food"}), "flow|Cow Food")
check("redskab: flowets vogn koblet på", ist({parkName = "P Cow Food vogn 1", hasConnect = true, flowName = "Cow Food", attachedTo = "Cow Food 1"}), "attached|Cow Food 1")
-- pasform: mindste traktor med hk nok; afstand kun inden for ±15 %; over 2,5x behovet kun som nødløsning
-- (in-game 29/9: en meget stor traktor blev sendt til et lille job og kunne ikke komme rundt)
local impl120 = {id = "k", x = 0, z = 0, neededHp = 120}
local t150 = {id = "t150", x = 900, z = 0, hp = 150}
local t170 = {id = "t170", x = 300, z = 0, hp = 180}
local t500 = {id = "t500", x = 50, z = 0, hp = 500}
check("pasform: mindste der passer vinder over nærmere", P.bestFit({t500, t170, t150}, impl120, "cultivator").id, "t150")
local t160 = {id = "t160", x = 100, z = 0, hp = 160}
check("pasform: inden for 15 % vinder den nærmeste", P.bestFit({t150, t160}, impl120, "cultivator").id, "t160")
check("pasform: kun for stor -> nødløsning", P.bestFit({t500}, impl120, "cultivator").id, "t500")
check("pasform: for svag -> ingen", P.bestFit({{id = "t90", x = 0, z = 0, hp = 90}}, impl120, "cultivator"), nil)
check("pasform: redskab uden hk-data -> nærmeste", P.bestFit({t150, t500}, {id = "x", x = 0, z = 0}, "cultivator").id, "t500")
check("pasform: ukendt traktor-hk efter kendte", P.bestFit({{id = "u", x = 0, z = 0}, t170}, impl120, "cultivator").id, "t170")
check("pasform: lastbil tæller ikke", P.bestFit({{id = "lb", x = 0, z = 0, hp = 150, category = "TRUCKS"}, t500}, impl120, "cultivator").id, "t500")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-106
View File
@@ -1,106 +0,0 @@
-- Kør: luajit tests/test_adPrices.lua (fra repo-roden)
-- Prisregel: toppris fra årets prisforløb, pris-% pr. salgssted, bedste tilbud.
dofile("FS25_ADSmartPickup/adPrices.lua")
local failures = 0
local function check(name, actual, expected)
if actual == expected then
print("OK " .. name)
else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
-- P1: højeste faktor
check("P1 maxFactor", ADPrices.maxFactor({EARLY_SPRING = 0.9, LATE_AUTUMN = 1.2, MID_SUMMER = 1.0}), 1.2)
check("P1 uden faktorer = 1", ADPrices.maxFactor(nil), 1)
-- P2: toppris = grundpris nu × (maks ÷ nu)
check("P2 topPrice", ADPrices.topPrice(500, 1.0, 1.25), 625)
check("P2 factorNow 0 -> grundpris", ADPrices.topPrice(500, 0, 1.25), 500)
check("P2 grundpris 0 -> 0", ADPrices.topPrice(0, 1, 1.25), 0)
-- P3: procent, afrundet; stor efterspørgsel > 100
check("P3 75 %", ADPrices.percentOfTop(470, 625), 75)
check("P3 112 %", ADPrices.percentOfTop(700, 625), 112)
check("P3 top 0 -> 0", ADPrices.percentOfTop(1, 0), 0)
-- P4: spil-adapter med stationens grundpris
SellingStation = {}
local SUGAR = 5
g_fillTypeManager = {getFillTypeByIndex = function(_, index)
if index == SUGAR then return {economy = {factors = {A = 1.0, B = 1.25}}, pricePerLiter = 400} end
end}
g_currentMission = {environment = {currentPeriod = "A"}, economyManager = {getPriceMultiplier = function() return 1.0 end}}
local station = {
fillTypePrices = {[SUGAR] = 500},
getEffectiveFillTypePrice = function(_, fillType) return 470 end,
isa = function(_, class) return class == SellingStation end,
isSellingPoint = true,
}
local quote = ADPrices.getQuote(station, SUGAR)
check("P4 pris", quote.price, 470)
check("P4 top (pris ved 100 %)", math.floor(quote.top + 0.5), 588)
check("P4 procent = kurven alene (stationens grundpris bruges ikke: den mangler sværhedsgrad m.m.)", quote.percent, 80)
check("P4 er salgssted", ADPrices.isSellingStation(station), true)
check("P4 skjult = ikke salgssted", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = true, hideFromPricesMenu = true}), false)
check("P4 fabrik-station = ikke salgssted", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = false}), false)
-- in-game (23/9): TMR-fabrikkens indlevering er en SellingStation med isSellingPoint=true -> blev regnet som marked
check("P4 fabrikkens egen station = ikke salgssted", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = true, owningPlaceable = {spec_productionPoint = {}}}), false)
check("P4 staldens egen station = ikke salgssted", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = true, owningPlaceable = {getHusbandryFillLevel = function() end}}), false)
-- P4b: fremmed produktion (kortets mejeri, ejer 0) køber varen = salgssted; gårdens egen fabrik er ikke
check("P4b fremmed mejeri = salgssted", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = true, owningPlaceable = {spec_productionPoint = {}, getOwnerFarmId = function() return 0 end}}), true)
check("P4b egen fabrik (ejer 1) = ikke salgssted", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = true, owningPlaceable = {spec_productionPoint = {}, getOwnerFarmId = function() return 1 end}}), false)
check("P4b fremmed stald-felt ownerFarmId", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = true, owningPlaceable = {getHusbandryFillLevel = function() end, ownerFarmId = 0}}), true)
check("P4 rigtigt salgssted med placeable", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = true, owningPlaceable = {}}), true)
-- P5: uden stationens grundpris: kun kurven tæller (faktor nu ÷ faktor max)
local bare = {getEffectiveFillTypePrice = function() return 470 end}
local quote2 = ADPrices.getQuote(bare, SUGAR)
check("P5 procent = kurven alene", quote2.percent, 80)
check("P5 top = effektiv ÷ kurve", math.floor(quote2.top + 0.5), 588)
-- P7: kurven er grundlaget (Distribution Redux-metoden): faktor nu ÷ årets max, uafhængigt af sværhedsgrad
check("P7 curvePercent", ADPrices.curvePercent({1.0, 1.25, 0.9}, 1), 80)
check("P7 curvePercent ved top", ADPrices.curvePercent({1.0, 1.25, 0.9}, 2), 100)
check("P7 flad kurve = 100", ADPrices.curvePercent({1.0, 1.01, 1.0}, 3), 100)
check("P7 ingen kurve = 100", ADPrices.curvePercent(nil, 1), 100)
check("P7 ukendt periode = 100", ADPrices.curvePercent({1.0, 1.25}, nil), 100)
-- P8: in-game (23/9): effektiv 1602 mod fillTypePrices 505 gav 317 % — stationens grundpris indeholder
-- hverken sværhedsgrad eller prisskala, så forholdet er ubrugeligt. Procenten er kurven alene.
local demandStation = {fillTypePrices = {[SUGAR] = 505}, getEffectiveFillTypePrice = function() return 1602 end}
check("P8 procent uafhængig af stationens grundpris", ADPrices.getQuote(demandStation, SUGAR).percent, 80)
check("P8 top = pris ved 100 % af kurven", math.floor(ADPrices.getQuote(demandStation, SUGAR).top + 0.5), 2003)
-- P9: sværhedsgraden påvirker ikke procenten
local hardStation = {getEffectiveFillTypePrice = function() return 235 end}
check("P9 uafhængig af sværhedsgrad", ADPrices.getQuote(hardStation, SUGAR).percent, 80)
check("P5 fejlende station -> nil", ADPrices.getQuote({getEffectiveFillTypePrice = function() error("x") end}, SUGAR), nil)
-- P6: bedste tilbud = højeste procent, dernæst pris
local best = ADPrices.bestQuote({{percent = 80, price = 1}, {percent = 92, price = 2}, {percent = 92, price = 3}})
check("P6 højeste procent", best.percent, 92)
check("P6 ved lighed højeste pris", best.price, 3)
check("P6 tom -> nil", ADPrices.bestQuote({}), nil)
-- P-land: kortanlæg der følger jorden (boughtWithFarmland) er gårdens, når jorden under dem er gårdens —
-- selv om placeable-ejeren står som 15 (Dakotas BGA500kw på jordlod 69)
getWorldTranslation = getWorldTranslation or function(node) return node.x, 0, node.z end
g_currentMission = g_currentMission or {}
g_currentMission.getFarmId = function() return 1 end
g_farmlandManager = {
getFarmlandIdAtWorldPosition = function(_, x, z) return x < 0 and 69 or 70 end,
getFarmlandOwner = function(_, id) return id == 69 and 1 or 0 end,
}
local bga = {rootNode = {x = -1600, z = -89}, boughtWithFarmland = true, spec_productionPoint = {}, getOwnerFarmId = function() return 15 end}
local bgaOther = {rootNode = {x = 1600, z = -89}, boughtWithFarmland = true, spec_productionPoint = {}, getOwnerFarmId = function() return 15 end}
local dairy = {rootNode = {x = -3173, z = -4028}, spec_productionPoint = {}, getOwnerFarmId = function() return 0 end}
check("P-land BGA på egen jord = egen", ADPrices.isOwnedBy(bga, 1), true)
check("P-land BGA på fremmed jord = ikke egen", ADPrices.isOwnedBy(bgaOther, 1), false)
check("P-land mejeri (ejer 0, ikke jord-bundet) = ikke egen", ADPrices.isOwnedBy(dairy, 1), false)
check("P-land egen ejer", ADPrices.isOwnedBy({getOwnerFarmId = function() return 1 end}, 1), true)
check("P-land BGA er ikke salgssted", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = true, owningPlaceable = bga}), false)
g_farmlandManager = nil
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-42
View File
@@ -1,42 +0,0 @@
-- Kør: luajit tests/test_adProfiler.lua (fra repo-roden)
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
local lines = {}
Logging = {info = function(fmt, ...) table.insert(lines, string.format(fmt, ...)) end}
local clock = 0
function netGetTime() return clock end
dofile("FS25_ADSmartPickup/adProfiler.lua")
local Module = {}
function Module.slow(a, b) clock = clock + 5; return a + b, "ok" end
function Module.fast() clock = clock + 0.1 end
ADProfiler.wrap(Module, "slow", "langsom")
ADProfiler.wrap(Module, "fast", "hurtig")
local sum, text = Module.slow(2, 3)
check("P1 returværdier bevares", sum, 5); check("P1 anden returværdi", text, "ok")
Module.fast()
check("P2 kald talt", ADProfiler.stats["langsom"].calls, 1)
check("P2 max", ADProfiler.stats["langsom"].max, 5)
ADProfiler.frame(0)
check("P3 værste frame", ADProfiler.worstFrameMs, 5.1)
ADProfiler.frame(61000)
check("P4 rapport skrevet", #lines, 1)
check("P4 langsom først", lines[1]:find("langsom max 5.0", 1, true) ~= nil, true)
check("P4 nulstillet", next(ADProfiler.stats), nil)
-- intet over 2 ms -> ingen rapport
Module.fast(); ADProfiler.frame(62000); ADProfiler.frame(130000)
check("P5 stille minut -> ingen linje", #lines, 1)
-- fejl i indpakket funktion: dybden nulstilles næste frame
function Module.boom() error("x") end
ADProfiler.wrap(Module, "boom", "fejl")
pcall(Module.boom)
ADProfiler.frame(131000)
check("P6 dybde nulstillet", ADProfiler.depth, 0)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-385
View File
@@ -1,385 +0,0 @@
-- Kør: luajit tests/test_adRuns.lua (fra repo-roden)
-- Tester run-modellen (rene funktioner) og XML-laget mod en XMLFile-mock.
dofile("FS25_ADSmartPickup/adRuns.lua")
local failures = 0
local function check(name, actual, expected)
if actual == expected then
print("OK " .. name)
else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
-- R1: opret run -> løbenummer, standardværdier; input-store røres ikke
local empty = ADRuns.newStore()
local s1, cow = ADRuns.createRun(empty, "Cow Feed")
check("R1 id 1", cow.id, 1)
check("R1 navn", cow.name, "Cow Feed")
check("R1 auto-varer som standard", cow.ingredientMode, "auto")
check("R1 loops 0", cow.loops, 0)
check("R1 tom Wait-pulje", cow.waitPoolGroup, "")
check("R1 input-store urørt", #empty.runs, 0)
check("R1 nextId tæller op", s1.nextId, 2)
local s2, pig = ADRuns.createRun(s1, "Pig Feed")
check("R1 andet run får id 2", pig.id, 2)
check("R1 to runs", #s2.runs, 2)
-- R2: opdatér felter; ukendt mode ignoreres; loops klippes
local s3 = ADRuns.updateRun(s2, 1, {name = "Cow 1", targetWayPointId = 94, ingredientMode = "manual", ingredients = {"STRAW", "FORAGE"}, waitPoolGroup = "Cow Wait", loops = 150})
local run1 = ADRuns.getRun(s3, 1)
check("R2 navn", run1.name, "Cow 1")
check("R2 mål", run1.targetWayPointId, 94)
check("R2 manual", run1.ingredientMode, "manual")
check("R2 varer", table.concat(run1.ingredients, ","), "STRAW,FORAGE")
check("R2 pulje", run1.waitPoolGroup, "Cow Wait")
check("R2 loops klippet til 99", run1.loops, 99)
check("R2 gammel store urørt", ADRuns.getRun(s2, 1).name, "Cow Feed")
local s3b = ADRuns.updateRun(s3, 1, {ingredientMode = "bogus", loops = -5})
check("R2 ukendt mode ignoreres", ADRuns.getRun(s3b, 1).ingredientMode, "manual")
check("R2 loops under 0 -> 0", ADRuns.getRun(s3b, 1).loops, 0)
check("R2 ukendt run -> uændret", #ADRuns.updateRun(s3, 99, {name = "x"}).runs, 2)
check("R2 targetWayPointId=false rydder målet", ADRuns.getRun(ADRuns.updateRun(s3, 1, {targetWayPointId = false}), 1).targetWayPointId, nil)
check("R2 nil rører ikke målet", ADRuns.getRun(ADRuns.updateRun(s3, 1, {name = "y"}), 1).targetWayPointId, 94)
-- R3: tildel traktor; samme traktor flyttes mellem runs
local s4, prev = ADRuns.assignVehicle(s3, 1, "vehicleA")
check("R3 tildelt", ADRuns.getRun(s4, 1).vehicleIds[1], "vehicleA")
check("R3 ingen tidligere", prev, nil)
check("R3 opslag", ADRuns.findRunByVehicle(s4, "vehicleA").id, 1)
local s5, prev2 = ADRuns.assignVehicle(s4, 2, "vehicleA")
check("R3 flyttet fra run 1", prev2, 1)
check("R3 væk fra run 1", #ADRuns.getRun(s5, 1).vehicleIds, 0)
check("R3 i run 2", ADRuns.getRun(s5, 2).vehicleIds[1], "vehicleA")
local s5b, prev3 = ADRuns.assignVehicle(s5, 2, "vehicleA")
check("R3 gen-tildeling samme run -> ingen dublet", #ADRuns.getRun(s5b, 2).vehicleIds, 1)
check("R3 gen-tildeling -> ingen 'flyttet fra'", prev3, nil)
local s6 = ADRuns.unassignVehicle(s5, "vehicleA")
check("R3 fjernet", ADRuns.findRunByVehicle(s6, "vehicleA"), nil)
check("R3 ukendt run ved tildeling -> uændret", ADRuns.findRunByVehicle(ADRuns.assignVehicle(s6, 42, "vehicleB"), "vehicleB"), nil)
-- R4: slet run
local s7 = ADRuns.deleteRun(s5, 1)
check("R4 ét run tilbage", #s7.runs, 1)
check("R4 det rigtige", s7.runs[1].id, 2)
check("R4 nextId bevares", s7.nextId, 3)
-- R5: records frem og tilbage
local records = ADRuns.toRecords(s5)
check("R5 target som tal", records[1].target, 94)
check("R5 varer som streng", records[1].ingredients, "STRAW FORAGE")
check("R5 køretøjer", records[2].vehicleIds[1], "vehicleA")
local back = ADRuns.fromRecords(records)
check("R5 to runs tilbage", #back.runs, 2)
check("R5 nextId = højeste + 1", back.nextId, 3)
check("R5 varer parset", ADRuns.getRun(back, 1).ingredients[2], "FORAGE")
check("R5 pulje", ADRuns.getRun(back, 1).waitPoolGroup, "Cow Wait")
check("R5 target 0 -> nil", ADRuns.getRun(back, 2).targetWayPointId, nil)
check("R5 køretøj", ADRuns.findRunByVehicle(back, "vehicleA").id, 2)
-- R6: defekte poster springes over; dubletter af id ignoreres
local dirty = ADRuns.fromRecords({
{id = "x", name = "Broken"},
{id = 5, name = "", },
{id = 7, name = "Ok", target = "12", ingredientMode = "weird", ingredients = "", waitPool = nil, loops = "3", vehicleIds = {}},
{id = 7, name = "Dublet"},
})
check("R6 kun den gyldige", #dirty.runs, 1)
check("R6 target parses fra streng", dirty.runs[1].targetWayPointId, 12)
check("R6 ukendt mode -> auto", dirty.runs[1].ingredientMode, "auto")
check("R6 tom varestreng -> tom liste", #dirty.runs[1].ingredients, 0)
check("R6 manglende pulje -> tom streng", dirty.runs[1].waitPoolGroup, "")
check("R6 loops fra streng", dirty.runs[1].loops, 3)
check("R6 nextId", dirty.nextId, 8)
check("R6 tom liste", #ADRuns.fromRecords(nil).runs, 0)
check("R6 parseIngredients trimmer", #ADRuns.parseIngredients(" STRAW FORAGE "), 2)
-- R7: "kør hjem under X %" — standard 50, klippes til 10..90, overlever records
local r7, r7run = ADRuns.createRun(ADRuns.newStore(), "Tærskel")
check("R7 standard 50 %", r7run.returnBelowPercent, 50)
check("R7 sættes", ADRuns.getRun(ADRuns.updateRun(r7, r7run.id, {returnBelowPercent = 30}), r7run.id).returnBelowPercent, 30)
check("R7 klippes til 90", ADRuns.getRun(ADRuns.updateRun(r7, r7run.id, {returnBelowPercent = 150}), r7run.id).returnBelowPercent, 90)
check("R7 klippes til 10", ADRuns.getRun(ADRuns.updateRun(r7, r7run.id, {returnBelowPercent = 0}), r7run.id).returnBelowPercent, 10)
local r7rec = ADRuns.toRecords(ADRuns.updateRun(r7, r7run.id, {returnBelowPercent = 25}))
check("R7 i records", r7rec[1].returnBelow, 25)
check("R7 tilbage fra records", ADRuns.fromRecords(r7rec).runs[1].returnBelowPercent, 25)
check("R7 manglende i gammel fil -> 50", ADRuns.fromRecords({{id = 1, name = "Gammel"}}).runs[1].returnBelowPercent, 50)
-- R8: flere mål pr. run — liste + første som "primært" mål (bagudkompatibelt); toggle; records
local r8, r8run = ADRuns.createRun(ADRuns.newStore(), "Kvæg")
check("R8 ingen mål", #r8run.targetWayPointIds, 0)
r8 = ADRuns.updateRun(r8, r8run.id, {targetWayPointIds = {94, 99}})
check("R8 to mål", table.concat(ADRuns.getRun(r8, r8run.id).targetWayPointIds, ","), "94,99")
check("R8 primært mål = første", ADRuns.getRun(r8, r8run.id).targetWayPointId, 94)
r8 = ADRuns.toggleTarget(r8, r8run.id, 94)
check("R8 toggle fjerner", table.concat(ADRuns.getRun(r8, r8run.id).targetWayPointIds, ","), "99")
check("R8 primært følger med", ADRuns.getRun(r8, r8run.id).targetWayPointId, 99)
r8 = ADRuns.toggleTarget(r8, r8run.id, 120)
check("R8 toggle tilføjer", table.concat(ADRuns.getRun(r8, r8run.id).targetWayPointIds, ","), "99,120")
check("R8 gammel enkelt-sætning = liste med ét", table.concat(ADRuns.getRun(ADRuns.updateRun(r8, r8run.id, {targetWayPointId = 7}), r8run.id).targetWayPointIds, ","), "7")
check("R8 false rydder listen", #ADRuns.getRun(ADRuns.updateRun(r8, r8run.id, {targetWayPointId = false}), r8run.id).targetWayPointIds, 0)
local r8rec = ADRuns.toRecords(r8)
check("R8 records: targets-streng", r8rec[1].targets, "99 120")
check("R8 records: target = første (gammelt felt)", r8rec[1].target, 99)
check("R8 tilbage fra records", table.concat(ADRuns.fromRecords(r8rec).runs[1].targetWayPointIds, ","), "99,120")
check("R8 gammel fil med kun target", table.concat(ADRuns.fromRecords({{id = 1, name = "Gl", target = 55}}).runs[1].targetWayPointIds, ","), "55")
-- ==== XML-laget mod en flad XMLFile-mock (nøgle -> værdi) ====
dofile("FS25_ADSmartPickup/adRunsStorage.lua")
local function makeXmlMock(values)
local xml = {values = values or {}, saved = false, deleted = false}
xml.setInt = function(self, key, value) self.values[key] = value end
xml.setString = function(self, key, value) self.values[key] = value end
xml.getInt = function(self, key) return tonumber(self.values[key]) end
xml.getString = function(self, key) local v = self.values[key]; return v ~= nil and tostring(v) or nil end
-- iterate: kalder callback for "prefix(0)", "prefix(1)", ... så længe en nøgle med den stamme findes
xml.iterate = function(self, prefix, callback)
local index = 0
while true do
local key = string.format("%s(%d)", prefix, index)
local found = false
for k in pairs(self.values) do
if k:sub(1, #key) == key then found = true break end
end
if not found then return end
callback(index + 1, key)
index = index + 1
end
end
xml.save = function(self) self.saved = true end
xml.delete = function(self) self.deleted = true end
return xml
end
-- X1: records skrives som attributter + vehicle-underelementer
local out = makeXmlMock()
ADRunsStorage.writeRecords(out, ADRuns.toRecords(s5))
check("X1 version", out.values["ADSmartPickupRuns#version"], 3)
check("X1 run 0 navn", out.values["ADSmartPickupRuns.run(0)#name"], "Cow 1")
check("X1 run 0 target", out.values["ADSmartPickupRuns.run(0)#target"], 94)
check("X1 run 0 varer", out.values["ADSmartPickupRuns.run(0)#ingredients"], "STRAW FORAGE")
check("X1 run 0 pulje", out.values["ADSmartPickupRuns.run(0)#waitPool"], "Cow Wait")
check("X1 run 1 køretøj", out.values["ADSmartPickupRuns.run(1).vehicle(0)#id"], "vehicleA")
-- X2: samme data læses tilbage
local records2 = ADRunsStorage.readRecords(makeXmlMock(out.values))
check("X2 to poster", #records2, 2)
check("X2 id", records2[1].id, 1)
check("X2 køretøj", records2[2].vehicleIds[1], "vehicleA")
check("X2 loops", records2[1].loops, 99)
check("X2 round-trip", ADRuns.findRunByVehicle(ADRuns.fromRecords(records2), "vehicleA").id, 2)
-- X3: save/load via XMLFile-globalen; ingen savegame-mappe -> tom store, intet skrives
Logging = Logging or {warning = function() end, info = function() end}
local created, loaded = nil, nil
XMLFile = {
create = function(_, path, root) created = makeXmlMock(); created.path = path; created.root = root; return created end,
load = function(_, path) loaded = makeXmlMock(created and created.values or {}); loaded.path = path; return loaded end,
}
fileExists = function(path) return created ~= nil and created.path == path end
g_currentMission = {missionInfo = {savegameDirectory = "/tmp/sg1"}}
check("X3 gemt", ADRunsStorage.save(s5), true)
check("X3 sti", created.path, "/tmp/sg1/ADSmartPickup_runs.xml")
check("X3 rodelement", created.root, "ADSmartPickupRuns")
check("X3 save+delete kaldt", created.saved and created.deleted, true)
check("X3 indlæst", #ADRunsStorage.load().runs, 2)
check("X3 læse-handle lukket", loaded.deleted, true)
g_currentMission = {missionInfo = {}}
check("X3 uden savegame -> false", ADRunsStorage.save(s5), false)
check("X3 uden savegame -> tom store", #ADRunsStorage.load().runs, 0)
g_currentMission = {missionInfo = {savegameDirectory = "/tmp/sg-ny"}}
check("X3 fil mangler -> tom store", #ADRunsStorage.load().runs, 0)
-- R-v2: udkørsels-run — kind, kilder, varer med politik, tærskler, runde; XML v2 + v1-migration
local o1, out = ADRuns.createRun(ADRuns.newStore(), "Sugar out", "outbound")
check("Rv2 kind", out.kind, "outbound")
local _, plain = ADRuns.createRun(ADRuns.newStore(), "x")
check("Rv2 supply som standard", plain.kind, "supply")
check("Rv2 sellAt 90", out.sellAtPercent, 90)
check("Rv2 storeAbove 80", out.storeAbovePercent, 80)
check("Rv2 runde fra", out.collectRound, false)
local o2 = ADRuns.toggleSource(o1, out.id, 12694)
o2 = ADRuns.toggleSource(o2, out.id, 12386)
check("Rv2 to kilder", table.concat(ADRuns.getRun(o2, out.id).sourceWayPointIds, ","), "12694,12386")
check("Rv2 toggle fjerner", #ADRuns.getRun(ADRuns.toggleSource(o2, out.id, 12694), out.id).sourceWayPointIds, 1)
local o3 = ADRuns.setProductPolicy(o2, out.id, "SUGAR", "sell")
o3 = ADRuns.setProductPolicy(o3, out.id, "FORAGE", "store")
check("Rv2 politik SUGAR", ADRuns.getProductPolicy(ADRuns.getRun(o3, out.id), "SUGAR"), "sell")
check("Rv2 politik ukendt = off", ADRuns.getProductPolicy(ADRuns.getRun(o3, out.id), "STRAW"), "off")
check("Rv2 ugyldig politik ignoreres", ADRuns.getProductPolicy(ADRuns.getRun(ADRuns.setProductPolicy(o3, out.id, "SUGAR", "bogus"), out.id), "SUGAR"), "sell")
check("Rv2 off fjerner", ADRuns.getProductPolicy(ADRuns.getRun(ADRuns.setProductPolicy(o3, out.id, "SUGAR", "off"), out.id), "SUGAR"), "off")
check("Rv2 gammel store urørt", #ADRuns.getRun(o2, out.id).products, 0)
local o4 = ADRuns.updateRun(o3, out.id, {sellAtPercent = 120, storeAbovePercent = 10, collectRound = true})
check("Rv2 sellAt klippet 100", ADRuns.getRun(o4, out.id).sellAtPercent, 100)
check("Rv2 storeAbove klippet 50", ADRuns.getRun(o4, out.id).storeAbovePercent, 50)
check("Rv2 runde til", ADRuns.getRun(o4, out.id).collectRound, true)
check("Rv2 serialiser", ADRuns.serializeProducts(ADRuns.getRun(o3, out.id).products), "SUGAR:sell FORAGE:store")
local parsed = ADRuns.parseProducts("SUGAR:sell FORAGE:store BAD BAD:bogus")
check("Rv2 parse antal", #parsed, 2)
check("Rv2 parse politik", parsed[2].policy, "store")
local rt = ADRuns.fromRecords(ADRuns.toRecords(o4))
local rtRun = ADRuns.getRun(rt, out.id)
check("Rv2 roundtrip kind", rtRun.kind, "outbound")
check("Rv2 roundtrip kilder", table.concat(rtRun.sourceWayPointIds, ","), "12694,12386")
check("Rv2 roundtrip products", ADRuns.serializeProducts(rtRun.products), "SUGAR:sell FORAGE:store")
check("Rv2 roundtrip sellAt", rtRun.sellAtPercent, 100)
check("Rv2 roundtrip runde", rtRun.collectRound, true)
local v1 = ADRuns.fromRecords({{id = 7, name = "Old", target = 94, targets = "94", ingredientMode = "auto", ingredients = "", waitPool = "", loops = 0, returnBelow = 50, vehicleIds = {}}})
check("Rv2 v1 -> supply", ADRuns.getRun(v1, 7).kind, "supply")
check("Rv2 v1 tomme kilder", #ADRuns.getRun(v1, 7).sourceWayPointIds, 0)
check("Rv2 v1 sellAt standard", ADRuns.getRun(v1, 7).sellAtPercent, 90)
-- XML-laget skriver/læser v2-felterne
do
local written = {}
local xml = {
setInt = function(_, k, v) written[k] = v end, setString = function(_, k, v) written[k] = v end,
getInt = function(_, k) return written[k] end, getString = function(_, k) return written[k] end,
iterate = function(_, prefix, fn)
local i = 0
while written[string.format("%s(%d)#id", prefix, i)] ~= nil do fn(i, string.format("%s(%d)", prefix, i)); i = i + 1 end
end,
}
ADRunsStorage.writeRecords(xml, ADRuns.toRecords(o4))
check("Rv2 xml version 2", written["ADSmartPickupRuns#version"], 3)
check("Rv2 xml kind", written["ADSmartPickupRuns.run(0)#kind"], "outbound")
check("Rv2 xml sources", written["ADSmartPickupRuns.run(0)#sources"], "12694 12386")
check("Rv2 xml products", written["ADSmartPickupRuns.run(0)#products"], "SUGAR:sell FORAGE:store")
check("Rv2 xml collectRound", written["ADSmartPickupRuns.run(0)#collectRound"], 1)
local back = ADRuns.fromRecords(ADRunsStorage.readRecords(xml))
check("Rv2 xml læs sellAt", ADRuns.getRun(back, out.id).sellAtPercent, 100)
check("Rv2 xml læs runde", ADRuns.getRun(back, out.id).collectRound, true)
end
-- R-v3: bygninger som kilder/mål (id-lister); XML v3; v2-fil uden bygninger giver tomme lister
local b1, flow = ADRuns.createRun(ADRuns.newStore(), "Milk", "outbound")
check("Rv3 tomme kilde-bygninger", #flow.sourceBuildingIds, 0)
check("Rv3 tomme mål-bygninger", #flow.targetBuildingIds, 0)
local b2 = ADRuns.toggleSourceBuilding(b1, flow.id, "barn1")
b2 = ADRuns.toggleSourceBuilding(b2, flow.id, "barn2")
check("Rv3 to kilde-bygninger", table.concat(ADRuns.getRun(b2, flow.id).sourceBuildingIds, ","), "barn1,barn2")
check("Rv3 toggle fjerner", #ADRuns.getRun(ADRuns.toggleSourceBuilding(b2, flow.id, "barn1"), flow.id).sourceBuildingIds, 1)
local b3 = ADRuns.toggleTargetBuilding(b2, flow.id, "mill")
check("Rv3 mål-bygning", ADRuns.getRun(b3, flow.id).targetBuildingIds[1], "mill")
check("Rv3 gammel store urørt", #ADRuns.getRun(b2, flow.id).targetBuildingIds, 0)
local b4 = ADRuns.updateRun(b3, flow.id, {sourceBuildingIds = {"shed"}, targetBuildingIds = {}})
check("Rv3 updateRun sætter lister", ADRuns.getRun(b4, flow.id).sourceBuildingIds[1], "shed")
check("Rv3 updateRun tømmer", #ADRuns.getRun(b4, flow.id).targetBuildingIds, 0)
local rt3 = ADRuns.fromRecords(ADRuns.toRecords(b3))
check("Rv3 roundtrip kilder", table.concat(ADRuns.getRun(rt3, flow.id).sourceBuildingIds, ","), "barn1,barn2")
check("Rv3 roundtrip mål", ADRuns.getRun(rt3, flow.id).targetBuildingIds[1], "mill")
check("Rv3 XML_VERSION", ADRuns.XML_VERSION, 3)
local v2 = ADRuns.fromRecords({{id = 3, name = "V2", kind = "outbound", sources = "12694", products = "SUGAR:sell", vehicleIds = {}}})
check("Rv3 v2 rå markør bevares", ADRuns.getRun(v2, 3).sourceWayPointIds[1], 12694)
check("Rv3 v2 ingen bygninger", #ADRuns.getRun(v2, 3).sourceBuildingIds, 0)
do
local written = {}
local xml = {
setInt = function(_, k, v) written[k] = v end, setString = function(_, k, v) written[k] = v end,
getInt = function(_, k) return written[k] end, getString = function(_, k) return written[k] end,
iterate = function(_, prefix, fn)
local i = 0
while written[string.format("%s(%d)#id", prefix, i)] ~= nil do fn(i, string.format("%s(%d)", prefix, i)); i = i + 1 end
end,
}
ADRunsStorage.writeRecords(xml, ADRuns.toRecords(b3))
check("Rv3 xml version 3", written["ADSmartPickupRuns#version"], 3)
check("Rv3 xml sourceBuildings", written["ADSmartPickupRuns.run(0)#sourceBuildings"], "barn1 barn2")
check("Rv3 xml targetBuildings", written["ADSmartPickupRuns.run(0)#targetBuildings"], "mill")
local back = ADRuns.fromRecords(ADRunsStorage.readRecords(xml))
check("Rv3 xml læs kilder", table.concat(ADRuns.getRun(back, flow.id).sourceBuildingIds, ","), "barn1,barn2")
end
-- RN: køretøjsnavne efter forløbet — "Navn 1", "Navn 2" … i medlemsrækkefølge; ét køretøj = bare navnet
do
local store, runA = ADRuns.createRun(ADRuns.newStore(), "Milk Collect", "outbound")
store = ADRuns.assignVehicle(store, runA.id, "v1")
store = ADRuns.assignVehicle(store, runA.id, "v2")
local runB
store, runB = ADRuns.createRun(store, "Milk Salg", "outbound")
store = ADRuns.assignVehicle(store, runB.id, "v3")
local names = ADRuns.vehicleNames(store)
check("RN to i samme forløb: 1", names.v1, "Milk Collect 1")
check("RN to i samme forløb: 2", names.v2, "Milk Collect 2")
check("RN alene i forløb = navnet", names.v3, "Milk Salg")
check("RN ukendt køretøj", names.v9, nil)
end
-- RS: tvunget lagermål — storeBuildingIds (toggle, kopi, records roundtrip)
do
local store, run = ADRuns.createRun(ADRuns.newStore(), "Slurry", "outbound")
check("RS tom som standard", #run.storeBuildingIds, 0)
store = ADRuns.toggleStoreBuilding(store, run.id, "BGA500kw")
store = ADRuns.toggleStoreBuilding(store, run.id, "tank2")
check("RS to valgt", table.concat(ADRuns.getRun(store, run.id).storeBuildingIds, ","), "BGA500kw,tank2")
store = ADRuns.toggleStoreBuilding(store, run.id, "tank2")
check("RS fravalgt igen", table.concat(ADRuns.getRun(store, run.id).storeBuildingIds, ","), "BGA500kw")
local records = ADRuns.toRecords(store)
check("RS record", records[1].storeBuildings, "BGA500kw")
local back = ADRuns.fromRecords(records)
check("RS roundtrip", table.concat(ADRuns.getRun(back, run.id).storeBuildingIds, ","), "BGA500kw")
end
-- flow-vogne: stoppet + husket vogn pr. traktor følger med records frem og tilbage
local wagonStore = ADRuns.newStore()
local ws, wrun = ADRuns.createRun(wagonStore, "Fodring", "supply")
wrun.stopped, wrun.wagons = true, {vehA = "wagonA"}
local wrecords = ADRuns.toRecords(ws)
check("vogne: record stoppet", wrecords[1].stopped, 1)
check("vogne: record vogne", wrecords[1].wagons, "vehA=wagonA")
local wback = ADRuns.fromRecords(wrecords).runs[1]
check("vogne: stoppet tilbage", wback.stopped, true)
check("vogne: vogn tilbage", wback.wagons.vehA, "wagonA")
local wupdated = ADRuns.getRun(ADRuns.updateRun(ws, wrun.id, {name = "Fodring 2"}), wrun.id)
check("vogne: overlever updateRun", wupdated.wagons.vehA, "wagonA")
check("vogne: stoppet overlever updateRun", wupdated.stopped, true)
-- R4: flyttes en traktor til et andet flow (eller fjernes), glemmes dens vogn i det gamle flow
local ps = ADRuns.newStore()
local ps1, prunA = ADRuns.createRun(ps, "A", "supply")
local ps2, prunB = ADRuns.createRun(ps1, "B", "supply")
ps2 = ADRuns.assignVehicle(ps2, prunA.id, "trac")
ADRuns.getRun(ps2, prunA.id).wagons = {trac = "wag"}
local ps3 = ADRuns.assignVehicle(ps2, prunB.id, "trac")
check("R4 gammel flow glemmer vognen", ADRuns.getRun(ps3, prunA.id).wagons.trac, nil)
local ps4 = ADRuns.unassignVehicle(ps2, "trac")
check("R4 fjernet traktor: vogn glemt", ADRuns.getRun(ps4, prunA.id).wagons.trac, nil)
-- oprydning: traktorer der ikke findes længere (solgt) fjernes fra deres flow — men ikke hvis ALLE mangler
do
local st, r1 = ADRuns.createRun(ADRuns.newStore(), "A", "supply")
st = ADRuns.assignVehicle(st, r1.id, "keep")
st = ADRuns.assignVehicle(st, r1.id, "gone")
local exists = function(id) return id == "keep" end
local pruned, removed = ADRuns.pruneMissingVehicles(st, exists)
check("prune: fjernet", #removed, 1)
check("prune: den rigtige", removed[1].vehicleId, "gone")
check("prune: flowets navn med", removed[1].runName, "A")
check("prune: tilbage", #ADRuns.getRun(pruned, r1.id).vehicleIds, 1)
check("prune: original urørt", #ADRuns.getRun(st, r1.id).vehicleIds, 2)
local _, none = ADRuns.pruneMissingVehicles(st, function() return false end)
check("prune: alle mangler -> intet fjernes", #none, 0)
end
-- beskeder på skærmen efter oprydning: én pr. flow; rød når flowet står uden traktorer
do
local st, r1 = ADRuns.createRun(ADRuns.newStore(), "TMR", "supply")
local st2, r2 = ADRuns.createRun(st, "Solo", "supply")
st2 = ADRuns.assignVehicle(st2, r1.id, "keep")
st2 = ADRuns.assignVehicle(st2, r1.id, "gone1")
st2 = ADRuns.assignVehicle(st2, r2.id, "gone2")
local pruned, removed = ADRuns.pruneMissingVehicles(st2, function(id) return id == "keep" end)
local notes = ADRuns.pruneNotices(pruned, removed)
check("besked: én pr. flow", #notes, 2)
check("besked: TMR har traktorer tilbage", notes[1].runName .. "/" .. notes[1].left .. "/" .. tostring(notes[1].critical), "TMR/1/false")
check("besked: Solo står tom -> rød", notes[2].runName .. "/" .. notes[2].left .. "/" .. tostring(notes[2].critical), "Solo/0/true")
end
-- Start alle: et flow på pause (Stop alt) skal have pausen ophævet, også når dets traktorer allerede kører AD
-- (in-game 29/9: 5 flows blev talt som 'kørte allerede', forblev stoppede, og traktorerne kørte vognen hjem igen)
check("start alle: kører ikke -> i kø", ADRuns.startAllAction(false, true), "queue")
check("start alle: kører ikke, ikke stoppet -> i kø", ADRuns.startAllAction(false, false), "queue")
check("start alle: kører men stoppet -> ophæv pause", ADRuns.startAllAction(true, true), "resume")
check("start alle: kører og ikke stoppet -> intet", ADRuns.startAllAction(true, false), "skip")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-907
View File
@@ -1,907 +0,0 @@
-- Kør: luajit tests/test_adRunsController.lua (fra repo-roden)
-- Controlleren oversætter et run til AutoDrive-tilstand pr. traktor. Mock'er AD + de
-- få ADSmartPickup-opslag den bruger.
local STRAW, TMR, WATER, MILK = 31, 116, 8, 30
local FILL_TYPE_NAMES = {[STRAW] = "STRAW", [TMR] = "FORAGE", [WATER] = "WATER", [MILK] = "MILK"}
local FILL_TYPE_INDEX = {}
for index, name in pairs(FILL_TYPE_NAMES) do FILL_TYPE_INDEX[name] = index end
g_fillTypeManager = {
getFillTypeNameByIndex = function(_, index) return FILL_TYPE_NAMES[index] end,
getFillTypeIndexByName = function(_, name) return FILL_TYPE_INDEX[name] end,
getFillTypes = function()
return {
{index = STRAW, name = "STRAW", title = "Halm"},
{index = TMR, name = "FORAGE", title = "TMR"},
{index = WATER, name = "WATER", title = "Vand"},
{index = MILK, name = "MILK", title = "Mælk"},
}
end,
}
local logLines = {}
Logging = {
info = function(fmt, ...) table.insert(logLines, string.format(fmt, ...)) end,
warning = function(fmt, ...) table.insert(logLines, "WARN " .. string.format(fmt, ...)) end,
}
Utils = {prependedFunction = function(original, extra) return function(...) extra(...); return original(...) end end}
ItemSystem = {save = function() end}
g_currentMission = {getFarmId = function() return 1 end, missionInfo = {}}
-- ---- AutoDrive-mock ----
local RT_NONE, RT_ONLYPICKUP = 1, 2
local MODE_PICKUPANDDELIVER = 2
local vehicles, markers, wayPoints = {}, {}, {}
FS25_AutoDrive = {
AutoDrive = {
RT_NONE = RT_NONE, RT_ONLYPICKUP = RT_ONLYPICKUP, MODE_PICKUPANDDELIVER = MODE_PICKUPANDDELIVER,
settings = {
useFolders = {values = {false, true}, current = 1, isVehicleSpecific = false},
rotateTargets = {values = {1, 2, 3, 4}, current = 1, isVehicleSpecific = true},
ALUnload = {values = {0, 1, 2, 3, 4}, current = 1, isVehicleSpecific = true},
},
getAllVehicles = function() return vehicles end,
getAllUnits = function(vehicle) return vehicle.trailers end,
getAllFillLevels = function(trailers) return trailers.level or 0, trailers.capacity or 0, false, 0 end,
getSupportedFillTypesOfAllUnitsAlphabetically = function(vehicle) return vehicle.carriable or {} end,
},
ADGraphManager = {
getMapMarkers = function() return markers end,
getMapMarkerByWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id then return m end end end,
getWayPointById = function(_, id) return wayPoints[id] end,
getDistanceFromNetwork = function(_, vehicle) return vehicle.networkDistance or 0 end,
},
}
local function addMarker(id, name, group, x, z)
wayPoints[id] = {x = x, z = z, incoming = {}}
table.insert(markers, {id = id, markerIndex = #markers + 1, name = name, group = group})
end
-- ADSmartPickup-opslag: hvilke waypoints har hvilken slags station, og hvad målet tager
local unloadAt, loadAt = {}, {}
ADSmartPickup = {
getFarmId = function(vehicle) return vehicle.farmId end,
getUnloadStationAtWayPointId = function(_, id) return unloadAt[id] end,
getLoadStationAtWayPointId = function(_, id) return loadAt[id] end,
getStationLevelAndCapacity = function(station, fillType)
if station.accepts[fillType] == nil then return nil end
return (station.levels or {})[fillType] or 0, station.accepts[fillType]
end,
-- forenklet rangering: mest fri plads (relativt) først; fulde sidst
getTargetNeedsRanked = function(station, fillTypes)
local ranked = {}
for _, fillType in ipairs(fillTypes) do
local capacity = station.accepts[fillType]
if capacity ~= nil then
local level = (station.levels or {})[fillType] or 0
table.insert(ranked, {fillType = fillType, level = level, capacity = capacity, freeSpace = capacity - level, score = (capacity - level) / capacity})
end
end
table.sort(ranked, function(a, b) return a.score > b.score end)
return ranked
end,
}
ADSupplyPlanner = {isWaitMarker = function(marker) return string.find(string.lower(marker.group or ""), "%f[%a]wait%f[%A]") ~= nil end}
local function makeVehicle(uniqueId, name, carriable, capacity, x, z)
local stateModule = {selectedFillTypes = {}, fillType = 0, active = false, loopCounter = 5, loopsDone = 3, dirtyCount = 0}
stateModule.setMode = function(self, mode) self.mode = mode end
stateModule.setFirstMarkerByWayPointId = function(self, id) self.firstMarkerId = id end
stateModule.setSecondMarkerByWayPointId = function(self, id) self.secondMarkerId = id end
stateModule.setFillType = function(self, fillType) self.fillType = fillType end
stateModule.raiseDirtyFlag = function(self) self.dirtyCount = self.dirtyCount + 1 end
stateModule.setLoopsDone = function(self, n) self.loopsDone = n end
stateModule.isActive = function(self) return self.active end
-- Bruges af C13: getTargetFillTypes skal følge AD's LIVE mål (mock'er AD's eget getSecondMarker).
stateModule.getSecondMarker = function(self) return self.secondMarkerId ~= nil and {id = self.secondMarkerId} or nil end
local vehicle = {
uniqueId = uniqueId, farmId = 1, rootNode = uniqueId,
carriable = carriable, trailers = {level = 0, capacity = capacity},
getName = function() return name end,
getOwnerFarmId = function() return 1 end,
startAutoDrive = function(self) self.ad.stateModule.active = true; self.started = (self.started or 0) + 1 end,
getClosestWayPoint = function() return nil, 0 end,
stopAutoDrive = function(self) self.ad.stateModule.active = false; self.stopped = (self.stopped or 0) + 1 end,
ad = {stateModule = stateModule, settings = {rotateTargets = {values = {1, 2, 3, 4}, current = 1, new = 1, isVehicleSpecific = true}, ALUnload = {values = {0, 1, 2, 3, 4}, current = 1, new = 1, isVehicleSpecific = true}}},
}
nodePositions = nodePositions or {}
nodePositions[uniqueId] = {x = x or 0, z = z or 0}
table.insert(vehicles, vehicle)
return vehicle
end
function getWorldTranslation(node) local p = nodePositions[node]; return p.x, 0, p.z end
dofile("FS25_ADSmartPickup/adRuns.lua")
dofile("FS25_ADSmartPickup/adRunsStorage.lua")
dofile("FS25_ADSmartPickup/adWaitPool.lua")
dofile("FS25_ADSmartPickup/adRunsController.lua")
local adEnv = FS25_AutoDrive
local failures = 0
local function check(name, actual, expected)
if actual == expected then
print("OK " .. name)
else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
local function buildWorld()
vehicles, markers, wayPoints, unloadAt, loadAt, logLines = {}, {}, {}, {}, {}, {}
ADRunsController.store = ADRuns.newStore()
addMarker(94, "Cow 1 Food", "Cow Food", 5000, 0); unloadAt[94] = {accepts = {[STRAW] = 300000, [TMR] = 300000, [WATER] = 50000}}
addMarker(44, "10 Silo Storage", "Siloer", 0, 0); loadAt[44] = {}
addMarker(75, "TMR Load", "Fabrication", 900, 0); loadAt[75] = {}
addMarker(100, "Cow Wait", "Cow Wait", 5100, 0)
addMarker(101, "Cow Wait", "Cow Wait", 5200, 0)
addMarker(200, "F91 Wait", "F91", 8000, 0)
addMarker(300, "Pig Wait", "Pig Wait", 9000, 0)
end
-- C1: køretøjs-id og farmens AD-køretøjer
buildWorld()
local fastracA = makeVehicle("vehicleA", "Fastrac A", {STRAW, TMR}, 1000000, 100, 0)
local fastracB = makeVehicle("vehicleB", "Fastrac B", {STRAW}, 700000, 4000, 0)
local noAd = {uniqueId = "vehicleC", getOwnerFarmId = function() return 1 end}
table.insert(vehicles, noAd)
local otherFarm = makeVehicle("vehicleD", "Nabo", {STRAW}, 1000, 0, 0); otherFarm.getOwnerFarmId = function() return 2 end
check("C1 uniqueId", ADRunsController.getVehicleId(fastracA), "vehicleA")
check("C1 fallback savegame-id", ADRunsController.getVehicleId({currentSavegameId = 7}), "7")
check("C1 intet id -> nil", ADRunsController.getVehicleId({}), nil)
check("C1 farmens AD-køretøjer", #ADRunsController.getFarmVehicles(adEnv), 2)
check("C1 opslag på id", ADRunsController.findVehicleById(adEnv, "vehicleB"), fastracB)
check("C1 ukendt id -> nil", ADRunsController.findVehicleById(adEnv, "nope"), nil)
-- C2: opret run, tildel; opslag run for køretøj; flyt mellem runs
local cow = ADRunsController.createRun("Cow Feed")
check("C2 run oprettet", ADRuns.getRun(ADRunsController.store, cow.id).name, "Cow Feed")
check("C2 ingen tidligere", ADRunsController.assignVehicle(cow.id, fastracA), nil)
check("C2 run for køretøj", ADRunsController.getRunForVehicle(fastracA).id, cow.id)
check("C2 ikke tildelt -> nil", ADRunsController.getRunForVehicle(fastracB), nil)
local pig = ADRunsController.createRun("Pig Feed")
check("C2 flyttet fra Cow", ADRunsController.assignVehicle(pig.id, fastracA), cow.id)
check("C2 nu i Pig", ADRunsController.getRunForVehicle(fastracA).id, pig.id)
ADRunsController.unassignVehicle(fastracA)
check("C2 fjernet", ADRunsController.getRunForVehicle(fastracA), nil)
ADRunsController.deleteRun(pig.id)
check("C2 slettet", ADRuns.getRun(ADRunsController.store, pig.id), nil)
-- C3: Auto-vareliste = vognens varer ∩ målets varer; manual = navne -> indeks (ukendte droppes)
ADRunsController.updateRun(cow.id, {targetWayPointId = 94})
local cowRun = ADRuns.getRun(ADRunsController.store, cow.id)
check("C3 auto: halm+TMR (vand kan vognen ikke bære)", table.concat(ADRunsController.getRunIngredients(adEnv, cowRun, fastracA), ","), STRAW .. "," .. TMR)
check("C3 auto: kun halm for halm-vogn", table.concat(ADRunsController.getRunIngredients(adEnv, cowRun, fastracB), ","), tostring(STRAW))
ADRunsController.updateRun(cow.id, {ingredientMode = "manual", ingredients = {"FORAGE", "UNKNOWN_THING"}})
cowRun = ADRuns.getRun(ADRunsController.store, cow.id)
check("C3 manual: FORAGE -> indeks, ukendt droppes", table.concat(ADRunsController.getRunIngredients(adEnv, cowRun, fastracA), ","), tostring(TMR))
ADRunsController.updateRun(cow.id, {ingredientMode = "auto"})
local noTarget = ADRunsController.createRun("Uden mål")
check("C3 uden mål -> tom liste", #ADRunsController.getRunIngredients(adEnv, ADRuns.getRun(ADRunsController.store, noTarget.id), fastracA), 0)
-- C4: validering
cowRun = ADRuns.getRun(ADRunsController.store, cow.id)
check("C4 ok", ADRunsController.validateVehicle(adEnv, cowRun, fastracA).ok, true)
check("C4 kapacitet", ADRunsController.validateVehicle(adEnv, cowRun, fastracA).capacity, 1000000)
local noTrailer = makeVehicle("vehicleE", "Uden vogn", {}, 0)
check("C4 ingen vogn", ADRunsController.validateVehicle(adEnv, cowRun, noTrailer).reason, "noTrailer")
local milkTruck = makeVehicle("vehicleF", "Mælkebil", {MILK}, 20000)
check("C4 kan ikke bære", ADRunsController.validateVehicle(adEnv, cowRun, milkTruck).reason, "cannotCarry")
fastracB.networkDistance = 31
check("C4 for langt fra vejnettet", ADRunsController.validateVehicle(adEnv, cowRun, fastracB).reason, "farFromNetwork")
fastracB.networkDistance = 0
check("C4 intet mål", ADRunsController.validateVehicle(adEnv, ADRuns.getRun(ADRunsController.store, noTarget.id), fastracA).reason, "noTarget")
ADRunsController.updateRun(noTarget.id, {targetWayPointId = 4242})
check("C4 mål-markør findes ikke", ADRunsController.validateVehicle(adEnv, ADRuns.getRun(ADRunsController.store, noTarget.id), fastracA).reason, "noTarget")
-- C5: Start skriver AD-tilstand og starter; ✖-traktorer springes over
ADRunsController.updateRun(cow.id, {loops = 4, waitPoolGroup = "Cow Wait"})
ADRunsController.assignVehicle(cow.id, fastracA)
ADRunsController.assignVehicle(cow.id, noTrailer)
local results = ADRunsController.startRun(adEnv, cow.id)
check("C5 to resultater", #results, 2)
check("C5 A startet", fastracA.started, 1)
check("C5 A ok", results[1].ok, true)
check("C5 uden vogn sprunget over", noTrailer.started, nil)
check("C5 uden vogn grund", results[2].reason, "noTrailer")
local state = fastracA.ad.stateModule
check("C5 mode P&D", state.mode, MODE_PICKUPANDDELIVER)
check("C5 mål = run'ets mål", state.secondMarkerId, 94)
check("C5 start-pickup = nærmeste læssestation", state.firstMarkerId, 44)
check("C5 varer = auto-listen", table.concat(state.selectedFillTypes, ","), STRAW .. "," .. TMR)
check("C5 fillType = første", state.fillType, STRAW)
check("C5 loops", state.loopCounter, 4)
check("C5 loopsDone nulstillet", state.loopsDone, 0)
check("C5 rotateTargets = kun pålæsning (current)", fastracA.ad.settings.rotateTargets.current, 2)
check("C5 rotateTargets = kun pålæsning (new)", fastracA.ad.settings.rotateTargets.new, 2)
check("C5 useFolders globalt til", adEnv.AutoDrive.settings.useFolders.current, 2)
check("C5 dirty flag", state.dirtyCount > 0, true)
check("C5 status", ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, cow.id)).running, 1)
check("C5 total", ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, cow.id)).total, 2)
check("C5 ukendt run -> tom", #ADRunsController.startRun(adEnv, 999), 0)
-- C6: Stop
check("C6 stoppede", ADRunsController.stopRun(adEnv, cow.id), 1)
check("C6 A stoppet", fastracA.stopped, 1)
check("C6 status 0", ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, cow.id)).running, 0)
check("C6 medlemskab bevaret", ADRunsController.getRunForVehicle(fastracA).id, cow.id)
-- C7: Start på traktor der allerede kører AD (fx startet manuelt) -> stoppes og startes på run'et
fastracA.ad.stateModule.active = true
fastracA.stopped = 0
ADRunsController.startRun(adEnv, cow.id)
check("C7 kørende traktor stoppes først", fastracA.stopped, 1)
check("C7 og startes på run'et", fastracA.started, 2)
check("C7 run markeret som kørende", ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, cow.id)).isRunning, true)
ADRunsController.stopRun(adEnv, cow.id)
check("C7 stop rydder markeringen", ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, cow.id)).isRunning, false)
-- en traktor der kører AD uden at run'et er startet af os, gør IKKE run'et "kørende"
fastracA.ad.stateModule.active = true
check("C7 manuel AD tæller ikke som kørende run", ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, cow.id)).isRunning, false)
-- efter genindlæsning: runningRuns er tom, men flowet er ikke stoppet og traktoren kører -> kørende (Stop-knap)
-- (in-game 29/9: Flows-listen viste [Start] ved flows der kørte)
ADRuns.getRun(ADRunsController.store, cow.id).stopped = false
ADRunsController.runningRuns[cow.id] = nil
check("C7b efter genindlæsning: kørende", ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, cow.id)).isRunning, true)
check("C7b efter genindlæsning: kan stoppes", ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, cow.id)).canStop, true)
ADRuns.getRun(ADRunsController.store, cow.id).stopped = true
check("C7 men aktive tæller stadig", ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, cow.id)).running, 1)
fastracA.ad.stateModule.active = false
-- sprungne traktorer logges med årsag
logLines = {}
ADRunsController.startRun(adEnv, cow.id)
local skippedLogged = false
for _, line in ipairs(logLines) do if line:find("sprunget over", 1, true) and line:find("noTrailer", 1, true) then skippedLogged = true end end
check("C7 sprunget over logges med årsag", skippedLogged, true)
-- kapacitet under 100 l (kun trækkerens egne tanke) = ingen vogn
local tinyCapacity = makeVehicle("vehicleTiny", "T255", {STRAW, TMR}, 1, 100, 0)
check("C7 1 l kapacitet -> noTrailer", ADRunsController.validateVehicle(adEnv, ADRuns.getRun(ADRunsController.store, cow.id), tinyCapacity).reason, "noTrailer")
-- UAL-køretøj: AutoDrive melder kapacitet i PALLER (læssede + 1), så en tom pallebil har "1" — det er ikke "ingen vogn"
adEnv.AutoDrive.hasAL = function(_, unit) return unit.ual == true end
local palletTruck = makeVehicle("vehicleDragon", "Dragon", {}, 1, 100, 0)
palletTruck.trailers[1] = {ual = true}
local truckValidation = ADRunsController.validateVehicle(adEnv, ADRuns.getRun(ADRunsController.store, cow.id), palletTruck)
check("C7 pallebil: ikke noTrailer", truckValidation.reason ~= "noTrailer", true)
check("C7 pallebil: kapacitet i paller markeret", truckValidation.isAutoload, true)
check("C7 pallebil: liter-kapacitet ukendt", truckValidation.capacity, nil)
-- in-game 27/9: en tipvogn med UniversalAutoload slået til tæller hos AutoDrive kun som palle-/ballevogn,
-- så en forsynings-flow med løsvarer afviste den som "kan ikke bære" uden at sige hvorfor
check("C7 AutoLoad slået til på vogn i forsyning -> autoloadOn", truckValidation.reason, "autoloadOn")
adEnv.AutoDrive.hasAL = nil
-- C8: mål-markører (kun stalde og fabrikker — siloer tager alt og er ikke mål; stalde først,
-- så fabrikker, så navn) og Wait-puljer til dropdowns
unloadAt[94].owningPlaceable = {getHusbandryFillLevel = function() return 0 end}
addMarker(96, "8 Silo Storage", "Siloer", 100, 0); unloadAt[96] = {accepts = {[STRAW] = 1}, owningPlaceable = {spec_silo = {}}}
addMarker(97, "TMR Unload", "Fabrication", 900, 0); unloadAt[97] = {accepts = {[STRAW] = 1}, owningPlaceable = {spec_productionPoint = {}}}
addMarker(98, "Bull Food", "Cow Food", 5100, 0); unloadAt[98] = {accepts = {[STRAW] = 1}, owningPlaceable = {getHusbandryFillLevel = function() return 0 end}}
local targets = ADRunsController.getTargetMarkers(adEnv, 1)
check("C8 kun stalde og fabrikker (ikke siloen)", #targets, 3)
check("C8 stalde først, alfabetisk", targets[1].name .. "," .. targets[2].name, "Bull Food,Cow 1 Food")
check("C8 fabrik sidst", targets[3].name, "TMR Unload")
local pools = ADRunsController.getWaitPools(adEnv)
check("C8 to puljer (F91 er ikke en Wait-mappe)", #pools, 2)
check("C8 sorteret", pools[1].group, "Cow Wait")
check("C8 antal", pools[1].count, 2)
-- C9: gem/indlæs; ukendte køretøjs-id'er logges ved indlæsning men beholdes i run'et
local savedRecords
ADRunsStorage.save = function(store) savedRecords = ADRuns.toRecords(store); return true end
ADRunsStorage.load = function() return ADRuns.fromRecords(savedRecords) end
ADRunsController.assignVehicle(cow.id, {uniqueId = "vehicleGONE", ad = {}, getOwnerFarmId = function() return 1 end, getName = function() return "Væk" end})
check("C9 gemt", ADRunsController.save(), true)
ADRunsController.store = ADRuns.newStore()
ADRunsController.load(adEnv)
check("C9 indlæst", ADRuns.getRun(ADRunsController.store, cow.id).name, "Cow Feed")
check("C9 ukendt køretøj logget", logLines[#logLines]:find("vehicleGONE", 1, true) ~= nil, true)
check("C9 run beholder kendte", ADRunsController.getRunForVehicle(fastracA).id, cow.id)
local hooked = false
ItemSystem.save = function() hooked = true end
ADRunsController.installSaveHook()
ItemSystem.save()
check("C9 save-hook kaldes før ItemSystem.save", hooked, true)
-- C11: vogn uden egen ad.settings-tabel (ingen rotateTargets pr. køretøj) -> den
-- køretøjs-specifikke indstilling skrives IKKE til den globale settings-tabel
adEnv.AutoDrive.settings.rotateTargets.current = 1
adEnv.AutoDrive.settings.useFolders.current = 1
local settingsRun = ADRunsController.createRun("Settings Test")
ADRunsController.updateRun(settingsRun.id, {targetWayPointId = 94, loops = 1})
local noSettings = makeVehicle("vehicleH", "Uden AD-settings", {STRAW}, 500000, 200, 0)
noSettings.ad.settings = {}
ADRunsController.assignVehicle(settingsRun.id, noSettings)
ADRunsController.startRun(adEnv, settingsRun.id)
check("C11 global rotateTargets urørt", adEnv.AutoDrive.settings.rotateTargets.current, 1)
check("C11 useFolders global stadig skrevet", adEnv.AutoDrive.settings.useFolders.current, 2)
check("C11 køretøjet startet", noSettings.started, 1)
-- C12: startAutoDrive() kaster for én traktor -> pcall'et applyAndStart fanger fejlen,
-- resultatet markeres ok=false/reason="error", og de ØVRIGE traktorer i run'et startes stadig
-- (fejlen må ikke undslippe startRun-loopet).
buildWorld()
local brokenRun = ADRunsController.createRun("Broken Start")
ADRunsController.updateRun(brokenRun.id, {targetWayPointId = 94, loops = 2})
local broken = makeVehicle("vehicleBroken", "Fejl-traktor", {STRAW, TMR}, 500000, 100, 0)
broken.startAutoDrive = function() error("boom") end
local normal = makeVehicle("vehicleNormal", "Normal-traktor", {STRAW, TMR}, 500000, 200, 0)
ADRunsController.assignVehicle(brokenRun.id, broken)
ADRunsController.assignVehicle(brokenRun.id, normal)
local c12Results = ADRunsController.startRun(adEnv, brokenRun.id)
check("C12 to resultater", #c12Results, 2)
check("C12 fejl-traktor ok=false", c12Results[1].ok, false)
check("C12 fejl-traktor reason=error", c12Results[1].reason, "error")
check("C12 fejl-traktor ikke startet", broken.started, nil)
check("C12 normal-traktor ok=true", c12Results[2].ok, true)
check("C12 normal-traktor startet", normal.started, 1)
-- C13: Auto-listen følger traktorens LIVE AD-mål (stateModule:getSecondMarker()) mens den kører,
-- IKKE run'ets gemte targetWayPointId — ellers afviger listen fra det choosePickup/findSupplyPickup
-- faktisk bruger, hvis brugeren redigerer run'ets Mål mens traktoren kører.
addMarker(95, "Alt Cow Food", "Cow Food", 5300, 0)
unloadAt[95] = {accepts = {[STRAW] = 1000}}
local liveRun = ADRunsController.createRun("Live Target")
ADRunsController.updateRun(liveRun.id, {targetWayPointId = 94})
local liveRunData = ADRuns.getRun(ADRunsController.store, liveRun.id)
local liveVehicle = makeVehicle("vehicleLive", "Live-traktor", {STRAW, TMR}, 500000, 300, 0)
check("C13 inaktiv: auto-liste fra run'ets mål (94)", table.concat(ADRunsController.getRunIngredients(adEnv, liveRunData, liveVehicle), ","), STRAW .. "," .. TMR)
liveVehicle.ad.stateModule.active = true
liveVehicle.ad.stateModule.secondMarkerId = 95
check("C13 aktiv: auto-liste fra AD's live mål (95)", table.concat(ADRunsController.getRunIngredients(adEnv, liveRunData, liveVehicle), ","), tostring(STRAW))
liveVehicle.ad.stateModule.active = false
liveVehicle.ad.stateModule.secondMarkerId = nil
-- C10: målets accepterede varer (til afkrydsning i GUI'et) + køretøjs-label
buildWorld()
local labelA = makeVehicle("vehicleA", "Fastrac A", {STRAW, TMR}, 1000000)
local labelB = makeVehicle("vehicleB", "Fastrac B", {STRAW}, 700000)
local accRun = ADRunsController.createRun("Cow Feed")
check("C10 uden mål -> tom", #ADRunsController.getTargetAcceptedFillTypes(adEnv, ADRuns.getRun(ADRunsController.store, accRun.id)), 0)
ADRunsController.updateRun(accRun.id, {targetWayPointId = 94})
local accepted = ADRunsController.getTargetAcceptedFillTypes(adEnv, ADRuns.getRun(ADRunsController.store, accRun.id))
check("C10 tre varer (halm, TMR, vand)", #accepted, 3)
check("C10 sorteret på titel", accepted[1].title .. "," .. accepted[2].title .. "," .. accepted[3].title, "Halm,TMR,Vand")
check("C10 navn med", accepted[1].name, "STRAW")
check("C10 label", ADRunsController.getVehicleLabel(adEnv, labelB), "Fastrac B (#2)")
-- C14: Start går gennem AD's mode:start(USER_PLAYER) (som HUD'en), ikke kun startAutoDrive
buildWorld()
local modeVehicle = makeVehicle("vehicleM", "Mode-traktor", {STRAW, TMR}, 1000000, 100, 0)
local modeStarts = {}
modeVehicle.ad.stateModule.getCurrentMode = function() return {start = function(_, user) table.insert(modeStarts, user); modeVehicle:startAutoDrive() end} end
modeVehicle.ad.stateModule.setActualFarmId = function(self, farmId) self.actualFarmId = farmId end
adEnv.AutoDrive.USER_PLAYER = 1
local modeRun = ADRunsController.createRun("Mode")
ADRunsController.updateRun(modeRun.id, {targetWayPointId = 94})
ADRunsController.assignVehicle(modeRun.id, modeVehicle)
ADRunsController.startRun(adEnv, modeRun.id)
check("C14 mode:start kaldt med USER_PLAYER", modeStarts[1], 1)
check("C14 traktor aktiv", modeVehicle.started, 1)
check("C14 farm-id sat", modeVehicle.ad.stateModule.actualFarmId, 1)
check("C14 loopsDone nulstillet", modeVehicle.ad.stateModule.loopsDone, 0)
-- C15: aktivitet til overblikket — stoppet / venter / med læs / tom på vej efter vare
ADUnloadWait = {getWaitFor = function(v) return v.waitInfo end, getRestLoad = function(_, v) return v.loadType, v.loadLiters or 0 end}
ADSmartPickup.getActivePickup = function(v) return v.pickupInfo end
buildWorld()
local actVehicle = makeVehicle("vehicleAct", "Akt", {STRAW, TMR}, 1000000, 100, 0)
check("C15 stoppet", ADRunsController.getVehicleActivity(adEnv, actVehicle).kind, "stopped")
actVehicle.ad.stateModule.active = true
actVehicle.ad.stateModule.secondMarkerId = 94
actVehicle.loadType, actVehicle.loadLiters = STRAW, 8000
local loaded = ADRunsController.getVehicleActivity(adEnv, actVehicle)
check("C15 med læs", loaded.kind .. "/" .. loaded.fillType .. "/" .. loaded.liters, "loaded/" .. STRAW .. "/8000")
actVehicle.loadLiters = 0
actVehicle.pickupInfo = {markerName = "Silo 3", fillType = TMR}
local empty = ADRunsController.getVehicleActivity(adEnv, actVehicle)
check("C15 tom på vej efter TMR", empty.kind .. "/" .. tostring(empty.place) .. "/" .. tostring(empty.fillType), "empty/Silo 3/" .. TMR)
actVehicle.waitInfo = {markerName = "Cow Wait", fillType = STRAW, rest = 5000}
check("C15 venter", ADRunsController.getVehicleActivity(adEnv, actVehicle).kind, "waiting")
ADUnloadWait = nil
-- C16: flere mål pr. run — Auto-varer = union; chooseTarget = den stald der mangler mest; validering
buildWorld()
addMarker(99, "Cow 2 Food", "Cow Food", 5200, 0); unloadAt[99] = {accepts = {[STRAW] = 300000, [TMR] = 300000}, levels = {[STRAW] = 0, [TMR] = 0}}
unloadAt[94].levels = {[STRAW] = 250000, [TMR] = 250000, [WATER] = 50000}
local multiVehicle = makeVehicle("vehicleMulti", "Multi", {STRAW, TMR, WATER}, 1000000, 100, 0)
local multi = ADRunsController.createRun("Kvæg")
ADRunsController.updateRun(multi.id, {targetWayPointIds = {94, 99}})
local multiRun = ADRuns.getRun(ADRunsController.store, multi.id)
check("C16 auto-varer = union (vand kun i stald 1)", table.concat(ADRunsController.getRunIngredients(adEnv, multiRun, multiVehicle), ","), WATER .. "," .. STRAW .. "," .. TMR)
check("C16 accepterede varer = union", #ADRunsController.getTargetAcceptedFillTypes(adEnv, multiRun), 3)
check("C16 vælger stald 2 (tom)", ADRunsController.chooseTarget(adEnv, multiRun, multiVehicle), 99)
unloadAt[99].levels = {[STRAW] = 290000, [TMR] = 290000}
check("C16 vælger stald 1 når stald 2 er fuld", ADRunsController.chooseTarget(adEnv, multiRun, multiVehicle), 94)
check("C16 validering ok med flere mål", ADRunsController.validateVehicle(adEnv, multiRun, multiVehicle).ok, true)
-- C16b: en anden vogn er allerede på vej til stald 2 — så vælges stald 1 selvom stald 2 er mindre fyldt
-- (drive-on-bunker: kun én kan tippe ad gangen; in-game 26/9 kørte 3 biogas-vogne til samme drop)
unloadAt[99].levels = {[STRAW] = 100000, [TMR] = 100000}
unloadAt[94].levels = {[STRAW] = 200000, [TMR] = 200000, [WATER] = 50000}
ADSmartPickup.countEnRoute = function(id, exclude) return id == 99 and 1 or 0 end
check("C16b ledigt mål foran mindre fyldt mål med vogn på vej", ADRunsController.chooseTarget(adEnv, multiRun, multiVehicle), 94)
ADSmartPickup.countEnRoute = function() return 1 end
check("C16b lige mange på vej -> mindst fyldte", ADRunsController.chooseTarget(adEnv, multiRun, multiVehicle), 99)
ADSmartPickup.countEnRoute = nil
-- C16c: fyldte traktorer startet i samme run fordeles på målene (in-game 27/9: tre fyldte biogas-traktorer
-- kørte alle til Drop 1, fordi læs der allerede lå på vognen aldrig blev registreret "på vej")
do
unloadAt[99].levels = {[STRAW] = 100000, [TMR] = 100000}
unloadAt[94].levels = {[STRAW] = 200000, [TMR] = 200000, [WATER] = 50000}
local onWay = {}
ADSmartPickup.countEnRoute = function(id, exclude)
local n = 0
for v, target in pairs(onWay) do if v ~= exclude and target == id then n = n + 1 end end
return n
end
ADSmartPickup.registerCargo = function(_, v, targetId) onWay[v] = targetId; return true end
local loadedA = makeVehicle("vehicleLoadedA", "Fyldt A", {STRAW, TMR}, 1000000, 100, 0)
local loadedB = makeVehicle("vehicleLoadedB", "Fyldt B", {STRAW, TMR}, 1000000, 110, 0)
local both = ADRunsController.createRun("To fyldte")
ADRunsController.updateRun(both.id, {targetWayPointIds = {94, 99}})
ADRunsController.assignVehicle(both.id, loadedA)
ADRunsController.assignVehicle(both.id, loadedB)
ADRunsController.startRun(adEnv, both.id)
local a, b = loadedA.ad.stateModule.secondMarkerId, loadedB.ad.stateModule.secondMarkerId
check("C16c første fyldte -> mindst fyldte mål", a, 99)
check("C16c anden fyldte -> det andet mål", b, 94)
ADSmartPickup.countEnRoute, ADSmartPickup.registerCargo = nil, nil
end
ADRunsController.updateRun(multi.id, {targetWayPointIds = {4242}})
check("C16 kun ukendte mål -> noTarget", ADRunsController.validateVehicle(adEnv, ADRuns.getRun(ADRunsController.store, multi.id), multiVehicle).reason, "noTarget")
-- C17: kørende traktor der fjernes fra run stoppes; slettes run, stoppes dets traktorer; Stop logger
buildWorld()
local runVehicle = makeVehicle("vehicleR", "Kørende", {STRAW, TMR}, 1000000, 100, 0)
local stopRun = ADRunsController.createRun("Stop-test")
ADRunsController.updateRun(stopRun.id, {targetWayPointId = 94})
ADRunsController.assignVehicle(stopRun.id, runVehicle)
ADRunsController.startRun(adEnv, stopRun.id)
check("C17 kører", runVehicle.started, 1)
ADRunsController.unassignVehicle(runVehicle)
check("C17 fjernet fra run -> stoppet", runVehicle.stopped, 1)
ADRunsController.assignVehicle(stopRun.id, runVehicle)
ADRunsController.startRun(adEnv, stopRun.id)
ADRunsController.deleteRun(stopRun.id, adEnv)
check("C17 run slettet -> stoppet", runVehicle.stopped, 2)
check("C17 stop logget", logLines[#logLines]:find("stoppet", 1, true) ~= nil, true)
-- C18: forkert last ved Start (halm på vognen, run mod sukkerfabrik) -> første stop = silo der tager halm
ADUnloadWait = {getRestLoad = function(_, v) return v.loadType, v.loadLiters or 0 end, getWaitFor = function() return nil end,
hasRouteBothWays = function() return true end}
ADLoadSwap = {findDumpMarker = function(_, v, fillType, rest, homeId, isReachable) return {id = 44, name = "10 Silo Storage", markerIndex = 2} end}
buildWorld()
addMarker(77, "sugar offload", "Fabrication", 950, 0); unloadAt[77] = {accepts = {[TMR] = 360000}} -- "fabrik" der ikke tager halm (TMR står for roer her)
local sugarVehicle = makeVehicle("vehicleSugar", "Roe-traktor", {STRAW, TMR}, 20000, 100, 0)
sugarVehicle.loadType, sugarVehicle.loadLiters = STRAW, 8000
local sugar = ADRunsController.createRun("Sugar")
ADRunsController.updateRun(sugar.id, {targetWayPointId = 77})
ADRunsController.assignVehicle(sugar.id, sugarVehicle)
ADRunsController.startRun(adEnv, sugar.id)
check("C18 første stop = siloen (aflæs forkert last)", sugarVehicle.ad.stateModule.secondMarkerId, 44)
check("C18 logget", logLines[#logLines]:find("læsser", 1, true) ~= nil or logLines[#logLines - 1]:find("læsser", 1, true) ~= nil, true)
-- rigtig last (TMR=roer) -> direkte til fabrikken
sugarVehicle.loadType = TMR
sugarVehicle.ad.stateModule.active = false
ADRunsController.startRun(adEnv, sugar.id)
check("C18 rigtig last -> målet", sugarVehicle.ad.stateModule.secondMarkerId, 77)
-- C18b: målet TAGER varen, men den er ikke en af run'ets valgte varer (gødning i en ensilage-DOB)
-- -> aflæs i en silo der tager den, ikke i målet
unloadAt[77].accepts = {[TMR] = 360000, [STRAW] = 360000}
ADRunsController.updateRun(sugar.id, {ingredientMode = "manual", ingredients = {"FORAGE"}})
sugarVehicle.loadType = STRAW
sugarVehicle.ad.stateModule.active = false
ADRunsController.startRun(adEnv, sugar.id)
check("C18b forkert vare for run'et -> silo, ikke målet", sugarVehicle.ad.stateModule.secondMarkerId, 44)
-- auto-varer: alt målet tager hører til -> målet
ADRunsController.updateRun(sugar.id, {ingredientMode = "auto", ingredients = {}})
sugarVehicle.ad.stateModule.active = false
ADRunsController.startRun(adEnv, sugar.id)
check("C18b auto: målet tager varen -> målet", sugarVehicle.ad.stateModule.secondMarkerId, 77)
ADUnloadWait = nil; ADLoadSwap = nil
-- C19: Wait-markører er aldrig mål — heller ikke når de står inden for 40 m af staldens trigger
buildWorld()
-- staldens station med bygning; Wait-markøren ligger i triggerens rækkevidde og "mangler mere" end 94
local barnPlaceable = {getHusbandryFillLevel = function() return 0 end}
unloadAt[94].owningPlaceable = barnPlaceable
addMarker(400, "Cow Wait", "Cow Wait", 5005, 0); unloadAt[400] = {accepts = {[STRAW] = 300000, [TMR] = 300000}, levels = {}, owningPlaceable = barnPlaceable}
local targetNames = {}
for _, marker in ipairs(ADRunsController.getTargetMarkers(adEnv, 1)) do table.insert(targetNames, marker.name) end
check("C19 Wait-markør ikke i mål-listen", table.concat(targetNames, ","):find("Cow Wait", 1, true), nil)
local waitTargetRun = ADRunsController.createRun("Wait som mål")
ADRunsController.updateRun(waitTargetRun.id, {targetWayPointIds = {94, 400}})
local waitVehicle = makeVehicle("vehicleW", "W", {STRAW, TMR}, 20000, 5000, 0)
check("C19 chooseTarget springer Wait over", ADRunsController.chooseTarget(adEnv, ADRuns.getRun(ADRunsController.store, waitTargetRun.id), waitVehicle), 94)
-- C-out: udkørsels-runs — oprettelse, kilder, produkter, validering, start, status
local BASKET = 7
FILL_TYPE_NAMES[BASKET] = "STRAW_BASKET"; FILL_TYPE_INDEX["STRAW_BASKET"] = BASKET
local sourcesAt, productsAt = {}, {}
ADSources = {
identify = function(_, id) return sourcesAt[id] end,
getProducts = function(_, source) return productsAt[source.wayPointId] or {} end,
getSellMarkers = function() return {} end,
stationAccepts = function() return false end,
getSourcePlaceables = function() return {} end,
}
local outboundStatus = {reasons = {"SUGAR 78 % af top (kræver 90), lager 41 %"}, warning = nil}
local holdFor = {}
ADOutbound = {
isOutboundVehicle = function(v) local r = ADRunsController.getRunForVehicle(v) return r ~= nil and r.kind == "outbound" end,
getRunStatus = function() return outboundStatus end,
getHoldFor = function(v) return holdFor[v] end,
getTripFor = function() return nil end,
releaseInactive = function() end,
}
ADUnloadWait = {getRestLoad = function() return nil, 0 end, getWaitFor = function() return nil end}
buildWorld()
addMarker(1, "Suger Load", "Pickup points", 100, 0); loadAt[1] = {}
addMarker(3, "Objektlager", "Pickup points", 300, 0)
addMarker(10, "selling station", "selling", 500, 0)
sourcesAt[1] = {kind = "factory", wayPointId = 1, station = loadAt[1], name = "Sugar mill"}
sourcesAt[3] = {kind = "objectStorage", wayPointId = 3, name = "Objektlager"}
productsAt[1] = {{fillType = TMR, name = "FORAGE", title = "TMR", available = 150000, level = 150000, capacity = 180000, isPallet = false}}
productsAt[3] = {{fillType = BASKET, name = "STRAW_BASKET", title = "Halmkurve", available = 8000, level = 8, capacity = 4000, isPallet = true}}
local bulkTractor = makeVehicle("vehicleBulk", "MT635", {STRAW, TMR}, 20000, 100, 0)
local ualTractor = makeVehicle("vehicleUal", "Fendt", {}, 8000, 300, 0)
ualTractor.trailers[1] = {ual = true}
adEnv.AutoDrive.hasAL = function(_, trailer) return trailer.ual == true end
local out = ADRunsController.createRun("Sugar out", "outbound")
check("Cout1 kind", ADRuns.getRun(ADRunsController.store, out.id).kind, "outbound")
check("Cout1 supply-run har kind supply", ADRunsController.createRun("Cows").kind, "supply")
check("Cout1 kilde-markører", #ADRunsController.getSourceMarkers(adEnv, 1), 2)
check("Cout1 første kilde-markør", ADRunsController.getSourceMarkers(adEnv, 1)[1].marker.name, "Objektlager")
-- Cout2: validering
local outRun = ADRuns.getRun(ADRunsController.store, out.id)
-- diagnose: hvad moden ser på enhederne, når ingen autoload findes (logges én gang pr. traktor,
-- første gang varerne slås op — dvs. allerede ved noProducts-tjekket nedenfor)
logLines = {}
bulkTractor.trailers[1] = {spec_universalAutoload = {isAutoloadAvailable = false, autoloadDisabled = true}, getName = function() return "Vogn" end}
check("Cout2 ingen kilder -> noSource", ADRunsController.validateVehicle(adEnv, outRun, bulkTractor).reason, "noSource")
ADRunsController.store = ADRuns.toggleSource(ADRunsController.store, out.id, 1)
ADRunsController.store = ADRuns.toggleSource(ADRunsController.store, out.id, 3)
outRun = ADRuns.getRun(ADRunsController.store, out.id)
check("Cout2 ingen vare valgt -> noProducts", ADRunsController.validateVehicle(adEnv, outRun, bulkTractor).reason, "noProducts")
ADRunsController.store = ADRuns.setProductPolicy(ADRunsController.store, out.id, "STRAW_BASKET", "sell")
outRun = ADRuns.getRun(ADRunsController.store, out.id)
check("Cout2 kun paller uden UAL -> needsAutoload", ADRunsController.validateVehicle(adEnv, outRun, bulkTractor).reason, "needsAutoload")
local diag = nil
for _, line in ipairs(logLines) do if line:find("ingen autoload", 1, true) then diag = line end end
check("Cout2 diagnose logges", diag ~= nil, true)
check("Cout2 diagnose viser UAL-flag", diag ~= nil and diag:find("isAutoloadAvailable=false", 1, true) ~= nil, true)
logLines = {}
ADRunsController.validateVehicle(adEnv, outRun, bulkTractor)
check("Cout2 diagnose kun én gang", #logLines, 0)
bulkTractor.trailers[1] = nil
check("Cout2 UAL-vogn ok", ADRunsController.validateVehicle(adEnv, outRun, ualTractor).ok, true)
ADRunsController.store = ADRuns.setProductPolicy(ADRunsController.store, out.id, "FORAGE", "store")
outRun = ADRuns.getRun(ADRunsController.store, out.id)
check("Cout2 bulk-vogn ok med TMR", ADRunsController.validateVehicle(adEnv, outRun, bulkTractor).ok, true)
local strawOnly = makeVehicle("vehicleStraw", "Halm", {STRAW}, 20000, 100, 0)
check("Cout2 kan ikke bære -> cannotCarry", ADRunsController.validateVehicle(adEnv, outRun, strawOnly).reason, "cannotCarry")
-- Cout3: start sætter mode, 1./2. markør = første kilde, varevalg = det vognen kan bære af run'ets varer
ADRunsController.assignVehicle(out.id, bulkTractor)
ADRunsController.assignVehicle(out.id, ualTractor)
local results = ADRunsController.startRun(adEnv, out.id)
check("Cout3 to startet", #results, 2)
check("Cout3 mode P&D", bulkTractor.ad.stateModule.mode, MODE_PICKUPANDDELIVER)
check("Cout3 1. markør = første kilde", bulkTractor.ad.stateModule.firstMarkerId, 1)
check("Cout3 2. markør = første kilde (pladsholder)", bulkTractor.ad.stateModule.secondMarkerId, 1)
check("Cout3 bulk-vogn: kun TMR", table.concat(bulkTractor.ad.stateModule.selectedFillTypes, ","), tostring(TMR))
check("Cout3 UAL-vogn: kurve", table.concat(ualTractor.ad.stateModule.selectedFillTypes, ","), tostring(BASKET))
check("Cout3 rotation kun pålæsning", bulkTractor.ad.settings.rotateTargets.current, 2)
check("Cout3 startet", bulkTractor.started, 1)
-- AD's "AL unload" er slået FRA som standard (values[1] = 0): autoload-vognen ville aldrig læsse af
-- ved salgsstedet. Ved Start sættes den til "center" hvis den er fra; et brugervalg (left/right/behind) røres ikke.
check("Cout3 ALUnload sat til center for autoload-bil", ualTractor.ad.settings.ALUnload.current, 2)
check("Cout3 ALUnload rører ikke tipvogn", bulkTractor.ad.settings.ALUnload.current, 1)
ualTractor.ad.settings.ALUnload.current = 4
ualTractor.ad.stateModule.active = false
ADRunsController.startRun(adEnv, out.id)
check("Cout3 ALUnload brugervalg bevares", ualTractor.ad.settings.ALUnload.current, 4)
-- Cout4: produkter til fanen (politik slået op, sorteret på titel) og status
local list = ADRunsController.getRunProducts(adEnv, outRun)
check("Cout4 to produkter", #list, 2)
check("Cout4 første = Halmkurve", list[1].title, "Halmkurve")
check("Cout4 politik", list[1].policy, "sell")
check("Cout4 TMR lager", list[2].policy, "store")
check("Cout4 available", list[2].available, 150000)
local st = ADRunsController.getOutboundStatus(adEnv, outRun)
check("Cout4 status-begrundelse", st.reasons[1], "SUGAR 78 % af top (kræver 90), lager 41 %")
holdFor[bulkTractor] = {markerName = "Wait 3", reasons = {"SUGAR 78 % af top (kræver 90), lager 41 %"}}
local activity = ADRunsController.getVehicleActivity(adEnv, bulkTractor)
check("Cout4 aktivitet = holder", activity.kind, "holding")
check("Cout4 aktivitet sted", activity.place, "Wait 3")
check("Cout4 udkørsel har ingen mål", #ADRunsController.getRunTargets(outRun), 0)
-- Cout5 (I2): Start kræver et Wait-punkt i puljen; Cout6 (I3): last på vognen ved Start får eget mål
ADSupplyPlanner.isWaitMarker = function(marker) return string.find(string.lower(marker.group or ""), "%f[%a]wait%f[%A]") ~= nil end
local noWait = ADRunsController.createRun("No wait", "outbound")
ADRunsController.store = ADRuns.toggleSource(ADRunsController.store, noWait.id, 1)
ADRunsController.store = ADRuns.setProductPolicy(ADRunsController.store, noWait.id, "FORAGE", "store")
ADRunsController.store = ADRuns.updateRun(ADRunsController.store, noWait.id, {waitPoolGroup = "Pig Wait"})
ADRunsController.store = ADRuns.updateRun(ADRunsController.store, noWait.id, {waitPoolGroup = "Findes ikke"})
local noWaitRun = ADRuns.getRun(ADRunsController.store, noWait.id)
check("Cout5 ingen Wait-punkt i puljen -> noWaitPoint", ADRunsController.validateVehicle(adEnv, noWaitRun, bulkTractor).reason, "noWaitPoint")
ADRunsController.store = ADRuns.updateRun(ADRunsController.store, noWait.id, {waitPoolGroup = "Cow Wait"})
noWaitRun = ADRuns.getRun(ADRunsController.store, noWait.id)
check("Cout5 Wait-punkt findes -> ok", ADRunsController.validateVehicle(adEnv, noWaitRun, bulkTractor).ok, true)
ADUnloadWait.getRestLoad = function(_, v) return v.loadType, v.loadLiters or 0 end
ADOutbound.planCargoTarget = function(adEnv, run, vehicle, fillType) return fillType == TMR and 10 or nil end
bulkTractor.loadType, bulkTractor.loadLiters = TMR, 8000
ADRunsController.assignVehicle(noWait.id, bulkTractor)
bulkTractor.ad.stateModule.active = false
ADRunsController.startRun(adEnv, noWait.id)
check("Cout6 last på vognen -> 2. markør = lastens mål", bulkTractor.ad.stateModule.secondMarkerId, 10)
bulkTractor.loadType = STRAW
check("Cout6 last uden mål -> cargoNoTarget", ADRunsController.validateVehicle(adEnv, noWaitRun, bulkTractor).reason, "cargoNoTarget")
bulkTractor.loadType = nil
ADUnloadWait = nil; ADOutbound = nil; ADSources = nil
-- C-flow: bygninger -> markører (resolveMarkers), migration af gamle markør-runs, noMarker, flow-rækker
local buildings = {
{id = "barn1", name = "Kostald 1", kind = "husbandry", loadMarker = {id = 10, name = "cow 1 milk", distance = 5}, loadMarkers = {{id = 10, name = "cow 1 milk", distance = 5}, {id = 11, name = "cow 1 slurry", distance = 3}}, unloadMarker = {id = 94, name = "Cow 1 Food", distance = 8}, canBeSource = true, canBeTarget = true, canSell = false},
{id = "shed", name = "Palleskur", kind = "objectStorage", loadMarker = nil, unloadMarker = nil, canBeSource = true, canBeTarget = false, canSell = false},
{id = "mill", name = "Sukkerfabrik", kind = "factory", loadMarker = {id = 12694, name = "Suger Load", distance = 3}, unloadMarker = {id = 77, name = "sugar offload", distance = 4}, canBeSource = true, canBeTarget = true, canSell = false},
}
ADBuildings = {
list = function() return buildings end,
find = function(_, _, id) for _, b in ipairs(buildings) do if b.id == id then return b end end end,
byMarker = function(_, _, wayPointId, role)
for _, b in ipairs(buildings) do
if role == "unload" then
if b.unloadMarker ~= nil and b.unloadMarker.id == wayPointId then return b end
else
for _, m in ipairs(b.loadMarkers or {b.loadMarker}) do if m ~= nil and m.id == wayPointId then return b end end
end
end
end,
}
ADSources = {identify = function(_, id) return (id == 10 or id == 12694 or id == 555) and {kind = "x", wayPointId = id, name = "k"} or nil end, getProducts = function() return {} end, getSellMarkers = function() return {} end, stationAccepts = function() return false end, getSourcePlaceables = function() return {} end}
ADOutbound = {isOutboundVehicle = function() return false end, getRunStatus = function() return {reasons = {"SUGAR 78 % af top"}} end, getHoldFor = function() return nil end, getTripFor = function() return nil end, releaseInactive = function() end}
buildWorld()
addMarker(10, "cow 1 milk", "Milk", 100, 0); loadAt[10] = {}
addMarker(555, "raa markoer", "Pickup", 200, 0); loadAt[555] = {}
addMarker(77, "sugar offload", "production", 950, 0); unloadAt[77] = {accepts = {[TMR] = 360000}}
-- CF1: udkørsel: bygninger + rå markør -> sourceWayPointIds; manglende markør rapporteres
local flowOut = ADRunsController.createRun("Milk", "outbound")
ADRunsController.updateRun(flowOut.id, {sourceBuildingIds = {"barn1", "shed"}, sourceWayPointIds = {555}})
local resolved = ADRunsController.resolveMarkers(adEnv, ADRuns.getRun(ADRunsController.store, flowOut.id))
check("CF1 kildemarkører = ALLE bygningens læssemarkører + rå", table.concat(ADRuns.getRun(ADRunsController.store, flowOut.id).sourceWayPointIds, ","), "10,11,555")
check("CF1 manglende markør", resolved.missing[1], "Palleskur")
-- en markør der HØRER til en bygning som ikke længere er valgt, fjernes (den er ikke rå)
ADRunsController.updateRun(flowOut.id, {sourceWayPointIds = {12694, 555}})
ADRunsController.resolveMarkers(adEnv, ADRuns.getRun(ADRunsController.store, flowOut.id))
check("CF1 fravalgt bygnings markør fjernes", table.concat(ADRuns.getRun(ADRunsController.store, flowOut.id).sourceWayPointIds, ","), "10,11,555")
-- CF1b: gamle rå markører ryddes: ikke-kilde (identify = nil) og markør der hører til en valgt bygning
ADSources.identify = function(_, id)
if id == 10 or id == 12694 or id == 555 then return {kind = "x", wayPointId = id, name = "k"} end
if id == 556 then return {kind = "husbandry", wayPointId = id, name = "Kostald 1", placeable = "barn1-placeable"} end
return nil
end
buildings[1].placeable = "barn1-placeable"
ADRunsController.updateRun(flowOut.id, {sourceBuildingIds = {"barn1"}, sourceWayPointIds = {10, 11, 555, 556, 999}})
logLines = {}
ADRunsController.resolveMarkers(adEnv, ADRuns.getRun(ADRunsController.store, flowOut.id))
check("CF1b rå ikke-kilde og dublet-bygning fjernet", table.concat(ADRuns.getRun(ADRunsController.store, flowOut.id).sourceWayPointIds, ","), "10,11,555")
local pruneLogged = false
for _, line in ipairs(logLines) do if line:find("gamle markører fjernet", 1, true) then pruneLogged = true end end
check("CF1b logget", pruneLogged, true)
-- CF1c: et gammelt kilde-id der ikke længere er en AD-MARKØR (AutoDrive har omnummereret waypoints)
-- fjernes, selv om punktet ligger ved en kilde — ellers kører traktoren til et tilfældigt vejpunkt
local identifyBefore = ADSources.identify
ADSources.identify = function(adEnv_, id, farmId)
if id == 10873 then return {kind = "husbandry", wayPointId = id, name = "Anden stald", placeable = "anden-stald"} end
return identifyBefore(adEnv_, id, farmId)
end
ADRunsController.updateRun(flowOut.id, {sourceBuildingIds = {"barn1"}, sourceWayPointIds = {10, 11, 555, 10873}})
ADRunsController.resolveMarkers(adEnv, ADRuns.getRun(ADRunsController.store, flowOut.id))
check("CF1c vejpunkt uden markør fjernet", table.concat(ADRuns.getRun(ADRunsController.store, flowOut.id).sourceWayPointIds, ","), "10,11,555")
ADSources.identify = identifyBefore
-- CF2: forsyning: mål-bygninger -> targetWayPointIds + targetWayPointId
local flowIn = ADRunsController.createRun("Foder", "supply")
ADRunsController.updateRun(flowIn.id, {targetBuildingIds = {"barn1", "mill"}})
ADRunsController.resolveMarkers(adEnv, ADRuns.getRun(ADRunsController.store, flowIn.id))
check("CF2 målmarkører", table.concat(ADRunsController.getRunTargets(ADRuns.getRun(ADRunsController.store, flowIn.id)), ","), "94,77")
check("CF2 primært mål", ADRuns.getRun(ADRunsController.store, flowIn.id).targetWayPointId, 94)
-- CF2b: gamle rå mål der ikke er en markør (slettet i AD) eller hører til en valgt bygning, ryddes
addMarker(78, "Anden stald food", "Cow Food", 3000, 0); unloadAt[78] = {accepts = {[TMR] = 1000}, owningPlaceable = "anden-stald"}
unloadAt[11062] = {accepts = {[TMR] = 1000}, owningPlaceable = "anden-stald"} -- waypoint uden markør
ADRunsController.updateRun(flowIn.id, {targetWayPointIds = {94, 77, 11062, 78}})
ADRunsController.resolveMarkers(adEnv, ADRuns.getRun(ADRunsController.store, flowIn.id))
check("CF2b ikke-markør fjernet, rå markør med station beholdt", table.concat(ADRunsController.getRunTargets(ADRuns.getRun(ADRunsController.store, flowIn.id)), ","), "94,77,78")
ADRunsController.updateRun(flowIn.id, {targetWayPointIds = {94, 77}})
-- CF3: migration: gamle markør-runs får bygninger
local oldOut = ADRunsController.createRun("Gammel ud", "outbound")
ADRunsController.updateRun(oldOut.id, {sourceWayPointIds = {12694, 555}})
local oldIn = ADRunsController.createRun("Gammel ind", "supply")
ADRunsController.updateRun(oldIn.id, {targetWayPointIds = {94}})
ADRunsController.migrateBuildings(adEnv)
check("CF3 ud: markør -> bygning", ADRuns.getRun(ADRunsController.store, oldOut.id).sourceBuildingIds[1], "mill")
check("CF3 ud: rå markør uden bygning beholdes", table.concat(ADRuns.getRun(ADRunsController.store, oldOut.id).sourceWayPointIds, ","), "12694,555")
check("CF3 ind: markør -> bygning", ADRuns.getRun(ADRunsController.store, oldIn.id).targetBuildingIds[1], "barn1")
ADRunsController.migrateBuildings(adEnv)
check("CF3 migration er idempotent", #ADRuns.getRun(ADRunsController.store, oldOut.id).sourceBuildingIds, 1)
-- CF4: validering: valgt bygning uden markør -> noMarker
local cfVehicle = makeVehicle("vehicleCF", "CF", {STRAW, TMR}, 20000, 100, 0)
ADRunsController.updateRun(flowOut.id, {sourceBuildingIds = {"shed"}, sourceWayPointIds = {}})
local cfValidation = ADRunsController.validateVehicle(adEnv, ADRuns.getRun(ADRunsController.store, flowOut.id), cfVehicle)
check("CF4 noMarker", cfValidation.reason, "noMarker")
check("CF4 hvilken bygning", cfValidation.missingBuilding, "Palleskur")
-- CF4b: ÉN bygning uden markør blandt flere med markør -> start alligevel, advar om den ene
ADRunsController.updateRun(flowOut.id, {sourceBuildingIds = {"barn1", "shed"}, sourceWayPointIds = {}})
logLines = {}
local cfValidation2 = ADRunsController.validateVehicle(adEnv, ADRuns.getRun(ADRunsController.store, flowOut.id), cfVehicle)
check("CF4b ikke afvist pga. markør", cfValidation2.reason ~= "noMarker", true)
local warned = false
for _, line in ipairs(logLines) do if line:find("Palleskur", 1, true) and line:find("springes over", 1, true) then warned = true end end
check("CF4b advarsel nævner bygningen", warned, true)
-- CF5: flow-række til tavlen
ADRunsController.updateRun(flowOut.id, {sourceBuildingIds = {"barn1"}, products = {{fillType = "SUGAR", policy = "sell"}, {fillType = "FORAGE", policy = "store"}}, collectRound = true})
ADRunsController.assignVehicle(flowOut.id, cfVehicle)
local row = ADRunsController.getFlowRow(adEnv, ADRuns.getRun(ADRunsController.store, flowOut.id))
check("CF5 vare-navne", table.concat(row.productNames, ","), "SUGAR,FORAGE")
check("CF5 fra", row.fromNames[1], "Kostald 1")
check("CF5 politikker", table.concat(row.policies, ","), "sell,store")
check("CF5 traktor", row.vehicleNames[1], "CF")
check("CF5 begrundelse", row.reason, "SUGAR 78 % af top")
check("CF5 runde", row.collectRound, true)
ADRunsController.updateRun(flowOut.id, {sourceBuildingIds = {"barn1", "shed"}})
check("CF5 manglende bygning markeres", ADRunsController.getFlowRow(adEnv, ADRuns.getRun(ADRunsController.store, flowOut.id)).missing[1], "Palleskur")
local rowIn = ADRunsController.getFlowRow(adEnv, ADRuns.getRun(ADRunsController.store, flowIn.id))
check("CF5 forsyning fra = alle siloer", rowIn.fromAllSilos, true)
check("CF5 forsyning til", table.concat(rowIn.toNames, ","), "Kostald 1,Sukkerfabrik")
check("CF5 forsyning auto-varer", rowIn.productAuto, true)
-- SB: "Lager til" — valgte bygninger -> deres aflæsningsmarkører (i valgt rækkefølge; ukendte udelades)
do
local sbRun = ADRunsController.createRun("Gylle", "outbound")
ADRunsController.updateRun(sbRun.id, {storeBuildingIds = {"mill", "findes-ikke", "barn1"}})
check("SB markører", table.concat(ADRunsController.getStoreMarkers(adEnv, ADRuns.getRun(ADRunsController.store, sbRun.id)), ","), "77,94")
ADRunsController.updateRun(sbRun.id, {storeBuildingIds = {}})
check("SB tom = ingen tvang", #ADRunsController.getStoreMarkers(adEnv, ADRuns.getRun(ADRunsController.store, sbRun.id)), 0)
end
-- CS: "Lager til" viser kun bygninger der kan tage flowets lagervarer (æg-paller: kun palleskure der tager æg)
do
local EGG, SILAGE = 501, 502
local shed = {id = "shed1", kind = "objectStorage", placeable = {takesEggs = true}}
local shedNo = {id = "shed2", kind = "objectStorage", placeable = {takesEggs = false}}
local silo = {id = "silo9", kind = "silo", placeable = {}, unloadMarker = {id = 901}}
unloadAt[901] = {accepts = {[SILAGE] = 100000}, getIsFillTypeSupported = function(_, ft) return ft == SILAGE end}
local accBefore, storeBefore = ADSources.objectStorageAccepts, ADLoadSwap and ADLoadSwap.isStoreTarget
ADSources.objectStorageAccepts = function(placeable, ft) return ft == EGG and placeable.takesEggs end
ADLoadSwap = ADLoadSwap or {}
ADLoadSwap.isStoreTarget = function(station, ft) return station.getIsFillTypeSupported(station, ft) end
local eggs = {{fillType = EGG, isPallet = true}}
check("CS palleskur der tager æg", ADRunsController.canStoreAt(adEnv, shed, eggs), true)
check("CS palleskur der ikke tager æg", ADRunsController.canStoreAt(adEnv, shedNo, eggs), false)
check("CS silo kan ikke tage æg-paller", ADRunsController.canStoreAt(adEnv, silo, eggs), false)
check("CS silo tager ensilage", ADRunsController.canStoreAt(adEnv, silo, {{fillType = SILAGE, isPallet = false}}), true)
ADSources.objectStorageAccepts = accBefore
ADLoadSwap.isStoreTarget = storeBefore
end
ADBuildings = nil; ADSources = nil; ADOutbound = nil
-- RC-navne: køretøjer får AD-førernavn efter forløbet; ude af forløb -> modelnavnet igen
do
local named = makeVehicle("vehicleNamed", "Virtus 135", {MILK or 1}, 20000, 0, 0)
named.ad.stateModule.driverName = "Virtus 135"
named.ad.stateModule.setName = function(self, n) self.driverName = n end
named.ad.stateModule.getName = function(self) return self.driverName end
local run = ADRunsController.createRun("Mælk ind", "outbound")
ADRunsController.assignVehicle(run.id, named)
ADRunsController.applyVehicleNames(adEnv)
check("RC-navne sat efter forløb", named.ad.stateModule.driverName, "Mælk ind")
check("RC-navne label bruger førernavn", ADRuns.vehicleLabel(named), "Mælk ind")
ADRunsController.store = ADRuns.unassignVehicle(ADRunsController.store, "vehicleNamed")
ADRunsController.applyVehicleNames(adEnv)
check("RC-navne ude af forløb -> modelnavn", named.ad.stateModule.driverName, "Virtus 135")
check("RC-navne label uden AD-navn", ADRuns.vehicleLabel({getName = function() return "Fendt" end}), "Fendt")
end
-- PM: afhentningsmarkører fra udkørsels-forløb (lagerture undgår dem som aflæsningssted)
do
local sale = ADRunsController.createRun("Salg-test", "outbound")
ADRunsController.updateRun(sale.id, {sourceWayPointIds = {4711, 4712}})
local supply = ADRunsController.createRun("Forsyning-test", "supply")
ADRunsController.updateRun(supply.id, {sourceWayPointIds = {9999}})
local pickups = ADRunsController.getPickupMarkers()
check("PM udkørsels-kilde med", pickups[4711], true)
check("PM forsyning tæller ikke", pickups[9999], nil)
end
-- SA: Start/Stop alle flows — kørende flows røres ikke ved Start alle; Stop alle stopper alt aktivt
do
buildWorld()
local idle = makeVehicle("vehicleAllA", "Alle A", {STRAW, TMR}, 1000000, 100, 0)
local busy = makeVehicle("vehicleAllB", "Alle B", {STRAW, TMR}, 1000000, 100, 0)
local runA = ADRunsController.createRun("Alle-A")
ADRunsController.updateRun(runA.id, {targetWayPointId = 94})
ADRunsController.assignVehicle(runA.id, idle)
local runB = ADRunsController.createRun("Alle-B")
ADRunsController.updateRun(runB.id, {targetWayPointId = 94})
ADRunsController.assignVehicle(runB.id, busy)
ADRunsController.createRun("Alle-tom") -- ingen traktorer: tæller ikke med
ADRunsController.startRun(adEnv, runB.id)
local busyStarts = busy.started
local summary = ADRunsController.startAllRuns(adEnv)
check("SA idle startet", idle.started, 1)
check("SA kørende flow ikke genstartet", busy.started, busyStarts)
check("SA mindst ét flow startet", summary.runsStarted >= 1, true)
check("SA kørende flow talt", summary.alreadyRunning >= 1, true)
check("SA traktorer startet tæller idle", summary.vehiclesStarted >= 1, true)
check("SA resultater med", type(summary.results), "table")
check("SA noget aktivt", ADRunsController.hasActiveRuns(adEnv), true)
local stopped = ADRunsController.stopAllRuns(adEnv)
check("SA stop alle stopper begge", stopped >= 2, true)
check("SA idle stoppet", idle.ad.stateModule.active, false)
check("SA busy stoppet", busy.ad.stateModule.active, false)
check("SA intet aktivt bagefter", ADRunsController.hasActiveRuns(adEnv), false)
check("SA isRunning ryddet", ADRunsController.getRunStatus(adEnv, ADRuns.getRun(ADRunsController.store, runA.id)).isRunning, false)
end
-- C20: Start alle i kø — én traktor ad gangen med STAGGER_MS mellemrum (in-game 27/9: ellers kører de
-- sammen på parkeringspladsen); pause ophæves
do
buildWorld()
local q1 = makeVehicle("vehicleQ1", "Q1", {STRAW, TMR}, 1000000, 100, 0)
local q2 = makeVehicle("vehicleQ2", "Q2", {STRAW, TMR}, 1000000, 110, 0)
local q3 = makeVehicle("vehicleQ3", "Q3", {}, 0, 120, 0) -- ingen vogn -> springes over, ikke i kø
local qrun = ADRunsController.createRun("Kø")
ADRunsController.updateRun(qrun.id, {targetWayPointIds = {94}})
ADRunsController.assignVehicle(qrun.id, q1)
ADRunsController.assignVehicle(qrun.id, q2)
ADRunsController.assignVehicle(qrun.id, q3)
local summary = ADRunsController.queueStartAll(adEnv)
check("C20 to i kø", summary.queued, 2)
check("C20 en sprunget over", #summary.skipped, 1)
check("C20 intet startet før første tick", (q1.started or 0) + (q2.started or 0), 0)
ADRunsController.processStartQueue(adEnv, 0)
check("C20 første startes straks", (q1.started or 0) + (q2.started or 0), 1)
ADRunsController.processStartQueue(adEnv, ADRunsController.STAGGER_MS - 1)
check("C20 næste venter mellemrummet", (q1.started or 0) + (q2.started or 0), 1)
ADRunsController.processStartQueue(adEnv, 1)
check("C20 næste efter mellemrummet", (q1.started or 0) + (q2.started or 0), 2)
check("C20 kø tom", #ADRunsController.startQueue, 0)
check("C20 run markeret kørende", ADRunsController.runningRuns[qrun.id], true)
end
-- C21: Stop alt og parkér — stopper flow-traktorerne, sender dem til AD-parkering, sætter pause
do
buildWorld()
local p1 = makeVehicle("vehicleP1", "P1", {STRAW, TMR}, 1000000, 100, 0)
local p2 = makeVehicle("vehicleP2", "P2", {STRAW, TMR}, 1000000, 110, 0)
local prun = ADRunsController.createRun("Park")
ADRunsController.updateRun(prun.id, {targetWayPointIds = {94}})
ADRunsController.assignVehicle(prun.id, p1)
ADRunsController.assignVehicle(prun.id, p2)
ADRunsController.startRun(adEnv, prun.id)
local parked, paused = {}, nil
local oldWork, oldJobs, oldSilos = ADFieldWork, ADFieldJobs, ADFieldSilos
ADFieldWork = {
parkMarkerId = function(_, v) return v == p1 and 700 or nil end,
startAd = function(_, v, mode, first) parked[v] = first end,
stopAllAssignments = function() return {} end,
}
ADFieldJobs = {setPaused = function(value) paused = value end}
ADFieldSilos = {stopAll = function() end}
ADRunsController.queueStartAll(adEnv)
local summary = ADRunsController.stopAllAndPark(adEnv)
check("C21 begge stoppet", (p1.stopped or 0) >= 1 and (p2.stopped or 0) >= 1, true)
check("C21 P1 kører til parkering", parked[p1], 700)
check("C21 P2 uden parkering bliver stående", parked[p2], nil)
check("C21 tæller parkerede", summary.parked, 1)
check("C21 nævner dem uden parkering", summary.noPark[1], "P2")
check("C21 pause sat", paused, true)
check("C21 startkø tømt", #ADRunsController.startQueue, 0)
check("C21 run ikke længere kørende", ADRunsController.runningRuns[prun.id], nil)
ADFieldWork, ADFieldJobs, ADFieldSilos = oldWork, oldJobs, oldSilos
end
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
File diff suppressed because it is too large Load Diff
-596
View File
@@ -1,596 +0,0 @@
-- Kør: luajit tests/test_adSources.lua (fra repo-roden)
-- Kilde-genkendelse ved en AD-markør (fabrik / silo / objektlager), lagerstatus pr. vare,
-- paller ved fabrikkens spawner, salgssteder, objektlager-udtag.
local SUGAR, FORAGE, BASKET, CRATE = 5, 6, 7, 8
local DESC = {
[SUGAR] = {index = SUGAR, name = "SUGAR", title = "Sukker", isPalletType = true},
[FORAGE] = {index = FORAGE, name = "FORAGE", title = "TMR"},
[BASKET] = {index = BASKET, name = "STRAW_BASKET", title = "Halmkurve", palletFilename = "basketPallet.xml"},
[CRATE] = {index = CRATE, name = "STRAW_CRATE", title = "Halmkasser", palletFilename = "cratePallet.xml"},
}
g_fillTypeManager = {
getFillTypeByIndex = function(_, index) return DESC[index] end,
getFillTypeNameByIndex = function(_, index) return DESC[index] and DESC[index].name end,
getFillTypeTitleByIndex = function(_, index) return DESC[index] and DESC[index].title end,
}
Logging = {info = function() end, warning = function() end}
getWorldTranslation = function(node) return node.x, 0, node.z end
SellingStation = {}
dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")
dofile("FS25_ADSmartPickup/adPrices.lua")
dofile("FS25_ADSmartPickup/adStorageStats.lua")
dofile("FS25_ADSmartPickup/adSources.lua")
local failures = 0
local function check(name, actual, expected)
if actual == expected then
print("OK " .. name)
else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
-- ---- fakes ----
local wayPoints, markers = {}, {}
local function addMarker(id, name, x, z)
wayPoints[id] = {id = id, x = x, z = z}
table.insert(markers, {id = id, markerIndex = #markers + 1, name = name, group = "g"})
end
local adEnv = {ADGraphManager = {
getWayPointById = function(_, id) return wayPoints[id] end,
getMapMarkers = function() return markers end,
getMapMarkerByWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id then return m end end end,
}}
local function storageOf(levels, capacities)
return {
fillLevels = levels,
getFillLevel = function(_, ft) return levels[ft] or 0 end,
getCapacity = function(_, ft) return capacities[ft] or 0 end,
}
end
local sugarStorage = storageOf({[SUGAR] = 150000, [BASKET] = 0}, {[SUGAR] = 180000, [BASKET] = 8000})
local factory = {
rootNode = {x = 100, z = 100}, getName = function() return "Sugar mill" end,
spec_productionPoint = {productionPoint = {
productions = {{outputs = {{type = SUGAR, amount = 1}}}, {outputs = {{type = BASKET, amount = 1}}}},
storage = sugarStorage,
palletSpawner = {spawnPlaces = {{startNode = {x = 110, z = 100}}}},
}},
}
local factoryStation = {owningPlaceable = factory, supportedFillTypes = {[SUGAR] = true}}
local silo = {rootNode = {x = 200, z = 200}, spec_silo = {storages = {storageOf({[FORAGE] = 74500}, {[FORAGE] = 12000000})}}, getName = function() return "Farma" end}
local siloStation = {owningPlaceable = silo}
local spawnCalls = {}
local objectStorage = {
rootNode = {x = 300, z = 300}, ownerFarmId = 1, getName = function() return "Objektlager" end,
spec_objectStorage = {
objectInfos = {
{objects = {{palletAttributes = {fillType = BASKET, fillLevel = 1000}}}, numObjects = 8},
{objects = {{baleAttributes = {fillType = FORAGE, fillLevel = 4000}}}, numObjects = 2},
},
numStoredObjects = 10, capacity = 4000,
},
removeAbstractObjectsFromStorage = function(self, index, amount, connection) table.insert(spawnCalls, {index, amount, connection}) end,
}
local sellStation = {owningPlaceable = {rootNode = {x = 500, z = 500}}, isa = function(_, c) return c == SellingStation end, isSellingPoint = true, acceptedFillTypes = {[SUGAR] = true}, getName = function() return "Grain Elevator" end}
local factoryUnload = {owningPlaceable = factory, isa = function() return true end, isSellingPoint = false}
addMarker(1, "Suger Load", 100, 90)
addMarker(2, "silo 2", 200, 190)
addMarker(3, "Objektlager", 300, 280) -- 20 m fra lageret
addMarker(4, "Langt væk", 300, 240) -- 60 m fra lageret
addMarker(5, "selling station", 500, 490)
addMarker(6, "sugar offload", 100, 80)
local loadAt = {[1] = factoryStation, [2] = siloStation}
local unloadAt = {[5] = sellStation, [6] = factoryUnload}
ADSmartPickup = {
getLoadStationAtWayPointId = function(_, id) return loadAt[id] end,
getUnloadStationAtWayPointId = function(_, id) return unloadAt[id] end,
getOwnStorageLevel = function(station, fillTypes)
local total = 0
for _, storage in pairs(station.owningPlaceable.spec_silo.storages) do
for _, ft in ipairs(fillTypes) do total = total + storage:getFillLevel(ft) end
end
return total
end,
}
local pallets = {}
local function addPallet(fillType, level, x, z, farmId)
table.insert(pallets, {spec_pallet = {}, rootNode = {x = x, z = z}, getOwnerFarmId = function() return farmId or 1 end,
getFillUnitFillType = function() return fillType end, getFillUnitFillLevel = function() return level end})
end
g_currentMission = {
placeableSystem = {placeables = {factory, silo, objectStorage}},
accessHandler = {canFarmAccess = function(_, farmId, placeable) return true end},
vehicleSystem = {vehicles = pallets},
}
-- S1: fabrik
local source1 = ADSources.identify(adEnv, 1, 1)
check("S1 fabrik", source1.kind, "factory")
check("S1 navn", source1.name, "Sugar mill")
local products1 = ADSources.getProducts(adEnv, source1, 1)
check("S1 to produkter", #products1, 2)
check("S1 SUGAR available", products1[1].available, 150000)
check("S1 SUGAR level", products1[1].level, 150000)
check("S1 SUGAR capacity", products1[1].capacity, 180000)
check("S1 SUGAR bulk", products1[1].isPallet, false)
check("S1 SUGAR kan også stå på paller", products1[1].canPallet, true)
check("S1 SUGAR paller ved spawner (ingen endnu)", products1[1].palletLiters, 0)
check("S1 TMR-lignende bulk kan ikke paller", products1[2].canPallet, true)
check("S1 BASKET er palle", products1[2].isPallet, true)
check("S1 BASKET tomt", products1[2].available, 0)
-- S2: silo
local source2 = ADSources.identify(adEnv, 2, 1)
check("S2 silo", source2.kind, "silo")
local products2 = ADSources.getProducts(adEnv, source2, 1)
check("S2 én vare", #products2, 1)
check("S2 FORAGE", products2[1].fillType, FORAGE)
check("S2 level", products2[1].level, 74500)
check("S2 capacity", products2[1].capacity, 12000000)
-- S3: objektlager 20 m fra markøren
local source3 = ADSources.identify(adEnv, 3, 1)
check("S3 objektlager", source3.kind, "objectStorage")
local products3 = ADSources.getProducts(adEnv, source3, 1)
check("S3 to varer", #products3, 2)
check("S3 kurve available", products3[1].available, 8000)
check("S3 kurve er palle", products3[1].isPallet, true)
check("S3 level = antal objekter", products3[1].level, 10)
check("S3 capacity", products3[1].capacity, 4000)
check("S3 baller tæller som palle", products3[2].isPallet, true)
local index, count = ADSources.getObjectInfoIndex(source3, BASKET)
check("S3 objectInfo-indeks", index, 1)
check("S3 antal", count, 8)
check("S3 countStored", ADSources.countStored(source3, FORAGE), 2)
check("S3 ukendt vare", ADSources.getObjectInfoIndex(source3, SUGAR), nil)
-- S4: paller ved fabrikkens spawner tæller med (inden for 25 m af spawn-noden)
addPallet(BASKET, 1000, 112, 100)
addPallet(BASKET, 1000, 115, 105)
addPallet(BASKET, 1000, 160, 100) -- 50 m væk: tæller ikke
addPallet(BASKET, 1000, 112, 102, 2) -- anden farm: tæller ikke
local products4 = ADSources.getProducts(adEnv, source1, 1)
check("S4 paller ved spawner", products4[2].available, 2000)
check("S4 antal paller ved spawner", products4[2].palletCount, 2)
check("S3 antal paller i objektlager", products3[1].palletCount, 8)
check("S4 bulk uændret", products4[1].available, 150000)
addPallet(SUGAR, 2000, 111, 101)
local products4b = ADSources.getProducts(adEnv, source1, 1)
check("S4 sukkerpaller tælles separat", products4b[1].palletLiters, 2000)
check("S4 bulk-mængde uden paller", products4b[1].available, 150000)
-- S5: salgssteder = kun markører hvis aflæsningsstation er et rigtigt salgssted
local sells = ADSources.getSellMarkers(adEnv, 1)
check("S5 ét salgssted", #sells, 1)
check("S5 markør", sells[1].marker.id, 5)
check("S5 tager SUGAR", ADSources.stationAccepts(sells[1].station, SUGAR), true)
check("S5 tager ikke FORAGE", ADSources.stationAccepts(sells[1].station, FORAGE), false)
-- palletriggerens varer ligger i supportedFillTypes (det AutoDrive bruger), ikke i acceptedFillTypes (kun bulk-trigger)
local palletSell = {acceptedFillTypes = {[SUGAR] = true}, supportedFillTypes = {[SUGAR] = true, [BASKET] = true}}
check("S5 pallevare via supportedFillTypes", ADSources.stationAccepts(palletSell, BASKET), true)
check("S5 ukendt vare stadig nej", ADSources.stationAccepts(palletSell, FORAGE), false)
check("S5 acceptedNames", ADSources.describeAccepted(palletSell), "STRAW_BASKET, SUGAR")
-- S6: spawnOne kalder spillets udtag med 1 objekt og uden klientforbindelse
check("S6 spawn ok", ADSources.spawnOne(source3, 1), true)
check("S6 indeks", spawnCalls[1][1], 1)
check("S6 antal", spawnCalls[1][2], 1)
check("S6 connection nil", spawnCalls[1][3], nil)
check("S6 fejl -> false", ADSources.spawnOne({kind = "objectStorage", placeable = {removeAbstractObjectsFromStorage = function() error("x") end}}, 1), false)
-- S7: markør uden station og >40 m fra objektlager -> nil
check("S7 ingen kilde", ADSources.identify(adEnv, 4, 1), nil)
check("S7 ukendt waypoint", ADSources.identify(adEnv, 99, 1), nil)
-- S8: hjælpere
check("S8 isPalletFillType palle", ADSources.isPalletFillType(BASKET), true)
check("S8 isPalletFillType palletype uden fil", ADSources.isPalletFillType(SUGAR), true)
check("S8 isPalletFillType bulk", ADSources.isPalletFillType(FORAGE), false)
-- C1: palle/bulk afgøres pr. kilde: fabrik med læssestation der tager varen = bulk; silo = altid bulk; objektlager = palle
check("S8 sukker ved fabrik med læssestation = bulk", ADSources.isPalletAt(source1, SUGAR), false)
check("S8 kurve ved fabrik (station tager dem ikke) = palle", ADSources.isPalletAt(source1, BASKET), true)
check("S8 silo altid bulk", ADSources.isPalletAt(source2, SUGAR), false)
check("S8 objektlager altid palle", ADSources.isPalletAt(source3, FORAGE), true)
-- S9 (I1): ren pallefabrik uden læssestation genkendes via afstand til spawner/rootNode
local basketFactory = {
rootNode = {x = 600, z = 600}, getName = function() return "Straw factory" end,
spec_productionPoint = {productionPoint = {productions = {{outputs = {{type = BASKET, amount = 1}}}}, storage = storageOf({[BASKET] = 0}, {[BASKET] = 8000}), palletSpawner = {spawnPlaces = {{startNode = {x = 610, z = 600}}}}}},
}
table.insert(g_currentMission.placeableSystem.placeables, basketFactory)
addMarker(7, "Straw factory pickup", 605, 590)
local source9 = ADSources.identify(adEnv, 7, 1)
check("S9 pallefabrik uden station = factory", source9.kind, "factory")
check("S9 station nil", source9.station, nil)
local products9 = ADSources.getProducts(adEnv, source9, 1)
check("S9 kurve er palle", products9[1].isPallet, true)
check("S9 fabrik foretrækkes frem for objektlager i nærheden", ADSources.identify(adEnv, 7, 1).placeable, basketFactory)
-- S9b: fabrik fundet på afstand, men produktionspunktet HAR en læssestation (sukkerfabrik hvor markøren
-- ikke rammer triggeren): stationen hentes fra produktionspunktet, og sukker er bulk, ikke paller
local mill2Storage = storageOf({[SUGAR] = 1000}, {[SUGAR] = 180000})
local mill2 = {
rootNode = {x = 1200, z = 1200}, getName = function() return "Sugar mill 2" end,
spec_productionPoint = {productionPoint = {productions = {{outputs = {{type = SUGAR, amount = 1}}}}, storage = mill2Storage,
loadingStation = {supportedFillTypes = {[SUGAR] = true}}}},
}
table.insert(g_currentMission.placeableSystem.placeables, mill2)
addMarker(9, "Mill 2 load", 1210, 1190)
local source9b = ADSources.identify(adEnv, 9, 1)
check("S9b fabrik", source9b.kind, "factory")
check("S9b station fra produktionspunktet", source9b.station, mill2.spec_productionPoint.productionPoint.loadingStation)
check("S9b sukker = bulk", ADSources.isPalletAt(source9b, SUGAR), false)
check("S9b produkt bulk", ADSources.getProducts(adEnv, source9b, 1)[1].isPallet, false)
local excluded = ADSources.getSourcePlaceables({source1, source2})
check("S8 udelukkede placeables", excluded[factory] and excluded[silo], true)
-- S10 (paller til lager): objektlager med plads der tager varen; nærmeste nåelige fra traktoren
objectStorage.getObjectStorageSupportsFillType = function(self, ft) return ft == BASKET or ft == FORAGE end
check("S10 tager kurve", ADSources.objectStorageAccepts(objectStorage, BASKET), true)
check("S10 tager ikke sukker", ADSources.objectStorageAccepts(objectStorage, SUGAR), false)
check("S10 fri pladser", ADSources.objectStorageFreeSlots(objectStorage), 3990)
local fullShed = {rootNode = {x = 700, z = 700}, spec_objectStorage = {capacity = 10, numStoredObjects = 10, objectInfos = {}}, getName = function() return "Fuldt skur" end}
table.insert(g_currentMission.placeableSystem.placeables, fullShed)
addMarker(8, "Fuldt skur", 700, 690)
check("S10 fuldt skur 0 pladser", ADSources.objectStorageFreeSlots(fullShed), 0)
local truck = {rootNode = {x = 650, z = 650}}
local store = ADSources.findPalletStore(adEnv, truck, BASKET, {}, function() return true end, 1)
check("S10 nærmeste med plads = Objektlager (skuret er fuldt)", store.wayPointId, 3)
check("S10 navn", store.name, "Objektlager")
check("S10 udelukket kilde -> nil", ADSources.findPalletStore(adEnv, truck, BASKET, {[objectStorage] = true}, function() return true end, 1), nil)
check("S10 uopnåelig -> nil", ADSources.findPalletStore(adEnv, truck, BASKET, {}, function() return false end, 1), nil)
check("S10 sukker: intet skur tager det", ADSources.findPalletStore(adEnv, truck, SUGAR, {}, function() return true end, 1), nil)
-- S11 (reserve): gårdens forbrug det næste døgn af en vare, minus hvad forbrugerne allerede har
local BEET, STRAW, WATER = 9, 10, 11
DESC[BEET] = {index = BEET, name = "SUGARBEET", title = "Roer"}; DESC[STRAW] = {index = STRAW, name = "STRAW", title = "Halm"}; DESC[WATER] = {index = WATER, name = "WATER", title = "Vand"}
local tmrStorage = storageOf({[BEET] = 20000, [FORAGE] = 500000}, {[BEET] = 2500000, [FORAGE] = 3500000})
local tmrFactory = {
rootNode = {x = 800, z = 800}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "TMR-fabrik" end,
spec_productionPoint = {productionPoint = {
productions = {{id = "tmr3", cyclesPerMinute = 200 / 60, inputs = {{type = BEET, amount = 45}}, outputs = {{type = FORAGE, amount = 240}}}},
storage = tmrStorage,
getIsProductionEnabled = function(self, id) return id == "tmr3" end,
}},
}
local cowBarn = {
rootNode = {x = 900, z = 900}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Kostald" end,
spec_husbandryStraw = {inputLitersPerHour = 500}, spec_husbandryWater = {litersPerHour = 100, automaticWaterSupply = true},
getHusbandryIsFillTypeSupported = function(self, ft) return ft == STRAW or ft == WATER end,
getHusbandryFillLevel = function(self, ft) return ft == STRAW and 2000 or 0 end,
getHusbandryFreeCapacity = function(self, ft) return ft == STRAW and 19375 or 0 end,
}
local otherFarmBarn = {rootNode = {x = 950, z = 950}, ownerFarmId = 2, getOwnerFarmId = function() return 2 end, spec_husbandryStraw = {inputLitersPerHour = 500}, getHusbandryIsFillTypeSupported = function() return true end, getHusbandryFillLevel = function() return 0 end, getHusbandryFreeCapacity = function() return 99999 end}
table.insert(g_currentMission.placeableSystem.placeables, tmrFactory)
table.insert(g_currentMission.placeableSystem.placeables, cowBarn)
table.insert(g_currentMission.placeableSystem.placeables, otherFarmBarn)
local demand = ADSources.getFarmDemand(adEnv, BEET, 1, {})
-- 45 l × (200/60) pr. min × 1440 min = 216.000 l/døgn; har 20.000 -> 196.000
check("S11 roer: døgnforbrug minus beholdning", demand.liters, 196000)
check("S11 forbruger-navn", demand.consumers[1], "TMR-fabrik")
local strawDemand = ADSources.getFarmDemand(adEnv, STRAW, 1, {})
-- 500 l/t × 24 = 12.000; har 2.000 -> 10.000 (under fri plads 19.375)
check("S11 halm: stald", strawDemand.liters, 10000)
check("S11 anden farm tæller ikke", #strawDemand.consumers, 1)
check("S11 automatisk vand = intet behov", ADSources.getFarmDemand(adEnv, WATER, 1, {}).liters, 0)
check("S11 output er ikke behov", ADSources.getFarmDemand(adEnv, FORAGE, 1, {}).liters, 0)
check("S11 udelukket forbruger", ADSources.getFarmDemand(adEnv, BEET, 1, {[tmrFactory] = true}).liters, 0)
tmrStorage.fillLevels[BEET] = 300000
check("S11 nok på lager -> 0", ADSources.getFarmDemand(adEnv, BEET, 1, {}).liters, 0)
tmrStorage.fillLevels[BEET] = 20000
tmrFactory.spec_productionPoint.productionPoint.getIsProductionEnabled = function() return false end
check("S11 slukket produktion -> 0", ADSources.getFarmDemand(adEnv, BEET, 1, {}).liters, 0)
-- S11b: staldens OUTPUT (mælk, gylle) er aldrig et behov — kun varer i foderkrybben er foder
local MILK_OUT = 13
local feedBarn = {
rootNode = {x = 960, z = 960}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Malkestald" end,
spec_husbandryFood = {litersPerHour = 2000, fillLevels = {[FORAGE] = 1000}},
getHusbandryIsFillTypeSupported = function(self, ft) return ft == MILK_OUT or ft == FORAGE end,
getHusbandryFillLevel = function(self, ft) return ft == MILK_OUT and 200000 or ft == FORAGE and 1000 or 0 end,
getHusbandryFreeCapacity = function(self, ft) return 9999999 end,
}
table.insert(g_currentMission.placeableSystem.placeables, feedBarn)
check("S11b mælk er ikke behov", ADSources.getFarmDemand(adEnv, MILK_OUT, 1, {}).liters, 0)
check("S11b foder er behov (2000 l/t × 24 − 1000)", ADSources.getFarmDemand(adEnv, FORAGE, 1, {}).liters, 47000)
-- tom krybbe: foderet kendes fra dyretypens foderdefinition
feedBarn.spec_husbandryFood.fillLevels = {}
feedBarn.getHusbandryFillLevel = function(self, ft) return ft == MILK_OUT and 200000 or 0 end
g_currentMission.animalFoodSystem = {getAnimalFood = function() return {groups = {{fillTypes = {FORAGE}}}} end}
check("S11b tom krybbe: foder stadig behov", ADSources.getFarmDemand(adEnv, FORAGE, 1, {}).liters, 48000)
check("S11b tom krybbe: mælk stadig ikke behov", ADSources.getFarmDemand(adEnv, MILK_OUT, 1, {}).liters, 0)
g_currentMission.animalFoodSystem = nil
table.remove(g_currentMission.placeableSystem.placeables)
-- S12 (inventar): alt gården har liggende, pr. bygning, kun bygninger med indhold
factory.ownerFarmId = 1; silo.ownerFarmId = 1; objectStorage.ownerFarmId = 1
local inventory = ADSources.getFarmInventory(adEnv, 1)
local byName = {}
for _, entry in ipairs(inventory) do byName[entry.name] = entry end
check("S12 sukkerfabrik med", byName["Sugar mill"] ~= nil, true)
check("S12 fabrik-kind", byName["Sugar mill"].kind, "factory")
check("S12 fabrik sukker", byName["Sugar mill"].entries[1].level, 150000)
check("S12 fabrik sukker er output", byName["Sugar mill"].entries[1].isOutput, true)
local beetEntry
for _, e in ipairs(byName["TMR-fabrik"].entries) do if e.fillType == BEET then beetEntry = e end end
check("S12 TMR-fabrik roer er input", beetEntry.isOutput, false)
check("S12 outputs først", byName["TMR-fabrik"].entries[1].isOutput, true)
check("S12 silo", byName["Farma"].entries[1].fillType, FORAGE)
check("S12 objektlager kurve som antal", byName["Objektlager"].entries[1].count, 8)
-- 10 af 4000 pladser optaget -> kurvene har 8 + 3990 ledige pladser; liter-kapacitet = pladser x 1000 l
check("S12 objektlager pladser = egne + ledige", byName["Objektlager"].entries[1].slots, 3998)
check("S12 objektlager kapacitet i liter", byName["Objektlager"].entries[1].capacity, 3998000)
check("S12 stald halm", byName["Kostald"].entries[1].level, 2000)
check("S12 stald kapacitet = niveau + fri", byName["Kostald"].entries[1].capacity, 21375)
check("S12 tomt skur udeladt", byName["Fuldt skur"], nil)
check("S12 anden farm udeladt", #inventory, 5)
-- S13: Wait-markører er hverken kilder eller lagermål, selv om et objektlager står inden for 40 m
-- (in-game 23/9: 'cow wait 1' blev valgt som palle-lager -> Dragon holdt ved Wait med stropperne af)
wayPoints[20] = {id = 20, x = 300, z = 275}
table.insert(markers, {id = 20, markerIndex = #markers + 1, name = "cow wait 1", group = "cow wait"})
check("S13 Wait-markør er ikke en kilde", ADSources.identify(adEnv, 20, 1), nil)
local truck2 = {rootNode = {x = 300, z = 270}}
local store13 = ADSources.findPalletStore(adEnv, truck2, BASKET, {}, function() return true end, 1)
check("S13 palle-lager = objektlagerets egen markør, ikke Wait", store13.wayPointId, 3)
-- S14: stald som kilde — markør ved staldens læssestation (mælketank/gylle); varer = det stationen udleverer
local MILK = 12
DESC[MILK] = {index = MILK, name = "MILK", title = "Mælk"}
local barnLevels = {[MILK] = 4200, [STRAW] = 2000}
local milkBarn = {
rootNode = {x = 1500, z = 1500}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Kostald" end,
getHusbandryFillLevel = function(self, ft) return barnLevels[ft] or 0 end,
getHusbandryCapacity = function(self, ft) return ft == MILK and 25000 or ft == STRAW and 21375 or 0 end,
getHusbandryFreeCapacity = function(self, ft) return ft == MILK and 20800 or 0 end,
getHusbandryIsFillTypeSupported = function(self, ft) return ft == MILK or ft == STRAW end,
}
local milkStation = {owningPlaceable = milkBarn, supportedFillTypes = {[MILK] = true}}
addMarker(10, "cow 1 milk", 1500, 1490); loadAt[10] = milkStation
local source14 = ADSources.identify(adEnv, 10, 1)
check("S14 stald", source14.kind, "husbandry")
check("S14 navn", source14.name, "Kostald")
local products14 = ADSources.getProducts(adEnv, source14, 1)
check("S14 én vare (kun det tanken udleverer)", #products14, 1)
check("S14 mælk", products14[1].fillType, MILK)
check("S14 niveau", products14[1].level, 4200)
check("S14 kapacitet", products14[1].capacity, 25000)
check("S14 bulk", products14[1].isPallet, false)
check("S14 isPalletAt stald = bulk", ADSources.isPalletAt(source14, MILK), false)
check("S14 kilde-markør i listen", ADRunsController ~= nil and true or true, true)
-- S14b: mælketank-udvidelse (siloExtension) — læssestationen trækker fra stald + tank; niveauet skal
-- være stationens samlede mængde, ikke kun staldens egen beholder
milkStation.getAllFillLevels = function(self, farmId) return {[MILK] = 1275 + 136912}, 10025000 end
local products14b = ADSources.getProducts(adEnv, source14, 1)
check("S14b stald + udvidelsestank", products14b[1].level, 138187)
check("S14b tilgængeligt = samlet", products14b[1].available, 138187)
milkStation.getAllFillLevels = nil
-- S14c: mælketank (silo med udvidelses-lager) — tanken selv er tom, puljen via læssestationen har mælken
local tank = {spec_silo = {storages = {{fillLevels = {}, getCapacity = function() return 10000000 end}}}}
local tankSource = {kind = "silo", placeable = tank, wayPointId = 999, station = {getAllFillLevels = function() return {[MILK] = 500000}, 70000000 end}}
local tankProducts = ADSources.getProducts(adEnv, tankSource, 1)
check("S14c tom tank viser puljens mælk", tankProducts[1] and tankProducts[1].level, 500000)
check("S14c kapacitet = puljens", tankProducts[1] and tankProducts[1].capacity, 70000000)
-- S14d: generelt lager (separatorhal): kilde via læssestationen, varer = stationens niveauer
local DIGESTATE = 30
DESC[DIGESTATE] = {index = DIGESTATE, name = "DIGESTATE", title = "Digestat"}
local separator = {rootNode = {x = 7000, z = 0}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Separator Hall" end, spec_manureSeparator = {}}
local sepStation = {owningPlaceable = separator, supportedFillTypes = {[DIGESTATE] = true}, getAllFillLevels = function() return {[DIGESTATE] = 42000}, 200000 end}
addMarker(77, "Digestate pickup", 7000, 10); loadAt[77] = sepStation
local sepSource = ADSources.identify(adEnv, 77, 1)
check("S14d separator = lager-kilde", sepSource and sepSource.kind, "storage")
local sepProducts = sepSource and ADSources.getProducts(adEnv, sepSource, 1) or {}
check("S14d digestat fra stationen", sepProducts[1] and sepProducts[1].fillType, DIGESTATE)
check("S14d mængde", sepProducts[1] and sepProducts[1].level, 42000)
-- S14e: hønsehus — æg kommer som PALLER fra staldens egen pallespawner (spec_husbandryPallets), ingen
-- læssestation ved æggene: markøren ved spawneren er en staldkilde, æggene er pallevarer
local EGGS = 31
DESC[EGGS] = {index = EGGS, name = "EGG", title = "Æg", isPallet = true}
local coopSpawner = {spawnPlaces = {{startNode = {x = 8000, z = 0}}}}
local coop = {rootNode = {x = 8040, z = 0}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Chicken Coop" end,
getHusbandryFillLevel = function() return 0 end,
spec_husbandryPallets = {fillTypeIndexToPalletSpawner = {[EGGS] = coopSpawner}, activeFillTypes = {EGGS}, fillLevels = {[EGGS] = 0}, capacities = {[EGGS] = 5000}}}
table.insert(g_currentMission.placeableSystem.placeables, coop)
addPallet(EGGS, 1000, 8003, 2); addPallet(EGGS, 1000, 8006, 2); addPallet(EGGS, 800, 8009, 2)
addMarker(78, "Egg Pickup", 8000, 12)
local coopSource = ADSources.identify(adEnv, 78, 1)
check("S14e hønsehus-kilde via pallespawner", coopSource and coopSource.kind, "husbandry")
local coopProducts = coopSource and ADSources.getProducts(adEnv, coopSource, 1) or {}
local eggs = nil
for _, item in ipairs(coopProducts) do if item.fillType == EGGS then eggs = item end end
check("S14e æg som pallevare", eggs and eggs.isPallet, true)
check("S14e æg på paller ved spawneren", eggs and eggs.available, 2800)
check("S14e tre paller", eggs and eggs.palletCount, 3)
check("S14e isPalletAt æg", coopSource and ADSources.isPalletAt(coopSource, EGGS), true)
-- S14f: hønsehuse på række 17 m fra hinanden (inden for PALLET_RADIUS) — en palle tæller kun for den
-- NÆRMESTE spawner, ellers "ser" et tomt hus naboernes æg og vognen kører forgæves derhen
local coop2Spawner = {spawnPlaces = {{startNode = {x = 8020, z = 0}}}}
local coop2 = {rootNode = {x = 8060, z = 0}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Chicken Coop" end,
getHusbandryFillLevel = function() return 0 end,
spec_husbandryPallets = {fillTypeIndexToPalletSpawner = {[EGGS] = coop2Spawner}, activeFillTypes = {EGGS}, fillLevels = {[EGGS] = 0}, capacities = {[EGGS] = 5000}}}
local emptyCoopSpawner = {spawnPlaces = {{startNode = {x = 8037, z = 0}}}}
local emptyCoop = {rootNode = {x = 8074, z = 0}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Chicken Coop" end,
getHusbandryFillLevel = function() return 0 end,
spec_husbandryPallets = {fillTypeIndexToPalletSpawner = {[EGGS] = emptyCoopSpawner}, activeFillTypes = {EGGS}, fillLevels = {[EGGS] = 0}, capacities = {[EGGS] = 5000}}}
table.insert(g_currentMission.placeableSystem.placeables, coop2)
table.insert(g_currentMission.placeableSystem.placeables, emptyCoop)
addPallet(EGGS, 1000, 8021, 2); addPallet(EGGS, 1000, 8023, 2)
local function eggsAt(placeable)
for _, item in ipairs(ADSources.getProducts(adEnv, {kind = "husbandry", placeable = placeable, station = nil}, 1)) do
if item.fillType == EGGS then return item end
end
end
check("S14f første hus kun egne paller", eggsAt(coop).available, 2800)
check("S14f nabohus kun egne paller", eggsAt(coop2).available, 2000)
check("S14f tomt hus ser ikke naboens æg", eggsAt(emptyCoop).available, 0)
check("S14f tomt hus 0 paller", eggsAt(emptyCoop).palletCount, 0)
table.remove(g_currentMission.placeableSystem.placeables)
table.remove(g_currentMission.placeableSystem.placeables)
-- S14g: spawnerens område er ALLE rækker fra start til slut — ikke kun første rækkes start. Hus A's
-- række 2 slutter tæt på hus B's start; en palle for enden af A's række 2 er stadig A's.
local rowA = {spawnPlaces = {{startNode = {x = 9000, z = 0}, endNode = {x = 9008, z = 0}}, {startNode = {x = 9000, z = 3}, endNode = {x = 9008, z = 3}}}}
local rowB = {spawnPlaces = {{startNode = {x = 9014, z = 3}, endNode = {x = 9022, z = 3}}}}
local function rowCoop(spawner, x)
return {rootNode = {x = x, z = -10}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Chicken Coop" end,
getHusbandryFillLevel = function() return 0 end,
spec_husbandryPallets = {fillTypeIndexToPalletSpawner = {[EGGS] = spawner}, activeFillTypes = {EGGS}, fillLevels = {[EGGS] = 0}, capacities = {[EGGS] = 5000}}}
end
local coopA, coopB = rowCoop(rowA, 9004), rowCoop(rowB, 9018)
table.insert(g_currentMission.placeableSystem.placeables, coopA)
table.insert(g_currentMission.placeableSystem.placeables, coopB)
addPallet(EGGS, 1000, 9008, 3) -- enden af A's række 2: 6 m til B's start, 8 m til A's første start
check("S14g palle for enden af egen række 2 er husets egen", eggsAt(coopA).available, 1000)
check("S14g naboen får den ikke", eggsAt(coopB).available, 0)
-- PlacementUtil-form (startX/startZ + dirX/dirZ*width) i stedet for noder
local placeC = {spawnPlaces = {{startX = 9000, startZ = 50, dirX = 1, dirZ = 0, width = 8}}}
local coopC = rowCoop(placeC, 9004)
table.insert(g_currentMission.placeableSystem.placeables, coopC)
addPallet(EGGS, 700, 9007, 51)
check("S14g PlacementUtil-plads tæller", eggsAt(coopC).available, 700)
table.remove(g_currentMission.placeableSystem.placeables)
table.remove(g_currentMission.placeableSystem.placeables)
table.remove(g_currentMission.placeableSystem.placeables)
-- S15: forklaring af en markør til advarslen (hvad opslaget ser)
check("S15 forklaring stald", ADSources.explainMarker(adEnv, 10, 1), "læssestation ved markøren hører til 'Kostald' (stald)")
check("S15 forklaring ingen station", ADSources.explainMarker(adEnv, 4, 1), "ingen læssestation ved markøren, og ingen fabrik/objektlager inden for 40 m")
loadAt[11] = {owningPlaceable = nil}
wayPoints[11] = {id = 11, x = 2000, z = 2000}; table.insert(markers, {id = 11, markerIndex = #markers + 1, name = "løs", group = "g"})
check("S15 station uden placeable", ADSources.explainMarker(adEnv, 11, 1), "læssestation ved markøren uden bygning (owningPlaceable = nil)")
-- S16: strengt opslag fejler, løst opslag (40 m, uanset retning) finder mælketanken -> stald; en SILO
-- accepteres aldrig løst (tætte rækker -> nabosilo)
local looseAt = {}
ADSmartPickup.getLoadStationNearWayPointId = function(_, id, farmId, maxDistance) return looseAt[id] end
addMarker(12, "cow 2 milk", 1600, 1600)
looseAt[12] = milkStation
check("S16 stald via løst opslag", ADSources.identify(adEnv, 12, 1).kind, "husbandry")
addMarker(13, "silo løst", 1700, 1700)
looseAt[13] = siloStation
check("S16 silo aldrig via løst opslag", ADSources.identify(adEnv, 13, 1), nil)
check("S16 forklaring nævner den løse station", ADSources.explainMarker(adEnv, 13, 1):find("silo", 1, true) ~= nil, true)
-- S16b: "Æg Pickup 1" står 12 m fra hønsehusets palleområde og 23 m fra svinestaldens gyllestation;
-- det nærmeste vinder (in-game 26/9: Pickup 1+2 blev genkendt som Pigsty og æggene aldrig hentet)
local looseDistance = {}
ADSmartPickup.getLoadStationNearWayPointId = function(_, id, farmId, maxDistance) return looseAt[id], looseDistance[id] end
local pigStation = {owningPlaceable = cowBarn}
addMarker(14, "Æg Pickup 1", 8000, 12)
looseAt[14], looseDistance[14] = pigStation, 23
local eggSource = ADSources.identify(adEnv, 14, 1)
check("S16b hønsehuset vinder over fjernere gyllestation", eggSource and eggSource.placeable, coop)
addMarker(15, "Slurry ved hønsehus", 8000, 30)
looseAt[15], looseDistance[15] = pigStation, 5
check("S16b nærmere gyllestation vinder stadig", ADSources.identify(adEnv, 15, 1).placeable, cowBarn)
-- S17: stald med ÉN station og TO læssepunkter (mælk i den ene ende, gylle i den anden): hver markør
-- giver kun de varer dens nærmeste læssepunkt udleverer (in-game 24/9, LargerAnimalBarns cowBarnBig)
local SLURRY = 13
DESC[SLURRY] = {index = SLURRY, name = "LIQUIDMANURE", title = "Gylle"}
barnLevels[SLURRY] = 9000
local twoEndBarn = {
rootNode = {x = 2500, z = 2500}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Stor kostald" end,
getHusbandryFillLevel = function(self, ft) return barnLevels[ft] or 0 end,
getHusbandryCapacity = function() return 25000 end,
}
local twoEndStation = {owningPlaceable = twoEndBarn, supportedFillTypes = {[MILK] = true, [SLURRY] = true},
loadTriggers = {{triggerNode = {x = 2500, z = 2470}, fillTypes = {[MILK] = true}}, {triggerNode = {x = 2500, z = 2530}, fillTypes = {[SLURRY] = true}}}}
addMarker(40, "Cow Milk", 2500, 2465); loadAt[40] = twoEndStation
addMarker(41, "Cow Slurry", 2500, 2535); loadAt[41] = twoEndStation
local milkEnd = ADSources.getProducts(adEnv, ADSources.identify(adEnv, 40, 1), 1)
check("S17 mælke-enden: én vare", #milkEnd, 1)
check("S17 mælke-enden: mælk", milkEnd[1].fillType, MILK)
local slurryEnd = ADSources.getProducts(adEnv, ADSources.identify(adEnv, 41, 1), 1)
check("S17 gylle-enden: én vare", #slurryEnd, 1)
check("S17 gylle-enden: gylle", slurryEnd[1].fillType, SLURRY)
-- læssepunkt uden fillTypes-tabel: stationens varer som før
twoEndStation.loadTriggers[1].fillTypes = nil
check("S17 uden punkt-varer: stationens varer", #ADSources.getProducts(adEnv, ADSources.identify(adEnv, 40, 1), 1), 2)
-- S18: gårdens forbrug og produktion pr. døgn (fabrikker + stalde) og bedste markedspris
tmrFactory.spec_productionPoint.productionPoint.getIsProductionEnabled = function(self, id) return id == "tmr3" end
tmrFactory.spec_productionPoint.productionPoint.productions[1].outputs = {{type = FORAGE, amount = 240}}
cowBarn.spec_husbandryWater.automaticWaterSupply = false
local rates = ADSources.getFarmRates(adEnv, 1)
-- TMR-fabrik: 45 l roer × 200/60 cyklus/min × 1440 = 216.000 l/døgn; TMR ud 240 × 200/60 × 1440 = 1.152.000
check("S18 roer forbrug", math.floor(rates[BEET].consumption + 0.5), 216000)
check("S18 TMR produktion", math.floor(rates[FORAGE].production + 0.5), 1152000)
check("S18 halm i stald 500 l/t", rates[STRAW].consumption, 12000)
check("S18 vand 100 l/t", rates[WATER].consumption, 2400)
check("S18 anden farm tæller ikke", rates[STRAW].consumption, 12000)
-- pris: bedste salgssted der tager varen; top = prisen ved 100 % af kurven
g_currentMission.storageSystem = {getUnloadingStations = function() return {sellStation, {isa = function() return true end, isSellingPoint = true, owningPlaceable = {}, acceptedFillTypes = {[SUGAR] = true}, getEffectiveFillTypePrice = function() return 2.0 end}} end}
sellStation.getEffectiveFillTypePrice = function() return 1.5 end
g_currentMission.environment = {currentPeriod = 1}
DESC[SUGAR].economy = {factors = {0.8, 1.0}}
local quote = ADSources.getMarketQuote(SUGAR)
check("S18 bedste pris", quote.price, 2.0)
check("S18 top ved 80 % af kurven", quote.top, 2.5)
check("S18 intet salgssted -> nil", ADSources.getMarketQuote(FORAGE), nil)
-- S19: staldenes produktion pr. døgn fra dyrenes output-kurver (liter pr. dyr pr. døgn efter alder)
local function curve(value) return {get = function(_, age) return age >= 12 and value or 0 end} end
g_currentMission.animalSystem = {getSubTypeByIndex = function(_, index)
if index == 1 then return {output = {milk = {fillType = MILK, curve = curve(360)}, liquidManure = curve(500)}} end
end}
g_fillTypeManager.getFillTypeIndexByName = function(_, name) for index, desc in pairs(DESC) do if desc.name == name then return index end end end
local producingBarn = {
rootNode = {x = 5000, z = 5000}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Malkestald" end,
getHusbandryFillLevel = function() return 0 end,
getClusters = function() return {
{getSubTypeIndex = function() return 1 end, getAge = function() return 24 end, getNumAnimals = function() return 10 end},
{getSubTypeIndex = function() return 1 end, getAge = function() return 6 end, getNumAnimals = function() return 5 end}, -- kalve: 0
} end,
}
table.insert(g_currentMission.placeableSystem.placeables, producingBarn)
local rates2 = ADSources.getFarmRates(adEnv, 1)
check("S19 mælk 10 køer × 360", rates2[MILK].production, 3600)
check("S19 gylle via navn", rates2[SLURRY].production, 5000)
-- S19b: andre dyretyper — høns: æg som "pallets" med fillType-indeks + gødning via navn
local EGG, MANURE_FT = 21, 22
DESC[EGG] = {index = EGG, name = "EGG", title = "Æg"}
DESC[MANURE_FT] = {index = MANURE_FT, name = "MANURE", title = "Gødning"}
local baseGetSubType = g_currentMission.animalSystem.getSubTypeByIndex
g_currentMission.animalSystem.getSubTypeByIndex = function(system, index)
if index == 2 then return {output = {pallets = {fillType = EGG, curve = curve(30)}, manure = curve(10)}} end
return baseGetSubType(system, index)
end
table.insert(g_currentMission.placeableSystem.placeables, {
rootNode = {x = 6000, z = 6000}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Hønsehus" end,
getHusbandryFillLevel = function() return 0 end,
getClusters = function() return {{subTypeIndex = 2, age = 12, numAnimals = 100}} end, -- felter i stedet for metoder
})
local rates3 = ADSources.getFarmRates(adEnv, 1)
check("S19b æg 100 høns × 30", rates3[EGG] and rates3[EGG].production, 3000)
check("S19b gødning via navn", rates3[MANURE_FT] and rates3[MANURE_FT].production, 1000)
check("S19b mælk uændret", rates3[MILK].production, 3600)
-- S20: statistik tæller kun produktioner der KØRER (ikke "mangler input"/slukket), og rater
-- ganges med spillets tidsjustering (1 / dage pr. periode)
ProductionPoint = {PROD_STATUS = {INACTIVE = 0, RUNNING = 1, MISSING_INPUTS = 2}}
local tmrProduction = tmrFactory.spec_productionPoint.productionPoint.productions[1]
tmrProduction.status = ProductionPoint.PROD_STATUS.MISSING_INPUTS
local idle = ADSources.getFarmRates(adEnv, 1)
check("S20 fabrik der mangler input tæller ikke", idle[FORAGE], nil)
tmrProduction.status = ProductionPoint.PROD_STATUS.RUNNING
g_currentMission.environment = {timeAdjustment = 0.5}
local running = ADSources.getFarmRates(adEnv, 1)
check("S20 kørende fabrik halveret ved 2 dage/periode", math.floor(running[FORAGE].production + 0.5), 576000)
check("S20 stald halveret", running[STRAW].consumption, 6000)
g_currentMission.environment, ProductionPoint = nil, nil
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-206
View File
@@ -1,206 +0,0 @@
-- Kør: luajit tests/test_adStorageStats.lua (fra repo-roden)
-- Lagerstatistik pr. vare: fyldning, værdi nu / ved toppris, gårdens samlede beholdning, dage tilbage.
dofile("FS25_ADSmartPickup/adStorageStats.lua")
local S = ADStorageStats
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else failures = failures + 1; print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual))) end
end
-- ST1: fuld beregning
local st = S.itemStats({fillType = 5, level = 150000, capacity = 180000}, 412000, {consumption = 130000, production = 48000}, {price = 1.6, top = 2.0})
check("ST1 procent", st.percent, 83)
check("ST1 værdi nu", st.valueNow, 240000)
check("ST1 værdi top", st.valueTop, 300000)
check("ST1 gårdens beholdning", st.farmTotal, 412000)
check("ST1 dage på netto (412k / 82k)", math.floor(st.days * 10 + 0.5) / 10, 5.0)
check("ST1 netto pr. døgn", st.net, -82000)
-- ST2: ingen salgssted, intet forbrug, ingen kapacitet
local st2 = S.itemStats({fillType = 6, level = 5000, capacity = 0}, 5000, nil, nil)
check("ST2 ingen pris -> nil", st2.valueNow, nil)
check("ST2 intet forbrug -> dage nil", st2.days, nil)
check("ST2 kapacitet 0 -> procent nil", st2.percent, nil)
-- ST3: bygningens samlede værdi (kun varer med pris)
local total = S.buildingTotals({st, st2, S.itemStats({fillType = 7, level = 1000, capacity = 2000}, 1000, nil, {price = 2, top = 3})})
check("ST3 samlet nu", total.valueNow, 242000)
check("ST3 samlet top", total.valueTop, 303000)
-- ST4: gårdens totaler fra inventar
local totals = S.farmTotals({{entries = {{fillType = 5, level = 100}, {fillType = 6, level = 5}}}, {entries = {{fillType = 5, level = 50}}}})
check("ST4 sum pr. vare", totals[5], 150)
check("ST4 anden vare", totals[6], 5)
-- ST5: foder-forbrug i et fælles trug fordeles efter truget indhold; tomt trug -> bedste foder
local split = S.splitTroughConsumption(2400, {[116] = 3000, [7] = 1000}, {[116] = 1.0, [7] = 0.8})
check("ST5 TMR 75 %", split[116], 1800)
check("ST5 ensilage 25 %", split[7], 600)
local empty = S.splitTroughConsumption(2400, {}, {[116] = 1.0, [7] = 0.8})
check("ST5 tomt trug -> bedste foder", empty[116], 2400)
check("ST5 tomt trug -> intet på ensilage", empty[7], nil)
-- ST6: gitter — varer i rækker á n celler, tom liste giver én tom række
local rows = S.chunk({"a", "b", "c", "d"}, 3)
check("ST6 to rækker", #rows, 2)
check("ST6 første række 3", #rows[1], 3)
check("ST6 anden række 1", rows[2][1], "d")
check("ST6 tom liste -> én tom række", #S.chunk({}, 3), 1)
-- ST7: rate-kolonne: forbrug med minus, produktion med plus, intet -> streg
local fmt = function(v) return string.format("%.1fk", v / 1000) end
check("ST7 begge", S.rateText(14400, 3600, fmt), "-14.4k +3.6k")
check("ST7 kun prod", S.rateText(0, 3600, fmt), "+3.6k")
check("ST7 kun forbrug", S.rateText(2000, 0, fmt), "-2.0k")
check("ST7 intet", S.rateText(0, 0, fmt), "-")
-- ST8: fane pr. bygningstype — filtrér inventar på kind, rækkefølgen bevares
local inv = {{kind = "silo", name = "A"}, {kind = "factory", name = "B"}, {kind = "silo", name = "C"}}
local silos = S.filterKind(inv, "silo")
check("ST8 to siloer", #silos, 2)
check("ST8 rækkefølge", silos[2].name, "C")
check("ST8 ingen stalde", #S.filterKind(inv, "husbandry"), 0)
-- ST9: produktion ≥ forbrug -> beholdningen stiger, ingen dage
local rising = S.itemStats({fillType = 8, level = 1000, capacity = 2000}, 6700000, {consumption = 11500000, production = 19200000}, nil)
check("ST9 stiger -> dage nil", rising.days, nil)
check("ST9 stiger-flag", rising.rising, true)
check("ST1 falder -> ikke stiger", st.rising, false)
check("ST2 intet forbrug -> ikke stiger", st2.rising, false)
-- ST3: palleskur — procent af PLADSER (24 paller af 4000), ikke liter mod pladsantal (600 %)
local egg = S.itemStats({fillType = 8, level = 24000, capacity = 4000000, count = 24, slots = 4000}, 24000, nil, nil)
check("ST3 paller med", egg.count, 24)
check("ST3 pladser med", egg.slots, 4000)
check("ST3 procent af pladser", egg.percent, 1)
-- ---- v1.52 vareoversigt ----
-- SV1: gruppér fanens bygninger pr. vare; bygninger sorteret mest først, varer efter titel
local titles = {[31] = "Straw", [5] = "Sugarbeet", [9] = "Milk"}
local titleOf = function(ft) return titles[ft] end
local siloInv = {
{name = "Farma #1", placeable = "p1", entries = {{fillType = 31, level = 300, capacity = 1000}, {fillType = 5, level = 50, capacity = 100}}},
{name = "Farma #2", placeable = "p2", entries = {{fillType = 31, level = 700, capacity = 1000}}},
{name = "Tom", placeable = "p3", entries = {}},
}
local groups = S.groupByFillType(siloInv, titleOf)
check("SV1 to varer", #groups, 2)
check("SV1 alfabetisk: Straw først", groups[1].title, "Straw")
check("SV1 straw total", groups[1].level, 1000)
check("SV1 straw kapacitet", groups[1].capacity, 2000)
check("SV1 straw procent", groups[1].percent, 50)
check("SV1 to siloer med straw", #groups[1].buildings, 2)
check("SV1 flest først", groups[1].buildings[1].name, "Farma #2")
check("SV1 bygning procent", groups[1].buildings[1].percent, 70)
check("SV1 placeable med", groups[1].buildings[2].placeable, "p1")
check("SV1 tomt inventar", #S.groupByFillType({}, titleOf), 0)
-- SV2: gårdens kapacitet pr. vare
local caps = S.farmCapacities(siloInv)
check("SV2 kapacitet straw", caps[31], 2000)
-- SV3: varens statistik: døgn til tom på gårdens netto, pris i % af top
local straw = S.groupStats(groups[1], {level = 1500, capacity = 3000}, {consumption = 1000, production = 0}, {price = 0.5, top = 1.0})
check("SV3 døgn til tom", straw.days, 1.5)
check("SV3 døgn til fuld nil (falder)", straw.daysToFull, nil)
check("SV3 værdi nu", straw.valueNow, 500)
check("SV3 pris % af top", straw.priceRatio, 0.5)
check("SV3 tomt-niveau gul (< 3 døgn)", straw.emptyLevel, "yellow")
check("SV3 samlet niveau", straw.severity, "yellow")
-- SV4: produceret vare -> døgn til fuld på gårdens ledige kapacitet
local sugar = S.groupStats({fillType = 5, title = "Sugar", level = 950, capacity = 1000, percent = 95, buildings = {}},
{level = 950, capacity = 1000}, {consumption = 0, production = 100}, nil)
check("SV4 døgn til fuld", sugar.daysToFull, 0.5)
check("SV4 producerer", sugar.producing, true)
check("SV4 fuld-niveau rød (95 %)", sugar.fullLevel, "red")
check("SV4 ingen pris -> nil", sugar.valueNow, nil)
-- SV5: fuld lagervare der ikke produceres er ikke et problem
local stock = S.groupStats({fillType = 31, title = "Straw", level = 990, capacity = 1000, percent = 99, buildings = {}}, nil, nil, nil)
check("SV5 fuld lager uden produktion -> intet", stock.fullLevel, nil)
check("SV5 severity nil", stock.severity, nil)
-- SV6: tærskler
check("SV6 < 1 døgn rød", S.emptyLevel(0.5), "red")
check("SV6 < 3 døgn gul", S.emptyLevel(2.9), "yellow")
check("SV6 5 døgn intet", S.emptyLevel(5), nil)
check("SV6 nil intet", S.emptyLevel(nil), nil)
check("SV6 fuld 85 % gul", S.fullLevel(85, true), "yellow")
check("SV6 fuld 84 % intet", S.fullLevel(84, true), nil)
-- SV7: bygningsniveau: stald-foder næsten tomt, fabrik-output næsten fuldt
check("SV7 stald 8 % rød", S.buildingLevel({percent = 8}, "husbandry", false), "red")
check("SV7 stald 20 % gul", S.buildingLevel({percent = 20}, "husbandry", false), "yellow")
check("SV7 stald-output (mælk) 8 % intet", S.buildingLevel({percent = 8}, "husbandry", true), nil)
check("SV7 output 96 % rød", S.buildingLevel({percent = 96, isOutput = true}, "factory", false), "red")
check("SV7 silo 50 % intet", S.buildingLevel({percent = 50}, "silo", false), nil)
-- SV8: kræver handling — rød tom > rød fuld > gul tom > gul fuld > sælg (efter kroner); højst max
local mk = function(t) t.buildings = {}; return t end
local list = S.attentionList({
mk({title = "A", emptyLevel = "yellow", days = 2, consumption = 1}),
mk({title = "B", fullLevel = "red", percent = 97, producing = true}),
mk({title = "C", emptyLevel = "red", days = 0.4, consumption = 1}),
mk({title = "D", priceRatio = 0.95, valueNow = 50000, net = 0}),
mk({title = "E", priceRatio = 0.97, valueNow = 900000, net = 0}),
mk({title = "F", priceRatio = 0.99, valueNow = 900000, net = -5}), -- gården æder den: sælg ikke
mk({title = "G", priceRatio = 0.99, valueNow = 5000, net = 0}), -- for lille
}, 4)
check("SV8 højst 4", #list, 4)
check("SV8 1: rød tom", list[1].stat.title, "C")
check("SV8 1: art", list[1].kind, "empty")
check("SV8 2: rød fuld", list[2].stat.title, "B")
check("SV8 3: gul tom", list[3].stat.title, "A")
check("SV8 4: største salg", list[4].stat.title, "E")
check("SV8 4: art", list[4].kind, "sell")
-- SV9: nøgletal for fanen
local kpi = S.kpis({
mk({valueNow = 100, valueTop = 150, emptyLevel = "red"}),
mk({valueNow = 50, valueTop = 50, fullLevel = "yellow"}),
mk({}),
})
check("SV9 værdi nu", kpi.valueNow, 150)
check("SV9 ekstra ved top", kpi.gain, 50)
check("SV9 løber tør", kpi.emptyCount, 1)
check("SV9 fyldt", kpi.fullCount, 1)
-- SV10: fyldningsbrøk til bjælker, klemt til [0,1]
check("SV10 halv", S.fraction(50), 0.5)
check("SV10 over 100", S.fraction(130), 1)
check("SV10 nil", S.fraction(nil), 0)
-- SV11: skur uden pladsgrænse (kapacitet 0) -> varens procent ukendt, ikke 940 %
local shedGroups = S.groupByFillType({
{name = "A", entries = {{fillType = 9, level = 40, capacity = 100}}},
{name = "B", entries = {{fillType = 9, level = 900, capacity = 0}}},
}, titleOf)
check("SV11 procent nil ved ubegrænset skur", shedGroups[1].percent, nil)
-- "kræver handling" -> opret flow: hvem bruger varen, og hjælper et flow overhovedet? (brugerønske 29/9)
do
local inv = {
{name = "Mølle", kind = "factory", placeable = "pMill", entries = {{fillType = 7, level = 1000, isOutput = false}, {fillType = 8, level = 50, isOutput = true}}},
{name = "Silo", kind = "silo", placeable = "pSilo", entries = {{fillType = 7, level = 90000}}},
{name = "Kostald", kind = "husbandry", placeable = "pBarn", entries = {{fillType = 7, level = 500, isOutput = false}}},
}
local consumers = S.consumersOf(inv, 7)
check("forbrugere: fabrik + stald (ikke silo)", #consumers, 2)
check("forbrugere: output tæller ikke", #S.consumersOf(inv, 8), 0)
check("råd: eksisterende flow -> åbn det", S.flowAdvice({farmLevel = 91500, consumerLevel = 1500, perDay = 20000, consumerCount = 2, existingRunId = 4}), "open")
check("råd: lager andre steder -> opret flow", S.flowAdvice({farmLevel = 91500, consumerLevel = 1500, perDay = 20000, consumerCount = 2}), "flow")
check("råd: intet at flytte -> producér/køb", S.flowAdvice({farmLevel = 12700, consumerLevel = 12700, perDay = 228000, consumerCount = 1}), "produce")
check("råd: lidt andre steder, stort forbrug -> producér/køb", S.flowAdvice({farmLevel = 20000, consumerLevel = 12000, perDay = 228000, consumerCount = 1}), "produce")
check("råd: ingen forbrugere -> intet", S.flowAdvice({farmLevel = 5000, consumerLevel = 0, perDay = 0, consumerCount = 0}), "none")
end
-- "sælg nu" -> udkørsels-flow (brugerønske 29/9)
check("salg: eksisterende salgsflow -> åbn", S.sellAdvice({sourceCount = 1, existingRunId = 3}), "open")
check("salg: ingen -> opret flow", S.sellAdvice({sourceCount = 2}), "flow")
check("salg: ingen bygning med varen -> intet", S.sellAdvice({sourceCount = 0}), "none")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-266
View File
@@ -1,266 +0,0 @@
-- Kør: luajit tests/test_adSupplyPlanner.lua (fra repo-roden)
-- Tester de rene behovs-funktioner; ingen spil- eller AutoDrive-mocks nødvendige.
dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")
local HAY, GRASS, SILAGE = 20, 21, 22
local failures = 0
local function check(name, actual, expected)
if actual == expected then
print("OK " .. name)
else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
-- scoreNeed: tommere lager scorer højere; ugyldig kapacitet giver 0
check("tomt scorer højere end halvfuldt", ADSupplyPlanner.scoreNeed(0, 1000, 1000) > ADSupplyPlanner.scoreNeed(500, 1000, 500), true)
check("kapacitet 0 giver score 0", ADSupplyPlanner.scoreNeed(0, 0, 0), 0)
-- rankNeeds: mest trængende først, fulde springes over, input muteres ikke
local needs = {
{fillType = HAY, level = 80000, capacity = 100000},
{fillType = GRASS, level = 20000, capacity = 100000},
{fillType = SILAGE, level = 99500, capacity = 100000},
}
local ranked = ADSupplyPlanner.rankNeeds(needs)
check("fuld ingrediens springes over", #ranked, 2)
check("mest trængende først", ranked[1].fillType, GRASS)
check("dernæst næstmest trængende", ranked[2].fillType, HAY)
check("freeSpace beregnes", ranked[1].freeSpace, 80000)
check("input muteres ikke", needs[1].freeSpace, nil)
check("tom liste giver tom liste", #ADSupplyPlanner.rankNeeds({}), 0)
-- rankNeeds: deterministisk ved lige score (laveste fillType først)
local tie = ADSupplyPlanner.rankNeeds({
{fillType = SILAGE, level = 0, capacity = 5000},
{fillType = HAY, level = 0, capacity = 5000},
})
check("lige score -> laveste fillType først", tie[1].fillType, HAY)
-- resolveIngredients
local multi = {HAY, GRASS}
local resolved = ADSupplyPlanner.resolveIngredients(nil, multi)
check("multi-valg bliver ingrediensliste", #resolved, 2)
check("listen er en kopi", resolved ~= multi, true)
check("enkelt-valg uden tilstand -> nil", ADSupplyPlanner.resolveIngredients(nil, {HAY}), nil)
local state = {ingredients = {HAY, GRASS, SILAGE}, narrowedTo = GRASS}
check("indsnævret valg genbruger gemt liste", #ADSupplyPlanner.resolveIngredients(state, {GRASS}), 3)
check("bruger valgte andet -> nil", ADSupplyPlanner.resolveIngredients(state, {HAY}), nil)
check("nyt multi-valg erstatter gemt liste", #ADSupplyPlanner.resolveIngredients(state, {HAY, SILAGE}), 2)
check("ugyldigt valg -> nil", ADSupplyPlanner.resolveIngredients(state, nil), nil)
-- Kildestrategi: lager-siloer tømmes mindst-først; tanke der løber fulde (mælk, gylle) fyldest-først
check("mælk hentes fyldest-først", ADSupplyPlanner.prefersFullestSource({"MILK"}), true)
check("gylle hentes fyldest-først", ADSupplyPlanner.prefersFullestSource({"LIQUIDMANURE"}), true)
check("roer hentes mindst-først", ADSupplyPlanner.prefersFullestSource({"SUGARBEET"}), false)
check("blandet valg -> mindst-først", ADSupplyPlanner.prefersFullestSource({"MILK", "WHEAT"}), false)
check("ukendt/tomt valg -> mindst-først", ADSupplyPlanner.prefersFullestSource({}), false)
check("mindst-først: lavere niveau er bedre", ADSupplyPlanner.isBetterSource(100, 500, false), true)
check("mindst-først: højere niveau er ikke bedre", ADSupplyPlanner.isBetterSource(900, 500, false), false)
check("fyldest-først: højere niveau er bedre", ADSupplyPlanner.isBetterSource(900, 500, true), true)
check("første kandidat er altid bedre end ingen", ADSupplyPlanner.isBetterSource(900, nil, false), true)
-- Fabriksstyret forsyning: hvilke produktioner "ønsker" input?
local MISSING_INPUTS, INACTIVE = 2, 0
check("tændt produktion ønskes", ADSupplyPlanner.isProductionWanted(true, INACTIVE, MISSING_INPUTS), true)
check("auto-slukket pga. mangel ønskes stadig", ADSupplyPlanner.isProductionWanted(false, MISSING_INPUTS, MISSING_INPUTS), true)
check("manuelt slukket ønskes ikke", ADSupplyPlanner.isProductionWanted(false, INACTIVE, MISSING_INPUTS), false)
check("ukendt status uden tænd -> ikke ønsket", ADSupplyPlanner.isProductionWanted(false, nil, MISSING_INPUTS), false)
-- collectProductionDemand: liter/min pr. vare fra ønskede produktioner, kun det vognen kan bære
local STRAW, MINERAL, WATER = 23, 24, 25
local productions = {
{id = "tmr", cyclesPerMinute = 2, inputs = {{type = SILAGE, amount = 100}, {type = STRAW, amount = 10}, {type = MINERAL, amount = 1}}},
{id = "mix", cyclesPerMinute = 1, inputs = {{type = SILAGE, amount = 50}, {type = WATER, amount = 500}}},
{id = "off", cyclesPerMinute = 5, inputs = {{type = HAY, amount = 100}}},
}
local wanted = {tmr = true, mix = true}
local canCarry = {[SILAGE] = true, [STRAW] = true, [MINERAL] = true, [HAY] = true}
local demand = ADSupplyPlanner.collectProductionDemand(productions, function(p) return wanted[p.id] == true end, canCarry)
check("forbrug summeres på tværs af produktioner", demand[SILAGE], 250)
check("forbrug ganges med cyklusser/min", demand[STRAW], 20)
check("uønsket produktion tæller ikke", demand[HAY], nil)
check("vare vognen ikke kan bære udelades", demand[WATER], nil)
check("tom produktionsliste -> tomt behov", next(ADSupplyPlanner.collectProductionDemand({}, function() return true end, canCarry)), nil)
check("produktion uden inputs tåles", next(ADSupplyPlanner.collectProductionDemand({{id = "x", cyclesPerMinute = 1}}, function() return true end, canCarry)), nil)
-- minutesToEmpty
check("minutter til tom", ADSupplyPlanner.minutesToEmpty(1000, 250), 4)
check("intet forbrug -> nil", ADSupplyPlanner.minutesToEmpty(1000, 0), nil)
check("ukendt forbrug -> nil", ADSupplyPlanner.minutesToEmpty(1000, nil), nil)
-- rankNeeds med forbrug: den der løber tør først vinder, selv om den er relativt fyldest
local timed = ADSupplyPlanner.rankNeeds({
{fillType = MINERAL, level = 500, capacity = 10000, litersPerMinute = 2}, -- 250 min, 95 % tom
{fillType = SILAGE, level = 50000, capacity = 100000, litersPerMinute = 250}, -- 200 min, 50 % tom
{fillType = STRAW, level = 90000, capacity = 100000, litersPerMinute = 20}, -- 4500 min
})
check("kortest tid til tom først", timed[1].fillType, SILAGE)
check("dernæst næstkortest", timed[2].fillType, MINERAL)
check("minutesToEmpty følger med", timed[1].minutesToEmpty, 200)
check("vare med forbrug slår vare uden", ADSupplyPlanner.rankNeeds({
{fillType = HAY, level = 0, capacity = 100000},
{fillType = STRAW, level = 90000, capacity = 100000, litersPerMinute = 20},
})[1].fillType, STRAW)
check("fuldt mål springes over trods forbrug", #ADSupplyPlanner.rankNeeds({
{fillType = STRAW, level = 99500, capacity = 100000, litersPerMinute = 20},
}), 0)
-- isProductionInput: en fabriks input må aldrig stjæles som kilde — medmindre
-- fabrikken også selv producerer varen (så er den et output, der må hentes)
local TMR = 26
local factory = {
{id = "tmr", inputs = {{type = SILAGE, amount = 100}, {type = STRAW, amount = 10}}, outputs = {{type = TMR, amount = 50}}},
{id = "pellets", inputs = {{type = TMR, amount = 10}}, outputs = {{type = HAY, amount = 5}}},
}
check("input til fabrikken er beskyttet", ADSupplyPlanner.isProductionInput(factory, STRAW), true)
check("output må hentes", ADSupplyPlanner.isProductionInput(factory, HAY), false)
check("både input og output (kæde) -> må hentes", ADSupplyPlanner.isProductionInput(factory, TMR), false)
check("vare fabrikken ikke kender -> ikke beskyttet", ADSupplyPlanner.isProductionInput(factory, WATER), false)
check("ingen produktioner -> ikke beskyttet", ADSupplyPlanner.isProductionInput({}, STRAW), false)
check("produktion uden inputs/outputs tåles", ADSupplyPlanner.isProductionInput({{id = "x"}}, STRAW), false)
-- rankNeeds med læs-størrelse: varer hvor et FULDT læs passer går forud, ellers mest trængende
local sized = ADSupplyPlanner.rankNeeds({
{fillType = STRAW, level = 40000, capacity = 50000, litersPerMinute = 500}, -- 80 min, kun 10000 fri
{fillType = TMR, level = 20000, capacity = 100000, litersPerMinute = 50}, -- 400 min, 80000 fri
}, 40000)
check("fuldt læs der passer vinder over mere trængende der ikke passer", sized[1].fillType, TMR)
check("den der ikke passer bevares som reserve", sized[2].fillType, STRAW)
check("fits markeres", sized[1].fits, true)
check("ikke-fits markeres", sized[2].fits, false)
local noneFit = ADSupplyPlanner.rankNeeds({
{fillType = STRAW, level = 40000, capacity = 50000, litersPerMinute = 500},
{fillType = TMR, level = 90000, capacity = 100000, litersPerMinute = 50},
}, 40000)
check("passer intet -> mest trængende først", noneFit[1].fillType, STRAW)
check("uden læs-størrelse -> som før", ADSupplyPlanner.rankNeeds({{fillType = STRAW, level = 40000, capacity = 50000}})[1].fits, nil)
-- Wait-punkter: MAPPEN har ordet "Wait" (uanset store/små); markørnavnet er ligegyldigt.
-- (Brugeren har mark-ventepladser som "F91 Wait" i mark-mapperne — de må ikke ryge i puljen.)
check("mappen 'Cow Wait' tæller", ADSupplyPlanner.isWaitMarker({name = "Cow Wait", group = "Cow Wait"}), true)
check("mappen 'Wait' tæller", ADSupplyPlanner.isWaitMarker({name = "P2", group = "Wait"}), true)
check("mappen 'wait stalde' tæller", ADSupplyPlanner.isWaitMarker({name = "3", group = "wait stalde"}), true)
check("mark-venteplads i mark-mappe tæller IKKE", ADSupplyPlanner.isWaitMarker({name = "F91 Wait", group = "Græs Marker"}), false)
check("mappen 'Waiter' tæller ikke", ADSupplyPlanner.isWaitMarker({name = "x", group = "Waiter"}), false)
check("uden mappe -> nej", ADSupplyPlanner.isWaitMarker({name = "Wait 1"}), false)
-- nærmeste ledige wait-punkt; candidates = {{marker, x, z}}, claims = {[markerId] = vehicle}
local V1, V2 = {}, {}
local waitPoints = {
{marker = {id = 1, name = "Wait A"}, x = 0, z = 0},
{marker = {id = 2, name = "Wait B"}, x = 100, z = 0},
{marker = {id = 3, name = "Wait C"}, x = 300, z = 0},
}
check("nærmeste ledige vælges", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {}, 10, 0, V1).marker.id, 1)
check("reserveret springes over", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {[1] = V2}, 10, 0, V1).marker.id, 2)
check("egen reservation tæller som ledig", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {[1] = V1}, 10, 0, V1).marker.id, 1)
check("alle optaget -> nil", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {[1] = V2, [2] = V2, [3] = V2}, 10, 0, V1), nil)
check("ingen punkter -> nil", ADSupplyPlanner.pickNearestFreeWaitPoint({}, {}, 0, 0, V1), nil)
-- fysisk optaget: et køretøj holder på punktet uden reservation (fx efter omplanlægning) -> spring over
local occ = ADSupplyPlanner.markOccupiedWaitPoints(waitPoints, {{x = waitPoints[1].x + 2, z = waitPoints[1].z}}, 6)
check("optaget punkt markeres", occ[waitPoints[1].marker.id], true)
check("optaget springes over", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {}, 10, 0, V1, nil, occ).marker.id, 2)
check("egen reservation vinder over optaget (det er en selv)", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {[1] = V1}, 10, 0, V1, nil, occ).marker.id, 1)
check("køretøj langt væk optager intet", next(ADSupplyPlanner.markOccupiedWaitPoints(waitPoints, {{x = 9999, z = 9999}}, 6)), nil)
-- ét køretøj mellem to punkter optager kun det NÆRMESTE
local between = ADSupplyPlanner.markOccupiedWaitPoints({{marker = {id = 7}, x = 0, z = 0}, {marker = {id = 8}, x = 6, z = 0}}, {{x = 2, z = 0}}, 6)
check("kun nærmeste punkt optages", (between[7] == true and between[8] == nil), true)
-- nåelighed: et punkt AD ikke kan finde vej til/fra springes over (ellers slukker AD sig selv)
local unreachableA = function(candidate) return candidate.marker.id ~= 1 end
check("unåeligt nærmeste springes over", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {}, 10, 0, V1, unreachableA).marker.id, 2)
check("intet nåeligt -> nil", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {}, 10, 0, V1, function() return false end), nil)
-- Hjem fra Wait: fri plads >= min(rest, halvdelen af målets kapacitet)
-- (en 700.000 l-vogn kan aldrig tømme HELE resten i en 300.000 l-stald)
check("hele resten passer -> hjem", ADSupplyPlanner.canUnloadRest(35000, 35000, 300000), true)
check("resten passer ikke endnu -> vent", ADSupplyPlanner.canUnloadRest(5000, 20000, 50000), false)
check("kæmpe rest: halv stald fri -> hjem", ADSupplyPlanner.canUnloadRest(150000, 700000, 300000), true)
check("kæmpe rest: under halv stald fri -> vent", ADSupplyPlanner.canUnloadRest(100000, 700000, 300000), false)
check("ukendt kapacitet -> kun hele resten tæller", ADSupplyPlanner.canUnloadRest(100000, 700000, nil), false)
-- canUnloadRest med run-tærskel: "under 30 %" = fri >= 70 % af kapaciteten
check("tærskel 30 %: 60 % fri er ikke nok", ADSupplyPlanner.canUnloadRest(60000, 90000, 100000, 0.7), false)
check("tærskel 30 %: 70 % fri er nok", ADSupplyPlanner.canUnloadRest(70000, 90000, 100000, 0.7), true)
check("hele resten passer vinder altid", ADSupplyPlanner.canUnloadRest(5000, 5000, 100000, 0.7), true)
check("uden tærskel = 50 %-reglen", ADSupplyPlanner.canUnloadRest(50000, 90000, 100000), true)
-- rankNeeds: fælles trug (scoreOverride) + fodervægt (priority): TMR før hø før græs,
-- men et separat halmlager der er tommere vinder over truget
local TMR, HAYX = 40, 41
local trough = ADSupplyPlanner.rankNeeds({
{fillType = GRASS, level = 0, capacity = 30000, scoreOverride = 0.3, priority = 0.4},
{fillType = TMR, level = 0, capacity = 30000, scoreOverride = 0.3, priority = 1.0},
{fillType = HAYX, level = 0, capacity = 30000, scoreOverride = 0.3, priority = 0.8},
{fillType = HAY, level = 20000, capacity = 100000}, -- halm-agtigt separat lager, 80 % tomt
})
check("separat lager 80 % tomt vinder over trug 30 % tomt", trough[1].fillType, HAY)
check("i truget: TMR (1.0) først", trough[2].fillType, TMR)
check("så hø (0.8)", trough[3].fillType, HAYX)
check("så græs (0.4)", trough[4].fillType, GRASS)
check("scoreOverride bruges som score", trough[2].score, 0.3)
-- Bedste foder først: lavere foder (ensilage 0,8) må kun hentes når truget er nødlidende
-- (fri ≥ EMERGENCY_TROUGH_FREE af kapaciteten), hvis der er bedre foder (TMR 1,0) i samme trug
local tmrNeed = {fillType = 116, priority = 1.0, score = 0.6, trough = true}
local silageNeed = {fillType = 7, priority = 0.8, score = 0.6, trough = true}
local strawNeed = {fillType = 31, priority = 0, score = 0.9}
check("NF1 bedste foder tillades altid", ADSupplyPlanner.isFoodAllowed(tmrNeed, 1.0), true)
check("NF1 ensilage afvist ved halvfuldt trug", ADSupplyPlanner.isFoodAllowed(silageNeed, 1.0), false)
silageNeed.score = 0.8
check("NF1 ensilage tilladt når truget er næsten tomt", ADSupplyPlanner.isFoodAllowed(silageNeed, 1.0), true)
check("NF1 ikke-foder (halm) påvirkes ikke", ADSupplyPlanner.isFoodAllowed(strawNeed, 1.0), true)
check("NF1 intet bedre foder på listen -> tilladt", ADSupplyPlanner.isFoodAllowed({fillType = 7, priority = 0.8, score = 0.1, trough = true}, 0.8), true)
check("NF1 bedste foderprioritet", ADSupplyPlanner.bestFoodPriority({tmrNeed, silageNeed, strawNeed}), 1.0)
-- Læs på vej (andre traktorer): lægges oven i målets niveau; foder deler trug -> hele trugets fri plads falder
local pendingNeeds = {
{fillType = 31, level = 5000, capacity = 20000}, -- halm
{fillType = 116, level = 0, capacity = 30000, scoreOverride = 1.0, troughCapacity = 30000}, -- TMR, tomt trug
{fillType = 7, level = 0, capacity = 30000, scoreOverride = 1.0, troughCapacity = 30000}, -- ensilage
}
local adjusted = ADSupplyPlanner.applyPending(pendingNeeds, {[116] = 20000, [31] = 20000})
check("AP halm: niveau + på vej, højst kapacitet", adjusted[1].level, 20000)
check("AP TMR: niveau + på vej", adjusted[2].level, 20000)
check("AP TMR: kapacitet = niveau + resterende fri", adjusted[2].capacity, 30000)
check("AP ensilage: truget har kun 10000 fri tilbage", adjusted[3].capacity - adjusted[3].level, 10000)
check("AP trug-score falder", math.floor(adjusted[3].scoreOverride * 100 + 0.5), 33)
check("AP input urørt", pendingNeeds[2].level, 0)
check("AP intet på vej -> samme værdier", ADSupplyPlanner.applyPending(pendingNeeds, {})[1].level, 5000)
local ranked = ADSupplyPlanner.rankNeeds(adjusted, 8000)
check("AP halm fuld (på vej) -> kun trug-varer tilbage", #ranked, 2)
-- PF: parallelt foder (høns: korn 80 % / roesnitter 20 % af det de spiser) — hver gruppe har sin andel af truget
do
local WHEAT, BARLEY, BEETCUT = 1, 2, 3
local groups = {{fillTypes = {WHEAT, BARLEY}, eatWeight = 0.8}, {fillTypes = {BEETCUT}, eatWeight = 0.2}}
local levels = {[WHEAT] = 60000, [BARLEY] = 20000, [BEETCUT] = 0}
local lvl, cap = ADSupplyPlanner.parallelFoodShare(groups, levels, 100000, BEETCUT)
check("PF roesnitter: andel 20k, har 0", lvl .. "/" .. cap, "0/20000")
lvl, cap = ADSupplyPlanner.parallelFoodShare(groups, levels, 100000, BARLEY)
check("PF korn-gruppen samlet: 80k af 80k", lvl .. "/" .. cap, "80000/80000")
check("PF ukendt vare -> nil", ADSupplyPlanner.parallelFoodShare(groups, levels, 100000, 99), nil)
end
-- cargoFor: læs der allerede er på vognen ved start tæller som "på vej" (in-game 27/9: tre fyldte
-- biogas-traktorer startet fra fanen kørte alle til Drop 1 — ingen var registreret på vej)
check("CF run-vare på vognen", select(2, ADSupplyPlanner.cargoFor(SILAGE, 427215, {SILAGE})), 427215)
check("CF vare uden for run'et -> nil", ADSupplyPlanner.cargoFor(HAY, 5000, {SILAGE}), nil)
check("CF tom vogn -> nil", ADSupplyPlanner.cargoFor(SILAGE, 0, {SILAGE}), nil)
check("CF ukendt vare -> nil", ADSupplyPlanner.cargoFor(nil, 5000, {SILAGE}), nil)
-- kilde med nok til et rigtigt læs: mindst fyldte først kun blandt kilder med >= 25 % af et læs; ellers fyldeste
-- (in-game 29/9: 4 foder-traktorer hentede 2-10k l i en gæringssilo der løbende fyldes, og kørte "tomme")
local SP = ADSupplyPlanner
check("kilde-grænse: 25 % af læsset", SP.ampleLevel(1000000), 250000)
check("kilde-grænse: ukendt læs -> kun minimum", SP.ampleLevel(nil), SP.MIN_AMPLE_LITERS)
check("kilde-grænse: lille vogn -> mindst minimum", SP.ampleLevel(10000), SP.MIN_AMPLE_LITERS)
check("vælg: næsten tom taber til rigelig", SP.isBetterSource(10000, 12000000, false, 250000), false)
check("vælg: blandt rigelige mindst fyldte", SP.isBetterSource(300000, 12000000, false, 250000), true)
check("vælg: ingen rigelige -> fyldeste", SP.isBetterSource(9000, 2000, false, 250000), true)
check("vælg: løber-fuld-varer uændret (fyldeste)", SP.isBetterSource(12000000, 300000, true, 250000), true)
check("vælg: første kandidat", SP.isBetterSource(10, nil, false, 250000), true)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-166
View File
@@ -1,166 +0,0 @@
-- Kør: luajit tests/test_adWaitPool.lua (fra repo-roden)
-- Wait-puljen: kandidater pr. mappe, reservationer, nærmeste ledige, rutetjek, redirect.
dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")
dofile("FS25_ADSmartPickup/adWaitPool.lua")
local failures = 0
local function check(name, actual, expected)
if actual == expected then
print("OK " .. name)
else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
local markers, wayPoints = {}, {}
local function addMarker(id, name, group, x, z)
wayPoints[id] = {id = id, x = x, z = z, incoming = {}}
local marker = {id = id, markerIndex = #markers + 1, name = name, group = group}
table.insert(markers, marker)
return marker
end
local unreachable = {}
local adEnv = {
ADGraphManager = {
getMapMarkers = function() return markers end,
getMapMarkerByWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id then return m end end end,
getWayPointById = function(_, id) return wayPoints[id] end,
getPathTo = function(_, vehicle, id) if unreachable[id] then return {} end return {{id = id}} end,
pathFromTo = function(_, from, to) if unreachable[to] then return {} end return {{id = to}} end,
},
}
getWorldTranslation = function(node) return node.x, 0, node.z end
addMarker(10, "cow wait 1", "cow wait", 0, 0)
addMarker(11, "cow wait 2", "cow wait", 50, 0)
addMarker(20, "Wait 3", "General Wait", 10, 0)
addMarker(30, "silo 1", "silo", 5, 0)
-- W1: kandidater
check("W1 kun mappen", #ADWaitPool.getCandidates(adEnv, "cow wait"), 2)
check("W1 tom pulje = alle Wait-mapper", #ADWaitPool.getCandidates(adEnv, ""), 3)
check("W1 nil = alle", #ADWaitPool.getCandidates(adEnv, nil), 3)
-- W2: nærmeste ledige fra vognens position; reserverede springes over; egen reservation OK
local v1, v2 = {name = "v1", rootNode = {x = 45, z = 0}}, {name = "v2", rootNode = {x = 45, z = 0}}
check("W2 nærmeste", ADWaitPool.pickFree(adEnv, v1, "cow wait").marker.id, 11)
ADWaitPool.claim(11, v1)
check("W2 reserveret springes over", ADWaitPool.pickFree(adEnv, v2, "cow wait").marker.id, 10)
check("W2 egen reservation genbruges", ADWaitPool.pickFree(adEnv, v1, "cow wait").marker.id, 11)
-- W3: claim / owner / release
check("W3 ejer", ADWaitPool.getOwner(11), v1)
ADWaitPool.releaseVehicle(v1)
check("W3 frigivet", ADWaitPool.getOwner(11), nil)
check("W3 nu ledig for v2", ADWaitPool.pickFree(adEnv, v2, "cow wait").marker.id, 11)
-- W4: nåelighed
unreachable[11] = true
check("W4 uopnåelig nærmeste -> næste", ADWaitPool.pickFree(adEnv, v2, "cow wait", function(c) return ADWaitPool.hasRouteTo(adEnv, v2, c.marker.id) end).marker.id, 10)
check("W4 hasRouteTo false", ADWaitPool.hasRouteTo(adEnv, v2, 11), false)
check("W4 hasRouteBothWays true", ADWaitPool.hasRouteBothWays(adEnv, v2, 10, 30), true)
check("W4 hasRouteBothWays hjem uopnåelig", ADWaitPool.hasRouteBothWays(adEnv, v2, 10, 11), false)
unreachable[11] = nil
check("W4 intet punkt -> nil", ADWaitPool.pickFree(adEnv, v2, "cow wait", function() return false end), nil)
-- W5: getClaims sorteret på mappe, dernæst id
ADWaitPool.claim(20, v2)
ADWaitPool.claim(10, v1)
local claims = ADWaitPool.getClaims(adEnv)
check("W5 to reservationer", #claims, 2)
check("W5 første = General Wait", claims[1].marker.group, "General Wait")
check("W5 anden = cow wait 1", claims[2].marker.id, 10)
ADWaitPool.releaseVehicle(v1)
ADWaitPool.releaseVehicle(v2)
check("W5 alt frigivet", #ADWaitPool.getClaims(adEnv), 0)
-- W6: redirect stopper trigger-aktivitet og sætter sti
local stopped = {}
local task = {
destinationID = 99, state = 1,
vehicle = {ad = {
trailerModule = {isActiveAtTrigger = function() return true end, stopUnloading = function() stopped.unload = true end, stopLoading = function() stopped.load = true end},
drivePathModule = {setPathTo = function(_, id) stopped.pathTo = id end},
}},
}
ADWaitPool.STATE_DRIVING = 2
ADWaitPool.redirect(task, 10)
check("W6 destination", task.destinationID, 10)
check("W6 state driving", task.state, 2)
check("W6 setPathTo", stopped.pathTo, 10)
check("W6 stopUnloading", stopped.unload, true)
-- WR: AD's rutesøgning siger nej, men vejnettet har en vej fra traktorens nærmeste waypoint -> godtages
-- (in-game 24/9: 'Æg storage' ved Chicken Wait 1, "ingen vej til 'Chicken Storage Unload'" trods vej)
do
wayPoints[901] = {id = 901, x = 0, z = 0, out = {902}}
wayPoints[902] = {id = 902, x = 5, z = 0, out = {903}}
wayPoints[903] = {id = 903, x = 10, z = 0, out = {}}
wayPoints[904] = {id = 904, x = 20, z = 0, out = {}} -- ingen indgang
unreachable[903], unreachable[904] = true, true
local vehicle = {getClosestWayPoint = function() return 901, 1 end}
check("WR AD nej, vejnettet ja -> nåelig", ADWaitPool.hasRouteTo(adEnv, vehicle, 903), true)
check("WR hverken AD eller vejnet -> ikke nåelig", ADWaitPool.hasRouteTo(adEnv, vehicle, 904), false)
check("WR uden nærmeste waypoint -> AD's svar", ADWaitPool.hasRouteTo(adEnv, {}, 903), false)
unreachable[903], unreachable[904] = nil, nil
end
-- RC: rutesvar gemmes 30 s pr. mål og 10 m-område (holdende vogne spurgte hvert 5. s -> hak hvert 5. s)
do
local pathCalls = 0
local realGetPathTo = adEnv.ADGraphManager.getPathTo
adEnv.ADGraphManager.getPathTo = function(self, vehicle, id) pathCalls = pathCalls + 1; return realGetPathTo(self, vehicle, id) end
local clock = 0
netGetTime = function() return clock end
local positions = {[500] = {100, 100}}
getWorldTranslation = function(node) local p = positions[node]; return p[1], 0, p[2] end
local parked = {rootNode = 500, getClosestWayPoint = function() return 901, 1 end}
ADWaitPool.hasRouteTo(adEnv, parked, 10)
ADWaitPool.hasRouteTo(adEnv, parked, 10)
check("RC samme svar genbruges", pathCalls, 1)
positions[500] = {104, 103}
ADWaitPool.hasRouteTo(adEnv, parked, 10)
check("RC samme 10 m-område genbruges", pathCalls, 1)
positions[500] = {160, 100}
ADWaitPool.hasRouteTo(adEnv, parked, 10)
check("RC nyt sted -> nyt svar", pathCalls, 2)
clock = 31000
ADWaitPool.hasRouteTo(adEnv, parked, 10)
check("RC efter 30 s -> nyt svar", pathCalls, 3)
local first = ADWaitPool.hasRouteTo(adEnv, parked, 11)
check("RC andet mål -> nyt svar", pathCalls, 4)
check("RC samme svar igen", ADWaitPool.hasRouteTo(adEnv, parked, 11), first)
check("RC uden nyt kald", pathCalls, 4)
adEnv.ADGraphManager.getPathTo = realGetPathTo
netGetTime = nil
end
-- MR: rute markør->markør gemmes (uafhængig af hvor vognen er) — hak 26/9: 30-39 ms pr. rutetjek
do
local calls = 0
local realFromTo = adEnv.ADGraphManager.pathFromTo
adEnv.ADGraphManager.pathFromTo = function(self, from, to) calls = calls + 1; return realFromTo(self, from, to) end
local savedClock = netGetTime
local t = 1000
netGetTime = function() return t end
ADWaitPool.resetMarkerRoutes()
local far = {name = "far", rootNode = {x = 5000, z = 5000}}
check("MR første: rute", ADWaitPool.hasRouteBothWays(adEnv, v2, 10, 30), true)
local afterFirst = calls
t = t + 60000
check("MR anden fra en helt anden position: samme svar", ADWaitPool.hasRouteBothWays(adEnv, far, 10, 30), true)
check("MR anden gang ingen ny ruteberegning", calls, afterFirst)
adEnv.ADGraphManager.pathFromTo = realFromTo
netGetTime = savedClock
end
-- ankerpunkt for "All": midt mellem flowets kilder (in-game 29/9: Milk Store ventede ved sukkerroeknuseren)
local ax, az = ADWaitPool.anchorOf({{x = 0, z = 0}, {x = 100, z = 50}})
check("anker: midtpunkt x", ax, 50)
check("anker: midtpunkt z", az, 25)
check("anker: ingen punkter", ADWaitPool.anchorOf({}), nil)
check("anker: nil", ADWaitPool.anchorOf(nil), nil)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-33
View File
@@ -1,33 +0,0 @@
-- Kør: luajit tests/test_fieldStatus.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFieldModel.lua")
dofile("FS25_ADSmartPickup/adFieldView.lua")
local V = ADFieldView
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
check("mangler markør", V.fieldStatus({hasMarker = false}), "error")
check("arbejder", V.fieldStatus({hasMarker = true, working = true}), "running")
check("arbejder men venter på maskine", V.fieldStatus({hasMarker = true, working = true, reasonKey = "spu_field_reason_allBusy"}), "waiting")
check("for bred", V.fieldStatus({hasMarker = true, working = true, reasonKey = "spu_field_reason_tooWide"}), "error")
check("ingen maskine", V.fieldStatus({hasMarker = true, reasonKey = "spu_field_reason_noRig"}), "error")
check("regn", V.fieldStatus({hasMarker = true, working = true, reasonKey = "spu_field_reason_rain"}), "waiting")
check("venter på spilleren (høster)", V.fieldStatus({hasMarker = true, awaiting = true}), "waiting")
check("start bestilt", V.fieldStatus({hasMarker = true, startRequested = true}), "waiting")
check("i ro", V.fieldStatus({hasMarker = true}), "stopped")
-- Start må ikke bestilles på en mark uden markør (knappen var før deaktiveret; review 29/9)
check("kan starte: med markør", V.canStart({hasMarker = true, working = false}), true)
check("kan starte: uden markør -> nej", V.canStart({hasMarker = false, working = false}), false)
check("kan stoppe: uden markør men i gang", V.canStart({hasMarker = false, working = true}), true)
-- global pause (Stop alt): arbejdstrin uden maskiner = stoppet, [Start] genoptager kun den mark (in-game 29/9)
check("pause: arbejdstrin -> stoppet", V.fieldStatus({hasMarker = true, working = true, paused = true}), "stopped")
check("pause: genoptaget mark -> kører", V.fieldStatus({hasMarker = true, working = true, paused = true, resumed = true}), "running")
check("pause: mangler markør stadig rød", V.fieldStatus({hasMarker = false, paused = true}), "error")
check("pause: arbejder-tæller falsk", V.isActiveWork({working = true, paused = true}), false)
check("pause: genoptaget arbejder-tæller sand", V.isActiveWork({working = true, paused = true, resumed = true}), true)
check("ingen pause: arbejder", V.isActiveWork({working = true}), true)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-21
View File
@@ -1,21 +0,0 @@
-- Kør: luajit tests/test_flowStatus.lua (fra repo-roden)
dofile("FS25_ADSmartPickup/adFlowView.lua")
local V = ADFlowView
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else
failures = failures + 1
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
end
end
check("kører", V.flowStatus({running = 3, total = 4, canStop = true}), "running")
check("kører men holder (begrundelse)", V.flowStatus({running = 2, total = 2, canStop = true, reason = "intet at hente"}), "waiting")
check("startet, venter på vogn", V.flowStatus({running = 0, total = 2, canStop = true}), "waiting")
check("advarsel", V.flowStatus({running = 1, total = 2, canStop = true, warning = "mål mangler"}), "error")
check("start afvist (ingen vogn)", V.flowStatus({running = 0, total = 2, lastStartFailed = true}), "error")
check("stoppet", V.flowStatus({running = 0, total = 2, stopped = true}), "stopped")
check("aldrig startet", V.flowStatus({running = 0, total = 2}), "stopped")
check("stoppet vinder over gammel afvisning", V.flowStatus({running = 0, total = 2, stopped = true, lastStartFailed = true}), "stopped")
check("farver findes", V.LEVEL_COLORS ~= nil and V.LEVEL_COLORS.waiting ~= nil and V.LEVEL_COLORS.stopped ~= nil, true)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-18
View File
@@ -1,18 +0,0 @@
-- Kør: luajit tests/test_noStrayGlobals.lua (fra repo-roden)
-- Fanger lokale navne der ved en fejl bruges som globale (fx 'implement' uden for sin closure — v1.49 gjorde
-- egne tømmevogne ubrugelige under høst). Læser luajit-bytecode: GGET/GSET af typiske lokale navne.
local SUSPECT = {implement = true, candidate = true, vehicle = true, rig = true, pair = true, assignment = true,
tractor = true, loan = true, field = true, info = true, marker = true, job = true, owner = true, order = true}
local failures = 0
local files = io.popen("ls FS25_ADSmartPickup/*.lua FS25_ADSmartPickup/gui/*.lua"):read("*a")
for file in files:gmatch("[^\n]+") do
local listing = io.popen("luajit -bl " .. file .. " 2>&1"):read("*a")
for op, name in listing:gmatch("(G[GS]ET)%s+%d+%s+%d+%s+; \"([%w_]+)\"") do
if SUSPECT[name] then
failures = failures + 1
print(string.format("FAIL %s: %s af global '%s'", file, op, name))
end
end
end
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
-186
View File
@@ -1,186 +0,0 @@
-- Kør: luajit tests/test_storageView.lua (fra repo-roden)
-- Røgtest af vareoversigten i fanerne Lagre (v1.52) med falske GUI-elementer: opbygning, fold ud,
-- opdatering hvert sekund og skift til bygningsvisning må ikke kaste, og tallene skal lande i cellerne.
local failures = 0
local function check(name, actual, expected)
if actual == expected then print("OK " .. name) else failures = failures + 1; print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual))) end
end
-- ---- falske GUI-elementer ----
local Element = {}
Element.__index = Element
local function newElement(names, size)
local element = setmetatable({children = {}, text = nil, visible = true, size = size or {0.5, 0.02}}, Element)
for _, name in ipairs(names or {}) do
element.children[name] = setmetatable({children = {}, name = name, visible = true, size = {0.2, 0.01}}, Element)
end
return element
end
function Element:clone() local copy = newElement({}, {self.size[1], self.size[2]}); for n, c in pairs(self.children) do copy.children[n] = setmetatable({children = {}, name = n, visible = true, size = {c.size[1], c.size[2]}}, Element) end; return copy end
function Element:getDescendantByName(name) return self.children[name] end
function Element:setText(text) self.text = text end
function Element:setTexts(texts) self.texts = texts end
function Element:setState(state) self.state = state end
function Element:setVisible(visible) self.visible = visible end
function Element:setSize(x, y) self.size = {x, y} end
function Element:setImageColor(_, r, g, b, a) self.color = {r, g, b, a} end
function Element:setTextColor(r, g, b, a) self.textColor = {r, g, b, a} end
function Element:delete() end
function Element:mouseEvent() return false end
-- ---- spil-globaler ----
Class = function() return {} end
TabbedMenuFrameElement = {}
InputAction = {}
FocusManager = {loadElementFromCustomValues = function() end, removeElement = function() end}
Logging = {warning = function(...) print("WARN", string.format(...)) end, info = function() end}
local titles = {[31] = "Straw", [5] = "Sugar", [9] = "Milk"}
g_fillTypeManager = {getFillTypeTitleByIndex = function(_, i) return titles[i] end, getFillTypeNameByIndex = function(_, i) return titles[i] end}
g_currentMission = {getFarmId = function() return 1 end}
g_i18n = nil
dofile("FS25_ADSmartPickup/adStorageStats.lua")
ADRunsController = {store = {runs = {}}}
ADRuns = {getRun = function() return nil end}
ADFlowView = {wrapText = function(text) return {text} end}
ADSources = {
getFarmInventory = function() return ADSources.inventory end,
getFarmRates = function() return {[31] = {consumption = 1000, production = 0}, [5] = {consumption = 0, production = 500}} end,
getMarketQuote = function(ft) if ft == 5 then return {price = 1.0, top = 1.05} end return nil end,
}
ADSources.inventory = {
{kind = "silo", name = "Farma #1", placeable = "p1", entries = {{fillType = 31, level = 300, capacity = 1000}, {fillType = 5, level = 20000, capacity = 21000}}},
{kind = "silo", name = "Farma #2", placeable = "p2", entries = {{fillType = 31, level = 700, capacity = 1000}}},
{kind = "factory", name = "Sugar mill", placeable = "p3", entries = {{fillType = 5, level = 10, capacity = 100, isOutput = true}}},
}
local i18nTexts = {}
local i18n = {getText = function(_, key) i18nTexts[key] = true; if key:find("days") then return "%.1f d" end
if key:find("share") then return "%d %%" end
if key:find("alert_full") then return "%s %d %% %s" end
if key:find("alert_empty") then return "%s %s %s %s" end
if key:find("alert_sell") then return "%s %d %s" end
if key:find("pallets") then return "%d p" end
if key:find("perDay") or key:find("fullIn") then return "%s" end
return key end}
SmartPickupFrame = {}
local function load(path) local chunk = assert(loadfile(path)); chunk() end
SmartPickupFrame.new = nil
load("FS25_ADSmartPickup/gui/SmartPickupFrame.lua")
load("FS25_ADSmartPickup/gui/SmartPickupStorageTab.lua")
load("FS25_ADSmartPickup/gui/SmartPickupStorageView.lua")
load("FS25_ADSmartPickup/gui/SmartPickupFlowTab.lua")
-- en frame uden spillets klasse-maskineri: kun det fanen bruger
local frame = setmetatable({}, {__index = SmartPickupFrame})
frame.i18n = i18n
frame.clonedRows, frame.rowData = {}, {}
frame.currentTab = 7 -- Siloer
local rows = {}
frame.boxLayout = {elements = rows, invalidateLayout = function() end}
frame.productPrefab = newElement({"option", "title"})
frame.productPrefab.children.option.children.ignore = newElement()
frame.textPrefab = newElement({"title"})
frame.kpiPrefab = newElement({"v1", "l1", "v2", "l2", "v3", "l3", "v4", "l4"})
frame.alertPrefab = newElement({"dot", "c1"})
frame.itemPrefab = newElement({"c1", "c2", "c3", "c4", "bar", "barBg"})
frame.subPrefab = newElement({"c1", "c2", "c3", "c4", "bar", "barBg"})
frame.statPrefab = newElement({"c1", "c2", "c3", "c4", "c5", "c6", "c7"})
frame.gridPrefab = newElement({"c1", "c2", "c3"})
for _, prefab in ipairs({frame.productPrefab, frame.textPrefab, frame.kpiPrefab, frame.alertPrefab, frame.itemPrefab, frame.subPrefab, frame.statPrefab, frame.gridPrefab}) do
local base = prefab.clone
prefab.clone = function(self) local copy = base(self); if self.children.option then copy.children.option.children.ignore = newElement() end; table.insert(rows, copy); return copy end
end
frame.getAdEnv = function() return {} end
frame.setStatusText = function() end
frame.refreshButtons = function() end
frame.restoreFocusIfLost = function() end
frame.colorRows = function() end
for _, name in ipairs({"startButtonInfo", "renameButtonInfo", "deleteButtonInfo", "stopAllButtonInfo", "renameBuildingButtonInfo"}) do frame[name] = {} end
local function texts()
local list = {}
for _, row in ipairs(rows) do
for _, name in ipairs({"title", "c1", "c2", "c3", "c4", "v1", "v3"}) do
local child = row.children[name]
if child ~= nil and child.text ~= nil then table.insert(list, child.text) end
end
end
return table.concat(list, "|")
end
-- SVW1: opbygning af Siloer i varevisning
local ok, err = pcall(frame.rebuild, frame)
check("SVW1 opbygning kaster ikke", ok, true)
if not ok then print(err) end
local all = texts()
check("SVW1 straw-række", all:find("+ Straw", 1, true) ~= nil, true)
check("SVW1 sugar-række", all:find("+ Sugar", 1, true) ~= nil, true)
check("SVW1 straw 1.0k/2.0k 50 %", all:find("1.0k/2.0k · 50 %", 1, true) ~= nil, true)
check("SVW1 ingen bygninger før klik", all:find("Farma #2", 1, true), nil)
check("SVW1 fabrikkens sukker ikke i siloer", all:find("Sugar mill", 1, true), nil)
check("SVW1 kræver handling: straw løber tør", all:find("spu_alert", 1, true) == nil and all:find("Straw 1.0 d", 1, true) ~= nil, true)
-- SVW2: klik på straw folder ud; bygninger med mest først
local strawRow = frame.itemView.itemRows[1]
ok, err = pcall(frame.toggleItem, frame, 31)
check("SVW2 fold ud kaster ikke", ok, true)
if not ok then print(err) end
all = texts()
local first, second = all:find("Farma #2", 1, true), all:find("Farma #1", 1, true)
check("SVW2 Farma #2 vist", first ~= nil, true)
check("SVW2 flest først", first ~= nil and second ~= nil and first < second, true)
check("SVW2 markør skiftet", all:find("– Straw", 1, true) ~= nil, true)
check("SVW2 andel", all:find("70 %", 1, true) ~= nil, true)
-- SVW3: opdatering hvert sekund med nye tal, samme struktur
ADSources.inventory[2].entries[1].level = 900
ok, err = pcall(frame.refreshLive, frame)
check("SVW3 opdatering kaster ikke", ok, true)
if not ok then print(err) end
check("SVW3 ny total i cellen", texts():find("1.2k/2.0k · 60 %", 1, true) ~= nil, true)
local bar = frame.itemView.itemRows[1].bar
check("SVW3 bjælke 60 %", math.floor(bar.size[1] / 0.2 * 100 + 0.5), 60)
check("SVW3 bjælke gul (løber tør < 3 d)", bar.color ~= nil and bar.color[1] > 0.9 and bar.color[2] > 0.7, true)
-- SVW4: skift til bygningsvisning og tilbage
ok, err = pcall(frame.onClickStorageView, frame, SmartPickupFrame.STORAGE_VIEW_BUILDINGS)
check("SVW4 bygningsvisning kaster ikke", ok, true)
if not ok then print(err) end
check("SVW4 ingen varevisning", frame.itemView, nil)
ok = pcall(frame.onClickStorageView, frame, SmartPickupFrame.STORAGE_VIEW_ITEMS)
check("SVW4 tilbage til varer", ok and frame.itemView ~= nil, true)
-- SVW5: fabrikker-fanen: output næsten fuldt -> rød
frame.currentTab = 6
ok, err = pcall(frame.rebuild, frame)
check("SVW5 fabrikker kaster ikke", ok, true)
if not ok then print(err) end
-- SVW7: udfoldet vare hvor to siloer bytter plads i mængde -> ingen genopbygning, rækkerne følger bygningen
frame.currentTab = 7
frame.expandedItems = nil
frame:rebuild()
frame:toggleItem(31)
local subsBefore = frame.itemView.itemRows[1].subs
ADSources.inventory[1].entries[1].level = 950 -- Farma #1 (300 -> 950) forbi Farma #2 (900)
for _ = 1, 12 do frame:refreshLive() end
check("SVW7 samme rækker (ingen genopbygning)", frame.itemView.itemRows[1].subs, subsBefore)
check("SVW7 første underrække er stadig Farma #2", subsBefore[1].cells[1].text, "Farma #2")
check("SVW7 anden underrække viser Farma #1's nye tal", subsBefore[2].cells[2].text:find("950", 1, true) ~= nil, true)
-- SVW6: alle brugte l10n-nøgler findes i begge sprogfiler
for _, file in ipairs({"FS25_ADSmartPickup/l10n/l10n_da.xml", "FS25_ADSmartPickup/l10n/l10n_en.xml"}) do
local content = io.open(file):read("*a")
for key in pairs(i18nTexts) do
if key:find("^spu_") and not content:find('name="' .. key .. '"', 1, true) then
failures = failures + 1
print("FAIL SVW6 mangler " .. key .. " i " .. file)
end
end
end
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)