Compare commits
66 Commits
v1.60.12.0
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| bb287a8154 | |||
| e229a27624 | |||
| ba98cf83d3 | |||
| a6662d6da1 | |||
| bf3b99aca4 | |||
| cbc9b2c8e3 | |||
| 1bda82a270 | |||
| 21b48d7664 | |||
| 88ddb3d6cd | |||
| 37cf84c101 | |||
| 991252848f | |||
| 6b533e62eb | |||
| c92c870297 | |||
| 18b6c82871 | |||
| 47afdebf64 | |||
| 2290bdde95 | |||
| 0c21ba49a0 | |||
| e1f7b19fef | |||
| 74537f7ec2 | |||
| d2970a1bda | |||
| bd106a2176 | |||
| acf94c8e88 | |||
| a3d234a3a2 | |||
| 68eb26d71d | |||
| 1c60529dfd | |||
| b33f2a3b01 | |||
| d2a4cf0d23 | |||
| 928299c8ee | |||
| e9f1251471 | |||
| 9fde03f950 | |||
| 979b7f72d5 | |||
| ba2a3e94f1 | |||
| 131b1ad10c | |||
| 19a10ab67c | |||
| 1e7b2ec699 | |||
| 817fc388a2 | |||
| 83394aabfd | |||
| 9e34a16dfa | |||
| d2453f78fb | |||
| 605c4e8d45 | |||
| 9c9c2badeb | |||
| e394a77833 | |||
| b77bda5334 | |||
| f0b8df8ae3 | |||
| b389fa3564 | |||
| 32f2886a55 | |||
| 2a7769d090 | |||
| 49eed51202 | |||
| 6dd1b5cac0 | |||
| 217498aefa | |||
| 2e7a7105c1 | |||
| 27017c0f62 | |||
| f813d4b3d8 | |||
| 4f676135ac | |||
| 2a36de0837 | |||
| c415bdc7a4 | |||
| a6506fea4e | |||
| 8abcbcdcb8 | |||
| 3c67591cb3 | |||
| 784144cd4e | |||
| 13e5dbf188 | |||
| 4de0bfcb46 | |||
| 53346cc916 | |||
| 4e8e9aa350 | |||
| f8864e26c3 | |||
| b33f7682b4 |
@@ -1 +1,5 @@
|
|||||||
*.zip
|
*.zip
|
||||||
|
docs/superpowers/
|
||||||
|
bench/
|
||||||
|
.superpowers/
|
||||||
|
tests/
|
||||||
|
|||||||
@@ -6,8 +6,51 @@ står i [docs/BRUGERMANUAL.md](docs/BRUGERMANUAL.md) (dansk) og [docs/USER_MANUA
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 1.62 — Kæder slås selv til
|
||||||
|
|
||||||
|
- **1.62.7** Nyt: konsolkommandoen `spuSellBlock` slår salgssteder fra og til (pr. savegame). Et salgssted, der er slået fra, bruges hverken til udkørsel eller i priserne. Til stationer, hvor aflæsningen går i stå, fx "Market Price Liquid Fill Station".
|
||||||
|
- **1.62.6** Pålæsningsdiagnosen logger kun, når en trigger med varen er inden for 20 m (køretøjer der holder ved Wait-punkter eller marker gav falske meldinger).
|
||||||
|
- Manualerne understreger nu, at parkering og hentning af redskaber kun virker ved at bakke: ruten til `P <navn>` og `Connect P <navn>` skal slutte som en bakrute.
|
||||||
|
- **1.62.5** Diagnose: står AutoDrive 30 s ved et LOAD-mål uden at fylde, skriver loggen én linje med markør, vare og for hver vogn den nærmeste AD-læssetrigger (afstand, har varen, står vognen i triggerens boks). Ingen indgreb.
|
||||||
|
- **1.62.4** Rettet: påfyldning af et langt sæt fik AutoDrive til at oprette ekstra markører med stationens navn (fx "Buying Station Multifruit"). Det gør AutoDrives `getMarkerAtStation`, når ingen markør passer til togets længde. Moden kalder den ikke længere; markøren findes ved stationens læssetriggere eller ved bygningen.
|
||||||
|
- **1.62.3** Rettet: påfyldning ved købestation valgte en anden stations markør for lange sæt. AutoDrive vil have markøren togets længde + 3–13 m forbi triggeren, så Puma + Kinze 4900 ventede på frø ved "Market Price Liquid Fill Station" i stedet for "Buying Station Multifruit". Nu vælges den nærmeste markør ved stationens egne læssetriggere; P-, Connect- og Wait-markører springes over. Loggen skriver, hvor et tomt redskab fyldes.
|
||||||
|
- **1.62.2** Kæderne læser fabrikkernes udgange (Sælg / Fordel / Behold). **Fordel:** spillet leverer selv til gårdens fabrikker, så kæden opretter intet flow, og fanen Kæder viser *fordeles af spillet*. **Behold** (spawn eller lager): et flow henter varen ved fabrikken som hidtil. **Sælg på en mellemvare** betyder, at næste led i kæden intet får, så kæden sætter den til Behold og henter den.
|
||||||
|
- **1.62.1** Rettet: udkørsel fra objektlager (fx Sell Egg fra Æglageret) stoppede halvfuld med "spawner ikke … (30 s uden fremdrift)". Lageret deler samme vare i flere grupper (paller med forskellig fyldning), og moden talte kun den første gruppe — når en lille gruppe blev tom, så lageret ud til at vokse. Nu tælles alle grupper. Advarslen viser lager- og vogntal, hvis den alligevel opstår.
|
||||||
|
- **1.62.0** **En kæde er noget du selv slår til:** tændte produktioner er ikke længere automatisk kæder (FS25 tænder nye produktioner som standard — 7 ensilagetårne gav 56 kæder og fik AIO-fabrikkens høproduktion tændt). Fanen **Kæder** har nederst *N tændte produktioner uden kæde* (foldet sammen); **[Kæde til]** gør en produktion til en kæde. En pauset kæde har **[Fjern]**. Ikke-valgte produktioner bruges stadig som kilde. Når en kæde fjernes, slettes dens tomme kædeflows (uden traktor), og mellemled kæden selv tændte slukkes igen. Rettet: kædens flowoversigt manglede traktorerne, så sammenlægning/oprydning kunne ramme et kædeflow med traktor. Udkørsel fra palleskur holder AutoDrive, mens moden tager paller ud.
|
||||||
|
|
||||||
|
## 1.61 — Produktionskæder
|
||||||
|
|
||||||
|
- **1.61.13** Udkørsel fra palleskur: mens moden tager paller ud (én ad gangen), holdes AutoDrives egen pålæsning, så vognen ikke kører halvfuld, når triggeren et øjeblik er tom. Pålæsningen slutter, når vognen er fuld, lageret er tomt, eller skuret ikke har udleveret noget i 30 s.
|
||||||
|
- **1.61.12** Oversigt-tavlen folder også kædeflows uden traktor sammen og viser navnene uden "Kæde: "; et kædeflow uden traktor har status *mangler traktor/vogn* (tavle og Flows-liste).
|
||||||
|
- **1.61.11** Markørvalg ved bygninger: når to markører står inden for 20 m af hinanden, afgør navnet — pålæsning foretrækker *Pickup/Hent/Læs/Load*, aflæsning *Unload/Drop/Aflæs*. Før vandt den nærmeste (Sell Egg kørte til "Chicken Storage Unload" i stedet for "Egg Storage Pickup").
|
||||||
|
- **1.61.10** Markflådens puljeliste viser korte maskinnavne (uden " Unrealistic" og parenteser som "(+ Custom Crops)"), så lange linjer ikke ombrydes med traktorens nummer alene på næste linje.
|
||||||
|
- **1.61.9** *Sælg fra* i udkørselsflows har valget **Altid (uanset pris)** øverst: flowet sælger, så snart der er noget at køre med, uden at vente på en god pris. Resumé og status viser *sælg altid*.
|
||||||
|
- **1.61.8** Kæderne regner **læs i vogne og høstertanke** med som lager (afgrøder og markvarer), og der kommer ingen forslag eller automatiske markskift for en afgrøde, mens den høstes. Klik på en varelinje i fanen **Kæder** åbner flowet, der kører varen, i redigeringsvinduet (så en stoppet kæde kan få traktor og vogn).
|
||||||
|
- **1.61.7** Kædelinjen viser *Gård* (uden klik) ved slutprodukter, gården selv bruger (fx ensilage, TMR, roesnitter), og *Sælg*/*Behold* kun ved varer, der faktisk kan sælges straks.
|
||||||
|
- **1.61.6** Slutprodukter sælges kun straks, hvis gården ikke selv bruger varen (stald, tændt produktion, forbrug eller flow). Varer kæden allerede havde sat til salg, sættes automatisk tilbage til *Behold*, når de ikke længere må sælges (in-game: TMR og roesnitter blev sat til salg).
|
||||||
|
- **1.61.5** Redskaber bredere end Courseplays 50 m (Unrealistic kan sættes til op til 120 m) afvises ikke længere: de bruges, og kursusbredden sættes til 50 m, før Courseplay laver kurset. Markflåden viser *kursus 50 m* ved dem.
|
||||||
|
- **1.61.4** Kæder mere realistiske: **ét kædeflow pr. bygning** med alle bygningens manglende varer (før ét pr. vare og bygning); nye varer lægges i bygningens eksisterende kædeflow, og tidligere tomme kædeflows (uden traktor) til samme bygning lægges sammen. **Slutprodukter sælges straks** som standard (fabrikkens udgang sættes til *Sælg direkte*); feltet *Sælg*/*Behold* på kædelinjen skifter, og *Behold* sætter varer, kæden selv satte til salg, tilbage. I Flows-listen foldes kædeflows uden traktor sammen i én linje. En såmaskine med gødningstank (fx Kinze 4905) bruges kun til såning, ikke som gødningsspreder.
|
||||||
|
- **1.61.3** Fanen **Kæder** sorteret: forslag til ny afgrøde står samlet øverst, én gang pr. mark og efter marknummer (før gentaget under hver kæde, der bruger afgrøden); kæderne står efter status (mangel/fejl, venter, dækket), så bygning og produkt. Kædelinjen viser stadig "· N forslag".
|
||||||
|
- **1.61.2** Flows-listen: dine egne flows står øverst; kædernes auto-oprettede flows samles nederst under *Kædernes egne flows* og vises uden "Kæde: ", så bygningen kan ses.
|
||||||
|
- **1.61.1** Fanen **Kæder** står nu efter **Marker**, og navne i kædelisten afkortes ikke længere (fx "+ Fermented Chaff", "+ Whole Crop Silage").
|
||||||
|
- **1.61.0** Produktionskæder: slå et produkt til i en fabrik, og moden holder det kørende helt tilbage til marken. Kæden regner baglæns gennem dine egne fabrikker og stalde til afgrøderne og ser 12 måneder frem. Ny fane **Kæder** med træ pr. rod-produktion, farvet status, fold ud/ind og **[Pause]**/**[Start]**. Marker markeret **Fri til kæden** skifter afgrøde automatisk (højst 2 i minuttet); andre marker får forslag med **[Godkend]**/**[Afvis]** (højst 3 pr. afgrøde) i fanen og som ét samlet punkt i **Kræver handling**. En afgrøde, du selv har valgt, ændres aldrig. Mellemled tændes og slukkes kun igen, hvis kæden tændte dem. Flows **Kæde: <vare> → <bygning>** oprettes med en ledig pool-traktor og tipvogn (ellers stoppet: *mangler traktor/vogn*; paller altid stoppet) og stoppes, ikke slettes, når kæden slås fra. Dine egne tændte produktioner bruges som kilde først; slukker du en produktion, tænder kæden den ikke igen, før du tænder en ny. Stoppede kædeflows slippes af kæden. Ingen handlinger de første 2 minutter, ingen markskift før alle marker er scannet, og intet slukkes ved delvise læsefejl. Begrænsninger: intet køb, ingen dyrekøb, flerårige afgrøder og græsmarker skiftes aldrig, grundudbytte uden gødningsbonus. **Vigtigt:** hver produktion, du tænder i en fabrik, prøver moden at forsyne — tænd kun det, du vil have produceret; sluk produktionen eller tryk [Pause] for at stoppe kæden. Manglende led forklares i klart sprog med råd (*Ingen af dine fabrikker laver Mel — byg en eller køb*, *Mølle kan kun lave 800 af 1.0k/md*, *Dine stalde leverer ikke nok Mælk …*, *… er slukket af dig*, *Kredsløb omkring …*): som rød tekst under varen i fanen Kæder, som én samlet rød linje i **Kræver handling** (*Kæder: 3 manglende led*, under lagerets røde linjer; klik åbner fanen Kæder med kæderne foldet ud) og som engangsbesked med kædens navn foran, når et nyt led mangler — højst 3 ad gangen, resten samlet i *+N manglende led — se fanen Kæder* (ikke i opstartsfristen eller mens markerne scannes). Markvarer, der ikke er en afgrødes hovedvare (hø, tørret lucerne/kløver, hakkelse, helsæd, halm …), står med kilden *fra marker* uden afgrødeplan eller advarsel, så fermenteringssiloer ikke giver falske *byg en eller køb*. Nyt **statusvindue** på skærmen (i stil med Simple Inspector), som slås til/fra med **numpad \*** (Insert bruges af Universal Autoload; kan ændres under Styring; virker til fods og i køretøjer): øverst til højre under info-boksen vises flow-traktorernes handling og fyld, markarbejde i gang, kæder med manglende led og advarsler (slukkede nødvendige fabrikker, køretøjer der har stået stille over 2 min — solgte vises ikke —, ventende kædeforslag); traktorlinjernes farve følger flowets niveau som Flows-listen — højst 18 linjer, opdateres hvert sekund, skjules ved menu/dialog, skjult HUD og søvn; tændt/slukket gemmes pr. bruger i `modSettings/FS25_ADSmartPickup/status.xml`. **Advarsel når en nødvendig fabrik slukkes** (også uden kæder): slukker du en produktion, hvis vare et tændt flow kører (valgt vare, eller målet tager den i auto og gården forbruger den), en stald æder eller en aktiv kæde bruger sammen med et flow/en stald, kommer én besked pr. slukning (*Advarsel: du slukkede Fermenteringssilo 3 (Ensilage) — Cow Food, Goat Food +1 bruger det. Lageret rækker ca. 12 dage.*) og en rød linje *Slukket men nødvendig: …* i **Kræver handling** (højst én, flere tælles som *+k*; klik åbner fanen Fabrikker) og i statusvinduet, så længe den er slukket og nødvendig. Ingen advarsel når en anden opskrift i samme fabrik laver varen og er tændt, en anden tændt produktion dækker med nok kapacitet, kun kæder bruger varen (meldes som manglende led), lageret rækker over 60 dage, kæden selv slukkede den, eller den allerede var slukket ved indlæsning; produktioner ingen bruger, advarer aldrig. Manual og ordliste opdateret.
|
||||||
|
|
||||||
## 1.60 — "Kræver handling" → flow, menuplacering, flow-vogne i guiden
|
## 1.60 — "Kræver handling" → flow, menuplacering, flow-vogne i guiden
|
||||||
|
|
||||||
|
- **1.60.21** Flow-vogne: kun et redskab med lastrum (ikke plov, kultivator eller såmaskine) huskes som flowets vogn. Før kunne en plov, der hang bag en flow-traktor, blive gemt som gyllevogn, og gyllen blev aldrig kørt.
|
||||||
|
- **1.60.20** Vinhøsterens bredde: også en lille bredde fra Courseplay (under 1,5 m) erstattes af vinrækkens 3 m.
|
||||||
|
- **1.60.19** Vinhøster uden kendt arbejdsbredde får vinrækkens 3 m (før "bredde ukendt", og marken blev ikke startet).
|
||||||
|
- **1.60.18** Vinhøsterens afgrøde læses fra `inputFruitTypeIndex` (feltnavnet i FS25).
|
||||||
|
- **1.60.17** Vinhøstere (fx Grégoire GL86) genkendes som høstere af den afgrøde, de er konfigureret til
|
||||||
|
(spec_vineCutter). Før fik vinmarker "ingen kan" (in-game: F22 hvide druer).
|
||||||
|
- **1.60.16** Tipvogne skiftes under AD-aflæsning til en tip-side, AutoDrive må aflæsse fra (`allowAIDischarge`).
|
||||||
|
Farmtech EDK 650 (Unrealistic) tillader kun bagudtip; stod den på venstre, højre eller kornluge, åbnede AD
|
||||||
|
aflæsningen, men intet løb ud, og vognen holdt med fuldt læs. CP-genstartslisten logges kun, når den ændrer sig.
|
||||||
|
- **1.60.15** Humle (HOPS, Dakota) behandles som vinafgrøde: aldrig slåafgrøde, og markmaskiner kører på rækkerne som ved druer.
|
||||||
|
- **1.60.14** Afkobling ved redskabets egen parkering godtager 8 m (før 5 m): lange redskaber som en valse står
|
||||||
|
med roden over 5 m inde i båsen, så afkoblingen fejlede med "stoppedAway".
|
||||||
|
- **1.60.13** CP-genstart efter indlæsning starter kun maskiner, hvis valgte CP-job er markarbejde. Moddets
|
||||||
|
markopgaver starter CP med eget job, så HUD'en kunne stå på fx bunkersilo, og HUD-starten gav "CP: No silo found".
|
||||||
- **1.60.12** "Start alle" ophæver altid pausen: et flow på pause, hvis traktorer stadig kørte en gemt AD-tur,
|
- **1.60.12** "Start alle" ophæver altid pausen: et flow på pause, hvis traktorer stadig kørte en gemt AD-tur,
|
||||||
blev talt som "kørte allerede" og forblev stoppet — traktoren hentede sin vogn og kørte den straks hjem igen.
|
blev talt som "kørte allerede" og forblev stoppet — traktoren hentede sin vogn og kørte den straks hjem igen.
|
||||||
- **1.60.11** Udkørselsregler vises kun, når en vare bruger dem: salgsgrænserne ("Sælg fra", "Sælg altid
|
- **1.60.11** Udkørselsregler vises kun, når en vare bruger dem: salgsgrænserne ("Sælg fra", "Sælg altid
|
||||||
|
|||||||
@@ -251,12 +251,43 @@ local function markerCandidates(adEnv, nodes)
|
|||||||
return found
|
return found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Markørnavnets rolle: "unload" (unload/drop/aflæs/aflever/indkast/tip) eller "load" (pickup/hent/læs/load) —
|
||||||
|
-- aflæsning tjekkes først ("unload" og "aflæs" indeholder "load"/"læs"). nil = neutralt navn.
|
||||||
|
local UNLOAD_WORDS = {"unload", "drop", "aflæs", "aflev", "indkast", "tip"}
|
||||||
|
local LOAD_WORDS = {"pickup", "pick up", "hent", "læs", "load"}
|
||||||
|
function ADBuildings.nameRole(name)
|
||||||
|
local lower = tostring(name or ""):lower()
|
||||||
|
for _, word in ipairs(UNLOAD_WORDS) do if lower:find(word, 1, true) then return "unload" end end
|
||||||
|
for _, word in ipairs(LOAD_WORDS) do if lower:find(word, 1, true) then return "load" end end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Når to markører står tæt (inden for NAME_WINDOW_M af den nærmeste), afgør navnet: den første med den ønskede rolle
|
||||||
|
-- flyttes forrest. Ellers urørt (nærmeste først). (in-game 30/9: Sell Egg tog "Chicken Storage Unload" 17 m frem for
|
||||||
|
-- "Egg Storage Pickup" 27 m ved Æglageret)
|
||||||
|
ADBuildings.NAME_WINDOW_M = 20
|
||||||
|
function ADBuildings.preferByName(candidates, wantRole)
|
||||||
|
local list = candidates or {}
|
||||||
|
if #list < 2 then return list end
|
||||||
|
local limit = (list[1].distance or 0) + ADBuildings.NAME_WINDOW_M
|
||||||
|
if ADBuildings.nameRole(list[1].name) == wantRole then return list end
|
||||||
|
for index, candidate in ipairs(list) do
|
||||||
|
if (candidate.distance or math.huge) > limit then break end
|
||||||
|
if ADBuildings.nameRole(candidate.name) == wantRole then
|
||||||
|
local reordered = {candidate}
|
||||||
|
for other, item in ipairs(list) do if other ~= index then table.insert(reordered, item) end end
|
||||||
|
return reordered
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
|
||||||
-- Læssemarkør for én læssegruppe. En pallespawner (upræcist startpunkt) får aldrig aflæsningens nærmeste markør —
|
-- Læssemarkør for én læssegruppe. En pallespawner (upræcist startpunkt) får aldrig aflæsningens nærmeste markør —
|
||||||
-- den tager sin næstnærmeste. Rigtige læssetriggere beholder markøren (Sugarbeet Crusher: pickup står tættere på
|
-- den tager sin næstnærmeste. Rigtige læssetriggere beholder markøren (Sugarbeet Crusher: pickup står tættere på
|
||||||
-- aflæsningstriggeren end drop). Gælder alle bygningstyper. (in-game 27/9 fårestald: spawner 9 m fra "Sheep Food",
|
-- aflæsningstriggeren end drop). Gælder alle bygningstyper. (in-game 27/9 fårestald: spawner 9 m fra "Sheep Food",
|
||||||
-- foderindkast 5 m -> Goat Food fik "Sheep Milk"; i stald 3 og 5 stod spawneren endda tættest, 5 mod 9 m)
|
-- foderindkast 5 m -> Goat Food fik "Sheep Milk"; i stald 3 og 5 stod spawneren endda tættest, 5 mod 9 m)
|
||||||
function ADBuildings.pickLoadMarker(candidates, unloadBest, yieldsToUnload)
|
function ADBuildings.pickLoadMarker(candidates, unloadBest, yieldsToUnload)
|
||||||
for _, candidate in ipairs(candidates or {}) do
|
for _, candidate in ipairs(ADBuildings.preferByName(candidates, "load")) do
|
||||||
local isUnloads = yieldsToUnload and unloadBest ~= nil and candidate.id == unloadBest.id
|
local isUnloads = yieldsToUnload and unloadBest ~= nil and candidate.id == unloadBest.id
|
||||||
if not isUnloads then
|
if not isUnloads then
|
||||||
return {id = candidate.id, name = candidate.name, distance = math.floor(candidate.distance + 0.5)}
|
return {id = candidate.id, name = candidate.name, distance = math.floor(candidate.distance + 0.5)}
|
||||||
@@ -314,7 +345,7 @@ local function unloadMarkerFor(adEnv, nodes, loadMarkers, allowSkipLoad)
|
|||||||
local x, z = getNodePosition(node)
|
local x, z = getNodePosition(node)
|
||||||
if x ~= nil then table.insert(positions, {x = x, z = z}) end
|
if x ~= nil then table.insert(positions, {x = x, z = z}) end
|
||||||
end
|
end
|
||||||
local best, bestOther = nil, nil
|
local best, bestOther, nearbyAll = nil, nil, {}
|
||||||
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||||||
if not ADSupplyPlanner.isWaitMarker(marker) then
|
if not ADSupplyPlanner.isWaitMarker(marker) then
|
||||||
local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id)
|
local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id)
|
||||||
@@ -323,6 +354,7 @@ local function unloadMarkerFor(adEnv, nodes, loadMarkers, allowSkipLoad)
|
|||||||
local dx, dz = wayPoint.x - position.x, wayPoint.z - position.z
|
local dx, dz = wayPoint.x - position.x, wayPoint.z - position.z
|
||||||
local distance = math.sqrt(dx * dx + dz * dz)
|
local distance = math.sqrt(dx * dx + dz * dz)
|
||||||
if distance < ADBuildings.MAX_MARKER_DISTANCE then
|
if distance < ADBuildings.MAX_MARKER_DISTANCE then
|
||||||
|
table.insert(nearbyAll, {marker = marker, distance = distance})
|
||||||
if best == nil or distance < best.distance then best = {marker = marker, distance = distance} end
|
if best == nil or distance < best.distance then best = {marker = marker, distance = distance} end
|
||||||
if not isLoad[marker.id] and (bestOther == nil or distance < bestOther.distance) then
|
if not isLoad[marker.id] and (bestOther == nil or distance < bestOther.distance) then
|
||||||
bestOther = {marker = marker, distance = distance}
|
bestOther = {marker = marker, distance = distance}
|
||||||
@@ -338,6 +370,16 @@ local function unloadMarkerFor(adEnv, nodes, loadMarkers, allowSkipLoad)
|
|||||||
if allowSkipLoad and bestOther ~= nil and bestOther.distance <= ADBuildings.MAX_DROP_DISTANCE then
|
if allowSkipLoad and bestOther ~= nil and bestOther.distance <= ADBuildings.MAX_DROP_DISTANCE then
|
||||||
chosen = bestOther
|
chosen = bestOther
|
||||||
end
|
end
|
||||||
|
-- navnet afgør mellem tætte markører: en "Unload/Drop"-markør frem for en "Pickup"-markør (in-game 30/9)
|
||||||
|
table.sort(nearbyAll, function(a, b) return a.distance < b.distance end)
|
||||||
|
if chosen ~= nil and ADBuildings.nameRole(chosen.marker.name) == "load" then
|
||||||
|
for _, entry in ipairs(nearbyAll) do
|
||||||
|
if entry.distance <= chosen.distance + ADBuildings.NAME_WINDOW_M and ADBuildings.nameRole(entry.marker.name) == "unload" then
|
||||||
|
chosen = entry
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
if chosen == nil then
|
if chosen == nil then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,643 @@
|
|||||||
|
-- AD Chain (kæde-kørsel)
|
||||||
|
-- Hvert minut (efter opstartsfristen): læs gården (ADChainData), byg kæden (ADChainModel), find manglende
|
||||||
|
-- afgrøder (ADCropPlan) og lad ADChainRun skifte frie marker, tænde/slukke mellemled og oprette/stoppe flows.
|
||||||
|
-- Samme tick advarer (ADDependency), når spilleren slukker en produktion gården er afhængig af — også uden kæder.
|
||||||
|
-- Resultatet gemmes i ADChain.last til Kæder-fanen. Tilstanden (byChain, pausede rødder, afviste forslag,
|
||||||
|
-- userOff og det sidst kendte sæt af rødder) gemmes i ADSmartPickup_chain.xml under spillets gem.
|
||||||
|
-- Spiladgang kun inde i funktioner (filen dofile's i test).
|
||||||
|
|
||||||
|
ADChain = {}
|
||||||
|
ADChain.FILE_NAME = "ADSmartPickup_chain.xml"
|
||||||
|
ADChain.ROOT = "ADSmartPickupChain"
|
||||||
|
ADChain.TICK_MS = 60000
|
||||||
|
ADChain.STARTUP_GRACE_MS = 120000
|
||||||
|
ADChain.MAX_FIELD_SWITCHES = 2
|
||||||
|
ADChain.MAX_NEW_FLOWS = 2
|
||||||
|
ADChain.LOG_PREFIX = "ADSmartPickup: kæde: "
|
||||||
|
-- Et afvist forslag vises igen, når manglen for afgrøden har ændret sig mere end dette.
|
||||||
|
ADChain.DISMISS_CHANGE = 0.25
|
||||||
|
|
||||||
|
ADChain.state = {byChain = {}, paused = {}, dismissed = {}, userOff = {}, finalKeep = {}, soldByChain = {}, optIn = {}}
|
||||||
|
ADChain.last = nil
|
||||||
|
-- {[nøgle] = true}: produktioner (ikke kædens) der var tændt ved sidste tick — til at se spillerens slukninger
|
||||||
|
ADChain.prevEnabled = nil
|
||||||
|
-- true fra indlæsning til første tick: kædens produktioner der er slukket, glemmes (spillet blev ikke gemt)
|
||||||
|
ADChain.afterLoad = false
|
||||||
|
-- run-id -> "noVehicle"|"pallet" (true = "noVehicle"): flows kæden oprettede stoppet, fordi der ikke var
|
||||||
|
-- traktor/vogn eller varen er på paller (kun i hukommelsen)
|
||||||
|
ADChain.noVehicle = {}
|
||||||
|
ADChain.switchCount = 0
|
||||||
|
ADChain.now = 0
|
||||||
|
ADChain.timer = 0
|
||||||
|
ADChain.collectFailed = false
|
||||||
|
-- {[rodnøgle|vare|art] = true}: manglende led der allerede er meldt som besked (kun i hukommelsen; nulstilles ved indlæsning)
|
||||||
|
ADChain.problemKeys = {}
|
||||||
|
-- {[nøgle] = true}: produktioner (ikke kædens) der var tændt ved sidste afhængighedstjek (kun kædens tick, kun på
|
||||||
|
-- hele data). nil efter indlæsning: det der allerede er slukket ved første tjek, advares der ikke om.
|
||||||
|
ADChain.depPrevEnabled = nil
|
||||||
|
|
||||||
|
local function contains(list, value)
|
||||||
|
for _, item in ipairs(list or {}) do if item == value then return true end end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sortedKeys(map)
|
||||||
|
local keys = {}
|
||||||
|
for key in pairs(map or {}) do table.insert(keys, key) end
|
||||||
|
table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
|
||||||
|
return keys
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- rene hjælpere ----
|
||||||
|
|
||||||
|
-- userOff = {[recipeKey] = true}: produktioner spilleren slukkede selv — kæden tænder dem ikke igen.
|
||||||
|
-- roots = {[recipeKey] = true} rødderne ved sidste tick (nil = ukendt, fx gem fra en ældre version).
|
||||||
|
function ADChain.newState()
|
||||||
|
-- finalKeep = {[rodnøgle] = true}: slutproduktet beholdes (standard: sælg straks); soldByChain = {["rod|vare"] = true}
|
||||||
|
-- optIn = {[opskriftsnøgle] = true}: produktioner spilleren selv har slået til som kæde (tændte produktioner er
|
||||||
|
-- ellers ikke kæder — FS25 tænder dem som standard)
|
||||||
|
return {byChain = {}, paused = {}, dismissed = {}, userOff = {}, roots = nil, finalKeep = {}, soldByChain = {}, optIn = {}}
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADChain.stateToRecords(state)
|
||||||
|
local records = {byChain = sortedKeys(state.byChain), paused = sortedKeys(state.paused), userOff = sortedKeys(state.userOff), dismissed = {},
|
||||||
|
roots = state.roots ~= nil and sortedKeys(state.roots) or nil,
|
||||||
|
finalKeep = sortedKeys(state.finalKeep or {}), soldByChain = sortedKeys(state.soldByChain or {}), optIn = sortedKeys(state.optIn or {})}
|
||||||
|
local ids = {}
|
||||||
|
for id in pairs(state.dismissed or {}) do table.insert(ids, id) end
|
||||||
|
table.sort(ids)
|
||||||
|
for _, id in ipairs(ids) do
|
||||||
|
local entry = state.dismissed[id]
|
||||||
|
table.insert(records.dismissed, {id = id, fruit = entry.fruit, missingHa = entry.missingHa})
|
||||||
|
end
|
||||||
|
return records
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADChain.stateFromRecords(records)
|
||||||
|
local state = ADChain.newState()
|
||||||
|
if records == nil then return state end
|
||||||
|
for _, key in ipairs(records.byChain or {}) do state.byChain[key] = true end
|
||||||
|
for _, key in ipairs(records.paused or {}) do state.paused[key] = true end
|
||||||
|
for _, key in ipairs(records.userOff or {}) do state.userOff[key] = true end
|
||||||
|
for _, key in ipairs(records.finalKeep or {}) do state.finalKeep[key] = true end
|
||||||
|
for _, key in ipairs(records.soldByChain or {}) do state.soldByChain[key] = true end
|
||||||
|
for _, key in ipairs(records.optIn or {}) do state.optIn[key] = true end
|
||||||
|
if records.roots ~= nil then
|
||||||
|
state.roots = {}
|
||||||
|
for _, key in ipairs(records.roots) do state.roots[key] = true end
|
||||||
|
end
|
||||||
|
for _, entry in ipairs(records.dismissed or {}) do
|
||||||
|
local id = tonumber(entry.id)
|
||||||
|
if id ~= nil and entry.fruit ~= nil and entry.fruit ~= "" then
|
||||||
|
state.dismissed[id] = {fruit = entry.fruit, missingHa = tonumber(entry.missingHa) or 0}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return state
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Afviste forslag beholdes kun mens manglen for afgrøden findes og har ændret sig højst 25 %.
|
||||||
|
function ADChain.pruneDismissed(dismissed, shortages)
|
||||||
|
local byFruit = {}
|
||||||
|
for _, shortage in ipairs(shortages or {}) do byFruit[shortage.fruit] = shortage.missingHa end
|
||||||
|
local kept = {}
|
||||||
|
for id, entry in pairs(dismissed or {}) do
|
||||||
|
local now = byFruit[entry.fruit]
|
||||||
|
local before = entry.missingHa or 0
|
||||||
|
if now ~= nil and math.abs(now - before) <= math.abs(before) * ADChain.DISMISS_CHANGE then
|
||||||
|
kept[id] = {fruit = entry.fruit, missingHa = before}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return kept
|
||||||
|
end
|
||||||
|
|
||||||
|
-- {[mark] = {fruit, missingHa}} -> {[mark] = afgrøde} (ADCropPlan.choose's dismissed)
|
||||||
|
function ADChain.dismissedFruits(dismissed)
|
||||||
|
local result = {}
|
||||||
|
for id, entry in pairs(dismissed or {}) do result[id] = entry.fruit end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADChain.unionFruits(usedFruits, fieldDemand)
|
||||||
|
local result = {}
|
||||||
|
for fruit in pairs(usedFruits or {}) do result[fruit] = true end
|
||||||
|
for fruit in pairs(fieldDemand or {}) do result[fruit] = true end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Modellens kopi af opskrifterne: pausede rødder markeres paused (kører for sig selv, men skaber intet nyt), og
|
||||||
|
-- slukkede userOff-produktioner får 0 cyklusser (de dækker intet behov). Originalerne røres ikke.
|
||||||
|
-- optIn = {[nøgle] = true}: kun de produktioner er rødder (andre tændte markeres notChosen: kører for sig selv som
|
||||||
|
-- kilde, men kæden skaber intet for dem). optIn = nil = alle tændte er rødder (som før).
|
||||||
|
function ADChain.modelRecipes(recipes, paused, userOff, optIn)
|
||||||
|
local result = {}
|
||||||
|
for index, recipe in ipairs(recipes or {}) do
|
||||||
|
local copy = {}
|
||||||
|
for key, value in pairs(recipe) do copy[key] = value end
|
||||||
|
if (paused or {})[recipe.key] then copy.paused = true end
|
||||||
|
if optIn ~= nil and recipe.enabled and not recipe.byChain and not optIn[recipe.key] then copy.notChosen = true end
|
||||||
|
if (userOff or {})[recipe.key] and not recipe.enabled then copy.cyclesPerMonth = 0 end
|
||||||
|
result[index] = copy
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local function copySet(map)
|
||||||
|
local result = {}
|
||||||
|
for key, value in pairs(map or {}) do result[key] = value end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Tændte produktioner der ikke er kædens (= rødderne, inkl. pausede).
|
||||||
|
function ADChain.enabledKeys(recipes)
|
||||||
|
local result = {}
|
||||||
|
for _, recipe in ipairs(recipes or {}) do
|
||||||
|
if recipe.enabled and not recipe.byChain then result[recipe.key] = true end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Spillerens valg og forældede nøgler, før kæden regner (rent: state røres ikke).
|
||||||
|
-- opts = {prevEnabled, isPartial, firstEval}. -> {userOff, byChain, paused, roots, added, forgotten, droppedPaused, cleared}
|
||||||
|
-- * kædens produktion er slukket: af spilleren -> userOff (added); første tick efter indlæsning -> glemt (forgotten)
|
||||||
|
-- * en produktion (ikke kædens) der var tændt sidste tick, er slukket -> userOff (added)
|
||||||
|
-- * userOff ryddes kun når der kommer en ny rod (spilleren tænder noget) — ikke når noget "ikke længere bruges"
|
||||||
|
-- (det gav tænd/sluk-løkker); nye slukninger i samme tick beholdes
|
||||||
|
-- * pausede nøgler for produktioner der er væk eller slukket, droppes (droppedPaused)
|
||||||
|
-- Delvise data: userOff ryddes ikke, rødderne opdateres ikke og pausede droppes ikke.
|
||||||
|
function ADChain.reconcile(state, recipes, opts)
|
||||||
|
opts = opts or {}
|
||||||
|
local roots = ADChain.enabledKeys(recipes)
|
||||||
|
local rootAdded = false
|
||||||
|
if state.roots ~= nil then
|
||||||
|
for key in pairs(roots) do if not state.roots[key] then rootAdded = true end end
|
||||||
|
end
|
||||||
|
local result = {userOff = copySet(state.userOff), byChain = copySet(state.byChain), paused = copySet(state.paused),
|
||||||
|
roots = opts.isPartial and state.roots or roots, added = {}, forgotten = {}, droppedPaused = {}, cleared = false}
|
||||||
|
if rootAdded and not opts.isPartial and next(result.userOff) ~= nil then
|
||||||
|
result.userOff, result.cleared = {}, true
|
||||||
|
end
|
||||||
|
local present = {}
|
||||||
|
for _, recipe in ipairs(recipes or {}) do
|
||||||
|
present[recipe.key] = recipe
|
||||||
|
if not recipe.enabled and recipe.byChain then
|
||||||
|
result.byChain[recipe.key] = nil
|
||||||
|
if opts.firstEval then
|
||||||
|
table.insert(result.forgotten, recipe.key)
|
||||||
|
elseif not result.userOff[recipe.key] then
|
||||||
|
result.userOff[recipe.key] = true
|
||||||
|
table.insert(result.added, recipe.key)
|
||||||
|
end
|
||||||
|
elseif not recipe.enabled and (opts.prevEnabled or {})[recipe.key] and not result.userOff[recipe.key] then
|
||||||
|
result.userOff[recipe.key] = true
|
||||||
|
table.insert(result.added, recipe.key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not opts.isPartial then
|
||||||
|
for key in pairs(state.paused or {}) do
|
||||||
|
if present[key] == nil or not present[key].enabled then
|
||||||
|
result.paused[key] = nil
|
||||||
|
table.insert(result.droppedPaused, key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(result.added); table.sort(result.forgotten); table.sort(result.droppedPaused)
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Rødder hvis flows ikke må stoppes: kædens aktuelle rødder + pausede rødder (pause rører intet).
|
||||||
|
function ADChain.activeRoots(chain, paused)
|
||||||
|
local result = {}
|
||||||
|
for _, root in ipairs((chain or {}).roots or {}) do result[root.key] = true end
|
||||||
|
for key in pairs(paused or {}) do result[key] = true end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Mest brugte Wait-pulje blandt forsyningsflows til bygningen, ellers "".
|
||||||
|
function ADChain.waitPoolFor(runs, building)
|
||||||
|
local counts, best, bestCount = {}, "", 0
|
||||||
|
for _, run in ipairs(runs or {}) do
|
||||||
|
local group = run.waitPoolGroup
|
||||||
|
if run.kind == "supply" and group ~= nil and group ~= "" and contains(run.targetBuildingIds, building) then
|
||||||
|
counts[group] = (counts[group] or 0) + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, group in ipairs(sortedKeys(counts)) do
|
||||||
|
if counts[group] > bestCount then best, bestCount = group, counts[group] end
|
||||||
|
end
|
||||||
|
return best
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADChain.flowName(fillTitle, buildingName)
|
||||||
|
return "Kæde: " .. tostring(fillTitle) .. " → " .. tostring(buildingName)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function covers(run, edge)
|
||||||
|
return contains(run.targetBuildingIds, edge.consumer)
|
||||||
|
and (run.ingredientMode ~= "manual" or contains(run.ingredients, edge.fillType))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Bruger en af de aktuelle kanter stadig flowet? -> kantens rodnøgle (flowet flyttes dertil), ellers nil.
|
||||||
|
function ADChain.stillNeeded(runView, edges)
|
||||||
|
for _, edge in ipairs(edges or {}) do
|
||||||
|
if covers(runView, edge) then return edge.rootKey end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- {[forbruger|vare] = "ok"|"auto"|"missing"|"stopped"|"noVehicle"|"pallet"} for hver kant.
|
||||||
|
function ADChain.flowStatus(edges, runs, noVehicle)
|
||||||
|
local result = {}
|
||||||
|
for _, edge in ipairs(edges or {}) do
|
||||||
|
-- udgangen står på Fordel: spillet leverer selv, intet flow
|
||||||
|
local status = edge.delivery == "auto" and "auto" or "missing"
|
||||||
|
for _, run in ipairs(edge.delivery == "auto" and {} or runs or {}) do
|
||||||
|
if run.kind == "supply" and covers(run, edge) then
|
||||||
|
if not run.stopped then status = "ok"; break end
|
||||||
|
local reason = (noVehicle or {})[run.id]
|
||||||
|
if reason == "pallet" then status = "pallet"
|
||||||
|
elseif reason then status = "noVehicle"
|
||||||
|
elseif status == "missing" then status = "stopped" end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
result[tostring(edge.consumer) .. "|" .. tostring(edge.fillType)] = status
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Flows i det format ADChainActions regner på.
|
||||||
|
function ADChain.runsView(runs)
|
||||||
|
local list = {}
|
||||||
|
for _, run in ipairs(runs or {}) do
|
||||||
|
table.insert(list, {id = run.id, name = run.name, kind = run.kind, targetBuildingIds = run.targetBuildingIds,
|
||||||
|
ingredientMode = run.ingredientMode, ingredients = run.ingredients, stopped = run.stopped == true,
|
||||||
|
chain = run.chain, waitPoolGroup = run.waitPoolGroup,
|
||||||
|
-- traktorerne: uden dem ligner alle kædeflows tomme (sammenlægning/sletning må kun ramme tomme)
|
||||||
|
vehicleIds = run.vehicleIds or {}})
|
||||||
|
end
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Manglende led der ikke var der sidst (nøgle = rodnøgle|vare|art). -> nye problemer (hver nøgle én gang), nøglerne nu.
|
||||||
|
-- Et problem der forsvinder, glemmes; kommer det igen, er det nyt. prevKeys røres ikke.
|
||||||
|
function ADChain.problemKey(problem)
|
||||||
|
return tostring(problem.rootKey) .. "|" .. tostring(problem.fillType) .. "|" .. tostring(problem.kind)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADChain.newProblems(prevKeys, problems)
|
||||||
|
local fresh, keys = {}, {}
|
||||||
|
for _, problem in ipairs(problems or {}) do
|
||||||
|
local key = ADChain.problemKey(problem)
|
||||||
|
if not keys[key] then
|
||||||
|
keys[key] = true
|
||||||
|
if not (prevKeys or {})[key] then table.insert(fresh, problem) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return fresh, keys
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- evaluering ----
|
||||||
|
|
||||||
|
local function farmId()
|
||||||
|
return g_currentMission ~= nil and g_currentMission.getFarmId ~= nil and g_currentMission:getFarmId() or 1
|
||||||
|
end
|
||||||
|
|
||||||
|
local function plan(data, state)
|
||||||
|
local recipes = ADChain.modelRecipes(data.recipes, state.paused, state.userOff, state.optIn)
|
||||||
|
local chain = ADChainModel.build(recipes, data.husbandry, data.crops, data.fieldGoods)
|
||||||
|
-- pausede rødder: det de bruger, er beskyttet mod slukning (modellen regnet som om de ikke var pauset)
|
||||||
|
local protected = {}
|
||||||
|
if next(state.paused) ~= nil then
|
||||||
|
protected = ADChainModel.build(ADChain.modelRecipes(data.recipes, {}, state.userOff, state.optIn), data.husbandry, data.crops, data.fieldGoods).needRecipes
|
||||||
|
end
|
||||||
|
local used = ADChain.unionFruits(data.usedFruits, chain.fieldDemand)
|
||||||
|
-- ufuldstændig markliste: udbuddet ville være undervurderet -> ingen mangler, forslag eller markskift endnu
|
||||||
|
local shortages, actions = {}, {}
|
||||||
|
if not data.fieldsIncomplete then
|
||||||
|
shortages = ADCropPlan.shortages(chain.fieldDemand, data.stock, data.fields, data.yields)
|
||||||
|
state.dismissed = ADChain.pruneDismissed(state.dismissed, shortages)
|
||||||
|
actions = ADCropPlan.choose(shortages, data.fields, used, data.yields, data.priceOf or data.prices, ADChain.dismissedFruits(state.dismissed), data.harvesting)
|
||||||
|
end
|
||||||
|
-- de ægte opskrifter (ikke kopien): en pauset rod er stadig tændt og må ikke "tændes" og overtages af kæden
|
||||||
|
local prod = ADChainActions.productions(data.recipes, chain.needRecipes, protected)
|
||||||
|
return {chain = chain, shortages = shortages, actions = actions, prod = prod, edges = ADChainActions.edges(chain, data.recipes)}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function logInfo(fmt, ...) Logging.info(ADChain.LOG_PREFIX .. fmt, ...) end
|
||||||
|
|
||||||
|
-- Spillerens valg og forældede nøgler ind i tilstanden (kun ved tick, før der regnes).
|
||||||
|
local function applyReconcile(state, data)
|
||||||
|
local result = ADChain.reconcile(state, data.recipes, {prevEnabled = ADChain.prevEnabled, isPartial = data.partial == true,
|
||||||
|
firstEval = ADChain.afterLoad})
|
||||||
|
if result.cleared then logInfo("ny produktion tændt — slukkede produktioner må bruges igen") end
|
||||||
|
for _, key in ipairs(result.added) do logInfo("spilleren slukkede %s — kæden tænder den ikke igen", key) end
|
||||||
|
for _, key in ipairs(result.forgotten) do logInfo("kæden glemmer %s (ikke tændt efter indlæsning)", key) end
|
||||||
|
for _, key in ipairs(result.droppedPaused) do logInfo("pause for %s fjernet: produktionen er væk eller slukket", key) end
|
||||||
|
state.userOff, state.byChain, state.paused, state.roots = result.userOff, result.byChain, result.paused, result.roots
|
||||||
|
ADChain.prevEnabled = ADChain.enabledKeys(data.recipes)
|
||||||
|
ADChain.afterLoad = false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Afhængighedstjek (spec afsnit 8): nye slukninger (kun act) og "Slukket men nødvendig"-posterne. Uafhængigt af
|
||||||
|
-- kædens rødder og af markscanningen; springes over ved læsefejl (så beholdes forrige poster, og slukninger i
|
||||||
|
-- mellemtiden meldes ved næste hele tjek).
|
||||||
|
-- Produktioner kæden selv er ved at tænde (plan.prod.enable), er ikke "slukket men nødvendig".
|
||||||
|
local function dependencyCheck(data, plan, runs, act, previous)
|
||||||
|
if ADDependency == nil or ADChainData.dependencyInputs == nil then return nil end
|
||||||
|
if data.readFailed then
|
||||||
|
return previous ~= nil and {warnings = {}, offNeeded = previous.offNeeded or {}} or nil
|
||||||
|
end
|
||||||
|
local inputs = ADChainData.dependencyInputs(data, plan.chain, runs)
|
||||||
|
local enabling, recipes = {}, {}
|
||||||
|
for _, key in ipairs(plan.prod ~= nil and plan.prod.enable or {}) do enabling[key] = true end
|
||||||
|
for _, recipe in ipairs(data.recipes or {}) do
|
||||||
|
if not enabling[recipe.key] then table.insert(recipes, recipe) end
|
||||||
|
end
|
||||||
|
local prev = act and ADChain.depPrevEnabled or nil
|
||||||
|
local result = {
|
||||||
|
warnings = ADDependency.warnings(prev, recipes, inputs.needed, inputs.stockDays, inputs.capacity, inputs.demand),
|
||||||
|
offNeeded = ADDependency.offNeeded(recipes, inputs.needed, inputs.stockDays, inputs.capacity, inputs.demand),
|
||||||
|
}
|
||||||
|
if act then ADChain.depPrevEnabled = ADDependency.enabledKeys(data.recipes) end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- act = true: udfør handlingerne (tick). Uden act kun en frisk visning (GUI).
|
||||||
|
function ADChain.evaluate(adEnv, act)
|
||||||
|
local state = ADChain.state
|
||||||
|
local ok, data = pcall(ADChainData.collect, adEnv, farmId(), state.byChain)
|
||||||
|
if not ok then
|
||||||
|
if not ADChain.collectFailed then Logging.warning(ADChain.LOG_PREFIX .. "gården kunne ikke læses: %s", tostring(data)) end
|
||||||
|
ADChain.collectFailed = true
|
||||||
|
return ADChain.last
|
||||||
|
end
|
||||||
|
ADChain.collectFailed = false
|
||||||
|
if act then
|
||||||
|
local okState, errState = pcall(applyReconcile, state, data)
|
||||||
|
if not okState then Logging.warning(ADChain.LOG_PREFIX .. "tilstanden kunne ikke opdateres: %s", tostring(errState)) end
|
||||||
|
end
|
||||||
|
local okPlan, result = pcall(plan, data, state)
|
||||||
|
if not okPlan then
|
||||||
|
Logging.warning(ADChain.LOG_PREFIX .. "kæden kunne ikke beregnes: %s", tostring(result))
|
||||||
|
return ADChain.last
|
||||||
|
end
|
||||||
|
if act and ADChainRun ~= nil then ADChainRun.apply(adEnv, data, result) end
|
||||||
|
local runs = ADChain.runsView(ADRunsController.store.runs)
|
||||||
|
local okDep, dependency = pcall(dependencyCheck, data, result, runs, act, ADChain.last ~= nil and ADChain.last.dependency or nil)
|
||||||
|
if not okDep then
|
||||||
|
Logging.warning(ADChain.LOG_PREFIX .. "afhængighedstjek fejlede: %s", tostring(dependency))
|
||||||
|
dependency = nil
|
||||||
|
end
|
||||||
|
local paused = {}
|
||||||
|
for key in pairs(state.paused) do paused[key] = true end
|
||||||
|
ADChain.last = {chain = result.chain, shortages = result.shortages, actions = result.actions, edges = result.edges,
|
||||||
|
missingFlows = ADChainActions.missingFlows(result.edges, runs), flowStatus = ADChain.flowStatus(result.edges, runs, ADChain.noVehicle),
|
||||||
|
paused = paused, userOff = copySet(state.userOff), finalKeep = copySet(state.finalKeep or {}),
|
||||||
|
sellable = ADChain.sellableRoots(data, result, runs), candidates = ADChain.candidates(data.recipes, state), data = data, fields = data.fields, fieldsIncomplete = data.fieldsIncomplete == true, titles = data.fillTitles, at = ADChain.now, switchCount = ADChain.switchCount,
|
||||||
|
dependency = dependency}
|
||||||
|
return ADChain.last
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Alder (ms, spillets ur) af det sidste resultat; math.huge når der intet er. Fanen genbruger et friskt resultat.
|
||||||
|
function ADChain.lastAge()
|
||||||
|
if ADChain.last == nil then return math.huge end
|
||||||
|
return ADChain.now - (ADChain.last.at or 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Engangsbesked pr. nyt manglende led (kun fra tick). Ikke i opstartsfristen og ikke mens gården er læst delvist
|
||||||
|
-- eller markscanningen kører (ellers falske alarmer lige efter indlæsning) — så huskes intet, og det meldes senere.
|
||||||
|
-- Højst MAX_PROBLEM_NOTES beskeder pr. tick; resten samles i én besked (alle logges stadig).
|
||||||
|
ADChain.MAX_PROBLEM_NOTES = 3
|
||||||
|
function ADChain.notifyProblems(last)
|
||||||
|
if last == nil or ADChain.now < ADChain.STARTUP_GRACE_MS then return 0 end
|
||||||
|
if last.fieldsIncomplete or (last.data ~= nil and last.data.partial) then return 0 end
|
||||||
|
local fresh, keys = ADChain.newProblems(ADChain.problemKeys, last.chain ~= nil and last.chain.problems or {})
|
||||||
|
ADChain.problemKeys = keys
|
||||||
|
if #fresh == 0 or ADFieldJobs == nil or ADFieldJobs.notify == nil then return 0 end
|
||||||
|
local textOf = function(key) return g_i18n ~= nil and g_i18n:getText(key) or key end
|
||||||
|
local fmt = SmartPickupFrame ~= nil and SmartPickupFrame.formatCompact or nil
|
||||||
|
local rootNames = {}
|
||||||
|
for _, root in ipairs(last.chain ~= nil and last.chain.roots or {}) do
|
||||||
|
rootNames[root.key] = tostring((last.titles or {})[root.name] or root.name)
|
||||||
|
end
|
||||||
|
for index, problem in ipairs(fresh) do
|
||||||
|
-- kædens navn foran, så to kæder der mangler samme vare kan skelnes
|
||||||
|
local text = tostring(rootNames[problem.rootKey] or problem.rootKey) .. ": " .. ADChainView.problemText(problem, last, textOf, nil, fmt)
|
||||||
|
Logging.info(ADChain.LOG_PREFIX .. "manglende led: %s", text)
|
||||||
|
if index <= ADChain.MAX_PROBLEM_NOTES then ADFieldJobs.notify(text, true) end
|
||||||
|
end
|
||||||
|
-- resten samles i én besked (fx mange fermenteringssiloer efter indlæsning); alle står i fanen Kæder
|
||||||
|
if #fresh > ADChain.MAX_PROBLEM_NOTES then
|
||||||
|
local ok, text = pcall(string.format, textOf("spu_chain_notifyMore"), #fresh - ADChain.MAX_PROBLEM_NOTES)
|
||||||
|
ADFieldJobs.notify(ok and text or ("+" .. tostring(#fresh - ADChain.MAX_PROBLEM_NOTES)), true)
|
||||||
|
end
|
||||||
|
return #fresh
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Én besked pr. slukning af en nødvendig produktion (kun fra tick; advarslerne tømmes, når de er meldt).
|
||||||
|
function ADChain.notifyDependencies(last)
|
||||||
|
local dependency = last ~= nil and last.dependency or nil
|
||||||
|
if dependency == nil or #(dependency.warnings or {}) == 0 or ADChain.now < ADChain.STARTUP_GRACE_MS then return 0 end
|
||||||
|
local warnings = dependency.warnings
|
||||||
|
dependency.warnings = {}
|
||||||
|
if ADFieldJobs == nil or ADFieldJobs.notify == nil then return 0 end
|
||||||
|
local textOf = function(key) return g_i18n ~= nil and g_i18n:getText(key) or key end
|
||||||
|
local titleOf = function(name) return tostring((last.titles or {})[name] or name) end
|
||||||
|
for _, warning in ipairs(warnings) do
|
||||||
|
local text = ADDependency.notifyText(warning, textOf, titleOf)
|
||||||
|
Logging.info(ADChain.LOG_PREFIX .. "nødvendig produktion slukket: %s", text)
|
||||||
|
ADFieldJobs.notify(text, true)
|
||||||
|
end
|
||||||
|
return #warnings
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADChain.tick(adEnv, dt)
|
||||||
|
if adEnv == nil then return end
|
||||||
|
ADChain.now = ADChain.now + (dt or 0)
|
||||||
|
ADChain.timer = ADChain.timer + (dt or 0)
|
||||||
|
if ADChain.now < ADChain.STARTUP_GRACE_MS or ADChain.timer < ADChain.TICK_MS then return end
|
||||||
|
ADChain.timer = 0
|
||||||
|
local last = ADChain.evaluate(adEnv, true)
|
||||||
|
local ok, err = pcall(ADChain.notifyProblems, last)
|
||||||
|
if not ok then Logging.warning(ADChain.LOG_PREFIX .. "besked om manglende led fejlede: %s", tostring(err)) end
|
||||||
|
local okDep, errDep = pcall(ADChain.notifyDependencies, last)
|
||||||
|
if not okDep then Logging.warning(ADChain.LOG_PREFIX .. "advarsel om slukket produktion fejlede: %s", tostring(errDep)) end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- GUI-handlinger ----
|
||||||
|
|
||||||
|
local function suggestionFor(fieldId)
|
||||||
|
for _, action in ipairs(ADChain.last ~= nil and ADChain.last.actions or {}) do
|
||||||
|
if action.fieldId == fieldId then return action end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Godkend forslag: marken får kædens afgrøde som mål (kilde "chain").
|
||||||
|
function ADChain.approve(fieldId)
|
||||||
|
local action = suggestionFor(fieldId)
|
||||||
|
if action == nil then return false end
|
||||||
|
local ok, err = pcall(function()
|
||||||
|
-- et auto-skift må aldrig overskrive en afgrøde spilleren har sat siden; et forslag godkender spilleren selv
|
||||||
|
if action.mode == "auto" then
|
||||||
|
local plan = ADFieldJobs.getField(fieldId).plan or {}
|
||||||
|
if plan.targetSource == "user" then error("spilleren har selv valgt afgrøde") end
|
||||||
|
end
|
||||||
|
ADFieldJobs.setTarget(fieldId, action.to, "chain")
|
||||||
|
end)
|
||||||
|
if not ok then
|
||||||
|
Logging.warning(ADChain.LOG_PREFIX .. "F%s: forslag kunne ikke godkendes: %s", tostring(fieldId), tostring(err))
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
Logging.info(ADChain.LOG_PREFIX .. "F%s: forslag godkendt: %s -> %s", tostring(fieldId), tostring(action.from), tostring(action.to))
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Afvis forslag: vises ikke igen, før manglen for afgrøden ændrer sig > 25 %.
|
||||||
|
function ADChain.dismiss(fieldId)
|
||||||
|
local action = suggestionFor(fieldId)
|
||||||
|
if action == nil then return false end
|
||||||
|
local missingHa = action.hectares or 0
|
||||||
|
for _, shortage in ipairs(ADChain.last.shortages or {}) do
|
||||||
|
if shortage.fruit == action.to then missingHa = shortage.missingHa end
|
||||||
|
end
|
||||||
|
ADChain.state.dismissed[fieldId] = {fruit = action.to, missingHa = missingHa}
|
||||||
|
Logging.info(ADChain.LOG_PREFIX .. "F%s: forslag %s afvist", tostring(fieldId), tostring(action.to))
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Tændte produktioner der ikke er valgt som kæde (og ikke pauset): {{key, name, buildingName}} til fanen Kæder.
|
||||||
|
function ADChain.candidates(recipes, state)
|
||||||
|
local list = {}
|
||||||
|
for _, recipe in ipairs(recipes or {}) do
|
||||||
|
if recipe.enabled and not recipe.byChain and not (state.optIn or {})[recipe.key] and not (state.paused or {})[recipe.key] then
|
||||||
|
table.insert(list, {key = recipe.key, name = recipe.name, buildingName = recipe.buildingName})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(list, function(a, b)
|
||||||
|
if tostring(a.buildingName) ~= tostring(b.buildingName) then return tostring(a.buildingName) < tostring(b.buildingName) end
|
||||||
|
return tostring(a.name) < tostring(b.name)
|
||||||
|
end)
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Slå en produktion til/fra som kæde (fra = også pausen ophæves, kæden slipper den ved næste tick).
|
||||||
|
function ADChain.setOptIn(recipeKey, on)
|
||||||
|
if recipeKey == nil then return end
|
||||||
|
ADChain.state.optIn[recipeKey] = on and true or nil
|
||||||
|
if not on then ADChain.state.paused[recipeKey] = nil end
|
||||||
|
Logging.info(ADChain.LOG_PREFIX .. "%s %s", tostring(recipeKey), on and "slået til som kæde" or "fjernet fra kæderne")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Rødder med et slutprodukt der kan sælges straks (gården bruger det ikke selv). -> {[rodnøgle] = true}
|
||||||
|
function ADChain.sellableRoots(data, result, runs)
|
||||||
|
local set = {}
|
||||||
|
local ok, sales = pcall(ADChainActions.finalSales, result.chain, data.recipes, result.edges, {}, ADChainActions.farmUses(data, runs))
|
||||||
|
for _, sale in ipairs(ok and sales or {}) do set[sale.key] = true end
|
||||||
|
return set
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Slutprodukt for en kæde: keep = true -> "Behold", ellers "Sælg straks" (standard). Anvendes ved næste tick.
|
||||||
|
function ADChain.setFinalKeep(rootKey, keep)
|
||||||
|
if rootKey == nil then return end
|
||||||
|
ADChain.state.finalKeep[rootKey] = keep and true or nil
|
||||||
|
Logging.info(ADChain.LOG_PREFIX .. "%s: slutprodukt %s", tostring(rootKey), keep and "beholdes" or "sælges straks")
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADChain.setPaused(rootKey, isPaused)
|
||||||
|
if rootKey == nil then return end
|
||||||
|
ADChain.state.paused[rootKey] = isPaused and true or nil
|
||||||
|
Logging.info(ADChain.LOG_PREFIX .. "%s %s", tostring(rootKey), isPaused and "på pause" or "genoptaget")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- gem / indlæs (samme sti-opslag som runs; skrives kun fra ADRunsController.save) ----
|
||||||
|
|
||||||
|
local function getPath()
|
||||||
|
local path = ADRunsStorage ~= nil and ADRunsStorage.getPath() or nil
|
||||||
|
if path == nil then return nil end
|
||||||
|
return (path:gsub(ADRunsStorage.FILE_NAME .. "$", ADChain.FILE_NAME))
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADChain.save()
|
||||||
|
local path = getPath()
|
||||||
|
if path == nil then return false end
|
||||||
|
local xmlFile = XMLFile.create("ADSmartPickupChain", path, ADChain.ROOT)
|
||||||
|
if xmlFile == nil then return false end
|
||||||
|
local records = ADChain.stateToRecords(ADChain.state)
|
||||||
|
for index, key in ipairs(records.byChain) do
|
||||||
|
xmlFile:setString(string.format("%s.byChain.recipe(%d)#key", ADChain.ROOT, index - 1), key)
|
||||||
|
end
|
||||||
|
for index, key in ipairs(records.paused) do
|
||||||
|
xmlFile:setString(string.format("%s.paused.root(%d)#key", ADChain.ROOT, index - 1), key)
|
||||||
|
end
|
||||||
|
for index, key in ipairs(records.userOff) do
|
||||||
|
xmlFile:setString(string.format("%s.userOff.recipe(%d)#key", ADChain.ROOT, index - 1), key)
|
||||||
|
end
|
||||||
|
for index, key in ipairs(records.finalKeep) do
|
||||||
|
xmlFile:setString(string.format("%s.finalKeep.root(%d)#key", ADChain.ROOT, index - 1), key)
|
||||||
|
end
|
||||||
|
for index, key in ipairs(records.soldByChain) do
|
||||||
|
xmlFile:setString(string.format("%s.soldByChain.output(%d)#key", ADChain.ROOT, index - 1), key)
|
||||||
|
end
|
||||||
|
for index, key in ipairs(records.optIn) do
|
||||||
|
xmlFile:setString(string.format("%s.optIn.recipe(%d)#key", ADChain.ROOT, index - 1), key)
|
||||||
|
end
|
||||||
|
if records.roots ~= nil then
|
||||||
|
xmlFile:setBool(ADChain.ROOT .. ".roots#known", true)
|
||||||
|
for index, key in ipairs(records.roots) do
|
||||||
|
xmlFile:setString(string.format("%s.roots.recipe(%d)#key", ADChain.ROOT, index - 1), key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for index, entry in ipairs(records.dismissed) do
|
||||||
|
local key = string.format("%s.dismissed.field(%d)", ADChain.ROOT, index - 1)
|
||||||
|
xmlFile:setInt(key .. "#id", entry.id)
|
||||||
|
xmlFile:setString(key .. "#fruit", entry.fruit)
|
||||||
|
xmlFile:setString(key .. "#missingHa", string.format("%.2f", entry.missingHa or 0))
|
||||||
|
end
|
||||||
|
xmlFile:save()
|
||||||
|
xmlFile:delete()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADChain.load()
|
||||||
|
ADChain.state = ADChain.newState()
|
||||||
|
ADChain.noVehicle = {}
|
||||||
|
ADChain.prevEnabled, ADChain.afterLoad = nil, true
|
||||||
|
ADChain.problemKeys = {}
|
||||||
|
ADChain.depPrevEnabled = nil
|
||||||
|
local path = getPath()
|
||||||
|
if path == nil or not fileExists(path) then return end
|
||||||
|
local xmlFile = XMLFile.load("ADSmartPickupChain", path)
|
||||||
|
if xmlFile == nil then return end
|
||||||
|
local records = {byChain = {}, paused = {}, userOff = {}, dismissed = {}, finalKeep = {}, soldByChain = {}, optIn = {}}
|
||||||
|
xmlFile:iterate(ADChain.ROOT .. ".optIn.recipe", function(_, key)
|
||||||
|
table.insert(records.optIn, xmlFile:getString(key .. "#key"))
|
||||||
|
end)
|
||||||
|
xmlFile:iterate(ADChain.ROOT .. ".finalKeep.root", function(_, key)
|
||||||
|
table.insert(records.finalKeep, xmlFile:getString(key .. "#key"))
|
||||||
|
end)
|
||||||
|
xmlFile:iterate(ADChain.ROOT .. ".soldByChain.output", function(_, key)
|
||||||
|
table.insert(records.soldByChain, xmlFile:getString(key .. "#key"))
|
||||||
|
end)
|
||||||
|
xmlFile:iterate(ADChain.ROOT .. ".byChain.recipe", function(_, key)
|
||||||
|
table.insert(records.byChain, xmlFile:getString(key .. "#key"))
|
||||||
|
end)
|
||||||
|
xmlFile:iterate(ADChain.ROOT .. ".paused.root", function(_, key)
|
||||||
|
table.insert(records.paused, xmlFile:getString(key .. "#key"))
|
||||||
|
end)
|
||||||
|
xmlFile:iterate(ADChain.ROOT .. ".userOff.recipe", function(_, key)
|
||||||
|
table.insert(records.userOff, xmlFile:getString(key .. "#key"))
|
||||||
|
end)
|
||||||
|
if xmlFile:getBool(ADChain.ROOT .. ".roots#known", false) then
|
||||||
|
records.roots = {}
|
||||||
|
xmlFile:iterate(ADChain.ROOT .. ".roots.recipe", function(_, key)
|
||||||
|
table.insert(records.roots, xmlFile:getString(key .. "#key"))
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
xmlFile:iterate(ADChain.ROOT .. ".dismissed.field", function(_, key)
|
||||||
|
table.insert(records.dismissed, {id = xmlFile:getInt(key .. "#id"), fruit = xmlFile:getString(key .. "#fruit"),
|
||||||
|
missingHa = tonumber(xmlFile:getString(key .. "#missingHa"))})
|
||||||
|
end)
|
||||||
|
xmlFile:delete()
|
||||||
|
ADChain.state = ADChain.stateFromRecords(records)
|
||||||
|
Logging.info(ADChain.LOG_PREFIX .. "%d produktioner tændt af kæden, %d pausede kæder indlæst", #records.byChain, #records.paused)
|
||||||
|
end
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
-- AD Chain Actions (ren logik): mellemled der skal tændes/slukkes, flows der mangler eller skal stoppes.
|
||||||
|
|
||||||
|
ADChainActions = {}
|
||||||
|
|
||||||
|
-- protected = {[nøgle] = cyklusser}: behov fra pausede rødder — slukkes ikke, men tændes heller ikke.
|
||||||
|
function ADChainActions.productions(recipes, needRecipes, protected)
|
||||||
|
local result = {enable = {}, disable = {}}
|
||||||
|
for _, recipe in ipairs(recipes or {}) do
|
||||||
|
local needed = ((needRecipes or {})[recipe.key] or 0) > 0
|
||||||
|
local kept = ((protected or {})[recipe.key] or 0) > 0
|
||||||
|
if needed and not recipe.enabled then table.insert(result.enable, recipe.key) end
|
||||||
|
if not needed and not kept and recipe.enabled and recipe.byChain then table.insert(result.disable, recipe.key) end
|
||||||
|
end
|
||||||
|
table.sort(result.enable); table.sort(result.disable)
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local SUPPLIED = {production = true, husbandry = true, field = true}
|
||||||
|
|
||||||
|
-- Hvordan varen når frem, ud fra producenternes udgang: "auto" = alle fordeler (spillet leverer selv til gårdens
|
||||||
|
-- fabrikker; kædens forbrugere er altid fabrikker) -> intet flow; "sold" = en producent sælger mellemvaren væk;
|
||||||
|
-- ellers "haul" (Behold: spawn/lager ved fabrikken, eller stald/mark) -> et flow henter. recipesByKey = {[nøgle] = opskrift}.
|
||||||
|
function ADChainActions.delivery(edge, recipesByKey)
|
||||||
|
local producers = edge.producers or {}
|
||||||
|
if #producers == 0 or recipesByKey == nil then return "haul" end
|
||||||
|
local all, sold = true, false
|
||||||
|
for _, key in ipairs(producers) do
|
||||||
|
local mode = ADChainActions.producerMode(recipesByKey, key, edge.fillType)
|
||||||
|
if mode == "sell" then sold = true end
|
||||||
|
if mode ~= "distribute" then all = false end
|
||||||
|
end
|
||||||
|
if sold then return "sold" end
|
||||||
|
return all and "auto" or "haul"
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADChainActions.producerMode(recipesByKey, key, fillType)
|
||||||
|
local recipe = (recipesByKey or {})[key]
|
||||||
|
return recipe ~= nil and (recipe.outputModes or {})[fillType] or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function producerKeys(node)
|
||||||
|
local keys = {}
|
||||||
|
for _, producer in ipairs(node.producers or {}) do table.insert(keys, producer.key) end
|
||||||
|
return keys
|
||||||
|
end
|
||||||
|
|
||||||
|
-- recipes (valgfri): gårdens opskrifter med outputModes -> kanten får delivery (se ADChainActions.delivery).
|
||||||
|
function ADChainActions.edges(chain, recipes)
|
||||||
|
local list, seen, byKey = {}, {}, nil
|
||||||
|
if recipes ~= nil then
|
||||||
|
byKey = {}
|
||||||
|
for _, recipe in ipairs(recipes) do byKey[recipe.key] = recipe end
|
||||||
|
end
|
||||||
|
local function walk(nodes, rootKey)
|
||||||
|
for _, node in ipairs(nodes or {}) do
|
||||||
|
local id = tostring(node.consumer) .. "|" .. tostring(node.fillType)
|
||||||
|
if SUPPLIED[node.source] and node.consumer ~= nil and not seen[id] then
|
||||||
|
seen[id] = true
|
||||||
|
local edge = {fillType = node.fillType, consumer = node.consumer, consumerName = node.consumerName, rootKey = rootKey,
|
||||||
|
producers = producerKeys(node)}
|
||||||
|
edge.delivery = ADChainActions.delivery(edge, byKey)
|
||||||
|
edge.soldBy = {}
|
||||||
|
for _, key in ipairs(edge.producers) do
|
||||||
|
if ADChainActions.producerMode(byKey, key, edge.fillType) == "sell" then table.insert(edge.soldBy, key) end
|
||||||
|
end
|
||||||
|
table.insert(list, edge)
|
||||||
|
end
|
||||||
|
walk(node.children, rootKey)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, root in ipairs((chain or {}).roots or {}) do walk(root.children, root.key) end
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
|
||||||
|
local function contains(list, value)
|
||||||
|
for _, item in ipairs(list or {}) do if item == value then return true end end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADChainActions.isCovered(edge, runs)
|
||||||
|
for _, run in ipairs(runs or {}) do
|
||||||
|
if run.kind == "supply" and contains(run.targetBuildingIds, edge.consumer)
|
||||||
|
and (run.ingredientMode ~= "manual" or contains(run.ingredients, edge.fillType)) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADChainActions.missingFlows(edges, runs)
|
||||||
|
local list = {}
|
||||||
|
for _, edge in ipairs(edges or {}) do
|
||||||
|
if edge.delivery ~= "auto" and not ADChainActions.isCovered(edge, runs) then table.insert(list, edge) end
|
||||||
|
end
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Mellemvarer en producent sælger væk (kanten er "sold") -> {{key, fillType}} én gang hver; kæden sætter dem til Behold.
|
||||||
|
function ADChainActions.soldIntermediates(edges)
|
||||||
|
local list, seen = {}, {}
|
||||||
|
for _, edge in ipairs(edges or {}) do
|
||||||
|
if edge.delivery == "sold" then
|
||||||
|
for _, key in ipairs(edge.soldBy or {}) do
|
||||||
|
local id = key .. "|" .. edge.fillType
|
||||||
|
if not seen[id] then
|
||||||
|
seen[id] = true
|
||||||
|
table.insert(list, {key = key, fillType = edge.fillType})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADChainActions.flowsToStop(runs, activeRootKeys)
|
||||||
|
local list = {}
|
||||||
|
for _, run in ipairs(runs or {}) do
|
||||||
|
-- også stoppede kædeflows uden traktor (de ryddes op); stoppede med traktor er allerede sluppet/spillerens
|
||||||
|
local idle = #(run.vehicleIds or {}) == 0
|
||||||
|
if run.chain ~= nil and run.chain ~= "" and (not run.stopped or idle) and not (activeRootKeys or {})[run.chain] then
|
||||||
|
table.insert(list, run)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- ét kædeflow pr. bygning (in-game 30/9: ét flow pr. vare og bygning gav mange stoppede flows) ----
|
||||||
|
|
||||||
|
-- Manglende kanter samlet pr. modtagerbygning: {consumer, consumerName, rootKey, fillTypes} i fundet rækkefølge.
|
||||||
|
function ADChainActions.groupMissing(missing)
|
||||||
|
local groups, byConsumer = {}, {}
|
||||||
|
for _, edge in ipairs(missing or {}) do
|
||||||
|
local group = byConsumer[edge.consumer]
|
||||||
|
if group == nil then
|
||||||
|
group = {consumer = edge.consumer, consumerName = edge.consumerName, rootKey = edge.rootKey, fillTypes = {}}
|
||||||
|
byConsumer[edge.consumer] = group
|
||||||
|
table.insert(groups, group)
|
||||||
|
end
|
||||||
|
if not contains(group.fillTypes, edge.fillType) then table.insert(group.fillTypes, edge.fillType) end
|
||||||
|
end
|
||||||
|
return groups
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isChainRun(run)
|
||||||
|
return run.chain ~= nil and run.chain ~= "" and run.kind == "supply" and run.ingredientMode == "manual"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Kædens eget flow (manuelle varer) til bygningen -> run-id, så nye varer lægges i det i stedet for et nyt flow.
|
||||||
|
function ADChainActions.chainRunFor(runs, consumer)
|
||||||
|
for _, run in ipairs(runs or {}) do
|
||||||
|
if isChainRun(run) and contains(run.targetBuildingIds, consumer) then return run.id end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Flere tomme kædeflows (uden traktorer) til samme bygning: det første beholdes med alle varerne, resten slettes.
|
||||||
|
-- -> {{keepId, add = {varer}, deleteIds = {id}}}
|
||||||
|
function ADChainActions.mergeIdle(runs)
|
||||||
|
local byConsumer, order = {}, {}
|
||||||
|
for _, run in ipairs(runs or {}) do
|
||||||
|
local target = (run.targetBuildingIds or {})[1]
|
||||||
|
if isChainRun(run) and target ~= nil and #(run.targetBuildingIds or {}) == 1 and #(run.vehicleIds or {}) == 0 then
|
||||||
|
if byConsumer[target] == nil then byConsumer[target] = {}; table.insert(order, target) end
|
||||||
|
table.insert(byConsumer[target], run)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local merges = {}
|
||||||
|
for _, target in ipairs(order) do
|
||||||
|
local list = byConsumer[target]
|
||||||
|
if #list > 1 then
|
||||||
|
local keep, add, deleteIds = list[1], {}, {}
|
||||||
|
for index = 2, #list do
|
||||||
|
for _, name in ipairs(list[index].ingredients or {}) do
|
||||||
|
if not contains(keep.ingredients, name) and not contains(add, name) then table.insert(add, name) end
|
||||||
|
end
|
||||||
|
table.insert(deleteIds, list[index].id)
|
||||||
|
end
|
||||||
|
table.insert(merges, {keepId = keep.id, add = add, deleteIds = deleteIds})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return merges
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Varer gården selv bruger: stalde æder dem, en tændt produktion bruger dem, gården har forbrug af dem, eller et
|
||||||
|
-- flow (ikke udkørsel) kører dem. -> {[vare] = true}. Den slags sælges aldrig straks (in-game 30/9: TMR og roesnitter).
|
||||||
|
function ADChainActions.farmUses(data, runs)
|
||||||
|
local uses = {}
|
||||||
|
for name in pairs((data or {}).barnFood or {}) do uses[name] = true end
|
||||||
|
for name, rate in pairs((data or {}).rates or {}) do
|
||||||
|
if (rate.consumption or 0) > 0 then uses[name] = true end
|
||||||
|
end
|
||||||
|
for _, recipe in ipairs((data or {}).recipes or {}) do
|
||||||
|
if recipe.enabled then
|
||||||
|
for name in pairs(recipe.inputs or {}) do uses[name] = true end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, run in ipairs(runs or {}) do
|
||||||
|
if run.kind ~= "outbound" and run.ingredientMode == "manual" then
|
||||||
|
for _, name in ipairs(run.ingredients or {}) do uses[name] = true end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return uses
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Slutprodukter der skal sælges straks: aktive rødders udgange, som ikke selv er en kant i kæden (mellemvare) og
|
||||||
|
-- som gården ikke selv bruger (farmUses). keep = {[rodnøgle] = true} for rødder sat til "Behold". -> {{key, fillType}}
|
||||||
|
function ADChainActions.finalSales(chain, recipes, edges, keep, farmUses)
|
||||||
|
local intermediate, byKey, list = {}, {}, {}
|
||||||
|
for _, edge in ipairs(edges or {}) do intermediate[edge.fillType] = true end
|
||||||
|
for _, recipe in ipairs(recipes or {}) do byKey[recipe.key] = recipe end
|
||||||
|
for _, root in ipairs((chain or {}).roots or {}) do
|
||||||
|
local recipe = byKey[root.key]
|
||||||
|
if recipe ~= nil and not (keep or {})[root.key] then
|
||||||
|
local names = {}
|
||||||
|
for name in pairs(recipe.outputs or {}) do table.insert(names, name) end
|
||||||
|
table.sort(names)
|
||||||
|
for _, name in ipairs(names) do
|
||||||
|
if not intermediate[name] and not (farmUses or {})[name] then table.insert(list, {key = root.key, fillType = name}) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Kædeflow for en kæde der er slået fra: uden traktor slettes det (auto-oprettet og tomt), ellers stoppes og slippes
|
||||||
|
-- det (bliver spillerens eget). (in-game 30/9: ellers 12 tomme "Kæde: …"-flows blandt spillerens egne)
|
||||||
|
function ADChainActions.releaseAction(run)
|
||||||
|
if #(run.vehicleIds or {}) == 0 then return "delete" end
|
||||||
|
return "release"
|
||||||
|
end
|
||||||
@@ -0,0 +1,511 @@
|
|||||||
|
-- AD Chain Data (læse-adapter)
|
||||||
|
-- Omsætter spillets tilstand (gårdens fabrikker, stalde, afgrøder, lager og marker) til de rene data som
|
||||||
|
-- ADChainModel og ADCropPlan regner på. Varer og afgrøder er NAVNE; mængder er liter pr. måned (spillets
|
||||||
|
-- rå rater: én 24-timers dag pr. måned, som lagerfanen). Al spiladgang sker inde i funktionerne, så filen kan
|
||||||
|
-- dofile's uden spillet. Hver bygning/mark/afgrøde læses i pcall; en fejl logges én gang pr. nøgle.
|
||||||
|
|
||||||
|
ADChainData = {}
|
||||||
|
ADChainData.LOG_PREFIX = "ADSmartPickup: kæde: "
|
||||||
|
ADChainData.PERMANENT = {GRAPE = true, GRAPEB = true, OLIVE = true, POPLAR = true}
|
||||||
|
ADChainData.warned = {}
|
||||||
|
|
||||||
|
-- Varige afgrøder (vin, oliven, poppel) skiftes aldrig; mod-vine hedder også GRAPE…
|
||||||
|
function ADChainData.isPermanent(fruitName)
|
||||||
|
if type(fruitName) ~= "string" or fruitName == "" then return false end
|
||||||
|
return ADChainData.PERMANENT[fruitName] == true or fruitName:sub(1, 5) == "GRAPE"
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADChainData.recipeKey(placeableUid, productionId)
|
||||||
|
return tostring(placeableUid) .. ":" .. tostring(productionId)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Samme omregning som lagerfanen (adSources addFactoryRates): cyklusser pr. 24-timers dag = pr. måned.
|
||||||
|
function ADChainData.cyclesPerMonth(production)
|
||||||
|
return (production.cyclesPerMinute or ((production.cyclesPerHour or 0) / 60)) * 1440
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADChainData.warnOnce(key, fmt, ...)
|
||||||
|
if ADChainData.warned[key] then return end
|
||||||
|
ADChainData.warned[key] = true
|
||||||
|
if Logging ~= nil and Logging.warning ~= nil then Logging.warning(ADChainData.LOG_PREFIX .. fmt, ...) end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- En læsefejl i dette kald: data.partial = true (også når advarslen allerede er logget), så kæden ikke
|
||||||
|
-- slukker eller stopper noget på et ufuldstændigt billede af gården.
|
||||||
|
-- data.readFailed skelner en ægte læsefejl fra en ufærdig markscanning (afhængighedstjekket bruger ikke markerne).
|
||||||
|
local function failed(data, key, fmt, ...)
|
||||||
|
data.partial, data.readFailed = true, true
|
||||||
|
ADChainData.warnOnce(key, fmt, ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- hjælpere ----
|
||||||
|
|
||||||
|
-- Navn for et fillType-indeks; husker indeks og titel til GUI og vognvalg.
|
||||||
|
local function nameResolver(data)
|
||||||
|
return function(index)
|
||||||
|
if index == nil or g_fillTypeManager == nil then return nil end
|
||||||
|
local name = g_fillTypeManager:getFillTypeNameByIndex(index)
|
||||||
|
if name == nil then return nil end
|
||||||
|
if data.fillIndex[name] == nil then
|
||||||
|
data.fillIndex[name] = index
|
||||||
|
local desc = g_fillTypeManager.getFillTypeByIndex ~= nil and g_fillTypeManager:getFillTypeByIndex(index) or nil
|
||||||
|
data.fillTitles[name] = tostring(desc ~= nil and desc.title or name)
|
||||||
|
end
|
||||||
|
return name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- {{type, amount}} -> {[navn] = mængde pr. cyklus}
|
||||||
|
local function amountsByName(list, nameOf)
|
||||||
|
local result = {}
|
||||||
|
for _, entry in ipairs(list or {}) do
|
||||||
|
local name = nameOf(entry.type)
|
||||||
|
if name ~= nil and (entry.amount or 0) > 0 then result[name] = (result[name] or 0) + entry.amount end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local function placeableId(placeable)
|
||||||
|
if placeable.uniqueId ~= nil then return tostring(placeable.uniqueId) end
|
||||||
|
return ADBuildings ~= nil and ADBuildings.getId ~= nil and ADBuildings.getId(placeable) or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Spillerens eget navn (ADNames) før spillets; samme rækkefølge som adSources/adBuildings.
|
||||||
|
local function placeableName(placeable, fallback)
|
||||||
|
local own = ADNames ~= nil and ADNames.forPlaceable ~= nil and ADNames.forPlaceable(placeable) or nil
|
||||||
|
if own ~= nil then return own end
|
||||||
|
if placeable.getName ~= nil then
|
||||||
|
local ok, name = pcall(placeable.getName, placeable)
|
||||||
|
if ok and name ~= nil then return tostring(name) end
|
||||||
|
end
|
||||||
|
return fallback
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isEnabled(point, production)
|
||||||
|
if point.getIsProductionEnabled ~= nil then return point:getIsProductionEnabled(production.id) == true end
|
||||||
|
for _, active in ipairs(point.activeProductions or {}) do
|
||||||
|
if active == production then return true end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- læsning pr. del ----
|
||||||
|
|
||||||
|
-- En fabriks produktioner -> opskrifter + punkter (samles først, så en fejl midtvejs ikke efterlader halve).
|
||||||
|
-- Udgangens fordeling (spillets Sælg/Fordel/Behold) -> "sell"|"distribute"|"keep". Behold dækker både spawn (paller
|
||||||
|
-- ved fabrikken) og lager (STORE fra ProductionStorageControl): begge dele skal hentes af et flow.
|
||||||
|
function ADChainData.outputModeName(mode)
|
||||||
|
local modes = ProductionPoint ~= nil and ProductionPoint.OUTPUT_MODE or {}
|
||||||
|
if mode ~= nil and mode == modes.DIRECT_SELL then return "sell" end
|
||||||
|
if mode ~= nil and mode == modes.AUTO_DELIVER then return "distribute" end
|
||||||
|
return "keep"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function outputModes(point, outputs, nameOf)
|
||||||
|
local modes = {}
|
||||||
|
if point.getOutputDistributionMode == nil then return modes end
|
||||||
|
for _, output in ipairs(outputs or {}) do
|
||||||
|
local name = output.type ~= nil and nameOf(output.type) or nil
|
||||||
|
if name ~= nil then
|
||||||
|
local ok, mode = pcall(point.getOutputDistributionMode, point, output.type)
|
||||||
|
modes[name] = ADChainData.outputModeName(ok and mode or nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return modes
|
||||||
|
end
|
||||||
|
|
||||||
|
local function readFactory(placeable, point, byChain, nameOf)
|
||||||
|
local uid = placeableId(placeable)
|
||||||
|
if uid == nil then return {}, {} end
|
||||||
|
local buildingName = placeableName(placeable, uid)
|
||||||
|
local recipes, points = {}, {}
|
||||||
|
for _, production in ipairs(point.productions or {}) do
|
||||||
|
local key = ADChainData.recipeKey(uid, production.id)
|
||||||
|
table.insert(recipes, {key = key, name = tostring(production.name or production.id), building = uid,
|
||||||
|
buildingName = buildingName, enabled = isEnabled(point, production), byChain = (byChain or {})[key] == true,
|
||||||
|
cyclesPerMonth = ADChainData.cyclesPerMonth(production),
|
||||||
|
inputs = amountsByName(production.inputs, nameOf), outputs = amountsByName(production.outputs, nameOf),
|
||||||
|
outputModes = outputModes(point, production.outputs, nameOf)})
|
||||||
|
points[key] = {point = point, productionId = production.id, placeable = placeable, buildingName = buildingName}
|
||||||
|
end
|
||||||
|
return recipes, points
|
||||||
|
end
|
||||||
|
|
||||||
|
local function readHerd(placeable, nameOf)
|
||||||
|
local uid = placeableId(placeable)
|
||||||
|
local outputs = {}
|
||||||
|
for index, perMonth in pairs(ADSources.getHusbandryOutputs(placeable) or {}) do
|
||||||
|
local name = nameOf(index)
|
||||||
|
if name ~= nil then outputs[name] = (outputs[name] or 0) + perMonth end
|
||||||
|
end
|
||||||
|
if uid == nil or next(outputs) == nil then return nil end
|
||||||
|
return {building = uid, buildingName = placeableName(placeable, uid), outputs = outputs}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Stald-foder: {[vare] = {staldnavn...}} for det truget æder nu (forbrug > 0) — til afhængighedstjekket.
|
||||||
|
local function readBarnFood(data, placeable, nameOf)
|
||||||
|
if ADSources.getHusbandryConsumption == nil then return end
|
||||||
|
local barnName
|
||||||
|
for index, perDay in pairs(ADSources.getHusbandryConsumption(placeable) or {}) do
|
||||||
|
local name = nameOf(index)
|
||||||
|
if name ~= nil and (perDay or 0) > 0 then
|
||||||
|
barnName = barnName or placeableName(placeable, placeableId(placeable) or "?")
|
||||||
|
data.barnFood[name] = data.barnFood[name] or {}
|
||||||
|
table.insert(data.barnFood[name], barnName)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function readPlaceables(data, farmId, byChain, nameOf)
|
||||||
|
local placeables = g_currentMission ~= nil and g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {}
|
||||||
|
for index, placeable in pairs(placeables) do
|
||||||
|
local ok, err = pcall(function()
|
||||||
|
if not ADPrices.isOwnedBy(placeable, farmId) then return end
|
||||||
|
local uid = placeableId(placeable)
|
||||||
|
if uid ~= nil then data.placeables[uid] = placeable end
|
||||||
|
local point = placeable.spec_productionPoint ~= nil and placeable.spec_productionPoint.productionPoint or nil
|
||||||
|
if point ~= nil then
|
||||||
|
local recipes, points = readFactory(placeable, point, byChain, nameOf)
|
||||||
|
for _, recipe in ipairs(recipes) do table.insert(data.recipes, recipe) end
|
||||||
|
for key, entry in pairs(points) do data.points[key] = entry end
|
||||||
|
elseif placeable.spec_husbandry ~= nil or placeable.getHusbandryFillLevel ~= nil then
|
||||||
|
local herd = readHerd(placeable, nameOf)
|
||||||
|
if herd ~= nil then table.insert(data.husbandry, herd) end
|
||||||
|
readBarnFood(data, placeable, nameOf)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
if not ok then
|
||||||
|
local key = tostring(placeable ~= nil and placeable.uniqueId or index)
|
||||||
|
failed(data, "placeable:" .. key, "bygning %s kunne ikke læses: %s", key, tostring(err))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(data.recipes, function(a, b) return a.key < b.key end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Markedspris for en afgrøde, først når kæden spørger (kun kandidatmarkernes afgrøder, kun ved mangel);
|
||||||
|
-- hver afgrøde slås højst op én gang pr. læsning. Fejl -> 0 (logget én gang).
|
||||||
|
local function priceResolver(data)
|
||||||
|
local cache = {}
|
||||||
|
return function(fruitName)
|
||||||
|
if cache[fruitName] ~= nil then return cache[fruitName] end
|
||||||
|
local index = data.fruitFillIndex[fruitName]
|
||||||
|
local price = 0
|
||||||
|
if index ~= nil then
|
||||||
|
local ok, quote = pcall(ADSources.getMarketQuote, index)
|
||||||
|
if ok then
|
||||||
|
price = quote ~= nil and quote.price or 0
|
||||||
|
else
|
||||||
|
ADChainData.warnOnce("price:" .. tostring(fruitName), "pris for %s kunne ikke læses: %s", tostring(fruitName), tostring(quote))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
cache[fruitName] = price
|
||||||
|
return price
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function readCrops(data, nameOf)
|
||||||
|
local fruits = g_fruitTypeManager ~= nil and g_fruitTypeManager.fruitTypes or {}
|
||||||
|
for _, fruit in ipairs(fruits) do
|
||||||
|
if fruit.fillType ~= nil and fruit.allowsSeeding ~= false and fruit.name ~= nil then
|
||||||
|
local ok, err = pcall(function()
|
||||||
|
local fillName = fruit.fillType.name or nameOf(fruit.fillType.index)
|
||||||
|
if fillName == nil then return end
|
||||||
|
if fruit.fillType.index ~= nil then nameOf(fruit.fillType.index) end
|
||||||
|
data.crops[fillName] = fruit.name
|
||||||
|
data.yields[fruit.name] = (tonumber(fruit.literPerSqm) or 0) * 10000
|
||||||
|
data.fruitFillIndex[fruit.name] = fruit.fillType.index
|
||||||
|
end)
|
||||||
|
if not ok then failed(data, "fruit:" .. tostring(fruit.name), "afgrøde %s kunne ikke læses: %s", tostring(fruit.name), tostring(err)) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- markvarer (hø, hakkelse, halm …) ----
|
||||||
|
-- Varer der høstes/laves på marken uden at være en afgrødes hovedvare. Spillets konvertere hedder forskelligt
|
||||||
|
-- mellem versioner/kort (FORAGEHARVESTER/FORAGE_HARVESTER), så alle navne prøves; et ukendt navn giver nil.
|
||||||
|
ADChainData.FIELD_CONVERTERS = {"FORAGEHARVESTER", "FORAGE_HARVESTER", "MOWER", "COMBINE", "CUTTER"}
|
||||||
|
-- Kendte markvarer (basisspil, FermentingSiloPack, Dakota); kun dem der findes som fill type bruges.
|
||||||
|
ADChainData.KNOWN_FIELD_GOODS = {"STRAW", "CHAFF", "CROP_WINDROW", "GRASS_WINDROW", "ALFALFA_WINDROW", "CLOVER_WINDROW",
|
||||||
|
"DRYGRASS_WINDROW", "DRYALFALFA_WINDROW", "DRYCLOVER_WINDROW", "CCM", "CCMRAW", "WCS"}
|
||||||
|
|
||||||
|
-- fruit.windrowFillType kan være et indeks eller en fill type-beskrivelse
|
||||||
|
local function windrowIndex(fruit)
|
||||||
|
local windrow = fruit.windrowFillType
|
||||||
|
if type(windrow) == "number" then return windrow end
|
||||||
|
if type(windrow) == "table" then
|
||||||
|
if windrow.index ~= nil then return windrow.index end
|
||||||
|
if windrow.name ~= nil and g_fillTypeManager.getFillTypeIndexByName ~= nil then
|
||||||
|
return g_fillTypeManager:getFillTypeIndexByName(windrow.name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Én konverters mål-indekser ({[fra] = {targetFillTypeIndex|fillTypeIndex}}, samme form som adFields.buildMowable)
|
||||||
|
local function converterTargets(converter, add)
|
||||||
|
for _, entry in pairs(converter or {}) do
|
||||||
|
if type(entry) == "table" then add(entry.targetFillTypeIndex or entry.fillTypeIndex) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- data.fieldGoods = {[navn] = true}. Kun eksisterende fill types; fejl logges én gang og gør ikke læsningen delvis
|
||||||
|
-- (en ændret spil-API må ikke blokere kæden for altid) — værst tænkeligt meldes en markvare som "køb".
|
||||||
|
local function readFieldGoods(data, nameOf)
|
||||||
|
local indices = {}
|
||||||
|
local function add(index) if type(index) == "number" then indices[index] = true end end
|
||||||
|
local function guarded(key, fn)
|
||||||
|
local ok, err = pcall(fn)
|
||||||
|
if not ok then ADChainData.warnOnce("fieldGoods:" .. key, "markvarer (%s) kunne ikke læses: %s", key, tostring(err)) end
|
||||||
|
end
|
||||||
|
if g_fillTypeManager == nil then return end
|
||||||
|
local fruitManager = g_fruitTypeManager
|
||||||
|
for _, fruit in ipairs(fruitManager ~= nil and fruitManager.fruitTypes or {}) do
|
||||||
|
guarded("windrow:" .. tostring(fruit.name), function() add(windrowIndex(fruit)) end)
|
||||||
|
end
|
||||||
|
if fruitManager ~= nil and fruitManager.getConverterDataByName ~= nil then
|
||||||
|
for _, name in ipairs(ADChainData.FIELD_CONVERTERS) do
|
||||||
|
guarded(name, function() converterTargets(fruitManager:getConverterDataByName(name), add) end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if g_fillTypeManager.getFillTypeIndexByName ~= nil then
|
||||||
|
for _, name in ipairs(ADChainData.KNOWN_FIELD_GOODS) do
|
||||||
|
guarded(name, function() add(g_fillTypeManager:getFillTypeIndexByName(name)) end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- tørrede varianter (vender: græs-vindue -> hø)
|
||||||
|
if g_fillTypeManager.getConverterDataByName ~= nil then
|
||||||
|
guarded("TEDDER", function()
|
||||||
|
local tedder = g_fillTypeManager:getConverterDataByName("TEDDER") or {}
|
||||||
|
local windrows = {}
|
||||||
|
for index in pairs(indices) do table.insert(windrows, index) end
|
||||||
|
for _, index in ipairs(windrows) do
|
||||||
|
local entry = tedder[index]
|
||||||
|
if type(entry) == "table" then add(entry.targetFillTypeIndex or entry.fillTypeIndex) end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
for index in pairs(indices) do
|
||||||
|
local ok, name = pcall(nameOf, index)
|
||||||
|
if ok and name ~= nil and data.crops[name] == nil then data.fieldGoods[name] = true end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Læs i gårdens køretøjer (vogne, høstertanke, marksilo-vogne) tæller som lager: kun afgrøder og markvarer, ikke
|
||||||
|
-- diesel o.l. loads = {{name, liters}} (in-game 30/9: høst på vej hjem gav falsk mangel og forslag).
|
||||||
|
function ADChainData.addVehicleLoads(data, loads)
|
||||||
|
for _, load in ipairs(loads or {}) do
|
||||||
|
local name, liters = load.name, load.liters or 0
|
||||||
|
local fruit = name ~= nil and (data.crops or {})[name] or nil
|
||||||
|
if liters > 0 and (fruit ~= nil or (data.fieldGoods or {})[name]) then
|
||||||
|
data.stockByFill[name] = (data.stockByFill[name] or 0) + liters
|
||||||
|
if fruit ~= nil then data.stock[fruit] = (data.stock[fruit] or 0) + liters end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function readVehicleLoads(data, farmId, nameOf)
|
||||||
|
local ok, err = pcall(function()
|
||||||
|
local loads = {}
|
||||||
|
for _, vehicle in pairs(g_currentMission.vehicleSystem.vehicles or {}) do
|
||||||
|
local spec = vehicle.spec_fillUnit
|
||||||
|
if spec ~= nil and vehicle.getOwnerFarmId ~= nil and vehicle:getOwnerFarmId() == farmId then
|
||||||
|
for index, unit in ipairs(spec.fillUnits or {}) do
|
||||||
|
local level = vehicle:getFillUnitFillLevel(index) or 0
|
||||||
|
if level > 0 then
|
||||||
|
table.insert(loads, {name = nameOf(vehicle:getFillUnitFillType(index) or unit.fillType), liters = level})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
ADChainData.addVehicleLoads(data, loads)
|
||||||
|
end)
|
||||||
|
if not ok then ADChainData.warnOnce("vehicleLoads", "læs i køretøjer kunne ikke læses: %s", tostring(err)) end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function readStock(data, adEnv, farmId, nameOf)
|
||||||
|
local ok, err = pcall(function()
|
||||||
|
for index, liters in pairs(ADStorageStats.farmTotals(ADSources.getFarmInventory(adEnv, farmId))) do
|
||||||
|
local name = nameOf(index)
|
||||||
|
if name ~= nil then data.stockByFill[name] = (data.stockByFill[name] or 0) + liters end
|
||||||
|
local fruit = data.crops[name or ""]
|
||||||
|
if fruit ~= nil then data.stock[fruit] = (data.stock[fruit] or 0) + liters end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
if not ok then failed(data, "stock", "lager kunne ikke læses: %s", tostring(err)) end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Gårdens forbrug og produktion pr. døgn (fabrikker der kører + stalde) pr. varenavn og spillets tidsfaktor
|
||||||
|
-- (rå rater pr. måned × faktor = pr. døgn) — til lagerdage og dækning i afhængighedstjekket.
|
||||||
|
local function readRates(data, adEnv, farmId, nameOf)
|
||||||
|
local environment = g_currentMission ~= nil and g_currentMission.environment or nil
|
||||||
|
local adjustment = environment ~= nil and tonumber(environment.timeAdjustment) or nil
|
||||||
|
data.timeAdjustment = adjustment ~= nil and adjustment > 0 and adjustment or 1
|
||||||
|
local ok, err = pcall(function()
|
||||||
|
for index, rate in pairs(ADSources.getFarmRates(adEnv, farmId, false) or {}) do
|
||||||
|
local name = nameOf(index)
|
||||||
|
if name ~= nil then data.rates[name] = {consumption = rate.consumption or 0, production = rate.production or 0} end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
if not ok then failed(data, "allRates", "gårdens forbrug kunne ikke læses: %s", tostring(err)) end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Afgrøder gården selv bruger: input i en tændt produktion eller foder/halm i en stald (kun staldenes forbrug
|
||||||
|
-- læses: feedOnly springer fabrikkernes rater over — dem dækker opskrifterne allerede).
|
||||||
|
local function readUsedFruits(data, adEnv, farmId, nameOf)
|
||||||
|
for _, recipe in ipairs(data.recipes) do
|
||||||
|
if recipe.enabled then
|
||||||
|
for name in pairs(recipe.inputs) do
|
||||||
|
if data.crops[name] ~= nil then data.usedFruits[data.crops[name]] = true end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local ok, err = pcall(function()
|
||||||
|
for index, rate in pairs(ADSources.getFarmRates(adEnv, farmId, true) or {}) do
|
||||||
|
local fruit = data.crops[nameOf(index) or ""]
|
||||||
|
if fruit ~= nil and (rate.consumption or 0) > 0 then data.usedFruits[fruit] = true end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
if not ok then failed(data, "rates", "staldforbrug kunne ikke læses: %s", tostring(err)) end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Markscanningen har ikke nået alle marker endnu (efter indlæsning ~én mark pr. 2 s): marklisten er ufuldstændig,
|
||||||
|
-- så udbud undervurderes -> fieldsIncomplete (kæden rører ingen marker) og partial (intet slukkes/stoppes).
|
||||||
|
local function readFields(data)
|
||||||
|
if ADFields.isScanComplete ~= nil and not ADFields.isScanComplete() then
|
||||||
|
data.partial, data.fieldsIncomplete = true, true
|
||||||
|
end
|
||||||
|
for _, info in ipairs(ADFields.list() or {}) do
|
||||||
|
local ok, entry = pcall(function()
|
||||||
|
local field = ADFieldJobs.getField(info.id)
|
||||||
|
local plan = field.plan or {}
|
||||||
|
local fruit = info.growing and info.fruitName or field.lastFruit
|
||||||
|
-- høst i gang: ingen forslag/skift for afgrøden, mens læssene er på vej hjem
|
||||||
|
if field.stage == "harvesting" and info.fruitName ~= nil then data.harvesting[info.fruitName] = true end
|
||||||
|
return {id = info.id, areaHa = ADFields.polygonAreaHa(info.verts), fruit = fruit,
|
||||||
|
targetFruit = plan.targetFruit, targetSource = plan.targetSource, chainFree = plan.chainFree == true,
|
||||||
|
permanent = ADChainData.isPermanent(info.fruitName or field.lastFruit), excluded = info.kind == "mowable"}
|
||||||
|
end)
|
||||||
|
if ok then
|
||||||
|
table.insert(data.fields, entry)
|
||||||
|
else
|
||||||
|
failed(data, "field:" .. tostring(info.id), "mark %s kunne ikke læses: %s", tostring(info.id), tostring(entry))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- afhængighedstjek (spec afsnit 8) ----
|
||||||
|
|
||||||
|
local function unloadStationsOf(placeable)
|
||||||
|
local storage = g_currentMission ~= nil and g_currentMission.storageSystem or nil
|
||||||
|
local result = {}
|
||||||
|
if storage == nil or storage.getUnloadingStations == nil then return result end
|
||||||
|
for _, station in pairs(storage:getUnloadingStations() or {}) do
|
||||||
|
if station.owningPlaceable == placeable then table.insert(result, station) end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Hvad auto-flowenes mål tager af varerne `goods` (navne): {[bygnings-id] = {[vare] = true}}.
|
||||||
|
-- Kun tændte forsyningsflows i auto-tilstand; hvert mål slås op én gang.
|
||||||
|
function ADChainData.acceptsMap(data, flows, goods)
|
||||||
|
local result = {}
|
||||||
|
for _, flow in ipairs(flows or {}) do
|
||||||
|
if flow.kind ~= "outbound" and not flow.stopped and flow.ingredientMode ~= "manual" then
|
||||||
|
for _, id in ipairs(flow.targetBuildingIds or {}) do
|
||||||
|
local placeable = data.placeables[id]
|
||||||
|
if result[id] == nil and placeable ~= nil then
|
||||||
|
result[id] = {}
|
||||||
|
local ok, err = pcall(function()
|
||||||
|
local stations = unloadStationsOf(placeable)
|
||||||
|
for _, name in ipairs(goods) do
|
||||||
|
local index = data.fillIndex[name]
|
||||||
|
for _, station in ipairs(stations) do
|
||||||
|
if index ~= nil and ADSources.stationAccepts(station, index) then result[id][name] = true; break end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
if not ok then ADChainData.warnOnce("accepts:" .. tostring(id), "mål %s kunne ikke læses: %s", tostring(id), tostring(err)) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Kædens input-varer (alle varer i de aktive kæders træer) -> {[vare] = {kædenavn...}}
|
||||||
|
function ADChainData.chainInputs(chain, titles)
|
||||||
|
local result = {}
|
||||||
|
local function walk(nodes, rootTitle)
|
||||||
|
for _, node in ipairs(nodes or {}) do
|
||||||
|
result[node.fillType] = result[node.fillType] or {}
|
||||||
|
local list = result[node.fillType]
|
||||||
|
if list[#list] ~= rootTitle then table.insert(list, rootTitle) end
|
||||||
|
walk(node.children, rootTitle)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, root in ipairs(chain ~= nil and chain.roots or {}) do
|
||||||
|
walk(root.children, tostring((titles or {})[root.name] or root.name))
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Alt afhængighedstjekket regner på, i liter pr. døgn: {needed, stockDays, capacity, demand}.
|
||||||
|
-- flows = ADChain.runsView(...); chain = kædemodellen (kan være uden rødder).
|
||||||
|
function ADChainData.dependencyInputs(data, chain, flows)
|
||||||
|
local goods, seen = {}, {}
|
||||||
|
for _, recipe in ipairs(data.recipes or {}) do
|
||||||
|
if not recipe.enabled and not recipe.byChain then
|
||||||
|
for name in pairs(recipe.outputs or {}) do
|
||||||
|
if not seen[name] then seen[name] = true; table.insert(goods, name) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(goods)
|
||||||
|
-- auto-flowets mål tager mange varer (fx biogas: korn, ensilage, gylle): kun varer gården faktisk forbruger
|
||||||
|
-- (stald eller fabrik) eller en kæde vil have, tæller
|
||||||
|
local consumed = {}
|
||||||
|
for _, name in ipairs(goods) do
|
||||||
|
local rate = (data.rates or {})[name]
|
||||||
|
if (rate ~= nil and (rate.consumption or 0) > 0) or ((chain ~= nil and chain.demand or {})[name] or 0) > 0 then
|
||||||
|
table.insert(consumed, name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local needed = ADDependency.neededOutputs(flows, data.barnFood, ADChainData.chainInputs(chain, data.fillTitles),
|
||||||
|
#consumed > 0 and ADChainData.acceptsMap(data, flows, consumed) or {})
|
||||||
|
local adjustment = data.timeAdjustment or 1
|
||||||
|
local capacity, demand, stockDays = {}, {}, {}
|
||||||
|
for name, perMonth in pairs(ADDependency.capacity(data.recipes)) do capacity[name] = perMonth * adjustment end
|
||||||
|
for _, name in ipairs(goods) do
|
||||||
|
local rate = (data.rates or {})[name] or {}
|
||||||
|
local consumption, production = rate.consumption or 0, rate.production or 0
|
||||||
|
local chainDemand = ((chain ~= nil and chain.demand or {})[name] or 0) * adjustment
|
||||||
|
demand[name] = math.max(consumption, chainDemand)
|
||||||
|
if consumption > production then
|
||||||
|
stockDays[name] = ((data.stockByFill or {})[name] or 0) / (consumption - production)
|
||||||
|
elseif consumption > 0 then
|
||||||
|
stockDays[name] = math.huge
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return {needed = needed, stockDays = stockDays, capacity = capacity, demand = demand}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- samlet ----
|
||||||
|
|
||||||
|
-- byChain = {[recipeKey] = true} for produktioner kæden selv har tændt (ikke rødder).
|
||||||
|
-- data.partial = true når noget ikke kunne læses i dette kald (data.readFailed) eller markscanningen kører; data.fieldsIncomplete = true mens markscanningen
|
||||||
|
-- ikke har nået alle marker.
|
||||||
|
function ADChainData.collect(adEnv, farmId, byChain)
|
||||||
|
local data = {recipes = {}, husbandry = {}, crops = {}, stock = {}, fields = {}, yields = {}, fruitFillIndex = {},
|
||||||
|
usedFruits = {}, points = {}, fillTitles = {}, fillIndex = {}, placeables = {}, barnFood = {}, stockByFill = {}, rates = {}, fieldGoods = {}, harvesting = {}}
|
||||||
|
local nameOf = nameResolver(data)
|
||||||
|
data.priceOf = priceResolver(data)
|
||||||
|
readPlaceables(data, farmId, byChain, nameOf)
|
||||||
|
readCrops(data, nameOf)
|
||||||
|
readFieldGoods(data, nameOf)
|
||||||
|
readStock(data, adEnv, farmId, nameOf)
|
||||||
|
readVehicleLoads(data, farmId, nameOf)
|
||||||
|
readRates(data, adEnv, farmId, nameOf)
|
||||||
|
readUsedFruits(data, adEnv, farmId, nameOf)
|
||||||
|
readFields(data)
|
||||||
|
return data
|
||||||
|
end
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
-- AD Chain Model (ren logik)
|
||||||
|
-- Sporer baglæns fra de produktioner spilleren selv har tændt (rødder) gennem gårdens egne fabrikker og stalde
|
||||||
|
-- til råvarerne. Alle mængder er liter pr. måned; varer og afgrøder er NAVNE. Se spec 2026-09-30-produktionskaeder.
|
||||||
|
|
||||||
|
ADChainModel = {}
|
||||||
|
ADChainModel.MAX_DEPTH = 8
|
||||||
|
|
||||||
|
local function byKey(a, b) return tostring(a.key) < tostring(b.key) end
|
||||||
|
|
||||||
|
-- producenter pr. vare: tændte først, så stabil rækkefølge
|
||||||
|
local function indexProducers(recipes)
|
||||||
|
local index = {}
|
||||||
|
for _, recipe in ipairs(recipes or {}) do
|
||||||
|
for name, amount in pairs(recipe.outputs or {}) do
|
||||||
|
if (amount or 0) > 0 and (recipe.cyclesPerMonth or 0) > 0 then
|
||||||
|
index[name] = index[name] or {}
|
||||||
|
table.insert(index[name], recipe)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, list in pairs(index) do
|
||||||
|
table.sort(list, function(a, b)
|
||||||
|
if (a.enabled == true) ~= (b.enabled == true) then return a.enabled == true end
|
||||||
|
return byKey(a, b)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
return index
|
||||||
|
end
|
||||||
|
|
||||||
|
local function indexHerds(husbandry)
|
||||||
|
local index = {}
|
||||||
|
for _, herd in ipairs(husbandry or {}) do
|
||||||
|
for name, perMonth in pairs(herd.outputs or {}) do
|
||||||
|
if (perMonth or 0) > 0 then
|
||||||
|
index[name] = index[name] or {}
|
||||||
|
table.insert(index[name], {building = herd.building, buildingName = herd.buildingName, left = perMonth})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return index
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sortedNames(map)
|
||||||
|
local names = {}
|
||||||
|
for name in pairs(map or {}) do table.insert(names, name) end
|
||||||
|
table.sort(names)
|
||||||
|
return names
|
||||||
|
end
|
||||||
|
|
||||||
|
-- fieldGoods = {[vare] = true}: varer der høstes/laves på marken uden at være en afgrødes hovedvare (hø, hakkelse,
|
||||||
|
-- halm …). Uden producent/stald er kilden "field" uden afgrøde: intet markbehov og intet problem.
|
||||||
|
function ADChainModel.build(recipes, husbandry, crops, fieldGoods)
|
||||||
|
local chain = {roots = {}, demand = {}, fieldDemand = {}, needRecipes = {}, problems = {}}
|
||||||
|
local producers, herds = indexProducers(recipes), indexHerds(husbandry)
|
||||||
|
-- Rødderne kører for sig selv med fuld fart: deres egne input kræves af deres eget træ. Som leverandør til en
|
||||||
|
-- anden rod har de derfor fuld kapacitet, men deres input regnes ikke med igen (standalone = ingen rekursion).
|
||||||
|
-- En pauset rod (recipe.paused) kører også for sig selv, men er ikke rod: kæden skaber intet nyt for den.
|
||||||
|
local capacity, roots, standalone = {}, {}, {}
|
||||||
|
for _, recipe in ipairs(recipes or {}) do
|
||||||
|
capacity[recipe.key] = recipe.cyclesPerMonth or 0
|
||||||
|
if recipe.enabled and not recipe.byChain then
|
||||||
|
if not recipe.paused and not recipe.notChosen then table.insert(roots, recipe) end
|
||||||
|
standalone[recipe.key] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(roots, byKey)
|
||||||
|
|
||||||
|
-- detail = manglende liter/md (flaskehals/køb), need = varens behov pr. måned i den gren
|
||||||
|
local function problem(kind, fillType, rootKey, detail, need)
|
||||||
|
table.insert(chain.problems, {kind = kind, fillType = fillType, rootKey = rootKey, detail = detail, need = need})
|
||||||
|
end
|
||||||
|
|
||||||
|
local need
|
||||||
|
local function inputsOf(recipe, cycles, depth, path, rootKey)
|
||||||
|
local children = {}
|
||||||
|
for _, name in ipairs(sortedNames(recipe.inputs)) do
|
||||||
|
local amount = recipe.inputs[name] or 0
|
||||||
|
if amount > 0 and cycles > 0 then
|
||||||
|
local child = need(name, amount * cycles, depth + 1, path, rootKey)
|
||||||
|
child.consumer, child.consumerName = recipe.building, recipe.buildingName
|
||||||
|
table.insert(children, child)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return children
|
||||||
|
end
|
||||||
|
|
||||||
|
need = function(name, perMonth, depth, path, rootKey)
|
||||||
|
local node = {fillType = name, perMonth = perMonth, coveredPerMonth = 0, producers = {}, herds = {}, children = {}}
|
||||||
|
chain.demand[name] = (chain.demand[name] or 0) + perMonth
|
||||||
|
if depth > ADChainModel.MAX_DEPTH or path[name] then
|
||||||
|
node.source = "cycle"
|
||||||
|
problem("cycle", name, rootKey, nil, perMonth)
|
||||||
|
return node
|
||||||
|
end
|
||||||
|
path[name] = true
|
||||||
|
local rest = perMonth
|
||||||
|
-- a) gårdens fabrikker
|
||||||
|
local makers = producers[name]
|
||||||
|
if makers ~= nil then
|
||||||
|
node.source = "production"
|
||||||
|
for _, recipe in ipairs(makers) do
|
||||||
|
local perCycle = recipe.outputs[name]
|
||||||
|
local cycles = math.min(rest / perCycle, capacity[recipe.key] or 0)
|
||||||
|
if cycles > 0 then
|
||||||
|
capacity[recipe.key] = capacity[recipe.key] - cycles
|
||||||
|
rest = rest - cycles * perCycle
|
||||||
|
table.insert(node.producers, {key = recipe.key, building = recipe.building, buildingName = recipe.buildingName, cycles = cycles})
|
||||||
|
if not standalone[recipe.key] then
|
||||||
|
chain.needRecipes[recipe.key] = (chain.needRecipes[recipe.key] or 0) + cycles
|
||||||
|
for _, child in ipairs(inputsOf(recipe, cycles, depth, path, rootKey)) do table.insert(node.children, child) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if rest <= 1e-9 then break end
|
||||||
|
end
|
||||||
|
if rest > 1e-9 and #node.producers > 0 then
|
||||||
|
problem("bottleneck", name, rootKey, rest, perMonth)
|
||||||
|
node.shortPerMonth = rest
|
||||||
|
end
|
||||||
|
if rest > 1e-9 and #node.producers == 0 then
|
||||||
|
node.source = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- b) stalde
|
||||||
|
if rest > 1e-9 and #node.producers == 0 and herds[name] ~= nil then
|
||||||
|
node.source = "husbandry"
|
||||||
|
for _, herd in ipairs(herds[name]) do
|
||||||
|
local take = math.min(rest, herd.left)
|
||||||
|
if take > 0 then
|
||||||
|
herd.left = herd.left - take
|
||||||
|
rest = rest - take
|
||||||
|
table.insert(node.herds, {building = herd.building, buildingName = herd.buildingName, perMonth = take})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- c) mark
|
||||||
|
if rest > 1e-9 and #node.producers == 0 and #node.herds == 0 and crops ~= nil and crops[name] ~= nil then
|
||||||
|
node.source, node.fruit = "field", crops[name]
|
||||||
|
chain.fieldDemand[node.fruit] = (chain.fieldDemand[node.fruit] or 0) + rest
|
||||||
|
rest = 0
|
||||||
|
end
|
||||||
|
-- c2) markvare uden egen afgrøde (fx hø, hakkelse): kommer fra markerne, ingen afgrødeplan
|
||||||
|
if rest > 1e-9 and #node.producers == 0 and #node.herds == 0 and fieldGoods ~= nil and fieldGoods[name] then
|
||||||
|
node.source, node.fruit = "field", nil
|
||||||
|
rest = 0
|
||||||
|
end
|
||||||
|
-- d) ingen kilde (eller rest efter stald), eller exhausted producers
|
||||||
|
if rest > 1e-9 and makers ~= nil and #node.producers == 0 and #node.herds == 0 then
|
||||||
|
node.source = "production"
|
||||||
|
node.shortPerMonth = rest
|
||||||
|
problem("bottleneck", name, rootKey, rest, perMonth)
|
||||||
|
elseif rest > 1e-9 and (node.source == nil or node.source == "husbandry") then
|
||||||
|
if node.source == nil then node.source = "buy" end
|
||||||
|
node.shortPerMonth = rest
|
||||||
|
problem("buy", name, rootKey, rest, perMonth)
|
||||||
|
end
|
||||||
|
node.coveredPerMonth = perMonth - (node.shortPerMonth or 0)
|
||||||
|
path[name] = nil
|
||||||
|
return node
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, root in ipairs(roots) do
|
||||||
|
local cycles = root.cyclesPerMonth or 0
|
||||||
|
table.insert(chain.roots, {key = root.key, name = root.name, building = root.building, buildingName = root.buildingName,
|
||||||
|
children = inputsOf(root, cycles, 0, {}, root.key)})
|
||||||
|
end
|
||||||
|
return chain
|
||||||
|
end
|
||||||
@@ -0,0 +1,362 @@
|
|||||||
|
-- AD Chain Run (kædens handlinger i spillet)
|
||||||
|
-- Udfører ADChain's plan: skifter frie marker (kun "auto", højst MAX_FIELD_SWITCHES pr. tick), tænder/slukker
|
||||||
|
-- mellemled, opretter manglende forsyningsflows (højst MAX_NEW_FLOWS pr. tick) og stopper flows for rødder der
|
||||||
|
-- er slået fra. Hver del kører i pcall, så en fejl ikke stopper resten. Spilkald kun inde i funktioner.
|
||||||
|
|
||||||
|
ADChainRun = {}
|
||||||
|
|
||||||
|
local function log(fmt, ...) Logging.info(ADChain.LOG_PREFIX .. fmt, ...) end
|
||||||
|
local function warn(fmt, ...) Logging.warning(ADChain.LOG_PREFIX .. fmt, ...) end
|
||||||
|
|
||||||
|
local UNLOADER = "unloader"
|
||||||
|
|
||||||
|
-- Samme rolletjek som ADPool.pairAll("unloader", …) (hasRole er lokal i adPool).
|
||||||
|
local function isUnloader(implement)
|
||||||
|
return implement.role == UNLOADER or (implement.roles ~= nil and implement.roles[UNLOADER] == true)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Traktor i et flow (også et STOPPET): ADPool.collect regner den ledig til korte lån, men ADRuns.assignVehicle
|
||||||
|
-- ville flytte den permanent ud af spillerens flow — kæden tager den aldrig.
|
||||||
|
local function belongsToFlow(tractor)
|
||||||
|
return tractor.vehicle ~= nil and ADRunsController.getRunForVehicle(tractor.vehicle) ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Første puljetømmevogn der kan bære varen + den bedst egnede ledige traktor (maskinpuljens regler: hk og
|
||||||
|
-- kategori via ADPool.bestFit). used = {[id] = true}: brugt i dette tick; blocked = ADPool.blocked(nu).
|
||||||
|
function ADChainRun.choosePair(implements, tractors, fillTypeIndex, used, blocked)
|
||||||
|
if fillTypeIndex == nil then return nil end
|
||||||
|
used, blocked = used or {}, blocked or {}
|
||||||
|
local free = {}
|
||||||
|
for _, tractor in ipairs(tractors or {}) do
|
||||||
|
if not used[tractor.id] and not blocked[tractor.id] and not tractor.offNetwork and not belongsToFlow(tractor) then
|
||||||
|
table.insert(free, tractor)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, implement in ipairs(implements or {}) do
|
||||||
|
if not used[implement.id] and not blocked[implement.id] and isUnloader(implement) and ADPool.canCarry(implement, fillTypeIndex) then
|
||||||
|
local tractor = ADPool.bestFit(free, implement, "unloader")
|
||||||
|
if tractor ~= nil then return {implement = implement, tractor = tractor} end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- marker ----
|
||||||
|
|
||||||
|
local function fieldById(fields, id)
|
||||||
|
for _, field in ipairs(fields or {}) do
|
||||||
|
if field.id == id then return field end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADChainRun.switchFields(data, actions)
|
||||||
|
local switched = 0
|
||||||
|
for _, action in ipairs(actions or {}) do
|
||||||
|
if switched >= ADChain.MAX_FIELD_SWITCHES then break end
|
||||||
|
local field = fieldById(data.fields, action.fieldId)
|
||||||
|
-- brugerens egen målafgrøde røres aldrig (dobbelttjek oven i ADCropPlan)
|
||||||
|
if action.mode == "auto" and field ~= nil and field.targetSource ~= "user" then
|
||||||
|
local ok, err = pcall(ADFieldJobs.setTarget, action.fieldId, action.to, "chain")
|
||||||
|
if ok then
|
||||||
|
switched = switched + 1
|
||||||
|
ADChain.switchCount = ADChain.switchCount + 1
|
||||||
|
local text = string.format("F%s: %s -> %s (%.1f ha) til %s", tostring(action.fieldId), tostring(action.from),
|
||||||
|
tostring(action.to), action.hectares or 0, tostring(action.reason))
|
||||||
|
log("%s", text)
|
||||||
|
pcall(ADFieldJobs.notify, text, false)
|
||||||
|
else
|
||||||
|
warn("F%s kunne ikke skiftes: %s", tostring(action.fieldId), tostring(err))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- mellemled ----
|
||||||
|
|
||||||
|
local function setProduction(data, key, isOn)
|
||||||
|
local entry = data.points[key]
|
||||||
|
if entry == nil or entry.point == nil or entry.point.setProductionState == nil then
|
||||||
|
warn("produktion %s findes ikke (længere)", tostring(key))
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local ok, err = pcall(entry.point.setProductionState, entry.point, entry.productionId, isOn)
|
||||||
|
if not ok then warn("produktion %s kunne ikke %s: %s", tostring(key), isOn and "tændes" or "slukkes", tostring(err)) end
|
||||||
|
return ok
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Spillerens slukninger er allerede lagt i state.userOff (ADChain.reconcile før planen): de tændes ikke igen.
|
||||||
|
-- Delvise data: intet slukkes (en rod der ikke kunne læses, ligner en rod der er slået fra).
|
||||||
|
function ADChainRun.toggleProductions(data, prod)
|
||||||
|
local state = ADChain.state
|
||||||
|
local byChain = state.byChain
|
||||||
|
for _, key in ipairs(prod.enable or {}) do
|
||||||
|
if not state.userOff[key] and setProduction(data, key, true) then
|
||||||
|
byChain[key] = true
|
||||||
|
log("tænder %s (mellemled i kæden)", key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if data.partial then return end
|
||||||
|
for _, key in ipairs(prod.disable or {}) do
|
||||||
|
if setProduction(data, key, false) then
|
||||||
|
byChain[key] = nil
|
||||||
|
log("slukker %s (kæden bruger den ikke længere)", key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- flows ----
|
||||||
|
|
||||||
|
local function collectPool(adEnv)
|
||||||
|
local ok, implements, tractors = pcall(ADPool.collect, adEnv, function(v)
|
||||||
|
return ADFieldWork.isBusyVehicle(v, ADRunsController.getVehicleId(v))
|
||||||
|
end)
|
||||||
|
if not ok then
|
||||||
|
warn("maskinpuljen kunne ikke læses: %s", tostring(implements))
|
||||||
|
return {}, {}
|
||||||
|
end
|
||||||
|
return implements or {}, tractors or {}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- kind = "noVehicle"|"pallet": vises som status på kanten i fanen Kæder
|
||||||
|
local function createStopped(run, kind, reason, ...)
|
||||||
|
run.stopped = true
|
||||||
|
ADChain.noVehicle[run.id] = kind
|
||||||
|
log("flow '%s' oprettet stoppet: " .. reason, tostring(run.name), ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Ét flow pr. modtagerbygning med alle dens manglende bulkvarer (manuelle varer); palle-varer får hver sit
|
||||||
|
-- stoppede flow. group = {consumer, consumerName, rootKey, fillTypes}.
|
||||||
|
local function flowTitle(data, fillTypes)
|
||||||
|
local first = data.fillTitles[fillTypes[1]] or fillTypes[1]
|
||||||
|
if #fillTypes == 1 then return first end
|
||||||
|
return string.format("%s +%d", tostring(first), #fillTypes - 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function startWithPair(adEnv, run, runId, pair, used)
|
||||||
|
used[pair.tractor.id], used[pair.implement.id] = true, true
|
||||||
|
ADRunsController.assignVehicle(runId, pair.tractor.vehicle)
|
||||||
|
run = ADRuns.getRun(ADRunsController.store, runId)
|
||||||
|
local wagons = ADRuns.copyWagons(run.wagons)
|
||||||
|
wagons[pair.tractor.id] = pair.implement.id
|
||||||
|
run.wagons = wagons
|
||||||
|
ADRunsController.resolveMarkers(adEnv, run)
|
||||||
|
local results = ADRunsController.startRun(adEnv, runId)
|
||||||
|
local started = 0
|
||||||
|
for _, result in ipairs(results or {}) do if result.ok then started = started + 1 end end
|
||||||
|
log("flow '%s' oprettet med '%s' + vogn '%s' (%d startet)", tostring(run.name), tostring(pair.tractor.name),
|
||||||
|
tostring(pair.implement.name), started)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function createGroupFlow(adEnv, data, group, fillTypes, pool, used)
|
||||||
|
local title = flowTitle(data, fillTypes)
|
||||||
|
local created = ADRunsController.createRun(ADChain.flowName(title, group.consumerName or group.consumer), "supply")
|
||||||
|
local runId = created.id
|
||||||
|
ADRunsController.updateRun(runId, {targetBuildingIds = {group.consumer}, ingredientMode = "manual", ingredients = fillTypes,
|
||||||
|
waitPoolGroup = ADChain.waitPoolFor(ADRunsController.store.runs, group.consumer), chain = group.rootKey})
|
||||||
|
-- updateRun erstatter store'en: slå altid run'et op igen
|
||||||
|
local run = ADRuns.getRun(ADRunsController.store, runId)
|
||||||
|
local fillIndex = data.fillIndex[fillTypes[1]]
|
||||||
|
if fillIndex ~= nil and ADSources.isPalletFillType(fillIndex) then
|
||||||
|
createStopped(run, "pallet", "palle-vare: vælg vogn med Universal Autoload (%s)", tostring(title))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if pool.implements == nil then pool.implements, pool.tractors = collectPool(adEnv) end
|
||||||
|
if pool.blocked == nil then pool.blocked = ADPool.blocked(ADFieldJobs.now or 0) end
|
||||||
|
local pair = ADChainRun.choosePair(pool.implements, pool.tractors, fillIndex, used, pool.blocked)
|
||||||
|
if pair == nil then
|
||||||
|
createStopped(run, "noVehicle", "ingen ledig traktor/vogn til %s", tostring(title))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
startWithPair(adEnv, run, runId, pair, used)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Nye varer til en bygning, der allerede har et kædeflow: lægges i det flow.
|
||||||
|
local function extendFlow(runId, fillTypes)
|
||||||
|
local run = ADRuns.getRun(ADRunsController.store, runId)
|
||||||
|
if run == nil then return false end
|
||||||
|
local ingredients = {}
|
||||||
|
for _, name in ipairs(run.ingredients or {}) do table.insert(ingredients, name) end
|
||||||
|
local added = {}
|
||||||
|
for _, name in ipairs(fillTypes) do
|
||||||
|
local present = false
|
||||||
|
for _, have in ipairs(ingredients) do if have == name then present = true end end
|
||||||
|
if not present then table.insert(ingredients, name); table.insert(added, name) end
|
||||||
|
end
|
||||||
|
if #added == 0 then return false end
|
||||||
|
ADRunsController.updateRun(runId, {ingredients = ingredients})
|
||||||
|
log("flow '%s' får også %s", tostring(run.name), table.concat(added, ", "))
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Tidligere kædeflows (ét pr. vare) til samme bygning uden traktorer lægges sammen i ét.
|
||||||
|
function ADChainRun.mergeIdleFlows(adEnv)
|
||||||
|
local merged = 0
|
||||||
|
for _, merge in ipairs(ADChainActions.mergeIdle(ADChain.runsView(ADRunsController.store.runs))) do
|
||||||
|
if #merge.add > 0 then extendFlow(merge.keepId, merge.add) end
|
||||||
|
for _, id in ipairs(merge.deleteIds) do
|
||||||
|
local ok, err = pcall(ADRunsController.deleteRun, id, adEnv)
|
||||||
|
if ok then
|
||||||
|
ADChain.noVehicle[id] = nil
|
||||||
|
merged = merged + 1
|
||||||
|
else
|
||||||
|
warn("kædeflow %s kunne ikke lægges sammen: %s", tostring(id), tostring(err))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if merged > 0 then log("%d tomme kædeflows lagt sammen (ét flow pr. bygning)", merged) end
|
||||||
|
return merged
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADChainRun.createFlows(adEnv, data, edges)
|
||||||
|
local runs = ADChain.runsView(ADRunsController.store.runs)
|
||||||
|
local missing = ADChainActions.missingFlows(edges, runs)
|
||||||
|
local pool, used, count = {}, {}, 0
|
||||||
|
for _, group in ipairs(ADChainActions.groupMissing(missing)) do
|
||||||
|
if count >= ADChain.MAX_NEW_FLOWS then break end
|
||||||
|
local bulk, pallets = {}, {}
|
||||||
|
for _, name in ipairs(group.fillTypes) do
|
||||||
|
local index = data.fillIndex[name]
|
||||||
|
table.insert((index ~= nil and ADSources.isPalletFillType(index)) and pallets or bulk, name)
|
||||||
|
end
|
||||||
|
local ok, err = pcall(function()
|
||||||
|
if #bulk > 0 then
|
||||||
|
local existing = ADChainActions.chainRunFor(runs, group.consumer)
|
||||||
|
if existing ~= nil then
|
||||||
|
if extendFlow(existing, bulk) then count = count + 1 end
|
||||||
|
else
|
||||||
|
createGroupFlow(adEnv, data, group, bulk, pool, used)
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, name in ipairs(pallets) do
|
||||||
|
createGroupFlow(adEnv, data, group, {name}, pool, used)
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
if not ok then warn("flow til %s kunne ikke oprettes: %s", tostring(group.consumerName), tostring(err)) end
|
||||||
|
end
|
||||||
|
return count
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Slutprodukter: sælg straks (fabrikkens udgang -> "Sælg direkte"), medmindre kæden står på "Behold". Varer kæden
|
||||||
|
-- selv satte til salg, sættes tilbage til "Behold", når spilleren vælger det.
|
||||||
|
local function outputMode(point, fillIndex)
|
||||||
|
if point.getOutputDistributionMode == nil then return nil end
|
||||||
|
local ok, mode = pcall(point.getOutputDistributionMode, point, fillIndex)
|
||||||
|
return ok and mode or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADChainRun.applyFinalSales(data, chain, edges)
|
||||||
|
if ProductionPoint == nil or ProductionPoint.OUTPUT_MODE == nil then return 0 end
|
||||||
|
local state, changed = ADChain.state, 0
|
||||||
|
local wanted = {}
|
||||||
|
local uses = ADChainActions.farmUses(data, ADChain.runsView(ADRunsController.store.runs))
|
||||||
|
for _, sale in ipairs(ADChainActions.finalSales(chain, data.recipes, edges, state.finalKeep, uses)) do
|
||||||
|
local id = sale.key .. "|" .. sale.fillType
|
||||||
|
wanted[id] = true
|
||||||
|
local entry, fillIndex = (data.points or {})[sale.key], data.fillIndex[sale.fillType]
|
||||||
|
if entry ~= nil and entry.point ~= nil and fillIndex ~= nil and entry.point.setOutputDistributionMode ~= nil
|
||||||
|
and outputMode(entry.point, fillIndex) ~= ProductionPoint.OUTPUT_MODE.DIRECT_SELL then
|
||||||
|
entry.point:setOutputDistributionMode(fillIndex, ProductionPoint.OUTPUT_MODE.DIRECT_SELL)
|
||||||
|
state.soldByChain[id] = true
|
||||||
|
changed = changed + 1
|
||||||
|
log("slutprodukt %s i %s sælges straks", tostring(sale.fillType), tostring(entry.buildingName or sale.key))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for id in pairs(state.soldByChain) do
|
||||||
|
local key, fillType = id:match("^(.*)|([^|]+)$")
|
||||||
|
-- sat til salg af kæden, men ikke længere ønsket (Behold, gården bruger varen, kæden væk): tilbage til Behold
|
||||||
|
if key ~= nil and not wanted[id] then
|
||||||
|
local entry, fillIndex = (data.points or {})[key], data.fillIndex[fillType]
|
||||||
|
if entry ~= nil and entry.point ~= nil and fillIndex ~= nil
|
||||||
|
and outputMode(entry.point, fillIndex) == ProductionPoint.OUTPUT_MODE.DIRECT_SELL then
|
||||||
|
entry.point:setOutputDistributionMode(fillIndex, ProductionPoint.OUTPUT_MODE.KEEP)
|
||||||
|
changed = changed + 1
|
||||||
|
log("slutprodukt %s beholdes igen", tostring(fillType))
|
||||||
|
end
|
||||||
|
state.soldByChain[id] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return changed
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Mellemvarer der står på Sælg: næste led i kæden fik intet. Sættes til Behold (spawn/lager), så kædens flow henter
|
||||||
|
-- dem ved fabrikken; tages ud af soldByChain, så slutprodukt-logikken ikke sætter dem til salg igen.
|
||||||
|
function ADChainRun.keepIntermediates(data, edges)
|
||||||
|
if ProductionPoint == nil or ProductionPoint.OUTPUT_MODE == nil then return 0 end
|
||||||
|
local changed = 0
|
||||||
|
for _, item in ipairs(ADChainActions.soldIntermediates(edges)) do
|
||||||
|
local entry, fillIndex = (data.points or {})[item.key], data.fillIndex[item.fillType]
|
||||||
|
if entry ~= nil and entry.point ~= nil and fillIndex ~= nil and entry.point.setOutputDistributionMode ~= nil
|
||||||
|
and outputMode(entry.point, fillIndex) == ProductionPoint.OUTPUT_MODE.DIRECT_SELL then
|
||||||
|
entry.point:setOutputDistributionMode(fillIndex, ProductionPoint.OUTPUT_MODE.KEEP)
|
||||||
|
ADChain.state.soldByChain[item.key .. "|" .. item.fillType] = nil
|
||||||
|
changed = changed + 1
|
||||||
|
log("mellemvare %s i %s sælges ikke længere (næste led i kæden bruger den) -> Behold", tostring(item.fillType),
|
||||||
|
tostring(entry.buildingName or item.key))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return changed
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Flows for rødder der er slået fra: flyt til en anden rod der stadig bruger kanten, ellers stop og slip (chain
|
||||||
|
-- ryddes, så spilleren kan starte flowet igen uden at kæden stopper det næste minut; det slettes ikke).
|
||||||
|
function ADChainRun.stopFlows(adEnv, chain, edges)
|
||||||
|
local changed = 0
|
||||||
|
local active = ADChain.activeRoots(chain, ADChain.state.paused)
|
||||||
|
for _, run in ipairs(ADChainActions.flowsToStop(ADChain.runsView(ADRunsController.store.runs), active)) do
|
||||||
|
local rootKey = ADChain.stillNeeded(run, edges)
|
||||||
|
if rootKey ~= nil then
|
||||||
|
ADRunsController.updateRun(run.id, {chain = rootKey})
|
||||||
|
log("flow '%s' hører nu til kæden %s", tostring(run.name), tostring(rootKey))
|
||||||
|
changed = changed + 1
|
||||||
|
elseif ADChainActions.releaseAction(run) == "delete" then
|
||||||
|
local ok, err = pcall(ADRunsController.deleteRun, run.id, adEnv)
|
||||||
|
if ok then
|
||||||
|
ADChain.noVehicle[run.id] = nil
|
||||||
|
log("tomt kædeflow '%s' slettet: kæden %s er slået fra", tostring(run.name), tostring(run.chain))
|
||||||
|
changed = changed + 1
|
||||||
|
else
|
||||||
|
warn("flow '%s' kunne ikke slettes: %s", tostring(run.name), tostring(err))
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local ok, err = pcall(ADRunsController.stopRun, adEnv, run.id)
|
||||||
|
if ok then
|
||||||
|
ADRunsController.updateRun(run.id, {chain = ""})
|
||||||
|
log("flow '%s' stoppet og sluppet af kæden: kæden %s er slået fra", tostring(run.name), tostring(run.chain))
|
||||||
|
changed = changed + 1
|
||||||
|
else
|
||||||
|
warn("flow '%s' kunne ikke stoppes: %s", tostring(run.name), tostring(err))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return changed
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Hele planen; hver del for sig i pcall.
|
||||||
|
function ADChainRun.apply(adEnv, data, result)
|
||||||
|
local okFields, errFields = pcall(ADChainRun.switchFields, data, result.actions)
|
||||||
|
if not okFields then warn("markskift fejlede: %s", tostring(errFields)) end
|
||||||
|
local okProd, errProd = pcall(ADChainRun.toggleProductions, data, result.prod)
|
||||||
|
if not okProd then warn("mellemled fejlede: %s", tostring(errProd)) end
|
||||||
|
local okMerge, mergedCount = pcall(ADChainRun.mergeIdleFlows, adEnv)
|
||||||
|
if not okMerge then warn("sammenlægning af kædeflows fejlede: %s", tostring(mergedCount)) end
|
||||||
|
local okNew, created = pcall(ADChainRun.createFlows, adEnv, data, result.edges)
|
||||||
|
local okSell, sellErr = pcall(ADChainRun.applyFinalSales, data, result.chain, result.edges)
|
||||||
|
if not okSell then warn("slutprodukter fejlede: %s", tostring(sellErr)) end
|
||||||
|
local okKeep, keepErr = pcall(ADChainRun.keepIntermediates, data, result.edges)
|
||||||
|
if not okKeep then warn("mellemvarer fejlede: %s", tostring(keepErr)) end
|
||||||
|
if okMerge and mergedCount > 0 then created = (okNew and created or 0) + mergedCount end
|
||||||
|
if not okNew then warn("nye flows fejlede: %s", tostring(created)) end
|
||||||
|
-- delvise data: en rod der ikke kunne læses, må ikke få sine flows stoppet
|
||||||
|
local okStop, stopped = true, 0
|
||||||
|
if not data.partial then
|
||||||
|
okStop, stopped = pcall(ADChainRun.stopFlows, adEnv, result.chain, result.edges)
|
||||||
|
if not okStop then warn("stop af flows fejlede: %s", tostring(stopped)) end
|
||||||
|
end
|
||||||
|
if (okNew and created > 0) or (okStop and stopped > 0) then
|
||||||
|
local okSave, errSave = pcall(ADRunsController.save)
|
||||||
|
if not okSave then warn("flows kunne ikke gemmes: %s", tostring(errSave)) end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,453 @@
|
|||||||
|
-- AD Chain View (ren logik)
|
||||||
|
-- Rækkerne til fanen Kæder ud fra ADChain.last. Hver rod er foldet sammen som standard (én linje med samlet
|
||||||
|
-- status + antal forslag); foldet ud vises varerne rekursivt (kilde, behov pr. måned, status, lager). Forslag
|
||||||
|
-- vises altid (under roden eller under afgrøden). Pausede rødder (som modellen ikke længere ser som rødder)
|
||||||
|
-- får en grå linje med [Genoptag]. Nederst frie marker + antal automatiske skift (+ "marker scannes endnu"). Ingen spiladgang:
|
||||||
|
-- titleOf(navn) -> titel, fmt(liter) -> kompakt tal, textOf(l10n-nøgle) -> tekst gives udefra.
|
||||||
|
-- Manglende led (Task 10): foldet ud står problemteksten (problemText) som en rød tekstrække ("problem", ingen celler)
|
||||||
|
-- lige under varen — teksten er for lang til en celle og ombrydes af fanen.
|
||||||
|
-- Række: {kind="header"|"root"|"node"|"suggest"|"auto"|"info"|"empty"|"problem", indent, level, cells={c1..c5}, text,
|
||||||
|
-- actions={[celle]={kind="approve"|"dismiss"|"pause"|"resume", fieldId|rootKey}}, toggle=rootKey, fillType}
|
||||||
|
|
||||||
|
ADChainView = {}
|
||||||
|
ADChainView.INDENT = " "
|
||||||
|
-- Tegnbudget pr. celle i flowListPrefab (gui/SmartPickupFrame.xml: textMaxWidth 192/162/62/244/92 px).
|
||||||
|
-- Antaget ~11,5 px pr. tegn (spuInfoText: 800 px ~ 70 tegn), fed ~12,5 px; c3 har kun tal (~10 px pr. tegn).
|
||||||
|
ADChainView.CELL_CHARS = {22, 20, 7, 28, 10} -- ca. tegn pr. celle (c1 192 px fed, c2 162, c3 62, c4 244, c5 92 fed)
|
||||||
|
ADChainView.MAX_SOURCE_NAMES = 2
|
||||||
|
-- Tegnbudget for en tekstlinje (addTextRow/spuInfoText: 800 px ~ 70 tegn), fx linjen med frie marker.
|
||||||
|
ADChainView.TEXT_ROW_CHARS = 70
|
||||||
|
-- "stramt": lager for en mark-afgrøde under så mange måneders behov -> gul
|
||||||
|
ADChainView.TIGHT_MONTHS = 3
|
||||||
|
-- nøgler rækkerne bruger (til l10n-testen)
|
||||||
|
ADChainView.TEXT_KEYS = {"spu_chain_optIn", "spu_chain_optOut", "spu_chain_notChosen", "spu_chain_candidatesShow", "spu_chain_candidatesHide", "spu_chain_finalFarm", "spu_chain_finalSell", "spu_chain_finalKeep", "spu_chain_suggestHeader", "spu_chain_empty", "spu_chain_approve", "spu_chain_dismiss", "spu_chain_auto",
|
||||||
|
"spu_chain_flowOk", "spu_chain_flowMissing", "spu_chain_flowPallet", "spu_chain_flowNone", "spu_chain_flowStopped", "spu_chain_bottleneck",
|
||||||
|
"spu_chain_buy", "spu_chain_cycle", "spu_chain_fieldShort", "spu_chain_tight", "spu_chain_noField", "spu_chain_buySource", "spu_chain_fromFields",
|
||||||
|
"spu_chain_pause", "spu_chain_resume", "spu_chain_scanning", "spu_chain_rootOk", "spu_chain_rootError", "spu_chain_rootWaiting",
|
||||||
|
"spu_chain_rootPaused", "spu_chain_rootSummary", "spu_chain_free", "spu_chain_switches", "spu_chain_gone", "spu_chain_col_item",
|
||||||
|
"spu_chain_col_source", "spu_chain_col_perMonth", "spu_chain_col_status", "spu_chain_col_stock",
|
||||||
|
"spu_chain_flowAuto", "spu_chain_problem_buy", "spu_chain_problem_bottleneck", "spu_chain_problem_off", "spu_chain_problem_cycle", "spu_chain_problem_barn"}
|
||||||
|
|
||||||
|
local FLOW_KEYS = {ok = "spu_chain_flowOk", missing = "spu_chain_flowNone", stopped = "spu_chain_flowStopped", noVehicle = "spu_chain_flowMissing",
|
||||||
|
pallet = "spu_chain_flowPallet", auto = "spu_chain_flowAuto"}
|
||||||
|
local ROOT_STATUS = {running = "spu_chain_rootOk", error = "spu_chain_rootError", waiting = "spu_chain_rootWaiting", stopped = "spu_chain_rootPaused"}
|
||||||
|
|
||||||
|
local function identity(value) return tostring(value) end
|
||||||
|
|
||||||
|
-- samme som SmartPickupFrame.formatCompact (standard-fmt når ingen gives, fx i beskeden fra kædens tick)
|
||||||
|
function ADChainView.compact(liters)
|
||||||
|
liters = math.floor((liters or 0) + 0.5)
|
||||||
|
if liters >= 1000000 then return string.format("%.1fM", liters / 1000000)
|
||||||
|
elseif liters >= 1000 then return string.format("%.1fk", liters / 1000) end
|
||||||
|
return string.format("%d", liters)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- UTF-8-sikker længde/afkortning (æøå og → er flere bytes) ----
|
||||||
|
|
||||||
|
function ADChainView.textLength(text)
|
||||||
|
local count = 0
|
||||||
|
for _ in tostring(text):gmatch("[^\128-\191]") do count = count + 1 end
|
||||||
|
return count
|
||||||
|
end
|
||||||
|
|
||||||
|
local function utf8Prefix(text, chars)
|
||||||
|
local result, count = {}, 0
|
||||||
|
for char in text:gmatch("[^\128-\191][\128-\191]*") do
|
||||||
|
if count >= chars then break end
|
||||||
|
table.insert(result, char)
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
return table.concat(result)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Afkort til celle-budgettet med ".." til sidst.
|
||||||
|
function ADChainView.fit(text, chars)
|
||||||
|
text = tostring(text or "")
|
||||||
|
if ADChainView.textLength(text) <= chars then return text end
|
||||||
|
return utf8Prefix(text, math.max(0, chars - 2)) .. ".."
|
||||||
|
end
|
||||||
|
|
||||||
|
local function fitCells(cells)
|
||||||
|
local result = {}
|
||||||
|
for index = 1, 5 do result[index] = ADChainView.fit(cells[index] or "", ADChainView.CELL_CHARS[index]) end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local function haText(ha)
|
||||||
|
return string.format("%.0f", ha or 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function plannedFruit(field)
|
||||||
|
if field.targetFruit ~= nil and field.targetFruit ~= "" then return field.targetFruit end
|
||||||
|
return field.fruit
|
||||||
|
end
|
||||||
|
|
||||||
|
local function edgeKey(node)
|
||||||
|
return tostring(node.consumer) .. "|" .. tostring(node.fillType)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function shortageByFruit(last)
|
||||||
|
local result = {}
|
||||||
|
for _, shortage in ipairs(last.shortages or {}) do result[shortage.fruit] = shortage end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local function stockOf(last, fruit)
|
||||||
|
local stock = last.data ~= nil and last.data.stock or nil
|
||||||
|
return stock ~= nil and fruit ~= nil and stock[fruit] or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Lager for en mark-node: afgrødens lager, eller varens eget lager for en markvare uden afgrøde (hø, hakkelse …).
|
||||||
|
local function fieldStockOf(last, node)
|
||||||
|
if node.fruit ~= nil then return stockOf(last, node.fruit) end
|
||||||
|
local byFill = last.data ~= nil and last.data.stockByFill or nil
|
||||||
|
return byFill ~= nil and byFill[node.fillType] or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Mark-afgrøde uden mangel men med lager under TIGHT_MONTHS måneders behov.
|
||||||
|
local function isTight(node, last, short)
|
||||||
|
if node.source ~= "field" or node.fruit == nil or short[node.fruit] ~= nil then return false end
|
||||||
|
return (stockOf(last, node.fruit) or 0) < ADChainView.TIGHT_MONTHS * (node.perMonth or 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function eachNode(nodes, fn, depth)
|
||||||
|
for _, node in ipairs(nodes or {}) do
|
||||||
|
fn(node, depth or 1)
|
||||||
|
eachNode(node.children, fn, (depth or 1) + 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADChainView.rootLevel(root, last)
|
||||||
|
last = last or {}
|
||||||
|
if (last.paused or {})[root.key] then return "stopped" end
|
||||||
|
for _, problem in ipairs((last.chain or {}).problems or {}) do
|
||||||
|
if problem.rootKey == root.key then return "error" end
|
||||||
|
end
|
||||||
|
local short, flowStatus = shortageByFruit(last), last.flowStatus or {}
|
||||||
|
local hasShortage, hasWaiting = false, false
|
||||||
|
eachNode(root.children, function(node)
|
||||||
|
if node.fruit ~= nil and short[node.fruit] ~= nil then hasShortage = true end
|
||||||
|
if (node.shortPerMonth or 0) > 0 then hasShortage = true end
|
||||||
|
local status = node.consumer ~= nil and flowStatus[edgeKey(node)] or nil
|
||||||
|
if (status ~= nil and status ~= "ok" and status ~= "auto") or isTight(node, last, short) then hasWaiting = true end
|
||||||
|
end)
|
||||||
|
if hasShortage then return "error" end
|
||||||
|
if hasWaiting then return "waiting" end
|
||||||
|
return "running"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Højst MAX_SOURCE_NAMES navne + " +k".
|
||||||
|
local function joinCapped(names)
|
||||||
|
local shown = {}
|
||||||
|
for index = 1, math.min(#names, ADChainView.MAX_SOURCE_NAMES) do shown[index] = names[index] end
|
||||||
|
local text = table.concat(shown, ", ")
|
||||||
|
if #names > #shown then text = text .. " +" .. tostring(#names - #shown) end
|
||||||
|
return text
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Opskrifter (alle, også slukkede) der laver varen, i datarækkefølge.
|
||||||
|
local function producersOf(last, fillType)
|
||||||
|
local list = {}
|
||||||
|
for _, recipe in ipairs(last.data ~= nil and last.data.recipes or {}) do
|
||||||
|
if ((recipe.outputs or {})[fillType] or 0) > 0 then table.insert(list, recipe) end
|
||||||
|
end
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Årsag i klart sprog med råd for et problem fra kædemodellen ({kind="buy"|"bottleneck"|"cycle", fillType, rootKey,
|
||||||
|
-- detail = manglende l/md, need = behov l/md}). Køb af en rest efter gårdens stalde -> stald-tekst. En producent spilleren selv har slukket (last.userOff, stadig
|
||||||
|
-- slukket) slår igennem som "slukket af dig" — det er den reelle årsag, uanset om modellen så køb eller flaskehals.
|
||||||
|
function ADChainView.problemText(problem, last, textOf, titleOf, fmt)
|
||||||
|
last, textOf, fmt = last or {}, textOf or identity, fmt or ADChainView.compact
|
||||||
|
titleOf = titleOf or function(name) return (last.titles or {})[name] or tostring(name) end
|
||||||
|
local item = titleOf(problem.fillType)
|
||||||
|
if problem.kind == "cycle" then return string.format(textOf("spu_chain_problem_cycle"), item) end
|
||||||
|
local userOff, names, seen = last.userOff or {}, {}, {}
|
||||||
|
for _, recipe in ipairs(producersOf(last, problem.fillType)) do
|
||||||
|
if userOff[recipe.key] and not recipe.enabled then
|
||||||
|
return string.format(textOf("spu_chain_problem_off"), tostring(recipe.buildingName), titleOf(recipe.name))
|
||||||
|
end
|
||||||
|
local name = tostring(recipe.buildingName)
|
||||||
|
if (recipe.cyclesPerMonth or 0) > 0 and not seen[name] then
|
||||||
|
seen[name] = true
|
||||||
|
table.insert(names, name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if problem.kind == "bottleneck" and #names > 0 then
|
||||||
|
local need = problem.need or problem.detail or 0
|
||||||
|
local made = math.max(0, need - (problem.detail or 0))
|
||||||
|
return string.format(textOf("spu_chain_problem_bottleneck"), joinCapped(names), fmt(math.floor(made + 0.5)), fmt(math.floor(need + 0.5)))
|
||||||
|
end
|
||||||
|
-- modellen melder også "køb" for resten, når gårdens stalde leverer for lidt: så er rådet ikke "byg en fabrik"
|
||||||
|
for _, herd in ipairs(last.data ~= nil and last.data.husbandry or {}) do
|
||||||
|
if ((herd.outputs or {})[problem.fillType] or 0) > 0 then
|
||||||
|
return string.format(textOf("spu_chain_problem_barn"), item)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return string.format(textOf("spu_chain_problem_buy"), item)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sourceText(node, last, textOf)
|
||||||
|
local names = {}
|
||||||
|
if node.source == "production" then
|
||||||
|
for _, producer in ipairs(node.producers or {}) do table.insert(names, tostring(producer.buildingName)) end
|
||||||
|
elseif node.source == "husbandry" then
|
||||||
|
for _, herd in ipairs(node.herds or {}) do table.insert(names, tostring(herd.buildingName)) end
|
||||||
|
elseif node.source == "field" then
|
||||||
|
-- markvare uden egen afgrøde (hø, hakkelse …): ingen afgrødeplan, bare "fra marker"
|
||||||
|
if node.fruit == nil then return textOf("spu_chain_fromFields") end
|
||||||
|
for _, field in ipairs(last.fields or {}) do
|
||||||
|
if plannedFruit(field) == node.fruit then table.insert(names, "F" .. tostring(field.id)) end
|
||||||
|
end
|
||||||
|
if #names == 0 then return textOf("spu_chain_noField") end
|
||||||
|
elseif node.source == "buy" then
|
||||||
|
return textOf("spu_chain_buySource")
|
||||||
|
else
|
||||||
|
return "-"
|
||||||
|
end
|
||||||
|
return joinCapped(names)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Nodens status (tekst, niveau): kredsløb/flaskehals/køb/mangel (rød) > flow ikke ok (gul) > stramt lager (gul) > flow ok.
|
||||||
|
local function nodeStatus(node, last, fmt, textOf, short)
|
||||||
|
if node.source == "cycle" then return textOf("spu_chain_cycle"), "error" end
|
||||||
|
if (node.shortPerMonth or 0) > 0 then
|
||||||
|
local key = node.source == "production" and "spu_chain_bottleneck" or "spu_chain_buy"
|
||||||
|
return string.format(textOf(key), fmt(node.shortPerMonth)), "error"
|
||||||
|
end
|
||||||
|
local shortage = node.fruit ~= nil and short[node.fruit] or nil
|
||||||
|
if shortage ~= nil then return string.format(textOf("spu_chain_fieldShort"), haText(shortage.missingHa)), "error" end
|
||||||
|
local status = node.consumer ~= nil and (last.flowStatus or {})[edgeKey(node)] or nil
|
||||||
|
-- udgangen står på Fordel: spillet leverer selv
|
||||||
|
if status == "auto" then return textOf("spu_chain_flowAuto"), "running" end
|
||||||
|
if status ~= nil and status ~= "ok" then return textOf(FLOW_KEYS[status] or "spu_chain_flowNone"), "waiting" end
|
||||||
|
if isTight(node, last, short) then return textOf("spu_chain_tight"), "waiting" end
|
||||||
|
if status == "ok" then return textOf("spu_chain_flowOk"), "running" end
|
||||||
|
return "", "running"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function joinText(cells)
|
||||||
|
local parts = {}
|
||||||
|
for _, cell in ipairs(cells) do
|
||||||
|
if cell ~= nil and cell ~= "" then table.insert(parts, cell) end
|
||||||
|
end
|
||||||
|
return table.concat(parts, " · ")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function makeRow(kind, indent, level, cells, actions, toggle)
|
||||||
|
local fitted = fitCells(cells)
|
||||||
|
return {kind = kind, indent = indent, level = level, cells = fitted, actions = actions or {}, toggle = toggle, text = joinText(fitted)}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function suggestRow(action, indent, titleOf, textOf)
|
||||||
|
local pad = string.rep(ADChainView.INDENT, indent)
|
||||||
|
local from = (action.from ~= nil and action.from ~= "") and titleOf(action.from) or "-"
|
||||||
|
local cells = {pad .. "F" .. tostring(action.fieldId) .. " " .. from, "→ " .. titleOf(action.to), haText(action.hectares) .. " ha"}
|
||||||
|
if action.mode == "suggest" then
|
||||||
|
cells[4], cells[5] = textOf("spu_chain_approve"), textOf("spu_chain_dismiss")
|
||||||
|
return makeRow("suggest", indent, "waiting", cells,
|
||||||
|
{[4] = {kind = "approve", fieldId = action.fieldId}, [5] = {kind = "dismiss", fieldId = action.fieldId}})
|
||||||
|
end
|
||||||
|
cells[4] = textOf("spu_chain_auto")
|
||||||
|
return makeRow("auto", indent, "running", cells)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Handlinger for de afgrøder (i rækkefølge) der hentes fra mark under roden.
|
||||||
|
local function rootFruits(root)
|
||||||
|
local fruits, seen = {}, {}
|
||||||
|
eachNode(root.children, function(node)
|
||||||
|
if node.source == "field" and node.fruit ~= nil and not seen[node.fruit] then
|
||||||
|
seen[node.fruit] = true
|
||||||
|
table.insert(fruits, node.fruit)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
return fruits
|
||||||
|
end
|
||||||
|
|
||||||
|
local function actionsFor(last, fruit, modes)
|
||||||
|
local list = {}
|
||||||
|
for _, action in ipairs(last.actions or {}) do
|
||||||
|
if action.to == fruit and modes[action.mode] then table.insert(list, action) end
|
||||||
|
end
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Alle forslag (mode "suggest") én gang pr. mark, efter marknummer (in-game 30/9: samme forslag stod under
|
||||||
|
-- hver kæde der bruger afgrøden, spredt mellem kæderne).
|
||||||
|
function ADChainView.suggestionList(last)
|
||||||
|
local byField, list = {}, {}
|
||||||
|
for _, action in ipairs((last or {}).actions or {}) do
|
||||||
|
if action.mode == "suggest" and action.fieldId ~= nil and not byField[action.fieldId] then
|
||||||
|
byField[action.fieldId] = true
|
||||||
|
table.insert(list, action)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(list, function(a, b) return (tonumber(a.fieldId) or 0) < (tonumber(b.fieldId) or 0) end)
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Kæder efter status (fejl/mangel, venter, dækket, pause), så bygning, så produkt.
|
||||||
|
local LEVEL_RANK = {error = 1, waiting = 2, running = 3, stopped = 4}
|
||||||
|
function ADChainView.sortRoots(roots, last)
|
||||||
|
local keyed = {}
|
||||||
|
for index, root in ipairs(roots or {}) do
|
||||||
|
table.insert(keyed, {root = root, rank = LEVEL_RANK[ADChainView.rootLevel(root, last)] or 5,
|
||||||
|
building = tostring(root.buildingName or ""), name = tostring(root.name or ""), index = index})
|
||||||
|
end
|
||||||
|
table.sort(keyed, function(a, b)
|
||||||
|
if a.rank ~= b.rank then return a.rank < b.rank end
|
||||||
|
if a.building ~= b.building then return a.building < b.building end
|
||||||
|
if a.name ~= b.name then return a.name < b.name end
|
||||||
|
return a.index < b.index
|
||||||
|
end)
|
||||||
|
local sorted = {}
|
||||||
|
for _, item in ipairs(keyed) do table.insert(sorted, item.root) end
|
||||||
|
return sorted
|
||||||
|
end
|
||||||
|
|
||||||
|
local function rootRows(rows, root, last, ctx, isExpanded)
|
||||||
|
local level = ADChainView.rootLevel(root, last)
|
||||||
|
local paused = (last.paused or {})[root.key] == true
|
||||||
|
local suggestions = {}
|
||||||
|
for _, fruit in ipairs(rootFruits(root)) do
|
||||||
|
for _, action in ipairs(actionsFor(last, fruit, {suggest = true})) do table.insert(suggestions, action) end
|
||||||
|
end
|
||||||
|
local status = ctx.textOf(ROOT_STATUS[level])
|
||||||
|
if #suggestions > 0 then status = string.format(ctx.textOf("spu_chain_rootSummary"), status, #suggestions) end
|
||||||
|
local marker = (#(root.children or {}) > 0) and (isExpanded and "– " or "+ ") or ""
|
||||||
|
-- slutproduktet: "Sælg straks" (standard) eller "Behold" — klik på feltet skifter
|
||||||
|
-- varer gården selv bruger (last.sellable uden roden) sælges aldrig: "Gård" uden klik
|
||||||
|
local keep = (last.finalKeep or {})[root.key] == true
|
||||||
|
local sellable = last.sellable == nil or last.sellable[root.key] == true
|
||||||
|
local finalText = not sellable and "spu_chain_finalFarm" or (keep and "spu_chain_finalKeep" or "spu_chain_finalSell")
|
||||||
|
local actions = {[5] = {kind = paused and "resume" or "pause", rootKey = root.key}}
|
||||||
|
if sellable then actions[3] = {kind = "finalMode", rootKey = root.key, keep = not keep} end
|
||||||
|
table.insert(rows, makeRow("root", 0, level, {marker .. ctx.titleOf(root.name), tostring(root.buildingName or ""),
|
||||||
|
ctx.textOf(finalText), status, ctx.textOf(paused and "spu_chain_resume" or "spu_chain_pause")}, actions, root.key))
|
||||||
|
if not isExpanded then return end
|
||||||
|
local shown, problems, told = {}, {}, {}
|
||||||
|
for _, problem in ipairs((last.chain or {}).problems or {}) do
|
||||||
|
if problem.rootKey == root.key then table.insert(problems, problem) end
|
||||||
|
end
|
||||||
|
local function problemRows(fillType, indent)
|
||||||
|
for _, problem in ipairs(problems) do
|
||||||
|
local key = tostring(problem.fillType) .. "|" .. tostring(problem.kind)
|
||||||
|
if (fillType == nil or problem.fillType == fillType) and not told[key] then
|
||||||
|
told[key] = true
|
||||||
|
table.insert(rows, {kind = "problem", indent = indent, level = "error",
|
||||||
|
text = string.rep(ADChainView.INDENT, indent) .. ADChainView.problemText(problem, last, ctx.textOf, ctx.titleOf, ctx.fmt)})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
eachNode(root.children, function(node, depth)
|
||||||
|
local text, nodeLevel = nodeStatus(node, last, ctx.fmt, ctx.textOf, ctx.short)
|
||||||
|
local stock = node.source == "field" and fieldStockOf(last, node) or nil
|
||||||
|
local row = makeRow("node", depth, nodeLevel, {string.rep(ADChainView.INDENT, depth) .. ctx.titleOf(node.fillType),
|
||||||
|
sourceText(node, last, ctx.textOf), ctx.fmt(node.perMonth or 0), text, stock ~= nil and ctx.fmt(stock) or ""})
|
||||||
|
row.fillType = node.fillType
|
||||||
|
-- klik åbner flowet der kører varen til modtageren (in-game 30/9: "flow stopped" kunne ikke redigeres)
|
||||||
|
if node.consumer ~= nil then row.openConsumer, row.openFillType = node.consumer, node.fillType end
|
||||||
|
table.insert(rows, row)
|
||||||
|
problemRows(node.fillType, depth + 1)
|
||||||
|
if node.source == "field" and node.fruit ~= nil and not shown[node.fruit] then
|
||||||
|
shown[node.fruit] = true
|
||||||
|
for _, action in ipairs(actionsFor(last, node.fruit, {auto = true})) do
|
||||||
|
table.insert(rows, suggestRow(action, depth + 1, ctx.titleOf, ctx.textOf))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
-- problemer uden en vist vare (bør ikke ske) står sidst under roden
|
||||||
|
problemRows(nil, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Pausede rødder: modelRecipes markerer dem paused, så modellen ser dem ikke som rødder -> grå linje med [Genoptag].
|
||||||
|
local function pausedRows(rows, last, ctx)
|
||||||
|
local present, keys = {}, {}
|
||||||
|
for _, root in ipairs(last.chain ~= nil and last.chain.roots or {}) do present[root.key] = true end
|
||||||
|
for key in pairs(last.paused or {}) do
|
||||||
|
if not present[key] then table.insert(keys, key) end
|
||||||
|
end
|
||||||
|
table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
|
||||||
|
for _, key in ipairs(keys) do
|
||||||
|
local recipe
|
||||||
|
for _, candidate in ipairs(last.data ~= nil and last.data.recipes or {}) do
|
||||||
|
if candidate.key == key then recipe = candidate end
|
||||||
|
end
|
||||||
|
local name = recipe ~= nil and recipe.name ~= nil and ctx.titleOf(recipe.name) or tostring(key)
|
||||||
|
-- pauset kæde: [Start] genoptager, [Fjern] tager den ud af kæderne
|
||||||
|
table.insert(rows, makeRow("root", 0, "stopped", {name, recipe ~= nil and tostring(recipe.buildingName or "") or "", "",
|
||||||
|
ctx.textOf("spu_chain_optOut"), ctx.textOf("spu_chain_resume")},
|
||||||
|
{[4] = {kind = "optOut", rootKey = key}, [5] = {kind = "resume", rootKey = key}}))
|
||||||
|
end
|
||||||
|
return #keys
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Tændte produktioner uden kæde: foldet sammen under én linje; udfoldet med [Kæde til] pr. produktion.
|
||||||
|
local function candidateRows(rows, last, ctx, isExpanded)
|
||||||
|
local list = last.candidates or {}
|
||||||
|
if #list == 0 then return end
|
||||||
|
local key = isExpanded and "spu_chain_candidatesHide" or "spu_chain_candidatesShow"
|
||||||
|
table.insert(rows, makeRow("root", 0, nil, {(isExpanded and "– " or "+ ") .. string.format(ctx.textOf(key), #list), "", "", "", ""}, {}, "__candidates"))
|
||||||
|
if not isExpanded then return end
|
||||||
|
for _, candidate in ipairs(list) do
|
||||||
|
table.insert(rows, makeRow("root", 1, nil, {ADChainView.INDENT .. ctx.titleOf(candidate.name), tostring(candidate.buildingName or ""), "",
|
||||||
|
ctx.textOf("spu_chain_notChosen"), ctx.textOf("spu_chain_optIn")}, {[5] = {kind = "optIn", rootKey = candidate.key}}))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- "Marker fri til kæden: F1, F2 +k" — så mange navne som tekstlinjen kan rumme, resten talt.
|
||||||
|
function ADChainView.freeText(names, textOf)
|
||||||
|
local format = textOf("spu_chain_free")
|
||||||
|
if #names == 0 then return string.format(format, "-") end
|
||||||
|
for count = #names, 1, -1 do
|
||||||
|
local shown = {}
|
||||||
|
for index = 1, count do shown[index] = names[index] end
|
||||||
|
local list = table.concat(shown, ", ")
|
||||||
|
if count < #names then list = list .. " +" .. tostring(#names - count) end
|
||||||
|
local text = string.format(format, list)
|
||||||
|
if ADChainView.textLength(text) <= ADChainView.TEXT_ROW_CHARS then return text end
|
||||||
|
end
|
||||||
|
return ADChainView.fit(string.format(format, "+" .. tostring(#names)), ADChainView.TEXT_ROW_CHARS)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function footerRows(rows, last, textOf)
|
||||||
|
local free = {}
|
||||||
|
for _, field in ipairs(last.fields or {}) do
|
||||||
|
if field.chainFree then table.insert(free, field.id) end
|
||||||
|
end
|
||||||
|
table.sort(free)
|
||||||
|
local names = {}
|
||||||
|
for _, id in ipairs(free) do table.insert(names, "F" .. tostring(id)) end
|
||||||
|
table.insert(rows, {kind = "info", indent = 0, text = ADChainView.freeText(names, textOf)})
|
||||||
|
table.insert(rows, {kind = "info", indent = 0, text = string.format(textOf("spu_chain_switches"), last.switchCount or 0)})
|
||||||
|
if last.fieldsIncomplete then table.insert(rows, {kind = "info", indent = 0, text = textOf("spu_chain_scanning")}) end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- expanded = {[rodnøgle] = true} for udfoldede rødder (alle andre er foldet sammen).
|
||||||
|
function ADChainView.rows(last, titleOf, fmt, textOf, expanded)
|
||||||
|
textOf, fmt, expanded = textOf or identity, fmt or identity, expanded or {}
|
||||||
|
local titles = last ~= nil and last.titles or {}
|
||||||
|
titleOf = titleOf or function(name) return (titles or {})[name] or tostring(name) end
|
||||||
|
local rows = {}
|
||||||
|
local roots = last ~= nil and last.chain ~= nil and last.chain.roots or {}
|
||||||
|
local hasPaused = last ~= nil and next(last.paused or {}) ~= nil
|
||||||
|
if #roots == 0 and not hasPaused then
|
||||||
|
table.insert(rows, {kind = "empty", indent = 0, text = textOf("spu_chain_empty")})
|
||||||
|
end
|
||||||
|
if last == nil then return rows end
|
||||||
|
if #roots > 0 or hasPaused then
|
||||||
|
table.insert(rows, makeRow("header", 0, nil, {textOf("spu_chain_col_item"), textOf("spu_chain_col_source"),
|
||||||
|
textOf("spu_chain_col_perMonth"), textOf("spu_chain_col_status"), textOf("spu_chain_col_stock")}))
|
||||||
|
end
|
||||||
|
local ctx = {titleOf = titleOf, fmt = fmt, textOf = textOf, short = shortageByFruit(last)}
|
||||||
|
local suggestions = ADChainView.suggestionList(last)
|
||||||
|
if #suggestions > 0 then
|
||||||
|
table.insert(rows, {kind = "info", indent = 0, text = textOf("spu_chain_suggestHeader")})
|
||||||
|
for _, action in ipairs(suggestions) do table.insert(rows, suggestRow(action, 0, titleOf, textOf)) end
|
||||||
|
end
|
||||||
|
for _, root in ipairs(ADChainView.sortRoots(roots, last)) do rootRows(rows, root, last, ctx, expanded[root.key] == true) end
|
||||||
|
pausedRows(rows, last, ctx)
|
||||||
|
candidateRows(rows, last, ctx, expanded["__candidates"] == true)
|
||||||
|
footerRows(rows, last, textOf)
|
||||||
|
return rows
|
||||||
|
end
|
||||||
@@ -169,12 +169,43 @@ function ADCpBridge.getSpecs(vehicle)
|
|||||||
return specs
|
return specs
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Afgrøder høsteren kan tage (alle skærebord/optagere i kæden). nil = ikke en høster.
|
-- Vinhøster (fx Grégoire GL86): afgrøden vælges i konfigurationen og står i spec_vineCutter, ikke i spec_cutter
|
||||||
|
-- (in-game 29/9: F22 hvide druer "none can do it" — moddet så aldrig vinhøstere som høstere).
|
||||||
|
function ADCpBridge.vineFruitSet(spec)
|
||||||
|
local fruits = {}
|
||||||
|
if spec == nil then return fruits end
|
||||||
|
for _, key in ipairs({"inputFruitTypeIndex", "fruitTypeIndex", "fruitType"}) do
|
||||||
|
if type(spec[key]) == "number" then fruits[spec[key]] = true end
|
||||||
|
end
|
||||||
|
if type(spec.fruitTypes) == "table" then
|
||||||
|
for key, value in pairs(spec.fruitTypes) do
|
||||||
|
if type(value) == "number" then fruits[value] = true
|
||||||
|
elseif value == true and type(key) == "number" then fruits[key] = true end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return fruits
|
||||||
|
end
|
||||||
|
|
||||||
|
local vineLogged = setmetatable({}, {__mode = "k"})
|
||||||
|
|
||||||
|
-- Afgrøder høsteren kan tage (alle skærebord/optagere/vinhøstere i kæden). nil = ikke en høster.
|
||||||
function ADCpBridge.getHarvestFruits(vehicle)
|
function ADCpBridge.getHarvestFruits(vehicle)
|
||||||
if vehicle.spec_combine == nil then return nil end
|
if vehicle.spec_combine == nil then return nil end
|
||||||
local fruits = {}
|
local fruits = {}
|
||||||
for _, object in ipairs(collect(vehicle)) do
|
for _, object in ipairs(collect(vehicle)) do
|
||||||
for fruit in pairs(ADCpBridge.harvestFruitSet(object.spec_cutter)) do fruits[fruit] = true end
|
for fruit in pairs(ADCpBridge.harvestFruitSet(object.spec_cutter)) do fruits[fruit] = true end
|
||||||
|
if object.spec_vineCutter ~= nil then
|
||||||
|
local vine = ADCpBridge.vineFruitSet(object.spec_vineCutter)
|
||||||
|
for fruit in pairs(vine) do fruits[fruit] = true end
|
||||||
|
if next(vine) == nil and not vineLogged[object] then
|
||||||
|
-- ukendt feltnavn i denne spilversion: vis nøglerne, så reglen kan rettes
|
||||||
|
vineLogged[object] = true
|
||||||
|
local keys = {}
|
||||||
|
for key, value in pairs(object.spec_vineCutter) do table.insert(keys, tostring(key) .. "=" .. type(value)) end
|
||||||
|
table.sort(keys)
|
||||||
|
Logging.info("ADSmartPickup: vinhøster uden afgrøde fundet — spec_vineCutter: %s", table.concat(keys, ", "))
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
return fruits
|
return fruits
|
||||||
end
|
end
|
||||||
@@ -360,9 +391,21 @@ local function courseplayWidth(vehicle)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Vinhøster uden AI-markører: bredden er vinrækken (3 m = Courseplays vin-gitter for druer).
|
||||||
|
ADCpBridge.VINE_ROW_WIDTH = 3
|
||||||
|
function ADCpBridge.widthOrVine(width, hasVineCutter)
|
||||||
|
-- under markarbejdets minimum (1,5 m) — også en lille bredde fra Courseplay (in-game 29/9: stadig widthUnknown)
|
||||||
|
if hasVineCutter and (width or 0) < 1.5 then return ADCpBridge.VINE_ROW_WIDTH end
|
||||||
|
return width
|
||||||
|
end
|
||||||
|
|
||||||
function ADCpBridge.getWorkWidth(vehicle)
|
function ADCpBridge.getWorkWidth(vehicle)
|
||||||
|
local hasVine = false
|
||||||
|
for _, object in ipairs(collect(vehicle)) do
|
||||||
|
if object.spec_vineCutter ~= nil then hasVine = true end
|
||||||
|
end
|
||||||
local cpWidth = courseplayWidth(vehicle)
|
local cpWidth = courseplayWidth(vehicle)
|
||||||
if cpWidth ~= nil then return cpWidth end
|
if cpWidth ~= nil then return ADCpBridge.widthOrVine(cpWidth, hasVine) end
|
||||||
local best = 0
|
local best = 0
|
||||||
for _, object in ipairs(collect(vehicle)) do
|
for _, object in ipairs(collect(vehicle)) do
|
||||||
local variable = variableWidth(object)
|
local variable = variableWidth(object)
|
||||||
@@ -374,7 +417,7 @@ function ADCpBridge.getWorkWidth(vehicle)
|
|||||||
if ok and type(width) == "number" and width > best then best = width end
|
if ok and type(width) == "number" and width > best then best = width end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return best
|
return ADCpBridge.widthOrVine(best, hasVine)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- AutoDrive kører også som AI-job i FS25: aktiv AD er ikke Courseplay (in-game 29/9: Milk Store ved Cow Wait
|
-- AutoDrive kører også som AI-job i FS25: aktiv AD er ikke Courseplay (in-game 29/9: Milk Store ved Cow Wait
|
||||||
@@ -476,6 +519,24 @@ function ADCpBridge.courseHasHeadlands(course)
|
|||||||
return not ok or type(n) ~= "number" or n > 0
|
return not ok or type(n) ~= "number" or n > 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Kursusbredde til Courseplay: højst 50 m (CP's grænse; Unrealistic-redskaber kan sættes bredere). nil = urørt.
|
||||||
|
ADCpBridge.CP_MAX_WIDTH = 50
|
||||||
|
function ADCpBridge.cpCourseWidth(width)
|
||||||
|
if type(width) == "number" and width > ADCpBridge.CP_MAX_WIDTH then return ADCpBridge.CP_MAX_WIDTH end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function clampCourseWidth(vehicle)
|
||||||
|
pcall(function()
|
||||||
|
local setting = vehicle:getCourseGeneratorSettings().workWidth
|
||||||
|
local capped = ADCpBridge.cpCourseWidth(setting:getValue())
|
||||||
|
if capped ~= nil then
|
||||||
|
setting:setValue(capped)
|
||||||
|
Logging.info("ADSmartPickup: kursusbredde for '%s' begrænset til %d m (Courseplays maksimum)", tostring(vehicle.getName ~= nil and vehicle:getName() or "?"), capped)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
-- Multi-tool: sæt antal sæt på kursus-generatoren før kurset laves (1 = normalt kursus).
|
-- Multi-tool: sæt antal sæt på kursus-generatoren før kurset laves (1 = normalt kursus).
|
||||||
local function setMultiTools(vehicle, n)
|
local function setMultiTools(vehicle, n)
|
||||||
pcall(function() vehicle:getCourseGeneratorSettings().multiTools:setValue(n or 1) end)
|
pcall(function() vehicle:getCourseGeneratorSettings().multiTools:setValue(n or 1) end)
|
||||||
@@ -500,7 +561,7 @@ end
|
|||||||
-- Vinmarker (druer/oliven): Courseplays kursus ligger som standard PÅ vinen — en sprøjte/spreder/harve kører
|
-- Vinmarker (druer/oliven): Courseplays kursus ligger som standard PÅ vinen — en sprøjte/spreder/harve kører
|
||||||
-- så ind i stolperne ("Blocked by an object", in-game 26/9 F22). Alt der arbejder i gangene køres til
|
-- så ind i stolperne ("Blocked by an object", in-game 26/9 F22). Alt der arbejder i gangene køres til
|
||||||
-- venstre for vinen; høsteren kører hen over rækken. nil = ikke en vinmark (rør intet).
|
-- venstre for vinen; høsteren kører hen over rækken. nil = ikke en vinmark (rør intet).
|
||||||
ADCpBridge.VINE_FRUITS = {GRAPE = true, GRAPEB = true, OLIVE = true, OLIVEB = true}
|
ADCpBridge.VINE_FRUITS = {GRAPE = true, GRAPEB = true, OLIVE = true, OLIVEB = true, HOPS = true}
|
||||||
function ADCpBridge.vineOffsetFor(fruitName, role)
|
function ADCpBridge.vineOffsetFor(fruitName, role)
|
||||||
if fruitName == nil or not ADCpBridge.VINE_FRUITS[string.upper(tostring(fruitName))] then return nil end
|
if fruitName == nil or not ADCpBridge.VINE_FRUITS[string.upper(tostring(fruitName))] then return nil end
|
||||||
if role == "harvester" then return 0 end
|
if role == "harvester" then return 0 end
|
||||||
@@ -576,6 +637,7 @@ function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone, opts)
|
|||||||
Logging.warning("ADSmartPickup: kunne ikke kopiere kursus: %s — laver nyt", tostring(copyError))
|
Logging.warning("ADSmartPickup: kunne ikke kopiere kursus: %s — laver nyt", tostring(copyError))
|
||||||
end
|
end
|
||||||
setMultiTools(vehicle, opts.multiTools or 1)
|
setMultiTools(vehicle, opts.multiTools or 1)
|
||||||
|
clampCourseWidth(vehicle)
|
||||||
-- kursusberegningen kører over mange frames på store marker: moden venter længere imens
|
-- kursusberegningen kører over mange frames på store marker: moden venter længere imens
|
||||||
local function generate(headlands)
|
local function generate(headlands)
|
||||||
ADCpBridge.setHeadlands(vehicle, headlands)
|
ADCpBridge.setHeadlands(vehicle, headlands)
|
||||||
|
|||||||
@@ -28,18 +28,28 @@ function ADCpResume.fromRecords(ids)
|
|||||||
return set
|
return set
|
||||||
end
|
end
|
||||||
|
|
||||||
-- vehicles = {{id, aiActive, controlled}} i fast rækkefølge -> id'er der skal startes (nil uden gemt liste)
|
-- vehicles = {{id, aiActive, controlled, fieldWork}} i fast rækkefølge -> id'er der skal startes (nil uden gemt liste).
|
||||||
|
-- fieldWork = false: CP's HUD-job er ikke markarbejde (fx bunkersilo) — HUD-start ville starte det forkerte job
|
||||||
|
-- (in-game 29/9: "CP: No silo found"); nil = ukendt (ældre CP) -> startes som før.
|
||||||
function ADCpResume.toStart(pending, vehicles)
|
function ADCpResume.toStart(pending, vehicles)
|
||||||
if pending == nil then return nil end
|
if pending == nil then return nil end
|
||||||
local ids = {}
|
local ids = {}
|
||||||
for _, vehicle in ipairs(vehicles or {}) do
|
for _, vehicle in ipairs(vehicles or {}) do
|
||||||
if pending[vehicle.id] and not vehicle.aiActive and not vehicle.controlled then table.insert(ids, vehicle.id) end
|
if pending[vehicle.id] and not vehicle.aiActive and not vehicle.controlled and vehicle.fieldWork ~= false then
|
||||||
|
table.insert(ids, vehicle.id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
return ids
|
return ids
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ---- spil ----
|
-- ---- spil ----
|
||||||
|
|
||||||
|
-- true/false fra CP's HUD; nil når CP-versionen ikke har funktionen
|
||||||
|
local function hudFieldWork(vehicle)
|
||||||
|
if vehicle.cpIsHudFieldWorkJobSelected == nil then return nil end
|
||||||
|
return vehicle:cpIsHudFieldWorkJobSelected() == true
|
||||||
|
end
|
||||||
|
|
||||||
local function vehicleState(adEnv, vehicle)
|
local function vehicleState(adEnv, vehicle)
|
||||||
local ad = vehicle.ad
|
local ad = vehicle.ad
|
||||||
return {
|
return {
|
||||||
@@ -48,6 +58,7 @@ local function vehicleState(adEnv, vehicle)
|
|||||||
adActive = ad ~= nil and ad.stateModule ~= nil and ad.stateModule:isActive(),
|
adActive = ad ~= nil and ad.stateModule ~= nil and ad.stateModule:isActive(),
|
||||||
aiActive = vehicle.getIsAIActive ~= nil and vehicle:getIsAIActive() == true,
|
aiActive = vehicle.getIsAIActive ~= nil and vehicle:getIsAIActive() == true,
|
||||||
controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled() == true,
|
controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled() == true,
|
||||||
|
fieldWork = hudFieldWork(vehicle),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -99,7 +110,12 @@ function ADCpResume.save(adEnv)
|
|||||||
end
|
end
|
||||||
xmlFile:save()
|
xmlFile:save()
|
||||||
xmlFile:delete()
|
xmlFile:delete()
|
||||||
|
-- gemmes ved hver runs-gem (flere gange i minuttet): log kun når listen ændrer sig
|
||||||
|
local signature = table.concat(ids, ",")
|
||||||
|
if signature ~= ADCpResume.lastLogged then
|
||||||
|
ADCpResume.lastLogged = signature
|
||||||
Logging.info(ADCpResume.LOG_PREFIX .. "gemt: %d maskine(r) kører Courseplay", #ids)
|
Logging.info(ADCpResume.LOG_PREFIX .. "gemt: %d maskine(r) kører Courseplay", #ids)
|
||||||
|
end
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
-- AD Crop Plan (ren logik)
|
||||||
|
-- Mangel pr. afgrøde over 12 måneder (lager + forventet høst mod kædens behov) og valg af marker der skal skifte
|
||||||
|
-- målafgrøde: "fri til kæden" -> auto, ellers forslag. Brugerens egen målafgrøde ændres aldrig automatisk.
|
||||||
|
|
||||||
|
ADCropPlan = {}
|
||||||
|
ADCropPlan.HORIZON_MONTHS = 12
|
||||||
|
ADCropPlan.MIN_SHARE = 0.10
|
||||||
|
ADCropPlan.MIN_HA = 0.5
|
||||||
|
-- højst så mange forslag (ikke auto-skift) pr. afgrøde, så listen ikke vokser til snesevis af rækker
|
||||||
|
ADCropPlan.MAX_SUGGEST_PER_FRUIT = 3
|
||||||
|
|
||||||
|
function ADCropPlan.plannedFruit(field)
|
||||||
|
if field.targetFruit ~= nil and field.targetFruit ~= "" then return field.targetFruit end
|
||||||
|
return field.fruit
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADCropPlan.shortages(demand, stock, fields, yields)
|
||||||
|
local list = {}
|
||||||
|
for fruit, perMonth in pairs(demand or {}) do
|
||||||
|
local perHa = (yields or {})[fruit] or 0
|
||||||
|
if perHa > 0 and (perMonth or 0) > 0 then
|
||||||
|
local need = perMonth * ADCropPlan.HORIZON_MONTHS
|
||||||
|
local supply = (stock or {})[fruit] or 0
|
||||||
|
for _, field in ipairs(fields or {}) do
|
||||||
|
if ADCropPlan.plannedFruit(field) == fruit then supply = supply + (field.areaHa or 0) * perHa end
|
||||||
|
end
|
||||||
|
local missing = need - supply
|
||||||
|
local missingHa = missing / perHa
|
||||||
|
if missing > need * ADCropPlan.MIN_SHARE and missingHa > ADCropPlan.MIN_HA then
|
||||||
|
table.insert(list, {fruit = fruit, needLiters = need, supplyLiters = supply, missingLiters = missing, missingHa = missingHa})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(list, function(a, b)
|
||||||
|
if a.missingHa ~= b.missingHa then return a.missingHa > b.missingHa end
|
||||||
|
return a.fruit < b.fruit
|
||||||
|
end)
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isCandidate(field, used, dismissedFruit, fruit)
|
||||||
|
if field.permanent or field.excluded then return false end
|
||||||
|
local planned = ADCropPlan.plannedFruit(field)
|
||||||
|
if planned ~= nil and planned ~= "" and used[planned] then return false end
|
||||||
|
return dismissedFruit ~= fruit
|
||||||
|
end
|
||||||
|
|
||||||
|
-- prices: {[afgrøde] = pris} eller en funktion afgrøde -> pris (slås kun op for kandidaternes afgrøder).
|
||||||
|
-- harvesting = {[afgrøde] = true}: afgrøder der høstes lige nu — ingen forslag/skift for dem (læssene er på vej).
|
||||||
|
function ADCropPlan.choose(shortages, fields, usedFruits, yields, prices, dismissed, harvesting)
|
||||||
|
local actions, taken = {}, {}
|
||||||
|
local used = usedFruits or {}
|
||||||
|
local priceOf = type(prices) == "function" and prices or function(fruit) return (prices or {})[fruit] or 0 end
|
||||||
|
local priceCache = {}
|
||||||
|
local function valueOf(fruit)
|
||||||
|
if fruit == "" then return 0 end
|
||||||
|
if priceCache[fruit] == nil then priceCache[fruit] = ((yields or {})[fruit] or 0) * (priceOf(fruit) or 0) end
|
||||||
|
return priceCache[fruit]
|
||||||
|
end
|
||||||
|
for _, shortage in ipairs(shortages or {}) do
|
||||||
|
local remaining = (harvesting or {})[shortage.fruit] and 0 or shortage.missingHa
|
||||||
|
local candidates = {}
|
||||||
|
for _, field in ipairs(fields or {}) do
|
||||||
|
if not taken[field.id] and isCandidate(field, used, (dismissed or {})[field.id], shortage.fruit) then
|
||||||
|
local planned = ADCropPlan.plannedFruit(field) or ""
|
||||||
|
local value = valueOf(planned)
|
||||||
|
table.insert(candidates, {field = field, value = value})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(candidates, function(a, b)
|
||||||
|
if a.value ~= b.value then return a.value < b.value end
|
||||||
|
local da, db = math.abs((a.field.areaHa or 0) - remaining), math.abs((b.field.areaHa or 0) - remaining)
|
||||||
|
if da ~= db then return da < db end
|
||||||
|
return a.field.id < b.field.id
|
||||||
|
end)
|
||||||
|
local suggested = 0
|
||||||
|
for _, candidate in ipairs(candidates) do
|
||||||
|
if remaining <= 0 then break end
|
||||||
|
local field = candidate.field
|
||||||
|
local auto = field.chainFree == true and field.targetSource ~= "user"
|
||||||
|
if auto or suggested < ADCropPlan.MAX_SUGGEST_PER_FRUIT then
|
||||||
|
table.insert(actions, {fieldId = field.id, from = ADCropPlan.plannedFruit(field) or "", to = shortage.fruit,
|
||||||
|
hectares = field.areaHa or 0, reason = shortage.fruit, mode = auto and "auto" or "suggest"})
|
||||||
|
taken[field.id] = true
|
||||||
|
remaining = remaining - (field.areaHa or 0)
|
||||||
|
if not auto then suggested = suggested + 1 end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return actions
|
||||||
|
end
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
-- AD Dependency (ren logik)
|
||||||
|
-- Advarsel når spilleren slukker en produktion gården er afhængig af (spec 2026-09-30-produktionskaeder afsnit 8).
|
||||||
|
-- En vare er "nødvendig" når et tændt forsyningsflow kører den (manuel vare, eller flowets mål tager den i
|
||||||
|
-- auto-tilstand), en stald æder den, eller en aktiv kæde har den som input. Varer er NAVNE; kapacitet og forbrug
|
||||||
|
-- i samme enhed (adapteren i adChain.lua vælger), lagerdage i spillets døgn. Ingen spiladgang.
|
||||||
|
|
||||||
|
ADDependency = {}
|
||||||
|
ADDependency.STOCK_DAYS_OK = 60 -- lager til mere end så mange døgn: ingen advarsel
|
||||||
|
ADDependency.MAX_USERS = 2 -- brugere nævnt i beskeden; resten "+k"
|
||||||
|
|
||||||
|
local function contains(list, value)
|
||||||
|
for _, item in ipairs(list or {}) do if item == value then return true end end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sortedKeys(map)
|
||||||
|
local keys = {}
|
||||||
|
for key in pairs(map or {}) do table.insert(keys, key) end
|
||||||
|
table.sort(keys)
|
||||||
|
return keys
|
||||||
|
end
|
||||||
|
|
||||||
|
-- flows = {{name, kind, stopped, targetBuildingIds, ingredientMode, ingredients, chain}} (chain = kædens rodnøgle for kædens flows)
|
||||||
|
-- barnFood = {[vare] = {staldnavn...}}, chainInputs = {[vare] = {kædenavn...}}
|
||||||
|
-- accepts = {[bygnings-id] = {[vare] = true}}: hvad auto-flowenes mål tager (adapteren slår op)
|
||||||
|
-- -> {[vare] = {users = {navn...}, onlyChain, strong}} (brugere i rækkefølgen flows, stalde, kæder; hver kun én gang).
|
||||||
|
-- strong = varen er valgt som manuel vare i et flow eller er kæde-input: behovet gælder også uden kendt forbrug.
|
||||||
|
-- Stald-foder og auto-flowenes mål er "svage": uden kendt forbrug (lagerdage) er de ikke nødvendige.
|
||||||
|
function ADDependency.neededOutputs(flows, barnFood, chainInputs, accepts)
|
||||||
|
local result = {}
|
||||||
|
local function add(name, user, isChain, isStrong)
|
||||||
|
if name == nil or user == nil then return end
|
||||||
|
local entry = result[name]
|
||||||
|
if entry == nil then
|
||||||
|
entry = {users = {}, onlyChain = true, strong = false}
|
||||||
|
result[name] = entry
|
||||||
|
end
|
||||||
|
if not isChain then entry.onlyChain = false end
|
||||||
|
if isStrong then entry.strong = true end
|
||||||
|
if not contains(entry.users, user) then table.insert(entry.users, user) end
|
||||||
|
end
|
||||||
|
for _, flow in ipairs(flows or {}) do
|
||||||
|
if flow.kind ~= "outbound" and not flow.stopped then
|
||||||
|
local user = tostring(flow.name or "?")
|
||||||
|
-- kædens egne flows (flow.chain) er kæde-behov: Task 10 melder kæderne, så de gør ikke onlyChain falsk
|
||||||
|
local isChainFlow = flow.chain ~= nil and flow.chain ~= ""
|
||||||
|
if flow.ingredientMode == "manual" then
|
||||||
|
for _, name in ipairs(flow.ingredients or {}) do add(name, user, isChainFlow, true) end
|
||||||
|
else
|
||||||
|
for _, building in ipairs(flow.targetBuildingIds or {}) do
|
||||||
|
for _, name in ipairs(sortedKeys((accepts or {})[building])) do add(name, user, isChainFlow, isChainFlow) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, name in ipairs(sortedKeys(barnFood)) do
|
||||||
|
for _, barn in ipairs(barnFood[name]) do add(name, tostring(barn), false) end
|
||||||
|
end
|
||||||
|
for _, name in ipairs(sortedKeys(chainInputs)) do
|
||||||
|
for _, root in ipairs(chainInputs[name]) do add(name, tostring(root), true, true) end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Tændte produktioner der ikke er kædens (samme regel som ADChain.enabledKeys) -> {[nøgle] = true}
|
||||||
|
function ADDependency.enabledKeys(recipes)
|
||||||
|
local result = {}
|
||||||
|
for _, recipe in ipairs(recipes or {}) do
|
||||||
|
if recipe.enabled and not recipe.byChain then result[recipe.key] = true end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Tændte produktioners kapacitet pr. vare (outputs × cyklusser pr. måned) -> {[vare] = mængde}
|
||||||
|
function ADDependency.capacity(recipes)
|
||||||
|
local result = {}
|
||||||
|
for _, recipe in ipairs(recipes or {}) do
|
||||||
|
if recipe.enabled then
|
||||||
|
for name, amount in pairs(recipe.outputs or {}) do
|
||||||
|
result[name] = (result[name] or 0) + (amount or 0) * (recipe.cyclesPerMonth or 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Dækker andre tændte produktioner varen? Ukendt/intet forbrug: enhver kapacitet dækker.
|
||||||
|
local function isCovered(name, otherCapacity, demand)
|
||||||
|
local capacity = (otherCapacity or {})[name] or 0
|
||||||
|
return capacity > 0 and capacity >= ((demand or {})[name] or 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Rækker lageret længe nok? nil = ukendt (advar, uden dage).
|
||||||
|
local function stockLasts(name, stockDays)
|
||||||
|
local days = (stockDays or {})[name]
|
||||||
|
return days ~= nil and days > ADDependency.STOCK_DAYS_OK
|
||||||
|
end
|
||||||
|
|
||||||
|
-- {[bygning|vare] = true} for tændte opskrifter: spilleren har valgt hvilken opskrift i fabrikken der kører.
|
||||||
|
local function enabledInBuilding(recipes)
|
||||||
|
local result = {}
|
||||||
|
for _, recipe in ipairs(recipes or {}) do
|
||||||
|
if recipe.enabled then
|
||||||
|
for name, amount in pairs(recipe.outputs or {}) do
|
||||||
|
if (amount or 0) > 0 then result[tostring(recipe.building) .. "|" .. name] = true end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Er behovet for varen reelt? Kun kæder: nej (Task 10 melder kæderne). Svagt behov (stald-foder, auto-flowets mål)
|
||||||
|
-- kræver kendt forbrug (lagerdage); manuel flow-vare og kæde-input gælder også uden.
|
||||||
|
local function isNeeded(need, name, stockDays)
|
||||||
|
if need == nil or need.onlyChain then return false end
|
||||||
|
return need.strong == true or (stockDays or {})[name] ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Første nødvendige, udækkede vare (efter navn) for en slukket produktion (ikke kædens) -> advarsel eller nil.
|
||||||
|
local function check(recipe, needed, stockDays, otherCapacity, demand, alternatives)
|
||||||
|
if recipe.enabled or recipe.byChain then return nil end
|
||||||
|
for _, name in ipairs(sortedKeys(recipe.outputs)) do
|
||||||
|
local need = (needed or {})[name]
|
||||||
|
if isNeeded(need, name, stockDays) and (recipe.outputs[name] or 0) > 0
|
||||||
|
and not alternatives[tostring(recipe.building) .. "|" .. name]
|
||||||
|
and not isCovered(name, otherCapacity, demand) and not stockLasts(name, stockDays) then
|
||||||
|
local days = (stockDays or {})[name]
|
||||||
|
return {recipeKey = recipe.key, building = recipe.building, buildingName = recipe.buildingName, fillType = name,
|
||||||
|
users = need.users, onlyChain = need.onlyChain == true, days = days}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Alle slukkede, nødvendige produktioner (én pr. fabrik) — "Slukket men nødvendig" i Kræver handling.
|
||||||
|
function ADDependency.offNeeded(recipes, needed, stockDays, otherCapacity, demand)
|
||||||
|
local result, seen = {}, {}
|
||||||
|
local alternatives = enabledInBuilding(recipes)
|
||||||
|
for _, recipe in ipairs(recipes or {}) do
|
||||||
|
local warning = check(recipe, needed, stockDays, otherCapacity, demand, alternatives)
|
||||||
|
if warning ~= nil and not seen[tostring(warning.building)] then
|
||||||
|
seen[tostring(warning.building)] = true
|
||||||
|
table.insert(result, warning)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Fælles regler (warnings og offNeeded): ingen advarsel når en anden opskrift i SAMME fabrik laver varen og er
|
||||||
|
-- tændt, når andre tændte produktioner dækker behovet, når lageret rækker > 60 dage, eller når kun kæder bruger varen.
|
||||||
|
-- Nye slukninger siden sidste evaluering: tændt i prevEnabled, slukket nu, nødvendig og ikke dækket.
|
||||||
|
-- prevEnabled = nil (første evaluering efter indlæsning) -> ingen. Én pr. fabrik og vare.
|
||||||
|
function ADDependency.warnings(prevEnabled, recipes, needed, stockDays, otherCapacity, demand)
|
||||||
|
local result, seen = {}, {}
|
||||||
|
if prevEnabled == nil then return result end
|
||||||
|
local alternatives = enabledInBuilding(recipes)
|
||||||
|
for _, recipe in ipairs(recipes or {}) do
|
||||||
|
if prevEnabled[recipe.key] then
|
||||||
|
local warning = check(recipe, needed, stockDays, otherCapacity, demand, alternatives)
|
||||||
|
local id = warning ~= nil and (tostring(warning.building) .. "|" .. warning.fillType) or nil
|
||||||
|
if warning ~= nil and not seen[id] then
|
||||||
|
seen[id] = true
|
||||||
|
table.insert(result, warning)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- "A, B +2"
|
||||||
|
function ADDependency.usersText(users)
|
||||||
|
local shown = {}
|
||||||
|
for index = 1, math.min(#(users or {}), ADDependency.MAX_USERS) do shown[index] = tostring(users[index]) end
|
||||||
|
if #shown == 0 then return "?" end
|
||||||
|
local text = table.concat(shown, ", ")
|
||||||
|
if #users > #shown then text = text .. " +" .. tostring(#users - #shown) end
|
||||||
|
return text
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Beskedtekst (spu_dep_warn med dage, spu_dep_warnNoDays uden).
|
||||||
|
function ADDependency.notifyText(warning, textOf, titleOf)
|
||||||
|
local title = tostring(titleOf ~= nil and titleOf(warning.fillType) or warning.fillType)
|
||||||
|
local users = ADDependency.usersText(warning.users)
|
||||||
|
local building = tostring(warning.buildingName or warning.building or "?")
|
||||||
|
local days = warning.days
|
||||||
|
if days ~= nil and days < math.huge then
|
||||||
|
return string.format(textOf("spu_dep_warn"), building, title, users, math.max(0, math.floor(days)))
|
||||||
|
end
|
||||||
|
return string.format(textOf("spu_dep_warnNoDays"), building, title, users)
|
||||||
|
end
|
||||||
@@ -146,7 +146,7 @@ end
|
|||||||
|
|
||||||
local PLAN_VALUES = {
|
local PLAN_VALUES = {
|
||||||
targetFruit = true, sow = true, rake = true, mulch = true, lime = true, roll = true,
|
targetFruit = true, sow = true, rake = true, mulch = true, lime = true, roll = true,
|
||||||
fertilize = true, weed = true, straw = true, soil = true, rigs = true, unloaders = true,
|
fertilize = true, weed = true, straw = true, chainFree = true, soil = true, rigs = true, unloaders = true,
|
||||||
}
|
}
|
||||||
|
|
||||||
function ADFieldGuide.choose(guide, id, value)
|
function ADFieldGuide.choose(guide, id, value)
|
||||||
@@ -206,6 +206,7 @@ local function rowsCrop(guide)
|
|||||||
if target(guide.draft) == "" and info(guide).kind ~= "mowable" then
|
if target(guide.draft) == "" and info(guide).kind ~= "mowable" then
|
||||||
table.insert(rows, toggle("sow", L("spu_guide_crop_sowAgain"), plan.sow))
|
table.insert(rows, toggle("sow", L("spu_guide_crop_sowAgain"), plan.sow))
|
||||||
end
|
end
|
||||||
|
table.insert(rows, toggle("chainFree", L("spu_field_chainFree"), plan.chainFree))
|
||||||
return rows
|
return rows
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -442,6 +443,8 @@ end
|
|||||||
function ADFieldGuide.result(guide, start)
|
function ADFieldGuide.result(guide, start)
|
||||||
local draft = guide.draft
|
local draft = guide.draft
|
||||||
local plan = ADGuide.deepCopy(draft.plan)
|
local plan = ADGuide.deepCopy(draft.plan)
|
||||||
|
-- kilden ejes af setTarget; kladdens gamle værdi må ikke overskrive den
|
||||||
|
plan.targetSource = nil
|
||||||
local t = plan.targetFruit or ""
|
local t = plan.targetFruit or ""
|
||||||
if t ~= "" then
|
if t ~= "" then
|
||||||
plan.mode = "auto"
|
plan.mode = "auto"
|
||||||
|
|||||||
@@ -309,8 +309,8 @@ function ADFieldJobs.forceReady(id)
|
|||||||
ADFieldJobs.setField(ADFieldModel.forceReady(ADFieldJobs.getField(id)))
|
ADFieldJobs.setField(ADFieldModel.forceReady(ADFieldJobs.getField(id)))
|
||||||
end
|
end
|
||||||
|
|
||||||
function ADFieldJobs.setTarget(id, fruitName)
|
function ADFieldJobs.setTarget(id, fruitName, source)
|
||||||
ADFieldJobs.setField(ADFieldModel.setTarget(ADFieldJobs.getField(id), fruitName))
|
ADFieldJobs.setField(ADFieldModel.setTarget(ADFieldJobs.getField(id), fruitName, source))
|
||||||
ADFieldJobs.log(id, "mål-afgrøde: %s", fruitName ~= "" and fruitName or "ingen")
|
ADFieldJobs.log(id, "mål-afgrøde: %s", fruitName ~= "" and fruitName or "ingen")
|
||||||
ADFieldJobs.save()
|
ADFieldJobs.save()
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ local DEFAULT_PLAN = {
|
|||||||
mode = "manual", product = "auto", rake = false, rigs = 1, unloaders = 1,
|
mode = "manual", product = "auto", rake = false, rigs = 1, unloaders = 1,
|
||||||
mulch = false, lime = true, soil = "auto", sow = false, sowFruit = "", roll = false,
|
mulch = false, lime = true, soil = "auto", sow = false, sowFruit = "", roll = false,
|
||||||
fertilize = true, weed = true, targetFruit = "", prepAfterMow = false, straw = "chop",
|
fertilize = true, weed = true, targetFruit = "", prepAfterMow = false, straw = "chop",
|
||||||
|
-- kæde: marken er frigivet til produktionskæden; hvem der valgte målafgrøden ("user"|"chain"|"")
|
||||||
|
chainFree = false, targetSource = "",
|
||||||
}
|
}
|
||||||
local STRAW_VALUES = {chop = true, collect = true, bale = true}
|
local STRAW_VALUES = {chop = true, collect = true, bale = true}
|
||||||
-- halm-trin efter høst (mejetærskeren lægger skår når halmen skal bruges)
|
-- halm-trin efter høst (mejetærskeren lægger skår når halmen skal bruges)
|
||||||
@@ -158,9 +160,11 @@ end
|
|||||||
|
|
||||||
-- Vælg mål-afgrøde fra menuen ("" = ingen). Med mål kører marken automatisk; en færdig/klar mark sættes
|
-- Vælg mål-afgrøde fra menuen ("" = ingen). Med mål kører marken automatisk; en færdig/klar mark sættes
|
||||||
-- til venter, så den kan starte forfra. Uden mål: tilbage til manuel.
|
-- til venter, så den kan starte forfra. Uden mål: tilbage til manuel.
|
||||||
function ADFieldModel.setTarget(field, fruitName)
|
function ADFieldModel.setTarget(field, fruitName, source)
|
||||||
local copy = copyField(field)
|
local copy = copyField(field)
|
||||||
copy.plan.targetFruit = fruitName or ""
|
copy.plan.targetFruit = fruitName or ""
|
||||||
|
-- kilden huskes kun mens der er et mål; standard er at spilleren valgte det
|
||||||
|
copy.plan.targetSource = copy.plan.targetFruit ~= "" and (source or "user") or ""
|
||||||
copy.held = nil
|
copy.held = nil
|
||||||
copy.plan.mode = copy.plan.targetFruit ~= "" and "auto" or "manual"
|
copy.plan.mode = copy.plan.targetFruit ~= "" and "auto" or "manual"
|
||||||
if not ADFieldModel.isWorking(field) and field.stage == "done" then
|
if not ADFieldModel.isWorking(field) and field.stage == "done" then
|
||||||
@@ -376,13 +380,18 @@ ADFieldModel.MIN_WORK_WIDTH = 1.5
|
|||||||
-- roller uden eget kursus (følger høsteren / samler baller): ingen arbejdsbredde
|
-- roller uden eget kursus (følger høsteren / samler baller): ingen arbejdsbredde
|
||||||
ADFieldModel.NO_WIDTH_ROLES = {unloader = true, baleCollector = true}
|
ADFieldModel.NO_WIDTH_ROLES = {unloader = true, baleCollector = true}
|
||||||
|
|
||||||
|
-- Bredden kurset laves med: højst CP's maksimum.
|
||||||
|
function ADFieldModel.effectiveWidth(width)
|
||||||
|
if width == nil then return nil end
|
||||||
|
return math.min(width, ADFieldModel.CP_MAX_WIDTH)
|
||||||
|
end
|
||||||
|
|
||||||
function ADFieldModel.checkWidth(role, width, requiredWidth)
|
function ADFieldModel.checkWidth(role, width, requiredWidth)
|
||||||
if width < ADFieldModel.MIN_WORK_WIDTH and not ADFieldModel.NO_WIDTH_ROLES[role] then
|
if width < ADFieldModel.MIN_WORK_WIDTH and not ADFieldModel.NO_WIDTH_ROLES[role] then
|
||||||
return false, "widthUnknown", {width}
|
return false, "widthUnknown", {width}
|
||||||
end
|
end
|
||||||
if width > ADFieldModel.CP_MAX_WIDTH + ADFieldModel.WIDTH_TOLERANCE then
|
-- bredere end CP's 50 m (Unrealistic kan sættes til 120 m): bruges; kurset begrænses til 50 m (brugerønske 30/9)
|
||||||
return false, "tooWide", {width, ADFieldModel.CP_MAX_WIDTH}
|
width = ADFieldModel.effectiveWidth(width)
|
||||||
end
|
|
||||||
if role ~= "mower" and requiredWidth ~= nil and width + ADFieldModel.WIDTH_TOLERANCE < requiredWidth then
|
if role ~= "mower" and requiredWidth ~= nil and width + ADFieldModel.WIDTH_TOLERANCE < requiredWidth then
|
||||||
return false, "tooNarrow", {width, requiredWidth}
|
return false, "tooNarrow", {width, requiredWidth}
|
||||||
end
|
end
|
||||||
@@ -463,6 +472,7 @@ function ADFieldModel.toRecord(field)
|
|||||||
soil = plan.soil, sow = flag(plan.sow), sowFruit = plan.sowFruit or "", roll = flag(plan.roll),
|
soil = plan.soil, sow = flag(plan.sow), sowFruit = plan.sowFruit or "", roll = flag(plan.roll),
|
||||||
fertilize = flag(plan.fertilize), weed = flag(plan.weed),
|
fertilize = flag(plan.fertilize), weed = flag(plan.weed),
|
||||||
targetFruit = plan.targetFruit or "", prepAfterMow = flag(plan.prepAfterMow), straw = plan.straw or "chop",
|
targetFruit = plan.targetFruit or "", prepAfterMow = flag(plan.prepAfterMow), straw = plan.straw or "chop",
|
||||||
|
chainFree = flag(plan.chainFree), targetSource = plan.targetSource or "",
|
||||||
kind = field.kind or "", lastFruit = field.lastFruit or "", care = table.concat(care, " "),
|
kind = field.kind or "", lastFruit = field.lastFruit or "", care = table.concat(care, " "),
|
||||||
mowWidth = field.mowWidth or 0,
|
mowWidth = field.mowWidth or 0,
|
||||||
mowVehicle = field.mowVehicleId or "",
|
mowVehicle = field.mowVehicleId or "",
|
||||||
@@ -503,6 +513,9 @@ function ADFieldModel.fromRecord(record)
|
|||||||
plan.weed = readFlag(record.weed, true)
|
plan.weed = readFlag(record.weed, true)
|
||||||
plan.targetFruit = record.targetFruit or ""
|
plan.targetFruit = record.targetFruit or ""
|
||||||
plan.prepAfterMow = readFlag(record.prepAfterMow, false)
|
plan.prepAfterMow = readFlag(record.prepAfterMow, false)
|
||||||
|
plan.chainFree = readFlag(record.chainFree, false)
|
||||||
|
-- gamle saves: et eksisterende mål er valgt af spilleren
|
||||||
|
plan.targetSource = record.targetSource or (plan.targetFruit ~= "" and "user" or "")
|
||||||
plan.straw = STRAW_VALUES[record.straw] and record.straw or "chop"
|
plan.straw = STRAW_VALUES[record.straw] and record.straw or "chop"
|
||||||
field.kind = readText(record.kind)
|
field.kind = readText(record.kind)
|
||||||
field.lastFruit = readText(record.lastFruit)
|
field.lastFruit = readText(record.lastFruit)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ ADFieldStorage.FIELD_TYPES = {
|
|||||||
rigs = "Int", unloaders = "Int", mulch = "Int", lime = "Int", soil = "String", sow = "Int",
|
rigs = "Int", unloaders = "Int", mulch = "Int", lime = "Int", soil = "String", sow = "Int",
|
||||||
sowFruit = "String", roll = "Int", fertilize = "Int", weed = "Int",
|
sowFruit = "String", roll = "Int", fertilize = "Int", weed = "Int",
|
||||||
kind = "String", lastFruit = "String", care = "String", targetFruit = "String", prepAfterMow = "Int", straw = "String",
|
kind = "String", lastFruit = "String", care = "String", targetFruit = "String", prepAfterMow = "Int", straw = "String",
|
||||||
|
chainFree = "Int", targetSource = "String",
|
||||||
mowWidth = "Float", mowVehicle = "String", rakeVehicle = "String", harvestVehicle = "String", windrowOffset = "String",
|
mowWidth = "Float", mowVehicle = "String", rakeVehicle = "String", harvestVehicle = "String", windrowOffset = "String",
|
||||||
dedicated = "String",
|
dedicated = "String",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ function ADFieldWork.getRigs(adEnv)
|
|||||||
manual = (state.manual or {})[id] == true,
|
manual = (state.manual or {})[id] == true,
|
||||||
width = ADCpBridge.getWorkWidth(vehicle), x = x, z = z,
|
width = ADCpBridge.getWorkWidth(vehicle), x = x, z = z,
|
||||||
-- tom spreder kan flere ting (gødning og kalk): alle dens roller
|
-- tom spreder kan flere ting (gødning og kalk): alle dens roller
|
||||||
roles = specs.sprayRoles or ADFieldFleet.rolesFor(role), filledRole = specs.filledRole, dumpable = specs.dumpable,
|
roles = ADPool.rolesOf(role, specs.sprayRoles), filledRole = specs.filledRole, dumpable = specs.dumpable,
|
||||||
}
|
}
|
||||||
if role == "harvester" then rig.fruits = ADCpBridge.getHarvestFruits(vehicle) or {} end
|
if role == "harvester" then rig.fruits = ADCpBridge.getHarvestFruits(vehicle) or {} end
|
||||||
if role == "seeder" then
|
if role == "seeder" then
|
||||||
@@ -379,6 +379,10 @@ local function refillPlan(adEnv, fieldId, vehicle, role, x, z)
|
|||||||
if fillType == nil then return nil end
|
if fillType == nil then return nil end
|
||||||
local empty = ADCpBridge.getRefillLevel(vehicle, fillType) <= 0
|
local empty = ADCpBridge.getRefillLevel(vehicle, fillType) <= 0
|
||||||
local refillId = empty and ADFieldWork.findRefillMarker(adEnv, vehicle, fillType, x, z) or nil
|
local refillId = empty and ADFieldWork.findRefillMarker(adEnv, vehicle, fillType, x, z) or nil
|
||||||
|
if refillId ~= nil then
|
||||||
|
local marker = adEnv.ADGraphManager:getMapMarkerByWayPointId(refillId)
|
||||||
|
log(fieldId, "'%s' er tom: fyldes med %s ved %s", ADRuns.vehicleLabel(vehicle), fillTypeName(fillType), tostring(marker ~= nil and marker.name or refillId))
|
||||||
|
end
|
||||||
return {fillType = fillType, empty = empty, refillId = refillId}
|
return {fillType = fillType, empty = empty, refillId = refillId}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -604,11 +608,65 @@ function ADFieldWork.buyingSupports(station, fillType)
|
|||||||
return ok and result == true
|
return ok and result == true
|
||||||
end
|
end
|
||||||
|
|
||||||
-- AD-markøren ved stationen: AutoDrives eget opslag (30 m fra triggeren), ellers nærmeste ikke-Wait
|
-- Parkering, kobling og ventepladser er aldrig en læssemarkør.
|
||||||
-- markør inden for 40 m af bygningen ("Liquid Buy Station" står ved tanken, ikke ved triggeren).
|
local function isServiceMarker(marker)
|
||||||
|
local name = string.lower(tostring(marker.name or ""))
|
||||||
|
local group = string.lower(tostring(marker.group or ""))
|
||||||
|
return string.find(name, "^p ") ~= nil or string.find(name, "^connect ") ~= nil
|
||||||
|
or string.find(name, "%f[%a]wait%f[%A]") ~= nil or string.find(group, "%f[%a]wait%f[%A]") ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- markers = {{id, name, group, x, z}}, points = triggerpositioner {{x, z}} -> id på nærmeste læssemarkør inden for
|
||||||
|
-- maxDistance m af en af triggerne, ellers nil.
|
||||||
|
function ADFieldWork.nearestStationMarker(markers, points, maxDistance)
|
||||||
|
local best, bestDistance = nil, maxDistance * maxDistance
|
||||||
|
for _, marker in ipairs(markers or {}) do
|
||||||
|
if not isServiceMarker(marker) then
|
||||||
|
for _, point in ipairs(points or {}) do
|
||||||
|
local d = (marker.x - point.x) ^ 2 + (marker.z - point.z) ^ 2
|
||||||
|
if d < bestDistance then best, bestDistance = marker.id, d end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return best
|
||||||
|
end
|
||||||
|
|
||||||
|
ADFieldWork.STATION_MARKER_M = 30
|
||||||
|
|
||||||
|
-- Stationens læssetriggere i verden (buying-/loading-station) -> {{x, z}}.
|
||||||
|
local function loadTriggerPoints(station)
|
||||||
|
local points = {}
|
||||||
|
for _, trigger in pairs(station.loadTriggers or {}) do
|
||||||
|
local node = trigger.triggerNode or trigger.aiNode
|
||||||
|
if node ~= nil then
|
||||||
|
local ok, x, _, z = pcall(getWorldTranslation, node)
|
||||||
|
if ok and x ~= nil then table.insert(points, {x = x, z = z}) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return points
|
||||||
|
end
|
||||||
|
|
||||||
|
local function markerList(adEnv)
|
||||||
|
local list = {}
|
||||||
|
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||||||
|
local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id)
|
||||||
|
if wayPoint ~= nil then
|
||||||
|
table.insert(list, {id = marker.id, name = marker.name, group = marker.group, x = wayPoint.x, z = wayPoint.z})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
|
||||||
|
-- AD-markøren ved stationen: den nærmeste markør ved stationens EGNE læssetriggere, ellers nærmeste ikke-Wait markør
|
||||||
|
-- inden for 40 m af bygningen ("Liquid Buy Station" står ved tanken, ikke ved triggeren). AutoDrives
|
||||||
|
-- ADTriggerManager:getMarkerAtStation bruges IKKE: den vil have markøren togets længde + 3..13 m forbi triggeren (et
|
||||||
|
-- langt sæt fik en anden stations markør: Puma + Kinze 4900 ventede på frø ved "Market Price Liquid Fill Station"), og
|
||||||
|
-- finder den ingen, OPRETTER den en ny markør med stationens navn (2 ekstra "Buying Station Multifruit", in-game 30/9).
|
||||||
local function markerAtStation(adEnv, vehicle, station, placeable)
|
local function markerAtStation(adEnv, vehicle, station, placeable)
|
||||||
local okAd, wayPointId = pcall(function() return adEnv.ADTriggerManager:getMarkerAtStation(station, vehicle, 30) end)
|
local okOwn, ownId = pcall(function()
|
||||||
if okAd and wayPointId ~= nil and wayPointId > 0 then return wayPointId end
|
return ADFieldWork.nearestStationMarker(markerList(adEnv), loadTriggerPoints(station), ADFieldWork.STATION_MARKER_M)
|
||||||
|
end)
|
||||||
|
if okOwn and ownId ~= nil then return ownId end
|
||||||
local node = placeable ~= nil and placeable.rootNode or nil
|
local node = placeable ~= nil and placeable.rootNode or nil
|
||||||
if node == nil then return nil end
|
if node == nil then return nil end
|
||||||
local px, _, pz = getWorldTranslation(node)
|
local px, _, pz = getWorldTranslation(node)
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ ADFields.MARKER_MAX_DISTANCE = 400
|
|||||||
ADFields.cache = {}
|
ADFields.cache = {}
|
||||||
ADFields.order = {}
|
ADFields.order = {}
|
||||||
ADFields.scanIndex = 0
|
ADFields.scanIndex = 0
|
||||||
|
-- {[markId] = true}: marker hvis scanning er afsluttet (med resultat) eller fejlede — til isScanComplete
|
||||||
|
ADFields.scanFailed = {}
|
||||||
|
ADFields.ownedDone = false
|
||||||
ADFields.mowable = nil
|
ADFields.mowable = nil
|
||||||
-- markliste og F-markører ændrer sig sjældent: slås op igen efter så lang tid (ikke hvert scan)
|
-- markliste og F-markører ændrer sig sjældent: slås op igen efter så lang tid (ikke hvert scan)
|
||||||
ADFields.OWNED_REFRESH_MS = 30000
|
ADFields.OWNED_REFRESH_MS = 30000
|
||||||
@@ -104,7 +107,7 @@ end
|
|||||||
|
|
||||||
-- Vin og oliven høstes med egen høster — aldrig slåafgrøder, selv om et mod har sat dem på MOWER-listen
|
-- Vin og oliven høstes med egen høster — aldrig slåafgrøder, selv om et mod har sat dem på MOWER-listen
|
||||||
-- (in-game 29/9: F22 hvide druer fik "Silage + så" og en skårlægger). Samme liste som ADCpBridge.VINE_FRUITS.
|
-- (in-game 29/9: F22 hvide druer fik "Silage + så" og en skårlægger). Samme liste som ADCpBridge.VINE_FRUITS.
|
||||||
ADFields.VINE_FRUITS = {GRAPE = true, GRAPEB = true, OLIVE = true, OLIVEB = true}
|
ADFields.VINE_FRUITS = {GRAPE = true, GRAPEB = true, OLIVE = true, OLIVEB = true, HOPS = true}
|
||||||
|
|
||||||
-- fruitName + indgangen fra getMowable() (nil = ikke på listen) -> slåbar?
|
-- fruitName + indgangen fra getMowable() (nil = ikke på listen) -> slåbar?
|
||||||
function ADFields.isMowableFruit(fruitName, mowableEntry)
|
function ADFields.isMowableFruit(fruitName, mowableEntry)
|
||||||
@@ -291,6 +294,17 @@ function ADFields.shapeSignature(verts)
|
|||||||
roundTo(cx, 1), roundTo(cz, 1))
|
roundTo(cx, 1), roundTo(cz, 1))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Markens areal i hektar (shoelace på {x,z}-polygonen, /10000); 0 uden en flade (under 3 hjørner).
|
||||||
|
function ADFields.polygonAreaHa(verts)
|
||||||
|
if type(verts) ~= "table" or #verts < 3 then return 0 end
|
||||||
|
local n, twiceArea = #verts, 0
|
||||||
|
for index = 1, n do
|
||||||
|
local a, b = verts[index], verts[index % n + 1]
|
||||||
|
twiceArea = twiceArea + a.x * b.z - b.x * a.z
|
||||||
|
end
|
||||||
|
return math.abs(twiceArea) / 2 / 10000
|
||||||
|
end
|
||||||
|
|
||||||
-- Hjørnernes gennemsnit (nil uden hjørner).
|
-- Hjørnernes gennemsnit (nil uden hjørner).
|
||||||
function ADFields.polygonCenter(verts)
|
function ADFields.polygonCenter(verts)
|
||||||
if verts == nil or #verts == 0 then return nil end
|
if verts == nil or #verts == 0 then return nil end
|
||||||
@@ -555,9 +569,20 @@ local function finishOwnedScan(scan)
|
|||||||
if not ids[id] then ADFields.cache[id] = nil end
|
if not ids[id] then ADFields.cache[id] = nil end
|
||||||
end
|
end
|
||||||
ADFields.order = scan.result
|
ADFields.order = scan.result
|
||||||
|
ADFields.ownedDone = true
|
||||||
ADFields.listCache = nil
|
ADFields.listCache = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Har scanningen nået alle ejede marker mindst én gang (resultat i cachen, eller scanningen fejlede)?
|
||||||
|
-- Indtil da er marklisten ufuldstændig (efter indlæsning fyldes den ~én mark pr. SCAN_INTERVAL_MS).
|
||||||
|
function ADFields.isScanComplete()
|
||||||
|
if not ADFields.ownedDone then return false end
|
||||||
|
for _, entry in ipairs(ADFields.order) do
|
||||||
|
if ADFields.cache[entry.id] == nil and not ADFields.scanFailed[entry.id] then return false end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
-- Kaldes HVERT frame: kører én opgave af den aktuelle scanning. En ny mark påbegyndes højst hvert
|
-- Kaldes HVERT frame: kører én opgave af den aktuelle scanning. En ny mark påbegyndes højst hvert
|
||||||
-- SCAN_INTERVAL_MS; marklisten opdateres hvert OWNED_REFRESH_MS.
|
-- SCAN_INTERVAL_MS; marklisten opdateres hvert OWNED_REFRESH_MS.
|
||||||
ADFields.SCAN_INTERVAL_MS = 2000
|
ADFields.SCAN_INTERVAL_MS = 2000
|
||||||
@@ -601,6 +626,7 @@ function ADFields.scanStep(adEnv, farmId, now)
|
|||||||
local ok, err = pcall(task)
|
local ok, err = pcall(task)
|
||||||
if not ok then
|
if not ok then
|
||||||
Logging.warning("ADSmartPickup: F%s kunne ikke scannes: %s", tostring(scan.id or "-"), tostring(err))
|
Logging.warning("ADSmartPickup: F%s kunne ikke scannes: %s", tostring(scan.id or "-"), tostring(err))
|
||||||
|
if scan.id ~= nil then ADFields.scanFailed[scan.id] = true end
|
||||||
ADFields.activeScan = nil
|
ADFields.activeScan = nil
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -617,7 +643,10 @@ function ADFields.scanStep(adEnv, farmId, now)
|
|||||||
else
|
else
|
||||||
ok, err = pcall(finishScan, adEnv, scan)
|
ok, err = pcall(finishScan, adEnv, scan)
|
||||||
end
|
end
|
||||||
if not ok then Logging.warning("ADSmartPickup: F%s scanning kunne ikke afsluttes: %s", tostring(scan.id or "-"), tostring(err)) end
|
if not ok then
|
||||||
|
Logging.warning("ADSmartPickup: F%s scanning kunne ikke afsluttes: %s", tostring(scan.id or "-"), tostring(err))
|
||||||
|
if scan.id ~= nil then ADFields.scanFailed[scan.id] = true end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Samme liste indtil næste scanning (list() kaldes hvert sekund; ingen ny tabel hver gang).
|
-- Samme liste indtil næste scanning (list() kaldes hvert sekund; ingen ny tabel hver gang).
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ end
|
|||||||
local RUN_FIELDS = {
|
local RUN_FIELDS = {
|
||||||
"id", "name", "kind", "sourceBuildingIds", "targetBuildingIds", "storeBuildingIds", "products",
|
"id", "name", "kind", "sourceBuildingIds", "targetBuildingIds", "storeBuildingIds", "products",
|
||||||
"ingredientMode", "ingredients", "sellAtPercent", "storeAbovePercent", "sellAbovePercent", "collectRound", "loops",
|
"ingredientMode", "ingredients", "sellAtPercent", "storeAbovePercent", "sellAbovePercent", "collectRound", "loops",
|
||||||
"returnBelowPercent", "waitPoolGroup", "vehicleIds", "wagons",
|
"returnBelowPercent", "waitPoolGroup", "vehicleIds", "wagons", "chain",
|
||||||
}
|
}
|
||||||
|
|
||||||
local function draftFrom(run, isNew)
|
local function draftFrom(run, isNew)
|
||||||
@@ -339,7 +339,12 @@ local function rowsRules(guide)
|
|||||||
if isOut(guide) then
|
if isOut(guide) then
|
||||||
local sells, stores = ADFlowView.policyUse(productPolicies(draft))
|
local sells, stores = ADFlowView.policyUse(productPolicies(draft))
|
||||||
local rows = {}
|
local rows = {}
|
||||||
if sells then table.insert(rows, choice("sellAtPercent", L("spu_sellAt"), percentOptions(ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT, 5), draft.sellAtPercent)) end
|
if sells then
|
||||||
|
-- "Altid" (0) øverst: sælg uanset pris (in-game 30/9)
|
||||||
|
local options = {{label = L("spu_sellAt_always"), value = 0}}
|
||||||
|
for _, option in ipairs(percentOptions(ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT, 5)) do table.insert(options, option) end
|
||||||
|
table.insert(rows, choice("sellAtPercent", L("spu_sellAt"), options, draft.sellAtPercent))
|
||||||
|
end
|
||||||
if stores then table.insert(rows, choice("storeAbovePercent", L("spu_storeAbove"), percentOptions(ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT, 5), draft.storeAbovePercent)) end
|
if stores then table.insert(rows, choice("storeAbovePercent", L("spu_storeAbove"), percentOptions(ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT, 5), draft.storeAbovePercent)) end
|
||||||
if sells then table.insert(rows, choice("sellAbovePercent", L("spu_sellAbove"), sellAboveOptions(), draft.sellAbovePercent)) end
|
if sells then table.insert(rows, choice("sellAbovePercent", L("spu_sellAbove"), sellAboveOptions(), draft.sellAbovePercent)) end
|
||||||
table.insert(rows, toggle("collectRound", L("spu_collectRound"), draft.collectRound))
|
table.insert(rows, toggle("collectRound", L("spu_collectRound"), draft.collectRound))
|
||||||
@@ -489,7 +494,7 @@ local SUMMARY = {
|
|||||||
if isOut(guide) then
|
if isOut(guide) then
|
||||||
local sells, stores = ADFlowView.policyUse(productPolicies(draft))
|
local sells, stores = ADFlowView.policyUse(productPolicies(draft))
|
||||||
local parts = {}
|
local parts = {}
|
||||||
if sells then table.insert(parts, L("spu_guide_flow_sumSellAt", {draft.sellAtPercent})) end
|
if sells then table.insert(parts, draft.sellAtPercent == 0 and L("spu_guide_flow_sumSellAlways") or L("spu_guide_flow_sumSellAt", {draft.sellAtPercent})) end
|
||||||
if stores then table.insert(parts, L("spu_guide_flow_sumStoreAbove", {draft.storeAbovePercent})) end
|
if stores then table.insert(parts, L("spu_guide_flow_sumStoreAbove", {draft.storeAbovePercent})) end
|
||||||
if sells and (draft.sellAbovePercent or 0) > 0 then table.insert(parts, L("spu_guide_flow_sumSellAbove", {draft.sellAbovePercent})) end
|
if sells and (draft.sellAbovePercent or 0) > 0 then table.insert(parts, L("spu_guide_flow_sumSellAbove", {draft.sellAbovePercent})) end
|
||||||
if draft.collectRound then table.insert(parts, L("spu_guide_flow_sumRound")) end
|
if draft.collectRound then table.insert(parts, L("spu_guide_flow_sumRound")) end
|
||||||
|
|||||||
@@ -52,20 +52,61 @@ end
|
|||||||
|
|
||||||
-- Tavlen: forsyning ("Ind til bygning") først, så udkørsel ("Ud fra bygning"); hver gruppe efter navn.
|
-- Tavlen: forsyning ("Ind til bygning") først, så udkørsel ("Ud fra bygning"); hver gruppe efter navn.
|
||||||
-- {{kind, runs}, ...}; tomme grupper udelades.
|
-- {{kind, runs}, ...}; tomme grupper udelades.
|
||||||
|
-- Spillerens egne flows først (forsyning, udkørsel), så kædernes egne flows (run.chain) samlet til sidst
|
||||||
|
-- (in-game 30/9: "Kæde: …"-flows blandede sig alfabetisk ind mellem de flows spilleren selv har sat op).
|
||||||
function ADFlowView.boardGroups(runs)
|
function ADFlowView.boardGroups(runs)
|
||||||
local byKind = {supply = {}, outbound = {}}
|
local byKind = {supply = {}, outbound = {}}
|
||||||
|
local chainRuns = {}
|
||||||
for _, run in ipairs(runs or {}) do
|
for _, run in ipairs(runs or {}) do
|
||||||
|
if run.chain ~= nil and run.chain ~= "" then
|
||||||
|
table.insert(chainRuns, run)
|
||||||
|
else
|
||||||
table.insert(byKind[run.kind == "outbound" and "outbound" or "supply"], run)
|
table.insert(byKind[run.kind == "outbound" and "outbound" or "supply"], run)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
local groups = {}
|
local groups = {}
|
||||||
for _, kind in ipairs({"supply", "outbound"}) do
|
for _, kind in ipairs({"supply", "outbound"}) do
|
||||||
if #byKind[kind] > 0 then
|
if #byKind[kind] > 0 then
|
||||||
table.insert(groups, {kind = kind, runs = sortedBy(byKind[kind], "name")})
|
table.insert(groups, {kind = kind, chain = false, runs = sortedBy(byKind[kind], "name")})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
if #chainRuns > 0 then table.insert(groups, {kind = "supply", chain = true, runs = sortedBy(chainRuns, "name")}) end
|
||||||
return groups
|
return groups
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Kort maskinnavn til lister: " Unrealistic" og parenteser som "(+ Custom Crops)" fjernes (lange navne blev ombrudt
|
||||||
|
-- med traktorens nummer alene på næste linje, in-game 30/9). Bliver intet tilbage, bruges det fulde navn.
|
||||||
|
function ADFlowView.shortVehicleName(name)
|
||||||
|
local full = tostring(name or "")
|
||||||
|
local short = full:gsub("%s*%b()", ""):gsub("%s+[Uu]nrealistic", ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||||
|
if short == "" then return full end
|
||||||
|
return short
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Kædeflow uden traktor (venter på en ledig traktor/vogn).
|
||||||
|
function ADFlowView.isIdleChain(run)
|
||||||
|
return run.chain ~= nil and run.chain ~= "" and #(run.vehicleIds or {}) == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Kædeflows uden traktor foldes sammen i listen (de venter på en ledig traktor/vogn). -> active, idle
|
||||||
|
function ADFlowView.splitIdle(runs)
|
||||||
|
local active, idle = {}, {}
|
||||||
|
for _, run in ipairs(runs or {}) do
|
||||||
|
table.insert(#(run.vehicleIds or {}) > 0 and active or idle, run)
|
||||||
|
end
|
||||||
|
return active, idle
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Navn i listen: kædeflows står under overskriften "Kædernes egne flows", så "Kæde: " fjernes (ellers skjuler
|
||||||
|
-- afkortningen hvilken bygning det gælder).
|
||||||
|
function ADFlowView.listName(run)
|
||||||
|
local name = tostring(run.name or "")
|
||||||
|
if run.chain ~= nil and run.chain ~= "" then
|
||||||
|
name = name:gsub("^Kæde:%s*", ""):gsub("^Chain:%s*", "")
|
||||||
|
end
|
||||||
|
return name
|
||||||
|
end
|
||||||
|
|
||||||
-- Samme rækkefølge som tavlen, som én liste (flow-vælgeren).
|
-- Samme rækkefølge som tavlen, som én liste (flow-vælgeren).
|
||||||
function ADFlowView.orderedRuns(runs)
|
function ADFlowView.orderedRuns(runs)
|
||||||
local result = {}
|
local result = {}
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ function ADFlowWagons.remember(wagons, tractorId, wagonId)
|
|||||||
return copy
|
return copy
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Må redskabet bag traktoren huskes som flowets vogn? Kun noget der kan rumme last — ikke plov/kultivator (ingen
|
||||||
|
-- plads) og ikke såmaskine (frøtank). s = {capacity, isSowingMachine}. in-game 30/9: 'Sell Slurry 2' huskede ploven.
|
||||||
|
function ADFlowWagons.isRememberableWagon(s)
|
||||||
|
return (s.capacity or 0) > 0 and not s.isSowingMachine
|
||||||
|
end
|
||||||
|
|
||||||
-- s = {hasRear, wagonId, wagonExists, hasConnect, wagonAttached, wagonBusy} -> "start" | "fetch" | "wait"
|
-- s = {hasRear, wagonId, wagonExists, hasConnect, wagonAttached, wagonBusy} -> "start" | "fetch" | "wait"
|
||||||
function ADFlowWagons.startAction(s)
|
function ADFlowWagons.startAction(s)
|
||||||
-- lånt ud af puljen (markarbejde, marksilo, på hjemtur): vent til den er fri
|
-- lånt ud af puljen (markarbejde, marksilo, på hjemtur): vent til den er fri
|
||||||
@@ -453,11 +459,16 @@ end
|
|||||||
|
|
||||||
-- Husk vognen bag traktoren som flowets (også når flowet lige er stoppet — rememberAll ser kun kørende flows).
|
-- Husk vognen bag traktoren som flowets (også når flowet lige er stoppet — rememberAll ser kun kørende flows).
|
||||||
-- in-game 29/9: Milk Store's tanke blev sat af ved Stop alt uden at være husket -> 'noTrailer' ved næste start.
|
-- in-game 29/9: Milk Store's tanke blev sat af ved Stop alt uden at være husket -> 'noTrailer' ved næste start.
|
||||||
|
local function isCargoWagon(object)
|
||||||
|
local _, capacity = ADPool.cargoOf(object)
|
||||||
|
return ADFlowWagons.isRememberableWagon({capacity = capacity, isSowingMachine = object.spec_sowingMachine ~= nil})
|
||||||
|
end
|
||||||
|
|
||||||
local function rememberWagonOf(vehicle)
|
local function rememberWagonOf(vehicle)
|
||||||
local run = ADRunsController.getRunForVehicle(vehicle)
|
local run = ADRunsController.getRunForVehicle(vehicle)
|
||||||
local rear = run ~= nil and ADHitch.rearImplementsOf(vehicle) or {}
|
local rear = run ~= nil and ADHitch.rearImplementsOf(vehicle) or {}
|
||||||
local tractorId = ADRunsController.getVehicleId(vehicle)
|
local tractorId = ADRunsController.getVehicleId(vehicle)
|
||||||
local wagonId = rear[1] ~= nil and ADRunsController.getVehicleId(rear[1].object) or nil
|
local wagonId = rear[1] ~= nil and isCargoWagon(rear[1].object) and ADRunsController.getVehicleId(rear[1].object) or nil
|
||||||
if wagonId == nil or tractorId == nil then return end
|
if wagonId == nil or tractorId == nil then return end
|
||||||
local updated = ADFlowWagons.remember(run.wagons, tractorId, wagonId)
|
local updated = ADFlowWagons.remember(run.wagons, tractorId, wagonId)
|
||||||
if updated ~= run.wagons then
|
if updated ~= run.wagons then
|
||||||
@@ -551,6 +562,15 @@ function ADFlowWagons.isEmptying(vehicle)
|
|||||||
return emptying[vehicle] ~= nil
|
return emptying[vehicle] ~= nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Kun læsning (statusvinduet): hvad flowets vogn-logik har traktoren i gang med -> "fetching"|"waiting"|"emptying"|"homing"|nil
|
||||||
|
function ADFlowWagons.statusOf(vehicle)
|
||||||
|
if fetching[vehicle] ~= nil then return "fetching" end
|
||||||
|
if waiting[vehicle] ~= nil then return "waiting" end
|
||||||
|
if emptying[vehicle] ~= nil then return "emptying" end
|
||||||
|
if homing[vehicle] ~= nil then return "homing" end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
function ADFlowWagons.sendHome(adEnv, run)
|
function ADFlowWagons.sendHome(adEnv, run)
|
||||||
for _, vehicleId in ipairs(run.vehicleIds or {}) do
|
for _, vehicleId in ipairs(run.vehicleIds or {}) do
|
||||||
local vehicle = ADRunsController.findVehicleById(adEnv, vehicleId)
|
local vehicle = ADRunsController.findVehicleById(adEnv, vehicleId)
|
||||||
@@ -615,7 +635,7 @@ local function rememberAll(adEnv)
|
|||||||
for _, vehicleId in ipairs(run.vehicleIds or {}) do
|
for _, vehicleId in ipairs(run.vehicleIds or {}) do
|
||||||
local vehicle = ADRunsController.findVehicleById(adEnv, vehicleId)
|
local vehicle = ADRunsController.findVehicleById(adEnv, vehicleId)
|
||||||
local rear = vehicle ~= nil and ADHitch.rearImplementsOf(vehicle) or {}
|
local rear = vehicle ~= nil and ADHitch.rearImplementsOf(vehicle) or {}
|
||||||
local wagonId = rear[1] ~= nil and ADRunsController.getVehicleId(rear[1].object) or nil
|
local wagonId = rear[1] ~= nil and isCargoWagon(rear[1].object) and ADRunsController.getVehicleId(rear[1].object) or nil
|
||||||
if wagonId ~= nil then
|
if wagonId ~= nil then
|
||||||
local updated = ADFlowWagons.remember(run.wagons, vehicleId, wagonId)
|
local updated = ADFlowWagons.remember(run.wagons, vehicleId, wagonId)
|
||||||
if updated ~= run.wagons then
|
if updated ~= run.wagons then
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ ADHitch.MARKER_PREFIXES = {
|
|||||||
}
|
}
|
||||||
ADHitch.TIMEOUT_REASON = {attach = "noAttachable", detach = "nothingToDetach", arrive = "notArrived"}
|
ADHitch.TIMEOUT_REASON = {attach = "noAttachable", detach = "nothingToDetach", arrive = "notArrived"}
|
||||||
ADHitch.ARRIVE_DISTANCE = 5 -- m fra markøren, AD standser traktoren tæt på waypointet
|
ADHitch.ARRIVE_DISTANCE = 5 -- m fra markøren, AD standser traktoren tæt på waypointet
|
||||||
|
ADHitch.DETACH_ARRIVE_DISTANCE = 8 -- afkobling: langt redskab (valse) står med roden >5 m inde i båsen
|
||||||
ADHitch.STILL_SPEED = 1 -- km/t
|
ADHitch.STILL_SPEED = 1 -- km/t
|
||||||
ADHitch.START_GRACE_MS = 3000 -- så længe må AD være om at komme i gang efter fetch/drop
|
ADHitch.START_GRACE_MS = 3000 -- så længe må AD være om at komme i gang efter fetch/drop
|
||||||
ADHitch.ACT_TIMEOUT_MS = 8000 -- så længe forsøges kobling efter ankomst
|
ADHitch.ACT_TIMEOUT_MS = 8000 -- så længe forsøges kobling efter ankomst
|
||||||
@@ -100,6 +101,14 @@ function ADHitch.findMarker(markers, name)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Hvor tæt på markøren sættet skal være: afkobling ved egen parkering godtager længere redskaber (in-game 29/9:
|
||||||
|
-- valsen stod 5 m inde i båsen -> 'stoppedAway'); efter bak hen til vognen udvides koblingsafstanden.
|
||||||
|
function ADHitch.arriveDistanceFor(action, crept)
|
||||||
|
if action == "detach" then return ADHitch.DETACH_ARRIVE_DISTANCE end
|
||||||
|
if crept then return ADHitch.ARRIVE_DISTANCE + ADHitch.CREPT_EXTRA_DISTANCE end
|
||||||
|
return ADHitch.ARRIVE_DISTANCE
|
||||||
|
end
|
||||||
|
|
||||||
-- state = {adActive, distance, speed, arriveDistance?} (arriveDistance: udvidet, når traktoren har bakket hen til vognen)
|
-- state = {adActive, distance, speed, arriveDistance?} (arriveDistance: udvidet, når traktoren har bakket hen til vognen)
|
||||||
local function arriveDistanceOf(state)
|
local function arriveDistanceOf(state)
|
||||||
return state.arriveDistance or ADHitch.ARRIVE_DISTANCE
|
return state.arriveDistance or ADHitch.ARRIVE_DISTANCE
|
||||||
@@ -525,7 +534,7 @@ function ADHitch.tick(adEnv, dt)
|
|||||||
if not succeeded then job.lastReason = detail end
|
if not succeeded then job.lastReason = detail end
|
||||||
end
|
end
|
||||||
local state = vehicleState(adEnv, vehicle, job.markerId)
|
local state = vehicleState(adEnv, vehicle, job.markerId)
|
||||||
if job.crept then state.arriveDistance = ADHitch.ARRIVE_DISTANCE + ADHitch.CREPT_EXTRA_DISTANCE end
|
state.arriveDistance = ADHitch.arriveDistanceFor(job.action, job.crept)
|
||||||
ADHitch.step(job, state, elapsed, succeeded)
|
ADHitch.step(job, state, elapsed, succeeded)
|
||||||
if job.state == "done" then
|
if job.state == "done" then
|
||||||
finish(vehicle, job, true, detail, adEnv)
|
finish(vehicle, job, true, detail, adEnv)
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
-- AD Load Diag
|
||||||
|
-- Diagnose når AutoDrive står ved et LOAD-mål uden at fylde (in-game 30/9: Fendt + MEGA-sprøjte ved "Market Price
|
||||||
|
-- Liquid Fill Station"). AutoDrive fylder kun, når en fyldenhed står i triggerens boks (trigger.fillableObjects) og
|
||||||
|
-- triggeren har varen. Efter REPORT_MS ved målet uden fremgang skrives én linje: markør, vare, og for hver vogn den
|
||||||
|
-- nærmeste AD-læssetrigger (afstand, har varen?, står vognen i boksen?). Kun når en trigger med varen er inden for
|
||||||
|
-- NEAR_M (holder ved Wait-punkter logges ikke). Ingen indgreb — kun log.
|
||||||
|
|
||||||
|
ADLoadDiag = {}
|
||||||
|
ADLoadDiag.LOG_PREFIX = "ADSmartPickup: pålæsning: "
|
||||||
|
ADLoadDiag.REPORT_MS = 30000
|
||||||
|
ADLoadDiag.NEAR_M = 20 -- m: trigger med varen herinden for = køretøjet står ved stationen (ellers Wait/mark: ingen log)
|
||||||
|
|
||||||
|
-- ---- ren logik ----
|
||||||
|
|
||||||
|
-- state (pr. opgave), dt, reached (ved målet), level (liter på vognene) -> true når der skal rapporteres (én gang pr.
|
||||||
|
-- ankomst). Fremgang eller kørsel nulstiller.
|
||||||
|
function ADLoadDiag.tick(state, dt, reached, level)
|
||||||
|
if not reached then
|
||||||
|
state.ms, state.reported, state.level = 0, false, nil
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if state.level == nil or level > state.level then
|
||||||
|
state.level, state.ms = level, 0
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
state.ms = (state.ms or 0) + (dt or 0)
|
||||||
|
if state.reported or state.ms < ADLoadDiag.REPORT_MS then return false end
|
||||||
|
state.reported = true
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- distances = afstande fra vognene til nærmeste AD-trigger med varen -> står ved stationen?
|
||||||
|
function ADLoadDiag.atStation(distances)
|
||||||
|
for _, d in ipairs(distances or {}) do
|
||||||
|
if d <= ADLoadDiag.NEAR_M then return true end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- spil ----
|
||||||
|
|
||||||
|
local function fillTypeName(fillType)
|
||||||
|
return g_fillTypeManager ~= nil and fillType ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or tostring(fillType)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function inBox(trigger, trailer)
|
||||||
|
for key, entry in pairs(trigger.fillableObjects or {}) do
|
||||||
|
if key == trailer or (type(entry) == "table" and entry.object == trailer) then return true end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function describeTrailer(adEnv, trailer, fillType)
|
||||||
|
local node = trailer.components ~= nil and trailer.components[1] ~= nil and trailer.components[1].node or trailer.rootNode
|
||||||
|
if node == nil then return nil end
|
||||||
|
local tx, _, tz = getWorldTranslation(node)
|
||||||
|
local nearest, nearestOk = nil, nil
|
||||||
|
for _, trigger in pairs(adEnv.ADTriggerManager.getLoadTriggers() or {}) do
|
||||||
|
local x, _, z = adEnv.ADTriggerManager.getTriggerPos(trigger)
|
||||||
|
if x ~= nil and (x ~= 0 or z ~= 0) then
|
||||||
|
local d = math.sqrt((x - tx) ^ 2 + (z - tz) ^ 2)
|
||||||
|
local ok = adEnv.ADTriggerManager.triggerSupportsFillType(trigger, fillType)
|
||||||
|
if nearest == nil or d < nearest.d then nearest = {d = d, ok = ok, trigger = trigger} end
|
||||||
|
if ok and (nearestOk == nil or d < nearestOk.d) then nearestOk = {d = d, trigger = trigger} end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local name = trailer.getName ~= nil and trailer:getName() or "?"
|
||||||
|
local parts = {}
|
||||||
|
if nearest ~= nil then
|
||||||
|
table.insert(parts, string.format("nærmeste AD-trigger %.1f m (%s varen, %s)", nearest.d, nearest.ok and "har" or "UDEN",
|
||||||
|
inBox(nearest.trigger, trailer) and "vognen i boksen" or "vognen IKKE i boksen"))
|
||||||
|
end
|
||||||
|
if nearestOk ~= nil and (nearest == nil or nearestOk.trigger ~= nearest.trigger) then
|
||||||
|
table.insert(parts, string.format("nærmeste med varen %.1f m (%s)", nearestOk.d,
|
||||||
|
inBox(nearestOk.trigger, trailer) and "i boksen" or "ikke i boksen"))
|
||||||
|
end
|
||||||
|
if nearestOk == nil then table.insert(parts, "INGEN AD-læssetrigger med varen") end
|
||||||
|
return string.format("'%s': %s", tostring(name), table.concat(parts, ", ")), nearestOk ~= nil and nearestOk.d or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function report(adEnv, task)
|
||||||
|
local vehicle = task.vehicle
|
||||||
|
local fillType = vehicle.ad.stateModule.getFillType ~= nil and vehicle.ad.stateModule:getFillType() or nil
|
||||||
|
local marker = task.destinationID ~= nil and adEnv.ADGraphManager:getMapMarkerByWayPointId(task.destinationID) or nil
|
||||||
|
local lines, distances = {}, {}
|
||||||
|
for _, trailer in ipairs(task.trailers or {}) do
|
||||||
|
if trailer.getFillUnits ~= nil and #trailer:getFillUnits() > 0 then
|
||||||
|
local ok, line, distance = pcall(describeTrailer, adEnv, trailer, fillType)
|
||||||
|
if ok and line ~= nil then table.insert(lines, line) end
|
||||||
|
if ok and distance ~= nil then table.insert(distances, distance) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- holder ved Wait/mark (ingen trigger med varen tæt på): det er ikke en fejlet pålæsning
|
||||||
|
if not ADLoadDiag.atStation(distances) then return end
|
||||||
|
local maxDistance = adEnv.AutoDrive.getMaxTriggerDistance ~= nil and adEnv.AutoDrive.getMaxTriggerDistance(vehicle) or nil
|
||||||
|
Logging.warning(ADLoadDiag.LOG_PREFIX .. "'%s' står ved '%s' uden at fylde %s i %d s (AD-triggerafstand %s m): %s",
|
||||||
|
tostring(ADRuns.vehicleLabel(vehicle)), tostring(marker ~= nil and marker.name or task.destinationID), fillTypeName(fillType),
|
||||||
|
math.floor(ADLoadDiag.REPORT_MS / 1000), tostring(maxDistance), #lines > 0 and table.concat(lines, "; ") or "ingen vogne med fyldenheder")
|
||||||
|
end
|
||||||
|
|
||||||
|
local states = setmetatable({}, {__mode = "k"})
|
||||||
|
|
||||||
|
function ADLoadDiag.after(adEnv, task, dt)
|
||||||
|
local vehicle = task.vehicle
|
||||||
|
if vehicle == nil or vehicle.ad == nil or vehicle.ad.drivePathModule == nil then return end
|
||||||
|
local state = states[task]
|
||||||
|
if state == nil then
|
||||||
|
state = {}
|
||||||
|
states[task] = state
|
||||||
|
end
|
||||||
|
local reached = vehicle.ad.drivePathModule:isTargetReached() and not vehicle.ad.trailerModule:isActiveAtTrigger()
|
||||||
|
local level = adEnv.AutoDrive.getAllFillLevels(task.trailers) or 0
|
||||||
|
if ADLoadDiag.tick(state, dt, reached, level) then report(adEnv, task) end
|
||||||
|
end
|
||||||
|
|
||||||
|
local isInstalled = false
|
||||||
|
|
||||||
|
function ADLoadDiag.install(adEnv)
|
||||||
|
local task = adEnv ~= nil and adEnv.LoadAtDestinationTask or nil
|
||||||
|
if isInstalled or task == nil or task.update == nil or adEnv.ADTriggerManager == nil then return end
|
||||||
|
isInstalled = true
|
||||||
|
local original = task.update
|
||||||
|
task.update = function(self, dt)
|
||||||
|
local result = original(self, dt)
|
||||||
|
local ok, err = pcall(ADLoadDiag.after, adEnv, self, dt)
|
||||||
|
if not ok and not self.spuDiagFailed then
|
||||||
|
self.spuDiagFailed = true
|
||||||
|
Logging.warning(ADLoadDiag.LOG_PREFIX .. "diagnose fejlede: %s", tostring(err))
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -660,6 +660,13 @@ end
|
|||||||
|
|
||||||
-- Objektlager: én palle ad gangen — spawn, vent til lageret er én mindre OG vognen har én
|
-- Objektlager: én palle ad gangen — spawn, vent til lageret er én mindre OG vognen har én
|
||||||
-- mere, gentag; skift vare når lageret er tomt for den aktuelle; stop ved fuld vogn.
|
-- mere, gentag; skift vare når lageret er tomt for den aktuelle; stop ved fuld vogn.
|
||||||
|
-- Palleskur: moden tager ét objekt ud ad gangen og afslutter selv pålæsningen (fuld / tomt lager / 30 s uden
|
||||||
|
-- udlevering). Imens holdes AD's egen pålæsning, ellers kører AD, så snart triggeren et øjeblik er tom (in-game 30/9:
|
||||||
|
-- Sell Egg kørte halvfuld). Først når AD har aktiveret UAL (det gør AD selv ved ankomst).
|
||||||
|
function ADOutbound.holdForSpawn(sourceKind, done, ualActive)
|
||||||
|
return sourceKind == "objectStorage" and not done and ualActive == true
|
||||||
|
end
|
||||||
|
|
||||||
local function updateSpawn(adEnv, state, task, source, dt)
|
local function updateSpawn(adEnv, state, task, source, dt)
|
||||||
local vehicle = task.vehicle
|
local vehicle = task.vehicle
|
||||||
-- AD aktiverede selv UAL med turens første vare; kun ved vareskift genaktiveres den.
|
-- AD aktiverede selv UAL med turens første vare; kun ved vareskift genaktiveres den.
|
||||||
@@ -676,7 +683,7 @@ local function updateSpawn(adEnv, state, task, source, dt)
|
|||||||
if stored < spawn.pending.stored and (loaded > spawn.pending.loaded or level > spawn.pending.level) then
|
if stored < spawn.pending.stored and (loaded > spawn.pending.loaded or level > spawn.pending.level) then
|
||||||
spawn.pending = nil
|
spawn.pending = nil
|
||||||
elseif spawn.pending.ms > ADOutbound.SPAWN_STALL_MS then
|
elseif spawn.pending.ms > ADOutbound.SPAWN_STALL_MS then
|
||||||
warnOnce(adEnv, vehicle, "spawn", string.format("'%s': objektlageret %s spawner ikke %s (30 s uden fremdrift); kører med det der er", tostring(ADRuns.vehicleLabel(vehicle)), tostring(source.name), fillTypeName(fillType)))
|
warnOnce(adEnv, vehicle, "spawn", string.format("'%s': objektlageret %s spawner ikke %s (30 s uden fremdrift: lager %d -> %d, vogn %d -> %d objekter); kører med det der er", tostring(ADRuns.vehicleLabel(vehicle)), tostring(source.name), fillTypeName(fillType), spawn.pending.stored, stored, spawn.pending.loaded, loaded))
|
||||||
state.done = true
|
state.done = true
|
||||||
return
|
return
|
||||||
else
|
else
|
||||||
@@ -763,6 +770,7 @@ function ADOutbound.beforeLoadUpdate(adEnv, task, dt)
|
|||||||
task:finished()
|
task:finished()
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
if ADOutbound.holdForSpawn(source.kind, state.done, task.activatedUALLoading) then return true end
|
||||||
end
|
end
|
||||||
local level = adEnv.AutoDrive.getAllFillLevels(task.trailers) or 0
|
local level = adEnv.AutoDrive.getAllFillLevels(task.trailers) or 0
|
||||||
if state.lastFill == nil or level > state.lastFill then
|
if state.lastFill == nil or level > state.lastFill then
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ function ADOutboundPlanner.evaluate(product, input)
|
|||||||
storagePercent = storagePercent,
|
storagePercent = storagePercent,
|
||||||
storageHigh = storagePercent >= (input.storeAbovePercent or 80),
|
storageHigh = storagePercent >= (input.storeAbovePercent or 80),
|
||||||
percent = percent,
|
percent = percent,
|
||||||
priceOk = percent ~= nil and percent >= (input.sellAtPercent or 90),
|
-- sælg fra 0 % = "Altid": uanset pris (også uden prisdata)
|
||||||
|
priceOk = input.sellAtPercent == 0 or (percent ~= nil and percent >= (input.sellAtPercent or 90)),
|
||||||
-- "sælg altid over X %": kilden er så fuld, at der sælges uanset pris
|
-- "sælg altid over X %": kilden er så fuld, at der sælges uanset pris
|
||||||
overflow = (input.sellAbovePercent or 0) > 0 and storagePercent >= input.sellAbovePercent,
|
overflow = (input.sellAbovePercent or 0) > 0 and storagePercent >= input.sellAbovePercent,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,13 @@ function ADPool.roleOf(specs)
|
|||||||
return role
|
return role
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Ekstra roller for et redskab: sprøjtens roller (gødning/kalk/ukrudt) — men ikke for en såmaskine med
|
||||||
|
-- gødningstank (Kinze 4905), den er kun såmaskine (in-game 30/9: hentet til ren gødning).
|
||||||
|
function ADPool.rolesOf(role, sprayRoles)
|
||||||
|
if role ~= "seeder" and sprayRoles ~= nil then return sprayRoles end
|
||||||
|
return ADFieldFleet.rolesFor(role)
|
||||||
|
end
|
||||||
|
|
||||||
-- Butiksdata er enten et tal eller en tabel pr. konfiguration: største tal, ellers nil.
|
-- Butiksdata er enten et tal eller en tabel pr. konfiguration: største tal, ellers nil.
|
||||||
function ADPool.numberOf(value)
|
function ADPool.numberOf(value)
|
||||||
if type(value) == "number" then return value end
|
if type(value) == "number" then return value end
|
||||||
@@ -402,7 +409,7 @@ function ADPool.collect(adEnv, isBusy)
|
|||||||
local carries, capacity = ADPool.cargoOf(vehicle)
|
local carries, capacity = ADPool.cargoOf(vehicle)
|
||||||
table.insert(implements, {carries = carries, capacity = capacity,
|
table.insert(implements, {carries = carries, capacity = capacity,
|
||||||
id = ADRunsController.getVehicleId(vehicle), vehicle = vehicle, name = ADRuns.vehicleLabel(vehicle),
|
id = ADRunsController.getVehicleId(vehicle), vehicle = vehicle, name = ADRuns.vehicleLabel(vehicle),
|
||||||
role = role, roles = specs.sprayRoles or ADFieldFleet.rolesFor(role), x = x, z = z, width = ADCpBridge.getWorkWidth(vehicle),
|
role = role, roles = ADPool.rolesOf(role, specs.sprayRoles), x = x, z = z, width = ADCpBridge.getWorkWidth(vehicle),
|
||||||
neededHp = neededHpOf(vehicle), connectId = connect.id, parkId = park.id,
|
neededHp = neededHpOf(vehicle), connectId = connect.id, parkId = park.id,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ function ADProfiler.install()
|
|||||||
{ADSources, "getFarmInventory", "gårdens lagre"},
|
{ADSources, "getFarmInventory", "gårdens lagre"},
|
||||||
{ADWaitPool, "hasRouteTo", "rutetjek"},
|
{ADWaitPool, "hasRouteTo", "rutetjek"},
|
||||||
{ADWaitPool, "hasRouteBothWays", "rutetjek frem+tilbage"},
|
{ADWaitPool, "hasRouteBothWays", "rutetjek frem+tilbage"},
|
||||||
|
{ADChain, "evaluate", "kæde"},
|
||||||
|
{ADChainData, "collect", "kæde: læs gården"},
|
||||||
}
|
}
|
||||||
for _, target in ipairs(targets) do
|
for _, target in ipairs(targets) do
|
||||||
ADProfiler.wrap(target[1], target[2], target[3])
|
ADProfiler.wrap(target[1], target[2], target[3])
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ local function copyRun(run)
|
|||||||
vehicleIds = copyList(run.vehicleIds),
|
vehicleIds = copyList(run.vehicleIds),
|
||||||
stopped = run.stopped == true,
|
stopped = run.stopped == true,
|
||||||
wagons = ADRuns.copyWagons(run.wagons),
|
wagons = ADRuns.copyWagons(run.wagons),
|
||||||
|
chain = run.chain or "",
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -145,6 +146,7 @@ function ADRuns.createRun(store, name, kind)
|
|||||||
loops = 0,
|
loops = 0,
|
||||||
returnBelowPercent = ADRuns.DEFAULT_RETURN_BELOW_PERCENT,
|
returnBelowPercent = ADRuns.DEFAULT_RETURN_BELOW_PERCENT,
|
||||||
vehicleIds = {},
|
vehicleIds = {},
|
||||||
|
chain = "",
|
||||||
}
|
}
|
||||||
new.nextId = new.nextId + 1
|
new.nextId = new.nextId + 1
|
||||||
table.insert(new.runs, run)
|
table.insert(new.runs, run)
|
||||||
@@ -200,10 +202,12 @@ function ADRuns.updateRun(store, runId, changes)
|
|||||||
if changes.targetBuildingIds ~= nil then run.targetBuildingIds = copyList(changes.targetBuildingIds) end
|
if changes.targetBuildingIds ~= nil then run.targetBuildingIds = copyList(changes.targetBuildingIds) end
|
||||||
if changes.storeBuildingIds ~= nil then run.storeBuildingIds = copyList(changes.storeBuildingIds) end
|
if changes.storeBuildingIds ~= nil then run.storeBuildingIds = copyList(changes.storeBuildingIds) end
|
||||||
if changes.products ~= nil then run.products = copyProducts(changes.products) end
|
if changes.products ~= nil then run.products = copyProducts(changes.products) end
|
||||||
if changes.sellAtPercent ~= nil then run.sellAtPercent = clampRange(changes.sellAtPercent, ADRuns.DEFAULT_SELL_AT_PERCENT, ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT) end
|
-- 0 = "Altid" (sælg uanset pris); ellers 50-100 %
|
||||||
|
if changes.sellAtPercent ~= nil then run.sellAtPercent = tonumber(changes.sellAtPercent) == 0 and 0 or clampRange(changes.sellAtPercent, ADRuns.DEFAULT_SELL_AT_PERCENT, ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT) end
|
||||||
if changes.storeAbovePercent ~= nil then run.storeAbovePercent = clampRange(changes.storeAbovePercent, ADRuns.DEFAULT_STORE_ABOVE_PERCENT, ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT) end
|
if changes.storeAbovePercent ~= nil then run.storeAbovePercent = clampRange(changes.storeAbovePercent, ADRuns.DEFAULT_STORE_ABOVE_PERCENT, ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT) end
|
||||||
if changes.sellAbovePercent ~= nil then run.sellAbovePercent = clampRange(changes.sellAbovePercent, ADRuns.DEFAULT_SELL_ABOVE_PERCENT, ADRuns.MIN_SELL_ABOVE_PERCENT, ADRuns.MAX_SELL_ABOVE_PERCENT) end
|
if changes.sellAbovePercent ~= nil then run.sellAbovePercent = clampRange(changes.sellAbovePercent, ADRuns.DEFAULT_SELL_ABOVE_PERCENT, ADRuns.MIN_SELL_ABOVE_PERCENT, ADRuns.MAX_SELL_ABOVE_PERCENT) end
|
||||||
if changes.collectRound ~= nil then run.collectRound = changes.collectRound == true end
|
if changes.collectRound ~= nil then run.collectRound = changes.collectRound == true end
|
||||||
|
if changes.chain ~= nil then run.chain = tostring(changes.chain) end
|
||||||
return new
|
return new
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -440,6 +444,7 @@ function ADRuns.toRecords(store)
|
|||||||
collectRound = run.collectRound and 1 or 0,
|
collectRound = run.collectRound and 1 or 0,
|
||||||
stopped = run.stopped and 1 or 0,
|
stopped = run.stopped and 1 or 0,
|
||||||
wagons = ADRuns.serializeWagons(run.wagons),
|
wagons = ADRuns.serializeWagons(run.wagons),
|
||||||
|
chain = run.chain or "",
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
return records
|
return records
|
||||||
@@ -480,7 +485,7 @@ function ADRuns.fromRecords(records)
|
|||||||
targetBuildingIds = parseIds(record.targetBuildings),
|
targetBuildingIds = parseIds(record.targetBuildings),
|
||||||
storeBuildingIds = parseIds(record.storeBuildings),
|
storeBuildingIds = parseIds(record.storeBuildings),
|
||||||
products = ADRuns.parseProducts(record.products),
|
products = ADRuns.parseProducts(record.products),
|
||||||
sellAtPercent = clampRange(record.sellAt, ADRuns.DEFAULT_SELL_AT_PERCENT, ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT),
|
sellAtPercent = tonumber(record.sellAt) == 0 and 0 or clampRange(record.sellAt, ADRuns.DEFAULT_SELL_AT_PERCENT, ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT),
|
||||||
storeAbovePercent = clampRange(record.storeAbove, ADRuns.DEFAULT_STORE_ABOVE_PERCENT, ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT),
|
storeAbovePercent = clampRange(record.storeAbove, ADRuns.DEFAULT_STORE_ABOVE_PERCENT, ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT),
|
||||||
sellAbovePercent = clampRange(record.sellAbove, ADRuns.DEFAULT_SELL_ABOVE_PERCENT, ADRuns.MIN_SELL_ABOVE_PERCENT, ADRuns.MAX_SELL_ABOVE_PERCENT),
|
sellAbovePercent = clampRange(record.sellAbove, ADRuns.DEFAULT_SELL_ABOVE_PERCENT, ADRuns.MIN_SELL_ABOVE_PERCENT, ADRuns.MAX_SELL_ABOVE_PERCENT),
|
||||||
collectRound = tonumber(record.collectRound) == 1,
|
collectRound = tonumber(record.collectRound) == 1,
|
||||||
@@ -494,6 +499,7 @@ function ADRuns.fromRecords(records)
|
|||||||
vehicleIds = copyList(record.vehicleIds),
|
vehicleIds = copyList(record.vehicleIds),
|
||||||
stopped = tonumber(record.stopped) == 1,
|
stopped = tonumber(record.stopped) == 1,
|
||||||
wagons = ADRuns.parseWagons(record.wagons),
|
wagons = ADRuns.parseWagons(record.wagons),
|
||||||
|
chain = record.chain or "",
|
||||||
})
|
})
|
||||||
store.nextId = math.max(store.nextId, id + 1)
|
store.nextId = math.max(store.nextId, id + 1)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1476,6 +1476,9 @@ function ADRunsController.save()
|
|||||||
pcall(ADFieldJobs.save)
|
pcall(ADFieldJobs.save)
|
||||||
end
|
end
|
||||||
if ADFieldSilos ~= nil then pcall(ADFieldSilos.save) end
|
if ADFieldSilos ~= nil then pcall(ADFieldSilos.save) end
|
||||||
|
-- kædens tilstand (tændte mellemled, pausede kæder, afviste forslag) — samme gem-regel som navnefilen
|
||||||
|
if ADChain ~= nil and ADChain.save ~= nil then pcall(ADChain.save) end
|
||||||
|
if ADSellBlock ~= nil and ADSellBlock.save ~= nil then pcall(ADSellBlock.save) end
|
||||||
-- navnefilen skal skrives under spillets gem: FS25 genskriver savegame-mappen, og filer ingen skriver
|
-- navnefilen skal skrives under spillets gem: FS25 genskriver savegame-mappen, og filer ingen skriver
|
||||||
-- under gemningen forsvinder (in-game 27/9: ADSmartPickup_names.xml blev slettet ved første gem)
|
-- under gemningen forsvinder (in-game 27/9: ADSmartPickup_names.xml blev slettet ved første gem)
|
||||||
if ADNames ~= nil and ADNames.byId ~= nil then pcall(ADNames.save) end
|
if ADNames ~= nil and ADNames.byId ~= nil then pcall(ADNames.save) end
|
||||||
@@ -1495,6 +1498,8 @@ function ADRunsController.load(adEnv)
|
|||||||
if ADNames ~= nil then pcall(ADNames.load) end
|
if ADNames ~= nil then pcall(ADNames.load) end
|
||||||
pcall(ADRunsController.migrateBuildings, adEnv)
|
pcall(ADRunsController.migrateBuildings, adEnv)
|
||||||
if ADFieldSilos ~= nil then pcall(ADFieldSilos.load) end
|
if ADFieldSilos ~= nil then pcall(ADFieldSilos.load) end
|
||||||
|
if ADChain ~= nil and ADChain.load ~= nil then pcall(ADChain.load) end
|
||||||
|
if ADSellBlock ~= nil and ADSellBlock.load ~= nil then pcall(ADSellBlock.load) end
|
||||||
if ADCpResume ~= nil then pcall(ADCpResume.load) end
|
if ADCpResume ~= nil then pcall(ADCpResume.load) end
|
||||||
local vehiclesById = getFarmVehiclesById(adEnv)
|
local vehiclesById = getFarmVehiclesById(adEnv)
|
||||||
for _, run in ipairs(store.runs) do
|
for _, run in ipairs(store.runs) do
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ function ADRunsStorage.writeRecords(xmlFile, records)
|
|||||||
-- maskinpulje: flowet stoppet + husket vogn pr. traktor
|
-- maskinpulje: flowet stoppet + husket vogn pr. traktor
|
||||||
xmlFile:setInt(key .. "#stopped", record.stopped or 0)
|
xmlFile:setInt(key .. "#stopped", record.stopped or 0)
|
||||||
xmlFile:setString(key .. "#wagons", record.wagons or "")
|
xmlFile:setString(key .. "#wagons", record.wagons or "")
|
||||||
|
xmlFile:setString(key .. "#chain", record.chain or "")
|
||||||
for vehicleIndex, vehicleId in ipairs(record.vehicleIds) do
|
for vehicleIndex, vehicleId in ipairs(record.vehicleIds) do
|
||||||
xmlFile:setString(string.format("%s.vehicle(%d)#id", key, vehicleIndex - 1), vehicleId)
|
xmlFile:setString(string.format("%s.vehicle(%d)#id", key, vehicleIndex - 1), vehicleId)
|
||||||
end
|
end
|
||||||
@@ -82,6 +83,7 @@ function ADRunsStorage.readRecords(xmlFile)
|
|||||||
collectRound = xmlFile:getInt(key .. "#collectRound"),
|
collectRound = xmlFile:getInt(key .. "#collectRound"),
|
||||||
stopped = xmlFile:getInt(key .. "#stopped"),
|
stopped = xmlFile:getInt(key .. "#stopped"),
|
||||||
wagons = xmlFile:getString(key .. "#wagons"),
|
wagons = xmlFile:getString(key .. "#wagons"),
|
||||||
|
chain = xmlFile:getString(key .. "#chain"),
|
||||||
vehicleIds = {},
|
vehicleIds = {},
|
||||||
}
|
}
|
||||||
xmlFile:iterate(key .. ".vehicle", function(_, vehicleKey)
|
xmlFile:iterate(key .. ".vehicle", function(_, vehicleKey)
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
-- AD Sell Block
|
||||||
|
-- Salgssteder spilleren har slået fra (in-game 30/9: "Market Price Liquid Fill Station" fra FarmFillStations gik i stå
|
||||||
|
-- ved aflæsning af gylle). Konsol: spuSellBlock viser salgsstederne; spuSellBlock <del af navn> slår dem fra/til.
|
||||||
|
-- Udkørsel (ADSources.findSellMarkers) og priser (ADSources.getMarketQuote) springer udelukkede over. Gemmes pr.
|
||||||
|
-- savegame i ADSmartPickup_sellblock.xml (skrives fra ADRunsController.save, som de andre filer).
|
||||||
|
|
||||||
|
ADSellBlock = {}
|
||||||
|
ADSellBlock.FILE_NAME = "ADSmartPickup_sellblock.xml"
|
||||||
|
ADSellBlock.ROOT = "sellBlock"
|
||||||
|
ADSellBlock.blocked = {} -- {[stationId] = visningsnavn}
|
||||||
|
|
||||||
|
-- ---- ren logik ----
|
||||||
|
|
||||||
|
-- stations = {{id, names = {...}}}, query = del af et navn (små/store bogstaver ligegyldige) -> matchende stationer
|
||||||
|
function ADSellBlock.match(stations, query)
|
||||||
|
local wanted = string.lower(tostring(query or ""))
|
||||||
|
local hits = {}
|
||||||
|
if wanted == "" then return hits end
|
||||||
|
for _, station in ipairs(stations or {}) do
|
||||||
|
for _, name in ipairs(station.names or {}) do
|
||||||
|
if string.find(string.lower(tostring(name)), wanted, 1, true) ~= nil then
|
||||||
|
table.insert(hits, station)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return hits
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Ny tabel: hver station skiftes (fra -> til, til -> fra). Første navn huskes til visning.
|
||||||
|
function ADSellBlock.toggle(blocked, stations)
|
||||||
|
local result = {}
|
||||||
|
for id, name in pairs(blocked or {}) do result[id] = name end
|
||||||
|
for _, station in ipairs(stations or {}) do
|
||||||
|
if result[station.id] ~= nil then
|
||||||
|
result[station.id] = nil
|
||||||
|
else
|
||||||
|
result[station.id] = tostring((station.names or {})[1] or station.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- spil ----
|
||||||
|
|
||||||
|
local function stationId(station)
|
||||||
|
local placeable = station ~= nil and station.owningPlaceable or nil
|
||||||
|
if placeable ~= nil and placeable.uniqueId ~= nil then return tostring(placeable.uniqueId) end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADSellBlock.isBlocked(station)
|
||||||
|
local id = stationId(station)
|
||||||
|
return id ~= nil and ADSellBlock.blocked[id] ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function stationNames(station)
|
||||||
|
local names = {}
|
||||||
|
local placeable = station.owningPlaceable
|
||||||
|
if placeable ~= nil and placeable.getName ~= nil then
|
||||||
|
local ok, name = pcall(placeable.getName, placeable)
|
||||||
|
if ok and name ~= nil then table.insert(names, name) end
|
||||||
|
end
|
||||||
|
if station.getName ~= nil then
|
||||||
|
local ok, name = pcall(station.getName, station)
|
||||||
|
if ok and name ~= nil then table.insert(names, name) end
|
||||||
|
end
|
||||||
|
return names
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Alle salgssteder med AD-markør -> {{id, names}} (markørens navn først: det spilleren ser i AutoDrive).
|
||||||
|
local function sellStations(adEnv)
|
||||||
|
local list, byId = {}, {}
|
||||||
|
local farmId = g_currentMission:getFarmId()
|
||||||
|
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||||||
|
local station = ADSmartPickup.getUnloadStationAtWayPointId(adEnv, marker.id, farmId)
|
||||||
|
local id = station ~= nil and ADPrices.isSellingStation(station) and stationId(station) or nil
|
||||||
|
if id ~= nil then
|
||||||
|
if byId[id] == nil then
|
||||||
|
byId[id] = {id = id, names = {}}
|
||||||
|
table.insert(list, byId[id])
|
||||||
|
for _, name in ipairs(stationNames(station)) do table.insert(byId[id].names, name) end
|
||||||
|
end
|
||||||
|
table.insert(byId[id].names, 1, marker.name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(list, function(a, b) return tostring(a.names[1]) < tostring(b.names[1]) end)
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADSellBlock:consoleSellBlock(...)
|
||||||
|
local query = table.concat({...}, " ")
|
||||||
|
local adEnv = FS25_AutoDrive
|
||||||
|
if adEnv == nil or adEnv.ADGraphManager == nil then return "AutoDrive ikke fundet" end
|
||||||
|
local stations = sellStations(adEnv)
|
||||||
|
if query == "" then
|
||||||
|
local lines = {"Salgssteder (spuSellBlock <del af navn> slår fra/til):"}
|
||||||
|
for _, station in ipairs(stations) do
|
||||||
|
table.insert(lines, string.format(" %s %s", ADSellBlock.blocked[station.id] ~= nil and "[FRA]" or "[til]", table.concat(station.names, " / ")))
|
||||||
|
end
|
||||||
|
return table.concat(lines, "\n")
|
||||||
|
end
|
||||||
|
local hits = ADSellBlock.match(stations, query)
|
||||||
|
if #hits == 0 then return string.format("Intet salgssted matcher '%s' (spuSellBlock uden navn viser listen)", query) end
|
||||||
|
ADSellBlock.blocked = ADSellBlock.toggle(ADSellBlock.blocked, hits)
|
||||||
|
local lines = {}
|
||||||
|
for _, station in ipairs(hits) do
|
||||||
|
local text = string.format("%s: %s", station.names[1], ADSellBlock.blocked[station.id] ~= nil and "slået FRA som salgssted" or "bruges igen")
|
||||||
|
table.insert(lines, text)
|
||||||
|
Logging.info("ADSmartPickup: salgssted " .. text)
|
||||||
|
end
|
||||||
|
if ADRunsController ~= nil and ADRunsController.save ~= nil then pcall(ADRunsController.save) end
|
||||||
|
return table.concat(lines, "\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getPath()
|
||||||
|
local path = ADRunsStorage ~= nil and ADRunsStorage.getPath() or nil
|
||||||
|
if path == nil then return nil end
|
||||||
|
return (path:gsub(ADRunsStorage.FILE_NAME .. "$", ADSellBlock.FILE_NAME))
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADSellBlock.save()
|
||||||
|
local path = getPath()
|
||||||
|
if path == nil then return false end
|
||||||
|
local xmlFile = XMLFile.create("ADSmartPickupSellBlock", path, ADSellBlock.ROOT)
|
||||||
|
if xmlFile == nil then return false end
|
||||||
|
local index = 0
|
||||||
|
for id, name in pairs(ADSellBlock.blocked) do
|
||||||
|
local key = string.format("%s.station(%d)", ADSellBlock.ROOT, index)
|
||||||
|
xmlFile:setString(key .. "#id", id)
|
||||||
|
xmlFile:setString(key .. "#name", name)
|
||||||
|
index = index + 1
|
||||||
|
end
|
||||||
|
xmlFile:save()
|
||||||
|
xmlFile:delete()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADSellBlock.load()
|
||||||
|
ADSellBlock.blocked = {}
|
||||||
|
local path = getPath()
|
||||||
|
if path == nil or not fileExists(path) then return end
|
||||||
|
local xmlFile = XMLFile.load("ADSmartPickupSellBlock", path)
|
||||||
|
if xmlFile == nil then return end
|
||||||
|
xmlFile:iterate(ADSellBlock.ROOT .. ".station", function(_, key)
|
||||||
|
local id = xmlFile:getString(key .. "#id")
|
||||||
|
if id ~= nil then ADSellBlock.blocked[id] = xmlFile:getString(key .. "#name") or id end
|
||||||
|
end)
|
||||||
|
xmlFile:delete()
|
||||||
|
end
|
||||||
|
|
||||||
|
if addConsoleCommand ~= nil then
|
||||||
|
addConsoleCommand("spuSellBlock", "Salgssteder: uden navn = liste; spuSellBlock <del af navn> slår fra/til", "consoleSellBlock", ADSellBlock)
|
||||||
|
end
|
||||||
@@ -28,6 +28,7 @@ if ADSmartPickup.dir ~= nil and source ~= nil then
|
|||||||
source(ADSmartPickup.dir .. "gui/SmartPickupFieldTab.lua")
|
source(ADSmartPickup.dir .. "gui/SmartPickupFieldTab.lua")
|
||||||
source(ADSmartPickup.dir .. "gui/SmartPickupMonthTab.lua")
|
source(ADSmartPickup.dir .. "gui/SmartPickupMonthTab.lua")
|
||||||
source(ADSmartPickup.dir .. "gui/SmartPickupFieldSiloTab.lua")
|
source(ADSmartPickup.dir .. "gui/SmartPickupFieldSiloTab.lua")
|
||||||
|
source(ADSmartPickup.dir .. "gui/SmartPickupChainTab.lua")
|
||||||
source(ADSmartPickup.dir .. "gui/SmartPickupGuide.lua")
|
source(ADSmartPickup.dir .. "gui/SmartPickupGuide.lua")
|
||||||
source(ADSmartPickup.dir .. "gui/SmartPickupFieldGuide.lua")
|
source(ADSmartPickup.dir .. "gui/SmartPickupFieldGuide.lua")
|
||||||
source(ADSmartPickup.dir .. "gui/SmartPickupFlowGuide.lua")
|
source(ADSmartPickup.dir .. "gui/SmartPickupFlowGuide.lua")
|
||||||
@@ -1151,6 +1152,9 @@ local function installHook(adEnv)
|
|||||||
if ADHitchCreep ~= nil and ADHitchCreep.install ~= nil then
|
if ADHitchCreep ~= nil and ADHitchCreep.install ~= nil then
|
||||||
ADHitchCreep.install(adEnv)
|
ADHitchCreep.install(adEnv)
|
||||||
end
|
end
|
||||||
|
if ADLoadDiag ~= nil and ADLoadDiag.install ~= nil then
|
||||||
|
ADLoadDiag.install(adEnv)
|
||||||
|
end
|
||||||
|
|
||||||
Logging.info(ADSmartPickup.LOG_PREFIX .. "aktiv (mindst fyldte silo med varen; flere valgte varer = behovsstyret forsyning; Wait-punkter for rest-læs; udkørsel til silo/salg)")
|
Logging.info(ADSmartPickup.LOG_PREFIX .. "aktiv (mindst fyldte silo med varen; flere valgte varer = behovsstyret forsyning; Wait-punkter for rest-læs; udkørsel til silo/salg)")
|
||||||
end
|
end
|
||||||
@@ -1309,6 +1313,12 @@ function ADSmartPickup:update(dt)
|
|||||||
Logging.warning(ADSmartPickup.LOG_PREFIX .. "marksilo-flow fejlede: %s", tostring(errSilos))
|
Logging.warning(ADSmartPickup.LOG_PREFIX .. "marksilo-flow fejlede: %s", tostring(errSilos))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
if ADChain ~= nil then
|
||||||
|
local okChain, errChain = pcall(ADChain.tick, getAutoDriveEnv(), dt)
|
||||||
|
if not okChain then
|
||||||
|
Logging.warning(ADSmartPickup.LOG_PREFIX .. "kæde fejlede: %s", tostring(errChain))
|
||||||
|
end
|
||||||
|
end
|
||||||
if ADFlowWagons ~= nil then
|
if ADFlowWagons ~= nil then
|
||||||
local okFlow, errFlow = pcall(ADFlowWagons.tick, getAutoDriveEnv(), dt)
|
local okFlow, errFlow = pcall(ADFlowWagons.tick, getAutoDriveEnv(), dt)
|
||||||
if not okFlow then Logging.warning(ADSmartPickup.LOG_PREFIX .. "flow-vogne fejlede: %s", tostring(errFlow)) end
|
if not okFlow then Logging.warning(ADSmartPickup.LOG_PREFIX .. "flow-vogne fejlede: %s", tostring(errFlow)) end
|
||||||
@@ -1328,6 +1338,16 @@ function ADSmartPickup:update(dt)
|
|||||||
Logging.warning(ADSmartPickup.LOG_PREFIX .. "kunne ikke gendanne varevalg: %s", tostring(err))
|
Logging.warning(ADSmartPickup.LOG_PREFIX .. "kunne ikke gendanne varevalg: %s", tostring(err))
|
||||||
supplyState = setmetatable({}, {__mode = "k"})
|
supplyState = setmetatable({}, {__mode = "k"})
|
||||||
end
|
end
|
||||||
|
if ADStatusHud ~= nil then pcall(ADStatusHud.update, dt, getAutoDriveEnv()) end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Statusvinduet på skærmen (adStatusHud.lua): tegner kun den cachede tekst.
|
||||||
|
function ADSmartPickup:draw()
|
||||||
|
if ADStatusHud ~= nil then pcall(ADStatusHud.draw) end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADSmartPickup:deleteMap()
|
||||||
|
if ADStatusHud ~= nil then pcall(ADStatusHud.delete) end
|
||||||
end
|
end
|
||||||
|
|
||||||
addModEventListener(ADSmartPickup)
|
addModEventListener(ADSmartPickup)
|
||||||
|
|||||||
@@ -585,9 +585,17 @@ function ADSources.getObjectInfoIndex(source, fillType)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Alle grupper med varen: lageret deler samme vare i flere objectInfos (fx paller med forskellig fyldning). Kun den
|
||||||
|
-- første talt -> tallet "voksede", når en lille gruppe blev tom, og spawn-ventningen gik i stå (Sell Egg 30/9).
|
||||||
function ADSources.countStored(source, fillType)
|
function ADSources.countStored(source, fillType)
|
||||||
local _, count = ADSources.getObjectInfoIndex(source, fillType)
|
local spec = source.placeable ~= nil and source.placeable.spec_objectStorage or nil
|
||||||
return count or 0
|
local count = 0
|
||||||
|
for _, objectInfo in ipairs(spec ~= nil and spec.objectInfos or {}) do
|
||||||
|
if getObjectInfoContent(objectInfo) == fillType then
|
||||||
|
count = count + (objectInfo.numObjects or 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return count
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Bed lageret spawne ÉT objekt (det knappen i spillet gør). connection = nil, så
|
-- Bed lageret spawne ÉT objekt (det knappen i spillet gør). connection = nil, så
|
||||||
@@ -636,7 +644,12 @@ function ADSources.describeAccepted(station)
|
|||||||
return table.concat(names, ", ")
|
return table.concat(names, ", ")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Markører hvis aflæsningsstation er et rigtigt salgssted: { {marker, station} }.
|
-- Salgssted spilleren har slået fra (spuSellBlock, ADSellBlock).
|
||||||
|
function ADSources.isSellBlocked(station)
|
||||||
|
return ADSellBlock ~= nil and ADSellBlock.isBlocked ~= nil and ADSellBlock.isBlocked(station) == true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Markører hvis aflæsningsstation er et rigtigt salgssted (og ikke slået fra): { {marker, station} }.
|
||||||
function ADSources.getSellMarkers(adEnv, farmId)
|
function ADSources.getSellMarkers(adEnv, farmId)
|
||||||
return cacheGet("sellMarkers", tostring(farmId), 10000, function() return ADSources.findSellMarkers(adEnv, farmId) end)
|
return cacheGet("sellMarkers", tostring(farmId), 10000, function() return ADSources.findSellMarkers(adEnv, farmId) end)
|
||||||
end
|
end
|
||||||
@@ -645,7 +658,7 @@ function ADSources.findSellMarkers(adEnv, farmId)
|
|||||||
local result = {}
|
local result = {}
|
||||||
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||||||
local station = ADSmartPickup.getUnloadStationAtWayPointId(adEnv, marker.id, farmId)
|
local station = ADSmartPickup.getUnloadStationAtWayPointId(adEnv, marker.id, farmId)
|
||||||
if station ~= nil and ADPrices.isSellingStation(station) then
|
if station ~= nil and ADPrices.isSellingStation(station) and not ADSources.isSellBlocked(station) then
|
||||||
table.insert(result, {marker = marker, station = station})
|
table.insert(result, {marker = marker, station = station})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -963,13 +976,38 @@ function ADSources.getFarmRates(adEnv, farmId, feedOnly)
|
|||||||
return rates
|
return rates
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Én stalds forbrug (foder efter trugets indhold, halm, vand): {[fillType] = liter} kun for consumption > 0.
|
||||||
|
-- Rå rate uden timeAdjustment (som getHusbandryOutputs).
|
||||||
|
function ADSources.getHusbandryConsumption(placeable)
|
||||||
|
local rates, result = {}, {}
|
||||||
|
if placeable == nil then return result end
|
||||||
|
pcall(addHusbandryRates, rates, placeable)
|
||||||
|
for fillType, rate in pairs(rates) do
|
||||||
|
if (rate.consumption or 0) > 0 then result[fillType] = rate.consumption end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Én stalds produktion (mælk, gødning, ...): {[fillType] = liter} kun for production > 0. Spillets rå rate
|
||||||
|
-- (pr. måned = én 24-timers dag) UDEN timeAdjustment — samme enhed som fabrikkernes cyklusser pr. måned i kæden.
|
||||||
|
function ADSources.getHusbandryOutputs(placeable)
|
||||||
|
local rates, outputs = {}, {}
|
||||||
|
if placeable == nil then return outputs end
|
||||||
|
pcall(addHusbandryRates, rates, placeable) -- giver kun forbrug; med for fuldstændighedens skyld
|
||||||
|
pcall(addAnimalProduction, rates, placeable)
|
||||||
|
for fillType, rate in pairs(rates) do
|
||||||
|
if (rate.production or 0) > 0 then outputs[fillType] = rate.production end
|
||||||
|
end
|
||||||
|
return outputs
|
||||||
|
end
|
||||||
|
|
||||||
-- Bedste pris lige nu blandt alle spillets salgssteder der tager varen (uanset markør/vejnet):
|
-- Bedste pris lige nu blandt alle spillets salgssteder der tager varen (uanset markør/vejnet):
|
||||||
-- {price, top} pr. liter; top = prisen ved årets højeste periode. nil når ingen tager varen.
|
-- {price, top} pr. liter; top = prisen ved årets højeste periode. nil når ingen tager varen.
|
||||||
function ADSources.getMarketQuote(fillType)
|
function ADSources.getMarketQuote(fillType)
|
||||||
local storage = g_currentMission ~= nil and g_currentMission.storageSystem or nil
|
local storage = g_currentMission ~= nil and g_currentMission.storageSystem or nil
|
||||||
local best = nil
|
local best = nil
|
||||||
for _, station in pairs(storage ~= nil and storage:getUnloadingStations() or {}) do
|
for _, station in pairs(storage ~= nil and storage:getUnloadingStations() or {}) do
|
||||||
if ADPrices.isSellingStation(station) and ADSources.stationAccepts(station, fillType) then
|
if ADPrices.isSellingStation(station) and not ADSources.isSellBlocked(station) and ADSources.stationAccepts(station, fillType) then
|
||||||
local quote = ADPrices.getQuote(station, fillType)
|
local quote = ADPrices.getQuote(station, fillType)
|
||||||
if quote ~= nil and (best == nil or quote.price > best.price) then
|
if quote ~= nil and (best == nil or quote.price > best.price) then
|
||||||
best = {price = quote.price, top = quote.top}
|
best = {price = quote.price, top = quote.top}
|
||||||
|
|||||||
@@ -0,0 +1,473 @@
|
|||||||
|
-- AD Status Hud
|
||||||
|
-- Statusvinduet på skærmen (i stil med Simple Inspector): slås til/fra med input-handlingen SPU_TOGGLE_STATUS
|
||||||
|
-- (standard numpad *). Øjebliksbilledet bygges af modens egne statusfunktioner (samme som fanerne) højst
|
||||||
|
-- hvert sekund i update() og kun mens vinduet er tændt; draw() tegner kun de cachede linjer og bredder.
|
||||||
|
-- Synligheden gemmes pr. bruger i modSettings/FS25_ADSmartPickup/status.xml (standard skjult).
|
||||||
|
-- Kører kun på en klient (ikke dedikeret server). Alle spilkald er beskyttet; fejl logges én gang pr. nøgle.
|
||||||
|
|
||||||
|
ADStatusHud = {}
|
||||||
|
|
||||||
|
ADStatusHud.LOG_PREFIX = "ADSmartPickup: status: "
|
||||||
|
ADStatusHud.REFRESH_MS = 1000
|
||||||
|
ADStatusHud.STUCK_WARN_MS = 120000 -- stået stille så længe (uden at Smart Pickup holder den) = advarsel
|
||||||
|
ADStatusHud.TEXT_SIZE_PX = 14
|
||||||
|
ADStatusHud.MARGIN_PX = 8
|
||||||
|
ADStatusHud.SCREEN_OFFSET_PX = 16 -- afstand til skærmens højre kant
|
||||||
|
ADStatusHud.TOP_Y = 1 - 0.12 -- øverste kant (normaliseret); under spillets info-boks
|
||||||
|
ADStatusHud.SETTINGS_DIR = "modSettings/FS25_ADSmartPickup/"
|
||||||
|
ADStatusHud.SETTINGS_FILE = "status.xml"
|
||||||
|
ADStatusHud.SETTINGS_ROOT = "adSmartPickupStatus"
|
||||||
|
ADStatusHud.HEADER_COLOR = {1, 1, 1, 1}
|
||||||
|
|
||||||
|
ADStatusHud.visible = false
|
||||||
|
ADStatusHud.lines = {} -- {{text, level, header, color, width}}
|
||||||
|
ADStatusHud.layout = nil -- {x, y, width, height, textSize, marginX, marginY, padding}
|
||||||
|
|
||||||
|
local timerMs = 0
|
||||||
|
local settingsLoaded = false
|
||||||
|
local overlays = nil
|
||||||
|
local loggedErrors = {}
|
||||||
|
local registerToggleActionRef -- sat nederst (tastens handler til krogen)
|
||||||
|
|
||||||
|
-- ---- hjælpere ----
|
||||||
|
|
||||||
|
local function logOnce(key, err)
|
||||||
|
if loggedErrors[key] then return end
|
||||||
|
loggedErrors[key] = true
|
||||||
|
if Logging ~= nil and Logging.warning ~= nil then
|
||||||
|
Logging.warning(ADStatusHud.LOG_PREFIX .. "%s fejlede: %s", tostring(key), tostring(err))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function guarded(key, fn, ...)
|
||||||
|
local ok, result = pcall(fn, ...)
|
||||||
|
if not ok then
|
||||||
|
logOnce(key, result)
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local function textOf(key)
|
||||||
|
if g_i18n == nil then return key end
|
||||||
|
local ok, text = pcall(g_i18n.getText, g_i18n, key)
|
||||||
|
return ok and text or key
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADStatusHud.isClient()
|
||||||
|
local mission = g_currentMission
|
||||||
|
if mission == nil or mission.getIsClient == nil then return false end
|
||||||
|
local ok, isClient = pcall(mission.getIsClient, mission)
|
||||||
|
return ok and isClient == true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Fanernes egne tekst-/statusfunktioner ligger som metoder på SmartPickupFrame og bruger kun self.i18n:
|
||||||
|
-- en let "frame" med spillets i18n genbruger dem uden at røre menuen. "Start afvist" (lastStartFailed, rødt flow)
|
||||||
|
-- huskes på den rigtige ESC-fane (ADSmartPickup.guiFrame): tabellen deles, så vinduet viser samme farve som
|
||||||
|
-- Flows-listen (flowLevel rydder en afvisning når flowet kører — det samme som fanen selv gør).
|
||||||
|
local function frameProxy()
|
||||||
|
if SmartPickupFrame == nil then return nil end
|
||||||
|
local real = ADSmartPickup ~= nil and ADSmartPickup.guiFrame or nil
|
||||||
|
local failed = {}
|
||||||
|
if type(real) == "table" then
|
||||||
|
if type(real.lastStartFailed) ~= "table" then real.lastStartFailed = {} end
|
||||||
|
failed = real.lastStartFailed
|
||||||
|
end
|
||||||
|
return setmetatable({i18n = g_i18n, lastStartFailed = failed}, {__index = SmartPickupFrame})
|
||||||
|
end
|
||||||
|
|
||||||
|
local function fillTitle(fillType)
|
||||||
|
if fillType == nil or g_fillTypeManager == nil then return "?" end
|
||||||
|
local title = g_fillTypeManager.getFillTypeTitleByIndex ~= nil and g_fillTypeManager:getFillTypeTitleByIndex(fillType)
|
||||||
|
or g_fillTypeManager:getFillTypeNameByIndex(fillType)
|
||||||
|
return tostring(title or "?")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- øjebliksbillede ----
|
||||||
|
|
||||||
|
local WAGON_ACTIONS = {
|
||||||
|
fetching = {key = "spu_status_act_fetchWagon", level = "waiting"},
|
||||||
|
waiting = {key = "spu_status_act_waitWagon", level = "waiting"},
|
||||||
|
emptying = {key = "spu_status_act_emptying", level = "running"},
|
||||||
|
homing = {key = "spu_status_act_homing", level = "running"},
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Traktorens handling som kort tekst + niveau (ADRunsController.getVehicleActivity som Flows-fanen).
|
||||||
|
local function vehicleAction(adEnv, vehicle)
|
||||||
|
local wagonState = ADFlowWagons ~= nil and ADFlowWagons.statusOf ~= nil and ADFlowWagons.statusOf(vehicle) or nil
|
||||||
|
if wagonState ~= nil and WAGON_ACTIONS[wagonState] ~= nil then
|
||||||
|
return textOf(WAGON_ACTIONS[wagonState].key), WAGON_ACTIONS[wagonState].level
|
||||||
|
end
|
||||||
|
local activity = ADRunsController.getVehicleActivity(adEnv, vehicle)
|
||||||
|
local place = tostring(activity.place or "?")
|
||||||
|
if activity.kind == "stopped" then
|
||||||
|
return textOf("spu_activity_stopped"), "stopped"
|
||||||
|
elseif activity.kind == "waiting" then
|
||||||
|
return string.format(textOf("spu_status_act_waiting"), place), "waiting"
|
||||||
|
elseif activity.kind == "holding" then
|
||||||
|
return string.format(textOf("spu_status_act_holding"), place), "waiting"
|
||||||
|
elseif activity.kind == "empty" and activity.fillType ~= nil then
|
||||||
|
return string.format(textOf("spu_status_act_fetch"), fillTitle(activity.fillType), place), "running"
|
||||||
|
end
|
||||||
|
return string.format(textOf("spu_status_act_driving"), place), "running"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Fyld i % af hele vogntoget (AutoDrives egne opgørelser, som kildevalget bruger); nil uden kapacitet.
|
||||||
|
local function fillPercent(adEnv, vehicle)
|
||||||
|
local autoDrive = adEnv.AutoDrive
|
||||||
|
if autoDrive == nil or autoDrive.getAllUnits == nil or autoDrive.getAllFillLevels == nil then return nil end
|
||||||
|
local level, capacity = autoDrive.getAllFillLevels(autoDrive.getAllUnits(vehicle))
|
||||||
|
if type(level) ~= "number" or type(capacity) ~= "number" or capacity <= 0 then return nil end
|
||||||
|
return math.max(0, math.min(100, level / capacity * 100))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function farmVehiclesById(adEnv)
|
||||||
|
local byId = {}
|
||||||
|
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||||
|
local id = ADRunsController.getVehicleId(vehicle)
|
||||||
|
if id ~= nil then byId[id] = vehicle end
|
||||||
|
end
|
||||||
|
return byId
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Én linje pr. traktor i et kørende flow (flows der kan stoppes: startet, eller holder vogn/henter).
|
||||||
|
function ADStatusHud.flowEntries(adEnv, proxy)
|
||||||
|
local entries = {}
|
||||||
|
if ADRunsController == nil or ADRunsController.store == nil or ADFlowView == nil then return entries end
|
||||||
|
local byId = farmVehiclesById(adEnv)
|
||||||
|
for _, run in ipairs(ADFlowView.orderedRuns(ADRunsController.store.runs or {})) do
|
||||||
|
local status = ADRunsController.getRunStatus(adEnv, run)
|
||||||
|
if status.canStop then
|
||||||
|
local runLevel = proxy ~= nil and proxy.flowLevel ~= nil and guarded("flowLevel", proxy.flowLevel, proxy, adEnv, run) or nil
|
||||||
|
for _, vehicleId in ipairs(run.vehicleIds or {}) do
|
||||||
|
local vehicle = byId[vehicleId]
|
||||||
|
if vehicle ~= nil then
|
||||||
|
local action, level = vehicleAction(adEnv, vehicle)
|
||||||
|
-- farven følger flowets niveau som Flows-listen (venter/fejl), ellers traktorens egen handling
|
||||||
|
if runLevel == "error" or runLevel == "waiting" then level = runLevel end
|
||||||
|
table.insert(entries, {vehicle = tostring(ADRuns.vehicleLabel(vehicle)), action = action,
|
||||||
|
fillPercent = guarded("fillPercent", fillPercent, adEnv, vehicle), level = level})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return entries
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Én linje pr. mark med aktivt markarbejde (samme statustekst og farve som Marker-fanen).
|
||||||
|
function ADStatusHud.fieldEntries(proxy)
|
||||||
|
local entries = {}
|
||||||
|
if ADFields == nil or ADFieldJobs == nil or ADFieldModel == nil or proxy == nil then return entries end
|
||||||
|
for _, info in ipairs(ADFields.list()) do
|
||||||
|
local field = ADFieldJobs.getField(info.id)
|
||||||
|
if field ~= nil and (ADFieldModel.isWorking(field) or ADFieldJobs.awaitingPlayer(info.id) ~= nil) then
|
||||||
|
local names = {}
|
||||||
|
for _, assignment in ipairs(ADFieldJobs.assignmentsFor(info.id)) do
|
||||||
|
table.insert(names, ADRuns.vehicleLabel(assignment.vehicle))
|
||||||
|
end
|
||||||
|
table.insert(entries, {
|
||||||
|
fieldId = info.id,
|
||||||
|
work = proxy:fieldStatusText(field, info, true),
|
||||||
|
vehicle = #names > 0 and ADFlowView.joinCounted(names, ", ", nil) or nil,
|
||||||
|
progress = SmartPickupFrame.monthCpProgress ~= nil and guarded("fieldProgress", SmartPickupFrame.monthCpProgress, info.id) or nil,
|
||||||
|
level = proxy:fieldLevel(info.id),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return entries
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Kæder med manglende led + ventende forslag + slukkede nødvendige fabrikker (samme punkter og tekster som
|
||||||
|
-- "Kræver handling"; de to sidste står under Advarsler).
|
||||||
|
function ADStatusHud.chainEntries(proxy)
|
||||||
|
local chains, suggestions = {}, {}
|
||||||
|
local result = {chains = chains, suggestions = suggestions, dependencies = {}}
|
||||||
|
if ADChain == nil or ADChain.last == nil or ADStorageStats == nil or ADStorageStats.chainEntries == nil or proxy == nil then
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
for _, entry in ipairs(ADStorageStats.chainEntries(ADChain.last)) do
|
||||||
|
if entry.kind == "chainProblem" then
|
||||||
|
table.insert(chains, {text = proxy:alertText(entry), level = "error"})
|
||||||
|
elseif entry.kind == "chainSuggest" then
|
||||||
|
table.insert(suggestions, {text = proxy:alertText(entry), level = "waiting"})
|
||||||
|
elseif entry.kind == "dependencyOff" then
|
||||||
|
table.insert(result.dependencies, {text = proxy:alertText(entry), level = "error"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Solgt/slettet køretøj (ADStuckDiag kan holde det et øjeblik efter salget).
|
||||||
|
local function isGone(vehicle)
|
||||||
|
if vehicle == nil or vehicle.isDeleted then return true end
|
||||||
|
if vehicle.rootNode == nil then return true end
|
||||||
|
return entityExists ~= nil and not entityExists(vehicle.rootNode)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Gårdens køretøjer der har stået stille over grænsen, uden at Smart Pickup selv holder dem (Wait/udkørsel).
|
||||||
|
function ADStatusHud.stuckEntries()
|
||||||
|
local entries = {}
|
||||||
|
if ADStuckDiag == nil or ADStuckDiag.stillVehicles == nil then return entries end
|
||||||
|
local farmId = g_currentMission ~= nil and g_currentMission.getFarmId ~= nil and g_currentMission:getFarmId() or nil
|
||||||
|
for _, still in ipairs(ADStuckDiag.stillVehicles(ADStatusHud.STUCK_WARN_MS)) do
|
||||||
|
local vehicle = still.vehicle
|
||||||
|
local owned = not isGone(vehicle) and (farmId == nil or (vehicle.getOwnerFarmId ~= nil and vehicle:getOwnerFarmId() == farmId))
|
||||||
|
local held = owned and ((ADUnloadWait ~= nil and ADUnloadWait.getWaitFor ~= nil and ADUnloadWait.getWaitFor(vehicle) ~= nil)
|
||||||
|
or (ADOutbound ~= nil and ADOutbound.getHoldFor ~= nil and ADOutbound.getHoldFor(vehicle) ~= nil))
|
||||||
|
if owned and not held then
|
||||||
|
table.insert(entries, {text = string.format(textOf("spu_status_stuck"), tostring(ADRuns.vehicleLabel(vehicle)),
|
||||||
|
math.floor(still.ms / 60000)), level = "error"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return entries
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADStatusHud.snapshot(adEnv)
|
||||||
|
local proxy = frameProxy()
|
||||||
|
local snapshot = {flows = {}, fields = {}, chains = {}, warnings = {}}
|
||||||
|
if adEnv ~= nil then
|
||||||
|
snapshot.flows = guarded("flows", ADStatusHud.flowEntries, adEnv, proxy) or {}
|
||||||
|
end
|
||||||
|
snapshot.fields = guarded("fields", ADStatusHud.fieldEntries, proxy) or {}
|
||||||
|
local chainResult = guarded("chains", ADStatusHud.chainEntries, proxy) or {}
|
||||||
|
snapshot.chains = chainResult.chains or {}
|
||||||
|
for _, entry in ipairs(chainResult.dependencies or {}) do table.insert(snapshot.warnings, entry) end
|
||||||
|
for _, entry in ipairs(guarded("stuck", ADStatusHud.stuckEntries) or {}) do table.insert(snapshot.warnings, entry) end
|
||||||
|
for _, entry in ipairs(chainResult.suggestions or {}) do table.insert(snapshot.warnings, entry) end
|
||||||
|
return snapshot
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- layout og cache (kun i update) ----
|
||||||
|
|
||||||
|
local function gameInfoDisplay()
|
||||||
|
local hud = g_currentMission ~= nil and g_currentMission.hud or nil
|
||||||
|
return hud ~= nil and hud.gameInfoDisplay or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Størrelser i skærmkoordinater, skaleret som spillets egen HUD (Simple Inspector-mønster).
|
||||||
|
local function measureUnits()
|
||||||
|
local display = gameInfoDisplay()
|
||||||
|
if display == nil or display.scalePixelToScreenHeight == nil or display.scalePixelToScreenVector == nil then return nil end
|
||||||
|
local textSize = display:scalePixelToScreenHeight(ADStatusHud.TEXT_SIZE_PX)
|
||||||
|
local marginX, marginY = display:scalePixelToScreenVector({ADStatusHud.MARGIN_PX, ADStatusHud.MARGIN_PX})
|
||||||
|
local offsetX = display:scalePixelToScreenVector({ADStatusHud.SCREEN_OFFSET_PX, 0})
|
||||||
|
return textSize, marginX, marginY, offsetX
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Øverste kant: fast under info-boksen; ligger info-boksens position lavere, rykkes vinduet ned under den.
|
||||||
|
local function topY(marginY)
|
||||||
|
local top = ADStatusHud.TOP_Y
|
||||||
|
local display = gameInfoDisplay()
|
||||||
|
if display ~= nil and display.getPosition ~= nil then
|
||||||
|
local ok, _, y = pcall(display.getPosition, display)
|
||||||
|
if ok and type(y) == "number" and y > 0.5 and y < 1 then top = math.min(top, y - marginY) end
|
||||||
|
end
|
||||||
|
return top
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADStatusHud.rebuild(adEnv)
|
||||||
|
local lines = ADStatusView.lines(ADStatusHud.snapshot(adEnv), textOf, ADStatusView.MAX_LINES)
|
||||||
|
local textSize, marginX, marginY, offsetX = measureUnits()
|
||||||
|
if textSize == nil then
|
||||||
|
-- spillets info-boks (skalering) findes ikke: vinduet kan ikke måles og tegnes
|
||||||
|
logOnce("measure", "gameInfoDisplay mangler eller kan ikke skalere")
|
||||||
|
ADStatusHud.lines, ADStatusHud.layout = {}, nil
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local maxWidth = 0
|
||||||
|
for _, line in ipairs(lines) do
|
||||||
|
setTextBold(line.header == true)
|
||||||
|
line.width = getTextWidth(textSize, line.text)
|
||||||
|
line.color = line.header and ADStatusHud.HEADER_COLOR or (ADFlowView.LEVEL_COLORS[line.level] or ADStatusHud.HEADER_COLOR)
|
||||||
|
if line.width > maxWidth then maxWidth = line.width end
|
||||||
|
end
|
||||||
|
setTextBold(false)
|
||||||
|
local width = maxWidth + 2 * marginX
|
||||||
|
local height = #lines * textSize + 2 * marginY
|
||||||
|
local top = topY(marginY)
|
||||||
|
ADStatusHud.lines = lines
|
||||||
|
ADStatusHud.layout = {x = 1 - offsetX - width, y = top - height, top = top, width = width, height = height,
|
||||||
|
textSize = textSize, marginX = marginX, marginY = marginY, padding = marginX * 0.5}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- overlays ----
|
||||||
|
|
||||||
|
local function createOverlays()
|
||||||
|
if overlays ~= nil then return overlays end
|
||||||
|
if g_overlayManager == nil or HUD == nil or HUD.COLOR == nil or HUD.COLOR.BACKGROUND == nil then return nil end
|
||||||
|
local r, g, b, a = unpack(HUD.COLOR.BACKGROUND)
|
||||||
|
local created = {
|
||||||
|
middle = g_overlayManager:createOverlay("gui.gameInfo_middle", 0, 0, 0, 0),
|
||||||
|
left = g_overlayManager:createOverlay("gui.gameInfo_left", 0, 0, 0, 0),
|
||||||
|
right = g_overlayManager:createOverlay("gui.gameInfo_right", 0, 0, 0, 0),
|
||||||
|
}
|
||||||
|
for _, overlay in pairs(created) do overlay:setColor(r, g, b, a) end
|
||||||
|
overlays = created
|
||||||
|
return overlays
|
||||||
|
end
|
||||||
|
|
||||||
|
local function deleteOverlays()
|
||||||
|
if overlays == nil then return end
|
||||||
|
for _, overlay in pairs(overlays) do
|
||||||
|
if overlay.delete ~= nil then pcall(overlay.delete, overlay) end
|
||||||
|
end
|
||||||
|
overlays = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- indstillinger (pr. bruger, ikke i savegamet) ----
|
||||||
|
|
||||||
|
local function settingsPath()
|
||||||
|
if getUserProfileAppPath == nil then return nil end
|
||||||
|
return getUserProfileAppPath() .. ADStatusHud.SETTINGS_DIR .. ADStatusHud.SETTINGS_FILE
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADStatusHud.loadSettings()
|
||||||
|
settingsLoaded = true
|
||||||
|
local path = settingsPath()
|
||||||
|
if path == nil or fileExists == nil or not fileExists(path) then return end
|
||||||
|
local xmlFile = XMLFile.load("ADSmartPickupStatus", path)
|
||||||
|
if xmlFile == nil then return end
|
||||||
|
ADStatusHud.visible = xmlFile:getBool(ADStatusHud.SETTINGS_ROOT .. "#visible", false) == true
|
||||||
|
xmlFile:delete()
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADStatusHud.saveSettings()
|
||||||
|
local path = settingsPath()
|
||||||
|
if path == nil then return end
|
||||||
|
if createFolder ~= nil then
|
||||||
|
createFolder(getUserProfileAppPath() .. "modSettings/")
|
||||||
|
createFolder(getUserProfileAppPath() .. ADStatusHud.SETTINGS_DIR)
|
||||||
|
end
|
||||||
|
local xmlFile = XMLFile.create("ADSmartPickupStatus", path, ADStatusHud.SETTINGS_ROOT)
|
||||||
|
if xmlFile == nil then
|
||||||
|
logOnce("saveSettings", "kunne ikke oprette " .. path)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
xmlFile:setBool(ADStatusHud.SETTINGS_ROOT .. "#visible", ADStatusHud.visible == true)
|
||||||
|
xmlFile:save()
|
||||||
|
xmlFile:delete()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- offentlige kald fra moden ----
|
||||||
|
|
||||||
|
function ADStatusHud.toggle()
|
||||||
|
ADStatusHud.visible = not ADStatusHud.visible
|
||||||
|
timerMs = ADStatusHud.REFRESH_MS -- byg straks ved næste update
|
||||||
|
if not ADStatusHud.visible then ADStatusHud.lines, ADStatusHud.layout = {}, nil end
|
||||||
|
guarded("saveSettings", ADStatusHud.saveSettings)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Action-event-callback (target = ADStatusHud).
|
||||||
|
function ADStatusHud.onToggleAction()
|
||||||
|
ADStatusHud.toggle()
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADStatusHud.update(dt, adEnv)
|
||||||
|
if not ADStatusHud.isClient() then return end
|
||||||
|
if not settingsLoaded then
|
||||||
|
guarded("loadSettings", ADStatusHud.loadSettings)
|
||||||
|
timerMs = ADStatusHud.REFRESH_MS
|
||||||
|
end
|
||||||
|
if not ADStatusHud.visible then return end
|
||||||
|
timerMs = timerMs + (dt or 0)
|
||||||
|
if timerMs < ADStatusHud.REFRESH_MS then return end
|
||||||
|
timerMs = 0
|
||||||
|
guarded("rebuild", ADStatusHud.rebuild, adEnv)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Tegnes ikke ved åben menu/dialog, skjult HUD, søvn eller åben chat.
|
||||||
|
local function shouldSkipDraw()
|
||||||
|
if g_gui ~= nil and g_gui:getIsGuiVisible() then return true end
|
||||||
|
if g_noHudModeEnabled then return true end
|
||||||
|
local mission = g_currentMission
|
||||||
|
local hud = mission ~= nil and mission.hud or nil
|
||||||
|
if hud == nil or hud.isVisible == false then return true end
|
||||||
|
if g_sleepManager ~= nil and g_sleepManager.getIsSleeping ~= nil and g_sleepManager:getIsSleeping() then return true end
|
||||||
|
if hud.chatDisplay ~= nil and hud.chatDisplay.getVisible ~= nil and hud.chatDisplay:getVisible() then return true end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function render(layout, bg)
|
||||||
|
bg.middle:setDimension(layout.width, layout.height)
|
||||||
|
bg.middle:setPosition(layout.x, layout.y)
|
||||||
|
bg.middle:render()
|
||||||
|
bg.left:setDimension(layout.padding, layout.height)
|
||||||
|
bg.left:setPosition(layout.x - layout.padding, layout.y)
|
||||||
|
bg.left:render()
|
||||||
|
bg.right:setDimension(layout.padding, layout.height)
|
||||||
|
bg.right:setPosition(layout.x + layout.width, layout.y)
|
||||||
|
bg.right:render()
|
||||||
|
setTextAlignment(RenderText.ALIGN_LEFT)
|
||||||
|
setTextVerticalAlignment(RenderText.VERTICAL_ALIGN_TOP)
|
||||||
|
local textX, textY = layout.x + layout.marginX, layout.top - layout.marginY
|
||||||
|
for _, line in ipairs(ADStatusHud.lines) do
|
||||||
|
local color = line.color
|
||||||
|
setTextBold(line.header == true)
|
||||||
|
setTextColor(color[1], color[2], color[3], color[4])
|
||||||
|
renderText(textX, textY, layout.textSize, line.text)
|
||||||
|
textY = textY - layout.textSize
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADStatusHud.draw()
|
||||||
|
if not ADStatusHud.visible or ADStatusHud.layout == nil or #ADStatusHud.lines == 0 then return end
|
||||||
|
if shouldSkipDraw() then return end
|
||||||
|
local ok0, bg = pcall(createOverlays)
|
||||||
|
if not ok0 or bg == nil then
|
||||||
|
-- uden baggrund tegnes intet (g_overlayManager/HUD.COLOR mangler eller createOverlay fejlede)
|
||||||
|
logOnce("overlays", ok0 and "g_overlayManager eller HUD.COLOR.BACKGROUND mangler" or bg)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local ok, err = pcall(render, ADStatusHud.layout, bg)
|
||||||
|
-- tekststate nulstilles altid, også efter en fejl (andre mods/spillet tegner videre med den)
|
||||||
|
setTextColor(1, 1, 1, 1)
|
||||||
|
setTextAlignment(RenderText.ALIGN_LEFT)
|
||||||
|
setTextVerticalAlignment(RenderText.VERTICAL_ALIGN_BASELINE)
|
||||||
|
setTextBold(false)
|
||||||
|
if not ok then logOnce("draw", err) end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADStatusHud.delete()
|
||||||
|
deleteOverlays()
|
||||||
|
if PlayerInputComponent ~= nil and PlayerInputComponent.spuStatusToggleHandler == registerToggleActionRef then
|
||||||
|
PlayerInputComponent.spuStatusToggleHandler = nil
|
||||||
|
end
|
||||||
|
ADStatusHud.lines, ADStatusHud.layout = {}, nil
|
||||||
|
settingsLoaded = false
|
||||||
|
timerMs = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- tast: global spiller-handling (til fods og i køretøjer) ----
|
||||||
|
|
||||||
|
-- PlayerInputComponent.registerGlobalPlayerActionEvents kaldes ved hvert kontekstskift (til fods / i køretøj);
|
||||||
|
-- samme mønster som FS25_Courseplay (CP_OPEN_INGAME_MENU) og FS25_EasyDevControls. Hjælpeteksten skjules i F1.
|
||||||
|
local function registerToggleAction(component)
|
||||||
|
if not ADStatusHud.isClient() then return end
|
||||||
|
local player = component ~= nil and component.player or nil
|
||||||
|
if player ~= nil and player.isOwner == false then return end
|
||||||
|
if g_inputBinding == nil or InputAction == nil or InputAction.SPU_TOGGLE_STATUS == nil then return end
|
||||||
|
local _, eventId = g_inputBinding:registerActionEvent(InputAction.SPU_TOGGLE_STATUS, ADStatusHud, ADStatusHud.onToggleAction,
|
||||||
|
false, true, false, true)
|
||||||
|
if eventId ~= nil then g_inputBinding:setActionEventTextVisibility(eventId, false) end
|
||||||
|
end
|
||||||
|
|
||||||
|
registerToggleActionRef = function(component) guarded("registerAction", registerToggleAction, component) end
|
||||||
|
|
||||||
|
-- Spillets klasse overlever en genindlæsning af savegamet, moden gør ikke: krogen lægges kun én gang (markør på
|
||||||
|
-- klassen) og kalder den aktuelle indlæsnings handler, så en gammel ADStatusHud aldrig registrerer tasten igen.
|
||||||
|
function ADStatusHud.installInputHook()
|
||||||
|
if PlayerInputComponent == nil or PlayerInputComponent.registerGlobalPlayerActionEvents == nil or Utils == nil then return false end
|
||||||
|
PlayerInputComponent.spuStatusToggleHandler = registerToggleActionRef
|
||||||
|
if PlayerInputComponent.spuStatusToggleHooked then return true end
|
||||||
|
PlayerInputComponent.registerGlobalPlayerActionEvents = Utils.appendedFunction(PlayerInputComponent.registerGlobalPlayerActionEvents,
|
||||||
|
function(component, ...)
|
||||||
|
local handler = PlayerInputComponent.spuStatusToggleHandler
|
||||||
|
if handler ~= nil then handler(component) end
|
||||||
|
end)
|
||||||
|
PlayerInputComponent.spuStatusToggleHooked = true
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
if PlayerInputComponent ~= nil and Utils ~= nil then
|
||||||
|
ADStatusHud.installInputHook()
|
||||||
|
end
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
-- AD Status View
|
||||||
|
-- Ren logik for statusvinduet på skærmen (slås til/fra med en tast, se adStatusHud.lua): øjebliksbilledet
|
||||||
|
-- -> færdige linjer {text, level, header}. Sektioner i fast rækkefølge Flows/Marker/Kæder/Advarsler, kun med
|
||||||
|
-- indhold; højst maxLines linjer (resten "+k mere"); hver tekst højst MAX_CHARS tegn (UTF-8-sikkert, "..").
|
||||||
|
--
|
||||||
|
-- snapshot = {
|
||||||
|
-- flows = {{vehicle, action, fillPercent (0-100|nil), level}},
|
||||||
|
-- fields = {{fieldId, work, vehicle|nil, progress (0-1|nil), level}},
|
||||||
|
-- chains = {{text, level}}, -- standard rød
|
||||||
|
-- warnings = {{text, level}}, -- standard gul
|
||||||
|
-- }
|
||||||
|
-- level = "running"|"waiting"|"error"|"stopped" (farver: ADFlowView.LEVEL_COLORS); overskrifter har level "header".
|
||||||
|
|
||||||
|
ADStatusView = {}
|
||||||
|
|
||||||
|
ADStatusView.MAX_LINES = 18
|
||||||
|
ADStatusView.MAX_CHARS = 48
|
||||||
|
ADStatusView.VEHICLE_CHARS = 18 -- traktornavnet må ikke æde hele linjen
|
||||||
|
ADStatusView.MIN_FLEX_CHARS = 6
|
||||||
|
ADStatusView.SEPARATOR = " · "
|
||||||
|
ADStatusView.ELLIPSIS = ".."
|
||||||
|
|
||||||
|
local LEVELS = {running = true, waiting = true, error = true, stopped = true}
|
||||||
|
|
||||||
|
local SECTIONS = {
|
||||||
|
{key = "flows", header = "spu_status_flows", defaultLevel = "running"},
|
||||||
|
{key = "fields", header = "spu_status_fields", defaultLevel = "running"},
|
||||||
|
{key = "chains", header = "spu_status_chains", defaultLevel = "error"},
|
||||||
|
{key = "warnings", header = "spu_status_warnings", defaultLevel = "waiting"},
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ---- UTF-8-sikker længde/afkortning (æøå er flere bytes) ----
|
||||||
|
|
||||||
|
function ADStatusView.textLength(text)
|
||||||
|
local count = 0
|
||||||
|
for _ in tostring(text):gmatch("[^\128-\191]") do count = count + 1 end
|
||||||
|
return count
|
||||||
|
end
|
||||||
|
|
||||||
|
local function utf8Prefix(text, chars)
|
||||||
|
local result, count = {}, 0
|
||||||
|
for char in text:gmatch("[^\128-\191][\128-\191]*") do
|
||||||
|
if count >= chars then break end
|
||||||
|
table.insert(result, char)
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
return table.concat(result)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADStatusView.fit(text, chars)
|
||||||
|
text = tostring(text or "")
|
||||||
|
if ADStatusView.textLength(text) <= chars then return text end
|
||||||
|
local ellipsis = ADStatusView.ELLIPSIS
|
||||||
|
return utf8Prefix(text, math.max(0, chars - ADStatusView.textLength(ellipsis))) .. ellipsis
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Dele samlet med " · "; delen på flexIndex får den plads der er tilbage, så fx fyldprocenten bevares.
|
||||||
|
local function joinFitted(parts, flexIndex)
|
||||||
|
local sep = ADStatusView.SEPARATOR
|
||||||
|
local fixed = ADStatusView.textLength(sep) * (#parts - 1)
|
||||||
|
for index, part in ipairs(parts) do
|
||||||
|
if index ~= flexIndex then fixed = fixed + ADStatusView.textLength(part) end
|
||||||
|
end
|
||||||
|
local budget = math.max(ADStatusView.MIN_FLEX_CHARS, ADStatusView.MAX_CHARS - fixed)
|
||||||
|
local fitted = {}
|
||||||
|
for index, part in ipairs(parts) do
|
||||||
|
fitted[index] = index == flexIndex and ADStatusView.fit(part, budget) or part
|
||||||
|
end
|
||||||
|
return ADStatusView.fit(table.concat(fitted, sep), ADStatusView.MAX_CHARS)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function percentText(value)
|
||||||
|
return string.format("%d %%", math.floor(value + 0.5))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- linjer pr. sektion ----
|
||||||
|
|
||||||
|
local function flowText(entry)
|
||||||
|
local parts = {ADStatusView.fit(entry.vehicle or "?", ADStatusView.VEHICLE_CHARS), tostring(entry.action or "")}
|
||||||
|
if type(entry.fillPercent) == "number" then table.insert(parts, percentText(entry.fillPercent)) end
|
||||||
|
return joinFitted(parts, 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function fieldText(entry)
|
||||||
|
local parts = {string.format("F%s %s", tostring(entry.fieldId or "?"), tostring(entry.work or ""))}
|
||||||
|
if entry.vehicle ~= nil and entry.vehicle ~= "" then
|
||||||
|
table.insert(parts, ADStatusView.fit(entry.vehicle, ADStatusView.VEHICLE_CHARS))
|
||||||
|
end
|
||||||
|
if type(entry.progress) == "number" then table.insert(parts, percentText(entry.progress * 100)) end
|
||||||
|
return joinFitted(parts, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function plainText(entry)
|
||||||
|
return ADStatusView.fit(entry.text or "", ADStatusView.MAX_CHARS)
|
||||||
|
end
|
||||||
|
|
||||||
|
local TEXT_OF_SECTION = {flows = flowText, fields = fieldText, chains = plainText, warnings = plainText}
|
||||||
|
|
||||||
|
local function levelOf(entry, default)
|
||||||
|
return LEVELS[entry.level] and entry.level or default
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Alle linjer uden loft.
|
||||||
|
local function allLines(snapshot, textOf)
|
||||||
|
local lines = {}
|
||||||
|
for _, section in ipairs(SECTIONS) do
|
||||||
|
local entries = snapshot[section.key] or {}
|
||||||
|
if #entries > 0 then
|
||||||
|
table.insert(lines, {text = ADStatusView.fit(textOf(section.header), ADStatusView.MAX_CHARS), level = "header", header = true})
|
||||||
|
for _, entry in ipairs(entries) do
|
||||||
|
table.insert(lines, {text = TEXT_OF_SECTION[section.key](entry), level = levelOf(entry, section.defaultLevel), header = false})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return lines
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Afkort til maxLines: sidste plads bliver "+k mere" (k = skjulte indholdslinjer); en overskrift uden
|
||||||
|
-- indhold lige før afkortningen droppes.
|
||||||
|
local function capLines(lines, maxLines, textOf)
|
||||||
|
if #lines <= maxLines then return lines end
|
||||||
|
local kept = {}
|
||||||
|
for index = 1, math.max(0, maxLines - 1) do kept[index] = lines[index] end
|
||||||
|
while #kept > 0 and kept[#kept].header do table.remove(kept) end
|
||||||
|
local hidden = 0
|
||||||
|
for index = #kept + 1, #lines do
|
||||||
|
if not lines[index].header then hidden = hidden + 1 end
|
||||||
|
end
|
||||||
|
local ok, text = pcall(string.format, textOf("spu_status_more"), hidden)
|
||||||
|
table.insert(kept, {text = ADStatusView.fit(ok and text or ("+" .. hidden), ADStatusView.MAX_CHARS), level = "stopped", header = false})
|
||||||
|
return kept
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADStatusView.lines(snapshot, textOf, maxLines)
|
||||||
|
snapshot = snapshot or {}
|
||||||
|
textOf = textOf or function(key) return key end
|
||||||
|
maxLines = math.max(1, math.floor(maxLines or ADStatusView.MAX_LINES))
|
||||||
|
local lines = allLines(snapshot, textOf)
|
||||||
|
if #lines == 0 then
|
||||||
|
return {{text = ADStatusView.fit(textOf("spu_status_idle"), ADStatusView.MAX_CHARS), level = "stopped", header = false}}
|
||||||
|
end
|
||||||
|
return capLines(lines, maxLines, textOf)
|
||||||
|
end
|
||||||
@@ -258,15 +258,114 @@ function ADStorageStats.attentionList(stats, max)
|
|||||||
for _, stat in ipairs(stats or {}) do
|
for _, stat in ipairs(stats or {}) do
|
||||||
for _, entry in ipairs(attentionEntries(stat)) do table.insert(list, entry) end
|
for _, entry in ipairs(attentionEntries(stat)) do table.insert(list, entry) end
|
||||||
end
|
end
|
||||||
|
return ADStorageStats.rankAttention(list, max)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Kæde-forslag ligger ikke på en vare (ingen stat): navnet til den stabile rækkefølge tages fra teksten.
|
||||||
|
local function entryName(entry)
|
||||||
|
return tostring(entry.stat ~= nil and entry.stat.title or entry.text)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADStorageStats.rankAttention(list, max)
|
||||||
table.sort(list, function(a, b)
|
table.sort(list, function(a, b)
|
||||||
if a.score ~= b.score then return a.score > b.score end
|
if a.score ~= b.score then return a.score > b.score end
|
||||||
return tostring(a.stat.title) < tostring(b.stat.title)
|
return entryName(a) < entryName(b)
|
||||||
end)
|
end)
|
||||||
local result = {}
|
local result = {}
|
||||||
for index = 1, math.min(#list, max or #list) do result[index] = list[index] end
|
for index = 1, math.min(#list, max or #list) do result[index] = list[index] end
|
||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- "Kræver handling" fra kæden (ADChain.last): ÉT gult punkt "Kæde: N forslag" (klik -> fanen Kæder), så
|
||||||
|
-- forslagene ikke fylder listen. Rangeres under alle røde punkter (>= 3000), øverst blandt de gule, over "sælg nu".
|
||||||
|
ADStorageStats.CHAIN_SCORE = 2000
|
||||||
|
-- Manglende led: ALLE kædeproblemer i ÉN rød post ("Kæder: N manglende led", klik -> fanen Kæder med kæderne
|
||||||
|
-- foldet ud), så en gård med mange kæder (fx fermenteringssiloer) ikke fylder "Kræver handling". N = unikke
|
||||||
|
-- (kæde, vare). Score CHAIN_PROBLEM_SCORE + alvor (0-99): under "rød fuld" (3000-3100), så lagerets røde poster
|
||||||
|
-- altid vises først, og over de gule (<= 2000). Alvor = manglende andel af behovet; kredsløb = 99.
|
||||||
|
ADStorageStats.CHAIN_PROBLEM_SCORE = 2900
|
||||||
|
|
||||||
|
local function severity(problems)
|
||||||
|
local missing, need = 0, 0
|
||||||
|
for _, problem in ipairs(problems) do
|
||||||
|
if problem.kind == "cycle" then return 99 end
|
||||||
|
missing, need = missing + (problem.detail or 0), need + (problem.need or 0)
|
||||||
|
end
|
||||||
|
if need <= 0 then return 99 end
|
||||||
|
return math.floor(99 * math.min(1, missing / need))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function chainProblemEntries(last)
|
||||||
|
local known = {}
|
||||||
|
for _, root in ipairs(last.chain ~= nil and last.chain.roots or {}) do known[root.key] = true end
|
||||||
|
local problems, seen, rootKeys, rootSeen = {}, {}, {}, {}
|
||||||
|
for _, problem in ipairs(last.chain ~= nil and last.chain.problems or {}) do
|
||||||
|
if problem.rootKey ~= nil and known[problem.rootKey] then
|
||||||
|
table.insert(problems, problem)
|
||||||
|
seen[tostring(problem.rootKey) .. "|" .. tostring(problem.fillType)] = true
|
||||||
|
if not rootSeen[problem.rootKey] then
|
||||||
|
rootSeen[problem.rootKey] = true
|
||||||
|
table.insert(rootKeys, problem.rootKey)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #problems == 0 then return {} end
|
||||||
|
local count = 0
|
||||||
|
for _ in pairs(seen) do count = count + 1 end
|
||||||
|
table.sort(rootKeys, function(a, b) return tostring(a) < tostring(b) end)
|
||||||
|
return {{kind = "chainProblem", level = "red", score = ADStorageStats.CHAIN_PROBLEM_SCORE + severity(problems), count = count,
|
||||||
|
rootKeys = rootKeys, text = string.format("Kæder: %d", count)}}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- "Slukket men nødvendig" (ADChain.last.dependency.offNeeded): højst MAX_DEPENDENCY_ENTRIES post (den mest hastende
|
||||||
|
-- fabrik; more = antal skjulte, vist som "+k"). Score DEPENDENCY_SCORE + hast (0-99, færre lagerdage = højere;
|
||||||
|
-- ukendt = 50): over "rød fuld" (3000-3100), under "rød tom" (3900-4000).
|
||||||
|
ADStorageStats.DEPENDENCY_SCORE = 3700
|
||||||
|
ADStorageStats.MAX_DEPENDENCY_ENTRIES = 1
|
||||||
|
|
||||||
|
local function urgency(days)
|
||||||
|
if days == nil then return 50 end
|
||||||
|
if days == math.huge then return 0 end
|
||||||
|
return math.max(0, 99 - math.min(99, math.floor(days)))
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADStorageStats.dependencyEntries(last)
|
||||||
|
local entries = {}
|
||||||
|
for _, item in ipairs(last ~= nil and last.dependency ~= nil and last.dependency.offNeeded or {}) do
|
||||||
|
local name = tostring(item.buildingName or item.building or "?")
|
||||||
|
table.insert(entries, {kind = "dependencyOff", level = "red", score = ADStorageStats.DEPENDENCY_SCORE + urgency(item.days),
|
||||||
|
building = item.building, buildingName = name, fillType = item.fillType, onlyChain = item.onlyChain == true, text = name})
|
||||||
|
end
|
||||||
|
entries = ADStorageStats.rankAttention(entries)
|
||||||
|
local result = {}
|
||||||
|
for index = 1, math.min(#entries, ADStorageStats.MAX_DEPENDENCY_ENTRIES) do result[index] = entries[index] end
|
||||||
|
if #entries > #result then result[#result].more = #entries - #result end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADStorageStats.chainEntries(last)
|
||||||
|
if last == nil then return {} end
|
||||||
|
local result = chainProblemEntries(last)
|
||||||
|
for _, entry in ipairs(ADStorageStats.dependencyEntries(last)) do table.insert(result, entry) end
|
||||||
|
local count = 0
|
||||||
|
for _, action in ipairs(last.actions or {}) do
|
||||||
|
if action.mode == "suggest" then count = count + 1 end
|
||||||
|
end
|
||||||
|
if count > 0 then
|
||||||
|
table.insert(result, {kind = "chainSuggest", level = "yellow", score = ADStorageStats.CHAIN_SCORE, count = count,
|
||||||
|
text = string.format("Kæde: %d", count)})
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Varepunkterne (allerede rangeret) + kædens punkter, samme loft.
|
||||||
|
function ADStorageStats.mergeAttention(attention, chainEntries, max)
|
||||||
|
local list = {}
|
||||||
|
for _, entry in ipairs(attention or {}) do table.insert(list, entry) end
|
||||||
|
for _, entry in ipairs(chainEntries or {}) do table.insert(list, entry) end
|
||||||
|
return ADStorageStats.rankAttention(list, max)
|
||||||
|
end
|
||||||
|
|
||||||
function ADStorageStats.kpis(stats)
|
function ADStorageStats.kpis(stats)
|
||||||
local kpi = {valueNow = 0, gain = 0, emptyCount = 0, fullCount = 0}
|
local kpi = {valueNow = 0, gain = 0, emptyCount = 0, fullCount = 0}
|
||||||
for _, stat in ipairs(stats or {}) do
|
for _, stat in ipairs(stats or {}) do
|
||||||
|
|||||||
@@ -53,6 +53,16 @@ function ADStuckDiag.describe(vehicle)
|
|||||||
return table.concat(parts, " ")
|
return table.concat(parts, " ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Kun læsning (statusvinduet): køretøjer der har holdt stille mindst minMs -> {{vehicle, ms}}, længst først.
|
||||||
|
function ADStuckDiag.stillVehicles(minMs)
|
||||||
|
local list = {}
|
||||||
|
for vehicle, ms in pairs(stillMs) do
|
||||||
|
if ms >= (minMs or ADStuckDiag.STILL_MS) then table.insert(list, {vehicle = vehicle, ms = ms}) end
|
||||||
|
end
|
||||||
|
table.sort(list, function(a, b) return a.ms > b.ms end)
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
|
||||||
function ADStuckDiag.tick(dt)
|
function ADStuckDiag.tick(dt)
|
||||||
timerMs = timerMs + (dt or 0)
|
timerMs = timerMs + (dt or 0)
|
||||||
if timerMs < ADStuckDiag.CHECK_INTERVAL_MS then return end
|
if timerMs < ADStuckDiag.CHECK_INTERVAL_MS then return end
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
-- AD Tip Side
|
||||||
|
-- Tipvogne med flere tip-sider (fx Farmtech EDK 650 Unrealistic: bag / kornluge / venstre / højre) tillader kun
|
||||||
|
-- AI-aflæsning (allowAIDischarge) på nogle af udløbene. Står vognen på en side uden, åbner AD aflæsningen ved
|
||||||
|
-- målet, men intet løber ud — vognen holder med fuldt læs (in-game 29/9: Chicken Food 2 med 786.712 l hvede på
|
||||||
|
-- højre side ved en Farma med 10 mio. l fri; Disponibel Vogn 3 fuld af løg). Når AD kører en vogn, skiftes den til
|
||||||
|
-- en side, AI må aflæsse fra.
|
||||||
|
|
||||||
|
ADTipSide = {}
|
||||||
|
ADTipSide.LOG_PREFIX = "ADSmartPickup: tip-side: "
|
||||||
|
|
||||||
|
-- ---- ren logik ----
|
||||||
|
|
||||||
|
-- sides = {{node = udløbsindeks}}, allowAI = {[udløbsindeks] = true}, current = nuværende side
|
||||||
|
-- -> sideindeks der skal vælges, eller nil (nuværende er fin / ingen side tilladt / ukendt)
|
||||||
|
function ADTipSide.choose(sides, allowAI, current)
|
||||||
|
local currentSide = (sides or {})[current]
|
||||||
|
if currentSide ~= nil and (allowAI or {})[currentSide.node] then return nil end
|
||||||
|
for index, side in ipairs(sides or {}) do
|
||||||
|
if (allowAI or {})[side.node] then return index end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- spil ----
|
||||||
|
|
||||||
|
local warned = setmetatable({}, {__mode = "k"})
|
||||||
|
|
||||||
|
local function sidesOf(object)
|
||||||
|
local spec = object.spec_trailer
|
||||||
|
local dis = object.spec_dischargeable
|
||||||
|
if spec == nil or dis == nil or spec.tipSides == nil or #spec.tipSides < 2 then return nil end
|
||||||
|
local sides, allowAI = {}, {}
|
||||||
|
for index, side in ipairs(spec.tipSides) do sides[index] = {node = side.dischargeNodeIndex} end
|
||||||
|
for index, node in ipairs(dis.dischargeNodes or {}) do
|
||||||
|
if node.allowAIDischarge then allowAI[index] = true end
|
||||||
|
end
|
||||||
|
return sides, allowAI, spec.preferedTipSideIndex
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Alle tipvogne bag traktoren: skift til en AI-tilladt side (kun mens vognen ikke tipper).
|
||||||
|
function ADTipSide.ensure(vehicle)
|
||||||
|
local children = vehicle.getChildVehicles ~= nil and vehicle:getChildVehicles() or {}
|
||||||
|
for _, object in pairs(children) do
|
||||||
|
if object ~= vehicle and object.setPreferedTipSide ~= nil then
|
||||||
|
local sides, allowAI, current = sidesOf(object)
|
||||||
|
local tipping = Trailer ~= nil and object.getTipState ~= nil and object:getTipState() ~= Trailer.TIPSTATE_CLOSED
|
||||||
|
local index = sides ~= nil and not tipping and ADTipSide.choose(sides, allowAI, current) or nil
|
||||||
|
if index ~= nil then
|
||||||
|
object:setPreferedTipSide(index)
|
||||||
|
if not warned[object] then
|
||||||
|
warned[object] = true
|
||||||
|
Logging.info(ADTipSide.LOG_PREFIX .. "'%s': tip-side %s -> %d (AutoDrive må kun aflæse fra den side)",
|
||||||
|
tostring(object.getName ~= nil and object:getName() or "?"), tostring(current), index)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -426,6 +426,9 @@ function ADUnloadWait.getClaims(adEnv)
|
|||||||
return ADWaitPool.getClaims(adEnv)
|
return ADWaitPool.getClaims(adEnv)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
ADUnloadWait.TIP_CHECK_MS = 2000
|
||||||
|
local tipChecked = setmetatable({}, {__mode = "k"})
|
||||||
|
|
||||||
function ADUnloadWait.install(adEnv)
|
function ADUnloadWait.install(adEnv)
|
||||||
local task, autoDrive = adEnv.UnloadAtDestinationTask, adEnv.AutoDrive
|
local task, autoDrive = adEnv.UnloadAtDestinationTask, adEnv.AutoDrive
|
||||||
if isInstalled or task == nil or task.update == nil or autoDrive == nil or autoDrive.getSetting == nil then
|
if isInstalled or task == nil or task.update == nil or autoDrive == nil or autoDrive.getSetting == nil then
|
||||||
@@ -438,6 +441,15 @@ function ADUnloadWait.install(adEnv)
|
|||||||
local originalUpdate, originalFinished, originalGetSetting = task.update, task.finished, autoDrive.getSetting
|
local originalUpdate, originalFinished, originalGetSetting = task.update, task.finished, autoDrive.getSetting
|
||||||
|
|
||||||
task.update = function(self, dt)
|
task.update = function(self, dt)
|
||||||
|
-- alle aflæsningsture (også udkørsel og tømning): tipvognen på en side AD må aflæsse fra (EDK 650, 29/9)
|
||||||
|
if ADTipSide ~= nil and self.vehicle ~= nil then
|
||||||
|
local now = g_time or 0
|
||||||
|
if (tipChecked[self.vehicle] or -math.huge) + ADUnloadWait.TIP_CHECK_MS <= now then
|
||||||
|
tipChecked[self.vehicle] = now
|
||||||
|
local okTip, errTip = pcall(ADTipSide.ensure, self.vehicle)
|
||||||
|
if not okTip then Logging.warning(ADUnloadWait.LOG_PREFIX .. "tip-side: %s", tostring(errTip)) end
|
||||||
|
end
|
||||||
|
end
|
||||||
local ok, err = pcall(ADUnloadWait.beforeUpdate, adEnv, self, dt)
|
local ok, err = pcall(ADUnloadWait.beforeUpdate, adEnv, self, dt)
|
||||||
if not ok then
|
if not ok then
|
||||||
Logging.warning(ADUnloadWait.LOG_PREFIX .. "fejl i Wait-punkt-logik, AutoDrive fortsætter selv: %s", tostring(err))
|
Logging.warning(ADUnloadWait.LOG_PREFIX .. "fejl i Wait-punkt-logik, AutoDrive fortsætter selv: %s", tostring(err))
|
||||||
|
|||||||
@@ -4,6 +4,14 @@
|
|||||||
-- siges det at et flow ikke hjælper (producér/køb). Afgørelsen er ADStorageStats.flowAdvice (testet).
|
-- siges det at et flow ikke hjælper (producér/køb). Afgørelsen er ADStorageStats.flowAdvice (testet).
|
||||||
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
|
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
|
||||||
|
|
||||||
|
-- Lagerfanen for en bygningstype (TAB_KINDS), fx "factory" -> fanen Fabrikker.
|
||||||
|
function SmartPickupFrame.tabOfKind(kind)
|
||||||
|
for tab, tabKind in pairs(SmartPickupFrame.TAB_KINDS or {}) do
|
||||||
|
if tabKind == kind then return tab end
|
||||||
|
end
|
||||||
|
return SmartPickupFrame.TAB_OVERVIEW
|
||||||
|
end
|
||||||
|
|
||||||
local function fillTypeName(fillType)
|
local function fillTypeName(fillType)
|
||||||
return g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or tostring(fillType)
|
return g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or tostring(fillType)
|
||||||
end
|
end
|
||||||
@@ -62,6 +70,10 @@ end
|
|||||||
|
|
||||||
-- entry fra ADStorageStats.attentionList -> advice ("open"|"flow"|"produce"|"none"), info
|
-- entry fra ADStorageStats.attentionList -> advice ("open"|"flow"|"produce"|"none"), info
|
||||||
function SmartPickupFrame:attentionAdvice(entry)
|
function SmartPickupFrame:attentionAdvice(entry)
|
||||||
|
-- kæde-forslag og manglende led har ingen vare (stat): klik fører til fanen Kæder
|
||||||
|
if entry ~= nil and (entry.kind == "chainSuggest" or entry.kind == "chainProblem") then return "chain", nil end
|
||||||
|
-- slukket men nødvendig: fanen Fabrikker (Kæder når kun en kæde bruger varen)
|
||||||
|
if entry ~= nil and entry.kind == "dependencyOff" then return entry.onlyChain and "chain" or "factories", nil end
|
||||||
if entry ~= nil and entry.kind == "sell" and entry.stat ~= nil then return sellAdviceFor(entry.stat) end
|
if entry ~= nil and entry.kind == "sell" and entry.stat ~= nil then return sellAdviceFor(entry.stat) end
|
||||||
if entry == nil or entry.kind ~= "empty" or entry.stat == nil then return "none", nil end
|
if entry == nil or entry.kind ~= "empty" or entry.stat == nil then return "none", nil end
|
||||||
local stat = entry.stat
|
local stat = entry.stat
|
||||||
@@ -83,6 +95,8 @@ function SmartPickupFrame:attentionAdvice(entry)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function SmartPickupFrame:attentionHint(advice, info)
|
function SmartPickupFrame:attentionHint(advice, info)
|
||||||
|
if advice == "chain" then return self.i18n:getText("spu_attention_hintChain") end
|
||||||
|
if advice == "factories" then return self.i18n:getText("spu_attention_hintFactories") end
|
||||||
if advice == "open" then return string.format(self.i18n:getText("spu_attention_hintOpen"), tostring(info.run.name)) end
|
if advice == "open" then return string.format(self.i18n:getText("spu_attention_hintOpen"), tostring(info.run.name)) end
|
||||||
if advice == "flow" then return self.i18n:getText(info.sell and "spu_attention_hintSellFlow" or "spu_attention_hintFlow") end
|
if advice == "flow" then return self.i18n:getText(info.sell and "spu_attention_hintSellFlow" or "spu_attention_hintFlow") end
|
||||||
if advice == "produce" then return self.i18n:getText("spu_attention_hintProduce") end
|
if advice == "produce" then return self.i18n:getText("spu_attention_hintProduce") end
|
||||||
@@ -91,6 +105,19 @@ end
|
|||||||
|
|
||||||
function SmartPickupFrame:onAttentionClick(entry)
|
function SmartPickupFrame:onAttentionClick(entry)
|
||||||
self:safe(function(frame)
|
self:safe(function(frame)
|
||||||
|
if entry ~= nil and (entry.kind == "chainSuggest" or entry.kind == "chainProblem") then
|
||||||
|
-- manglende led: kæderne med problemer foldes ud, så problemteksten står under den røde linje
|
||||||
|
if entry.kind == "chainProblem" then
|
||||||
|
frame.chainExpanded = frame.chainExpanded or {}
|
||||||
|
for _, rootKey in ipairs(entry.rootKeys or {}) do frame.chainExpanded[rootKey] = true end
|
||||||
|
end
|
||||||
|
frame:switchTab(SmartPickupFrame.TAB_CHAINS)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if entry ~= nil and entry.kind == "dependencyOff" then
|
||||||
|
frame:switchTab(entry.onlyChain and SmartPickupFrame.TAB_CHAINS or SmartPickupFrame.tabOfKind("factory"))
|
||||||
|
return
|
||||||
|
end
|
||||||
local adEnv = frame.getAdEnv()
|
local adEnv = frame.getAdEnv()
|
||||||
local advice, info = frame:attentionAdvice(entry)
|
local advice, info = frame:attentionAdvice(entry)
|
||||||
if adEnv == nil or advice == "none" then return end
|
if adEnv == nil or advice == "none" then return end
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
-- Smart Pickup — fanen Kæder
|
||||||
|
-- Produktionskæderne som træ: rod (produktion spilleren har tændt) -> varer -> råvarer, med kilde, behov pr.
|
||||||
|
-- måned og flowstatus; afgrødeforslag kan godkendes/afvises og en rod sættes på pause. Rækkerne bygges af
|
||||||
|
-- ADChainView (testet); her kun kloning, farve og klik. Et resultat yngre end CHAIN_MAX_AGE_MS (fra kædens eget
|
||||||
|
-- tick eller en tidligere visning) genbruges; ellers evalueres (visning, handler ikke). Klik evaluerer altid.
|
||||||
|
|
||||||
|
SmartPickupFrame.CHAIN_MAX_AGE_MS = 30000
|
||||||
|
|
||||||
|
function SmartPickupFrame:chainLast(adEnv, force)
|
||||||
|
local age = ADChain.lastAge ~= nil and ADChain.lastAge() or math.huge
|
||||||
|
if force or ADChain.last == nil or age >= SmartPickupFrame.CHAIN_MAX_AGE_MS then
|
||||||
|
ADChain.evaluate(adEnv)
|
||||||
|
end
|
||||||
|
return ADChain.last
|
||||||
|
end
|
||||||
|
|
||||||
|
function SmartPickupFrame:chainTitle(last, name)
|
||||||
|
local title = last ~= nil and last.titles ~= nil and last.titles[name] or nil
|
||||||
|
return title or self:fruitTitle(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Klik på en kæde-knap: udfør, evaluér straks (ny visning) og genopbyg. Et forslag der ikke længere findes
|
||||||
|
-- (approve/dismiss -> false) meldes i statuslinjen som i "Kræver handling".
|
||||||
|
function SmartPickupFrame:onChainAction(action)
|
||||||
|
local ok = true
|
||||||
|
if action.kind == "approve" then
|
||||||
|
ok = ADChain.approve(action.fieldId)
|
||||||
|
elseif action.kind == "dismiss" then
|
||||||
|
ok = ADChain.dismiss(action.fieldId)
|
||||||
|
elseif action.kind == "optIn" or action.kind == "optOut" then
|
||||||
|
ADChain.setOptIn(action.rootKey, action.kind == "optIn")
|
||||||
|
elseif action.kind == "finalMode" then
|
||||||
|
ADChain.setFinalKeep(action.rootKey, action.keep)
|
||||||
|
elseif action.kind == "pause" or action.kind == "resume" then
|
||||||
|
ADChain.setPaused(action.rootKey, action.kind == "pause")
|
||||||
|
end
|
||||||
|
if ok == false then
|
||||||
|
local text = self.i18n:getText("spu_chain_gone")
|
||||||
|
self.statusOverride, self.statusOverrideTicks = text, SmartPickupFrame.STATUS_OVERRIDE_TICKS
|
||||||
|
if ADFieldJobs ~= nil and ADFieldJobs.notify ~= nil then ADFieldJobs.notify(text, false) end
|
||||||
|
end
|
||||||
|
local adEnv = self.getAdEnv()
|
||||||
|
if adEnv ~= nil then self:chainLast(adEnv, true) end
|
||||||
|
self:requestRebuild()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Klik på en varelinje: åbn flowet der kører varen til modtageren (kædens eget flow til bygningen, ellers et flow
|
||||||
|
-- der dækker varen) i Flows-fanens redigeringsvindue; uden flow en besked.
|
||||||
|
function SmartPickupFrame:openChainFlow(consumer, fillType)
|
||||||
|
local runs = ADChain.runsView(ADRunsController.store.runs)
|
||||||
|
local runId = ADChainActions.chainRunFor(runs, consumer)
|
||||||
|
if runId == nil then
|
||||||
|
for _, run in ipairs(runs) do
|
||||||
|
if runId == nil and ADChainActions.isCovered({consumer = consumer, fillType = fillType}, {run}) then runId = run.id end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if runId == nil then
|
||||||
|
self.statusOverride = self.i18n:getText("spu_chain_noFlow")
|
||||||
|
self.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS
|
||||||
|
self:requestRebuild()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self.selectedRunId = runId
|
||||||
|
self:switchTab(SmartPickupFrame.TAB_FLOWS)
|
||||||
|
self:openEditPanel("flow", runId)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Klik på rodens linje uden for knappen: fold ud/sammen (huskes kun mens spillet kører).
|
||||||
|
function SmartPickupFrame:toggleChainRoot(rootKey)
|
||||||
|
self.chainExpanded = self.chainExpanded or {}
|
||||||
|
self.chainExpanded[rootKey] = not self.chainExpanded[rootKey] or nil
|
||||||
|
self:requestRebuild()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Hvilken celle blev der klikket i? Højeste celle hvis venstre kant ligger til venstre for klikket.
|
||||||
|
local function clickedCell(frame, cells)
|
||||||
|
if frame.lastClickX == nil then return nil end
|
||||||
|
for index = #cells, 1, -1 do
|
||||||
|
local cell = cells[index]
|
||||||
|
if cell ~= nil and cell.absPosition ~= nil and frame.lastClickX >= cell.absPosition[1] then return index end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Række med prik + c1..c5 (flowListPrefab). Knapper er celler (entry.actions[celle], som [Start]/[Stop] i
|
||||||
|
-- Flows-listen); et klik andetsteds på en rodlinje folder den ud/sammen.
|
||||||
|
function SmartPickupFrame:addChainRow(entry)
|
||||||
|
local unpackFn = table.unpack or unpack
|
||||||
|
local row = self:addRow(self.flowListPrefab, {kind = "chainRow"})
|
||||||
|
local cells = {}
|
||||||
|
for index = 1, 5 do cells[index] = row:getDescendantByName("c" .. index) end
|
||||||
|
SmartPickupFrame.setCells(cells, entry.cells)
|
||||||
|
local dot = row:getDescendantByName("dot")
|
||||||
|
local color = entry.level ~= nil and ADFlowView.LEVEL_COLORS[entry.level] or nil
|
||||||
|
if dot ~= nil and dot.setImageColor ~= nil and color ~= nil then dot:setImageColor(nil, unpackFn(color)) end
|
||||||
|
-- kun rod og varer har en status-prik (overskrift, forslag og auto-skift har ingen)
|
||||||
|
if dot ~= nil and dot.setVisible ~= nil and entry.kind ~= "root" and entry.kind ~= "node" then dot:setVisible(false) end
|
||||||
|
if cells[4] ~= nil and cells[4].setTextColor ~= nil and color ~= nil and entry.level ~= "running" and entry.kind ~= "suggest" then
|
||||||
|
cells[4]:setTextColor(unpackFn(color))
|
||||||
|
end
|
||||||
|
local actions, toggle = entry.actions or {}, entry.toggle
|
||||||
|
if next(actions) == nil and toggle == nil and entry.openConsumer ~= nil then
|
||||||
|
self:makeRowClickable(row, function(frame) frame:openChainFlow(entry.openConsumer, entry.openFillType) end)
|
||||||
|
elseif next(actions) ~= nil or toggle ~= nil then
|
||||||
|
self:makeRowClickable(row, function(frame)
|
||||||
|
local action = actions[clickedCell(frame, cells) or 0]
|
||||||
|
if action ~= nil then
|
||||||
|
frame:onChainAction(action)
|
||||||
|
elseif toggle ~= nil then
|
||||||
|
frame:toggleChainRoot(toggle)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
return row
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Manglende led: problemteksten som (ombrudt) tekstrække i rød under varelinjen.
|
||||||
|
function SmartPickupFrame:addChainProblemRow(entry)
|
||||||
|
local unpackFn = table.unpack or unpack
|
||||||
|
local color = ADFlowView.LEVEL_COLORS ~= nil and ADFlowView.LEVEL_COLORS[entry.level] or nil
|
||||||
|
for _, line in ipairs(ADFlowView.wrapText(entry.text, SmartPickupFrame.TEXT_ROW_CHARS)) do
|
||||||
|
local title = self:addRow(self.textPrefab, {kind = "text"}):getDescendantByName("title")
|
||||||
|
title:setText(line)
|
||||||
|
if color ~= nil and title.setTextColor ~= nil then title:setTextColor(unpackFn(color)) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function SmartPickupFrame:fillChainRows(adEnv)
|
||||||
|
local i18n = self.i18n
|
||||||
|
local last = self:chainLast(adEnv, false)
|
||||||
|
local rows = ADChainView.rows(last, function(name) return self:chainTitle(last, name) end,
|
||||||
|
SmartPickupFrame.formatCompact, function(key) return i18n:getText(key) end, self.chainExpanded or {})
|
||||||
|
for _, entry in ipairs(rows) do
|
||||||
|
if entry.kind == "info" or entry.kind == "empty" then
|
||||||
|
self:addTextRow(entry.text)
|
||||||
|
elseif entry.kind == "problem" then
|
||||||
|
self:addChainProblemRow(entry)
|
||||||
|
else
|
||||||
|
self:addChainRow(entry)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -307,16 +307,16 @@ function SmartPickupFrame:fillPoolRows(adEnv)
|
|||||||
self:addTextRow(i18n:getText("spu_pool_section_tractors"))
|
self:addTextRow(i18n:getText("spu_pool_section_tractors"))
|
||||||
for _, tractor in ipairs(tractors) do
|
for _, tractor in ipairs(tractors) do
|
||||||
local row = self:addRow(self.vehiclePrefab, {kind = "fleetVehicle", vehicleId = tractor.id})
|
local row = self:addRow(self.vehiclePrefab, {kind = "fleetVehicle", vehicleId = tractor.id})
|
||||||
local status = string.format(i18n:getText("spu_pool_tractor_" .. tractor.status), tostring(tractor.arg))
|
local status = string.format(i18n:getText("spu_pool_tractor_" .. tractor.status), ADFlowView.shortVehicleName(tractor.arg))
|
||||||
local hp = tractor.hp ~= nil and string.format(", %.0f hk", tractor.hp) or ""
|
local hp = tractor.hp ~= nil and string.format(", %.0f hk", tractor.hp) or ""
|
||||||
row:getDescendantByName("title"):setText(string.format(" %s%s — %s", tostring(tractor.label), hp, status))
|
row:getDescendantByName("title"):setText(string.format(" %s%s — %s", ADFlowView.shortVehicleName(tractor.label), hp, status))
|
||||||
row:getDescendantByName("option"):setState(tractor.enabled and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
|
row:getDescendantByName("option"):setState(tractor.enabled and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
|
||||||
end
|
end
|
||||||
if #tractors == 0 then self:addTextRow(" " .. i18n:getText("spu_pool_noTractors")) end
|
if #tractors == 0 then self:addTextRow(" " .. i18n:getText("spu_pool_noTractors")) end
|
||||||
self:addTextRow(i18n:getText("spu_pool_section_implements"))
|
self:addTextRow(i18n:getText("spu_pool_section_implements"))
|
||||||
for _, implement in ipairs(implements) do
|
for _, implement in ipairs(implements) do
|
||||||
local status = string.format(i18n:getText("spu_pool_implement_" .. implement.status), tostring(implement.arg))
|
local status = string.format(i18n:getText("spu_pool_implement_" .. implement.status), ADFlowView.shortVehicleName(implement.arg))
|
||||||
self:addTextRow(string.format(" %s — %s: %s", tostring(implement.label), self:roleText(implement.role), status))
|
self:addTextRow(string.format(" %s — %s: %s", ADFlowView.shortVehicleName(implement.label), self:roleText(implement.role), status))
|
||||||
end
|
end
|
||||||
if #implements == 0 then self:addTextRow(" " .. i18n:getText("spu_pool_noImplements")) end
|
if #implements == 0 then self:addTextRow(" " .. i18n:getText("spu_pool_noImplements")) end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -232,9 +232,7 @@ function SmartPickupFrame:fillFlowTabRows(adEnv)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
self:addTextRow(i18n:getText("spu_flowList_hint"))
|
self:addTextRow(i18n:getText("spu_flowList_hint"))
|
||||||
for _, group in ipairs(ADFlowView.boardGroups(ADRunsController.store.runs)) do
|
local function addFlowRow(run)
|
||||||
self:addTextRow(i18n:getText(group.kind == "outbound" and "spu_board_outbound" or "spu_board_supply"))
|
|
||||||
for _, run in ipairs(group.runs) do
|
|
||||||
local row = self:addRow(self.flowListPrefab, {kind = "flowList"})
|
local row = self:addRow(self.flowListPrefab, {kind = "flowList"})
|
||||||
local entry = {runId = run.id, row = row, dot = row:getDescendantByName("dot"),
|
local entry = {runId = run.id, row = row, dot = row:getDescendantByName("dot"),
|
||||||
cells = {row:getDescendantByName("c1"), row:getDescendantByName("c2"), row:getDescendantByName("c3"),
|
cells = {row:getDescendantByName("c1"), row:getDescendantByName("c2"), row:getDescendantByName("c3"),
|
||||||
@@ -253,6 +251,22 @@ function SmartPickupFrame:fillFlowTabRows(adEnv)
|
|||||||
end)
|
end)
|
||||||
table.insert(self.flowListRows, entry)
|
table.insert(self.flowListRows, entry)
|
||||||
end
|
end
|
||||||
|
for _, group in ipairs(ADFlowView.boardGroups(ADRunsController.store.runs)) do
|
||||||
|
self:addTextRow(i18n:getText(group.chain and "spu_board_chain" or group.kind == "outbound" and "spu_board_outbound" or "spu_board_supply"))
|
||||||
|
local runs, idle = group.runs, {}
|
||||||
|
-- kædeflows uden traktor (venter på en ledig traktor/vogn) foldes sammen (in-game 30/9)
|
||||||
|
if group.chain then runs, idle = ADFlowView.splitIdle(group.runs) end
|
||||||
|
for _, run in ipairs(runs) do addFlowRow(run) end
|
||||||
|
if #idle > 0 then
|
||||||
|
local key = self.showIdleChainFlows and "spu_board_chainIdleHide" or "spu_board_chainIdleShow"
|
||||||
|
self:addLinkRow(" " .. string.format(i18n:getText(key), #idle), function(frame)
|
||||||
|
frame.showIdleChainFlows = not frame.showIdleChainFlows
|
||||||
|
frame:requestRebuild()
|
||||||
|
end)
|
||||||
|
if self.showIdleChainFlows then
|
||||||
|
for _, run in ipairs(idle) do addFlowRow(run) end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
self:refreshFlowListRows(adEnv)
|
self:refreshFlowListRows(adEnv)
|
||||||
end
|
end
|
||||||
@@ -266,7 +280,7 @@ function SmartPickupFrame:refreshFlowListRows(adEnv)
|
|||||||
local cells = self:flowCells(adEnv, run)
|
local cells = self:flowCells(adEnv, run)
|
||||||
local color = ADFlowView.LEVEL_COLORS[self:flowLevel(adEnv, run)]
|
local color = ADFlowView.LEVEL_COLORS[self:flowLevel(adEnv, run)]
|
||||||
local canStop = ADRunsController.getRunStatus(adEnv, run).canStop
|
local canStop = ADRunsController.getRunStatus(adEnv, run).canStop
|
||||||
entry.cells[1]:setText(tostring(run.name))
|
entry.cells[1]:setText(ADFlowView.listName(run))
|
||||||
entry.cells[2]:setText(tostring(cells[2]))
|
entry.cells[2]:setText(tostring(cells[2]))
|
||||||
entry.cells[3]:setText(tostring(cells[5]))
|
entry.cells[3]:setText(tostring(cells[5]))
|
||||||
entry.cells[4]:setText(tostring(cells[6]))
|
entry.cells[4]:setText(tostring(cells[6]))
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ function SmartPickupFrame:flowCells(adEnv, run)
|
|||||||
if row.kind == "outbound" then
|
if row.kind == "outbound" then
|
||||||
local sells, stores = ADFlowView.policyUse(row.policies)
|
local sells, stores = ADFlowView.policyUse(row.policies)
|
||||||
local parts = {}
|
local parts = {}
|
||||||
if sells then table.insert(parts, string.format(i18n:getText("spu_flow_ruleSell"), row.sellAtPercent or 90)) end
|
if sells then table.insert(parts, row.sellAtPercent == 0 and i18n:getText("spu_flow_ruleSellAlways") or string.format(i18n:getText("spu_flow_ruleSell"), row.sellAtPercent or 90)) end
|
||||||
if stores then table.insert(parts, string.format(i18n:getText("spu_flow_ruleStore"), row.storeAbovePercent or 80)) end
|
if stores then table.insert(parts, string.format(i18n:getText("spu_flow_ruleStore"), row.storeAbovePercent or 80)) end
|
||||||
rule = table.concat(parts, ", ")
|
rule = table.concat(parts, ", ")
|
||||||
if row.collectRound then rule = rule .. ", " .. i18n:getText("spu_flow_round") end
|
if row.collectRound then rule = rule .. ", " .. i18n:getText("spu_flow_round") end
|
||||||
@@ -64,7 +64,9 @@ function SmartPickupFrame:flowCells(adEnv, run)
|
|||||||
elseif row.reason ~= nil then
|
elseif row.reason ~= nil then
|
||||||
status = row.reason
|
status = row.reason
|
||||||
end
|
end
|
||||||
local name = (run.id == self.selectedRunId and "> " or "") .. tostring(run.name)
|
-- kædeflow uden traktor: sig hvorfor det står stille i stedet for flowets regel
|
||||||
|
if ADFlowView.isIdleChain(run) then status = i18n:getText("spu_flow_chainIdle") end
|
||||||
|
local name = (run.id == self.selectedRunId and "> " or "") .. ADFlowView.listName(run)
|
||||||
return {name, products, from, to, string.format("%d/%d", row.running, row.total), status}
|
return {name, products, from, to, string.format("%d/%d", row.running, row.total), status}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -103,9 +105,7 @@ function SmartPickupFrame:fillOverviewRows(adEnv)
|
|||||||
local i18n = self.i18n
|
local i18n = self.i18n
|
||||||
self:addTextRow(i18n:getText("spu_overview_hint"))
|
self:addTextRow(i18n:getText("spu_overview_hint"))
|
||||||
self:addCellRow(self.flowPrefab, {i18n:getText("spu_col_flow"), i18n:getText("spu_col_goods"), i18n:getText("spu_col_from"), i18n:getText("spu_col_to"), i18n:getText("spu_col_tractors"), i18n:getText("spu_col_status")})
|
self:addCellRow(self.flowPrefab, {i18n:getText("spu_col_flow"), i18n:getText("spu_col_goods"), i18n:getText("spu_col_from"), i18n:getText("spu_col_to"), i18n:getText("spu_col_tractors"), i18n:getText("spu_col_status")})
|
||||||
for _, group in ipairs(ADFlowView.boardGroups(ADRunsController.store.runs)) do
|
local function addBoardRow(run)
|
||||||
self:addTextRow(i18n:getText(group.kind == "outbound" and "spu_board_outbound" or "spu_board_supply"))
|
|
||||||
for _, run in ipairs(group.runs) do
|
|
||||||
local cells = self:addCellRow(self.flowPrefab, self:flowCells(adEnv, run))
|
local cells = self:addCellRow(self.flowPrefab, self:flowCells(adEnv, run))
|
||||||
if cells[1] ~= nil and cells[1].parent ~= nil then
|
if cells[1] ~= nil and cells[1].parent ~= nil then
|
||||||
local runId = run.id
|
local runId = run.id
|
||||||
@@ -113,6 +113,22 @@ function SmartPickupFrame:fillOverviewRows(adEnv)
|
|||||||
end
|
end
|
||||||
table.insert(self.overviewRows, {runId = run.id, cells = cells})
|
table.insert(self.overviewRows, {runId = run.id, cells = cells})
|
||||||
end
|
end
|
||||||
|
for _, group in ipairs(ADFlowView.boardGroups(ADRunsController.store.runs)) do
|
||||||
|
self:addTextRow(i18n:getText(group.chain and "spu_board_chain" or group.kind == "outbound" and "spu_board_outbound" or "spu_board_supply"))
|
||||||
|
local runs, idle = group.runs, {}
|
||||||
|
-- kædeflows uden traktor foldes sammen som i Flows-listen (in-game 30/9)
|
||||||
|
if group.chain then runs, idle = ADFlowView.splitIdle(group.runs) end
|
||||||
|
for _, run in ipairs(runs) do addBoardRow(run) end
|
||||||
|
if #idle > 0 then
|
||||||
|
local key = self.showIdleChainFlows and "spu_board_chainIdleHide" or "spu_board_chainIdleShow"
|
||||||
|
self:addLinkRow(" " .. string.format(i18n:getText(key), #idle), function(frame)
|
||||||
|
frame.showIdleChainFlows = not frame.showIdleChainFlows
|
||||||
|
frame:requestRebuild()
|
||||||
|
end)
|
||||||
|
if self.showIdleChainFlows then
|
||||||
|
for _, run in ipairs(idle) do addBoardRow(run) end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
if #ADRunsController.store.runs == 0 then
|
if #ADRunsController.store.runs == 0 then
|
||||||
self:addTextRow(i18n:getText("spu_noRuns"))
|
self:addTextRow(i18n:getText("spu_noRuns"))
|
||||||
|
|||||||
@@ -16,8 +16,19 @@ SmartPickupFrame.TAB_OVERVIEW, SmartPickupFrame.TAB_FLOWS, SmartPickupFrame.TAB_
|
|||||||
SmartPickupFrame.MONTH_REBUILD_TICKS = 5
|
SmartPickupFrame.MONTH_REBUILD_TICKS = 5
|
||||||
-- Marksilo-fanen har klikbare valg: genopbygges sjældnere, så fokus ikke rykker midt i et valg
|
-- Marksilo-fanen har klikbare valg: genopbygges sjældnere, så fokus ikke rykker midt i et valg
|
||||||
SmartPickupFrame.FIELDSILO_REBUILD_TICKS = 30
|
SmartPickupFrame.FIELDSILO_REBUILD_TICKS = 30
|
||||||
|
-- Kæder-fanen (gui/SmartPickupChainTab.lua): klikbare forslag/pause, så genopbygning sjældent (som Marksiloer)
|
||||||
|
SmartPickupFrame.TAB_CHAINS = 10
|
||||||
|
SmartPickupFrame.CHAIN_REBUILD_TICKS = 30
|
||||||
-- Lagerfaner: én pr. bygningstype (indeks -> kind i ADSources.getFarmInventory)
|
-- Lagerfaner: én pr. bygningstype (indeks -> kind i ADSources.getFarmInventory)
|
||||||
SmartPickupFrame.TAB_NAMES = {"spu_tab_overview", "spu_tab_flows", "spu_tab_fields", "spu_tab_month", "spu_tab_fieldsilos", "spu_tab_factories", "spu_tab_silos", "spu_tab_barns", "spu_tab_sheds"}
|
SmartPickupFrame.TAB_NAMES = {"spu_tab_overview", "spu_tab_flows", "spu_tab_fields", "spu_tab_month", "spu_tab_fieldsilos", "spu_tab_factories", "spu_tab_silos", "spu_tab_barns", "spu_tab_sheds", "spu_tab_chains"}
|
||||||
|
-- Visningsrækkefølge (fane-id'erne ovenfor er faste og bruges overalt): Kæder står efter Marker (in-game 30/9).
|
||||||
|
SmartPickupFrame.TAB_DISPLAY = {1, 2, 3, 10, 4, 5, 6, 7, 8, 9}
|
||||||
|
function SmartPickupFrame.tabPosition(tabId)
|
||||||
|
for position, id in ipairs(SmartPickupFrame.TAB_DISPLAY) do
|
||||||
|
if id == tabId then return position end
|
||||||
|
end
|
||||||
|
return tabId
|
||||||
|
end
|
||||||
SmartPickupFrame.TAB_KINDS = {[6] = "factory", [7] = "silo", [8] = "husbandry", [9] = "objectStorage"}
|
SmartPickupFrame.TAB_KINDS = {[6] = "factory", [7] = "silo", [8] = "husbandry", [9] = "objectStorage"}
|
||||||
|
|
||||||
function SmartPickupFrame.getTabKind(tab)
|
function SmartPickupFrame.getTabKind(tab)
|
||||||
@@ -96,8 +107,9 @@ function SmartPickupFrame:initialize()
|
|||||||
if self.selectorPrefab ~= nil and self.subCategoryBox ~= nil and self.subCategoryPaging ~= nil then
|
if self.selectorPrefab ~= nil and self.subCategoryBox ~= nil and self.subCategoryPaging ~= nil then
|
||||||
self.selectorPrefab:unlinkElement()
|
self.selectorPrefab:unlinkElement()
|
||||||
FocusManager:removeElement(self.selectorPrefab)
|
FocusManager:removeElement(self.selectorPrefab)
|
||||||
for index, key in ipairs(SmartPickupFrame.TAB_NAMES) do
|
for position, index in ipairs(SmartPickupFrame.TAB_DISPLAY) do
|
||||||
self.subCategoryPaging:addText(tostring(index))
|
local key = SmartPickupFrame.TAB_NAMES[index]
|
||||||
|
self.subCategoryPaging:addText(tostring(position))
|
||||||
local tab = self.selectorPrefab:clone(self.subCategoryBox)
|
local tab = self.selectorPrefab:clone(self.subCategoryBox)
|
||||||
FocusManager:loadElementFromCustomValues(tab)
|
FocusManager:loadElementFromCustomValues(tab)
|
||||||
tab:setText(self.i18n:getText(key))
|
tab:setText(self.i18n:getText(key))
|
||||||
@@ -267,11 +279,11 @@ function SmartPickupFrame:switchTab(tabIndex)
|
|||||||
end
|
end
|
||||||
if self.guide ~= nil and not self:hasGuideOn(tabIndex) then self:closeGuide() end
|
if self.guide ~= nil and not self:hasGuideOn(tabIndex) then self:closeGuide() end
|
||||||
self.currentTab = tabIndex
|
self.currentTab = tabIndex
|
||||||
for index, tab in ipairs(self.subCategoryTabs) do
|
for index, tab in pairs(self.subCategoryTabs) do
|
||||||
tab:setSelected(index == tabIndex)
|
tab:setSelected(index == tabIndex)
|
||||||
end
|
end
|
||||||
if self.subCategoryPaging ~= nil then
|
if self.subCategoryPaging ~= nil then
|
||||||
self.subCategoryPaging:setState(tabIndex, false)
|
self.subCategoryPaging:setState(SmartPickupFrame.tabPosition(tabIndex), false)
|
||||||
end
|
end
|
||||||
self:requestRebuild()
|
self:requestRebuild()
|
||||||
self:safe(function(frame)
|
self:safe(function(frame)
|
||||||
@@ -279,8 +291,9 @@ function SmartPickupFrame:switchTab(tabIndex)
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- pilene/paging giver en position i visningsrækkefølgen
|
||||||
function SmartPickupFrame:onClickTab(state)
|
function SmartPickupFrame:onClickTab(state)
|
||||||
self:switchTab(state)
|
self:switchTab(SmartPickupFrame.TAB_DISPLAY[state] or state)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- De faste rækker: Flow-vælger + Wait-pulje for alle flows; Ture/kør hjem for forsyning; Sælg fra/
|
-- De faste rækker: Flow-vælger + Wait-pulje for alle flows; Ture/kør hjem for forsyning; Sælg fra/
|
||||||
@@ -466,6 +479,18 @@ function SmartPickupFrame:rebuild()
|
|||||||
self:restoreFocusIfLost()
|
self:restoreFocusIfLost()
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if self.currentTab == SmartPickupFrame.TAB_CHAINS then
|
||||||
|
if adEnv ~= nil then
|
||||||
|
self:fillChainRows(adEnv)
|
||||||
|
end
|
||||||
|
self.statusRow = self:addTextRow(self.statusOverride or "")
|
||||||
|
self.monthRebuildTicks = SmartPickupFrame.CHAIN_REBUILD_TICKS
|
||||||
|
self:colorRows()
|
||||||
|
self.boxLayout:invalidateLayout()
|
||||||
|
self:refreshButtons()
|
||||||
|
self:restoreFocusIfLost()
|
||||||
|
return
|
||||||
|
end
|
||||||
if self.currentTab == SmartPickupFrame.TAB_MONTH then
|
if self.currentTab == SmartPickupFrame.TAB_MONTH then
|
||||||
if adEnv ~= nil then
|
if adEnv ~= nil then
|
||||||
self:fillMonthRows(adEnv)
|
self:fillMonthRows(adEnv)
|
||||||
@@ -611,7 +636,8 @@ function SmartPickupFrame:refreshButtons()
|
|||||||
wanted = self.overviewButtons
|
wanted = self.overviewButtons
|
||||||
elseif self.currentTab == SmartPickupFrame.TAB_FIELDS then
|
elseif self.currentTab == SmartPickupFrame.TAB_FIELDS then
|
||||||
wanted = self:getFieldButtons()
|
wanted = self:getFieldButtons()
|
||||||
elseif self.currentTab == SmartPickupFrame.TAB_MONTH or self.currentTab == SmartPickupFrame.TAB_FIELDSILOS then
|
elseif self.currentTab == SmartPickupFrame.TAB_MONTH or self.currentTab == SmartPickupFrame.TAB_FIELDSILOS
|
||||||
|
or self.currentTab == SmartPickupFrame.TAB_CHAINS then
|
||||||
wanted = self.monthButtons
|
wanted = self.monthButtons
|
||||||
elseif SmartPickupFrame.getTabKind(self.currentTab) ~= nil then
|
elseif SmartPickupFrame.getTabKind(self.currentTab) ~= nil then
|
||||||
wanted = self.storageButtons
|
wanted = self.storageButtons
|
||||||
@@ -636,7 +662,14 @@ function SmartPickupFrame:refreshLive()
|
|||||||
self:setStatusText(self.i18n:getText("spu_status_noAd"))
|
self:setStatusText(self.i18n:getText("spu_status_noAd"))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if self.currentTab == SmartPickupFrame.TAB_MONTH or self.currentTab == SmartPickupFrame.TAB_FIELDSILOS then
|
if self.currentTab == SmartPickupFrame.TAB_MONTH or self.currentTab == SmartPickupFrame.TAB_FIELDSILOS
|
||||||
|
or self.currentTab == SmartPickupFrame.TAB_CHAINS then
|
||||||
|
if self.currentTab == SmartPickupFrame.TAB_CHAINS then
|
||||||
|
-- "forslaget gælder ikke længere" står STATUS_OVERRIDE_TICKS sekunder
|
||||||
|
self.statusOverrideTicks = (self.statusOverrideTicks or 0) - 1
|
||||||
|
if self.statusOverrideTicks <= 0 then self.statusOverride = nil end
|
||||||
|
self:setStatusText(self.statusOverride or "")
|
||||||
|
end
|
||||||
self.monthRebuildTicks = (self.monthRebuildTicks or 0) - 1
|
self.monthRebuildTicks = (self.monthRebuildTicks or 0) - 1
|
||||||
if self.monthRebuildTicks <= 0 then self:requestRebuild() end
|
if self.monthRebuildTicks <= 0 then self:requestRebuild() end
|
||||||
self:refreshButtons()
|
self:refreshButtons()
|
||||||
|
|||||||
@@ -183,8 +183,33 @@ function SmartPickupFrame:addKpiRow(stats)
|
|||||||
return row
|
return row
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Varepunkterne + kædens punkter (røde manglende led, højst 2, røde "Slukket men nødvendig", højst 2, og ét samlet
|
||||||
|
-- punkt for afgrødeforslag), samme loft MAX_ATTENTION. ADChain.last genbruges; er det ældre end CHAIN_MAX_AGE_MS,
|
||||||
|
-- evalueres der (visning, handler ikke), så en slukket nødvendig fabrik ses straks.
|
||||||
|
function SmartPickupFrame:attentionFor(stats)
|
||||||
|
local attention = ADStorageStats.attentionList(stats, SmartPickupFrame.MAX_ATTENTION)
|
||||||
|
if ADChain ~= nil and ADChain.evaluate ~= nil and self.chainLast ~= nil and self.getAdEnv ~= nil then
|
||||||
|
local okEnv, adEnv = pcall(self.getAdEnv)
|
||||||
|
if okEnv and adEnv ~= nil then pcall(self.chainLast, self, adEnv) end
|
||||||
|
end
|
||||||
|
if ADChain == nil or ADChain.last == nil then return attention end
|
||||||
|
return ADStorageStats.mergeAttention(attention, ADStorageStats.chainEntries(ADChain.last), SmartPickupFrame.MAX_ATTENTION)
|
||||||
|
end
|
||||||
|
|
||||||
function SmartPickupFrame:alertText(entry)
|
function SmartPickupFrame:alertText(entry)
|
||||||
local stat, fc, i18n = entry.stat, SmartPickupFrame.formatCompact, self.i18n
|
local stat, fc, i18n = entry.stat, SmartPickupFrame.formatCompact, self.i18n
|
||||||
|
if entry.kind == "chainSuggest" then
|
||||||
|
return string.format(i18n:getText("spu_attention_chainSuggests"), entry.count or 0)
|
||||||
|
end
|
||||||
|
if entry.kind == "chainProblem" then
|
||||||
|
return string.format(i18n:getText("spu_attention_chainProblems"), entry.count or 0)
|
||||||
|
end
|
||||||
|
if entry.kind == "dependencyOff" then
|
||||||
|
if (entry.more or 0) > 0 then
|
||||||
|
return string.format(i18n:getText("spu_attention_depOffMore"), tostring(entry.buildingName), entry.more)
|
||||||
|
end
|
||||||
|
return string.format(i18n:getText("spu_attention_depOff"), tostring(entry.buildingName))
|
||||||
|
end
|
||||||
if entry.kind == "empty" then
|
if entry.kind == "empty" then
|
||||||
return string.format(i18n:getText("spu_alert_empty"), stat.title, self:formatDays(stat.days), fc(stat.farmLevel or 0), fc(math.abs(stat.net or 0)))
|
return string.format(i18n:getText("spu_alert_empty"), stat.title, self:formatDays(stat.days), fc(stat.farmLevel or 0), fc(math.abs(stat.net or 0)))
|
||||||
elseif entry.kind == "full" then
|
elseif entry.kind == "full" then
|
||||||
@@ -263,7 +288,7 @@ end
|
|||||||
function SmartPickupFrame:fillItemOverview(adEnv, inventory, fullInventory)
|
function SmartPickupFrame:fillItemOverview(adEnv, inventory, fullInventory)
|
||||||
local i18n = self.i18n
|
local i18n = self.i18n
|
||||||
local stats = self:computeItemStats(adEnv, inventory, fullInventory)
|
local stats = self:computeItemStats(adEnv, inventory, fullInventory)
|
||||||
local attention = ADStorageStats.attentionList(stats, SmartPickupFrame.MAX_ATTENTION)
|
local attention = self:attentionFor(stats)
|
||||||
self.itemView = {kpiRow = self:addKpiRow(stats), alertRows = {}, itemRows = {}, key = self:itemStructureKey(stats, attention)}
|
self.itemView = {kpiRow = self:addKpiRow(stats), alertRows = {}, itemRows = {}, key = self:itemStructureKey(stats, attention)}
|
||||||
self.itemRebuildTicks = SmartPickupFrame.ITEM_REBUILD_TICKS
|
self.itemRebuildTicks = SmartPickupFrame.ITEM_REBUILD_TICKS
|
||||||
self:addTextRow(i18n:getText("spu_attention"))
|
self:addTextRow(i18n:getText("spu_attention"))
|
||||||
@@ -307,7 +332,7 @@ function SmartPickupFrame:refreshItemOverview(adEnv, inventory, fullInventory)
|
|||||||
local view = self.itemView
|
local view = self.itemView
|
||||||
if view == nil then return end
|
if view == nil then return end
|
||||||
local stats = self:computeItemStats(adEnv, inventory, fullInventory)
|
local stats = self:computeItemStats(adEnv, inventory, fullInventory)
|
||||||
local attention = ADStorageStats.attentionList(stats, SmartPickupFrame.MAX_ATTENTION)
|
local attention = self:attentionFor(stats)
|
||||||
if self:itemStructureKey(stats, attention) ~= view.key then
|
if self:itemStructureKey(stats, attention) ~= view.key then
|
||||||
self.itemRebuildTicks = (self.itemRebuildTicks or 0) - 1
|
self.itemRebuildTicks = (self.itemRebuildTicks or 0) - 1
|
||||||
if self.itemRebuildTicks <= 0 then self:requestRebuild() end
|
if self.itemRebuildTicks <= 0 then self:requestRebuild() end
|
||||||
|
|||||||
@@ -151,7 +151,7 @@
|
|||||||
<text name="spu_field_fieldSiloNotEmptied" text="! Marksilo %s tømmes ikke hjem — lav et Ud fra bygning-flow"/>
|
<text name="spu_field_fieldSiloNotEmptied" text="! Marksilo %s tømmes ikke hjem — lav et Ud fra bygning-flow"/>
|
||||||
<text name="spu_field_section_fleet" text="— Markflåde, fælles for ALLE marker (flueben = må sendes ud; faste maskiner vælges ved marken) —"/>
|
<text name="spu_field_section_fleet" text="— Markflåde, fælles for ALLE marker (flueben = må sendes ud; faste maskiner vælges ved marken) —"/>
|
||||||
<text name="spu_field_rig_working" text="i gang på F%s"/>
|
<text name="spu_field_rig_working" text="i gang på F%s"/>
|
||||||
<text name="spu_field_tooWideShort" text="!! over 50 m"/>
|
<text name="spu_field_tooWideShort" text="kursus 50 m"/>
|
||||||
<text name="spu_field_widthUnknownShort" text="!! bredde ukendt — sæt i Courseplay"/>
|
<text name="spu_field_widthUnknownShort" text="!! bredde ukendt — sæt i Courseplay"/>
|
||||||
<text name="spu_field_noRigs" text="ingen køretøjer med slåmaskine, vender, river eller læssevogn"/>
|
<text name="spu_field_noRigs" text="ingen køretøjer med slåmaskine, vender, river eller læssevogn"/>
|
||||||
<text name="spu_field_status" text="Marker i gang: %d af %d"/>
|
<text name="spu_field_status" text="Marker i gang: %d af %d"/>
|
||||||
@@ -167,6 +167,7 @@
|
|||||||
<text name="spu_field_careNeed" text="trænger til %s"/>
|
<text name="spu_field_careNeed" text="trænger til %s"/>
|
||||||
<text name="spu_field_fertilize" text="Gød op til max (når afgrøden vokser)"/>
|
<text name="spu_field_fertilize" text="Gød op til max (når afgrøden vokser)"/>
|
||||||
<text name="spu_field_lime" text="Kalk når jorden trænger"/>
|
<text name="spu_field_lime" text="Kalk når jorden trænger"/>
|
||||||
|
<text name="spu_field_chainFree" text="Fri til kæden (moden må skifte afgrøde)"/>
|
||||||
<text name="spu_field_mulch" text="Stubbehakning efter høst"/>
|
<text name="spu_field_mulch" text="Stubbehakning efter høst"/>
|
||||||
<text name="spu_field_need_fert" text="gødning"/>
|
<text name="spu_field_need_fert" text="gødning"/>
|
||||||
<text name="spu_field_need_lime" text="kalk"/>
|
<text name="spu_field_need_lime" text="kalk"/>
|
||||||
@@ -309,6 +310,7 @@
|
|||||||
<text name="spu_policy_sell" text="Salg"/>
|
<text name="spu_policy_sell" text="Salg"/>
|
||||||
<text name="spu_policy_sellElseStore" text="Salg, ellers lager"/>
|
<text name="spu_policy_sellElseStore" text="Salg, ellers lager"/>
|
||||||
<text name="spu_policy_tooltip" text="Lager: kør til en silo (paller: et palleskur) når der er et fuldt læs eller lageret er højt. Salg: sælg når prisen er tæt på årets top — men aldrig det gårdens fabrikker og stalde skal bruge det næste døgn. Salg, ellers lager: sælg ved god pris, ellers til lager når lageret er højt."/>
|
<text name="spu_policy_tooltip" text="Lager: kør til en silo (paller: et palleskur) når der er et fuldt læs eller lageret er højt. Salg: sælg når prisen er tæt på årets top — men aldrig det gårdens fabrikker og stalde skal bruge det næste døgn. Salg, ellers lager: sælg ved god pris, ellers til lager når lageret er højt."/>
|
||||||
|
<text name="spu_sellAt_always" text="Altid (uanset pris)"/>
|
||||||
<text name="spu_sellAt" text="Sælg fra (% af årets toppris)"/>
|
<text name="spu_sellAt" text="Sælg fra (% af årets toppris)"/>
|
||||||
<text name="spu_sellAt_tooltip" text="Der køres til salg når salgsstedets pris er mindst denne andel af varens toppris i årets prisforløb — uanset mængde. Stor efterspørgsel tæller altid."/>
|
<text name="spu_sellAt_tooltip" text="Der køres til salg når salgsstedets pris er mindst denne andel af varens toppris i årets prisforløb — uanset mængde. Stor efterspørgsel tæller altid."/>
|
||||||
<text name="spu_storeAbove" text="Kør til lager over"/>
|
<text name="spu_storeAbove" text="Kør til lager over"/>
|
||||||
@@ -349,6 +351,8 @@
|
|||||||
<text name="spu_flow_noSource" text="(ingen kilde)"/>
|
<text name="spu_flow_noSource" text="(ingen kilde)"/>
|
||||||
<text name="spu_flow_noTarget" text="(intet mål)"/>
|
<text name="spu_flow_noTarget" text="(intet mål)"/>
|
||||||
<text name="spu_flow_noVehicle" text="(ingen traktor)"/>
|
<text name="spu_flow_noVehicle" text="(ingen traktor)"/>
|
||||||
|
<text name="spu_flow_ruleSellAlways" text="sælg altid"/>
|
||||||
|
<text name="spu_flow_chainIdle" text="mangler traktor/vogn"/>
|
||||||
<text name="spu_flow_ruleSell" text="pris >= %d %%"/>
|
<text name="spu_flow_ruleSell" text="pris >= %d %%"/>
|
||||||
<text name="spu_flow_ruleStore" text="lager > %d %%"/>
|
<text name="spu_flow_ruleStore" text="lager > %d %%"/>
|
||||||
<text name="spu_flow_ruleOut" text="pris >= %d %%, lager > %d %%"/>
|
<text name="spu_flow_ruleOut" text="pris >= %d %%, lager > %d %%"/>
|
||||||
@@ -376,6 +380,9 @@
|
|||||||
<text name="spu_col_to" text="Til"/>
|
<text name="spu_col_to" text="Til"/>
|
||||||
<text name="spu_col_tractors" text="Kører"/>
|
<text name="spu_col_tractors" text="Kører"/>
|
||||||
<text name="spu_col_status" text="Status"/>
|
<text name="spu_col_status" text="Status"/>
|
||||||
|
<text name="spu_board_chainIdleShow" text="%d kædeflows venter på en ledig traktor/vogn — klik for at vise"/>
|
||||||
|
<text name="spu_board_chainIdleHide" text="%d kædeflows uden traktor — klik for at skjule"/>
|
||||||
|
<text name="spu_board_chain" text="· Kædernes egne flows (oprettes af fanen Kæder) ·"/>
|
||||||
<text name="spu_board_supply" text="· Ind til bygning ·"/>
|
<text name="spu_board_supply" text="· Ind til bygning ·"/>
|
||||||
<text name="spu_board_outbound" text="· Ud fra bygning ·"/>
|
<text name="spu_board_outbound" text="· Ud fra bygning ·"/>
|
||||||
<text name="spu_showAllVehicles" text="Vis alle traktorer (%d skjult)"/>
|
<text name="spu_showAllVehicles" text="Vis alle traktorer (%d skjult)"/>
|
||||||
@@ -513,6 +520,7 @@
|
|||||||
<text name="spu_guide_flow_none" text="ingen valgt"/>
|
<text name="spu_guide_flow_none" text="ingen valgt"/>
|
||||||
<text name="spu_guide_flow_goodsAuto" text="Automatisk"/>
|
<text name="spu_guide_flow_goodsAuto" text="Automatisk"/>
|
||||||
<text name="spu_guide_flow_sumGoods" text="%d varer"/>
|
<text name="spu_guide_flow_sumGoods" text="%d varer"/>
|
||||||
|
<text name="spu_guide_flow_sumSellAlways" text="sælg altid"/>
|
||||||
<text name="spu_guide_flow_sumSellAt" text="sælg fra %d %%"/>
|
<text name="spu_guide_flow_sumSellAt" text="sælg fra %d %%"/>
|
||||||
<text name="spu_guide_flow_sumStoreAbove" text="lager over %d %%"/>
|
<text name="spu_guide_flow_sumStoreAbove" text="lager over %d %%"/>
|
||||||
<text name="spu_guide_flow_sumSellAbove" text="sælg altid over %d %%"/>
|
<text name="spu_guide_flow_sumSellAbove" text="sælg altid over %d %%"/>
|
||||||
@@ -574,5 +582,80 @@
|
|||||||
<text name="spu_item_fullIn" text="fuld om %s"/>
|
<text name="spu_item_fullIn" text="fuld om %s"/>
|
||||||
<text name="spu_item_balanced" text="i balance"/>
|
<text name="spu_item_balanced" text="i balance"/>
|
||||||
<text name="spu_item_share" text="%d %% af varen"/>
|
<text name="spu_item_share" text="%d %% af varen"/>
|
||||||
|
<text name="spu_tab_chains" text="Kæder"/>
|
||||||
|
<text name="spu_chain_noFlow" text="Intet flow kører den vare endnu — kæden opretter det, når der er en ledig traktor/vogn"/>
|
||||||
|
<text name="spu_chain_optIn" text="[Kæde til]"/>
|
||||||
|
<text name="spu_chain_optOut" text="[Fjern]"/>
|
||||||
|
<text name="spu_chain_notChosen" text="ingen kæde"/>
|
||||||
|
<text name="spu_chain_candidatesShow" text="%d tændte produktioner uden kæde — klik for at vise"/>
|
||||||
|
<text name="spu_chain_candidatesHide" text="%d tændte produktioner uden kæde — klik for at skjule"/>
|
||||||
|
<text name="spu_chain_finalFarm" text="Gård"/>
|
||||||
|
<text name="spu_chain_finalSell" text="Sælg"/>
|
||||||
|
<text name="spu_chain_finalKeep" text="Behold"/>
|
||||||
|
<text name="spu_chain_suggestHeader" text="Forslag til ny afgrøde"/>
|
||||||
|
<text name="spu_chain_empty" text="Ingen kæder: slå et produkt til i en fabrik"/>
|
||||||
|
<text name="spu_chain_col_item" text="Vare"/>
|
||||||
|
<text name="spu_chain_col_source" text="Kilde"/>
|
||||||
|
<text name="spu_chain_col_perMonth" text="l/md"/>
|
||||||
|
<text name="spu_chain_col_status" text="Status"/>
|
||||||
|
<text name="spu_chain_col_stock" text="Lager"/>
|
||||||
|
<text name="spu_chain_approve" text="Forslag [Godkend]"/>
|
||||||
|
<text name="spu_chain_dismiss" text="[Afvis]"/>
|
||||||
|
<text name="spu_chain_auto" text="skiftet (fri mark)"/>
|
||||||
|
<text name="spu_chain_gone" text="Forslaget gælder ikke længere"/>
|
||||||
|
<text name="spu_chain_flowOk" text="flow kører"/>
|
||||||
|
<text name="spu_chain_flowMissing" text="mangler traktor/vogn"/>
|
||||||
|
<text name="spu_chain_flowNone" text="intet flow"/>
|
||||||
|
<text name="spu_chain_flowAuto" text="fordeles af spillet"/>
|
||||||
|
<text name="spu_chain_flowPallet" text="palle: vælg UAL-vogn"/>
|
||||||
|
<text name="spu_chain_flowStopped" text="flow stoppet"/>
|
||||||
|
<text name="spu_chain_bottleneck" text="flaskehals: -%s/md"/>
|
||||||
|
<text name="spu_chain_buy" text="skal købes: %s/md"/>
|
||||||
|
<text name="spu_chain_cycle" text="kredsløb"/>
|
||||||
|
<text name="spu_chain_fieldShort" text="mangler %s ha"/>
|
||||||
|
<text name="spu_chain_tight" text="lager < 3 md"/>
|
||||||
|
<text name="spu_chain_noField" text="ingen mark"/>
|
||||||
|
<text name="spu_chain_buySource" text="køb"/>
|
||||||
|
<text name="spu_chain_fromFields" text="fra marker"/>
|
||||||
|
<text name="spu_chain_notifyMore" text="+%d manglende led — se fanen Kæder"/>
|
||||||
|
<text name="spu_chain_pause" text="[Pause]"/>
|
||||||
|
<text name="spu_chain_resume" text="[Start]"/>
|
||||||
|
<text name="spu_chain_rootOk" text="dækket"/>
|
||||||
|
<text name="spu_chain_rootError" text="mangel"/>
|
||||||
|
<text name="spu_chain_rootWaiting" text="venter"/>
|
||||||
|
<text name="spu_chain_rootPaused" text="på pause"/>
|
||||||
|
<text name="spu_chain_rootSummary" text="%s · %d forslag"/>
|
||||||
|
<text name="spu_chain_free" text="Marker fri til kæden: %s"/>
|
||||||
|
<text name="spu_chain_switches" text="Automatiske skift siden indlæsning: %d"/>
|
||||||
|
<text name="spu_chain_scanning" text="Marker scannes endnu – kæden skifter ingen marker før alle er set"/>
|
||||||
|
<text name="spu_attention_chainSuggests" text="Kæde: %d forslag"/>
|
||||||
|
<text name="spu_attention_hintChain" text=" → klik: se Kæder"/>
|
||||||
|
<text name="spu_chain_problem_buy" text="Ingen af dine fabrikker laver %s — byg en eller køb"/>
|
||||||
|
<text name="spu_chain_problem_barn" text="Dine stalde leverer ikke nok %s — køb resten eller flere dyr"/>
|
||||||
|
<text name="spu_chain_problem_bottleneck" text="%s kan kun lave %s af %s/md"/>
|
||||||
|
<text name="spu_chain_problem_off" text="%s (%s) er slukket af dig — tænd den eller sæt kæden på pause"/>
|
||||||
|
<text name="spu_chain_problem_cycle" text="Kredsløb omkring %s — kæden kan ikke beregnes"/>
|
||||||
|
<text name="spu_attention_chainProblems" text="Kæder: %d manglende led"/>
|
||||||
|
<text name="input_SPU_TOGGLE_STATUS" text="AD Smart Pickup: statusvindue"/>
|
||||||
|
<text name="spu_status_flows" text="Flows"/>
|
||||||
|
<text name="spu_status_fields" text="Marker"/>
|
||||||
|
<text name="spu_status_chains" text="Kæder"/>
|
||||||
|
<text name="spu_status_warnings" text="Advarsler"/>
|
||||||
|
<text name="spu_status_more" text="+%d mere"/>
|
||||||
|
<text name="spu_status_idle" text="Smart Pickup: intet kører lige nu"/>
|
||||||
|
<text name="spu_status_act_fetch" text="henter %s ved %s"/>
|
||||||
|
<text name="spu_status_act_driving" text="kører til %s"/>
|
||||||
|
<text name="spu_status_act_waiting" text="venter ved %s"/>
|
||||||
|
<text name="spu_status_act_holding" text="holder ved %s"/>
|
||||||
|
<text name="spu_status_act_fetchWagon" text="henter vogn"/>
|
||||||
|
<text name="spu_status_act_waitWagon" text="venter på vogn"/>
|
||||||
|
<text name="spu_status_act_emptying" text="tømmer vognen"/>
|
||||||
|
<text name="spu_status_act_homing" text="på vej hjem"/>
|
||||||
|
<text name="spu_status_stuck" text="%s står stille (%d min)"/>
|
||||||
|
<text name="spu_dep_warn" text="Advarsel: du slukkede %s (%s) — %s bruger det. Lageret rækker ca. %d dage."/>
|
||||||
|
<text name="spu_dep_warnNoDays" text="Advarsel: du slukkede %s (%s) — %s bruger det."/>
|
||||||
|
<text name="spu_attention_depOff" text="Slukket men nødvendig: %s"/>
|
||||||
|
<text name="spu_attention_depOffMore" text="Slukket men nødvendig: %s +%d"/>
|
||||||
|
<text name="spu_attention_hintFactories" text=" → klik: se Fabrikker"/>
|
||||||
</texts>
|
</texts>
|
||||||
</l10n>
|
</l10n>
|
||||||
|
|||||||
@@ -151,7 +151,7 @@
|
|||||||
<text name="spu_field_fieldSiloNotEmptied" text="! Field silo %s is not emptied home — create an Out of building flow"/>
|
<text name="spu_field_fieldSiloNotEmptied" text="! Field silo %s is not emptied home — create an Out of building flow"/>
|
||||||
<text name="spu_field_section_fleet" text="— Field fleet, shared by ALL fields (ticked = may be sent out; dedicated machines are set per field) —"/>
|
<text name="spu_field_section_fleet" text="— Field fleet, shared by ALL fields (ticked = may be sent out; dedicated machines are set per field) —"/>
|
||||||
<text name="spu_field_rig_working" text="working on F%s"/>
|
<text name="spu_field_rig_working" text="working on F%s"/>
|
||||||
<text name="spu_field_tooWideShort" text="!! over 50 m"/>
|
<text name="spu_field_tooWideShort" text="course 50 m"/>
|
||||||
<text name="spu_field_widthUnknownShort" text="!! width unknown — set in Courseplay"/>
|
<text name="spu_field_widthUnknownShort" text="!! width unknown — set in Courseplay"/>
|
||||||
<text name="spu_field_noRigs" text="no vehicles with mower, tedder, rake or loading wagon"/>
|
<text name="spu_field_noRigs" text="no vehicles with mower, tedder, rake or loading wagon"/>
|
||||||
<text name="spu_field_status" text="Fields in progress: %d of %d"/>
|
<text name="spu_field_status" text="Fields in progress: %d of %d"/>
|
||||||
@@ -167,6 +167,7 @@
|
|||||||
<text name="spu_field_careNeed" text="needs %s"/>
|
<text name="spu_field_careNeed" text="needs %s"/>
|
||||||
<text name="spu_field_fertilize" text="Fertilize to max (while the crop grows)"/>
|
<text name="spu_field_fertilize" text="Fertilize to max (while the crop grows)"/>
|
||||||
<text name="spu_field_lime" text="Lime when the soil needs it"/>
|
<text name="spu_field_lime" text="Lime when the soil needs it"/>
|
||||||
|
<text name="spu_field_chainFree" text="Free for chains (mod may change the crop)"/>
|
||||||
<text name="spu_field_mulch" text="Mulch stubble after harvest"/>
|
<text name="spu_field_mulch" text="Mulch stubble after harvest"/>
|
||||||
<text name="spu_field_need_fert" text="fertilizer"/>
|
<text name="spu_field_need_fert" text="fertilizer"/>
|
||||||
<text name="spu_field_need_lime" text="lime"/>
|
<text name="spu_field_need_lime" text="lime"/>
|
||||||
@@ -309,6 +310,7 @@
|
|||||||
<text name="spu_policy_sell" text="Sell"/>
|
<text name="spu_policy_sell" text="Sell"/>
|
||||||
<text name="spu_policy_sellElseStore" text="Sell, else store"/>
|
<text name="spu_policy_sellElseStore" text="Sell, else store"/>
|
||||||
<text name="spu_policy_tooltip" text="Store: drive to a silo (pallets: a pallet shed) when there is a full load or the storage is high. Sell: sell when the price is near the yearly top — never what the farm's factories and barns need for the next day. Sell, else store: sell at a good price, otherwise to storage when storage is high."/>
|
<text name="spu_policy_tooltip" text="Store: drive to a silo (pallets: a pallet shed) when there is a full load or the storage is high. Sell: sell when the price is near the yearly top — never what the farm's factories and barns need for the next day. Sell, else store: sell at a good price, otherwise to storage when storage is high."/>
|
||||||
|
<text name="spu_sellAt_always" text="Always (any price)"/>
|
||||||
<text name="spu_sellAt" text="Sell from (% of yearly top price)"/>
|
<text name="spu_sellAt" text="Sell from (% of yearly top price)"/>
|
||||||
<text name="spu_sellAt_tooltip" text="Drive to sell when the selling point pays at least this share of the good's top price in the yearly price curve — regardless of amount. Great demand always qualifies."/>
|
<text name="spu_sellAt_tooltip" text="Drive to sell when the selling point pays at least this share of the good's top price in the yearly price curve — regardless of amount. Great demand always qualifies."/>
|
||||||
<text name="spu_storeAbove" text="Drive to storage above"/>
|
<text name="spu_storeAbove" text="Drive to storage above"/>
|
||||||
@@ -349,6 +351,8 @@
|
|||||||
<text name="spu_flow_noSource" text="(no source)"/>
|
<text name="spu_flow_noSource" text="(no source)"/>
|
||||||
<text name="spu_flow_noTarget" text="(no target)"/>
|
<text name="spu_flow_noTarget" text="(no target)"/>
|
||||||
<text name="spu_flow_noVehicle" text="(no tractor)"/>
|
<text name="spu_flow_noVehicle" text="(no tractor)"/>
|
||||||
|
<text name="spu_flow_ruleSellAlways" text="always sell"/>
|
||||||
|
<text name="spu_flow_chainIdle" text="needs a tractor/trailer"/>
|
||||||
<text name="spu_flow_ruleSell" text="price >= %d %%"/>
|
<text name="spu_flow_ruleSell" text="price >= %d %%"/>
|
||||||
<text name="spu_flow_ruleStore" text="storage > %d %%"/>
|
<text name="spu_flow_ruleStore" text="storage > %d %%"/>
|
||||||
<text name="spu_flow_ruleOut" text="price >= %d %%, storage > %d %%"/>
|
<text name="spu_flow_ruleOut" text="price >= %d %%, storage > %d %%"/>
|
||||||
@@ -376,6 +380,9 @@
|
|||||||
<text name="spu_col_to" text="To"/>
|
<text name="spu_col_to" text="To"/>
|
||||||
<text name="spu_col_tractors" text="Running"/>
|
<text name="spu_col_tractors" text="Running"/>
|
||||||
<text name="spu_col_status" text="Status"/>
|
<text name="spu_col_status" text="Status"/>
|
||||||
|
<text name="spu_board_chainIdleShow" text="%d chain flows are waiting for a free tractor/trailer — click to show"/>
|
||||||
|
<text name="spu_board_chainIdleHide" text="%d chain flows without a tractor — click to hide"/>
|
||||||
|
<text name="spu_board_chain" text="· Flows created by chains (Chains tab) ·"/>
|
||||||
<text name="spu_board_supply" text="· Into building ·"/>
|
<text name="spu_board_supply" text="· Into building ·"/>
|
||||||
<text name="spu_board_outbound" text="· Out of building ·"/>
|
<text name="spu_board_outbound" text="· Out of building ·"/>
|
||||||
<text name="spu_showAllVehicles" text="Show all tractors (%d hidden)"/>
|
<text name="spu_showAllVehicles" text="Show all tractors (%d hidden)"/>
|
||||||
@@ -513,6 +520,7 @@
|
|||||||
<text name="spu_guide_flow_none" text="none chosen"/>
|
<text name="spu_guide_flow_none" text="none chosen"/>
|
||||||
<text name="spu_guide_flow_goodsAuto" text="Automatic"/>
|
<text name="spu_guide_flow_goodsAuto" text="Automatic"/>
|
||||||
<text name="spu_guide_flow_sumGoods" text="%d goods"/>
|
<text name="spu_guide_flow_sumGoods" text="%d goods"/>
|
||||||
|
<text name="spu_guide_flow_sumSellAlways" text="always sell"/>
|
||||||
<text name="spu_guide_flow_sumSellAt" text="sell from %d %%"/>
|
<text name="spu_guide_flow_sumSellAt" text="sell from %d %%"/>
|
||||||
<text name="spu_guide_flow_sumStoreAbove" text="store above %d %%"/>
|
<text name="spu_guide_flow_sumStoreAbove" text="store above %d %%"/>
|
||||||
<text name="spu_guide_flow_sumSellAbove" text="always sell above %d %%"/>
|
<text name="spu_guide_flow_sumSellAbove" text="always sell above %d %%"/>
|
||||||
@@ -574,5 +582,80 @@
|
|||||||
<text name="spu_item_fullIn" text="full in %s"/>
|
<text name="spu_item_fullIn" text="full in %s"/>
|
||||||
<text name="spu_item_balanced" text="balanced"/>
|
<text name="spu_item_balanced" text="balanced"/>
|
||||||
<text name="spu_item_share" text="%d %% of the good"/>
|
<text name="spu_item_share" text="%d %% of the good"/>
|
||||||
|
<text name="spu_tab_chains" text="Chains"/>
|
||||||
|
<text name="spu_chain_noFlow" text="No flow carries that good yet — the chain creates one when a tractor/trailer is free"/>
|
||||||
|
<text name="spu_chain_optIn" text="[Chain on]"/>
|
||||||
|
<text name="spu_chain_optOut" text="[Remove]"/>
|
||||||
|
<text name="spu_chain_notChosen" text="no chain"/>
|
||||||
|
<text name="spu_chain_candidatesShow" text="%d productions on without a chain — click to show"/>
|
||||||
|
<text name="spu_chain_candidatesHide" text="%d productions on without a chain — click to hide"/>
|
||||||
|
<text name="spu_chain_finalFarm" text="Farm"/>
|
||||||
|
<text name="spu_chain_finalSell" text="Sell"/>
|
||||||
|
<text name="spu_chain_finalKeep" text="Keep"/>
|
||||||
|
<text name="spu_chain_suggestHeader" text="Crop change suggestions"/>
|
||||||
|
<text name="spu_chain_empty" text="No chains: enable a product in a factory"/>
|
||||||
|
<text name="spu_chain_col_item" text="Goods"/>
|
||||||
|
<text name="spu_chain_col_source" text="Source"/>
|
||||||
|
<text name="spu_chain_col_perMonth" text="l/mo"/>
|
||||||
|
<text name="spu_chain_col_status" text="Status"/>
|
||||||
|
<text name="spu_chain_col_stock" text="Stock"/>
|
||||||
|
<text name="spu_chain_approve" text="Suggest [Approve]"/>
|
||||||
|
<text name="spu_chain_dismiss" text="[Reject]"/>
|
||||||
|
<text name="spu_chain_auto" text="switched (free field)"/>
|
||||||
|
<text name="spu_chain_gone" text="The suggestion no longer applies"/>
|
||||||
|
<text name="spu_chain_flowOk" text="flow running"/>
|
||||||
|
<text name="spu_chain_flowMissing" text="no tractor/trailer"/>
|
||||||
|
<text name="spu_chain_flowNone" text="no flow"/>
|
||||||
|
<text name="spu_chain_flowAuto" text="distributed by game"/>
|
||||||
|
<text name="spu_chain_flowPallet" text="pallet: UAL trailer"/>
|
||||||
|
<text name="spu_chain_flowStopped" text="flow stopped"/>
|
||||||
|
<text name="spu_chain_bottleneck" text="bottleneck: -%s/mo"/>
|
||||||
|
<text name="spu_chain_buy" text="must buy: %s/mo"/>
|
||||||
|
<text name="spu_chain_cycle" text="loop"/>
|
||||||
|
<text name="spu_chain_fieldShort" text="short %s ha"/>
|
||||||
|
<text name="spu_chain_tight" text="stock < 3 mo"/>
|
||||||
|
<text name="spu_chain_noField" text="no field"/>
|
||||||
|
<text name="spu_chain_buySource" text="buy"/>
|
||||||
|
<text name="spu_chain_fromFields" text="from fields"/>
|
||||||
|
<text name="spu_chain_notifyMore" text="+%d more missing links — see the Chains tab"/>
|
||||||
|
<text name="spu_chain_pause" text="[Pause]"/>
|
||||||
|
<text name="spu_chain_resume" text="[Resume]"/>
|
||||||
|
<text name="spu_chain_rootOk" text="covered"/>
|
||||||
|
<text name="spu_chain_rootError" text="short"/>
|
||||||
|
<text name="spu_chain_rootWaiting" text="waiting"/>
|
||||||
|
<text name="spu_chain_rootPaused" text="paused"/>
|
||||||
|
<text name="spu_chain_rootSummary" text="%s · %d sugg."/>
|
||||||
|
<text name="spu_chain_free" text="Fields free for the chain: %s"/>
|
||||||
|
<text name="spu_chain_switches" text="Automatic switches since load: %d"/>
|
||||||
|
<text name="spu_chain_scanning" text="Fields still being scanned – no field changes until all are seen"/>
|
||||||
|
<text name="spu_attention_chainSuggests" text="Chain: %d suggestions"/>
|
||||||
|
<text name="spu_attention_hintChain" text=" → click: see Chains"/>
|
||||||
|
<text name="spu_chain_problem_buy" text="None of your factories makes %s — build one or buy it"/>
|
||||||
|
<text name="spu_chain_problem_barn" text="Barns make too little %s — buy the rest or more animals"/>
|
||||||
|
<text name="spu_chain_problem_bottleneck" text="%s can only make %s of %s/mo"/>
|
||||||
|
<text name="spu_chain_problem_off" text="%s (%s) was turned off by you — turn it on or pause the chain"/>
|
||||||
|
<text name="spu_chain_problem_cycle" text="Loop around %s — the chain cannot be calculated"/>
|
||||||
|
<text name="spu_attention_chainProblems" text="Chains: %d missing links"/>
|
||||||
|
<text name="input_SPU_TOGGLE_STATUS" text="AD Smart Pickup: status window"/>
|
||||||
|
<text name="spu_status_flows" text="Flows"/>
|
||||||
|
<text name="spu_status_fields" text="Fields"/>
|
||||||
|
<text name="spu_status_chains" text="Chains"/>
|
||||||
|
<text name="spu_status_warnings" text="Warnings"/>
|
||||||
|
<text name="spu_status_more" text="+%d more"/>
|
||||||
|
<text name="spu_status_idle" text="Smart Pickup: nothing running right now"/>
|
||||||
|
<text name="spu_status_act_fetch" text="fetching %s at %s"/>
|
||||||
|
<text name="spu_status_act_driving" text="driving to %s"/>
|
||||||
|
<text name="spu_status_act_waiting" text="waiting at %s"/>
|
||||||
|
<text name="spu_status_act_holding" text="holding at %s"/>
|
||||||
|
<text name="spu_status_act_fetchWagon" text="fetching wagon"/>
|
||||||
|
<text name="spu_status_act_waitWagon" text="waiting for wagon"/>
|
||||||
|
<text name="spu_status_act_emptying" text="emptying the wagon"/>
|
||||||
|
<text name="spu_status_act_homing" text="heading home"/>
|
||||||
|
<text name="spu_status_stuck" text="%s standing still (%d min)"/>
|
||||||
|
<text name="spu_dep_warn" text="Warning: you switched off %s (%s) — %s uses it. Stock lasts about %d days."/>
|
||||||
|
<text name="spu_dep_warnNoDays" text="Warning: you switched off %s (%s) — %s uses it."/>
|
||||||
|
<text name="spu_attention_depOff" text="Switched off but needed: %s"/>
|
||||||
|
<text name="spu_attention_depOffMore" text="Switched off but needed: %s +%d"/>
|
||||||
|
<text name="spu_attention_hintFactories" text=" → click: see Factories"/>
|
||||||
</texts>
|
</texts>
|
||||||
</l10n>
|
</l10n>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||||
<modDesc descVersion="107">
|
<modDesc descVersion="107">
|
||||||
<author>masterdraco</author>
|
<author>masterdraco</author>
|
||||||
<version>1.60.12.0</version>
|
<version>1.62.7.0</version>
|
||||||
<title>
|
<title>
|
||||||
<en>AD Smart Pickup</en>
|
<en>AD Smart Pickup</en>
|
||||||
</title>
|
</title>
|
||||||
@@ -31,6 +31,9 @@
|
|||||||
<sourceFile filename="adSources.lua"/>
|
<sourceFile filename="adSources.lua"/>
|
||||||
<sourceFile filename="adBuildings.lua"/>
|
<sourceFile filename="adBuildings.lua"/>
|
||||||
<sourceFile filename="adOutbound.lua"/>
|
<sourceFile filename="adOutbound.lua"/>
|
||||||
|
<sourceFile filename="adTipSide.lua"/>
|
||||||
|
<sourceFile filename="adLoadDiag.lua"/>
|
||||||
|
<sourceFile filename="adSellBlock.lua"/>
|
||||||
<sourceFile filename="adUnloadWait.lua"/>
|
<sourceFile filename="adUnloadWait.lua"/>
|
||||||
<sourceFile filename="adFieldModel.lua"/>
|
<sourceFile filename="adFieldModel.lua"/>
|
||||||
<sourceFile filename="adFieldFleet.lua"/>
|
<sourceFile filename="adFieldFleet.lua"/>
|
||||||
@@ -54,6 +57,25 @@
|
|||||||
<sourceFile filename="adFieldJobs.lua"/>
|
<sourceFile filename="adFieldJobs.lua"/>
|
||||||
<sourceFile filename="adMonthView.lua"/>
|
<sourceFile filename="adMonthView.lua"/>
|
||||||
<sourceFile filename="adProfiler.lua"/>
|
<sourceFile filename="adProfiler.lua"/>
|
||||||
|
<sourceFile filename="adChainModel.lua"/>
|
||||||
|
<sourceFile filename="adCropPlan.lua"/>
|
||||||
|
<sourceFile filename="adChainActions.lua"/>
|
||||||
|
<sourceFile filename="adDependency.lua"/>
|
||||||
|
<sourceFile filename="adChainData.lua"/>
|
||||||
|
<sourceFile filename="adChain.lua"/>
|
||||||
|
<sourceFile filename="adChainRun.lua"/>
|
||||||
|
<sourceFile filename="adChainView.lua"/>
|
||||||
|
<sourceFile filename="adStatusView.lua"/>
|
||||||
|
<sourceFile filename="adStatusHud.lua"/>
|
||||||
<sourceFile filename="adSmartPickup.lua"/>
|
<sourceFile filename="adSmartPickup.lua"/>
|
||||||
</extraSourceFiles>
|
</extraSourceFiles>
|
||||||
|
<actions>
|
||||||
|
<action name="SPU_TOGGLE_STATUS"/>
|
||||||
|
</actions>
|
||||||
|
<inputBinding>
|
||||||
|
<actionBinding action="SPU_TOGGLE_STATUS">
|
||||||
|
<!-- numpad * (Insert bruges af FS25_UniversalAutoload) -->
|
||||||
|
<binding device="KB_MOUSE_DEFAULT" input="KEY_KP_multiply"/>
|
||||||
|
</actionBinding>
|
||||||
|
</inputBinding>
|
||||||
</modDesc>
|
</modDesc>
|
||||||
|
|||||||
@@ -16,11 +16,16 @@ Alt sættes op i menuen **Smart Pickup** i ESC-menuen.
|
|||||||
startes eller stoppes samlet fra en farvet liste.
|
startes eller stoppes samlet fra en farvet liste.
|
||||||
- **Flow-vogne og maskinpulje:** en traktor uden vogn kan selv hente sin vogn ved en Connect-markør og
|
- **Flow-vogne og maskinpulje:** en traktor uden vogn kan selv hente sin vogn ved en Connect-markør og
|
||||||
sætte den på plads igen. Frakoblede redskaber og ledige traktorer bruges som en fælles pulje.
|
sætte den på plads igen. Frakoblede redskaber og ledige traktorer bruges som en fælles pulje.
|
||||||
|
- **Produktionskæder:** slå et produkt til i en fabrik, og moden holder det kørende tilbage til marken. Fanen Kæder viser træet, marker markeret "Fri til kæden" skifter afgrøde automatisk, og andre marker får forslag, du godkender eller afviser. Mellemled og flows oprettes efter behov.
|
||||||
- **Markarbejde:** slåning, høst, klargøring, såning og pleje på alle marker. AutoDrive kører på vejen,
|
- **Markarbejde:** slåning, høst, klargøring, såning og pleje på alle marker. AutoDrive kører på vejen,
|
||||||
og Courseplay arbejder på marken.
|
og Courseplay arbejder på marken.
|
||||||
- **Marksiloer:** varer i marksiloer køres automatisk hjem til gårdens lagre.
|
- **Marksiloer:** varer i marksiloer køres automatisk hjem til gårdens lagre.
|
||||||
- **Lagerfaner:** beholdning, værdi og forbrug pr. vare og bygning. Listen "Kræver handling" opretter
|
- **Lagerfaner:** beholdning, værdi og forbrug pr. vare og bygning. Listen "Kræver handling" opretter
|
||||||
det flow, der løser problemet.
|
det flow, der løser problemet.
|
||||||
|
- **Statusvindue:** numpad \* viser et lille vindue øverst til højre med flow-traktorernes handling og fyld,
|
||||||
|
markarbejde i gang, kæder med manglende led og advarsler.
|
||||||
|
- **Advarsel når en nødvendig fabrik slukkes:** slukker du en produktion, som et flow, en stald eller en kæde
|
||||||
|
bruger, advarer moden og viser hvor længe lageret rækker.
|
||||||
- **Stop alt og parkér:** stopper alt, folder redskaberne sammen og parkerer maskinerne. Pausen gemmes
|
- **Stop alt og parkér:** stopper alt, folder redskaberne sammen og parkerer maskinerne. Pausen gemmes
|
||||||
med savegamet.
|
med savegamet.
|
||||||
|
|
||||||
@@ -44,20 +49,13 @@ Alt sættes op i menuen **Smart Pickup** i ESC-menuen.
|
|||||||
|
|
||||||
## Feedback
|
## Feedback
|
||||||
|
|
||||||
Fejl og idéer: opret et issue her i repoet. Vedlæg gerne `log.txt` fra
|
Fejl og idéer: giv udvikleren besked (issues her kræver en konto på git.powerdata.dk). Vedlæg gerne `log.txt` fra
|
||||||
`Documents/My Games/FarmingSimulator2025/`, eller i det mindste linjerne der starter med `ADSmartPickup`.
|
`Documents/My Games/FarmingSimulator2025/`, eller i det mindste linjerne der starter med `ADSmartPickup`.
|
||||||
|
|
||||||
## Udvikling
|
## Udvikling
|
||||||
|
|
||||||
- Mod-koden ligger i `FS25_ADSmartPickup/`, og testene ligger i `tests/` (ren Lua, kører uden spillet).
|
- Mod-koden ligger i `FS25_ADSmartPickup/`.
|
||||||
- Kør testene:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
for t in tests/test_*.lua; do luajit $t >/dev/null 2>&1 || echo FAIL $t; done
|
|
||||||
```
|
|
||||||
|
|
||||||
- Byg zip: `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .`
|
- Byg zip: `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .`
|
||||||
- Design og planer: `docs/superpowers/`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -77,17 +75,23 @@ is set up in the **Smart Pickup** menu in the ESC menu.
|
|||||||
start or stop them from a colour-coded list.
|
start or stop them from a colour-coded list.
|
||||||
- **Flow trailers and machine pool:** a tractor without a trailer can fetch its trailer at a Connect
|
- **Flow trailers and machine pool:** a tractor without a trailer can fetch its trailer at a Connect
|
||||||
marker and park it again. Detached implements and free tractors form a shared pool.
|
marker and park it again. Detached implements and free tractors form a shared pool.
|
||||||
|
- **Production chains:** switch on a product in a factory and the mod keeps it running back to the field. The Chains tab shows the tree, fields marked "Free for chains" change crop automatically, and other fields get suggestions you approve or reject. Intermediate goods and flows are set up as needed.
|
||||||
- **Field work:** mowing, harvesting, preparation, sowing and crop care on every field. AutoDrive
|
- **Field work:** mowing, harvesting, preparation, sowing and crop care on every field. AutoDrive
|
||||||
drives the roads and Courseplay works the field.
|
drives the roads and Courseplay works the field.
|
||||||
- **Field silos:** goods in field silos are brought home to the farm's storages automatically.
|
- **Field silos:** goods in field silos are brought home to the farm's storages automatically.
|
||||||
- **Storage tabs:** stock, value and consumption per good and building. The "Needs attention" list
|
- **Storage tabs:** stock, value and consumption per good and building. The "Needs attention" list
|
||||||
creates the flow that fixes the problem.
|
creates the flow that fixes the problem.
|
||||||
|
- **Status window:** numpad \* shows a small window in the top right with the flow tractors' action and
|
||||||
|
fill, field work in progress, chains with missing links and warnings.
|
||||||
|
- **Warning when a needed factory is switched off:** switch off a production that a flow, a barn or a chain
|
||||||
|
uses, and the mod warns you and shows how long the stock lasts.
|
||||||
- **Stop all and park:** stops everything, folds implements and parks the machines. The pause is saved
|
- **Stop all and park:** stops everything, folds implements and parks the machines. The pause is saved
|
||||||
with the savegame.
|
with the savegame.
|
||||||
|
|
||||||
**Install:** download `FS25_ADSmartPickup.zip` from
|
**Install:** download `FS25_ADSmartPickup.zip` from
|
||||||
[Releases](https://git.powerdata.dk/masterdraco/fs25-adsmartpickup/releases). Put the zip, not unpacked,
|
[Releases](https://git.powerdata.dk/masterdraco/fs25-adsmartpickup/releases). Put the zip, not unpacked,
|
||||||
in `Documents/My Games/FarmingSimulator2025/mods/`.
|
in `Documents/My Games/FarmingSimulator2025/mods/`. Enable the mod together with AutoDrive when you pick
|
||||||
|
the savegame. The **Smart Pickup** menu is in the ESC menu, just above Settings.
|
||||||
|
|
||||||
**Requirements:** AutoDrive is required. Courseplay is needed for field work, and Universal Autoload is
|
**Requirements:** AutoDrive is required. Courseplay is needed for field work, and Universal Autoload is
|
||||||
needed for pallets.
|
needed for pallets.
|
||||||
@@ -95,5 +99,8 @@ needed for pallets.
|
|||||||
**Docs:** [User manual](docs/USER_MANUAL.md) · [Danish manual](docs/BRUGERMANUAL.md) ·
|
**Docs:** [User manual](docs/USER_MANUAL.md) · [Danish manual](docs/BRUGERMANUAL.md) ·
|
||||||
[Changelog](CHANGELOG.md)
|
[Changelog](CHANGELOG.md)
|
||||||
|
|
||||||
**Feedback:** open an issue in this repository. Attach `log.txt`, or at least the lines starting with
|
**Feedback:** tell the developer (issues here need an account on git.powerdata.dk). Attach `log.txt`, or at least the lines starting with
|
||||||
`ADSmartPickup`.
|
`ADSmartPickup`.
|
||||||
|
|
||||||
|
**Development:** the mod code is in `FS25_ADSmartPickup/`. Build the zip with
|
||||||
|
`cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .`
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
# Simuleringer
|
|
||||||
|
|
||||||
`bench_hak.lua` genskaber "hak hvert 5. sekund" uden spillet: falsk verden i brugerens proportioner
|
|
||||||
(13.550 waypoints, 600 markører, 120 stationer, 40 køretøjer, 15 holdende vogne) og modulernes rigtige kode.
|
|
||||||
|
|
||||||
luajit bench/bench_hak.lua FS25_ADSmartPickup # nuværende kode
|
|
||||||
git worktree add /tmp/old <commit>; luajit bench/bench_hak.lua /tmp/old/FS25_ADSmartPickup # sammenlign
|
|
||||||
|
|
||||||
Resultat 26/9: værste frame 124,7 ms (v1.27.3) -> 0,9 ms (v1.28.0), 0 tunge frames efter opvarmning.
|
|
||||||
luajit er hurtigere end spillets Lua: forholdet før/efter er det der tæller, ikke de absolutte ms.
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
-- Simulering af "hak hvert 5. sekund" (kør: luajit bench/bench_hak.lua <mappe med modulerne>)
|
|
||||||
-- Falsk verden i brugerens proportioner: 13.550 waypoints, 600 markører (150 Wait), 120 stationer med 3
|
|
||||||
-- triggere, 40 køretøjer, 15 holdende vogne der genplanlægger hvert 5. s. Kører modulernes RIGTIGE kode
|
|
||||||
-- (Wait-punktsøgning, markør -> station, salgssteder) frame for frame og måler det værste frame.
|
|
||||||
-- luajit er hurtigere end spillets Lua; FORHOLDET mellem før og efter er det der tæller.
|
|
||||||
local dir = arg[1] or "FS25_ADSmartPickup"
|
|
||||||
math.randomseed(42)
|
|
||||||
local function noop() end
|
|
||||||
Logging = {info = noop, warning = noop}
|
|
||||||
MathUtil = {vector2Length = function(x, z) return math.sqrt(x * x + z * z) end}
|
|
||||||
function entityExists() return true end
|
|
||||||
local nodes = {}
|
|
||||||
function getWorldTranslation(node) local p = nodes[node]; return p[1], 0, p[2] end
|
|
||||||
local simTime = 0
|
|
||||||
g_currentMission = {time = 0, getFarmId = function() return 1 end, accessHandler = {canFarmAccess = function() return true end}}
|
|
||||||
function netGetTime() return os.clock() * 1000 end
|
|
||||||
addModEventListener = noop
|
|
||||||
Utils = {prependedFunction = function(a) return a end}
|
|
||||||
|
|
||||||
-- vejnet og markører
|
|
||||||
local wayPoints, markers = {}, {}
|
|
||||||
for id = 1, 13550 do
|
|
||||||
local x, z = (id % 120) * 60 - 3600, math.floor(id / 120) * 60 - 3600
|
|
||||||
wayPoints[id] = {id = id, x = x, z = z, out = {id + 1 <= 13550 and id + 1 or 1}, incoming = {id > 1 and id - 1 or 13550}}
|
|
||||||
end
|
|
||||||
for index = 1, 600 do
|
|
||||||
local id = index * 22
|
|
||||||
table.insert(markers, {id = id, markerIndex = index, name = (index <= 150 and "Wait " or "Marker ") .. index, group = index <= 150 and "Cow Wait" or "All"})
|
|
||||||
end
|
|
||||||
-- stationer ved hver 5. markør, 3 triggere hver
|
|
||||||
local stations = {}
|
|
||||||
for index = 1, 120 do
|
|
||||||
local wp = wayPoints[markers[150 + index].id]
|
|
||||||
local triggers = {}
|
|
||||||
for t = 1, 3 do
|
|
||||||
local node = #nodes + 1
|
|
||||||
nodes[node] = {wp.x + t, wp.z + t}
|
|
||||||
table.insert(triggers, {exactFillRootNode = node})
|
|
||||||
end
|
|
||||||
table.insert(stations, {unloadTriggers = triggers, owningPlaceable = {}, getOwnerFarmId = function() return 1 end, name = "S" .. index,
|
|
||||||
isa = function() return false end, getIsFillTypeAllowed = function() return true end})
|
|
||||||
end
|
|
||||||
g_currentMission.storageSystem = {getUnloadingStations = function() return stations end, getLoadingStations = function() return {} end}
|
|
||||||
g_currentMission.placeableSystem = {placeables = {}}
|
|
||||||
-- køretøjer
|
|
||||||
local vehicles = {}
|
|
||||||
for index = 1, 40 do
|
|
||||||
local node = #nodes + 1
|
|
||||||
nodes[node] = {math.random(-3600, 3600), math.random(-3600, 3600)}
|
|
||||||
table.insert(vehicles, {rootNode = node, spec_motorized = {}, getChildVehicles = function() return {} end})
|
|
||||||
end
|
|
||||||
g_currentMission.vehicleSystem = {vehicles = vehicles}
|
|
||||||
local adEnv = {ADGraphManager = {
|
|
||||||
getMapMarkers = function() return markers end,
|
|
||||||
getWayPointById = function(_, id) return wayPoints[id] end,
|
|
||||||
getMapMarkerByWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id then return m end end end,
|
|
||||||
getPathTo = function(_, _, id) return {{id = id}} end,
|
|
||||||
}}
|
|
||||||
|
|
||||||
for _, file in ipairs({"adCache.lua", "adSupplyPlanner.lua", "adWaitPool.lua", "adPrices.lua", "adSmartPickup.lua"}) do
|
|
||||||
local chunk = loadfile(dir .. "/" .. file)
|
|
||||||
if chunk ~= nil then pcall(chunk) end
|
|
||||||
end
|
|
||||||
if ADCache ~= nil then ADCache.enabled = true end
|
|
||||||
ADPrices = ADPrices or {}
|
|
||||||
ADPrices.isSellingStation = ADPrices.isSellingStation or function() return true end
|
|
||||||
|
|
||||||
-- én genplanlægning for en holdende vogn: Wait-punkt + stationsopslag over alle markører (salgssteder/lager)
|
|
||||||
local function replan(vehicle)
|
|
||||||
ADWaitPool.pickFree(adEnv, vehicle, "Cow Wait", function() return true end)
|
|
||||||
for _, marker in ipairs(markers) do
|
|
||||||
ADSmartPickup.getUnloadStationAtWayPointId(adEnv, marker.id, 1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local t0 = os.clock()
|
|
||||||
replan(vehicles[1])
|
|
||||||
local single = (os.clock() - t0) * 1000
|
|
||||||
|
|
||||||
-- 30 s med 60 fps; 15 holdende vogne. Gammel kode: alle timere starter på 0. Ny: jitter hvis koden har det.
|
|
||||||
local jitter = ADCache ~= nil and 5000 or 0 -- ny kode spreder vognenes genplanlægning over 0-5 s (adOutbound CHECK_JITTER_MS)
|
|
||||||
local timers = {}
|
|
||||||
for index = 1, 15 do timers[index] = jitter > 0 and math.random(0, jitter) or 0 end
|
|
||||||
local worst, steady, frames = 0, 0, 0
|
|
||||||
for frame = 1, 60 * 60 do
|
|
||||||
simTime = simTime + 16.7
|
|
||||||
g_currentMission.time = simTime
|
|
||||||
local started = os.clock()
|
|
||||||
for index = 1, 15 do
|
|
||||||
timers[index] = timers[index] + 16.7
|
|
||||||
if timers[index] >= 5000 then
|
|
||||||
timers[index] = 0
|
|
||||||
replan(vehicles[index])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local spent = (os.clock() - started) * 1000
|
|
||||||
if spent > worst then worst = spent end
|
|
||||||
if simTime > 12000 and spent > steady then steady = spent end
|
|
||||||
if simTime > 12000 and spent > 3 then heavy = (heavy or 0) + 1; heavyAt = heavyAt or {}; if #heavyAt < 8 then table.insert(heavyAt, string.format("%.1fs:%.1fms", simTime / 1000, spent)) end end
|
|
||||||
frames = frames + 1
|
|
||||||
end
|
|
||||||
print(" tunge frames (>3 ms) efter opvarmning:", heavy or 0, table.concat(heavyAt or {}, " "))
|
|
||||||
print(string.format("%-12s én genplanlægning %.2f ms | værste frame (60 s): %.2f ms | værste efter opvarmning: %.2f ms", dir:find("old") and "FØR" or "EFTER", single, worst, steady))
|
|
||||||
+188
-6
@@ -1,6 +1,6 @@
|
|||||||
# AD Smart Pickup — brugermanual
|
# AD Smart Pickup — brugermanual
|
||||||
|
|
||||||
Version 1.60.11 · Farming Simulator 25 · tillæg til AutoDrive
|
Version 1.61.0 · Farming Simulator 25 · tillæg til AutoDrive
|
||||||
|
|
||||||
*English version: [USER_MANUAL.md](USER_MANUAL.md)*
|
*English version: [USER_MANUAL.md](USER_MANUAL.md)*
|
||||||
|
|
||||||
@@ -93,8 +93,10 @@ til tom*: den vare, der slipper op først, hentes først.
|
|||||||
|
|
||||||
**Reserve.** Det, gårdens egne stalde og fabrikker skal bruge det næste døgn. Reserven sælges aldrig.
|
**Reserve.** Det, gårdens egne stalde og fabrikker skal bruge det næste døgn. Reserven sælges aldrig.
|
||||||
|
|
||||||
**Sæt.** En traktor med redskab eller vogn koblet på, eller en selvkørende maskine. Et **fast sæt** er et
|
**Sæt.** En traktor med redskab eller vogn koblet på, eller en selvkørende maskine. Et redskab eller en
|
||||||
redskab, der ikke har en Connect-markør. Det bliver siddende og udlånes aldrig.
|
vogn er **bundet** og udlånes aldrig, når det hører til et flow (flowets traktorer og vogne, også når flowet
|
||||||
|
er stoppet), eller når det ikke har en Connect-markør (et **fast sæt**, fx en vintraktor med sit
|
||||||
|
vinredskab).
|
||||||
|
|
||||||
**Maskinpulje.** Frakoblede redskaber og vogne med parkering og Connect-markør samt ledige traktorer uden
|
**Maskinpulje.** Frakoblede redskaber og vogne med parkering og Connect-markør samt ledige traktorer uden
|
||||||
redskab. Moden parrer dem selv, når der er brug for det.
|
redskab. Moden parrer dem selv, når der er brug for det.
|
||||||
@@ -129,6 +131,12 @@ Det er **mappens** navn, der tæller. En markør, der hedder *F91 Wait* i en mar
|
|||||||
Wait-punkt. Wait-markører bliver aldrig brugt som kilde eller mål. Et Wait-punkt regnes som optaget,
|
Wait-punkt. Wait-markører bliver aldrig brugt som kilde eller mål. Et Wait-punkt regnes som optaget,
|
||||||
når det er reserveret, eller når et andet køretøj holder højst 6 m fra det.
|
når det er reserveret, eller når et andet køretøj holder højst 6 m fra det.
|
||||||
|
|
||||||
|
> **Vigtigt: parkering og hentning virker kun ved at bakke.** Traktoren skal **bakke** redskabet ind på
|
||||||
|
> parkeringen og **bakke** hen til trækøjet, når den henter det. Lav derfor AutoDrive-ruten til både
|
||||||
|
> `P <navn>` og `Connect P <navn>` som en **bakrute** (AutoDrives bak-forbindelse) det sidste stykke, så
|
||||||
|
> traktoren står med krogen mod redskabets trækøje. Kører traktoren forlæns ind, vender krogen væk fra
|
||||||
|
> redskabet, og så kan den hverken sætte det rigtigt af eller koble på.
|
||||||
|
|
||||||
### Parkering: "P <navn>"
|
### Parkering: "P <navn>"
|
||||||
|
|
||||||
Moden bruger AutoDrives egen **parkeringsdestination** på hvert køretøj og redskab (sættes i AutoDrive).
|
Moden bruger AutoDrives egen **parkeringsdestination** på hvert køretøj og redskab (sættes i AutoDrive).
|
||||||
@@ -149,7 +157,8 @@ En **Connect-markør** er stedet, hvor traktoren holder for at koble et redskab
|
|||||||
- Kører du selv en traktor med AutoDrives "Kør til" til en Connect-markør, kobler moden også på.
|
- Kører du selv en traktor med AutoDrives "Kør til" til en Connect-markør, kobler moden også på.
|
||||||
- Omdøber du en parkering, skal Connect-markøren omdøbes med.
|
- Omdøber du en parkering, skal Connect-markøren omdøbes med.
|
||||||
|
|
||||||
Et redskab eller en vogn **uden** Connect-markør er et fast sæt og bliver aldrig lånt ud.
|
Et redskab eller en vogn **uden** Connect-markør er et fast sæt og bliver aldrig lånt ud. Det samme
|
||||||
|
gælder et flows vogne, selv om de har en Connect-markør (se [Flow-vogne](#flow-vogne)).
|
||||||
|
|
||||||
### Afkobling: "Afkobl"
|
### Afkobling: "Afkobl"
|
||||||
|
|
||||||
@@ -330,6 +339,27 @@ Sæt **Ja** ud for de bygninger, flowet skal bruge. Bygninger uden markør er sk
|
|||||||
**Vis alle bygninger** til. Listen er sorteret efter navn, så *Silo 9* kommer før *Silo 10*. En bygning,
|
**Vis alle bygninger** til. Listen er sorteret efter navn, så *Silo 9* kommer før *Silo 10*. En bygning,
|
||||||
du har solgt, står som *Fjernet bygning (findes ikke mere) — sæt NEJ*.
|
du har solgt, står som *Fjernet bygning (findes ikke mere) — sæt NEJ*.
|
||||||
|
|
||||||
|
### Statusvindue
|
||||||
|
|
||||||
|
Tryk **numpad \*** (gangetegnet på det numeriske tastatur) for at slå et lille statusvindue til eller fra øverst til højre på skærmen, lige under
|
||||||
|
spillets info-boks. Tasten virker både til fods og i et køretøj og kan ændres under *Indstillinger → Styring*
|
||||||
|
(*AD Smart Pickup: statusvindue*). Vinduet opdateres hvert sekund og viser kun de afsnit, der har indhold:
|
||||||
|
|
||||||
|
- **Flows** — én linje pr. traktor i et kørende flow: *traktor · handling · fyld %* (fx *Milk Collect 1 ·
|
||||||
|
henter Mælk ved Kostald · 45 %*). Grøn = kører, gul = venter (Wait, vogn), rød = flowet har en advarsel,
|
||||||
|
grå = stoppet.
|
||||||
|
- **Marker** — én linje pr. mark med markarbejde i gang: *F92 Høstes · Roeoptager · 45 %* (farve som i Marker-fanen).
|
||||||
|
- **Kæder** — én linje, når kæder mangler et led: *Kæder: 3 manglende led*.
|
||||||
|
- **Advarsler** — fabrikker gården er afhængig af, men som er slukket (*Slukket men nødvendig: Fermenteringssilo 3*,
|
||||||
|
se [Advarsel når en nødvendig fabrik slukkes](#advarsel-når-en-nødvendig-fabrik-slukkes)), køretøjer der har
|
||||||
|
stået stille i over 2 minutter (uden at Smart Pickup selv holder dem ved Wait/udkørsel; solgte køretøjer vises
|
||||||
|
ikke) og ventende kædeforslag (*Kæde: 2 forslag*).
|
||||||
|
|
||||||
|
Højst 18 linjer; resten samles i *+k mere*. Vinduet skjules, mens en menu eller dialog er åben, når HUD'en er
|
||||||
|
skjult, og når du sover. Om vinduet er tændt, gemmes pr. bruger i
|
||||||
|
`Dokumenter/My Games/FarmingSimulator2025/modSettings/FS25_ADSmartPickup/status.xml` (ikke i savegamet);
|
||||||
|
standard er slukket.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Forsyning: hold stalde og fabrikker fyldt
|
## 5. Forsyning: hold stalde og fabrikker fyldt
|
||||||
@@ -606,6 +636,151 @@ Markflådens visning i fanen **Marker** viser puljetraktorer og puljeredskaber m
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Produktionskæder
|
||||||
|
|
||||||
|
> **Vigtigt:** Hver produktion, du tænder i en fabrik, regner moden som noget, du vil have produceret. Moden
|
||||||
|
> prøver så at forsyne den: marker markeret fri skifter til de afgrøder, der mangler, andre marker får
|
||||||
|
> forslag, mellemled tændes, og der oprettes flows med traktorer fra maskinpuljen. Tænd derfor **kun**
|
||||||
|
> produktioner, du faktisk vil have kørende. Vil du stoppe en kæde, så sluk produktionen eller tryk
|
||||||
|
> **[Pause]** i fanen Kæder.
|
||||||
|
|
||||||
|
#### Hvad er en kæde?
|
||||||
|
|
||||||
|
En **kæde** holder en produktion kørende helt tilbage til marken. Du slår et produkt til i en fabrik. Så
|
||||||
|
regner moden baglæns: hvad skal produktionen bruge, hvilke af dine egne fabrikker og stalde laver det, og
|
||||||
|
hvilke afgrøder skal der til. Fabrikken, du slog til, kaldes **rod-produktionen**.
|
||||||
|
|
||||||
|
- Moden ser 12 måneder frem. Den gør først noget, når afgrøden mangler mere end 10 % og mere end 0,5 ha.
|
||||||
|
- Marker, du har markeret **Fri til kæden**, skifter afgrøde af sig selv (højst 2 skift i minuttet).
|
||||||
|
- Andre marker får et **forslag**, som du godkender eller afviser.
|
||||||
|
- En afgrøde, du selv har valgt på en mark, ændres aldrig automatisk.
|
||||||
|
- Kæden gør ingenting de første 2 minutter efter indlæsning. Kan moden ikke læse alle data, slukkes eller
|
||||||
|
stoppes intet.
|
||||||
|
- Marker skiftes og foreslås først, når markscanningen har set alle dine marker. Med mange marker kan det tage
|
||||||
|
et par minutter; imens står der *Marker scannes endnu* nederst i fanen.
|
||||||
|
- Moden bruger først dine egne tændte produktioner som kilde. Den tænder kun en ny, hvis de ikke kan nok.
|
||||||
|
|
||||||
|
#### Fanen Kæder
|
||||||
|
|
||||||
|
Fanen **Kæder** viser et træ pr. rod-produktion: øverst rod-produktionen, under den hvert led med
|
||||||
|
kolonnerne **Vare**, **Kilde**, **l/md**, **Status** og **Lager**. Er der ingen kæder, står der *Ingen kæder:
|
||||||
|
slå et produkt til i en fabrik*.
|
||||||
|
|
||||||
|
- Rod-produktionens status er *dækket*, *mangel*, *venter* eller *på pause*. Efter navnet står antal
|
||||||
|
forslag.
|
||||||
|
- En farvet prik viser status: **grøn** kører, **gul** venter, **rød** fejl, **grå** stoppet. Statusteksten
|
||||||
|
får samme farve.
|
||||||
|
- Rækkerne folder ud og ind. Fold ud for at se leddene, kilderne og markerne.
|
||||||
|
- Advarsler i rækkerne: *flaskehals: -X/md*, *skal købes: X/md*, *kredsløb*, *mangler X ha*, *lager < 3 md*
|
||||||
|
og *ingen mark*.
|
||||||
|
- Markvarer, der ikke er en afgrødes hovedvare — fx hø, tørret lucerne og kløver, hakkelse, helsæd og halm —
|
||||||
|
står med kilden *fra marker*. Dem høster eller slår du selv, så kæden laver ingen afgrødeplan og ingen
|
||||||
|
advarsel for dem; lageret står i kolonnen **Lager**.
|
||||||
|
- **[Pause]** stopper kædens nye handlinger for den rod-produktion. Det, den bruger, slukkes ikke. Andre
|
||||||
|
kæder kører videre. **[Start]** genoptager den.
|
||||||
|
- Nederst står *Automatiske skift siden indlæsning*.
|
||||||
|
|
||||||
|
#### Manglende led (røde beskeder)
|
||||||
|
|
||||||
|
Mangler et led i en kæde, forklarer moden hvorfor og hvad du kan gøre. Teksten står tre steder:
|
||||||
|
|
||||||
|
- **I fanen Kæder**: fold kæden ud; under den røde vare står årsagen i rødt.
|
||||||
|
- **I Kræver handling**: én samlet rød linje for alle kæder, fx *Kæder: 3 manglende led*. Den står under
|
||||||
|
lagerets røde linjer (løber tør / næsten fuldt). Et klik åbner fanen Kæder med kæderne foldet ud.
|
||||||
|
- **Som besked på skærmen**: første gang et nyt led mangler, med kædens navn foran (fx *Brød: …*). Samme besked kommer først igen, når problemet har
|
||||||
|
været væk og vender tilbage. Der kommer højst 3 beskeder ad gangen; resten samles i én besked: *+5 manglende led — se fanen
|
||||||
|
Kæder*. De første 2 minutter efter indlæsning og mens markerne scannes, sendes ingen besked.
|
||||||
|
|
||||||
|
Beskederne:
|
||||||
|
|
||||||
|
- *Ingen af dine fabrikker laver Mel — byg en eller køb*: ingen fabrik, stald eller mark leverer varen.
|
||||||
|
- *Dine stalde leverer ikke nok Mælk — køb resten eller flere dyr*: stalden laver varen, men for lidt.
|
||||||
|
- *Mølle kan kun lave 800 af 1.0k/md*: fabrikken kan ikke følge med (flaskehals).
|
||||||
|
- *Mølle (Mel) er slukket af dig — tænd den eller sæt kæden på pause*: du har selv slukket produktionen.
|
||||||
|
- *Kredsløb omkring Mel — kæden kan ikke beregnes*: varen bruges til at lave sig selv.
|
||||||
|
|
||||||
|
#### Advarsel når en nødvendig fabrik slukkes
|
||||||
|
|
||||||
|
Slukker du en produktion, som gården er afhængig af, advarer moden dig — også når ingen kæde er i gang. En
|
||||||
|
produktion er nødvendig, når en af dens varer bruges af gården:
|
||||||
|
|
||||||
|
- et tændt forsyningsflow har varen valgt som vare,
|
||||||
|
- et tændt flow i auto kører til et mål, der tager varen, og gården forbruger varen,
|
||||||
|
- en stald æder varen lige nu, eller
|
||||||
|
- en aktiv kæde har varen som input (bruger kun kæder varen, meldes det i stedet som manglende led, se ovenfor).
|
||||||
|
|
||||||
|
Ved flows i auto og stalde skal gårdens forbrug af varen kendes; ellers regnes varen ikke som nødvendig.
|
||||||
|
|
||||||
|
Eksempel: du slukker fermenteringssiloerne, men flowene *Cow Food*, *Goat Food* og *Biogas Silage* henter
|
||||||
|
ensilage. Så kommer beskeden *Advarsel: du slukkede Fermenteringssilo 3 (Ensilage) — Cow Food, Goat Food +1
|
||||||
|
bruger det. Lageret rækker ca. 12 dage.* (dagene ud fra gårdens lager og forbrug; kendes de ikke, udelades
|
||||||
|
de). Beskeden kommer én gang pr. slukning, højst et minut efter.
|
||||||
|
|
||||||
|
Så længe produktionen er slukket og stadig nødvendig, står der en rød linje i **Kræver handling**:
|
||||||
|
*Slukket men nødvendig: Fermenteringssilo 3* (højst én linje — den mest hastende; er flere slukket, står
|
||||||
|
antallet bagefter: *Slukket men nødvendig: Fermenteringssilo 3 +2*; også i statusvinduet under Advarsler). Et klik åbner
|
||||||
|
lagerfanen **Fabrikker**.
|
||||||
|
|
||||||
|
Der advares ikke, når:
|
||||||
|
|
||||||
|
- en anden opskrift i samme fabrik laver varen og er tændt (du har valgt, hvilken opskrift der kører — fx én
|
||||||
|
ensilage-opskrift i en fermenteringssilo),
|
||||||
|
- en anden tændt produktion laver varen med nok kapacitet,
|
||||||
|
- gården har lager til mere end 60 dage,
|
||||||
|
- kæden selv slukkede produktionen, eller
|
||||||
|
- produktionen allerede var slukket, da spillet blev indlæst (så vises kun linjen i Kræver handling).
|
||||||
|
|
||||||
|
Produktioner, som ingen flows, stalde eller kæder bruger (fx et bageri uden flow), giver aldrig en advarsel.
|
||||||
|
|
||||||
|
#### Markér en mark fri til kæden
|
||||||
|
|
||||||
|
Åbn marken i markguiden og slå **Fri til kæden (moden må skifte afgrøde)** til. Kun sådan en mark skifter
|
||||||
|
afgrøde automatisk. I fanen står **Marker fri til kæden** med de marker, der er markeret.
|
||||||
|
|
||||||
|
Flerårige afgrøder (vin, oliven, poppel) og græsmarker skiftes aldrig, heller ikke når de er markeret.
|
||||||
|
|
||||||
|
#### Forslag: Godkend og Afvis
|
||||||
|
|
||||||
|
Mangler kæden areal på en mark, der ikke er fri, står der et forslag i kæde-træet: **Forslag [Godkend]**
|
||||||
|
og **[Afvis]**. Der vises højst 3 forslag pr. afgrøde. I **Kræver handling** står ét samlet punkt, fx
|
||||||
|
*Kæde: 3 forslag*; et klik åbner fanen Kæder.
|
||||||
|
|
||||||
|
- **[Godkend]** skifter markens afgrøde.
|
||||||
|
- **[Afvis]** fjerner forslaget. Det kommer igen, hvis afgrødens mangel ændrer sig mere end 25 %.
|
||||||
|
- Er forslaget forældet, står der *Forslaget gælder ikke længere*.
|
||||||
|
- Marker, moden har skiftet på egen hånd, er mærket *skiftet (fri mark)*.
|
||||||
|
|
||||||
|
#### Mellemled
|
||||||
|
|
||||||
|
Kræver en kæde en mellemvare (fx mel til brød), tænder moden den produktion, der laver den.
|
||||||
|
|
||||||
|
- Moden slukker kun en produktion igen, hvis kæden selv tændte den.
|
||||||
|
- Slukker du selv en produktion, som en kæde bruger (også en, du selv tændte), lader kæden den være slukket
|
||||||
|
og regner den ikke som kilde. Først når du tænder en ny produktion i en fabrik, må kæden bruge den igen.
|
||||||
|
- Slukker du rod-produktionen, stoppes kædens flows (se nedenfor), medmindre en anden kæde stadig bruger
|
||||||
|
dem.
|
||||||
|
|
||||||
|
#### Kæde-flows
|
||||||
|
|
||||||
|
For hvert led opretter moden et flow med navnet **Kæde: <vare> → <bygning>** (højst 2 nye i minuttet).
|
||||||
|
|
||||||
|
- Flowet får en ledig pool-traktor og en tipvogn. Moden bruger aldrig en traktor, der hører til et andet
|
||||||
|
flow.
|
||||||
|
- Findes der ingen, oprettes flowet stoppet, og kæden viser *mangler traktor/vogn*.
|
||||||
|
- Flows med paller oprettes altid stoppet, og kæden viser *palle: vælg UAL-vogn*: vælg selv en UAL-vogn.
|
||||||
|
- Slås kæden fra, **stoppes** flowene, og kæden slipper dem. De slettes ikke. Starter du et igen, lader
|
||||||
|
kæden det køre.
|
||||||
|
- Status i kæden: *flow kører*, *flow stoppet*, *mangler traktor/vogn*, *palle: vælg UAL-vogn* eller
|
||||||
|
*intet flow*.
|
||||||
|
|
||||||
|
#### Begrænsninger
|
||||||
|
|
||||||
|
- Hver produktion, du tænder i en fabrik, prøver moden at forsyne. Tænd kun det, du vil have produceret.
|
||||||
|
- Moden **køber ikke** varer og **køber ikke dyr**. Skal noget købes, står det som *skal købes*.
|
||||||
|
- Flerårige afgrøder (vin, oliven, poppel) og græsmarker skiftes aldrig.
|
||||||
|
- Planen bruger grundudbyttet uden gødningsbonus, så den regner hellere med for mange hektar end for få.
|
||||||
|
- Kæden bruger kun dine egne fabrikker og stalde.
|
||||||
|
|
||||||
## 8. Start, stop og gemning
|
## 8. Start, stop og gemning
|
||||||
|
|
||||||
**Start flow** gør følgende på hver godkendt traktor:
|
**Start flow** gør følgende på hver godkendt traktor:
|
||||||
@@ -664,6 +839,8 @@ ens. Øverst vælger du **Vis: Varer | Bygninger**. Fanen husker valget. Tallene
|
|||||||
| *Hvede løber tør om … — gården har …, bruger …/døgn* | Gården bruger mere, end den producerer, og varen rækker under 3 døgn (rød under 1 døgn) |
|
| *Hvede løber tør om … — gården har …, bruger …/døgn* | Gården bruger mere, end den producerer, og varen rækker under 3 døgn (rød under 1 døgn) |
|
||||||
| *Mælk er 92 % fuld — fuld om …* | En produceret vare er mindst 85 % fuld (rød fra 95 %) |
|
| *Mælk er 92 % fuld — fuld om …* | En produceret vare er mindst 85 % fuld (rød fra 95 %) |
|
||||||
| *Sælg Raps nu: prisen er 94 % af årets top — …* | Prisen er mindst 90 % af årets top, beholdningen er værd mindst 10.000, og gården bruger den ikke op |
|
| *Sælg Raps nu: prisen er 94 % af årets top — …* | Prisen er mindst 90 % af årets top, beholdningen er værd mindst 10.000, og gården bruger den ikke op |
|
||||||
|
| *Kæder: 3 manglende led* | En eller flere kæder mangler et led (se [Manglende led](#manglende-led-røde-beskeder)); står under lagerets røde linjer; klik åbner fanen Kæder |
|
||||||
|
| *Slukket men nødvendig: Fermenteringssilo 3 +2* | Du har slukket en produktion, som et flow, en stald eller en kæde bruger (se [Advarsel når en nødvendig fabrik slukkes](#advarsel-når-en-nødvendig-fabrik-slukkes)); klik åbner fanen Fabrikker |
|
||||||
|
|
||||||
**Klik på en linje** for at handle:
|
**Klik på en linje** for at handle:
|
||||||
|
|
||||||
@@ -1029,6 +1206,7 @@ FS25 kan ikke selv omdøbe bygninger, så moden har sine egne navne. De gemmes i
|
|||||||
| `spuNameFleet` | Giver alle traktorer og lastbiler uden for flows butiksnavnet (*Fendt 942 1*, *Fendt 942 2* …) og omdøber deres egen parkering til `P <navn>`, hvis ingen anden deler den. Overskriver egne navne på de køretøjer. Gem spillet bagefter. Pas på: en eksisterende `Connect <gammelt navn>`-markør passer ikke længere |
|
| `spuNameFleet` | Giver alle traktorer og lastbiler uden for flows butiksnavnet (*Fendt 942 1*, *Fendt 942 2* …) og omdøber deres egen parkering til `P <navn>`, hvis ingen anden deler den. Overskriver egne navne på de køretøjer. Gem spillet bagefter. Pas på: en eksisterende `Connect <gammelt navn>`-markør passer ikke længere |
|
||||||
| `spuConnect` | Kobler det nærmeste redskab (inden for ca. 3 m) på den maskine, du sidder i |
|
| `spuConnect` | Kobler det nærmeste redskab (inden for ca. 3 m) på den maskine, du sidder i |
|
||||||
| `spuDetach` | Kobler redskaberne bag den maskine, du sidder i, af |
|
| `spuDetach` | Kobler redskaberne bag den maskine, du sidder i, af |
|
||||||
|
| `spuSellBlock [del af navn]` | Uden navn: viser alle salgssteder, og hvilke der er slået fra. Med navn: slår de salgssteder fra/til, hvis navn indeholder teksten (fx `spuSellBlock market price`). Et salgssted, der er slået fra, bruges hverken til udkørsel eller i priserne. Gemmes pr. savegame. Til salgssteder, hvor aflæsningen går i stå |
|
||||||
| `spuTrip A > B > C` | Kører en tur over flere markører: Connect-markører kobler på, Afkobl eller redskabets egen parkering kobler af, og ved andre markører holdes der 10 sekunder. Mest til test |
|
| `spuTrip A > B > C` | Kører en tur over flere markører: Connect-markører kobler på, Afkobl eller redskabets egen parkering kobler af, og ved andre markører holdes der 10 sekunder. Mest til test |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -1116,7 +1294,7 @@ ADSmartPickup: F109 …
|
|||||||
|
|
||||||
Moden testes løbende, og fejlrapporter er meget velkomne.
|
Moden testes løbende, og fejlrapporter er meget velkomne.
|
||||||
|
|
||||||
1. Opret et issue i repoet: <https://git.powerdata.dk/masterdraco/fs25-adsmartpickup/issues>
|
1. Giv udvikleren besked — eller opret et issue i repoet, hvis du har en konto: <https://git.powerdata.dk/masterdraco/fs25-adsmartpickup/issues>
|
||||||
2. Beskriv, hvad du gjorde, hvad du forventede, og hvad der skete. Nævn versionen (står i
|
2. Beskriv, hvad du gjorde, hvad du forventede, og hvad der skete. Nævn versionen (står i
|
||||||
`log.txt` og i mod-listen).
|
`log.txt` og i mod-listen).
|
||||||
3. Vedlæg `log.txt` fra `Documents/My Games/FarmingSimulator2025/log.txt`. Er filen for stor, så vedlæg
|
3. Vedlæg `log.txt` fra `Documents/My Games/FarmingSimulator2025/log.txt`. Er filen for stor, så vedlæg
|
||||||
@@ -1139,7 +1317,7 @@ Moden testes løbende, og fejlrapporter er meget velkomne.
|
|||||||
| **Connect-markør** | Markøren `Connect <parkeringsnavn>`, hvor en traktor kobler et redskab på |
|
| **Connect-markør** | Markøren `Connect <parkeringsnavn>`, hvor en traktor kobler et redskab på |
|
||||||
| **Afkobl-markør** | Markør, hvor redskaber bag traktoren kobles af |
|
| **Afkobl-markør** | Markør, hvor redskaber bag traktoren kobles af |
|
||||||
| **Solo-traktor** | Traktor uden vogn eller redskab bagpå |
|
| **Solo-traktor** | Traktor uden vogn eller redskab bagpå |
|
||||||
| **Fast sæt** | Redskab uden Connect-markør. Det bliver siddende og udlånes aldrig |
|
| **Fast sæt** | Redskab uden Connect-markør. Det bliver siddende og udlånes aldrig. Et flows vogne udlånes heller aldrig |
|
||||||
| **Maskinpulje** | Frakoblede redskaber/vogne og ledige traktorer, som moden parrer efter behov |
|
| **Maskinpulje** | Frakoblede redskaber/vogne og ledige traktorer, som moden parrer efter behov |
|
||||||
| **Markflåde** | De maskiner, markarbejdet må bruge |
|
| **Markflåde** | De maskiner, markarbejdet må bruge |
|
||||||
| **UAL** | Universal Autoload, mod der læsser paller automatisk |
|
| **UAL** | Universal Autoload, mod der læsser paller automatisk |
|
||||||
@@ -1148,3 +1326,7 @@ Moden testes løbende, og fejlrapporter er meget velkomne.
|
|||||||
| **Reserve** | Det, gårdens egne stalde og fabrikker bruger det næste døgn |
|
| **Reserve** | Det, gårdens egne stalde og fabrikker bruger det næste døgn |
|
||||||
| **Tid til tom** | Hvor længe en fabriks lager af en vare holder ved det nuværende forbrug |
|
| **Tid til tom** | Hvor længe en fabriks lager af en vare holder ved det nuværende forbrug |
|
||||||
| **Marksilo** | En silo ved en mark, der bruges som mellemlager |
|
| **Marksilo** | En silo ved en mark, der bruges som mellemlager |
|
||||||
|
| **Kæde** | Den samlede vej fra en produktion tilbage gennem fabrikker og stalde til afgrøderne på markerne |
|
||||||
|
| **Rod-produktion** | Den produktion, du selv slår til, og som kæden holder kørende |
|
||||||
|
| **Slukket men nødvendig** | En slukket produktion, hvis vare et flow, en stald eller en kæde bruger — moden advarer én gang og viser en rød linje i Kræver handling |
|
||||||
|
| **Fri til kæden** | Markering på en mark, der lader moden skifte afgrøde automatisk |
|
||||||
|
|||||||
@@ -1,255 +0,0 @@
|
|||||||
# Testplan — Markarbejde (fanen Marker), version 1.28.0.0 (+ guider v1.31/v1.32)
|
|
||||||
|
|
||||||
Stor test-session. Kør testene i rækkefølge: de første bygger opsætningen, de senere bygger på den.
|
|
||||||
Hver test har **Gør**, **Forvent** og **Log** (linjer i `log.txt`, der beviser at det virkede). Loglinjer
|
|
||||||
begynder altid med `ADSmartPickup: F<marknr>`.
|
|
||||||
|
|
||||||
Kryds af undervejs. Skriv resultatet (OK / FEJL + hvad der skete) ud for hver test, så retter vi i
|
|
||||||
samme rækkefølge.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 0. Forberedelse
|
|
||||||
|
|
||||||
- [ ] **0.1 Version.** Log: `Available mod: … (Version: 1.28.0.0) FS25_ADSmartPickup`.
|
|
||||||
- [ ] **0.2 Fanen Marker åbner.** ESC → Smart Pickup → fanen **Marker**. Forvent: "Marker indlæses …"
|
|
||||||
i få sekunder, derefter én række pr. ejet mark (også korn-, roe- og bare marker, ikke kun græs).
|
|
||||||
Afgrøde-kolonnen viser afgrøden eller "bar jord".
|
|
||||||
- [ ] **0.3 F-markører.** Marker uden `F<nr>`-markør står med `!` foran marknummeret og "! mangler
|
|
||||||
markør F…". Lav markører til de marker, der skal testes.
|
|
||||||
- [ ] **0.4 Markflåden.** Nederst: alle maskiner med slåmaskine, vender, river, læssevogn, høster,
|
|
||||||
aflæsningsvogn, kultivator, plov, såmaskine, tromle, strigle/sprøjte, gødnings-/kalkspreder,
|
|
||||||
stubhakker — med rolle og bredde. Tjek:
|
|
||||||
- [ ] Høstere viser "N afgrøder".
|
|
||||||
- [ ] Såmaskiner viser "frø ok" eller "!! tom for frø".
|
|
||||||
- [ ] Strigle viser "strigle (småt ukrudt)", ukrudtssprøjte viser "sprøjte".
|
|
||||||
- [ ] En **tom** sprøjte/spreder vises ikke (den har ingen rolle før den har fyld på).
|
|
||||||
- [ ] Maskiner i et Smart Pickup-flow kan godt ses, men sendes aldrig ud.
|
|
||||||
- [ ] **0.5 Bredder.** BiG M 450 viser **40,8 m** (Courseplays egen bredde), læssevognene 40,8 m.
|
|
||||||
|
|
||||||
## 1. Etape 1 genbekræftet (græs → ensilage)
|
|
||||||
|
|
||||||
Rest fra 25/9: slåning og opsamlingsstart virkede; afslutningen blev aldrig set.
|
|
||||||
|
|
||||||
- [ ] **1.1 Afslutning af opsamling.** Mark 22 (eller en anden lille græsmark) på Manuel + Ensilage,
|
|
||||||
BiG M + én Boss Alpin med flueben → **Start nu**.
|
|
||||||
- Forvent: slåmaskine → F22 → Courseplay slår → hjem. Læssevogn → F22 → Courseplay samler op med
|
|
||||||
**slåmaskinens kursus** → AutoDrive tømmer ved fuld → tilbage → færdig → rest til lager → parkering.
|
|
||||||
Marken står **Færdig**, og der kommer en grøn besked "F22 er færdig".
|
|
||||||
- Log: `mower … sendt til F22`, `Courseplay startet (mower)`, `mowing -> collecting`,
|
|
||||||
`læssevogn … aflæsser i <gæringssilo>`, `Courseplay startet (collector, mower-kursus kopieret)`,
|
|
||||||
`collector … færdig`, `collecting -> done`.
|
|
||||||
- [ ] **1.2 Gæringssilo.** Ensilage-læs går til nærmeste **gæringssilo** (FermentingSilo/GrassSilageSilo),
|
|
||||||
ikke en Farma-silo. Log: `aflæsser i <gæringssilo>` eller `ingen gæringssilo kan tage græsset (…)`.
|
|
||||||
- [ ] **1.3 Sidde i maskinen.** Sæt dig i slåmaskinen mens Courseplay kører, og stig ud igen.
|
|
||||||
Forvent: den slippes IKKE (ingen `frigivet`-linje). Kør selv > 3 km/t uden CP → `frigivet — spilleren kører det`.
|
|
||||||
- [ ] **1.4 Sovende køretøj.** Parkerede maskiner kører af sted med det samme ved udsendelse, uden at
|
|
||||||
du skal sætte dig ind.
|
|
||||||
|
|
||||||
## 2. Hø og river
|
|
||||||
|
|
||||||
- [ ] **2.1 Hø uden river.** Plan = **Hø**, Riv = Nej. Slå → **Vendes** (venderen i sin egen bredde) →
|
|
||||||
Samles op (læssevogn på slåkurset, ≥ 40,8 m) → hø-silo (kun almindelig silo, aldrig gæringssilo).
|
|
||||||
Log: `mowing -> tedding`, `Courseplay startet (tedder)`, `tedding -> collecting`.
|
|
||||||
- [ ] **2.2 Smal læssevogn uden river.** En læssevogn under 40,8 m → status "læssevogn for smal:
|
|
||||||
… — sæt Riv til eller brug en bredere".
|
|
||||||
- [ ] **2.3 Riv (midterrive).** Riv = Ja. Log: `river lægger skåret 0.x m fra sin kursuslinje`,
|
|
||||||
derefter `Courseplay startet (collector, rake-kursus kopieret)`. Læssevognen rammer skårene.
|
|
||||||
- [ ] **2.4 Sideriver.** Med en river der lægger skåret til siden: log viser fx `3.2 m`, og
|
|
||||||
`skår ligger 3.2 m til … -> læssevognens forskydning …`. **Tjek at vognen kører OVER skåret.**
|
|
||||||
Kører den ved siden af (forkert side), så skriv det ned — så vender jeg fortegnet.
|
|
||||||
- [ ] **2.5 Riv til ensilage.** Plan = Ensilage + Riv = Ja: Slå → Rives → Samles op.
|
|
||||||
|
|
||||||
## 3. Høst (korn, raps, majs …)
|
|
||||||
|
|
||||||
- [ ] **3.1 Høst.** En moden kornmark, Manuel, høster med passende skærebord + én aflæsningsvogn
|
|
||||||
(traktor + tipvogn/overlæssevogn) med flueben → **Start nu**.
|
|
||||||
- Forvent: høster → F-markør → Courseplay høster. Aflæsningsvognen sendes ud når høsteren
|
|
||||||
arbejder, følger den (Courseplays combine-unloader) og kører fulde læs til siloen.
|
|
||||||
- Log: `harvester … sendt`, `Courseplay startet (harvester)`, `unloader … sendt`,
|
|
||||||
`aflæsningsvogn … følger høsteren -> <silo>`.
|
|
||||||
- [ ] **3.2 Afslutning.** Høsten færdig → `harvesting -> <næste trin eller done>`,
|
|
||||||
`aflæsningsvogn … stoppes — høsten er færdig`, evt. `kører rest-læs … til lager`, parkering.
|
|
||||||
- [ ] **3.3 Forkert skærebord.** Høster der ikke kan tage afgrøden → status "ingen høster i
|
|
||||||
markflåden kan tage opgaven (afgrøde/frø/ukrudt)". Ingen udsendelse.
|
|
||||||
- [ ] **3.4 Flere aflæsningsvogne.** "Aflæsningsvogne under høst" = 2 → to vogne sendes (én ad gangen).
|
|
||||||
- [ ] **3.6 Halm samles op.** Halm efter høst = *Samles op til silo*. Mejetærskeren lægger skår
|
|
||||||
(Courseplay-indstilling halmskår = til), derefter `harvesting -> strawCollecting`, en læssevogn kører
|
|
||||||
**høsterens kursus** (`harvester-kursus kopieret`) og kører halmen til en silo der tager STRAW.
|
|
||||||
- [ ] **3.7 Halm til baller.** Halm = *Presses til baller*: `-> baling` (ballepresser på høsterens
|
|
||||||
kursus) → `-> baleCollecting` (ballesamler/Universal Autoload, `Courseplay startet (ballesamler)`),
|
|
||||||
fulde læs til ballelager (`ballesamler … kører fulde læs til …`).
|
|
||||||
- [ ] **3.8 Halm hakkes.** Halm = *Hakkes*: mejetærskeren lægger IKKE skår (Courseplay halmskår = fra).
|
|
||||||
- [ ] **3.5 Regn.** Når det regner: høst/slå/vend/riv sendes ikke ud, status "venter på tørvejr".
|
|
||||||
Kørende maskiner fortsætter.
|
|
||||||
|
|
||||||
## 4. Roer
|
|
||||||
|
|
||||||
- [ ] **4.1 Roehøst.** Roemark (sukkerroer), roeoptager (Ropa) + aflæsningsvogn → som 3.1, men med
|
|
||||||
roer. Log viser `harvester 'Ropa …'`. Roerne går til nærmeste silo/lager der tager SUGARBEET
|
|
||||||
(brug **Tving lager** hvis de skal et bestemt sted hen, fx roeknuseren).
|
|
||||||
|
|
||||||
## 5. Efter høst: klargøring og såning
|
|
||||||
|
|
||||||
Sæt på en kornmark før høst: Efter høst → Stubbehakning Nej, Jordbehandling **Auto**, **Så bagefter** Ja,
|
|
||||||
Afgrøde "Samme som høstet", Tromle Ja. Kalk Ja.
|
|
||||||
|
|
||||||
- [ ] **5.1 Kæde.** Efter høsten fortsætter marken selv: (kalk hvis jorden trænger) → pløj eller
|
|
||||||
kultivér → så → tromle → **Færdig**. Log: `harvesting -> cultivating`, `cultivating -> sowing` …
|
|
||||||
- [ ] **5.2 Auto-jordbehandling.** Står "Jorden trænger til: pløjning" under marken → plov sendes.
|
|
||||||
Ellers kultivator.
|
|
||||||
- [ ] **5.3 Såning.** Såmaskinen sættes til afgrøden (`setSeedIndex`), og den rigtige afgrøde spirer.
|
|
||||||
- [ ] **5.4 Ude af såperioden.** Afgrøde der ikke kan sås nu → status "<afgrøde> kan ikke sås i denne
|
|
||||||
periode", ingen udsendelse.
|
|
||||||
- [ ] **5.5 Tom for frø / påfyldning.** Såmaskine med lidt frø. Moden leder efter en læssestation med frø
|
|
||||||
på lager og en AutoDrive-markør ved triggeren (fx en silo/big-bag-station med SEEDS).
|
|
||||||
- Findes den: log `… fyldes op med SEEDS ved <markør> af AutoDrive når den er tom`. Når den er tom
|
|
||||||
midt på marken, kører AutoDrive derhen, fylder op, kører tilbage, og Courseplay fortsætter.
|
|
||||||
- Findes den ikke: log `ingen påfyldning af SEEDS med AutoDrive-markør fundet …`. Når den er tom:
|
|
||||||
rød besked `'…' er tom for SEEDS — fyld op, så fortsætter marken`, og marken går IKKE videre.
|
|
||||||
- [ ] **5.6 Bar mark alene.** Vælg en bar mark (stub/pløjet) → **Start nu** → kun klargøring/såning.
|
|
||||||
|
|
||||||
## 6. Pleje mens afgrøden vokser
|
|
||||||
|
|
||||||
- [ ] **6.1 Behov vises.** En voksende mark viser "Jorden trænger til: gødning, ukrudt …" og status
|
|
||||||
"— trænger til Gødes". Manuel: intet sker før **Start nu**.
|
|
||||||
- [ ] **6.2 Gødning.** Start nu (eller Auto) → gødningsspreder/-sprøjte med fyld sendes → `pleje:
|
|
||||||
fertilizing` → `fertilizing -> idle`. Højst 2 gange pr. vækstcyklus.
|
|
||||||
- [ ] **6.3 Ukrudt.** Høstafgrøde med tæt ukrudt → strigle (småt ukrudt) eller ukrudtssprøjte
|
|
||||||
(stort ukrudt, > 4). Græs ukrudtsbehandles ikke.
|
|
||||||
- [ ] **6.4 Kalk.** Jord der trænger til kalk → kalkspreder.
|
|
||||||
- [ ] **6.5 Påfyldning.** Sprøjte/spreder der løber tør → AutoDrive fylder op og kører tilbage.
|
|
||||||
- [ ] **6.6 Tom sprøjte/spreder (v1.29.2).** Tom sættes i gang → log `er tom — fyldes med X ved <markør> og
|
|
||||||
kører så til F<nr>` → AutoDrive (LOAD) fylder op og kører til marken → Courseplay starter med fyld.
|
|
||||||
Ingen påfyldning med AD-markør → sendes IKKE ud; marken viser "er tom for X … fyld den op".
|
|
||||||
Sikkerhedsnet: Courseplay startes aldrig med tom tank ("er stadig tom … ved marken").
|
|
||||||
- [ ] **6.7 Forkert vare (v1.28.8).** Spreder med kalk sendes til gødskning kun hvis ingen tom/gødet spreder
|
|
||||||
findes → log `tømt for N l LIME (skal bruges til fertilizer)` → fyldes med gødning.
|
|
||||||
|
|
||||||
## 7. Multi-maskine (Courseplay multi-tool)
|
|
||||||
|
|
||||||
- [ ] **7.1 To slåmaskiner.** "Maskiner pr. trin" = 2 på en græsmark med to BiG M med flueben.
|
|
||||||
Forvent: begge sendes samtidig, den ene laver kurset (`bane -1`), den anden kopierer det (`bane 1`)
|
|
||||||
og de kører i konvoj. Log: `… sendt … bane -1`, `… bane 1`, `Courseplay startet (mower, bane -1)`,
|
|
||||||
`Courseplay startet (mower, multi-kursus kopieret, bane 1)`.
|
|
||||||
- [ ] **7.2 For få maskiner.** 3 pr. trin men kun 2 ledige → status "multi-tool: slåmaskine — kun 2 af 3 ledige".
|
|
||||||
- [ ] **7.3 Læssevogne på multi-kursus.** Efter 7.1 sendes 2 læssevogne, én pr. bane.
|
|
||||||
|
|
||||||
## 9. Vælg og glem (afgrøde på marken)
|
|
||||||
|
|
||||||
- [ ] **9.1 Kornmark → hvede.** En mark med byg der vokser: vælg *Afgrøde på marken* = Hvede. Linjen siger
|
|
||||||
"Byg får lov at vokse færdig og høstes; derefter klargøres og tilsås med Hvede". Kørsel skjules (auto).
|
|
||||||
Når byggen er moden: høst → (halm) → klargøring → såning af **hvede** (ikke byg). Log:
|
|
||||||
`mål-afgrøde: WHEAT`, `starter (harvest: harvesting)`, `sowing`.
|
|
||||||
- [ ] **9.2 Bar mark.** Bar mark med mål = Hvede: starter selv klargøring + såning, når det er hvede-såtid.
|
|
||||||
Uden for såtid: status "venter på såtid for Hvede".
|
|
||||||
- [ ] **9.3 Græs → græs.** Græsmark med mål = Græs: slås og samles op hver gang, ingen pløjning/såning.
|
|
||||||
- [ ] **9.4 Græs → hvede.** Græsmark med mål = Hvede: slås og samles op én gang, derefter klargøring
|
|
||||||
(jordbehandling-valget vises) og såning af hvede.
|
|
||||||
- [ ] **9.5 Hele året.** Lad en mark med mål køre en hel sæson uden at røre den: vækst → pleje → høst →
|
|
||||||
klargøring → såning → ... uden at hænge og uden dobbelt-såning.
|
|
||||||
- [ ] **9.6 Alle ledige maskiner.** Markflåde: *Brug alle ledige maskiner* = Ja (standard): maskiner uden
|
|
||||||
flueben sendes også ud. = Nej: kun maskiner med flueben. Maskiner du selv kører, maskiner i flows og
|
|
||||||
maskiner AutoDrive/Courseplay allerede kører, bruges aldrig.
|
|
||||||
- [ ] **9.7 Fjern målet.** Vælg *Ingen — planlæg selv*: marken går tilbage til Manuel.
|
|
||||||
|
|
||||||
## 8. Robusthed
|
|
||||||
|
|
||||||
- [ ] **8.1 Genindlæsning midt i et trin.** Gem og genstart under fx opsamling → marken fortsætter på
|
|
||||||
samme trin, sæt sendes ud igen, slåkurset hentes fra slåmaskinen.
|
|
||||||
- [ ] **8.2 Stop mark.** **Stop mark** midt i høst → alle maskiner på marken stopper; høst står **Klar**
|
|
||||||
igen, pleje/klargøring står **Venter**.
|
|
||||||
- [ ] **8.3 Ingen vej.** Mark hvis F-markør ikke er forbundet → status "… nåede ikke F…" / "intet lager:
|
|
||||||
… AutoDrive finder ingen rute …" og nyt forsøg efter 5 min.
|
|
||||||
- [ ] **8.4 Beskeder.** Grøn besked når en mark/pleje er færdig; rød besked ved fejl (hver kun én gang i træk).
|
|
||||||
- [ ] **8.5 Ydelse.** Ingen mærkbar lag med fanen åben og mange marker (én mark scannes pr. sekund).
|
|
||||||
|
|
||||||
## 10. Guiden "Opsæt mark" (v1.31)
|
|
||||||
|
|
||||||
- [ ] **10.1 Oversigt.** Klik F22 i tabellen. Forvent: resumé-linjer (Afgrøde, …, Lager) med "[Ret]", hele
|
|
||||||
status, markør. Ingen lang detaljeliste og ingen markflåde nederst. Knapper: Start nu, Opsæt mark, Markflåde.
|
|
||||||
- [ ] **10.2 Guiden fra bunden.** Opsæt mark → "Trin 1 af N — Afgrøde". Klik Næste gennem alle trin uden at
|
|
||||||
scrolle. Tællingen passer med de viste trin (græs: ingen Klargøring; bar mark: ingen Pleje/Lager).
|
|
||||||
- [ ] **10.3 Valg gemmes.** Vælg fx Hø + Riv, 2 ad gangen, Lager: Vælg selv + en silo → Oversigt viser valgene
|
|
||||||
→ **Kør automatisk**. Forvent: tabellens Plan "Hø + riv (x2)", Kørsel "Auto". Log: `F22 guide gemt: mode=auto …`.
|
|
||||||
- [ ] **10.4 Annullér.** Åbn guiden, skift afgrøde, Annullér → intet ændret. Samme med ESC (menuen lukkes).
|
|
||||||
- [ ] **10.5 Ret én linje.** Klik "Lager: …" i oversigten → "Ret: Lager"; skift → **Gem** → tilbage i
|
|
||||||
oversigten med ændringen; Kørsel (Auto/Manuel) uændret. Tilbage i et Ret-trin fortryder.
|
|
||||||
- [ ] **10.6 Ret fra Oversigt-trinnet.** I guidens sidste trin: klik en linje → ret → **OK** → tilbage på Oversigt.
|
|
||||||
- [ ] **10.7 Mål-afgrøde skifter trin.** Kornmark: vælg Græs som afgrøde → trinnet "Afgrødens valg" viser
|
|
||||||
Ensilage/Hø; tællingen ændres. På Oversigt kun **Kør automatisk** (ingen "Gem uden at starte").
|
|
||||||
- [ ] **10.8 Faste maskiner.** Maskiner → Faste = Ja → sæt flueben ved 2 maskiner → gem. Åbn en anden mark:
|
|
||||||
de 2 vises ikke; linje "2 maskiner er faste på andre marker (F22)". Log: `guide gemt … faste=…`.
|
|
||||||
- [ ] **10.9 Marksilo.** Mark med silo inden for 150 m: Lager-trinnet spørger "Brug marksiloen?". Ja → resumé "Marksilo".
|
|
||||||
- [ ] **10.10 Uden valgt mark.** Ingen mark valgt → Opsæt mark → trin "Mark" med listen; klik en mark → Afgrøde.
|
|
||||||
Næste uden valgt mark giver "Vælg en mark først".
|
|
||||||
- [ ] **10.11 Markflåde.** Knappen Markflåde viser flåden (flueben, "fast på F22"); klik et flueben gemmes;
|
|
||||||
**Til marker** går tilbage.
|
|
||||||
- [ ] **10.12 Klik routes.** Alle Ja/Nej, valg og flueben i guiden reagerer (ingen "GUI-fejl" i statuslinjen/log).
|
|
||||||
|
|
||||||
## 11. Guiden "Opsæt flow" (v1.32)
|
|
||||||
|
|
||||||
- [ ] **11.1 Flows-fanen.** Tavlen øverst; valgt flow (">") har resumé-linjer med "[Ret]" og Status. Ingen
|
|
||||||
lang detaljeliste og ingen faste felter (Run, Wait-pulje …). Knapper: Start flow, Opsæt flow, Nyt flow, Slet flow.
|
|
||||||
- [ ] **11.2 Nyt flow.** Nyt flow → "Trin 1 af 8 — Navn" → *Skift navn …* → skriv navn → Næste gennem Retning
|
|
||||||
(Ud fra bygning), Fra (en fabrik), Varer (sæt en vare på Lager → trinnet Lager dukker op, tællingen bliver 9),
|
|
||||||
Regler, Vent ved, Traktorer (flueben) → Oversigt → **Start flow**. Forvent: flowet findes, kører, log
|
|
||||||
`run '…': …` med kind/products/storeBuildingIds.
|
|
||||||
- [ ] **11.3 Annullér nyt flow.** Nyt flow → Annullér (eller ESC) → intet nyt flow i tavlen.
|
|
||||||
- [ ] **11.4 Ret én linje.** Klik "Traktorer: …" → "Ret: Traktorer" → sæt flueben → **Gem** → tilbage; traktoren er i flowet.
|
|
||||||
- [ ] **11.5 Omdøb.** Klik "Navn: …" → tekstfeltet åbner → nyt navn står i tavlen.
|
|
||||||
- [ ] **11.6 Andre flows skjult.** Traktor i et andet flow vises ikke i trinnet Traktorer; linje "N traktorer kører i andre flows".
|
|
||||||
- [ ] **11.7 Forsyning.** Opsæt et forsyningsflow: Til = stald, Varer = Vælg selv → varerne stalden tager vises; Regler viser Ture og "Kør hjem fra Wait under".
|
|
||||||
- [ ] **11.8 Nyt flow fra Oversigt-fanen** hopper til Flows med guiden åben.
|
|
||||||
- [ ] **11.9 Klik routes.** Alle valg (også pr. vare-politik) reagerer; ingen "GUI-fejl".
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 12. Genbrug af Courseplay-kurser (v1.34)
|
|
||||||
|
|
||||||
Log-linjer at kigge efter (`ADSmartPickup: F<nr> …`): `nyt kursus beregnes til '<maskine>' (<grund>)`,
|
|
||||||
`kursus genbrugt ('<maskine>', <bredde> m) — ingen ny beregning`, `kursus kopieret fra '<A>' til '<B>'`,
|
|
||||||
`markens form er ændret — N gemt(e) kursus(er) glemt`. Registret står i `ADSmartPickup_fields.xml` under `<courses>`.
|
|
||||||
|
|
||||||
- [ ] **12.1 Første gang = ny beregning.** Stor mark (Dakota 16x) → kultivator sendes ud → log `nyt kursus beregnes … (intet kursus gemt)`
|
|
||||||
→ CP starter; `<courses><course vehicle=… field=… width=… sig=… settings=… waypoints=…/>` står i filen efter gem.
|
|
||||||
- [ ] **12.2 Genbrug.** Samme maskine til samme mark igen (næste pleje/klargøring, eller efter "Stop" og start) → log
|
|
||||||
`kursus genbrugt` → CP kører UDEN beregnings-hak og kører hele marken fra start (ikke kun sidste stykke).
|
|
||||||
⚠ Usikkert: accepterer CP 8.1 at jobbet startes på det eksisterende kursus uden `onClickGenerateFieldWorkCourse`?
|
|
||||||
(validate kræver kun `hasCpCourse`). Tegn på fejl: `Courseplay: …no course…`, eller maskinen kører ingenting.
|
|
||||||
- [ ] **12.3 Efter genstart.** Gem, genindlæs, vent de 2 min → samme maskine til samme mark → `kursus genbrugt`
|
|
||||||
(CP har selv gemt kurset i CpAssignedCourses.xml; antal punkter matcher registret).
|
|
||||||
- [ ] **12.4 Kopi fra anden maskine.** En anden maskine med samme CP-bredde til samme mark → `kursus kopieret fra 'A'`
|
|
||||||
→ den kører kurset (ikke A's rest). ⚠ Usikkert: kopieret kursus + lane/vin som i dag med `cpCopyCourse`.
|
|
||||||
- [ ] **12.5 Ny bredde.** Skift CP-bredden i HUD'en (eller en anden maskine med anden bredde) → `nyt kursus beregnes … (ny bredde)`.
|
|
||||||
- [ ] **12.6 Spilleren laver selv kursus i CP's HUD** på maskinen → næste udsendelse genbruger det IKKE
|
|
||||||
(antal punkter passer ikke) → kopi fra en anden maskine eller `… (kurset på maskinen er ændret)`.
|
|
||||||
- [ ] **12.7 Ændret mark.** Lav marken om (kort-redigering/mark-tilkøb der ændrer omridset) → efter scanning, eller når
|
|
||||||
marken gemmes i "Opsæt mark", log `markens form er ændret — N … glemt` → næste start beregner nyt og gemmer det.
|
|
||||||
- [ ] **12.8 Multi-tool.** 2 sæt på samme mark: lederen genbruger kun et kursus lavet til 2 sæt/samme bane; følgeren kopierer
|
|
||||||
stadig lederens kursus (log `…, multi-kursus kopieret, bane N`), og begge kører hver sin bane.
|
|
||||||
- [ ] **12.9 Vinmark.** Sprøjte (skip 1) og harve (skip 0) på samme vinmark må ikke genbruge hinandens kursus (vin-indstillinger
|
|
||||||
indgår i nøglen); høsteren (offset 0) heller ikke.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Kendte usikkerheder (første gang i spillet)
|
|
||||||
|
|
||||||
| Område | Hvad der er gættet | Tegn på fejl |
|
|
||||||
|---|---|---|
|
|
||||||
| Vækstfase | `getFieldDataAtWorldPosition` anden returværdi = vækststadie | Høstet stub vises som voksende, eller moden mark som bar |
|
|
||||||
| Høsterens afgrøder | `spec_cutter.fruitTypes`/`fruitTypeIndices` | Høster vises med "0 afgrøder" |
|
|
||||||
| Combine-unloader | stationsvalg ud fra silovalget | `ingen aflæsningsstation Courseplay kan bruge` |
|
|
||||||
| Multi-tool | `multiTools` + `laneOffset` + `Course:setPosition` | Begge maskiner kører samme bane |
|
|
||||||
| Sideriver | fortegn på forskydningen | Læssevognen kører ved siden af skåret |
|
|
||||||
| Påfyldning | AutoDrive LOAD til den læssestation moden fandt (AutoDrives eget automatiske mål er slået fra i AutoDrive) | Sættet står ved påfyldningen og læsser ikke, eller kører ikke tilbage |
|
|
||||||
| Ukrudt | `weedSystem`-værdi ≥ 2 = tæt | Ukrudtsbehandling uden ukrudt, eller aldrig |
|
|
||||||
| Nysået som "bar" | nysået afgrøde læses som voksende | Marken står "Færdig" efter såning (ingen dobbelt-såning, men ingen næste høst-start før den er moden) |
|
|
||||||
| Halm-valg i guiden | `fruitType.hasWindrow/windrowName` = afgrøder med halm | Halm-valget mangler ved hvede eller vises ved majs |
|
|
||||||
| Ballesamler | Courseplays BALE_FINDER + AutoDrive til ballelager | Ballesamleren starter ikke, eller kører ikke fulde læs væk |
|
|
||||||
| Kursus-genbrug (v1.34) | CP-job startes på maskinens eksisterende kursus uden ny beregning | `Courseplay: no course`, eller maskinen starter midt/sidst i kurset |
|
|
||||||
|
|
||||||
- [ ] **Høsterens rest (v1.32.2).** Modet høster en mark med aflæsningsvogne → ved høstens slutning stoppes
|
|
||||||
vognene; høsteren med korn i tanken → log `'Mejetærsker' kører rest-læs (N l) til lager` → AutoDrive
|
|
||||||
tømmer den ved lageret → parkering. Kunne AD ikke tømme den: besked "har stadig korn i tanken … tøm den".
|
|
||||||
⚠ Usikkert: kan AutoDrive aflæsse en mejetærskers tank (røret) ved en silo?
|
|
||||||
+198
-7
@@ -1,6 +1,6 @@
|
|||||||
# AD Smart Pickup — user manual
|
# AD Smart Pickup — user manual
|
||||||
|
|
||||||
Version 1.60.11 · Farming Simulator 25 · add-on for AutoDrive
|
Version 1.61.0 · Farming Simulator 25 · add-on for AutoDrive
|
||||||
|
|
||||||
*Dansk version: [BRUGERMANUAL.md](BRUGERMANUAL.md)*
|
*Dansk version: [BRUGERMANUAL.md](BRUGERMANUAL.md)*
|
||||||
|
|
||||||
@@ -93,8 +93,10 @@ empty*: the good that runs out first is fetched first.
|
|||||||
|
|
||||||
**Reserve.** What the farm's own barns and factories will use in the next day. The reserve is never sold.
|
**Reserve.** What the farm's own barns and factories will use in the next day. The reserve is never sold.
|
||||||
|
|
||||||
**Set.** A tractor with an implement or trailer attached, or a self-propelled machine. A **fixed set** is
|
**Set.** A tractor with an implement or trailer attached, or a self-propelled machine. An implement or
|
||||||
an implement that has no Connect marker. It stays attached and is never lent out.
|
trailer is **bound** and never lent out when it belongs to a flow (the flow's tractors and trailers, also
|
||||||
|
while the flow is stopped), or when it has no Connect marker (a **fixed set**, e.g. a vineyard tractor with
|
||||||
|
its vineyard implement).
|
||||||
|
|
||||||
**Machine pool.** Detached implements and trailers with a parking spot and a Connect marker, plus free
|
**Machine pool.** Detached implements and trailers with a parking spot and a Connect marker, plus free
|
||||||
tractors without an implement. The mod pairs them up when needed.
|
tractors without an implement. The mod pairs them up when needed.
|
||||||
@@ -129,6 +131,12 @@ It is the **folder's** name that counts. A marker called *F91 Wait* inside a fie
|
|||||||
point. Wait markers are never used as a source or target. A Wait point counts as occupied when it is
|
point. Wait markers are never used as a source or target. A Wait point counts as occupied when it is
|
||||||
reserved, or when another vehicle stands within 6 m of it.
|
reserved, or when another vehicle stands within 6 m of it.
|
||||||
|
|
||||||
|
> **Important: parking and fetching only work by reversing.** The tractor has to **reverse** the implement
|
||||||
|
> into its parking spot and **reverse** up to the drawbar when it fetches it. So build the AutoDrive route
|
||||||
|
> to both `P <name>` and `Connect P <name>` as a **reverse route** (AutoDrive's reverse connection) for the
|
||||||
|
> last stretch, so the tractor ends with its hitch facing the implement's drawbar. If the tractor drives in
|
||||||
|
> forwards, its hitch points away from the implement, and it can neither drop it properly nor hitch it.
|
||||||
|
|
||||||
### Parking: "P <name>"
|
### Parking: "P <name>"
|
||||||
|
|
||||||
The mod uses AutoDrive's own **park destination** on each vehicle and implement (set in AutoDrive).
|
The mod uses AutoDrive's own **park destination** on each vehicle and implement (set in AutoDrive).
|
||||||
@@ -149,7 +157,8 @@ A **Connect marker** is where the tractor stops to hitch an implement or trailer
|
|||||||
- If you drive a tractor with AutoDrive's "Drive to" to a Connect marker yourself, the mod hitches too.
|
- If you drive a tractor with AutoDrive's "Drive to" to a Connect marker yourself, the mod hitches too.
|
||||||
- If you rename a parking spot, rename the Connect marker as well.
|
- If you rename a parking spot, rename the Connect marker as well.
|
||||||
|
|
||||||
An implement or trailer **without** a Connect marker is a fixed set and is never lent out.
|
An implement or trailer **without** a Connect marker is a fixed set and is never lent out. The same goes
|
||||||
|
for a flow's trailers, even when they have a Connect marker (see [Flow trailers](#flow-trailers)).
|
||||||
|
|
||||||
### Detaching: "Afkobl"
|
### Detaching: "Afkobl"
|
||||||
|
|
||||||
@@ -333,6 +342,29 @@ Set **Yes** for the buildings the flow should use. Buildings without a marker ar
|
|||||||
on **Show all buildings**. The list is sorted by name, so *Silo 9* comes before *Silo 10*. A building you
|
on **Show all buildings**. The list is sorted by name, so *Silo 9* comes before *Silo 10*. A building you
|
||||||
have sold appears as *Removed building (no longer exists) — set NO*.
|
have sold appears as *Removed building (no longer exists) — set NO*.
|
||||||
|
|
||||||
|
### Status window
|
||||||
|
|
||||||
|
Press **numpad \*** (the multiply key on the numeric keypad) to toggle a small status window in the top right corner of the screen, just below
|
||||||
|
the game's info box. The key works both on foot and in a vehicle and can be changed under *Settings →
|
||||||
|
Controls* (*AD Smart Pickup: status window*). The window refreshes every second and only shows sections
|
||||||
|
that have something in them:
|
||||||
|
|
||||||
|
- **Flows** — one line per tractor in a running flow: *tractor · action · fill %* (e.g. *Milk Collect 1 ·
|
||||||
|
fetching Milk at Cow Barn · 45 %*). Green = running, yellow = waiting (Wait, trailer), red = the flow has
|
||||||
|
a warning, grey = stopped.
|
||||||
|
- **Fields** — one line per field with field work in progress: *F92 Harvesting · Beet harvester · 45 %*
|
||||||
|
(colours as in the Fields tab).
|
||||||
|
- **Chains** — one line when chains have a missing link: *Chains: 3 missing links*.
|
||||||
|
- **Warnings** — factories the farm depends on that are switched off (*Switched off but needed: Fermenting
|
||||||
|
silo 3*, see [Warning when a needed factory is switched off](#warning-when-a-needed-factory-is-switched-off)),
|
||||||
|
vehicles that have stood still for more than 2 minutes (without Smart Pickup holding them at Wait/outbound;
|
||||||
|
sold vehicles are not shown) and pending chain suggestions (*Chain: 2 suggestions*).
|
||||||
|
|
||||||
|
At most 18 lines; the rest is summed up as *+k more*. The window is hidden while a menu or dialog is open,
|
||||||
|
when the HUD is hidden and while you sleep. Whether it is on is saved per user in
|
||||||
|
`Documents/My Games/FarmingSimulator2025/modSettings/FS25_ADSmartPickup/status.xml` (not in the savegame);
|
||||||
|
it is off by default.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Supply: keep barns and factories filled
|
## 5. Supply: keep barns and factories filled
|
||||||
@@ -603,7 +635,8 @@ and while the flow is running.
|
|||||||
The machine pool is used by field work and field silos when no ready-made set is free.
|
The machine pool is used by field work and field silos when no ready-made set is free.
|
||||||
|
|
||||||
- **Pool implement/trailer:** detached, owned by the farm, with an AutoDrive parking spot and a Connect
|
- **Pool implement/trailer:** detached, owned by the farm, with an AutoDrive parking spot and a Connect
|
||||||
marker for that parking spot. Implements without a Connect marker are fixed sets and are never lent.
|
marker for that parking spot, and not bound to a flow. Implements without a Connect marker are fixed sets
|
||||||
|
and are never lent.
|
||||||
- **Pool tractor:** an ordinary tractor with nothing attached at the rear, with its own AutoDrive parking
|
- **Pool tractor:** an ordinary tractor with nothing attached at the rear, with its own AutoDrive parking
|
||||||
spot, on AutoDrive's road network and not driven by you. Field implements are only hitched behind
|
spot, on AutoDrive's road network and not driven by you. Field implements are only hitched behind
|
||||||
tractors. Unloading trailers can also be pulled by other vehicles.
|
tractors. Unloading trailers can also be pulled by other vehicles.
|
||||||
@@ -618,6 +651,157 @@ The field fleet view in the **Fields** tab shows pool tractors and pool implemen
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Production chains
|
||||||
|
|
||||||
|
> **Important:** Every production you switch on in a factory counts as something you want produced. The mod
|
||||||
|
> then tries to supply it: fields marked free switch to the crops that are short, other fields get
|
||||||
|
> suggestions, intermediate productions are switched on, and flows are created with tractors from the
|
||||||
|
> machine pool. So **only** switch on productions you actually want running. To stop a chain, switch the
|
||||||
|
> production off or press **[Pause]** in the Chains tab.
|
||||||
|
|
||||||
|
#### What is a chain?
|
||||||
|
|
||||||
|
A **chain** keeps a production running all the way back to the field. You switch on a product in a
|
||||||
|
factory. The mod then works backwards: what the production needs, which of your own factories and barns
|
||||||
|
make it, and which crops are needed. The factory you switched on is the **root production**.
|
||||||
|
|
||||||
|
- The mod looks 12 months ahead. It acts only when a crop is short by more than 10 % and more than 0.5 ha.
|
||||||
|
- Fields you marked **Free for chains** change crop by themselves (at most 2 changes a minute).
|
||||||
|
- Other fields get a **suggestion** that you approve or reject.
|
||||||
|
- A crop you chose on a field is never changed automatically.
|
||||||
|
- The chain does nothing during the first 2 minutes after loading. If the mod cannot read all data,
|
||||||
|
nothing is switched off or stopped.
|
||||||
|
- Fields are changed and suggested only once the field scan has seen all your fields. With many fields this
|
||||||
|
can take a few minutes; meanwhile the tab says *Fields still being scanned* at the bottom.
|
||||||
|
- The mod uses your own running productions as a source first. It switches on a new one only if they are
|
||||||
|
not enough.
|
||||||
|
|
||||||
|
#### The Chains tab
|
||||||
|
|
||||||
|
The **Chains** tab shows one tree per root production: the root at the top, and below it each link with
|
||||||
|
the columns **Goods**, **Source**, **l/mo**, **Status** and **Stock**. With no chains it says *No chains:
|
||||||
|
enable a product in a factory*.
|
||||||
|
|
||||||
|
- The root's status is *covered*, *short*, *waiting* or *paused*. The number of suggestions follows the name.
|
||||||
|
- A coloured dot shows the status: **green** running, **yellow** waiting, **red** error, **grey** stopped.
|
||||||
|
The status text gets the same colour.
|
||||||
|
- Rows fold out and in. Fold out to see the links, sources and fields.
|
||||||
|
- Warnings in the rows: *bottleneck: -X/mo*, *must buy: X/mo*, *loop*, *short X ha*, *stock < 3 mo* and
|
||||||
|
*no field*.
|
||||||
|
- Field goods that are not a crop's main product — for example hay, dried alfalfa and clover, chaff, whole-crop
|
||||||
|
and straw — have the source *from fields*. You harvest or mow them yourself, so the chain makes no crop plan
|
||||||
|
and no warning for them; the stock is shown in the **Stock** column.
|
||||||
|
- **[Pause]** stops the chain's new actions for that root production. What it uses is not switched off.
|
||||||
|
Other chains carry on. **[Resume]** continues it.
|
||||||
|
- At the bottom: *Automatic switches since load*.
|
||||||
|
|
||||||
|
#### Missing links (red messages)
|
||||||
|
|
||||||
|
If a link in a chain is missing, the mod explains why and what you can do. The text appears in three places:
|
||||||
|
|
||||||
|
- **In the Chains tab**: fold the chain out; the reason is shown in red below the red goods row.
|
||||||
|
- **In Needs attention**: one combined red line for all chains, for example *Chains: 3 missing links*. It is
|
||||||
|
listed below the storage's red lines (running out / almost full). Clicking opens the Chains tab with the
|
||||||
|
chains folded out.
|
||||||
|
- **As an on-screen message**: the first time a new link goes missing, with the chain's name in front (for example *Bread: …*). The same message only returns after the
|
||||||
|
problem has gone away and come back. At most 3 messages come at a time; the rest are combined into one message:
|
||||||
|
*+5 more missing links — see the Chains tab*. No messages during the first 2 minutes after loading or while
|
||||||
|
fields are being scanned.
|
||||||
|
|
||||||
|
The messages:
|
||||||
|
|
||||||
|
- *None of your factories makes Flour — build one or buy it*: no factory, barn or field supplies the goods.
|
||||||
|
- *Barns make too little Milk — buy the rest or more animals*: the barn makes the goods, but too little.
|
||||||
|
- *Mill can only make 800 of 1.0k/mo*: the factory cannot keep up (bottleneck).
|
||||||
|
- *Mill (Flour) was turned off by you — turn it on or pause the chain*: you switched the production off yourself.
|
||||||
|
- *Loop around Flour — the chain cannot be calculated*: the goods are needed to make themselves.
|
||||||
|
|
||||||
|
#### Warning when a needed factory is switched off
|
||||||
|
|
||||||
|
If you switch off a production the farm depends on, the mod warns you — also when no chain is running. A
|
||||||
|
production is needed when one of its goods is used by the farm:
|
||||||
|
|
||||||
|
- a running supply flow has the goods chosen as goods,
|
||||||
|
- a running flow in auto goes to a target that takes the goods, and the farm consumes the goods,
|
||||||
|
- a barn is eating the goods right now, or
|
||||||
|
- an active chain has the goods as input (if only chains use the goods, it is reported as a missing link
|
||||||
|
instead, see above).
|
||||||
|
|
||||||
|
For auto flows and barns the farm's consumption of the goods must be known; otherwise the goods do not count as needed.
|
||||||
|
|
||||||
|
Example: you switch off the fermenting silos, but the flows *Cow Food*, *Goat Food* and *Biogas Silage* fetch
|
||||||
|
silage. You then get the message *Warning: you switched off Fermenting silo 3 (Silage) — Cow Food, Goat Food
|
||||||
|
+1 uses it. Stock lasts about 12 days.* (days from the farm's stock and consumption; left out when unknown).
|
||||||
|
The message comes once per switch-off, at most a minute later.
|
||||||
|
|
||||||
|
As long as the production is off and still needed, **Needs attention** shows a red line: *Switched off but
|
||||||
|
needed: Fermenting silo 3* (at most one line — the most urgent; if more are off, the number follows: *Switched off
|
||||||
|
but needed: Fermenting silo 3 +2*; also in the status window under Warnings). Clicking opens the storage
|
||||||
|
tab **Factories**.
|
||||||
|
|
||||||
|
No warning when:
|
||||||
|
|
||||||
|
- another recipe in the same factory makes the goods and is on (you chose which recipe runs — for example one
|
||||||
|
silage recipe in a fermenting silo),
|
||||||
|
- another running production makes the goods with enough capacity,
|
||||||
|
- the farm has stock for more than 60 days,
|
||||||
|
- the chain switched the production off itself, or
|
||||||
|
- the production was already off when the game was loaded (then only the Needs attention line is shown).
|
||||||
|
|
||||||
|
Productions that no flow, barn or chain uses (for example a bakery without a flow) never give a warning.
|
||||||
|
|
||||||
|
#### Mark a field free for chains
|
||||||
|
|
||||||
|
Open the field in the field guide and switch on **Free for chains (mod may change the crop)**. Only such a
|
||||||
|
field changes crop automatically. The tab lists them under **Fields free for the chain**.
|
||||||
|
|
||||||
|
Permanent crops (vine, olive, poplar) and grass fields are never changed, even when marked.
|
||||||
|
|
||||||
|
#### Suggestions: Approve and Reject
|
||||||
|
|
||||||
|
If the chain lacks area on a field that is not free, the chain tree shows a suggestion: **Suggest
|
||||||
|
[Approve]** and **[Reject]**. At most 3 suggestions are shown per crop. **Needs attention** shows one
|
||||||
|
combined entry, for example *Chain: 3 suggestions*; clicking it opens the Chains tab.
|
||||||
|
|
||||||
|
- **[Approve]** changes the field's crop.
|
||||||
|
- **[Reject]** removes the suggestion. It returns if the crop's shortage changes by more than 25 %.
|
||||||
|
- An outdated suggestion says *The suggestion no longer applies*.
|
||||||
|
- Fields the mod changed on its own are marked *switched (free field)*.
|
||||||
|
|
||||||
|
#### Intermediate goods
|
||||||
|
|
||||||
|
If a chain needs an intermediate good (for example flour for bread), the mod switches on the production
|
||||||
|
that makes it.
|
||||||
|
|
||||||
|
- The mod switches a production off again only if the chain switched it on.
|
||||||
|
- If you switch off a production a chain uses (even one you switched on yourself), the chain leaves it off
|
||||||
|
and does not count it as a source. Only when you switch on a new production in a factory may the chain
|
||||||
|
use it again.
|
||||||
|
- If you switch off the root production, the chain's flows are stopped (see below), unless another chain
|
||||||
|
still uses them.
|
||||||
|
|
||||||
|
#### Chain flows
|
||||||
|
|
||||||
|
For each link the mod creates a flow named **Chain: <goods> → <building>** (at most 2 new per minute).
|
||||||
|
|
||||||
|
- The flow gets a free pool tractor and a tipping trailer. The mod never takes a tractor that belongs to
|
||||||
|
another flow.
|
||||||
|
- If none is available, the flow is created stopped and the chain shows *no tractor/trailer*.
|
||||||
|
- Flows with pallets are always created stopped and the chain shows *pallet: UAL trailer*: choose a UAL
|
||||||
|
trailer yourself.
|
||||||
|
- When the chain is switched off, its flows are **stopped** and released by the chain. They are not
|
||||||
|
deleted. If you start one again, the chain lets it run.
|
||||||
|
- Status in the chain: *flow running*, *flow stopped*, *no tractor/trailer*, *pallet: UAL trailer* or
|
||||||
|
*no flow*.
|
||||||
|
|
||||||
|
#### Limitations
|
||||||
|
|
||||||
|
- The mod tries to supply every production you switch on in a factory. Only switch on what you want produced.
|
||||||
|
- The mod does **not buy** goods and does **not buy animals**. What must be bought shows as *must buy*.
|
||||||
|
- Permanent crops (vine, olive, poplar) and grass fields are never changed.
|
||||||
|
- The plan uses the base yield without fertiliser bonus, so it errs on too many hectares rather than too few.
|
||||||
|
- The chain uses only your own factories and barns.
|
||||||
|
|
||||||
## 8. Start, stop and saving
|
## 8. Start, stop and saving
|
||||||
|
|
||||||
**Start flow** does the following for each approved tractor:
|
**Start flow** does the following for each approved tractor:
|
||||||
@@ -677,6 +861,8 @@ every second.
|
|||||||
| *Wheat runs out in … — farm has …, uses …/day* | The farm uses more than it produces, and the good lasts less than 3 days (red below 1 day) |
|
| *Wheat runs out in … — farm has …, uses …/day* | The farm uses more than it produces, and the good lasts less than 3 days (red below 1 day) |
|
||||||
| *Milk is 92 % full — full in …* | A produced good is at least 85 % full (red from 95 %) |
|
| *Milk is 92 % full — full in …* | A produced good is at least 85 % full (red from 95 %) |
|
||||||
| *Sell Canola now: price is 94 % of the yearly top — …* | The price is at least 90 % of the yearly top, the stock is worth at least 10,000, and the farm does not use it up |
|
| *Sell Canola now: price is 94 % of the yearly top — …* | The price is at least 90 % of the yearly top, the stock is worth at least 10,000, and the farm does not use it up |
|
||||||
|
| *Chains: 3 missing links* | One or more chains are missing a link (see [Missing links](#missing-links-red-messages)); listed below the storage's red lines; clicking opens the Chains tab |
|
||||||
|
| *Switched off but needed: Fermenting silo 3 +2* | You switched off a production that a flow, a barn or a chain uses (see [Warning when a needed factory is switched off](#warning-when-a-needed-factory-is-switched-off)); clicking opens the Factories tab |
|
||||||
|
|
||||||
**Click a line** to act on it:
|
**Click a line** to act on it:
|
||||||
|
|
||||||
@@ -1058,6 +1244,7 @@ Open the game's console to use them:
|
|||||||
| `spuNameFleet` | Gives all tractors and trucks outside flows their shop name (*Fendt 942 1*, *Fendt 942 2* …) and renames their own parking spot to `P <name>` if nobody else shares it. Overwrites custom names on those vehicles. Save the game afterwards. Note: an existing `Connect <old name>` marker no longer matches |
|
| `spuNameFleet` | Gives all tractors and trucks outside flows their shop name (*Fendt 942 1*, *Fendt 942 2* …) and renames their own parking spot to `P <name>` if nobody else shares it. Overwrites custom names on those vehicles. Save the game afterwards. Note: an existing `Connect <old name>` marker no longer matches |
|
||||||
| `spuConnect` | Hitches the nearest implement (within about 3 m) to the machine you are sitting in |
|
| `spuConnect` | Hitches the nearest implement (within about 3 m) to the machine you are sitting in |
|
||||||
| `spuDetach` | Detaches the implements behind the machine you are sitting in |
|
| `spuDetach` | Detaches the implements behind the machine you are sitting in |
|
||||||
|
| `spuSellBlock [part of name]` | Without a name: lists all selling points and which ones are switched off. With a name: switches off/on the selling points whose name contains the text (e.g. `spuSellBlock market price`). A switched-off selling point is used neither for outbound trips nor in prices. Saved per savegame. For selling points where unloading keeps stalling |
|
||||||
| `spuTrip A > B > C` | Drives a trip over several markers: Connect markers hitch, Afkobl or the implement's own parking detach, and at other markers it stops for 10 seconds. Mostly for testing |
|
| `spuTrip A > B > C` | Drives a trip over several markers: Connect markers hitch, Afkobl or the implement's own parking detach, and at other markers it stops for 10 seconds. Mostly for testing |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -1146,7 +1333,7 @@ ADSmartPickup: F109 …
|
|||||||
|
|
||||||
The mod is being tested continuously, and bug reports are very welcome.
|
The mod is being tested continuously, and bug reports are very welcome.
|
||||||
|
|
||||||
1. Open an issue in the repository: <https://git.powerdata.dk/masterdraco/fs25-adsmartpickup/issues>
|
1. Tell the developer — or open an issue in the repository if you have an account: <https://git.powerdata.dk/masterdraco/fs25-adsmartpickup/issues>
|
||||||
2. Describe what you did, what you expected, and what happened. Mention the version (shown in `log.txt`
|
2. Describe what you did, what you expected, and what happened. Mention the version (shown in `log.txt`
|
||||||
and in the mod list).
|
and in the mod list).
|
||||||
3. Attach `log.txt` from `Documents/My Games/FarmingSimulator2025/log.txt`. If the file is too big,
|
3. Attach `log.txt` from `Documents/My Games/FarmingSimulator2025/log.txt`. If the file is too big,
|
||||||
@@ -1169,7 +1356,7 @@ The mod is being tested continuously, and bug reports are very welcome.
|
|||||||
| **Connect marker** | The marker `Connect <parking name>`, where a tractor hitches an implement |
|
| **Connect marker** | The marker `Connect <parking name>`, where a tractor hitches an implement |
|
||||||
| **Afkobl marker** | Marker where implements behind the tractor are detached |
|
| **Afkobl marker** | Marker where implements behind the tractor are detached |
|
||||||
| **Solo tractor** | Tractor with no trailer or implement attached at the rear |
|
| **Solo tractor** | Tractor with no trailer or implement attached at the rear |
|
||||||
| **Fixed set** | Implement without a Connect marker. It stays attached and is never lent out |
|
| **Fixed set** | Implement without a Connect marker. It stays attached and is never lent out. A flow's trailers are never lent out either |
|
||||||
| **Machine pool** | Detached implements/trailers and free tractors that the mod pairs up as needed |
|
| **Machine pool** | Detached implements/trailers and free tractors that the mod pairs up as needed |
|
||||||
| **Field fleet** | The machines field work is allowed to use |
|
| **Field fleet** | The machines field work is allowed to use |
|
||||||
| **UAL** | Universal Autoload, a mod that loads pallets automatically |
|
| **UAL** | Universal Autoload, a mod that loads pallets automatically |
|
||||||
@@ -1178,3 +1365,7 @@ The mod is being tested continuously, and bug reports are very welcome.
|
|||||||
| **Reserve** | What the farm's own barns and factories use in the next day |
|
| **Reserve** | What the farm's own barns and factories use in the next day |
|
||||||
| **Time to empty** | How long a factory's stock of a good lasts at the current consumption |
|
| **Time to empty** | How long a factory's stock of a good lasts at the current consumption |
|
||||||
| **Field silo** | A silo near a field used as intermediate storage |
|
| **Field silo** | A silo near a field used as intermediate storage |
|
||||||
|
| **Chain** | The whole path from a production back through factories and barns to the crops in the fields |
|
||||||
|
| **Root production** | The production you switch on yourself, which the chain keeps running |
|
||||||
|
| **Switched off but needed** | A switched-off production whose goods a flow, a barn or a chain uses — the mod warns once and shows a red line in Needs attention |
|
||||||
|
| **Free for chains** | A field marking that lets the mod change the crop automatically |
|
||||||
|
|||||||
@@ -1,221 +0,0 @@
|
|||||||
# Behovsstyret forsyning Implementation Plan
|
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
||||||
|
|
||||||
**Goal:** Én AutoDrive-traktor holder alle input i en produktion (fx TMR-blander) fyldt ved selv at vælge vare + kilde-silo pr. tur.
|
|
||||||
|
|
||||||
**Architecture:** Den eksisterende wrapper om `ADMultipleTargetsManager.getNextPickup` udvides: ved >1 valgt vare i AD læses aflæsningsmålets lager, ingredienserne rangeres efter behov (rene funktioner i ny fil), og en kilde findes blandt alle AD-markører. AD's varevalg indsnævres til én vare pr. tur og gendannes når AD stopper.
|
|
||||||
|
|
||||||
**Tech Stack:** Lua 5.1 (FS25 mod-sandbox), luajit til tests med håndskrevne mocks, AutoDrive 3.0.1.2 via global `FS25_AutoDrive`.
|
|
||||||
|
|
||||||
**Spec:** `docs/superpowers/specs/2026-09-21-behovsstyret-forsyning-design.md`
|
|
||||||
|
|
||||||
## Global Constraints
|
|
||||||
|
|
||||||
- Rør aldrig `FS25_AutoDrive.zip`; al adgang via den globale `FS25_AutoDrive`-tabel.
|
|
||||||
- Enhver fejl i spil-/AD-API → `Logging.warning` + AutoDrives originale `getNextPickup`. Aldrig crash.
|
|
||||||
- Præcis én valgt vare → opførsel uændret; de 11 eksisterende tests forbliver grønne.
|
|
||||||
- Markør↔station-tærskel: 40 m (`MAX_MARKER_TO_TRIGGER_DISTANCE`). `MIN_FREE_LITERS = 1000`.
|
|
||||||
- Rene funktioner muterer ikke input (returnér nye tabeller).
|
|
||||||
- Tests køres fra repo-roden: `luajit tests/<fil>.lua`. Commits uden Co-Authored-By.
|
|
||||||
- Verificerede AD-fakta: `selectedFillTypes` er et array-felt på stateModule og gemmes i savegame; `setFillType(ft)` bevarer valget hvis `ft` er i det; `isActive()`, `getSecondMarker()`, `raiseDirtyFlag()` findes; `ADGraphManager:getMapMarkers()` returnerer array indekseret på `markerIndex`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 1: `adSupplyPlanner.lua` — rene funktioner
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `FS25_ADSmartPickup/adSupplyPlanner.lua`
|
|
||||||
- Test: `tests/test_adSupplyPlanner.lua`
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Produces (global tabel `ADSupplyPlanner`):
|
|
||||||
- `MIN_FREE_LITERS` (number)
|
|
||||||
- `scoreNeed(level, capacity, freeSpace) -> number` (højere = mere trængende)
|
|
||||||
- `rankNeeds(needs) -> ranked` hvor `needs = {{fillType, level, capacity}, …}` og `ranked = {{fillType, level, capacity, freeSpace, score}, …}` sorteret mest trængende først, uden dem med `freeSpace < MIN_FREE_LITERS`
|
|
||||||
- `resolveIngredients(state, selected) -> ingredients|nil` hvor `state = nil | {ingredients = {ft…}, narrowedTo = ft}`
|
|
||||||
- `copyList(list) -> ny liste`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Skriv fejlende test** — `tests/test_adSupplyPlanner.lua`:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- Kør: luajit tests/test_adSupplyPlanner.lua (fra repo-roden)
|
|
||||||
dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")
|
|
||||||
|
|
||||||
local HAY, GRASS, SILAGE = 20, 21, 22
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then
|
|
||||||
print("OK " .. name)
|
|
||||||
else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- scoreNeed: tommere lager scorer højere; ugyldig kapacitet giver 0
|
|
||||||
check("tomt scorer højere end halvfuldt", ADSupplyPlanner.scoreNeed(0, 1000, 1000) > ADSupplyPlanner.scoreNeed(500, 1000, 500), true)
|
|
||||||
check("kapacitet 0 giver score 0", ADSupplyPlanner.scoreNeed(0, 0, 0), 0)
|
|
||||||
|
|
||||||
-- rankNeeds: mest trængende først, fulde springes over, input muteres ikke
|
|
||||||
local needs = {
|
|
||||||
{fillType = HAY, level = 80000, capacity = 100000},
|
|
||||||
{fillType = GRASS, level = 20000, capacity = 100000},
|
|
||||||
{fillType = SILAGE, level = 99500, capacity = 100000},
|
|
||||||
}
|
|
||||||
local ranked = ADSupplyPlanner.rankNeeds(needs)
|
|
||||||
check("fuld ingrediens springes over", #ranked, 2)
|
|
||||||
check("mest trængende først", ranked[1].fillType, GRASS)
|
|
||||||
check("dernæst næstmest trængende", ranked[2].fillType, HAY)
|
|
||||||
check("freeSpace beregnes", ranked[1].freeSpace, 80000)
|
|
||||||
check("input muteres ikke", needs[1].freeSpace, nil)
|
|
||||||
check("tom liste giver tom liste", #ADSupplyPlanner.rankNeeds({}), 0)
|
|
||||||
|
|
||||||
-- rankNeeds: deterministisk ved lige score (laveste fillType først)
|
|
||||||
local tie = ADSupplyPlanner.rankNeeds({
|
|
||||||
{fillType = SILAGE, level = 0, capacity = 5000},
|
|
||||||
{fillType = HAY, level = 0, capacity = 5000},
|
|
||||||
})
|
|
||||||
check("lige score -> laveste fillType først", tie[1].fillType, HAY)
|
|
||||||
|
|
||||||
-- resolveIngredients
|
|
||||||
local multi = {HAY, GRASS}
|
|
||||||
local resolved = ADSupplyPlanner.resolveIngredients(nil, multi)
|
|
||||||
check("multi-valg bliver ingrediensliste", #resolved, 2)
|
|
||||||
check("listen er en kopi", resolved ~= multi, true)
|
|
||||||
check("enkelt-valg uden tilstand -> nil", ADSupplyPlanner.resolveIngredients(nil, {HAY}), nil)
|
|
||||||
local state = {ingredients = {HAY, GRASS, SILAGE}, narrowedTo = GRASS}
|
|
||||||
check("indsnævret valg genbruger gemt liste", #ADSupplyPlanner.resolveIngredients(state, {GRASS}), 3)
|
|
||||||
check("bruger valgte andet -> nil", ADSupplyPlanner.resolveIngredients(state, {HAY}), nil)
|
|
||||||
check("nyt multi-valg erstatter gemt liste", #ADSupplyPlanner.resolveIngredients(state, {HAY, SILAGE}), 2)
|
|
||||||
check("ugyldigt valg -> nil", ADSupplyPlanner.resolveIngredients(state, nil), nil)
|
|
||||||
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Kør og se den fejle** — `luajit tests/test_adSupplyPlanner.lua` → fejler med "cannot open FS25_ADSmartPickup/adSupplyPlanner.lua".
|
|
||||||
|
|
||||||
- [ ] **Step 3: Implementér** — `FS25_ADSmartPickup/adSupplyPlanner.lua`:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- AD Supply Planner
|
|
||||||
-- Rene funktioner (ingen spil-API) til behovsstyret forsyning: hvilken ingrediens
|
|
||||||
-- har aflæsningsmålet mest brug for, og hvilken ingrediensliste gælder for køretøjet.
|
|
||||||
|
|
||||||
ADSupplyPlanner = {}
|
|
||||||
|
|
||||||
-- Mindre fri plads end dette i målet regnes som "fuldt" (undgår ture med læs der ikke kan komme af).
|
|
||||||
ADSupplyPlanner.MIN_FREE_LITERS = 1000
|
|
||||||
|
|
||||||
function ADSupplyPlanner.copyList(list)
|
|
||||||
local copy = {}
|
|
||||||
for index, value in ipairs(list) do
|
|
||||||
copy[index] = value
|
|
||||||
end
|
|
||||||
return copy
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Hvor meget trænger målet til denne ingrediens? Højere = hentes først.
|
|
||||||
-- Alle tal er liter i aflæsningsmålets lager. Standard: relativ tomhed (0..1),
|
|
||||||
-- så en lille næsten-tom tank vinder over en stor halvfuld.
|
|
||||||
function ADSupplyPlanner.scoreNeed(level, capacity, freeSpace)
|
|
||||||
if capacity <= 0 then
|
|
||||||
return 0
|
|
||||||
end
|
|
||||||
return freeSpace / capacity
|
|
||||||
end
|
|
||||||
|
|
||||||
-- needs: {{fillType, level, capacity}, ...}. Returnerer NY liste, mest trængende først;
|
|
||||||
-- ingredienser uden plads i målet er sorteret fra.
|
|
||||||
function ADSupplyPlanner.rankNeeds(needs)
|
|
||||||
local ranked = {}
|
|
||||||
for _, need in ipairs(needs) do
|
|
||||||
local freeSpace = need.capacity - need.level
|
|
||||||
if freeSpace >= ADSupplyPlanner.MIN_FREE_LITERS then
|
|
||||||
table.insert(ranked, {
|
|
||||||
fillType = need.fillType,
|
|
||||||
level = need.level,
|
|
||||||
capacity = need.capacity,
|
|
||||||
freeSpace = freeSpace,
|
|
||||||
score = ADSupplyPlanner.scoreNeed(need.level, need.capacity, freeSpace),
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
table.sort(ranked, function(a, b)
|
|
||||||
if a.score ~= b.score then
|
|
||||||
return a.score > b.score
|
|
||||||
end
|
|
||||||
return a.fillType < b.fillType
|
|
||||||
end)
|
|
||||||
return ranked
|
|
||||||
end
|
|
||||||
|
|
||||||
-- state: nil eller {ingredients, narrowedTo} fra forrige tur. selected: AD's aktuelle varevalg.
|
|
||||||
-- >1 valgt = (ny) ingrediensliste. Præcis den vare vi selv indsnævrede til = gemt liste.
|
|
||||||
-- Alt andet = brugeren har valgt noget andet -> nil (behovsstyring fra).
|
|
||||||
function ADSupplyPlanner.resolveIngredients(state, selected)
|
|
||||||
if type(selected) ~= "table" then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
if #selected > 1 then
|
|
||||||
return ADSupplyPlanner.copyList(selected)
|
|
||||||
end
|
|
||||||
if state ~= nil and #selected == 1 and selected[1] == state.narrowedTo then
|
|
||||||
return ADSupplyPlanner.copyList(state.ingredients)
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4: Kør tests** — `luajit tests/test_adSupplyPlanner.lua` → `ALLE TESTS OK`.
|
|
||||||
|
|
||||||
- [ ] **Step 5: Commit** — `git add FS25_ADSmartPickup/adSupplyPlanner.lua tests/test_adSupplyPlanner.lua && git commit -m "feat: ADSupplyPlanner — behovsrangering og ingrediensliste-tilstand"`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 2: Integration i `adSmartPickup.lua`
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `FS25_ADSmartPickup/adSmartPickup.lua` (hele filen omstruktureres let; se kode)
|
|
||||||
- Test: `tests/test_adSmartPickup.lua`
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Consumes: `ADSupplyPlanner.rankNeeds`, `.resolveIngredients`, `.copyList` (Task 1).
|
|
||||||
- Produces: `ADSmartPickup.choosePickup(adEnv, driver) -> choice|nil` med `choice = {markerIndex, markerName, group, level, fillType?, need?}`; `ADSmartPickup.findLeastFilledPickup` returnerer nu samme `choice`-tabel (uden `fillType`).
|
|
||||||
|
|
||||||
- [ ] **Step 1: Udvid test-mocks og skriv fejlende tests** i `tests/test_adSmartPickup.lua`:
|
|
||||||
- `dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")` før `adSmartPickup.lua`.
|
|
||||||
- `unloadStations`-liste + `getUnloadingStations` i `g_currentMission.storageSystem`; nulstilles i `resetWorld()`.
|
|
||||||
- `getMapMarkers = function() return markers end` i `ADGraphManager`-mock.
|
|
||||||
- `getSecondMarker = function() return nil end` i `makeDriver` (rigtig AD har den altid).
|
|
||||||
- Nye helpers `makeUnloadStation(node, x, z, levels, capacities, placeable, isBroken)` og `makeSupplyDriver(firstMarker, secondMarker, fillTypes)` (muterbar stateModule-mock med `setFillType`, `raiseDirtyFlag`, `isActive`).
|
|
||||||
- Tests S1–S9 (se Step 1-koden i implementeringen; dækker spec'ens testliste).
|
|
||||||
|
|
||||||
- [ ] **Step 2: Kør og se de nye fejle** — `luajit tests/test_adSmartPickup.lua` → S-tests FAIL, de 11 gamle OK.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Implementér** — i `adSmartPickup.lua`:
|
|
||||||
- `supplyState = setmetatable({}, {__mode = "k"})` (køretøj → `{ingredients, narrowedTo}`).
|
|
||||||
- `getDistanceToTriggers(triggers, nodeFields, x, z)` + `getClosestStation(stations, triggersField, nodeFields, wayPoint, farmId)`; `getLoadStationAtMarker` (felt `loadTriggers`, node `triggerNode`) og `getUnloadStationAtMarker` (felt `unloadTriggers`, noder `exactFillRootNode`, `triggerNode`, `aiNode`).
|
|
||||||
- `collectMarkerStations(adEnv, candidateMarkers, farmId, excludedPlaceable)` → `{{marker, station}, …}`; `findLeastFilled(markerStations, fillTypes, farmId)` → `choice|nil` (eksisterende eget-lager-først-rangering).
|
|
||||||
- `getTargetNeeds(station, ingredients, farmId)`: pr. understøttet ingrediens summeres `storage:getFillLevel(ft)` og `storage:getFreeCapacity(ft)` over `station.targetStorages`; `capacity = level + free`. Ingen storages → `station:getFreeCapacity(ft, farmId)` som `capacity`, `level = 0`.
|
|
||||||
- `ADSmartPickup.findSupplyPickup(adEnv, driver, ingredients)`, `applySupplyChoice(driver, ingredients, choice)` (`setFillType`, `selectedFillTypes = {ft}`, `raiseDirtyFlag`, gem tilstand), `ADSmartPickup.choosePickup` (supply → ellers mappe-logik → ellers nil).
|
|
||||||
- Hook bruger `choice.group` til AD's rotations-bogføring og logger vare + behov ved supply-valg.
|
|
||||||
- `restoreStoppedVehicles()` kaldt fra `update` (pcall): når `not stateModule:isActive()` og valget stadig er `{narrowedTo}` → `selectedFillTypes = kopi af ingredients` + dirty-flag; tilstand slettes.
|
|
||||||
|
|
||||||
- [ ] **Step 4: Kør alle tests** — `luajit tests/test_adSupplyPlanner.lua && luajit tests/test_adSmartPickup.lua` → begge `ALLE TESTS OK`.
|
|
||||||
|
|
||||||
- [ ] **Step 5: Commit** — `git add -A && git commit -m "feat: behovsstyret forsyning — én traktor holder alle input fyldt"`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 3: Pakning, docs, deploy
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `FS25_ADSmartPickup/modDesc.xml` (version `1.1.0.0`, `<sourceFile filename="adSupplyPlanner.lua"/>` FØR `adSmartPickup.lua`, beskrivelse), `README.md`
|
|
||||||
|
|
||||||
- [ ] **Step 1:** Opdatér modDesc + README (betjening: multi-vælg varer; begrænsninger fra spec).
|
|
||||||
- [ ] **Step 2:** Byg: `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip . && cd ..`; verificér med `unzip -l` at begge .lua + modDesc + icon er med.
|
|
||||||
- [ ] **Step 3:** Deploy atomisk (spillet kan køre): `cp FS25_ADSmartPickup.zip ~/FS25-data/mods/.FS25_ADSmartPickup.zip.tmp && mv ~/FS25-data/mods/.FS25_ADSmartPickup.zip.tmp ~/FS25-data/mods/FS25_ADSmartPickup.zip`.
|
|
||||||
- [ ] **Step 4:** Code-review (ecc:code-reviewer) af diff; ret CRITICAL/HIGH.
|
|
||||||
- [ ] **Step 5:** Commit + `git push gitea`. Opdatér memory + Obsidian-noten `projekter/FS25 silo-patches + AD Smart Pickup.md`.
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,78 +0,0 @@
|
|||||||
# Flow-tavle — implementeringsplan
|
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
||||||
|
|
||||||
**Goal:** Én tavle med ét flow pr. række (Vare · Fra · Til · Regel · Traktorer · Status); Fra/Til vælges som bygninger, markører findes automatisk. Faner: Flows · Lagre.
|
|
||||||
|
|
||||||
**Architecture:** Ny spil-adapter `adBuildings.lua` (bygninger + markør-opslag + cache). Modellen får bygnings-id-lister; controlleren udleder waypoint-lister fra dem (`resolveMarkers`) så planlægger/hooks er uændrede. GUI omskrives: `SmartPickupFrame.lua` (faner, tavle, fælles), `SmartPickupFlowTab.lua` (detail-blok), `SmartPickupStorageTab.lua` (uændret).
|
|
||||||
|
|
||||||
**Tech Stack:** FS25 Lua, AutoDrive-globaler via `FS25_AutoDrive`, luajit-tests.
|
|
||||||
|
|
||||||
**Spec:** `docs/superpowers/specs/2026-09-23-flow-tavle-design.md`
|
|
||||||
|
|
||||||
## Global Constraints
|
|
||||||
|
|
||||||
- Motoren (adOutbound, adOutboundPlanner, adSupplyPlanner, adUnloadWait, adSmartPickup-hooks) røres ikke.
|
|
||||||
- Byg kun når alle tests er grønne (`for t in tests/test_*.lua; do luajit $t|tail -1; done`), derefter zip → `~/FS25-data/mods/` → commit → push gitea main.
|
|
||||||
- Version 1.13.0.0. XML-version 3; v1/v2 læses.
|
|
||||||
- Wait-markører er aldrig bygningsmarkører. Siloers læssemarkør = strengt opslag; alt andet = nærmeste ≤ 40 m.
|
|
||||||
|
|
||||||
## Review Focus
|
|
||||||
|
|
||||||
1. Run med bygning der er solgt siden sidst: rækken viser "(mangler)", Start afviser med noMarker, intet crash. (Task 3 test)
|
|
||||||
2. v2-fil med markør der ikke kan oversættes til en bygning: markøren beholdes rå og virker som før. (Task 2 test)
|
|
||||||
3. To bygninger med markører inden for 40 m af hinanden (silo-rækker): hver silo får sin egen markør (strengt opslag). (Task 1 test)
|
|
||||||
4. Forsynings-run: Fra vises som "alle siloer" og sources-listen røres ikke. (Task 3 test)
|
|
||||||
5. Fanen rebuild'es mens et run kører: Start/Stop-knappen og status følger `isRunning`. (in-game)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 1: adBuildings
|
|
||||||
|
|
||||||
**Files:** Create `FS25_ADSmartPickup/adBuildings.lua`; Test `tests/test_adBuildings.lua`.
|
|
||||||
|
|
||||||
**Produces:**
|
|
||||||
```lua
|
|
||||||
ADBuildings.MAX_MARKER_DISTANCE = 40; ADBuildings.CACHE_MS = 1000
|
|
||||||
ADBuildings.list(adEnv, farmId) -- {building...} sorteret kind, navn (cachet)
|
|
||||||
ADBuildings.find(adEnv, farmId, id) -- building | nil
|
|
||||||
ADBuildings.byMarker(adEnv, farmId, wayPointId, role) -- role "load"|"unload": building hvis markøren er dens markør, ellers nil
|
|
||||||
ADBuildings.getId(placeable) -- uniqueId eller "pos:x:z"
|
|
||||||
ADBuildings.invalidate()
|
|
||||||
building = {id, name, kind, placeable, loadMarker={id,name,distance}|nil, unloadMarker=…|nil, canBeSource, canBeTarget, canSell}
|
|
||||||
```
|
|
||||||
Kilder til bygninger: `g_currentMission.placeableSystem.placeables` (ejet af farm eller sell) — kind: factory (spec_productionPoint), silo (spec_silo), husbandry (getHusbandryFillLevel), objectStorage (spec_objectStorage), sell (ADPrices.isSellingStation på placeable.spec_sellingPoint / unloadingStation via storageSystem). Læssetriggere: `productionPoint.loadingStation.loadTriggers`, `spec_silo.loadingStation.loadTriggers`, husbandry: alle loadingStations i storageSystem med owningPlaceable == placeable. Aflæsningstriggere: unloadingStations med owningPlaceable == placeable. Objektlager/pallespawner: rootNode/spawnPlaces[1].startNode.
|
|
||||||
Markørvalg: nærmeste markør (ikke Wait) til en trigger-node ≤ 40 m; for silo-load: kun markører hvor `ADSmartPickup.getLoadStationAtWayPointId(marker.id)` == siloens station.
|
|
||||||
|
|
||||||
- [ ] Tests B1–B7 (kind, markører, silo strengt, Wait udelukket, byMarker, id-fallback, cache-invalidering) → RED → implementér → GREEN → commit `feat: adBuildings`.
|
|
||||||
|
|
||||||
### Task 2: Model v3
|
|
||||||
|
|
||||||
**Files:** Modify `adRuns.lua`, `adRunsStorage.lua`; Test `tests/test_adRuns.lua`.
|
|
||||||
|
|
||||||
**Produces:** run-felter `sourceBuildingIds`, `targetBuildingIds` (lister af strenge); `ADRuns.toggleSourceBuilding/toggleTargetBuilding(store, runId, id)`; XML v3 `#sourceBuildings`/`#targetBuildings` (mellemrumsadskilte). `ADRuns.XML_VERSION = 3`. Migration sker i controlleren (Task 3), ikke i modellen.
|
|
||||||
|
|
||||||
- [ ] Tests R-v3 → RED → implementér → GREEN → commit.
|
|
||||||
|
|
||||||
### Task 3: Controller — resolve + migration + validering
|
|
||||||
|
|
||||||
**Files:** Modify `adRunsController.lua`; Test `tests/test_adRunsController.lua`.
|
|
||||||
|
|
||||||
**Produces:**
|
|
||||||
```lua
|
|
||||||
ADRunsController.resolveMarkers(adEnv, run) -- udfylder run.sourceWayPointIds (outbound) / targetWayPointIds (supply) fra bygninger; rå markører (uden bygning) bevares; returnerer {missing = {buildingName...}}
|
|
||||||
ADRunsController.migrateBuildings(adEnv) -- ved load: markører → bygnings-id via byMarker; logger
|
|
||||||
ADRunsController.getFlowRow(adEnv, run) -- {product, from, to, rule, vehicles, status, warning} tekster til tavlen
|
|
||||||
validateVehicle: ny grund "noMarker" når en valgt bygning mangler markør
|
|
||||||
```
|
|
||||||
Start/plan: `startRun` og `ADOutbound.buildPlanInput` kalder resolveMarkers først (buildPlanInput via ADRunsController.getRunForVehicle → resolve i getRunForVehicle? nej: i `ADSmartPickup.choosePickup`/`ADOutbound.choosePickup` kaldes `ADRunsController.resolveMarkers(adEnv, run)` én gang pr. plan).
|
|
||||||
|
|
||||||
- [ ] Tests C-flow → RED → implementér → GREEN → commit.
|
|
||||||
|
|
||||||
### Task 4: GUI — Flows-fane
|
|
||||||
|
|
||||||
**Files:** Rewrite `gui/SmartPickupFrame.lua` (faner Flows/Lagre, tavle, fælles rækker, knapper), Create `gui/SmartPickupFlowTab.lua` (detail-blok, callbacks), delete `SmartPickupSupplyTab.lua`/`SmartPickupOutboundTab.lua` (indhold flyttes), Modify `gui/SmartPickupFrame.xml` (statiske rækker: runOption "Flow", waitPool, loops, returnBelow, sellAt, storeAbove; prefabs som i dag), l10n da/en (`spu_tab_flows`, `spu_flow_*`, `spu_direction_in/out`, `spu_from_allSilos`, `spu_noMarker`, `spu_reason_noMarker`, `spu_section_*`), `adSmartPickup.lua` (source()).
|
|
||||||
|
|
||||||
- [ ] Implementér; syntaks- og definitions-tjek; in-game.
|
|
||||||
|
|
||||||
### Task 5: README, version, byg, deploy, push, memory.
|
|
||||||
@@ -1,569 +0,0 @@
|
|||||||
# Udkørsel af produkter — implementeringsplan
|
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
||||||
|
|
||||||
**Goal:** Runs med retning "Udkørsel": traktorer henter fabrikkers output og lagervarer (bulk og paller, også fra objektlagre) og kører dem til silo eller salgssted efter politik pr. vare, pristærskel og lagertærskel.
|
|
||||||
|
|
||||||
**Architecture:** Ren planlægger (`adOutboundPlanner`) + rene prisregler (`adPrices`) uden spil-API; spiladaptere i `adSources`/`adPrices`; AutoDrive-integration i `adOutbound` via to hooks (næste pålæsning + læsseopgaven), samme mønster som `adUnloadWait`. Wait-puljen udskilles i `adWaitPool` og deles. GUI får tre faner (Oversigt · Forsyning · Udkørsel) med fælles byggeklodser.
|
|
||||||
|
|
||||||
**Tech Stack:** FS25 Lua (GIANTS-sandbox), AutoDrive 3.x via `FS25_AutoDrive`-globalen, Universal Autoload via AD's AL-lag, luajit-tests uden spil.
|
|
||||||
|
|
||||||
**Spec:** `docs/superpowers/specs/2026-09-23-udkoersel-design.md`
|
|
||||||
|
|
||||||
## Global Constraints
|
|
||||||
|
|
||||||
- Alle spil-API-kald i hooks i `pcall`; fejl → hold ved Wait + WARN én gang, aldrig AutoDrive-stop.
|
|
||||||
- Modellen (`adRuns`) muterer aldrig: ny store ved hver ændring.
|
|
||||||
- Log-præfiks `ADSmartPickup: ` i alle moduler; alle run-ændringer logges.
|
|
||||||
- XML-version 2; version 1-filer læses som `kind="supply"`.
|
|
||||||
- Standarder: `sellAtPercent` 90 (50–100), `storeAbovePercent` 80 (50–95), `collectRound` false.
|
|
||||||
- Politikker: `store` · `sell` · `sellElseStore` (+ "off" = varen udelades af `products`).
|
|
||||||
- Kildetyper: fabrik (produktionspunkt), silo (`spec_silo`), objektlager (`spec_objectStorage`), genkendt inden for 40 m af markøren.
|
|
||||||
- Tests køres fra repo-roden: `luajit tests/<fil>.lua`; alle eksisterende tests skal blive grønne.
|
|
||||||
- Version bump til 1.12.0.0 i `modDesc.xml`; byg `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .`; deploy til `~/FS25-data/mods/`.
|
|
||||||
|
|
||||||
## Review Focus
|
|
||||||
|
|
||||||
1. Traktor med UAL-vogn står ved et objektlager hvis spawn sidder fast (`objectSpawn.isActive` for evigt): forventet: 30 s uden fremdrift → læsningen afsluttes med det der er, ingen palle efterlades ud over den ene der evt. er spawnet. (Task 6 test O5)
|
|
||||||
2. Bedst betalende salgssted er uopnåeligt, næstbedste kan nås: forventet: næstbedste vælges uden advarsel; kun når intet kan nås, vises rød advarsel én gang. (Task 4 test P6 + Task 6 test O7)
|
|
||||||
3. Vare hvis salgsstation ikke findes på kortet, politik `sell`: forventet: varen står som "intet salgssted" i UI og planlæggeren springer den over uden fejl. (Task 4 test P7)
|
|
||||||
4. Version 1-runs-fil (uden `kind`) indlæses: forventet: alle runs bliver Forsyning, intet går tabt, næste gem skriver version 2. (Task 1 test R-v2)
|
|
||||||
5. Bulk-vogn med opsamlingsrunde og to kilder hvor den første giver fuldt læs: forventet: der køres direkte til målet, kilde 2 besøges ikke. (Task 6 test O6)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 1: Model + lagring v2
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `FS25_ADSmartPickup/adRuns.lua`
|
|
||||||
- Modify: `FS25_ADSmartPickup/adRunsStorage.lua`
|
|
||||||
- Test: `tests/test_adRuns.lua` (tilføj sektion R-v2)
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Produces: `ADRuns.KINDS = {supply=true, outbound=true}`, `ADRuns.POLICIES = {store=true, sell=true, sellElseStore=true}`, `ADRuns.DEFAULT_SELL_AT_PERCENT=90`, `ADRuns.DEFAULT_STORE_ABOVE_PERCENT=80`, `ADRuns.createRun(store, name, kind)`, run-felter `kind, sourceWayPointIds, products={{fillType="SUGAR", policy="sell"}}, sellAtPercent, storeAbovePercent, collectRound`, `ADRuns.toggleSource(store, runId, wayPointId)`, `ADRuns.setProductPolicy(store, runId, fillTypeName, policy)` (policy "off" fjerner), `ADRuns.getProductPolicy(run, fillTypeName)` → policy eller "off", `ADRuns.serializeProducts/parseProducts`, `ADRuns.XML_VERSION = 2`.
|
|
||||||
|
|
||||||
- [ ] **Step 1: Skriv fejlende tests** — tilføj nederst i `tests/test_adRuns.lua` før den afsluttende opsummering:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- R-v2: udkørsels-run — kind, kilder, varer med politik, tærskler, runde; XML v2 + v1-migration
|
|
||||||
local o1, out = ADRuns.createRun(ADRuns.newStore(), "Sugar out", "outbound")
|
|
||||||
check("Rv2 kind", out.kind, "outbound")
|
|
||||||
check("Rv2 supply som standard", ADRuns.createRun(ADRuns.newStore(), "x").kind == nil and "nil" or select(2, ADRuns.createRun(ADRuns.newStore(), "x")).kind, "supply")
|
|
||||||
check("Rv2 sellAt 90", out.sellAtPercent, 90)
|
|
||||||
check("Rv2 storeAbove 80", out.storeAbovePercent, 80)
|
|
||||||
check("Rv2 runde fra", out.collectRound, false)
|
|
||||||
local o2 = ADRuns.toggleSource(o1, out.id, 12694)
|
|
||||||
o2 = ADRuns.toggleSource(o2, out.id, 12386)
|
|
||||||
check("Rv2 to kilder", table.concat(ADRuns.getRun(o2, out.id).sourceWayPointIds, ","), "12694,12386")
|
|
||||||
check("Rv2 toggle fjerner", #ADRuns.getRun(ADRuns.toggleSource(o2, out.id, 12694), out.id).sourceWayPointIds, 1)
|
|
||||||
local o3 = ADRuns.setProductPolicy(o2, out.id, "SUGAR", "sell")
|
|
||||||
o3 = ADRuns.setProductPolicy(o3, out.id, "FORAGE", "store")
|
|
||||||
check("Rv2 politik SUGAR", ADRuns.getProductPolicy(ADRuns.getRun(o3, out.id), "SUGAR"), "sell")
|
|
||||||
check("Rv2 politik ukendt = off", ADRuns.getProductPolicy(ADRuns.getRun(o3, out.id), "STRAW"), "off")
|
|
||||||
check("Rv2 ugyldig politik ignoreres", ADRuns.getProductPolicy(ADRuns.getRun(ADRuns.setProductPolicy(o3, out.id, "SUGAR", "bogus"), out.id), "SUGAR"), "sell")
|
|
||||||
check("Rv2 off fjerner", ADRuns.getProductPolicy(ADRuns.getRun(ADRuns.setProductPolicy(o3, out.id, "SUGAR", "off"), out.id), "SUGAR"), "off")
|
|
||||||
check("Rv2 gammel store urørt", #ADRuns.getRun(o2, out.id).products, 0)
|
|
||||||
local o4 = ADRuns.updateRun(o3, out.id, {sellAtPercent = 120, storeAbovePercent = 10, collectRound = true})
|
|
||||||
check("Rv2 sellAt klippet 100", ADRuns.getRun(o4, out.id).sellAtPercent, 100)
|
|
||||||
check("Rv2 storeAbove klippet 50", ADRuns.getRun(o4, out.id).storeAbovePercent, 50)
|
|
||||||
check("Rv2 runde til", ADRuns.getRun(o4, out.id).collectRound, true)
|
|
||||||
check("Rv2 serialiser", ADRuns.serializeProducts(ADRuns.getRun(o3, out.id).products), "SUGAR:sell FORAGE:store")
|
|
||||||
local parsed = ADRuns.parseProducts("SUGAR:sell FORAGE:store BAD BAD:bogus")
|
|
||||||
check("Rv2 parse antal", #parsed, 2)
|
|
||||||
check("Rv2 parse politik", parsed[2].policy, "store")
|
|
||||||
local rt = ADRuns.fromRecords(ADRuns.toRecords(o4))
|
|
||||||
local rtRun = ADRuns.getRun(rt, out.id)
|
|
||||||
check("Rv2 roundtrip kind", rtRun.kind, "outbound")
|
|
||||||
check("Rv2 roundtrip kilder", table.concat(rtRun.sourceWayPointIds, ","), "12694,12386")
|
|
||||||
check("Rv2 roundtrip products", ADRuns.serializeProducts(rtRun.products), "SUGAR:sell FORAGE:store")
|
|
||||||
check("Rv2 roundtrip sellAt", rtRun.sellAtPercent, 100)
|
|
||||||
check("Rv2 roundtrip runde", rtRun.collectRound, true)
|
|
||||||
-- v1-post uden kind/sources/products -> supply med standarder
|
|
||||||
local v1 = ADRuns.fromRecords({{id = 7, name = "Old", target = 94, targets = "94", ingredientMode = "auto", ingredients = "", waitPool = "", loops = 0, returnBelow = 50, vehicleIds = {}}})
|
|
||||||
check("Rv2 v1 -> supply", ADRuns.getRun(v1, 7).kind, "supply")
|
|
||||||
check("Rv2 v1 tomme kilder", #ADRuns.getRun(v1, 7).sourceWayPointIds, 0)
|
|
||||||
check("Rv2 v1 sellAt standard", ADRuns.getRun(v1, 7).sellAtPercent, 90)
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Kør testen** — `luajit tests/test_adRuns.lua` → FAIL (`toggleSource` nil).
|
|
||||||
|
|
||||||
- [ ] **Step 3: Implementér i `adRuns.lua`**:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
ADRuns.XML_VERSION = 2
|
|
||||||
ADRuns.KINDS = {supply = true, outbound = true}
|
|
||||||
ADRuns.POLICIES = {store = true, sell = true, sellElseStore = true}
|
|
||||||
ADRuns.DEFAULT_SELL_AT_PERCENT, ADRuns.MIN_SELL_AT_PERCENT, ADRuns.MAX_SELL_AT_PERCENT = 90, 50, 100
|
|
||||||
ADRuns.DEFAULT_STORE_ABOVE_PERCENT, ADRuns.MIN_STORE_ABOVE_PERCENT, ADRuns.MAX_STORE_ABOVE_PERCENT = 80, 50, 95
|
|
||||||
|
|
||||||
local function copyProducts(products)
|
|
||||||
local copy = {}
|
|
||||||
for index, product in ipairs(products or {}) do
|
|
||||||
copy[index] = {fillType = product.fillType, policy = product.policy}
|
|
||||||
end
|
|
||||||
return copy
|
|
||||||
end
|
|
||||||
|
|
||||||
local function clampRange(value, default, min, max)
|
|
||||||
local number = tonumber(value) or default
|
|
||||||
return math.max(min, math.min(max, math.floor(number)))
|
|
||||||
end
|
|
||||||
```
|
|
||||||
`copyRun` får `kind, sourceWayPointIds = copyList(...), products = copyProducts(...), sellAtPercent, storeAbovePercent, collectRound`. `createRun(store, name, kind)` sætter `kind = ADRuns.KINDS[kind] and kind or "supply"`, `sourceWayPointIds = {}`, `products = {}`, `sellAtPercent = 90`, `storeAbovePercent = 80`, `collectRound = false`. `updateRun` håndterer `changes.kind` (kun gyldige), `sourceWayPointIds` (copyList), `products` (copyProducts), `sellAtPercent`/`storeAbovePercent` (clampRange), `collectRound` (`== true`).
|
|
||||||
|
|
||||||
```lua
|
|
||||||
function ADRuns.toggleSource(store, runId, wayPointId)
|
|
||||||
local run = ADRuns.getRun(store, runId)
|
|
||||||
if run == nil then return copyStore(store) end
|
|
||||||
local sources, found = {}, false
|
|
||||||
for _, id in ipairs(run.sourceWayPointIds) do
|
|
||||||
if id == wayPointId then found = true else table.insert(sources, id) end
|
|
||||||
end
|
|
||||||
if not found then table.insert(sources, wayPointId) end
|
|
||||||
return ADRuns.updateRun(store, runId, {sourceWayPointIds = sources})
|
|
||||||
end
|
|
||||||
|
|
||||||
function ADRuns.getProductPolicy(run, fillTypeName)
|
|
||||||
for _, product in ipairs(run.products or {}) do
|
|
||||||
if product.fillType == fillTypeName then return product.policy end
|
|
||||||
end
|
|
||||||
return "off"
|
|
||||||
end
|
|
||||||
|
|
||||||
-- policy "off" fjerner varen; ukendt politik ignoreres (store uændret).
|
|
||||||
function ADRuns.setProductPolicy(store, runId, fillTypeName, policy)
|
|
||||||
local run = ADRuns.getRun(store, runId)
|
|
||||||
if run == nil or (policy ~= "off" and not ADRuns.POLICIES[policy]) then return copyStore(store) end
|
|
||||||
local products = {}
|
|
||||||
for _, product in ipairs(run.products) do
|
|
||||||
if product.fillType ~= fillTypeName then table.insert(products, product) end
|
|
||||||
end
|
|
||||||
if policy ~= "off" then table.insert(products, {fillType = fillTypeName, policy = policy}) end
|
|
||||||
return ADRuns.updateRun(store, runId, {products = products})
|
|
||||||
end
|
|
||||||
|
|
||||||
function ADRuns.serializeProducts(products)
|
|
||||||
local parts = {}
|
|
||||||
for _, product in ipairs(products or {}) do table.insert(parts, product.fillType .. ":" .. product.policy) end
|
|
||||||
return table.concat(parts, " ")
|
|
||||||
end
|
|
||||||
|
|
||||||
function ADRuns.parseProducts(text)
|
|
||||||
local products = {}
|
|
||||||
for token in string.gmatch(tostring(text or ""), "%S+") do
|
|
||||||
local name, policy = string.match(token, "^([^:]+):(%a+)$")
|
|
||||||
if name ~= nil and ADRuns.POLICIES[policy] then table.insert(products, {fillType = name, policy = policy}) end
|
|
||||||
end
|
|
||||||
return products
|
|
||||||
end
|
|
||||||
```
|
|
||||||
`toRecords` tilføjer `kind, sources = table.concat(run.sourceWayPointIds, " "), products = serializeProducts, sellAt, storeAbove, collectRound = run.collectRound and 1 or 0`. `fromRecords` læser dem med standarder (`kind` ugyldig/nil → "supply"; `collectRound = tonumber(record.collectRound) == 1`).
|
|
||||||
|
|
||||||
`adRunsStorage.lua`: `writeRecords` skriver `#kind` (string), `#sources`, `#products`, `#sellAt` (int), `#storeAbove` (int), `#collectRound` (int); `readRecords` læser dem (`getString`/`getInt`; manglende attribut → nil → standard i fromRecords).
|
|
||||||
|
|
||||||
- [ ] **Step 4: Kør** `luajit tests/test_adRuns.lua && luajit tests/test_adRunsController.lua` → alle OK.
|
|
||||||
- [ ] **Step 5: Commit** `feat: run-model v2 — retning, kilder, varepolitik, tærskler, runde (XML v2 + v1-migration)`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 2: adWaitPool udskilt fra adUnloadWait
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `FS25_ADSmartPickup/adWaitPool.lua`
|
|
||||||
- Modify: `FS25_ADSmartPickup/adUnloadWait.lua`, `adLoadSwap.lua` (excludedPlaceables), `modDesc.xml` (sourceFile før adUnloadWait)
|
|
||||||
- Test: `tests/test_adWaitPool.lua` (ny), eksisterende `tests/test_adSmartPickup.lua` skal stadig køre
|
|
||||||
|
|
||||||
**Interfaces (Produces):**
|
|
||||||
```lua
|
|
||||||
ADWaitPool.getCandidates(adEnv, poolGroup) -- {{marker, x, z}}
|
|
||||||
ADWaitPool.claim(markerId, vehicle) / ADWaitPool.releaseVehicle(vehicle) / ADWaitPool.getOwner(markerId)
|
|
||||||
ADWaitPool.pickFree(adEnv, vehicle, poolGroup, isReachable) -- candidate eller nil (nærmeste ledige fra vognens position)
|
|
||||||
ADWaitPool.hasRouteTo(adEnv, vehicle, wayPointId) / ADWaitPool.hasRouteBothWays(adEnv, vehicle, toId, homeId)
|
|
||||||
ADWaitPool.redirect(task, destinationID) -- stopper AD's trigger-aktivitet, sætter task.destinationID/state/setPathTo
|
|
||||||
ADWaitPool.getClaims() -- {{marker, vehicle}} sorteret (til GUI)
|
|
||||||
ADWaitPool.STATE_DRIVING -- sættes af adUnloadWait.install (task.STATE_DRIVING or 2)
|
|
||||||
ADLoadSwap.findDumpMarker(adEnv, vehicle, fillType, rest, homeWayPointId, isReachable, excludedPlaceables) -- 7. param: {[placeable]=true} udelukkes
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 1: Test** `tests/test_adWaitPool.lua`: mock `FS25_AutoDrive.ADGraphManager` (markers i mapperne "Cow Wait" og "silo", waypoints, `getPathTo` der returnerer `{ {id=target} }` for nåelige), `ADSupplyPlanner` via `dofile`. Tjek: (W1) `getCandidates(adEnv, "Cow Wait")` giver kun Wait-markører i den mappe, tom pulje = alle Wait-mapper; (W2) `pickFree` returnerer nærmeste ledige, springer reserverede over, returnerer eget reserverede; (W3) `claim/releaseVehicle/getOwner`; (W4) `pickFree` med `isReachable` der afviser nærmeste → næste; (W5) `getClaims` sorteret på mappe.
|
|
||||||
- [ ] **Step 2: Kør** → FAIL (fil mangler).
|
|
||||||
- [ ] **Step 3: Implementér** `adWaitPool.lua` ved at flytte `getWaitCandidates`, `claims`, `pathReaches`, `hasRouteTo`, `hasRouteBothWays`, `redirect`, `getClaims`-sorteringen fra `adUnloadWait.lua` (samme kode, offentlige navne som ovenfor; `claims` er `setmetatable({}, {__mode="v"})`; `getClaims` itererer `claims` direkte: `{marker = ADGraphManager:getMapMarkerByWayPointId(id), vehicle}`). `adUnloadWait.lua` kalder dem (`claims[...]` → `ADWaitPool.claim/getOwner/releaseVehicle`; `pickNearestFreeWaitPoint(getWaitCandidates(...), claims, ...)` → `ADSupplyPlanner.pickNearestFreeWaitPoint(ADWaitPool.getCandidates(adEnv, pool), ADWaitPool.claims, x, z, vehicle, isReachable)`); `ADUnloadWait.getClaims/hasRouteTo/hasRouteBothWays` bliver delegater. `ADLoadSwap.findDumpMarker`: `if excludedPlaceables ~= nil and excludedPlaceables[station.owningPlaceable] then skip`.
|
|
||||||
- [ ] **Step 4: Kør** alle tests: `for t in tests/test_*.lua; do luajit $t | tail -1; done` → grønne.
|
|
||||||
- [ ] **Step 5: Commit** `refactor: Wait-puljen udskilt i adWaitPool (reservationer, rutetjek, redirect) til deling med udkørsel`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 3: adPrices — toppris og pris-%
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `FS25_ADSmartPickup/adPrices.lua`
|
|
||||||
- Test: `tests/test_adPrices.lua`
|
|
||||||
|
|
||||||
**Interfaces (Produces):**
|
|
||||||
```lua
|
|
||||||
ADPrices.maxFactor(factors) -- højeste værdi i {[period]=factor}; 1 uden faktorer
|
|
||||||
ADPrices.topPrice(baseNow, factorNow, maxFactor) -- baseNow * maxFactor / factorNow; baseNow når factorNow <= 0
|
|
||||||
ADPrices.percentOfTop(price, top) -- afrundet heltal; 0 når top <= 0
|
|
||||||
ADPrices.isSellingStation(station) -- SellingStation, isSellingPoint, ikke hideFromPricesMenu
|
|
||||||
ADPrices.getQuote(station, fillType) -- {price, top, percent} eller nil (spil-API i pcall)
|
|
||||||
ADPrices.bestQuote(quotes) -- højeste percent, dernæst price; nil ved tom
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 1: Test** (P1) `maxFactor({EARLY_SPRING=0.9, LATE_AUTUMN=1.2})` = 1.2; (P2) `topPrice(500, 1.0, 1.25)` = 625; `topPrice(500, 0, 1.25)` = 500; (P3) `percentOfTop(470, 625)` = 75; `percentOfTop(700, 625)` = 112 (stor efterspørgsel > 100); `percentOfTop(1, 0)` = 0; (P4) `getQuote` med fake station `{fillTypePrices={[5]=500}, getEffectiveFillTypePrice=function(_,ft) return 470 end, isa=function() return true end, isSellingPoint=true}` og `g_fillTypeManager.getFillTypeByIndex` → `{economy={factors={A=1.0,B=1.25}}, pricePerLiter=400}`, `g_currentMission.environment.currentPeriod="A"` → percent 75; (P5) uden `fillTypePrices` bruges `pricePerLiter × factorNow × economyManager:getPriceMultiplier()` (1.0) → top 500, percent 94 (470/500); (P6) `bestQuote` vælger højeste percent.
|
|
||||||
- [ ] **Step 2: Kør** → FAIL.
|
|
||||||
- [ ] **Step 3: Implementér**:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
ADPrices = {}
|
|
||||||
ADPrices.LOG_PREFIX = "ADSmartPickup: "
|
|
||||||
|
|
||||||
function ADPrices.maxFactor(factors)
|
|
||||||
local best = nil
|
|
||||||
for _, factor in pairs(factors or {}) do
|
|
||||||
if type(factor) == "number" and (best == nil or factor > best) then best = factor end
|
|
||||||
end
|
|
||||||
return best or 1
|
|
||||||
end
|
|
||||||
|
|
||||||
function ADPrices.topPrice(baseNow, factorNow, maxFactor)
|
|
||||||
if baseNow == nil or baseNow <= 0 then return 0 end
|
|
||||||
if factorNow == nil or factorNow <= 0 or maxFactor == nil or maxFactor <= 0 then return baseNow end
|
|
||||||
return baseNow * maxFactor / factorNow
|
|
||||||
end
|
|
||||||
|
|
||||||
function ADPrices.percentOfTop(price, top)
|
|
||||||
if top == nil or top <= 0 or price == nil then return 0 end
|
|
||||||
return math.floor(100 * price / top + 0.5)
|
|
||||||
end
|
|
||||||
|
|
||||||
function ADPrices.isSellingStation(station)
|
|
||||||
return station ~= nil and station.isa ~= nil and SellingStation ~= nil and station:isa(SellingStation)
|
|
||||||
and station.isSellingPoint == true and not station.hideFromPricesMenu
|
|
||||||
end
|
|
||||||
|
|
||||||
local function getCurrentPeriod()
|
|
||||||
local environment = g_currentMission ~= nil and g_currentMission.environment or nil
|
|
||||||
return environment ~= nil and environment.currentPeriod or nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function ADPrices.getQuote(station, fillType)
|
|
||||||
local ok, effective = pcall(station.getEffectiveFillTypePrice, station, fillType)
|
|
||||||
if not ok or type(effective) ~= "number" then return nil end
|
|
||||||
local fillTypeDesc = g_fillTypeManager ~= nil and g_fillTypeManager.getFillTypeByIndex ~= nil and g_fillTypeManager:getFillTypeByIndex(fillType) or nil
|
|
||||||
local factors = fillTypeDesc ~= nil and fillTypeDesc.economy ~= nil and fillTypeDesc.economy.factors or nil
|
|
||||||
local period = getCurrentPeriod()
|
|
||||||
local factorNow = factors ~= nil and period ~= nil and tonumber(factors[period]) or 1
|
|
||||||
local baseNow = station.fillTypePrices ~= nil and tonumber(station.fillTypePrices[fillType]) or nil
|
|
||||||
if baseNow == nil or baseNow <= 0 then
|
|
||||||
local multiplier = 1
|
|
||||||
local economy = g_currentMission ~= nil and g_currentMission.economyManager or nil
|
|
||||||
if economy ~= nil and economy.getPriceMultiplier ~= nil then
|
|
||||||
local okMult, value = pcall(economy.getPriceMultiplier, economy)
|
|
||||||
if okMult and type(value) == "number" then multiplier = value end
|
|
||||||
end
|
|
||||||
baseNow = (fillTypeDesc ~= nil and tonumber(fillTypeDesc.pricePerLiter) or 0) * factorNow * multiplier
|
|
||||||
end
|
|
||||||
local top = ADPrices.topPrice(baseNow, factorNow, ADPrices.maxFactor(factors))
|
|
||||||
return {price = effective, top = top, percent = ADPrices.percentOfTop(effective, top)}
|
|
||||||
end
|
|
||||||
|
|
||||||
function ADPrices.bestQuote(quotes)
|
|
||||||
local best = nil
|
|
||||||
for _, quote in ipairs(quotes or {}) do
|
|
||||||
if best == nil or quote.percent > best.percent or (quote.percent == best.percent and quote.price > best.price) then best = quote end
|
|
||||||
end
|
|
||||||
return best
|
|
||||||
end
|
|
||||||
```
|
|
||||||
- [ ] **Step 4: Kør** → OK. **Step 5: Commit** `feat: adPrices — toppris fra årets prisforløb og pris-% pr. salgssted`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 4: adOutboundPlanner (ren)
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `FS25_ADSmartPickup/adOutboundPlanner.lua`
|
|
||||||
- Test: `tests/test_adOutboundPlanner.lua`
|
|
||||||
|
|
||||||
**Interfaces (Produces):**
|
|
||||||
```lua
|
|
||||||
-- input:
|
|
||||||
-- { capacity = liter, hasAutoload = bool, sellAtPercent, storeAbovePercent, collectRound,
|
|
||||||
-- products = { { fillType = idx, name = "SUGAR", policy = "sell"|"store"|"sellElseStore", isPallet = bool,
|
|
||||||
-- sources = { {wayPointId, available, level, capacity} ... }, -- kun kilder med available > 0
|
|
||||||
-- quote = {percent, price, wayPointId, stationName} | nil, -- bedste NÅELIGE salgssted
|
|
||||||
-- store = {wayPointId, name, free} | nil } ... } }
|
|
||||||
-- output: trip | nil, reasons
|
|
||||||
-- trip = { kind = "sell"|"store", fillTypes = {idx...}, names = {...}, sourceIds = {wayPointId...}, targetWayPointId, targetName, percent|nil }
|
|
||||||
-- reasons = { "SUGAR 78 % af top (kræver 90), lager 41 %", ... }
|
|
||||||
ADOutboundPlanner.plan(input)
|
|
||||||
ADOutboundPlanner.evaluate(product, input) -- {available, fullLoad, storageHigh, storagePercent, priceOk, percent}
|
|
||||||
ADOutboundPlanner.MIN_AVAILABLE_LITERS = 1
|
|
||||||
ADOutboundPlanner.ROUND_MIN_FREE_FRACTION = 0.1
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 1: Tests** (`local P = ADOutboundPlanner` efter `dofile`):
|
|
||||||
- P1 salg ved pris over tærskel, uanset mængde: SUGAR sell, sources {{1, 5000, ...}}, quote 92, capacity 20000 → trip.kind "sell", targetWayPointId = quote.wayPointId, sourceIds {1}.
|
|
||||||
- P2 pris under tærskel, lager lavt → nil, reasons[1] = "SUGAR 78 % af top (kræver 90), lager 41 %".
|
|
||||||
- P3 sellElseStore, pris lav, lager 85 % → "store" til store.wayPointId.
|
|
||||||
- P4 store med fuldt læs (available ≥ capacity) og lager 20 % → store.
|
|
||||||
- P5 rangering: to sell-kandidater 95 % og 91 % → første vælges (højeste %).
|
|
||||||
- P6 quote nil (intet nåeligt salgssted), policy sell, lager højt → nil, reason "SUGAR: intet nåeligt salgssted".
|
|
||||||
- P7 store nil ved store-behov → nil, reason "FORAGE: ingen silo med plads".
|
|
||||||
- P8 runde bulk (collectRound, hasAutoload=false): SUGAR i kilde A (3000) og B (9000), capacity 10000 → sourceIds {B, A} (mest først), ét fillType.
|
|
||||||
- P9 runde UAL: STRAW_BASKET (sell 95, station S) + STRAW_CRATE (sell 93, station S) + STRAW_BROOM (sell 96, station T) → fillTypes {BROOM} kun (højest, T) — og med BROOM fjernet: {BASKET, CRATE} samme station S.
|
|
||||||
- P10 uden runde: kun første fillType og kun én kilde (den med mest).
|
|
||||||
- P11 policy off/ingen sources → nil, reasons tom for varer uden kilder ("SUGAR: intet på lager").
|
|
||||||
- [ ] **Step 2: Kør** → FAIL. **Step 3: Implementér**:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
ADOutboundPlanner = {}
|
|
||||||
ADOutboundPlanner.MIN_AVAILABLE_LITERS = 1
|
|
||||||
ADOutboundPlanner.ROUND_MIN_FREE_FRACTION = 0.1
|
|
||||||
|
|
||||||
local function sumAvailable(product)
|
|
||||||
local total, high, percent = 0, false, 0
|
|
||||||
for _, source in ipairs(product.sources or {}) do
|
|
||||||
total = total + (source.available or 0)
|
|
||||||
if source.capacity ~= nil and source.capacity > 0 then
|
|
||||||
local p = 100 * (source.level or 0) / source.capacity
|
|
||||||
percent = math.max(percent, p)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return total, percent
|
|
||||||
end
|
|
||||||
|
|
||||||
function ADOutboundPlanner.evaluate(product, input)
|
|
||||||
local available, storagePercent = sumAvailable(product)
|
|
||||||
local percent = product.quote ~= nil and product.quote.percent or nil
|
|
||||||
return {
|
|
||||||
available = available,
|
|
||||||
fullLoad = input.capacity ~= nil and input.capacity > 0 and available >= input.capacity,
|
|
||||||
storagePercent = storagePercent,
|
|
||||||
storageHigh = storagePercent >= (input.storeAbovePercent or 80),
|
|
||||||
percent = percent,
|
|
||||||
priceOk = percent ~= nil and percent >= (input.sellAtPercent or 90),
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
local function wantsSell(policy) return policy == "sell" or policy == "sellElseStore" end
|
|
||||||
local function wantsStore(policy) return policy == "store" or policy == "sellElseStore" end
|
|
||||||
|
|
||||||
local function reasonFor(product, e, input)
|
|
||||||
if e.available < ADOutboundPlanner.MIN_AVAILABLE_LITERS then return product.name .. ": intet på lager" end
|
|
||||||
if wantsSell(product.policy) and product.quote == nil and not (wantsStore(product.policy) and product.store ~= nil) then
|
|
||||||
return product.name .. ": intet nåeligt salgssted"
|
|
||||||
end
|
|
||||||
if wantsStore(product.policy) and product.store == nil and (e.fullLoad or e.storageHigh) and not e.priceOk then
|
|
||||||
return product.name .. ": ingen silo med plads"
|
|
||||||
end
|
|
||||||
local parts = {}
|
|
||||||
if wantsSell(product.policy) and e.percent ~= nil then
|
|
||||||
table.insert(parts, string.format("%d %% af top (kræver %d)", e.percent, input.sellAtPercent or 90))
|
|
||||||
end
|
|
||||||
table.insert(parts, string.format("lager %d %%", math.floor(e.storagePercent + 0.5)))
|
|
||||||
return product.name .. " " .. table.concat(parts, ", ")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- kandidater: {product, e, kind, targetWayPointId, targetName, score}
|
|
||||||
local function collectCandidates(input)
|
|
||||||
local sell, store, reasons = {}, {}, {}
|
|
||||||
for _, product in ipairs(input.products or {}) do
|
|
||||||
local e = ADOutboundPlanner.evaluate(product, input)
|
|
||||||
local added = false
|
|
||||||
if e.available >= ADOutboundPlanner.MIN_AVAILABLE_LITERS then
|
|
||||||
if wantsSell(product.policy) and e.priceOk and product.quote ~= nil then
|
|
||||||
table.insert(sell, {product = product, e = e, kind = "sell", targetWayPointId = product.quote.wayPointId, targetName = product.quote.stationName, percent = e.percent})
|
|
||||||
added = true
|
|
||||||
elseif wantsStore(product.policy) and (e.fullLoad or e.storageHigh) and product.store ~= nil then
|
|
||||||
table.insert(store, {product = product, e = e, kind = "store", targetWayPointId = product.store.wayPointId, targetName = product.store.name})
|
|
||||||
added = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if not added then table.insert(reasons, reasonFor(product, e, input)) end
|
|
||||||
end
|
|
||||||
table.sort(sell, function(a, b)
|
|
||||||
if a.e.percent ~= b.e.percent then return a.e.percent > b.e.percent end
|
|
||||||
if a.e.available ~= b.e.available then return a.e.available > b.e.available end
|
|
||||||
return a.product.name < b.product.name
|
|
||||||
end)
|
|
||||||
table.sort(store, function(a, b)
|
|
||||||
if a.e.storagePercent ~= b.e.storagePercent then return a.e.storagePercent > b.e.storagePercent end
|
|
||||||
if a.e.available ~= b.e.available then return a.e.available > b.e.available end
|
|
||||||
return a.product.name < b.product.name
|
|
||||||
end)
|
|
||||||
return sell, store, reasons
|
|
||||||
end
|
|
||||||
|
|
||||||
local function sortedSources(product)
|
|
||||||
local sources = {}
|
|
||||||
for _, source in ipairs(product.sources or {}) do
|
|
||||||
if (source.available or 0) >= ADOutboundPlanner.MIN_AVAILABLE_LITERS then table.insert(sources, source) end
|
|
||||||
end
|
|
||||||
table.sort(sources, function(a, b) return a.available > b.available end)
|
|
||||||
return sources
|
|
||||||
end
|
|
||||||
|
|
||||||
local function buildTrip(primary, candidates, input)
|
|
||||||
local trip = {kind = primary.kind, fillTypes = {primary.product.fillType}, names = {primary.product.name}, sourceIds = {}, targetWayPointId = primary.targetWayPointId, targetName = primary.targetName, percent = primary.percent}
|
|
||||||
local members = {primary}
|
|
||||||
if input.collectRound and input.hasAutoload and primary.product.isPallet then
|
|
||||||
for _, candidate in ipairs(candidates) do
|
|
||||||
if candidate ~= primary and candidate.kind == primary.kind and candidate.targetWayPointId == primary.targetWayPointId and candidate.product.isPallet then
|
|
||||||
table.insert(members, candidate)
|
|
||||||
table.insert(trip.fillTypes, candidate.product.fillType)
|
|
||||||
table.insert(trip.names, candidate.product.name)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local seen = {}
|
|
||||||
for _, member in ipairs(members) do
|
|
||||||
local sources = sortedSources(member.product)
|
|
||||||
local limit = input.collectRound and #sources or math.min(1, #sources)
|
|
||||||
for index = 1, limit do
|
|
||||||
local id = sources[index].wayPointId
|
|
||||||
if not seen[id] then seen[id] = true; table.insert(trip.sourceIds, id) end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return trip
|
|
||||||
end
|
|
||||||
|
|
||||||
function ADOutboundPlanner.plan(input)
|
|
||||||
local sell, store, reasons = collectCandidates(input)
|
|
||||||
if sell[1] ~= nil then return buildTrip(sell[1], sell, input), reasons end
|
|
||||||
if store[1] ~= nil then return buildTrip(store[1], store, input), reasons end
|
|
||||||
return nil, reasons
|
|
||||||
end
|
|
||||||
```
|
|
||||||
- [ ] **Step 4: Kør** → OK. **Step 5: Commit** `feat: adOutboundPlanner — turvurdering, prisregel, politikker, opsamlingsrunde (ren)`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 5: adSources — kilde-genkendelse og lagerstatus
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `FS25_ADSmartPickup/adSources.lua`
|
|
||||||
- Test: `tests/test_adSources.lua`
|
|
||||||
|
|
||||||
**Interfaces (Produces):**
|
|
||||||
```lua
|
|
||||||
ADSources.MAX_DISTANCE = 40; ADSources.PALLET_RADIUS = 25
|
|
||||||
ADSources.identify(adEnv, wayPointId, farmId) -- {kind="factory"|"silo"|"objectStorage", placeable, station|nil, wayPointId, name} | nil
|
|
||||||
ADSources.getProducts(adEnv, source, farmId) -- { {fillType, name, available, level, capacity, isPallet} } (kun varer med kapacitet/indhold)
|
|
||||||
ADSources.isPalletFillType(fillType) -- fillTypeDesc.palletFilename ~= nil
|
|
||||||
ADSources.getSellMarkers(adEnv, farmId) -- { {marker, station} } salgssteder (ADPrices.isSellingStation)
|
|
||||||
ADSources.getObjectInfoIndex(source, fillType) -- objectInfo-indeks + antal for varen i et objektlager, eller nil
|
|
||||||
ADSources.spawnOne(source, objectInfoIndex) -- pcall placeable:removeAbstractObjectsFromStorage(index, 1, nil) -> ok
|
|
||||||
ADSources.countStored(source, fillType) -- antal objekter af varen i lageret
|
|
||||||
ADSources.getSourcePlaceables(sources) -- {[placeable]=true} til udelukkelse
|
|
||||||
```
|
|
||||||
Genkendelse: læssestation ved markøren (`ADSmartPickup.getLoadStationAtWayPointId`) med `owningPlaceable.spec_productionPoint` → factory; med `spec_silo` → silo; ellers nærmeste placeable med `spec_objectStorage` inden for 40 m af waypointet (`g_currentMission.placeableSystem.placeables`, `getWorldTranslation(placeable.rootNode)`, `canFarmAccess`) → objectStorage.
|
|
||||||
Fabrik-produkter: `for production in productionPoint.productions: for output in production.outputs: type` (unik); `level = productionPoint.storage:getFillLevel(ft)`, `capacity = storage:getCapacity(ft)` (fallback `storage.capacities[ft]`); paller i nærheden: `g_currentMission.vehicleSystem.vehicles` med `spec_pallet` eller `isPallet`, ejer = farm, fillUnit-varen = ft, afstand ≤ 25 m fra `productionPoint.palletSpawner.spawnPlaces[1].startNode` (fallback placeable.rootNode); `available = level + Σ palle-fyldning`.
|
|
||||||
Silo: `spec_silo.storages[*]` fillLevels > 0 → `level = getOwnStorageLevel`, `capacity = storage:getCapacity(ft)`.
|
|
||||||
Objektlager: `spec_objectStorage.objectInfos[i].objects[1]` med `palletAttributes.fillType/fillLevel` eller `baleAttributes.fillType/fillLevel`; `numObjects` → available = numObjects × fillLevel; level = `spec.numStoredObjects`, capacity = `spec.capacity`; isPallet = true.
|
|
||||||
|
|
||||||
- [ ] **Step 1: Tests** med fakes: (S1) markør ved fabrik → factory med SUGAR output level 150000, capacity 180000, ingen paller → available 150000; (S2) markør ved silo → silo med FORAGE 74500; (S3) markør uden station, objektlager 20 m væk → objectStorage, produkt STRAW_BASKET available 8×1000, isPallet true, `getObjectInfoIndex` = 1 og 8; (S4) paller ved spawner: 2 paller à 1000 STRAW_BASKET inden for 25 m + 1 uden for → available = storage + 2000; (S5) `getSellMarkers` giver kun markører hvis unload-station er SellingStation med isSellingPoint; (S6) `spawnOne` kalder `removeAbstractObjectsFromStorage(1, 1, nil)` (fake tæller); (S7) markør 60 m fra objektlager → nil.
|
|
||||||
- [ ] **Step 2–4:** implementér som beskrevet, kør, grønne. **Step 5: Commit** `feat: adSources — fabrik, silo og objektlager som kilder; paller ved spawner; salgssteder`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 6: adOutbound — hooks, hold, objektlager-udtag, runde, advarsler
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `FS25_ADSmartPickup/adOutbound.lua`
|
|
||||||
- Test: `tests/test_adOutbound.lua`
|
|
||||||
|
|
||||||
**Interfaces (Produces):**
|
|
||||||
```lua
|
|
||||||
ADOutbound.CHECK_INTERVAL_MS = 5000; ADOutbound.NO_PROGRESS_MS = 15000; ADOutbound.SPAWN_STALL_MS = 30000
|
|
||||||
ADOutbound.isOutboundVehicle(vehicle) -- run for vognen har kind "outbound"
|
|
||||||
ADOutbound.choosePickup(adEnv, vehicle) -- kaldes fra getNextPickup-wrap; returnerer choice {markerIndex, markerName, group, station|nil, fillTypes, fillType} eller nil
|
|
||||||
ADOutbound.buildPlanInput(adEnv, run, vehicle) -- samler kilder/priser/lager til planlæggeren (spil-API)
|
|
||||||
ADOutbound.beforeLoadUpdate(adEnv, task, dt) -- true = håndteret (spring original update over)
|
|
||||||
ADOutbound.beforeLoadFinished(adEnv, task) -- true = fortsæt runde (spring original finished over)
|
|
||||||
ADOutbound.releaseInactive()
|
|
||||||
ADOutbound.getHoldFor(vehicle) -- {markerName, reasons} | nil
|
|
||||||
ADOutbound.getRunStatus(run) -- {reasons, warning} (seneste)
|
|
||||||
ADOutbound.install(adEnv) -- wrapper LoadAtDestinationTask.update/finished
|
|
||||||
```
|
|
||||||
|
|
||||||
Adfærd (fra spec afsnit 3–4):
|
|
||||||
- `choosePickup`: `trips[vehicle]=nil`; input = buildPlanInput; `trip, reasons = ADOutboundPlanner.plan(input)`; status[run.id] = reasons. Tur → `stateModule.selectedFillTypes = trip.fillTypes; setFillType(trip.fillTypes[1]); setSecondMarkerByWayPointId(trip.targetWayPointId); raiseDirtyFlag`; `trips[vehicle] = {run, trip, queue = copy(trip.sourceIds), visited = {}, current = trip.sourceIds[1]}`; log `'MT635' udkørsel: 8×STRAW_BASKET fra Straw factory → Halm-salg (95 % af top)`; returnér choice for første kilde (station = kildens læssestation hvis bulk, ellers nil). Ingen tur → `ADWaitPool.pickFree(adEnv, vehicle, run.waitPoolGroup, isReachable=hasRouteTo)`; fundet → claim, `holds[vehicle] = {markerId, reasons}`, log én gang pr. ændret begrundelse, returnér choice for Wait-markøren (station nil); intet Wait-punkt → hold ved første kilde (warn én gang) og returnér den.
|
|
||||||
- `buildPlanInput`: for hver kilde i `run.sourceWayPointIds`: `ADSources.identify` (nil → spring over + warn én gang "kilde markør X ikke genkendt"); products = union over kilder af `ADSources.getProducts`, filtreret til `ADRuns.getProductPolicy ~= "off"` og til det vognen kan bære (`bulk: AutoDrive.getSupportedFillTypesOfAllUnitsAlphabetically`, pallet: hasAutoload = `AutoDrive:hasAL(trailer)` for en af `getAllUnits`); pr. produkt med sell-politik: quotes over `ADSources.getSellMarkers` hvor `station.acceptedFillTypes[ft]` (eller `getIsFillTypeSupported`) og `ADWaitPool.hasRouteBothWays(adEnv, vehicle, marker.id, sourceId)` → `ADPrices.getQuote`; bedste = `ADPrices.bestQuote`; ingen nåelig men mindst én unåelig → warnOnce(vehicle, "sell:"..ft, "'%s' kan ikke nå salgssted '%s' for %s – tjek vejnettet"); pr. produkt med store-politik: `ADLoadSwap.findDumpMarker(adEnv, vehicle, ft, math.min(available, capacity), nil, isReachable, ADSources.getSourcePlaceables(sources))` → store; nil → warnOnce(vehicle, "store:"..ft, "'%s' finder ingen silo med plads til %s (%s)").
|
|
||||||
- `warnOnce(vehicle, key, text)`: `warned[vehicle][key] ~= text` → Logging.warning + pcall `adEnv.AutoDriveMessageEvent.sendMessageOrNotification(vehicle, adEnv.ADMessagesManager.messageTypes.ERROR, "ADSmartPickup: " .. text, 8000)`; status[run.id].warning = text. Ryddes når nøglens tilstand ændrer sig (ny tekst) eller turen lykkes.
|
|
||||||
- `beforeLoadUpdate`: hold → hvis `task.destinationID == hold.markerId` og `drivePathModule:isTargetReached()`: `specialDrivingModule:stopVehicle(); specialDrivingModule:update(dt)`; timer 5 s → `choosePickup` igen; tur → `ADWaitPool.releaseVehicle`, `holds[vehicle]=nil`, `ADWaitPool.redirect(task, trip.sourceIds[1])`, `task.activatedUALLoading=false`; returnér true. Ikke fremme → false.
|
|
||||||
Tur aktiv og `isTargetReached()`: kilde = `state.sources[task.destinationID]`; objektlager → `updateSpawn` (nedenfor); derefter fremdriftsvagt: `level = AutoDrive.getAllFillLevels(task.trailers)`; stiger → `noProgressMs=0`; ellers `+dt`; `> NO_PROGRESS_MS` og `level > 0` og ingen spawn i gang → `tryNextSourceOrFinish(task)`; returnér false (original kører altid, den aktiverer UAL og stopper vognen).
|
|
||||||
- `updateSpawn(state, task, source, dt)`: første gang: `index = ADSources.getObjectInfoIndex(source, currentFillType)`; hvis `task.activatedUALLoading` og vognen står (`vehicle.lastSpeedReal < 0.0005`): hvis ingen udestående spawn: `ualIsFull()` på en UAL-vogn → done; `countStored == 0` → skift til næste fillType i `trip.fillTypes` (nyt index) eller done; ellers `ADSources.spawnOne` → `pending = {stored = countStored, loaded = countLoaded(trailers), ms = 0}`; udestående: `countStored < pending.stored and countLoaded > pending.loaded` → pending=nil (næste); `pending.ms > SPAWN_STALL_MS` → done + warn "objektlager spawner ikke". `countLoaded` = antal nøgler i `spec_universalAutoload.loadedObjects` summeret over trailers. done → `task:finished()` (via original finished) og returnér.
|
|
||||||
- `tryNextSourceOrFinish(task)`: næste id i `state.queue` som ikke er besøgt, og fri kapacitet ≥ 10 % (`fillFreeCapacity/capacity`) → `visited[current]=true; current=next; ADWaitPool.redirect(task, next); task.activatedUALLoading=false; activePickups opdateres via ADSmartPickup.setActivePickup(vehicle, station, fillTypes)` (ny lille funktion i adSmartPickup); ellers `task:finished()`.
|
|
||||||
- `beforeLoadFinished`: kaldes fra wrap af `finished`; hvis der er tur, kilde ikke fuld (ikke `filledToUnload`) og en ubesøgt kilde med plads → redirect som ovenfor og returnér true (spring original over); ellers `trips[vehicle]=nil` og false.
|
|
||||||
- `releaseInactive`: for inaktive køretøjer: trips/holds ryddes, `ADWaitPool.releaseVehicle`.
|
|
||||||
|
|
||||||
- [ ] **Step 1: Tests** (fake AD-miljø som i `test_adRunsController.lua` + fakes for `ADSources`, `ADPrices`, `ADWaitPool`, `ADRunsController.getRunForVehicle`): (O1) tur → selectedFillTypes/2. markør sat, choice = kilde; (O2) ingen tur → Wait-markør claimet, hold registreret, choice = Wait; (O3) hold + 5 s + nu tur → redirect til kilde, hold frigivet; (O4) objektlager: spawn kaldes én ad gangen; stored falder + loaded stiger → næste spawn; `ualIsFull` → finished kaldt; (O5) spawn uden fremdrift 30 s → finished + WARN; (O6) runde bulk: kilde A fylder vognen (`filledToUnload`) → finished kaldt, ingen redirect; kilde A giver 30 % → redirect til B; (O7) intet nåeligt salgssted → `sendMessageOrNotification` kaldt én gang med ERROR, anden gang ikke; (O8) `releaseInactive` rydder hold og claim.
|
|
||||||
- [ ] **Step 2–4:** implementér, kør, grønne. **Step 5: Commit** `feat: adOutbound — udkørsels-hooks: turvalg, hold ved Wait, objektlager-udtag, opsamlingsrunde, advarsler på skærm`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 7: Controller — udkørsels-runs
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `FS25_ADSmartPickup/adRunsController.lua`
|
|
||||||
- Test: `tests/test_adRunsController.lua` (sektion C-out)
|
|
||||||
|
|
||||||
**Interfaces (Produces):**
|
|
||||||
```lua
|
|
||||||
ADRunsController.createRun(name, kind)
|
|
||||||
ADRunsController.getSourceMarkers(adEnv, farmId) -- markører der er kilder: {marker, kind}
|
|
||||||
ADRunsController.getRunProducts(adEnv, run) -- { {fillType, name, title, policy, isPallet, available, percent|nil, stationName|nil} } sorteret på titel
|
|
||||||
ADRunsController.validateVehicle(adEnv, run, vehicle) -- outbound: reasons noSource | noTrailer | noProducts | needsAutoload | cannotCarry | farFromNetwork
|
|
||||||
ADRunsController.getOutboundStatus(adEnv, run) -- {products = getRunProducts, reasons, warning} (fra ADOutbound.getRunStatus)
|
|
||||||
```
|
|
||||||
`applyRunToVehicle`: `if run.kind == "outbound"`: mode P&D, `setFirstMarkerByWayPointId(run.sourceWayPointIds[1])`, `setSecondMarkerByWayPointId(run.sourceWayPointIds[1])`, `selectedFillTypes` = produkt-fillTypes vognen kan bære, `setFillType(first)`, loopCounter 0, useFolders true, rotateTargets ONLYPICKUP; ingen dump-logik. `getVehicleActivity`: `ADOutbound.getHoldFor(vehicle)` → `{kind="waiting", place=markerName, reason=reasons[1]}`. `updateRun` logger som før. `getRunTargets` for outbound returnerer `{}` (mål vælges pr. tur).
|
|
||||||
|
|
||||||
- [ ] **Step 1: Tests** (C-out1) createRun kind; (C-out2) validate: ingen kilder → noSource; bulk-vogn og kun pallevarer → needsAutoload; ingen politik → noProducts; ok-tilfælde; (C-out3) startRun sætter 1./2. markør = kilde 1 og selectedFillTypes; (C-out4) getRunProducts slår politik op.
|
|
||||||
- [ ] **Step 2–4:** implementér, kør alle tests. **Step 5: Commit** `feat: controller — udkørsels-runs: start, validering, produkter, status`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 8: Wiring i adSmartPickup, adUnloadWait, modDesc
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `adSmartPickup.lua` (source() nye gui-filer, `installHook`: outbound-gren i `getNextPickup`-wrap, `ADOutbound.install(adEnv)`, `ADSmartPickup.setActivePickup`, `update`: `ADOutbound.releaseInactive`), `adUnloadWait.lua` (`beforeUpdate`: return hvis `ADOutbound.isOutboundVehicle`), `modDesc.xml` (sourceFiles: adPrices, adOutboundPlanner, adSources, adWaitPool, adOutbound før adUnloadWait/adSmartPickup; version 1.12.0.0; beskrivelse EN udvidet med udkørsel).
|
|
||||||
|
|
||||||
`getNextPickup`-wrap, ny gren først:
|
|
||||||
```lua
|
|
||||||
if ADOutbound ~= nil and ADOutbound.isOutboundVehicle(driver) then
|
|
||||||
local ok, choice = pcall(ADOutbound.choosePickup, adEnv, driver)
|
|
||||||
if not ok then
|
|
||||||
Logging.warning(ADSmartPickup.LOG_PREFIX .. "fejl i udkørsel, bruger AutoDrives rotation: %s", tostring(choice))
|
|
||||||
activePickups[driver] = nil
|
|
||||||
return originalGetNextPickup(self, driver, forcedSkip)
|
|
||||||
end
|
|
||||||
if choice == nil then activePickups[driver] = nil; return originalGetNextPickup(self, driver, forcedSkip) end
|
|
||||||
activePickups[driver] = choice.station ~= nil and {station = choice.station, fillTypes = choice.fillTypes, markerName = choice.markerName, fillType = choice.fillType} or nil
|
|
||||||
self.pickups[choice.group] = self.pickups[choice.group] or {}
|
|
||||||
self.pickups[choice.group].lastTarget = choice.markerIndex
|
|
||||||
self.pickups[choice.group].lastVehicle = driver
|
|
||||||
return choice.markerIndex
|
|
||||||
end
|
|
||||||
```
|
|
||||||
- [ ] **Step 1:** ret filer. **Step 2:** `luajit tests/test_adSmartPickup.lua` grøn; `for t in tests/test_*.lua; do luajit $t | tail -1; done`. **Step 3: Commit** `feat: udkørsel koblet på AutoDrive-hooks; Wait-logik springer udkørsels-traktorer over`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 9: GUI — tre faner
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `gui/SmartPickupFrame.lua` (faner, fælles), `gui/SmartPickupFrame.xml` (nye statiske rækker + productPrefab + sourcePrefab), `l10n/l10n_da.xml`, `l10n/l10n_en.xml`
|
|
||||||
- Create: `gui/SmartPickupSupplyTab.lua` (flyt: fillTargetRows, fillNeedsRows, refreshNeedsRows, fillIngredientRows, describeTargetNeeds, onClickTarget/IngredientMode/Ingredient/Loops/ReturnBelow, `SmartPickupFrame:rebuildSupplyTab(adEnv, run)`), `gui/SmartPickupOutboundTab.lua` (`rebuildOutboundTab(adEnv, run)`: Kilder-rækker (ingredientPrefab med kind="source"), Varer-rækker (productPrefab: MultiTextOption med 4 tilstande Fra/Lager/Salg/Salg, ellers lager + titel "Sukker — 150.0k · 78 % af top (Grain Elevator)"), Regler (sellAtOption 50..100 trin 5, storeAboveOption 50..95 trin 5, collectRoundOption), status-/advarselsrække; callbacks onClickSource/onClickPolicy/onClickSellAt/onClickStoreAbove/onClickCollectRound; `describeOutboundOverview(run)` til Oversigt).
|
|
||||||
- `SmartPickupFrame`: `TAB_OVERVIEW, TAB_SUPPLY, TAB_OUTBOUND = 1, 2, 3`, `TAB_NAMES = {"spu_tab_overview","spu_tab_supply","spu_tab_outbound"}`, `tabKind = {nil, "supply", "outbound"}`; `fillRunOption` viser kun runs med fanens kind; `onNewRunName` opretter med fanens kind (Oversigt → supply); `setStaticRowsVisible(tab)` viser forsynings-rækker (ingredientMode, waitPool, loops, returnBelow) kun på fane 2, udkørsels-rækker (waitPool, sellAt, storeAbove, collectRound) kun på fane 3 (waitPool er fælles); blok-overskrifter via `addTextRow` ("— Mål og varer —" stil: brug tekstnøglerne `spu_section_*`). `refreshLive` opdaterer også produktrækkernes titler (pris-%) hvert sekund og udkørsels-status.
|
|
||||||
- l10n (da/en): `spu_tab_supply` "Forsyning"/"Supply", `spu_tab_outbound` "Udkørsel"/"Outbound", `spu_sources` "Kilder (fabrik, silo eller objektlager)", `spu_products` "Varer", `spu_policy_off/store/sell/sellElseStore` "Fra/Lager/Salg/Salg, ellers lager", `spu_sellAt` "Sælg fra (% af årets toppris)", `spu_sellAt_tooltip`, `spu_storeAbove` "Kør til lager over", `spu_storeAbove_tooltip`, `spu_collectRound` "Saml op på én tur", `spu_collectRound_tooltip`, `spu_section_run/targets/rules/wait/vehicles/status`, `spu_reason_noSource` "ingen genkendt kilde", `spu_reason_noProducts` "ingen vare valgt", `spu_reason_needsAutoload` "pallevarer kræver Universal Autoload på vognen", `spu_product_row` "%s — %s · %s", `spu_price_pct` "%d %% af top (%s)", `spu_price_none` "intet salgssted", `spu_outbound_waiting` "venter: %s", `spu_activity_holding` "holder ved %s (%s)".
|
|
||||||
- [ ] **Step 1:** XML + l10n; **Step 2:** split + ny fane; **Step 3:** `luajit` alle tests (GUI testes in-game); **Step 4: Commit** `feat(gui): faner Oversigt/Forsyning/Udkørsel; udkørsels-run med kilder, varepolitik, regler`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 10: README, version, byg, deploy, in-game
|
|
||||||
|
|
||||||
- [ ] README: nyt afsnit "Udkørsel (v1.12)" (kilder, politikker, prisregel, lagertærskel, runde, objektlager-krav om markørplacering, advarsler). modDesc 1.12.0.0.
|
|
||||||
- [ ] `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip . && cp ../FS25_ADSmartPickup.zip ~/FS25-data/mods/`
|
|
||||||
- [ ] Commit `chore: v1.12.0.0 — udkørsel af produkter` + push til Gitea (`git push gitea main` med token fra vault).
|
|
||||||
- [ ] In-game-test (bruger): savegame 3 — se spec afsnit 4.
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,174 +0,0 @@
|
|||||||
# Guider til opsætning (marker + flows) — implementeringsplan
|
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans (native; brugeren har givet fuld autonomi). Trin bruger `- [ ]`.
|
|
||||||
|
|
||||||
**Goal:** Fields- og Flows-fanen får en oversigt med resumé (klik = ret ét trin) og en trin-for-trin-guide ("Opsæt mark" / "Opsæt flow") på én fælles guide-ramme.
|
|
||||||
|
|
||||||
**Architecture:** En ren guide-motor (`adGuide.lua`) styrer trin, synlighed, tælling, næste/tilbage/spring. Oven på den to rene trin-definitioner (`adFieldGuide.lua`, `adFlowGuide.lua`) der ud fra en kladde + et `context` (det spil-afhængige, hentet af GUI'et) giver rækker (som data), valg, standarder, resumé og gem-resultat. GUI-laget (`gui/SmartPickupGuide.lua` fælles trin-skærm, `gui/SmartPickupFieldGuide.lua`, `gui/SmartPickupFlowGuide.lua`) er tyndt: bygger context, tegner rækkerne, router klik og gemmer via de eksisterende `ADFieldJobs`/`ADRunsController`-funktioner.
|
|
||||||
|
|
||||||
**Tech Stack:** Lua 5.1 (FS25), luajit til tests, FS25 GUI (klonede prefabs).
|
|
||||||
|
|
||||||
**Spec:** `docs/superpowers/specs/2026-09-26-guider-design.md`
|
|
||||||
|
|
||||||
## Global Constraints
|
|
||||||
|
|
||||||
- Lua 5.1: ingen goto, ingen coroutines, ingen `#` på tabeller med huller; pcall om spil-API.
|
|
||||||
- Gemt format (`ADSmartPickup_fields.xml`, `ADSmartPickup_runs.xml`) ændres ikke. Guiderne skriver kun via `ADFieldJobs.setTarget/setPlan` og `ADRunsController.createRun/updateRun/assignVehicle/unassignVehicle`.
|
|
||||||
- Intet gemmes før [Gem]/[Kør automatisk]/[Start flow]. [Annullér], fanebyt og lukket menu kasserer kladden.
|
|
||||||
- Rene moduler returnerer nye tabeller (kladden kopieres ved hvert valg).
|
|
||||||
- Filer under ~800 linjer; `SmartPickupFrame.lua` (971) får kun små routing-ændringer.
|
|
||||||
- Nye .lua i modDesc (rene moduler) eller `source()` i adSmartPickup.lua (GUI-filer), i rigtig rækkefølge.
|
|
||||||
- Al ny tekst i både `l10n_da.xml` og `l10n_en.xml`.
|
|
||||||
- Guide-rækker laves KUN med `ingredientPrefab` (Ja/Nej, flueben) og `productPrefab` (valg) med `kind = "guideRow"`, routet i `onClickIngredient`/`onClickProduct`; klikbare tekstlinjer via `makeRowClickable` + pendingClick. Ingen guide-rækker på `vehiclePrefab` (v1.30.4-fælden).
|
|
||||||
- Bundknapper: kun MENU_ACTIVATE, MENU_EXTRA_1, MENU_EXTRA_2, MENU_CANCEL (+ MENU_BACK) — andre findes ikke sikkert i FS25.
|
|
||||||
- Tests: `for t in tests/test_*.lua; do luajit $t | tail -1; done` — alle grønne.
|
|
||||||
|
|
||||||
## Review Focus
|
|
||||||
|
|
||||||
1. Klik på guide-rækker gemmes aldrig (forkert routing) → alle nye kinds routes via `guideRow`; testplan-punkt in-game.
|
|
||||||
2. Kladde muteres ved valg (Annullér gemmer alligevel) → test: `choose` ændrer ikke den gamle guide/kladde.
|
|
||||||
3. Trin forsvinder/dukker op efter et valg (fx mål-afgrøde græs på kornmark) → tælling og næste følger de synlige trin; test.
|
|
||||||
4. [Ret] fra oversigten må kun gemme det ene trin og bevare Auto/Manuel → test `result(guide, nil)` bevarer mode.
|
|
||||||
5. Faste maskiner: maskine fast på anden mark flyttes kun hvis brugeren vælger den (skjult i listen) → test `applyRigs`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Beslutninger (uklarheder i spec'en)
|
|
||||||
|
|
||||||
- **[Opsæt mark] uden valgt mark** vises også (åbner trin 1 "Mark"); ellers kunne trin 1 aldrig nås.
|
|
||||||
- **"Så igen bagefter"** (plan.sow uden mål-afgrøde) står i trin 2 under "Behold det der gror" for høst-/bare marker — ellers forsvinder muligheden.
|
|
||||||
- **Kalk**: i Klargøring ("Vælg selv"); når Klargøring ikke vises (ren græsmark) står Kalk i Pleje.
|
|
||||||
- **Klargøring "Automatisk"** = jord auto, kalk ja, mulch nej, tromle nej (anvendes kun når trinnet er synligt).
|
|
||||||
- **Maskiner-trinnet**: antal ad gangen + aflæsningsvogne først, så "Faste maskiner?" + listen (listen kan være lang). Aflæsningsvogne 1–3 (modellens `MAX_UNLOADERS`), ikke 1–5.
|
|
||||||
- **Mark uden markør**: gemmes med den valgte tilstand; job-løkken venter allerede på markøren (dagens regel). Oversigten viser "sæt markøren".
|
|
||||||
- **Mål-afgrøde** tvinger Auto (som i dag); på Oversigt-trinnet vises da kun [Kør automatisk].
|
|
||||||
- **[Ret] fra trin 8** vender tilbage til Oversigt-trinnet (kladden bevares); [Tilbage] i et [Ret]-trin fortryder det ene trins ændringer.
|
|
||||||
- **Flows**: bundlinjen har plads til 4 knapper → [Start/Stop] [Opsæt flow] [Nyt flow] [Slet]; Omdøb ligger på resuméets Navn-linje; Start alle/Stop alle bliver på fanen Oversigt.
|
|
||||||
- **Halm**: `context.straw` = afgrøder med halm (FS25 `fruitType.hasWindrow/windrowName`); kan det ikke aflæses (tom), vises halm-valget for alle høstafgrøder som i dag.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Etape A — fælles ramme + Marker (v1.31.0.0)
|
|
||||||
|
|
||||||
### Task A1: Guide-motoren `adGuide.lua`
|
|
||||||
|
|
||||||
**Files:** Create `FS25_ADSmartPickup/adGuide.lua`, `tests/test_adGuide.lua`; Modify `modDesc.xml` (før adFieldModel.lua).
|
|
||||||
|
|
||||||
**Interfaces (Produces):**
|
|
||||||
```lua
|
|
||||||
ADGuide.new(def, draft, context, options) -> guide
|
|
||||||
-- def = {order = {stepId...}, isVisible = function(stepId, guide) -> bool}
|
|
||||||
-- options = {step = id|nil, single = bool, returnTo = "close"|"summary"}
|
|
||||||
-- guide = {def, draft, context, step, single, returnTo, snapshot}
|
|
||||||
ADGuide.visibleSteps(guide) -> {stepId...}
|
|
||||||
ADGuide.position(guide) -> index|nil, total
|
|
||||||
ADGuide.isLast(guide) -> bool
|
|
||||||
ADGuide.next(guide) -> guide, event -- event nil | "done"
|
|
||||||
ADGuide.back(guide) -> guide, event -- event nil | "cancel"
|
|
||||||
ADGuide.jump(guide, stepId, returnTo) -> guide (single, snapshot = kopi af kladden)
|
|
||||||
ADGuide.withDraft(guide, draft) / ADGuide.withContext(guide, context) -> guide
|
|
||||||
ADGuide.deepCopy(value) ; ADGuide.toggleItem(list, item, on) -> ny liste
|
|
||||||
ADGuide.labelText(label, getText) -> string -- label = {key, args} | {text}
|
|
||||||
ADGuide.lineText(line, getText) -> "Titel: del, del" -- line = {step, title = label, parts = {label...}}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] Test (RED): næste/tilbage springer skjulte trin over; tælling "3 af 5" over synlige; `next` på sidste = "done"; `back` på første = "cancel"; `jump(..., "summary")` + `next` lander på `summary` med single=false; `jump(..., "close")` + `next` = "done"; `back` i spring gendanner snapshot; `withDraft` returnerer ny guide (gammel urørt); trin der bliver usynligt efter et valg: `next` går til næste synlige i `order`; `toggleItem` uden dubletter; `labelText` med args og manglende args (pcall).
|
|
||||||
- [ ] Kør `luajit tests/test_adGuide.lua` → FAIL (fil mangler).
|
|
||||||
- [ ] Implementér; kør → ALLE TESTS OK.
|
|
||||||
|
|
||||||
### Task A2: Markguidens trin `adFieldGuide.lua`
|
|
||||||
|
|
||||||
**Files:** Create `FS25_ADSmartPickup/adFieldGuide.lua`, `tests/test_adFieldGuide.lua`; Modify `modDesc.xml` (efter adFieldView.lua).
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Consumes: `ADGuide.*`, `ADFieldModel.product/MAX_RIGS/MAX_UNLOADERS`, `ADFieldFleet.visibleForField`, `ADFlowView.naturalLess`.
|
|
||||||
- Produces:
|
|
||||||
```lua
|
|
||||||
ADFieldGuide.ORDER = {"field","crop","cropChoice","prep","care","machines","store","summary"}
|
|
||||||
ADFieldGuide.open(field, context, options) -> guide -- context.info påkrævet
|
|
||||||
ADFieldGuide.openPicker(context) -> guide -- trin "field"
|
|
||||||
ADFieldGuide.pickField(guide, field, context) -> guide -- videre til trinnet efter "field"
|
|
||||||
ADFieldGuide.isVisible(stepId, guide) -> bool
|
|
||||||
ADFieldGuide.rows(guide) -> {row...}
|
|
||||||
-- row = {type="choice", id, title, options={{label, value}}, value}
|
|
||||||
-- | {type="toggle", id, title, off, on, value}
|
|
||||||
-- | {type="check", id, item, title, value}
|
|
||||||
-- | {type="text", label} | {type="link", id, value, label}
|
|
||||||
ADFieldGuide.choose(guide, id, value) -> guide
|
|
||||||
ADFieldGuide.summary(guide) -> {line...}
|
|
||||||
ADFieldGuide.result(guide, start) -> {fieldId, targetFruit, targetChanged, plan, rigIds}
|
|
||||||
ADFieldGuide.applyRigs(rigIdsByField, fieldId, rigIds) -> ny {[fieldId] = ids}
|
|
||||||
ADFieldGuide.stepKey(stepId) / hintKey(stepId) -> l10n-nøgler
|
|
||||||
```
|
|
||||||
- context: `{info, sowable={{name,title}}, mowable={[name]=true}, straw={[name]=true}|nil, rigs={{id,text,role}}, rigIdsByField, fieldSilos={{id,text}}, stores={{id,text}}|nil, storeHidden, fields={{id,text}}}`.
|
|
||||||
- kladde: `{fieldId, plan (kopi), originalTarget, prepAuto, useDedicated, storeMode ("auto"|"manual"|"fieldSilo"), storeIds, pickField}`.
|
|
||||||
|
|
||||||
- [ ] Test (RED): synlige trin for (a) græs uden mål: crop, cropChoice, care, machines, store, summary (ingen prep); (b) høst m. halm: + prep, cropChoice (halm); (c) høst uden halm (`straw` sat, afgrøden ikke med): ingen cropChoice; (d) bar mark uden mål: crop, prep, machines, summary (ingen care/store); (e) kornmark med mål græs: cropChoice (ensilage/hø) + prep; picker: kun "field". Standarder: ny mark → prepAuto, useDedicated=false, storeMode auto; plan med soil=plow → prepAuto=false; plan med rigIds → useDedicated; storeIds lig marksilo → fieldSilo. `choose` ændrer ikke gammel guide; `rigIds`-check toggler; `result`: prepAuto overskriver soil/mulch/lime/roll kun når prep er synlig; mode auto/manual efter start, uændret ved start=nil, auto ved mål; storeBuildingIds pr. storeMode; rigIds tom når useDedicated=false; targetChanged. `applyRigs` fjerner maskinen fra anden mark. Rækker: maskiner-trinnet skjuler maskiner faste på andre marker + tekst med antal; marksilo-spørgsmål kun når fieldSilos findes; "Vælg selv" viser siloerne. Resumé: linjer kun for synlige trin, med rigtige nøgler (fx `spu_guide_sum_dedicated` args {3}).
|
|
||||||
- [ ] Kør → FAIL; implementér; kør → OK. Hele suiten grøn.
|
|
||||||
|
|
||||||
### Task A3: Fælles trin-skærm `gui/SmartPickupGuide.lua`
|
|
||||||
|
|
||||||
**Files:** Create `gui/SmartPickupGuide.lua`; Modify `adSmartPickup.lua` (source efter SmartPickupFlowTab.lua), `gui/SmartPickupFrame.lua` (initGuideButtons i new, routing `guideRow` i onClickIngredient, refreshButtons → `getGuideButtons`, switchTab/onFrameClose lukker guiden, rebuild for faner med åben guide), `gui/SmartPickupOutboundTab.lua` (onClickProduct → guideRow).
|
|
||||||
|
|
||||||
**Interfaces (Produces):**
|
|
||||||
```lua
|
|
||||||
SmartPickupFrame.GUIDE_HOOKS[kind] = {model, tab, context(frame, adEnv, guide) -> context, save(frame, guide, start), choose(frame, id, value) -> handled}
|
|
||||||
SmartPickupFrame:initGuideButtons(i18n)
|
|
||||||
SmartPickupFrame:openGuide(kind, guide) SmartPickupFrame:closeGuide()
|
|
||||||
SmartPickupFrame:fillGuideRows(adEnv) -- overskrift "Trin 3 af 7 — …", forklaring, rækker
|
|
||||||
SmartPickupFrame:guideText(label) -> string
|
|
||||||
SmartPickupFrame:addLinkRow(text, action)
|
|
||||||
SmartPickupFrame:onClickGuideRow(state, data)
|
|
||||||
SmartPickupFrame:getGuideButtons() -> knapliste
|
|
||||||
SmartPickupFrame:onClickGuideNext/Back/Cancel/SaveOnly()
|
|
||||||
```
|
|
||||||
- [ ] Skriv filen; `luajit -bl` syntakstjek.
|
|
||||||
|
|
||||||
### Task A4: Marker-fanen: oversigt, markflåde, guide
|
|
||||||
|
|
||||||
**Files:** Create `gui/SmartPickupFieldGuide.lua` (context, gem, knapper for fanen); Modify `gui/SmartPickupFieldTab.lua` (fillFieldRows → oversigt/markflåde/guide; fjern den lange detaljeliste), l10n da/en.
|
|
||||||
|
|
||||||
- Oversigt: hint, tabel, for valgt mark: overskrift, resumé-linjer (klik = `ADGuide.jump(step, "close")`), fuld status, markør-linje, marksilo-advarsel. Knapper: [Opsæt mark] (EXTRA_1), [Start nu/Stop] (ACTIVATE), [Markflåde] (EXTRA_2).
|
|
||||||
- Markflåde: `fillFleetRows(adEnv, nil)` (alle maskiner). Knap [Til marker] (EXTRA_2).
|
|
||||||
- Gem: `setTarget` hvis ændret → `setPlan(plan)` → `applyRigs` → `setPlan(fid, {rigIds})` for ændrede marker.
|
|
||||||
- [ ] Skriv, syntakstjek, suiten grøn.
|
|
||||||
|
|
||||||
### Task A5: Docs + ship v1.31.0.0
|
|
||||||
|
|
||||||
- [ ] TESTPLAN-markarbejde.md: afsnit "Guider (v1.31)"; BRUGERMANUAL/USER_MANUAL: Marker-afsnit kort.
|
|
||||||
- [ ] Ship-procedure (version 1.30.4.0 → 1.31.0.0), commit `feat(gui): guide "Opsæt mark" …`, push gitea.
|
|
||||||
|
|
||||||
# Etape B — Flows på samme ramme (v1.32.0.0)
|
|
||||||
|
|
||||||
### Task B1: Flowguidens trin `adFlowGuide.lua`
|
|
||||||
|
|
||||||
**Files:** Create `FS25_ADSmartPickup/adFlowGuide.lua`, `tests/test_adFlowGuide.lua`; modDesc (efter adFlowView.lua).
|
|
||||||
|
|
||||||
**Interfaces (Produces):**
|
|
||||||
```lua
|
|
||||||
ADFlowGuide.ORDER = {"name","direction","buildings","goods","store","rules","wait","tractors","summary"}
|
|
||||||
ADFlowGuide.open(run, context, options) -> guide -- run = eksisterende run
|
|
||||||
ADFlowGuide.openNew(name, context) -> guide -- nyt flow (trin "name" synligt)
|
|
||||||
ADFlowGuide.isVisible(stepId, guide); ADFlowGuide.rows(guide); ADFlowGuide.choose(guide, id, value)
|
|
||||||
ADFlowGuide.summary(guide) -> {line...}
|
|
||||||
ADFlowGuide.result(guide) -> {isNew, runId, name, kind, changes, vehicleIds}
|
|
||||||
ADFlowGuide.vehicleChanges(oldIds, newIds) -> added, removed
|
|
||||||
ADFlowGuide.previewRun(draft) -> run-formet tabel til controllerens opslag
|
|
||||||
```
|
|
||||||
- context: `{buildings={{id,name,label,hasMarker,text}}, fillTypes={{name,title}}, products={{name,title}}, storeBuildings={{id,text}}, waitPools={{group,count}}, vehicles={{id,text,otherRunId,otherRunName,blocked}}, sellAtSteps, storeAboveSteps, returnBelowSteps, maxLoops}`.
|
|
||||||
- kladde: kopi af run-felterne + `isNew`, `showAllBuildings`, `storeAuto`.
|
|
||||||
- [ ] Test (RED): name-trin kun ved nyt flow; store-trin kun ved udkørsel med en vare på Lager/Salg-ellers-lager; retning skifter bygnings-rolle; varer: forsyning auto/vælg (ingredients toggles), udkørsel politik pr. vare (products); regler pr. retning; traktorer i andre flows/blokerede skjult + tekst; resumé pr. retning; result.changes; vehicleChanges; choose immutabelt.
|
|
||||||
- [ ] FAIL → implementér → OK.
|
|
||||||
|
|
||||||
### Task B2: Flows-fanen i GUI'et
|
|
||||||
|
|
||||||
**Files:** Create `gui/SmartPickupFlowGuide.lua`; Modify `gui/SmartPickupFrame.lua` (rebuild for Flows-fanen: tavle + resumé, statiske rækker skjult; refreshLive/refreshButtons), l10n.
|
|
||||||
|
|
||||||
- Oversigt: tavlen (`fillOverviewRows`), valgt flow: resumé (klik = ret ét trin; Navn-linjen = omdøb-dialog), status-linje (live). Knapper: [Start/Stop] (ACTIVATE), [Opsæt flow] (EXTRA_1), [Nyt flow] (EXTRA_2), [Slet] (CANCEL).
|
|
||||||
- Gem: nyt → `createRun`; `updateRun(changes)`; traktorer via `vehicleChanges` + `assignVehicle/unassignVehicle`; `resolveMarkers`; `save()`; [Start flow] → `startRun`.
|
|
||||||
- [ ] Skriv, syntakstjek, suiten grøn.
|
|
||||||
|
|
||||||
### Task B3: Docs + ship v1.32.0.0; selv-review
|
|
||||||
|
|
||||||
- [ ] TESTPLAN + manualer (Flows-afsnit), ship 1.31.0.0 → 1.32.0.0.
|
|
||||||
- [ ] Code-review-agent på hele diffen; ret CRITICAL/HIGH; evt. v1.32.1.0.
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,919 +0,0 @@
|
|||||||
# Maskinpulje etape 1 (v1.48.0.0) Implementation Plan
|
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
||||||
|
|
||||||
**Goal:** Markarbejdet kan sende en ledig solo-traktor, der henter et frakoblet puljeredskab (plov, kultivator,
|
|
||||||
såmaskine …), udfører arbejdet med Courseplay, sætter redskabet af på dets AD-parkering og kører hjem.
|
|
||||||
|
|
||||||
**Architecture:** Nyt modul `adPool.lua` (ren planner + tynd spil-adapter) finder puljeredskaber (frakoblet +
|
|
||||||
AD-parkering "X" + markør "Connect X") og puljetraktorer og parrer dem. `ADFieldWork.dispatch` bruger puljen som
|
|
||||||
fallback, når ingen færdig kombination er ledig; en ny fase `fetching` kører `ADHitch.fetch` og fortsætter så i
|
|
||||||
den eksisterende `driving`-fase. Når en opgave slutter, kører `ADHitch.returnHome` redskabet til dets parkering
|
|
||||||
(afkobling) og traktoren hjem. Marker, der venter, betjenes længst-ventende først.
|
|
||||||
|
|
||||||
**Tech Stack:** Lua 5.1 (FS25-scripting), luajit til tests, AutoDrive 3.0.1.2 og Courseplay 8.1.0.3 API'er.
|
|
||||||
|
|
||||||
**Spec:** `docs/superpowers/specs/2026-09-28-maskinpulje-design.md`
|
|
||||||
|
|
||||||
## Global Constraints
|
|
||||||
|
|
||||||
- Kun etape 1 (markarbejde). Tømmevogne ved høst (`decision.helper == true`) og marksilo-flowet bruger IKKE puljen i denne version.
|
|
||||||
- Puljeredskab = frakoblet + AD-parkering "X" (`advd.parkDestination >= 1`) + markør "Connect X" (store/små bogstaver ligegyldigt).
|
|
||||||
- Puljetraktor = motoriseret, intet koblet bag på, ikke i et flow (`ADRunsController.getRunForVehicle == nil`), ikke optaget, ikke styret af spilleren, egen AD-parkering, flueben i markflåden (`ADFieldFleet.isEnabled`).
|
|
||||||
- Færdige kombinationer foretrækkes altid; puljen kun når `pick`/`pickMany` intet giver.
|
|
||||||
- Redskab nærmest marken, derefter nærmeste traktor til redskabet med nok hk; manglende hk-data = tilladt.
|
|
||||||
- Karantæne for et redskab, der ikke kunne kobles på: 10 min (`ADPool.QUARANTINE_MS = 600000`).
|
|
||||||
- En traktor køres kun hjem, når et af mod'ens egne jobs slutter — aldrig af sig selv.
|
|
||||||
- Flows (runs) rører aldrig puljen.
|
|
||||||
- Kommentarer og logtekster på dansk, som resten af koden. Ingen Co-Authored-By i commits. Push kun til remote `gitea`.
|
|
||||||
- Tests køres fra repo-roden: `luajit tests/test_<navn>.lua` — output slutter med `ALLE TESTS OK`.
|
|
||||||
- Build: `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .`; deploy mens spillet kører: `cp` til `~/FS25-data/mods/.FS25_ADSmartPickup.new` og `mv` på plads.
|
|
||||||
|
|
||||||
## Review Focus
|
|
||||||
|
|
||||||
- **To marker vil have samme plov samtidig** → kun den første får den; den anden venter (reservation) — testet i Task 1 (`pairFor` springer reserverede over).
|
|
||||||
- **Redskab uden hk-data eller traktor uden motor-data** → tillades (intet går i stå) — testet i Task 1 (`powerOk` nil-tilfælde, `numberOf` af tabel-specs).
|
|
||||||
- **Traktor på hjemtur bliver valgt til et nyt job** → må ikke ske; den er optaget til den er hjemme — testet i Task 3 (`ADPool.isReturning`) og brugt i `getRigs`/`poolTractors`.
|
|
||||||
- **Spilleren kobler selv et puljeredskab på og kører** → traktoren køres ikke hjem af mod'en — dækket af, at `returnHome` kun kaldes fra `finishAssignment`/fejlveje (Task 5); ingen tick-regel.
|
|
||||||
- **Kobling fejler (trækøje står skævt)** → traktor hjem, redskab i karantæne, marken prøver igen — testet i Task 1 (karantæne i `pairFor`) og Task 4 (fejlvej).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 1: `adPool.lua` — ren planlægning
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `FS25_ADSmartPickup/adPool.lua`
|
|
||||||
- Test: `tests/test_adPool.lua`
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Consumes: `ADFieldFleet.roleFromSpecs(specs)` (adFieldFleet.lua:27), `ADFieldModel.checkWidth(role, width, requiredWidth)` (bruges via `accept`), `ADHitch.parseMarker(name)` (adHitch.lua).
|
|
||||||
- Produces:
|
|
||||||
- `ADPool.roleOf(specs) -> role|nil` (specs = `ADCpBridge.getSpecs`-tabel; `unloader` når `specs.spec_trailer and specs.spec_dischargeable` og ingen anden rolle).
|
|
||||||
- `ADPool.numberOf(value) -> number|nil`
|
|
||||||
- `ADPool.powerOk(tractorHp, neededHp) -> bool`
|
|
||||||
- `ADPool.connectFor(parkName, markers) -> marker|nil` (markers = `{{name, id}}`)
|
|
||||||
- `ADPool.pairFor(role, fieldX, fieldZ, implements, tractors, blocked, accept) -> pair | nil, why, args`
|
|
||||||
- implement = `{id, name, role, roles?, x, z, width, neededHp, connectId, parkId}`
|
|
||||||
- tractor = `{id, name, x, z, hp, parkId}`
|
|
||||||
- blocked = `{[id] = true}` (reserverede + karantæne, både redskab- og traktor-id)
|
|
||||||
- pair = `{implement = implement, tractor = tractor}`
|
|
||||||
- why ∈ `noPoolImplement` / `noPoolTractor` / `tooWeak` (args = `{neededHp}`)
|
|
||||||
- `ADPool.queueOrder(infos, waitingSince) -> infos (ny tabel)`
|
|
||||||
- `ADPool.reserve(owner, ids)`, `ADPool.release(owner)`, `ADPool.quarantine(id, nowMs)`, `ADPool.blocked(nowMs) -> {[id]=true}`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Write the failing test**
|
|
||||||
|
|
||||||
Create `tests/test_adPool.lua`:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- Kør: luajit tests/test_adPool.lua (fra repo-roden)
|
|
||||||
dofile("FS25_ADSmartPickup/adFieldFleet.lua")
|
|
||||||
dofile("FS25_ADSmartPickup/adHitch.lua")
|
|
||||||
dofile("FS25_ADSmartPickup/adPool.lua")
|
|
||||||
local P = ADPool
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
-- rolle ud fra redskabets egne specs
|
|
||||||
check("rolle: plov", P.roleOf({spec_plow = true}), "plow")
|
|
||||||
check("rolle: kultivator", P.roleOf({spec_cultivator = true}), "cultivator")
|
|
||||||
check("rolle: vogn med tip", P.roleOf({spec_trailer = true, spec_dischargeable = true}), "unloader")
|
|
||||||
check("rolle: vogn der også er læssevogn er læssevogn", P.roleOf({spec_trailer = true, spec_dischargeable = true, spec_forageWagon = true}), "collector")
|
|
||||||
check("rolle: intet", P.roleOf({}), nil)
|
|
||||||
-- hk fra butiksdata: tal eller tabel (konfigurationer)
|
|
||||||
check("tal", P.numberOf(180), 180)
|
|
||||||
check("tabel: største", P.numberOf({base = 120, config = 150}), 150)
|
|
||||||
check("tom tabel", P.numberOf({}), nil)
|
|
||||||
check("nil", P.numberOf(nil), nil)
|
|
||||||
check("hk: nok", P.powerOk(200, 180), true)
|
|
||||||
check("hk: for lidt", P.powerOk(150, 180), false)
|
|
||||||
check("hk: redskab uden data", P.powerOk(150, nil), true)
|
|
||||||
check("hk: traktor uden data", P.powerOk(nil, 180), true)
|
|
||||||
-- Connect-markør til en parkering
|
|
||||||
local markers = {{name = "Start", id = 1}, {name = "Bredal", id = 4}, {name = "Connect Bredal", id = 9}, {name = "connect plov 1", id = 12}}
|
|
||||||
check("connect: Bredal", P.connectFor("Bredal", markers).id, 9)
|
|
||||||
check("connect: små bogstaver", P.connectFor("Plov 1", markers).id, 12)
|
|
||||||
check("connect: ingen", P.connectFor("Start", markers), nil)
|
|
||||||
check("connect: uden parkering", P.connectFor(nil, markers), nil)
|
|
||||||
-- parring: redskab nærmest marken, så traktor nærmest redskabet
|
|
||||||
local implements = {
|
|
||||||
{id = "plovFar", name = "Plov far", role = "plow", x = 500, z = 0, connectId = 20, parkId = 21},
|
|
||||||
{id = "plovNear", name = "Plov nær", role = "plow", x = 100, z = 0, neededHp = 180, connectId = 30, parkId = 31},
|
|
||||||
{id = "kult", name = "Kultivator", role = "cultivator", x = 0, z = 0, connectId = 40, parkId = 41},
|
|
||||||
}
|
|
||||||
local tractors = {
|
|
||||||
{id = "small", name = "Lille", x = 110, z = 0, hp = 120, parkId = 2},
|
|
||||||
{id = "big", name = "Stor", x = 300, z = 0, hp = 250, parkId = 3},
|
|
||||||
{id = "mid", name = "Mellem", x = 150, z = 0, hp = 190, parkId = 5},
|
|
||||||
}
|
|
||||||
local pair = P.pairFor("plow", 0, 0, implements, tractors, {})
|
|
||||||
check("par: nærmeste plov til marken", pair.implement.id, "plovNear")
|
|
||||||
check("par: nærmeste stærke traktor til ploven", pair.tractor.id, "mid")
|
|
||||||
local blockedPair = P.pairFor("plow", 0, 0, implements, tractors, {plovNear = true})
|
|
||||||
check("par: reserveret plov springes over", blockedPair.implement.id, "plovFar")
|
|
||||||
check("par: plov uden hk-krav tager nærmeste traktor", blockedPair.tractor.id, "big")
|
|
||||||
local none, why = P.pairFor("seeder", 0, 0, implements, tractors, {})
|
|
||||||
check("par: ingen såmaskine", none, nil); check("par: grund", why, "noPoolImplement")
|
|
||||||
local weak, weakWhy, weakArgs = P.pairFor("plow", 0, 0, {implements[2]}, {tractors[1]}, {})
|
|
||||||
check("par: for svag", weak, nil); check("par: grund tooWeak", weakWhy, "tooWeak"); check("par: hk i args", weakArgs[1], 180)
|
|
||||||
local noTractor, noTractorWhy = P.pairFor("plow", 0, 0, implements, {}, {})
|
|
||||||
check("par: ingen traktor", noTractor, nil); check("par: grund noPoolTractor", noTractorWhy, "noPoolTractor")
|
|
||||||
local rejected, rejectedWhy = P.pairFor("plow", 0, 0, implements, tractors, {}, function() return false end)
|
|
||||||
check("par: accept afviser alle", rejected, nil); check("par: grund", rejectedWhy, "noPoolImplement")
|
|
||||||
local multi = P.pairFor("fertilizer", 0, 0, {{id = "spreder", role = "fertilizer", roles = {fertilizer = true, liming = true}, x = 0, z = 0}}, tractors, {})
|
|
||||||
check("par: redskab med flere roller", multi.implement.id, "spreder")
|
|
||||||
-- reservationer og karantæne
|
|
||||||
P.reserve("F12", {"plovNear", "mid"})
|
|
||||||
check("reserveret: redskab", P.blocked(0).plovNear, true)
|
|
||||||
check("reserveret: traktor", P.blocked(0).mid, true)
|
|
||||||
P.release("F12")
|
|
||||||
check("frigivet", P.blocked(0).plovNear, nil)
|
|
||||||
P.quarantine("plovNear", 1000)
|
|
||||||
check("karantæne: aktiv", P.blocked(1000 + P.QUARANTINE_MS - 1).plovNear, true)
|
|
||||||
check("karantæne: udløbet", P.blocked(1000 + P.QUARANTINE_MS).plovNear, nil)
|
|
||||||
-- kø: længst ventende først, lige -> lavest mark-id, ikke-ventende bagerst i oprindelig rækkefølge
|
|
||||||
local infos = {{id = 5}, {id = 3}, {id = 9}, {id = 1}}
|
|
||||||
local ordered = P.queueOrder(infos, {[9] = 100, [3] = 50, [1] = 50})
|
|
||||||
check("kø: 1.", ordered[1].id, 1); check("kø: 2.", ordered[2].id, 3); check("kø: 3.", ordered[3].id, 9); check("kø: 4.", ordered[4].id, 5)
|
|
||||||
check("kø: rører ikke input", infos[1].id, 5)
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Run test to verify it fails**
|
|
||||||
|
|
||||||
Run: `luajit tests/test_adPool.lua`
|
|
||||||
Expected: fejl `cannot open FS25_ADSmartPickup/adPool.lua`
|
|
||||||
|
|
||||||
- [ ] **Step 3: Write minimal implementation**
|
|
||||||
|
|
||||||
Create `FS25_ADSmartPickup/adPool.lua`:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- AD Pool
|
|
||||||
-- Maskinpulje: plove, kultivatorer, såmaskiner … holder frakoblet på deres AD-parkering "X", og en ledig
|
|
||||||
-- solo-traktor henter dem via markøren "Connect X", når markarbejdet mangler en færdig kombination.
|
|
||||||
-- Ren logik øverst (testes med luajit), spil-adapter nederst.
|
|
||||||
|
|
||||||
ADPool = {}
|
|
||||||
ADPool.LOG_PREFIX = "ADSmartPickup: pulje: "
|
|
||||||
ADPool.QUARANTINE_MS = 600000 -- redskab der ikke kunne kobles på, springes over så længe
|
|
||||||
|
|
||||||
-- ---- ren logik ----
|
|
||||||
|
|
||||||
-- Rolle for et frakoblet redskab ud fra dets egne specs (samme prioritet som markflåden); en ren vogn med
|
|
||||||
-- tip/snegl er tømmevogn.
|
|
||||||
function ADPool.roleOf(specs)
|
|
||||||
local role = ADFieldFleet.roleFromSpecs(specs)
|
|
||||||
if role ~= nil and role ~= "unloader" then return role end
|
|
||||||
if specs.spec_trailer and specs.spec_dischargeable then return "unloader" end
|
|
||||||
return role
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Butiksdata er enten et tal eller en tabel pr. konfiguration: største tal, ellers nil.
|
|
||||||
function ADPool.numberOf(value)
|
|
||||||
if type(value) == "number" then return value end
|
|
||||||
if type(value) ~= "table" then return nil end
|
|
||||||
local best = nil
|
|
||||||
for _, entry in pairs(value) do
|
|
||||||
if type(entry) == "number" and (best == nil or entry > best) then best = entry end
|
|
||||||
end
|
|
||||||
return best
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Mangler et af tallene, tillades det (intet må gå i stå på manglende data).
|
|
||||||
function ADPool.powerOk(tractorHp, neededHp)
|
|
||||||
if tractorHp == nil or neededHp == nil or tractorHp <= 0 or neededHp <= 0 then return true end
|
|
||||||
return tractorHp >= neededHp
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Markøren "Connect <parkering>" for en parkering (navneregel).
|
|
||||||
function ADPool.connectFor(parkName, markers)
|
|
||||||
if type(parkName) ~= "string" then return nil end
|
|
||||||
local wanted = parkName:lower()
|
|
||||||
for _, marker in pairs(markers or {}) do
|
|
||||||
local action, target = ADHitch.parseMarker(marker.name)
|
|
||||||
if action == "attach" and target ~= nil and target:lower() == wanted then return marker end
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local function hasRole(item, role)
|
|
||||||
return item.role == role or (item.roles ~= nil and item.roles[role] == true)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function distanceSq(ax, az, bx, bz)
|
|
||||||
return (ax - bx) ^ 2 + (az - bz) ^ 2
|
|
||||||
end
|
|
||||||
|
|
||||||
local function nearest(items, x, z, keep)
|
|
||||||
local best, bestDistance = nil, math.huge
|
|
||||||
for _, item in ipairs(items) do
|
|
||||||
if keep(item) then
|
|
||||||
local distance = distanceSq(item.x, item.z, x, z)
|
|
||||||
if distance < bestDistance then best, bestDistance = item, distance end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return best
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Redskab med rollen nærmest marken, derefter nærmeste ledige traktor med nok hk til redskabet.
|
|
||||||
-- blocked = {[id] = true} (reserverede og redskaber i karantæne). accept (valgfri): implement -> bool.
|
|
||||||
function ADPool.pairFor(role, fieldX, fieldZ, implements, tractors, blocked, accept)
|
|
||||||
blocked = blocked or {}
|
|
||||||
local free = {}
|
|
||||||
for _, tractor in ipairs(tractors or {}) do
|
|
||||||
if not blocked[tractor.id] then table.insert(free, tractor) end
|
|
||||||
end
|
|
||||||
local candidates = {}
|
|
||||||
for _, implement in ipairs(implements or {}) do
|
|
||||||
if hasRole(implement, role) and not blocked[implement.id] and (accept == nil or accept(implement)) then
|
|
||||||
table.insert(candidates, implement)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if #candidates == 0 then return nil, "noPoolImplement" end
|
|
||||||
if #free == 0 then return nil, "noPoolTractor" end
|
|
||||||
table.sort(candidates, function(a, b)
|
|
||||||
return distanceSq(a.x, a.z, fieldX, fieldZ) < distanceSq(b.x, b.z, fieldX, fieldZ)
|
|
||||||
end)
|
|
||||||
local weakest = nil
|
|
||||||
for _, implement in ipairs(candidates) do
|
|
||||||
local tractor = nearest(free, implement.x, implement.z, function(t) return ADPool.powerOk(t.hp, implement.neededHp) end)
|
|
||||||
if tractor ~= nil then return {implement = implement, tractor = tractor} end
|
|
||||||
if weakest == nil or (implement.neededHp or 0) < weakest then weakest = implement.neededHp end
|
|
||||||
end
|
|
||||||
return nil, "tooWeak", {weakest or 0}
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Ventende marker først (længst ventende først, lige -> lavest id); resten bagefter i oprindelig rækkefølge.
|
|
||||||
function ADPool.queueOrder(infos, waitingSince)
|
|
||||||
local waiting, rest = {}, {}
|
|
||||||
for _, info in ipairs(infos or {}) do
|
|
||||||
if waitingSince ~= nil and waitingSince[info.id] ~= nil then table.insert(waiting, info) else table.insert(rest, info) end
|
|
||||||
end
|
|
||||||
table.sort(waiting, function(a, b)
|
|
||||||
local wa, wb = waitingSince[a.id], waitingSince[b.id]
|
|
||||||
if wa ~= wb then return wa < wb end
|
|
||||||
return a.id < b.id
|
|
||||||
end)
|
|
||||||
for _, info in ipairs(rest) do table.insert(waiting, info) end
|
|
||||||
return waiting
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Reservationer (i hukommelsen): owner -> {id, ...}. Karantæne: id -> udløbstid.
|
|
||||||
local reservations, quarantined = {}, {}
|
|
||||||
|
|
||||||
function ADPool.reserve(owner, ids)
|
|
||||||
reservations[owner] = ids
|
|
||||||
end
|
|
||||||
|
|
||||||
function ADPool.release(owner)
|
|
||||||
reservations[owner] = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function ADPool.quarantine(id, nowMs)
|
|
||||||
quarantined[id] = nowMs + ADPool.QUARANTINE_MS
|
|
||||||
end
|
|
||||||
|
|
||||||
function ADPool.blocked(nowMs)
|
|
||||||
local result = {}
|
|
||||||
for _, ids in pairs(reservations) do
|
|
||||||
for _, id in ipairs(ids) do result[id] = true end
|
|
||||||
end
|
|
||||||
for id, untilMs in pairs(quarantined) do
|
|
||||||
if nowMs < untilMs then result[id] = true end
|
|
||||||
end
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4: Run test to verify it passes**
|
|
||||||
|
|
||||||
Run: `luajit tests/test_adPool.lua`
|
|
||||||
Expected: sidste linje `ALLE TESTS OK`
|
|
||||||
|
|
||||||
- [ ] **Step 5: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add FS25_ADSmartPickup/adPool.lua tests/test_adPool.lua
|
|
||||||
git commit -m "feat(pulje): ren planlægning — rolle, hk-tjek, Connect-navneregel, parring, kø, reservation/karantæne"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 2: `adPool.lua` — spil-adapter (indsamling af puljen)
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `FS25_ADSmartPickup/adPool.lua` (tilføj nederst)
|
|
||||||
- Modify: `FS25_ADSmartPickup/modDesc.xml` (registrér `adPool.lua` efter `adHitch.lua`)
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Consumes: `ADCpBridge.getSpecs(vehicle)` (adCpBridge.lua:153), `ADCpBridge.getWorkWidth(vehicle)` (:363), `ADRunsController.getFarmVehicles(adEnv)`, `ADRunsController.getRunForVehicle(vehicle)`, `ADRunsController.getVehicleId(vehicle)`, `ADRuns.vehicleLabel(vehicle)`, `ADFieldFleet.isEnabled(mode, fleet, id)`, `ADFieldJobs.state`, `ADHitch.rearImplementsOf(vehicle)` (Task 3), `ADPool.isReturning(vehicle)` (Task 3), `adEnv.ADGraphManager:getMapMarkers()`, `:getMapMarkerById(index)`, `g_currentMission.vehicleSystem.vehicles`.
|
|
||||||
- Produces: `ADPool.collect(adEnv, isBusy) -> implements, tractors` (formater som Task 1; implement har også `.vehicle`, tractor har `.vehicle`); `isBusy(vehicle) -> bool` gives af kalderen (markarbejdets optaget-regel).
|
|
||||||
|
|
||||||
- [ ] **Step 1: Tilføj adapteren**
|
|
||||||
|
|
||||||
Tilføj nederst i `FS25_ADSmartPickup/adPool.lua`:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- ---- spil ----
|
|
||||||
|
|
||||||
local function parkMarker(adEnv, vehicle)
|
|
||||||
local index = vehicle.advd ~= nil and vehicle.advd.parkDestination or nil
|
|
||||||
if type(index) ~= "number" or index < 1 then return nil end
|
|
||||||
return adEnv.ADGraphManager:getMapMarkerById(index)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function positionOf(vehicle)
|
|
||||||
local x, _, z = getWorldTranslation(vehicle.rootNode)
|
|
||||||
return x, z
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Butiksdata: redskabets krævede effekt (hk). specs indlæses først, når butikken har vist varen.
|
|
||||||
local function neededHpOf(vehicle)
|
|
||||||
if g_storeManager == nil or vehicle.configFileName == nil then return nil end
|
|
||||||
local item = g_storeManager:getItemByXMLFilename(vehicle.configFileName)
|
|
||||||
if item == nil then return nil end
|
|
||||||
if item.specs == nil and StoreItemUtil ~= nil and StoreItemUtil.loadSpecsFromXML ~= nil then
|
|
||||||
pcall(StoreItemUtil.loadSpecsFromXML, item)
|
|
||||||
end
|
|
||||||
return item.specs ~= nil and ADPool.numberOf(item.specs.neededPower) or nil
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Traktorens effekt i hk: motorens topeffekt (kW) omregnet; ellers butiksdata.
|
|
||||||
local function tractorHpOf(vehicle)
|
|
||||||
local motor = vehicle.spec_motorized ~= nil and vehicle.spec_motorized.motor or nil
|
|
||||||
local kw = motor ~= nil and motor.peakMotorPower or nil
|
|
||||||
if type(kw) == "number" and kw > 0 then return kw * 1.35962 end
|
|
||||||
if g_storeManager == nil or vehicle.configFileName == nil then return nil end
|
|
||||||
local item = g_storeManager:getItemByXMLFilename(vehicle.configFileName)
|
|
||||||
return item ~= nil and item.specs ~= nil and ADPool.numberOf(item.specs.power) or nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local function isAttached(vehicle)
|
|
||||||
return vehicle.getAttacherVehicle ~= nil and vehicle:getAttacherVehicle() ~= nil
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Puljeredskaber (frakoblet, parkering "X", markør "Connect X") og puljetraktorer.
|
|
||||||
-- isBusy(vehicle): markarbejdets optaget-regel (opgave, AD/CP kører, lånt af marksilo, på hjemtur).
|
|
||||||
function ADPool.collect(adEnv, isBusy)
|
|
||||||
local markers = adEnv.ADGraphManager:getMapMarkers()
|
|
||||||
local farmId = g_currentMission:getFarmId()
|
|
||||||
local implements, tractors = {}, {}
|
|
||||||
-- spillets egen liste: frakoblede redskaber er ikke nødvendigvis i AutoDrive's køretøjsliste
|
|
||||||
for _, vehicle in ipairs(g_currentMission.vehicleSystem.vehicles) do
|
|
||||||
local owned = vehicle.getOwnerFarmId ~= nil and vehicle:getOwnerFarmId() == farmId
|
|
||||||
if owned and vehicle.spec_motorized == nil and vehicle.spec_attachable ~= nil and not isAttached(vehicle) then
|
|
||||||
local park = parkMarker(adEnv, vehicle)
|
|
||||||
local connect = park ~= nil and ADPool.connectFor(park.name, markers) or nil
|
|
||||||
local specs = connect ~= nil and ADCpBridge.getSpecs(vehicle) or nil
|
|
||||||
local role = specs ~= nil and ADPool.roleOf(specs) or nil
|
|
||||||
if role ~= nil then
|
|
||||||
local x, z = positionOf(vehicle)
|
|
||||||
table.insert(implements, {
|
|
||||||
id = ADRunsController.getVehicleId(vehicle), vehicle = vehicle, name = ADRuns.vehicleLabel(vehicle),
|
|
||||||
role = role, roles = specs.sprayRoles, x = x, z = z, width = ADCpBridge.getWorkWidth(vehicle),
|
|
||||||
neededHp = neededHpOf(vehicle), connectId = connect.id, parkId = park.id,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local state = ADFieldJobs.state
|
|
||||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
|
||||||
local id = ADRunsController.getVehicleId(vehicle)
|
|
||||||
local park = parkMarker(adEnv, vehicle)
|
|
||||||
local controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled()
|
|
||||||
if id ~= nil and park ~= nil and vehicle.spec_attacherJoints ~= nil and #ADHitch.rearImplementsOf(vehicle) == 0
|
|
||||||
and ADRunsController.getRunForVehicle(vehicle) == nil and not controlled and not isBusy(vehicle)
|
|
||||||
and not ADPool.isReturning(vehicle) and ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id) then
|
|
||||||
local x, z = positionOf(vehicle)
|
|
||||||
table.insert(tractors, {id = id, vehicle = vehicle, name = ADRuns.vehicleLabel(vehicle), x = x, z = z,
|
|
||||||
hp = tractorHpOf(vehicle), parkId = park.id})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return implements, tractors
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Registrér modulet**
|
|
||||||
|
|
||||||
I `FS25_ADSmartPickup/modDesc.xml` efter linjen `<sourceFile filename="adHitch.lua"/>`:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<sourceFile filename="adPool.lua"/>
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Syntakstjek og tests**
|
|
||||||
|
|
||||||
Run: `luajit -bl FS25_ADSmartPickup/adPool.lua >/dev/null && echo syntaks-ok && luajit tests/test_adPool.lua | tail -1`
|
|
||||||
Expected: `syntaks-ok` og `ALLE TESTS OK`
|
|
||||||
|
|
||||||
- [ ] **Step 4: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add FS25_ADSmartPickup/adPool.lua FS25_ADSmartPickup/modDesc.xml
|
|
||||||
git commit -m "feat(pulje): spil-adapter — frakoblede redskaber med parkering+Connect og ledige solo-traktorer med hk"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 3: `adHitch.lua` — hjemtur og "på hjemtur"-status
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `FS25_ADSmartPickup/adHitch.lua`
|
|
||||||
- Modify: `FS25_ADSmartPickup/adPool.lua` (isReturning-register)
|
|
||||||
- Test: `tests/test_adHitch.lua`, `tests/test_adPool.lua`
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Consumes: `ADHitch.route(adEnv, vehicle, markerIds, onDone)` (findes), `attachedImplements`/`rearImplements` (findes i adHitch.lua).
|
|
||||||
- Produces:
|
|
||||||
- `ADHitch.homeRoute(implementParkIds, tractorParkId) -> {id, ...}` (ren)
|
|
||||||
- `ADHitch.rearImplementsOf(vehicle) -> {{object, z, name}}` (offentlig udgave af den lokale)
|
|
||||||
- `ADHitch.returnHome(adEnv, tractor, onDone) -> bool` (false = intet puljeredskab bag / ingen parkering)
|
|
||||||
- `ADPool.setReturning(vehicle, isOn)`, `ADPool.isReturning(vehicle) -> bool`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Write the failing tests**
|
|
||||||
|
|
||||||
Tilføj i `tests/test_adHitch.lua` før `print(failures == 0`:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- hjemtur: hvert puljeredskabs parkering (afkobles der), så traktorens egen parkering
|
|
||||||
local home = H.homeRoute({4}, 2)
|
|
||||||
check("hjem: to trin", #home, 2); check("hjem: redskab først", home[1], 4); check("hjem: traktor sidst", home[2], 2)
|
|
||||||
check("hjem: uden redskab kun traktor", #H.homeRoute({}, 2), 1)
|
|
||||||
check("hjem: samme parkering to gange tæller én", #H.homeRoute({4, 4}, 2), 2)
|
|
||||||
check("hjem: uden traktorparkering", #H.homeRoute({4}, nil), 1)
|
|
||||||
```
|
|
||||||
|
|
||||||
Tilføj i `tests/test_adPool.lua` før `print(failures == 0`:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- på hjemtur: optaget indtil hjemme
|
|
||||||
local tractorObject = {}
|
|
||||||
check("hjemtur: ikke fra start", P.isReturning(tractorObject), false)
|
|
||||||
P.setReturning(tractorObject, true)
|
|
||||||
check("hjemtur: markeret", P.isReturning(tractorObject), true)
|
|
||||||
P.setReturning(tractorObject, false)
|
|
||||||
check("hjemtur: hjemme", P.isReturning(tractorObject), false)
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Run tests to verify they fail**
|
|
||||||
|
|
||||||
Run: `luajit tests/test_adHitch.lua | tail -3; luajit tests/test_adPool.lua | tail -3`
|
|
||||||
Expected: fejl `attempt to call field 'homeRoute' (a nil value)` og `attempt to call field 'isReturning'`
|
|
||||||
|
|
||||||
- [ ] **Step 3: Implement**
|
|
||||||
|
|
||||||
I `FS25_ADSmartPickup/adPool.lua`, efter `ADPool.blocked`:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- Traktorer på hjemtur (sætter redskab af, kører hjem): optaget for markarbejdet til de er hjemme.
|
|
||||||
local returning = setmetatable({}, {__mode = "k"})
|
|
||||||
|
|
||||||
function ADPool.setReturning(vehicle, isOn)
|
|
||||||
returning[vehicle] = isOn and true or nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function ADPool.isReturning(vehicle)
|
|
||||||
return returning[vehicle] == true
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
I `FS25_ADSmartPickup/adHitch.lua`, i den rene del (efter `ADHitch.waitAfter`):
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- Hjemtur: hvert puljeredskabs parkering (der sættes det af), så traktorens egen parkering.
|
|
||||||
function ADHitch.homeRoute(implementParkIds, tractorParkId)
|
|
||||||
local route, seen = {}, {}
|
|
||||||
for _, id in ipairs(implementParkIds or {}) do
|
|
||||||
if not seen[id] then seen[id] = true table.insert(route, id) end
|
|
||||||
end
|
|
||||||
if tractorParkId ~= nil then table.insert(route, tractorParkId) end
|
|
||||||
return route
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
I spil-delen af `adHitch.lua`, lige efter `local function isImplementPark(...) ... end`:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
function ADHitch.rearImplementsOf(vehicle)
|
|
||||||
return ADHitch.rearImplements(attachedImplements(vehicle))
|
|
||||||
end
|
|
||||||
|
|
||||||
local function parkIdOf(adEnv, vehicle)
|
|
||||||
local index = vehicle.advd ~= nil and vehicle.advd.parkDestination or nil
|
|
||||||
local marker = type(index) == "number" and index >= 1 and adEnv.ADGraphManager:getMapMarkerById(index) or nil
|
|
||||||
return marker ~= nil and marker.id or nil
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
Efter `function ADHitch.route(...) ... end`:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- Puljeredskaber bag traktoren sættes af på deres parkering, og traktoren kører hjem til sin egen.
|
|
||||||
-- Kun redskaber med en Connect-markør til deres parkering er puljeredskaber; andre bliver siddende.
|
|
||||||
function ADHitch.returnHome(adEnv, tractor, onDone)
|
|
||||||
local markers = adEnv.ADGraphManager:getMapMarkers()
|
|
||||||
local parkIds = {}
|
|
||||||
for _, implement in ipairs(ADHitch.rearImplementsOf(tractor)) do
|
|
||||||
local parkId = parkIdOf(adEnv, implement.object)
|
|
||||||
local park = parkId ~= nil and adEnv.ADGraphManager:getMapMarkerByWayPointId(parkId) or nil
|
|
||||||
if park ~= nil and ADPool.connectFor(park.name, markers) ~= nil then table.insert(parkIds, parkId) end
|
|
||||||
end
|
|
||||||
if #parkIds == 0 then return false end
|
|
||||||
local route = ADHitch.homeRoute(parkIds, parkIdOf(adEnv, tractor))
|
|
||||||
ADPool.setReturning(tractor, true)
|
|
||||||
ADHitch.route(adEnv, tractor, route, function(vehicle, ok, detail)
|
|
||||||
ADPool.setReturning(vehicle, false)
|
|
||||||
if onDone ~= nil then onDone(vehicle, ok, detail) end
|
|
||||||
end)
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4: Run tests to verify they pass**
|
|
||||||
|
|
||||||
Run: `luajit tests/test_adHitch.lua | tail -1; luajit tests/test_adPool.lua | tail -1; luajit -bl FS25_ADSmartPickup/adHitch.lua >/dev/null && echo syntaks-ok`
|
|
||||||
Expected: `ALLE TESTS OK` to gange og `syntaks-ok`
|
|
||||||
|
|
||||||
- [ ] **Step 5: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add FS25_ADSmartPickup/adHitch.lua FS25_ADSmartPickup/adPool.lua tests/test_adHitch.lua tests/test_adPool.lua
|
|
||||||
git commit -m "feat(pulje): hjemtur — puljeredskaber sættes af på deres parkering, traktoren kører til sin egen; optaget imens"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 4: Markarbejde — puljen som fallback og fasen `fetching`
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `FS25_ADSmartPickup/adFieldWork.lua` (`getRigs` :132-170, `dispatch` :382-482, `follow` :1083-1143, `release` :278-283)
|
|
||||||
- Modify: `FS25_ADSmartPickup/l10n/l10n_da.xml`, `FS25_ADSmartPickup/l10n/l10n_en.xml`
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Consumes: `ADCpBridge.getSeedFruits/hasSeeds/isMechanicalWeeder(vehicle)`, `ADPool.collect(adEnv, isBusy)`, `ADPool.pairFor(...)`, `ADPool.blocked(nowMs)`, `ADPool.reserve/release/quarantine`, `ADPool.isReturning`, `ADHitch.fetch(adEnv, vehicle, markerId, onDone)`, `ADHitch.returnHome`.
|
|
||||||
- Produces: assignment-felter `pool = {implementId, implementName, connectId}`, `phase = "fetching"`, `hitch = {done=bool, ok=bool, detail=any}`; field-årsager `spu_field_reason_noPoolTractor`, `spu_field_reason_fetching` (tooWeak som fritekst).
|
|
||||||
|
|
||||||
- [ ] **Step 1: Optaget-regel genbrugelig + hjemtur tæller som optaget**
|
|
||||||
|
|
||||||
I `adFieldWork.lua` før `function ADFieldWork.getRigs`:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- Optaget for markarbejdet: vores egen opgave, AutoDrive/Courseplay kører den, lånt af marksilo-flowet,
|
|
||||||
-- eller på hjemtur fra puljen (sætter redskab af).
|
|
||||||
local function isBusyVehicle(vehicle, id)
|
|
||||||
return ADFieldJobs.state.assignments[id] ~= nil or isAdActive(vehicle) or ADCpBridge.isWorking(vehicle)
|
|
||||||
or (ADFieldSilos ~= nil and ADFieldSilos.isLoaned(vehicle)) or (ADPool ~= nil and ADPool.isReturning(vehicle))
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
I `getRigs` erstat `busy = state.assignments[id] ~= nil or isAdActive(vehicle) or ADCpBridge.isWorking(vehicle)\n or (ADFieldSilos ~= nil and ADFieldSilos.isLoaned(vehicle)),` med:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
busy = isBusyVehicle(vehicle, id),
|
|
||||||
```
|
|
||||||
|
|
||||||
og tilføj som sidste alternativ i `busyWhy`-kæden (før `or nil`):
|
|
||||||
|
|
||||||
```lua
|
|
||||||
or (ADPool ~= nil and ADPool.isReturning(vehicle) and "sætter redskab af") or nil,
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Fallback i dispatch**
|
|
||||||
|
|
||||||
I `ADFieldWork.dispatch`, lige efter `if rigs == nil then` og FØR `if #offNetwork > 0 ...`, indsæt:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- ingen færdig kombination: en ledig solo-traktor henter et puljeredskab (kun hovedopgaver, ikke tømmevogne)
|
|
||||||
if not decision.helper and (decision.count or 1) == 1 and ADPool ~= nil
|
|
||||||
and (why == "noRig" or why == "allBusy" or why == "cannotDo") then
|
|
||||||
local pooled, poolWhy, poolArgs = ADFieldWork.dispatchPool(adEnv, field, info, role, roleAccept)
|
|
||||||
if pooled then return end
|
|
||||||
if poolWhy == "tooWeak" or (poolWhy == "noPoolTractor" and why ~= "allBusy") then
|
|
||||||
why, args = poolWhy, poolArgs
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
Tilføj før `function ADFieldWork.dispatch` en ny funktion:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- Puljekombination: nærmeste puljeredskab til marken + nærmeste ledige traktor med nok hk. Traktoren kører til
|
|
||||||
-- "Connect X", kobler på og fortsætter så som en almindelig udsendelse (fase fetching -> driving).
|
|
||||||
function ADFieldWork.dispatchPool(adEnv, field, info, role, roleAccept)
|
|
||||||
local implements, tractors = ADPool.collect(adEnv, function(vehicle)
|
|
||||||
return isBusyVehicle(vehicle, ADRunsController.getVehicleId(vehicle))
|
|
||||||
end)
|
|
||||||
local accept = function(implement)
|
|
||||||
-- samme felter som getRigs giver sæt, så markens krav (frø, afgrøde, ukrudt) tjekkes på redskabet selv
|
|
||||||
local candidate = {role = implement.role, roles = implement.roles, vehicle = implement.vehicle}
|
|
||||||
if role == "seeder" then
|
|
||||||
candidate.seedFruits = ADCpBridge.getSeedFruits(implement.vehicle)
|
|
||||||
candidate.hasSeeds = ADCpBridge.hasSeeds(implement.vehicle)
|
|
||||||
end
|
|
||||||
if role == "weeder" then candidate.mechanical = ADCpBridge.isMechanicalWeeder(implement.vehicle) end
|
|
||||||
if roleAccept ~= nil and not roleAccept(candidate) then return false end
|
|
||||||
local required = ADFieldModel.requiredWidth(field, role)
|
|
||||||
return (implement.width or 0) <= 0 or ADFieldModel.checkWidth(role, implement.width, required)
|
|
||||||
end
|
|
||||||
local pair, why, args = ADPool.pairFor(role, info.x, info.z, implements, tractors, ADPool.blocked(now()), accept)
|
|
||||||
if pair == nil then return false, why, args end
|
|
||||||
local tractor, implement = pair.tractor, pair.implement
|
|
||||||
local assignment = {
|
|
||||||
vehicleId = tractor.id, vehicle = tractor.vehicle, fieldId = field.id, role = role, stage = field.stage,
|
|
||||||
helper = false, phase = "fetching", since = now(), width = implement.width, groupSize = 1, leader = true,
|
|
||||||
startX = tractor.x, startZ = tractor.z,
|
|
||||||
pool = {implementId = implement.id, implementName = implement.name, connectId = implement.connectId},
|
|
||||||
hitch = {done = false},
|
|
||||||
}
|
|
||||||
ADFieldJobs.state.assignments[tractor.id] = assignment
|
|
||||||
ADPool.reserve(assignment, {implement.id, tractor.id})
|
|
||||||
ADHitch.fetch(adEnv, tractor.vehicle, implement.connectId, function(_, ok, detail)
|
|
||||||
assignment.hitch = {done = true, ok = ok, detail = detail}
|
|
||||||
end)
|
|
||||||
note(field.id, {reason = {key = "spu_field_reason_fetching", name = implement.name}})
|
|
||||||
log(field.id, "%s: '%s' henter '%s' (%s)", role, tractor.name, implement.name, "pulje")
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Frigiv reservation ved release**
|
|
||||||
|
|
||||||
I `ADFieldWork.release`, før `ADFieldJobs.state.assignments[assignment.vehicleId] = nil`:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
if ADPool ~= nil and assignment.pool ~= nil then ADPool.release(assignment) end
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4: Fasen fetching i follow**
|
|
||||||
|
|
||||||
I `ADFieldWork.follow`, efter linjen `wake(vehicle)` og før `if assignment.phase == "driving" then`, gør `if` til en kæde ved at indsætte:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
if assignment.phase == "fetching" then
|
|
||||||
local hitch = assignment.hitch
|
|
||||||
if not hitch.done then return end
|
|
||||||
ADPool.release(assignment)
|
|
||||||
if not hitch.ok then
|
|
||||||
-- koblingen fejlede: redskabet i karantæne, traktoren hjem, marken prøver igen
|
|
||||||
ADPool.quarantine(assignment.pool.implementId, now())
|
|
||||||
ADFieldWork.release(assignment)
|
|
||||||
note(assignment.fieldId, {retryAt = now() + ADFieldWork.NO_RIG_RETRY_MS,
|
|
||||||
reason = {text = string.format("kunne ikke koble '%s' på (%s) — prøver et andet om lidt",
|
|
||||||
tostring(assignment.pool.implementName), tostring(hitch.detail))}})
|
|
||||||
local park = parkMarkerId(adEnv, vehicle)
|
|
||||||
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
|
||||||
return
|
|
||||||
end
|
|
||||||
-- koblet på: bredden med redskabet på (for bred -> hjem igen)
|
|
||||||
local width = ADCpBridge.getWorkWidth(vehicle)
|
|
||||||
local okWidth, widthReason, widthArgs = ADFieldModel.checkWidth(assignment.role, width, ADFieldModel.requiredWidth(field, assignment.role))
|
|
||||||
if not okWidth then
|
|
||||||
ADFieldWork.release(assignment)
|
|
||||||
note(assignment.fieldId, {retryAt = now() + ADFieldModel.RETRY_MS, reason = {key = "spu_field_reason_" .. tostring(widthReason), role = assignment.role, args = widthArgs}})
|
|
||||||
ADHitch.returnHome(adEnv, vehicle)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
assignment.width, assignment.phase, assignment.since = width, "driving", now()
|
|
||||||
assignment.startX, assignment.startZ = vehiclePosition(vehicle)
|
|
||||||
startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, info.markerId, nil)
|
|
||||||
assignment.drive = {mode = adEnv.AutoDrive.MODE_DRIVETO, first = info.markerId}
|
|
||||||
note(assignment.fieldId, {reason = false})
|
|
||||||
log(assignment.fieldId, "%s '%s' har '%s' på — kører til %s (%.1f m)", assignment.role, ADRuns.vehicleLabel(vehicle),
|
|
||||||
tostring(assignment.pool.implementName), tostring(info.markerName), width or 0)
|
|
||||||
elseif assignment.phase == "driving" then
|
|
||||||
```
|
|
||||||
|
|
||||||
(og fjern det oprindelige `if assignment.phase == "driving" then`, som nu er `elseif`.)
|
|
||||||
|
|
||||||
Tilføj også `fetching` til spiller-reglen øverst i `follow`, så AD's kørsel under hentning ikke tolkes som spillerens: `steeredByAi` er allerede sand, mens AD kører; ingen ændring nødvendig.
|
|
||||||
|
|
||||||
- [ ] **Step 5: Tekster**
|
|
||||||
|
|
||||||
`l10n/l10n_da.xml` (ved de andre `spu_field_reason_`):
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<text name="spu_field_reason_noPoolTractor" text="ingen ledig traktor til at hente et redskab til %s"/>
|
|
||||||
<text name="spu_field_reasonShort_noPoolTractor" text="venter på traktor"/>
|
|
||||||
<text name="spu_field_reason_fetching" text="henter '%s'"/>
|
|
||||||
<text name="spu_field_reasonShort_fetching" text="henter redskab"/>
|
|
||||||
```
|
|
||||||
|
|
||||||
`l10n/l10n_en.xml`:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<text name="spu_field_reason_noPoolTractor" text="no free tractor to fetch an implement for %s"/>
|
|
||||||
<text name="spu_field_reasonShort_noPoolTractor" text="waiting for tractor"/>
|
|
||||||
<text name="spu_field_reason_fetching" text="fetching '%s'"/>
|
|
||||||
<text name="spu_field_reasonShort_fetching" text="fetching implement"/>
|
|
||||||
```
|
|
||||||
|
|
||||||
`tooWeak` bruger fritekst (hk står før rollen, og `fieldReasonText` sætter rollen først): i Step 2 erstattes linjen
|
|
||||||
`why, args = poolWhy, poolArgs` med:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
if poolWhy == "tooWeak" then
|
|
||||||
note(field.id, {reason = {text = string.format("ingen ledig traktor med ≥%.0f hk til %s-redskabet",
|
|
||||||
(poolArgs or {})[1] or 0, tostring(role))}, retryAt = now() + ADFieldWork.NO_RIG_RETRY_MS})
|
|
||||||
return
|
|
||||||
end
|
|
||||||
why, args = poolWhy, poolArgs
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 6: Syntakstjek og alle tests**
|
|
||||||
|
|
||||||
Run: `luajit -bl FS25_ADSmartPickup/adFieldWork.lua >/dev/null && echo syntaks-ok; for f in tests/test_*.lua; do luajit $f >/dev/null 2>&1 || echo "FEJL $f"; done; echo færdig`
|
|
||||||
Expected: `syntaks-ok`, ingen `FEJL`-linjer, `færdig`
|
|
||||||
|
|
||||||
- [ ] **Step 7: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add FS25_ADSmartPickup/adFieldWork.lua FS25_ADSmartPickup/l10n/l10n_da.xml FS25_ADSmartPickup/l10n/l10n_en.xml
|
|
||||||
git commit -m "feat(pulje): markarbejdet sender en solo-traktor efter et puljeredskab når ingen færdig kombination er ledig (fase fetching)"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 5: Markarbejde — hjemtur når opgaven slutter
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `FS25_ADSmartPickup/adFieldWork.lua` (`finishAssignment` :834-862, fejlvejen i `follow` "nåede ikke" :1115-1120)
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Consumes: `ADHitch.returnHome(adEnv, tractor, onDone) -> bool`.
|
|
||||||
- Produces: ingen nye navne.
|
|
||||||
|
|
||||||
- [ ] **Step 1: finishAssignment**
|
|
||||||
|
|
||||||
Erstat i `finishAssignment`:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
local park = not assignment.adHandlesEnd and not assignment.manual and parkMarkerId(adEnv, vehicle) or nil
|
|
||||||
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
|
||||||
```
|
|
||||||
|
|
||||||
med:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- puljeredskab bag traktoren: sæt det af på sin parkering, traktoren hjem (afhænger kun af hvad der hænger
|
|
||||||
-- bag, så det virker også efter genindlæsning); ellers som før
|
|
||||||
local pooled = not assignment.adHandlesEnd and not assignment.manual and ADHitch ~= nil
|
|
||||||
and ADHitch.returnHome(adEnv, vehicle, function(_, ok, detail)
|
|
||||||
if not ok then log(assignment.fieldId, "'%s' kom ikke hjem fra puljen: %s", ADRuns.vehicleLabel(vehicle), tostring(detail)) end
|
|
||||||
end)
|
|
||||||
local park = not pooled and not assignment.adHandlesEnd and not assignment.manual and parkMarkerId(adEnv, vehicle) or nil
|
|
||||||
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
|
||||||
if pooled then log(assignment.fieldId, "'%s' sætter redskabet af og kører hjem", ADRuns.vehicleLabel(vehicle)) end
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: "nåede ikke frem" og "tom" — også hjem med redskabet**
|
|
||||||
|
|
||||||
I `follow`, i grenen der slutter med
|
|
||||||
|
|
||||||
```lua
|
|
||||||
local park = parkMarkerId(adEnv, vehicle)
|
|
||||||
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
|
||||||
```
|
|
||||||
|
|
||||||
erstat de to linjer med:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
if not (ADHitch ~= nil and ADHitch.returnHome(adEnv, vehicle)) then
|
|
||||||
local park = parkMarkerId(adEnv, vehicle)
|
|
||||||
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
I `finishAssignment`, i "tom såmaskine/sprøjte"-grenen efter `ADFieldWork.release(assignment)`, indsæt:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
if ADHitch ~= nil then ADHitch.returnHome(adEnv, vehicle) end
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Syntakstjek og alle tests**
|
|
||||||
|
|
||||||
Run: `luajit -bl FS25_ADSmartPickup/adFieldWork.lua >/dev/null && echo syntaks-ok; for f in tests/test_*.lua; do luajit $f >/dev/null 2>&1 || echo "FEJL $f"; done; echo færdig`
|
|
||||||
Expected: `syntaks-ok`, ingen `FEJL`, `færdig`
|
|
||||||
|
|
||||||
- [ ] **Step 4: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add FS25_ADSmartPickup/adFieldWork.lua
|
|
||||||
git commit -m "feat(pulje): når opgaven slutter (eller fejler) sættes puljeredskabet af på sin parkering og traktoren kører hjem"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 6: Kø — længst ventende mark først
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `FS25_ADSmartPickup/adFieldJobs.lua` (`stepField` dispatch-gren ~:421-426, `tick` løkke ~:486)
|
|
||||||
- Test: `tests/test_adPool.lua` (dækket af `queueOrder`-tests i Task 1)
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Consumes: `ADPool.queueOrder(infos, waitingSince)`.
|
|
||||||
- Produces: `ADFieldJobs.waitingSince = {[fieldId] = nowMs}`.
|
|
||||||
|
|
||||||
- [ ] **Step 1: Registrér ventetid**
|
|
||||||
|
|
||||||
I `adFieldJobs.lua` ved siden af `ADFieldJobs.lastDecision = {}`:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- Hvornår en mark begyndte at vente på en maskine (kø: længst ventende får næste ledige traktor).
|
|
||||||
ADFieldJobs.waitingSince = {}
|
|
||||||
```
|
|
||||||
|
|
||||||
I `stepField`, erstat dispatch-grenen:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
local before = ADFieldJobs.assignmentCount()
|
|
||||||
ADFieldWork.dispatch(adEnv, field, info, decision)
|
|
||||||
if ADFieldJobs.assignmentCount() > before then ADFieldJobs.lastDispatchAt = ADFieldJobs.now end
|
|
||||||
```
|
|
||||||
|
|
||||||
med:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
local before = ADFieldJobs.assignmentCount()
|
|
||||||
ADFieldWork.dispatch(adEnv, field, info, decision)
|
|
||||||
if ADFieldJobs.assignmentCount() > before then
|
|
||||||
ADFieldJobs.lastDispatchAt = ADFieldJobs.now
|
|
||||||
ADFieldJobs.waitingSince[info.id] = nil
|
|
||||||
elseif ADFieldJobs.waitingSince[info.id] == nil then
|
|
||||||
ADFieldJobs.waitingSince[info.id] = ADFieldJobs.now
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
I samme funktion, i grenen `decision.action == "advance"` efter `ADFieldJobs.setField(nextField)`:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
ADFieldJobs.waitingSince[info.id] = nil
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Kørsel i kø-rækkefølge**
|
|
||||||
|
|
||||||
I `ADFieldJobs.tick` erstat `for index, info in ipairs(ADFields.list()) do` med:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
local infos = ADFields.list()
|
|
||||||
if ADPool ~= nil then infos = ADPool.queueOrder(infos, ADFieldJobs.waitingSince) end
|
|
||||||
for index, info in ipairs(infos) do
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Tests**
|
|
||||||
|
|
||||||
Run: `for f in tests/test_*.lua; do luajit $f >/dev/null 2>&1 || echo "FEJL $f"; done; luajit -bl FS25_ADSmartPickup/adFieldJobs.lua >/dev/null && echo syntaks-ok`
|
|
||||||
Expected: ingen `FEJL`, `syntaks-ok`
|
|
||||||
|
|
||||||
- [ ] **Step 4: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add FS25_ADSmartPickup/adFieldJobs.lua
|
|
||||||
git commit -m "feat(pulje): marker der venter på en maskine betjenes længst-ventende først"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 7: Version, build, deploy og in-game test
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `FS25_ADSmartPickup/modDesc.xml` (`<version>1.47.4.0</version>` → `1.48.0.0`)
|
|
||||||
|
|
||||||
- [ ] **Step 1: Bump version, alle tests, build, deploy**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd ~/fs25-adsmartpickup
|
|
||||||
sed -i 's#<version>1\.47\.4\.0</version>#<version>1.48.0.0</version>#' FS25_ADSmartPickup/modDesc.xml
|
|
||||||
for f in tests/test_*.lua; do luajit $f >/dev/null 2>&1 || echo "FEJL $f"; done
|
|
||||||
rm -f FS25_ADSmartPickup.zip; (cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .)
|
|
||||||
cp FS25_ADSmartPickup.zip ~/FS25-data/mods/.FS25_ADSmartPickup.new && mv ~/FS25-data/mods/.FS25_ADSmartPickup.new ~/FS25-data/mods/FS25_ADSmartPickup.zip
|
|
||||||
unzip -p ~/FS25-data/mods/FS25_ADSmartPickup.zip modDesc.xml | grep -m1 '<version>'
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: ingen `FEJL`, `<version>1.48.0.0</version>`
|
|
||||||
|
|
||||||
- [ ] **Step 2: Commit og push**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add FS25_ADSmartPickup/modDesc.xml
|
|
||||||
git commit -m "chore: v1.48.0.0 — maskinpulje etape 1 (markarbejde)"
|
|
||||||
TOKEN=$(grep -A15 -i "^## Gitea" ~/credentials-vault.md | grep -oE "[a-f0-9]{40}" | head -1); git -c http.extraHeader="Authorization: Basic $(printf 'masterdraco:%s' "$TOKEN" | base64 -w0)" push -q gitea main
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: In-game test (savegame4, Golden Lands) — spilleren udfører, vi læser loggen**
|
|
||||||
|
|
||||||
Forudsætning: et redskab til markarbejde (fx kultivator/plov) med AD-parkering "X" og markør "Connect X" (bakkespor), Challengeren solo med egen AD-parkering, og en mark med markør der skal kultiveres/pløjes (plan auto eller Start).
|
|
||||||
|
|
||||||
Forventet i `log.txt` (grep `ADSmartPickup`):
|
|
||||||
1. `F<n>: cultivator: 'Challenger MT635' henter '<redskab>' (pulje)`
|
|
||||||
2. `kobling: 'Challenger MT635' koblede '<redskab>' på ved 'Connect X'`
|
|
||||||
3. `F<n>: cultivator 'Challenger MT635' har '<redskab>' på — kører til <markør>`
|
|
||||||
4. CP arbejder; `F<n>: cultivator '…' færdig`
|
|
||||||
5. `F<n>: '…' sætter redskabet af og kører hjem`, `kobling: … koblede '<redskab>' af ved 'X'`, `… er færdig med turen (2 trin)`
|
|
||||||
|
|
||||||
Fejlscenarie: flyt redskabet 3 m væk fra sin plads → `kunne ikke koble … på` efter 8 s, traktoren kører hjem, fanen viser årsagen, redskabet springes over i 10 min.
|
|
||||||
@@ -1,690 +0,0 @@
|
|||||||
# Flows/Fields: farvet liste + redigeringsvindue — Implementation Plan
|
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
||||||
|
|
||||||
**Goal:** Flows- og Fields-fanen viser kun en farvet liste med Start/Stop pr. række; klik på en række åbner et redigeringsvindue (panel i samme frame) med de nuværende redigeringslinjer.
|
|
||||||
|
|
||||||
**Architecture:** Rene statusfunktioner (`ADFlowView.flowStatus`, `ADFieldView.fieldStatus`) afgør niveau/farve og testes med luajit. GUI: rækkehjælperne får et mål-layout (`self.rowTarget`), så den eksisterende resumé-/guide-kode kan tegne i vinduets egen `ScrollingLayout`. Vinduet er et skjult overlay i `SmartPickupFrame.xml`, styret af en ny mixin `gui/SmartPickupEditPanel.lua`.
|
|
||||||
|
|
||||||
**Tech Stack:** FS25 Lua (GIANTS GUI: Bitmap/Text/ScrollingLayout, TabbedMenuFrameElement menuButtonInfo), luajit-tests i `tests/`.
|
|
||||||
|
|
||||||
**Spec:** `docs/superpowers/specs/2026-09-29-flow-field-list-edit-panel-design.md`
|
|
||||||
|
|
||||||
## Global Constraints
|
|
||||||
|
|
||||||
- Ingen ændring af flow-/mark-logik, guiderne eller Oversigt-tavlen/flåde-visningen.
|
|
||||||
- Rækker er ≈835 px brede (som lagervisningen).
|
|
||||||
- Farver: grøn `{0.30, 0.75, 0.35, 1}` = running, gul `{0.95, 0.80, 0.25, 1}` = waiting, rød `{0.90, 0.35, 0.30, 1}` = error, grå `{0.55, 0.55, 0.55, 1}` = stopped.
|
|
||||||
- Al GUI-kode i `self:safe(...)`; ingen utilsigtede globals (`tests/test_noStrayGlobals.lua`).
|
|
||||||
- Byg kun når alle tests er grønne: `for t in tests/test_*.lua; do luajit $t >/dev/null || echo FAIL $t; done`. Zip: `cd FS25_ADSmartPickup && zip -qr ../FS25_ADSmartPickup.zip .`, kopi til `~/FS25-data/mods/`, commit, push remote `gitea` main (Basic-auth header med token fra `~/credentials-vault.md` sektion Gitea, linje med "API-token").
|
|
||||||
- l10n: nye tekster i både `l10n/l10n_da.xml` og `l10n/l10n_en.xml`, præfiks `spu_`.
|
|
||||||
|
|
||||||
## Review Focus
|
|
||||||
|
|
||||||
- Klik på listen bag et åbent vindue: skal intet gøre (vinduet ejer klik) — pinned af `makeRowClickable`-regel i Task 2 (klik tæller kun inde i rækkens eget layout, og listen ignorerer klik når vinduet er åbent).
|
|
||||||
- ESC med vindue åbent: lukker vinduet, ikke ESC-menuen; med et [Edit]-trin åbent: lukker trinnet først — Task 3.
|
|
||||||
- Flow/mark slettet mens vinduet er åbent: vinduet lukker sig med besked — Task 3 (`refreshEditPanel`).
|
|
||||||
- Start/Stop på en række for et andet flow end det valgte: må ikke påvirke det valgte — Task 4 (handling bærer sit eget id).
|
|
||||||
- Flow startet men alle traktorer afvist ("ingen vogn"): rød, ikke grå — pinned af test i Task 1.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 1: Rene statusfunktioner for flows (+ fælles farver)
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `FS25_ADSmartPickup/adFlowView.lua` (tilføj i bunden)
|
|
||||||
- Test: `tests/test_flowStatus.lua` (ny)
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Produces: `ADFlowView.LEVEL_COLORS = {running={...}, waiting={...}, error={...}, stopped={...}}`;
|
|
||||||
`ADFlowView.flowStatus(s) -> level` hvor `s = {running=int, total=int, stopped=bool, canStop=bool, warning=string|nil, reason=string|nil, lastStartFailed=bool}` og `level ∈ {"running","waiting","error","stopped"}`.
|
|
||||||
|
|
||||||
- [ ] **Step 1: Write the failing test** — `tests/test_flowStatus.lua`:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- Kør: luajit tests/test_flowStatus.lua (fra repo-roden)
|
|
||||||
dofile("FS25_ADSmartPickup/adFlowView.lua")
|
|
||||||
local V = ADFlowView
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
check("kører", V.flowStatus({running = 3, total = 4, canStop = true}), "running")
|
|
||||||
check("kører men holder (begrundelse)", V.flowStatus({running = 2, total = 2, canStop = true, reason = "intet at hente"}), "waiting")
|
|
||||||
check("startet, venter på vogn", V.flowStatus({running = 0, total = 2, canStop = true}), "waiting")
|
|
||||||
check("advarsel", V.flowStatus({running = 1, total = 2, canStop = true, warning = "mål mangler"}), "error")
|
|
||||||
check("start afvist (ingen vogn)", V.flowStatus({running = 0, total = 2, lastStartFailed = true}), "error")
|
|
||||||
check("stoppet", V.flowStatus({running = 0, total = 2, stopped = true}), "stopped")
|
|
||||||
check("aldrig startet", V.flowStatus({running = 0, total = 2}), "stopped")
|
|
||||||
check("stoppet vinder over gammel afvisning", V.flowStatus({running = 0, total = 2, stopped = true, lastStartFailed = true}), "stopped")
|
|
||||||
check("farver findes", V.LEVEL_COLORS.waiting ~= nil and V.LEVEL_COLORS.stopped ~= nil, true)
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Run** `luajit tests/test_flowStatus.lua` — Expected: FAIL (flowStatus nil).
|
|
||||||
|
|
||||||
- [ ] **Step 3: Implement** — append to `adFlowView.lua`:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- ---- status pr. flow (Flows-listen) ----
|
|
||||||
|
|
||||||
ADFlowView.LEVEL_COLORS = {
|
|
||||||
running = {0.30, 0.75, 0.35, 1},
|
|
||||||
waiting = {0.95, 0.80, 0.25, 1},
|
|
||||||
error = {0.90, 0.35, 0.30, 1},
|
|
||||||
stopped = {0.55, 0.55, 0.55, 1},
|
|
||||||
}
|
|
||||||
|
|
||||||
-- s = {running, total, stopped, canStop, warning, reason, lastStartFailed} -> "running"|"waiting"|"error"|"stopped"
|
|
||||||
function ADFlowView.flowStatus(s)
|
|
||||||
if s.stopped then return "stopped" end
|
|
||||||
if s.warning ~= nil then return "error" end
|
|
||||||
if (s.running or 0) == 0 and s.lastStartFailed then return "error" end
|
|
||||||
if (s.running or 0) > 0 and s.reason == nil then return "running" end
|
|
||||||
if (s.running or 0) > 0 or s.canStop then return "waiting" end
|
|
||||||
return "stopped"
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4: Run** all tests — Expected: `ALLE TESTS OK` in test_flowStatus, no FAIL elsewhere.
|
|
||||||
- [ ] **Step 5: Commit** `git add tests/test_flowStatus.lua FS25_ADSmartPickup/adFlowView.lua && git commit -m "feat(flows): ren statusfunktion + farver til Flows-listen"`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 2: Rækker kan tegnes i et andet layout
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.lua` (`new`: felter; `addRow`; `clearRows`; `colorRows`)
|
|
||||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFlowTab.lua` (`makeRowClickable`)
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Produces: `self.rowTarget` (standard `nil` = `self.boxLayout`); `SmartPickupFrame:withRowTarget(layout, fn)` kører `fn(self)` med rækker klonet ind i `layout`; `self.panelRows` (kloner i vinduet) adskilt fra `self.clonedRows`; `SmartPickupFrame:clearPanelRows()`; `SmartPickupFrame:colorLayout(layout)`. Hver klon får `row.spuLayout = layout`.
|
|
||||||
|
|
||||||
- [ ] **Step 1: `addRow`** — replace body:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
function SmartPickupFrame:addRow(prefab, data)
|
|
||||||
local layout = self.rowTarget or self.boxLayout
|
|
||||||
local row = prefab:clone(layout)
|
|
||||||
row.spuLayout = layout
|
|
||||||
FocusManager:loadElementFromCustomValues(row)
|
|
||||||
table.insert(layout == self.boxLayout and self.clonedRows or self.panelRows, row)
|
|
||||||
local option = row:getDescendantByName("option")
|
|
||||||
if option ~= nil then
|
|
||||||
option:setTexts({self.i18n:getText("ui_no"), self.i18n:getText("ui_yes")})
|
|
||||||
self.rowData[option] = data
|
|
||||||
end
|
|
||||||
return row
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Rækker klonet ind i et andet layout (redigeringsvinduet) mens fn kører.
|
|
||||||
function SmartPickupFrame:withRowTarget(layout, fn)
|
|
||||||
local previous = self.rowTarget
|
|
||||||
self.rowTarget = layout
|
|
||||||
local ok, err = pcall(fn, self)
|
|
||||||
self.rowTarget = previous
|
|
||||||
if not ok then error(err, 0) end
|
|
||||||
end
|
|
||||||
|
|
||||||
function SmartPickupFrame:clearPanelRows()
|
|
||||||
for _, row in ipairs(self.panelRows or {}) do
|
|
||||||
FocusManager:removeElement(row)
|
|
||||||
row:delete()
|
|
||||||
end
|
|
||||||
self.panelRows = {}
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: `new`** — next to `clonedRows = {}` initialisation add `self.panelRows = {}` (find `clonedRows` in `SmartPickupFrame.new` and add on the same pattern).
|
|
||||||
- [ ] **Step 3: `colorRows`** — split into `colorLayout(layout)` and keep `colorRows()` calling `self:colorLayout(self.boxLayout)`:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
function SmartPickupFrame:colorLayout(layout)
|
|
||||||
local colors = InGameMenuSettingsFrame ~= nil and InGameMenuSettingsFrame.COLOR_ALTERNATING or nil
|
|
||||||
if colors == nil or layout == nil then return end
|
|
||||||
local unpackFn = table.unpack or unpack
|
|
||||||
local isEvenRow = false
|
|
||||||
for _, element in ipairs(layout.elements) do
|
|
||||||
if element.setImageColor ~= nil and colors[isEvenRow] ~= nil then
|
|
||||||
element:setImageColor(nil, unpackFn(colors[isEvenRow]))
|
|
||||||
end
|
|
||||||
isEvenRow = not isEvenRow
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function SmartPickupFrame:colorRows()
|
|
||||||
self:colorLayout(self.boxLayout)
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4: `makeRowClickable`** — clicks count inside the row's own layout, and list rows ignore clicks while the panel is open:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
local layout = element.spuLayout or frame.boxLayout
|
|
||||||
local blockedByPanel = frame.editPanelOpen and layout == frame.boxLayout
|
|
||||||
if not used and not eventUsed and not blockedByPanel and isUp and button == MOUSE_LEFT and (element.getIsVisible == nil or element:getIsVisible())
|
|
||||||
and isInside(element, posX, posY) and isInside(layout, posX, posY) then
|
|
||||||
```
|
|
||||||
(replace the existing `if not used and not eventUsed and isUp ...` condition lines; keep the body.)
|
|
||||||
|
|
||||||
- [ ] **Step 5: Run all tests** — no FAIL (behaviour unchanged: `rowTarget` nil everywhere).
|
|
||||||
- [ ] **Step 6: Commit** `git commit -am "refactor(gui): rækker kan klones ind i et andet layout (redigeringsvindue)"`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 3: Redigeringsvinduet (overlay + mixin)
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.xml` (overlay efter `boxLayout`)
|
|
||||||
- Modify: `FS25_ADSmartPickup/gui/guiProfiles.xml` (profiler)
|
|
||||||
- Create: `FS25_ADSmartPickup/gui/SmartPickupEditPanel.lua`
|
|
||||||
- Modify: `FS25_ADSmartPickup/adSmartPickup.lua` (source efter `SmartPickupFlowGuide.lua`)
|
|
||||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.lua` (`rebuild`: fyld vinduet; `refreshButtons`: vinduets knapper; `refreshLive`: `refreshEditPanel`; `onFrameClose`: luk vinduet)
|
|
||||||
- Modify: `l10n/l10n_da.xml`, `l10n/l10n_en.xml`
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Consumes: Task 2 (`withRowTarget`, `clearPanelRows`, `colorLayout`, `editPanelOpen`).
|
|
||||||
- Produces: `SmartPickupFrame:openEditPanel(kind, id)` (`kind ∈ {"flow","field"}`), `closeEditPanel()`, `fillEditPanel(adEnv)`, `refreshEditPanel(adEnv)`, felterne `self.editPanelOpen`, `self.editPanelKind`, `self.editPanelId`; hook-tabel `SmartPickupFrame.EDIT_PANEL_KINDS[kind] = {title(frame,id)->string, exists(frame,id)->bool, fill(frame,adEnv,id), status(frame,adEnv,id)->{text,level}, startStop(frame,id), startStopText(frame,adEnv,id)->string, canDelete=bool, delete(frame,id)}` — Task 4/7 registrerer "flow"/"field".
|
|
||||||
|
|
||||||
- [ ] **Step 1: Profiles** — add to `guiProfiles.xml` (before `</Profiles>`):
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<!-- redigeringsvindue (v1.58): mørk flade + panel; størrelser sættes i Lua ud fra boxLayout -->
|
|
||||||
<Profile name="spuOverlayBackdrop" extends="spuBar">
|
|
||||||
<imageColor value="0 0 0 0.65"/>
|
|
||||||
<positionOrigin value="topLeft"/>
|
|
||||||
<size value="100px 100px"/>
|
|
||||||
</Profile>
|
|
||||||
<Profile name="spuOverlayPanel" extends="spuBar">
|
|
||||||
<imageColor value="0.09 0.09 0.09 0.97"/>
|
|
||||||
<positionOrigin value="topLeft"/>
|
|
||||||
<size value="100px 100px"/>
|
|
||||||
</Profile>
|
|
||||||
<Profile name="spuPanelTitle" extends="spuCellBold">
|
|
||||||
<textSize value="20px"/>
|
|
||||||
</Profile>
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: XML overlay** — directly after `</ScrollingLayout>` of `boxLayout`:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<!-- redigeringsvindue (skjult; placeres/størrelse sættes i SmartPickupEditPanel.lua) -->
|
|
||||||
<GuiElement profile="baseReference" id="editOverlay" visible="false">
|
|
||||||
<Bitmap profile="spuOverlayBackdrop" id="editBackdrop"/>
|
|
||||||
<Bitmap profile="spuOverlayPanel" id="editPanel">
|
|
||||||
<Text profile="spuPanelTitle" id="editTitle" position="20px -16px"/>
|
|
||||||
<Text profile="spuCell" id="editStatus" position="20px -46px"/>
|
|
||||||
<ScrollingLayout profile="fs25_settingsLayout" id="editLayout" position="0px -80px"/>
|
|
||||||
</Bitmap>
|
|
||||||
</GuiElement>
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Mixin** — create `gui/SmartPickupEditPanel.lua`:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- Smart Pickup — redigeringsvindue (v1.58)
|
|
||||||
-- Klik på en række i Flows/Fields åbner et panel oven på listen. Indholdet tegnes af den eksisterende
|
|
||||||
-- resumé-/guide-kode i vinduets egen ScrollingLayout (SmartPickupFrame:withRowTarget). Knapper i bunden via
|
|
||||||
-- menuButtonInfo: Luk (MENU_BACK = ESC), Start/Stop, Slet. Hver art registrerer sig i EDIT_PANEL_KINDS.
|
|
||||||
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
|
|
||||||
|
|
||||||
SmartPickupFrame.EDIT_PANEL_KINDS = SmartPickupFrame.EDIT_PANEL_KINDS or {}
|
|
||||||
SmartPickupFrame.EDIT_PANEL_WIDTH, SmartPickupFrame.EDIT_PANEL_HEIGHT = 0.78, 0.86 -- andel af listens flade
|
|
||||||
|
|
||||||
function SmartPickupFrame:initEditPanelButtons(i18n)
|
|
||||||
self.panelCloseButtonInfo = {inputAction = InputAction.MENU_BACK, text = i18n:getText("spu_panel_close"), callback = function() self:onClickPanelClose() end}
|
|
||||||
self.panelStartButtonInfo = {inputAction = InputAction.MENU_ACTIVATE, text = i18n:getText("spu_button_start"), callback = function() self:onClickPanelStartStop() end}
|
|
||||||
self.panelDeleteButtonInfo = {inputAction = InputAction.MENU_EXTRA_2, text = i18n:getText("spu_button_delete"), callback = function() self:onClickPanelDelete() end}
|
|
||||||
self.panelButtons = {self.panelCloseButtonInfo, self.panelStartButtonInfo, self.panelDeleteButtonInfo}
|
|
||||||
end
|
|
||||||
|
|
||||||
function SmartPickupFrame:editPanelKind()
|
|
||||||
return self.editPanelOpen and SmartPickupFrame.EDIT_PANEL_KINDS[self.editPanelKind] or nil
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Vinduet dækker listens flade; panelet er centreret i den.
|
|
||||||
function SmartPickupFrame:layoutEditPanel()
|
|
||||||
local box = self.boxLayout
|
|
||||||
local x, y, w, h = box.position[1], box.position[2], box.size[1], box.size[2]
|
|
||||||
self.editBackdrop:setPosition(x, y)
|
|
||||||
self.editBackdrop:setSize(w, h)
|
|
||||||
local pw, ph = w * SmartPickupFrame.EDIT_PANEL_WIDTH, h * SmartPickupFrame.EDIT_PANEL_HEIGHT
|
|
||||||
self.editPanel:setPosition(x + (w - pw) / 2, y - (h - ph) / 2)
|
|
||||||
self.editPanel:setSize(pw, ph)
|
|
||||||
local top = 80 * g_pixelSizeScaledY
|
|
||||||
self.editLayout:setSize(pw, ph - top)
|
|
||||||
end
|
|
||||||
|
|
||||||
function SmartPickupFrame:openEditPanel(kind, id)
|
|
||||||
if SmartPickupFrame.EDIT_PANEL_KINDS[kind] == nil then return end
|
|
||||||
self.editPanelOpen, self.editPanelKind, self.editPanelId = true, kind, id
|
|
||||||
self:closeGuide()
|
|
||||||
self.editOverlay:setVisible(true)
|
|
||||||
self:layoutEditPanel()
|
|
||||||
self:requestRebuild()
|
|
||||||
end
|
|
||||||
|
|
||||||
function SmartPickupFrame:closeEditPanel()
|
|
||||||
if not self.editPanelOpen then return end
|
|
||||||
self.editPanelOpen, self.editPanelKind, self.editPanelId = false, nil, nil
|
|
||||||
self:closeGuide()
|
|
||||||
self:clearPanelRows()
|
|
||||||
if self.editOverlay ~= nil then self.editOverlay:setVisible(false) end
|
|
||||||
self:requestRebuild()
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Kaldes fra rebuild (efter listen): vinduets rækker genopbygges i editLayout.
|
|
||||||
function SmartPickupFrame:fillEditPanel(adEnv)
|
|
||||||
self:clearPanelRows()
|
|
||||||
local hooks = self:editPanelKind()
|
|
||||||
if hooks == nil then return end
|
|
||||||
if not hooks.exists(self, self.editPanelId) then
|
|
||||||
self.statusOverride = self.i18n:getText("spu_panel_gone")
|
|
||||||
self.statusOverrideTicks = SmartPickupFrame.STATUS_OVERRIDE_TICKS
|
|
||||||
self.editPanelOpen, self.editPanelKind, self.editPanelId = false, nil, nil
|
|
||||||
self.editOverlay:setVisible(false)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
self.editTitle:setText(hooks.title(self, self.editPanelId))
|
|
||||||
self:withRowTarget(self.editLayout, function(frame)
|
|
||||||
if frame.guide ~= nil then
|
|
||||||
frame:fillGuideRows(adEnv)
|
|
||||||
else
|
|
||||||
hooks.fill(frame, adEnv, frame.editPanelId)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
self:colorLayout(self.editLayout)
|
|
||||||
self.editLayout:invalidateLayout()
|
|
||||||
self:refreshEditPanel(adEnv)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Hvert sekund: status + knaptekster.
|
|
||||||
function SmartPickupFrame:refreshEditPanel(adEnv)
|
|
||||||
local hooks = self:editPanelKind()
|
|
||||||
if hooks == nil or adEnv == nil then return end
|
|
||||||
if not hooks.exists(self, self.editPanelId) then self:closeEditPanel() return end
|
|
||||||
local status = hooks.status(self, adEnv, self.editPanelId)
|
|
||||||
self.editStatus:setText(status.text or "")
|
|
||||||
local color = ADFlowView.LEVEL_COLORS[status.level] or ADFlowView.LEVEL_COLORS.stopped
|
|
||||||
if self.editStatus.setTextColor ~= nil then self.editStatus:setTextColor(color[1], color[2], color[3], color[4]) end
|
|
||||||
self.panelStartButtonInfo.text = hooks.startStopText(self, adEnv, self.editPanelId)
|
|
||||||
self.panelDeleteButtonInfo.disabled = not hooks.canDelete
|
|
||||||
end
|
|
||||||
|
|
||||||
function SmartPickupFrame:onClickPanelClose()
|
|
||||||
self:safe(function(frame)
|
|
||||||
-- et åbent [Edit]-trin lukkes først (kladden kasseres som ved Annullér)
|
|
||||||
if frame.guide ~= nil then
|
|
||||||
frame:closeGuide()
|
|
||||||
frame:requestRebuild()
|
|
||||||
return
|
|
||||||
end
|
|
||||||
frame:closeEditPanel()
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function SmartPickupFrame:onClickPanelStartStop()
|
|
||||||
self:safe(function(frame)
|
|
||||||
local hooks = frame:editPanelKind()
|
|
||||||
if hooks ~= nil then hooks.startStop(frame, frame.editPanelId) end
|
|
||||||
frame:requestRebuild()
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function SmartPickupFrame:onClickPanelDelete()
|
|
||||||
self:safe(function(frame)
|
|
||||||
local hooks = frame:editPanelKind()
|
|
||||||
if hooks ~= nil and hooks.canDelete then hooks.delete(frame, frame.editPanelId) end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4: Wire into frame** —
|
|
||||||
- `adSmartPickup.lua`: after `source(ADSmartPickup.dir .. "gui/SmartPickupFlowGuide.lua")` add `source(ADSmartPickup.dir .. "gui/SmartPickupEditPanel.lua")`.
|
|
||||||
- `SmartPickupFrame.new`: where `initFlowButtons(i18n)` is called, also call `self:initEditPanelButtons(i18n)`.
|
|
||||||
- `rebuild`: in the Flows branch and the Fields branch, after `self.boxLayout:invalidateLayout()` add `if self.editPanelOpen then self:fillEditPanel(adEnv) end`.
|
|
||||||
- `refreshButtons`: first branch: `if self.editPanelOpen then wanted = self.guide ~= nil and self:getGuideButtons() or self.panelButtons elseif self:hasGuideOn(...)` — and in the guide-in-panel case replace the guide list's `self.backButtonInfo` by `self.panelCloseButtonInfo` (build `self.panelGuideButtons` once: copy of `getGuideButtons()` with index 1 swapped).
|
|
||||||
- `refreshLive`: at the end `if self.editPanelOpen then self:refreshEditPanel(adEnv) end`.
|
|
||||||
- `onFrameClose`: before `closeGuide()` add `self.editPanelOpen, self.editPanelKind, self.editPanelId = false, nil, nil; if self.editOverlay ~= nil then self.editOverlay:setVisible(false) end; self:clearPanelRows()`.
|
|
||||||
- `switchTab`: `if self.editPanelOpen then self:closeEditPanel() end` at the top.
|
|
||||||
- Guide rendering while panel is open: `fillFlowTabRows`/`fillFieldRows` start with `if self:hasGuideOn(TAB) and self:fillGuideRows(adEnv) then return end` — guard with `not self.editPanelOpen and` so the guide draws only in the panel.
|
|
||||||
- [ ] **Step 5: l10n** — add `spu_panel_close` ("Luk"/"Close"), `spu_panel_gone` ("Posten findes ikke længere — vinduet er lukket"/"That item no longer exists — window closed"), `spu_button_delete` if not present (check `grep spu_button_delete l10n/l10n_da.xml` first; reuse existing delete text key if it exists).
|
|
||||||
- [ ] **Step 6: Run all tests** (noStrayGlobals must pass) and validate XML: `python3 -c "import xml.dom.minidom as m; [m.parse(f) for f in ('FS25_ADSmartPickup/gui/SmartPickupFrame.xml','FS25_ADSmartPickup/gui/guiProfiles.xml','FS25_ADSmartPickup/l10n/l10n_da.xml','FS25_ADSmartPickup/l10n/l10n_en.xml')]"`.
|
|
||||||
- [ ] **Step 7: Commit** `git commit -am "feat(gui): redigeringsvindue (overlay) med Luk/ESC, Start/Stop, Slet"`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 4: Flows-fanen som farvet liste + "flow" i vinduet
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.xml` (ny `flowListPrefab`, fjernes ved `initialize`)
|
|
||||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.lua` (`initialize`-listen: `"flowListPrefab"`; `onClickStartStop` husker afvisning)
|
|
||||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFlowGuide.lua` (`fillFlowTabRows`, `refreshFlowTab`, `initFlowButtons`, registrér `EDIT_PANEL_KINDS.flow`)
|
|
||||||
- Modify: `l10n/*.xml`
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Consumes: `ADFlowView.flowStatus`, `ADFlowView.LEVEL_COLORS` (Task 1); `openEditPanel`, `EDIT_PANEL_KINDS` (Task 3); `makeRowClickable` (Task 2).
|
|
||||||
- Produces: `SmartPickupFrame:flowLevel(adEnv, run) -> level`, `self.lastStartFailed[runId] = bool`, `SmartPickupFrame:toggleRun(adEnv, runId)`.
|
|
||||||
|
|
||||||
- [ ] **Step 1: Prefab** — in the XML after `flowPrefab`:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<!-- Flows-listen (v1.58): ● · navn · varer · kører/i alt · status · [Start]/[Stop]. ~835 px. -->
|
|
||||||
<Bitmap profile="fs25_multiTextOptionContainer" id="flowListPrefab">
|
|
||||||
<Bitmap profile="spuDot" name="dot" position="20px 0px"/>
|
|
||||||
<Text profile="spuCellBold" name="c1" position="38px 0px" size="200px 40px" textMaxWidth="192px"/>
|
|
||||||
<Text profile="spuCell" name="c2" position="238px 0px" size="170px 40px" textMaxWidth="162px"/>
|
|
||||||
<Text profile="spuCell" name="c3" position="408px 0px" size="70px 40px" textMaxWidth="62px"/>
|
|
||||||
<Text profile="spuCell" name="c4" position="478px 0px" size="252px 40px" textMaxWidth="244px"/>
|
|
||||||
<Text profile="spuCellBold" name="c5" position="730px 0px" size="100px 40px" textMaxWidth="92px"/>
|
|
||||||
</Bitmap>
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Toggle + afvisning** — in `SmartPickupFlowGuide.lua` add:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- Start/Stop for ét flow (rækkens knap og vinduets knap). Husker om sidste start blev helt afvist (rød).
|
|
||||||
function SmartPickupFrame:toggleRun(adEnv, runId)
|
|
||||||
local run = ADRuns.getRun(ADRunsController.store, runId)
|
|
||||||
if adEnv == nil or run == nil then return end
|
|
||||||
self.lastStartFailed = self.lastStartFailed or {}
|
|
||||||
if ADRunsController.getRunStatus(adEnv, run).canStop then
|
|
||||||
ADRunsController.stopRun(adEnv, run.id)
|
|
||||||
self.lastStartFailed[run.id] = nil
|
|
||||||
self.statusOverride = nil
|
|
||||||
else
|
|
||||||
local started, skipped, reasons = self:summarizeStartResults(ADRunsController.startRun(adEnv, run.id))
|
|
||||||
self.lastStartFailed[run.id] = started == 0 and skipped > 0
|
|
||||||
local text = string.format(self.i18n:getText("spu_status_started"), started, skipped)
|
|
||||||
self:showOverride(text, reasons)
|
|
||||||
if skipped > 0 and ADFieldJobs ~= nil and ADFieldJobs.notify ~= nil then
|
|
||||||
ADFieldJobs.notify(string.format("%s: %s — %s", tostring(run.name), text, table.concat(reasons, "; ")), started == 0)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
ADRunsController.save()
|
|
||||||
end
|
|
||||||
|
|
||||||
function SmartPickupFrame:flowLevel(adEnv, run)
|
|
||||||
local row = ADRunsController.getFlowRow(adEnv, run)
|
|
||||||
local status = ADRunsController.getRunStatus(adEnv, run)
|
|
||||||
return ADFlowView.flowStatus({running = row.running, total = row.total, stopped = run.stopped == true,
|
|
||||||
canStop = status.canStop == true, warning = row.warning, reason = row.reason,
|
|
||||||
lastStartFailed = (self.lastStartFailed or {})[run.id] == true})
|
|
||||||
end
|
|
||||||
```
|
|
||||||
And make `onClickStartStop` (Frame.lua) call `frame:toggleRun(adEnv, run.id)` instead of its inline start/stop body (keep `ADRunsController.save()` + `requestRebuild()` there).
|
|
||||||
|
|
||||||
- [ ] **Step 3: List** — replace `fillFlowTabRows` body after the guide guard:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
function SmartPickupFrame:fillFlowTabRows(adEnv)
|
|
||||||
if not self.editPanelOpen and self:hasGuideOn(SmartPickupFrame.TAB_FLOWS) and self:fillGuideRows(adEnv) then return end
|
|
||||||
self.flowListRows = {}
|
|
||||||
local i18n = self.i18n
|
|
||||||
if #ADRunsController.store.runs == 0 then
|
|
||||||
self:addTextRow(i18n:getText("spu_noRuns"))
|
|
||||||
return
|
|
||||||
end
|
|
||||||
self:addTextRow(i18n:getText("spu_flowList_hint"))
|
|
||||||
for _, group in ipairs(ADFlowView.boardGroups(ADRunsController.store.runs)) do
|
|
||||||
self:addTextRow(i18n:getText(group.kind == "outbound" and "spu_board_outbound" or "spu_board_supply"))
|
|
||||||
for _, run in ipairs(group.runs) do
|
|
||||||
local row = self:addRow(self.flowListPrefab, {kind = "flowList"})
|
|
||||||
local entry = {runId = run.id, row = row, dot = row:getDescendantByName("dot"),
|
|
||||||
cells = {row:getDescendantByName("c1"), row:getDescendantByName("c2"), row:getDescendantByName("c3"),
|
|
||||||
row:getDescendantByName("c4"), row:getDescendantByName("c5")}}
|
|
||||||
local runId = run.id
|
|
||||||
local actionCell = entry.cells[5]
|
|
||||||
self:makeRowClickable(row, function(frame)
|
|
||||||
-- klik på [Start]/[Stop]-feltet = handling; ellers åbn vinduet
|
|
||||||
if frame.lastClickX ~= nil and actionCell.absPosition ~= nil and frame.lastClickX >= actionCell.absPosition[1] then
|
|
||||||
frame:toggleRun(frame.getAdEnv(), runId)
|
|
||||||
frame:requestRebuild()
|
|
||||||
else
|
|
||||||
frame.selectedRunId = runId
|
|
||||||
frame:openEditPanel("flow", runId)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
table.insert(self.flowListRows, entry)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
self:refreshFlowListRows(adEnv)
|
|
||||||
end
|
|
||||||
|
|
||||||
function SmartPickupFrame:refreshFlowListRows(adEnv)
|
|
||||||
local unpackFn = table.unpack or unpack
|
|
||||||
for _, entry in ipairs(self.flowListRows or {}) do
|
|
||||||
local run = ADRuns.getRun(ADRunsController.store, entry.runId)
|
|
||||||
if run ~= nil then
|
|
||||||
local cells = self:flowCells(adEnv, run)
|
|
||||||
local level = self:flowLevel(adEnv, run)
|
|
||||||
local color = ADFlowView.LEVEL_COLORS[level]
|
|
||||||
local canStop = ADRunsController.getRunStatus(adEnv, run).canStop
|
|
||||||
entry.cells[1]:setText(tostring(run.name))
|
|
||||||
entry.cells[2]:setText(tostring(cells[2]))
|
|
||||||
entry.cells[3]:setText(tostring(cells[5]))
|
|
||||||
entry.cells[4]:setText(tostring(cells[6]))
|
|
||||||
entry.cells[5]:setText(self.i18n:getText(canStop and "spu_flowList_stop" or "spu_flowList_start"))
|
|
||||||
if entry.dot ~= nil and entry.dot.setImageColor ~= nil then entry.dot:setImageColor(nil, unpackFn(color)) end
|
|
||||||
if entry.cells[4].setTextColor ~= nil then entry.cells[4]:setTextColor(unpackFn(color)) end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
In `makeRowClickable` (Task 2's function) store the click x before setting `pendingClick`: `frame.lastClickX = posX`.
|
|
||||||
|
|
||||||
- [ ] **Step 4: `refreshFlowTab`** — replace body:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
function SmartPickupFrame:refreshFlowTab(adEnv)
|
|
||||||
if not self.editPanelOpen and self:hasGuideOn(SmartPickupFrame.TAB_FLOWS) then
|
|
||||||
self:setStatusText(self.statusOverride or "")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
self:refreshFlowListRows(adEnv)
|
|
||||||
local running, total = 0, 0
|
|
||||||
for _, run in ipairs(ADRunsController.store.runs) do
|
|
||||||
total = total + 1
|
|
||||||
if ADRunsController.getRunStatus(adEnv, run).running > 0 then running = running + 1 end
|
|
||||||
end
|
|
||||||
self:setStatusText(self.statusOverride or string.format(self.i18n:getText("spu_flowList_status"), running, total))
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 5: Buttons + panel kind** — in `initFlowButtons`: `self.flowButtons = {self.backButtonInfo, self.flowNewButtonInfo}`. Register:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
SmartPickupFrame.EDIT_PANEL_KINDS.flow = {
|
|
||||||
canDelete = true,
|
|
||||||
exists = function(frame, id) return ADRuns.getRun(ADRunsController.store, id) ~= nil end,
|
|
||||||
title = function(frame, id)
|
|
||||||
local run = ADRuns.getRun(ADRunsController.store, id)
|
|
||||||
return string.format(frame.i18n:getText("spu_panel_flowTitle"), tostring(run ~= nil and run.name or "?"))
|
|
||||||
end,
|
|
||||||
fill = function(frame, adEnv, id)
|
|
||||||
local run = ADRuns.getRun(ADRunsController.store, id)
|
|
||||||
if run ~= nil then frame:fillFlowSummary(adEnv, run) end
|
|
||||||
end,
|
|
||||||
status = function(frame, adEnv, id)
|
|
||||||
local run = ADRuns.getRun(ADRunsController.store, id)
|
|
||||||
return {text = tostring(frame:flowCells(adEnv, run)[6]), level = frame:flowLevel(adEnv, run)}
|
|
||||||
end,
|
|
||||||
startStopText = function(frame, adEnv, id)
|
|
||||||
local run = ADRuns.getRun(ADRunsController.store, id)
|
|
||||||
return frame.i18n:getText(ADRunsController.getRunStatus(adEnv, run).canStop and "spu_button_stop" or "spu_button_start")
|
|
||||||
end,
|
|
||||||
startStop = function(frame, id) frame:toggleRun(frame.getAdEnv(), id) end,
|
|
||||||
delete = function(frame, id) frame.selectedRunId = id; frame:onClickDelete() end,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
In `fillFlowSummary` drop the first heading row (the panel title shows the name): remove the `spu_guide_flow_section` line. Add "Opsæt flow" as a last link row in the summary: `self:addLinkRow(i18n:getText("spu_guide_flow_open"), function(frame) frame.selectedRunId = runId; frame:onClickFlowSetup() end)`.
|
|
||||||
After a delete (`onClickDelete`'s YesNo callback path) the run no longer exists → `refreshEditPanel` closes the panel (Review Focus).
|
|
||||||
|
|
||||||
- [ ] **Step 6: l10n** — `spu_flowList_hint` ("Klik på et flow for at rette det. [Start]/[Stop] styrer flowet direkte." / en), `spu_flowList_start` ("[Start]"), `spu_flowList_stop` ("[Stop]"), `spu_flowList_status` ("Flows i gang: %d af %d" / "Flows running: %d of %d"), `spu_panel_flowTitle` ("Rediger flow: %s" / "Edit flow: %s").
|
|
||||||
- [ ] **Step 7: Run all tests + XML check.**
|
|
||||||
- [ ] **Step 8: Commit** `git commit -am "feat(flows): Flows-fanen som farvet liste med Start/Stop pr. række; klik åbner redigeringsvinduet"`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 5: Etape 1 build + in-game test
|
|
||||||
|
|
||||||
- [ ] **Step 1:** bump `modDesc.xml` `<version>` to `1.58.0.0`; build zip; copy to `~/FS25-data/mods/`; `unzip -p ~/FS25-data/mods/FS25_ADSmartPickup.zip modDesc.xml | grep -o '<version>[^<]*'` shows 1.58.0.0.
|
|
||||||
- [ ] **Step 2:** commit + push gitea main.
|
|
||||||
- [ ] **Step 3: In-game checklist (brugeren):** liste med prikker/farver; [Start]/[Stop] på en række virker og rører ikke andre flows; klik på navnet åbner vinduet; [Edit] → Gem inde i vinduet; Luk og ESC lukker vinduet (ESC med åbent trin lukker trinnet først); klik på listen bagved gør intet; Slet flow fra vinduet lukker det; log uden `GUI-fejl`. Ret layout-afvigelser (størrelser/placering) efter screenshot før Task 6.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 6: Ren statusfunktion for marker
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `FS25_ADSmartPickup/adFieldView.lua`
|
|
||||||
- Test: `tests/test_fieldStatus.lua` (ny)
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Produces: `ADFieldView.fieldStatus(s) -> level` hvor `s = {hasMarker=bool, working=bool, awaiting=bool, startRequested=bool, reasonKey=string|nil}`; `ADFieldView.ERROR_REASONS` (sæt af reason-nøgler).
|
|
||||||
|
|
||||||
- [ ] **Step 1: Test** — `tests/test_fieldStatus.lua`:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- Kør: luajit tests/test_fieldStatus.lua (fra repo-roden)
|
|
||||||
dofile("FS25_ADSmartPickup/adFieldView.lua")
|
|
||||||
local V = ADFieldView
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
check("mangler markør", V.fieldStatus({hasMarker = false}), "error")
|
|
||||||
check("arbejder", V.fieldStatus({hasMarker = true, working = true}), "running")
|
|
||||||
check("arbejder men venter på maskine", V.fieldStatus({hasMarker = true, working = true, reasonKey = "spu_field_reason_allBusy"}), "waiting")
|
|
||||||
check("for bred", V.fieldStatus({hasMarker = true, working = true, reasonKey = "spu_field_reason_tooWide"}), "error")
|
|
||||||
check("ingen maskine", V.fieldStatus({hasMarker = true, reasonKey = "spu_field_reason_noRig"}), "error")
|
|
||||||
check("regn", V.fieldStatus({hasMarker = true, working = true, reasonKey = "spu_field_reason_rain"}), "waiting")
|
|
||||||
check("venter på spilleren (høster)", V.fieldStatus({hasMarker = true, awaiting = true}), "waiting")
|
|
||||||
check("start bestilt", V.fieldStatus({hasMarker = true, startRequested = true}), "waiting")
|
|
||||||
check("i ro", V.fieldStatus({hasMarker = true}), "stopped")
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
```
|
|
||||||
(If `adFieldView.lua` needs other modules loaded, add the same `dofile`s as `tests/test_adFieldView.lua` uses.)
|
|
||||||
- [ ] **Step 2: Run** — FAIL (fieldStatus nil).
|
|
||||||
- [ ] **Step 3: Implement** — append to `adFieldView.lua`:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- ---- status pr. mark (Fields-listen) ----
|
|
||||||
|
|
||||||
-- Årsager der betyder "kan ikke" (rød) — resten er "venter" (gul).
|
|
||||||
ADFieldView.ERROR_REASONS = {
|
|
||||||
spu_field_reason_noRig = true, spu_field_reason_cannotDo = true, spu_field_reason_tooWide = true,
|
|
||||||
spu_field_reason_tooNarrow = true, spu_field_reason_widthUnknown = true, spu_field_reason_noSowFruit = true,
|
|
||||||
}
|
|
||||||
|
|
||||||
-- s = {hasMarker, working, awaiting, startRequested, reasonKey} -> "running"|"waiting"|"error"|"stopped"
|
|
||||||
function ADFieldView.fieldStatus(s)
|
|
||||||
if not s.hasMarker then return "error" end
|
|
||||||
if s.reasonKey ~= nil and ADFieldView.ERROR_REASONS[s.reasonKey] then return "error" end
|
|
||||||
if s.awaiting or s.startRequested then return "waiting" end
|
|
||||||
if s.working then return s.reasonKey ~= nil and "waiting" or "running" end
|
|
||||||
if s.reasonKey ~= nil then return "waiting" end
|
|
||||||
return "stopped"
|
|
||||||
end
|
|
||||||
```
|
|
||||||
- [ ] **Step 4: Run all tests** — green.
|
|
||||||
- [ ] **Step 5: Commit** `git commit -am "feat(fields): ren statusfunktion til Fields-listen"`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 7: Fields-fanen som farvet liste + "field" i vinduet
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFrame.xml` (`fieldPrefab`: dot forrest, c7 = handling; kolonner forskydes)
|
|
||||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFieldTab.lua` (`fieldCells`, `fillFieldRows`, `refreshFieldRows`, `onClickFieldStartStop` tager id, register `EDIT_PANEL_KINDS.field`)
|
|
||||||
- Modify: `FS25_ADSmartPickup/gui/SmartPickupFieldGuide.lua` (`getFieldButtons` uden Start/Stop når listen vises)
|
|
||||||
- Modify: `l10n/*.xml`
|
|
||||||
|
|
||||||
**Interfaces:**
|
|
||||||
- Consumes: `ADFieldView.fieldStatus` (Task 6); `openEditPanel`/`EDIT_PANEL_KINDS` (Task 3); `makeRowClickable` + `frame.lastClickX` (Task 2/4); `ADFlowView.LEVEL_COLORS` (Task 1).
|
|
||||||
- Produces: `SmartPickupFrame:fieldLevel(id) -> level`; `SmartPickupFrame:toggleField(id)`.
|
|
||||||
|
|
||||||
- [ ] **Step 1: Prefab** — `fieldPrefab` becomes (≈835 px):
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<Bitmap profile="fs25_multiTextOptionContainer" id="fieldPrefab">
|
|
||||||
<Bitmap profile="spuDot" name="dot" position="20px 0px"/>
|
|
||||||
<Text profile="spuCellBold" name="c1" position="38px 0px" size="82px 40px" textMaxWidth="76px"/>
|
|
||||||
<Text profile="spuCell" name="c2" position="120px 0px" size="120px 40px" textMaxWidth="112px"/>
|
|
||||||
<Text profile="spuCell" name="c3" position="240px 0px" size="100px 40px" textMaxWidth="92px"/>
|
|
||||||
<Text profile="spuCell" name="c4" position="340px 0px" size="70px 40px" textMaxWidth="62px"/>
|
|
||||||
<Text profile="spuCell" name="c5" position="410px 0px" size="200px 40px" textMaxWidth="192px"/>
|
|
||||||
<Text profile="spuCell" name="c6" position="610px 0px" size="120px 40px" textMaxWidth="112px"/>
|
|
||||||
<Text profile="spuCellBold" name="c7" position="730px 0px" size="100px 40px" textMaxWidth="92px"/>
|
|
||||||
</Bitmap>
|
|
||||||
```
|
|
||||||
The header row (column titles) passes a 7th title `""`.
|
|
||||||
- [ ] **Step 2: Toggle + level** — refactor `onClickFieldStartStop` into `toggleField(id)` (same body, `id` parameter instead of `frame.selectedFieldId`); `onClickFieldStartStop` calls `self:toggleField(self.selectedFieldId)`. Add:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
function SmartPickupFrame:fieldLevel(id)
|
|
||||||
local field, info = ADFieldJobs.getField(id), ADFields.getInfo(id)
|
|
||||||
if field == nil or info == nil then return "stopped" end
|
|
||||||
local reason = type(field.reason) == "table" and field.reason.key or nil
|
|
||||||
return ADFieldView.fieldStatus({hasMarker = info.markerId ~= nil, working = ADFieldModel.isWorking(field),
|
|
||||||
awaiting = ADFieldJobs.awaitingPlayer(id) ~= nil, startRequested = ADFieldJobs.isStartRequested ~= nil and ADFieldJobs.isStartRequested(id),
|
|
||||||
reasonKey = reason})
|
|
||||||
end
|
|
||||||
|
|
||||||
function SmartPickupFrame:fieldActionText(id)
|
|
||||||
local field = ADFieldJobs.getField(id)
|
|
||||||
if ADFieldJobs.awaitingPlayer(id) ~= nil then return self.i18n:getText("spu_field_button_takeover") end
|
|
||||||
return self.i18n:getText(field ~= nil and ADFieldModel.isWorking(field) and "spu_flowList_stop" or "spu_flowList_start")
|
|
||||||
end
|
|
||||||
```
|
|
||||||
(If `ADFieldJobs.isStartRequested` does not exist, leave the guarded call — it evaluates to false.)
|
|
||||||
- [ ] **Step 3: List rows** — in `fillFieldRows`: guide guard gets `not self.editPanelOpen and`; `fieldCells` appends `self:fieldActionText(info.id)` as 7th cell and no longer prefixes `"> "`; row click:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
self:makeRowClickable(cells[1].parent, function(frame)
|
|
||||||
if frame.lastClickX ~= nil and cells[7] ~= nil and cells[7].absPosition ~= nil and frame.lastClickX >= cells[7].absPosition[1] then
|
|
||||||
frame:toggleField(fieldId)
|
|
||||||
else
|
|
||||||
frame.selectedFieldId = fieldId
|
|
||||||
frame:openEditPanel("field", fieldId)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
```
|
|
||||||
Remove the trailing `fillFieldOverview` call from `fillFieldRows` (it now lives in the panel). In `refreshFieldRows` after `setCells` color the dot and c5 by `self:fieldLevel(entry.id)` (same code as `refreshFlowListRows`; `entry.cells[1].parent:getDescendantByName("dot")`).
|
|
||||||
- [ ] **Step 4: Panel kind** —
|
|
||||||
|
|
||||||
```lua
|
|
||||||
SmartPickupFrame.EDIT_PANEL_KINDS.field = {
|
|
||||||
canDelete = false,
|
|
||||||
exists = function(frame, id) return ADFields.getInfo(id) ~= nil end,
|
|
||||||
title = function(frame, id) return string.format(frame.i18n:getText("spu_panel_fieldTitle"), id) end,
|
|
||||||
fill = function(frame, adEnv, id) frame:fillFieldOverview(adEnv, ADFields.getInfo(id)) end,
|
|
||||||
status = function(frame, adEnv, id)
|
|
||||||
return {text = frame:fieldStatusText(ADFieldJobs.getField(id), ADFields.getInfo(id), false), level = frame:fieldLevel(id)}
|
|
||||||
end,
|
|
||||||
startStopText = function(frame, adEnv, id) return frame:fieldActionText(id) end,
|
|
||||||
startStop = function(frame, id) frame:toggleField(id) end,
|
|
||||||
delete = function() end,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
`fillFieldOverview`'s own heading row (`spu_field_section`) is dropped when drawn in the panel: guard it with `if not self.editPanelOpen then ... end`.
|
|
||||||
`getFieldButtons`: list view keeps Back, Opsæt mark (needs selected field → move "Opsæt mark" into the panel as a link row like flows), Flåde; Start/Stop leaves the bottom bar.
|
|
||||||
- [ ] **Step 5: l10n** — `spu_panel_fieldTitle` ("Mark F%d" / "Field F%d").
|
|
||||||
- [ ] **Step 6: Run all tests + XML check.**
|
|
||||||
- [ ] **Step 7: Commit** `git commit -am "feat(fields): Fields-fanen som farvet liste med Start/Stop pr. række; klik åbner redigeringsvinduet"`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 8: Etape 2 build + in-game test
|
|
||||||
|
|
||||||
- [ ] **Step 1:** bump to `1.59.0.0`; build; deploy; verify version in zip; commit + push.
|
|
||||||
- [ ] **Step 2: In-game checklist:** Fields-liste med prikker/farver; [Start]/[Stop]/[Overtag] pr. række; klik åbner vinduet med markens linjer; [Ret] → Gem i vinduet; Flåde-visning uændret; Luk/ESC.
|
|
||||||
- [ ] **Step 3:** Update memory `project_fs25_adsmartpickup.md` with the new GUI structure (EditPanel mixin, rowTarget) and the Obsidian note `projekter/FS25 silo-patches + AD Smart Pickup.md` with a line about v1.58/1.59.
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
# Behovsstyret forsyning ("Feed the factory") — design
|
|
||||||
|
|
||||||
Dato: 2026-09-21 · Status: godkendt af bruger
|
|
||||||
|
|
||||||
## Problem
|
|
||||||
|
|
||||||
En produktion med flere input (fx TMR-blander: hø, græs, ensilage, sukkerroer) kræver i dag
|
|
||||||
én AutoDrive-traktor pr. vare, fordi AD's Pickup&Deliver kun kører én vare fra ét sted.
|
|
||||||
Varerne ligger spredt over mange lokationer på kortet.
|
|
||||||
|
|
||||||
## Løsning
|
|
||||||
|
|
||||||
Én traktor holder alle input fyldt. For hver tur vælger moden den ingrediens aflæsningsmålet
|
|
||||||
har mest brug for, finder en silo der har den, skifter AD's vare og sender traktoren derhen.
|
|
||||||
|
|
||||||
### Betjening (ingen ny GUI, ingen config-fil)
|
|
||||||
|
|
||||||
1. Aflæsningsmål = produktionens markør.
|
|
||||||
2. Multi-vælg ingredienserne i AD's vare-dropdown (>1 vare = funktionen er aktiv).
|
|
||||||
3. "Brug mapper" + "Rotér mål: pålæsning" slået til (krævet for at AD kalder `getNextPickup`).
|
|
||||||
4. Start Pickup&Deliver. AD's loop-tæller virker uændret (N læs → parkér, 0 = uendeligt).
|
|
||||||
|
|
||||||
Med præcis én valgt vare er opførslen uændret (eksisterende mappe-rotation, mindst fyldte først).
|
|
||||||
|
|
||||||
### Logik pr. tur (i `getNextPickup`-wrapperen)
|
|
||||||
|
|
||||||
1. **Ingrediensliste pr. køretøj.** Ved >1 valgt vare gemmes listen i modens egen tilstand
|
|
||||||
(`vehicle → {ingredients, narrowedTo}`). Derefter indsnævres AD's `selectedFillTypes` til den
|
|
||||||
valgte vare, fordi AD ellers selv roterer vare ved triggeren
|
|
||||||
(`ADStateModule:selectPreferredFillTypeFromFillLevels`).
|
|
||||||
Ved næste kald: er AD's valg stadig `{narrowedTo}` → brug den gemte liste. Er det noget andet →
|
|
||||||
brugeren har ændret valget: ny multi-liste erstatter, enkelt-valg sletter tilstanden.
|
|
||||||
2. **Behov.** Find `UnloadingStation` nærmest anden markør (≤ 40 m). For hver ingrediens summeres
|
|
||||||
`level`, `capacity` over `station.targetStorages`; `free = capacity - level`.
|
|
||||||
Ingredienser stationen ikke understøtter, eller med `free < MIN_FREE_LITERS`, springes over.
|
|
||||||
3. **Rangering.** `ADSupplyPlanner.scoreNeed(level, capacity, freeSpace)` → højeste score først.
|
|
||||||
4. **Kilde.** For ingredienserne i behovs-rækkefølge: blandt *alle* AD-markører (ikke kun én mappe)
|
|
||||||
findes markører med farm-tilgængelig `LoadingStation` ≤ 40 m der har varen. Aflæsningsmålets
|
|
||||||
egen placeable udelukkes som kilde. Rangering som eksisterende: eget lager før nabo-lån,
|
|
||||||
mindst fyldte først. Første ingrediens med en kilde vinder.
|
|
||||||
5. **Effekt.** `setFillType(vare)`, `selectedFillTypes = {vare}`, dirty-flag, returnér markørens
|
|
||||||
`markerIndex`. Logges som `ADSmartPickup: '<traktor>' -> <markør> (<vare>, behov …)`.
|
|
||||||
6. **Fallback.** Intet behov / ingen kilde / ingen station / fejl (`pcall`) → AutoDrives
|
|
||||||
originale `getNextPickup` (ved fejl: `Logging.warning`). Aldrig crash.
|
|
||||||
|
|
||||||
## Filer
|
|
||||||
|
|
||||||
| Fil | Ansvar |
|
|
||||||
|---|---|
|
|
||||||
| `FS25_ADSmartPickup/adSupplyPlanner.lua` (ny) | Rene funktioner: `scoreNeed`, `rankNeeds`, `resolveIngredients` (tilstandslogik). Ingen spil-API. |
|
|
||||||
| `FS25_ADSmartPickup/adSmartPickup.lua` | Spil-/AD-adaptere (station ved markør, lagerniveauer, kilde-søgning) + hook. Kilde-søgning generaliseres til at tage en markørliste. |
|
|
||||||
| `FS25_ADSmartPickup/modDesc.xml` | Ny sourceFile før `adSmartPickup.lua`, version 1.1.0.0, beskrivelse. |
|
|
||||||
| `tests/test_adSupplyPlanner.lua` (ny) | Enhedstests af de rene funktioner. |
|
|
||||||
| `tests/test_adSmartPickup.lua` | Integration gennem hook'en med mocks; eksisterende 11 tests skal forblive grønne. |
|
|
||||||
|
|
||||||
## Tests (luajit, mocks)
|
|
||||||
|
|
||||||
- mest trængende ingrediens vælges; vare og AD-valg skiftes; rigtig markør returneres
|
|
||||||
- ingrediens uden plads i målet springes over
|
|
||||||
- ingrediens uden kilde springes over → næste i rækken
|
|
||||||
- kilde i anden mappe end første markør findes
|
|
||||||
- aflæsningsmålets egen station bruges ikke som kilde
|
|
||||||
- gemt liste genbruges når AD's valg er indsnævret; brugerændring nulstiller/erstatter
|
|
||||||
- én vare valgt → eksisterende opførsel uændret
|
|
||||||
- intet behov / ingen aflæsningsstation / API-fejl → `ORIGINAL` (+ WARN ved fejl)
|
|
||||||
|
|
||||||
## Kendte begrænsninger
|
|
||||||
|
|
||||||
- AD læsser fuld vogn; er der mindre plads end ét læs, venter traktoren ved aflæsning (AD-standard).
|
|
||||||
- Ingredienslisten lever kun i hukommelsen — efter savegame-reload multi-vælges varerne igen.
|
|
||||||
- Forudsætter at målet er en placeable med aflæsningstrigger (produktionspunkt/silo/stald),
|
|
||||||
ikke en blandevogn. Bekræftes ved første in-game test via log.txt.
|
|
||||||
- Server-side logik; MP-klienter ser vareskiftet via AD's eget dirty-flag.
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
# Runs-UI ("Cow Feed") — design
|
|
||||||
|
|
||||||
Dato: 2026-09-22 · Status: godkendt af bruger (sektion 1–3) · Version: v1.8
|
|
||||||
|
|
||||||
## Problem
|
|
||||||
|
|
||||||
Al opsætning sker i dag pr. traktor i AutoDrives HUD: mål, multi-valg af varer (tændknap for
|
|
||||||
behovsstyring), mapper + rotation, loop-tæller. Med fire ens "Fastrac 2135" til to stalde og flere
|
|
||||||
Wait-puljer på vej (Cow/Pig/Sheep Wait) mangler overblik og central kontrol: *hvad* kører *hvor*
|
|
||||||
med *hvilke varer*, og hvor venter de.
|
|
||||||
|
|
||||||
## Løsning
|
|
||||||
|
|
||||||
Et **run** er den centrale enhed: navn, aflæsningsmål, varer, Wait-pulje, tildelte traktorer,
|
|
||||||
loop-tæller. En ESC-menu-fane ("Smart Pickup") viser og redigerer runs og starter/stopper dem.
|
|
||||||
"Start" skriver run'et ind i hver traktors AutoDrive-tilstand og kalder AD's start — alt det
|
|
||||||
eksisterende (behovsstyring, fabriks-inputs, Wait-punkter) virker uændret ovenpå, fordi det
|
|
||||||
læser præcis den tilstand. Traktorer uden run kører som i dag.
|
|
||||||
|
|
||||||
Valgt tilgang: **run-lag oven på AutoDrives tilstand** (fravalgt: egen dispatcher der kalder AD's
|
|
||||||
lav-niveau-opgaver — genopfinder P&D og knækker ved AD-opdateringer; konfig-fil uden UI — ikke det
|
|
||||||
brugeren bad om).
|
|
||||||
|
|
||||||
## 1. Datamodel og lagring
|
|
||||||
|
|
||||||
```
|
|
||||||
Run
|
|
||||||
id løbenummer (stabilt, unikt i savegame)
|
|
||||||
name "Cow Feed"
|
|
||||||
targetWayPointId AD-waypoint-id for mål-markøren (stald/fabrik); vises som markørnavn
|
|
||||||
ingredientMode "auto" | "manual"
|
|
||||||
ingredients liste af fillType-NAVNE (kun ved manual), fx {"STRAW","FORAGE"}
|
|
||||||
waitPoolGroup AD-mappenavn (fx "Cow Wait"); tom streng = alle Wait-mapper, nærmeste ledige
|
|
||||||
loops 0 = uendeligt, N = antal ture (AD's loop-tæller)
|
|
||||||
vehicleIds liste af køretøjs-referencer (se nedenfor)
|
|
||||||
|
|
||||||
Runtime (ikke gemt):
|
|
||||||
isRunning afledt: mindst én tildelt traktor har AD aktiv og run-medlemskab
|
|
||||||
runMembership vehicle -> run.id (svagt nøglet tabel)
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Køretøjs-reference:** AD's `uniqueId` (streng) når den findes på køretøjet, ellers savegame-id
|
|
||||||
(`vehicle.currentSavegameId`). Navne bruges aldrig som nøgle (fire hedder "Fastrac 2135 4WS").
|
|
||||||
- **WaitPool** er afledt, ikke gemt: alle AD-mapper med ordet "Wait" i navnet (`ADSupplyPlanner.isWaitMarker`).
|
|
||||||
Omdøbning sker i AutoDrive; vi læser kun.
|
|
||||||
- **Fil:** `savegameN/ADSmartPickup_runs.xml`, samme mappe som `AutoDrive_config.xml`. Skrives i
|
|
||||||
spillets gem-hook (som AD), læses ved `loadMap`. Ukendte køretøjs-referencer droppes med loglinje;
|
|
||||||
run'et beholdes. Runs der kørte ved gem genstartes IKKE ved load (AD gendanner selv aktive køretøjer).
|
|
||||||
- **Regler:** en traktor kan kun være i ét run (tildeles den et andet, flyttes den — UI'et siger det).
|
|
||||||
Singleplayer først; multiplayer kræver events og er uden for scope.
|
|
||||||
|
|
||||||
XML-format:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<ADSmartPickupRuns version="1">
|
|
||||||
<run id="1" name="Cow Feed" target="11066" ingredientMode="manual" ingredients="STRAW FORAGE"
|
|
||||||
waitPool="Cow Wait" loops="0">
|
|
||||||
<vehicle id="..."/>
|
|
||||||
<vehicle id="..."/>
|
|
||||||
</run>
|
|
||||||
</ADSmartPickupRuns>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. UI — ESC-fane "Smart Pickup"
|
|
||||||
|
|
||||||
Forbillede: FarmOperationsDashboard (`TabbedMenuFrameElement`, XML-layout med `SmoothList`,
|
|
||||||
`MultiTextOption`, `Button`; registreret via `g_gui:loadGui` + indsættelse i `InGameMenu.pagingElement`).
|
|
||||||
|
|
||||||
```
|
|
||||||
┌ Smart Pickup ─────────────────────────────────────────────────────────────┐
|
|
||||||
│ [Runs] [Wait-puljer] (under-faner) │
|
|
||||||
├──────────────────┬────────────────────────────────────────────────────────┤
|
|
||||||
│ RUNS │ RUN: Cow Feed ● Kører (2/2) │
|
|
||||||
│ ▶ Cow Feed 2/2 │ Mål: [Cow 1 Food ▼] (AD-markør) │
|
|
||||||
│ Cow 2 Feed 1/2 │ Varer: (•) Auto: alt målet tager / fabrikken │
|
|
||||||
│ TMR Fabrik 1/1 │ ( ) Vælg: [x] Halm [x] TMR [ ] Vand │
|
|
||||||
│ │ Wait-pulje: [Cow Wait ▼] (6 punkter)│
|
|
||||||
│ [+ Nyt run] │ Ture (loop): [0 = uendeligt ◄►] │
|
|
||||||
│ [Slet run] │ Traktorer: │
|
|
||||||
│ │ Fastrac 2135 (#1) vogn: 1.000.000 l halm+TMR ✔ │
|
|
||||||
│ │ Fastrac 2135 (#2) vogn: — INGEN VOGN ✖ │
|
|
||||||
│ │ [+ Tilføj traktor ▼] [Fjern] │
|
|
||||||
│ │ [▶ Start run] [■ Stop run] Status: 1 advarsel │
|
|
||||||
└──────────────────┴────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Runs-liste (venstre):** alle runs med "kører X af Y". Valg → detaljer til højre. `+ Nyt run`
|
|
||||||
opretter "Run N" (navn redigeres via AD-lignende tekstdialog). `Slet run` spørger ja/nej.
|
|
||||||
- **Mål:** dropdown over AD-markører der ligger ved en aflæsningsstation (`getUnloadStationAtMarker`
|
|
||||||
≠ nil) — ikke alle markører.
|
|
||||||
- **Varer:** *Auto* = det eksisterende: fabrik → ønskede produktioners inputs (v1.6); stald → alle
|
|
||||||
varer stalden tager (`getHusbandryIsFillTypeSupported`); andet mål → stationens understøttede varer.
|
|
||||||
*Vælg* = afkrydsning blandt målets mulige varer.
|
|
||||||
- **Wait-pulje:** dropdown "Alle (nærmeste ledige)" + hver mappe med "Wait" i navnet (antal punkter).
|
|
||||||
- **Ture:** 0–99.
|
|
||||||
- **Traktorer:** dropdown over farmens køretøjer med `vehicle.ad` (navn + `#løbenummer`). Pr. række:
|
|
||||||
vogn-kapacitet (AD `getAllFillLevels`), hvilke af run'ets varer vognen kan bære, og ✔/✖ med tekst:
|
|
||||||
"ingen vogn", "kan ikke bære: TMR", "N m fra vejnettet", "i run X (flyttes)".
|
|
||||||
- **Start/Stop** gælder hele run'et; ✖-traktorer springes over med besked i rækken.
|
|
||||||
- **Wait-puljer-fanen:** liste over Wait-mapper: antal punkter, hvilke traktorer holder hvor lige nu
|
|
||||||
(fra `adUnloadWait`'s reservationer). Kun visning.
|
|
||||||
- Live-opdatering hvert sekund mens siden er åben. Dansk + engelsk l10n. Ingen nye tastaturgenveje.
|
|
||||||
|
|
||||||
## 3. Start/Stop, integration, fejl og test
|
|
||||||
|
|
||||||
**Start run** (pr. tildelt traktor):
|
|
||||||
1. Validér: vogn tilkoblet og kan bære ≥1 af run'ets varer (`AutoDrive.getSupportedFillTypesOfAllUnitsAlphabetically`);
|
|
||||||
≤ 30 m fra vejnettet (`ADGraphManager:getDistanceFromNetwork`); mål-markør findes. Fejl → ✖ på rækken,
|
|
||||||
traktoren springes over, run'et fortsætter med de andre.
|
|
||||||
2. Skriv AD-tilstand: `stateModule:setMode(AutoDrive.MODE_PICKUPANDDELIVER)`;
|
|
||||||
`setSecondMarkerByWayPointId(target)`; `setFirstMarkerByWayPointId(nærmeste markør med læssestation)`
|
|
||||||
(kun en gyldig start — moden vælger reel kilde pr. tur); `selectedFillTypes` = run'ets varer
|
|
||||||
(Auto: udledt liste) + `raiseDirtyFlag`; loop-tæller; per-køretøj `vehicle.ad.settings.useFolders`
|
|
||||||
= til og `rotateTargets` = kun pålæsning (`current` og `new`).
|
|
||||||
3. `vehicle:startAutoDrive()`.
|
|
||||||
4. `runMembership[vehicle] = run.id`.
|
|
||||||
|
|
||||||
**Stop run:** `vehicle:stopAutoDrive()` pr. tildelt traktor; medlemskab bevares (konfigureret, ikke kørende).
|
|
||||||
|
|
||||||
**Integration:** de eksisterende hooks læser AD-tilstanden uændret. Run'et læses to steder:
|
|
||||||
(a) `adUnloadWait`: Wait-kandidater begrænses til run'ets mappe, hvis sat; (b) `choosePickup`:
|
|
||||||
for run-traktorer tages ingredienslisten fra run'et (Auto/manual) i stedet for "≥2 valgt i dropdown",
|
|
||||||
så tændknappen ikke længere kræves. Traktorer uden run: præcis som i dag.
|
|
||||||
|
|
||||||
**Fejlhåndtering:** al UI-kode og Start/Stop i `pcall`; fejl vises i status-linjen og logges med
|
|
||||||
`ADSmartPickup:`-præfiks; aldrig crash. Manglende AD-API → fanen viser "AutoDrive-API ikke fundet",
|
|
||||||
resten af moden upåvirket.
|
|
||||||
|
|
||||||
**Test:** rene funktioner i luajit (run-model: opret/omdøb/tildel/flyt; validering; Auto-vareliste;
|
|
||||||
XML ↔ tabel; kandidat-filtrering pr. Wait-pulje). GUI og AD-skrivning verificeres in-game efter
|
|
||||||
tjekliste: opret run → tildel 2 traktorer (én uden vogn) → Start (én ✖) → Stop → gem/load → run bevaret.
|
|
||||||
|
|
||||||
**Afgrænsning (YAGNI):** singleplayer; ingen kilde-begrænsning pr. run; ingen omdøbning af
|
|
||||||
Wait-mapper; ingen auto-genstart ved load; ingen genvejstast-dialog.
|
|
||||||
|
|
||||||
## Filer (forventet)
|
|
||||||
|
|
||||||
- `FS25_ADSmartPickup/adRuns.lua` — run-model + XML (rene funktioner hvor muligt)
|
|
||||||
- `FS25_ADSmartPickup/adRunsController.lua` — Start/Stop, validering mod AD/spil-API, medlemskab
|
|
||||||
- `FS25_ADSmartPickup/gui/SmartPickupFrame.lua` + `gui/SmartPickupFrame.xml` + `gui/guiProfiles.xml`
|
|
||||||
- `FS25_ADSmartPickup/l10n/l10n_da.xml`, `l10n_en.xml`
|
|
||||||
- `tests/test_adRuns.lua`
|
|
||||||
- Tilpasninger i `adSmartPickup.lua` (ingredienser fra run) og `adUnloadWait.lua` (pulje-filter)
|
|
||||||
|
|
||||||
## Efterfølgende (ikke i v1.8): læs-bytte fra Wait
|
|
||||||
|
|
||||||
In-game 2026-09-22 21:55: begge stalde fulde af halm, mangler TMR; alle fire traktorer venter ved
|
|
||||||
Cow Wait med vognene fulde af halm → ingen kan hente TMR (deadlock).
|
|
||||||
|
|
||||||
Design: mens en traktor venter, tjekkes målets behov. Hvis målet er fuldt for vognens vare, og en
|
|
||||||
ANDEN vare i traktorens ingrediensliste mangler så meget at et fuldt læs passer (hysterese), og
|
|
||||||
vognen kan bære den: find en silo (lager-placeable med aflæsningstrigger ved en AD-markør, fri plads
|
|
||||||
≥ rest, rute til/fra) — foretræk den moden ellers henter varen fra — og omdiriger aflæsningsopgaven
|
|
||||||
dertil. AD læsser af, vognen er tom, næste tur vælger moden den manglende vare efter behov. Log:
|
|
||||||
"bytter læs: N l STRAW tilbage i <silo>, målet mangler FORAGE". Ingen silo → bliv ved Wait.
|
|
||||||
Kræver at traktoren har begge varer i sin liste (run eller multi-valg).
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
# Flow-tavle — design
|
|
||||||
|
|
||||||
Dato: 2026-09-23 · Status: godkendt af bruger (grundform valgt: flow-tavle) · Version: v1.13
|
|
||||||
|
|
||||||
## Problem
|
|
||||||
|
|
||||||
Opsætningen er markør-baseret: brugeren vælger AD-markører som kilder og mål, og moden gætter
|
|
||||||
bygningen ud fra markørens geometri (trigger bag næsen, 40 m, 12 m). Fejl viser sig som
|
|
||||||
"kilde-markør 10873 er ikke …" i stedet for "Kostald 1", først når run'et starter. Forsyning og
|
|
||||||
Udkørsel er to faner med hver sin lange rulleside, Oversigt er en tredje. Brugeren: "det er noget rod".
|
|
||||||
|
|
||||||
## Løsning
|
|
||||||
|
|
||||||
Én tavle med ét flow pr. række: **Vare · Fra · Til · Regel · Traktorer · Status**. Fra og Til vælges
|
|
||||||
som **bygninger**; moden finder selv markøren ved bygningens læsse-/aflæsningstrigger og viser den.
|
|
||||||
Redigering sker i en detail-blok under tavlen for det valgte flow. Faner: **Flows** · **Lagre**.
|
|
||||||
Motoren (planlægger, hooks, XML-model) er uændret: et flow ER et run; bygninger oversættes til
|
|
||||||
markører når run'et startes og planlægges.
|
|
||||||
|
|
||||||
Fravalgt: "Bygninger først" (runs implicit → svært at se hvad der kører) og "Sætnings-run"
|
|
||||||
(mindst ændring, men fanerne forbliver adskilte).
|
|
||||||
|
|
||||||
## 1. Bygninger (`adBuildings.lua`, spil-adapter)
|
|
||||||
|
|
||||||
`ADBuildings.list(adEnv, farmId)` → liste af bygninger gården kan bruge:
|
|
||||||
|
|
||||||
```
|
|
||||||
{ id, name, kind = "factory"|"silo"|"husbandry"|"objectStorage"|"sell",
|
|
||||||
placeable, loadMarker = {id, name, distance} | nil, unloadMarker = {…} | nil,
|
|
||||||
canBeSource = bool, canBeTarget = bool, canSell = bool }
|
|
||||||
```
|
|
||||||
|
|
||||||
- Kilde: fabrik, silo, stald (læssestation), objektlager. Mål (forsyning): stald, fabrik.
|
|
||||||
Salg: SellingStation der er et rigtigt salgssted (ikke fabrikkers/staldes indleveringer).
|
|
||||||
Siloer og palleskure er automatiske lagermål (som i dag) og vælges ikke.
|
|
||||||
- `id` = `placeable.uniqueId`; mangler det, `"pos:" .. afrundet x/z`.
|
|
||||||
- Markør-opslag: for hver AD-markør (Wait-markører udelukket) afstanden til bygningens
|
|
||||||
læsse-triggere (loadTriggers.triggerNode) og aflæsnings-triggere (unloadTriggers exactFillRootNode/
|
|
||||||
triggerNode/aiNode) samt objektlagerets/pallespawnerens node; nærmeste ≤ 40 m vinder.
|
|
||||||
Siloer bruger det strenge opslag (bag næsen) for læssemarkøren, som i dag.
|
|
||||||
- Én scanning pr. sekund højst (cache), da fanen spørger ved hver opdatering.
|
|
||||||
- `ADBuildings.find(adEnv, farmId, id)`, `ADBuildings.byMarker(adEnv, farmId, wayPointId)` (omvendt
|
|
||||||
opslag til migration).
|
|
||||||
|
|
||||||
## 2. Model
|
|
||||||
|
|
||||||
Run får `sourceBuildingIds` og `targetBuildingIds` (lister af bygnings-id). De eksisterende
|
|
||||||
`sourceWayPointIds`/`targetWayPointIds` bevares som **afledte** felter: controlleren udfylder dem fra
|
|
||||||
bygningerne ved Start og ved hver plan (`ADRunsController.resolveMarkers(adEnv, run)`), så
|
|
||||||
planlægger og hooks er uændrede. XML-version 3 skriver `#sourceBuildings`/`#targetBuildings`;
|
|
||||||
version 2-filer læses, og bygninger udledes fra markørerne ved første indlæsning
|
|
||||||
(`ADBuildings.byMarker`); kan en markør ikke oversættes, beholdes den som rå markør.
|
|
||||||
|
|
||||||
Forsyning: Fra = "alle siloer" (ingen valg; sources er alle markører som i dag). Til = bygninger.
|
|
||||||
Udkørsel: Fra = bygninger. Til = politik pr. vare (Lager/Salg/Salg, ellers lager) — målet vælges
|
|
||||||
automatisk pr. tur som i dag.
|
|
||||||
|
|
||||||
## 3. Fanen Flows
|
|
||||||
|
|
||||||
**Tavle** (øverst, tekstrækker, én pr. run, den valgte markeret med "▶"):
|
|
||||||
`Mælk · Kostald 1 + Kostald 2 → Mejeri (salg) · pris ≥ 90 % · Virtus · venter: 42 % af top`
|
|
||||||
- Vare: run'ets varer (forsyning auto = "behov"), Fra/Til = bygningsnavne (+ "!" hvis en bygning
|
|
||||||
mangler markør), Regel = kort tekst, Traktorer = navne, Status = én linje (som Oversigt i dag:
|
|
||||||
kører/holder/venter/begrundelse/advarsel).
|
|
||||||
- Under tavlen: Wait-puljer (som i dag) og en linje "Vælg flow ovenfor for at rette".
|
|
||||||
|
|
||||||
**Detail-blok for valgt flow** (efter tavlen; run-vælgeren `Flow:` er en MultiTextOption):
|
|
||||||
- *Retning*: Ind til bygning / Ud fra bygning (klonet Ja/Nej-række).
|
|
||||||
- *Fra* (kun Ud): afkrydsning pr. bygning der kan være kilde: `Kostald 1 (stald) — markør 'cow 1 milk' 12 m` eller `— INGEN MARKØR inden for 40 m`.
|
|
||||||
- *Til* (kun Ind): afkrydsning pr. stald/fabrik med samme markørtekst.
|
|
||||||
- *Varer*: Ind: Auto/Vælg + afkrydsning; Ud: politik pr. vare med mængde og pris-% (som i dag).
|
|
||||||
- *Regler*: Ind: ture, kør hjem under; Ud: sælg fra, lager over, saml op.
|
|
||||||
- *Vent ved*: Wait-pulje.
|
|
||||||
- *Traktorer*: som i dag (låst når vognen ikke passer).
|
|
||||||
- Knapper: Nyt flow / Omdøb / Slet / Start-Stop (som i dag). Nyt flow spørger navn og starter som "Ind".
|
|
||||||
|
|
||||||
Fanen **Lagre** uændret. Oversigt og de to gamle faner udgår.
|
|
||||||
|
|
||||||
## 4. Validering og fejl
|
|
||||||
|
|
||||||
- Bygning uden markør: rækken siger det, tavlen viser "!" ved navnet, Start afviser med
|
|
||||||
`noMarker:<bygning>` i statuslinjen ("Kostald 2 mangler en AD-markør ved mælketanken").
|
|
||||||
- Bygning der er forsvundet (solgt): udgår af listen; run'et beholder id'et og viser "(mangler)".
|
|
||||||
- Alle eksisterende afvisningsgrunde uændrede.
|
|
||||||
|
|
||||||
## 5. Tests
|
|
||||||
|
|
||||||
- `adBuildings`: kind pr. placeable, markør-opslag (nærmeste ≤ 40 m, Wait udelukket, silo strengt),
|
|
||||||
byMarker, id-fallback.
|
|
||||||
- model: v3 roundtrip, v2-migration med byMarker-oversættelse, rå markør bevares.
|
|
||||||
- controller: resolveMarkers udfylder waypoint-lister; noMarker-validering; Start med bygninger.
|
|
||||||
- GUI in-game (savegame 3 og 2).
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
# Udkørsel af produkter — design
|
|
||||||
|
|
||||||
Dato: 2026-09-23 · Status: godkendt af bruger (afsnit 1–4) · Version: v1.12
|
|
||||||
|
|
||||||
## Problem
|
|
||||||
|
|
||||||
Modden styrer i dag kun forsyning *ind* til stalde og fabrikker. Fabrikkernes output (sukker,
|
|
||||||
TMR, halmvarer på paller) og varer der står på lager skal køres *ud*: til en silo som
|
|
||||||
mellemlager, eller til et salgssted — men kun når prisen er god, så man ikke sælger i bund.
|
|
||||||
Uden det løber fabrikkernes udgangslager fuldt, og produktionen stopper. Paller ligger ofte i
|
|
||||||
objektlagre (fx HVF-pakkens palle-/ballelager) hvor man normalt klikker "tag ud" manuelt.
|
|
||||||
|
|
||||||
## Løsning
|
|
||||||
|
|
||||||
Et run får en **retning**: *Forsyning* (som i dag) eller *Udkørsel*. Et udkørsels-run har en
|
|
||||||
liste af **kilder** (fabrik, silo eller objektlager), en **vareliste** med politik pr. vare
|
|
||||||
(Lager / Salg / Salg, ellers lager / Fra), en **pristærskel** (% af årets toppris), en
|
|
||||||
**lagertærskel** (% fyldning) og valget **opsamlingsrunde**. Før hver tur vurderer en ren
|
|
||||||
planlægger om en tur er berettiget, vælger vare(r) + mål, og sætter AutoDrives tilstand;
|
|
||||||
AutoDrive kører, læsser (også paller via Universal Autoload) og læsser af. Ingen tur → traktoren
|
|
||||||
holder ved et Wait-punkt og prøver igen hvert 5. sekund.
|
|
||||||
|
|
||||||
Valgt tilgang: nyt modul der wrapper AutoDrives læsseopgave (samme mønster som Wait-modulet
|
|
||||||
wrapper aflæsseopgaven). Fravalgt: AutoDrives egen "automatisk aflæsningsmål" (ingen pristærskel,
|
|
||||||
ingen lager-mulighed, ingen ventelogik) og egen kørselsstyring med DriveTo-opgaver (genopfinder
|
|
||||||
AutoDrives læsse-/pallehåndtering).
|
|
||||||
|
|
||||||
## 1. Faner og datamodel
|
|
||||||
|
|
||||||
### Faner i ESC-siden
|
|
||||||
|
|
||||||
1. **Oversigt** (som i dag): alle runs med status, traktorernes aktivitet, behovsrække. Udkørsels-
|
|
||||||
runs vises med prislinje i stedet for behov: `SUGAR 412 kr (78 % af top) → venter`.
|
|
||||||
2. **Forsyning**: de nuværende runs, samme indhold, grupperet i blokke med overskrift:
|
|
||||||
*Run* (vælger + Nyt/Omdøb/Slet) · *Mål og varer* · *Ventepunkter og ture* · *Traktorer* ·
|
|
||||||
*Status* (+ Start/Stop).
|
|
||||||
3. **Udkørsel**: *Run* · *Kilder* (liste af markører, tilføj/fjern) · *Varer* (én række pr. vare:
|
|
||||||
navn, politik, aktuel pris i % af top) · *Regler* (pristærskel 50–100, standard 90;
|
|
||||||
lagertærskel 50–95, standard 80; opsamlingsrunde Ja/Nej) · *Ventepunkter* · *Traktorer* ·
|
|
||||||
*Status* (+ Start/Stop).
|
|
||||||
|
|
||||||
Run-vælger, Wait-pulje, traktorliste, knapper og statuslinje er fælles byggeklodser; hver fane
|
|
||||||
viser kun runs med sin egen `kind`.
|
|
||||||
|
|
||||||
### Kilder
|
|
||||||
|
|
||||||
Kilde = AD-markør. Kildetypen genkendes ved markøren (nærmeste inden for 40 m, som triggere i
|
|
||||||
dag):
|
|
||||||
|
|
||||||
- **Fabrik** (produktionspunkt): tilgængeligt pr. vare = udgangslager + paller af varen der
|
|
||||||
allerede står ved pallespawneren (inden for 25 m af spawner-noden).
|
|
||||||
- **Silo** (spec_silo med læssestation): tilgængeligt = lagerniveau for varen.
|
|
||||||
- **Objektlager** (spec_objectStorage): tilgængeligt = antal × fyldning pr. palle for varen
|
|
||||||
(`objectInfos[*].objects[*].palletAttributes`). Udtag sker automatisk, se afsnit 3.
|
|
||||||
|
|
||||||
Varelisten for et run = alle varer kilderne rummer eller producerer, som mindst én tildelt vogn
|
|
||||||
kan bære. Bulk-varer kræver tipvogn/tank; pallevarer kræver Universal Autoload.
|
|
||||||
|
|
||||||
### Datamodel (adRuns.lua, rene funktioner, ny store ved hver ændring)
|
|
||||||
|
|
||||||
```
|
|
||||||
Run (fælles)
|
|
||||||
id, name, waitPoolGroup, vehicleIds, kind = "supply" | "outbound"
|
|
||||||
Run (supply, uændret)
|
|
||||||
targetWayPointId, targetWayPointIds, ingredientMode, ingredients, loops, returnBelowPercent
|
|
||||||
Run (outbound)
|
|
||||||
sourceWayPointIds liste af AD-waypoint-id'er (1..n)
|
|
||||||
products liste af {fillType = NAVN, policy = "store"|"sell"|"sellElseStore"|"off"}
|
|
||||||
sellAtPercent 50..100, standard 90
|
|
||||||
storeAbovePercent 50..95, standard 80
|
|
||||||
collectRound bool, standard false
|
|
||||||
```
|
|
||||||
|
|
||||||
XML-version 2 i samme `ADSmartPickup_runs.xml`. Version 1-filer læses og får `kind="supply"`.
|
|
||||||
Ukendte varer i `products` (mod fjernet) ignoreres ved indlæsning med én log-linje.
|
|
||||||
|
|
||||||
## 2. Turvurdering og prisregel
|
|
||||||
|
|
||||||
`adOutboundPlanner.lua` er rent Lua (ingen spil-API). Input: run, vognens kapacitet (liter) og
|
|
||||||
om den har UAL, kildestatus pr. vare `{available, level, capacity}` pr. kilde, prisstatus pr.
|
|
||||||
vare `{percent, stationId, reachable}`. Output: en tur eller `nil` + begrundelse.
|
|
||||||
|
|
||||||
Pr. vare:
|
|
||||||
|
|
||||||
- **Tilgængeligt**: sum over kilderne.
|
|
||||||
- **Fuldt læs**: tilgængeligt ≥ vognens kapacitet.
|
|
||||||
- **Lager højt**: mindst én kilde har `level/capacity ≥ storeAbovePercent/100` for varen.
|
|
||||||
- **Pris-%**: bedst betalende *nåelige* salgssted der tager varen (adPrices). Toppris =
|
|
||||||
stationens aktuelle grundpris for varen × (årets højeste periodefaktor ÷ nuværende periodes
|
|
||||||
faktor). Pris-% = aktuel effektiv pris ÷ toppris. Stor efterspørgsel giver > 100 % og opfylder
|
|
||||||
altid reglen. Mangler stationens grundpris, bruges `fillType.pricePerLiter × højeste faktor ×
|
|
||||||
sværhedsgradens multiplikator` som toppris.
|
|
||||||
|
|
||||||
Beslutning, i rækkefølge:
|
|
||||||
|
|
||||||
1. **Salgskandidater**: politik ∈ {sell, sellElseStore}, pris-% ≥ sellAtPercent, tilgængeligt > 0.
|
|
||||||
Kører **uanset mængde**. Rangering: højeste pris-% først, derefter størst tilgængeligt.
|
|
||||||
2. **Lagerkandidater**: politik ∈ {store, sellElseStore} og (fuldt læs ELLER lager højt).
|
|
||||||
Rangering: højeste fyldningsgrad først, derefter størst tilgængeligt.
|
|
||||||
3. Ingen kandidat → hold ved Wait. Log én gang pr. ændret begrundelse:
|
|
||||||
`venter: SUGAR 78 % af top (kræver 90), lager 41 %`.
|
|
||||||
|
|
||||||
**Opsamlingsrunde** (`collectRound`):
|
|
||||||
|
|
||||||
- Bulk-vogn: én vare, men fra flere kilder efter tur indtil vognen er fuld (afsnit 3).
|
|
||||||
- UAL-vogn: alle kandidater med samme mål på én tur; vognens vareliste sættes til dem alle, og
|
|
||||||
AutoDrive skifter selv vare når der ikke kommer flere af den første.
|
|
||||||
|
|
||||||
**Mål**:
|
|
||||||
|
|
||||||
- Salg: det salgssted der betaler bedst for hovedvaren (første i rangeringen), tager alle rundens
|
|
||||||
varer, og kan nås begge veje.
|
|
||||||
- Lager: nærmeste silo-markør med læsse+aflæsningsstation der tager varen, fri plads ≥ hele læsset,
|
|
||||||
ikke selv kilde, fabrik eller stald, rute begge veje (samme regler som læs-byttet).
|
|
||||||
|
|
||||||
Prisen tjekkes kun ved turens start.
|
|
||||||
|
|
||||||
## 3. Kørsel, hold og pallelæsning
|
|
||||||
|
|
||||||
`adOutbound.lua` med to hooks:
|
|
||||||
|
|
||||||
**Start (controller).** Som forsyning: PickupAndDeliver, rotation "kun pålæsning", mapper til.
|
|
||||||
1. markør = første kilde, 2. markør = første kilde (pladsholder, sættes pr. tur). Turen
|
|
||||||
registreres i `activePickups`, så "stå og læs"-grebet og 30 s-sikkerhedsventilen gælder.
|
|
||||||
|
|
||||||
**Hook 1 — næste pålæsning** (det eksisterende `getNextPickup`-wrap): udkørsels-traktor →
|
|
||||||
planlæggeren spørges. Tur → vognens `selectedFillTypes` = turens varer, 2. markør = målet,
|
|
||||||
kilden returneres. Ingen tur → ledigt Wait-punkt fra run'ets pulje returneres (reservation +
|
|
||||||
rutetjek via adWaitPool), traktoren markeres "holder".
|
|
||||||
|
|
||||||
**Hook 2 — læsseopgaven** (prepend på `LoadAtDestinationTask.update`):
|
|
||||||
|
|
||||||
- *Holder ved Wait:* traktoren stoppes ved punktet; planlæggeren spørges hvert 5. sekund. Tur →
|
|
||||||
opgaven omdirigeres til kilden (`redirect`), varer + 2. markør sættes, "holder" slippes.
|
|
||||||
- *Ved objektlager:* når markøren er nået, traktoren står stille og AutoDrive har aktiveret UAL:
|
|
||||||
kald `placeable:removeAbstractObjectsFromStorage(objectInfoIndex, 1, nil)` for turens vare;
|
|
||||||
vent til lagerets antal falder OG UAL's antal læssede stiger; gentag. Stop når
|
|
||||||
`ualIsFull()`, lageret er tomt for varen, eller intet er sket i 30 s → `task:finished()`.
|
|
||||||
Ved runde med flere pallevarer tages varerne efter tur. `connection = nil` så
|
|
||||||
AutomaticBaleStorage-moddens kø ikke aktiveres.
|
|
||||||
- *Kør uanset mængde:* stiger vognens fyldning ikke i 15 s og vognen ikke er tom → `task:finished()`.
|
|
||||||
- *Runde, bulk:* når læsning stopper ved kilde A med fri plads ≥ 10 % og en anden kilde i listen
|
|
||||||
har samme vare, omdirigeres opgaven til kilde B i stedet for at afslutte. Hver kilde højst én
|
|
||||||
gang pr. tur.
|
|
||||||
|
|
||||||
**Aflæsning** er AutoDrives egen (tip i silo; tip eller UAL-aflæsning ved salgssted).
|
|
||||||
`adUnloadWait` springer udkørsels-traktorer over (run'ets `kind`). Efter aflæsning går AutoDrive
|
|
||||||
selv til næste pålæsning → hook 1 igen. Stop som i dag.
|
|
||||||
|
|
||||||
`adWaitPool.lua` udskilles fra `adUnloadWait.lua`: reservationer (markør-id → køretøj),
|
|
||||||
`hasRouteTo`/`hasRouteBothWays`, `redirect`, kandidatliste for en pulje. Begge moduler bruger den.
|
|
||||||
|
|
||||||
## 4. Fejl, advarsler og tests
|
|
||||||
|
|
||||||
**Uopnåeligt salgssted / ingen silo.** Kan det bedst betalende salgssted ikke nås, tages det
|
|
||||||
næstbedste nåelige. Kan intet nås (eller ingen silo med plads ved Lager), vises en rød
|
|
||||||
AutoDrive-notifikation på skærmen
|
|
||||||
(`AutoDriveMessageEvent.sendMessageOrNotification(vehicle, ERROR, …)`):
|
|
||||||
`ADSmartPickup: 'MT635' kan ikke nå salgssted 'Grain Elevator' for SUGAR – tjek vejnettet`.
|
|
||||||
Samme tekst i AutoDrives notifikations-historik, log.txt (WARN) og run'ets statuslinje.
|
|
||||||
Traktoren holder ved Wait og prøver igen hvert 5. sekund. Gentages først når traktor, station
|
|
||||||
eller vare ændrer sig.
|
|
||||||
|
|
||||||
**Fejlhåndtering:** spil-API-kald i pcall; fejl → hold ved Wait + WARN én gang, aldrig
|
|
||||||
AutoDrive-stop. Validering ved Start (nye grunde): `noSource` (markør uden genkendelig kilde),
|
|
||||||
`needsAutoload` (pallevare uden UAL), `noProducts` (ingen vare med politik ≠ Fra). Objektlager
|
|
||||||
der ikke spawner → 30 s-vagt afslutter læsningen med det der er.
|
|
||||||
|
|
||||||
**Tests (luajit, uden spil):**
|
|
||||||
|
|
||||||
- planlægger: prisregel, tærskler, alle fire politikker, rangering, runde bulk/UAL, uanset mængde
|
|
||||||
- priser: toppris, pris-%, stor efterspørgsel > 100 %, fallback uden stationspris
|
|
||||||
- model + XML v2 + migration fra v1
|
|
||||||
- objektlager-udtag som tilstandsmaskine mod fake-lager (fuld, tom, stall)
|
|
||||||
- kilde-genkendelse mod fake-markører (fabrik/silo/objektlager, paller ved spawner)
|
|
||||||
- controller-validering (noSource, needsAutoload, noProducts)
|
|
||||||
|
|
||||||
**In-game (savegame 3):** sukker → salgssted ved høj pris; TMR → Farma-silo; halmkurve fra
|
|
||||||
fabrik og fra objektlager → halm-salgsstedet (UAL); uopnåeligt salgssted → advarsel på skærm.
|
|
||||||
|
|
||||||
## Filer (forventet)
|
|
||||||
|
|
||||||
```
|
|
||||||
FS25_ADSmartPickup/
|
|
||||||
adRuns.lua kind, sourceWayPointIds, products, tærskler, collectRound; XML v2
|
|
||||||
adRunsStorage.lua v2 læs/skriv, v1-migration
|
|
||||||
adOutboundPlanner.lua NY, ren
|
|
||||||
adPrices.lua NY, ren del + spiladapter
|
|
||||||
adSources.lua NY, kilde-genkendelse + lagerstatus
|
|
||||||
adOutbound.lua NY, hooks, objektlager-udtag, hold, runde
|
|
||||||
adWaitPool.lua NY, udskilt fra adUnloadWait.lua
|
|
||||||
adUnloadWait.lua bruger adWaitPool; springer outbound over
|
|
||||||
adRunsController.lua start/validering for outbound
|
|
||||||
adSmartPickup.lua source() af nye filer; getNextPickup-wrap kalder adOutbound
|
|
||||||
gui/SmartPickupFrame.lua faner + fælles blokke
|
|
||||||
gui/SupplyTab.lua NY
|
|
||||||
gui/OutboundTab.lua NY
|
|
||||||
l10n/l10n_da.xml, l10n_en.xml, README.md
|
|
||||||
tests/test_adOutboundPlanner.lua, test_adPrices.lua, test_adSources.lua,
|
|
||||||
tests/test_adOutbound.lua, test_adRuns.lua (v2), test_adRunsController.lua (outbound)
|
|
||||||
```
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
# Markarbejde, etape 1: slåafgrøder (græs, kløver, lucerne)
|
|
||||||
|
|
||||||
Dato: 2026-09-25 · Mod: FS25_ADSmartPickup · Branch: `feature/markarbejde`
|
|
||||||
|
|
||||||
## Mål
|
|
||||||
|
|
||||||
Når en græs-, kløver- eller lucernemark er klar, sender Smart Pickup selv maskinerne ud. Slåmaskinen slår
|
|
||||||
marken via Courseplay. Ved **ensilage** samler en læssevogn skårene op og kører dem til den rigtige silo.
|
|
||||||
Ved **hø** vender en vender først græsset (og en river river det evt. sammen), og derefter samles høet op.
|
|
||||||
Alt kørsel på vej sker med AutoDrive. Alt arbejde på marken sker med Courseplay.
|
|
||||||
|
|
||||||
Succes: en mark sat til *Auto* går fra "klar" til "færdig" med græsset i en silo, uden at spilleren rører
|
|
||||||
noget, og menuen viser hele tiden, hvad der sker og hvorfor.
|
|
||||||
|
|
||||||
Ikke med i etape 1: mejetærskerafgrøder (etape 2), roer (etape 3), jordbehandling og såning, baller.
|
|
||||||
|
|
||||||
## Arbejdsdeling
|
|
||||||
|
|
||||||
| Del | Ansvar |
|
|
||||||
|---|---|
|
|
||||||
| **Smart Pickup (vores)** | Hvilken mark, hvilket trin, hvilken maskine, hvilken silo. Holder status og viser den |
|
|
||||||
| **AutoDrive** | Kørsel på vej til markens `F`-markør og til silo; overtager ved "vogn fuld" og genstarter Courseplay |
|
|
||||||
| **Courseplay** | Markgrænse, kursus, selve arbejdet på marken, "fuld"/"færdig"-signaler |
|
|
||||||
|
|
||||||
## Beslutninger (med begrundelse)
|
|
||||||
|
|
||||||
1. **Faste sæt.** Redskabet sidder fast på traktoren (eller er selvkørende, fx Krone BiG M 450). Mod'en
|
|
||||||
kobler aldrig redskaber på. *Begrundelse:* auto-påkobling var det, der aldrig blev pålideligt i AI-Bonden.
|
|
||||||
2. **Markflåde med forslag og flueben.** Mod'en foreslår sæt ud fra redskabets spec (`spec_mower`,
|
|
||||||
`spec_tedder`, `spec_windrower`, `spec_forageWagon`); kun sæt med flueben bliver sendt ud.
|
|
||||||
*Begrundelse:* mod'en må aldrig tage en maskine, spilleren selv bruger. Et sæt, spilleren sidder i
|
|
||||||
(`getIsControlled`/`getIsEntered`), springes altid over.
|
|
||||||
3. **Staging-markør pr. mark = AutoDrive-markør med navnet `F<marknummer>`** (fx `F109`), stort/småt
|
|
||||||
ligegyldigt, evt. med mellemrum (`F 109`). Mangler den, vises marken med "!" og sendes ikke ud.
|
|
||||||
4. **Auto/Manuel pr. mark.** Auto starter selv ved "klar"; Manuel venter på *Start nu*. Standard: Manuel
|
|
||||||
(intet sker uventet efter opdatering).
|
|
||||||
5. **Plan pr. mark: Ensilage eller Hø.** Standard: Ensilage. **Riv** er et valg pr. mark, standard *fra*:
|
|
||||||
læssevognene i Unrealistic Pack kan køre 40 m bredt og samler spredt hø op.
|
|
||||||
6. **Bredde.** Maskiner kører med deres *aktuelle* bredde (Unrealistic Packs `variableWorkWidth`,
|
|
||||||
`aiKeepCurrentWidth`). Courseplay kan højst 50 m → et sæt over 50 m sendes ikke ud (advarsel).
|
|
||||||
Marken husker **slåbredden**; opsamling/vending/rivning kræver bredde ≥ slåbredden.
|
|
||||||
**Revideret efter in-game test 25/9:** et nyt Courseplay-kursus til læssevognen lå forskudt i forhold til
|
|
||||||
skårene (anden start/retning/forager). Derfor genbruger vender, river og læssevogn **slåmaskinens
|
|
||||||
kursus** (`vehicle:cpCopyCourse`). Kurset gemmes pr. mark; efter genindlæsning hentes det fra
|
|
||||||
slåmaskinen (id gemt som `mowVehicle`), som Courseplay selv gemmer kurset på. Uden kendt slåkursus
|
|
||||||
laves et nyt som reserve (logges).
|
|
||||||
**Revideret igen 25/9 (smalle standardmaskiner):** vender og river kører hele marken i egen bredde med
|
|
||||||
eget kursus (intet breddekrav). Læssevognen følger skårene: uden riv slåmaskinens kursus og bredde ≥
|
|
||||||
slåbredde; med riv rivens kursus (skåret ligger midt i banen) og intet breddekrav. Riv kan vælges til
|
|
||||||
både hø og ensilage.
|
|
||||||
**Sideriver (25/9):** mens riven arbejder, måles nedlægningsområdets midte (runtime-felt
|
|
||||||
`dropWindrowWorkAreaIndex`, ellers riverens XML) i traktorens ramme minus rivens CP-forskydning =
|
|
||||||
skårets sideforskydning (gemt pr. mark). Læssevognen på rivens kursus får `toolOffsetX = egen −
|
|
||||||
skårets` (±10 m) og får sin egen værdi tilbage bagefter.
|
|
||||||
7. **Silovalg** genbruger Smart Pickups motor (`ADLoadSwap`). *Tving lager* pr. mark (som "Lager til" i
|
|
||||||
flows). **Revideret 25/9:** ensilage går først til egne **gæringssiloer** (produktion skår -> SILAGE,
|
|
||||||
fx FermentingSiloPack/GrassSilageSilo), nærmest marken, ellers nærmeste silo med plads; hø kun til
|
|
||||||
almindelige siloer (gæringssiloer laver også hø om til ensilage).
|
|
||||||
8. **Marksilo.** En silo, hvis afstand til markens polygon er ≤ 150 m, er et mellemlager. Den må gerne
|
|
||||||
vælges (korte ture). Oversigten viser "!" ved marken, hvis intet Smart Pickup-udkørselsflow har
|
|
||||||
siloen som kilde — hjemkørslen klares af de eksisterende flows, ikke af markdelen.
|
|
||||||
9. **"Klar"** = ≥ 90 % af markens prøvepunkter står i høstvinduet for markens slåafgrøde
|
|
||||||
(`FSDensityMapUtil.getFruitArea`, samme metode som Arbejdsseddel, bevist i spillet). `field.fieldState`
|
|
||||||
bruges ikke (frossen NPC-plan for spillerejede marker).
|
|
||||||
10. **Slåafgrøde** afgøres ud fra spillets konvertere: `MOWER` giver et skår, og `TEDDER` gør skåret til en
|
|
||||||
`DRY…`-type (græs, lucerne, kløver, kløver2). Rug/bønner (→ halm) er udelukket. Ensilage-varen er
|
|
||||||
skåret (`GRASS_WINDROW` …), hø-varen er tørskåret (`DRYGRASS_WINDROW` …).
|
|
||||||
11. **Én maskine, én mark.** Flere marker kan køre samtidig. Højst 2 læssevogne pr. mark.
|
|
||||||
12. **Rækkefølge.** Næste trin starter først, når det forrige er *færdigt* på hele marken (ingen vender
|
|
||||||
bag slåmaskinen). Enklere og uden kollisioner mellem Courseplay-kurser.
|
|
||||||
13. **Fejl stopper ikke alt.** Kan Courseplay ikke lave kursus, prøves igen efter 5 min; imens står marken
|
|
||||||
med grunden. Andre marker kører videre.
|
|
||||||
14. **Genindlæsning.** Markernes trin gemmes i `ADSmartPickup_fields.xml` i savegame-mappen. Efter
|
|
||||||
indlæsning genoptages trinnet: sæt, der ikke længere kører, bliver sendt ud igen.
|
|
||||||
|
|
||||||
## Opbygning
|
|
||||||
|
|
||||||
Rene moduler (ingen spil-API, testes med `luajit`):
|
|
||||||
|
|
||||||
| Fil | Ansvar |
|
|
||||||
|---|---|
|
|
||||||
| `adFieldModel.lua` | Mark-plan og -status som data: plan, tilstand, overgange (`nextStage`), bredde-regler, gem/indlæs-records |
|
|
||||||
| `adFieldFleet.lua` | Rolle ud fra redskabs-specs, forslag, valg af ledigt sæt (nærmest markøren) |
|
|
||||||
| `adFieldView.lua` | Rækker og tekster til fanen Marker (sortering, filtre, statusord) |
|
|
||||||
|
|
||||||
Spil-moduler (tynde, pcall'ede):
|
|
||||||
|
|
||||||
| Fil | Ansvar |
|
|
||||||
|---|---|
|
|
||||||
| `adFields.lua` | Ejede marker, polygon/prøvepunkter, afgrøde, klar-andel, `F`-markør, marksiloer |
|
|
||||||
| `adCpBridge.lua` | Start Courseplay på en mark (grænse → kursus → start, async med annullering), læs aktiv/færdig |
|
|
||||||
| `adFieldJobs.lua` | Tick hvert sekund: opdag klar, send sæt ud, følg ankomst → CP → færdig, næste trin, gem |
|
|
||||||
| `gui/SmartPickupFieldTab.lua` | Fanen **Marker**: tabel, detalje, markflåde |
|
|
||||||
|
|
||||||
### Forløb pr. mark
|
|
||||||
|
|
||||||
```
|
|
||||||
Ensilage: idle → ready → mowing → collecting → done
|
|
||||||
Hø: idle → ready → mowing → tedding → [raking] → collecting → done
|
|
||||||
done → idle, når marken ikke længere er slået (vokser igen)
|
|
||||||
```
|
|
||||||
|
|
||||||
Hvert arbejdstrin har undertilstande pr. sæt: `driving` (AutoDrive til `F`) → `starting` (CP laver
|
|
||||||
kursus) → `working` (CP kører) → `returning` (AutoDrive til parkering). Trinnet er færdigt, når Courseplay
|
|
||||||
meldte færdig for mindst ét sæt, og intet sæt på marken stadig arbejder.
|
|
||||||
|
|
||||||
**Slå/vende/rive:** AutoDrive `DRIVETO` til `F`-markøren. Ved stop inden for 30 m starter
|
|
||||||
`adCpBridge` Courseplay med `fieldPosition` = et prøvepunkt inde i marken. Når CP er færdig: AutoDrive
|
|
||||||
`DRIVETO` til sættets parkering (sættets AD-parkeringsmarkør, ellers bliver det stående ved `F`).
|
|
||||||
|
|
||||||
**Opsamling:** Før CP startes sættes AutoDrive til `PICKUPANDDELIVER`, første markør = `F`, anden =
|
|
||||||
silo, `setStartHelper(true)` med hjælper Courseplay. Så klarer AutoDrive og Courseplay selv
|
|
||||||
fuld → aflæs → tilbage → `restartCP`. Ved CP færdig: AutoDrive tømmer resten (`enableParkAtJobFinished`)
|
|
||||||
og parkerer.
|
|
||||||
|
|
||||||
## Fanen Marker
|
|
||||||
|
|
||||||
- Tabel: **Mark · Afgrøde · Plan · Auto/Man. · Status · Maskiner**. Kun slåafgrøde-marker som standard
|
|
||||||
(*Vis alle marker* viser resten). Klik på en række → detalje.
|
|
||||||
- Detalje: Plan (Ensilage/Hø), Auto/Manuel, Riv, Tving lager (liste som "Lager til"), **Start nu**/**Stop**,
|
|
||||||
status med grund, markør og slåbredde.
|
|
||||||
- Markflåde: foreslåede sæt med flueben, rolle og aktuel bredde; advarsel over 50 m.
|
|
||||||
- Knapper på fanen: Start nu / Stop på valgt mark (samme placering som flows).
|
|
||||||
- Statusord: *Venter · Klar · Slås · Vendes · Rives · Samles op · Færdig* + grund ("venter på
|
|
||||||
slåmaskine", "! mangler markør F109", "læssevogn for smal: 20 m < 40,8 m", "Courseplay: intet kursus").
|
|
||||||
|
|
||||||
## Fejlhåndtering
|
|
||||||
|
|
||||||
| Situation | Hvad sker |
|
|
||||||
|---|---|
|
|
||||||
| Ingen `F`-markør | "!" i tabellen, intet sendes |
|
|
||||||
| Intet ledigt sæt med rollen | Status "venter på …", prøver hvert 5 s |
|
|
||||||
| Sæt for bredt (> 50 m) eller for smalt (< slåbredde) | Sættet springes over med grund |
|
|
||||||
| AutoDrive kan ikke finde vej | Sættet frigives, status med grund, andet sæt prøves |
|
|
||||||
| CP-grænse/kursus fejler | 5 min pause for marken, grund vises |
|
|
||||||
| Spilleren sætter sig i et sæt | Sættet frigives fra marken (arbejdet fortsætter ikke automatisk) |
|
|
||||||
| Genindlæsning | Trin genoptages fra gemt fil |
|
|
||||||
|
|
||||||
## Test
|
|
||||||
|
|
||||||
- `tests/test_adFieldModel.lua`, `tests/test_adFieldFleet.lua`, `tests/test_adFieldView.lua`: alle
|
|
||||||
overgange, bredde-regler, forslag, sortering/filtre, gem/indlæs.
|
|
||||||
- In-game testprotokol (manualen + denne spec): én mark Manuel → Start nu → slå → saml op → silo, derefter
|
|
||||||
Hø, derefter Auto. Loglinjer: `ADSmartPickup: F109 …`.
|
|
||||||
|
|
||||||
## Senere udvidelser (noter)
|
|
||||||
|
|
||||||
- **Flere maskiner om samme mark, maks 5** (Courseplays multi-tool-grænse, 2026-09-25): slå/vende/rive med
|
|
||||||
op til 5 sæt på én mark samtidig via Courseplays konvoj (multi-tool), og op til 5 læssevogne. Kræver at
|
|
||||||
`ADFieldModel.maxRigs` bliver en indstilling pr. mark (1–5) og at sættene får samme CP-kursus med
|
|
||||||
hver sin position i konvojen.
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
# Guider til opsætning: marker og flows — design
|
|
||||||
|
|
||||||
Dato: 2026-09-26 · Status: udkast til godkendelse · Mod: FS25_ADSmartPickup (v1.30.4.0 → v1.31/v1.32)
|
|
||||||
|
|
||||||
Leveres i to etaper, der hver kan testes for sig:
|
|
||||||
- **Etape A (v1.31):** fælles guide-ramme + Fields-fanen ("Opsæt mark").
|
|
||||||
- **Etape B (v1.32):** Flows-fanen på samme ramme ("Opsæt flow"), så de to faner er ens at bruge.
|
|
||||||
|
|
||||||
## Formål
|
|
||||||
|
|
||||||
Fields-fanen er blevet en lang liste af valg, der er svær at overskue (brugerens ord: "uoverskueligt").
|
|
||||||
Målet er, at man sætter en mark op med **ét spørgsmål ad gangen**, og at alt, der kan afgøres automatisk,
|
|
||||||
aldrig bliver spurgt om. En eksisterende opsætning skal kunne rettes uden at gå hele guiden igennem.
|
|
||||||
|
|
||||||
Succes: brugeren kan sætte F22 op fra bunden på under et minut uden at scrolle, og kan rette ét valg
|
|
||||||
(fx lager) med to tryk.
|
|
||||||
|
|
||||||
## Hvad brugeren ser
|
|
||||||
|
|
||||||
### Oversigt (fanens standardvisning)
|
|
||||||
|
|
||||||
1. Marktabellen som i dag (Mark · Afgrøde · Plan · Kørsel · Status (kort) · Maskiner).
|
|
||||||
2. Når en mark er valgt: et **resumé** på én linje pr. emne, hver med sin egen række man kan klikke for at
|
|
||||||
rette det ene trin ("Ret"):
|
|
||||||
- Afgrøde: `Behold det der gror` / `-> Hvede`
|
|
||||||
- Afgrødens valg: `Ensilage, river` / `Halm hakkes`
|
|
||||||
- Klargøring: `Automatisk` / `Plov, kalk`
|
|
||||||
- Pleje: `Gødning + ukrudt`
|
|
||||||
- Maskiner: `3 faste` / `Ledige maskiner` · `1 ad gangen, 1 aflæsningsvogn`
|
|
||||||
- Lager: `Marksilo` / `Automatisk` / `F22 Silo`
|
|
||||||
- Status (hele teksten, ombrudt)
|
|
||||||
3. Knapper i bundlinjen (når en mark er valgt): **[Opsæt mark]**, **[Start nu / Stop]**, **[Markflåde]**.
|
|
||||||
Uden valgt mark: **[Markflåde]**.
|
|
||||||
4. Den lange detaljeliste og den fælles markflåde nederst forsvinder fra oversigten.
|
|
||||||
|
|
||||||
### Guide (erstatter fanens indhold, mens den er åben)
|
|
||||||
|
|
||||||
Ét trin pr. skærm: overskrift ("Trin 3 af 7 — Græs: ensilage eller hø?"), en kort forklaring, valgene som
|
|
||||||
rækker, og bundknapperne **[Næste]** (eller **[Gem]** på sidste trin), **[Tilbage]**, **[Annullér]**.
|
|
||||||
Trin der ikke er relevante for marken springes over, og tællingen følger de viste trin.
|
|
||||||
|
|
||||||
| # | Trin | Vises når | Valg | Standard |
|
|
||||||
|---|------|-----------|------|----------|
|
|
||||||
| 1 | Mark | guiden er åbnet uden valgt mark | liste over marker (nr, afgrøde, status) | — |
|
|
||||||
| 2 | Afgrøde | altid | "Behold det der gror" + afgrøder der kan sås | nuværende plan |
|
|
||||||
| 3 | Afgrødens valg | slåafgrøde (nu eller mål): ensilage/hø + river ja/nej. Høstafgrøde med halm: hakkes/samles/presses | se "Vises når" | nuværende plan |
|
|
||||||
| 4 | Klargøring | altid (efter høst / bar mark) | "Automatisk (anbefalet)" / "Vælg selv" → plov, kultivering, kalk, tromle, mulch | Automatisk |
|
|
||||||
| 5 | Pleje | afgrøde der gror | gødning ja/nej, ukrudt ja/nej | ja/ja |
|
|
||||||
| 6 | Maskiner | altid | "Faste maskiner til marken?" ja/nej; ja → liste (kun ledige + dem der allerede er faste her); antal ad gangen (1–5); aflæsningsvogne ved høst (1–5) | nej / 1 / 1 |
|
|
||||||
| 7 | Lager | høst/slåning | marksilo findes: "Brug marksiloen?" ja/nej; ellers/nej: "Automatisk (anbefalet)" / "Vælg selv" → de 8 nærmeste siloer | Automatisk |
|
|
||||||
| 8 | Oversigt | altid | resumé med [Ret] pr. linje; [Kør automatisk] (Auto) / [Gem uden at starte] (Manual) | — |
|
|
||||||
|
|
||||||
### Redigering
|
|
||||||
|
|
||||||
- **[Opsæt mark]** på en opsat mark åbner guiden med de nuværende valg udfyldt.
|
|
||||||
- **[Ret]** på en resumé-linje (oversigt eller trin 8) åbner kun det ene trin; **[Gem]** der gemmer og går
|
|
||||||
tilbage dit man kom fra.
|
|
||||||
- **[Annullér]** kasserer ændringer i guiden (intet gemmes før [Gem]/[Kør automatisk]).
|
|
||||||
|
|
||||||
### Markflåde
|
|
||||||
|
|
||||||
Egen visning (knap **[Markflåde]**): "Brug alle ledige maskiner" + listen med flueben, "fast på F22",
|
|
||||||
"i gang på F108", bredde-advarsler. Samme indhold som i dag, flyttet ud af markens detaljer.
|
|
||||||
|
|
||||||
## Hvad der ALDRIG spørges om (automatisk)
|
|
||||||
|
|
||||||
Scanning af marken og markøren, hvornår der høstes/slås/gødes/sprøjtes, påfyldning (eget lager før køb),
|
|
||||||
tømning ved forkert vare, ventetid ved regn, bredde-kontrol, valg af nærmeste maskine, ruter, gæringssilo ved
|
|
||||||
ensilage, marksilo-tømning (advarsel hvis intet flow tømmer den).
|
|
||||||
|
|
||||||
## Etape B: Flows-fanen på samme måde
|
|
||||||
|
|
||||||
Brugerens ønske: "det samme for flow opsætningen så det bliver ensrettet".
|
|
||||||
|
|
||||||
**Oversigt:** flow-tavlen som i dag (én linje pr. flow med status). Valgt flow → resumé med [Ret]-linjer
|
|
||||||
(Retning · Fra · Til/Lager · Varer · Regler · Vent ved · Traktorer · Status) og bundknapperne
|
|
||||||
**[Opsæt flow]**, **[Start/Stop]**, **[Nyt flow]**, **[Omdøb]**, **[Slet]**, **[Start alle / Stop alle]**.
|
|
||||||
Den lange detaljeliste forsvinder.
|
|
||||||
|
|
||||||
**Guiden "Opsæt flow"** — samme trin-skærm, knapper og [Ret]/[Annullér] som markguiden:
|
|
||||||
|
|
||||||
| # | Trin | Vises når | Valg | Standard |
|
|
||||||
|---|------|-----------|------|----------|
|
|
||||||
| 1 | Navn | nyt flow | tekst (FS25-tekstdialog) | "Flow N" |
|
|
||||||
| 2 | Retning | altid | Ind til bygning (forsyning) / Ud fra bygning (udkørsel) | nuværende |
|
|
||||||
| 3 | Fra / Til | altid | bygninger med markør, fast rækkefølge (navn, markør), "Vis alle" | nuværende |
|
|
||||||
| 4 | Varer | altid | forsyning: "Automatisk (det bygningen mangler)" / "Vælg selv" → varer; udkørsel: pr. vare Sælg / Lager / Begge / Fra | Automatisk / nuværende |
|
|
||||||
| 5 | Lager | udkørsel med Lager | "Automatisk (nærmeste med plads)" / "Vælg selv" → lagerbygninger | Automatisk |
|
|
||||||
| 6 | Regler | altid | udkørsel: sælg ved pris-%, lager over %, samlerunde ja/nej; forsyning: antal ture, retur fra Wait under % | nuværende / standard |
|
|
||||||
| 7 | Vent ved | altid | Wait-puljer | nuværende |
|
|
||||||
| 8 | Traktorer | altid | ledige traktorer (+ dem i flowet); traktorer i andre flows skjult med linje "N i andre flows" | nuværende |
|
|
||||||
| 9 | Oversigt | altid | resumé med [Ret]; [Start flow] / [Gem uden at starte] | — |
|
|
||||||
|
|
||||||
Automatisk (spørges aldrig): markør ved bygningen, rutetjek, bedste foder først/parallelt foder, læs-bytte,
|
|
||||||
kildevalg (mindst/fyldeste først), fordeling på flere mål, hvile af tomme kilder, traktornavne efter flowet.
|
|
||||||
|
|
||||||
## Arkitektur
|
|
||||||
|
|
||||||
- **`adGuide.lua` (ny, ren Lua, testet):** den fælles guide-motor, uafhængig af marker/flows:
|
|
||||||
trin-liste, aktuelt trin, kladde, næste/tilbage/spring (til [Ret]), "returnér til oversigt", tælling
|
|
||||||
"Trin 3 af 7" over de synlige trin. Både markguiden og flowguiden er en trin-definition oven på den.
|
|
||||||
- **`gui/SmartPickupGuide.lua` (ny):** den fælles trin-skærm i menuen (overskrift, forklaring, valgrækker,
|
|
||||||
bundknapper Næste/Tilbage/Annullér/Gem) — ét sted, så de to guider ser og opfører sig ens.
|
|
||||||
- **`adFlowGuide.lua` (ny, etape B, ren Lua, testet):** flowguidens trin, valg, standarder og resumé; gemmer
|
|
||||||
via `ADRunsController.updateRun`/`assignVehicle` som i dag.
|
|
||||||
- **`adFieldGuide.lua` (ny, ren Lua, testet):** markguidens trin (oven på `ADGuide`).
|
|
||||||
- `ADFieldGuide.open(field, info, context)` → guide-tilstand `{fieldId, draft, steps, index, returnTo}`;
|
|
||||||
`draft` er en kopi af markens plan (+ faste maskiner), så intet ændres før gem.
|
|
||||||
- `ADFieldGuide.steps(draft, info, context)` → listen af synlige trin (tabellen ovenfor).
|
|
||||||
- `ADFieldGuide.choices(step, draft, info, context)` → rækkerne for trinnet (tekstnøgler + værdier).
|
|
||||||
- `ADFieldGuide.choose(guide, key, value)` → ny guide med ændret draft (immutabelt).
|
|
||||||
- `ADFieldGuide.next/back/jump(guide, step)`; `ADFieldGuide.result(guide)` → `{plan, rigIds}` til gem.
|
|
||||||
- `ADFieldGuide.summary(field, info, context)` → resumé-linjerne `{step, key, args}`.
|
|
||||||
- `context` = det spil-afhængige: marksilo findes?, siloer i nærheden, maskiner (ledige/faste), afgrøder
|
|
||||||
der kan sås. Hentes af GUI'et, så modellen kan testes uden spillet.
|
|
||||||
- **`gui/SmartPickupFieldTab.lua`:** oversigt (tabel + resumé) og guide-visning; bygger rækker ud fra
|
|
||||||
`ADFieldGuide`, gemmer via `ADFieldJobs.setPlan` og `ADFieldFleet.dedicate`. Fil-størrelse: guide-visningen
|
|
||||||
lægges i `gui/SmartPickupFieldGuide.lua` (ny), så ingen fil passerer ~800 linjer.
|
|
||||||
- **`gui/SmartPickupFrame.lua`:** bundknapper for fanen (Opsæt mark, Start/Stop, Markflåde, Næste/Tilbage/
|
|
||||||
Annullér/Gem i guiden) og routing af klik.
|
|
||||||
- **Uændret:** `ADFieldModel` (plan-felter og lagring), `ADFieldJobs`, `ADFieldWork`, gemt XML. Guiden er et
|
|
||||||
nyt ansigt på de samme data — eksisterende opsætninger virker videre.
|
|
||||||
|
|
||||||
## Fejl og kanttilfælde
|
|
||||||
|
|
||||||
- Mark uden markør: guiden kan køres, men oversigten siger "sæt markøren F22" og [Kør automatisk] gemmer som
|
|
||||||
Manual indtil markøren findes (samme regel som i dag).
|
|
||||||
- Maskine der er fast på en anden mark vises ikke i trin 6 (som v1.30.3); linje "N maskiner er faste på
|
|
||||||
andre marker".
|
|
||||||
- Marken forsvinder (solgt/bygget over) mens guiden er åben: guiden lukkes, oversigten vises.
|
|
||||||
- Guiden er åben og menuen lukkes: kladden kasseres.
|
|
||||||
|
|
||||||
## Test
|
|
||||||
|
|
||||||
- `tests/test_adGuide.lua`: motoren — næste/tilbage over skjulte trin, spring+retur, tælling, annullér.
|
|
||||||
- `tests/test_adFlowGuide.lua` (etape B): trin pr. retning, varer auto/vælg, lager-trin kun ved Lager, resumé.
|
|
||||||
- `tests/test_adFieldGuide.lua`: synlige trin pr. marktype (slåafgrøde, høst med halm, bar mark, mål-afgrøde),
|
|
||||||
standardvalg, udfyldning fra eksisterende plan, `choose` ændrer kun kladden, `result` giver plan + rigIds,
|
|
||||||
[Ret]-spring og tilbage, "Automatisk" vs "Vælg selv" i klargøring og lager, marksilo-spørgsmålet kun når en
|
|
||||||
findes, resumé-linjerne.
|
|
||||||
- Hele suiten skal være grøn; GUI-routing verificeres in-game (testplan-punkt).
|
|
||||||
|
|
||||||
## Uden for scope
|
|
||||||
|
|
||||||
Vinmarker (egne roller), genoptagelse af Courseplay efter genstart, fanerne Factories/Silos/Barns/Pallet sheds.
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
# Markarbejde, etape 2–3 + multi-maskine: høst, roer, jordbehandling, såning
|
|
||||||
|
|
||||||
Dato: 2026-09-26 (nat) · Bygger på `2026-09-25-markarbejde-etape1-design.md` · Branch `feature/markarbejde-2`
|
|
||||||
|
|
||||||
Brugeren gav fuldt mandat ("byg resten, ALLE PUNKTER, du træffer selv gode valg") og tester i spillet dagen
|
|
||||||
efter. Beslutningerne herunder er truffet uden dialog og skal læses igennem.
|
|
||||||
|
|
||||||
## Mål
|
|
||||||
|
|
||||||
Alle gårdens marker kan køres af Smart Pickup: slåafgrøder (etape 1) og nu **høstafgrøder** (korn, raps,
|
|
||||||
majs, roer, kartofler … alt der høstes med mejetærsker/optager), inkl. **aflæsningsvogne**, **jordbehandling**
|
|
||||||
efter høst og **såning**. Hvert trin kan køres med **1–5 maskiner** på samme mark (Courseplays multi-tool).
|
|
||||||
|
|
||||||
## Beslutninger
|
|
||||||
|
|
||||||
1. **Høst er én pipeline for alle høstafgrøder.** Roer og korn adskiller sig kun ved hvilken høster der
|
|
||||||
kan tage afgrøden. Høsteren matches på afgrøden: skærebordets/optagerens frugttyper (`spec_cutter`)
|
|
||||||
skal omfatte markens afgrøde. Finsnittere (cutter med `fruitTypeConverters`) bruges ikke til høst.
|
|
||||||
2. **Aflæsning under høst = Courseplays combine-unloader** (`COMBINE_UNLOADER_CP`, bevist i AI-Bonden):
|
|
||||||
vognen følger høsteren, tømmer den og kører til en **station**. Stationen vælges af Smart Pickups
|
|
||||||
silovalg (nærmeste silo med plads der tager afgrøden, eller *Tving lager*); findes den ikke i
|
|
||||||
Courseplays stationsliste, bruges nærmeste station. Antal vogne pr. mark: 1–3 (plan).
|
|
||||||
3. **Høsteren sendes hjem** (AD-parkering) når Courseplay er færdig og tanken er tømt. Aflæsningsvogne
|
|
||||||
stoppes når høsten er færdig; har de rest, kører AutoDrive den til lageret (samme reserve som
|
|
||||||
læssevognen), ellers parkering.
|
|
||||||
4. **Efter høst** (valg pr. mark): *Ingenting*, *Kultivér* eller *Pløj*, og derefter evt. **Så**.
|
|
||||||
Såmaskinen sættes til planens afgrøde (standard: samme afgrøde som blev høstet). Kan afgrøden ikke
|
|
||||||
sås i den nuværende periode, venter marken med grund. Tom for frø -> sættet springes over med grund
|
|
||||||
(moden fylder ikke frø på; det gør brugeren/AutoDrive-refill).
|
|
||||||
5. **Multi-maskine (1–5 pr. trin, plan pr. mark).** Trin med eget kursus (slå, vend, riv, høst,
|
|
||||||
kultivér, pløj, så) laver ét Courseplay-kursus med `multiTools = n` på første sæt; de øvrige kopierer det,
|
|
||||||
og hver får sin bane (`laneOffset`: 2 -> −1,1 · 3 -> −1,0,1 · 4 -> −2,−1,1,2 · 5 -> −2…2). Et multi-trin
|
|
||||||
startes kun når **alle n** sæt er ledige (ellers kører én bane ikke). Læssevognen på et multi-kursus
|
|
||||||
kræver også n vogne, én pr. bane.
|
|
||||||
6. **Rolle-rækkefølge** når et køretøj har flere redskaber: høster > læssevogn > river > vender >
|
|
||||||
slåmaskine > såmaskine > kultivator > plov > aflæsningsvogn (traktor+vogn som Courseplay kan bruge som
|
|
||||||
combine-unloader).
|
|
||||||
7. **Afgrødetype pr. mark:** slåafgrøde (etape 1) eller høstafgrøde (alt andet med en høstbar frugt). Plan
|
|
||||||
*Høst* er standard for høstafgrøder. Marker uden afgrøde (pløjet/stub) kan få *Start nu* for
|
|
||||||
jordbehandling/såning alene.
|
|
||||||
8. **Stadig ikke med:** gødning/kalk/sprøjtning, baller, halm efter mejetærsker, auto-påfyldning af frø.
|
|
||||||
|
|
||||||
## Opbygning (ændringer)
|
|
||||||
|
|
||||||
| Fil | Ændring |
|
|
||||||
|---|---|
|
|
||||||
| `adFieldModel.lua` | Planer `harvest`, jordbehandling, såning, `rigs`/`unloaders`; trin `harvesting/cultivating/plowing/sowing`; hjælperoller pr. trin; `lanePositions(n)` |
|
|
||||||
| `adFieldFleet.lua` | Nye roller; `pick` tager afgrøde (høster skal kunne tage den); `pickMany` til multi-trin |
|
|
||||||
| `adCpBridge.lua` | `startUnloader`, `canHarvest`, frø-valg (`setSeedFruit`, `isPlantable`, `hasSeeds`), multi-tool kursus + lane |
|
|
||||||
| `adFields.lua` | Afgrødetype (slå/høst), sidst høstede afgrøde |
|
|
||||||
| `adFieldWork.lua` (ny) | Udsendelse og opfølgning af sæt (AD/CP) — flyttet ud af `adFieldJobs.lua` der blev for stor |
|
|
||||||
| `gui/SmartPickupFieldTab.lua` | Plan-rækker pr. afgrødetype, maskiner pr. trin, aflæsningsvogne, efter høst, så + afgrøde |
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
# Marksilo-flow — design
|
|
||||||
|
|
||||||
Dato: 2026-09-27 · Status: godkendt i chat, afventer review af denne spec
|
|
||||||
|
|
||||||
## Formål
|
|
||||||
|
|
||||||
Høsten lægges ofte i siloer ude ved markerne (mellemlager). I dag skal spilleren selv huske at køre varerne
|
|
||||||
hjem. Marksilo-flowet kigger jævnligt alle marksiloer igennem og sender selv op til **x** ledige vogne ud for at
|
|
||||||
hente varerne hjem til gårdens lagre.
|
|
||||||
|
|
||||||
**Succes:** står der varer i en marksilo, som gården kan tage, bliver de kørt hjem uden at spilleren gør noget,
|
|
||||||
med højst x vogne ude ad gangen, og uden at tage vogne fra markarbejde, andre flows eller spilleren.
|
|
||||||
|
|
||||||
## Brugerens valg (fra brainstorm)
|
|
||||||
|
|
||||||
| Spørgsmål | Valg |
|
|
||||||
|---|---|
|
|
||||||
| Hvor er "hjem"? | Nærmeste af de gårdlagre, spilleren vælger i flowet, der tager varen og har plads |
|
|
||||||
| Hvad er "x vogne"? | Lånte ledige vogne fra hele gården — ingen fast tilknytning — højst x ude samtidig |
|
|
||||||
| Hvilke vogne må lånes? | Samme regel og flueben som markarbejdet (`fleetMode` "all"/"ticked", `ADFieldJobs.state.fleet`) |
|
|
||||||
|
|
||||||
## Begreber
|
|
||||||
|
|
||||||
- **Marksilo:** bygning af typen silo inden for `ADFields.FIELD_SILO_DISTANCE` (150 m) af en marks polygon
|
|
||||||
(`ADFields.fieldSilos`), samlet over alle marker, uden dubletter. En bygning, der er valgt som gårdlager i
|
|
||||||
flowet, er aldrig marksilo. Det samme gælder siloer, der er kilde, mål eller lager i et flow (tilføjet efter review
|
|
||||||
2026-09-27: ellers tømte flowet gårdsiloer, som forsyningsflows bruger, fordi de ligger under 150 m fra en mark).
|
|
||||||
- **Gårdlager:** bygning spilleren vælger i flowet (samme bygningsvælger som udkørslens lagre, `storeBuildings`).
|
|
||||||
- **Lån:** en vogn, flowet har sendt på én tur. Lån ligger kun i hukommelsen.
|
|
||||||
|
|
||||||
## Flowet
|
|
||||||
|
|
||||||
Marksilo-flowet er ÉN fast opsætning pr. savegame — ikke en ny run-type. Beslutning (2026-09-27, under
|
|
||||||
planlægning): run-editoren (`SmartPickupFrame`) er bygget til præcis to typer (forsyning/udkørsel) mange steder,
|
|
||||||
og runs har faste vogne, Start/Stop pr. vogn og Wait-puljer, som intet af det passer til lånte vogne. En egen
|
|
||||||
underfane + egen gemmefil giver langt færre indgreb i eksisterende kode.
|
|
||||||
|
|
||||||
Opsætning (`ADSmartPickup_fieldsilos.xml` i savegame-mappen, gemmes sammen med runs):
|
|
||||||
- `enabled` — til/fra, standard fra
|
|
||||||
- `storeIds` — gårdlagre (bygnings-id'er fra `ADBuildings`, kun `kind == "silo"`)
|
|
||||||
- `maxVehicles` — x, 1–10, standard 2
|
|
||||||
- `minLiters` — tærskel pr. silo og vare: 5000 / 10000 / 25000 / 50000, standard 10000
|
|
||||||
|
|
||||||
## Planlægning (ren logik, nyt modul `adFieldSiloPlanner.lua`, testes med luajit)
|
|
||||||
|
|
||||||
Input: marksiloer `{id, markerId|nil, stock = {[fillType] = liters}}`, gårdlagre `{id, markerId, accepts =
|
|
||||||
{[fillType] = freeLiters}, x, z}`, aktive lån `{siloId, fillType, liters}`, ledige vogne `{id, x, z, capacity,
|
|
||||||
carries = {[fillType] = true}}`, `maxVehicles`, `minLiters`.
|
|
||||||
|
|
||||||
1. **Kvalificerede opgaver:** pr. (silo, vare) hvor silo har markør, `liters >= minLiters`, og mindst ét
|
|
||||||
gårdlager har `freeLiters >= minLiters` for varen. Resten = liters minus det aktive lån på samme (silo, vare)
|
|
||||||
allerede er på vej med; kun opgaver med rest `>= minLiters` tæller.
|
|
||||||
Siloer uden markør → status `noMarker`; varer intet gårdlager tager → status `noFarmStore`.
|
|
||||||
2. **Rækkefølge:** fuldeste rest først; lige → lavest silo-id (stabilt).
|
|
||||||
3. **Vogne:** ledige pladser = `maxVehicles - #aktive lån`. For hver opgave i rækkefølge, så længe der er
|
|
||||||
pladser: nærmeste ledige vogn (til siloen), der kan bære varen; resten reduceres med vognens kapacitet;
|
|
||||||
samme opgave får flere vogne, så længe resten stadig er `>= minLiters`.
|
|
||||||
4. **Gårdlager pr. lån:** nærmeste gårdlager (til siloen) der tager varen med `freeLiters >= min(vognens
|
|
||||||
kapacitet, rest)`; ellers det med mest plads; ellers intet (opgaven springes over denne runde).
|
|
||||||
|
|
||||||
Output: `{dispatch = {{vehicleId, siloId, siloMarkerId, fillType, storeMarkerId, liters}}, status = {...}}`.
|
|
||||||
|
|
||||||
## Udførelse (`adFieldSilos.lua`)
|
|
||||||
|
|
||||||
- **Takt:** planlæg hvert 60. s (spiltid via `dt`), kun når flowet kører. Lagertal læses som i dag
|
|
||||||
(`ADSmartPickup.getStationLevelAndCapacity` / silo-lagre).
|
|
||||||
- **Ledige vogne:** kun vogne med AD-parkeringspunkt (ellers bliver de holdende på lagerets markør); `ADFieldWork.getRigs(adEnv)` filtreret til `role == "unloader"` (traktor + vogn, som Courseplay
|
|
||||||
godkender som tømmevogn — ikke sprøjter/høstere med tank), `enabled and not busy and not controlled`, og
|
|
||||||
vogne der har aflæsbare enheder (`AutoDrive.getAllDischargeableUnits`), der understøtter varen.
|
|
||||||
`getRigs.busy` udvides med `ADFieldSilos.isLoaned(vehicle)`, så markarbejdet ikke tager en lånt vogn
|
|
||||||
(flows tager allerede kun deres egne tilknyttede vogne).
|
|
||||||
- **Afsend:** AutoDrive "Hent og aflever": første markør = silomarkør, anden = gårdlagerets markør, vare =
|
|
||||||
lånets vare, `loopCounter = 1`, parkering ved job slut slået til (`enableParkAtJobFinished`); start som
|
|
||||||
markarbejdet (`startAd` → mode:start). Log: `marksilo: '<vogn>' henter <liter> l <vare> i <silo> → <lager>`.
|
|
||||||
- **Frigivelse:** lånet slettes når AD ikke længere er aktivt for vognen (turen og parkeringen er færdig),
|
|
||||||
når spilleren sætter sig i den, eller når flowet stoppes (stop = AD stoppes på lånte vogne).
|
|
||||||
- **Vagt:** er vognen ikke kommet i gang (ingen stigning i fyldning og står stille) efter 120 s, stoppes AD,
|
|
||||||
lånet slettes, og der logges én advarsel pr. (silo, vare), så samme opgave ikke fejler i ring hvert minut
|
|
||||||
(opgaven får 10 min pause).
|
|
||||||
- **Genindlæsning:** lån gemmes ikke. Ved indlæsning er alle vogne frie for flowet; en vogn AutoDrive selv
|
|
||||||
genstarter, kører sin tur færdig som almindelig AD-tur.
|
|
||||||
|
|
||||||
## GUI
|
|
||||||
|
|
||||||
Ny underfane "Marksiloer" efter "Måned". Indstillinger: gårdlagre (eksisterende vælger),
|
|
||||||
"Maks. vogne ude" (1–10), "Mindste mængde" (5.000 / 10.000 / 25.000 / 50.000 l), Start/Stop.
|
|
||||||
Status: `x/y vogne ude · n siloer venter`, og én række pr. marksilo med indhold: silo, vare, liter, på vej,
|
|
||||||
status (`ok` / `mangler markør` / `ingen gård tager <vare>`). Tekster i `l10n_da` + `l10n_en` (`spu_fs_*`).
|
|
||||||
|
|
||||||
## Uden for scope
|
|
||||||
|
|
||||||
- Salg direkte fra marksilo (kan senere bruge udkørslens prisregel).
|
|
||||||
- Faste vogne i flowet (valg 1 i brainstorm) — fravalgt.
|
|
||||||
- Lån der overlever genindlæsning.
|
|
||||||
|
|
||||||
## Test
|
|
||||||
|
|
||||||
- `tests/test_adFieldSiloPlanner.lua`: kvalificering (tærskel, markør, gård tager varen), fuldeste først,
|
|
||||||
x-loft inkl. aktive lån, flere vogne til én stor silo, vogn der ikke kan bære varen springes over, nærmeste
|
|
||||||
gårdlager med plads, fallback til mest plads, intet lager → ingen afsendelse.
|
|
||||||
- `tests/test_adFieldSiloConfig.lua`: standarder, grænser (x 1–10, tærskel kun de fire værdier), til/fra af gårdlager, records frem og tilbage.
|
|
||||||
- In-game: silo ved en mark med 50.000 l hvede, gårdlager = Farma-silo, x = 2 → to vogne kører, afleverer,
|
|
||||||
parkerer; markarbejdet tager ikke de lånte vogne imens.
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
# Flow-vogne — design
|
|
||||||
|
|
||||||
Dato: 2026-09-28 · Status: godkendt i chat ("1 vognen hører til flowet", "1 automatisk", "ja alle tre", "når du har
|
|
||||||
det hele klar"). Bygger på maskinpuljen (`2026-09-28-maskinpulje-design.md`).
|
|
||||||
|
|
||||||
## Formål
|
|
||||||
|
|
||||||
Alle påhæng har nu egen AD-parkering "P X" og en markør "Connect P X". Flows (fodring, mælk, slurry …) skal ikke
|
|
||||||
længere have traktor og vogn låst sammen: et flow henter **sin egen** vogn, når det starter, og når flowet ikke har
|
|
||||||
noget job længere, køres vognen på plads og traktoren hjem, så begge bliver ledige i puljen.
|
|
||||||
|
|
||||||
## Brugerens valg
|
|
||||||
|
|
||||||
| Spørgsmål | Valg |
|
|
||||||
|---|---|
|
|
||||||
| Hvem ejer en flow-vogn? | Flowet. Puljen må kun låne den, når flowet er stoppet/slettet/uden bygning |
|
|
||||||
| Hvordan kendes flowets vogn? | Automatisk: vognen der hænger på flowets traktor huskes pr. traktor |
|
|
||||||
| Hvornår "intet job"? | Alle tre: bygning væk, flow slettet, flow stoppet |
|
|
||||||
| Mejetærskere/skærebord | Uændret — skærebord er aldrig i puljen; mejetærsker henter ikke redskaber |
|
|
||||||
|
|
||||||
## Begreber
|
|
||||||
|
|
||||||
- **Flowets vogn:** `run.wagons = {[traktorId] = vognId}`. Opdateres automatisk: hænger der et redskab bagpå en af
|
|
||||||
flowets traktorer (mens flowet ikke er stoppet), huskes det som traktorens vogn. Gemmes i `ADSmartPickup_runs.xml`
|
|
||||||
(`wagons="traktorId=vognId;…"`).
|
|
||||||
- **Stoppet flow:** `run.stopped = true`, gemt. Sættes af Stop på flowet, "Stop alle"/"Stop alt og parkér" og
|
|
||||||
"bygning væk"; ryddes af Start / "Start alle". Ikke udledt af om AD kører (en traktor ved et Wait-punkt kan holde
|
|
||||||
stille uden at flowet er stoppet).
|
|
||||||
- **Bygning væk:** en bygning i flowets kilde-/mål-/lagerliste findes ikke længere som placeable (uniqueId) i to
|
|
||||||
tjek i træk (60 s mellem tjek).
|
|
||||||
|
|
||||||
## Adfærd
|
|
||||||
|
|
||||||
1. **Husk vogn** (hvert 10. s): for hvert ikke-stoppet flow og hver traktor med et bagerste redskab → `wagons[traktor]
|
|
||||||
= redskab` (ændringer gemmes med næste gem).
|
|
||||||
2. **Start** (`startVehicle`): er traktoren solo og har den en husket vogn:
|
|
||||||
- vognen frakoblet og ikke reserveret/lånt → `ADHitch.fetch` til "Connect <vognens parkering>", derefter startes
|
|
||||||
traktoren i flowet som i dag. Fanen/loggen: "henter sin vogn".
|
|
||||||
- vognen er lånt ud / koblet på en anden → traktoren venter ("venter på sin vogn"), der prøves igen hvert 10. s.
|
|
||||||
- vognen findes ikke eller har ingen Connect-markør → startes som i dag (solo; valideringen afgør).
|
|
||||||
3. **Stop / "Stop alle" / "Stop alt og parkér"**: `stopped = true`; hver traktor stoppes og køres hjem med
|
|
||||||
`ADHitch.returnHome` (vogn af på sin parkering, traktor til sin egen). Vognen forbliver flowets.
|
|
||||||
4. **Slet flow**: som stop; derefter er vognen ikke længere flowets (flowet findes ikke).
|
|
||||||
5. **Bygning væk**: flowet stoppes som i 3, markeres i loggen og med en besked: "Flow 'X': bygning findes ikke længere
|
|
||||||
— stoppet, traktorer kørt hjem".
|
|
||||||
|
|
||||||
## Puljen og flows
|
|
||||||
|
|
||||||
- `ADPool.collect`: traktor i et flow er kun puljetraktor, når flowet er **stoppet**.
|
|
||||||
- Puljeredskab: en vogn, der er husket af et **ikke-stoppet** flow, er aldrig puljeredskab.
|
|
||||||
- `ADFieldWork.getRigs`: uændret for kørende flows; traktorer i stoppede flows må indgå.
|
|
||||||
|
|
||||||
## Komponenter
|
|
||||||
|
|
||||||
- `adFlowWagons.lua` (nyt): ren logik (serialisering af `wagons`, beslutning ved start: fetch/wait/start, "bygning
|
|
||||||
væk"-debounce, ejer-opslag) + spil-del (tick: husk vogne, ventende starter, bygningstjek; `fetchThenStart`,
|
|
||||||
`sendHome(run)`).
|
|
||||||
- `adRuns.lua`/`adRunsStorage.lua`: felterne `stopped`, `wagons` i records.
|
|
||||||
- `adRunsController.lua`: kroge i `startVehicle`, `stopRun`, `deleteRun`, `stopAllRuns`, `stopAllAndPark`,
|
|
||||||
`startRun`/`startAllRuns`/`queueStartAll` (ryd `stopped`).
|
|
||||||
- `adPool.lua`: udelukker vogne ejet af ikke-stoppede flows; traktorer i stoppede flows tæller.
|
|
||||||
- `adSmartPickup.lua`: `ADFlowWagons.tick` i update-løkken.
|
|
||||||
|
|
||||||
## Test
|
|
||||||
|
|
||||||
- `tests/test_adFlowWagons.lua`: serialisering frem/tilbage, startbeslutning (fetch/wait/start), debounce af
|
|
||||||
bygning væk, ejer-opslag (stoppet vs kørende).
|
|
||||||
- `tests/test_adRuns.lua`: records med `stopped` og `wagons`.
|
|
||||||
- In-game: savegame2 — stop et fodringsflow → vogne sættes på plads, traktorer hjem; start det → traktorerne henter
|
|
||||||
deres egne vogne og kører.
|
|
||||||
|
|
||||||
## Uden for scope
|
|
||||||
|
|
||||||
- Valg af vogn i GUI (automatisk er valgt).
|
|
||||||
- At et flow tager en anden vogn end sin egen.
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
# Maskinpulje — design
|
|
||||||
|
|
||||||
Dato: 2026-09-28 · Status: godkendt i chat (afsnit 1–4), afventer review af denne spec
|
|
||||||
|
|
||||||
## Formål
|
|
||||||
|
|
||||||
Plove, harver, kultivatorer, såmaskiner, tømmevogne m.m. bruges kun en gang imellem. I dag skal hvert af dem have
|
|
||||||
en traktor koblet fast på hele året, fordi markarbejdet kun kan sende en traktor, der allerede har det rigtige
|
|
||||||
redskab bag sig. Maskinpuljen gør traktorer og redskaber til fælles puljer: et job venter i kø, til en traktor er
|
|
||||||
ledig; traktoren henter det redskab, jobbet kræver, udfører arbejdet, parkerer redskabet på dets plads og kører
|
|
||||||
selv hjem til sin egen parkering.
|
|
||||||
|
|
||||||
**Succes:** med fx 3 ledige traktorer, 2 plove og 1 kultivator bliver markernes arbejde udført uden at nogen
|
|
||||||
traktor står låst til et redskab — færre køretøjer at købe og vedligeholde. Flows, der kører hele tiden (fodring,
|
|
||||||
slurry, mælk …), påvirkes ikke.
|
|
||||||
|
|
||||||
## Brugerens valg (fra brainstorm)
|
|
||||||
|
|
||||||
| Spørgsmål | Valg |
|
|
||||||
|---|---|
|
|
||||||
| Hvordan findes redskabets Connect-markør? | Navneregel: AD-parkering "X" ↔ markør "Connect X" |
|
|
||||||
| Hvem er i puljen? | Automatisk ud fra opsætningen — ingen afkrydsning |
|
|
||||||
| Hvilken traktor henter? | Nærmeste ledige med nok hk |
|
|
||||||
| Omfang | Markarbejde + tømmevogne ved høst + marksilo-flowet. Flows er undtaget |
|
|
||||||
| Tømmevogne ved høst | Også fra puljen, men dedikeret til marken, til høsten dér er færdig |
|
|
||||||
| Arkitektur | Udvid markarbejdets eget maskinvalg (ikke en separat puljemanager) |
|
|
||||||
|
|
||||||
## Begreber
|
|
||||||
|
|
||||||
- **Puljeredskab:** et redskab eller en vogn, der holder **frakoblet**, har en AD-parkering (`advd.parkDestination`
|
|
||||||
→ markør "X"), og hvor der findes en markør "Connect X" (`ADHitch.parseMarker` = attach, navn = X; store/små
|
|
||||||
bogstaver ligegyldigt). Uden Connect-markør er redskabet ikke i puljen — det er spillerens måde at holde det
|
|
||||||
udenfor.
|
|
||||||
- **Puljevogn:** puljeredskab med rollen `unloader` (vogn med tip/snegl, `spec_trailer` + `spec_dischargeable`).
|
|
||||||
- **Puljetraktor:** motoriseret køretøj uden noget koblet bag på (`ADHitch.rearImplements` tom), ikke i et flow
|
|
||||||
(`ADRunsController.getRunForVehicle == nil`), ikke optaget (samme `busy`-regel som `getRigs`), ikke styret af
|
|
||||||
spilleren, med egen AD-parkering (`vehicle.advd.parkDestination >= 1`) og flueben i markflåden
|
|
||||||
(`ADFieldFleet.isEnabled`).
|
|
||||||
- **Færdig kombination:** det markarbejdet bruger i dag — traktor med redskabet allerede koblet på (`getRigs`).
|
|
||||||
- **Puljekombination:** {traktor, redskab, Connect-markør-id, redskabets parkering-id, traktorens parkering-id}.
|
|
||||||
|
|
||||||
AD-parkering: hver maskine har sin egen `advd.parkDestination`; AD bruger det valgte redskabs, når et er koblet på
|
|
||||||
(`AutoDriveVehicleData.lua:88-95`). Traktorens egen parkering læses derfor altid på traktoren selv.
|
|
||||||
|
|
||||||
## Komponenter
|
|
||||||
|
|
||||||
### `adPool.lua` (nyt, ren logik + tynd spil-adapter)
|
|
||||||
|
|
||||||
Ren logik (testes med luajit):
|
|
||||||
- `ADPool.roleOf(specs)` — rolle for et frakoblet redskab ud fra dets egne specs via `ADFieldFleet.ROLE_SPECS`;
|
|
||||||
vogn med `spec_trailer` + `spec_dischargeable` og intet andet → `unloader`.
|
|
||||||
- `ADPool.isPoolImplement(item)` — `item = {attached, parkMarkerName, connectMarkerId}` → frakoblet, med parkering
|
|
||||||
og Connect-markør.
|
|
||||||
- `ADPool.powerOk(tractorPower, neededPower)` — true når et af tallene mangler (nil/0), ellers `tractorPower >=
|
|
||||||
neededPower`.
|
|
||||||
- `ADPool.pairFor(role, fieldX, fieldZ, implements, tractors, reserved, accept)` — redskab med rollen nærmest marken
|
|
||||||
(ikke reserveret, `accept(implement)` sand), derefter nærmeste ledige traktor til redskabet med `powerOk`.
|
|
||||||
Returnerer `pair` eller `nil, why, args` (`noPoolImplement` / `noPoolTractor` / `tooWeak` med hk-tal).
|
|
||||||
- `ADPool.queueOrder(fields)` — markers ventende rækkefølge: `waitingSince` stigende, lige → lavest mark-id.
|
|
||||||
- `ADPool.isReturnDue(rearImplements, poolParkIds)` — traktor har et puljeredskab bag sig (dets parkering har en
|
|
||||||
Connect-markør) → hjemtur skal køres.
|
|
||||||
|
|
||||||
Spil-adapter:
|
|
||||||
- `ADPool.collect(adEnv)` → `implements, tractors` fra `g_currentMission.vehicleSystem.vehicles` (gårdens), med
|
|
||||||
position, rolle, specs til `acceptFor`, hk og parkering/Connect-id.
|
|
||||||
- hk: `g_storeManager:getItemByXMLFilename(vehicle.configFileName).specs` → traktor `power`, redskab
|
|
||||||
`neededPower`; mangler de, bruges nil (→ tilladt).
|
|
||||||
- Reservationer: `ADPool.reserve(implementId, tractorId, owner)` / `release(owner)`; i hukommelsen, ryddes ved
|
|
||||||
genindlæsning (se Genindlæsning).
|
|
||||||
- Karantæne: redskab, der ikke kunne kobles på, springes over i 10 min (`ADPool.QUARANTINE_MS = 600000`).
|
|
||||||
|
|
||||||
### `adHitch.lua` (findes, v1.47.4 — udvides let)
|
|
||||||
|
|
||||||
Udfører kørslerne: `fetch` (kør til Connect + kobl på), `route` (trin-tur, afkobling ved redskabets parkering).
|
|
||||||
Ny: `ADHitch.returnHome(adEnv, tractor, onDone)` = `route({redskabets parkering, traktorens parkering})`; ved
|
|
||||||
parkeringen kobles puljeredskabet af (samme regel som i dag i ruter).
|
|
||||||
|
|
||||||
### Markarbejdet (`adFieldWork.lua`, `adFieldJobs.lua`)
|
|
||||||
|
|
||||||
- `dispatch`: som i dag først færdige kombinationer. Returnerer `pick`/`pickMany` intet (`noRig`/`allBusy`/
|
|
||||||
`cannotDo`), forsøges `ADPool.pairFor` for de manglende. Puljekombinationens assignment får `phase = "fetching"`,
|
|
||||||
`pool = {implementId, connectId, implementParkId, tractorParkId}`, nøgle = traktorens id.
|
|
||||||
- `follow`: fase `fetching` → når `ADHitch.fetch` melder ok: bredde tjekkes med redskabet på (`checkWidth`; for
|
|
||||||
bred → hjemtur + årsag `tooWide`), derefter samme vej som i dag (`startAd` til markens markør / påfyldning,
|
|
||||||
`phase = "driving"`). Fejl → hjemtur, karantæne, markens `retryAt` som ved `noRig`.
|
|
||||||
- `finishAssignment`: har traktoren et puljeredskab bag sig (`ADPool.isReturnDue`) → `ADHitch.returnHome` i
|
|
||||||
stedet for den almindelige parkeringstur; fase `returning`; assignment slettes, når hjemturen er færdig (eller
|
|
||||||
fejler — så med advarsel).
|
|
||||||
- Kø: `tick` gennemgår ventende marker i `ADPool.queueOrder`. `waitingSince` sættes, når en mark første gang ikke
|
|
||||||
fik en maskine, og nulstilles ved udsendelse. `DISPATCH_GAP_MS` (10 s) beholdes.
|
|
||||||
- Fanen viser faserne "henter X" og "sætter X af" og årsagerne `noPoolTractor`/`tooWeak`.
|
|
||||||
|
|
||||||
### Tømning ved høst
|
|
||||||
|
|
||||||
- Helper-dispatch (`role = "unloader"`, `helper = true`) bruger samme fallback: puljevogn + puljetraktor.
|
|
||||||
- Efter `fetch`: `startUnloader` som i dag. Assignmentet bliver hos marken, til høststadiet er slut (som i dag);
|
|
||||||
først da `returnHome`. Ingen frigivelse mellem læs.
|
|
||||||
|
|
||||||
### Marksilo-flowet (`adFieldSilos.lua`, `adFieldSiloPlanner.lua`)
|
|
||||||
|
|
||||||
- `collectVehicles` tilføjer puljekombinationer (puljevogn der kan bære varen + nærmeste puljetraktor) som
|
|
||||||
kandidater med `pool = {...}`; planneren behandler dem som vogne (position = vognens).
|
|
||||||
- `withoutHarvestReserve` tæller puljevogne med, så høsten går først.
|
|
||||||
- Lån med `pool`: `ADHitch.fetch` → AD "Hent og aflever" som i dag (`enableParkAtJobFinished`) → når AD er
|
|
||||||
inaktiv: `returnHome` → lånet slettes, når traktoren er hjemme. Vagten (120 s uden fremgang) starter efter
|
|
||||||
koblingen.
|
|
||||||
|
|
||||||
## Genindlæsning
|
|
||||||
|
|
||||||
Reservationer og assignments gemmes ikke (som i dag). Efter indlæsning ser en traktor med puljeredskab ud som en
|
|
||||||
færdig kombination; når markarbejdet eller marksilo-flowet afslutter et job med den, køres `returnHome`. Reglen
|
|
||||||
afhænger kun af, hvad der hænger bag traktoren, ikke af gemt tilstand. En traktor, som ingen af vores jobs bruger,
|
|
||||||
køres aldrig hjem af sig selv (spilleren kan have koblet et puljeredskab på for at køre selv).
|
|
||||||
|
|
||||||
## Fejl
|
|
||||||
|
|
||||||
| Situation | Handling |
|
|
||||||
|---|---|
|
|
||||||
| Kobling fejler (`noAttachable`, `stoppedAway`) | Traktor hjem, redskab i karantæne 10 min, marken prøver igen (andet redskab eller venter) |
|
|
||||||
| Redskab for bredt når koblet på | Hjemtur, årsag `tooWide` i fanen |
|
|
||||||
| Spilleren sætter sig i traktoren | Assignment afbrydes som i dag; reservationer frigives |
|
|
||||||
| Ingen traktor stærk nok | Årsag `tooWeak` med hk-tal: "ingen traktor med ≥180 hk ledig til 'Plov 1'" |
|
|
||||||
| Hjemtur fejler | Advarsel i loggen; traktor og redskab frigives (spilleren må ordne det) |
|
|
||||||
|
|
||||||
## Undtaget / uden for scope
|
|
||||||
|
|
||||||
- Flows (runs) — deres køretøjer er aldrig i puljen (uændret regel i `getRigs`).
|
|
||||||
- Vogne uden Connect-markør — dedikerede som i dag.
|
|
||||||
- Genbrug af et redskab direkte til næste mark uden at parkere (senere optimering).
|
|
||||||
- Flere redskaber pr. traktor (front + bag).
|
|
||||||
- Egen GUI-side for puljen.
|
|
||||||
|
|
||||||
## Etaper
|
|
||||||
|
|
||||||
1. **v1.48.0.0 — markarbejde med pulje:** `adPool.lua`, `returnHome`, dispatch-fallback, fase fetching/returning,
|
|
||||||
kø-rækkefølge, fane-tekster. In-game: savegame4, mark der skal harves/pløjes, redskab med parkering + Connect,
|
|
||||||
solo-traktor henter → arbejder → parkerer → hjem.
|
|
||||||
2. **v1.49.0.0 — tømmevogn ved høst fra puljen.**
|
|
||||||
3. **v1.50.0.0 — marksilo med puljevogne** (inkl. høst-reserve over puljevogne).
|
|
||||||
|
|
||||||
## Test
|
|
||||||
|
|
||||||
- `tests/test_adPool.lua`: puljeredskab ja/nej (koblet på, uden parkering, uden Connect), `roleOf` inkl.
|
|
||||||
`unloader`, `powerOk` (nok/for lidt/manglende data), `pairFor` (redskab nærmest marken, traktor nærmest redskabet,
|
|
||||||
reserveret og karantæne springes over, `tooWeak`/`noPoolTractor`/`noPoolImplement`), `queueOrder`,
|
|
||||||
`isReturnDue`.
|
|
||||||
- `tests/test_adHitch.lua`: `returnHome`-rutens trin.
|
|
||||||
- `tests/test_adFieldJobs.lua`: færdig kombination foretrækkes; fallback til pulje; kø længst ventende først.
|
|
||||||
- `tests/test_adFieldSiloPlanner.lua` (etape 3): puljekandidater og høst-reserve med puljevogne.
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
# Flows og Fields: farvet liste + redigeringsvindue
|
|
||||||
|
|
||||||
Dato: 2026-09-29 · Status: godkendt i chat ("men kør"), afventer review af denne spec
|
|
||||||
|
|
||||||
## Formål
|
|
||||||
|
|
||||||
Flows- og Fields-fanen er i dag én lang rullende side: listen over flows/marker efterfulgt af den valgte
|
|
||||||
posts redigeringslinjer. Det er svært at overskue (brugerens screenshot 29/9 17:15). Målet:
|
|
||||||
|
|
||||||
- Fanen viser **kun listen**, med farvet status og **Start/Stop pr. række**.
|
|
||||||
- **Klik på en række** åbner et **redigeringsvindue** (popup) oven på listen med de nuværende linjer.
|
|
||||||
|
|
||||||
Succes = man kan se med ét blik hvad der kører/venter/fejler, starte/stoppe en post uden at vælge den først,
|
|
||||||
og redigere i et vindue der lukkes med Luk/ESC — uden at noget af flow-/mark-logikken ændres.
|
|
||||||
|
|
||||||
## Ikke-mål
|
|
||||||
|
|
||||||
- Ingen ændring af flow-/mark-logik, guiderne ("Nyt flow", "Opsæt mark") eller deres trin.
|
|
||||||
- Oversigt-tavlen og Fields' flåde-visning er uændrede.
|
|
||||||
- Ingen ny FS25-GUI-skærm/dialog (fravalgt: al rækkekode skulle genbygges; fokus/ESC oven på ESC-menuen er
|
|
||||||
skrøbeligt). "Popup" er et panel inde i samme frame.
|
|
||||||
|
|
||||||
## Brugeroplevelse
|
|
||||||
|
|
||||||
### Flows-fanen (liste)
|
|
||||||
|
|
||||||
Én række pr. flow: `● navn · varer · kørende/i alt · statustekst · [Start]/[Stop]`.
|
|
||||||
|
|
||||||
| Niveau | Prik/statusfarve | Hvornår |
|
|
||||||
|---|---|---|
|
|
||||||
| running | grøn | mindst én traktor kører og ingen er blokeret |
|
|
||||||
| waiting | gul | startet, men venter: ingen varer at hente, venter på vogn, traktor optaget, holder ved Wait |
|
|
||||||
| error | rød | kan ikke køre: sidste start afvist (fx ingen vogn), advarsel (`!!`) |
|
|
||||||
| stopped | grå | stoppet / aldrig startet |
|
|
||||||
|
|
||||||
- `[Start]/[Stop]` bruger den eksisterende `ADRunsController.startRun/stopRun` (samme regler som knaplinjen, inkl.
|
|
||||||
sprunget-over-besked i højre side).
|
|
||||||
- Klik på rækken (ikke knappen) åbner redigeringsvinduet for flowet.
|
|
||||||
- Knaplinjen (i dag Tilbage, Start/Stop, Opsæt flow, Nyt flow, Slet) bliver: Tilbage, Nyt flow. Start/Stop sidder på
|
|
||||||
rækkerne; "Opsæt flow" og "Slet" flyttes ind i vinduet.
|
|
||||||
|
|
||||||
### Fields-fanen (liste)
|
|
||||||
|
|
||||||
Nuværende kolonner (Mark, Afgrøde, Plan, Kørsel, Status, Traktorer) + `●` forrest og `[Start]/[Stop]` bagerst.
|
|
||||||
|
|
||||||
| Niveau | Hvornår |
|
|
||||||
|---|---|
|
|
||||||
| running (grøn) | marken har aktive opgaver der arbejder |
|
|
||||||
| waiting (gul) | startet men venter: på maskine, vejr, påfyldning, "startes når en maskine er ledig" |
|
|
||||||
| error (rød) | mangler markør, arbejdsbredde for bred/ukendt, ingen egnet maskine |
|
|
||||||
| stopped (grå) | ingen plan / stoppet |
|
|
||||||
|
|
||||||
- `[Start]/[Stop]` genbruger `onClickFieldStartStop` (inkl. "Overtag høster").
|
|
||||||
- Klik på rækken åbner vinduet med markens resumé og [Ret]-linjer.
|
|
||||||
|
|
||||||
### Redigeringsvinduet (fælles)
|
|
||||||
|
|
||||||
- Mørk halvgennemsigtig baggrund over listen; centreret panel (~70 % bredde, ~80 % højde).
|
|
||||||
- Top: titel ("Rediger: Cow Food" / "Mark F12"), statuslinje i niveau-farve.
|
|
||||||
- Midte: egen rullende liste med præcis det indhold, der i dag vises under listen:
|
|
||||||
flow → `fillFlowSummary`-linjerne; mark → `fillFieldOverview`-linjerne. [Edit]/[Ret] åbner trinnet
|
|
||||||
(guiden i enkelt-trin-tilstand) **inde i vinduet** med Gem/Tilbage som i dag.
|
|
||||||
- Bund: `Luk` · `Start/Stop` · `Slet` (kun flow). ESC/Tilbage lukker vinduet (ikke menuen); står et trin åbent,
|
|
||||||
går ESC først tilbage til resuméet.
|
|
||||||
- Mens vinduet er åbent, opdateres statuslinjen og listen bagved hvert sekund som i dag.
|
|
||||||
|
|
||||||
## Arkitektur
|
|
||||||
|
|
||||||
### Rene funktioner (testbare)
|
|
||||||
|
|
||||||
- `ADFlowView.flowStatus(s)` → `{level, text}` hvor `s = {running, total, canStop, lastStartSkipped, warning,
|
|
||||||
waitReason, holdReason, stopped}`. Afgør niveau efter tabellen ovenfor.
|
|
||||||
- `ADFieldView.fieldStatus(s)` → `{level, text}` hvor `s = {working, awaiting, starting, missingMarker,
|
|
||||||
widthProblem, noMachine, hasPlan}`.
|
|
||||||
- `ADFlowView.LEVEL_COLORS` (grøn/gul/rød/grå) — flyttes/udleveres fra `SmartPickupStorageView`s lokale tabel,
|
|
||||||
som også eksporterer `setTextColor` til genbrug.
|
|
||||||
|
|
||||||
### GUI
|
|
||||||
|
|
||||||
- **XML** (`SmartPickupFrame.xml`): efter `boxLayout` et `GuiElement id="editOverlay"` (skjult) med
|
|
||||||
`Bitmap id="editBackdrop"` (fuld flade, sort ~60 %), `Bitmap id="editPanel"` (centreret, baggrund som
|
|
||||||
fs25-paneler) med `Text id="editTitle"`, `Text id="editStatus"`, `ScrollingLayout id="editLayout"` (+ slider) og
|
|
||||||
en knaplinje med tre `Button`s (`editClose`, `editStartStop`, `editDelete`).
|
|
||||||
- **Rækkeprefabs**: `flowPrefab` og `fieldPrefab` får en `Bitmap name="dot"` (profil `spuDot`) forrest og en
|
|
||||||
`Button name="rowAction"` bagerst. Kolonnebredder justeres så rækken stadig er ≈835 px.
|
|
||||||
- **Mål-layout for rækker**: `addRow`/`addTextRow`/`addCellRow`/`addLinkRow`/`addGuideRow` kloner i dag altid ind i
|
|
||||||
`self.boxLayout`. Der indføres `self.rowTarget` (standard `boxLayout`); vinduet sætter det til `editLayout`
|
|
||||||
mens det fyldes. `clearRows` holder klonerne pr. layout, så listen og vinduet kan genopbygges hver for sig.
|
|
||||||
- **Ny fil** `gui/SmartPickupEditPanel.lua` (mixin på `SmartPickupFrame`, `source()`s som de andre):
|
|
||||||
`openEditPanel(kind, id)`, `closeEditPanel()`, `fillEditPanel(adEnv)`, `refreshEditPanel(adEnv)`,
|
|
||||||
knap-callbacks. `kind ∈ {"flow","field"}`.
|
|
||||||
- **Input**: `backButtonInfo`-callback og ESC tjekker først `self.editPanelOpen` (luk trin → luk vindue →
|
|
||||||
normal Tilbage). Klik på backdrop gør intet (blokerer klik til listen bagved via `mouseEvent` der returnerer
|
|
||||||
"brugt").
|
|
||||||
- **Guide i vinduet**: `openFlowStep`/`openFieldStep` sætter `returnTo = "panel"`, så Gem/Tilbage vender tilbage til
|
|
||||||
vinduets resumé i stedet for at lukke guiden til fanen.
|
|
||||||
|
|
||||||
### Dataflow
|
|
||||||
|
|
||||||
Klik på række → `openEditPanel(kind, id)` → `selectedRunId`/`selectedFieldId` sættes (eksisterende felter) →
|
|
||||||
`requestRebuild` → `rebuild` tegner listen i `boxLayout` og, når vinduet er åbent, vinduets indhold i
|
|
||||||
`editLayout`. Start/Stop i række eller vindue kalder de eksisterende controller-funktioner og `requestRebuild`.
|
|
||||||
|
|
||||||
## Fejlhåndtering
|
|
||||||
|
|
||||||
- Alt nyt GUI-arbejde kører i `self:safe(...)` som resten af frame'n; fejl vises i statuslinjen og logges.
|
|
||||||
- Forsvinder flowet/marken mens vinduet er åbent (slettet), lukkes vinduet og statuslinjen siger hvorfor.
|
|
||||||
- `rowAction`-knappen er deaktiveret når handlingen ikke er mulig (fx mark uden markør → ingen Start).
|
|
||||||
|
|
||||||
## Test
|
|
||||||
|
|
||||||
- luajit: `tests/test_flowStatus.lua` og `tests/test_fieldStatus.lua` for alle niveauer i tabellerne +
|
|
||||||
grænsetilfælde (0 traktorer, stoppet med advarsel, venter + afvist).
|
|
||||||
- Eksisterende tests skal forblive grønne (`test_noStrayGlobals` dækker den nye fil).
|
|
||||||
- In-game (brugeren): liste med farver, Start/Stop pr. række, åbn/luk vindue med klik/Luk/ESC, [Edit] → Gem inde i
|
|
||||||
vinduet, Slet flow fra vinduet, klik på listen bag vinduet gør intet, mark "Overtag høster" virker fra rækken.
|
|
||||||
|
|
||||||
## Levering
|
|
||||||
|
|
||||||
To etaper, hver sin version og in-game test:
|
|
||||||
|
|
||||||
1. **Flows**: rene statusfunktioner + fælles vindue + Flows-liste.
|
|
||||||
2. **Fields**: mark-status + Fields-liste genbruger vinduet.
|
|
||||||
|
|
||||||
## Risici
|
|
||||||
|
|
||||||
- **Klik gennem backdrop** til listen: afbødes ved at backdrop'en fanger `mouseEvent`; fallback: skjul listens rækker
|
|
||||||
(`setVisible(false)`) mens vinduet er åbent.
|
|
||||||
- **Musehjul/rulning** i to ScrollingLayouts: hver får sin egen slider; FS25 ruller det layout musen er over.
|
|
||||||
- **Knapper i rækkeprefabs** (Button i en klonet Bitmap) er ikke brugt før i modden: onClick sættes efter
|
|
||||||
`clone` pr. række (som `makeRowClickable`), ikke via XML-callback.
|
|
||||||
@@ -1,254 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adBuildings.lua (fra repo-roden)
|
|
||||||
-- Bygninger gården kan bruge + automatisk markør ved læsse-/aflæsningstrigger (≤ 40 m, Wait udelukket,
|
|
||||||
-- siloer strengt), omvendt opslag markør -> bygning, id-fallback, cache.
|
|
||||||
|
|
||||||
local SUGAR, FORAGE, MILK, BASKET = 5, 6, 12, 7
|
|
||||||
local DESC = {[SUGAR] = {name = "SUGAR", title = "Sukker", isPalletType = true}, [FORAGE] = {name = "FORAGE", title = "TMR"}, [MILK] = {name = "MILK", title = "Mælk"}, [BASKET] = {name = "STRAW_BASKET", title = "Kurve", palletFilename = "x"}}
|
|
||||||
g_fillTypeManager = {getFillTypeByIndex = function(_, i) return DESC[i] end, getFillTypeNameByIndex = function(_, i) return DESC[i] and DESC[i].name end, getFillTypeTitleByIndex = function(_, i) return DESC[i] and DESC[i].title end}
|
|
||||||
Logging = {info = function() end, warning = function() end}
|
|
||||||
getWorldTranslation = function(node) return node.x, 0, node.z end
|
|
||||||
SellingStation = {}
|
|
||||||
dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")
|
|
||||||
dofile("FS25_ADSmartPickup/adPrices.lua")
|
|
||||||
dofile("FS25_ADSmartPickup/adBuildings.lua")
|
|
||||||
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else failures = failures + 1; print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual))) end
|
|
||||||
end
|
|
||||||
|
|
||||||
local markers, wayPoints = {}, {}
|
|
||||||
local function addMarker(id, name, group, x, z)
|
|
||||||
wayPoints[id] = {id = id, x = x, z = z}
|
|
||||||
table.insert(markers, {id = id, markerIndex = #markers + 1, name = name, group = group})
|
|
||||||
end
|
|
||||||
local loadStations, unloadStations = {}, {}
|
|
||||||
local adEnv = {ADGraphManager = {
|
|
||||||
getWayPointById = function(_, id) return wayPoints[id] end,
|
|
||||||
getMapMarkers = function() return markers end,
|
|
||||||
getMapMarkerByWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id then return m end end end,
|
|
||||||
}}
|
|
||||||
local strictLoadAt = {}
|
|
||||||
ADSmartPickup = {getLoadStationAtWayPointId = function(_, id) return strictLoadAt[id] end}
|
|
||||||
g_currentMission = {
|
|
||||||
placeableSystem = {placeables = {}},
|
|
||||||
storageSystem = {getLoadingStations = function() return loadStations end, getUnloadingStations = function() return unloadStations end},
|
|
||||||
accessHandler = {canFarmAccess = function() return true end},
|
|
||||||
getFarmId = function() return 1 end,
|
|
||||||
}
|
|
||||||
local function placeable(uid, name, x, z, extra)
|
|
||||||
local p = {uniqueId = uid, rootNode = {x = x, z = z}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return name end}
|
|
||||||
for k, v in pairs(extra or {}) do p[k] = v end
|
|
||||||
table.insert(g_currentMission.placeableSystem.placeables, p)
|
|
||||||
return p
|
|
||||||
end
|
|
||||||
|
|
||||||
-- verden: kostald med mælketank (load) og foder (unload), sukkerfabrik (unload + pallespawner), to siloer i række, objektlager, salgssted
|
|
||||||
local barn = placeable("barn1", "Kostald 1", 0, 0, {getHusbandryFillLevel = function() return 0 end})
|
|
||||||
-- som FS25_LargerAnimalBarns' cowBarnBig: ÉN læssestation med TO læssepunkter (gylle + mælk)
|
|
||||||
local milkStation = {owningPlaceable = barn, loadTriggers = {{triggerNode = {x = 20, z = 0}}, {triggerNode = {x = 0, z = 30}}}}
|
|
||||||
local barnUnload = {owningPlaceable = barn, unloadTriggers = {{exactFillRootNode = {x = -20, z = 0}}}}
|
|
||||||
table.insert(loadStations, milkStation); table.insert(unloadStations, barnUnload)
|
|
||||||
local mill = placeable("mill", "Sukkerfabrik", 500, 0, {spec_productionPoint = {productionPoint = {productions = {}, storage = {}, palletSpawner = {spawnPlaces = {{startNode = {x = 520, z = 0}}}}, unloadingStation = nil}}})
|
|
||||||
local millUnload = {owningPlaceable = mill, unloadTriggers = {{exactFillRootNode = {x = 480, z = 0}}}, isa = function() return true end, isSellingPoint = true}
|
|
||||||
table.insert(unloadStations, millUnload)
|
|
||||||
local silo1 = placeable("silo1", "Silo 1", 1000, 0, {spec_silo = {}})
|
|
||||||
local silo2 = placeable("silo2", "Silo 2", 1015, 0, {spec_silo = {}})
|
|
||||||
local silo1Station = {owningPlaceable = silo1, loadTriggers = {{triggerNode = {x = 1000, z = 5}}}}
|
|
||||||
local silo2Station = {owningPlaceable = silo2, loadTriggers = {{triggerNode = {x = 1015, z = 5}}}}
|
|
||||||
table.insert(loadStations, silo1Station); table.insert(loadStations, silo2Station)
|
|
||||||
table.insert(unloadStations, {owningPlaceable = silo1, unloadTriggers = {{exactFillRootNode = {x = 1000, z = 5}}}})
|
|
||||||
table.insert(unloadStations, {owningPlaceable = silo2, unloadTriggers = {{exactFillRootNode = {x = 1015, z = 5}}}})
|
|
||||||
local shed = placeable("shed", "Palleskur", 2000, 0, {spec_objectStorage = {objectInfos = {}, capacity = 100}})
|
|
||||||
local sellPoint = placeable("sell", "Mejeri", 3000, 0, {})
|
|
||||||
local sellStation = {owningPlaceable = sellPoint, unloadTriggers = {{exactFillRootNode = {x = 3000, z = 10}}}, isa = function() return true end, isSellingPoint = true}
|
|
||||||
table.insert(unloadStations, sellStation)
|
|
||||||
local other = placeable("otherfarm", "Naboens", 4000, 0, {spec_silo = {}, ownerFarmId = 2, getOwnerFarmId = function() return 2 end})
|
|
||||||
|
|
||||||
addMarker(1, "cow 1 milk", "Milk", 25, 0) -- 5 m fra mælketriggeren
|
|
||||||
addMarker(2, "Cow 1 food", "Cow food", -30, 0) -- 10 m fra fodertriggeren
|
|
||||||
addMarker(3, "cow wait 1", "cow wait", 22, 0) -- Wait: må ALDRIG vælges (tættere end mælkemarkøren)
|
|
||||||
addMarker(11, "Cow 1 Slurry", "Slurry", 0, 33) -- 3 m fra gylletriggeren
|
|
||||||
addMarker(4, "Suger Load", "Pickup", 525, 0) -- 5 m fra pallespawneren
|
|
||||||
addMarker(5, "sugar offload", "production", 470, 0)
|
|
||||||
addMarker(6, "silo 1", "silo", 1000, 12) -- strengt: silo1
|
|
||||||
addMarker(7, "silo 2", "silo", 1015, 12) -- strengt: silo2 (7 m fra silo1's trigger-node på x-aksen? nej: 15 m)
|
|
||||||
strictLoadAt[6] = silo1Station; strictLoadAt[7] = silo2Station
|
|
||||||
addMarker(8, "Storage pickup", "Pickup", 2010, 0)
|
|
||||||
addMarker(9, "Mejeri", "selling", 3000, 20)
|
|
||||||
addMarker(10, "Langt væk", "All", 9000, 9000)
|
|
||||||
|
|
||||||
-- B1: liste, kind og markører
|
|
||||||
local list = ADBuildings.list(adEnv, 1)
|
|
||||||
local byId = {}
|
|
||||||
for _, b in ipairs(list) do byId[b.id] = b end
|
|
||||||
check("B1 antal (naboens silo udeladt)", #list, 6)
|
|
||||||
check("B1 stald", byId.barn1.kind, "husbandry")
|
|
||||||
check("B1 stald læssemarkør = mælk (ikke Wait, som er tættere)", byId.barn1.loadMarker.id, 1)
|
|
||||||
-- én markør pr. læssestation (mælketank OG gyllebeholder), sorteret på markørnavn
|
|
||||||
check("B1 stald har to læssemarkører", #byId.barn1.loadMarkers, 2)
|
|
||||||
check("B1 første = cow 1 milk (navnesorteret, uanset store/små)", byId.barn1.loadMarkers[1].id, 1)
|
|
||||||
check("B1 anden = Cow 1 Slurry", byId.barn1.loadMarkers[2].name, "Cow 1 Slurry")
|
|
||||||
check("B1 gyllemarkør -> stald via byMarker", ADBuildings.byMarker(adEnv, 1, 11, "load").id, "barn1")
|
|
||||||
check("B1 stald aflæsningsmarkør = foder", byId.barn1.unloadMarker.id, 2)
|
|
||||||
check("B1 stald afstand", byId.barn1.loadMarker.distance, 5)
|
|
||||||
check("B1 fabrik læssemarkør via pallespawner", byId.mill.loadMarker.id, 4)
|
|
||||||
check("B1 fabrik aflæsningsmarkør", byId.mill.unloadMarker.id, 5)
|
|
||||||
check("B1 fabrik er ikke salgssted", byId.mill.canSell, false)
|
|
||||||
check("B1 objektlager markør via rootNode", byId.shed.loadMarker.id, 8)
|
|
||||||
check("B1 salgssted", byId.sell.kind, "sell")
|
|
||||||
check("B1 salgssted markør", byId.sell.unloadMarker.id, 9)
|
|
||||||
check("B1 salgssted canSell", byId.sell.canSell, true)
|
|
||||||
check("B1 stald kan være kilde og mål", byId.barn1.canBeSource and byId.barn1.canBeTarget, true)
|
|
||||||
check("B1 silo kilde, ikke mål", byId.silo1.canBeSource and not byId.silo1.canBeTarget, true)
|
|
||||||
|
|
||||||
-- B2: siloer i række: strengt opslag giver hver sin markør
|
|
||||||
check("B2 silo 1 markør", byId.silo1.loadMarker.id, 6)
|
|
||||||
check("B2 silo 2 markør", byId.silo2.loadMarker.id, 7)
|
|
||||||
strictLoadAt[7] = nil
|
|
||||||
ADBuildings.invalidate()
|
|
||||||
check("B2 silo uden strengt match: ingen læssemarkør", ADBuildings.find(adEnv, 1, "silo2").loadMarker, nil)
|
|
||||||
strictLoadAt[7] = silo2Station
|
|
||||||
ADBuildings.invalidate()
|
|
||||||
|
|
||||||
-- B3: byMarker (omvendt opslag)
|
|
||||||
check("B3 mælkemarkør -> stald (load)", ADBuildings.byMarker(adEnv, 1, 1, "load").id, "barn1")
|
|
||||||
check("B3 fodermarkør -> stald (unload)", ADBuildings.byMarker(adEnv, 1, 2, "unload").id, "barn1")
|
|
||||||
check("B3 fodermarkør er ikke load", ADBuildings.byMarker(adEnv, 1, 2, "load"), nil)
|
|
||||||
check("B3 fjern markør -> nil", ADBuildings.byMarker(adEnv, 1, 10, "load"), nil)
|
|
||||||
|
|
||||||
-- B4: id-fallback uden uniqueId
|
|
||||||
check("B4 id fra position", ADBuildings.getId({rootNode = {x = 12.6, z = -3.2}}), "pos:13:-3")
|
|
||||||
check("B4 id fra uniqueId", ADBuildings.getId(barn), "barn1")
|
|
||||||
|
|
||||||
-- B5: cache: samme tabel inden for CACHE_MS, ny efter invalidate
|
|
||||||
local first = ADBuildings.list(adEnv, 1)
|
|
||||||
check("B5 cachet", ADBuildings.list(adEnv, 1) == first, true)
|
|
||||||
ADBuildings.invalidate()
|
|
||||||
check("B5 ny efter invalidate", ADBuildings.list(adEnv, 1) ~= first, true)
|
|
||||||
|
|
||||||
-- B6: sortering efter NAVN (naturlig talrækkefølge), dernæst første markørnavn — ikke efter slags
|
|
||||||
local order = {}
|
|
||||||
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do table.insert(order, b.name) end
|
|
||||||
check("B6 rækkefølge", table.concat(order, ","), "Kostald 1,Mejeri,Palleskur,Silo 1,Silo 2,Sukkerfabrik")
|
|
||||||
check("B6 naturlig sortering", ADBuildings.naturalLess("Silo 9", "Silo 10"), true)
|
|
||||||
check("B6 naturlig sortering, tal før tekst-lighed", ADBuildings.naturalLess("25 Silo Storage", "26 Silo Storage"), true)
|
|
||||||
check("B6 naturlig sortering ens navn -> markørnavn", ADBuildings.compare({name = "Farma", loadMarkers = {{name = "30 Silo"}}}, {name = "Farma", loadMarkers = {{name = "4 Silo"}}}), false)
|
|
||||||
|
|
||||||
-- B-DOB: silo der kun kan FYLDES (drive-on-bunker ved biogas: aflæsning, ingen læssestation) = mål, ikke kilde
|
|
||||||
do
|
|
||||||
local dob = placeable("dob1", "DOB 115", 5000, 0, {spec_silo = {}})
|
|
||||||
table.insert(unloadStations, {owningPlaceable = dob, unloadTriggers = {{exactFillRootNode = {x = 5000, z = 5}}}})
|
|
||||||
addMarker(21, "Biogas Drop 1", "Biogas", 5000, 12)
|
|
||||||
ADBuildings.invalidate()
|
|
||||||
local byIdDob = {}
|
|
||||||
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byIdDob[b.id] = b end
|
|
||||||
check("B-DOB kan være mål", byIdDob.dob1.canBeTarget, true)
|
|
||||||
check("B-DOB ikke kilde (intet at hente)", byIdDob.dob1.canBeSource, false)
|
|
||||||
check("B-DOB aflæsningsmarkør", byIdDob.dob1.unloadMarker.id, 21)
|
|
||||||
check("B-DOB almindelig silo stadig ikke mål", byIdDob.silo1.canBeTarget, false)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- B-SEP: bygning med læssestation men uden silo/fabrik/stald-spec (Pumps&Hoses separatorhal: digestat-udlevering)
|
|
||||||
do
|
|
||||||
local sep = placeable("sep1", "Separator Hall", 7000, 0, {spec_manureSeparator = {}})
|
|
||||||
table.insert(loadStations, {owningPlaceable = sep, loadTriggers = {{triggerNode = {x = 7000, z = 5}}}})
|
|
||||||
addMarker(41, "Digestate pickup", "Biogas", 7000, 10)
|
|
||||||
ADBuildings.invalidate()
|
|
||||||
local bySep = {}
|
|
||||||
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do bySep[b.id] = b end
|
|
||||||
check("B-SEP findes som lager", bySep.sep1 and bySep.sep1.kind, "storage")
|
|
||||||
check("B-SEP kan være kilde", bySep.sep1 and bySep.sep1.canBeSource, true)
|
|
||||||
check("B-SEP læssemarkør", bySep.sep1 and bySep.sep1.loadMarker and bySep.sep1.loadMarker.id, 41)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- B-DROP: fabrik med pickup-markør TÆT på aflæsningstriggeren og en separat drop-markør længere væk
|
|
||||||
-- (Sugarbeet Crusher): aflæsningsmarkøren skal være drop, ikke bygningens egen læssemarkør
|
|
||||||
do
|
|
||||||
local crusher = placeable("crusher1", "Sugarbeet Crusher", 8000, 0, {spec_productionPoint = {productionPoint = {productions = {}, storage = {}}}})
|
|
||||||
table.insert(loadStations, {owningPlaceable = crusher, loadTriggers = {{triggerNode = {x = 8000, z = 5}}}})
|
|
||||||
table.insert(unloadStations, {owningPlaceable = crusher, unloadTriggers = {{exactFillRootNode = {x = 8003, z = 2}}}})
|
|
||||||
addMarker(51, "Sugar Beet Cut Pickup", "Fabrication", 8000, 8) -- 3 m fra læs, ~6 m fra aflæsning
|
|
||||||
addMarker(52, "Sugar Beet Cut Beet Drop", "Fabrication", 8012, 2) -- 9 m fra aflæsning
|
|
||||||
ADBuildings.invalidate()
|
|
||||||
local byC = {}
|
|
||||||
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byC[b.id] = b end
|
|
||||||
check("B-DROP læssemarkør = pickup", byC.crusher1.loadMarker.id, 51)
|
|
||||||
check("B-DROP aflæsningsmarkør = drop (ikke pickup)", byC.crusher1.unloadMarker.id, 52)
|
|
||||||
-- kun én fælles markør (AIO Load / Unload): bruges til begge
|
|
||||||
local aio = placeable("aio1", "AIO", 9000, 0, {spec_productionPoint = {productionPoint = {productions = {}, storage = {}}}})
|
|
||||||
table.insert(loadStations, {owningPlaceable = aio, loadTriggers = {{triggerNode = {x = 9000, z = 5}}}})
|
|
||||||
table.insert(unloadStations, {owningPlaceable = aio, unloadTriggers = {{exactFillRootNode = {x = 9002, z = 5}}}})
|
|
||||||
addMarker(53, "AIO Load / Unload", "Fabrication", 9000, 9)
|
|
||||||
ADBuildings.invalidate()
|
|
||||||
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byC[b.id] = b end
|
|
||||||
check("B-DROP fælles markør bruges til aflæsning", byC.aio1.unloadMarker.id, 53)
|
|
||||||
check("B-DROP silo beholder sin egen markør (ikke nabosiloens)", byC.silo1.unloadMarker.id, 6)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- B-COOP: hønsehus — foder aflæsses i den ene ende, æg-paller spawnes i den anden (ingen læssestation)
|
|
||||||
do
|
|
||||||
local coop = placeable("coop1", "Chicken Coop", 10000, 0, {getHusbandryFillLevel = function() return 0 end,
|
|
||||||
spec_husbandryFood = {triggers = {{someTrigger = {exactFillRootNode = {x = 9970, z = 0}}}}}, -- ukendt feltsti: søges
|
|
||||||
spec_husbandryPallets = {fillTypeIndexToPalletSpawner = {[99] = {spawnPlaces = {{startNode = {x = 10030, z = 0}}}}}}})
|
|
||||||
table.insert(unloadStations, {owningPlaceable = coop, unloadTriggers = {}}) -- som chickenBarnBig: tom station
|
|
||||||
addMarker(61, "Chicken Food", "Chicken", 9965, 0)
|
|
||||||
addMarker(62, "Egg Pickup", "Chicken", 10035, 0)
|
|
||||||
ADBuildings.invalidate()
|
|
||||||
local byCoop = {}
|
|
||||||
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byCoop[b.id] = b end
|
|
||||||
check("B-COOP læssemarkør ved æggene", byCoop.coop1.loadMarker and byCoop.coop1.loadMarker.id, 62)
|
|
||||||
check("B-COOP aflæsningsmarkør ved foderet", byCoop.coop1.unloadMarker and byCoop.coop1.unloadMarker.id, 61)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- B-FUEL: brændstoftank (læssestation kun med DIESEL) er ikke et lager/en kilde
|
|
||||||
do
|
|
||||||
local fmBefore = g_fillTypeManager
|
|
||||||
g_fillTypeManager = {getFillTypeNameByIndex = function(_, index) return index == 900 and "DIESEL" or "X" end}
|
|
||||||
local tank = placeable("fuel1", "Small Petrol Tank", 11000, 0, {})
|
|
||||||
table.insert(loadStations, {owningPlaceable = tank, supportedFillTypes = {[900] = true}, loadTriggers = {{triggerNode = {x = 11000, z = 3}}}})
|
|
||||||
ADBuildings.invalidate()
|
|
||||||
local byFuel = {}
|
|
||||||
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byFuel[b.id] = b end
|
|
||||||
check("B-FUEL brændstoftank udeladt", byFuel.fuel1, nil)
|
|
||||||
g_fillTypeManager = fmBefore
|
|
||||||
end
|
|
||||||
|
|
||||||
-- B-SCAN: søgning efter trigger-noder i indlejrede tabeller (dybde ≤ 3, ingen løkker)
|
|
||||||
do
|
|
||||||
local loop = {}
|
|
||||||
loop.self = loop
|
|
||||||
local nodes = ADBuildings.findTriggerNodes({a = {b = {exactFillRootNode = "n1"}}, c = {triggerNode = "n2"}, d = loop, e = {f = {g = {h = {exactFillRootNode = "for-dybt"}}}}})
|
|
||||||
table.sort(nodes)
|
|
||||||
check("B-SCAN finder n1 og n2, ikke for dybt", table.concat(nodes, ","), "n1,n2")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- B-SHED: palleskur læsser og aflæsser samme sted — aflæsningsmarkøren er den nærmeste (ikke "den anden")
|
|
||||||
do
|
|
||||||
local shedX = placeable("shedX", "Bale & Pallet Storage", 12000, 0, {spec_objectStorage = {objectInfos = {}, capacity = 4000}})
|
|
||||||
addMarker(71, "Chicken Storage Unload", "Chicken", 12010, 0) -- 10 m
|
|
||||||
addMarker(72, "Chicken Storage Pickup", "Chicken", 12018, 0) -- 18 m
|
|
||||||
ADBuildings.invalidate()
|
|
||||||
local byShed = {}
|
|
||||||
for _, b in ipairs(ADBuildings.list(adEnv, 1)) do byShed[b.id] = b end
|
|
||||||
check("B-SHED aflæsning = nærmeste (Unload)", byShed.shedX.unloadMarker and byShed.shedX.unloadMarker.id, 71)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
-- pickLoadMarker: en pallespawner viger for aflæsningen, når begge vil have samme markør og aflæsningen står
|
|
||||||
-- tættere (in-game 27/9 fårestald: spawner 9 m fra "Sheep Food", foderindkast 5 m -> Goat Food fik "Sheep Milk")
|
|
||||||
do
|
|
||||||
local food, milk = {id = 1, name = "Sheep Food 1", distance = 9}, {id = 2, name = "Sheep Milk 1", distance = 13}
|
|
||||||
local unloadBest = {id = 1, name = "Sheep Food 1", distance = 5}
|
|
||||||
check("PL spawner viger for tættere aflæsning", ADBuildings.pickLoadMarker({food, milk}, unloadBest, true).name, "Sheep Milk 1")
|
|
||||||
check("PL rigtig læssetrigger beholder markøren (Sugarbeet Crusher)", ADBuildings.pickLoadMarker({food, milk}, unloadBest, false).name, "Sheep Food 1")
|
|
||||||
-- in-game 27/9 fårestald 3 og 5: spawner 5 m fra "Sheep Food", foderindkast 9 m -> spawneren vandt stadig
|
|
||||||
check("PL spawner viger også når den selv står tættest", ADBuildings.pickLoadMarker({food, milk}, {id = 1, name = "Sheep Food 1", distance = 12}, true).name, "Sheep Milk 1")
|
|
||||||
check("PL rigtig læssetrigger tættest beholder", ADBuildings.pickLoadMarker({food, milk}, {id = 1, name = "Sheep Food 1", distance = 12}, false).name, "Sheep Food 1")
|
|
||||||
check("PL ingen konflikt", ADBuildings.pickLoadMarker({milk}, unloadBest, true).name, "Sheep Milk 1")
|
|
||||||
check("PL eneste kandidat er aflæsningens -> ingen læssemarkør", ADBuildings.pickLoadMarker({food}, unloadBest, true), nil)
|
|
||||||
check("PL ingen kandidater", ADBuildings.pickLoadMarker({}, unloadBest, true), nil)
|
|
||||||
end
|
|
||||||
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adCache.lua (fra repo-roden)
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
g_currentMission = {time = 0}
|
|
||||||
dofile("FS25_ADSmartPickup/adCache.lua")
|
|
||||||
local calls = 0
|
|
||||||
local function lookup() calls = calls + 1; return "station" end
|
|
||||||
check("K1 slået fra: altid opslag", ADCacheGet("b", "k", 1000, lookup) .. ADCacheGet("b", "k", 1000, lookup), "stationstation")
|
|
||||||
check("K1 to opslag", calls, 2)
|
|
||||||
ADCache.enabled = true
|
|
||||||
calls = 0
|
|
||||||
ADCacheGet("b", "k", 1000, lookup); ADCacheGet("b", "k", 1000, lookup)
|
|
||||||
check("K2 genbrugt", calls, 1)
|
|
||||||
g_currentMission.time = 900
|
|
||||||
ADCacheGet("b", "k", 1000, lookup)
|
|
||||||
check("K2 inden for levetid", calls, 1)
|
|
||||||
g_currentMission.time = 1600
|
|
||||||
ADCacheGet("b", "k", 1000, lookup)
|
|
||||||
check("K3 efter levetid + spredning (maks 1500) -> nyt opslag", calls, 2)
|
|
||||||
local nothing = 0
|
|
||||||
ADCacheGet("n", "x", 1000, function() nothing = nothing + 1; return nil end)
|
|
||||||
check("K4 intet fundet huskes", ADCacheGet("n", "x", 1000, function() nothing = nothing + 1; return "ny" end), nil)
|
|
||||||
check("K4 kun ét opslag", nothing, 1)
|
|
||||||
-- spredt udløb: 200 poster lavet samtidig udløber ikke samtidig
|
|
||||||
g_currentMission.time = 10000
|
|
||||||
for i = 1, 200 do ADCacheGet("spread", i, 10000, function() return i end) end
|
|
||||||
local expired = 0
|
|
||||||
g_currentMission.time = 22000
|
|
||||||
for i = 1, 200 do ADCacheGet("spread", i, 10000, function() expired = expired + 1; return i end) end
|
|
||||||
check("K5 kun en del udløbet efter 12 s (spredt 10-15 s)", expired > 20 and expired < 180, true)
|
|
||||||
ADCache.clear()
|
|
||||||
check("K6 ryddet", next(ADCache.buckets), nil)
|
|
||||||
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adCourseCache.lua (fra repo-roden)
|
|
||||||
-- Kursus-registret: hvilken mark/bredde/markform/indstillinger en maskines nuværende Courseplay-kursus blev
|
|
||||||
-- lavet til. Genbrug kun når ALT matcher; egen maskine først, ellers en anden maskines kursus (kopi).
|
|
||||||
dofile("FS25_ADSmartPickup/adCourseCache.lua")
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local function count(map)
|
|
||||||
local n = 0
|
|
||||||
for _ in pairs(map) do n = n + 1 end
|
|
||||||
return n
|
|
||||||
end
|
|
||||||
|
|
||||||
local C = ADCourseCache
|
|
||||||
local SIG = "n=4;a=960000;c=800,300"
|
|
||||||
|
|
||||||
-- ---- indstillings-nøglen ----
|
|
||||||
check("K1 standard", C.settingsKey({}), "off=0.0;lane=-;multi=1;vine=-;skip=-")
|
|
||||||
check("K2 nil = standard", C.settingsKey(nil), C.settingsKey({}))
|
|
||||||
check("K3 alt sat", C.settingsKey({toolOffset = 1.25, lane = -1, multiTools = 2, vineOffset = -1, vineSkip = 1}),
|
|
||||||
"off=1.3;lane=-1;multi=2;vine=-1.0;skip=1")
|
|
||||||
check("K4 -0.0 = 0.0", C.settingsKey({toolOffset = -0.0001}), C.settingsKey({toolOffset = 0}))
|
|
||||||
check("K5 float-støj i forskydning tæller ikke", C.settingsKey({toolOffset = 1.2000001}), C.settingsKey({toolOffset = 1.2}))
|
|
||||||
check("K6 anden bane = anden nøgle", C.settingsKey({multiTools = 2, lane = 1}) ~= C.settingsKey({multiTools = 2, lane = -1}), true)
|
|
||||||
check("K7 vin-offset indgår", C.settingsKey({vineOffset = 0}) ~= C.settingsKey({vineOffset = -1}), true)
|
|
||||||
check("K8 vin-skip indgår", C.settingsKey({vineOffset = -1, vineSkip = 0}) ~= C.settingsKey({vineOffset = -1, vineSkip = 1}), true)
|
|
||||||
|
|
||||||
local settings = C.settingsKey({})
|
|
||||||
local request = {field = 22, width = 30, sig = SIG, settings = settings}
|
|
||||||
local own = C.newRecord({vehicle = "v1", field = 22, width = 30.0, sig = SIG, settings = settings, waypoints = 812})
|
|
||||||
|
|
||||||
-- ---- key/matches ----
|
|
||||||
check("M1 samme alt", C.matches(own, request), true)
|
|
||||||
check("M2 bredde som float32 fra XML (30.0000001)", C.matches(C.newRecord({vehicle = "v1", field = 22, width = 30.0000001, sig = SIG, settings = settings}), request), true)
|
|
||||||
check("M3 ny bredde", C.matches(own, {field = 22, width = 12, sig = SIG, settings = settings}), false)
|
|
||||||
check("M4 anden mark", C.matches(own, {field = 23, width = 30, sig = SIG, settings = settings}), false)
|
|
||||||
check("M5 form ændret", C.matches(own, {field = 22, width = 30, sig = "n=5;a=960000;c=800,300", settings = settings}), false)
|
|
||||||
check("M6 andre indstillinger", C.matches(own, {field = 22, width = 30, sig = SIG, settings = C.settingsKey({toolOffset = 2})}), false)
|
|
||||||
check("M7 ukendt form (nil) matcher aldrig", C.matches(C.newRecord({vehicle = "v1", field = 22, width = 30, settings = settings}), {field = 22, width = 30, settings = settings}), false)
|
|
||||||
check("M8 intet register", C.matches(nil, request), false)
|
|
||||||
check("M9 key", C.key(request), "22|30.0|" .. SIG .. "|" .. settings)
|
|
||||||
check("M10 key uden form = nil", C.key({field = 22, width = 30, settings = settings}), nil)
|
|
||||||
|
|
||||||
-- ---- grund til ny beregning ----
|
|
||||||
check("R1 intet gemt", C.mismatchReason(nil, request), "intet kursus gemt")
|
|
||||||
check("R2 form ændret", C.mismatchReason(own, {field = 22, width = 30, sig = "x", settings = settings}), "markens form ændret")
|
|
||||||
check("R3 ny bredde", C.mismatchReason(own, {field = 22, width = 12, sig = SIG, settings = settings}), "ny bredde")
|
|
||||||
check("R4 anden mark", C.mismatchReason(own, {field = 9, width = 30, sig = SIG, settings = settings}), "kursus til en anden mark")
|
|
||||||
check("R5 indstillinger", C.mismatchReason(own, {field = 22, width = 30, sig = SIG, settings = "x"}), "andre indstillinger")
|
|
||||||
check("R6 ukendt form", C.mismatchReason(own, {field = 22, width = 30, settings = settings}), "markens form ukendt")
|
|
||||||
|
|
||||||
-- ---- register: immutabelt ----
|
|
||||||
local records = C.withRecord({}, own)
|
|
||||||
check("I1 tilføjet", records.v1 ~= nil, true)
|
|
||||||
local other = C.newRecord({vehicle = "v2", field = 22, width = 30, sig = SIG, settings = settings, waypoints = 812})
|
|
||||||
local both = C.withRecord(records, other)
|
|
||||||
check("I2 original urørt", count(records), 1)
|
|
||||||
check("I3 begge", count(both), 2)
|
|
||||||
check("I4 without", count(C.without(both, "v1")), 1)
|
|
||||||
check("I5 without urørt original", count(both), 2)
|
|
||||||
check("I6 newRecord uden vehicle = nil", C.newRecord({field = 22}), nil)
|
|
||||||
|
|
||||||
-- ---- findReusable ----
|
|
||||||
local rec, how = C.findReusable(both, request, "v1")
|
|
||||||
check("F1 egen maskine først", rec and rec.vehicle, "v1")
|
|
||||||
check("F2 egen = own", how, "own")
|
|
||||||
rec, how = C.findReusable(both, request, "v3")
|
|
||||||
check("F3 anden maskines kursus", rec and rec.vehicle, "v1")
|
|
||||||
check("F4 = copy", how, "copy")
|
|
||||||
-- egen maskines kursus er til en anden bredde -> kopi fra v2
|
|
||||||
local mixed = C.withRecord(both, C.newRecord({vehicle = "v1", field = 22, width = 12, sig = SIG, settings = settings}))
|
|
||||||
rec, how = C.findReusable(mixed, request, "v1")
|
|
||||||
check("F5 egen passer ikke -> kopi fra v2", rec and rec.vehicle, "v2")
|
|
||||||
check("F6 kopi", how, "copy")
|
|
||||||
-- intet passer -> grunden fra egen post
|
|
||||||
local changed = {field = 22, width = 30, sig = "n=6;a=1;c=0,0", settings = settings}
|
|
||||||
rec, how = C.findReusable(both, changed, "v1")
|
|
||||||
check("F7 intet passer", rec, nil)
|
|
||||||
check("F8 grund: form ændret", how, "markens form ændret")
|
|
||||||
rec, how = C.findReusable({}, request, "v1")
|
|
||||||
check("F9 tomt register", how, "intet kursus gemt")
|
|
||||||
-- gyldighedstjek: egen maskines kursus er væk/ændret -> kopi fra v2; alle ugyldige -> ny
|
|
||||||
rec, how = C.findReusable(both, request, "v1", function(r) return r.vehicle ~= "v1" end)
|
|
||||||
check("F10 egen ugyldig -> kopi", rec and rec.vehicle, "v2")
|
|
||||||
rec, how = C.findReusable(both, request, "v1", function() return false end)
|
|
||||||
check("F11 alle ugyldige", rec, nil)
|
|
||||||
check("F12 grund: kurset på maskinen er ændret", how, "kurset på maskinen er ændret")
|
|
||||||
-- multi-tool: lederens kursus (2 sæt, bane -1) genbruges ikke til et enkelt sæt
|
|
||||||
local multi = C.newRecord({vehicle = "v1", field = 22, width = 30, sig = SIG, settings = C.settingsKey({multiTools = 2, lane = -1})})
|
|
||||||
rec = C.findReusable(C.withRecord({}, multi), request, "v1")
|
|
||||||
check("F13 multi-kursus != enkelt", rec, nil)
|
|
||||||
rec, how = C.findReusable(C.withRecord({}, multi), {field = 22, width = 30, sig = SIG, settings = C.settingsKey({multiTools = 2, lane = -1})}, "v1")
|
|
||||||
check("F14 multi samme bane genbruges", how, "own")
|
|
||||||
-- deterministisk: flere kandidater -> laveste id
|
|
||||||
local many = C.withRecord(C.withRecord({}, C.newRecord({vehicle = "v9", field = 22, width = 30, sig = SIG, settings = settings})),
|
|
||||||
C.newRecord({vehicle = "v4", field = 22, width = 30, sig = SIG, settings = settings}))
|
|
||||||
rec = C.findReusable(many, request, "v1")
|
|
||||||
check("F15 deterministisk valg", rec and rec.vehicle, "v4")
|
|
||||||
|
|
||||||
-- ---- invalidateField ----
|
|
||||||
local fieldRecords = C.withRecord(both, C.newRecord({vehicle = "v5", field = 7, width = 30, sig = "n=3;a=50;c=3,3", settings = settings}))
|
|
||||||
local kept, removed = C.invalidateField(fieldRecords, 22, "n=5;a=1;c=0,0")
|
|
||||||
check("V1 to poster for marken slettet", removed, 2)
|
|
||||||
check("V2 anden mark bevaret", kept.v5 ~= nil, true)
|
|
||||||
check("V3 original urørt", count(fieldRecords), 3)
|
|
||||||
kept, removed = C.invalidateField(fieldRecords, 22, SIG)
|
|
||||||
check("V4 samme form: intet slettes", removed, 0)
|
|
||||||
check("V4b samme tabel tilbage", kept, fieldRecords)
|
|
||||||
kept, removed = C.invalidateField(fieldRecords, 22, nil)
|
|
||||||
check("V5 ukendt form: intet slettes", removed, 0)
|
|
||||||
kept, removed = C.invalidateField(nil, 22, SIG)
|
|
||||||
check("V6 nil-register", removed, 0)
|
|
||||||
|
|
||||||
-- ---- lim: prepare/afterStart/checkField med falske spil-objekter ----
|
|
||||||
dofile("FS25_ADSmartPickup/adCpBridge.lua")
|
|
||||||
local saves, logs = 0, {}
|
|
||||||
ADFieldJobs = {state = {courseRecords = {}}, save = function() saves = saves + 1 end,
|
|
||||||
log = function(_, fmt, ...) table.insert(logs, string.format(fmt, ...)) end}
|
|
||||||
ADRuns = {vehicleLabel = function(v) return v.label end}
|
|
||||||
Logging = {warning = function(...) print("WARN", ...) end}
|
|
||||||
local function fakeCourse(n)
|
|
||||||
return {getNumberOfWaypoints = function() return n end}
|
|
||||||
end
|
|
||||||
local function fakeVehicle(id, width, course)
|
|
||||||
return {label = id, course = course,
|
|
||||||
getFieldWorkCourse = function(self) return self.course end,
|
|
||||||
getCourseGeneratorSettings = function() return {workWidth = {getValue = function() return width end}} end}
|
|
||||||
end
|
|
||||||
local tractorA = fakeVehicle("A", 30, nil)
|
|
||||||
local tractorB = fakeVehicle("B", 30, nil)
|
|
||||||
ADRunsController = {getFarmVehicles = function() return {tractorA, tractorB} end,
|
|
||||||
getVehicleId = function(v) return v.label end}
|
|
||||||
local fieldObj, info = {id = 22}, {shapeSig = SIG, fruitName = "WHEAT"}
|
|
||||||
local assignA = {vehicle = tractorA, vehicleId = "A", role = "cultivator"}
|
|
||||||
|
|
||||||
-- G1: intet gemt -> ny beregning, opts urørt (ingen reuse/course)
|
|
||||||
local opts, ctx = C.prepare({}, fieldObj, info, assignA, {})
|
|
||||||
check("G1 ny beregning: ingen reuse", opts.reuse, nil)
|
|
||||||
check("G1b ingen kursus", opts.course, nil)
|
|
||||||
check("G1c log: nyt kursus (intet gemt)", logs[#logs]:find("nyt kursus beregnes", 1, true) ~= nil and logs[#logs]:find("intet kursus gemt", 1, true) ~= nil, true)
|
|
||||||
-- CP har lavet kurset (812 punkter) -> registreres
|
|
||||||
tractorA.course = fakeCourse(812)
|
|
||||||
C.afterStart(tractorA, "A", ctx)
|
|
||||||
check("G2 registreret", ADFieldJobs.state.courseRecords.A ~= nil and ADFieldJobs.state.courseRecords.A.waypoints, 812)
|
|
||||||
check("G2b gemt til fil", saves, 1)
|
|
||||||
-- G3: næste gang -> genbrug
|
|
||||||
opts, ctx = C.prepare({}, fieldObj, info, assignA, {})
|
|
||||||
check("G3 genbrug", opts.reuse, true)
|
|
||||||
check("G3b log genbrugt", logs[#logs]:find("kursus genbrugt", 1, true) ~= nil, true)
|
|
||||||
C.afterStart(tractorA, "A", ctx)
|
|
||||||
check("G3c uændret register gemmes ikke igen", saves, 1)
|
|
||||||
-- G4: B skal samme mark -> kopi fra A
|
|
||||||
local assignB = {vehicle = tractorB, vehicleId = "B", role = "cultivator"}
|
|
||||||
opts, ctx = C.prepare({}, fieldObj, info, assignB, {})
|
|
||||||
check("G4 kopi fra A", opts.course, tractorA.course)
|
|
||||||
check("G4b log kopieret", logs[#logs]:find("kopieret fra 'A'", 1, true) ~= nil, true)
|
|
||||||
tractorB.course = fakeCourse(812)
|
|
||||||
C.afterStart(tractorB, "B", ctx)
|
|
||||||
check("G4c B registreret", ADFieldJobs.state.courseRecords.B ~= nil, true)
|
|
||||||
-- G5: spilleren har lavet et nyt kursus på A i CP's HUD (andet antal punkter) -> kopi fra B
|
|
||||||
tractorA.course = fakeCourse(400)
|
|
||||||
opts = C.prepare({}, fieldObj, info, assignA, {})
|
|
||||||
check("G5 A's kursus ændret -> kopi fra B", opts.course, tractorB.course)
|
|
||||||
-- G6: kursus kopieret fra slåmaskinen (opts.course) -> registret bruges ikke, maskinens post glemmes
|
|
||||||
local mowerCourse = fakeCourse(99)
|
|
||||||
opts, ctx = C.prepare({}, fieldObj, info, assignB, {course = mowerCourse})
|
|
||||||
check("G6 opts urørt", opts.course, mowerCourse)
|
|
||||||
check("G6b ingen kontekst", ctx, nil)
|
|
||||||
tractorB.course = fakeCourse(99)
|
|
||||||
C.afterStart(tractorB, "B", ctx)
|
|
||||||
check("G6c B glemt", ADFieldJobs.state.courseRecords.B, nil)
|
|
||||||
-- G7: vinmark: vin-offset indgår i nøglen
|
|
||||||
local vineReq = C.requestFor(tractorA, 22, {shapeSig = SIG, fruitName = "GRAPE"}, "fertilizer", {})
|
|
||||||
check("G7 vin i nøglen", vineReq.settings, C.settingsKey({vineOffset = -1, vineSkip = 1}))
|
|
||||||
-- G8: markens form ændret -> A's post glemmes, log
|
|
||||||
local before = saves
|
|
||||||
check("G8 checkField sletter", C.checkField(22, "n=9;a=1;c=0,0"), 1)
|
|
||||||
check("G8b register tomt for marken", ADFieldJobs.state.courseRecords.A, nil)
|
|
||||||
check("G8c gemt", saves, before + 1)
|
|
||||||
check("G9 samme form: intet", C.checkField(22, "n=9;a=1;c=0,0"), 0)
|
|
||||||
|
|
||||||
-- G10: fejl i spil-API'et -> falder tilbage til ny beregning (opts urørt), ingen crash
|
|
||||||
local brokenVehicle = {label = "X", getFieldWorkCourse = function() error("boom") end}
|
|
||||||
local plainOpts = {lane = 1}
|
|
||||||
local okPrep, gotOpts, gotCtx = pcall(C.prepare, {}, fieldObj, nil, {vehicle = brokenVehicle, vehicleId = "X", role = "x"}, plainOpts)
|
|
||||||
check("G10 ingen crash", okPrep, true)
|
|
||||||
check("G10b opts urørt", gotOpts, plainOpts)
|
|
||||||
check("G10c ingen kontekst", gotCtx, nil)
|
|
||||||
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adCpBridge.lua (fra repo-roden)
|
|
||||||
-- Kun getWorkWidth: variabel bredde (Unrealistic Pack) vinder over AI-markørernes grundbredde.
|
|
||||||
dofile("FS25_ADSmartPickup/adCpBridge.lua")
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function withMarkers(width)
|
|
||||||
return {getAIMarkers = function() return 1, 2, 3, 4, width end}
|
|
||||||
end
|
|
||||||
|
|
||||||
-- W1: læssevogn med variabel bredde 20,4 + 20,4 = 40,8 m, AI-markører siger 2,4 m
|
|
||||||
local wagon = withMarkers(2.4)
|
|
||||||
wagon.getVariableWorkWidth = function(_, isLeft) return isLeft and 20.4 or -20.4, nil, true end
|
|
||||||
local tractor = {getAttachedImplements = function() return {{object = wagon}} end}
|
|
||||||
check("W1 variabel bredde", ADCpBridge.getWorkWidth(tractor), 40.8)
|
|
||||||
-- W2: ugyldig variabel bredde -> AI-markører
|
|
||||||
local plain = withMarkers(9.5)
|
|
||||||
plain.getVariableWorkWidth = function() return 0, nil, false end
|
|
||||||
check("W2 AI-markører", ADCpBridge.getWorkWidth(plain), 9.5)
|
|
||||||
-- W3: selvkørende slåmaskine uden redskaber, kun variabel bredde
|
|
||||||
local mower = {getVariableWorkWidth = function(_, isLeft) return isLeft and 20.4 or -20.4, nil, true end}
|
|
||||||
check("W3 selvkørende", ADCpBridge.getWorkWidth(mower), 40.8)
|
|
||||||
|
|
||||||
-- W4: Courseplays egen bredde (HUD'en) vinder — det er den marken køres med
|
|
||||||
local cpVehicle = {getVariableWorkWidth = function(_, isLeft) return isLeft and 20.4 or -0.4, nil, true end,
|
|
||||||
getCourseGeneratorSettings = function() return {workWidth = {getValue = function() return 40.8 end}} end}
|
|
||||||
check("W4 Courseplay-bredde", ADCpBridge.getWorkWidth(cpVehicle), 40.8)
|
|
||||||
-- W5: Courseplay-indstilling 0/ugyldig -> egen beregning
|
|
||||||
local zeroCp = {getVariableWorkWidth = function(_, isLeft) return isLeft and 5 or -5, nil, true end,
|
|
||||||
getCourseGeneratorSettings = function() return {workWidth = {getValue = function() return 0 end}} end}
|
|
||||||
check("W5 CP 0 -> egen beregning", ADCpBridge.getWorkWidth(zeroCp), 10)
|
|
||||||
|
|
||||||
-- WO: skårets sideforskydning på en river, målt fra nedlægningsområdet (venstre positiv), minus rivens
|
|
||||||
-- egen CP-forskydning. Noder er tal; localToLocal-mock giver nodens x i traktorens ramme.
|
|
||||||
local nodeX = {[11] = 2.0, [12] = 4.0, [21] = -1.0, [22] = 1.0}
|
|
||||||
function localToLocal(node) return nodeX[node] or 0, 0, 0 end
|
|
||||||
local function rake(dropIndex, runtime)
|
|
||||||
local areas = {
|
|
||||||
{start = 1, width = 2, height = 3, dropWindrowWorkAreaIndex = runtime and dropIndex or nil},
|
|
||||||
{start = 10, width = 11, height = 12},
|
|
||||||
{start = 20, width = 21, height = 22},
|
|
||||||
}
|
|
||||||
return {spec_windrower = {}, spec_workArea = {workAreas = areas}, configFileName = "rake.xml"}
|
|
||||||
end
|
|
||||||
local sideRake = rake(2, true)
|
|
||||||
local tractorWithRake = {rootNode = 100, getAttachedImplements = function() return {{object = sideRake}} end,
|
|
||||||
getCpSettings = function() return {toolOffsetX = {getValue = function() return 0.5 end}} end}
|
|
||||||
check("WO1 sideriver (runtime-felt)", ADCpBridge.getWindrowOffset(tractorWithRake), 2.5)
|
|
||||||
local centerRake = rake(3, true)
|
|
||||||
local tractorCenter = {rootNode = 100, getAttachedImplements = function() return {{object = centerRake}} end}
|
|
||||||
check("WO2 midterrive", ADCpBridge.getWindrowOffset(tractorCenter), 0)
|
|
||||||
-- WO3: runtime-felt mangler -> riverens XML (workArea(0) windrower#dropWindrowWorkAreaIndex = 2)
|
|
||||||
ADCpBridge.readDropIndicesFromXml = function(fileName) return fileName == "rake.xml" and {[1] = 2} or {} end
|
|
||||||
local xmlRake = rake(2, false)
|
|
||||||
local tractorXml = {rootNode = 100, getAttachedImplements = function() return {{object = xmlRake}} end}
|
|
||||||
check("WO3 fra XML", ADCpBridge.getWindrowOffset(tractorXml), 3)
|
|
||||||
check("WO4 ingen river -> nil", ADCpBridge.getWindrowOffset({rootNode = 100}), nil)
|
|
||||||
|
|
||||||
-- SR: sprøjte/spreder -> rolle ud fra fyldet
|
|
||||||
check("SR gødning", ADCpBridge.sprayRoleForFillType("FERTILIZER"), "fertilizer")
|
|
||||||
check("SR flydende", ADCpBridge.sprayRoleForFillType("LIQUIDFERTILIZER"), "fertilizer")
|
|
||||||
check("SR gylle", ADCpBridge.sprayRoleForFillType("LIQUIDMANURE"), "fertilizer")
|
|
||||||
check("SR møg", ADCpBridge.sprayRoleForFillType("MANURE"), "fertilizer")
|
|
||||||
check("SR digestat", ADCpBridge.sprayRoleForFillType("DIGESTATE"), "fertilizer")
|
|
||||||
check("SR kalk", ADCpBridge.sprayRoleForFillType("LIME"), "limer")
|
|
||||||
check("SR ukrudt", ADCpBridge.sprayRoleForFillType("HERBICIDE"), "weeder")
|
|
||||||
check("SR vand/ukendt", ADCpBridge.sprayRoleForFillType("WATER"), nil)
|
|
||||||
-- SR2: tom Bredal-spreder (tager gødning OG kalk) kan begge dele; fyldt med kalk er den primært kalkspreder
|
|
||||||
local bredal = {FERTILIZER = true, LIME = true}
|
|
||||||
local p1, r1, f1 = ADCpBridge.sprayRolesFor(nil, bredal)
|
|
||||||
check("SR2 tom spreder primært gødning", p1, "fertilizer")
|
|
||||||
check("SR2 tom spreder kan kalke", r1 and r1.limer, true)
|
|
||||||
check("SR2 tom = intet fyld", f1, nil)
|
|
||||||
local p2, r2, f2 = ADCpBridge.sprayRolesFor("LIME", bredal)
|
|
||||||
check("SR2 med kalk = kalkspreder", p2, "limer")
|
|
||||||
check("SR2 med kalk kan stadig gøde (efter tømning)", r2 and r2.fertilizer, true)
|
|
||||||
check("SR2 fyldets rolle", f2, "limer")
|
|
||||||
check("SR2 ukrudtssprøjte", (ADCpBridge.sprayRolesFor(nil, {HERBICIDE = true, WATER = true})), "weeder")
|
|
||||||
check("SR2 vandvogn er ingen sprøjte", (ADCpBridge.sprayRolesFor(nil, {WATER = true})), nil)
|
|
||||||
-- SR3: kun en lille rest må hældes ud (100.000 l gødning blev hældt ud in-game 26/9)
|
|
||||||
check("SR3 tom", ADCpBridge.isDumpable(0, 4000), true)
|
|
||||||
check("SR3 lille rest 300 af 4000", ADCpBridge.isDumpable(300, 4000), true)
|
|
||||||
check("SR3 halv tank 2000 af 4000", ADCpBridge.isDumpable(2000, 4000), false)
|
|
||||||
check("SR3 100000 l aldrig", ADCpBridge.isDumpable(100000, 1000000), false)
|
|
||||||
check("SR3 over 2000 l aldrig", ADCpBridge.isDumpable(2500, 100000), false)
|
|
||||||
-- VN: vinmark — i gangen for alt undtagen høsteren; almindelige marker røres ikke
|
|
||||||
check("VN sprøjte på druer = venstre", ADCpBridge.vineOffsetFor("GRAPEB", "fertilizer"), -1)
|
|
||||||
check("VN harve på druer = venstre", ADCpBridge.vineOffsetFor("grape", "cultivator"), -1)
|
|
||||||
check("VN høster på druer = på vinen", ADCpBridge.vineOffsetFor("GRAPEB", "harvester"), 0)
|
|
||||||
check("VN hvede røres ikke", ADCpBridge.vineOffsetFor("WHEAT", "fertilizer"), nil)
|
|
||||||
check("VN ukendt afgrøde", ADCpBridge.vineOffsetFor(nil, "weeder"), nil)
|
|
||||||
check("VN sprøjte springer 1 række over (begge sider)", ADCpBridge.vineSkipFor("GRAPEB", "weeder"), 1)
|
|
||||||
check("VN gødning springer 1 over", ADCpBridge.vineSkipFor("GRAPEB", "fertilizer"), 1)
|
|
||||||
check("VN harve springer ingen over", ADCpBridge.vineSkipFor("GRAPEB", "cultivator"), 0)
|
|
||||||
check("VN høster springer ingen over", ADCpBridge.vineSkipFor("GRAPEB", "harvester"), 0)
|
|
||||||
check("VN hvede røres ikke", ADCpBridge.vineSkipFor("WHEAT", "weeder"), nil)
|
|
||||||
|
|
||||||
-- HF: høsterens afgrøder fra skærebordet (liste eller opslag); finsnitter tager ingen
|
|
||||||
local list = ADCpBridge.harvestFruitSet({fruitTypes = {3, 5}})
|
|
||||||
check("HF liste", list[3] and list[5], true)
|
|
||||||
local lookup = ADCpBridge.harvestFruitSet({fruitTypes = {[3] = true}})
|
|
||||||
check("HF opslag", lookup[3], true)
|
|
||||||
local forage = ADCpBridge.harvestFruitSet({fruitTypes = {3}, fruitTypeConverters = {[3] = {}}})
|
|
||||||
check("HF finsnitter tager intet", next(forage), nil)
|
|
||||||
check("HF fruitTypeIndices", ADCpBridge.harvestFruitSet({fruitTypeIndices = {7}})[7], true)
|
|
||||||
|
|
||||||
-- PL: plantbar i perioden
|
|
||||||
local fruitPlant = {growthDataSeasonal = {periods = {[3] = {plantingAllowed = true}, [4] = {plantingAllowed = false}}}}
|
|
||||||
check("PL tilladt", ADCpBridge.isPlantableIn(fruitPlant, 3), true)
|
|
||||||
check("PL ikke nu", ADCpBridge.isPlantableIn(fruitPlant, 4), false)
|
|
||||||
check("PL ukendt data = tilladt", ADCpBridge.isPlantableIn({}, 4), true)
|
|
||||||
|
|
||||||
-- CR: kursus-genbrug — start på maskinens eget kursus uden at CP beregner et nyt (opts.reuse)
|
|
||||||
do
|
|
||||||
local generated, started = 0, 0
|
|
||||||
AIJobType = {FIELDWORK_CP = 1}
|
|
||||||
function getWorldTranslation() return 0, 0, 0 end
|
|
||||||
local job = {cpJobParameters = {}}
|
|
||||||
function job.applyCurrentState() end
|
|
||||||
function job.setValues() end
|
|
||||||
function job.validate() return true end
|
|
||||||
function job.registerFieldBoundaryDetectionCallback() end
|
|
||||||
function job.detectFieldBoundary() return true, false, "" end
|
|
||||||
function job.onClickGenerateFieldWorkCourse(_, callback) generated = generated + 1; callback(true) end
|
|
||||||
g_currentMission = {aiJobTypeManager = {createJob = function() return job end},
|
|
||||||
aiSystem = {startJob = function() started = started + 1 end}}
|
|
||||||
local course = {}
|
|
||||||
local vehicle = {rootNode = 1, getOwnerFarmId = function() return 1 end, getFieldWorkCourse = function() return course end}
|
|
||||||
local reason = "?"
|
|
||||||
ADCpBridge.start(vehicle, 0, 0, function(ok, r) reason = ok and (r or "generated") or "fail" end, {reuse = true})
|
|
||||||
check("CR1 genbrug: ingen beregning", generated, 0)
|
|
||||||
check("CR2 genbrug: CP startet", started, 1)
|
|
||||||
check("CR3 genbrug: reason", reason, "reused")
|
|
||||||
-- ingen kursus på maskinen -> beregnes alligevel
|
|
||||||
vehicle.getFieldWorkCourse = function() return nil end
|
|
||||||
ADCpBridge.start(vehicle, 0, 0, function(ok, r) reason = ok and (r or "generated") or "fail" end, {reuse = true})
|
|
||||||
check("CR4 intet kursus -> beregnes", generated, 1)
|
|
||||||
check("CR5 reason ny", reason, "generated")
|
|
||||||
-- uden reuse: beregnes som før
|
|
||||||
vehicle.getFieldWorkCourse = function() return course end
|
|
||||||
ADCpBridge.start(vehicle, 0, 0, function(ok, r) reason = ok and (r or "generated") or "fail" end, {})
|
|
||||||
check("CR6 uden reuse -> beregnes", generated, 2)
|
|
||||||
AIJobType, g_currentMission = nil, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
-- forager: 2 runder og start på forageren (så der ikke køres ind i træer o.l. i hjørnerne)
|
|
||||||
local recorded = {}
|
|
||||||
local function setting(name) return {setValue = function(_, v) recorded[name] = v end} end
|
|
||||||
local headlandVehicle = {getCourseGeneratorSettings = function() return {numberOfHeadlands = setting("headlands"), startOnHeadland = setting("startOnHeadland")} end}
|
|
||||||
check("forager: sat", ADCpBridge.setHeadlands(headlandVehicle, 2), true)
|
|
||||||
check("forager: 2 runder", recorded.headlands, 2)
|
|
||||||
check("forager: start på forageren", recorded.startOnHeadland, true)
|
|
||||||
ADCpBridge.setHeadlands(headlandVehicle, 0)
|
|
||||||
check("forager: ingen -> ikke start på forageren", recorded.startOnHeadland, false)
|
|
||||||
check("forager: køretøj uden CP", ADCpBridge.setHeadlands({}, 2), false)
|
|
||||||
check("forager: standard er 2", ADCpBridge.HEADLANDS, 2)
|
|
||||||
-- genbrugt kursus uden forager beregnes på ny
|
|
||||||
check("forager i kursus: 2 runder", ADCpBridge.courseHasHeadlands({getNumberOfHeadlands = function() return 2 end}), true)
|
|
||||||
check("forager i kursus: ingen", ADCpBridge.courseHasHeadlands({getNumberOfHeadlands = function() return 0 end}), false)
|
|
||||||
check("forager i kursus: ukendt -> antag ja", ADCpBridge.courseHasHeadlands({}), true)
|
|
||||||
check("forager i kursus: intet kursus", ADCpBridge.courseHasHeadlands(nil), false)
|
|
||||||
-- isWorking: AutoDrive kører også som AI i FS25 — det er ikke Courseplay (in-game 29/9: Milk Store holdt ved
|
|
||||||
-- Cow Wait med AD aktiv og "ventede på sig selv" som 'Courseplay kører')
|
|
||||||
local function fakeVehicle(cp, ai, ad)
|
|
||||||
return {getIsCpFieldWorkActive = function() return cp end, getIsAIActive = function() return ai end,
|
|
||||||
ad = {stateModule = {isActive = function() return ad end}}}
|
|
||||||
end
|
|
||||||
check("CP arbejder: Courseplay markarbejde", ADCpBridge.isWorking(fakeVehicle(true, true, false)), true)
|
|
||||||
check("CP arbejder: AI uden AD (CP-kørsel)", ADCpBridge.isWorking(fakeVehicle(false, true, false)), true)
|
|
||||||
check("CP arbejder: AutoDrive kører -> ikke CP", ADCpBridge.isWorking(fakeVehicle(false, true, true)), false)
|
|
||||||
check("CP arbejder: intet", ADCpBridge.isWorking(fakeVehicle(false, false, false)), false)
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,26 +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)
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adFieldFleet.lua (fra repo-roden)
|
|
||||||
dofile("FS25_ADSmartPickup/adFieldModel.lua")
|
|
||||||
dofile("FS25_ADSmartPickup/adFieldFleet.lua")
|
|
||||||
local F = ADFieldFleet
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- FL1: rolle ud fra redskabets spec (læssevogn vinder over andet på samme køretøj)
|
|
||||||
check("FL1 slåmaskine", F.roleFromSpecs({spec_mower = true}), "mower")
|
|
||||||
check("FL1 vender", F.roleFromSpecs({spec_tedder = true}), "tedder")
|
|
||||||
check("FL1 river", F.roleFromSpecs({spec_windrower = true}), "rake")
|
|
||||||
check("FL1 læssevogn", F.roleFromSpecs({spec_forageWagon = true, spec_mower = true}), "collector")
|
|
||||||
check("FL1 intet", F.roleFromSpecs({spec_trailer = true}), nil)
|
|
||||||
|
|
||||||
local function rig(id, role, x, extra)
|
|
||||||
local r = {id = id, role = role, enabled = true, busy = false, controlled = false, width = 40.8, x = x, z = 0, label = id}
|
|
||||||
for k, v in pairs(extra or {}) do r[k] = v end
|
|
||||||
return r
|
|
||||||
end
|
|
||||||
|
|
||||||
-- FL2: nærmeste ledige med flueben og rigtig rolle
|
|
||||||
local picked = F.pick({rig("far", "mower", 500), rig("near", "mower", 50), rig("wagon", "collector", 10)}, "mower", 0, 0, nil)
|
|
||||||
check("FL2 nærmeste slåmaskine", picked.id, "near")
|
|
||||||
|
|
||||||
-- FL3: uden flueben / optaget / spiller i sædet springes over
|
|
||||||
local _, why = F.pick({rig("a", "mower", 1, {enabled = false})}, "mower", 0, 0)
|
|
||||||
check("FL3 intet med flueben", why, "noRig")
|
|
||||||
local _, whyBusy = F.pick({rig("a", "mower", 1, {busy = true}), rig("b", "mower", 2, {controlled = true})}, "mower", 0, 0)
|
|
||||||
check("FL3 alle optaget", whyBusy, "allBusy")
|
|
||||||
check("FL3 ingen med rollen", select(2, F.pick({rig("a", "tedder", 1)}, "mower", 0, 0)), "noRig")
|
|
||||||
|
|
||||||
-- FL4: læssevogn smallere end slåbredden springes over, bredere vælges
|
|
||||||
local wide = F.pick({rig("smal", "collector", 1, {width = 20}), rig("bred", "collector", 900, {width = 40.8})}, "collector", 0, 0, 40.8)
|
|
||||||
check("FL4 bred valgt selv om længere væk", wide.id, "bred")
|
|
||||||
-- FL5: kun for smalle -> grund tooNarrow med tal
|
|
||||||
local none, whyNarrow, args = F.pick({rig("smal", "collector", 1, {width = 20})}, "collector", 0, 0, 40.8)
|
|
||||||
check("FL5 ingen", none, nil); check("FL5 grund", whyNarrow, "tooNarrow"); check("FL5 bredde", args[1], 20)
|
|
||||||
-- FL6: over 50 m -> tooWide
|
|
||||||
check("FL6 for bred", select(2, F.pick({rig("x", "mower", 1, {width = 60})}, "mower", 0, 0)), "tooWide")
|
|
||||||
|
|
||||||
-- FL7: toggle giver ny tabel
|
|
||||||
local enabled = {a = true}
|
|
||||||
local toggled = F.toggle(enabled, "b")
|
|
||||||
check("FL7 b til", toggled.b, true); check("FL7 original urørt", enabled.b, nil)
|
|
||||||
check("FL7 a fra", F.toggle(toggled, "a").a, nil)
|
|
||||||
|
|
||||||
-- FL8: nye roller i prioriteret rækkefølge
|
|
||||||
check("FL8 høster", F.roleFromSpecs({spec_combine = true, spec_cutter = true}), "harvester")
|
|
||||||
check("FL8 såmaskine før kultivator (combi)", F.roleFromSpecs({spec_sowingMachine = true, spec_cultivator = true, sprayRole = "fertilizer"}), "seeder")
|
|
||||||
check("FL8 kultivator", F.roleFromSpecs({spec_cultivator = true}), "cultivator")
|
|
||||||
check("FL8 plov", F.roleFromSpecs({spec_plow = true}), "plow")
|
|
||||||
check("FL8 tromle", F.roleFromSpecs({spec_roller = true}), "roller")
|
|
||||||
check("FL8 strigle", F.roleFromSpecs({spec_weeder = true}), "weeder")
|
|
||||||
check("FL8 stubhakker", F.roleFromSpecs({spec_mulcher = true}), "mulcher")
|
|
||||||
check("FL8 gødningssprøjte", F.roleFromSpecs({sprayRole = "fertilizer"}), "fertilizer")
|
|
||||||
check("FL8 kalkspreder", F.roleFromSpecs({sprayRole = "limer"}), "limer")
|
|
||||||
check("FL8 ukrudtssprøjte", F.roleFromSpecs({sprayRole = "weeder"}), "weeder")
|
|
||||||
check("FL8 aflæsningsvogn", F.roleFromSpecs({canUnload = true}), "unloader")
|
|
||||||
check("FL8 traktor med plov er ikke aflæsningsvogn", F.roleFromSpecs({spec_plow = true, canUnload = true}), "plow")
|
|
||||||
|
|
||||||
-- FL9: accept-filter (fx høster der kan tage afgrøden, strigle kun til småt ukrudt)
|
|
||||||
local combineWheat = rig("jd", "harvester", 10, {fruits = {[3] = true}})
|
|
||||||
local combineBeet = rig("ropa", "harvester", 1, {fruits = {[9] = true}})
|
|
||||||
local forWheat = F.pick({combineBeet, combineWheat}, "harvester", 0, 0, nil, function(c) return c.fruits[3] == true end)
|
|
||||||
check("FL9 høster der kan tage hvede", forWheat.id, "jd")
|
|
||||||
local _, whyFruit = F.pick({combineBeet}, "harvester", 0, 0, nil, function(c) return c.fruits[3] == true end)
|
|
||||||
check("FL9 ingen kan tage afgrøden", whyFruit, "cannotDo")
|
|
||||||
|
|
||||||
-- FL10: flere sæt til multi-tool — alle eller ingen, nærmeste først
|
|
||||||
local m1, m2, m3 = rig("m1", "mower", 30), rig("m2", "mower", 10), rig("m3", "mower", 20, {busy = true})
|
|
||||||
local two = F.pickMany({m1, m2, m3}, "mower", 2, 0, 0)
|
|
||||||
check("FL10 to valgt", #two, 2)
|
|
||||||
check("FL10 nærmeste først", two[1].id, "m2")
|
|
||||||
local none, whyMany, args = F.pickMany({m1, m2, m3}, "mower", 3, 0, 0)
|
|
||||||
check("FL10 for få -> ingen", none, nil)
|
|
||||||
check("FL10 grund", whyMany, "notEnough")
|
|
||||||
check("FL10 ledige/krævet", args[1] .. "/" .. args[2], "2/3")
|
|
||||||
check("FL10 ingen med rollen", select(2, F.pickMany({m1}, "tedder", 1, 0, 0)), "noRig")
|
|
||||||
|
|
||||||
-- FL11: markflådens tilstand: alle ledige maskiner (standard) eller kun med flueben
|
|
||||||
check("FL11 alle: uden flueben med", F.isEnabled("all", {}, "v1"), true)
|
|
||||||
check("FL11 flueben: uden flueben ude", F.isEnabled("ticked", {}, "v1"), false)
|
|
||||||
check("FL11 flueben: med flueben", F.isEnabled("ticked", {v1 = true}, "v1"), true)
|
|
||||||
check("FL11 ukendt tilstand = alle", F.isEnabled(nil, {}, "v1"), true)
|
|
||||||
|
|
||||||
-- FL12: ballepresser og ballesamler (ballevogn/Universal Autoload som Courseplays ballesamler kan bruge)
|
|
||||||
check("FL12 ballepresser", F.roleFromSpecs({spec_baler = true}), "baler")
|
|
||||||
check("FL12 ballesamler", F.roleFromSpecs({canBaleCollect = true}), "baleCollector")
|
|
||||||
check("FL12 ballesamler før aflæsningsvogn", F.roleFromSpecs({canBaleCollect = true, canUnload = true}), "baleCollector")
|
|
||||||
check("FL12 høster før presser", F.roleFromSpecs({spec_combine = true, spec_baler = true}), "harvester")
|
|
||||||
|
|
||||||
-- FL-spreder: tom spreder med rollerne {gødning, kalk} vælges til kalkning; spreder med forkert vare
|
|
||||||
-- på (kalk, skal gøde) kun hvis ingen anden kan — og den nærmeste med rigtig/ingen vare først
|
|
||||||
local spreaders = {
|
|
||||||
{id = "kalk", role = "limer", roles = {fertilizer = true, limer = true}, filledRole = "limer", enabled = true, x = 1, z = 0, width = 12},
|
|
||||||
{id = "tom", role = "fertilizer", roles = {fertilizer = true, limer = true}, enabled = true, x = 50, z = 0, width = 12},
|
|
||||||
}
|
|
||||||
check("FLS tom spreder tager kalkning", F.pick({spreaders[2]}, "limer", 0, 0) and F.pick({spreaders[2]}, "limer", 0, 0).id, "tom")
|
|
||||||
check("FLS gødskning: tom før nærmere med kalk på", F.pick(spreaders, "fertilizer", 0, 0).id, "tom")
|
|
||||||
check("FLS kun kalk-sprederen: bruges (tømmes)", F.pick({spreaders[1]}, "fertilizer", 0, 0).id, "kalk")
|
|
||||||
check("FLS kalkning: den med kalk på foretrækkes", F.pick(spreaders, "limer", 0, 0).id, "kalk")
|
|
||||||
-- FLS2: fuld af en anden vare (ikke dumpable) bruges slet ikke til opgaven
|
|
||||||
local fullOther = {id = "fuld", role = "fertilizer", roles = {fertilizer = true, weeder = true}, filledRole = "fertilizer", dumpable = false, enabled = true, x = 0, z = 0, width = 30}
|
|
||||||
check("FLS2 fuld gødningssprøjte bruges ikke til ukrudt", F.pick({fullOther}, "weeder", 0, 0), nil)
|
|
||||||
check("FLS2 men gerne til gødning", F.pick({fullOther}, "fertilizer", 0, 0).id, "fuld")
|
|
||||||
|
|
||||||
-- FLD: faste maskiner pr. mark
|
|
||||||
local sprayA = {id = "sprøjte", role = "fertilizer"}
|
|
||||||
local sprayB = {id = "spreder", role = "fertilizer"}
|
|
||||||
local harve = {id = "harve", role = "cultivator"}
|
|
||||||
local all = {sprayA, sprayB, harve}
|
|
||||||
local byField = {[22] = {"sprøjte"}}
|
|
||||||
local function ids(list) local out = {} for _, c in ipairs(list) do table.insert(out, c.id) end return table.concat(out, ",") end
|
|
||||||
check("FLD F22 gødskning: kun sin faste", ids(F.filterDedicated(all, 22, "fertilizer", byField)), "sprøjte")
|
|
||||||
check("FLD F109 gødskning: ikke F22's", ids(F.filterDedicated(all, 109, "fertilizer", byField)), "spreder,harve")
|
|
||||||
check("FLD F22 kultivering: ingen fast med rollen -> frie", ids(F.filterDedicated(all, 22, "cultivator", byField)), "spreder,harve")
|
|
||||||
check("FLD uden faste = alle", ids(F.filterDedicated(all, 5, "fertilizer", {})), "sprøjte,spreder,harve")
|
|
||||||
local moved = F.dedicate(byField, 109, "sprøjte", true)
|
|
||||||
check("FLD flyt: væk fra F22", #moved[22], 0)
|
|
||||||
check("FLD flyt: fast på F109", moved[109][1], "sprøjte")
|
|
||||||
check("FLD original urørt", byField[22][1], "sprøjte")
|
|
||||||
check("FLD fjern", #F.dedicate(moved, 109, "sprøjte", false)[109], 0)
|
|
||||||
|
|
||||||
-- FLV: menuen skjuler maskiner der er faste på en anden mark end den valgte
|
|
||||||
local vrigs = {{id = "vogn"}, {id = "harve"}, {id = "plov"}}
|
|
||||||
local vis, hid, flds = F.visibleForField(vrigs, 18, {[22] = {"vogn", "harve"}})
|
|
||||||
check("FLV F18 ser kun de frie", #vis .. "/" .. hid .. "/" .. tostring(flds[1]), "1/2/22")
|
|
||||||
check("FLV F22 ser alle", #(F.visibleForField(vrigs, 22, {[22] = {"vogn", "harve"}})), 3)
|
|
||||||
check("FLV ingen valgt = alle", #(F.visibleForField(vrigs, nil, {[22] = {"vogn"}})), 3)
|
|
||||||
|
|
||||||
-- FL-plov: FS25-plove har også spec_cultivator — de er plove, men må også tage kultivering
|
|
||||||
check("FLP plov med kultivator-spec er plov", F.roleFromSpecs({spec_plow = true, spec_cultivator = true}), "plow")
|
|
||||||
check("FLP plov kan kultivere", F.rolesFor("plow").cultivator, true)
|
|
||||||
check("FLP plov kan pløje", F.rolesFor("plow").plow, true)
|
|
||||||
check("FLP kultivator kan ikke pløje", F.rolesFor("cultivator"), nil)
|
|
||||||
local plough = {id = "p", role = "plow", roles = F.rolesFor("plow"), enabled = true, x = 0, z = 0, width = 3}
|
|
||||||
check("FLP plov vælges til kultivering", (F.pick({plough}, "cultivator", 0, 0, nil)).id, "p")
|
|
||||||
check("FLP rolesFor giver en kopi", F.rolesFor("plow") ~= F.rolesFor("plow"), true)
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,255 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adFieldGuide.lua (fra repo-roden)
|
|
||||||
dofile("FS25_ADSmartPickup/adGuide.lua")
|
|
||||||
dofile("FS25_ADSmartPickup/adFieldModel.lua")
|
|
||||||
dofile("FS25_ADSmartPickup/adFieldFleet.lua")
|
|
||||||
dofile("FS25_ADSmartPickup/adFlowView.lua")
|
|
||||||
dofile("FS25_ADSmartPickup/adFieldGuide.lua")
|
|
||||||
local FG, G = ADFieldGuide, ADGuide
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local SOWABLE = {{name = "WHEAT", title = "Hvede"}, {name = "BARLEY", title = "Byg"}, {name = "GRASS", title = "Græs"}, {name = "MAIZE", title = "Majs"}}
|
|
||||||
local RIGS = {
|
|
||||||
{id = "v1", text = "Fendt 1 — slåmaskine", role = "mower"},
|
|
||||||
{id = "v2", text = "Claas 2 — mejetærsker", role = "harvester"},
|
|
||||||
{id = "v3", text = "Sprøjte 3 — sprøjte", role = "weeder"},
|
|
||||||
{id = "v4", text = "Vogn 4 — læssevogn", role = "collector"},
|
|
||||||
}
|
|
||||||
local function ctx(info, extra)
|
|
||||||
local context = {
|
|
||||||
info = info, sowable = SOWABLE, mowable = {GRASS = true}, straw = {WHEAT = true, BARLEY = true},
|
|
||||||
rigs = RIGS, rigIdsByField = {[90] = {"v3"}}, fieldSilos = {}, storeHidden = 0,
|
|
||||||
stores = info.kind ~= "bare" and {{id = "s1", text = "Silo A, 300 m"}, {id = "s2", text = "Silo B, 1,2 km"}} or nil,
|
|
||||||
fields = {{id = 1, text = "F1 Græs"}, {id = 2, text = "F2 Hvede"}},
|
|
||||||
}
|
|
||||||
for key, value in pairs(extra or {}) do context[key] = value end
|
|
||||||
return context
|
|
||||||
end
|
|
||||||
local GRASS = {id = 1, kind = "mowable", fruitName = "GRASS", cropTitle = "Græs", markerId = 5}
|
|
||||||
local WHEAT = {id = 2, kind = "harvest", fruitName = "WHEAT", cropTitle = "Hvede", markerId = 6}
|
|
||||||
local MAIZE = {id = 3, kind = "harvest", fruitName = "MAIZE", cropTitle = "Majs", markerId = 7}
|
|
||||||
local BARE = {id = 4, kind = "bare", markerId = 8}
|
|
||||||
|
|
||||||
local function steps(guide) return table.concat(G.visibleSteps(guide), ",") end
|
|
||||||
local function field(id, plan)
|
|
||||||
local f = ADFieldModel.newField(id)
|
|
||||||
return ADFieldModel.with(f, {plan = plan or {}})
|
|
||||||
end
|
|
||||||
local function findRow(rows, id, item)
|
|
||||||
for _, row in ipairs(rows) do
|
|
||||||
if row.id == id and (item == nil or row.item == item) then return row end
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
local function countType(rows, rowType)
|
|
||||||
local n = 0
|
|
||||||
for _, row in ipairs(rows) do if row.type == rowType then n = n + 1 end end
|
|
||||||
return n
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ---- synlige trin pr. marktype ----
|
|
||||||
check("FG1 græs", steps(FG.open(field(1), ctx(GRASS))), "crop,cropChoice,care,machines,store,summary")
|
|
||||||
check("FG1 hvede m. halm", steps(FG.open(field(2), ctx(WHEAT))), "crop,cropChoice,prep,care,machines,store,summary")
|
|
||||||
check("FG1 majs uden halm", steps(FG.open(field(3), ctx(MAIZE))), "crop,prep,care,machines,store,summary")
|
|
||||||
check("FG1 bar mark", steps(FG.open(field(4), ctx(BARE))), "crop,prep,machines,summary")
|
|
||||||
check("FG1 halm ukendt -> vis", steps(FG.open(field(3), ctx(MAIZE, {straw = {}}))), "crop,cropChoice,prep,care,machines,store,summary")
|
|
||||||
check("FG1 hvede med mål græs", steps(FG.open(field(2, {targetFruit = "GRASS"}), ctx(WHEAT))), "crop,cropChoice,prep,care,machines,store,summary")
|
|
||||||
check("FG1 bar med mål hvede", steps(FG.open(field(4, {targetFruit = "WHEAT"}), ctx(BARE))), "crop,cropChoice,prep,care,machines,summary")
|
|
||||||
check("FG1 græs med mål græs: ingen klargøring", steps(FG.open(field(1, {targetFruit = "GRASS"}), ctx(GRASS))), "crop,cropChoice,care,machines,store,summary")
|
|
||||||
check("FG1 græs med mål hvede: klargøring", steps(FG.open(field(1, {targetFruit = "WHEAT"}), ctx(GRASS))), "crop,cropChoice,prep,care,machines,store,summary")
|
|
||||||
local picker = FG.openPicker(ctx(GRASS))
|
|
||||||
check("FG1 vælger: kun mark", steps(picker), "field")
|
|
||||||
check("FG1 vælger: trin", picker.step, "field")
|
|
||||||
check("FG1 vælger: position uden total", (G.position(picker)), 1)
|
|
||||||
|
|
||||||
-- ---- standarder og udfyldning ----
|
|
||||||
local g = FG.open(field(2), ctx(WHEAT))
|
|
||||||
check("FG2 starter på afgrøde", g.step, "crop")
|
|
||||||
check("FG2 klargøring auto", g.draft.prepAuto, true)
|
|
||||||
check("FG2 ingen faste", g.draft.useDedicated, false)
|
|
||||||
check("FG2 lager auto", g.draft.storeMode, "auto")
|
|
||||||
check("FG2 kladde er kopi", g.draft.plan ~= nil and g.draft.fieldId, 2)
|
|
||||||
local plowed = FG.open(field(2, {soil = "plow"}), ctx(WHEAT))
|
|
||||||
check("FG2 pløj -> vælg selv", plowed.draft.prepAuto, false)
|
|
||||||
local withRigs = FG.open(field(2, {rigIds = {"v2"}}), ctx(WHEAT))
|
|
||||||
check("FG2 faste maskiner -> ja", withRigs.draft.useDedicated, true)
|
|
||||||
local silo = FG.open(field(1, {storeBuildingIds = {"m1"}}), ctx(GRASS, {fieldSilos = {{id = "m1", text = "Marksilo"}}}))
|
|
||||||
check("FG2 marksilo valgt", silo.draft.storeMode, "fieldSilo")
|
|
||||||
local manual = FG.open(field(1, {storeBuildingIds = {"s2"}}), ctx(GRASS))
|
|
||||||
check("FG2 valgt silo -> vælg selv", manual.draft.storeMode, "manual")
|
|
||||||
check("FG2 valgt silo i listen", manual.draft.storeIds[1], "s2")
|
|
||||||
|
|
||||||
-- ---- choose er immutabel ----
|
|
||||||
local chosen = FG.choose(g, "targetFruit", "BARLEY")
|
|
||||||
check("FG3 ny kladde", chosen.draft.plan.targetFruit, "BARLEY")
|
|
||||||
check("FG3 gammel urørt", g.draft.plan.targetFruit, "")
|
|
||||||
local toggled = FG.choose(withRigs, "rigIds", {item = "v1", on = true})
|
|
||||||
check("FG3 check tilføjer", table.concat(toggled.draft.plan.rigIds, ","), "v2,v1")
|
|
||||||
check("FG3 gammel liste urørt", table.concat(withRigs.draft.plan.rigIds, ","), "v2")
|
|
||||||
check("FG3 check fjerner", table.concat(FG.choose(toggled, "rigIds", {item = "v2", on = false}).draft.plan.rigIds, ","), "v1")
|
|
||||||
check("FG3 produkt hø", FG.choose(FG.open(field(1), ctx(GRASS)), "product", true).draft.plan.product, "hay")
|
|
||||||
check("FG3 produkt ensilage", FG.choose(FG.open(field(1), ctx(GRASS)), "product", false).draft.plan.product, "silage")
|
|
||||||
|
|
||||||
-- ---- rækker ----
|
|
||||||
local cropRows = FG.rows(g)
|
|
||||||
local target = findRow(cropRows, "targetFruit")
|
|
||||||
check("FG4 afgrøde: valg", target.type, "choice")
|
|
||||||
check("FG4 afgrøde: behold først", target.options[1].value, "")
|
|
||||||
check("FG4 afgrøde: såbare", #target.options, 5)
|
|
||||||
check("FG4 så igen for høstmark", findRow(cropRows, "sow") ~= nil, true)
|
|
||||||
check("FG4 så igen skjult med mål", findRow(FG.rows(chosen), "sow"), nil)
|
|
||||||
check("FG4 så igen skjult på græs", findRow(FG.rows(FG.open(field(1), ctx(GRASS))), "sow"), nil)
|
|
||||||
|
|
||||||
local choiceGrass = FG.rows(G.jump(FG.open(field(1), ctx(GRASS)), "cropChoice", "close"))
|
|
||||||
check("FG5 græs: ensilage/hø", findRow(choiceGrass, "product") ~= nil, true)
|
|
||||||
check("FG5 græs: river", findRow(choiceGrass, "rake") ~= nil, true)
|
|
||||||
check("FG5 græs: ingen halm", findRow(choiceGrass, "straw"), nil)
|
|
||||||
local choiceWheat = FG.rows(G.jump(g, "cropChoice", "close"))
|
|
||||||
check("FG5 hvede: halm", findRow(choiceWheat, "straw").type, "choice")
|
|
||||||
check("FG5 hvede: ingen ensilage", findRow(choiceWheat, "product"), nil)
|
|
||||||
|
|
||||||
local prepRows = FG.rows(G.jump(g, "prep", "close"))
|
|
||||||
check("FG6 klargøring auto: kun valget + tekst", findRow(prepRows, "soil"), nil)
|
|
||||||
check("FG6 klargøring valg", findRow(prepRows, "prepAuto").type, "choice")
|
|
||||||
local prepManual = FG.rows(G.jump(FG.choose(g, "prepAuto", false), "prep", "close"))
|
|
||||||
check("FG6 vælg selv: jord", findRow(prepManual, "soil") ~= nil, true)
|
|
||||||
check("FG6 vælg selv: kalk", findRow(prepManual, "lime") ~= nil, true)
|
|
||||||
check("FG6 vælg selv: mulch", findRow(prepManual, "mulch") ~= nil, true)
|
|
||||||
check("FG6 vælg selv: ingen tromle uden såning", findRow(prepManual, "roll"), nil)
|
|
||||||
|
|
||||||
-- pleje: Automatisk/Manuelt som klargøring (brugerønske 29/9); knapperne vises under Manuelt
|
|
||||||
local careAutoRows = FG.rows(G.jump(g, "care", "close"))
|
|
||||||
check("FG7 pleje: valg auto/manuel", findRow(careAutoRows, "careAuto").type, "choice")
|
|
||||||
check("FG7 pleje auto: ingen knapper", findRow(careAutoRows, "fertilize"), nil)
|
|
||||||
check("FG7 standard: pleje auto", g.draft.careAuto, true)
|
|
||||||
check("FG7 uden gødning -> manuel", FG.open(field(2, {fertilize = false}), ctx(WHEAT)).draft.careAuto, false)
|
|
||||||
local autoCare = FG.result(FG.choose(FG.open(field(2, {fertilize = false, weed = false}), ctx(WHEAT)), "careAuto", true), true).plan
|
|
||||||
check("FG7 auto slår gødning til", autoCare.fertilize, true)
|
|
||||||
check("FG7 auto slår ukrudt til", autoCare.weed, true)
|
|
||||||
local careGrass = FG.rows(G.jump(FG.choose(FG.open(field(1), ctx(GRASS)), "careAuto", false), "care", "close"))
|
|
||||||
check("FG7 græs: ingen ukrudt", findRow(careGrass, "weed"), nil)
|
|
||||||
check("FG7 græs: kalk i pleje", findRow(careGrass, "lime") ~= nil, true)
|
|
||||||
local careWheat = FG.rows(G.jump(FG.choose(g, "careAuto", false), "care", "close"))
|
|
||||||
check("FG7 hvede: ukrudt", findRow(careWheat, "weed") ~= nil, true)
|
|
||||||
check("FG7 hvede: kalk ikke i pleje", findRow(careWheat, "lime"), nil)
|
|
||||||
|
|
||||||
local machinesOff = FG.rows(G.jump(g, "machines", "close"))
|
|
||||||
check("FG8 antal ad gangen", findRow(machinesOff, "rigs").type, "choice")
|
|
||||||
check("FG8 aflæsningsvogne ved høst", findRow(machinesOff, "unloaders") ~= nil, true)
|
|
||||||
check("FG8 ingen liste uden faste", countType(machinesOff, "check"), 0)
|
|
||||||
check("FG8 ingen aflæsningsvogne på græs", findRow(FG.rows(G.jump(FG.open(field(1), ctx(GRASS)), "machines", "close")), "unloaders"), nil)
|
|
||||||
local machinesOn = FG.rows(G.jump(FG.choose(g, "useDedicated", true), "machines", "close"))
|
|
||||||
check("FG8 liste: v3 fast på F90 skjult", findRow(machinesOn, "rigIds", "v3"), nil)
|
|
||||||
check("FG8 liste: tre synlige", countType(machinesOn, "check"), 3)
|
|
||||||
local hiddenText = nil
|
|
||||||
for _, row in ipairs(machinesOn) do if row.type == "text" and row.label.key == "spu_field_hiddenDedicated" then hiddenText = row end end
|
|
||||||
check("FG8 tekst om skjulte", hiddenText ~= nil and hiddenText.label.args[1], 1)
|
|
||||||
check("FG8 tekst marker", hiddenText ~= nil and hiddenText.label.args[2], "F90")
|
|
||||||
local ownFirst = FG.rows(G.jump(withRigs, "machines", "close"))
|
|
||||||
local firstCheck = nil
|
|
||||||
for _, row in ipairs(ownFirst) do if row.type == "check" and firstCheck == nil then firstCheck = row end end
|
|
||||||
check("FG8 egne først", firstCheck.item, "v2")
|
|
||||||
check("FG8 egne sat", firstCheck.value, true)
|
|
||||||
|
|
||||||
local storeAuto = FG.rows(G.jump(FG.open(field(1), ctx(GRASS)), "store", "close"))
|
|
||||||
check("FG9 ingen marksilo-spørgsmål uden marksilo", findRow(storeAuto, "useFieldSilo"), nil)
|
|
||||||
check("FG9 automatisk/vælg selv", findRow(storeAuto, "storeAuto").type, "choice")
|
|
||||||
check("FG9 auto: ingen siloliste", countType(storeAuto, "check"), 0)
|
|
||||||
local storeManual = FG.rows(G.jump(FG.choose(FG.open(field(1), ctx(GRASS)), "storeAuto", false), "store", "close"))
|
|
||||||
check("FG9 vælg selv: siloer", countType(storeManual, "check"), 2)
|
|
||||||
local siloCtx = ctx(GRASS, {fieldSilos = {{id = "m1", text = "Marksilo F1"}}})
|
|
||||||
local storeSilo = FG.rows(G.jump(FG.open(field(1), siloCtx), "store", "close"))
|
|
||||||
check("FG9 marksilo-spørgsmål", findRow(storeSilo, "useFieldSilo").type, "toggle")
|
|
||||||
local useSilo = FG.choose(FG.open(field(1), siloCtx), "useFieldSilo", true)
|
|
||||||
check("FG9 marksilo ja -> skjul auto", findRow(FG.rows(G.jump(useSilo, "store", "close")), "storeAuto"), nil)
|
|
||||||
|
|
||||||
local pickRows = FG.rows(picker)
|
|
||||||
check("FG10 vælger: links", countType(pickRows, "link"), 2)
|
|
||||||
check("FG10 vælger: id", findRow(pickRows, "field").value, 1)
|
|
||||||
local picked = FG.pickField(picker, field(2), ctx(WHEAT))
|
|
||||||
check("FG10 valgt mark", picked.draft.fieldId, 2)
|
|
||||||
check("FG10 videre til afgrøde", picked.step, "crop")
|
|
||||||
check("FG10 mark-trin stadig talt", (G.position(picked)), 2)
|
|
||||||
|
|
||||||
-- ---- resultat ----
|
|
||||||
local r = FG.result(g, true)
|
|
||||||
check("FG11 auto ved start", r.plan.mode, "auto")
|
|
||||||
check("FG11 manuel uden start", FG.result(g, false).plan.mode, "manual")
|
|
||||||
local autoField = FG.open(field(2, {mode = "auto"}), ctx(WHEAT))
|
|
||||||
check("FG11 Ret bevarer auto", FG.result(autoField, nil).plan.mode, "auto")
|
|
||||||
check("FG11 Ret bevarer manuel", FG.result(g, nil).plan.mode, "manual")
|
|
||||||
check("FG11 mål tvinger auto", FG.result(chosen, false).plan.mode, "auto")
|
|
||||||
check("FG11 mål ændret", FG.result(chosen, true).targetChanged, true)
|
|
||||||
check("FG11 mål uændret", r.targetChanged, false)
|
|
||||||
check("FG11 ingen rigIds i plan", r.plan.rigIds, nil)
|
|
||||||
local soilPlan = FG.result(FG.open(field(2, {soil = "plow", mulch = true, lime = false, roll = true}), ctx(WHEAT)), true).plan
|
|
||||||
check("FG12 vælg selv bevarer jord", soilPlan.soil, "plow")
|
|
||||||
local autoPrep = FG.result(FG.choose(FG.open(field(2, {soil = "plow", mulch = true, lime = false, roll = true}), ctx(WHEAT)), "prepAuto", true), true).plan
|
|
||||||
check("FG12 auto: jord", autoPrep.soil, "auto")
|
|
||||||
check("FG12 auto: mulch", autoPrep.mulch, false)
|
|
||||||
check("FG12 auto: kalk", autoPrep.lime, true)
|
|
||||||
check("FG12 auto: tromle", autoPrep.roll, false)
|
|
||||||
local grassNoPrep = FG.result(FG.choose(FG.open(field(1, {lime = false}), ctx(GRASS)), "prepAuto", true), true).plan
|
|
||||||
check("FG12 græs: auto rører ikke kalk", grassNoPrep.lime, false)
|
|
||||||
check("FG13 lager auto", #FG.result(FG.open(field(1, {storeBuildingIds = {"s1"}}), ctx(GRASS)) and FG.choose(FG.open(field(1, {storeBuildingIds = {"s1"}}), ctx(GRASS)), "storeAuto", true), true).plan.storeBuildingIds, 0)
|
|
||||||
check("FG13 lager vælg selv", table.concat(FG.result(FG.choose(FG.choose(FG.open(field(1), ctx(GRASS)), "storeAuto", false), "storeBuildingIds", {item = "s2", on = true}), true).plan.storeBuildingIds, ","), "s2")
|
|
||||||
check("FG13 lager marksilo", table.concat(FG.result(useSilo, true).plan.storeBuildingIds, ","), "m1")
|
|
||||||
check("FG13 bar mark bevarer lager", table.concat(FG.result(FG.open(field(4, {storeBuildingIds = {"s9"}}), ctx(BARE)), true).plan.storeBuildingIds, ","), "s9")
|
|
||||||
check("FG14 faste fra -> tom", #FG.result(FG.choose(withRigs, "useDedicated", false), true).rigIds, 0)
|
|
||||||
check("FG14 faste til", table.concat(FG.result(toggled, true).rigIds, ","), "v2,v1")
|
|
||||||
|
|
||||||
-- ---- faste maskiner på tværs af marker ----
|
|
||||||
local byField = FG.applyRigs({[90] = {"v3", "v5"}, [2] = {"v2"}}, 2, {"v2", "v3"})
|
|
||||||
check("FG15 fjernet fra anden mark", table.concat(byField[90], ","), "v5")
|
|
||||||
check("FG15 sat på marken", table.concat(byField[2], ","), "v2,v3")
|
|
||||||
check("FG15 ny mark", table.concat(FG.applyRigs({}, 7, {"v1"})[7], ","), "v1")
|
|
||||||
|
|
||||||
-- ---- resumé ----
|
|
||||||
local function lineFor(lines, step)
|
|
||||||
for _, line in ipairs(lines) do if line.step == step then return line end end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
local function partKeys(line)
|
|
||||||
local keys = {}
|
|
||||||
for _, part in ipairs(line.parts) do table.insert(keys, part.key or ("=" .. tostring(part.text))) end
|
|
||||||
return table.concat(keys, ",")
|
|
||||||
end
|
|
||||||
local sum = FG.summary(g)
|
|
||||||
check("FG16 linjer for synlige trin", #sum, 6)
|
|
||||||
check("FG16 afgrøde behold", partKeys(lineFor(sum, "crop")), "spu_guide_crop_keep")
|
|
||||||
check("FG16 halm", partKeys(lineFor(sum, "cropChoice")), "spu_guide_sum_straw_chop")
|
|
||||||
check("FG16 klargøring auto", partKeys(lineFor(sum, "prep")), "spu_guide_sum_auto")
|
|
||||||
check("FG16 pleje auto", partKeys(lineFor(sum, "care")), "spu_guide_sum_auto")
|
|
||||||
check("FG16 pleje manuel", partKeys(lineFor(FG.summary(FG.choose(g, "careAuto", false)), "care")), "spu_guide_sum_fert,spu_guide_sum_weed")
|
|
||||||
check("FG16 maskiner", partKeys(lineFor(sum, "machines")), "spu_guide_sum_freeRigs,spu_guide_sum_rigs,spu_guide_sum_unloaders")
|
|
||||||
check("FG16 lager auto", partKeys(lineFor(sum, "store")), "spu_guide_sum_auto")
|
|
||||||
check("FG16 ingen mark/summary-linje", lineFor(sum, "summary"), nil)
|
|
||||||
local sumTarget = FG.summary(chosen)
|
|
||||||
check("FG17 mål", partKeys(lineFor(sumTarget, "crop")), "spu_guide_sum_target")
|
|
||||||
check("FG17 mål titel", lineFor(sumTarget, "crop").parts[1].args[1], "Byg")
|
|
||||||
local sumRigs = FG.summary(withRigs)
|
|
||||||
check("FG17 faste", lineFor(sumRigs, "machines").parts[1].key, "spu_guide_sum_dedicated")
|
|
||||||
check("FG17 faste antal", lineFor(sumRigs, "machines").parts[1].args[1], 1)
|
|
||||||
local sumManual = FG.summary(FG.open(field(2, {soil = "plow", lime = true, mulch = true}), ctx(WHEAT)))
|
|
||||||
check("FG17 klargøring vælg selv", partKeys(lineFor(sumManual, "prep")), "spu_field_soil_plow,spu_guide_sum_lime,spu_guide_sum_mulch")
|
|
||||||
check("FG17 lager valgt silo", partKeys(lineFor(FG.summary(manual), "store")), "=Silo B, 1,2 km")
|
|
||||||
check("FG17 lager marksilo", partKeys(lineFor(FG.summary(useSilo), "store")), "spu_guide_sum_fieldSilo")
|
|
||||||
local sumHay = FG.summary(FG.choose(FG.choose(FG.open(field(1), ctx(GRASS)), "product", true), "rake", true))
|
|
||||||
check("FG17 hø + river", partKeys(lineFor(sumHay, "cropChoice")), "spu_field_plan_hay,spu_guide_sum_rake")
|
|
||||||
local sumNoCare = FG.summary(FG.choose(FG.choose(FG.choose(g, "careAuto", false), "fertilize", false), "weed", false))
|
|
||||||
check("FG17 ingen pleje", partKeys(lineFor(sumNoCare, "care")), "spu_guide_sum_noCare")
|
|
||||||
local sumSow = FG.summary(FG.choose(g, "sow", true))
|
|
||||||
check("FG17 så igen", partKeys(lineFor(sumSow, "crop")), "spu_guide_crop_keep,spu_guide_sum_sowAgain")
|
|
||||||
|
|
||||||
-- ---- oversigts-trinnet: links pr. linje ----
|
|
||||||
local summaryRows = FG.rows(G.new(g.def, g.draft, g.context, {step = "summary"}))
|
|
||||||
check("FG18 oversigt: link pr. linje", countType(summaryRows, "link"), 6)
|
|
||||||
check("FG18 oversigt: link-id", findRow(summaryRows, "jump").value, "crop")
|
|
||||||
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adFieldJobs.lua (fra repo-roden)
|
|
||||||
dofile("FS25_ADSmartPickup/adFieldModel.lua")
|
|
||||||
dofile("FS25_ADSmartPickup/adFieldJobs.lua")
|
|
||||||
local J = ADFieldJobs
|
|
||||||
local M = ADFieldModel
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local info = {id = 109, markerId = 5, mowable = true, readyRatio = 1}
|
|
||||||
|
|
||||||
-- J1: klar auto-mark med markør -> start; uden markør -> vent
|
|
||||||
local auto = M.onReadiness(M.newField(109), 1); auto.plan.mode = "auto"
|
|
||||||
check("J1 start", J.decide(auto, info, {}, 0).action, "start")
|
|
||||||
check("J1 uden markør", J.decide(auto, {id = 109, mowable = true}, {}, 0).action, "wait")
|
|
||||||
|
|
||||||
-- J2: arbejdende trin uden sæt -> send rolle; alle sæt færdige -> næste trin
|
|
||||||
local mowing = M.start(auto)
|
|
||||||
local wanted = J.decide(mowing, info, {}, 0)
|
|
||||||
check("J2 send slåmaskine", wanted.action, "dispatch"); check("J2 rolle", wanted.role, "mower")
|
|
||||||
check("J2 sæt i gang -> ingenting", J.decide(mowing, info, {{phase = "working"}}, 0).action, "none")
|
|
||||||
local mowedInfo = {id = 109, markerId = 5, mowable = true, readyRatio = 0}
|
|
||||||
check("J2 færdig -> videre", J.decide(mowing, mowedInfo, {{phase = "finished"}}, 0).action, "advance")
|
|
||||||
-- J3: sæt frigivet (spilleren tog det) tæller ikke som færdigt
|
|
||||||
check("J3 frigivet -> send nyt", J.decide(mowing, info, {{phase = "released"}}, 0).action, "dispatch")
|
|
||||||
-- J4: slåning færdig men marken stadig klar -> ikke videre (afbrudt)
|
|
||||||
check("J4 afbrudt slåning", J.decide(mowing, {id = 109, markerId = 5, mowable = true, readyRatio = 0.8}, {{phase = "finished"}}, 0).action, "dispatch")
|
|
||||||
-- J5: opsamling efter genindlæsning uden sæt -> send læssevogn
|
|
||||||
local collecting = M.advance(mowing)
|
|
||||||
check("J5 genoptag opsamling", J.decide(collecting, info, {}, 0).role, "collector")
|
|
||||||
check("J5 to vogne maks", J.decide(collecting, info, {{phase = "working"}, {phase = "driving"}}, 0).action, "none")
|
|
||||||
|
|
||||||
-- J6: Courseplay efterlader et hjørne -> efter 2 færdige slå-ture går marken videre alligevel
|
|
||||||
local standing = {id = 109, markerId = 5, mowable = true, readyRatio = 0.8}
|
|
||||||
check("J6 én tur -> slå igen", J.decide(mowing, standing, {{phase = "finished"}}, 0).action, "dispatch")
|
|
||||||
check("J6 to ture -> videre", J.decide(mowing, standing, {{phase = "finished"}, {phase = "finished"}}, 0).action, "advance")
|
|
||||||
|
|
||||||
-- J7: slået mark uden sæt (fx sluppet undervejs) -> videre til opsamling, ikke en ny slå-tur
|
|
||||||
check("J7 slået uden sæt -> videre", J.decide(mowing, {id = 109, markerId = 5, mowable = true, readyRatio = 0.1}, {}, 0).action, "advance")
|
|
||||||
check("J7 stående uden sæt -> slå", J.decide(mowing, info, {}, 0).action, "dispatch")
|
|
||||||
check("J7 slået men sæt i gang -> vent", J.decide(mowing, {id = 109, markerId = 5, mowable = true, readyRatio = 0.1}, {{phase = "working"}}, 0).action, "none")
|
|
||||||
|
|
||||||
-- J8: høst — høster først (hovedrolle), aflæsningsvogne kun mens høsteren arbejder
|
|
||||||
local harvestInfo = {id = 50, markerId = 7, kind = "harvest", readyRatio = 1, fruitIndex = 3}
|
|
||||||
local harvesting = M.start(M.onReadiness(M.newField(50), 1), "harvest", {})
|
|
||||||
local d = J.decide(harvesting, harvestInfo, {}, 0)
|
|
||||||
check("J8 send høster", d.role, "harvester"); check("J8 ikke hjælper", d.helper, false)
|
|
||||||
local withHarvester = {{phase = "working", stage = "harvesting", helper = false}}
|
|
||||||
local dHelper = J.decide(harvesting, harvestInfo, withHarvester, 0)
|
|
||||||
check("J8 så aflæsningsvogn", dHelper.role, "unloader"); check("J8 hjælper", dHelper.helper, true)
|
|
||||||
check("J8 vogn på plads -> intet", J.decide(harvesting, harvestInfo, {withHarvester[1], {phase = "working", stage = "harvesting", helper = true}}, 0).action, "none")
|
|
||||||
-- manuel transport: høsteren venter på spilleren -> ingen høster mere og endnu ingen tømmevogn
|
|
||||||
local awaiting = {{phase = "awaitingPlayer", stage = "harvesting", helper = false}}
|
|
||||||
check("J8b venter på spilleren -> intet (ingen ny høster, ingen tømmevogn)", J.decide(harvesting, harvestInfo, awaiting, 0).action, "none")
|
|
||||||
-- høsteren færdig, afgrøden væk (marken er nu bar) -> videre, selv om vognen stadig kører
|
|
||||||
local afterInfo = {id = 50, markerId = 7, kind = "bare", readyRatio = 0}
|
|
||||||
check("J8 høst færdig -> videre", J.decide(harvesting, afterInfo, {{phase = "finished", stage = "harvesting"}, {phase = "working", stage = "harvesting", helper = true}}, 0).action, "advance")
|
|
||||||
-- sæt fra et tidligere trin (vogn på vej hjem) tæller ikke på det nye trin
|
|
||||||
local cultivating = M.advance(harvesting, {})
|
|
||||||
check("J8 gammel vogn tæller ikke", J.decide(cultivating, afterInfo, {{phase = "working", stage = "harvesting", helper = true}}, 0).role, "cultivator")
|
|
||||||
|
|
||||||
-- J9: multi-tool — hele holdet sendes samlet; mens nogen kører, sendes ingen nye hovedsæt
|
|
||||||
local multiMow = M.with(mowing, {plan = {rigs = 3}})
|
|
||||||
local dMulti = J.decide(multiMow, info, {}, 0)
|
|
||||||
check("J9 tre slåmaskiner", dMulti.count, 3)
|
|
||||||
check("J9 én kører stadig -> ingen nye", J.decide(multiMow, info, {{phase = "working"}, {phase = "finished"}}, 0).action, "none")
|
|
||||||
|
|
||||||
-- J10: pleje under vækst — Auto starter, Manuel viser behovet
|
|
||||||
local growingInfo = {id = 60, markerId = 8, kind = "harvest", growing = true, fert = 0, fertMax = 2, lime = 2, weed = 0, readyRatio = 0.2}
|
|
||||||
local autoField = M.with(M.newField(60), {plan = {mode = "auto"}})
|
|
||||||
local care = J.decide(autoField, growingInfo, {}, 0)
|
|
||||||
check("J10 auto -> pleje", care.action, "care"); check("J10 gødning", care.stage, "fertilizing")
|
|
||||||
local manualCare = J.decide(M.newField(60), growingInfo, {}, 0)
|
|
||||||
check("J10 manuel -> intet, men behov vist", manualCare.action, "none"); check("J10 behov", manualCare.care, "fertilizing")
|
|
||||||
check("J10 Start nu -> pleje", J.decide(M.with(M.newField(60), {startRequested = true}), growingInfo, {}, 0).action, "care")
|
|
||||||
check("J10 uden markør -> vent", J.decide(autoField, {id = 60, kind = "harvest", growing = true, fert = 0, fertMax = 2}, {}, 0).action, "wait")
|
|
||||||
|
|
||||||
-- J11: regn — vejrfølsomme trin sendes ikke ud (kørende sæt fortsætter); klargøring/pleje kører videre
|
|
||||||
local rain = {raining = true}
|
|
||||||
local dRain = J.decide(mowing, info, {}, 0, rain)
|
|
||||||
check("J11 slåning venter i regn", dRain.action, "wait"); check("J11 grund", dRain.reason, "rain")
|
|
||||||
check("J11 kørende slåmaskine fortsætter", J.decide(mowing, info, {{phase = "working"}}, 0, rain).action, "none")
|
|
||||||
check("J11 høst venter", J.decide(harvesting, harvestInfo, {}, 0, rain).reason, "rain")
|
|
||||||
check("J11 kultivering kører i regn", J.decide(cultivating, afterInfo, {}, 0, rain).role, "cultivator")
|
|
||||||
check("J11 start af høst venter", J.decide(M.with(M.onReadiness(M.newField(51), 1), {startRequested = true, kind = "harvest"}), harvestInfo, {}, 0, rain).reason, "rain")
|
|
||||||
local silageCollect = M.advance(mowing)
|
|
||||||
check("J11 ensilage-opsamling kører i regn", J.decide(silageCollect, info, {}, 0, rain).action, "dispatch")
|
|
||||||
local hayCollect = M.with(silageCollect, {plan = {product = "hay"}})
|
|
||||||
check("J11 hø-opsamling venter", J.decide(hayCollect, info, {}, 0, rain).reason, "rain")
|
|
||||||
check("J11 tørvejr", J.decide(mowing, info, {}, 0, {raining = false}).action, "dispatch")
|
|
||||||
|
|
||||||
-- J12: bar mark med mål-afgrøde klargøres straks — kun selve såningen venter på såtiden
|
|
||||||
local bareInfo = {id = 90, markerId = 9, kind = "bare", readyRatio = 0, needs = {}}
|
|
||||||
local bareTarget = M.with(M.newField(90), {plan = {targetFruit = "WHEAT", mode = "auto"}})
|
|
||||||
local canSow = {plantable = function(name) return name == "WHEAT" end}
|
|
||||||
check("J12 bar + såtid -> start", J.decide(bareTarget, bareInfo, {}, 0, canSow).action, "start")
|
|
||||||
check("J12 bar + ikke såtid -> start alligevel (klargøring)", J.decide(bareTarget, bareInfo, {}, 0, {plantable = function() return false end}).action, "start")
|
|
||||||
check("J12 bar uden markør -> vent", J.decide(bareTarget, {id = 90, kind = "bare"}, {}, 0, canSow).action, "wait")
|
|
||||||
check("J12 voksende mark med mål -> lad den vokse", J.decide(bareTarget, {id = 90, markerId = 9, kind = "harvest", growing = true, readyRatio = 0.3}, {}, 0, canSow).action, "none")
|
|
||||||
|
|
||||||
-- J13: afbrudt slåning efter genindlæsning (in-game 27/9 F90): genvækst i mellemhøjde -> vent; hjørne efter en tur -> slå straks
|
|
||||||
local regrowing = {id = 90, markerId = 9, mowable = true, readyRatio = 0.5, immatureRatio = 0.5}
|
|
||||||
local dRegrow = J.decide(mowing, regrowing, {}, 0)
|
|
||||||
check("J13 genvækst -> vent", dRegrow.action, "wait"); check("J13 grund", dRegrow.reason, "regrowing")
|
|
||||||
check("J13 genvækst men tur færdig -> slå hjørnet", J.decide(mowing, regrowing, {{phase = "finished"}}, 0).action, "dispatch")
|
|
||||||
check("J13 halvt slået uden genvækst -> slå resten", J.decide(mowing, {id = 90, markerId = 9, mowable = true, readyRatio = 0.6, immatureRatio = 0}, {}, 0).action, "dispatch")
|
|
||||||
|
|
||||||
-- J14: kultivering på en mark med uhøstede kartofler (in-game 27/9 F16) -> tilbage til klar; rest efter høst -> ikke
|
|
||||||
local cultivatingField = M.with(auto, {stage = "cultivating", kind = "harvest"})
|
|
||||||
check("J14 uhøstet -> spol tilbage", J.decide(cultivatingField, {id = 16, markerId = 9, kind = "harvest", phase = "ready", readyRatio = 1}, {}, 0).action, "rewind")
|
|
||||||
check("J14 høstrest 30 % -> kultiver", J.decide(cultivatingField, {id = 16, markerId = 9, kind = "harvest", phase = "ready", readyRatio = 0.3}, {}, 0).action ~= "rewind", true)
|
|
||||||
check("J14 stub -> kultiver", J.decide(cultivatingField, {id = 16, markerId = 9, kind = "bare", phase = "cut", readyRatio = 0}, {}, 0).action ~= "rewind", true)
|
|
||||||
|
|
||||||
|
|
||||||
-- resumeField: efter genindlæsning stopper spillet alle arbejdere; en CP-maskine der står PÅ en mark hvis
|
|
||||||
-- aktuelle trin passer til maskinens rolle, meldes ind på den mark igen (in-game 27/9)
|
|
||||||
do
|
|
||||||
local square = {{x = 0, z = 0}, {x = 100, z = 0}, {x = 100, z = 100}, {x = 0, z = 100}}
|
|
||||||
local fields = {
|
|
||||||
{id = 90, stage = "collecting", verts = square},
|
|
||||||
{id = 111, stage = "harvesting", verts = {{x = 200, z = 0}, {x = 300, z = 0}, {x = 300, z = 100}, {x = 200, z = 100}}},
|
|
||||||
}
|
|
||||||
check("RF høster på F111", ADFieldJobs.resumeField(250, 50, "harvester", fields), 111)
|
|
||||||
check("RF læssevogn på F90", ADFieldJobs.resumeField(50, 50, "collector", fields), 90)
|
|
||||||
check("RF forkert rolle til trinnet -> nil", ADFieldJobs.resumeField(250, 50, "collector", fields), nil)
|
|
||||||
check("RF uden for alle marker -> nil", ADFieldJobs.resumeField(150, 50, "harvester", fields), nil)
|
|
||||||
check("RF mark uden omrids -> nil", ADFieldJobs.resumeField(50, 50, "collector", {{id = 1, stage = "collecting", verts = {}}}), nil)
|
|
||||||
check("RF på kanten tæller som indenfor", ADFieldJobs.resumeField(0.5, 50, "collector", fields), 90)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
-- startAt: hvor Courseplay skal starte i kurset (CP's standard er "first" = forfra, også på en halvt høstet mark;
|
|
||||||
-- in-game 27/9: mejetærskeren startede F111 forfra)
|
|
||||||
check("SA eget kursus med husket waypoint -> last", ADFieldJobs.startAtFor({reuse = true, remembered = true, stage = "harvesting", standingRatio = 0.4}), "last")
|
|
||||||
check("SA halvt høstet -> nearest", ADFieldJobs.startAtFor({stage = "harvesting", standingRatio = 0.4}), "nearest")
|
|
||||||
check("SA halvt slået -> nearest", ADFieldJobs.startAtFor({stage = "mowing", standingRatio = 0.5}), "nearest")
|
|
||||||
check("SA urørt mark -> first", ADFieldJobs.startAtFor({stage = "harvesting", standingRatio = 1}), "first")
|
|
||||||
check("SA næsten urørt (97 %) -> first", ADFieldJobs.startAtFor({stage = "harvesting", standingRatio = 0.97}), "first")
|
|
||||||
check("SA ukendt fremdrift -> first", ADFieldJobs.startAtFor({stage = "harvesting"}), "first")
|
|
||||||
check("SA klargøring (ingen afgrøde at måle) -> first", ADFieldJobs.startAtFor({stage = "cultivating", standingRatio = 0.3}), "first")
|
|
||||||
check("SA husket waypoint uden eget kursus -> ikke last", ADFieldJobs.startAtFor({reuse = false, remembered = true, stage = "cultivating"}), "first")
|
|
||||||
|
|
||||||
|
|
||||||
-- shouldResume: kun maskiner der stadig er midt i jobbet genstartes (in-game 27/9: to ROPA'er med et husket
|
|
||||||
-- waypoint fra et gammelt job kørte ud ad vejene mod det gamle kursus)
|
|
||||||
check("SR står på en mark i gang med sit trin -> ja", ADFieldJobs.shouldResume({fieldId = 111, distance = 400}), true)
|
|
||||||
check("SR lige ved sit huskede waypoint -> ja", ADFieldJobs.shouldResume({distance = 12}), true)
|
|
||||||
check("SR langt fra waypointet og ingen mark -> nej", ADFieldJobs.shouldResume({distance = 850}), false)
|
|
||||||
check("SR ukendt afstand og ingen mark -> nej", ADFieldJobs.shouldResume({}), false)
|
|
||||||
|
|
||||||
|
|
||||||
-- mayDispatch: markarbejdet sender højst én maskine ud pr. DISPATCH_GAP_MS på tværs af alle marker
|
|
||||||
-- (in-game 27/9: 6 maskiner kørte fra parkeringen i samme sekund efter Start alle)
|
|
||||||
check("MD første udsendelse", ADFieldJobs.mayDispatch(5000, nil), true)
|
|
||||||
check("MD for tæt på forrige", ADFieldJobs.mayDispatch(5000 + ADFieldJobs.DISPATCH_GAP_MS - 1, 5000), false)
|
|
||||||
check("MD efter mellemrummet", ADFieldJobs.mayDispatch(5000 + ADFieldJobs.DISPATCH_GAP_MS, 5000), true)
|
|
||||||
|
|
||||||
|
|
||||||
-- halvt høstet mark (in-game 27/9 F111): høsten er ikke færdig før næsten intet står; en mark i klargøring med
|
|
||||||
-- stående afgrøde spoles tilbage
|
|
||||||
do
|
|
||||||
local halfInfo = {id = 111, markerId = 5, kind = "harvest", phase = "ready", readyRatio = 0.45, standingRatio = 0.45, fruitIndex = 1}
|
|
||||||
check("HH halvt høstet uden høster -> send høster (ikke videre)", J.decide(harvesting, halfInfo, {}, 0).action, "dispatch")
|
|
||||||
local doneInfo = {id = 111, markerId = 5, kind = "harvest", phase = "ready", readyRatio = 0.02, standingRatio = 0.02, fruitIndex = 1}
|
|
||||||
check("HH næsten intet står -> videre", J.decide(harvesting, doneInfo, {}, 0).action, "advance")
|
|
||||||
local cultivating = M.with(harvesting, {stage = "cultivating"})
|
|
||||||
check("HH klargøring med halv mark stående -> spol tilbage", J.decide(cultivating, halfInfo, {}, 0).action, "rewind")
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
-- startTimedOut: CP må beregne kursus i op til 5 min (in-game 27/9 F90: 140 ha tog over 60 s, moden opgav midt i)
|
|
||||||
check("ST start uden beregning > 60 s -> opgiv", ADFieldJobs.startTimedOut(61000, false), true)
|
|
||||||
check("ST beregner stadig efter 90 s -> vent", ADFieldJobs.startTimedOut(90000, true), false)
|
|
||||||
check("ST beregner over 5 min -> opgiv", ADFieldJobs.startTimedOut(301000, true), true)
|
|
||||||
check("ST under 60 s -> vent", ADFieldJobs.startTimedOut(30000, false), false)
|
|
||||||
|
|
||||||
|
|
||||||
-- shouldEmptyHarvester: høsten er færdig, men høsteren har korn i tanken og står ved marken -> tøm først
|
|
||||||
-- (in-game 27/9 F15: marken gik til halm med fuld tank, ingen tømmevogn kom)
|
|
||||||
check("EH korn i tanken ved marken -> tøm", ADFieldJobs.shouldEmptyHarvester({fill = 12000, near = true}), true)
|
|
||||||
check("EH tom tank -> videre", ADFieldJobs.shouldEmptyHarvester({fill = 0, near = true}), false)
|
|
||||||
check("EH langt væk -> videre", ADFieldJobs.shouldEmptyHarvester({fill = 12000, near = false}), false)
|
|
||||||
check("EH optaget -> videre", ADFieldJobs.shouldEmptyHarvester({fill = 12000, near = true, busy = true}), false)
|
|
||||||
check("EH spiller i -> videre", ADFieldJobs.shouldEmptyHarvester({fill = 12000, near = true, controlled = true}), false)
|
|
||||||
check("EH allerede forsøgt -> videre", ADFieldJobs.shouldEmptyHarvester({fill = 12000, near = true, tried = true}), false)
|
|
||||||
|
|
||||||
|
|
||||||
-- nearestPoint: CP bruger markens punkt som kursets startsted — tag det nærmest traktoren (in-game 27/9 F17: første
|
|
||||||
-- punkt lå 280 m væk i den anden ende, og CP kørte gennem skoven)
|
|
||||||
do
|
|
||||||
local pts = {{x = 1600, z = -1839}, {x = 1350, z = -1740}, {x = 1500, z = -1800}}
|
|
||||||
local p = ADFieldJobs.nearestPoint(pts, 1340, -1733)
|
|
||||||
check("NP nærmest traktoren", p.x .. "," .. p.z, "1350,-1740")
|
|
||||||
check("NP uden punkter -> nil", ADFieldJobs.nearestPoint({}, 0, 0), nil)
|
|
||||||
check("NP uden position -> første", ADFieldJobs.nearestPoint(pts, nil, nil).x, 1600)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- pulje: en traktor der henter et redskab tæller som i gang (ellers sendes endnu en ud)
|
|
||||||
check("pulje: henter -> ingenting", J.decide(mowing, info, {{phase = "fetching"}}, 0).action, "none")
|
|
||||||
-- JP: klargøring der allerede er gjort (fx efter genindlæsning) går videre uden at sende maskinen ud igen
|
|
||||||
-- (in-game 28/9: F17 blev kultiveret 3 gange — opgaver gemmes ikke, så "færdig" blev aldrig set)
|
|
||||||
local cultField = M.with(M.newField(17), {stage = "cultivating", kind = "bare", plan = {mode = "auto"}})
|
|
||||||
local cultInfo = {id = 17, markerId = 5, kind = "bare", readyRatio = 0, needs = {}}
|
|
||||||
cultInfo.prepared = 0.98
|
|
||||||
check("JP kultiveret -> videre", J.decide(cultField, cultInfo, {}, 0).action, "advance")
|
|
||||||
cultInfo.prepared = 0.4
|
|
||||||
check("JP halvt kultiveret -> send maskine", J.decide(cultField, cultInfo, {}, 0).action, "dispatch")
|
|
||||||
cultInfo.prepared = nil
|
|
||||||
check("JP ukendt jord -> som før (send)", J.decide(cultField, cultInfo, {}, 0).action, "dispatch")
|
|
||||||
cultInfo.prepared = 0.98
|
|
||||||
check("JP kultivator i gang -> vent på den", J.decide(cultField, cultInfo, {{phase = "working", stage = "cultivating"}}, 0).action, "none")
|
|
||||||
local plowField = M.with(cultField, {stage = "plowing"})
|
|
||||||
check("JP kultiveret er ikke pløjet -> send plov", J.decide(plowField, cultInfo, {}, 0).action, "dispatch")
|
|
||||||
cultInfo.plowed = 0.97
|
|
||||||
check("JP pløjet -> videre", J.decide(plowField, cultInfo, {}, 0).action, "advance")
|
|
||||||
local sowField = M.with(cultField, {stage = "sowing"})
|
|
||||||
check("JP såning berøres ikke af klargjort jord", J.decide(sowField, cultInfo, {}, 0).action ~= "advance", true)
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,324 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adFieldModel.lua (fra repo-roden)
|
|
||||||
dofile("FS25_ADSmartPickup/adFieldModel.lua")
|
|
||||||
local M = ADFieldModel
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- M1: ny mark = venter, manuel ensilage uden riv
|
|
||||||
local f = M.newField(109)
|
|
||||||
check("M1 stage", f.stage, "idle")
|
|
||||||
check("M1 mode", f.plan.mode, "manual")
|
|
||||||
check("M1 product auto", f.plan.product, "auto")
|
|
||||||
check("M1 auto = ensilage på slåafgrøde", M.product(f.plan, "mowable"), "silage")
|
|
||||||
check("M1 auto = høst ellers", M.product(f.plan, "harvest"), "harvest")
|
|
||||||
check("M1 rake", f.plan.rake, false)
|
|
||||||
|
|
||||||
-- M2: klar ved >= 90 %
|
|
||||||
check("M2 89 % ikke klar", M.onReadiness(f, 0.89).stage, "idle")
|
|
||||||
local ready = M.onReadiness(f, 0.9)
|
|
||||||
check("M2 90 % klar", ready.stage, "ready")
|
|
||||||
check("M2 immutabel", f.stage, "idle")
|
|
||||||
|
|
||||||
-- M3: klar mark slået af spilleren -> tilbage til venter
|
|
||||||
check("M3 slået manuelt", M.onReadiness(ready, 0.3).stage, "idle")
|
|
||||||
check("M3 lidt under 90 % forbliver klar", M.onReadiness(ready, 0.8).stage, "ready")
|
|
||||||
|
|
||||||
-- M4: trin pr. plan
|
|
||||||
check("M4 ensilage", table.concat(M.workStages({product = "silage"}), ","), "mowing,collecting")
|
|
||||||
check("M4 ensilage m. riv", table.concat(M.workStages({product = "silage", rake = true}), ","), "mowing,raking,collecting")
|
|
||||||
check("M4 hø", table.concat(M.workStages({product = "hay", rake = false}), ","), "mowing,tedding,collecting")
|
|
||||||
check("M4 hø m. riv", table.concat(M.workStages({product = "hay", rake = true}), ","), "mowing,tedding,raking,collecting")
|
|
||||||
|
|
||||||
-- M5: start: auto starter selv, manuel kun på forespørgsel
|
|
||||||
check("M5 manuel starter ikke", M.shouldStart(ready), false)
|
|
||||||
local auto = M.onReadiness(M.newField(5), 1)
|
|
||||||
auto.plan.mode = "auto"
|
|
||||||
check("M5 auto starter", M.shouldStart(auto), true)
|
|
||||||
local requested = M.newField(6); requested.stage = "ready"; requested.startRequested = true
|
|
||||||
check("M5 Start nu", M.shouldStart(requested), true)
|
|
||||||
local started = M.start(ready)
|
|
||||||
check("M5 start -> slås", started.stage, "mowing")
|
|
||||||
check("M5 start rydder forespørgsel", started.startRequested, false)
|
|
||||||
local oldMow = M.with(ready, {mowVehicleId = "old"})
|
|
||||||
check("M5 start glemmer gammel slåmaskine", M.start(oldMow).mowVehicleId, nil)
|
|
||||||
|
|
||||||
-- M6: videre gennem trinene; færdig mark bliver klar igen når den er vokset
|
|
||||||
local hay = M.start(M.onReadiness(M.newField(7), 1)); hay.plan.product = "hay"
|
|
||||||
hay = M.advance(hay); check("M6 hø -> vendes", hay.stage, "tedding")
|
|
||||||
hay = M.advance(hay); check("M6 -> samles op", hay.stage, "collecting")
|
|
||||||
hay = M.advance(hay); check("M6 -> færdig", hay.stage, "done")
|
|
||||||
check("M6 færdig forbliver ved lav andel", M.onReadiness(hay, 0.1).stage, "done")
|
|
||||||
check("M6 færdig -> klar når vokset", M.onReadiness(hay, 0.95).stage, "ready")
|
|
||||||
check("M6 arbejdende mark ignorerer andel", M.onReadiness(started, 0).stage, "mowing")
|
|
||||||
check("M6 isWorking", M.isWorking(started), true)
|
|
||||||
check("M6 stop -> klar", M.stop(started).stage, "ready")
|
|
||||||
|
|
||||||
-- M7: roller, maks sæt, bredde
|
|
||||||
check("M7 rolle slå", M.roleFor("mowing"), "mower")
|
|
||||||
check("M7 rolle saml", M.roleFor("collecting"), "collector")
|
|
||||||
check("M7 rolle klar = ingen", M.roleFor("ready"), nil)
|
|
||||||
check("M7 maks læssevogne = planens sæt", M.maxRigs("collecting", {rigs = 2}), 2)
|
|
||||||
check("M7 maks slå", M.maxRigs("mowing"), 1)
|
|
||||||
local okWide, whyWide = M.checkWidth("mower", 60, nil)
|
|
||||||
check("M7 over 50 m", okWide, false); check("M7 grund", whyWide, "tooWide")
|
|
||||||
check("M7 40,8 m slå ok", (M.checkWidth("mower", 40.8, nil)), true)
|
|
||||||
-- M7b: bredde under 1,5 m er ikke læst rigtigt (spreder gav 0,5 m -> CP-kursus hver halve meter på 72 ha
|
|
||||||
-- -> spillet frøs, in-game 26/9) — sendes aldrig ud
|
|
||||||
local okTiny, whyTiny = M.checkWidth("fertilizer", 0.5, nil)
|
|
||||||
check("M7b 0,5 m afvises", okTiny, false)
|
|
||||||
check("M7b grund", whyTiny, "widthUnknown")
|
|
||||||
check("M7b 3 m strigle ok", (M.checkWidth("weeder", 3, nil)), true)
|
|
||||||
check("M7b aflæsningsvogn uden bredde ok", (M.checkWidth("unloader", 0, nil)), true)
|
|
||||||
check("M7b ballesamler uden bredde ok", (M.checkWidth("baleCollector", 0, nil)), true)
|
|
||||||
local okNarrow, whyNarrow, args = M.checkWidth("collector", 20, 40.8)
|
|
||||||
check("M7 for smal", okNarrow, false); check("M7 grund smal", whyNarrow, "tooNarrow"); check("M7 args", args[2], 40.8)
|
|
||||||
check("M7 lige bred ok (tolerance)", (M.checkWidth("collector", 40.76, 40.8)), true)
|
|
||||||
check("M7 ukendt slåbredde ok", (M.checkWidth("tedder", 10, nil)), true)
|
|
||||||
|
|
||||||
-- wantsRig: kun arbejdende trin, under maks, efter pause
|
|
||||||
check("M7 wantsRig slå", M.wantsRig(started, 0, 0).role, "mower")
|
|
||||||
check("M7 wantsRig fuld", M.wantsRig(started, 1, 0), nil)
|
|
||||||
local paused = M.start(M.onReadiness(M.newField(8), 1)); paused.retryAt = 1000
|
|
||||||
check("M7 wantsRig i pause", M.wantsRig(paused, 0, 500), nil)
|
|
||||||
check("M7 wantsRig efter pause", M.wantsRig(paused, 0, 1000).role, "mower")
|
|
||||||
check("M7 wantsRig klar = nil", M.wantsRig(ready, 0, 0), nil)
|
|
||||||
|
|
||||||
-- M8: gem/indlæs bevarer trin, plan og slåbredde
|
|
||||||
local saved = M.advance(M.start(M.onReadiness(M.newField(109), 1))); saved.mowWidth = 40.8; saved.mowVehicleId = "vehicleBigM"
|
|
||||||
saved.plan = {mode = "auto", product = "hay", rake = true, storeBuildingIds = {"b12", "b7"}, rigIds = {"v1", "v2"}}
|
|
||||||
local back = M.fromRecord(M.toRecord(saved))
|
|
||||||
check("M8 id", back.id, 109)
|
|
||||||
check("M8 stage", back.stage, saved.stage)
|
|
||||||
check("M8 mode", back.plan.mode, "auto")
|
|
||||||
check("M8 product", back.plan.product, "hay")
|
|
||||||
check("M8 rake", back.plan.rake, true)
|
|
||||||
check("M8 lager", table.concat(back.plan.storeBuildingIds, " "), "b12 b7")
|
|
||||||
check("M8 faste maskiner", table.concat(back.plan.rigIds, " "), "v1 v2")
|
|
||||||
check("M8 slåbredde", back.mowWidth, 40.8)
|
|
||||||
check("M8 slåmaskine", back.mowVehicleId, "vehicleBigM")
|
|
||||||
check("M8 ingen slåmaskine", M.fromRecord({id = 4, stage = "idle"}).mowVehicleId, nil)
|
|
||||||
check("M8 ukendt trin -> venter", M.fromRecord({id = 3, stage = "bogus"}).stage, "idle")
|
|
||||||
|
|
||||||
-- M9: with() kopierer og fletter planen
|
|
||||||
local base = M.start(M.onReadiness(M.newField(1), 1))
|
|
||||||
local noted = M.with(base, {reason = {text = "x"}, retryAt = 5, plan = {product = "hay"}})
|
|
||||||
check("M9 grund", noted.reason.text, "x"); check("M9 pause", noted.retryAt, 5)
|
|
||||||
check("M9 plan flettet", noted.plan.product, "hay"); check("M9 mode bevaret", noted.plan.mode, "manual")
|
|
||||||
check("M9 trin bevaret", noted.stage, "mowing"); check("M9 original urørt", base.plan.product, "auto")
|
|
||||||
|
|
||||||
-- M10: Start nu på en mark der ikke er helt klar -> klar; arbejdende mark røres ikke
|
|
||||||
check("M10 venter -> klar", M.forceReady(M.newField(2)).stage, "ready")
|
|
||||||
check("M10 arbejdende uændret", M.forceReady(base).stage, "mowing")
|
|
||||||
|
|
||||||
-- M11: krævet bredde og kursus-kilde pr. rolle (smalle standardmaskiner, 25/9)
|
|
||||||
local cut = M.with(M.newField(3), {mowWidth = 40.8})
|
|
||||||
check("M11 vender kræver ingen bredde", M.requiredWidth(cut, "tedder"), nil)
|
|
||||||
check("M11 river kræver ingen bredde", M.requiredWidth(cut, "rake"), nil)
|
|
||||||
check("M11 læssevogn uden riv = slåbredde", M.requiredWidth(cut, "collector"), 40.8)
|
|
||||||
local raked = M.with(cut, {plan = {rake = true}})
|
|
||||||
check("M11 læssevogn efter riv = ingen krav", M.requiredWidth(raked, "collector"), nil)
|
|
||||||
check("M11 vender eget kursus", M.courseSource(cut, "tedder"), nil)
|
|
||||||
check("M11 river eget kursus", M.courseSource(raked, "rake"), nil)
|
|
||||||
check("M11 læssevogn uden riv -> slåkursus", M.courseSource(cut, "collector"), "mower")
|
|
||||||
check("M11 læssevogn efter riv -> rivekursus", M.courseSource(raked, "collector"), "rake")
|
|
||||||
check("M11 slåmaskine eget kursus", M.courseSource(cut, "mower"), nil)
|
|
||||||
-- gem/indlæs rivemaskine
|
|
||||||
local withRake = M.with(raked, {rakeVehicleId = "vehicleRake"})
|
|
||||||
check("M11 rivemaskine gemt", M.fromRecord(M.toRecord(withRake)).rakeVehicleId, "vehicleRake")
|
|
||||||
check("M11 start glemmer rivemaskine", M.start(M.with(M.onReadiness(M.newField(9), 1), {rakeVehicleId = "x"})).rakeVehicleId, nil)
|
|
||||||
|
|
||||||
-- M12: sideriver — læssevognens CP-forskydning = egen forskydning − skårets forskydning (venstre positiv),
|
|
||||||
-- holdt inden for Courseplays ±10 m; skårets forskydning gemmes pr. mark
|
|
||||||
check("M12 midterrive", M.collectorOffset(0, 0), 0)
|
|
||||||
check("M12 skår 3 m til venstre", M.collectorOffset(0, 3), -3)
|
|
||||||
check("M12 vogn med egen forskydning", M.collectorOffset(0.5, -2), 2.5)
|
|
||||||
check("M12 holdes inden for 10 m", M.collectorOffset(0, 14), -10)
|
|
||||||
check("M12 ukendt skår -> egen forskydning", M.collectorOffset(0.4, nil), 0.4)
|
|
||||||
local side = M.with(M.newField(11), {windrowOffset = 3.2})
|
|
||||||
check("M12 skår gemt", M.fromRecord(M.toRecord(side)).windrowOffset, 3.2)
|
|
||||||
check("M12 intet skår gemt", M.fromRecord(M.toRecord(M.newField(12))).windrowOffset, nil)
|
|
||||||
check("M12 start glemmer skår", M.start(M.with(M.onReadiness(M.newField(13), 1), {windrowOffset = 1})).windrowOffset, nil)
|
|
||||||
|
|
||||||
-- M13: gæringssilo = produktion der laver markens skår om til ensilage (FermentingSiloPack m.fl.)
|
|
||||||
local GRASS_W, DRY_W, SILAGE, CHAFF, BREAD = 10, 11, 20, 30, 40
|
|
||||||
local fermenter = {{inputs = {{type = CHAFF}}, outputs = {{type = SILAGE}}}, {inputs = {{type = GRASS_W}}, outputs = {{type = SILAGE}}}}
|
|
||||||
check("M13 græs -> ensilage", M.isFermenter(fermenter, GRASS_W, SILAGE), true)
|
|
||||||
check("M13 hø ikke i denne", M.isFermenter(fermenter, DRY_W, SILAGE), false)
|
|
||||||
check("M13 bageri ikke", M.isFermenter({{inputs = {{type = GRASS_W}}, outputs = {{type = BREAD}}}}, GRASS_W, SILAGE), false)
|
|
||||||
check("M13 ingen produktioner", M.isFermenter(nil, GRASS_W, SILAGE), false)
|
|
||||||
check("M13 ukendt ensilage-type", M.isFermenter(fermenter, GRASS_W, nil), false)
|
|
||||||
|
|
||||||
-- M14: høstkæde + klargøring efter behov
|
|
||||||
local plan = {mode = "manual", product = "auto", rake = false, rigs = 1, unloaders = 1, mulch = false, lime = true,
|
|
||||||
soil = "auto", sow = true, sowFruit = "", roll = true, fertilize = true, weed = true, storeBuildingIds = {}}
|
|
||||||
check("M14 høst + kultivér + så + tromle", table.concat(M.workStages(plan, "harvest", {}), ","), "harvesting,cultivating,sowing,rolling")
|
|
||||||
check("M14 kalk + pløj når jorden trænger", table.concat(M.workStages(plan, "harvest", {lime = true, plow = true}), ","), "harvesting,liming,plowing,sowing,rolling")
|
|
||||||
local noSoil = {soil = "none", sow = false, lime = false, mulch = true}
|
|
||||||
check("M14 kun stubbehakning", table.concat(M.workStages(noSoil, "harvest", {}), ","), "harvesting,mulching")
|
|
||||||
check("M14 bar mark = kun klargøring", table.concat(M.workStages(plan, "bare", {plow = true}), ","), "plowing,sowing,rolling")
|
|
||||||
check("M14 tvunget pløj", table.concat(M.prepStages({soil = "plow"}, {}), ","), "plowing")
|
|
||||||
local harvestField = M.start(M.with(M.newField(20), {plan = {sow = true, soil = "cultivate"}}), "harvest", {})
|
|
||||||
check("M14 start = høst", harvestField.stage, "harvesting")
|
|
||||||
check("M14 husker art", harvestField.kind, "harvest")
|
|
||||||
local afterHarvest = M.advance(harvestField, {})
|
|
||||||
check("M14 høst -> kultivér", afterHarvest.stage, "cultivating")
|
|
||||||
check("M14 -> så", M.advance(afterHarvest, {}).stage, "sowing")
|
|
||||||
check("M14 så -> færdig", M.advance(M.advance(afterHarvest, {}), {}).stage, "done")
|
|
||||||
check("M14 kalk springes over når jorden er kalket", M.advance(M.with(harvestField, {plan = {lime = true}}), {lime = false}).stage, "cultivating")
|
|
||||||
check("M14 tom kæde -> færdig", M.start(M.with(M.newField(21), {plan = {soil = "none", sow = false, lime = false}}), "bare", {}).stage, "done")
|
|
||||||
|
|
||||||
-- M15: hjælpere under høst og multi-tool-baner
|
|
||||||
check("M15 aflæsningsvogn efter høsteren", M.wantsRig(harvestField, {primary = 1, helpers = 0}, 0).role, "unloader")
|
|
||||||
check("M15 hjælper-flag", M.wantsRig(harvestField, {primary = 1, helpers = 0}, 0).helper, true)
|
|
||||||
check("M15 alle vogne på plads -> intet", M.wantsRig(harvestField, {primary = 1, helpers = 1}, 0), nil)
|
|
||||||
local multi = M.with(harvestField, {plan = {rigs = 3, unloaders = 2}})
|
|
||||||
check("M15 multi: 3 høstere samlet", M.wantsRig(multi, {primary = 0, helpers = 0}, 0).count, 3)
|
|
||||||
check("M15 multi: 2 vogne", M.maxHelpers("harvesting", multi.plan), 2)
|
|
||||||
check("M15 ingen hjælper ved slåning", M.maxHelpers("mowing", multi.plan), 0)
|
|
||||||
check("M15 baner 2", table.concat(M.lanePositions(2), ","), "-1,1")
|
|
||||||
check("M15 baner 3", table.concat(M.lanePositions(3), ","), "-1,0,1")
|
|
||||||
check("M15 baner 4", table.concat(M.lanePositions(4), ","), "-2,-1,1,2")
|
|
||||||
check("M15 baner 5", table.concat(M.lanePositions(5), ","), "-2,-1,0,1,2")
|
|
||||||
check("M15 over 5 -> 5", #M.lanePositions(9), 5)
|
|
||||||
check("M15 maxRigs holdes 1-5", M.maxRigs("mowing", {rigs = 7}), 5)
|
|
||||||
|
|
||||||
-- M16: pleje under vækst
|
|
||||||
local growing = {kind = "harvest", growing = true, fert = 1, fertMax = 2, lime = 2, weed = 0}
|
|
||||||
local idleField = M.newField(30)
|
|
||||||
check("M16 gødning", M.careNeed(idleField, growing), "fertilizing")
|
|
||||||
check("M16 ukrudt før gødning", M.careNeed(idleField, {kind = "harvest", growing = true, fert = 1, fertMax = 2, lime = 2, weed = 3}), "weeding")
|
|
||||||
check("M16 græs ukrudtsbehandles ikke", M.careNeed(idleField, {kind = "mowable", growing = true, fert = 2, fertMax = 2, lime = 2, weed = 3}), nil)
|
|
||||||
check("M16 kalk", M.careNeed(idleField, {kind = "harvest", growing = true, fert = 2, fertMax = 2, lime = 0, weed = 0}), "careLiming")
|
|
||||||
check("M16 intet behov", M.careNeed(idleField, {kind = "harvest", growing = true, fert = 2, fertMax = 2, lime = 1, weed = 1}), nil)
|
|
||||||
check("M16 ikke voksende", M.careNeed(idleField, {kind = "harvest", growing = false, fert = 0, fertMax = 2}), nil)
|
|
||||||
check("M16 gødning slået fra", M.careNeed(M.with(idleField, {plan = {fertilize = false}}), growing), nil)
|
|
||||||
check("M16 arbejdende mark", M.careNeed(harvestField, growing), nil)
|
|
||||||
local cared = M.startCare(idleField, "fertilizing")
|
|
||||||
check("M16 pleje-trin", cared.stage, "fertilizing")
|
|
||||||
check("M16 rolle", M.roleFor(cared.stage), "fertilizer")
|
|
||||||
check("M16 kalk-pleje rolle", M.roleFor("careLiming"), "limer")
|
|
||||||
check("M16 pleje -> idle", M.advance(cared, {}).stage, "idle")
|
|
||||||
local twice = M.startCare(M.advance(cared, {}), "fertilizing")
|
|
||||||
check("M16 højst 2 gange", M.careNeed(M.advance(twice, {}), growing), nil)
|
|
||||||
check("M16 ny cyklus nulstiller", M.careNeed(M.advance(M.start(M.advance(twice, {}), "harvest", {}), {}), growing) ~= "x", true)
|
|
||||||
check("M16 start nulstiller pleje-tæller", next(M.start(twice, "harvest", {}).careRuns), nil)
|
|
||||||
check("M16 stop af pleje -> idle", M.stop(cared).stage, "idle")
|
|
||||||
check("M16 stop af høst -> klar", M.stop(harvestField).stage, "ready")
|
|
||||||
|
|
||||||
-- M17: såafgrøde og gem/indlæs af den udvidede plan
|
|
||||||
check("M17 så samme afgrøde", M.sowFruit(M.with(M.newField(40), {lastFruit = "WHEAT"})), "WHEAT")
|
|
||||||
check("M17 planens afgrøde vinder", M.sowFruit(M.with(M.newField(40), {lastFruit = "WHEAT", plan = {sowFruit = "CANOLA"}})), "CANOLA")
|
|
||||||
local full = M.with(M.startCare(M.newField(41), "weeding"), {kind = "harvest", lastFruit = "BARLEY",
|
|
||||||
plan = {rigs = 3, unloaders = 2, mulch = true, lime = false, soil = "plow", sow = true, sowFruit = "WHEAT", roll = true, fertilize = false, weed = false, product = "harvest"}})
|
|
||||||
local round = M.fromRecord(M.toRecord(full))
|
|
||||||
check("M17 trin", round.stage, "weeding")
|
|
||||||
check("M17 sæt", round.plan.rigs, 3)
|
|
||||||
check("M17 vogne", round.plan.unloaders, 2)
|
|
||||||
check("M17 stub", round.plan.mulch, true)
|
|
||||||
check("M17 kalk fra", round.plan.lime, false)
|
|
||||||
check("M17 jord", round.plan.soil, "plow")
|
|
||||||
check("M17 så", round.plan.sow, true)
|
|
||||||
check("M17 såafgrøde", round.plan.sowFruit, "WHEAT")
|
|
||||||
check("M17 tromle", round.plan.roll, true)
|
|
||||||
check("M17 gødning fra", round.plan.fertilize, false)
|
|
||||||
check("M17 ukrudt fra", round.plan.weed, false)
|
|
||||||
check("M17 art", round.kind, "harvest")
|
|
||||||
check("M17 sidste afgrøde", round.lastFruit, "BARLEY")
|
|
||||||
check("M17 pleje-tæller", round.careRuns.weeding, 1)
|
|
||||||
check("M17 produkt", round.plan.product, "harvest")
|
|
||||||
local legacy = M.fromRecord({id = 5, stage = "collecting", mode = "auto", product = "hay", rake = 1})
|
|
||||||
check("M17 etape 1-post: hø", legacy.plan.product, "hay")
|
|
||||||
check("M17 etape 1-post: standard kalk til", legacy.plan.lime, true)
|
|
||||||
check("M17 etape 1-post: 1 sæt", legacy.plan.rigs, 1)
|
|
||||||
|
|
||||||
-- M18: Start nu på en bar mark (klar-andel 0) må ikke nulstilles af vækst-reglen før den starter
|
|
||||||
local bare = M.with(M.forceReady(M.newField(70)), {startRequested = true})
|
|
||||||
check("M18 bar mark forbliver klar", M.onReadiness(bare, 0).stage, "ready")
|
|
||||||
check("M18 uden forespørgsel -> venter", M.onReadiness(M.forceReady(M.newField(71)), 0).stage, "idle")
|
|
||||||
check("M18 vejrfølsom: høst", M.isWeatherSensitive("harvesting", {}, "harvest"), true)
|
|
||||||
check("M18 vejrfølsom: kultivering ikke", M.isWeatherSensitive("cultivating", {}, "harvest"), false)
|
|
||||||
|
|
||||||
-- M19: mål-afgrøde — "vælg mark + afgrøde, glem den"
|
|
||||||
local target = M.with(M.newField(80), {plan = {targetFruit = "WHEAT"}})
|
|
||||||
local wheatNow = M.applyTarget(target.plan, "harvest", "BARLEY")
|
|
||||||
check("M19 mål -> auto", wheatNow.mode, "auto")
|
|
||||||
check("M19 høst byg -> så hvede", wheatNow.sow, true)
|
|
||||||
check("M19 såafgrøde = mål", wheatNow.sowFruit, "WHEAT")
|
|
||||||
check("M19 kæde: høst -> kultivér -> så", table.concat(M.workStages(wheatNow, "harvest", {}), ","), "harvesting,cultivating,sowing")
|
|
||||||
local grassToWheat = M.applyTarget(target.plan, "mowable", "GRASS")
|
|
||||||
check("M19 græs -> hvede: slå, saml op, så klargør og så", table.concat(M.workStages(grassToWheat, "mowable", {}), ","), "mowing,collecting,cultivating,sowing")
|
|
||||||
local grassTarget = M.applyTarget(M.with(M.newField(81), {plan = {targetFruit = "GRASS"}}).plan, "mowable", "GRASS")
|
|
||||||
check("M19 græs -> græs: kun slå og saml op", table.concat(M.workStages(grassTarget, "mowable", {}), ","), "mowing,collecting")
|
|
||||||
check("M19 græs -> græs: ingen såning", grassTarget.sow, false)
|
|
||||||
local bareTarget = M.applyTarget(target.plan, "bare", nil)
|
|
||||||
check("M19 bar mark -> klargør og så", table.concat(M.workStages(bareTarget, "bare", {plow = true}), ","), "plowing,sowing")
|
|
||||||
check("M19 uden mål: plan urørt", M.applyTarget(M.newField(82).plan, "harvest", "BARLEY").mode, "manual")
|
|
||||||
local started = M.start(target, "harvest", {}, "BARLEY")
|
|
||||||
check("M19 start anvender målet", started.plan.sowFruit, "WHEAT")
|
|
||||||
check("M19 start: høst først", started.stage, "harvesting")
|
|
||||||
check("M19 målet gemmes", M.fromRecord(M.toRecord(started)).plan.targetFruit, "WHEAT")
|
|
||||||
check("M19 klargøring efter slåning gemmes", M.fromRecord(M.toRecord(M.start(target, "mowable", {}, "GRASS"))).plan.prepAfterMow, true)
|
|
||||||
-- M20: skal en bar mark med mål starte? (altid — kun såningen venter på såtiden)
|
|
||||||
check("M20 bar + sås nu -> start", M.shouldStartBare(target, "bare", true), true)
|
|
||||||
check("M20 bar + ikke såtid -> klargør alligevel (kun såningen venter)", M.shouldStartBare(target, "bare"), true)
|
|
||||||
check("M20 uden mål -> ingen auto-start", M.shouldStartBare(M.newField(83), "bare", true), false)
|
|
||||||
check("M20 voksende mark -> nej", M.shouldStartBare(target, "harvest", true), false)
|
|
||||||
check("M20 arbejdende mark -> nej", M.shouldStartBare(started, "bare", true), false)
|
|
||||||
check("M20 færdig (lige sået) -> nej, ingen dobbelt-såning", M.shouldStartBare(M.with(target, {stage = "done"}), "bare", true), false)
|
|
||||||
|
|
||||||
-- M21: vælg mål-afgrøde fra menuen: auto, og en færdig mark nulstilles så den kan starte
|
|
||||||
local chosen = M.setTarget(M.with(M.newField(84), {stage = "done"}), "CANOLA")
|
|
||||||
check("M21 mål sat", chosen.plan.targetFruit, "CANOLA"); check("M21 auto", chosen.plan.mode, "auto")
|
|
||||||
check("M21 færdig -> venter", chosen.stage, "idle")
|
|
||||||
check("M21 arbejdende mark beholder trin", M.setTarget(started, "CANOLA").stage, "harvesting")
|
|
||||||
local cleared = M.setTarget(chosen, "")
|
|
||||||
check("M21 mål fjernet", cleared.plan.targetFruit, ""); check("M21 tilbage til manuel", cleared.plan.mode, "manual")
|
|
||||||
|
|
||||||
-- M22: halm efter høst — hakkes (standard), samles op, eller presses til baller og samles
|
|
||||||
local strawBase = {soil = "none", sow = false, lime = false}
|
|
||||||
check("M22 hakkes: kun høst", table.concat(M.workStages(strawBase, "harvest", {}), ","), "harvesting")
|
|
||||||
check("M22 samles op", table.concat(M.workStages(M.with(M.newField(1), {plan = {soil = "none", lime = false, straw = "collect"}}).plan, "harvest", {}), ","), "harvesting,strawCollecting")
|
|
||||||
check("M22 baller", table.concat(M.workStages(M.with(M.newField(1), {plan = {soil = "none", lime = false, straw = "bale"}}).plan, "harvest", {}), ","), "harvesting,baling,baleCollecting")
|
|
||||||
local baleChain = M.start(M.with(M.newField(2), {plan = {straw = "bale", soil = "cultivate", lime = false}}), "harvest", {})
|
|
||||||
check("M22 høst -> presses", M.advance(baleChain, {}).stage, "baling")
|
|
||||||
check("M22 presses -> baller samles", M.advance(M.advance(baleChain, {}), {}).stage, "baleCollecting")
|
|
||||||
check("M22 -> kultivér", M.advance(M.advance(M.advance(baleChain, {}), {}), {}).stage, "cultivating")
|
|
||||||
check("M22 roller", M.roleFor("baling") .. "," .. M.roleFor("baleCollecting") .. "," .. M.roleFor("strawCollecting"), "baler,baleCollector,collector")
|
|
||||||
check("M22 halm-læssevogn følger høsterens kursus", M.courseSource(M.with(M.newField(3), {stage = "strawCollecting"}), "collector"), "harvester")
|
|
||||||
check("M22 presser følger høsterens kursus", M.courseSource(M.with(M.newField(3), {stage = "baling"}), "baler"), "harvester")
|
|
||||||
check("M22 græs-læssevogn følger stadig slåkurset", M.courseSource(M.with(M.newField(3), {stage = "collecting"}), "collector"), "mower")
|
|
||||||
check("M22 halm-læssevogn uden breddekrav", M.requiredWidth(M.with(M.newField(3), {stage = "strawCollecting", mowWidth = 40}), "collector"), nil)
|
|
||||||
check("M22 presning vejrfølsom", M.isWeatherSensitive("baling", {}, "harvest"), true)
|
|
||||||
check("M22 ballesamling ikke vejrfølsom", M.isWeatherSensitive("baleCollecting", {}, "harvest"), false)
|
|
||||||
check("M22 gemmes", M.fromRecord(M.toRecord(M.with(M.newField(4), {plan = {straw = "bale"}}))).plan.straw, "bale")
|
|
||||||
check("M22 ukendt -> hakkes", M.fromRecord({id = 5, straw = "x"}).plan.straw, "chop")
|
|
||||||
|
|
||||||
-- M23: ingen ny tabel når vækst-reglen ikke ændrer noget (undgår affald hvert sekund -> GC-hak)
|
|
||||||
local calm = M.newField(95)
|
|
||||||
check("M23 uændret -> samme tabel", M.onReadiness(calm, 0.2) == calm, true)
|
|
||||||
check("M23 ændret -> ny tabel", M.onReadiness(calm, 0.95) ~= calm, true)
|
|
||||||
-- M24: passive marker kan springes over (manuel, uden mål, venter, ingen forespørgsel)
|
|
||||||
check("M24 manuel venter = passiv", M.isPassive(calm), true)
|
|
||||||
check("M24 auto = ikke passiv", M.isPassive(M.with(calm, {plan = {mode = "auto"}})), false)
|
|
||||||
check("M24 mål = ikke passiv", M.isPassive(M.setTarget(calm, "WHEAT")), false)
|
|
||||||
check("M24 klar = ikke passiv", M.isPassive(M.with(calm, {stage = "ready"})), false)
|
|
||||||
check("M24 i gang = ikke passiv", M.isPassive(started), false)
|
|
||||||
check("M24 Start nu = ikke passiv", M.isPassive(M.with(calm, {startRequested = true})), false)
|
|
||||||
|
|
||||||
-- Stop holder en bar mark (ellers starter klargøringen straks forfra); Start eller ny mål-afgrøde slipper
|
|
||||||
local stoppedBare = M.stop(M.with(target, {stage = "cultivating"}))
|
|
||||||
check("M21 stoppet bar mark starter ikke selv", M.shouldStartBare(stoppedBare, "bare"), false)
|
|
||||||
check("M21 Start slipper", M.shouldStartBare(M.with(stoppedBare, {startRequested = true}), "bare"), true)
|
|
||||||
check("M21 ny mål-afgrøde slipper", M.shouldStartBare(M.setTarget(stoppedBare, "BARLEY"), "bare"), true)
|
|
||||||
check("M21 start nulstiller hold (næste sæson starter selv)", M.start(stoppedBare, "bare", {}, "WHEAT").held, nil)
|
|
||||||
-- gemt produkt (silage/hø) gælder kun slåafgrøder: en vinmark der engang fejlagtigt blev "slåbar" (F22, 29/9)
|
|
||||||
check("produkt: gemt silage på høstmark -> høst", ADFieldModel.product({product = "silage"}, "harvest"), "harvest")
|
|
||||||
check("produkt: gemt hø på slåmark -> hø", ADFieldModel.product({product = "hay"}, "mowable"), "hay")
|
|
||||||
check("produkt: auto på slåmark -> silage", ADFieldModel.product({product = "auto"}, "mowable"), "silage")
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adFieldSiloConfig.lua (fra repo-roden)
|
|
||||||
dofile("FS25_ADSmartPickup/adFieldSiloConfig.lua")
|
|
||||||
local C = ADFieldSiloConfig
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local cfg = C.new()
|
|
||||||
check("standard fra", cfg.enabled, false)
|
|
||||||
check("standard x=2", cfg.maxVehicles, 2)
|
|
||||||
check("standard tærskel 10000", cfg.minLiters, 10000)
|
|
||||||
check("x under 1 -> 1", C.with(cfg, {maxVehicles = 0}).maxVehicles, 1)
|
|
||||||
check("x over 10 -> 10", C.with(cfg, {maxVehicles = 99}).maxVehicles, 10)
|
|
||||||
check("tærskel snappes til nærmeste valg", C.with(cfg, {minLiters = 12000}).minLiters, 10000)
|
|
||||||
check("tærskel 40000 -> 50000", C.with(cfg, {minLiters = 40000}).minLiters, 50000)
|
|
||||||
check("with muterer ikke", cfg.maxVehicles, 2)
|
|
||||||
local on = C.toggleStore(cfg, "silo1")
|
|
||||||
check("toggle til", C.hasStore(on, "silo1"), true)
|
|
||||||
check("toggle muterer ikke", C.hasStore(cfg, "silo1"), false)
|
|
||||||
check("toggle fra", C.hasStore(C.toggleStore(on, "silo1"), "silo1"), false)
|
|
||||||
local full = C.with(C.toggleStore(C.toggleStore(cfg, "a"), "b"), {enabled = true, maxVehicles = 4, minLiters = 25000})
|
|
||||||
local back = C.fromRecord(C.toRecord(full))
|
|
||||||
check("record: enabled", back.enabled, true)
|
|
||||||
check("record: lagre", table.concat(back.storeIds, " "), "a b")
|
|
||||||
check("record: x", back.maxVehicles, 4)
|
|
||||||
check("record: tærskel", back.minLiters, 25000)
|
|
||||||
check("tom record -> standard", C.fromRecord(nil).maxVehicles, 2)
|
|
||||||
check("record uden tal -> standard tærskel", C.fromRecord({enabled = 1}).minLiters, 10000)
|
|
||||||
-- kør til produktion + foderreserve
|
|
||||||
check("standard: ikke til produktion", cfg.toProduction, false)
|
|
||||||
check("standard foderreserve 7 døgn", cfg.reserveDays, 7)
|
|
||||||
check("reserve snappes (10 -> 7)", C.with(cfg, {reserveDays = 10}).reserveDays, 7)
|
|
||||||
check("reserve 40 -> 30", C.with(cfg, {reserveDays = 40}).reserveDays, 30)
|
|
||||||
local prod = C.with(C.toggleStore(cfg, "a"), {toProduction = true, reserveDays = 14})
|
|
||||||
check("with bevarer lagre", C.hasStore(prod, "a"), true)
|
|
||||||
check("with muterer ikke (produktion)", cfg.toProduction, false)
|
|
||||||
local prodBack = C.fromRecord(C.toRecord(prod))
|
|
||||||
check("record: til produktion", prodBack.toProduction, true)
|
|
||||||
check("record: foderreserve", prodBack.reserveDays, 14)
|
|
||||||
check("gammel record uden felter -> standard", C.fromRecord({enabled = 1}).reserveDays, 7)
|
|
||||||
check("toggleStore bevarer produktion", C.toggleStore(prod, "b").toProduction, true)
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adFieldSiloPlanner.lua (fra repo-roden)
|
|
||||||
dofile("FS25_ADSmartPickup/adFieldSiloPlanner.lua")
|
|
||||||
local P = ADFieldSiloPlanner
|
|
||||||
local WHEAT, CANOLA = 1, 2
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local function silo(id, x, stock, markerId) return {id = id, name = id, markerId = markerId == nil and (id .. "m") or markerId or nil, x = x, z = 0, stock = stock} end
|
|
||||||
local function store(id, x, free) return {id = id, markerId = id .. "m", x = x, z = 0, free = free} end
|
|
||||||
local function vehicle(id, x, capacity, carries, cargo) return {id = id, x = x, z = 0, capacity = capacity, carries = carries or {[WHEAT] = true, [CANOLA] = true}, cargo = cargo} end
|
|
||||||
local function base(over)
|
|
||||||
local input = {silos = {}, stores = {store("farm", 0, {[WHEAT] = 1000000, [CANOLA] = 1000000})}, loans = {}, vehicles = {}, paused = {}, maxVehicles = 2, minLiters = 10000}
|
|
||||||
for k, v in pairs(over or {}) do input[k] = v end
|
|
||||||
return input
|
|
||||||
end
|
|
||||||
|
|
||||||
-- kvalificering
|
|
||||||
local r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 9999})}, vehicles = {vehicle("v1", 400, 50000)}}))
|
|
||||||
check("under tærskel -> ingen afsendelse", #r.dispatch, 0)
|
|
||||||
check("under tærskel -> ingen status", #r.status, 0)
|
|
||||||
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 30000}, false)}, vehicles = {vehicle("v1", 400, 50000)}}))
|
|
||||||
check("uden markør -> noMarker", r.status[1].state, "noMarker")
|
|
||||||
check("uden markør -> ingen afsendelse", #r.dispatch, 0)
|
|
||||||
r = P.plan(base({stores = {store("farm", 0, {[CANOLA] = 1000000})}, silos = {silo("A", 500, {[WHEAT] = 30000})}, vehicles = {vehicle("v1", 400, 50000)}}))
|
|
||||||
check("ingen gård tager varen -> noFarmStore", r.status[1].state, "noFarmStore")
|
|
||||||
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 30000})}, vehicles = {vehicle("v1", 400, 50000)}, paused = {[P.key("A", WHEAT)] = true}}))
|
|
||||||
check("pause -> paused, ingen afsendelse", r.status[1].state .. #r.dispatch, "paused0")
|
|
||||||
|
|
||||||
-- afsendelse, fuldeste først, x-loft
|
|
||||||
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 20000}), silo("B", 900, {[WHEAT] = 80000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 950, 50000), vehicle("v3", 1000, 50000)}}))
|
|
||||||
check("x=2 -> to afsendt", #r.dispatch, 2)
|
|
||||||
check("fuldeste silo først", r.dispatch[1].siloId, "B")
|
|
||||||
check("nærmeste vogn til B", r.dispatch[1].vehicleId, "v2")
|
|
||||||
check("B's rest (30000) får vogn nr. 2", r.dispatch[2].siloId, "B")
|
|
||||||
check("lager-markør med", r.dispatch[1].storeMarkerId, "farmm")
|
|
||||||
check("liter = vognens kapacitet", r.dispatch[1].liters, 50000)
|
|
||||||
check("anden tur = resten", r.dispatch[2].liters, 30000)
|
|
||||||
|
|
||||||
-- aktive lån tæller mod x og mod siloens rest
|
|
||||||
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 60000})}, loans = {{siloId = "A", fillType = WHEAT, liters = 50000}}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000)}}))
|
|
||||||
check("lån på vej -> rest 10000 -> én ny vogn", #r.dispatch, 1)
|
|
||||||
check("status viser på vej", r.status[1].onWay, 50000)
|
|
||||||
r = P.plan(base({maxVehicles = 1, silos = {silo("A", 500, {[WHEAT] = 90000})}, loans = {{siloId = "Z", fillType = WHEAT, liters = 1}}, vehicles = {vehicle("v1", 400, 50000)}}))
|
|
||||||
check("x=1 med ét lån ude -> ingen", #r.dispatch, 0)
|
|
||||||
check("frie pladser 0", r.freeSlots, 0)
|
|
||||||
|
|
||||||
-- flere varer i én silo = hver sin opgave
|
|
||||||
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 40000, [CANOLA] = 20000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 410, 50000)}}))
|
|
||||||
check("to varer -> to afsendelser", #r.dispatch, 2)
|
|
||||||
check("fuldeste vare først", r.dispatch[1].fillType, WHEAT)
|
|
||||||
|
|
||||||
-- vogne der ikke kan
|
|
||||||
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 40000})}, vehicles = {vehicle("v1", 400, 50000, {[CANOLA] = true}), vehicle("v2", 2000, 50000)}}))
|
|
||||||
check("vogn der ikke kan bære varen springes over", r.dispatch[1].vehicleId, "v2")
|
|
||||||
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 40000})}, vehicles = {vehicle("v1", 400, 50000, nil, CANOLA), vehicle("v2", 2000, 50000)}}))
|
|
||||||
check("vogn med anden vare på springes over", r.dispatch[1].vehicleId, "v2")
|
|
||||||
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 40000})}, vehicles = {vehicle("v1", 400, 0)}}))
|
|
||||||
check("vogn uden kapacitet -> ingen", #r.dispatch, 0)
|
|
||||||
|
|
||||||
-- lagervalg
|
|
||||||
r = P.plan(base({stores = {store("near", 400, {[WHEAT] = 20000}), store("far", 5000, {[WHEAT] = 900000})}, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000)}}))
|
|
||||||
check("nærmeste lager uden plads til læsset -> det med plads", r.dispatch[1].storeId, "far")
|
|
||||||
r = P.plan(base({stores = {store("small", 400, {[WHEAT] = 30000}), store("tiny", 300, {[WHEAT] = 15000})}, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000)}}))
|
|
||||||
check("intet har plads til hele læsset -> mest plads", r.dispatch[1].storeId, "small")
|
|
||||||
check("reserveret plads: vogn 2 til det andet lager", r.dispatch[2].storeId, "tiny")
|
|
||||||
r = P.plan(base({stores = {store("full", 400, {[WHEAT] = 5000})}, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)}}))
|
|
||||||
check("lager med under tærsklen -> noFarmStore", r.status[1].state, "noFarmStore")
|
|
||||||
|
|
||||||
-- input muteres ikke
|
|
||||||
local input = base({silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)}})
|
|
||||||
P.plan(input)
|
|
||||||
check("input.stores urørt", input.stores[1].free[WHEAT], 1000000)
|
|
||||||
|
|
||||||
|
|
||||||
-- I1: lån på vej til et lager reserverer pladsen på tværs af planlægningsrunder
|
|
||||||
r = P.plan(base({stores = {store("s", 400, {[WHEAT] = 30000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
|
|
||||||
loans = {{siloId = "A", fillType = WHEAT, liters = 30000, storeId = "s"}}, vehicles = {vehicle("v1", 400, 50000)}}))
|
|
||||||
check("I1 lager fyldt af lån på vej -> ingen ny vogn dertil", #r.dispatch, 0)
|
|
||||||
r = P.plan(base({stores = {store("s", 400, {[WHEAT] = 30000}), store("t", 900, {[WHEAT] = 500000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
|
|
||||||
loans = {{siloId = "A", fillType = WHEAT, liters = 30000, storeId = "s"}}, vehicles = {vehicle("v1", 400, 50000)}}))
|
|
||||||
check("I1 næste vogn til det andet lager", r.dispatch[1] ~= nil and r.dispatch[1].storeId, "t")
|
|
||||||
|
|
||||||
-- kør til produktion: foder først, overskud til fabrik
|
|
||||||
local function factory(id, x, free) return {id = id, kind = "production", markerId = id .. "m", x = x, z = 0, free = free} end
|
|
||||||
local farmAndFactory = {store("farm", 5000, {[WHEAT] = 1000000}), factory("mill", 100, {[WHEAT] = 1000000})}
|
|
||||||
r = P.plan(base({stores = farmAndFactory, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)},
|
|
||||||
feedReserve = {[WHEAT] = 100000}, farmStock = {[WHEAT] = 20000}}))
|
|
||||||
check("P1 reserve ikke dækket -> gårdlager selv om fabrik er nærmere", r.dispatch[1].storeId, "farm")
|
|
||||||
r = P.plan(base({stores = farmAndFactory, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)},
|
|
||||||
feedReserve = {[WHEAT] = 100000}, farmStock = {[WHEAT] = 150000}}))
|
|
||||||
check("P2 reserve dækket -> fabrik", r.dispatch[1].storeId, "mill")
|
|
||||||
r = P.plan(base({stores = farmAndFactory, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)}}))
|
|
||||||
check("P3 ikke foder (ingen reserve) -> fabrik", r.dispatch[1].storeId, "mill")
|
|
||||||
r = P.plan(base({stores = farmAndFactory, silos = {silo("A", 500, {[WHEAT] = 100000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000)},
|
|
||||||
feedReserve = {[WHEAT] = 100000}, farmStock = {[WHEAT] = 70000}}))
|
|
||||||
check("P4 første læs dækker resten af reserven -> gård", r.dispatch[1].storeId, "farm")
|
|
||||||
check("P4 andet læs er overskud -> fabrik", r.dispatch[2].storeId, "mill")
|
|
||||||
r = P.plan(base({stores = farmAndFactory, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)},
|
|
||||||
feedReserve = {[WHEAT] = 100000}, farmStock = {[WHEAT] = 60000}, loans = {{siloId = "B", fillType = WHEAT, liters = 50000, storeId = "farm"}}}))
|
|
||||||
check("P5 læs på vej til gården tæller med i reserven -> fabrik", r.dispatch[1] ~= nil and r.dispatch[1].storeId, "mill")
|
|
||||||
r = P.plan(base({stores = {store("farm", 5000, {[WHEAT] = 1000000}), factory("mill", 100, {[CANOLA] = 1000000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
|
|
||||||
vehicles = {vehicle("v1", 400, 50000)}, farmStock = {[WHEAT] = 999999}}))
|
|
||||||
check("P6 ingen fabrik tager varen -> gårdlager", r.dispatch[1].storeId, "farm")
|
|
||||||
r = P.plan(base({stores = {store("farm", 5000, {[CANOLA] = 1000000}), factory("mill", 100, {[WHEAT] = 1000000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
|
|
||||||
vehicles = {vehicle("v1", 400, 50000)}, feedReserve = {[WHEAT] = 100000}}))
|
|
||||||
check("P7 reserve mangler men ingen gård tager varen -> fabrik", r.dispatch[1].storeId, "mill")
|
|
||||||
check("P7 status ok (ikke noFarmStore)", r.status[1].state, "ok")
|
|
||||||
check("P7 status viser destination", r.status[1].toProduction, true)
|
|
||||||
|
|
||||||
-- fabrik kun når hele læsset kan være der (in-game 28/9: vogn med 252.000 l ventede timevis ved en fuld fabrik)
|
|
||||||
r = P.plan(base({stores = {store("farm", 5000, {[WHEAT] = 1000000}), factory("mill", 100, {[WHEAT] = 30000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
|
|
||||||
vehicles = {vehicle("v1", 400, 50000)}}))
|
|
||||||
check("P8 fabrik uden plads til hele læsset -> gårdlager", r.dispatch[1].storeId, "farm")
|
|
||||||
r = P.plan(base({stores = {store("farm", 5000, {[CANOLA] = 1000000}), factory("mill", 100, {[WHEAT] = 30000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
|
|
||||||
vehicles = {vehicle("v1", 400, 50000)}}))
|
|
||||||
check("P9 kun fabrik uden plads til hele læsset -> ingen afsendelse", #r.dispatch, 0)
|
|
||||||
|
|
||||||
-- høsten først: tømmevogne holdes tilbage til høstere der arbejder
|
|
||||||
local vs = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000, {[CANOLA] = true}), vehicle("v3", 400, 50000)}
|
|
||||||
local left = P.withoutHarvestReserve(vs, {{fillType = WHEAT, count = 1}})
|
|
||||||
check("R1 én hvede-vogn holdt tilbage", #left, 2)
|
|
||||||
check("R1 raps-vognen er fri", left[1].id == "v2" or left[2].id == "v2", true)
|
|
||||||
left = P.withoutHarvestReserve(vs, {{fillType = WHEAT, count = 5}})
|
|
||||||
check("R2 alle hvede-vogne holdt tilbage", #left, 1)
|
|
||||||
check("R3 intet behov -> alle fri", #P.withoutHarvestReserve(vs, {}), 3)
|
|
||||||
check("R4 input muteres ikke", #vs, 3)
|
|
||||||
|
|
||||||
-- Tom silo: lånt vogn med last står ved en tom marksilo -> resten køres videre (AD venter ellers til fuld)
|
|
||||||
-- (in-game 28/9: Tømmevogn 2 og 4 stod ved tom F17 Unload med kartofler)
|
|
||||||
local restState = {atSilo = true, stillMs = 45000, siloStock = 0, cargo = 12000}
|
|
||||||
check("RS1 tom silo med last -> kør videre", P.restDue(restState), true)
|
|
||||||
check("RS2 siloen har stadig varer -> vent", P.restDue({atSilo = true, stillMs = 45000, siloStock = 30000, cargo = 12000}), false)
|
|
||||||
check("RS3 ikke ved siloen -> nej", P.restDue({atSilo = false, stillMs = 45000, siloStock = 0, cargo = 12000}), false)
|
|
||||||
check("RS4 lige ankommet -> vent", P.restDue({atSilo = true, stillMs = 5000, siloStock = 0, cargo = 12000}), false)
|
|
||||||
check("RS5 tom vogn -> nej", P.restDue({atSilo = true, stillMs = 45000, siloStock = 0, cargo = 0}), false)
|
|
||||||
local restStores = {
|
|
||||||
{id = "fab", kind = "production", markerId = 1, x = 10, z = 0, free = {[10] = 5000}},
|
|
||||||
{id = "gaard", kind = "silo", markerId = 2, x = 500, z = 0, free = {[10] = 900000}},
|
|
||||||
}
|
|
||||||
check("RS6 fabrik uden plads til hele resten -> gårdsilo", P.restStore(restStores, {x = 0, z = 0}, 10, 12000).id, "gaard")
|
|
||||||
check("RS7 fabrik med plads -> fabrik", P.restStore(restStores, {x = 0, z = 0}, 10, 4000).id, "fab")
|
|
||||||
check("RS8 intet tager varen", P.restStore(restStores, {x = 0, z = 0}, 99, 4000), nil)
|
|
||||||
-- Fuld fabrik: lånt vogn med last holder ved et lager uden plads -> resten til et andet lager (gård)
|
|
||||||
check("SF1 fuld fabrik -> videre", P.storeFullDue({atStore = true, stillMs = 70000, cargo = 12000, storeFree = 0}), true)
|
|
||||||
check("SF2 fabrikken tager stadig imod -> vent", P.storeFullDue({atStore = true, stillMs = 70000, cargo = 12000, storeFree = 8000}), false)
|
|
||||||
check("SF3 lige ankommet -> vent", P.storeFullDue({atStore = true, stillMs = 20000, cargo = 12000, storeFree = 0}), false)
|
|
||||||
check("SF4 ikke ved lageret -> nej", P.storeFullDue({atStore = false, stillMs = 70000, cargo = 12000, storeFree = 0}), false)
|
|
||||||
local fullStores = {
|
|
||||||
{id = "fab", kind = "production", markerId = 1, x = 10, z = 0, free = {[10] = 0}},
|
|
||||||
{id = "gaard", kind = "silo", markerId = 2, x = 500, z = 0, free = {[10] = 900000}},
|
|
||||||
}
|
|
||||||
check("SF5 andet lager end det fulde", P.restStore(fullStores, {x = 0, z = 0}, 10, 12000, "fab").id, "gaard")
|
|
||||||
check("SF6 kun det fulde tager varen -> intet", P.restStore({fullStores[1]}, {x = 0, z = 0}, 10, 12000, "fab"), nil)
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adFieldSilos.lua (fra repo-roden)
|
|
||||||
dofile("FS25_ADSmartPickup/adFieldSiloPlanner.lua")
|
|
||||||
dofile("FS25_ADSmartPickup/adFieldSiloConfig.lua")
|
|
||||||
dofile("FS25_ADSmartPickup/adFieldSilos.lua")
|
|
||||||
local S = ADFieldSilos
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local loan = {startedMs = 0, lastProgressMs = 0, lastFill = 0}
|
|
||||||
check("kører, tidligt -> behold", S.releaseReason(loan, {adActive = true, controlled = false, fill = 0, moving = true}, 5000), nil)
|
|
||||||
check("AD slukket efter 40 s -> done", S.releaseReason(loan, {adActive = false, controlled = false, fill = 0}, 40000), "done")
|
|
||||||
check("spiller i vognen -> player", S.releaseReason(loan, {adActive = true, controlled = true, fill = 0}, 5000), "player")
|
|
||||||
check("står stille uden fyld i 120 s -> stuck", S.releaseReason(loan, {adActive = true, controlled = false, fill = 0, moving = false}, 120001), "stuck")
|
|
||||||
check("kører stadig efter 120 s -> behold", S.releaseReason(loan, {adActive = true, controlled = false, fill = 0, moving = true}, 120001), nil)
|
|
||||||
local loading = {startedMs = 0, lastProgressMs = 110000, lastFill = 0}
|
|
||||||
check("fyldning steg for nylig -> behold", S.releaseReason(loading, {adActive = true, controlled = false, fill = 20000, moving = false}, 125000), nil)
|
|
||||||
|
|
||||||
-- I2/I3: vagten gælder kun før turen er kommet i gang; AD der slukker straks uden fremdrift = fejl
|
|
||||||
local going = {startedMs = 0, lastFill = 30000, hasProgressed = true}
|
|
||||||
check("I2 i gang, står i kø 500 s -> behold", S.releaseReason(going, {adActive = true, controlled = false, fill = 30000, moving = false}, 500000), nil)
|
|
||||||
check("I2 aflæsning (fyld falder) er fremdrift", S.releaseReason({startedMs = 0, lastFill = 30000}, {adActive = true, controlled = false, fill = 20000, moving = false}, 500000), nil)
|
|
||||||
check("I3 AD slukket efter 10 s uden fremdrift -> failed", S.releaseReason({startedMs = 0, lastFill = 0}, {adActive = false, controlled = false, fill = 0}, 10000), "failed")
|
|
||||||
check("I3 AD slukket efter 10 s med fremdrift -> done", S.releaseReason({startedMs = 0, lastFill = 0, hasProgressed = true}, {adActive = false, controlled = false, fill = 0}, 10000), "done")
|
|
||||||
|
|
||||||
-- Røgtest af spil-laget med mocks: planNow låner nærmeste tømmevogn, isLoaned = optaget,
|
|
||||||
-- frakobling (setConfig enabled=false) stopper AD og frigiver (Review Focus).
|
|
||||||
do
|
|
||||||
local WHEAT = 1
|
|
||||||
Logging = {info = function() end, warning = function() end}
|
|
||||||
g_currentMission = {getFarmId = function() return 1 end}
|
|
||||||
g_fillTypeManager = {getFillTypeByIndex = function() return {title = "Hvede"} end}
|
|
||||||
function getWorldTranslation(node) return node.x, 0, node.z end
|
|
||||||
ADRuns = {vehicleLabel = function(v) return v.name end}
|
|
||||||
local function storage(levels, free) return {fillLevels = levels, fillTypes = {[WHEAT] = true}, getFreeCapacity = function() return free end} end
|
|
||||||
local fieldSilo = {id = "fs", name = "Marksilo 1", kind = "silo", placeable = {rootNode = {x = 1000, z = 0}, spec_silo = {storages = {storage({[WHEAT] = 60000}, 0)}}}}
|
|
||||||
local farm = {id = "farm", name = "Gårdsilo", kind = "silo", placeable = {rootNode = {x = 0, z = 0}, spec_silo = {storages = {storage({}, 500000)}}}}
|
|
||||||
ADBuildings = {list = function() return {fieldSilo, farm} end,
|
|
||||||
getMarkers = function(b, role) return {{id = b.id .. "-" .. role}} end}
|
|
||||||
ADFields = {list = function() return {{id = 1}} end, fieldSilos = function() return {fieldSilo, farm} end}
|
|
||||||
local started, settingsSet, noPark, noRoute, residue = {}, {}, {}, {}, 0
|
|
||||||
ADRunsController = {store = {runs = {}}, setSetting = function(_, v, name, value) settingsSet[v] = settingsSet[v] or {}; settingsSet[v][name] = value end}
|
|
||||||
ADUnloadWait = {hasRouteTo = function(_, _, id) return not noRoute[id] end}
|
|
||||||
local function makeVehicle(name, x)
|
|
||||||
local module = {active = false}
|
|
||||||
function module:setMode() end
|
|
||||||
function module:setFirstMarkerByWayPointId(id) self.first = id end
|
|
||||||
function module:setSecondMarkerByWayPointId(id) self.second = id end
|
|
||||||
function module:setFillType() end
|
|
||||||
function module:setLoopsDone() end
|
|
||||||
function module:raiseDirtyFlag() end
|
|
||||||
function module:isActive() return self.active end
|
|
||||||
return {name = name, x = x, lastSpeedReal = 0, ad = {stateModule = module},
|
|
||||||
stopAutoDrive = function(self) self.ad.stateModule.active = false; self.stopped = true end}
|
|
||||||
end
|
|
||||||
local near, far = makeVehicle("Nær", 900), makeVehicle("Fjern", 5000)
|
|
||||||
local adEnv = {AutoDrive = {MODE_PICKUPANDDELIVER = 2, RT_NONE = 0,
|
|
||||||
getAllDischargeableUnits = function() return {{fillUnitIndex = 1, object = {
|
|
||||||
getFillUnitCapacity = function() return 40000 end,
|
|
||||||
getFillUnitSupportedFillTypes = function() return {[WHEAT] = true} end,
|
|
||||||
getFillUnitFillLevel = function() return residue end, getFillUnitFillType = function() return WHEAT end}}} end,
|
|
||||||
getAllUnits = function() return {} end, getAllFillLevels = function() return 0 end}}
|
|
||||||
ADFieldWork = {
|
|
||||||
getRigs = function()
|
|
||||||
local rigs = {}
|
|
||||||
-- som spillets vehicleSystem: slettede køretøjer er ikke med
|
|
||||||
if not near.isDeleted then table.insert(rigs, {id = "n", vehicle = near, role = "unloader", enabled = true, busy = S.isLoaned(near), controlled = false, x = 900, z = 0}) end
|
|
||||||
if not far.isDeleted then table.insert(rigs, {id = "f", vehicle = far, role = "unloader", enabled = true, busy = S.isLoaned(far), controlled = false, x = 5000, z = 0}) end
|
|
||||||
table.insert(rigs, {id = "h", vehicle = far, role = "harvester", enabled = true, busy = false, controlled = false, x = 1000, z = 0})
|
|
||||||
return rigs
|
|
||||||
end,
|
|
||||||
enableParkAtJobFinished = function() end,
|
|
||||||
parkMarkerId = function(_, v) if noPark[v] then return nil end return 777 end,
|
|
||||||
startAd = function(_, v, _, first, second, fillType) v.ad.stateModule.active = true; table.insert(started, {v = v, first = first, second = second, fillType = fillType}) end,
|
|
||||||
}
|
|
||||||
S.setConfig(ADFieldSiloConfig.with(ADFieldSiloConfig.toggleStore(ADFieldSiloConfig.new(), "farm"), {enabled = true, maxVehicles = 1}))
|
|
||||||
local plan = S.planNow(adEnv)
|
|
||||||
check("RØG én afsendelse (x=1)", #started, 1)
|
|
||||||
check("RØG nærmeste tømmevogn", started[1] ~= nil and started[1].v.name, "Nær")
|
|
||||||
check("RØG fra marksilo-markør", started[1] ~= nil and started[1].first, "fs-load")
|
|
||||||
check("RØG til gårdlager-markør", started[1] ~= nil and started[1].second, "farm-unload")
|
|
||||||
check("RØG varen med", started[1] ~= nil and started[1].fillType, WHEAT)
|
|
||||||
check("RØG gårdlager er ikke marksilo", #plan.status, 1)
|
|
||||||
check("RØG vognen er lånt", S.isLoaned(near), true)
|
|
||||||
check("RØG x nået -> ingen flere", #S.planNow(adEnv).dispatch, 0)
|
|
||||||
check("I4 rotation slået fra på lånt vogn", settingsSet[near] ~= nil and settingsSet[near].rotateTargets, 0)
|
|
||||||
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = false}))
|
|
||||||
check("RØG frakobling stopper AD", near.stopped, true)
|
|
||||||
check("RØG frakobling frigiver", S.isLoaned(near), false)
|
|
||||||
|
|
||||||
-- I5: vogn uden parkeringspunkt lånes ikke (ville blive holdende på lagerets markør)
|
|
||||||
started = {}
|
|
||||||
noPark[near] = true
|
|
||||||
near.stopped = nil
|
|
||||||
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = true}))
|
|
||||||
S.planNow(adEnv)
|
|
||||||
check("I5 uden parkering -> fjern vogn med parkering", started[1] ~= nil and started[1].v.name, "Fjern")
|
|
||||||
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = false}))
|
|
||||||
noPark[near] = nil
|
|
||||||
|
|
||||||
-- I3: ingen rute til siloen -> ingen afsendelse
|
|
||||||
started = {}
|
|
||||||
noRoute["fs-load"] = true
|
|
||||||
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = true}))
|
|
||||||
S.planNow(adEnv)
|
|
||||||
check("I3 ingen rute -> ingen afsendelse", #started, 0)
|
|
||||||
noRoute["fs-load"] = nil
|
|
||||||
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = false}))
|
|
||||||
|
|
||||||
-- I6: silo der er kilde/mål i et flow er ikke marksilo
|
|
||||||
ADRunsController.store.runs = {{kind = "supply", sourceBuildingIds = {}, targetBuildingIds = {"fs"}, storeBuildingIds = {}}}
|
|
||||||
local silos = S.collectBuildings(adEnv)
|
|
||||||
check("I6 silo i et flow tømmes ikke", #silos, 0)
|
|
||||||
ADRunsController.store.runs = {}
|
|
||||||
|
|
||||||
-- in-game 27/9: marksilo-flowet flyttede sukkerroer fra en Farma-silo til nabo-Farma-siloen (samme lagerklynge)
|
|
||||||
local neighbour = {id = "nb", name = "Nabo-Farma", kind = "silo", placeable = {rootNode = {x = 40, z = 0}, spec_silo = {storages = {storage({[WHEAT] = 60000}, 0)}}}}
|
|
||||||
ADFields.fieldSilos = function() return {fieldSilo, farm, neighbour} end
|
|
||||||
local withNeighbour = S.collectBuildings(adEnv)
|
|
||||||
local names = {}
|
|
||||||
for _, silo in ipairs(withNeighbour) do table.insert(names, silo.id) end
|
|
||||||
check("gårdens nabosilo (40 m fra gårdlager) er ikke marksilo", table.concat(names, ","), "fs")
|
|
||||||
ADFields.fieldSilos = function() return {fieldSilo, farm} end
|
|
||||||
|
|
||||||
-- in-game 27/9: gårdens Farma-klynge er 300 m lang — siloer over 100 m fra et valgt gårdlager blev marksiloer.
|
|
||||||
-- En silo i samme klynge (≤ 60 m til en nabo) som et gårdlager er gårdens.
|
|
||||||
local c1 = {id = "c1", name = "Klynge 1", kind = "silo", placeable = {rootNode = {x = 50, z = 0}, spec_silo = {storages = {storage({[WHEAT] = 60000}, 0)}}}}
|
|
||||||
local c2 = {id = "c2", name = "Klynge 2", kind = "silo", placeable = {rootNode = {x = 105, z = 0}, spec_silo = {storages = {storage({[WHEAT] = 60000}, 0)}}}}
|
|
||||||
ADBuildings.list = function() return {fieldSilo, farm, c1, c2} end
|
|
||||||
ADFields.fieldSilos = function() return {fieldSilo, farm, c1, c2} end
|
|
||||||
local clustered = {}
|
|
||||||
for _, silo in ipairs(S.collectBuildings(adEnv)) do table.insert(clustered, silo.id) end
|
|
||||||
check("gårdens klynge (105 m via nabo) er ikke marksilo", table.concat(clustered, ","), "fs")
|
|
||||||
ADBuildings.list = function() return {fieldSilo, farm} end
|
|
||||||
ADFields.fieldSilos = function() return {fieldSilo, farm} end
|
|
||||||
|
|
||||||
-- M5: rest under 100 l tæller ikke som last
|
|
||||||
residue = 50
|
|
||||||
local vehicles = S.collectVehicles(adEnv)
|
|
||||||
check("M5 rest på 50 l er ikke last", vehicles[1].cargo, nil)
|
|
||||||
residue = 0
|
|
||||||
|
|
||||||
-- M4: slettet lånt vogn fjernes uden at vælte tick
|
|
||||||
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = true}))
|
|
||||||
started = {}
|
|
||||||
S.planNow(adEnv)
|
|
||||||
local loaned = started[1] ~= nil and started[1].v or nil
|
|
||||||
if loaned ~= nil then loaned.isDeleted = true end
|
|
||||||
local okTick = pcall(S.tick, adEnv, 2000)
|
|
||||||
check("M4 tick tåler slettet vogn", okTick, true)
|
|
||||||
check("M4 slettet vogn frigivet", loaned ~= nil and S.isLoaned(loaned), false)
|
|
||||||
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = false}))
|
|
||||||
end
|
|
||||||
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adFieldStorage.lua (fra repo-roden)
|
|
||||||
-- Lagringen skal dække HVERT felt modellen gemmer (FIELD_TYPES), og en mark skal overleve en tur gennem XML.
|
|
||||||
dofile("FS25_ADSmartPickup/adFieldModel.lua")
|
|
||||||
dofile("FS25_ADSmartPickup/adFieldStorage.lua")
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- falsk XMLFile: gemmer værdier pr. nøgle
|
|
||||||
local function fakeXml()
|
|
||||||
local values = {}
|
|
||||||
local xml = {}
|
|
||||||
for _, kind in ipairs({"Int", "String", "Float"}) do
|
|
||||||
xml["set" .. kind] = function(_, key, value) values[key] = value end
|
|
||||||
xml["get" .. kind] = function(_, key) return values[key] end
|
|
||||||
end
|
|
||||||
return xml
|
|
||||||
end
|
|
||||||
|
|
||||||
local field = ADFieldModel.with(ADFieldModel.startCare(ADFieldModel.newField(77), "weeding"), {
|
|
||||||
kind = "harvest", lastFruit = "WHEAT", mowWidth = 40.8, mowVehicleId = "v1", rakeVehicleId = "v2", windrowOffset = -2.5,
|
|
||||||
plan = {mode = "auto", product = "harvest", rake = true, storeBuildingIds = {"b1"}, rigs = 3, unloaders = 2,
|
|
||||||
mulch = true, lime = false, soil = "plow", sow = true, sowFruit = "CANOLA", roll = true, fertilize = false, weed = false},
|
|
||||||
})
|
|
||||||
local record = ADFieldModel.toRecord(field)
|
|
||||||
for key in pairs(record) do
|
|
||||||
check("ST1 felt med i lagringen: " .. key, ADFieldStorage.FIELD_TYPES[key] ~= nil, true)
|
|
||||||
end
|
|
||||||
local xml = fakeXml()
|
|
||||||
ADFieldStorage.writeRecord(xml, "k", record)
|
|
||||||
local back = ADFieldModel.fromRecord(ADFieldStorage.readRecord(xml, "k"))
|
|
||||||
local again = ADFieldModel.toRecord(back)
|
|
||||||
for key, value in pairs(record) do
|
|
||||||
check("ST2 rundtur: " .. key, tostring(again[key]), tostring(value))
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ST3-ST6: kursus-registret (genbrug af Courseplay-kurser) gemmes i samme fil, <courses><course .../></courses>
|
|
||||||
dofile("FS25_ADSmartPickup/adCourseCache.lua")
|
|
||||||
local course = ADCourseCache.newRecord({vehicle = "v7", field = 22, width = 30.0, sig = "n=4;a=960000;c=800,300",
|
|
||||||
settings = ADCourseCache.settingsKey({multiTools = 2, lane = -1}), waypoints = 812})
|
|
||||||
for key in pairs(course) do
|
|
||||||
check("ST3 kursus-felt med i lagringen: " .. key, ADFieldStorage.COURSE_TYPES[key] ~= nil, true)
|
|
||||||
end
|
|
||||||
local cxml = fakeXml()
|
|
||||||
ADFieldStorage.writeCourseRecord(cxml, "c", course)
|
|
||||||
local cback = ADFieldStorage.readCourseRecord(cxml, "c")
|
|
||||||
for key, value in pairs(course) do
|
|
||||||
check("ST4 kursus-rundtur: " .. key, tostring(cback[key]), tostring(value))
|
|
||||||
end
|
|
||||||
check("ST4b matcher stadig efter rundtur", ADCourseCache.matches(cback, {field = 22, width = 30, sig = course.sig, settings = course.settings}), true)
|
|
||||||
|
|
||||||
-- hel fil: save -> load med falsk XMLFile (iterate over indekserede nøgler)
|
|
||||||
local function fileXml(store)
|
|
||||||
local xml = fakeXml()
|
|
||||||
for _, kind in ipairs({"Int", "String", "Float"}) do
|
|
||||||
xml["set" .. kind] = function(_, key, value) store[key] = value end
|
|
||||||
xml["get" .. kind] = function(_, key) return store[key] end
|
|
||||||
end
|
|
||||||
function xml.iterate(_, base, fn)
|
|
||||||
local index = 0
|
|
||||||
while true do
|
|
||||||
local prefix = string.format("%s(%d)", base, index)
|
|
||||||
local found = false
|
|
||||||
for key in pairs(store) do
|
|
||||||
if key:sub(1, #prefix + 1) == prefix .. "#" or key:sub(1, #prefix + 1) == prefix .. "." then found = true break end
|
|
||||||
end
|
|
||||||
if not found then return end
|
|
||||||
fn(index + 1, prefix)
|
|
||||||
index = index + 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
function xml.save() end
|
|
||||||
function xml.delete() end
|
|
||||||
return xml
|
|
||||||
end
|
|
||||||
local disk = {}
|
|
||||||
ADRunsStorage = {FILE_NAME = "runs.xml", getPath = function() return "/save/runs.xml" end}
|
|
||||||
XMLFile = {create = function() return fileXml(disk) end, load = function() return fileXml(disk) end}
|
|
||||||
function fileExists() return true end
|
|
||||||
check("ST5 gem", ADFieldStorage.save({fields = {[77] = field}, fleet = {}, fleetMode = "all",
|
|
||||||
courseRecords = ADCourseCache.withRecord({}, course)}), true)
|
|
||||||
local loaded = ADFieldStorage.load()
|
|
||||||
check("ST6 kursus indlæst", loaded.courseRecords.v7 ~= nil and loaded.courseRecords.v7.waypoints, 812)
|
|
||||||
check("ST6b mark indlæst", loaded.fields[77] ~= nil, true)
|
|
||||||
-- ST7: gammel fil uden <courses> -> tomt register (bagudkompatibelt)
|
|
||||||
disk = {}
|
|
||||||
ADFieldStorage.save({fields = {}, fleet = {}, fleetMode = "all"})
|
|
||||||
local old = ADFieldStorage.load()
|
|
||||||
check("ST7 gammel fil: tomt register", next(old.courseRecords), nil)
|
|
||||||
|
|
||||||
-- ST8: manuel transport pr. maskine gemmes og indlæses; gammel fil uden -> tom
|
|
||||||
disk = {}
|
|
||||||
ADFieldStorage.save({fields = {}, fleet = {}, fleetMode = "all", manual = {vehicleCombine = true}})
|
|
||||||
check("ST8 manuel transport indlæst", ADFieldStorage.load().manual.vehicleCombine, true)
|
|
||||||
check("ST8 gammel fil: ingen manuelle", next(old.manual or {}), nil)
|
|
||||||
|
|
||||||
-- ST9: pause ("Stop alt og parkér") gemmes, så intet starter af sig selv efter indlæsning
|
|
||||||
disk = {}
|
|
||||||
ADFieldStorage.save({fields = {}, fleet = {}, fleetMode = "all", paused = true})
|
|
||||||
check("ST9 pause indlæst", ADFieldStorage.load().paused, true)
|
|
||||||
disk = {}
|
|
||||||
ADFieldStorage.save({fields = {}, fleet = {}, fleetMode = "all"})
|
|
||||||
check("ST9 ingen pause", ADFieldStorage.load().paused, false)
|
|
||||||
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adFieldView.lua (fra repo-roden)
|
|
||||||
dofile("FS25_ADSmartPickup/adFieldModel.lua")
|
|
||||||
dofile("FS25_ADSmartPickup/adFieldView.lua")
|
|
||||||
local V = ADFieldView
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local infos = {
|
|
||||||
{id = 109, mowable = true, markerId = 5},
|
|
||||||
{id = 12, mowable = true, markerId = nil},
|
|
||||||
{id = 90, mowable = false, markerId = 7},
|
|
||||||
}
|
|
||||||
local fields = {[109] = ADFieldModel.newField(109), [12] = ADFieldModel.newField(12)}
|
|
||||||
|
|
||||||
-- V1: alle marker (etape 2: også høst og bare), sorteret efter nummer
|
|
||||||
local rows = V.rows(infos, fields)
|
|
||||||
check("V1 tre rækker", #rows, 3)
|
|
||||||
check("V1 sorteret", rows[1].info.id, 12)
|
|
||||||
check("V1 model med", rows[3].field.id, 109)
|
|
||||||
-- V2: mangler model -> ny standardmark
|
|
||||||
check("V2 standardmodel", rows[2].field.stage, "idle")
|
|
||||||
-- V3: status — manglende markør vinder over trin
|
|
||||||
check("V3 mangler markør", V.statusKey(fields[12], infos[2]), "spu_field_noMarker")
|
|
||||||
check("V3 trin", V.statusKey(fields[109], infos[1]), "spu_field_stage_idle")
|
|
||||||
|
|
||||||
-- V4: planens tekstnøgler pr. markart
|
|
||||||
check("V4 ensilage", V.planKey({product = "auto"}, "mowable"), "spu_field_plan_silage")
|
|
||||||
check("V4 hø", V.planKey({product = "hay"}, "mowable"), "spu_field_plan_hay")
|
|
||||||
check("V4 høst", V.planKey({product = "auto"}, "harvest"), "spu_field_plan_harvest")
|
|
||||||
check("V4 bar mark", V.planKey({product = "auto"}, "bare"), "spu_field_plan_prep")
|
|
||||||
-- V5: jordtilstand som korte tekstdele
|
|
||||||
local parts = V.soilParts({lime = 0.2, fert = 1, fertMax = 2, weed = 3, needs = {lime = true, plow = true}})
|
|
||||||
check("V5 dele", table.concat(parts, ","), "lime,plow,fert,weed")
|
|
||||||
check("V5 intet", #V.soilParts({lime = 2, fert = 2, fertMax = 2, weed = 0, needs = {}}), 0)
|
|
||||||
|
|
||||||
-- cropText: høstet = stub (afgrøde), bar = bar jord, ellers afgrøden
|
|
||||||
local texts = {spu_field_stubble = "stub (%s)", spu_field_bare = "bar jord"}
|
|
||||||
local function T(key) return texts[key] end
|
|
||||||
check("FVcrop stub", ADFieldView.cropText({kind = "bare", stubbleOf = "Hvede", cropTitle = "Hvede"}, T), "stub (Hvede)")
|
|
||||||
check("FVcrop bar", ADFieldView.cropText({kind = "bare"}, T), "bar jord")
|
|
||||||
check("FVcrop afgrøde", ADFieldView.cropText({kind = "harvest", cropTitle = "Majs"}, T), "Majs")
|
|
||||||
|
|
||||||
-- FVstore: marksilo først, gæringssilo, så afstand; valgte vises altid; kun de nærmeste af resten
|
|
||||||
local stores = {
|
|
||||||
{id = "fjern", distance = 3000},
|
|
||||||
{id = "nær", distance = 200},
|
|
||||||
{id = "mark", distance = 90, fieldSilo = true},
|
|
||||||
{id = "valgt", distance = 5000, chosen = true},
|
|
||||||
{id = "gær", distance = 800, fermenter = true},
|
|
||||||
{id = "midt", distance = 1200},
|
|
||||||
}
|
|
||||||
local storeRows, hiddenStores = ADFieldView.storeRows(stores, 2)
|
|
||||||
local order = {}
|
|
||||||
for _, e in ipairs(storeRows) do table.insert(order, e.id) end
|
|
||||||
check("FVstore rækkefølge", table.concat(order, ","), "mark,gær,nær,midt,valgt")
|
|
||||||
check("FVstore skjulte", hiddenStores, 1)
|
|
||||||
check("FVstore meter", ADFieldView.distanceText(90.4), "90 m")
|
|
||||||
check("FVstore km", ADFieldView.distanceText(1420), "1,4 km")
|
|
||||||
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adFieldWorkBuying.lua (fra repo-roden)
|
|
||||||
-- Påfyldning ved købestation (in-game 26/9: "Liquid Buy Station", FarmFillStations buyingStationConfigurable):
|
|
||||||
-- ingen lagerbeholdning, så varen læses fra udbud/triggere/AI-varer.
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
Logging = {info = function() end, warning = function() end}
|
|
||||||
dofile("FS25_ADSmartPickup/adFieldWork.lua")
|
|
||||||
local HERBICIDE, LIME, SEEDS = 20, 21, 22
|
|
||||||
local W = ADFieldWork
|
|
||||||
check("BS1 udbudte varer", W.buyingSupports({providedFillTypes = {[HERBICIDE] = true}}, HERBICIDE), true)
|
|
||||||
check("BS2 triggerens varer", W.buyingSupports({loadTriggers = {{fillTypes = {[HERBICIDE] = true}}}}, HERBICIDE), true)
|
|
||||||
check("BS3 AI-varer", W.buyingSupports({getAISupportedFillTypes = function() return {[SEEDS] = true} end}, SEEDS), true)
|
|
||||||
check("BS4 sælger ikke kalk", W.buyingSupports({providedFillTypes = {[HERBICIDE] = true}}, LIME), false)
|
|
||||||
check("BS5 fejl i opslag = nej", W.buyingSupports({getAISupportedFillTypes = function() error("x") end}, LIME), false)
|
|
||||||
check("BS6 ingen station", W.buyingSupports(nil, LIME), false)
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adFields.lua (fra repo-roden)
|
|
||||||
dofile("FS25_ADSmartPickup/adFields.lua")
|
|
||||||
local F = ADFields
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- F1/F2: markørnavne
|
|
||||||
check("F1 F109", F.parseMarkerName("F109"), 109)
|
|
||||||
check("F1 småt", F.parseMarkerName("f7"), 7)
|
|
||||||
check("F1 mellemrum", F.parseMarkerName(" F 12 "), 12)
|
|
||||||
check("F2 andet navn", F.parseMarkerName("Field 109"), nil)
|
|
||||||
check("F2 tekst efter", F.parseMarkerName("F109 wait"), nil)
|
|
||||||
check("F2 nil", F.parseMarkerName(nil), nil)
|
|
||||||
|
|
||||||
-- F3: slåafgrøde = MOWER giver et skår som TEDDER gør til DRY…; halm udelukket
|
|
||||||
local names = {[10] = "GRASS_WINDROW", [11] = "DRYGRASS_WINDROW", [20] = "STRAW", [30] = "CLOVER_WINDROW", [31] = "DRYCLOVER_WINDROW", [40] = "BEANSTRAW"}
|
|
||||||
local mower = {[1] = {fillTypeIndex = 10}, [2] = {fillTypeIndex = 20}, [3] = {fillTypeIndex = 30}, [4] = {fillTypeIndex = 40}}
|
|
||||||
local tedder = {[10] = {targetFillTypeIndex = 11}, [30] = {targetFillTypeIndex = 31}, [40] = {targetFillTypeIndex = 20}}
|
|
||||||
local mowable = F.buildMowable(mower, tedder, function(index) return names[index] end)
|
|
||||||
check("F3 græs", mowable[1] ~= nil, true)
|
|
||||||
check("F3 græs skår", mowable[1].windrow, 10)
|
|
||||||
check("F3 græs hø", mowable[1].hay, 11)
|
|
||||||
check("F3 rug (halm) ikke", mowable[2], nil)
|
|
||||||
check("F3 kløver", mowable[3].hay, 31)
|
|
||||||
check("F3 bønnehalm -> halm ikke", mowable[4], nil)
|
|
||||||
|
|
||||||
-- F4: markens art og om den vokser
|
|
||||||
local kind, growing = F.classify(3, false, 0.4)
|
|
||||||
check("F4 høstafgrøde", kind, "harvest"); check("F4 vokser", growing, true)
|
|
||||||
check("F4 klar høst vokser ikke", select(2, F.classify(3, false, 0.95)), false)
|
|
||||||
check("F4 slåafgrøde", (F.classify(1, true, 0.2)), "mowable")
|
|
||||||
check("F4 ingen afgrøde = bar", (F.classify(nil, false, 0)), "bare")
|
|
||||||
check("F4 bar vokser ikke", select(2, F.classify(nil, false, 0)), false)
|
|
||||||
|
|
||||||
-- F5: jordens behov ud fra gennemsnit og spillets indstillinger
|
|
||||||
local needs = F.soilNeeds({lime = 0.2, plowLevel = 0}, {limeRequired = true, plowingRequired = true})
|
|
||||||
check("F5 kalk", needs.lime, true); check("F5 pløj", needs.plow, true)
|
|
||||||
local off = F.soilNeeds({lime = 0, plowLevel = 0}, {limeRequired = false, plowingRequired = false})
|
|
||||||
check("F5 kalk slået fra i spillet", off.lime, false); check("F5 pløjning slået fra", off.plow, false)
|
|
||||||
check("F5 kalket jord", F.soilNeeds({lime = 2, plowLevel = 1}, {limeRequired = true, plowingRequired = true}).lime, false)
|
|
||||||
check("F5 ukendt niveau = intet behov", F.soilNeeds({}, {limeRequired = true, plowingRequired = true}).plow, false)
|
|
||||||
|
|
||||||
-- F6: vækststadie -> fase ud fra afgrødens egne grænser
|
|
||||||
local wheat = {minHarvestingGrowthState = 6, maxHarvestingGrowthState = 7, cutState = 9, witheredState = 8}
|
|
||||||
check("F6 intet", F.growthPhase(0, wheat), "none")
|
|
||||||
check("F6 spirer/vokser", F.growthPhase(3, wheat), "growing")
|
|
||||||
check("F6 klar", F.growthPhase(6, wheat), "ready")
|
|
||||||
check("F6 visnet", F.growthPhase(8, wheat), "withered")
|
|
||||||
check("F6 høstet stub", F.growthPhase(9, wheat), "cut")
|
|
||||||
check("F6 ukendt stadie", F.growthPhase(nil, wheat), nil)
|
|
||||||
check("F6 FS25-navne (minHarvestGrowthState)", F.growthPhase(6, {minHarvestGrowthState = 5, maxHarvestGrowthState = 6, cutState = 8}), "ready")
|
|
||||||
-- F7: vokser = fase growing; ukendt fase -> voksende pixels og ikke klar
|
|
||||||
check("F7 fase vinder", F.isGrowing("cut", 100, 0.1), false)
|
|
||||||
check("F7 fase vokser", F.isGrowing("growing", 0, 0), true)
|
|
||||||
check("F7 reserve: voksende pixels", F.isGrowing(nil, 50, 0.2), true)
|
|
||||||
check("F7 reserve: ingen pixels (stub)", F.isGrowing(nil, 0, 0), false)
|
|
||||||
check("F7 reserve: klar", F.isGrowing(nil, 50, 0.95), false)
|
|
||||||
|
|
||||||
-- F8: slåafgrøde fuldt udvokset (Dakota-græs 3 = mellemhøjde, 4-5 = fuld)
|
|
||||||
check("F8 prime græs", F.primeState({minHarvestingGrowthState = 3, maxHarvestingGrowthState = 5}), 4)
|
|
||||||
check("F8 prime ét klar-stadie", F.primeState({minHarvestingGrowthState = 6, maxHarvestingGrowthState = 6}), 6)
|
|
||||||
check("F8 prime ukendt", F.primeState({}), nil)
|
|
||||||
check("F8 mellemhøjde tæller ikke", F.isPrime(3, 4), false)
|
|
||||||
check("F8 fuld højde tæller", F.isPrime(5, 4), true)
|
|
||||||
check("F8 ukendt stadie tæller", F.isPrime(nil, 4), true)
|
|
||||||
check("F8 ingen krav", F.isPrime(3, nil), true)
|
|
||||||
|
|
||||||
-- F9: kartofler (Dakota): 6 = klar men kun forberedelig, 10 = toppet (høstklar)
|
|
||||||
local potato = {minHarvestingGrowthState = 10, maxHarvestingGrowthState = 10, minPreparingGrowthState = 6, maxPreparingGrowthState = 6, witheredState = 7, cutState = 8}
|
|
||||||
check("F9 utoppet er forberedelig", F.isPreparable(6, potato), true)
|
|
||||||
check("F9 voksende ikke", F.isPreparable(5, potato), false)
|
|
||||||
check("F9 fase klar", F.growthPhase(6, potato), "ready")
|
|
||||||
check("F9 toppet klar", F.growthPhase(10, potato), "ready")
|
|
||||||
check("F9 ingen forberedelse", F.isPreparable(6, {minPreparingGrowthState = -1}), false)
|
|
||||||
|
|
||||||
-- jordtype -> klargjort/pløjet (værdierne kommer fra spillets fieldGroundSystem)
|
|
||||||
local groundValues = {STUBBLE_TILLAGE = 1, CULTIVATED = 2, SEEDBED = 3, PLOWED = 4, ROLLED_SEEDBED = 5, SOWN = 6}
|
|
||||||
local function cls(v) local p, pl = ADFields.classifyGround(v, groundValues) return tostring(p) .. "|" .. tostring(pl) end
|
|
||||||
check("jord: pløjet", cls(4), "true|true")
|
|
||||||
check("jord: kultiveret", cls(2), "true|false")
|
|
||||||
check("jord: stubbearbejdet", cls(1), "true|false")
|
|
||||||
check("jord: sået", cls(6), "false|false")
|
|
||||||
check("jord: ukendt værdi", cls(nil), "nil|nil")
|
|
||||||
groundValues.NONE = 0
|
|
||||||
check("jord: ingen markjord tæller ikke med", cls(0), "nil|nil")
|
|
||||||
-- vin/oliven er aldrig slåafgrøder, selv om et mod har sat dem på MOWER-listen (in-game 29/9: F22 hvide druer fik
|
|
||||||
-- planen "Silage + så" og en skårlægger sendt ud)
|
|
||||||
check("vin: druer ikke slåbare", ADFields.isMowableFruit("GRAPE", {windrow = "GRASS_WINDROW"}), false)
|
|
||||||
check("vin: oliven ikke slåbare", ADFields.isMowableFruit("oliveb", {windrow = "X"}), false)
|
|
||||||
check("græs slåbart", ADFields.isMowableFruit("GRASS", {windrow = "GRASS_WINDROW"}), true)
|
|
||||||
check("ikke på MOWER-listen", ADFields.isMowableFruit("WHEAT", nil), false)
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adFieldsPolygon.lua (fra repo-roden)
|
|
||||||
-- FS25-marker har ingen polygonPoints — omridset hentes med field:getDensityMapPolygon():getVerticesList().
|
|
||||||
-- In-game 26/9 (Dakota 16x, marker på 1,7 x 0,6 km): uden omrids blev kun markskiltet (posX/posZ) prøvet;
|
|
||||||
-- F108 med majs viste "bar jord" og "mangler markør" (markøren stod ved kanten, 890 m fra skiltet).
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
Logging = {warning = function() end, info = function() end}
|
|
||||||
|
|
||||||
local MAIZE = 10
|
|
||||||
function getWorldTranslation() return 0, 0, 0 end
|
|
||||||
function getTerrainHeightAtWorldPos() return 0 end
|
|
||||||
-- markskiltet står i marken, men ved en stribe uden afgrøde (x > 1400)
|
|
||||||
function cropAt(x) return x <= 1400 and MAIZE or nil end
|
|
||||||
FSDensityMapUtil = {
|
|
||||||
getFieldDataAtWorldPosition = function() return 1, 2 end,
|
|
||||||
getFruitTypeIndexAtWorldPos = function(x, z) return cropAt(x) end,
|
|
||||||
getFruitArea = function() return 0, 10 end,
|
|
||||||
}
|
|
||||||
FieldDensityMap = {SPRAY_LEVEL = 4, LIME_LEVEL = 5, PLOW_LEVEL = 6}
|
|
||||||
DensityCoordType = {POINT_VECTOR_VECTOR = 1}
|
|
||||||
DensityMapModifier = {new = function() return {setParallelogramWorldCoords = function() end, executeGet = function() return 1, 1 end} end}
|
|
||||||
g_fruitTypeManager = {
|
|
||||||
getFruitTypeByIndex = function() return {name = "MAIZE", fillType = {index = 60, title = "Majs"}, minHarvestingGrowthState = 7, maxHarvestingGrowthState = 7, cutState = 9} end,
|
|
||||||
getConverterDataByName = function() return {} end,
|
|
||||||
}
|
|
||||||
g_fillTypeManager = {getConverterDataByName = function() return {} end, getFillTypeNameByIndex = function() return "X" end}
|
|
||||||
g_farmlandManager = {getFarmlandOwner = function() return 1 end}
|
|
||||||
local polygon = {getVerticesList = function() return {0, 0, 1600, 0, 1600, 600, 0, 600} end}
|
|
||||||
local field = {getId = function() return 108 end, farmland = {id = 108}, posX = 1500, posZ = 50,
|
|
||||||
getDensityMapPolygon = function() return polygon end}
|
|
||||||
g_fieldManager = {getFields = function() return {field} end}
|
|
||||||
g_currentMission = {terrainRootNode = 1, missionInfo = {}, fieldGroundSystem = {
|
|
||||||
getDensityMapData = function() return 1, 0, 2 end, getMaxValue = function() return 2 end}}
|
|
||||||
local adEnv = {ADGraphManager = {
|
|
||||||
getMapMarkers = function() return {{id = 900, name = "F108"}} end,
|
|
||||||
-- markøren står 5 m uden for marken ved den modsatte kant: 1505 m fra skiltet
|
|
||||||
getWayPointById = function() return {x = -5, z = 300} end,
|
|
||||||
}}
|
|
||||||
|
|
||||||
dofile("FS25_ADSmartPickup/adFields.lua")
|
|
||||||
local now = 0
|
|
||||||
for _ = 1, 1000 do
|
|
||||||
if ADFields.getInfo(108) ~= nil then break end
|
|
||||||
ADFields.scanStep(adEnv, 1, now)
|
|
||||||
now = now + 16
|
|
||||||
end
|
|
||||||
local info = ADFields.getInfo(108)
|
|
||||||
check("PG1 marken er scannet", info ~= nil, true)
|
|
||||||
check("PG2 omrids fra getDensityMapPolygon", info and #info.verts, 4)
|
|
||||||
check("PG3 majsen findes trods skiltet på bar stribe", info and info.fruitName, "MAIZE")
|
|
||||||
check("PG4 ikke bar mark", info and info.kind, "harvest")
|
|
||||||
check("PG5 markør ved kanten findes (afstand til omridset)", info and info.markerId, 900)
|
|
||||||
check("PG6 afstand = til kanten", info and info.markerDistance, 5)
|
|
||||||
check("PG6b markens form-signatur (kursus-genbrug)", info and info.shapeSig, "n=4;a=960000;c=800,300")
|
|
||||||
|
|
||||||
-- PG7-9: field.fieldState er spillets sandhed (fields.xml: MAIZE growthState 2); jord-bits fra
|
|
||||||
-- getFieldDataAtWorldPosition (2. værdi) er IKKE vækststadie. Høstet mark (cutState) = bar, vist som stub.
|
|
||||||
|
|
||||||
field.fieldState = {fruitTypeIndex = MAIZE, growthState = 2}
|
|
||||||
FSDensityMapUtil.getFieldDataAtWorldPosition = function() return true, 0 end -- jord-bits 0 lignede "ingen vækst"
|
|
||||||
local function rescan()
|
|
||||||
ADFields.cache, ADFields.listCache, ADFields.activeScan, ADFields.lastScanStart = {}, nil, nil, nil
|
|
||||||
for _ = 1, 2000 do
|
|
||||||
if ADFields.getInfo(108) ~= nil then break end
|
|
||||||
ADFields.scanStep(adEnv, 1, now)
|
|
||||||
now = now + 16
|
|
||||||
end
|
|
||||||
return ADFields.getInfo(108)
|
|
||||||
end
|
|
||||||
info = rescan()
|
|
||||||
check("PG7 majs i vækst (fieldState), ikke bar", info and info.kind, "harvest")
|
|
||||||
check("PG7 fase vokser", info and info.phase, "growing")
|
|
||||||
field.fieldState = {fruitTypeIndex = MAIZE, growthState = 9}
|
|
||||||
info = rescan()
|
|
||||||
check("PG8 høstet (cutState) = bar", info and info.kind, "bare")
|
|
||||||
check("PG8 stub af majs", info and info.stubbleOf, "Majs")
|
|
||||||
field.fieldState = {fruitTypeIndex = 0, growthState = 0}
|
|
||||||
cropAt = function() return nil end
|
|
||||||
info = rescan()
|
|
||||||
check("PG9 ingen afgrøde = bar uden stub", info and info.kind .. "/" .. tostring(info.stubbleOf), "bare/nil")
|
|
||||||
-- PG10-12: bygget-over mark (fields.xml: groundType NONE — gårdsplads/stald/biogas) er ikke med i listen
|
|
||||||
field.fieldState = {fruitTypeIndex = 0, growthState = 0, groundType = 0}
|
|
||||||
info = rescan()
|
|
||||||
check("PG10 ingen markjord = kan ikke dyrkes", info and info.cultivable, false)
|
|
||||||
check("PG10 ikke i marklisten", #ADFields.list(), 0)
|
|
||||||
field.fieldState = {fruitTypeIndex = MAIZE, growthState = 2, groundType = 7}
|
|
||||||
cropAt = function(x) return MAIZE end
|
|
||||||
info = rescan()
|
|
||||||
check("PG11 markjord = kan dyrkes", info and info.cultivable, true)
|
|
||||||
check("PG11 i marklisten", #ADFields.list(), 1)
|
|
||||||
check("PG12 ingen punkter på markjord", ADFields.isCultivable(nil, 0), false)
|
|
||||||
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adFieldsScan.lua (fra repo-roden)
|
|
||||||
-- Markscanningen som arbejdskø: bliver færdig, giver rigtigt resultat, og intet frame laver mere end ét
|
|
||||||
-- prøvepunkts arbejde (in-game 26/9: en hel mark i ét frame gav hak).
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
Logging = {warning = function(fmt, ...) print("WARN " .. string.format(fmt, ...)) end, info = function() end}
|
|
||||||
|
|
||||||
-- falsk verden: én mark (F7) med hvede der er klar, polygon med 4 hjørner
|
|
||||||
local GRASS, WHEAT = 1, 3
|
|
||||||
local nodes = {[101] = {0, 0}, [102] = {100, 0}, [103] = {100, 100}, [104] = {0, 100}}
|
|
||||||
function getWorldTranslation(node) local p = nodes[node]; return p[1], 0, p[2] end
|
|
||||||
function getTerrainHeightAtWorldPos() return 0 end
|
|
||||||
local calls = 0
|
|
||||||
FSDensityMapUtil = {
|
|
||||||
getFieldDataAtWorldPosition = function() calls = calls + 1; return true, 170 end, -- på mark (2. værdi = jord-bits, IKKE vækst)
|
|
||||||
getFruitTypeIndexAtWorldPos = function() calls = calls + 1; return WHEAT end,
|
|
||||||
getFruitArea = function(_, _, _, _, _, _, _, any) calls = calls + 1; return 0, any and 10 or 10 end,
|
|
||||||
}
|
|
||||||
FieldDensityMap = {SPRAY_LEVEL = 4, LIME_LEVEL = 5, PLOW_LEVEL = 6}
|
|
||||||
DensityCoordType = {POINT_VECTOR_VECTOR = 1}
|
|
||||||
DensityMapModifier = {new = function() return {setParallelogramWorldCoords = function() end, executeGet = function() calls = calls + 1; return 1, 1 end} end}
|
|
||||||
g_fruitTypeManager = {
|
|
||||||
getFruitTypeByIndex = function(_, index) return {name = index == WHEAT and "WHEAT" or "GRASS", fillType = {index = 50, title = "Hvede"}, minHarvestingGrowthState = 6, maxHarvestingGrowthState = 7, cutState = 9} end,
|
|
||||||
getConverterDataByName = function() return {} end,
|
|
||||||
}
|
|
||||||
g_fillTypeManager = {getConverterDataByName = function() return {} end, getFillTypeNameByIndex = function() return "X" end}
|
|
||||||
g_farmlandManager = {getFarmlandOwner = function() return 1 end}
|
|
||||||
-- spillets egen marktilstand (field.fieldState, som fields.xml): afgrøde + vækststadie
|
|
||||||
local field = {getId = function() return 7 end, farmland = {id = 70}, polygonPoints = {101, 102, 103, 104}, posX = 50, posZ = 50,
|
|
||||||
fieldState = {fruitTypeIndex = WHEAT, growthState = 6}}
|
|
||||||
g_fieldManager = {getFields = function() return {field} end}
|
|
||||||
g_currentMission = {terrainRootNode = 1, missionInfo = {}, fieldGroundSystem = {
|
|
||||||
getDensityMapData = function() return 1, 0, 2 end, getMaxValue = function() return 2 end}}
|
|
||||||
local adEnv = {ADGraphManager = {
|
|
||||||
getMapMarkers = function() return {{id = 900, name = "F7"}, {id = 901, name = "Silo"}} end,
|
|
||||||
getWayPointById = function(_, id) return {x = 40, z = 40} end,
|
|
||||||
}}
|
|
||||||
|
|
||||||
dofile("FS25_ADSmartPickup/adFields.lua")
|
|
||||||
|
|
||||||
local maxPerFrame, frames = 0, 0
|
|
||||||
local now = 0
|
|
||||||
for _ = 1, 200 do
|
|
||||||
calls = 0
|
|
||||||
ADFields.scanStep(adEnv, 1, now)
|
|
||||||
if calls > maxPerFrame then maxPerFrame = calls end
|
|
||||||
frames = frames + 1
|
|
||||||
now = now + 16
|
|
||||||
if ADFields.getInfo(7) ~= nil then break end
|
|
||||||
end
|
|
||||||
-- markliste først, så marken (efter SCAN_INTERVAL) — kør videre til marken er scannet
|
|
||||||
for _ = 1, 400 do
|
|
||||||
if ADFields.getInfo(7) ~= nil then break end
|
|
||||||
calls = 0
|
|
||||||
ADFields.scanStep(adEnv, 1, now)
|
|
||||||
if calls > maxPerFrame then maxPerFrame = calls end
|
|
||||||
now = now + 16
|
|
||||||
end
|
|
||||||
local info = ADFields.getInfo(7)
|
|
||||||
check("SC1 marken er scannet", info ~= nil, true)
|
|
||||||
check("SC2 afgrøde", info and info.fruitName, "WHEAT")
|
|
||||||
check("SC3 art = høst", info and info.kind, "harvest")
|
|
||||||
check("SC4 klar-andel", info and info.readyRatio, 1)
|
|
||||||
check("SC5 fase klar", info and info.phase, "ready")
|
|
||||||
check("SC6 markør F7", info and info.markerId, 900)
|
|
||||||
check("SC7 prøvepunkter", info and #info.points, 5)
|
|
||||||
check("SC8 gødning læst", info and info.fert, 1)
|
|
||||||
check("SC9 højst ét punkts arbejde pr. frame (<= 4 opslag)", maxPerFrame <= 4, true)
|
|
||||||
print(" (flest opslag i ét frame: " .. maxPerFrame .. ")")
|
|
||||||
check("SC10 marklisten", #ADFields.list(), 1)
|
|
||||||
|
|
||||||
-- SC11: med ur kører flere opgaver pr. frame inden for budgettet (0,5 ms), højst MAX_TASKS_PER_FRAME
|
|
||||||
local fakeTime = 0
|
|
||||||
netGetTime = function() fakeTime = fakeTime + 0.1; return fakeTime end -- hver opgave "koster" 0,1 ms
|
|
||||||
ADFields.cache, ADFields.listCache, ADFields.activeScan, ADFields.lastScanStart = {}, nil, nil, nil
|
|
||||||
local frames2 = 0
|
|
||||||
for _ = 1, 400 do
|
|
||||||
if ADFields.getInfo(7) ~= nil then break end
|
|
||||||
ADFields.scanStep(adEnv, 1, now)
|
|
||||||
now = now + 16
|
|
||||||
frames2 = frames2 + 1
|
|
||||||
end
|
|
||||||
check("SC11 scannet igen med ur", ADFields.getInfo(7) ~= nil, true)
|
|
||||||
check("SC11 færre frames end én opgave pr. frame", frames2 < frames + 0, true)
|
|
||||||
print(" (frames uden ur: " .. frames .. ", med ur: " .. frames2 .. ")")
|
|
||||||
netGetTime = nil
|
|
||||||
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adFieldsShape.lua (fra repo-roden)
|
|
||||||
-- Markens form-fingeraftryk (genbrug af Courseplay-kurser): antal hjørner, areal og tyngdepunkt, afrundet så
|
|
||||||
-- små float-forskelle ikke tæller — men en ændret mark (nyt hjørne, flyttet kant) giver en ny signatur.
|
|
||||||
dofile("FS25_ADSmartPickup/adFields.lua")
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function rect(x0, z0, w, h)
|
|
||||||
return {{x = x0, z = z0}, {x = x0 + w, z = z0}, {x = x0 + w, z = z0 + h}, {x = x0, z = z0 + h}}
|
|
||||||
end
|
|
||||||
|
|
||||||
-- S1: rektangel 1600 x 600 -> 4 hjørner, 960.000 m², tyngdepunkt (800, 300)
|
|
||||||
check("S1 rektangel", ADFields.shapeSignature(rect(0, 0, 1600, 600)), "n=4;a=960000;c=800,300")
|
|
||||||
-- S2: omløbsretning er ligegyldig (arealet er absolut)
|
|
||||||
local reversed = {}
|
|
||||||
for index = 4, 1, -1 do table.insert(reversed, rect(0, 0, 1600, 600)[index]) end
|
|
||||||
check("S2 modsat omløb", ADFields.shapeSignature(reversed), "n=4;a=960000;c=800,300")
|
|
||||||
-- S3: små float-forskelle (cm) tæller ikke
|
|
||||||
local noisy = rect(0.0003, -0.0002, 1600.0004, 599.9997)
|
|
||||||
check("S3 float-støj", ADFields.shapeSignature(noisy), "n=4;a=960000;c=800,300")
|
|
||||||
-- S4: flyttet kant (1 m bredere) -> ny signatur
|
|
||||||
check("S4 kant flyttet", ADFields.shapeSignature(rect(0, 0, 1601, 600)) ~= ADFields.shapeSignature(rect(0, 0, 1600, 600)), true)
|
|
||||||
-- S5: ekstra hjørne på kanten (samme areal) -> ny signatur
|
|
||||||
local extra = {{x = 0, z = 0}, {x = 800, z = 0}, {x = 1600, z = 0}, {x = 1600, z = 600}, {x = 0, z = 600}}
|
|
||||||
check("S5 ekstra hjørne", ADFields.shapeSignature(extra), "n=5;a=960000;c=800,300")
|
|
||||||
-- S6: samme form et andet sted på kortet -> anden signatur
|
|
||||||
check("S6 flyttet mark", ADFields.shapeSignature(rect(100, 0, 1600, 600)), "n=4;a=960000;c=900,300")
|
|
||||||
-- S7: tom/ingen polygon -> nil
|
|
||||||
check("S7 tom", ADFields.shapeSignature({}), nil)
|
|
||||||
check("S8 nil", ADFields.shapeSignature(nil), nil)
|
|
||||||
-- S9: under 3 hjørner er ingen flade -> nil
|
|
||||||
check("S9 to punkter", ADFields.shapeSignature({{x = 0, z = 0}, {x = 1, z = 1}}), nil)
|
|
||||||
-- S10: trekant: areal 50, tyngdepunkt (10/3, 10/3) -> (3, 3)
|
|
||||||
check("S10 trekant", ADFields.shapeSignature({{x = 0, z = 0}, {x = 10, z = 0}, {x = 0, z = 10}}), "n=3;a=50;c=3,3")
|
|
||||||
-- S11: negative koordinater afrundes symmetrisk
|
|
||||||
check("S11 negative", ADFields.shapeSignature(rect(-1000, -500, 200, 100)), "n=4;a=20000;c=-900,-450")
|
|
||||||
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adFleetNames.lua (fra repo-roden)
|
|
||||||
dofile("FS25_ADSmartPickup/adFleetNames.lua")
|
|
||||||
local N = ADFleetNames
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local plan = N.plan({
|
|
||||||
{id = "b", model = "Puma 260 CVX"}, {id = "a", model = "Puma 260 CVX"}, {id = "c", model = "Fastrac 4220"},
|
|
||||||
{id = "d", model = "Cow Food", inRun = true, name = "Cow Food 1"}, {id = "e", model = "CR11", selfPropelled = true, name = "Mejetærsker"},
|
|
||||||
})
|
|
||||||
local byId = {}
|
|
||||||
for _, entry in ipairs(plan) do byId[entry.id] = entry end
|
|
||||||
check("navne: samme model nummereres (efter id)", byId.a.name, "Puma 260 CVX 1")
|
|
||||||
check("navne: anden af samme model", byId.b.name, "Puma 260 CVX 2")
|
|
||||||
check("navne: enlig model uden nummer", byId.c.name, "Fastrac 4220")
|
|
||||||
check("navne: flow-traktor beholder navn", byId.d.name, "Cow Food 1")
|
|
||||||
check("navne: selvkørende beholder navn", byId.e.name, "Mejetærsker")
|
|
||||||
check("parkering: P + navn", byId.a.parkName, "P Puma 260 CVX 1")
|
|
||||||
check("parkering: flow-traktor", byId.d.parkName, "P Cow Food 1")
|
|
||||||
check("omdøb: kun hvis ændret", byId.d.rename, false)
|
|
||||||
check("omdøb: ny navn", byId.a.rename, true)
|
|
||||||
-- R7: parkeringer omdøbes kun for maskiner der selv omdøbes, og ikke delte parkeringer
|
|
||||||
check("R7 flow-traktor: parkering røres ikke", byId.d.renamePark, false)
|
|
||||||
check("R7 selvkørende: parkering røres ikke", byId.e.renamePark, false)
|
|
||||||
check("R7 omdøbt traktor: parkering omdøbes", byId.a.renamePark, true)
|
|
||||||
local shared = N.plan({{id = "x", model = "M", park = 5}, {id = "y", model = "Q", park = 5}}, {[5] = 2})
|
|
||||||
check("R7 delt parkering røres ikke", shared[1].renamePark, false)
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,314 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adFlowGuide.lua (fra repo-roden)
|
|
||||||
dofile("FS25_ADSmartPickup/adGuide.lua")
|
|
||||||
dofile("FS25_ADSmartPickup/adRuns.lua")
|
|
||||||
dofile("FS25_ADSmartPickup/adFlowView.lua")
|
|
||||||
dofile("FS25_ADSmartPickup/adFlowGuide.lua")
|
|
||||||
local FG, G = ADFlowGuide, ADGuide
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function newRun(kind, changes)
|
|
||||||
local store, run = ADRuns.createRun(ADRuns.newStore(), "Mælk ud", kind)
|
|
||||||
if changes ~= nil then store = ADRuns.updateRun(store, run.id, changes) end
|
|
||||||
return ADRuns.getRun(store, run.id)
|
|
||||||
end
|
|
||||||
local function ctx(extra)
|
|
||||||
local context = {
|
|
||||||
buildings = {
|
|
||||||
{id = "b1", name = "Kostald", label = "Kostald M", hasMarker = true, text = "Kostald — M 5 m"},
|
|
||||||
{id = "b2", name = "Silo", label = "Silo M", hasMarker = true, text = "Silo — M 3 m"},
|
|
||||||
{id = "b3", name = "Lade", hasMarker = false, text = "Lade — INGEN MARKØR"},
|
|
||||||
},
|
|
||||||
fillTypes = {{name = "HAY", title = "Hø"}, {name = "STRAW", title = "Halm"}},
|
|
||||||
products = {{name = "MILK", title = "Mælk"}, {name = "EGG", title = "Æg"}},
|
|
||||||
storeBuildings = {{id = "s1", name = "Silo Nord", text = "Silo Nord — M 5 m"}, {id = "s2", name = "Palleskur", text = "Palleskur — M 2 m"}},
|
|
||||||
waitPools = {{group = "Wait A", count = 2}},
|
|
||||||
vehicles = {
|
|
||||||
{id = "t1", label = "Traktor 1", text = "Traktor 1 — 1000 l — OK"},
|
|
||||||
{id = "t2", text = "Traktor 2", otherRunId = 9, otherRunName = "Andet"},
|
|
||||||
{id = "t3", text = "Traktor 3", blocked = true},
|
|
||||||
{id = "t4", text = "Traktor 4"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for key, value in pairs(extra or {}) do context[key] = value end
|
|
||||||
return context
|
|
||||||
end
|
|
||||||
local function steps(guide) return table.concat(G.visibleSteps(guide), ",") end
|
|
||||||
local function findRow(rows, id, item)
|
|
||||||
for _, row in ipairs(rows) do
|
|
||||||
if row.id == id and (item == nil or row.item == item) then return row end
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
local function countType(rows, rowType)
|
|
||||||
local n = 0
|
|
||||||
for _, row in ipairs(rows) do if row.type == rowType then n = n + 1 end end
|
|
||||||
return n
|
|
||||||
end
|
|
||||||
local function textKeys(rows)
|
|
||||||
local keys = {}
|
|
||||||
for _, row in ipairs(rows) do if row.type == "text" and row.label.key ~= nil then table.insert(keys, row.label.key) end end
|
|
||||||
return table.concat(keys, ",")
|
|
||||||
end
|
|
||||||
local function at(guide, step) return G.jump(guide, step, "close") end
|
|
||||||
|
|
||||||
-- ---- trin ----
|
|
||||||
local supply = FG.open(newRun("supply"), ctx())
|
|
||||||
check("FL1 forsyning", steps(supply), "direction,buildings,goods,rules,wait,tractors,summary")
|
|
||||||
local out = FG.open(newRun("outbound"), ctx())
|
|
||||||
check("FL1 udkørsel uden lager-vare", steps(out), "direction,buildings,goods,rules,wait,tractors,summary")
|
|
||||||
local outStore = FG.open(newRun("outbound", {products = {{fillType = "MILK", policy = "sellElseStore"}}}), ctx())
|
|
||||||
check("FL1 udkørsel med lager", steps(outStore), "direction,buildings,goods,store,rules,wait,tractors,summary")
|
|
||||||
local new = FG.openNew("Flow 3", ctx())
|
|
||||||
check("FL1 nyt flow har navn", steps(new), "name,direction,buildings,goods,rules,wait,tractors,summary")
|
|
||||||
check("FL1 nyt flow starter på navn", new.step, "name")
|
|
||||||
check("FL1 nyt flow er forsyning", new.draft.kind, "supply")
|
|
||||||
check("FL1 nyt flow navn", new.draft.name, "Flow 3")
|
|
||||||
check("FL1 eksisterende starter på retning", supply.step, "direction")
|
|
||||||
|
|
||||||
-- ---- titler følger retningen ----
|
|
||||||
check("FL2 til", FG.stepKey("buildings", supply), "spu_guide_flow_step_to")
|
|
||||||
check("FL2 fra", FG.stepKey("buildings", out), "spu_guide_flow_step_from")
|
|
||||||
check("FL2 andre", FG.stepKey("rules", out), "spu_guide_flow_step_rules")
|
|
||||||
check("FL2 hint", FG.hintKey("goods", out), "spu_guide_flow_hint_goods_out")
|
|
||||||
check("FL2 hint forsyning", FG.hintKey("goods", supply), "spu_guide_flow_hint_goods_in")
|
|
||||||
|
|
||||||
-- ---- choose er immutabel ----
|
|
||||||
local toOut = FG.choose(supply, "kind", "outbound")
|
|
||||||
check("FL3 retning skiftet", toOut.draft.kind, "outbound")
|
|
||||||
check("FL3 gammel urørt", supply.draft.kind, "supply")
|
|
||||||
local withTarget = FG.choose(supply, "buildings", {item = "b1", on = true})
|
|
||||||
check("FL3 forsyning: mål", table.concat(withTarget.draft.targetBuildingIds, ","), "b1")
|
|
||||||
check("FL3 forsyning: kilder urørt", #withTarget.draft.sourceBuildingIds, 0)
|
|
||||||
local withSource = FG.choose(out, "buildings", {item = "b2", on = true})
|
|
||||||
check("FL3 udkørsel: kilde", table.concat(withSource.draft.sourceBuildingIds, ","), "b2")
|
|
||||||
local policy = FG.choose(out, "policy", {item = "MILK", value = "store"})
|
|
||||||
check("FL3 politik", policy.draft.products[1].policy, "store")
|
|
||||||
check("FL3 politik gør lager-trin synligt", FG.isVisible("store", policy), true)
|
|
||||||
local off = FG.choose(policy, "policy", {item = "MILK", value = "off"})
|
|
||||||
check("FL3 politik fra fjerner", #off.draft.products, 0)
|
|
||||||
local replaced = FG.choose(policy, "policy", {item = "MILK", value = "sell"})
|
|
||||||
check("FL3 politik erstattes", #replaced.draft.products, 1)
|
|
||||||
check("FL3 politik ny", replaced.draft.products[1].policy, "sell")
|
|
||||||
check("FL3 navn", FG.choose(new, "name", "Æg til salg").draft.name, "Æg til salg")
|
|
||||||
check("FL3 tomt navn ignoreres", FG.choose(new, "name", " ").draft.name, "Flow 3")
|
|
||||||
|
|
||||||
-- ---- rækker ----
|
|
||||||
local dirRows = FG.rows(supply)
|
|
||||||
check("FL4 retning valg", findRow(dirRows, "kind").type, "choice")
|
|
||||||
check("FL4 retning værdi", findRow(dirRows, "kind").value, "supply")
|
|
||||||
local buildingRows = FG.rows(at(supply, "buildings"))
|
|
||||||
check("FL4 bygninger med markør", countType(buildingRows, "check"), 2)
|
|
||||||
check("FL4 vis alle", findRow(buildingRows, "showAllBuildings").type, "toggle")
|
|
||||||
local allRows = FG.rows(at(FG.choose(supply, "showAllBuildings", true), "buildings"))
|
|
||||||
check("FL4 vis alle: tre", countType(allRows, "check"), 3)
|
|
||||||
local noHidden = FG.rows(at(FG.open(newRun("supply"), ctx({buildings = {{id = "b1", name = "Kostald", hasMarker = true, text = "K"}}})), "buildings"))
|
|
||||||
check("FL4 ingen vis-alle uden skjulte", findRow(noHidden, "showAllBuildings"), nil)
|
|
||||||
local chosenFirst = FG.rows(at(FG.choose(supply, "buildings", {item = "b2", on = true}), "buildings"))
|
|
||||||
local firstCheck
|
|
||||||
for _, row in ipairs(chosenFirst) do if row.type == "check" and firstCheck == nil then firstCheck = row end end
|
|
||||||
check("FL4 fast rækkefølge (navn)", firstCheck.item, "b1")
|
|
||||||
check("FL4 valgt markeret", findRow(chosenFirst, "buildings", "b2").value, true)
|
|
||||||
check("FL4 ingen bygninger", textKeys(FG.rows(at(FG.open(newRun("supply"), ctx({buildings = {}})), "buildings"))), "spu_noBuildings")
|
|
||||||
|
|
||||||
local goodsAuto = FG.rows(at(supply, "goods"))
|
|
||||||
check("FL5 forsyning auto", findRow(goodsAuto, "ingredientMode").value, "auto")
|
|
||||||
check("FL5 forsyning auto: ingen liste", countType(goodsAuto, "check"), 0)
|
|
||||||
local goodsManual = FG.rows(at(FG.choose(supply, "ingredientMode", "manual"), "goods"))
|
|
||||||
check("FL5 forsyning vælg: varer", countType(goodsManual, "check"), 2)
|
|
||||||
check("FL5 forsyning vælg uden varer", textKeys(FG.rows(at(FG.choose(FG.open(newRun("supply"), ctx({fillTypes = {}})), "ingredientMode", "manual"), "goods"))), "spu_guide_flow_noGoodsTo")
|
|
||||||
local goodsOut = FG.rows(at(policy, "goods"))
|
|
||||||
check("FL5 udkørsel: politik pr. vare", countType(goodsOut, "choice"), 2)
|
|
||||||
check("FL5 udkørsel: mælk lager", findRow(goodsOut, "policy", "MILK").value, "store")
|
|
||||||
check("FL5 udkørsel: æg fra", findRow(goodsOut, "policy", "EGG").value, "off")
|
|
||||||
check("FL5 udkørsel: politikker", #findRow(goodsOut, "policy", "EGG").options, 4)
|
|
||||||
check("FL5 udkørsel uden varer", textKeys(FG.rows(at(FG.open(newRun("outbound"), ctx({products = {}})), "goods"))), "spu_guide_flow_noProducts")
|
|
||||||
|
|
||||||
local storeRows = FG.rows(at(outStore, "store"))
|
|
||||||
check("FL6 lager auto", findRow(storeRows, "storeAuto").value, true)
|
|
||||||
check("FL6 lager auto: ingen liste", countType(storeRows, "check"), 0)
|
|
||||||
local storeManual = FG.rows(at(FG.choose(outStore, "storeAuto", false), "store"))
|
|
||||||
check("FL6 lager vælg selv", countType(storeManual, "check"), 2)
|
|
||||||
local chosenStore = FG.open(newRun("outbound", {products = {{fillType = "MILK", policy = "store"}}, storeBuildingIds = {"s2"}}), ctx())
|
|
||||||
check("FL6 valgt lager -> vælg selv", chosenStore.draft.storeAuto, false)
|
|
||||||
|
|
||||||
local rulesOut = FG.rows(at(out, "rules"))
|
|
||||||
check("FL7 udkørsel: sælg fra", findRow(rulesOut, "sellAtPercent").value, 90)
|
|
||||||
check("FL7 udkørsel: sælg-trin", #findRow(rulesOut, "sellAtPercent").options, 11)
|
|
||||||
check("FL7 udkørsel: lager over", findRow(rulesOut, "storeAbovePercent").value, 80)
|
|
||||||
check("FL7 udkørsel: samlerunde", findRow(rulesOut, "collectRound").type, "toggle")
|
|
||||||
check("FL7 udkørsel: ingen ture", findRow(rulesOut, "loops"), nil)
|
|
||||||
local rulesIn = FG.rows(at(supply, "rules"))
|
|
||||||
check("FL7 forsyning: ture", findRow(rulesIn, "loops").value, 0)
|
|
||||||
check("FL7 forsyning: ture-valg", #findRow(rulesIn, "loops").options, ADRuns.MAX_LOOPS + 1)
|
|
||||||
check("FL7 forsyning: 0 = uendeligt", findRow(rulesIn, "loops").options[1].label.key, "spu_loops_infinite")
|
|
||||||
check("FL7 forsyning: retur", findRow(rulesIn, "returnBelowPercent").value, 50)
|
|
||||||
check("FL7 forsyning: ingen salg", findRow(rulesIn, "sellAtPercent"), nil)
|
|
||||||
|
|
||||||
local waitRows = FG.rows(at(supply, "wait"))
|
|
||||||
check("FL8 vent: alle først", findRow(waitRows, "waitPoolGroup").options[1].value, "")
|
|
||||||
check("FL8 vent: puljer", #findRow(waitRows, "waitPoolGroup").options, 2)
|
|
||||||
|
|
||||||
local assigned = FG.open(newRun("supply", {}), ctx())
|
|
||||||
assigned = FG.choose(assigned, "vehicleIds", {item = "t4", on = true})
|
|
||||||
local tractorRows = FG.rows(at(assigned, "tractors"))
|
|
||||||
check("FL9 traktorer: kun egne + ledige", countType(tractorRows, "check"), 2)
|
|
||||||
check("FL9 egne først", (function() for _, r in ipairs(tractorRows) do if r.type == "check" then return r.item end end end)(), "t4")
|
|
||||||
check("FL9 skjult andre flows", findRow(tractorRows, "vehicleIds", "t2"), nil)
|
|
||||||
check("FL9 skjult blokeret", findRow(tractorRows, "vehicleIds", "t3"), nil)
|
|
||||||
check("FL9 linjer", textKeys(tractorRows), "spu_guide_flow_otherFlows")
|
|
||||||
-- blokerede traktorer: foldet sammen som standard (in-game 27/9: 37 linjer der ikke kunne bruges fyldte trinnet);
|
|
||||||
-- linjen med antallet er et link der folder navn + grund ud og ind
|
|
||||||
local function blockedLine(rows)
|
|
||||||
for _, r in ipairs(rows) do if r.type == "text" and r.label.text ~= nil and r.label.text:find("Traktor 3", 1, true) then return r end end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
local function blockedLink(rows)
|
|
||||||
for _, r in ipairs(rows) do if r.type == "link" and r.id == "showBlocked" then return r end end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
check("FL9 blokerede foldet sammen som standard", blockedLine(tractorRows), nil)
|
|
||||||
check("FL9 link med antal", blockedLink(tractorRows) ~= nil and blockedLink(tractorRows).label.key, "spu_guide_flow_blockedShow")
|
|
||||||
local expanded = FG.choose(assigned, "showBlocked", true)
|
|
||||||
local expandedRows = FG.rows(at(expanded, "tractors"))
|
|
||||||
check("FL9 foldet ud viser navn + grund", blockedLine(expandedRows) ~= nil, true)
|
|
||||||
check("FL9 foldet ud: link skjuler igen", blockedLink(expandedRows).label.key, "spu_guide_flow_blockedHide")
|
|
||||||
check("FL9 fold ud ændrer ikke kladden", expanded.draft.vehicleIds[1], "t4")
|
|
||||||
check("FL9 fold sammen igen", blockedLine(FG.rows(at(FG.choose(expanded, "showBlocked", true), "tractors"))), nil)
|
|
||||||
local inThisRun = FG.open(newRun("supply"), ctx({vehicles = {{id = "t2", text = "T2", otherRunId = 1, otherRunName = "Mælk ud"}}}))
|
|
||||||
check("FL9 traktor i dette flow er ikke 'andet flow'", countType(FG.rows(at(inThisRun, "tractors")), "check"), 1)
|
|
||||||
|
|
||||||
-- ---- resumé ----
|
|
||||||
local function lineFor(lines, step)
|
|
||||||
for _, line in ipairs(lines) do if line.step == step then return line end end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
local function partText(line)
|
|
||||||
local parts = {}
|
|
||||||
for _, part in ipairs(line.parts) do table.insert(parts, part.key or ("=" .. tostring(part.text))) end
|
|
||||||
return table.concat(parts, ",")
|
|
||||||
end
|
|
||||||
local sumIn = FG.summary(FG.choose(withTarget, "vehicleIds", {item = "t1", on = true}))
|
|
||||||
check("FL10 navn", partText(lineFor(sumIn, "name")), "=Mælk ud")
|
|
||||||
check("FL10 retning", partText(lineFor(sumIn, "direction")), "spu_direction_in")
|
|
||||||
check("FL10 til", partText(lineFor(sumIn, "buildings")), "=Kostald")
|
|
||||||
check("FL10 til-titel", lineFor(sumIn, "buildings").title.key, "spu_guide_flow_step_to")
|
|
||||||
check("FL10 varer auto", partText(lineFor(sumIn, "goods")), "spu_guide_flow_goodsAuto")
|
|
||||||
check("FL10 regler", partText(lineFor(sumIn, "rules")), "spu_guide_flow_sumLoopsInf,spu_guide_flow_sumReturn")
|
|
||||||
check("FL10 vent", partText(lineFor(sumIn, "wait")), "spu_waitPool_all")
|
|
||||||
check("FL10 traktorer", partText(lineFor(sumIn, "tractors")), "=Traktor 1")
|
|
||||||
check("FL10 intet lager-trin", lineFor(sumIn, "store"), nil)
|
|
||||||
-- traktor der ikke findes længere (solgt): aldrig rå id i resuméet (in-game 29/9)
|
|
||||||
local withGhost = FG.choose(FG.choose(withTarget, "vehicleIds", {item = "t1", on = true}), "vehicleIds", {item = "vehicleGONE", on = true})
|
|
||||||
check("FL10b resumé springer manglende over", partText(lineFor(FG.summary(withGhost), "tractors")), "=Traktor 1")
|
|
||||||
local onlyGhost = FG.choose(withTarget, "vehicleIds", {item = "vehicleGONE", on = true})
|
|
||||||
check("FL10b kun manglende -> ingen traktorer", partText(lineFor(FG.summary(onlyGhost), "tractors")), "spu_guide_flow_sumNoTractors")
|
|
||||||
local sumOut = FG.summary(FG.choose(FG.choose(policy, "collectRound", true), "sellAtPercent", 95))
|
|
||||||
check("FL11 retning ud", partText(lineFor(sumOut, "direction")), "spu_direction_out")
|
|
||||||
check("FL11 fra tom", partText(lineFor(sumOut, "buildings")), "spu_guide_flow_none")
|
|
||||||
check("FL11 varer", partText(lineFor(sumOut, "goods")), "spu_policy_store")
|
|
||||||
check("FL11 varenavn foran", lineFor(sumOut, "goods").parts[1].prefix, "Mælk: ")
|
|
||||||
check("FL11 lager auto", partText(lineFor(sumOut, "store")), "spu_guide_sum_auto")
|
|
||||||
-- varen er kun "lager": salgsgrænsen vises ikke (in-game 29/9)
|
|
||||||
check("FL11 regler (kun lager)", partText(lineFor(sumOut, "rules")), "spu_guide_flow_sumStoreAbove,spu_guide_flow_sumRound")
|
|
||||||
check("FL11 lager over", lineFor(sumOut, "rules").parts[1].args[1], 80)
|
|
||||||
check("FL11 ingen traktorer", partText(lineFor(sumOut, "tractors")), "spu_guide_flow_sumNoTractors")
|
|
||||||
local manyGoods = FG.summary(FG.choose(FG.choose(FG.choose(supply, "ingredientMode", "manual"), "ingredients", {item = "HAY", on = true}), "ingredients", {item = "STRAW", on = true}))
|
|
||||||
check("FL12 valgte varer", partText(lineFor(manyGoods, "goods")), "=Hø + Halm")
|
|
||||||
check("FL12 oversigt: links", countType(FG.rows(G.new(supply.def, supply.draft, supply.context, {step = "summary"})), "link"), 7)
|
|
||||||
|
|
||||||
-- ---- resultat ----
|
|
||||||
local r = FG.result(FG.choose(FG.choose(policy, "storeAuto", false), "storeBuildingIds", {item = "s1", on = true}))
|
|
||||||
check("FL13 ikke ny", r.isNew, false)
|
|
||||||
check("FL13 run-id", r.runId, 1)
|
|
||||||
check("FL13 retning", r.changes.kind, "outbound")
|
|
||||||
check("FL13 lager valgt", table.concat(r.changes.storeBuildingIds, ","), "s1")
|
|
||||||
check("FL13 varer", r.changes.products[1].fillType, "MILK")
|
|
||||||
check("FL13 lager-resumé bruger navn", (function() for _, l in ipairs(FG.summary(FG.choose(FG.choose(policy, "storeAuto", false), "storeBuildingIds", {item = "s1", on = true}))) do if l.step == "store" then return l.parts[1].text end end end)(), "Silo Nord")
|
|
||||||
local rAuto = FG.result(FG.choose(chosenStore, "storeAuto", true))
|
|
||||||
check("FL13 lager auto -> tom", #rAuto.changes.storeBuildingIds, 0)
|
|
||||||
local hiddenStore = FG.open(newRun("outbound", {storeBuildingIds = {"s2"}}), ctx())
|
|
||||||
check("FL13 lager usynligt: bevares", table.concat(FG.result(hiddenStore).changes.storeBuildingIds, ","), "s2")
|
|
||||||
local rNew = FG.result(FG.choose(new, "vehicleIds", {item = "t1", on = true}))
|
|
||||||
check("FL14 nyt", rNew.isNew, true)
|
|
||||||
check("FL14 navn", rNew.name, "Flow 3")
|
|
||||||
check("FL14 traktorer", table.concat(rNew.vehicleIds, ","), "t1")
|
|
||||||
check("FL14 ingen run-id", rNew.runId, nil)
|
|
||||||
local added, removed = FG.vehicleChanges({"a", "b"}, {"b", "c"})
|
|
||||||
check("FL15 tilføjet", table.concat(added, ","), "c")
|
|
||||||
check("FL15 fjernet", table.concat(removed, ","), "a")
|
|
||||||
|
|
||||||
-- ---- forhåndsvisning (til controllerens opslag) ----
|
|
||||||
local markers = {b1 = {101, 102}, b2 = {201}}
|
|
||||||
local preview = FG.previewRun(withSource.draft, function(id) return markers[id] or {} end)
|
|
||||||
check("FL16 kilder -> markører", table.concat(preview.sourceWayPointIds, ","), "201")
|
|
||||||
local previewIn = FG.previewRun(withTarget.draft, function(id) return markers[id] or {} end)
|
|
||||||
check("FL16 mål -> markører", table.concat(previewIn.targetWayPointIds, ","), "101,102")
|
|
||||||
check("FL16 primært mål", previewIn.targetWayPointId, 101)
|
|
||||||
check("FL16 kladde urørt", withTarget.draft.targetWayPointIds == nil or #withTarget.draft.targetWayPointIds == 0, true)
|
|
||||||
|
|
||||||
|
|
||||||
-- ---- solo-traktor + ledig vogn (in-game 29/9: flow kunne ikke oprettes uden at omdøbe en vogns parkering) ----
|
|
||||||
local soloCtx = ctx({
|
|
||||||
vehicles = {
|
|
||||||
{id = "s1", label = "Solo 1", text = "Solo 1", solo = true},
|
|
||||||
{id = "s2", label = "Solo 2", text = "Solo 2", solo = true},
|
|
||||||
{id = "s3", label = "Solo 3", text = "Solo 3", solo = true},
|
|
||||||
{id = "t1", label = "Traktor 1", text = "Traktor 1 — OK"},
|
|
||||||
},
|
|
||||||
wagons = {{id = "w1", label = "P Disponibel Vogn 4"}, {id = "w2", label = "P Disponibel Vogn 5"}},
|
|
||||||
})
|
|
||||||
local soloGuide = FG.open(newRun("supply"), soloCtx)
|
|
||||||
local one = FG.choose(soloGuide, "vehicleIds", {item = "s1", on = true})
|
|
||||||
check("WG1 solo tildeles første ledige vogn", one.draft.wagons.s1, "w1")
|
|
||||||
local two = FG.choose(one, "vehicleIds", {item = "s2", on = true})
|
|
||||||
check("WG2 næste solo får næste vogn", two.draft.wagons.s2, "w2")
|
|
||||||
check("WG2 første beholder sin", two.draft.wagons.s1, "w1")
|
|
||||||
local three = FG.choose(two, "vehicleIds", {item = "s3", on = true})
|
|
||||||
check("WG3 ingen ledig vogn tilbage -> ingen tildeling", three.draft.wagons.s3, nil)
|
|
||||||
local soloRows = FG.rows(at(three, "tractors"))
|
|
||||||
local wagonRow = findRow(soloRows, "wagon", "s1")
|
|
||||||
check("WG4 vognvalg under solo-traktor", wagonRow ~= nil and wagonRow.type, "choice")
|
|
||||||
check("WG4 vognvalg viser tildelt vogn", wagonRow ~= nil and wagonRow.value, "w1")
|
|
||||||
check("WG4 valgmuligheder uden de andres vogne", wagonRow ~= nil and #wagonRow.options, 1)
|
|
||||||
local noFree = false
|
|
||||||
for _, row in ipairs(soloRows) do if row.label ~= nil and row.label.key == "spu_guide_flow_noFreeWagon" then noFree = true end end
|
|
||||||
check("WG5 solo uden ledig vogn får besked", noFree, true)
|
|
||||||
check("WG5 almindelig traktor har intet vognvalg", findRow(FG.rows(at(FG.choose(soloGuide, "vehicleIds", {item = "t1", on = true}), "tractors")), "wagon", "t1"), nil)
|
|
||||||
local swapped = FG.choose(FG.choose(one, "vehicleIds", {item = "s2", on = false}), "wagon", {item = "s1", value = "w2"})
|
|
||||||
check("WG6 skift vogn", swapped.draft.wagons.s1, "w2")
|
|
||||||
local off = FG.choose(two, "vehicleIds", {item = "s1", on = false})
|
|
||||||
check("WG7 fravalgt traktor slipper vognen", off.draft.wagons.s1, nil)
|
|
||||||
local freed = FG.choose(off, "vehicleIds", {item = "s3", on = true})
|
|
||||||
check("WG7 frigjort vogn går til næste", freed.draft.wagons.s3, "w1")
|
|
||||||
check("WG8 resultat har vognene", FG.result(two).wagons.s2, "w2")
|
|
||||||
check("WG8 resultat uden fravalgte", FG.result(off).wagons.s1, nil)
|
|
||||||
local remembered = newRun("supply", {vehicleIds = {"s2"}})
|
|
||||||
remembered.wagons = {s2 = "w2"}
|
|
||||||
local reopened = FG.open(remembered, soloCtx)
|
|
||||||
check("WG9 husket vogn i kladden", reopened.draft.wagons.s2, "w2")
|
|
||||||
check("WG9 ny solo tager ikke den huskede", FG.choose(reopened, "vehicleIds", {item = "s1", on = true}).draft.wagons.s1, "w1")
|
|
||||||
check("WG9 kladden kopierer (run urørt)", (function() local g = FG.choose(reopened, "wagon", {item = "s2", value = "w1"}); return remembered.wagons.s2 end)(), "w2")
|
|
||||||
|
|
||||||
|
|
||||||
-- regler: kun de relevante for varernes politik
|
|
||||||
local storeOnly = FG.open(newRun("outbound", {products = {{fillType = "MILK", policy = "store"}}}), ctx())
|
|
||||||
local function keys(rows) local r = {} for _, row in ipairs(rows) do if row.id then table.insert(r, row.id) end end return table.concat(r, ",") end
|
|
||||||
check("RL1 kun lager: ingen salgsvalg", keys(FG.rows(at(storeOnly, "rules"))), "storeAbovePercent,collectRound")
|
|
||||||
local sellOnly = FG.open(newRun("outbound", {products = {{fillType = "MILK", policy = "sell"}}}), ctx())
|
|
||||||
check("RL2 kun salg: ingen lagervalg", keys(FG.rows(at(sellOnly, "rules"))), "sellAtPercent,sellAbovePercent,collectRound")
|
|
||||||
local function ruleKeys(guide)
|
|
||||||
for _, line in ipairs(FG.summary(guide)) do
|
|
||||||
if line.step == "rules" then local r = {} for _, p in ipairs(line.parts) do table.insert(r, p.key) end return table.concat(r, ",") end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
check("RL3 resumé kun lager", ruleKeys(storeOnly), "spu_guide_flow_sumStoreAbove")
|
|
||||||
check("RL4 resumé kun salg", ruleKeys(sellOnly), "spu_guide_flow_sumSellAt")
|
|
||||||
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adFlowView.lua (fra repo-roden)
|
|
||||||
-- Ren oprydning af Flows-fanen: tavlens rækkefølge, hvilke traktorer og bygninger der vises.
|
|
||||||
|
|
||||||
dofile("FS25_ADSmartPickup/adFlowView.lua")
|
|
||||||
local V = ADFlowView
|
|
||||||
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then
|
|
||||||
print("OK " .. name)
|
|
||||||
else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local function ids(list, field)
|
|
||||||
local out = {}
|
|
||||||
for _, item in ipairs(list) do table.insert(out, tostring(item[field or "id"])) end
|
|
||||||
return table.concat(out, ",")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- FV1: tavlen — forsyning (Ind) først, så udkørsel (Ud); hver gruppe sorteret efter navn (naturligt)
|
|
||||||
local runs = {
|
|
||||||
{id = 1, name = "Pig Food", kind = "supply"},
|
|
||||||
{id = 2, name = "Milk Sale", kind = "outbound"},
|
|
||||||
{id = 3, name = "Cow Food", kind = "supply"},
|
|
||||||
{id = 4, name = "Biogas 10", kind = "supply"},
|
|
||||||
{id = 5, name = "Biogas 9", kind = "supply"},
|
|
||||||
{id = 6, name = "Digestate", kind = "outbound"},
|
|
||||||
}
|
|
||||||
local groups = V.boardGroups(runs)
|
|
||||||
check("FV1 to grupper", #groups, 2)
|
|
||||||
check("FV1 Ind først", groups[1].kind, "supply")
|
|
||||||
check("FV1 Ind sorteret (Biogas 9 før 10)", ids(groups[1].runs), "5,4,3,1")
|
|
||||||
check("FV1 Ud sorteret", ids(groups[2].runs), "6,2")
|
|
||||||
check("FV1 samlet rækkefølge til vælgeren", ids(V.orderedRuns(runs)), "5,4,3,1,6,2")
|
|
||||||
check("FV1 tom gruppe udelades", #V.boardGroups({{id = 1, name = "A", kind = "supply"}}), 1)
|
|
||||||
|
|
||||||
-- FV2: traktorer — egne først, så ledige brugbare; andre flows og låste kun med "vis alle"
|
|
||||||
local vehicles = {
|
|
||||||
{id = "a", label = "Zetor", state = "free"},
|
|
||||||
{id = "b", label = "Fendt", state = "in"},
|
|
||||||
{id = "c", label = "Case", state = "other"},
|
|
||||||
{id = "d", label = "Claas", state = "blocked"},
|
|
||||||
{id = "e", label = "Deutz", state = "free"},
|
|
||||||
}
|
|
||||||
check("FV2 standard: egne + ledige, sorteret", ids(V.vehicleRows(vehicles, false)), "b,e,a")
|
|
||||||
check("FV2 vis alle: + andre flows + låste til sidst", ids(V.vehicleRows(vehicles, true)), "b,e,a,c,d")
|
|
||||||
check("FV2 antal skjulte", V.hiddenCount(vehicles, V.vehicleRows(vehicles, false)), 2)
|
|
||||||
|
|
||||||
-- FV3: bygninger — valgte først; uden markør skjult medmindre valgt eller "vis alle"
|
|
||||||
local buildings = {
|
|
||||||
{id = "s2", name = "Silo 2", hasMarker = true},
|
|
||||||
{id = "s10", name = "Silo 10", hasMarker = true},
|
|
||||||
{id = "x", name = "Gammel lade", hasMarker = false},
|
|
||||||
{id = "y", name = "Anden uden", hasMarker = false},
|
|
||||||
{id = "a", name = "Anlæg", hasMarker = true},
|
|
||||||
}
|
|
||||||
local chosen = {s10 = true, y = true}
|
|
||||||
-- fast rækkefølge (et tryk må ikke flytte rækken — in-game 26/9 hoppede valgte drops op i toppen)
|
|
||||||
check("FV3 standard: fast rækkefølge efter navn, valgte bliver hvor de er", ids(V.buildingRows(buildings, chosen, false)), "y,a,s2,s10")
|
|
||||||
check("FV3 vis alle: uden markør til sidst", ids(V.buildingRows(buildings, chosen, true)), "y,a,s2,s10,x")
|
|
||||||
-- ens bygningsnavne (5 x "BGA (1) : DOB 115") sorteres efter markørnavnet
|
|
||||||
local drops = {
|
|
||||||
{id = "d5", name = "DOB", label = "Biogas Drop 5", hasMarker = true},
|
|
||||||
{id = "d3", name = "DOB", label = "Biogas Drop 3", hasMarker = true},
|
|
||||||
{id = "d1", name = "DOB", label = "Biogas Drop 1", hasMarker = true},
|
|
||||||
{id = "d4", name = "DOB", label = "Biogas Drop 4", hasMarker = true},
|
|
||||||
{id = "d2", name = "DOB", label = "Biogas Drop 2", hasMarker = true},
|
|
||||||
}
|
|
||||||
check("FV3 ens navne efter markør", ids(V.buildingRows(drops, {d2 = true}, false)), "d1,d2,d3,d4,d5")
|
|
||||||
|
|
||||||
-- FV4: ens navne slås sammen med antal ("Chicken Coop x5"), rækkefølge efter første forekomst
|
|
||||||
check("FV4 sammenlæg", V.joinCounted({"Chicken Coop", "Chicken Coop", "Cow Barn", "Chicken Coop"}, " + ", "-"), "Chicken Coop x3 + Cow Barn")
|
|
||||||
check("FV4 ingen dubletter", V.joinCounted({"A", "B"}, " + ", "-"), "A + B")
|
|
||||||
check("FV4 tom -> tekst", V.joinCounted({}, " + ", "(ingen)"), "(ingen)")
|
|
||||||
|
|
||||||
-- FV-wrap: lange tekstrækker ombrydes ved mellemrum; indrykning gentages; kort tekst uændret
|
|
||||||
local wrapped = V.wrapText(" Prepared and sown with Sugar Beet as soon as it is sowing time for it", 30)
|
|
||||||
check("FVwrap flere linjer", #wrapped > 1, true)
|
|
||||||
check("FVwrap første linje indrykket", string.sub(wrapped[1], 1, 4), " ")
|
|
||||||
check("FVwrap anden linje indrykket", string.sub(wrapped[2], 1, 4), " ")
|
|
||||||
local maxLen = 0
|
|
||||||
for _, line in ipairs(wrapped) do if #line > maxLen then maxLen = #line end end
|
|
||||||
check("FVwrap ingen linje over grænsen", maxLen <= 30, true)
|
|
||||||
check("FVwrap intet ord tabt", table.concat(wrapped, " "):gsub("%s+", " "):gsub("^ ", ""), "Prepared and sown with Sugar Beet as soon as it is sowing time for it")
|
|
||||||
check("FVwrap kort tekst", V.wrapText("Kort", 30)[1], "Kort")
|
|
||||||
check("FVwrap langt ord deles ikke", V.wrapText("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 10)[1], "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
|
|
||||||
|
|
||||||
|
|
||||||
-- salgs-/lagerregler vises kun når en vare bruger dem (in-game 29/9: 'sell from 90 %' på et rent lager-flow)
|
|
||||||
local sells, stores = ADFlowView.policyUse({"store"})
|
|
||||||
check("politik: kun lager -> sælger ikke", sells, false)
|
|
||||||
check("politik: kun lager -> lagrer", stores, true)
|
|
||||||
sells, stores = ADFlowView.policyUse({"sell"})
|
|
||||||
check("politik: kun salg", tostring(sells) .. tostring(stores), "truefalse")
|
|
||||||
sells, stores = ADFlowView.policyUse({"sellElseStore", "off"})
|
|
||||||
check("politik: sælg ellers lager -> begge", tostring(sells) .. tostring(stores), "truetrue")
|
|
||||||
sells, stores = ADFlowView.policyUse({})
|
|
||||||
check("politik: ingen varer endnu -> vis begge", tostring(sells) .. tostring(stores), "truetrue")
|
|
||||||
sells, stores = ADFlowView.policyUse({"off"})
|
|
||||||
check("politik: kun fra -> vis begge", tostring(sells) .. tostring(stores), "truetrue")
|
|
||||||
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adFlowWagons.lua (fra repo-roden)
|
|
||||||
dofile("FS25_ADSmartPickup/adRuns.lua")
|
|
||||||
dofile("FS25_ADSmartPickup/adFlowWagons.lua")
|
|
||||||
local W = ADFlowWagons
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
-- gem/indlæs: traktor -> vogn
|
|
||||||
local text = W.serialize({vehicleB = "wagon2", vehicleA = "wagon1"})
|
|
||||||
check("gem: sorteret", text, "vehicleA=wagon1;vehicleB=wagon2")
|
|
||||||
local parsed = W.parse(text)
|
|
||||||
check("indlæs: A", parsed.vehicleA, "wagon1")
|
|
||||||
check("indlæs: B", parsed.vehicleB, "wagon2")
|
|
||||||
check("indlæs: tom", next(W.parse("")), nil)
|
|
||||||
check("indlæs: nil", next(W.parse(nil)), nil)
|
|
||||||
check("indlæs: skrald springes over", next(W.parse("=x;;y=")), nil)
|
|
||||||
-- husk vogn: ny tabel, gammel urørt
|
|
||||||
local before = {t1 = "w1"}
|
|
||||||
local after = W.remember(before, "t1", "w9")
|
|
||||||
check("husk: ny vogn", after.t1, "w9")
|
|
||||||
check("husk: original urørt", before.t1, "w1")
|
|
||||||
check("husk: samme vogn -> samme tabel", W.remember(after, "t1", "w9"), after)
|
|
||||||
-- start: hent / vent / start
|
|
||||||
check("start: har redskab bagpå", W.startAction({hasRear = true, wagonId = "w1", wagonExists = true, hasConnect = true}), "start")
|
|
||||||
check("start: ingen husket vogn", W.startAction({}), "start")
|
|
||||||
check("start: vognen findes ikke", W.startAction({wagonId = "w1", wagonExists = false, hasConnect = true}), "start")
|
|
||||||
check("start: vognen uden Connect", W.startAction({wagonId = "w1", wagonExists = true, hasConnect = false}), "start")
|
|
||||||
check("start: vognen på en anden", W.startAction({wagonId = "w1", wagonExists = true, hasConnect = true, wagonAttached = true}), "wait")
|
|
||||||
check("start: vognen lånt ud", W.startAction({wagonId = "w1", wagonExists = true, hasConnect = true, wagonBusy = true}), "wait")
|
|
||||||
check("start: hent", W.startAction({wagonId = "w1", wagonExists = true, hasConnect = true}), "fetch")
|
|
||||||
-- ejer: flowets vogn er flowets — også når flowet er stoppet (in-game 29/9: marksilo lånte Cow Food-vognen)
|
|
||||||
local runs = {{id = 1, stopped = false, wagons = {t1 = "w1"}}, {id = 2, stopped = true, wagons = {t2 = "w2"}}}
|
|
||||||
check("ejer: kørende flow holder", W.isHeld(runs, "w1"), true)
|
|
||||||
check("ejer: stoppet flow holder stadig", W.isHeld(runs, "w2"), true)
|
|
||||||
check("ejer: ukendt vogn", W.isHeld(runs, "w3"), false)
|
|
||||||
check("ejer: flowet findes", W.ownerOf(runs, "w2").id, 2)
|
|
||||||
-- bygning væk: to tjek i træk
|
|
||||||
local count, gone = W.missingStep(0, true)
|
|
||||||
check("væk: første tjek -> ikke endnu", gone, false)
|
|
||||||
count, gone = W.missingStep(count, true)
|
|
||||||
check("væk: andet tjek -> væk", gone, true)
|
|
||||||
count, gone = W.missingStep(1, false)
|
|
||||||
check("væk: fundet igen nulstiller", count, 0)
|
|
||||||
check("væk: fundet igen -> ikke væk", gone, false)
|
|
||||||
-- manglende bygninger i et flow
|
|
||||||
local exists = {a = true, b = true}
|
|
||||||
check("mangler: alle findes", #W.missingBuildings({sourceBuildingIds = {"a"}, targetBuildingIds = {"b"}}, exists, exists), 0)
|
|
||||||
check("mangler: én væk", W.missingBuildings({targetBuildingIds = {"a", "c"}, storeBuildingIds = {}}, exists, {a = true, c = true})[1], "c")
|
|
||||||
-- review v1.51
|
|
||||||
check("R1 traktoren er lånt ud af puljen -> vent", W.startAction({tractorBusy = true, hasRear = true, wagonId = "w1", wagonExists = true, hasConnect = true}), "wait")
|
|
||||||
local runs2 = {{id = 1, stopped = true, wagons = {t1 = "w1"}}, {id = 2, stopped = false, wagons = {t9 = "w1"}}}
|
|
||||||
check("R4 holdt hvis ET kørende flow har vognen", W.isHeld(runs2, "w1"), true)
|
|
||||||
local seen = {a = true, c = true}
|
|
||||||
check("R+ bygning aldrig set i sessionen tæller ikke som væk", #W.missingBuildings({targetBuildingIds = {"b"}}, {a = true}, seen), 0)
|
|
||||||
check("R+ bygning set og nu væk", W.missingBuildings({targetBuildingIds = {"c"}}, {a = true}, seen)[1], "c")
|
|
||||||
-- gæt flowets vogn ud fra parkeringsnavnet (ingen husket vogn: vognene var sat på plads før v1.51)
|
|
||||||
local cands = {{id = "v1", parkName = "P Cow Food vogn 1"}, {id = "v3", parkName = "P Cow Food vogn 3"},
|
|
||||||
{id = "g1", parkName = "P Goat Food vogn 1"}, {id = "v2", parkName = "P Cow Food vogn 2"}}
|
|
||||||
check("gæt: samme nummer", W.guessWagon("Cow Food", 3, cands, {}), "v3")
|
|
||||||
check("gæt: nummer optaget -> anden med flowets navn", W.guessWagon("Cow Food", 3, cands, {v3 = true}), "v1")
|
|
||||||
check("gæt: andet flow", W.guessWagon("Goat Food", 2, cands, {}), "g1")
|
|
||||||
check("gæt: intet passer", W.guessWagon("Pig Food", 1, cands, {}), nil)
|
|
||||||
check("gæt: store/små bogstaver", W.guessWagon("cow food", 2, cands, {}), "v2")
|
|
||||||
check("gæt: 'Cow Food 1' matcher ikke 'Cow Food 10'", W.guessWagon("Cow Food", 1, {{id = "x", parkName = "P Cow Food vogn 10"}, {id = "y", parkName = "P Cow Food vogn 1"}}, {}), "y")
|
|
||||||
-- tøm vognen før den parkeres (flow stoppet): mål -> silo/fabrik -> hjem
|
|
||||||
check("tøm: tom vogn -> hjem", W.emptyStep("start", {cargo = 0}), "home")
|
|
||||||
check("tøm: last -> flowets mål", W.emptyStep("start", {cargo = 5000, hasTarget = true}), "target")
|
|
||||||
check("tøm: last uden mål -> silo", W.emptyStep("start", {cargo = 5000, hasTarget = false}), "dump")
|
|
||||||
check("tøm: kører stadig -> vent", W.emptyStep("target", {cargo = 5000, adActive = true}), "wait")
|
|
||||||
check("tøm: mål fuldt, rest -> silo", W.emptyStep("target", {cargo = 2000}), "dump")
|
|
||||||
check("tøm: målet tog det hele -> hjem", W.emptyStep("target", {cargo = 0}), "home")
|
|
||||||
check("tøm: silo tog det -> hjem", W.emptyStep("dump", {cargo = 0}), "home")
|
|
||||||
check("tøm: silo kunne ikke -> giv op, hjem", W.emptyStep("dump", {cargo = 2000}), "giveUp")
|
|
||||||
check("tøm: for længe -> giv op", W.emptyStep("target", {cargo = 5000, adActive = true, elapsedMs = W.EMPTY_TIMEOUT_MS}), "giveUp")
|
|
||||||
-- flowet startes igen mens traktoren tømmer/kører hjem fra et stop: hjemturen afbrydes, den kører videre med vognen
|
|
||||||
-- (in-game 29/9: Stop+Start med 1 s mellemrum -> tømning ved forkert mål i 10 min, vogn sat af, traktor i garage)
|
|
||||||
check("genstart: tømmer -> afbryd", W.abortHomeTripOnStart({emptying = true}), true)
|
|
||||||
check("genstart: flowets hjemtur -> afbryd", W.abortHomeTripOnStart({flowHoming = true}), true)
|
|
||||||
check("genstart: puljens hjemtur (markarbejde) -> rør ikke", W.abortHomeTripOnStart({poolReturning = true}), false)
|
|
||||||
check("genstart: intet i gang", W.abortHomeTripOnStart({}), false)
|
|
||||||
-- tøm-mål: kun flowets egne mål — AD's 2. markør kan være forældet (æg-markør) eller et Wait-punkt (in-game 29/9)
|
|
||||||
check("tøm-mål: AD's mål er et af flowets", W.emptyTarget(12638, {12636, 12638}), 12638)
|
|
||||||
check("tøm-mål: forældet/Wait-markør -> flowets første mål", W.emptyTarget(12658, {12636, 12638}), 12636)
|
|
||||||
check("tøm-mål: intet AD-mål -> flowets første", W.emptyTarget(nil, {12636}), 12636)
|
|
||||||
check("tøm-mål: flow uden mål -> intet (silo)", W.emptyTarget(12658, {}), nil)
|
|
||||||
check("tøm-mål: ingen liste -> intet", W.emptyTarget(12658, nil), nil)
|
|
||||||
-- ledig traktor (puljen/markarbejde/marksilo må låne den): kun solo fra et stoppet flow
|
|
||||||
check("ledig: intet flow", W.tractorFree({}), true)
|
|
||||||
check("ledig: kørende flow", W.tractorFree({hasRun = true}), false)
|
|
||||||
check("ledig: stoppet flow, solo", W.tractorFree({hasRun = true, stopped = true}), true)
|
|
||||||
check("ledig: stoppet flow, flow-vogn bagpå", W.tractorFree({hasRun = true, stopped = true, carriesFlowWagon = true}), false)
|
|
||||||
check("ledig: stoppet flow, tømmer/kører hjem", W.tractorFree({hasRun = true, stopped = true, homeTrip = true}), false)
|
|
||||||
-- venteårsag i loggen (før stod der altid "i brug andetsteds")
|
|
||||||
check("vent-årsag: traktor optaget", W.waitReason({tractorBusy = true}), "tractorBusy")
|
|
||||||
check("vent-årsag: vogn på en anden", W.waitReason({wagonAttached = true}), "wagonAttached")
|
|
||||||
check("vent-årsag: vogn reserveret", W.waitReason({wagonBusy = true}), "wagonReserved")
|
|
||||||
-- tømning går i stå ved et fuldt mål (in-game 29/9: fåre-/BGA-vogne stod 10 min ved fuldt mål efter Stop alt)
|
|
||||||
check("tøm: står ved fuldt mål -> silo", W.emptyStep("target", {cargo = 5000, adActive = true, stalled = true}), "dump")
|
|
||||||
check("tøm: står ved silo uden at tømme -> næste silo", W.emptyStep("dump", {cargo = 5000, adActive = true, stalled = true, dumpAttempts = 1}), "dump")
|
|
||||||
check("tøm: 3 siloer prøvet -> giv op", W.emptyStep("dump", {cargo = 5000, adActive = true, stalled = true, dumpAttempts = W.MAX_DUMP_ATTEMPTS}), "giveUp")
|
|
||||||
check("tøm: kører mod målet (ikke i stå) -> vent", W.emptyStep("target", {cargo = 5000, adActive = true, stalled = false}), "wait")
|
|
||||||
check("i stå: tæt på, uændret længe", W.isStalled({nearTarget = true, sinceChangeMs = W.EMPTY_STALL_MS}), true)
|
|
||||||
check("i stå: tæt på, lige ankommet", W.isStalled({nearTarget = true, sinceChangeMs = 5000}), false)
|
|
||||||
check("i stå: på vej (langt væk)", W.isStalled({nearTarget = false, sinceChangeMs = 999999}), false)
|
|
||||||
-- hent vogn: ét nyt forsøg efter "stoppedAway" (AD standsede undervejs — fx spærret af en anden traktor)
|
|
||||||
check("hent igen: stoppedAway første gang", W.retryFetch("stoppedAway (37 m fra markøren)", 0), true)
|
|
||||||
check("hent igen: stoppedAway anden gang -> nej", W.retryFetch("stoppedAway", 1), false)
|
|
||||||
check("hent igen: noAttachable -> nej (markøren skal rettes)", W.retryFetch("noAttachable", 0), false)
|
|
||||||
-- manuel påkobling (spilleren sendte traktoren til Connect): traktor i et kørende flow startes bagefter
|
|
||||||
-- (in-game 29/9: Cow Food 3 hentede sin vogn manuelt efter et fejlet forsøg og blev holdende)
|
|
||||||
check("manuel kobling: kørende flow, egen vogn -> start", W.startAfterManualAttach({flowRunning = true, ownWagon = true}), true)
|
|
||||||
check("manuel kobling: stoppet flow -> nej", W.startAfterManualAttach({flowRunning = false, ownWagon = true}), false)
|
|
||||||
check("manuel kobling: AD kører allerede -> nej", W.startAfterManualAttach({flowRunning = true, ownWagon = true, adActive = true}), false)
|
|
||||||
check("manuel kobling: fremmed vogn -> nej", W.startAfterManualAttach({flowRunning = true, ownWagon = false}), false)
|
|
||||||
-- husk vogn ved hjemtur: kun når flowet KØRTE med den (stop); ikke ved oprydning efter indlæsning, hvor traktoren
|
|
||||||
-- kan have et puljeredskab på fra et lån (in-game 29/9: Biogas Silage "huskede" en læssevogn fra markarbejdet)
|
|
||||||
check("husk ved stop af kørende flow", W.shouldRememberOnHome("stop"), true)
|
|
||||||
check("husk ikke ved oprydning efter indlæsning", W.shouldRememberOnHome("afterLoad"), false)
|
|
||||||
-- forældreløs påkobling (hentet før genindlæsning, opgaven tabt): sættes tilbage efter 2 min uden ejer
|
|
||||||
check("forældreløs: ingen ejer, længe nok -> hjem", W.orphanAction({owned = false, idleMs = W.ORPHAN_MS}), "home")
|
|
||||||
check("forældreløs: har ejer -> intet", W.orphanAction({owned = true, idleMs = W.ORPHAN_MS}), "none")
|
|
||||||
check("forældreløs: for kort tid -> vent", W.orphanAction({owned = false, idleMs = 1000}), "wait")
|
|
||||||
-- parkeret sæt på traktorens egen plads er ikke forældreløst (in-game 29/9: vinmarks-REX blev skilt ad)
|
|
||||||
check("forældreløs: står på egen parkering -> intet", W.orphanAction({owned = false, atOwnPark = true, idleMs = W.ORPHAN_MS}), "none")
|
|
||||||
|
|
||||||
-- rækkefølge for ledige vogne i flow-guiden: navnematch, så disponible, så resten (alfabetisk)
|
|
||||||
local ranked = W.rankWagons("Sugarbeet Cut", {
|
|
||||||
{id = "a", label = "P Tedder"}, {id = "b", label = "P Disponibel Vogn 5"},
|
|
||||||
{id = "c", label = "P Sugarbeet Cut vogn 1"}, {id = "d", label = "P Disponibel Vogn 4"}, {id = "e", label = "P Bredal vogn"}})
|
|
||||||
local order = {}
|
|
||||||
for _, w in ipairs(ranked) do table.insert(order, w.id) end
|
|
||||||
check("rank: navn, disponible, resten", table.concat(order, ","), "c,d,b,e,a")
|
|
||||||
check("rank: tom liste", #W.rankWagons("X", {}), 0)
|
|
||||||
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
-- Kør: luajit tests/test_adGuide.lua (fra repo-roden)
|
|
||||||
dofile("FS25_ADSmartPickup/adGuide.lua")
|
|
||||||
local G = ADGuide
|
|
||||||
local failures = 0
|
|
||||||
local function check(name, actual, expected)
|
|
||||||
if actual == expected then print("OK " .. name) else
|
|
||||||
failures = failures + 1
|
|
||||||
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- trin a..e + summary; b vises kun når draft.showB, d aldrig
|
|
||||||
local def = {
|
|
||||||
order = {"a", "b", "c", "d", "e", "summary"},
|
|
||||||
isVisible = function(step, guide)
|
|
||||||
if step == "b" then return guide.draft.showB == true end
|
|
||||||
if step == "d" then return false end
|
|
||||||
return true
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
|
|
||||||
local guide = G.new(def, {showB = false, value = 1}, {x = 1})
|
|
||||||
check("G1 start på første synlige", guide.step, "a")
|
|
||||||
local index, total = G.position(guide)
|
|
||||||
check("G1 position", index, 1)
|
|
||||||
check("G1 total over synlige", total, 4)
|
|
||||||
check("G1 synlige", table.concat(G.visibleSteps(guide), ","), "a,c,e,summary")
|
|
||||||
|
|
||||||
-- næste springer skjulte over
|
|
||||||
local g2, event = G.next(guide)
|
|
||||||
check("G2 næste springer b over", g2.step, "c")
|
|
||||||
check("G2 intet event", event, nil)
|
|
||||||
check("G2 gammel guide urørt", guide.step, "a")
|
|
||||||
local g3 = G.next(g2)
|
|
||||||
check("G3 springer d over", g3.step, "e")
|
|
||||||
check("G3 position", (G.position(g3)), 3)
|
|
||||||
local g4 = G.next(g3)
|
|
||||||
check("G4 summary er sidst", G.isLast(g4), true)
|
|
||||||
local _, done = G.next(g4)
|
|
||||||
check("G4 næste på sidste = done", done, "done")
|
|
||||||
|
|
||||||
-- tilbage
|
|
||||||
local back = G.back(g3)
|
|
||||||
check("G5 tilbage til c", back.step, "c")
|
|
||||||
local _, cancel = G.back(guide)
|
|
||||||
check("G5 tilbage på første = cancel", cancel, "cancel")
|
|
||||||
|
|
||||||
-- valg der gør et trin synligt ændrer tælling og næste
|
|
||||||
local shown = G.withDraft(guide, {showB = true, value = 1})
|
|
||||||
check("G6 withDraft ny guide", guide.draft.showB, false)
|
|
||||||
check("G6 b synlig", G.next(shown).step, "b")
|
|
||||||
check("G6 total", select(2, G.position(shown)), 5)
|
|
||||||
|
|
||||||
-- aktuelt trin bliver usynligt: næste går til næste synlige i rækkefølgen, position nil
|
|
||||||
local onB = G.new(def, {showB = true}, {}, {step = "b"})
|
|
||||||
local hidden = G.withDraft(onB, {showB = false})
|
|
||||||
check("G7 position nil for skjult trin", (G.position(hidden)), nil)
|
|
||||||
check("G7 næste efter skjult", G.next(hidden).step, "c")
|
|
||||||
check("G7 tilbage fra skjult", G.back(hidden).step, "a")
|
|
||||||
|
|
||||||
-- spring (Ret) fra oversigt-trinnet: næste -> tilbage til summary, ikke single
|
|
||||||
local fromSummary = G.jump(g4, "c", "summary")
|
|
||||||
check("G8 spring trin", fromSummary.step, "c")
|
|
||||||
check("G8 single", fromSummary.single, true)
|
|
||||||
local returned, ev = G.next(fromSummary)
|
|
||||||
check("G8 retur til summary", returned.step, "summary")
|
|
||||||
check("G8 ikke single mere", returned.single, false)
|
|
||||||
check("G8 intet event", ev, nil)
|
|
||||||
-- tilbage i spring gendanner kladden fra før springet
|
|
||||||
local changed = G.withDraft(fromSummary, {showB = false, value = 99})
|
|
||||||
local undone = G.back(changed)
|
|
||||||
check("G9 tilbage gendanner kladde", undone.draft.value, 1)
|
|
||||||
check("G9 tilbage til summary", undone.step, "summary")
|
|
||||||
|
|
||||||
-- spring fra fanens oversigt (close): næste = done, tilbage = cancel
|
|
||||||
local single = G.new(def, {value = 5}, {}, {step = "e", single = true, returnTo = "close"})
|
|
||||||
local _, singleDone = G.next(single)
|
|
||||||
check("G10 single close næste = done", singleDone, "done")
|
|
||||||
local _, singleCancel = G.back(single)
|
|
||||||
check("G10 single close tilbage = cancel", singleCancel, "cancel")
|
|
||||||
check("G10 single isLast", G.isLast(single), true)
|
|
||||||
|
|
||||||
-- withContext
|
|
||||||
local ctxGuide = G.withContext(guide, {x = 2})
|
|
||||||
check("G11 ny context", ctxGuide.context.x, 2)
|
|
||||||
check("G11 gammel context", guide.context.x, 1)
|
|
||||||
|
|
||||||
-- deepCopy / toggleItem
|
|
||||||
local original = {list = {1, 2}, nested = {a = {b = 1}}}
|
|
||||||
local copy = G.deepCopy(original)
|
|
||||||
copy.list[1] = 9
|
|
||||||
copy.nested.a.b = 7
|
|
||||||
check("G12 deepCopy liste", original.list[1], 1)
|
|
||||||
check("G12 deepCopy indlejret", original.nested.a.b, 1)
|
|
||||||
local list = {"x", "y"}
|
|
||||||
check("G13 toggle til", table.concat(G.toggleItem(list, "z", true), ","), "x,y,z")
|
|
||||||
check("G13 ingen dublet", table.concat(G.toggleItem(list, "x", true), ","), "x,y")
|
|
||||||
check("G13 toggle fra", table.concat(G.toggleItem(list, "x", false), ","), "y")
|
|
||||||
check("G13 original urørt", #list, 2)
|
|
||||||
|
|
||||||
-- tekster
|
|
||||||
local texts = {greet = "Hej %s", n = "%d stk", title = "Afgrøde", plain = "Ingen"}
|
|
||||||
local function T(key) return texts[key] or key end
|
|
||||||
check("G14 label args", G.labelText({key = "greet", args = {"Bo"}}, T), "Hej Bo")
|
|
||||||
check("G14 label tekst", G.labelText({text = "rå"}, T), "rå")
|
|
||||||
check("G14 manglende args fejler ikke", G.labelText({key = "n"}, T), "%d stk")
|
|
||||||
check("G14 prefix", G.labelText({key = "plain", prefix = "Mælk: "}, T), "Mælk: Ingen")
|
|
||||||
check("G14 nil label", G.labelText(nil, T), "")
|
|
||||||
check("G15 linje", G.lineText({step = "crop", title = {key = "title"}, parts = {{key = "plain"}, {key = "n", args = {3}}}}, T), "Afgrøde: Ingen, 3 stk")
|
|
||||||
check("G15 linje uden dele", G.lineText({step = "crop", title = {key = "title"}, parts = {}}, T), "Afgrøde: -")
|
|
||||||
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
|
||||||
os.exit(failures == 0 and 0 or 1)
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user